diff --git a/.coveragerc b/.coveragerc index 65e4656297f..e7454ccfa9c 100644 --- a/.coveragerc +++ b/.coveragerc @@ -537,7 +537,6 @@ omit = homeassistant/components/sensor/socialblade.py homeassistant/components/sensor/solaredge.py homeassistant/components/sensor/sonarr.py - homeassistant/components/sensor/speedtest.py homeassistant/components/sensor/spotcrime.py homeassistant/components/sensor/srp_energy.py homeassistant/components/sensor/starlingbank.py @@ -580,6 +579,7 @@ omit = homeassistant/components/skybell/* homeassistant/components/smappee/* homeassistant/components/sonos/* + homeassistant/components/speedtestdotnet/* homeassistant/components/spc.py homeassistant/components/spider/* homeassistant/components/switch/acer_projector.py diff --git a/homeassistant/components/sensor/speedtest.py b/homeassistant/components/sensor/speedtest.py deleted file mode 100644 index f834b51b064..00000000000 --- a/homeassistant/components/sensor/speedtest.py +++ /dev/null @@ -1,171 +0,0 @@ -""" -Support for Speedtest.net. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/sensor.speedtest/ -""" -import logging - -import voluptuous as vol - -from homeassistant.components.sensor import DOMAIN, PLATFORM_SCHEMA -from homeassistant.const import ATTR_ATTRIBUTION, CONF_MONITORED_CONDITIONS -import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.event import track_time_change -from homeassistant.helpers.restore_state import RestoreEntity -import homeassistant.util.dt as dt_util - -REQUIREMENTS = ['speedtest-cli==2.0.2'] - -_LOGGER = logging.getLogger(__name__) - -ATTR_BYTES_RECEIVED = 'bytes_received' -ATTR_BYTES_SENT = 'bytes_sent' -ATTR_SERVER_COUNTRY = 'server_country' -ATTR_SERVER_HOST = 'server_host' -ATTR_SERVER_ID = 'server_id' -ATTR_SERVER_LATENCY = 'latency' -ATTR_SERVER_NAME = 'server_name' - -CONF_ATTRIBUTION = "Data retrieved from Speedtest by Ookla" -CONF_SECOND = 'second' -CONF_MINUTE = 'minute' -CONF_HOUR = 'hour' -CONF_SERVER_ID = 'server_id' -CONF_MANUAL = 'manual' - -ICON = 'mdi:speedometer' - -SENSOR_TYPES = { - 'ping': ['Ping', 'ms'], - '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.Optional(CONF_HOUR): - vol.All(cv.ensure_list, [vol.All(vol.Coerce(int), vol.Range(0, 23))]), - vol.Optional(CONF_MANUAL, default=False): cv.boolean, - vol.Optional(CONF_MINUTE, default=[0]): - vol.All(cv.ensure_list, [vol.All(vol.Coerce(int), vol.Range(0, 59))]), - vol.Optional(CONF_SECOND, default=[0]): - vol.All(cv.ensure_list, [vol.All(vol.Coerce(int), vol.Range(0, 59))]), - vol.Optional(CONF_SERVER_ID): cv.positive_int, -}) - - -def setup_platform(hass, config, add_entities, discovery_info=None): - """Set up the Speedtest sensor.""" - data = SpeedtestData(hass, config) - - dev = [] - for sensor in config[CONF_MONITORED_CONDITIONS]: - dev.append(SpeedtestSensor(data, sensor)) - - add_entities(dev) - - def update(call=None): - """Update service for manual updates.""" - data.update(dt_util.now()) - for sensor in dev: - sensor.update() - - hass.services.register(DOMAIN, 'update_speedtest', update) - - -class SpeedtestSensor(RestoreEntity): - """Implementation of a speedtest.net sensor.""" - - def __init__(self, speedtest_data, sensor_type): - """Initialize the sensor.""" - self._name = SENSOR_TYPES[sensor_type][0] - self.speedtest_client = speedtest_data - self.type = sensor_type - self._state = None - self._data = None - self._unit_of_measurement = SENSOR_TYPES[self.type][1] - - @property - def name(self): - """Return the name of the sensor.""" - return '{} {}'.format('Speedtest', 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 icon(self): - """Return icon.""" - return ICON - - @property - def device_state_attributes(self): - """Return the state attributes.""" - if self._data is not None: - return { - ATTR_ATTRIBUTION: CONF_ATTRIBUTION, - ATTR_BYTES_RECEIVED: self._data['bytes_received'], - ATTR_BYTES_SENT: self._data['bytes_sent'], - ATTR_SERVER_COUNTRY: self._data['server']['country'], - ATTR_SERVER_ID: self._data['server']['id'], - ATTR_SERVER_LATENCY: self._data['server']['latency'], - ATTR_SERVER_NAME: self._data['server']['name'], - } - - def update(self): - """Get the latest data and update the states.""" - self._data = self.speedtest_client.data - if self._data is None: - return - - if self.type == 'ping': - self._state = self._data['ping'] - elif self.type == 'download': - self._state = round(self._data['download'] / 10**6, 2) - elif self.type == 'upload': - self._state = round(self._data['upload'] / 10**6, 2) - - async def async_added_to_hass(self): - """Handle all entity which are about to be added.""" - await super().async_added_to_hass() - state = await self.async_get_last_state() - if not state: - return - self._state = state.state - - -class SpeedtestData: - """Get the latest data from speedtest.net.""" - - def __init__(self, hass, config): - """Initialize the data object.""" - self.data = None - self._server_id = config.get(CONF_SERVER_ID) - if not config.get(CONF_MANUAL): - track_time_change( - hass, self.update, second=config.get(CONF_SECOND), - minute=config.get(CONF_MINUTE), hour=config.get(CONF_HOUR)) - - def update(self, now): - """Get the latest data from speedtest.net.""" - import speedtest - _LOGGER.debug("Executing speedtest...") - - servers = [] if self._server_id is None else [self._server_id] - - speed = speedtest.Speedtest() - speed.get_servers(servers) - speed.get_best_server() - speed.download() - speed.upload() - - self.data = speed.results.dict() diff --git a/homeassistant/components/speedtestdotnet/__init__.py b/homeassistant/components/speedtestdotnet/__init__.py new file mode 100644 index 00000000000..ce6f376d1b0 --- /dev/null +++ b/homeassistant/components/speedtestdotnet/__init__.py @@ -0,0 +1,93 @@ +""" +Support for testing internet speed via Speedtest.net. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/speedtestdotnet/ +""" +import logging +from datetime import timedelta + +import voluptuous as vol + +import homeassistant.helpers.config_validation as cv +from homeassistant.components.speedtestdotnet.const import DOMAIN, \ + DATA_UPDATED, SENSOR_TYPES +from homeassistant.const import CONF_MONITORED_CONDITIONS, \ + CONF_UPDATE_INTERVAL +from homeassistant.helpers.discovery import async_load_platform +from homeassistant.helpers.dispatcher import dispatcher_send +from homeassistant.helpers.event import async_track_time_interval +from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN + +REQUIREMENTS = ['speedtest-cli==2.0.2'] + +_LOGGER = logging.getLogger(__name__) + +CONF_SERVER_ID = 'server_id' +CONF_MANUAL = 'manual' + +DEFAULT_INTERVAL = timedelta(hours=1) + +CONFIG_SCHEMA = vol.Schema({ + DOMAIN: vol.Schema({ + vol.Optional(CONF_SERVER_ID): cv.positive_int, + vol.Optional(CONF_UPDATE_INTERVAL, default=DEFAULT_INTERVAL): + vol.All( + cv.time_period, cv.positive_timedelta + ), + vol.Optional(CONF_MANUAL, default=False): cv.boolean, + vol.Optional(CONF_MONITORED_CONDITIONS, default=list(SENSOR_TYPES)): + vol.All(cv.ensure_list, [vol.In(list(SENSOR_TYPES))]) + }) +}, extra=vol.ALLOW_EXTRA) + + +async def async_setup(hass, config): + """Set up the Speedtest.net component.""" + conf = config[DOMAIN] + data = hass.data[DOMAIN] = SpeedtestData(hass, conf.get(CONF_SERVER_ID)) + + if not conf[CONF_MANUAL]: + async_track_time_interval( + hass, data.update, conf[CONF_UPDATE_INTERVAL] + ) + + def update(call=None): + """Service call to manually update the data.""" + data.update() + + hass.services.async_register(DOMAIN, 'speedtest', update) + + hass.async_create_task( + async_load_platform( + hass, + SENSOR_DOMAIN, + DOMAIN, + conf[CONF_MONITORED_CONDITIONS], + config + ) + ) + + return True + + +class SpeedtestData: + """Get the latest data from speedtest.net.""" + + def __init__(self, hass, server_id): + """Initialize the data object.""" + self.data = None + self._hass = hass + self._servers = [] if server_id is None else [server_id] + + def update(self, now=None): + """Get the latest data from speedtest.net.""" + import speedtest + _LOGGER.debug("Executing speedtest.net speedtest") + speed = speedtest.Speedtest() + speed.get_servers(self._servers) + speed.get_best_server() + speed.download() + speed.upload() + self.data = speed.results.dict() + dispatcher_send(self._hass, DATA_UPDATED) diff --git a/homeassistant/components/speedtestdotnet/const.py b/homeassistant/components/speedtestdotnet/const.py new file mode 100644 index 00000000000..7f19d796fd0 --- /dev/null +++ b/homeassistant/components/speedtestdotnet/const.py @@ -0,0 +1,10 @@ +"""Consts used by Speedtest.net.""" + +DOMAIN = 'speedtestdotnet' +DATA_UPDATED = '{}_data_updated'.format(DOMAIN) + +SENSOR_TYPES = { + 'ping': ['Ping', 'ms'], + 'download': ['Download', 'Mbit/s'], + 'upload': ['Upload', 'Mbit/s'], +} diff --git a/homeassistant/components/speedtestdotnet/sensor.py b/homeassistant/components/speedtestdotnet/sensor.py new file mode 100644 index 00000000000..33557ddacab --- /dev/null +++ b/homeassistant/components/speedtestdotnet/sensor.py @@ -0,0 +1,123 @@ +""" +Support for Speedtest.net internet speed testing sensor. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/sensor.speedtestdotnet/ +""" +import logging + +from homeassistant.components.speedtestdotnet.const import \ + DOMAIN as SPEEDTESTDOTNET_DOMAIN, DATA_UPDATED, SENSOR_TYPES +from homeassistant.const import ATTR_ATTRIBUTION +from homeassistant.core import callback +from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.helpers.restore_state import RestoreEntity + +DEPENDENCIES = ['speedtestdotnet'] + +_LOGGER = logging.getLogger(__name__) + +ATTR_BYTES_RECEIVED = 'bytes_received' +ATTR_BYTES_SENT = 'bytes_sent' +ATTR_SERVER_COUNTRY = 'server_country' +ATTR_SERVER_HOST = 'server_host' +ATTR_SERVER_ID = 'server_id' +ATTR_SERVER_LATENCY = 'latency' +ATTR_SERVER_NAME = 'server_name' + +ATTRIBUTION = 'Data retrieved from Speedtest.net by Ookla' + +ICON = 'mdi:speedometer' + + +async def async_setup_platform(hass, config, async_add_entities, + discovery_info): + """Set up the Speedtest.net sensor.""" + data = hass.data[SPEEDTESTDOTNET_DOMAIN] + async_add_entities( + [SpeedtestSensor(data, sensor) for sensor in discovery_info] + ) + + +class SpeedtestSensor(RestoreEntity): + """Implementation of a speedtest.net sensor.""" + + def __init__(self, speedtest_data, sensor_type): + """Initialize the sensor.""" + self._name = SENSOR_TYPES[sensor_type][0] + self.speedtest_client = speedtest_data + self.type = sensor_type + self._state = None + self._data = None + self._unit_of_measurement = SENSOR_TYPES[self.type][1] + + @property + def name(self): + """Return the name of the sensor.""" + return '{} {}'.format('Speedtest', 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 icon(self): + """Return icon.""" + return ICON + + @property + def should_poll(self): + """Return the polling requirement for this sensor.""" + return False + + @property + def device_state_attributes(self): + """Return the state attributes.""" + attributes = { + ATTR_ATTRIBUTION: ATTRIBUTION + } + if self._data is not None: + return attributes.update({ + ATTR_BYTES_RECEIVED: self._data['bytes_received'], + ATTR_BYTES_SENT: self._data['bytes_sent'], + ATTR_SERVER_COUNTRY: self._data['server']['country'], + ATTR_SERVER_ID: self._data['server']['id'], + ATTR_SERVER_LATENCY: self._data['server']['latency'], + ATTR_SERVER_NAME: self._data['server']['name'], + }) + return attributes + + async def async_added_to_hass(self): + """Handle entity which will be added.""" + await super().async_added_to_hass() + state = await self.async_get_last_state() + if not state: + return + self._state = state.state + + async_dispatcher_connect( + self.hass, DATA_UPDATED, self._schedule_immediate_update + ) + + def update(self): + """Get the latest data and update the states.""" + self._data = self.speedtest_client.data + if self._data is None: + return + + if self.type == 'ping': + self._state = self._data['ping'] + elif self.type == 'download': + self._state = round(self._data['download'] / 10**6, 2) + elif self.type == 'upload': + self._state = round(self._data['upload'] / 10**6, 2) + + @callback + def _schedule_immediate_update(self): + self.async_schedule_update_ha_state(True) diff --git a/homeassistant/components/speedtestdotnet/services.yaml b/homeassistant/components/speedtestdotnet/services.yaml new file mode 100644 index 00000000000..db813affe76 --- /dev/null +++ b/homeassistant/components/speedtestdotnet/services.yaml @@ -0,0 +1,2 @@ +speedtest: + description: Immediately take a speedest with Speedtest.net \ No newline at end of file diff --git a/requirements_all.txt b/requirements_all.txt index 93e5b45fcf5..4ce0ae263aa 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1566,7 +1566,7 @@ solaredge==0.0.2 # homeassistant.components.climate.honeywell somecomfort==0.5.2 -# homeassistant.components.sensor.speedtest +# homeassistant.components.speedtestdotnet speedtest-cli==2.0.2 # homeassistant.components.spider