diff --git a/.coveragerc b/.coveragerc index eb73cb66c30..3ccfdeb3569 100644 --- a/.coveragerc +++ b/.coveragerc @@ -619,6 +619,7 @@ omit = homeassistant/components/sensor/imap_email_content.py homeassistant/components/sensor/imap.py homeassistant/components/sensor/influxdb.py + homeassistant/components/sensor/iperf3.py homeassistant/components/sensor/irish_rail_transport.py homeassistant/components/sensor/kwb.py homeassistant/components/sensor/lacrosse.py diff --git a/Dockerfile b/Dockerfile index 5081b4ba721..75d9e9eb716 100644 --- a/Dockerfile +++ b/Dockerfile @@ -12,6 +12,7 @@ LABEL maintainer="Paulus Schoutsen " #ENV INSTALL_LIBCEC no #ENV INSTALL_PHANTOMJS no #ENV INSTALL_SSOCR no +#ENV INSTALL_IPERF3 no VOLUME /config diff --git a/homeassistant/components/sensor/iperf3.py b/homeassistant/components/sensor/iperf3.py new file mode 100644 index 00000000000..1a209faf17f --- /dev/null +++ b/homeassistant/components/sensor/iperf3.py @@ -0,0 +1,178 @@ +""" +Support for Iperf3 network measurement tool. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/sensor.iperf3/ +""" +import logging +from datetime import timedelta + +import voluptuous as vol + +from homeassistant.components.sensor import DOMAIN, PLATFORM_SCHEMA +from homeassistant.const import ( + ATTR_ATTRIBUTION, ATTR_ENTITY_ID, CONF_MONITORED_CONDITIONS, + CONF_HOST, CONF_PORT) +import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.entity import Entity + +REQUIREMENTS = ['iperf3==0.1.10'] + +_LOGGER = logging.getLogger(__name__) + +ATTR_PROTOCOL = 'Protocol' +ATTR_REMOTE_HOST = 'Remote Server' +ATTR_REMOTE_PORT = 'Remote Port' +ATTR_VERSION = 'Version' + +CONF_ATTRIBUTION = 'Data retrieved using Iperf3' +CONF_DURATION = 'duration' + +DEFAULT_DURATION = 10 +DEFAULT_PORT = 5201 + +IPERF3_DATA = 'iperf3' + +SCAN_INTERVAL = timedelta(minutes=30) + +SERVICE_NAME = 'iperf3_update' + +ICON = 'mdi:speedometer' + +SENSOR_TYPES = { + 'download': ['Download', 'Mbit/s'], + 'upload': ['Upload', 'Mbit/s'], +} + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Required(CONF_MONITORED_CONDITIONS): + vol.All(cv.ensure_list, [vol.In(list(SENSOR_TYPES))]), + vol.Required(CONF_PORT, default=DEFAULT_PORT): cv.port, + vol.Required(CONF_HOST): cv.string, + vol.Optional(CONF_DURATION, default=DEFAULT_DURATION): vol.Range(5, 10), +}) + + +SERVICE_SCHEMA = vol.Schema({ + vol.Optional(ATTR_ENTITY_ID): cv.string, +}) + + +def setup_platform(hass, config, add_devices, discovery_info=None): + """Set up the Iperf3 sensor.""" + if hass.data.get(IPERF3_DATA) is None: + hass.data[IPERF3_DATA] = {} + hass.data[IPERF3_DATA]['sensors'] = [] + + dev = [] + for sensor in config[CONF_MONITORED_CONDITIONS]: + dev.append( + Iperf3Sensor(config[CONF_HOST], + config[CONF_PORT], + config[CONF_DURATION], + sensor)) + + hass.data[IPERF3_DATA]['sensors'].extend(dev) + add_devices(dev) + + def _service_handler(service): + """Update service for manual updates.""" + entity_id = service.data.get('entity_id') + all_iperf3_sensors = hass.data[IPERF3_DATA]['sensors'] + + for sensor in all_iperf3_sensors: + if entity_id is not None: + if sensor.entity_id == entity_id: + sensor.update() + sensor.schedule_update_ha_state() + break + else: + sensor.update() + sensor.schedule_update_ha_state() + + for sensor in dev: + hass.services.register(DOMAIN, SERVICE_NAME, _service_handler, + schema=SERVICE_SCHEMA) + + +class Iperf3Sensor(Entity): + """A Iperf3 sensor implementation.""" + + def __init__(self, server, port, duration, sensor_type): + """Initialize the sensor.""" + self._attrs = { + ATTR_ATTRIBUTION: CONF_ATTRIBUTION, + } + self._name = \ + "{} {}".format(SENSOR_TYPES[sensor_type][0], server) + self._state = None + self._sensor_type = sensor_type + self._unit_of_measurement = SENSOR_TYPES[sensor_type][1] + self._port = port + self._server = server + self._duration = duration + self.result = None + + @property + def name(self): + """Return the name of the sensor.""" + return self._name + + @property + def state(self): + """Return the state of the device.""" + return self._state + + @property + def unit_of_measurement(self): + """Return the unit of measurement of this entity, if any.""" + return self._unit_of_measurement + + @property + def device_state_attributes(self): + """Return the state attributes.""" + if self.result is not None: + self._attrs[ATTR_ATTRIBUTION] = CONF_ATTRIBUTION + self._attrs[ATTR_PROTOCOL] = self.result.protocol + self._attrs[ATTR_REMOTE_HOST] = self.result.remote_host + self._attrs[ATTR_REMOTE_PORT] = self.result.remote_port + self._attrs[ATTR_VERSION] = self.result.version + return self._attrs + + def update(self): + """Get the latest data and update the states.""" + import iperf3 + client = iperf3.Client() + client.duration = self._duration + client.server_hostname = self._server + client.port = self._port + client.verbose = False + + # when testing download bandwith, reverse must be True + if self._sensor_type == 'download': + client.reverse = True + + try: + self.result = client.run() + except (OSError, AttributeError) as error: + self.result = None + _LOGGER.error("Iperf3 sensor error: %s", error) + return + + if self.result is not None and \ + hasattr(self.result, 'error') and \ + self.result.error is not None: + _LOGGER.error("Iperf3 sensor error: %s", self.result.error) + self.result = None + return + + if self._sensor_type == 'download': + self._state = round(self.result.received_Mbps, 2) + + elif self._sensor_type == 'upload': + self._state = round(self.result.sent_Mbps, 2) + + @property + def icon(self): + """Return icon.""" + return ICON diff --git a/requirements_all.txt b/requirements_all.txt index 0879c539ea8..7a40a1aa48e 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -451,6 +451,9 @@ insteonlocal==0.53 # homeassistant.components.insteon_plm insteonplm==0.9.2 +# homeassistant.components.sensor.iperf3 +iperf3==0.1.10 + # homeassistant.components.verisure jsonpath==0.75 diff --git a/virtualization/Docker/Dockerfile.dev b/virtualization/Docker/Dockerfile.dev index 06676140702..d0599c2e74c 100644 --- a/virtualization/Docker/Dockerfile.dev +++ b/virtualization/Docker/Dockerfile.dev @@ -13,6 +13,7 @@ LABEL maintainer="Paulus Schoutsen " #ENV INSTALL_PHANTOMJS no #ENV INSTALL_COAP no #ENV INSTALL_SSOCR no +#ENV INSTALL_IPERF3 no VOLUME /config diff --git a/virtualization/Docker/scripts/iperf3 b/virtualization/Docker/scripts/iperf3 new file mode 100755 index 00000000000..2d9d5a33761 --- /dev/null +++ b/virtualization/Docker/scripts/iperf3 @@ -0,0 +1,11 @@ +#!/bin/bash +# Sets up iperf3. + +# Stop on errors +set -e + +PACKAGES=( + iperf3 +) + +apt-get install -y --no-install-recommends ${PACKAGES[@]} diff --git a/virtualization/Docker/setup_docker_prereqs b/virtualization/Docker/setup_docker_prereqs index 302dfba2e1d..3bb4136c991 100755 --- a/virtualization/Docker/setup_docker_prereqs +++ b/virtualization/Docker/setup_docker_prereqs @@ -10,6 +10,7 @@ INSTALL_FFMPEG="${INSTALL_FFMPEG:-yes}" INSTALL_LIBCEC="${INSTALL_LIBCEC:-yes}" INSTALL_PHANTOMJS="${INSTALL_PHANTOMJS:-yes}" INSTALL_SSOCR="${INSTALL_SSOCR:-yes}" +INSTALL_IPERF3="${INSTALL_IPERF3:-yes}" # Required debian packages for running hass or components PACKAGES=( @@ -64,6 +65,10 @@ if [ "$INSTALL_SSOCR" == "yes" ]; then virtualization/Docker/scripts/ssocr fi +if [ "$INSTALL_IPERF3" == "yes" ]; then + virtualization/Docker/scripts/iperf3 +fi + # Remove packages apt-get remove -y --purge ${PACKAGES_DEV[@]} apt-get -y --purge autoremove