""" Tracks the latency of a host by sending ICMP echo requests (ping). For more details about this platform, please refer to the documentation at https://home-assistant.io/components/binary_sensor.ping/ """ import logging import subprocess import re import sys from datetime import timedelta import voluptuous as vol import homeassistant.helpers.config_validation as cv from homeassistant.components.binary_sensor import ( BinarySensorDevice, PLATFORM_SCHEMA) from homeassistant.const import CONF_NAME, CONF_HOST _LOGGER = logging.getLogger(__name__) ATTR_ROUND_TRIP_TIME_AVG = 'round_trip_time_avg' ATTR_ROUND_TRIP_TIME_MAX = 'round_trip_time_max' ATTR_ROUND_TRIP_TIME_MDEV = 'round_trip_time_mdev' ATTR_ROUND_TRIP_TIME_MIN = 'round_trip_time_min' CONF_PING_COUNT = 'count' DEFAULT_NAME = 'Ping Binary sensor' DEFAULT_PING_COUNT = 5 DEFAULT_DEVICE_CLASS = 'connectivity' SCAN_INTERVAL = timedelta(minutes=5) PING_MATCHER = re.compile( r'(?P\d+.\d+)\/(?P\d+.\d+)\/(?P\d+.\d+)\/(?P\d+.\d+)') PING_MATCHER_BUSYBOX = re.compile( r'(?P\d+.\d+)\/(?P\d+.\d+)\/(?P\d+.\d+)') WIN32_PING_MATCHER = re.compile( r'(?P\d+)ms.+(?P\d+)ms.+(?P\d+)ms') PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Required(CONF_HOST): cv.string, vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, vol.Optional(CONF_PING_COUNT, default=DEFAULT_PING_COUNT): cv.positive_int, }) def setup_platform(hass, config, add_devices, discovery_info=None): """Set up the Ping Binary sensor.""" name = config.get(CONF_NAME) host = config.get(CONF_HOST) count = config.get(CONF_PING_COUNT) add_devices([PingBinarySensor(name, PingData(host, count))], True) class PingBinarySensor(BinarySensorDevice): """Representation of a Ping Binary sensor.""" def __init__(self, name, ping): """Initialize the Ping Binary sensor.""" self._name = name self.ping = ping @property def name(self): """Return the name of the device.""" return self._name @property def device_class(self): """Return the class of this sensor.""" return DEFAULT_DEVICE_CLASS @property def is_on(self): """Return true if the binary sensor is on.""" return self.ping.available @property def device_state_attributes(self): """Return the state attributes of the ICMP checo request.""" if self.ping.data is not False: return { ATTR_ROUND_TRIP_TIME_AVG: self.ping.data['avg'], ATTR_ROUND_TRIP_TIME_MAX: self.ping.data['max'], ATTR_ROUND_TRIP_TIME_MDEV: self.ping.data['mdev'], ATTR_ROUND_TRIP_TIME_MIN: self.ping.data['min'], } def update(self): """Get the latest data.""" self.ping.update() class PingData: """The Class for handling the data retrieval.""" def __init__(self, host, count): """Initialize the data object.""" self._ip_address = host self._count = count self.data = {} self.available = False if sys.platform == 'win32': self._ping_cmd = [ 'ping', '-n', str(self._count), '-w', '1000', self._ip_address] else: self._ping_cmd = [ 'ping', '-n', '-q', '-c', str(self._count), '-W1', self._ip_address] def ping(self): """Send ICMP echo request and return details if success.""" pinger = subprocess.Popen( self._ping_cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE) try: out = pinger.communicate() _LOGGER.debug("Output is %s", str(out)) if sys.platform == 'win32': match = WIN32_PING_MATCHER.search(str(out).split('\n')[-1]) rtt_min, rtt_avg, rtt_max = match.groups() return { 'min': rtt_min, 'avg': rtt_avg, 'max': rtt_max, 'mdev': ''} if 'max/' not in str(out): match = PING_MATCHER_BUSYBOX.search(str(out).split('\n')[-1]) rtt_min, rtt_avg, rtt_max = match.groups() return { 'min': rtt_min, 'avg': rtt_avg, 'max': rtt_max, 'mdev': ''} match = PING_MATCHER.search(str(out).split('\n')[-1]) rtt_min, rtt_avg, rtt_max, rtt_mdev = match.groups() return { 'min': rtt_min, 'avg': rtt_avg, 'max': rtt_max, 'mdev': rtt_mdev} except (subprocess.CalledProcessError, AttributeError): return False def update(self): """Retrieve the latest details from the host.""" self.data = self.ping() self.available = bool(self.data)