Ping + Arp custom_component device tracker

Me too, “Platform not found: device_tracker.ping_arp” after upgrading to 0.94.0.

Got it fixed by modifying device_tracker.py

"""
Tracks devices by sending a ICMP echo request (ping) and query arp table.

For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/device_tracker.ping/
"""
import logging
import subprocess
import sys
import re
from datetime import timedelta

import voluptuous as vol

import homeassistant.helpers.config_validation as cv
from homeassistant.components.device_tracker import (
    PLATFORM_SCHEMA)
from homeassistant.components.device_tracker.const import (
    CONF_SCAN_INTERVAL, SCAN_INTERVAL, SOURCE_TYPE_ROUTER)
from homeassistant import util
from homeassistant import const

_LOGGER = logging.getLogger(__name__)

CONF_PING_COUNT = 'count'

PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
    vol.Required(const.CONF_HOSTS): {cv.string: cv.string},
    vol.Optional(CONF_PING_COUNT, default=1): cv.positive_int,
})


class Host:
    """Host object with ping detection."""

    def __init__(self, ip_address, dev_id, hass, config):
        """Initialize the Host pinger."""
        self.hass = hass
        self.ip_address = ip_address
        self.dev_id = dev_id
        self._count = config[CONF_PING_COUNT]
        self._ping_cmd = ['ping', '-n', '-q', '-c1', '-W1', self.ip_address]
        self._parp_cmd = ['arp', '-n', self.ip_address]

    def ping(self):
        """Send an ICMP echo request and return True if success."""
        pinger = subprocess.Popen(self._ping_cmd, stdout=subprocess.PIPE, stderr=subprocess.DEVNULL)
        try:
            pinger.communicate()
            return pinger.returncode == 0
        except subprocess.CalledProcessError:
            return False

   
    def parp(self):
        """Get the MAC address for a given IP."""
        arp = subprocess.Popen(self._parp_cmd, stdout=subprocess.PIPE)
        out, _ = arp.communicate() 
        match = re.search(r'(([0-9A-Fa-f]{1,2}\:){5}[0-9A-Fa-f]{1,2})', str(out))
        if match:
             return True
        return False

    def update(self, see):
        """Update device state by sending one or more ping messages."""
        failed = 0
        while failed < self._count:  # check more times if host is unreachable

            if self.ping():
                see(dev_id=self.dev_id, source_type=SOURCE_TYPE_ROUTER)
                _LOGGER.info("Ping OK from %s", self.ip_address)
                return True
            _LOGGER.info("No response from %s failed=%d", self.ip_address, failed)
           
            if self.parp():  
                 see(dev_id=self.dev_id, source_type=SOURCE_TYPE_ROUTER)
                 _LOGGER.info("Arp OK from %s", self.ip_address)
                 return True
            _LOGGER.info("No MAC address found") 
            failed += 1 

def setup_scanner(hass, config, see, discovery_info=None):
    """Set up the Host objects and return the update function."""
    hosts = [Host(ip, dev_id, hass, config) for (dev_id, ip) in
             config[const.CONF_HOSTS].items()]

    interval = config.get(CONF_SCAN_INTERVAL, timedelta(seconds=len(hosts) * config[CONF_PING_COUNT]) + DEFAULT_SCAN_INTERVAL)

    _LOGGER.info("Started ping tracker with interval=%s on hosts: %s", interval, ",".join([host.ip_address for host in hosts]))

    def update_interval(now):
        """Update all the hosts on every interval time."""
        try:
            for host in hosts:
                host.update(see)
        finally:
            hass.helpers.event.track_point_in_utc_time(
                update_interval, util.dt.utcnow() + interval)

    update_interval(None)
    return True
1 Like

Doesn’t work; devices do not show up in the integrations page…

Hi Anilet,

I had to change default_scan_interval to just scan_interval on line 87 to get it to start working. Thanks for posting, I would have never figured it out.

FYI…this is what works for me.

"""
Tracks devices by sending a ICMP echo request (ping) and query arp table.

For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/device_tracker.ping/
"""

import logging
import subprocess
import sys
import re
from datetime import timedelta

import voluptuous as vol

import homeassistant.helpers.config_validation as cv
from homeassistant.components.device_tracker import (
    PLATFORM_SCHEMA)
from homeassistant.components.device_tracker.const import (
    CONF_SCAN_INTERVAL, SCAN_INTERVAL, SOURCE_TYPE_ROUTER)
from homeassistant import util
from homeassistant import const

_LOGGER = logging.getLogger(__name__)

CONF_PING_COUNT = 'count'

PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
    vol.Required(const.CONF_HOSTS): {cv.string: cv.string},
    vol.Optional(CONF_PING_COUNT, default=1): cv.positive_int,
})


class Host:
    """Host object with ping detection."""

    def __init__(self, ip_address, dev_id, hass, config):
        """Initialize the Host pinger."""
        self.hass = hass
        self.ip_address = ip_address
        self.dev_id = dev_id
        self._count = config[CONF_PING_COUNT]
        self._ping_cmd = ['ping', '-n', '-q', '-c1', '-W1', self.ip_address]
        self._parp_cmd = ['arp', '-n', self.ip_address]

    def ping(self):
        """Send an ICMP echo request and return True if success."""
        pinger = subprocess.Popen(self._ping_cmd, stdout=subprocess.PIPE, stderr=subprocess.DEVNULL)
        try:
            pinger.communicate()
            return pinger.returncode == 0
        except subprocess.CalledProcessError:
            return False

   
    def parp(self):
        """Get the MAC address for a given IP."""
        arp = subprocess.Popen(self._parp_cmd, stdout=subprocess.PIPE)
        out, _ = arp.communicate() 
        match = re.search(r'(([0-9A-Fa-f]{1,2}\:){5}[0-9A-Fa-f]{1,2})', str(out))
        if match:
             return True
        return False

    def update(self, see):
        """Update device state by sending one or more ping messages."""
        failed = 0
        while failed < self._count:  # check more times if host is unreachable

            if self.ping():
                see(dev_id=self.dev_id, source_type=SOURCE_TYPE_ROUTER)
                _LOGGER.info("Ping OK from %s", self.ip_address)
                return True
            _LOGGER.info("No response from %s failed=%d", self.ip_address, failed)
           
            if self.parp():  
                 see(dev_id=self.dev_id, source_type=SOURCE_TYPE_ROUTER)
                 _LOGGER.info("Arp OK from %s", self.ip_address)
                 return True
            _LOGGER.info("No MAC address found") 
            failed += 1 

def setup_scanner(hass, config, see, discovery_info=None):
    """Set up the Host objects and return the update function."""
    hosts = [Host(ip, dev_id, hass, config) for (dev_id, ip) in
             config[const.CONF_HOSTS].items()]

    interval = config.get(CONF_SCAN_INTERVAL, timedelta(seconds=len(hosts) * config[CONF_PING_COUNT]) + SCAN_INTERVAL)

    _LOGGER.info("Started ping tracker with interval=%s on hosts: %s", interval, ",".join([host.ip_address for host in hosts]))

    def update_interval(now):
        """Update all the hosts on every interval time."""
        try:
            for host in hosts:
                host.update(see)
        finally:
            hass.helpers.event.track_point_in_utc_time(
                update_interval, util.dt.utcnow() + interval)

    update_interval(None)
    return True
1 Like

@Twit HA restarted without error and I assumed everything working, anyway thanks for figuring it out.

Could find new device which not in the hosts list?

Hello,

it doesnt work, even with the __init__.py in the ping_arp directory…

i get : Integration ping_arp not found when trying to verify its device_tracker platform

when i restart Ha

any idea?

Same, anyone know how to get it working? I’ve put it in my /custom_components/ping_arp/ folder on hass.io, tried with and without manifest.json and init.py

@Olivier974 @banana999

Unfortunately I don’t run Hassio, I run Hass on Ubuntu. I only have an empty __init__.py file and a device_tracker.py file (with my code above in it) in my ping_arp folder. I don’t have a manifest.json file and it works.

I presume you are not using the code from the first post as this stopped working at 0.88.

Sorry I can’t be more help.

I do vaguely remember that I had to install a dependency, I think it was “sudo apt-get install net-tools” as arp was not installed but I think the log actually said that.

Simon

thanks @Twit,

but i have read that :
You might have to install the packages for arp and nmap. If you are using Hass.io then just move forward to the configuration as all requirements are already fulfilled

here : https://www.home-assistant.io/components/nmap_tracker/

if someone could help more…

any news here? I’m also struggeling with the

Platform error device_tracker.ping_arp - Integration 'ping_arp' not found.

error on hassio. The nmap device tracker is working fine (so nmap and net-tools are installed just fine on my raspberry via hassio).

edit: With __init__.py and manifest.json and a complete restart of my raspberry, the error has gone.

Anybody tried this with Android phones?

So I tried this with an Android phone (pixel 2) and it does not work. The phone is marked at home even if I’m away for several hours…

So, it works with Android phones too but not if you run HA on docker. Reason is that when using host mode, the component sees 2 network interfaces and only one is updated.

Below my version, it now works by passing the network interface. I will keep you posted if it works well.

Config as follows:

- platform: ping_arp
    count: 2
    iface: eth0
    consider_home: 180
    hosts:
      ronald_gsm_ping_arp: 192.168.1.24
      evelien_gsm_ping_arp: 192.168.1.57

Custom component:

"""
Tracks devices by sending a ICMP echo request (ping) and query arp table.

For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/device_tracker.ping/
"""

import logging
import subprocess
import sys
import re
from datetime import timedelta

import voluptuous as vol

import homeassistant.helpers.config_validation as cv
from homeassistant.components.device_tracker import (
    PLATFORM_SCHEMA)
from homeassistant.components.device_tracker.const import (
    CONF_SCAN_INTERVAL, SCAN_INTERVAL, SOURCE_TYPE_ROUTER)
from homeassistant import util
from homeassistant import const

_LOGGER = logging.getLogger(__name__)

CONF_PING_COUNT = 'count'
CONF_IFACE = 'iface'

PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
    vol.Required(const.CONF_HOSTS): {cv.string: cv.string},
    vol.Optional(CONF_PING_COUNT, default=1): cv.positive_int,
    vol.Optional(CONF_IFACE): cv.string,
})


class Host:
    """Host object with ping detection."""

    def __init__(self, ip_address, dev_id, hass, config):
        """Initialize the Host pinger."""
        self.hass = hass
        self.ip_address = ip_address
        self.dev_id = dev_id
        self._count = config[CONF_PING_COUNT]
        self._iface = config[CONF_IFACE]

        if self._iface:
            self._ping_cmd = ['ping', '-I', self._iface, '-n', '-q', '-c1', '-W1', self.ip_address]
            self._parp_cmd = ['arp', '-i', self._iface, '-n', self.ip_address]
        else:
            self._ping_cmd = ['ping', '-n', '-q', '-c1', '-W1', self.ip_address]
            self._parp_cmd = ['arp', '-n', self.ip_address]

    def ping(self):
        """Send an ICMP echo request and return True if success."""
        pinger = subprocess.Popen(self._ping_cmd, stdout=subprocess.PIPE, stderr=subprocess.DEVNULL)
        try:
            pinger.communicate()
            return pinger.returncode == 0
        except subprocess.CalledProcessError:
            return False

   
    def parp(self):
        """Get the MAC address for a given IP."""
        arp = subprocess.Popen(self._parp_cmd, stdout=subprocess.PIPE)
        out, _ = arp.communicate() 
        match = re.search(r'(([0-9A-Fa-f]{1,2}\:){5}[0-9A-Fa-f]{1,2})', str(out))
        if match:
             return True
        return False

    def update(self, see):
        """Update device state by sending one or more ping messages."""
        failed = 0
        while failed < self._count:  # check more times if host is unreachable

            if self.ping():
                see(dev_id=self.dev_id, source_type=SOURCE_TYPE_ROUTER)
                _LOGGER.info("Ping OK from %s", self.ip_address)
                return True
            _LOGGER.info("No response from %s failed=%d", self.ip_address, failed)
           
            if self.parp():  
                 see(dev_id=self.dev_id, source_type=SOURCE_TYPE_ROUTER)
                 _LOGGER.info("Arp OK from %s", self.ip_address)
                 return True
            _LOGGER.info("No MAC address found") 
            failed += 1 

def setup_scanner(hass, config, see, discovery_info=None):
    """Set up the Host objects and return the update function."""
    hosts = [Host(ip, dev_id, hass, config) for (dev_id, ip) in
             config[const.CONF_HOSTS].items()]

    interval = config.get(CONF_SCAN_INTERVAL, timedelta(seconds=len(hosts) * config[CONF_PING_COUNT]) + SCAN_INTERVAL)

    _LOGGER.info("Started ping tracker with interval=%s on hosts: %s", interval, ",".join([host.ip_address for host in hosts]))

    def update_interval(now):
        """Update all the hosts on every interval time."""
        try:
            for host in hosts:
                host.update(see)
        finally:
            hass.helpers.event.track_point_in_utc_time(
                update_interval, util.dt.utcnow() + interval)

    update_interval(None)
    return True
2 Likes

Thanks for this component! It was also my feeling that arp+ping or just arp will work much better and spam the network less than nmap, and after trying out nmap, arp -a is probably more reliable too.

However, why list individual IPs in the config? I believe the mac addresses, even though they can be randomized by wifi clients, are more stable than IPs especially when the devices are on their home network. …Or the component could just take some include/exclude ranges like the nmap integration does, and return everything that’s in the table returned by ‘arp -a’, skipping the ping part completely. I think that since the arp timeout is just 30 seconds, the ping part isn’t so crucial.

I’ll try to come up with a modification of this component to parse arp -a output, which seems quite reliable on my network both in terms of false positives and false negatives excepting the 30s timeout.

As far as I’ve seen with my Android phone, the phone’s IP remains in the arp table for a while but the mac is no longer printed after a short while so those lines without the mac can be ignored.

So I now have my version of the addon: https://github.com/balrog-kun/ha_ping_arp/blob/2697a8aa72e470749d57262a6ceb569078aaedf0/device_tracker.py (based on the nmap component’s code instead of the one posted in this thread)

For the “hosts” and “exclude” settings it uses the same format a the nmap plugin but they’re optional. I use them as follows but you can provide multiple values.

    hosts: 10.0.6.0/24
    exclude: 10.0.6.1

What it does is it looks at the arp table only and doesn’t do real scanning. Then it pings the addresses that match the hosts/exclude masks. On my orange-pi where HA is running on Armbian, it seems that the arp table usually (not always) has a complete view of the local wifi network clients. I set sysctl net.ipv4.conf.all.arp_accept=1 which should help this to be true but somehow I don’t think it makes a difference. On the other machines on my network, whether this setting is 0 or 1, they have a much smaller number of entries in the arp cache at any time. So I’m not sure what’s happening there, but this approach where I only try to ping machines that are in the ARP table, is working well for me for the last 2 days. I’m thinking maybe I should add a setting to do an occasional “fping -a -c 1 -t 2000 -g 10.0.6.0/24” every few minutes just to make sure potential missing devices are eventually added to the ARP cache.

This version also provides the hostnames, which are somehow resolved much more quickly on this armbian install than on other machines.

Another thing I changed here is that I only send a single ping to each device instead of doing 3 retries. However, I have a longer timeout for that ping (3s seems to be the max supported by ping). In my testing, even if my android phone is sleeping, it will eventually catch up and reply to that ping, it just needs time but doesn’t need a re-send.

1 Like

Hi @rdehuyss,

I’ve tried your and @balrog 's component, but don’t succeed. There are no device_trackers being made, and nothing appears in the logs. Any idea on how to troubleshoot?

Thanks
Cadish

If you run a localized Linux other than English, you will encounter the same error that I had…

See https://github.com/balrog-kun/ha_ping_arp/issues/1

1 Like

Solved it. Needed to use

iface: eno1

instead of eth0. I had logged in on ssh to hassio first, and saw that the interface there was eth0, but needed the interface of my system, being eno1.

Everything works now. Thanks!
Cadish

1 Like
Thanks for the component @balrog. The idea of ​​using arp with ping is excellent,
 but unfortunately, after installing, the file known_devices.yaml does not update with the devices. 
My default iface is eth0 and the language is English. 
In Debug I realize that it ping the IPs of my network, but does not update the device file. What can I do?? 
my version of Home Assistant is the most updated. 

custom_components.ha_ping_arp.device_tracker] Running ping -n -q -c1 -W2.0 10.1.1.52

[custom_components.ha_ping_arp.device_tracker] arp scan done

[custom_components.ha_ping_arp.device_tracker] last results []