From 29e659cf4c4de7b9e25d0f9d8953444e91bc52d6 Mon Sep 17 00:00:00 2001 From: Vincent Van Den Berghe Date: Tue, 13 Mar 2018 22:20:56 +0100 Subject: [PATCH 001/144] Fixed SI units for current consumption --- homeassistant/components/sensor/smappee.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/sensor/smappee.py b/homeassistant/components/sensor/smappee.py index 51595d19b1a..c59798d16d7 100644 --- a/homeassistant/components/sensor/smappee.py +++ b/homeassistant/components/sensor/smappee.py @@ -21,17 +21,17 @@ SENSOR_TYPES = { 'active_power': ['Active Power', 'mdi:power-plug', 'local', 'W', 'active_power'], 'current': - ['Current', 'mdi:gauge', 'local', 'Amps', 'current'], + ['Current', 'mdi:gauge', 'local', 'A', 'current'], 'voltage': ['Voltage', 'mdi:gauge', 'local', 'V', 'voltage'], 'active_cosfi': ['Power Factor', 'mdi:gauge', 'local', '%', 'active_cosfi'], 'alwayson_today': - ['Always On Today', 'mdi:gauge', 'remote', 'kW', 'alwaysOn'], + ['Always On Today', 'mdi:gauge', 'remote', 'kWh', 'alwaysOn'], 'solar_today': - ['Solar Today', 'mdi:white-balance-sunny', 'remote', 'kW', 'solar'], + ['Solar Today', 'mdi:white-balance-sunny', 'remote', 'kWh', 'solar'], 'power_today': - ['Power Today', 'mdi:power-plug', 'remote', 'kW', 'consumption'] + ['Power Today', 'mdi:power-plug', 'remote', 'kWh', 'consumption'] } SCAN_INTERVAL = timedelta(seconds=30) From 855ed2b4e423147ce07fa69d6295ba561e6cb349 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sun, 3 Jun 2018 16:54:23 -0400 Subject: [PATCH 002/144] Version bump to 0.72.0.dev0 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 4c9757b3260..5644c3d0a1f 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -1,7 +1,7 @@ # coding: utf-8 """Constants used by Home Assistant components.""" MAJOR_VERSION = 0 -MINOR_VERSION = 71 +MINOR_VERSION = 72 PATCH_VERSION = '0.dev0' __short_version__ = '{}.{}'.format(MAJOR_VERSION, MINOR_VERSION) __version__ = '{}.{}'.format(__short_version__, PATCH_VERSION) From aec425d1f6ff35dabae71a296d1a4d64729265b2 Mon Sep 17 00:00:00 2001 From: Diogo Gomes Date: Sun, 3 Jun 2018 22:01:48 +0100 Subject: [PATCH 003/144] Weather Platform - IPMA (#14716) * initial commit * lint * update with pyipma * Added test * Added test * lint * missing dep * address comments * lint * make sure list is iterable * don't bother with list * mock dependency * no need to add test requirements * last correction --- homeassistant/components/weather/ipma.py | 172 +++++++++++++++++++++++ requirements_all.txt | 3 + tests/components/weather/test_ipma.py | 85 +++++++++++ 3 files changed, 260 insertions(+) create mode 100644 homeassistant/components/weather/ipma.py create mode 100644 tests/components/weather/test_ipma.py diff --git a/homeassistant/components/weather/ipma.py b/homeassistant/components/weather/ipma.py new file mode 100644 index 00000000000..ef4f1b349d7 --- /dev/null +++ b/homeassistant/components/weather/ipma.py @@ -0,0 +1,172 @@ +""" +Support for IPMA weather service. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/weather.ipma/ +""" +import logging +from datetime import timedelta + +import async_timeout +import voluptuous as vol + +from homeassistant.components.weather import ( + WeatherEntity, PLATFORM_SCHEMA, ATTR_FORECAST_CONDITION, + ATTR_FORECAST_PRECIPITATION, ATTR_FORECAST_TEMP, + ATTR_FORECAST_TEMP_LOW, ATTR_FORECAST_TIME) +from homeassistant.const import \ + CONF_NAME, TEMP_CELSIUS, CONF_LATITUDE, CONF_LONGITUDE +from homeassistant.helpers.aiohttp_client import async_get_clientsession +from homeassistant.helpers import config_validation as cv +from homeassistant.util import Throttle + +REQUIREMENTS = ['pyipma==1.1.3'] + +_LOGGER = logging.getLogger(__name__) + +ATTRIBUTION = 'Instituto Português do Mar e Atmosfera' + +ATTR_WEATHER_DESCRIPTION = "description" + +MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=30) + +CONDITION_CLASSES = { + 'cloudy': [4, 5, 24, 25, 27], + 'fog': [16, 17, 26], + 'hail': [21, 22], + 'lightning': [19], + 'lightning-rainy': [20, 23], + 'partlycloudy': [2, 3], + 'pouring': [8, 11], + 'rainy': [6, 7, 9, 10, 12, 13, 14, 15], + 'snowy': [18], + 'snowy-rainy': [], + 'sunny': [1], + 'windy': [], + 'windy-variant': [], + 'exceptional': [], +} + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Optional(CONF_NAME): cv.string, + vol.Optional(CONF_LATITUDE): cv.latitude, + vol.Optional(CONF_LONGITUDE): cv.longitude, +}) + + +async def async_setup_platform(hass, config, async_add_devices, + discovery_info=None): + """Set up the ipma platform.""" + latitude = config.get(CONF_LATITUDE, hass.config.latitude) + longitude = config.get(CONF_LONGITUDE, hass.config.longitude) + + if None in (latitude, longitude): + _LOGGER.error("Latitude or longitude not set in Home Assistant config") + return + + from pyipma import Station + + websession = async_get_clientsession(hass) + with async_timeout.timeout(10, loop=hass.loop): + station = await Station.get(websession, float(latitude), + float(longitude)) + + _LOGGER.debug("Initializing ipma weather: coordinates %s, %s", + latitude, longitude) + + async_add_devices([IPMAWeather(station, config)], True) + + +class IPMAWeather(WeatherEntity): + """Representation of a weather condition.""" + + def __init__(self, station, config): + """Initialise the platform with a data instance and station name.""" + self._station_name = config.get(CONF_NAME, station.local) + self._station = station + self._condition = None + self._forecast = None + self._description = None + + @Throttle(MIN_TIME_BETWEEN_UPDATES) + async def async_update(self): + """Update Condition and Forecast.""" + with async_timeout.timeout(10, loop=self.hass.loop): + self._condition = await self._station.observation() + self._forecast = await self._station.forecast() + self._description = self._forecast[0].description + + @property + def attribution(self): + """Return the attribution.""" + return ATTRIBUTION + + @property + def name(self): + """Return the name of the station.""" + return self._station_name + + @property + def condition(self): + """Return the current condition.""" + return next((k for k, v in CONDITION_CLASSES.items() + if self._forecast[0].idWeatherType in v), None) + + @property + def temperature(self): + """Return the current temperature.""" + return self._condition.temperature + + @property + def pressure(self): + """Return the current pressure.""" + return self._condition.pressure + + @property + def humidity(self): + """Return the name of the sensor.""" + return self._condition.humidity + + @property + def wind_speed(self): + """Return the current windspeed.""" + return self._condition.windspeed + + @property + def wind_bearing(self): + """Return the current wind bearing (degrees).""" + return self._condition.winddirection + + @property + def temperature_unit(self): + """Return the unit of measurement.""" + return TEMP_CELSIUS + + @property + def forecast(self): + """Return the forecast array.""" + if self._forecast: + fcdata_out = [] + for data_in in self._forecast: + data_out = {} + data_out[ATTR_FORECAST_TIME] = data_in.forecastDate + data_out[ATTR_FORECAST_CONDITION] =\ + next((k for k, v in CONDITION_CLASSES.items() + if int(data_in.idWeatherType) in v), None) + data_out[ATTR_FORECAST_TEMP_LOW] = data_in.tMin + data_out[ATTR_FORECAST_TEMP] = data_in.tMax + data_out[ATTR_FORECAST_PRECIPITATION] = data_in.precipitaProb + + fcdata_out.append(data_out) + + return fcdata_out + + @property + def device_state_attributes(self): + """Return the state attributes.""" + data = dict() + + if self._description: + data[ATTR_WEATHER_DESCRIPTION] = self._description + + return data diff --git a/requirements_all.txt b/requirements_all.txt index fd2bb5b4f5a..fcd1e3726e9 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -834,6 +834,9 @@ pyialarm==0.2 # homeassistant.components.device_tracker.icloud pyicloud==0.9.1 +# homeassistant.components.weather.ipma +pyipma==1.1.3 + # homeassistant.components.sensor.irish_rail_transport pyirishrail==0.0.2 diff --git a/tests/components/weather/test_ipma.py b/tests/components/weather/test_ipma.py new file mode 100644 index 00000000000..7df6166a2b6 --- /dev/null +++ b/tests/components/weather/test_ipma.py @@ -0,0 +1,85 @@ +"""The tests for the IPMA weather component.""" +import unittest +from unittest.mock import patch +from collections import namedtuple + +from homeassistant.components import weather +from homeassistant.components.weather import ( + ATTR_WEATHER_HUMIDITY, ATTR_WEATHER_PRESSURE, ATTR_WEATHER_TEMPERATURE, + ATTR_WEATHER_WIND_BEARING, ATTR_WEATHER_WIND_SPEED) +from homeassistant.util.unit_system import METRIC_SYSTEM +from homeassistant.setup import setup_component + +from tests.common import get_test_home_assistant, MockDependency + + +class MockStation(): + """Mock Station from pyipma.""" + + @classmethod + async def get(cls, websession, lat, lon): + """Mock Factory.""" + return MockStation() + + async def observation(self): + """Mock Observation.""" + Observation = namedtuple('Observation', ['temperature', 'humidity', + 'windspeed', 'winddirection', + 'precipitation', 'pressure', + 'description']) + + return Observation(18, 71.0, 3.94, 'NW', 0, 1000.0, '---') + + async def forecast(self): + """Mock Forecast.""" + Forecast = namedtuple('Forecast', ['precipitaProb', 'tMin', 'tMax', + 'predWindDir', 'idWeatherType', + 'classWindSpeed', 'longitude', + 'forecastDate', 'classPrecInt', + 'latitude', 'description']) + + return [Forecast(73.0, 13.7, 18.7, 'NW', 6, 2, -8.64, + '2018-05-31', 2, 40.61, + 'Aguaceiros, com vento Moderado de Noroeste')] + + @property + def local(self): + """Mock location.""" + return "HomeTown" + + +class TestIPMA(unittest.TestCase): + """Test the IPMA weather component.""" + + def setUp(self): + """Setup things to be run when tests are started.""" + self.hass = get_test_home_assistant() + self.hass.config.units = METRIC_SYSTEM + self.lat = self.hass.config.latitude = 40.00 + self.lon = self.hass.config.longitude = -8.00 + + def tearDown(self): + """Stop down everything that was started.""" + self.hass.stop() + + @MockDependency("pyipma") + @patch("pyipma.Station", new=MockStation) + def test_setup(self, mock_pyipma): + """Test for successfully setting up the IPMA platform.""" + self.assertTrue(setup_component(self.hass, weather.DOMAIN, { + 'weather': { + 'name': 'HomeTown', + 'platform': 'ipma', + } + })) + + state = self.hass.states.get('weather.hometown') + self.assertEqual(state.state, 'rainy') + + data = state.attributes + self.assertEqual(data.get(ATTR_WEATHER_TEMPERATURE), 18.0) + self.assertEqual(data.get(ATTR_WEATHER_HUMIDITY), 71) + self.assertEqual(data.get(ATTR_WEATHER_PRESSURE), 1000.0) + self.assertEqual(data.get(ATTR_WEATHER_WIND_SPEED), 3.94) + self.assertEqual(data.get(ATTR_WEATHER_WIND_BEARING), 'NW') + self.assertEqual(state.attributes.get('friendly_name'), 'HomeTown') From 39843a73de1b8e2a42be1e9300580726baecd18f Mon Sep 17 00:00:00 2001 From: Sebastian Muszynski Date: Mon, 4 Jun 2018 07:39:50 +0200 Subject: [PATCH 004/144] Add additional 86sw model identifier of the LAN protocol V2 (#14799) --- homeassistant/components/binary_sensor/xiaomi_aqara.py | 4 ++-- homeassistant/components/xiaomi_aqara.py | 2 +- requirements_all.txt | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/binary_sensor/xiaomi_aqara.py b/homeassistant/components/binary_sensor/xiaomi_aqara.py index 72a4cfdfbaa..ebdcdc6ca70 100644 --- a/homeassistant/components/binary_sensor/xiaomi_aqara.py +++ b/homeassistant/components/binary_sensor/xiaomi_aqara.py @@ -43,10 +43,10 @@ def setup_platform(hass, config, add_devices, discovery_info=None): data_key = 'channel_0' devices.append(XiaomiButton(device, 'Switch', data_key, hass, gateway)) - elif model in ['86sw1', 'sensor_86sw1.aq1']: + elif model in ['86sw1', 'sensor_86sw1', 'sensor_86sw1.aq1']: devices.append(XiaomiButton(device, 'Wall Switch', 'channel_0', hass, gateway)) - elif model in ['86sw2', 'sensor_86sw2.aq1']: + elif model in ['86sw2', 'sensor_86sw2', 'sensor_86sw2.aq1']: devices.append(XiaomiButton(device, 'Wall Switch (Left)', 'channel_0', hass, gateway)) devices.append(XiaomiButton(device, 'Wall Switch (Right)', diff --git a/homeassistant/components/xiaomi_aqara.py b/homeassistant/components/xiaomi_aqara.py index ae3a4e0be72..2090f522709 100644 --- a/homeassistant/components/xiaomi_aqara.py +++ b/homeassistant/components/xiaomi_aqara.py @@ -23,7 +23,7 @@ from homeassistant.helpers.event import async_track_point_in_utc_time from homeassistant.util.dt import utcnow from homeassistant.util import slugify -REQUIREMENTS = ['PyXiaomiGateway==0.9.4'] +REQUIREMENTS = ['PyXiaomiGateway==0.9.5'] _LOGGER = logging.getLogger(__name__) diff --git a/requirements_all.txt b/requirements_all.txt index fcd1e3726e9..00ed2f88cb7 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -46,7 +46,7 @@ PyMVGLive==1.1.4 PyMata==2.14 # homeassistant.components.xiaomi_aqara -PyXiaomiGateway==0.9.4 +PyXiaomiGateway==0.9.5 # homeassistant.components.rpi_gpio # RPi.GPIO==0.6.1 From 1d23f7f9003e9fa88e5554aa9027e3a975f981ad Mon Sep 17 00:00:00 2001 From: quthla Date: Mon, 4 Jun 2018 13:24:28 +0200 Subject: [PATCH 005/144] Allow Kodi live streams to be recognized as paused (#14623) --- homeassistant/components/media_player/kodi.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/media_player/kodi.py b/homeassistant/components/media_player/kodi.py index 68a9da55ae4..7fa8d5b3fe8 100644 --- a/homeassistant/components/media_player/kodi.py +++ b/homeassistant/components/media_player/kodi.py @@ -393,7 +393,7 @@ class KodiDevice(MediaPlayerDevice): if not self._players: return STATE_IDLE - if self._properties['speed'] == 0 and not self._properties['live']: + if self._properties['speed'] == 0: return STATE_PAUSED return STATE_PLAYING From bd1b1a9ff9dccf8f3000cec1888aa094b28a0c71 Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Mon, 4 Jun 2018 14:44:55 +0200 Subject: [PATCH 006/144] Update syntax (#14812) --- homeassistant/components/sensor/moon.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/sensor/moon.py b/homeassistant/components/sensor/moon.py index 75b8a1f72bd..0c57c98c0af 100644 --- a/homeassistant/components/sensor/moon.py +++ b/homeassistant/components/sensor/moon.py @@ -4,7 +4,6 @@ Support for tracking the moon phases. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/sensor.moon/ """ -import asyncio import logging import voluptuous as vol @@ -26,8 +25,8 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ }) -@asyncio.coroutine -def async_setup_platform(hass, config, async_add_devices, discovery_info=None): +async def async_setup_platform( + hass, config, async_add_devices, discovery_info=None): """Set up the Moon sensor.""" name = config.get(CONF_NAME) @@ -71,8 +70,7 @@ class MoonSensor(Entity): """Icon to use in the frontend, if any.""" return ICON - @asyncio.coroutine - def async_update(self): + async def async_update(self): """Get the time and updates the states.""" from astral import Astral From 816efa02d1487daae1f2253060513f1ace7a9710 Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Mon, 4 Jun 2018 18:49:26 +0200 Subject: [PATCH 007/144] Use pihole module to get data (#14809) --- homeassistant/components/sensor/pi_hole.py | 148 +++++++++++---------- requirements_all.txt | 3 + 2 files changed, 82 insertions(+), 69 deletions(-) diff --git a/homeassistant/components/sensor/pi_hole.py b/homeassistant/components/sensor/pi_hole.py index 027c12569a6..8e8c784e68b 100644 --- a/homeassistant/components/sensor/pi_hole.py +++ b/homeassistant/components/sensor/pi_hole.py @@ -1,23 +1,26 @@ """ -Support for getting statistical data from a Pi-Hole system. +Support for getting statistical data from a Pi-hole system. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/sensor.pi_hole/ """ -import logging -import json from datetime import timedelta +import logging import voluptuous as vol -import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.entity import Entity from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.const import ( - CONF_NAME, CONF_HOST, CONF_SSL, CONF_VERIFY_SSL, CONF_MONITORED_CONDITIONS) + CONF_HOST, CONF_MONITORED_CONDITIONS, CONF_NAME, CONF_SSL, CONF_VERIFY_SSL) +from homeassistant.exceptions import PlatformNotReady +from homeassistant.helpers.aiohttp_client import async_get_clientsession +import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.entity import Entity +from homeassistant.util import Throttle + +REQUIREMENTS = ['pihole==0.1.2'] _LOGGER = logging.getLogger(__name__) -_ENDPOINT = '/api.php' ATTR_BLOCKED_DOMAINS = 'domains_blocked' ATTR_PERCENTAGE_TODAY = 'percentage_today' @@ -32,25 +35,27 @@ DEFAULT_NAME = 'Pi-Hole' DEFAULT_SSL = False DEFAULT_VERIFY_SSL = True -SCAN_INTERVAL = timedelta(minutes=5) +MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=5) MONITORED_CONDITIONS = { - 'dns_queries_today': ['DNS Queries Today', - 'queries', 'mdi:comment-question-outline'], - 'ads_blocked_today': ['Ads Blocked Today', - 'ads', 'mdi:close-octagon-outline'], - 'ads_percentage_today': ['Ads Percentage Blocked Today', - '%', 'mdi:close-octagon-outline'], - 'domains_being_blocked': ['Domains Blocked', - 'domains', 'mdi:block-helper'], - 'queries_cached': ['DNS Queries Cached', - 'queries', 'mdi:comment-question-outline'], - 'queries_forwarded': ['DNS Queries Forwarded', - 'queries', 'mdi:comment-question-outline'], - 'unique_clients': ['DNS Unique Clients', - 'clients', 'mdi:account-outline'], - 'unique_domains': ['DNS Unique Domains', - 'domains', 'mdi:domain'], + 'ads_blocked_today': + ['Ads Blocked Today', 'ads', 'mdi:close-octagon-outline'], + 'ads_percentage_today': + ['Ads Percentage Blocked Today', '%', 'mdi:close-octagon-outline'], + 'clients_ever_seen': + ['Seen Clients', 'clients', 'mdi:account-outline'], + 'dns_queries_today': + ['DNS Queries Today', 'queries', 'mdi:comment-question-outline'], + 'domains_being_blocked': + ['Domains Blocked', 'domains', 'mdi:block-helper'], + 'queries_cached': + ['DNS Queries Cached', 'queries', 'mdi:comment-question-outline'], + 'queries_forwarded': + ['DNS Queries Forwarded', 'queries', 'mdi:comment-question-outline'], + 'unique_clients': + ['DNS Unique Clients', 'clients', 'mdi:account-outline'], + 'unique_domains': + ['DNS Unique Domains', 'domains', 'mdi:domain'], } PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ @@ -65,100 +70,105 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ }) -def setup_platform(hass, config, add_devices, discovery_info=None): - """Set up the Pi-Hole sensor.""" +async def async_setup_platform( + hass, config, async_add_devices, discovery_info=None): + """Set up the Pi-hole sensor.""" + from pihole import PiHole + name = config.get(CONF_NAME) host = config.get(CONF_HOST) - use_ssl = config.get(CONF_SSL) + use_tls = config.get(CONF_SSL) location = config.get(CONF_LOCATION) - verify_ssl = config.get(CONF_VERIFY_SSL) + verify_tls = config.get(CONF_VERIFY_SSL) - api = PiHoleAPI('{}/{}'.format(host, location), use_ssl, verify_ssl) + session = async_get_clientsession(hass) + pi_hole = PiHoleData(PiHole( + host, hass.loop, session, location=location, tls=use_tls, + verify_tls=verify_tls)) - sensors = [PiHoleSensor(hass, api, name, condition) + await pi_hole.async_update() + + if pi_hole.api.data is None: + raise PlatformNotReady + + sensors = [PiHoleSensor(pi_hole, name, condition) for condition in config[CONF_MONITORED_CONDITIONS]] - add_devices(sensors, True) + async_add_devices(sensors, True) class PiHoleSensor(Entity): - """Representation of a Pi-Hole sensor.""" + """Representation of a Pi-hole sensor.""" - def __init__(self, hass, api, name, variable): - """Initialize a Pi-Hole sensor.""" - self._hass = hass - self._api = api + def __init__(self, pi_hole, name, condition): + """Initialize a Pi-hole sensor.""" + self.pi_hole = pi_hole self._name = name - self._var_id = variable + self._condition = condition - variable_info = MONITORED_CONDITIONS[variable] - self._var_name = variable_info[0] - self._var_units = variable_info[1] - self._var_icon = variable_info[2] + variable_info = MONITORED_CONDITIONS[condition] + self._condition_name = variable_info[0] + self._unit_of_measurement = variable_info[1] + self._icon = variable_info[2] + self.data = {} @property def name(self): """Return the name of the sensor.""" - return "{} {}".format(self._name, self._var_name) + return "{} {}".format(self._name, self._condition_name) @property def icon(self): """Icon to use in the frontend, if any.""" - return self._var_icon + return self._icon @property def unit_of_measurement(self): """Return the unit the value is expressed in.""" - return self._var_units + return self._unit_of_measurement - # pylint: disable=no-member @property def state(self): """Return the state of the device.""" try: - return round(self._api.data[self._var_id], 2) + return round(self.data[self._condition], 2) except TypeError: - return self._api.data[self._var_id] + return self.data[self._condition] - # pylint: disable=no-member @property def device_state_attributes(self): """Return the state attributes of the Pi-Hole.""" return { - ATTR_BLOCKED_DOMAINS: self._api.data['domains_being_blocked'], + ATTR_BLOCKED_DOMAINS: self.data['domains_being_blocked'], } @property def available(self): """Could the device be accessed during the last update call.""" - return self._api.available + return self.pi_hole.available - def update(self): - """Get the latest data from the Pi-Hole API.""" - self._api.update() + async def async_update(self): + """Get the latest data from the Pi-hole API.""" + await self.pi_hole.async_update() + self.data = self.pi_hole.api.data -class PiHoleAPI(object): +class PiHoleData(object): """Get the latest data and update the states.""" - def __init__(self, host, use_ssl, verify_ssl): + def __init__(self, api): """Initialize the data object.""" - from homeassistant.components.sensor.rest import RestData - - uri_scheme = 'https://' if use_ssl else 'http://' - resource = "{}{}{}".format(uri_scheme, host, _ENDPOINT) - - self._rest = RestData('GET', resource, None, None, None, verify_ssl) - self.data = None + self.api = api self.available = True - self.update() - def update(self): - """Get the latest data from the Pi-Hole.""" + @Throttle(MIN_TIME_BETWEEN_UPDATES) + async def async_update(self): + """Get the latest data from the Pi-hole.""" + from pihole.exceptions import PiHoleError + try: - self._rest.update() - self.data = json.loads(self._rest.data) + await self.api.get_data() self.available = True - except TypeError: - _LOGGER.error("Unable to fetch data from Pi-Hole") + except PiHoleError: + _LOGGER.error("Unable to fetch data from Pi-hole") self.available = False diff --git a/requirements_all.txt b/requirements_all.txt index 00ed2f88cb7..59cec2c1e6a 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -633,6 +633,9 @@ pifacedigitalio==3.0.5 # homeassistant.components.light.piglow piglow==1.2.4 +# homeassistant.components.sensor.pi_hole +pihole==0.1.2 + # homeassistant.components.pilight pilight==0.1.1 From 61a41bb8fcd1cc31b5bc20175c0ab8fc49427b6b Mon Sep 17 00:00:00 2001 From: vandenberghev Date: Mon, 4 Jun 2018 20:08:17 +0200 Subject: [PATCH 008/144] Fix issue #14426: [homeassistant.components.sensor] smappee: Error on device update! https://github.com/home-assistant/home-assistant/issues/14426 --- homeassistant/components/sensor/smappee.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/sensor/smappee.py b/homeassistant/components/sensor/smappee.py index 5b84962144d..0263a1266c6 100644 --- a/homeassistant/components/sensor/smappee.py +++ b/homeassistant/components/sensor/smappee.py @@ -189,8 +189,10 @@ class SmappeeSensor(Entity): data = self._smappee.sensor_consumption[self._location_id]\ .get(int(sensor_id)) if data: - consumption = data.get('records')[-1] - _LOGGER.debug("%s (%s) %s", - sensor_name, sensor_id, consumption) - value = consumption.get(self._smappe_name) - self._state = value + tempdata = data.get('records'); + if tempdata: + consumption = tempdata[-1] + _LOGGER.debug("%s (%s) %s", + sensor_name, sensor_id, consumption) + value = consumption.get(self._smappe_name) + self._state = value From e370d523ec5051e3095313af79aa9ff52defe390 Mon Sep 17 00:00:00 2001 From: Sebastian Muszynski Date: Mon, 4 Jun 2018 23:50:18 +0200 Subject: [PATCH 009/144] Bump python-miio version (Closes: #13749) (#14796) * Bump python-miio version * Fix Xiaomi Power Strip V1 support (Closes: #13749) --- homeassistant/components/device_tracker/xiaomi_miio.py | 2 +- homeassistant/components/fan/xiaomi_miio.py | 2 +- homeassistant/components/light/xiaomi_miio.py | 2 +- homeassistant/components/remote/xiaomi_miio.py | 2 +- homeassistant/components/sensor/xiaomi_miio.py | 2 +- homeassistant/components/switch/xiaomi_miio.py | 4 ++-- homeassistant/components/vacuum/xiaomi_miio.py | 2 +- requirements_all.txt | 2 +- 8 files changed, 9 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/device_tracker/xiaomi_miio.py b/homeassistant/components/device_tracker/xiaomi_miio.py index c5769253657..5d6e1453124 100644 --- a/homeassistant/components/device_tracker/xiaomi_miio.py +++ b/homeassistant/components/device_tracker/xiaomi_miio.py @@ -20,7 +20,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Required(CONF_TOKEN): vol.All(cv.string, vol.Length(min=32, max=32)), }) -REQUIREMENTS = ['python-miio==0.3.9', 'construct==2.9.41'] +REQUIREMENTS = ['python-miio==0.4.0', 'construct==2.9.41'] def get_scanner(hass, config): diff --git a/homeassistant/components/fan/xiaomi_miio.py b/homeassistant/components/fan/xiaomi_miio.py index 2acc3895f3e..2f00de08005 100644 --- a/homeassistant/components/fan/xiaomi_miio.py +++ b/homeassistant/components/fan/xiaomi_miio.py @@ -49,7 +49,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ 'zhimi.humidifier.ca1']), }) -REQUIREMENTS = ['python-miio==0.3.9', 'construct==2.9.41'] +REQUIREMENTS = ['python-miio==0.4.0', 'construct==2.9.41'] ATTR_MODEL = 'model' diff --git a/homeassistant/components/light/xiaomi_miio.py b/homeassistant/components/light/xiaomi_miio.py index 24eab7ebd4a..cba15f6df9f 100644 --- a/homeassistant/components/light/xiaomi_miio.py +++ b/homeassistant/components/light/xiaomi_miio.py @@ -42,7 +42,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ 'philips.light.candle2']), }) -REQUIREMENTS = ['python-miio==0.3.9', 'construct==2.9.41'] +REQUIREMENTS = ['python-miio==0.4.0', 'construct==2.9.41'] # The light does not accept cct values < 1 CCT_MIN = 1 diff --git a/homeassistant/components/remote/xiaomi_miio.py b/homeassistant/components/remote/xiaomi_miio.py index e731d421e69..8a3e51b55b3 100644 --- a/homeassistant/components/remote/xiaomi_miio.py +++ b/homeassistant/components/remote/xiaomi_miio.py @@ -22,7 +22,7 @@ from homeassistant.exceptions import PlatformNotReady import homeassistant.helpers.config_validation as cv from homeassistant.util.dt import utcnow -REQUIREMENTS = ['python-miio==0.3.9', 'construct==2.9.41'] +REQUIREMENTS = ['python-miio==0.4.0', 'construct==2.9.41'] _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/sensor/xiaomi_miio.py b/homeassistant/components/sensor/xiaomi_miio.py index 066dc384007..f7bc9488cc5 100644 --- a/homeassistant/components/sensor/xiaomi_miio.py +++ b/homeassistant/components/sensor/xiaomi_miio.py @@ -25,7 +25,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, }) -REQUIREMENTS = ['python-miio==0.3.9', 'construct==2.9.41'] +REQUIREMENTS = ['python-miio==0.4.0', 'construct==2.9.41'] ATTR_POWER = 'power' ATTR_CHARGING = 'charging' diff --git a/homeassistant/components/switch/xiaomi_miio.py b/homeassistant/components/switch/xiaomi_miio.py index 149acd76c07..b0d251822b0 100644 --- a/homeassistant/components/switch/xiaomi_miio.py +++ b/homeassistant/components/switch/xiaomi_miio.py @@ -39,7 +39,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ 'chuangmi.plug.v3']), }) -REQUIREMENTS = ['python-miio==0.3.9', 'construct==2.9.41'] +REQUIREMENTS = ['python-miio==0.4.0', 'construct==2.9.41'] ATTR_POWER = 'power' ATTR_TEMPERATURE = 'temperature' @@ -142,7 +142,7 @@ async def async_setup_platform(hass, config, async_add_devices, elif model in ['qmi.powerstrip.v1', 'zimi.powerstrip.v2']: from miio import PowerStrip - plug = PowerStrip(host, token) + plug = PowerStrip(host, token, model=model) device = XiaomiPowerStripSwitch(name, plug, model, unique_id) devices.append(device) hass.data[DATA_KEY][host] = device diff --git a/homeassistant/components/vacuum/xiaomi_miio.py b/homeassistant/components/vacuum/xiaomi_miio.py index 620014a1bae..f6789d78b9a 100644 --- a/homeassistant/components/vacuum/xiaomi_miio.py +++ b/homeassistant/components/vacuum/xiaomi_miio.py @@ -19,7 +19,7 @@ from homeassistant.const import ( ATTR_ENTITY_ID, CONF_HOST, CONF_NAME, CONF_TOKEN, STATE_OFF, STATE_ON) import homeassistant.helpers.config_validation as cv -REQUIREMENTS = ['python-miio==0.3.9', 'construct==2.9.41'] +REQUIREMENTS = ['python-miio==0.4.0', 'construct==2.9.41'] _LOGGER = logging.getLogger(__name__) diff --git a/requirements_all.txt b/requirements_all.txt index 59cec2c1e6a..9e7d73b053b 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1029,7 +1029,7 @@ python-juicenet==0.0.5 # homeassistant.components.sensor.xiaomi_miio # homeassistant.components.switch.xiaomi_miio # homeassistant.components.vacuum.xiaomi_miio -python-miio==0.3.9 +python-miio==0.4.0 # homeassistant.components.media_player.mpd python-mpd2==1.0.0 From ad9621ebe59df85413541f09868d6d3e82b5f2d3 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 5 Jun 2018 10:49:54 -0400 Subject: [PATCH 010/144] Use hass iconset (#14185) --- homeassistant/components/config/__init__.py | 2 +- homeassistant/components/hassio/__init__.py | 2 +- homeassistant/components/history.py | 2 +- homeassistant/components/logbook.py | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/config/__init__.py b/homeassistant/components/config/__init__.py index 5a8800d9583..b907d4b4217 100644 --- a/homeassistant/components/config/__init__.py +++ b/homeassistant/components/config/__init__.py @@ -21,7 +21,7 @@ ON_DEMAND = ('zwave',) async def async_setup(hass, config): """Set up the config component.""" await hass.components.frontend.async_register_built_in_panel( - 'config', 'config', 'mdi:settings') + 'config', 'config', 'hass:settings') async def setup_panel(panel_name): """Set up a panel.""" diff --git a/homeassistant/components/hassio/__init__.py b/homeassistant/components/hassio/__init__.py index 45c35dcdd2a..0fbb2a57ca9 100644 --- a/homeassistant/components/hassio/__init__.py +++ b/homeassistant/components/hassio/__init__.py @@ -178,7 +178,7 @@ def async_setup(hass, config): if 'frontend' in hass.config.components: yield from hass.components.frontend.async_register_built_in_panel( - 'hassio', 'Hass.io', 'mdi:home-assistant') + 'hassio', 'Hass.io', 'hass:home-assistant') if 'http' in config: yield from hassio.update_hass_api(config['http']) diff --git a/homeassistant/components/history.py b/homeassistant/components/history.py index c27e394ce28..7ee1c70487f 100644 --- a/homeassistant/components/history.py +++ b/homeassistant/components/history.py @@ -274,7 +274,7 @@ async def async_setup(hass, config): hass.http.register_view(HistoryPeriodView(filters, use_include_order)) await hass.components.frontend.async_register_built_in_panel( - 'history', 'history', 'mdi:poll-box') + 'history', 'history', 'hass:poll-box') return True diff --git a/homeassistant/components/logbook.py b/homeassistant/components/logbook.py index 1ea0b586d33..bcfae533abf 100644 --- a/homeassistant/components/logbook.py +++ b/homeassistant/components/logbook.py @@ -100,7 +100,7 @@ async def setup(hass, config): hass.http.register_view(LogbookView(config.get(DOMAIN, {}))) await hass.components.frontend.async_register_built_in_panel( - 'logbook', 'logbook', 'mdi:format-list-bulleted-type') + 'logbook', 'logbook', 'hass:format-list-bulleted-type') hass.services.async_register( DOMAIN, 'log', log_message, schema=LOG_MESSAGE_SCHEMA) From b3b4f7468dd2c236f008e412664f22b66711f856 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 5 Jun 2018 10:50:16 -0400 Subject: [PATCH 011/144] Further cleanup frontend (#14805) * Remove registering panels * Remove unused image * Lint --- homeassistant/components/frontend/__init__.py | 196 ++++-------------- .../www_static/images/logo_tellduslive.png | Bin 7796 -> 0 bytes tests/components/test_frontend.py | 11 +- 3 files changed, 39 insertions(+), 168 deletions(-) delete mode 100644 homeassistant/components/frontend/www_static/images/logo_tellduslive.png diff --git a/homeassistant/components/frontend/__init__.py b/homeassistant/components/frontend/__init__.py index 5dad77f64ce..3f2f9ded22a 100644 --- a/homeassistant/components/frontend/__init__.py +++ b/homeassistant/components/frontend/__init__.py @@ -5,7 +5,6 @@ For more details about this component, please refer to the documentation at https://home-assistant.io/components/frontend/ """ import asyncio -import hashlib import json import logging import os @@ -30,8 +29,6 @@ REQUIREMENTS = ['home-assistant-frontend==20180603.0'] DOMAIN = 'frontend' DEPENDENCIES = ['api', 'websocket_api', 'http', 'system_log'] -URL_PANEL_COMPONENT_FP = '/frontend/panels/{}-{}.html' - CONF_THEMES = 'themes' CONF_EXTRA_HTML_URL = 'extra_html_url' CONF_EXTRA_HTML_URL_ES5 = 'extra_html_url_es5' @@ -101,7 +98,7 @@ SCHEMA_GET_PANELS = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend({ }) -class AbstractPanel: +class Panel: """Abstract class for panels.""" # Name of the webcomponent @@ -113,44 +110,12 @@ class AbstractPanel: # Title to show in the sidebar (optional) sidebar_title = None - # Url to the webcomponent (depending on JS version) - webcomponent_url_es5 = None - webcomponent_url_latest = None - # Url to show the panel in the frontend frontend_url_path = None # Config to pass to the webcomponent config = None - @asyncio.coroutine - def async_register(self, hass): - """Register panel with HASS.""" - panels = hass.data.get(DATA_PANELS) - if panels is None: - panels = hass.data[DATA_PANELS] = {} - - if self.frontend_url_path in panels: - _LOGGER.warning("Overwriting component %s", self.frontend_url_path) - - if DATA_FINALIZE_PANEL in hass.data: - yield from hass.data[DATA_FINALIZE_PANEL](self) - - panels[self.frontend_url_path] = self - - @callback - def async_register_index_routes(self, router, index_view): - """Register routes for panel to be served by index view.""" - router.add_route( - 'get', '/{}'.format(self.frontend_url_path), index_view.get) - router.add_route( - 'get', '/{}/{{extra:.+}}'.format(self.frontend_url_path), - index_view.get) - - -class BuiltInPanel(AbstractPanel): - """Panel that is part of hass_frontend.""" - def __init__(self, component_name, sidebar_title, sidebar_icon, frontend_url_path, config): """Initialize a built-in panel.""" @@ -160,6 +125,16 @@ class BuiltInPanel(AbstractPanel): self.frontend_url_path = frontend_url_path or component_name self.config = config + @callback + def async_register_index_routes(self, router, index_view): + """Register routes for panel to be served by index view.""" + router.add_route( + 'get', '/{}'.format(self.frontend_url_path), index_view.get) + router.add_route( + 'get', '/{}/{{extra:.+}}'.format(self.frontend_url_path), + index_view.get) + + @callback def to_response(self, hass, request): """Panel as dictionary.""" return { @@ -171,95 +146,25 @@ class BuiltInPanel(AbstractPanel): } -class ExternalPanel(AbstractPanel): - """Panel that is added by a custom component.""" - - REGISTERED_COMPONENTS = set() - - def __init__(self, component_name, path, md5, sidebar_title, sidebar_icon, - frontend_url_path, config): - """Initialize an external panel.""" - self.component_name = component_name - self.path = path - self.md5 = md5 - self.sidebar_title = sidebar_title - self.sidebar_icon = sidebar_icon - self.frontend_url_path = frontend_url_path or component_name - self.config = config - - @asyncio.coroutine - def async_finalize(self, hass, frontend_repository_path): - """Finalize this panel for usage. - - frontend_repository_path is set, will be prepended to path of built-in - components. - """ - try: - if self.md5 is None: - self.md5 = yield from hass.async_add_job( - _fingerprint, self.path) - except OSError: - _LOGGER.error('Cannot find or access %s at %s', - self.component_name, self.path) - hass.data[DATA_PANELS].pop(self.frontend_url_path) - return - - self.webcomponent_url_es5 = self.webcomponent_url_latest = \ - URL_PANEL_COMPONENT_FP.format(self.component_name, self.md5) - - if self.component_name not in self.REGISTERED_COMPONENTS: - hass.http.register_static_path( - self.webcomponent_url_latest, self.path, - # if path is None, we're in prod mode, so cache static assets - frontend_repository_path is None) - self.REGISTERED_COMPONENTS.add(self.component_name) - - def to_response(self, hass, request): - """Panel as dictionary.""" - result = { - 'component_name': self.component_name, - 'icon': self.sidebar_icon, - 'title': self.sidebar_title, - 'url_path': self.frontend_url_path, - 'config': self.config, - } - if _is_latest(hass.data[DATA_JS_VERSION], request): - result['url'] = self.webcomponent_url_latest - else: - result['url'] = self.webcomponent_url_es5 - return result - - @bind_hass -@asyncio.coroutine -def async_register_built_in_panel(hass, component_name, sidebar_title=None, - sidebar_icon=None, frontend_url_path=None, - config=None): +async def async_register_built_in_panel(hass, component_name, + sidebar_title=None, sidebar_icon=None, + frontend_url_path=None, config=None): """Register a built-in panel.""" - panel = BuiltInPanel(component_name, sidebar_title, sidebar_icon, - frontend_url_path, config) - yield from panel.async_register(hass) + panel = Panel(component_name, sidebar_title, sidebar_icon, + frontend_url_path, config) + panels = hass.data.get(DATA_PANELS) + if panels is None: + panels = hass.data[DATA_PANELS] = {} -@bind_hass -@asyncio.coroutine -def async_register_panel(hass, component_name, path, md5=None, - sidebar_title=None, sidebar_icon=None, - frontend_url_path=None, config=None): - """Register a panel for the frontend. + if panel.frontend_url_path in panels: + _LOGGER.warning("Overwriting component %s", panel.frontend_url_path) - component_name: name of the web component - path: path to the HTML of the web component - (required unless url is provided) - md5: the md5 hash of the web component (for versioning in URL, optional) - sidebar_title: title to show in the sidebar (optional) - sidebar_icon: icon to show next to title in sidebar (optional) - url_path: name to use in the URL (defaults to component_name) - config: config to be passed into the web component - """ - panel = ExternalPanel(component_name, path, md5, sidebar_title, - sidebar_icon, frontend_url_path, config) - yield from panel.async_register(hass) + if DATA_FINALIZE_PANEL in hass.data: + hass.data[DATA_FINALIZE_PANEL](panel) + + panels[panel.frontend_url_path] = panel @bind_hass @@ -278,11 +183,10 @@ def add_manifest_json_key(key, val): MANIFEST_JSON[key] = val -@asyncio.coroutine -def async_setup(hass, config): +async def async_setup(hass, config): """Set up the serving of the frontend.""" if list(hass.auth.async_auth_providers): - client = yield from hass.auth.async_create_client( + client = await hass.auth.async_create_client( 'Home Assistant Frontend', redirect_uris=['/'], no_secret=True, @@ -331,24 +235,22 @@ def async_setup(hass, config): index_view = IndexView(repo_path, js_version, client) hass.http.register_view(index_view) - async def finalize_panel(panel): + @callback + def async_finalize_panel(panel): """Finalize setup of a panel.""" - if hasattr(panel, 'async_finalize'): - await panel.async_finalize(hass, repo_path) panel.async_register_index_routes(hass.http.app.router, index_view) - yield from asyncio.wait([ + await asyncio.wait([ async_register_built_in_panel(hass, panel) for panel in ('dev-event', 'dev-info', 'dev-service', 'dev-state', 'dev-template', 'dev-mqtt', 'kiosk')], loop=hass.loop) - hass.data[DATA_FINALIZE_PANEL] = finalize_panel + hass.data[DATA_FINALIZE_PANEL] = async_finalize_panel # Finalize registration of panels that registered before frontend was setup # This includes the built-in panels from line above. - yield from asyncio.wait( - [finalize_panel(panel) for panel in hass.data[DATA_PANELS].values()], - loop=hass.loop) + for panel in hass.data[DATA_PANELS].values(): + async_finalize_panel(panel) if DATA_EXTRA_HTML_URL not in hass.data: hass.data[DATA_EXTRA_HTML_URL] = set() @@ -456,38 +358,23 @@ class IndexView(HomeAssistantView): return tpl - @asyncio.coroutine - def get(self, request, extra=None): + async def get(self, request, extra=None): """Serve the index view.""" hass = request.app['hass'] latest = self.repo_path is not None or \ _is_latest(self.js_option, request) - if request.path == '/': - panel = 'states' - else: - panel = request.path.split('/')[1] - - if panel == 'states': - panel_url = '' - elif latest: - panel_url = hass.data[DATA_PANELS][panel].webcomponent_url_latest - else: - panel_url = hass.data[DATA_PANELS][panel].webcomponent_url_es5 - no_auth = '1' if hass.config.api.api_password and not request[KEY_AUTHENTICATED]: # do not try to auto connect on load no_auth = '0' - template = yield from hass.async_add_job(self.get_template, latest) + template = await hass.async_add_job(self.get_template, latest) extra_key = DATA_EXTRA_HTML_URL if latest else DATA_EXTRA_HTML_URL_ES5 template_params = dict( no_auth=no_auth, - panel_url=panel_url, - panels=hass.data[DATA_PANELS], theme_color=MANIFEST_JSON['theme_color'], extra_urls=hass.data[extra_key], ) @@ -506,7 +393,7 @@ class ManifestJSONView(HomeAssistantView): url = '/manifest.json' name = 'manifestjson' - @asyncio.coroutine + @callback def get(self, request): # pylint: disable=no-self-use """Return the manifest.json.""" msg = json.dumps(MANIFEST_JSON, sort_keys=True) @@ -537,23 +424,16 @@ class TranslationsView(HomeAssistantView): url = '/api/translations/{language}' name = 'api:translations' - @asyncio.coroutine - def get(self, request, language): + async def get(self, request, language): """Return translations.""" hass = request.app['hass'] - resources = yield from async_get_translations(hass, language) + resources = await async_get_translations(hass, language) return self.json({ 'resources': resources, }) -def _fingerprint(path): - """Fingerprint a file.""" - with open(path) as fil: - return hashlib.md5(fil.read().encode('utf-8')).hexdigest() - - def _is_latest(js_option, request): """ Return whether we should serve latest untranspiled code. diff --git a/homeassistant/components/frontend/www_static/images/logo_tellduslive.png b/homeassistant/components/frontend/www_static/images/logo_tellduslive.png deleted file mode 100644 index 7ea78f8ef3aad4d3cd982835c797693a68264c00..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 7796 zcmeAS@N?(olHy`uVBq!ia0y~yU^D|^4mJh`hSz)ctz=+eP)PO&@?~JCQe$9fXklRZ z#lXPO@PdJ%)PRBERRRNp)eHs(@q#(K0&N%=7&r?&B8wRqxP?KOkzv*x2?hoR_7YED zSM~>N>>Mf@Or9zC85kJYlDyqr7{K7C^X_^E2HC5gE{-7;x8BZWpAqu(>hbroVvN#Z zS0a*X-WaNFR8^A_?(K9--j}iF{`8C~cW2&g6%9Dhm2{Y?t4T_<_fulH&PGATqj@4a z5e%(!H_d&!cfaNPxvY-m@7|r+yZg=F&%bTYSDLlCAGllnzxdqmd7tgu7{Qhg2Erhs zvt_Np;>6?`F4w$2onM-`PoLpI;3CD#9c*W1pS+$Cabu2pVZKhvztDe`?KdKRZq3=} z&hS9darMq1qpcHm?&rAqSSe$z&?@VwMTwtTJDsmMT1$O;U2~WrVM2G*x=fd%UPYyZ z`UyV@m>qwY)o3wQ6uqfQ_Ap^DWNvWm>X<6XKRdcRAy()A?kjFruDN!meoudMGR)rfg~#*d z%w!gq1$EjV*SUmqE!w7c>*@DOEh&@l{p_2ce1Gzri($4-uyJXQ<{^`?MFzZ%w>4bu z+?%}q_>s@P!Rh%749spS{w{Bu6s@9v@8h^_tK`<}=bGCbF*$?N(5(B?do~7UrL0Mw zq8@Ili@vSSxDhic?d8qr5YHG%{@KcpW3(6?>XS_Gw5^doBO^G^!=B?-+eM|Bx0ydZ ze=B&JPvWA@zW)plmM!q~l;CFDvf`~)Q*ryglW)=kqs3#D7ICZct(Lbwl%>wdP~fw$ z!}F?yrtRTp=T+lG3t!481l~5VoBrml|HPbGm5c`zCoWi-+>(;@ylY3zR!)}O!y2N6 zy91KAk8NO3_3fq^4ldi9ip?2ba2INR zJ#}7Y(eKNAqJouyN0uZ{=(x4(LjC;oC-2J=9^N?8%EsUj^d%!iCxb`$p@sLO(~fEx z(&nGr&i?n1kv@4fF26&Xp{Bd*&ymU~pWjPb&t|KxV&A+XS3vOOjdQ||a>pdZ7;+{o z^?Yerwc=mZhK?gOg575Q9k+I^m^eeL{(G*5H={#5pTtI)`Lep7u+w8dQRFx2$#Q_wguP24VLp3256=}p@br*s$i zcRY=n=Jn?RV@-5b+nP(kx6AzBEY972-8owGY!}1{nyFmv&55DcHH{Nns~TJ zA&)`s+?6M+({2R0=-%hOohR?&vrk<7h|{rxS(S`?rY&XVO+FiOdRz7R75DbMTL&_n z|8ue>TXNzA z)39TQSqvmJrc5u)4e&a@p>oNQL%u1u-+o=u(RSAJQ>pf>N=7royV8r@PIy<&+cP0v zIiPQm=cBxn_x3GM_u($8*vG(T=FP3H$N5C{PGsD%m(3@(-G04r^WEuBnjam{nw2OL zFpG(Y=i|kos(CqEi)5C?A9BpN{kHvw$D@`>69hYy^BDfdR=x5$E1@>?*3IU-+itT5 zYRrrdNMiY$$MDzTLciB=@*kWjen2y*Oo~-8c}4217rD3d-n*Zui9Ygn#ni>V#!lbZ zCZsL<)|>l%s@=x7-<)e?d1t4KxAZO)Jhp)$NZa;w@87TCxwrrB;LqK?S8l4%^kYrW zt{EDcH{9!dHp{J~XQHU@jWT^_7q;At#pkMBZgbo;u_2~ zZEJ6z`Mqw+(&KA>tj-jB5-_W2zN_#2<*a9QpEUBHJS^|ZP$Qr1^5CDon3I$Gvzxd5uPvTcd9Yfff0~aj|Lwd*$A9=d5;=LR zEnrqtddHI)$F8R*-1fcUw$f$a5wQ+wo`08&Q{H};QCn2jXR|@OqLFiU#)KWdn|=ib z{xs0p+w+~BO>%zv>)HO^?EQ=E;zN+&C|ZkpIu z`P0q6bhh1fk@_evxFh*`i|^#6h8teG>ZY`Adu!(_ckbz)1ux59O5b;RRwu9Y>0w2l zP6ki+reH?1nJZX*Wp8b(=Gu1moXgsRM;f6;fBm1l?wk4V?}};sCs(^Z*p{d)#`A`? zQO`F2cF^`B8I8u=?aqPPdtW|ZRe99uYUKW~tu=qAMOB|)`6;{6=ik099yThfFXw&y z(;#?orueV_*DjkpvUHnPGizQ??)Jl%jLSQss;_IVTKl7TtK!?I@vApQ*R5Qla{5JD zT<6hGdt?LasvLG}eg5S5zjId0wznWn#(~}avI|$qajxu}m2_43thU(7Mc(D-zZjd# z#=qLyoqfnn?Df9`sgA}yrjy%W3zf)o=Iow3Z@KS{u&et@*1pPF7wY-)RY+-fo-m_~ z-v78`-O)R1X7o+hO7uQ=_ke8RWFZB+TM|nASKdEitd@UolD=r!<_&G-|5on06&;da zVs>>^*YER++1@UTJ8sKP+WJ=JtZ}bt{o}hWCHb~bF9*u0q>W03&@Zp<`=>CG@BX4K_2-vj!@#18jS(OX(k34nMJhyqz<+|S7?cC<` zD`&6ls{a?dHQX&^SIO2ZtGn3$i6`E+Ncyy}+R-G3r{AcJb=%VPq-(|8Kc$XsyIr|V zOy1Zh)ZWyw)GSPM`4%_(kfY&mT6g5i+~Rs|byvkNlIwn=@4Q5jboMPPGH<`Td3PQ6 z9t%IEj1`-1*DbbseQK(9N$JX4;mbVPW*q%q_V~itho4oxX`SMh5OmvkK*;pxlHXsL zvOoL~zfrbZJ7?5-t&t!>q&`W>ei2bs&gdy%tNUCO>9#&OJs_VF;xaw6WcW8L@(2~AHMxvmzKLreBFmdZ%^Eso+p+sJR)Ns6lyAuk5qL zvR7i~eKO~qGRw}Mx+eErWJ+#rw^QZ6;zfzcEi4uPY;;wYrk}XsQ@5f1;I`Xm9lzZT z`8)gjy8R+AvT_5iepWC4K10$>_2s=)rrXYJU9&dxC-NwGZ#IuQd`szirB_%KMYbAFtp)Z+_PF%WmE}o!jpw7`oJD z)W5MadFoj9*haUWJM^o|zE>-B-_E|!b6acYj+cR}qwCjNy*_nkrrQd=DLS77Hf_6- z>}rzo&q&CmcTr)+@dfYvqj#P8w8D!0cAoh8%W7|PWPg=!HTm)}V1HofzyGY?#5t4A zg1@f6|1#=Y*yr_{DLI)fd$jvqgeIGFoJi1feA>PxdkU-eJ4xrb;t5&5o>(rmvJMiG zid`obU)zv-ZQ1!(R$7bKaYSby{5_4KxN}00$jRSD0*}L+ck+lPUjA#<kw16K3i z=fwS6^x1B6|NM=4j@5dCyxtvmtg^H9V;6R<|H1e)ql@{okh?-o^UogN^Nr6WoEIy8 zPP4f>$9-$r(-#@VVM^Pstls+l-%+z{?b(G}rrx!jFP|GG`hC8y<*ZXr9dFdQy+7%C zV^2Y&fRVea@Hw_K%~m)6R$eSy^>Xp@x#vsjzHQQ)v%&ZL%qi#i^zR(ny3fp(*U|Ep zxp!fu`@AVP;;V(k4AY+S7GAFAelq#Rj=cC)ntJW?QY!R?WeRi`pV+!uHZ7O;8pEF0 zYs-vv=N4KnUHif~wB+TwIDPl8Pp|8AzTV9%9m89xt~a^-NsGsYJG&z8O+CN)Vo%R< zgX;#bj!RcHUwu1ozRHWsr{x2#`p3W7p>nnPlV|&r()D*#ckeQCzI8Qb`Kk2zFTNI; z=xvmKs>riqL+F#}LuKvdu4kuQDu@oX$ed!TcD1_so!_d|qeeEr9&V{IE44^3O8m~!*X9bOt{Z>Sq1h<%+ zt~+yzb-!8dvgv=+#1ff5^lxYtS}S_1blcuV35$JJJ=$knRr;i(|8|h6UiGD$Kf_;6 zKl#0>EIw?@ny5!LWm~kCb*;Z8p2+-R>xM}4{^(V&eol5dp;cY*P{NDY01w7nB z?OwZFy%{q1-_LOO)vM-I*L^Aq`S<6R`0`x_trs%tBO5m`R>a&WQ8Ou=vMRo2_geO- zztZK=m)XkHCR$}@|H|06`D&&2{1@AV%&WxC&)hHiaZO>}xo?NArsOc}6WR7oRjYTG z`|}qtDAeTU<~(^0 zX@wkH&Ixw>+4|8_KY0W_ZGQf(YGT0WGpc=Jn@sk6KL5mgqf*7x&zq{x8vHhDh_`wh zuzucV#hiu>U$@lm{rB}|Ca2TsF4t8b9=*-2z2cSkW96$Antt}Pwf>#SI`Q`7`3=it z`wDEVv);+E)_;qAoLaN*}$Pldre-=DGg)zV@@TRs}va?tlIB{Dr6Q zmISVwv-zq`wdNtV2kMEJBd2fU@p}E>Z?k)dO})#f=5oRASL(-V)4fBrLhNe;g)Vwl z95mf$fBvV#AL;V)^~^3e82%JiF7VzJUANF`)#FTEcg>yu)W2=JXD*kw_IB}>NUfQ> z*ZuitdOrT;sdIt1ml|`0Zf4gP5HNGNQFZ#(srNtjzRFqWyEpdHzRPcQ_b!*8x=*@&%I?tq zXR9LoHZ@jgmWJvLm^`Rz5rf@Tc&IitU?Sz&kC>+bcORc}R_riNM;Twh!Jvu%lY z+?@^F@3Zm)MfvA6Z(#i4cjMap_PZ0^ZXaKK^0wWU1iu?$Ti4X+{+)esyS#^8q}l58HBYHw%6)F3|Kd9LS6YqX+$;=?n6+P=;+BRkXyG@bbU+Mles|@&jCPmzB zm6}ly>x1w_@8>!`zjvta54^H@(czH8)y1-By6oo1SUml|x;AyzXT=}xPCX%vKZ0%) zF^93&buF`B_3LEew{H)Gw*~5Ii&+^0MT}ZC)*}e{VBiRCQ<7=8Z;wjXpk{^JKTH`nwfTi{-7hv&}D) zIK=kAI`OpZBsa-bzH0KXOSfk{-ScKkx!jGk7Nwk{!QZx}dCt`>?o@q0+4%;8jaE*S z!aBPHr}hP%TP7!+JpZM|-Jnp}@T1|X^Q|=ISFYJ=c<<(~y-T;f)#8wD5O4K8vi#f8 z>?yBadDSLJ}vbN0K~*=0Mf z-ko10`77~#YQ{@n`_1PzT$1ZMmfLpkA5RPK0q;X;Ta$XWuxNc1 z>r@LZ{2%!(b;X~kuff7W=U9GUiBG%R@oML_=`!52=XHH}aFp>78%MNJ;$`;VRexhn zsV;>xwWnKLpAS@2y*aj`BsnjBc1-EbD9ijUdj(!( zG7Ijzc4N~QZ&dO`>O5-ImM`+- z{xxas$=Ds&s#{n#eV=<&^dIHWl9S&j@$H))Q#|4Eu{%fXE@tnUwH93Yoa}yk>2B!Z z+uzmq`dskanYvvzyI$_9?XxSJUX`||zUX-pcK7wK>sGg9<77-$c2)a-fAv!e^ z?YC{*Bw>5%%iC++Qn|-&MDL$|?%K0|wrgi!`&u6oyfswryN<;zw#=5SKMw+r-<)N7 zwczVNPzfcrt@L?#a=W*i$QGk5UsYc`=T;HEz4H6!t=?s!Yc;J}YL?&m`{@3(18Ta? z%jFi_(pK(kJ^QQpSij2+h8q2xyN?zgU-RYR-hGU@`q3*-y|-D(uNtlJFzt21k&{`f z8;xsY(pldX^qjJaul~ulK2Ay@r(uI-=b_HULThC!oqrq5DvdbxTWDW#X26e5t$lxf zdu}$(zLoH^yU%s)>ZOh?zeV)tuLlK;<*9pB+E>?XtyR|LR!+~lyfkax{zG9lr_*E> zTS;rlaz{tMoWAPCCfhwRZAzd@Jf9`x>f@i8`Yv(#QIWe2{WGvP_r9t-Yvr8<5AU*v z)|@f$3S?+m`#fjun``mb_GjL|+M>q!Mvb>?{XPBC^E>2b_e6(2b$(vCm&f%6L*nc0 z_qLzhZSgKLdb{d<%D8lH+s&w}kCR;) z=5DX3sJJS3h>hd+0bi}0)vu=CN$pi%YMOc>V(qnif0xHy|Gw4Du0S>V@>`Xvk~8e7 z8lcGE@2AysJ1W%PII`3%WJ%uXzrS*_FCAQ`v!?EqQn*yn+GTfG%~j_+y53+A*w$Nh z_fy-duHWZ2EED-P$M)#7m5(%k-FoS$$n7gnMDG=Zxv9lGl1)zW4dj_x;vo&i7jd4zV?CSh{QRms5K~9{>J4 z>G#Ic(iOYv@8{fCSiPKQnfzL-tgaYA)6 zUE4)VjdiwX{CwrRUeDF-21BBB$aJpcw&;5~veG`PFE*}>o4)+Tx4!n~KKCtVWjvg} zeXoFx@l|PAlJjiis=}MWt16EkaH_h%h!ef#qR*HNj8b~OJbemq$ihcUOzds5md8X(|;WKOU&e| z?DD-kD_(52n%@P=Y_}JbbMDOHzq%=DW6|VkQ@eH96Iyr~5~Y6>>=e9y>e{T=S8i4R zf3$s{X$!A_+4(PfV)yLLD%^4N)$JE-iSvCWL0O=3)&1N**H&fz)mYay)QZT zr)P6WE95lqy}vf+*u}8@Go|0knx2o%yC2rHfl(ocdFsxoK7SWx{rXbr{_Xx*ZSmXv z*@`&~9Jdd=5BM=}RsH9ATaK-?dUYz~r|kaoEkdAlcD6b_PdW7L=kQhMmc0`3y1g&YPB=|+&(Sq`yrNIQapO%Tk42{vpH1cew&;|9haWZV$KbuTrmf%9-@^)8e|Nn-bV9|cHYG{=Y_W2u4L@2>xwW*d8c-eo1NL&#Qlh_ ze!_{v=B?rrUi5dgo#j_fEN&zNX~YaA}cmDdv# zT6Hs0^CCZ;wDOZXR21iSXlce|{xj)|9E;02<4kU_JW@+_(#gqDGSJ}_cRB? foF>Q(4UEtCYX|er3=uzM3gUaZ`njxgN@xNA&f%&S diff --git a/tests/components/test_frontend.py b/tests/components/test_frontend.py index 657497b868b..2f83d923e2b 100644 --- a/tests/components/test_frontend.py +++ b/tests/components/test_frontend.py @@ -8,7 +8,7 @@ import pytest from homeassistant.setup import async_setup_component from homeassistant.components.frontend import ( DOMAIN, CONF_JS_VERSION, CONF_THEMES, CONF_EXTRA_HTML_URL, - CONF_EXTRA_HTML_URL_ES5, DATA_PANELS) + CONF_EXTRA_HTML_URL_ES5) from homeassistant.components import websocket_api as wapi @@ -183,15 +183,6 @@ def test_extra_urls_es5(mock_http_client_with_urls): assert text.find('href="https://domain.com/my_extra_url_es5.html"') >= 0 -@asyncio.coroutine -def test_panel_without_path(hass): - """Test panel registration without file path.""" - yield from hass.components.frontend.async_register_panel( - 'test_component', 'nonexistant_file') - yield from async_setup_component(hass, 'frontend', {}) - assert 'test_component' not in hass.data[DATA_PANELS] - - async def test_get_panels(hass, hass_ws_client): """Test get_panels command.""" await async_setup_component(hass, 'frontend') From 640e49996428a91356348d6b2029e0fae99a0cb1 Mon Sep 17 00:00:00 2001 From: Hugo Dupras Date: Tue, 5 Jun 2018 17:55:53 +0200 Subject: [PATCH 012/144] netatmo api is now in pip as pyatmo (#14824) Signed-off-by: Hugo D. (jabesq) --- homeassistant/components/binary_sensor/netatmo.py | 4 ++-- homeassistant/components/camera/netatmo.py | 4 ++-- homeassistant/components/climate/netatmo.py | 8 ++++---- homeassistant/components/netatmo.py | 12 +++++------- homeassistant/components/sensor/netatmo.py | 8 ++++---- requirements_all.txt | 6 +++--- 6 files changed, 20 insertions(+), 22 deletions(-) diff --git a/homeassistant/components/binary_sensor/netatmo.py b/homeassistant/components/binary_sensor/netatmo.py index fd0e30ccebc..10fc2ccc3ff 100644 --- a/homeassistant/components/binary_sensor/netatmo.py +++ b/homeassistant/components/binary_sensor/netatmo.py @@ -68,12 +68,12 @@ def setup_platform(hass, config, add_devices, discovery_info=None): module_name = None - import lnetatmo + import pyatmo try: data = CameraData(netatmo.NETATMO_AUTH, home) if not data.get_camera_names(): return None - except lnetatmo.NoDevice: + except pyatmo.NoDevice: return None welcome_sensors = config.get( diff --git a/homeassistant/components/camera/netatmo.py b/homeassistant/components/camera/netatmo.py index bf2dfe39bd8..5b8effd5dcc 100644 --- a/homeassistant/components/camera/netatmo.py +++ b/homeassistant/components/camera/netatmo.py @@ -35,7 +35,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): netatmo = hass.components.netatmo home = config.get(CONF_HOME) verify_ssl = config.get(CONF_VERIFY_SSL, True) - import lnetatmo + import pyatmo try: data = CameraData(netatmo.NETATMO_AUTH, home) for camera_name in data.get_camera_names(): @@ -46,7 +46,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): continue add_devices([NetatmoCamera(data, camera_name, home, camera_type, verify_ssl)]) - except lnetatmo.NoDevice: + except pyatmo.NoDevice: return None diff --git a/homeassistant/components/climate/netatmo.py b/homeassistant/components/climate/netatmo.py index 49452662fc4..a4b921037db 100644 --- a/homeassistant/components/climate/netatmo.py +++ b/homeassistant/components/climate/netatmo.py @@ -44,7 +44,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): netatmo = hass.components.netatmo device = config.get(CONF_RELAY) - import lnetatmo + import pyatmo try: data = ThermostatData(netatmo.NETATMO_AUTH, device) for module_name in data.get_module_names(): @@ -53,7 +53,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): module_name not in config[CONF_THERMOSTAT]: continue add_devices([NetatmoThermostat(data, module_name)], True) - except lnetatmo.NoDevice: + except pyatmo.NoDevice: return None @@ -168,8 +168,8 @@ class ThermostatData(object): @Throttle(MIN_TIME_BETWEEN_UPDATES) def update(self): """Call the NetAtmo API to update the data.""" - import lnetatmo - self.thermostatdata = lnetatmo.ThermostatData(self.auth) + import pyatmo + self.thermostatdata = pyatmo.ThermostatData(self.auth) self.target_temperature = self.thermostatdata.setpoint_temp self.setpoint_mode = self.thermostatdata.setpoint_mode self.current_temperature = self.thermostatdata.temp diff --git a/homeassistant/components/netatmo.py b/homeassistant/components/netatmo.py index 44a54c95512..a635d1820db 100644 --- a/homeassistant/components/netatmo.py +++ b/homeassistant/components/netatmo.py @@ -16,9 +16,7 @@ from homeassistant.helpers import discovery import homeassistant.helpers.config_validation as cv from homeassistant.util import Throttle -REQUIREMENTS = [ - 'https://github.com/jabesq/netatmo-api-python/archive/' - 'v0.9.2.1.zip#lnetatmo==0.9.2.1'] +REQUIREMENTS = ['pyatmo==1.0.0'] _LOGGER = logging.getLogger(__name__) @@ -45,11 +43,11 @@ CONFIG_SCHEMA = vol.Schema({ def setup(hass, config): """Set up the Netatmo devices.""" - import lnetatmo + import pyatmo global NETATMO_AUTH try: - NETATMO_AUTH = lnetatmo.ClientAuth( + NETATMO_AUTH = pyatmo.ClientAuth( config[DOMAIN][CONF_API_KEY], config[DOMAIN][CONF_SECRET_KEY], config[DOMAIN][CONF_USERNAME], config[DOMAIN][CONF_PASSWORD], 'read_station read_camera access_camera ' @@ -111,8 +109,8 @@ class CameraData(object): @Throttle(MIN_TIME_BETWEEN_UPDATES) def update(self): """Call the Netatmo API to update the data.""" - import lnetatmo - self.camera_data = lnetatmo.CameraData(self.auth, size=100) + import pyatmo + self.camera_data = pyatmo.CameraData(self.auth, size=100) @Throttle(MIN_TIME_BETWEEN_EVENT_UPDATES) def update_event(self): diff --git a/homeassistant/components/sensor/netatmo.py b/homeassistant/components/sensor/netatmo.py index f09e1d4f395..191e587feaf 100644 --- a/homeassistant/components/sensor/netatmo.py +++ b/homeassistant/components/sensor/netatmo.py @@ -70,7 +70,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): data = NetAtmoData(netatmo.NETATMO_AUTH, config.get(CONF_STATION, None)) dev = [] - import lnetatmo + import pyatmo try: if CONF_MODULES in config: # Iterate each module @@ -92,7 +92,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): else: _LOGGER.warning("Ignoring unknown var %s for mod %s", variable, module_name) - except lnetatmo.NoDevice: + except pyatmo.NoDevice: return None add_devices(dev, True) @@ -305,8 +305,8 @@ class NetAtmoData(object): @Throttle(MIN_TIME_BETWEEN_UPDATES) def update(self): """Call the Netatmo API to update the data.""" - import lnetatmo - self.station_data = lnetatmo.WeatherStationData(self.auth) + import pyatmo + self.station_data = pyatmo.WeatherStationData(self.auth) if self.station is not None: self.data = self.station_data.lastData( diff --git a/requirements_all.txt b/requirements_all.txt index 9e7d73b053b..87256c8eb7a 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -412,9 +412,6 @@ https://github.com/aparraga/braviarc/archive/0.3.7.zip#braviarc==0.3.7 # homeassistant.components.media_player.spotify https://github.com/happyleavesaoc/spotipy/archive/544614f4b1d508201d363e84e871f86c90aa26b2.zip#spotipy==2.4.4 -# homeassistant.components.netatmo -https://github.com/jabesq/netatmo-api-python/archive/v0.9.2.1.zip#lnetatmo==0.9.2.1 - # homeassistant.components.neato https://github.com/jabesq/pybotvac/archive/v0.0.5.zip#pybotvac==0.0.5 @@ -733,6 +730,9 @@ pyasn1-modules==0.1.5 # homeassistant.components.notify.xmpp pyasn1==0.3.7 +# homeassistant.components.netatmo +pyatmo==1.0.0 + # homeassistant.components.apple_tv pyatv==0.3.10 From cb6c869c2fb97021b042feff38ea99d9b14d5e2d Mon Sep 17 00:00:00 2001 From: Mischa Gruber Date: Tue, 5 Jun 2018 18:15:34 +0200 Subject: [PATCH 013/144] Action parameter doesn't longer have to be the first parameter (#14815) * Action parameter doesn't longer have to be the first parameter * Minified code upon suggestion --- homeassistant/components/binary_sensor/mystrom.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/binary_sensor/mystrom.py b/homeassistant/components/binary_sensor/mystrom.py index 93d56a97c42..5c1d9a576a6 100644 --- a/homeassistant/components/binary_sensor/mystrom.py +++ b/homeassistant/components/binary_sensor/mystrom.py @@ -29,6 +29,7 @@ class MyStromView(HomeAssistantView): url = '/api/mystrom' name = 'api:mystrom' + supported_actions = ['single', 'double', 'long', 'touch'] def __init__(self, add_devices): """Initialize the myStrom URL endpoint.""" @@ -44,16 +45,18 @@ class MyStromView(HomeAssistantView): @asyncio.coroutine def _handle(self, hass, data): """Handle requests to the myStrom endpoint.""" - button_action = list(data.keys())[0] - button_id = data[button_action] - entity_id = '{}.{}_{}'.format(DOMAIN, button_id, button_action) + button_action = next(( + parameter for parameter in data + if parameter in self.supported_actions), None) - if button_action not in ['single', 'double', 'long', 'touch']: + if button_action is None: _LOGGER.error( "Received unidentified message from myStrom button: %s", data) return ("Received unidentified message: {}".format(data), HTTP_UNPROCESSABLE_ENTITY) + button_id = data[button_action] + entity_id = '{}.{}_{}'.format(DOMAIN, button_id, button_action) if entity_id not in self.buttons: _LOGGER.info("New myStrom button/action detected: %s/%s", button_id, button_action) From 21d05a8b4d1d5dc3fb381a5fe5280d458b9e3f3c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20N=C3=B8rager=20S=C3=B8rensen?= <6843486+simse@users.noreply.github.com> Date: Tue, 5 Jun 2018 19:13:16 +0200 Subject: [PATCH 014/144] Fixes an issue in Xiaomi TV platform that would some TVs not sleep correctly (#14829) --- homeassistant/components/media_player/xiaomi_tv.py | 4 ++-- requirements_all.txt | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/media_player/xiaomi_tv.py b/homeassistant/components/media_player/xiaomi_tv.py index be40bf7d010..d44ac138e41 100644 --- a/homeassistant/components/media_player/xiaomi_tv.py +++ b/homeassistant/components/media_player/xiaomi_tv.py @@ -13,7 +13,7 @@ from homeassistant.components.media_player import ( SUPPORT_TURN_ON, SUPPORT_TURN_OFF, MediaPlayerDevice, PLATFORM_SCHEMA, SUPPORT_VOLUME_STEP) -REQUIREMENTS = ['pymitv==1.0.0'] +REQUIREMENTS = ['pymitv==1.4.0'] DEFAULT_NAME = "Xiaomi TV" @@ -39,7 +39,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): if host is not None: # Check if there's a valid TV at the IP address. - if not Discover().checkIp(host): + if not Discover().check_ip(host): _LOGGER.error( "Could not find Xiaomi TV with specified IP: %s", host ) diff --git a/requirements_all.txt b/requirements_all.txt index 87256c8eb7a..24c2df99ebb 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -884,7 +884,7 @@ pymailgunner==1.4 pymediaroom==0.6.3 # homeassistant.components.media_player.xiaomi_tv -pymitv==1.0.0 +pymitv==1.4.0 # homeassistant.components.mochad pymochad==0.2.0 From f1aba5511f69ffcab01b84073bba36cb53d01fd4 Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Tue, 5 Jun 2018 19:44:41 +0200 Subject: [PATCH 015/144] Limit to 3 decimals (fixes #14773) --- homeassistant/components/sensor/simulated.py | 2 +- tests/components/sensor/test_simulated.py | 38 +++++++++----------- 2 files changed, 17 insertions(+), 23 deletions(-) diff --git a/homeassistant/components/sensor/simulated.py b/homeassistant/components/sensor/simulated.py index 9dac0b48bc2..9f114cf2c56 100644 --- a/homeassistant/components/sensor/simulated.py +++ b/homeassistant/components/sensor/simulated.py @@ -100,7 +100,7 @@ class SimulatedSensor(Entity): else: periodic = amp * (math.sin((2*math.pi*time_delta/period) + phase)) noise = self._random.gauss(mu=0, sigma=fwhm) - return mean + periodic + noise + return round(mean + periodic + noise, 3) async def async_update(self): """Update the sensor.""" diff --git a/tests/components/sensor/test_simulated.py b/tests/components/sensor/test_simulated.py index 3bfccc629fd..d226c79cff5 100644 --- a/tests/components/sensor/test_simulated.py +++ b/tests/components/sensor/test_simulated.py @@ -1,13 +1,14 @@ """The tests for the simulated sensor.""" import unittest +from tests.common import get_test_home_assistant + from homeassistant.components.sensor.simulated import ( - CONF_UNIT, CONF_AMP, CONF_MEAN, CONF_PERIOD, CONF_PHASE, CONF_FWHM, - CONF_SEED, DEFAULT_NAME, DEFAULT_AMP, DEFAULT_MEAN, - DEFAULT_PHASE, DEFAULT_FWHM, DEFAULT_SEED) + CONF_AMP, CONF_FWHM, CONF_MEAN, CONF_PERIOD, CONF_PHASE, CONF_SEED, + CONF_UNIT, DEFAULT_AMP, DEFAULT_FWHM, DEFAULT_MEAN, DEFAULT_NAME, + DEFAULT_PHASE, DEFAULT_SEED) from homeassistant.const import CONF_FRIENDLY_NAME from homeassistant.setup import setup_component -from tests.common import get_test_home_assistant class TestSimulatedSensor(unittest.TestCase): @@ -27,24 +28,17 @@ class TestSimulatedSensor(unittest.TestCase): 'sensor': { 'platform': 'simulated'} } - self.assertTrue( - setup_component(self.hass, 'sensor', config)) + self.assertTrue(setup_component(self.hass, 'sensor', config)) self.hass.block_till_done() + assert len(self.hass.states.entity_ids()) == 1 state = self.hass.states.get('sensor.simulated') - assert state.attributes.get( - CONF_FRIENDLY_NAME) == DEFAULT_NAME - assert state.attributes.get( - CONF_AMP) == DEFAULT_AMP - assert state.attributes.get( - CONF_UNIT) is None - assert state.attributes.get( - CONF_MEAN) == DEFAULT_MEAN - assert state.attributes.get( - CONF_PERIOD) == 60.0 - assert state.attributes.get( - CONF_PHASE) == DEFAULT_PHASE - assert state.attributes.get( - CONF_FWHM) == DEFAULT_FWHM - assert state.attributes.get( - CONF_SEED) == DEFAULT_SEED + + assert state.attributes.get(CONF_FRIENDLY_NAME) == DEFAULT_NAME + assert state.attributes.get(CONF_AMP) == DEFAULT_AMP + assert state.attributes.get(CONF_UNIT) is None + assert state.attributes.get(CONF_MEAN) == DEFAULT_MEAN + assert state.attributes.get(CONF_PERIOD) == 60.0 + assert state.attributes.get(CONF_PHASE) == DEFAULT_PHASE + assert state.attributes.get(CONF_FWHM) == DEFAULT_FWHM + assert state.attributes.get(CONF_SEED) == DEFAULT_SEED From 549abd9c7eb628be248c955216c99d2d5be480d4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20Kr=C3=BCger?= Date: Tue, 5 Jun 2018 20:06:25 +0200 Subject: [PATCH 016/144] Improved Fritz!Box thermostat support (#14789) --- .coveragerc | 2 +- homeassistant/components/climate/fritzbox.py | 25 ++- tests/components/climate/test_fritzbox.py | 172 +++++++++++++++++++ 3 files changed, 194 insertions(+), 5 deletions(-) create mode 100644 tests/components/climate/test_fritzbox.py diff --git a/.coveragerc b/.coveragerc index dfbbb232efc..c8958d98178 100644 --- a/.coveragerc +++ b/.coveragerc @@ -97,7 +97,7 @@ omit = homeassistant/components/*/envisalink.py homeassistant/components/fritzbox.py - homeassistant/components/*/fritzbox.py + homeassistant/components/switch/fritzbox.py homeassistant/components/eufy.py homeassistant/components/*/eufy.py diff --git a/homeassistant/components/climate/fritzbox.py b/homeassistant/components/climate/fritzbox.py index 839da8c9d53..fa3ca31c770 100755 --- a/homeassistant/components/climate/fritzbox.py +++ b/homeassistant/components/climate/fritzbox.py @@ -13,21 +13,27 @@ from homeassistant.components.fritzbox import ( ATTR_STATE_DEVICE_LOCKED, ATTR_STATE_BATTERY_LOW, ATTR_STATE_LOCKED) from homeassistant.components.climate import ( ATTR_OPERATION_MODE, ClimateDevice, STATE_ECO, STATE_HEAT, STATE_MANUAL, - SUPPORT_OPERATION_MODE, SUPPORT_TARGET_TEMPERATURE) + STATE_OFF, STATE_ON, SUPPORT_OPERATION_MODE, + SUPPORT_TARGET_TEMPERATURE) from homeassistant.const import ( ATTR_TEMPERATURE, PRECISION_HALVES, TEMP_CELSIUS) - DEPENDENCIES = ['fritzbox'] _LOGGER = logging.getLogger(__name__) SUPPORT_FLAGS = (SUPPORT_TARGET_TEMPERATURE | SUPPORT_OPERATION_MODE) -OPERATION_LIST = [STATE_HEAT, STATE_ECO] +OPERATION_LIST = [STATE_HEAT, STATE_ECO, STATE_OFF, STATE_ON] MIN_TEMPERATURE = 8 MAX_TEMPERATURE = 28 +# special temperatures for on/off in Fritz!Box API (modified by pyfritzhome) +ON_API_TEMPERATURE = 127.0 +OFF_API_TEMPERATURE = 126.5 +ON_REPORT_SET_TEMPERATURE = 30.0 +OFF_REPORT_SET_TEMPERATURE = 0.0 + def setup_platform(hass, config, add_devices, discovery_info=None): """Set up the Fritzbox smarthome thermostat platform.""" @@ -88,6 +94,9 @@ class FritzboxThermostat(ClimateDevice): @property def target_temperature(self): """Return the temperature we try to reach.""" + if self._target_temperature in (ON_API_TEMPERATURE, + OFF_API_TEMPERATURE): + return None return self._target_temperature def set_temperature(self, **kwargs): @@ -102,9 +111,13 @@ class FritzboxThermostat(ClimateDevice): @property def current_operation(self): """Return the current operation mode.""" + if self._target_temperature == ON_API_TEMPERATURE: + return STATE_ON + if self._target_temperature == OFF_API_TEMPERATURE: + return STATE_OFF if self._target_temperature == self._comfort_temperature: return STATE_HEAT - elif self._target_temperature == self._eco_temperature: + if self._target_temperature == self._eco_temperature: return STATE_ECO return STATE_MANUAL @@ -119,6 +132,10 @@ class FritzboxThermostat(ClimateDevice): self.set_temperature(temperature=self._comfort_temperature) elif operation_mode == STATE_ECO: self.set_temperature(temperature=self._eco_temperature) + elif operation_mode == STATE_OFF: + self.set_temperature(temperature=OFF_REPORT_SET_TEMPERATURE) + elif operation_mode == STATE_ON: + self.set_temperature(temperature=ON_REPORT_SET_TEMPERATURE) @property def min_temp(self): diff --git a/tests/components/climate/test_fritzbox.py b/tests/components/climate/test_fritzbox.py new file mode 100644 index 00000000000..ccffef9e547 --- /dev/null +++ b/tests/components/climate/test_fritzbox.py @@ -0,0 +1,172 @@ +"""The tests for the demo climate component.""" +import unittest +from unittest.mock import Mock, patch + +import requests + +from homeassistant.components.climate.fritzbox import FritzboxThermostat + + +class TestFritzboxClimate(unittest.TestCase): + """Test Fritz!Box heating thermostats.""" + + def setUp(self): + """Create a mock device to test on.""" + self.device = Mock() + self.device.name = 'Test Thermostat' + self.device.actual_temperature = 18.0 + self.device.target_temperature = 19.5 + self.device.comfort_temperature = 22.0 + self.device.eco_temperature = 16.0 + self.device.present = True + self.device.device_lock = True + self.device.lock = False + self.device.battery_low = True + self.device.set_target_temperature = Mock() + self.device.update = Mock() + mock_fritz = Mock() + mock_fritz.login = Mock() + self.thermostat = FritzboxThermostat(self.device, mock_fritz) + + def test_init(self): + """Test instance creation.""" + self.assertEqual(18.0, self.thermostat._current_temperature) + self.assertEqual(19.5, self.thermostat._target_temperature) + self.assertEqual(22.0, self.thermostat._comfort_temperature) + self.assertEqual(16.0, self.thermostat._eco_temperature) + + def test_supported_features(self): + """Test supported features property.""" + self.assertEqual(129, self.thermostat.supported_features) + + def test_available(self): + """Test available property.""" + self.assertTrue(self.thermostat.available) + self.thermostat._device.present = False + self.assertFalse(self.thermostat.available) + + def test_name(self): + """Test name property.""" + self.assertEqual('Test Thermostat', self.thermostat.name) + + def test_temperature_unit(self): + """Test temperature_unit property.""" + self.assertEqual('°C', self.thermostat.temperature_unit) + + def test_precision(self): + """Test precision property.""" + self.assertEqual(0.5, self.thermostat.precision) + + def test_current_temperature(self): + """Test current_temperature property incl. special temperatures.""" + self.assertEqual(18, self.thermostat.current_temperature) + + def test_target_temperature(self): + """Test target_temperature property.""" + self.assertEqual(19.5, self.thermostat.target_temperature) + + self.thermostat._target_temperature = 126.5 + self.assertEqual(None, self.thermostat.target_temperature) + + self.thermostat._target_temperature = 127.0 + self.assertEqual(None, self.thermostat.target_temperature) + + @patch.object(FritzboxThermostat, 'set_operation_mode') + def test_set_temperature_operation_mode(self, mock_set_op): + """Test set_temperature by operation_mode.""" + self.thermostat.set_temperature(operation_mode='test_mode') + mock_set_op.assert_called_once_with('test_mode') + + def test_set_temperature_temperature(self): + """Test set_temperature by temperature.""" + self.thermostat.set_temperature(temperature=23.0) + self.thermostat._device.set_target_temperature.\ + assert_called_once_with(23.0) + + @patch.object(FritzboxThermostat, 'set_operation_mode') + def test_set_temperature_none(self, mock_set_op): + """Test set_temperature with no arguments.""" + self.thermostat.set_temperature() + mock_set_op.assert_not_called() + self.thermostat._device.set_target_temperature.assert_not_called() + + @patch.object(FritzboxThermostat, 'set_operation_mode') + def test_set_temperature_operation_mode_precedence(self, mock_set_op): + """Test set_temperature for precedence of operation_mode arguement.""" + self.thermostat.set_temperature(operation_mode='test_mode', + temperature=23.0) + mock_set_op.assert_called_once_with('test_mode') + self.thermostat._device.set_target_temperature.assert_not_called() + + def test_current_operation(self): + """Test operation mode property for different temperatures.""" + self.thermostat._target_temperature = 127.0 + self.assertEqual('on', self.thermostat.current_operation) + self.thermostat._target_temperature = 126.5 + self.assertEqual('off', self.thermostat.current_operation) + self.thermostat._target_temperature = 22.0 + self.assertEqual('heat', self.thermostat.current_operation) + self.thermostat._target_temperature = 16.0 + self.assertEqual('eco', self.thermostat.current_operation) + self.thermostat._target_temperature = 12.5 + self.assertEqual('manual', self.thermostat.current_operation) + + def test_operation_list(self): + """Test operation_list property.""" + self.assertEqual(['heat', 'eco', 'off', 'on'], + self.thermostat.operation_list) + + @patch.object(FritzboxThermostat, 'set_temperature') + def test_set_operation_mode(self, mock_set_temp): + """Test set_operation_mode by all modes and with a non-existing one.""" + values = { + 'heat': 22.0, + 'eco': 16.0, + 'on': 30.0, + 'off': 0.0} + for mode, temp in values.items(): + print(mode, temp) + + mock_set_temp.reset_mock() + self.thermostat.set_operation_mode(mode) + mock_set_temp.assert_called_once_with(temperature=temp) + + mock_set_temp.reset_mock() + self.thermostat.set_operation_mode('non_existing_mode') + mock_set_temp.assert_not_called() + + def test_min_max_temperature(self): + """Test min_temp and max_temp properties.""" + self.assertEqual(8.0, self.thermostat.min_temp) + self.assertEqual(28.0, self.thermostat.max_temp) + + def test_device_state_attributes(self): + """Test device_state property.""" + attr = self.thermostat.device_state_attributes + self.assertEqual(attr['device_locked'], True) + self.assertEqual(attr['locked'], False) + self.assertEqual(attr['battery_low'], True) + + def test_update(self): + """Test update function.""" + device = Mock() + device.update = Mock() + device.actual_temperature = 10.0 + device.target_temperature = 11.0 + device.comfort_temperature = 12.0 + device.eco_temperature = 13.0 + self.thermostat._device = device + + self.thermostat.update() + + device.update.assert_called_once_with() + self.assertEqual(10.0, self.thermostat._current_temperature) + self.assertEqual(11.0, self.thermostat._target_temperature) + self.assertEqual(12.0, self.thermostat._comfort_temperature) + self.assertEqual(13.0, self.thermostat._eco_temperature) + + def test_update_http_error(self): + """Test exception handling of update function.""" + self.device.update.side_effect = requests.exceptions.HTTPError + self.thermostat.update() + self.thermostat._fritz.login.assert_called_once_with() From 103639455c73e214eaa13a082560d24932b714e9 Mon Sep 17 00:00:00 2001 From: Luc Touraille Date: Tue, 5 Jun 2018 20:38:50 +0200 Subject: [PATCH 017/144] Add Freebox device tracker (#12727) * Add a device tracker for Freebox routers * Automatic setup of Freebox device tracker based on discovery * Make the Freebox device tracker asynchronous --- .coveragerc | 1 + .../components/device_tracker/freebox.py | 120 ++++++++++++++++++ homeassistant/components/discovery.py | 1 + requirements_all.txt | 3 + 4 files changed, 125 insertions(+) create mode 100644 homeassistant/components/device_tracker/freebox.py diff --git a/.coveragerc b/.coveragerc index c8958d98178..c51f6bc37cb 100644 --- a/.coveragerc +++ b/.coveragerc @@ -397,6 +397,7 @@ omit = homeassistant/components/device_tracker/bt_home_hub_5.py homeassistant/components/device_tracker/cisco_ios.py homeassistant/components/device_tracker/ddwrt.py + homeassistant/components/device_tracker/freebox.py homeassistant/components/device_tracker/fritz.py homeassistant/components/device_tracker/google_maps.py homeassistant/components/device_tracker/gpslogger.py diff --git a/homeassistant/components/device_tracker/freebox.py b/homeassistant/components/device_tracker/freebox.py new file mode 100644 index 00000000000..67957ca99b9 --- /dev/null +++ b/homeassistant/components/device_tracker/freebox.py @@ -0,0 +1,120 @@ +""" +Support for device tracking through Freebox routers. + +This tracker keeps track of the devices connected to the configured Freebox. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/device_tracker.freebox/ +""" +import asyncio +import copy +import logging +import socket +from collections import namedtuple +from datetime import timedelta + +import voluptuous as vol + +import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.event import async_track_time_interval +from homeassistant.components.device_tracker import ( + PLATFORM_SCHEMA, CONF_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL) +from homeassistant.const import ( + CONF_HOST, CONF_PORT) + +REQUIREMENTS = ['aiofreepybox==0.0.3'] + +_LOGGER = logging.getLogger(__name__) + +FREEBOX_CONFIG_FILE = 'freebox.conf' + +PLATFORM_SCHEMA = vol.All( + PLATFORM_SCHEMA.extend({ + vol.Required(CONF_HOST): cv.string, + vol.Required(CONF_PORT): cv.port + })) + +MIN_TIME_BETWEEN_SCANS = timedelta(seconds=10) + + +async def async_setup_scanner(hass, config, async_see, discovery_info=None): + """Set up the Freebox device tracker and start the polling.""" + freebox_config = copy.deepcopy(config) + if discovery_info is not None: + freebox_config[CONF_HOST] = discovery_info['properties']['api_domain'] + freebox_config[CONF_PORT] = discovery_info['properties']['https_port'] + _LOGGER.info("Discovered Freebox server: %s:%s", + freebox_config[CONF_HOST], freebox_config[CONF_PORT]) + + scanner = FreeboxDeviceScanner(hass, freebox_config, async_see) + interval = freebox_config.get(CONF_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL) + await scanner.async_start(hass, interval) + return True + + +Device = namedtuple('Device', ['id', 'name', 'ip']) + + +def _build_device(device_dict): + return Device( + device_dict['l2ident']['id'], + device_dict['primary_name'], + device_dict['l3connectivities'][0]['addr']) + + +class FreeboxDeviceScanner(object): + """This class scans for devices connected to the Freebox.""" + + def __init__(self, hass, config, async_see): + """Initialize the scanner.""" + from aiofreepybox import Freepybox + + self.host = config[CONF_HOST] + self.port = config[CONF_PORT] + self.token_file = hass.config.path(FREEBOX_CONFIG_FILE) + self.async_see = async_see + + # Hardcode the app description to avoid invalidating the authentication + # file at each new version. + # The version can be changed if we want the user to re-authorize HASS + # on her Freebox. + app_desc = { + 'app_id': 'hass', + 'app_name': 'Home Assistant', + 'app_version': '0.65', + 'device_name': socket.gethostname() + } + + api_version = 'v1' # Use the lowest working version. + self.fbx = Freepybox( + app_desc=app_desc, + token_file=self.token_file, + api_version=api_version) + + async def async_start(self, hass, interval): + """Perform a first update and start polling at the given interval.""" + await self.async_update_info() + interval = max(interval, MIN_TIME_BETWEEN_SCANS) + async_track_time_interval(hass, self.async_update_info, interval) + + async def async_update_info(self, now=None): + """Check the Freebox for devices.""" + from aiofreepybox.exceptions import HttpRequestError + + _LOGGER.info('Scanning devices') + + await self.fbx.open(self.host, self.port) + try: + hosts = await self.fbx.lan.get_hosts_list() + except HttpRequestError: + _LOGGER.exception('Failed to scan devices') + else: + active_devices = [_build_device(device) + for device in hosts + if device['active']] + + if active_devices: + await asyncio.wait([self.async_see(mac=d.id, host_name=d.name) + for d in active_devices]) + + await self.fbx.close() diff --git a/homeassistant/components/discovery.py b/homeassistant/components/discovery.py index 69447b81cd4..00d4291539b 100644 --- a/homeassistant/components/discovery.py +++ b/homeassistant/components/discovery.py @@ -84,6 +84,7 @@ SERVICE_HANDLERS = { 'kodi': ('media_player', 'kodi'), 'volumio': ('media_player', 'volumio'), 'nanoleaf_aurora': ('light', 'nanoleaf_aurora'), + 'freebox': ('device_tracker', 'freebox'), } OPTIONAL_SERVICE_HANDLERS = { diff --git a/requirements_all.txt b/requirements_all.txt index 24c2df99ebb..8673f4aa22f 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -81,6 +81,9 @@ aioautomatic==0.6.5 # homeassistant.components.sensor.dnsip aiodns==1.1.1 +# homeassistant.components.device_tracker.freebox +aiofreepybox==0.0.3 + # homeassistant.components.emulated_hue # homeassistant.components.http aiohttp_cors==0.7.0 From a6880c452f20419c16c6d7f03c6f58a1b5a0bf1f Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 6 Jun 2018 01:08:36 -0700 Subject: [PATCH 018/144] Migrate entity registry to using websocket (#14830) * Migrate to using websocket * Lint --- .../components/config/entity_registry.py | 87 +++++++++++++----- .../components/config/test_entity_registry.py | 91 +++++++++++-------- 2 files changed, 117 insertions(+), 61 deletions(-) diff --git a/homeassistant/components/config/entity_registry.py b/homeassistant/components/config/entity_registry.py index 4b9a2c89da0..c594bf1f99e 100644 --- a/homeassistant/components/config/entity_registry.py +++ b/homeassistant/components/config/entity_registry.py @@ -2,48 +2,85 @@ import voluptuous as vol from homeassistant.core import callback -from homeassistant.components.http import HomeAssistantView -from homeassistant.components.http.data_validator import RequestDataValidator from homeassistant.helpers.entity_registry import async_get_registry +from homeassistant.components import websocket_api +from homeassistant.helpers import config_validation as cv + +DEPENDENCIES = ['websocket_api'] + +WS_TYPE_GET = 'config/entity_registry/get' +SCHEMA_WS_GET = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend({ + vol.Required('type'): WS_TYPE_GET, + vol.Required('entity_id'): cv.entity_id +}) + +WS_TYPE_UPDATE = 'config/entity_registry/update' +SCHEMA_WS_UPDATE = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend({ + vol.Required('type'): WS_TYPE_UPDATE, + vol.Required('entity_id'): cv.entity_id, + # If passed in, we update value. Passing None will remove old value. + vol.Optional('name'): vol.Any(str, None), +}) async def async_setup(hass): """Enable the Entity Registry views.""" - hass.http.register_view(ConfigManagerEntityView) + hass.components.websocket_api.async_register_command( + WS_TYPE_GET, websocket_get_entity, + SCHEMA_WS_GET + ) + hass.components.websocket_api.async_register_command( + WS_TYPE_UPDATE, websocket_update_entity, + SCHEMA_WS_UPDATE + ) return True -class ConfigManagerEntityView(HomeAssistantView): - """View to interact with an entity registry entry.""" +@callback +def websocket_get_entity(hass, connection, msg): + """Handle get entity registry entry command. - url = '/api/config/entity_registry/{entity_id}' - name = 'api:config:entity_registry:entity' - - async def get(self, request, entity_id): - """Get the entity registry settings for an entity.""" - hass = request.app['hass'] + Async friendly. + """ + async def retrieve_entity(): + """Get entity from registry.""" registry = await async_get_registry(hass) - entry = registry.entities.get(entity_id) + entry = registry.entities.get(msg['entity_id']) if entry is None: - return self.json_message('Entry not found', 404) + connection.send_message_outside(websocket_api.error_message( + msg['id'], websocket_api.ERR_NOT_FOUND, 'Entity not found')) + return - return self.json(_entry_dict(entry)) + connection.send_message_outside(websocket_api.result_message( + msg['id'], _entry_dict(entry) + )) - @RequestDataValidator(vol.Schema({ - # If passed in, we update value. Passing None will remove old value. - vol.Optional('name'): vol.Any(str, None), - })) - async def post(self, request, entity_id, data): - """Update the entity registry settings for an entity.""" - hass = request.app['hass'] + hass.async_add_job(retrieve_entity()) + + +@callback +def websocket_update_entity(hass, connection, msg): + """Handle get camera thumbnail websocket command. + + Async friendly. + """ + async def update_entity(): + """Get entity from registry.""" registry = await async_get_registry(hass) - if entity_id not in registry.entities: - return self.json_message('Entry not found', 404) + if msg['entity_id'] not in registry.entities: + connection.send_message_outside(websocket_api.error_message( + msg['id'], websocket_api.ERR_NOT_FOUND, 'Entity not found')) + return - entry = registry.async_update_entity(entity_id, **data) - return self.json(_entry_dict(entry)) + entry = registry.async_update_entity( + msg['entity_id'], name=msg['name']) + connection.send_message_outside(websocket_api.result_message( + msg['id'], _entry_dict(entry) + )) + + hass.async_add_job(update_entity()) @callback diff --git a/tests/components/config/test_entity_registry.py b/tests/components/config/test_entity_registry.py index fd7c6999477..1591b8da1d2 100644 --- a/tests/components/config/test_entity_registry.py +++ b/tests/components/config/test_entity_registry.py @@ -1,18 +1,16 @@ """Test entity_registry API.""" import pytest -from homeassistant.setup import async_setup_component from homeassistant.helpers.entity_registry import RegistryEntry from homeassistant.components.config import entity_registry from tests.common import mock_registry, MockEntity, MockEntityPlatform @pytest.fixture -def client(hass, aiohttp_client): +def client(hass, hass_ws_client): """Fixture that can interact with the config manager API.""" - hass.loop.run_until_complete(async_setup_component(hass, 'http', {})) hass.loop.run_until_complete(entity_registry.async_setup(hass)) - yield hass.loop.run_until_complete(aiohttp_client(hass.http.app)) + yield hass.loop.run_until_complete(hass_ws_client(hass)) async def test_get_entity(hass, client): @@ -31,20 +29,26 @@ async def test_get_entity(hass, client): ), }) - resp = await client.get( - '/api/config/entity_registry/test_domain.name') - assert resp.status == 200 - data = await resp.json() - assert data == { + await client.send_json({ + 'id': 5, + 'type': 'config/entity_registry/get', + 'entity_id': 'test_domain.name', + }) + msg = await client.receive_json() + + assert msg['result'] == { 'entity_id': 'test_domain.name', 'name': 'Hello World' } - resp = await client.get( - '/api/config/entity_registry/test_domain.no_name') - assert resp.status == 200 - data = await resp.json() - assert data == { + await client.send_json({ + 'id': 6, + 'type': 'config/entity_registry/get', + 'entity_id': 'test_domain.no_name', + }) + msg = await client.receive_json() + + assert msg['result'] == { 'entity_id': 'test_domain.no_name', 'name': None } @@ -69,13 +73,16 @@ async def test_update_entity(hass, client): assert state is not None assert state.name == 'before update' - resp = await client.post( - '/api/config/entity_registry/test_domain.world', json={ - 'name': 'after update' - }) - assert resp.status == 200 - data = await resp.json() - assert data == { + await client.send_json({ + 'id': 6, + 'type': 'config/entity_registry/update', + 'entity_id': 'test_domain.world', + 'name': 'after update', + }) + + msg = await client.receive_json() + + assert msg['result'] == { 'entity_id': 'test_domain.world', 'name': 'after update' } @@ -103,13 +110,16 @@ async def test_update_entity_no_changes(hass, client): assert state is not None assert state.name == 'name of entity' - resp = await client.post( - '/api/config/entity_registry/test_domain.world', json={ - 'name': 'name of entity' - }) - assert resp.status == 200 - data = await resp.json() - assert data == { + await client.send_json({ + 'id': 6, + 'type': 'config/entity_registry/update', + 'entity_id': 'test_domain.world', + 'name': 'name of entity', + }) + + msg = await client.receive_json() + + assert msg['result'] == { 'entity_id': 'test_domain.world', 'name': 'name of entity' } @@ -120,15 +130,24 @@ async def test_update_entity_no_changes(hass, client): async def test_get_nonexisting_entity(client): """Test get entry.""" - resp = await client.get( - '/api/config/entity_registry/test_domain.non_existing') - assert resp.status == 404 + await client.send_json({ + 'id': 6, + 'type': 'config/entity_registry/get', + 'entity_id': 'test_domain.no_name', + }) + msg = await client.receive_json() + + assert not msg['success'] async def test_update_nonexisting_entity(client): """Test get entry.""" - resp = await client.post( - '/api/config/entity_registry/test_domain.non_existing', json={ - 'name': 'some name' - }) - assert resp.status == 404 + await client.send_json({ + 'id': 6, + 'type': 'config/entity_registry/update', + 'entity_id': 'test_domain.no_name', + 'name': 'new-name' + }) + msg = await client.receive_json() + + assert not msg['success'] From fa2e6ada26751ca7a3b54530a1dff068cb3ed78e Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 6 Jun 2018 01:12:43 -0700 Subject: [PATCH 019/144] Route themes and translations over websocket (#14828) --- homeassistant/components/frontend/__init__.py | 90 +++++----- homeassistant/components/websocket_api.py | 8 +- tests/components/test_frontend.py | 160 +++++++++++++----- tests/components/test_websocket_api.py | 5 +- 4 files changed, 175 insertions(+), 88 deletions(-) diff --git a/homeassistant/components/frontend/__init__.py b/homeassistant/components/frontend/__init__.py index 3f2f9ded22a..f9ace910481 100644 --- a/homeassistant/components/frontend/__init__.py +++ b/homeassistant/components/frontend/__init__.py @@ -96,6 +96,15 @@ WS_TYPE_GET_PANELS = 'get_panels' SCHEMA_GET_PANELS = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend({ vol.Required('type'): WS_TYPE_GET_PANELS, }) +WS_TYPE_GET_THEMES = 'frontend/get_themes' +SCHEMA_GET_THEMES = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend({ + vol.Required('type'): WS_TYPE_GET_THEMES, +}) +WS_TYPE_GET_TRANSLATIONS = 'frontend/get_translations' +SCHEMA_GET_TRANSLATIONS = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend({ + vol.Required('type'): WS_TYPE_GET_TRANSLATIONS, + vol.Required('language'): str, +}) class Panel: @@ -195,7 +204,12 @@ async def async_setup(hass, config): client = None hass.components.websocket_api.async_register_command( - WS_TYPE_GET_PANELS, websocket_handle_get_panels, SCHEMA_GET_PANELS) + WS_TYPE_GET_PANELS, websocket_get_panels, SCHEMA_GET_PANELS) + hass.components.websocket_api.async_register_command( + WS_TYPE_GET_THEMES, websocket_get_themes, SCHEMA_GET_THEMES) + hass.components.websocket_api.async_register_command( + WS_TYPE_GET_TRANSLATIONS, websocket_get_translations, + SCHEMA_GET_TRANSLATIONS) hass.http.register_view(ManifestJSONView) conf = config.get(DOMAIN, {}) @@ -262,16 +276,14 @@ async def async_setup(hass, config): for url in conf.get(CONF_EXTRA_HTML_URL_ES5, []): add_extra_html_url(hass, url, True) - async_setup_themes(hass, conf.get(CONF_THEMES)) - - hass.http.register_view(TranslationsView) + _async_setup_themes(hass, conf.get(CONF_THEMES)) return True -def async_setup_themes(hass, themes): +@callback +def _async_setup_themes(hass, themes): """Set up themes data and services.""" - hass.http.register_view(ThemesView) hass.data[DATA_DEFAULT_THEME] = DEFAULT_THEME if themes is None: hass.data[DATA_THEMES] = {} @@ -400,40 +412,6 @@ class ManifestJSONView(HomeAssistantView): return web.Response(text=msg, content_type="application/manifest+json") -class ThemesView(HomeAssistantView): - """View to return defined themes.""" - - requires_auth = False - url = '/api/themes' - name = 'api:themes' - - @callback - def get(self, request): - """Return themes.""" - hass = request.app['hass'] - - return self.json({ - 'themes': hass.data[DATA_THEMES], - 'default_theme': hass.data[DATA_DEFAULT_THEME], - }) - - -class TranslationsView(HomeAssistantView): - """View to return backend defined translations.""" - - url = '/api/translations/{language}' - name = 'api:translations' - - async def get(self, request, language): - """Return translations.""" - hass = request.app['hass'] - - resources = await async_get_translations(hass, language) - return self.json({ - 'resources': resources, - }) - - def _is_latest(js_option, request): """ Return whether we should serve latest untranspiled code. @@ -467,7 +445,7 @@ def _is_latest(js_option, request): @callback -def websocket_handle_get_panels(hass, connection, msg): +def websocket_get_panels(hass, connection, msg): """Handle get panels command. Async friendly. @@ -480,3 +458,33 @@ def websocket_handle_get_panels(hass, connection, msg): connection.to_write.put_nowait(websocket_api.result_message( msg['id'], panels)) + + +@callback +def websocket_get_themes(hass, connection, msg): + """Handle get themes command. + + Async friendly. + """ + connection.to_write.put_nowait(websocket_api.result_message(msg['id'], { + 'themes': hass.data[DATA_THEMES], + 'default_theme': hass.data[DATA_DEFAULT_THEME], + })) + + +@callback +def websocket_get_translations(hass, connection, msg): + """Handle get translations command. + + Async friendly. + """ + async def send_translations(): + """Send a camera still.""" + resources = await async_get_translations(hass, msg['language']) + connection.send_message_outside(websocket_api.result_message( + msg['id'], { + 'resources': resources, + } + )) + + hass.async_add_job(send_translations()) diff --git a/homeassistant/components/websocket_api.py b/homeassistant/components/websocket_api.py index 11094acd3e2..e16e5524f95 100644 --- a/homeassistant/components/websocket_api.py +++ b/homeassistant/components/websocket_api.py @@ -38,6 +38,7 @@ MAX_PENDING_MSG = 512 ERR_ID_REUSE = 1 ERR_INVALID_FORMAT = 2 ERR_NOT_FOUND = 3 +ERR_UNKNOWN_COMMAND = 4 TYPE_AUTH = 'auth' TYPE_AUTH_INVALID = 'auth_invalid' @@ -353,8 +354,11 @@ class ActiveConnection: 'Identifier values have to increase.')) elif msg['type'] not in handlers: - # Unknown command - break + self.log_error( + 'Received invalid command: {}'.format(msg['type'])) + self.to_write.put_nowait(error_message( + cur_id, ERR_UNKNOWN_COMMAND, + 'Unknown command.')) else: handler, schema = handlers[msg['type']] diff --git a/tests/components/test_frontend.py b/tests/components/test_frontend.py index 2f83d923e2b..2f118f24ef0 100644 --- a/tests/components/test_frontend.py +++ b/tests/components/test_frontend.py @@ -11,6 +11,19 @@ from homeassistant.components.frontend import ( CONF_EXTRA_HTML_URL_ES5) from homeassistant.components import websocket_api as wapi +from tests.common import mock_coro + + +CONFIG_THEMES = { + DOMAIN: { + CONF_THEMES: { + 'happy': { + 'primary-color': 'red' + } + } + } +} + @pytest.fixture def mock_http_client(hass, aiohttp_client): @@ -101,68 +114,109 @@ def test_states_routes(mock_http_client): assert resp.status == 200 -@asyncio.coroutine -def test_themes_api(mock_http_client_with_themes): +async def test_themes_api(hass, hass_ws_client): """Test that /api/themes returns correct data.""" - resp = yield from mock_http_client_with_themes.get('/api/themes') - json = yield from resp.json() - assert json['default_theme'] == 'default' - assert json['themes'] == {'happy': {'primary-color': 'red'}} + assert await async_setup_component(hass, 'frontend', CONFIG_THEMES) + client = await hass_ws_client(hass) + + await client.send_json({ + 'id': 5, + 'type': 'frontend/get_themes', + }) + msg = await client.receive_json() + + assert msg['result']['default_theme'] == 'default' + assert msg['result']['themes'] == {'happy': {'primary-color': 'red'}} -@asyncio.coroutine -def test_themes_set_theme(hass, mock_http_client_with_themes): +async def test_themes_set_theme(hass, hass_ws_client): """Test frontend.set_theme service.""" - yield from hass.services.async_call(DOMAIN, 'set_theme', {'name': 'happy'}) - yield from hass.async_block_till_done() - resp = yield from mock_http_client_with_themes.get('/api/themes') - json = yield from resp.json() - assert json['default_theme'] == 'happy' + assert await async_setup_component(hass, 'frontend', CONFIG_THEMES) + client = await hass_ws_client(hass) - yield from hass.services.async_call( - DOMAIN, 'set_theme', {'name': 'default'}) - yield from hass.async_block_till_done() - resp = yield from mock_http_client_with_themes.get('/api/themes') - json = yield from resp.json() - assert json['default_theme'] == 'default' + await hass.services.async_call( + DOMAIN, 'set_theme', {'name': 'happy'}, blocking=True) + + await client.send_json({ + 'id': 5, + 'type': 'frontend/get_themes', + }) + msg = await client.receive_json() + + assert msg['result']['default_theme'] == 'happy' + + await hass.services.async_call( + DOMAIN, 'set_theme', {'name': 'default'}, blocking=True) + + await client.send_json({ + 'id': 6, + 'type': 'frontend/get_themes', + }) + msg = await client.receive_json() + + assert msg['result']['default_theme'] == 'default' -@asyncio.coroutine -def test_themes_set_theme_wrong_name(hass, mock_http_client_with_themes): +async def test_themes_set_theme_wrong_name(hass, hass_ws_client): """Test frontend.set_theme service called with wrong name.""" - yield from hass.services.async_call(DOMAIN, 'set_theme', {'name': 'wrong'}) - yield from hass.async_block_till_done() - resp = yield from mock_http_client_with_themes.get('/api/themes') - json = yield from resp.json() - assert json['default_theme'] == 'default' + assert await async_setup_component(hass, 'frontend', CONFIG_THEMES) + client = await hass_ws_client(hass) + + await hass.services.async_call( + DOMAIN, 'set_theme', {'name': 'wrong'}, blocking=True) + + await client.send_json({ + 'id': 5, + 'type': 'frontend/get_themes', + }) + + msg = await client.receive_json() + + assert msg['result']['default_theme'] == 'default' -@asyncio.coroutine -def test_themes_reload_themes(hass, mock_http_client_with_themes): +async def test_themes_reload_themes(hass, hass_ws_client): """Test frontend.reload_themes service.""" + assert await async_setup_component(hass, 'frontend', CONFIG_THEMES) + client = await hass_ws_client(hass) + with patch('homeassistant.components.frontend.load_yaml_config_file', return_value={DOMAIN: { CONF_THEMES: { 'sad': {'primary-color': 'blue'} }}}): - yield from hass.services.async_call(DOMAIN, 'set_theme', - {'name': 'happy'}) - yield from hass.services.async_call(DOMAIN, 'reload_themes') - yield from hass.async_block_till_done() - resp = yield from mock_http_client_with_themes.get('/api/themes') - json = yield from resp.json() - assert json['themes'] == {'sad': {'primary-color': 'blue'}} - assert json['default_theme'] == 'default' + await hass.services.async_call( + DOMAIN, 'set_theme', {'name': 'happy'}, blocking=True) + await hass.services.async_call(DOMAIN, 'reload_themes', blocking=True) + + await client.send_json({ + 'id': 5, + 'type': 'frontend/get_themes', + }) + + msg = await client.receive_json() + + assert msg['result']['themes'] == {'sad': {'primary-color': 'blue'}} + assert msg['result']['default_theme'] == 'default' -@asyncio.coroutine -def test_missing_themes(mock_http_client): +async def test_missing_themes(hass, hass_ws_client): """Test that themes API works when themes are not defined.""" - resp = yield from mock_http_client.get('/api/themes') - assert resp.status == 200 - json = yield from resp.json() - assert json['default_theme'] == 'default' - assert json['themes'] == {} + await async_setup_component(hass, 'frontend') + + client = await hass_ws_client(hass) + await client.send_json({ + 'id': 5, + 'type': 'frontend/get_themes', + }) + + msg = await client.receive_json() + + assert msg['id'] == 5 + assert msg['type'] == wapi.TYPE_RESULT + assert msg['success'] + assert msg['result']['default_theme'] == 'default' + assert msg['result']['themes'] == {} @asyncio.coroutine @@ -204,3 +258,23 @@ async def test_get_panels(hass, hass_ws_client): assert msg['result']['map']['url_path'] == 'map' assert msg['result']['map']['icon'] == 'mdi:account-location' assert msg['result']['map']['title'] == 'Map' + + +async def test_get_translations(hass, hass_ws_client): + """Test get_translations command.""" + await async_setup_component(hass, 'frontend') + client = await hass_ws_client(hass) + + with patch('homeassistant.components.frontend.async_get_translations', + side_effect=lambda hass, lang: mock_coro({'lang': lang})): + await client.send_json({ + 'id': 5, + 'type': 'frontend/get_translations', + 'language': 'nl', + }) + msg = await client.receive_json() + + assert msg['id'] == 5 + assert msg['type'] == wapi.TYPE_RESULT + assert msg['success'] + assert msg['result'] == {'resources': {'lang': 'nl'}} diff --git a/tests/components/test_websocket_api.py b/tests/components/test_websocket_api.py index cff103142b0..fbd8584a7d1 100644 --- a/tests/components/test_websocket_api.py +++ b/tests/components/test_websocket_api.py @@ -311,8 +311,9 @@ def test_unknown_command(websocket_client): 'type': 'unknown_command', }) - msg = yield from websocket_client.receive() - assert msg.type == WSMsgType.close + msg = yield from websocket_client.receive_json() + assert not msg['success'] + assert msg['error']['code'] == wapi.ERR_UNKNOWN_COMMAND async def test_auth_with_token(hass, aiohttp_client, hass_access_token): From 6d26915c69071b70b0428d6cb034e8ba579c6995 Mon Sep 17 00:00:00 2001 From: Roman Date: Wed, 6 Jun 2018 11:38:50 +0200 Subject: [PATCH 020/144] Feature/gearbest library update (Closes: #14813) (#14833) --- homeassistant/components/sensor/gearbest.py | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/sensor/gearbest.py b/homeassistant/components/sensor/gearbest.py index aa1d2d9eff0..d71419ba79e 100644 --- a/homeassistant/components/sensor/gearbest.py +++ b/homeassistant/components/sensor/gearbest.py @@ -16,7 +16,7 @@ from homeassistant.helpers.entity import Entity from homeassistant.helpers.event import track_time_interval from homeassistant.const import (CONF_NAME, CONF_ID, CONF_URL, CONF_CURRENCY) -REQUIREMENTS = ['gearbest_parser==1.0.5'] +REQUIREMENTS = ['gearbest_parser==1.0.7'] _LOGGER = logging.getLogger(__name__) CONF_ITEMS = 'items' diff --git a/requirements_all.txt b/requirements_all.txt index 8673f4aa22f..81f60e47c6d 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -344,7 +344,7 @@ gTTS-token==1.1.1 # gattlib==0.20150805 # homeassistant.components.sensor.gearbest -gearbest_parser==1.0.5 +gearbest_parser==1.0.7 # homeassistant.components.sensor.gitter gitterpy==0.1.7 From bef15264b7d6da67caaf57de3f9b9276a4d218bf Mon Sep 17 00:00:00 2001 From: Sebastian Muszynski Date: Wed, 6 Jun 2018 19:51:59 +0200 Subject: [PATCH 021/144] Ignore the mistaken long_both_click event of the 86sw (Closes: #14802) (#14808) --- homeassistant/components/binary_sensor/xiaomi_aqara.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/binary_sensor/xiaomi_aqara.py b/homeassistant/components/binary_sensor/xiaomi_aqara.py index ebdcdc6ca70..3f8fc3dbb36 100644 --- a/homeassistant/components/binary_sensor/xiaomi_aqara.py +++ b/homeassistant/components/binary_sensor/xiaomi_aqara.py @@ -330,7 +330,7 @@ class XiaomiButton(XiaomiBinarySensor): click_type = 'both' elif value == 'shake': click_type = 'shake' - elif value == 'long_click': + elif value in ['long_click', 'long_both_click']: return False else: _LOGGER.warning("Unsupported click_type detected: %s", value) From d8adb4bdb044dd0e597a9ad0f359ee0b6bf28f3b Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 6 Jun 2018 22:42:01 -0400 Subject: [PATCH 022/144] Bump frontend to 20180607.0 --- homeassistant/components/frontend/__init__.py | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/frontend/__init__.py b/homeassistant/components/frontend/__init__.py index f9ace910481..d61b6f50a96 100644 --- a/homeassistant/components/frontend/__init__.py +++ b/homeassistant/components/frontend/__init__.py @@ -24,7 +24,7 @@ from homeassistant.core import callback from homeassistant.helpers.translation import async_get_translations from homeassistant.loader import bind_hass -REQUIREMENTS = ['home-assistant-frontend==20180603.0'] +REQUIREMENTS = ['home-assistant-frontend==20180607.0'] DOMAIN = 'frontend' DEPENDENCIES = ['api', 'websocket_api', 'http', 'system_log'] diff --git a/requirements_all.txt b/requirements_all.txt index 81f60e47c6d..0449a695b66 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -392,7 +392,7 @@ hipnotify==1.0.8 holidays==0.9.5 # homeassistant.components.frontend -home-assistant-frontend==20180603.0 +home-assistant-frontend==20180607.0 # homeassistant.components.homekit_controller # homekit==0.6 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 47f54954cd2..207b0a8545f 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -81,7 +81,7 @@ hbmqtt==0.9.2 holidays==0.9.5 # homeassistant.components.frontend -home-assistant-frontend==20180603.0 +home-assistant-frontend==20180607.0 # homeassistant.components.influxdb # homeassistant.components.sensor.influxdb From bf74cab7af359691528a4d7587a3d89e7bca7945 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 7 Jun 2018 09:58:54 -0400 Subject: [PATCH 023/144] Fix non awaited test (#14854) --- tests/components/test_init.py | 50 ++++++++++++++++++----------------- 1 file changed, 26 insertions(+), 24 deletions(-) diff --git a/tests/components/test_init.py b/tests/components/test_init.py index c8c7e0d809b..1e565054637 100644 --- a/tests/components/test_init.py +++ b/tests/components/test_init.py @@ -74,30 +74,6 @@ class TestComponentsCore(unittest.TestCase): self.hass.block_till_done() self.assertEqual(1, len(calls)) - @patch('homeassistant.core.ServiceRegistry.call') - async def test_turn_on_to_not_block_for_domains_without_service(self, - mock_call): - """Test if turn_on is blocking domain with no service.""" - async_mock_service(self.hass, 'light', SERVICE_TURN_ON) - - # We can't test if our service call results in services being called - # because by mocking out the call service method, we mock out all - # So we mimic how the service registry calls services - service_call = ha.ServiceCall('homeassistant', 'turn_on', { - 'entity_id': ['light.test', 'sensor.bla', 'light.bla'] - }) - service = self.hass.services._services['homeassistant']['turn_on'] - await service.func(service_call) - - self.assertEqual(2, mock_call.call_count) - self.assertEqual( - ('light', 'turn_on', {'entity_id': ['light.bla', 'light.test']}, - True), - mock_call.call_args_list[0][0]) - self.assertEqual( - ('sensor', 'turn_on', {'entity_id': ['sensor.bla']}, False), - mock_call.call_args_list[1][0]) - @patch('homeassistant.config.os.path.isfile', Mock(return_value=True)) def test_reload_core_conf(self): """Test reload core conf service.""" @@ -284,3 +260,29 @@ async def test_turn_on_multiple_intent(hass): assert call.domain == 'light' assert call.service == 'turn_on' assert call.data == {'entity_id': ['light.test_lights_2']} + + +async def test_turn_on_to_not_block_for_domains_without_service(hass): + """Test if turn_on is blocking domain with no service.""" + await comps.async_setup(hass, {}) + async_mock_service(hass, 'light', SERVICE_TURN_ON) + hass.states.async_set('light.Bowl', STATE_ON) + hass.states.async_set('light.Ceiling', STATE_OFF) + + # We can't test if our service call results in services being called + # because by mocking out the call service method, we mock out all + # So we mimic how the service registry calls services + service_call = ha.ServiceCall('homeassistant', 'turn_on', { + 'entity_id': ['light.test', 'sensor.bla', 'light.bla'] + }) + service = hass.services._services['homeassistant']['turn_on'] + + with patch('homeassistant.core.ServiceRegistry.async_call', + side_effect=lambda *args: mock_coro()) as mock_call: + await service.func(service_call) + + assert mock_call.call_count == 2 + assert mock_call.call_args_list[0][0] == ( + 'light', 'turn_on', {'entity_id': ['light.bla', 'light.test']}, True) + assert mock_call.call_args_list[1][0] == ( + 'sensor', 'turn_on', {'entity_id': ['sensor.bla']}, False) From 0b405c33c428b8be25d592e8ecba28bd4cc88ba6 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 7 Jun 2018 10:00:42 -0400 Subject: [PATCH 024/144] Update Hue flow title (#14852) --- homeassistant/components/hue/strings.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/hue/strings.json b/homeassistant/components/hue/strings.json index fc9e91c93d7..f8873894a01 100644 --- a/homeassistant/components/hue/strings.json +++ b/homeassistant/components/hue/strings.json @@ -1,6 +1,6 @@ { "config": { - "title": "Philips Hue Bridge", + "title": "Philips Hue", "step": { "init": { "title": "Pick Hue bridge", From 83ce9450f79d4407eea8d0ad2becb49e6a939690 Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Thu, 7 Jun 2018 16:04:58 +0200 Subject: [PATCH 025/144] Upgrade Mastodon.py to 1.3.0 (#14858) --- homeassistant/components/notify/mastodon.py | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/notify/mastodon.py b/homeassistant/components/notify/mastodon.py index 3ba95407fec..e29289722e8 100644 --- a/homeassistant/components/notify/mastodon.py +++ b/homeassistant/components/notify/mastodon.py @@ -13,7 +13,7 @@ from homeassistant.components.notify import ( from homeassistant.const import CONF_ACCESS_TOKEN import homeassistant.helpers.config_validation as cv -REQUIREMENTS = ['Mastodon.py==1.2.2'] +REQUIREMENTS = ['Mastodon.py==1.3.0'] _LOGGER = logging.getLogger(__name__) diff --git a/requirements_all.txt b/requirements_all.txt index 0449a695b66..c68ec249fc8 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -31,7 +31,7 @@ DoorBirdPy==0.1.3 HAP-python==2.2.2 # homeassistant.components.notify.mastodon -Mastodon.py==1.2.2 +Mastodon.py==1.3.0 # homeassistant.components.isy994 PyISY==1.1.0 From 6b2b92a732813c93c4412567c1ce900e21fecc64 Mon Sep 17 00:00:00 2001 From: Anders Melchiorsen Date: Thu, 7 Jun 2018 16:06:29 +0200 Subject: [PATCH 026/144] Improvements to LIFX reliability (#14848) * Improve fault tolerance of LIFX initialization * Update aiolifx to 0.6.3 * Use list comprehension --- homeassistant/components/light/lifx.py | 27 +++++++++++--------------- requirements_all.txt | 2 +- 2 files changed, 12 insertions(+), 17 deletions(-) diff --git a/homeassistant/components/light/lifx.py b/homeassistant/components/light/lifx.py index dff5ccd42ac..421356f07bc 100644 --- a/homeassistant/components/light/lifx.py +++ b/homeassistant/components/light/lifx.py @@ -30,7 +30,7 @@ import homeassistant.util.color as color_util _LOGGER = logging.getLogger(__name__) -REQUIREMENTS = ['aiolifx==0.6.1', 'aiolifx_effects==0.1.2'] +REQUIREMENTS = ['aiolifx==0.6.3', 'aiolifx_effects==0.1.2'] UDP_BROADCAST_PORT = 56700 @@ -201,7 +201,7 @@ def merge_hsbk(base, change): """Copy change on top of base, except when None.""" if change is None: return None - return list(map(lambda x, y: y if y is not None else x, base, change)) + return [b if c is None else c for b, c in zip(base, change)] class LIFXManager(object): @@ -256,7 +256,7 @@ class LIFXManager(object): async def start_effect(self, entities, service, **kwargs): """Start a light effect on entities.""" - devices = list(map(lambda l: l.device, entities)) + devices = [light.device for light in entities] if service == SERVICE_EFFECT_PULSE: effect = aiolifx_effects().EffectPulse( @@ -314,12 +314,13 @@ class LIFXManager(object): # Read initial state ack = AwaitAioLIFX().wait - version_resp = await ack(device.get_version) - if version_resp: - color_resp = await ack(device.get_color) + color_resp = await ack(device.get_color) + if color_resp: + version_resp = await ack(device.get_version) - if version_resp is None or color_resp is None: + if color_resp is None or version_resp is None: _LOGGER.error("Failed to initialize %s", device.ip_addr) + device.registered = False else: device.timeout = MESSAGE_TIMEOUT device.retry_count = MESSAGE_RETRIES @@ -440,18 +441,13 @@ class LIFXLight(Light): @property def brightness(self): """Return the brightness of this light between 0..255.""" - brightness = convert_16_to_8(self.device.color[2]) - _LOGGER.debug("brightness: %d", brightness) - return brightness + return convert_16_to_8(self.device.color[2]) @property def color_temp(self): """Return the color temperature.""" kelvin = self.device.color[3] - temperature = color_util.color_temperature_kelvin_to_mired(kelvin) - - _LOGGER.debug("color_temp: %d", temperature) - return temperature + return color_util.color_temperature_kelvin_to_mired(kelvin) @property def is_on(self): @@ -564,7 +560,6 @@ class LIFXLight(Light): async def async_update(self): """Update bulb status.""" - _LOGGER.debug("%s async_update", self.who) if self.available and not self.lock.locked(): await AwaitAioLIFX().wait(self.device.get_color) @@ -627,7 +622,7 @@ class LIFXStrip(LIFXColor): zones = list(range(0, num_zones)) else: - zones = list(filter(lambda x: x < num_zones, set(zones))) + zones = [x for x in set(zones) if x < num_zones] # Zone brightness is not reported when powered off if not self.is_on and hsbk[2] is None: diff --git a/requirements_all.txt b/requirements_all.txt index c68ec249fc8..ce34f2662eb 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -95,7 +95,7 @@ aiohue==1.5.0 aioimaplib==0.7.13 # homeassistant.components.light.lifx -aiolifx==0.6.1 +aiolifx==0.6.3 # homeassistant.components.light.lifx aiolifx_effects==0.1.2 From f6963315631bdecd7f6acca41369eeb0b9b473bb Mon Sep 17 00:00:00 2001 From: starkillerOG Date: Thu, 7 Jun 2018 16:57:45 +0200 Subject: [PATCH 027/144] Add general sound mode support (#14729) * Media player: general sound mode support * General sound mode support * White spaces * Add sound mode support to demo media player * white space * remove unnessesary code --- .../components/media_player/__init__.py | 52 +++++++++++++++++++ homeassistant/components/media_player/demo.py | 31 +++++++++-- .../components/media_player/services.yaml | 10 ++++ 3 files changed, 88 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/media_player/__init__.py b/homeassistant/components/media_player/__init__.py index 20a1a473ba8..7452b7dd186 100644 --- a/homeassistant/components/media_player/__init__.py +++ b/homeassistant/components/media_player/__init__.py @@ -57,6 +57,7 @@ ENTITY_IMAGE_CACHE = { SERVICE_PLAY_MEDIA = 'play_media' SERVICE_SELECT_SOURCE = 'select_source' +SERVICE_SELECT_SOUND_MODE = 'select_sound_mode' SERVICE_CLEAR_PLAYLIST = 'clear_playlist' ATTR_MEDIA_VOLUME_LEVEL = 'volume_level' @@ -81,6 +82,8 @@ ATTR_APP_ID = 'app_id' ATTR_APP_NAME = 'app_name' ATTR_INPUT_SOURCE = 'source' ATTR_INPUT_SOURCE_LIST = 'source_list' +ATTR_SOUND_MODE = 'sound_mode' +ATTR_SOUND_MODE_LIST = 'sound_mode_list' ATTR_MEDIA_ENQUEUE = 'enqueue' ATTR_MEDIA_SHUFFLE = 'shuffle' @@ -109,6 +112,7 @@ SUPPORT_STOP = 4096 SUPPORT_CLEAR_PLAYLIST = 8192 SUPPORT_PLAY = 16384 SUPPORT_SHUFFLE_SET = 32768 +SUPPORT_SELECT_SOUND_MODE = 65536 # Service call validation schemas MEDIA_PLAYER_SCHEMA = vol.Schema({ @@ -132,6 +136,10 @@ MEDIA_PLAYER_SELECT_SOURCE_SCHEMA = MEDIA_PLAYER_SCHEMA.extend({ vol.Required(ATTR_INPUT_SOURCE): cv.string, }) +MEDIA_PLAYER_SELECT_SOUND_MODE_SCHEMA = MEDIA_PLAYER_SCHEMA.extend({ + vol.Required(ATTR_SOUND_MODE): cv.string, +}) + MEDIA_PLAYER_PLAY_MEDIA_SCHEMA = MEDIA_PLAYER_SCHEMA.extend({ vol.Required(ATTR_MEDIA_CONTENT_TYPE): cv.string, vol.Required(ATTR_MEDIA_CONTENT_ID): cv.string, @@ -167,6 +175,9 @@ SERVICE_TO_METHOD = { SERVICE_SELECT_SOURCE: { 'method': 'async_select_source', 'schema': MEDIA_PLAYER_SELECT_SOURCE_SCHEMA}, + SERVICE_SELECT_SOUND_MODE: { + 'method': 'async_select_sound_mode', + 'schema': MEDIA_PLAYER_SELECT_SOUND_MODE_SCHEMA}, SERVICE_PLAY_MEDIA: { 'method': 'async_play_media', 'schema': MEDIA_PLAYER_PLAY_MEDIA_SCHEMA}, @@ -197,6 +208,8 @@ ATTR_TO_PROPERTY = [ ATTR_APP_NAME, ATTR_INPUT_SOURCE, ATTR_INPUT_SOURCE_LIST, + ATTR_SOUND_MODE, + ATTR_SOUND_MODE_LIST, ATTR_MEDIA_SHUFFLE, ] @@ -346,6 +359,17 @@ def select_source(hass, source, entity_id=None): hass.services.call(DOMAIN, SERVICE_SELECT_SOURCE, data) +@bind_hass +def select_sound_mode(hass, sound_mode, entity_id=None): + """Send the media player the command to select sound mode.""" + data = {ATTR_SOUND_MODE: sound_mode} + + if entity_id: + data[ATTR_ENTITY_ID] = entity_id + + hass.services.call(DOMAIN, SERVICE_SELECT_SOUND_MODE, data) + + @bind_hass def clear_playlist(hass, entity_id=None): """Send the media player the command for clear playlist.""" @@ -399,6 +423,8 @@ async def async_setup(hass, config): params['position'] = service.data.get(ATTR_MEDIA_SEEK_POSITION) elif service.service == SERVICE_SELECT_SOURCE: params['source'] = service.data.get(ATTR_INPUT_SOURCE) + elif service.service == SERVICE_SELECT_SOUND_MODE: + params['sound_mode'] = service.data.get(ATTR_SOUND_MODE) elif service.service == SERVICE_PLAY_MEDIA: params['media_type'] = \ service.data.get(ATTR_MEDIA_CONTENT_TYPE) @@ -580,6 +606,16 @@ class MediaPlayerDevice(Entity): """List of available input sources.""" return None + @property + def sound_mode(self): + """Name of the current sound mode.""" + return None + + @property + def sound_mode_list(self): + """List of available sound modes.""" + return None + @property def shuffle(self): """Boolean if shuffle is enabled.""" @@ -723,6 +759,17 @@ class MediaPlayerDevice(Entity): """ return self.hass.async_add_job(self.select_source, source) + def select_sound_mode(self, sound_mode): + """Select sound mode.""" + raise NotImplementedError() + + def async_select_sound_mode(self, sound_mode): + """Select sound mode. + + This method must be run in the event loop and returns a coroutine. + """ + return self.hass.async_add_job(self.select_sound_mode, sound_mode) + def clear_playlist(self): """Clear players playlist.""" raise NotImplementedError() @@ -796,6 +843,11 @@ class MediaPlayerDevice(Entity): """Boolean if select source command supported.""" return bool(self.supported_features & SUPPORT_SELECT_SOURCE) + @property + def support_select_sound_mode(self): + """Boolean if select sound mode command supported.""" + return bool(self.supported_features & SUPPORT_SELECT_SOUND_MODE) + @property def support_clear_playlist(self): """Boolean if clear playlist command supported.""" diff --git a/homeassistant/components/media_player/demo.py b/homeassistant/components/media_player/demo.py index 22fe1d005f7..2c74feae847 100644 --- a/homeassistant/components/media_player/demo.py +++ b/homeassistant/components/media_player/demo.py @@ -8,8 +8,8 @@ from homeassistant.components.media_player import ( MEDIA_TYPE_MUSIC, MEDIA_TYPE_TVSHOW, MEDIA_TYPE_MOVIE, SUPPORT_NEXT_TRACK, SUPPORT_PAUSE, SUPPORT_PLAY_MEDIA, SUPPORT_PREVIOUS_TRACK, SUPPORT_TURN_OFF, SUPPORT_TURN_ON, SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET, - SUPPORT_SELECT_SOURCE, SUPPORT_CLEAR_PLAYLIST, SUPPORT_PLAY, - SUPPORT_SHUFFLE_SET, MediaPlayerDevice) + SUPPORT_SELECT_SOURCE, SUPPORT_SELECT_SOUND_MODE, SUPPORT_CLEAR_PLAYLIST, + SUPPORT_PLAY, SUPPORT_SHUFFLE_SET, MediaPlayerDevice) from homeassistant.const import STATE_OFF, STATE_PAUSED, STATE_PLAYING import homeassistant.util.dt as dt_util @@ -28,22 +28,26 @@ def setup_platform(hass, config, add_devices, discovery_info=None): YOUTUBE_COVER_URL_FORMAT = 'https://img.youtube.com/vi/{}/hqdefault.jpg' +SOUND_MODE_LIST = ['Dummy Music', 'Dummy Movie'] +DEFAULT_SOUND_MODE = 'Dummy Music' YOUTUBE_PLAYER_SUPPORT = \ SUPPORT_PAUSE | SUPPORT_VOLUME_SET | SUPPORT_VOLUME_MUTE | \ SUPPORT_TURN_ON | SUPPORT_TURN_OFF | SUPPORT_PLAY_MEDIA | SUPPORT_PLAY | \ - SUPPORT_SHUFFLE_SET + SUPPORT_SHUFFLE_SET | SUPPORT_SELECT_SOUND_MODE MUSIC_PLAYER_SUPPORT = \ SUPPORT_PAUSE | SUPPORT_VOLUME_SET | SUPPORT_VOLUME_MUTE | \ SUPPORT_TURN_ON | SUPPORT_TURN_OFF | SUPPORT_CLEAR_PLAYLIST | \ SUPPORT_PLAY | SUPPORT_SHUFFLE_SET | \ - SUPPORT_PREVIOUS_TRACK | SUPPORT_NEXT_TRACK + SUPPORT_PREVIOUS_TRACK | SUPPORT_NEXT_TRACK | \ + SUPPORT_SELECT_SOUND_MODE NETFLIX_PLAYER_SUPPORT = \ SUPPORT_PAUSE | SUPPORT_TURN_ON | SUPPORT_TURN_OFF | \ SUPPORT_SELECT_SOURCE | SUPPORT_PLAY | SUPPORT_SHUFFLE_SET | \ - SUPPORT_PREVIOUS_TRACK | SUPPORT_NEXT_TRACK + SUPPORT_PREVIOUS_TRACK | SUPPORT_NEXT_TRACK | \ + SUPPORT_SELECT_SOUND_MODE class AbstractDemoPlayer(MediaPlayerDevice): @@ -58,6 +62,8 @@ class AbstractDemoPlayer(MediaPlayerDevice): self._volume_level = 1.0 self._volume_muted = False self._shuffle = False + self._sound_mode_list = SOUND_MODE_LIST + self._sound_mode = DEFAULT_SOUND_MODE @property def should_poll(self): @@ -89,6 +95,16 @@ class AbstractDemoPlayer(MediaPlayerDevice): """Boolean if shuffling is enabled.""" return self._shuffle + @property + def sound_mode(self): + """Return the current sound mode.""" + return self._sound_mode + + @property + def sound_mode_list(self): + """Return a list of available sound modes.""" + return self._sound_mode_list + def turn_on(self): """Turn the media player on.""" self._player_state = STATE_PLAYING @@ -124,6 +140,11 @@ class AbstractDemoPlayer(MediaPlayerDevice): self._shuffle = shuffle self.schedule_update_ha_state() + def select_sound_mode(self, sound_mode): + """Select sound mode.""" + self._sound_mode = sound_mode + self.schedule_update_ha_state() + class DemoYoutubePlayer(AbstractDemoPlayer): """A Demo media player that only supports YouTube.""" diff --git a/homeassistant/components/media_player/services.yaml b/homeassistant/components/media_player/services.yaml index 0a6c413a688..765f7e1f0f7 100644 --- a/homeassistant/components/media_player/services.yaml +++ b/homeassistant/components/media_player/services.yaml @@ -144,6 +144,16 @@ select_source: description: Name of the source to switch to. Platform dependent. example: 'video1' +select_sound_mode: + description: Send the media player the command to change sound mode. + fields: + entity_id: + description: Name(s) of entities to change sound mode on. + example: 'media_player.marantz' + sound_mode: + description: Name of the sound mode to switch to. + example: 'Music' + clear_playlist: description: Send the media player the command to clear players playlist. fields: From d14d2fe588e7afe97184facb26e04e074cd346ca Mon Sep 17 00:00:00 2001 From: Matthew Treinish Date: Thu, 7 Jun 2018 10:43:51 -0700 Subject: [PATCH 028/144] Add IBM Watson IoT Platform component (#13664) This commit adds a new history component for the IBM Watson IoT Platform. The IBM Watson IoT Platform allows for tracking of devices and analytics on top of the device data. This new component allows users to have home assistant automatically populate a watson iot platform board with device data from devices managed by home assistant. --- .coveragerc | 1 + homeassistant/components/watson_iot.py | 214 +++++++++++++++++++++++++ requirements_all.txt | 3 + 3 files changed, 218 insertions(+) create mode 100644 homeassistant/components/watson_iot.py diff --git a/.coveragerc b/.coveragerc index c51f6bc37cb..e38ceeba5ce 100644 --- a/.coveragerc +++ b/.coveragerc @@ -749,6 +749,7 @@ omit = homeassistant/components/tts/picotts.py homeassistant/components/vacuum/mqtt.py homeassistant/components/vacuum/roomba.py + homeassistant/components/watson_iot.py homeassistant/components/weather/bom.py homeassistant/components/weather/buienradar.py homeassistant/components/weather/darksky.py diff --git a/homeassistant/components/watson_iot.py b/homeassistant/components/watson_iot.py new file mode 100644 index 00000000000..246cf3a96c2 --- /dev/null +++ b/homeassistant/components/watson_iot.py @@ -0,0 +1,214 @@ +""" +A component which allows you to send data to the IBM Watson IoT Platform. + +For more details about this component, please refer to the documentation at +https://home-assistant.io/components/watson_iot/ +""" + +import logging +import queue +import threading +import time + +import voluptuous as vol + +from homeassistant.const import ( + CONF_DOMAINS, CONF_ENTITIES, CONF_EXCLUDE, CONF_INCLUDE, + CONF_TOKEN, CONF_TYPE, EVENT_STATE_CHANGED, EVENT_HOMEASSISTANT_STOP, + STATE_UNAVAILABLE, STATE_UNKNOWN) +from homeassistant.helpers import state as state_helper +import homeassistant.helpers.config_validation as cv + +REQUIREMENTS = ['ibmiotf==0.3.4'] + +_LOGGER = logging.getLogger(__name__) + +CONF_ORG = 'organization' +CONF_ID = 'id' + +DOMAIN = 'watson_iot' + +RETRY_DELAY = 20 +MAX_TRIES = 3 + +CONFIG_SCHEMA = vol.Schema({ + DOMAIN: vol.All(vol.Schema({ + vol.Required(CONF_ORG): cv.string, + vol.Required(CONF_TYPE): cv.string, + vol.Required(CONF_ID): cv.string, + vol.Required(CONF_TOKEN): cv.string, + vol.Optional(CONF_EXCLUDE, default={}): vol.Schema({ + vol.Optional(CONF_ENTITIES, default=[]): cv.entity_ids, + vol.Optional(CONF_DOMAINS, default=[]): + vol.All(cv.ensure_list, [cv.string]) + }), + vol.Optional(CONF_INCLUDE, default={}): vol.Schema({ + vol.Optional(CONF_ENTITIES, default=[]): cv.entity_ids, + vol.Optional(CONF_DOMAINS, default=[]): + vol.All(cv.ensure_list, [cv.string]) + }), + })), +}, extra=vol.ALLOW_EXTRA) + + +def setup(hass, config): + """Set up the Watson IoT Platform component.""" + from ibmiotf import gateway + + conf = config[DOMAIN] + + include = conf[CONF_INCLUDE] + exclude = conf[CONF_EXCLUDE] + whitelist_e = set(include[CONF_ENTITIES]) + whitelist_d = set(include[CONF_DOMAINS]) + blacklist_e = set(exclude[CONF_ENTITIES]) + blacklist_d = set(exclude[CONF_DOMAINS]) + + client_args = { + 'org': conf[CONF_ORG], + 'type': conf[CONF_TYPE], + 'id': conf[CONF_ID], + 'auth-method': 'token', + 'auth-token': conf[CONF_TOKEN], + } + watson_gateway = gateway.Client(client_args) + + def event_to_json(event): + """Add an event to the outgoing list.""" + state = event.data.get('new_state') + if state is None or state.state in ( + STATE_UNKNOWN, '', STATE_UNAVAILABLE) or \ + state.entity_id in blacklist_e or state.domain in blacklist_d: + return + + if (whitelist_e and state.entity_id not in whitelist_e) or \ + (whitelist_d and state.domain not in whitelist_d): + return + + try: + _state_as_value = float(state.state) + except ValueError: + _state_as_value = None + + if _state_as_value is None: + try: + _state_as_value = float(state_helper.state_as_number(state)) + except ValueError: + _state_as_value = None + + out_event = { + 'tags': { + 'domain': state.domain, + 'entity_id': state.object_id, + }, + 'time': event.time_fired.isoformat(), + 'fields': { + 'state': state.state + } + } + if _state_as_value is not None: + out_event['fields']['state_value'] = _state_as_value + + for key, value in state.attributes.items(): + if key != 'unit_of_measurement': + # If the key is already in fields + if key in out_event['fields']: + key = key + "_" + # For each value we try to cast it as float + # But if we can not do it we store the value + # as string + try: + out_event['fields'][key] = float(value) + except (ValueError, TypeError): + out_event['fields'][key] = str(value) + + return out_event + + instance = hass.data[DOMAIN] = WatsonIOTThread( + hass, watson_gateway, event_to_json) + instance.start() + + def shutdown(event): + """Shut down the thread.""" + instance.queue.put(None) + instance.join() + + hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, shutdown) + + return True + + +class WatsonIOTThread(threading.Thread): + """A threaded event handler class.""" + + def __init__(self, hass, gateway, event_to_json): + """Initialize the listener.""" + threading.Thread.__init__(self, name='WatsonIOT') + self.queue = queue.Queue() + self.gateway = gateway + self.gateway.connect() + self.event_to_json = event_to_json + self.write_errors = 0 + self.shutdown = False + hass.bus.listen(EVENT_STATE_CHANGED, self._event_listener) + + def _event_listener(self, event): + """Listen for new messages on the bus and queue them for Watson IOT.""" + item = (time.monotonic(), event) + self.queue.put(item) + + def get_events_json(self): + """Return an event formatted for writing.""" + events = [] + + try: + item = self.queue.get() + + if item is None: + self.shutdown = True + else: + event_json = self.event_to_json(item[1]) + if event_json: + events.append(event_json) + + except queue.Empty: + pass + + return events + + def write_to_watson(self, events): + """Write preprocessed events to watson.""" + import ibmiotf + + for event in events: + for retry in range(MAX_TRIES + 1): + try: + for field in event['fields']: + value = event['fields'][field] + device_success = self.gateway.publishDeviceEvent( + event['tags']['domain'], + event['tags']['entity_id'], + field, 'json', value) + if not device_success: + _LOGGER.error( + "Failed to publish message to watson iot") + continue + break + except (ibmiotf.MissingMessageEncoderException, IOError): + if retry < MAX_TRIES: + time.sleep(RETRY_DELAY) + else: + _LOGGER.exception( + "Failed to publish message to watson iot") + + def run(self): + """Process incoming events.""" + while not self.shutdown: + event = self.get_events_json() + if event: + self.write_to_watson(event) + self.queue.task_done() + + def block_till_done(self): + """Block till all events processed.""" + self.queue.join() diff --git a/requirements_all.txt b/requirements_all.txt index ce34f2662eb..76492dee899 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -438,6 +438,9 @@ hydrawiser==0.1.1 # homeassistant.components.sensor.htu21d # i2csense==0.0.4 +# homeassistant.components.watson_iot +ibmiotf==0.3.4 + # homeassistant.components.light.iglo iglo==1.2.7 From a6c1192bfc864d5a0ee88f4f134d4155d0007aaf Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Thu, 7 Jun 2018 19:49:14 +0200 Subject: [PATCH 029/144] Upgrade aiohttp to 3.3.0 (#14766) --- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- setup.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index e76dc24d9dd..c69e9eb4af4 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -1,4 +1,4 @@ -aiohttp==3.2.1 +aiohttp==3.3.0 astral==1.6.1 async_timeout==3.0.0 attrs==18.1.0 diff --git a/requirements_all.txt b/requirements_all.txt index 76492dee899..d2b52d17961 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1,5 +1,5 @@ # Home Assistant core -aiohttp==3.2.1 +aiohttp==3.3.0 astral==1.6.1 async_timeout==3.0.0 attrs==18.1.0 diff --git a/setup.py b/setup.py index 4390b980f9e..a4d15feb7fc 100755 --- a/setup.py +++ b/setup.py @@ -42,7 +42,7 @@ DOWNLOAD_URL = '{}/archive/{}.zip'.format(GITHUB_URL, hass_const.__version__) PACKAGES = find_packages(exclude=['tests', 'tests.*']) REQUIRES = [ - 'aiohttp==3.2.1', + 'aiohttp==3.3.0', 'astral==1.6.1', 'async_timeout==3.0.0', 'attrs==18.1.0', From bb4d1773d31f3681d7fa1ed14dfa9200b5b1ca3b Mon Sep 17 00:00:00 2001 From: Philip Rosenberg-Watt Date: Thu, 7 Jun 2018 11:50:12 -0600 Subject: [PATCH 030/144] Add min_temp and max_temp to MQTT climate device (#14690) * Add min_temp and max_temp to MQTT climate device * Add unit tests * Remove blank line * Fix unit tests & temp return values * PEP-8 fixes * Remove unused import --- homeassistant/components/climate/mqtt.py | 32 +++++++++++++++++++--- tests/components/climate/test_mqtt.py | 34 +++++++++++++++++++++--- 2 files changed, 59 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/climate/mqtt.py b/homeassistant/components/climate/mqtt.py index 1d98a5733f7..5397daeb784 100644 --- a/homeassistant/components/climate/mqtt.py +++ b/homeassistant/components/climate/mqtt.py @@ -17,7 +17,7 @@ from homeassistant.components.climate import ( PLATFORM_SCHEMA as CLIMATE_PLATFORM_SCHEMA, STATE_AUTO, ATTR_OPERATION_MODE, SUPPORT_TARGET_TEMPERATURE, SUPPORT_OPERATION_MODE, SUPPORT_SWING_MODE, SUPPORT_FAN_MODE, SUPPORT_AWAY_MODE, SUPPORT_HOLD_MODE, - SUPPORT_AUX_HEAT) + SUPPORT_AUX_HEAT, DEFAULT_MIN_TEMP, DEFAULT_MAX_TEMP) from homeassistant.const import ( STATE_ON, STATE_OFF, ATTR_TEMPERATURE, CONF_NAME, CONF_VALUE_TEMPLATE) from homeassistant.components.mqtt import ( @@ -70,6 +70,9 @@ CONF_SWING_MODE_LIST = 'swing_modes' CONF_INITIAL = 'initial' CONF_SEND_IF_OFF = 'send_if_off' +CONF_MIN_TEMP = 'min_temp' +CONF_MAX_TEMP = 'max_temp' + SCHEMA_BASE = CLIMATE_PLATFORM_SCHEMA.extend(MQTT_BASE_PLATFORM_SCHEMA.schema) PLATFORM_SCHEMA = SCHEMA_BASE.extend({ vol.Optional(CONF_POWER_COMMAND_TOPIC): mqtt.valid_publish_topic, @@ -116,6 +119,10 @@ PLATFORM_SCHEMA = SCHEMA_BASE.extend({ vol.Optional(CONF_SEND_IF_OFF, default=True): cv.boolean, vol.Optional(CONF_PAYLOAD_ON, default="ON"): cv.string, vol.Optional(CONF_PAYLOAD_OFF, default="OFF"): cv.string, + + vol.Optional(CONF_MIN_TEMP, default=DEFAULT_MIN_TEMP): vol.Coerce(float), + vol.Optional(CONF_MAX_TEMP, default=DEFAULT_MAX_TEMP): vol.Coerce(float) + }).extend(mqtt.MQTT_AVAILABILITY_SCHEMA.schema) @@ -181,19 +188,22 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None): config.get(CONF_PAYLOAD_OFF), config.get(CONF_AVAILABILITY_TOPIC), config.get(CONF_PAYLOAD_AVAILABLE), - config.get(CONF_PAYLOAD_NOT_AVAILABLE)) + config.get(CONF_PAYLOAD_NOT_AVAILABLE), + config.get(CONF_MIN_TEMP), + config.get(CONF_MAX_TEMP)) ]) class MqttClimate(MqttAvailability, ClimateDevice): - """Representation of a demo climate device.""" + """Representation of an MQTT climate device.""" def __init__(self, hass, name, topic, value_templates, qos, retain, mode_list, fan_mode_list, swing_mode_list, target_temperature, away, hold, current_fan_mode, current_swing_mode, current_operation, aux, send_if_off, payload_on, payload_off, availability_topic, - payload_available, payload_not_available): + payload_available, payload_not_available, + min_temp, max_temp): """Initialize the climate device.""" super().__init__(availability_topic, qos, payload_available, payload_not_available) @@ -219,6 +229,8 @@ class MqttClimate(MqttAvailability, ClimateDevice): self._send_if_off = send_if_off self._payload_on = payload_on self._payload_off = payload_off + self._min_temp = min_temp + self._max_temp = max_temp @asyncio.coroutine def async_added_to_hass(self): @@ -619,3 +631,15 @@ class MqttClimate(MqttAvailability, ClimateDevice): support |= SUPPORT_AUX_HEAT return support + + @property + def min_temp(self): + """Return the minimum temperature.""" + # pylint: disable=no-member + return self._min_temp + + @property + def max_temp(self): + """Return the maximum temperature.""" + # pylint: disable=no-member + return self._max_temp diff --git a/tests/components/climate/test_mqtt.py b/tests/components/climate/test_mqtt.py index 663393503ac..255d482d584 100644 --- a/tests/components/climate/test_mqtt.py +++ b/tests/components/climate/test_mqtt.py @@ -9,9 +9,9 @@ from homeassistant.setup import setup_component from homeassistant.components import climate from homeassistant.const import STATE_OFF, STATE_UNAVAILABLE from homeassistant.components.climate import ( - SUPPORT_OPERATION_MODE, SUPPORT_TARGET_TEMPERATURE, - SUPPORT_FAN_MODE, SUPPORT_SWING_MODE, SUPPORT_HOLD_MODE, - SUPPORT_AWAY_MODE, SUPPORT_AUX_HEAT) + SUPPORT_OPERATION_MODE, SUPPORT_TARGET_TEMPERATURE, + SUPPORT_FAN_MODE, SUPPORT_SWING_MODE, SUPPORT_HOLD_MODE, + SUPPORT_AWAY_MODE, SUPPORT_AUX_HEAT, DEFAULT_MIN_TEMP, DEFAULT_MAX_TEMP) from tests.common import (get_test_home_assistant, mock_mqtt_component, fire_mqtt_message, mock_component) @@ -53,6 +53,8 @@ class TestMQTTClimate(unittest.TestCase): self.assertEqual("low", state.attributes.get('fan_mode')) self.assertEqual("off", state.attributes.get('swing_mode')) self.assertEqual("off", state.attributes.get('operation_mode')) + self.assertEqual(DEFAULT_MIN_TEMP, state.attributes.get('min_temp')) + self.assertEqual(DEFAULT_MAX_TEMP, state.attributes.get('max_temp')) def test_supported_features(self): """Test the supported_features.""" @@ -541,3 +543,29 @@ class TestMQTTClimate(unittest.TestCase): self.hass.block_till_done() state = self.hass.states.get(ENTITY_CLIMATE) self.assertEqual(74656, state.attributes.get('current_temperature')) + + def test_min_temp_custom(self): + """Test a custom min temp.""" + config = copy.deepcopy(DEFAULT_CONFIG) + config['climate']['min_temp'] = 26 + + assert setup_component(self.hass, climate.DOMAIN, config) + + state = self.hass.states.get(ENTITY_CLIMATE) + min_temp = state.attributes.get('min_temp') + + self.assertIsInstance(min_temp, float) + self.assertEqual(26, state.attributes.get('min_temp')) + + def test_max_temp_custom(self): + """Test a custom max temp.""" + config = copy.deepcopy(DEFAULT_CONFIG) + config['climate']['max_temp'] = 60 + + assert setup_component(self.hass, climate.DOMAIN, config) + + state = self.hass.states.get(ENTITY_CLIMATE) + max_temp = state.attributes.get('max_temp') + + self.assertIsInstance(max_temp, float) + self.assertEqual(60, max_temp) From 67d137cfd5ebe397a6d5d1605fa55b36c465c219 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 7 Jun 2018 14:23:09 -0400 Subject: [PATCH 031/144] Store config entry id in entity registry (#14851) * Store config entry id in entity registry * Lint --- homeassistant/helpers/entity_platform.py | 8 +++++++- homeassistant/helpers/entity_registry.py | 7 ++++++- tests/helpers/test_entity_platform.py | 21 ++++++++++++++++----- tests/helpers/test_entity_registry.py | 3 ++- 4 files changed, 31 insertions(+), 8 deletions(-) diff --git a/homeassistant/helpers/entity_platform.py b/homeassistant/helpers/entity_platform.py index 00a7e49840e..ab6c3a084c0 100644 --- a/homeassistant/helpers/entity_platform.py +++ b/homeassistant/helpers/entity_platform.py @@ -260,9 +260,15 @@ class EntityPlatform(object): suggested_object_id = '{} {}'.format( self.entity_namespace, suggested_object_id) + if self.config_entry is not None: + config_entry_id = self.config_entry.entry_id + else: + config_entry_id = None + entry = registry.async_get_or_create( self.domain, self.platform_name, entity.unique_id, - suggested_object_id=suggested_object_id) + suggested_object_id=suggested_object_id, + config_entry_id=config_entry_id) if entry.disabled: self.logger.info( diff --git a/homeassistant/helpers/entity_registry.py b/homeassistant/helpers/entity_registry.py index 35cc1015aaf..4a2cd5fa50c 100644 --- a/homeassistant/helpers/entity_registry.py +++ b/homeassistant/helpers/entity_registry.py @@ -43,6 +43,7 @@ class RegistryEntry: unique_id = attr.ib(type=str) platform = attr.ib(type=str) name = attr.ib(type=str, default=None) + config_entry_id = attr.ib(type=str, default=None) disabled_by = attr.ib( type=str, default=None, validator=attr.validators.in_((DISABLED_HASS, DISABLED_USER, None))) @@ -106,7 +107,7 @@ class EntityRegistry: @callback def async_get_or_create(self, domain, platform, unique_id, *, - suggested_object_id=None): + suggested_object_id=None, config_entry_id=None): """Get entity. Create if it doesn't exist.""" entity_id = self.async_get_entity_id(domain, platform, unique_id) if entity_id: @@ -114,8 +115,10 @@ class EntityRegistry: entity_id = self.async_generate_entity_id( domain, suggested_object_id or '{}_{}'.format(platform, unique_id)) + entity = RegistryEntry( entity_id=entity_id, + config_entry_id=config_entry_id, unique_id=unique_id, platform=platform, ) @@ -179,6 +182,7 @@ class EntityRegistry: for entity_id, info in data.items(): entities[entity_id] = RegistryEntry( entity_id=entity_id, + config_entry_id=info.get('config_entry_id'), unique_id=info['unique_id'], platform=info['platform'], name=info.get('name'), @@ -205,6 +209,7 @@ class EntityRegistry: for entry in self.entities.values(): data[entry.entity_id] = { + 'config_entry_id': entry.config_entry_id, 'unique_id': entry.unique_id, 'platform': entry.platform, 'name': entry.name, diff --git a/tests/helpers/test_entity_platform.py b/tests/helpers/test_entity_platform.py index 4e09f9576f2..9fa178022dc 100644 --- a/tests/helpers/test_entity_platform.py +++ b/tests/helpers/test_entity_platform.py @@ -16,7 +16,7 @@ import homeassistant.util.dt as dt_util from tests.common import ( get_test_home_assistant, MockPlatform, fire_time_changed, mock_registry, - MockEntity, MockEntityPlatform, MockConfigEntry, mock_coro) + MockEntity, MockEntityPlatform, MockConfigEntry) _LOGGER = logging.getLogger(__name__) DOMAIN = "test_domain" @@ -516,11 +516,19 @@ async def test_entity_registry_updates(hass): async def test_setup_entry(hass): """Test we can setup an entry.""" - async_setup_entry = Mock(return_value=mock_coro(True)) + registry = mock_registry(hass) + + async def async_setup_entry(hass, config_entry, async_add_devices): + """Mock setup entry method.""" + async_add_devices([ + MockEntity(name='test1', unique_id='unique') + ]) + return True + platform = MockPlatform( async_setup_entry=async_setup_entry ) - config_entry = MockConfigEntry() + config_entry = MockConfigEntry(entry_id='super-mock-id') entity_platform = MockEntityPlatform( hass, platform_name=config_entry.domain, @@ -528,10 +536,13 @@ async def test_setup_entry(hass): ) assert await entity_platform.async_setup_entry(config_entry) - + await hass.async_block_till_done() full_name = '{}.{}'.format(entity_platform.domain, config_entry.domain) assert full_name in hass.config.components - assert len(async_setup_entry.mock_calls) == 1 + assert len(hass.states.async_entity_ids()) == 1 + assert len(registry.entities) == 1 + assert registry.entities['test_domain.test1'].config_entry_id == \ + 'super-mock-id' async def test_setup_entry_platform_not_ready(hass, caplog): diff --git a/tests/helpers/test_entity_registry.py b/tests/helpers/test_entity_registry.py index 492b97f6387..6808206243f 100644 --- a/tests/helpers/test_entity_registry.py +++ b/tests/helpers/test_entity_registry.py @@ -86,7 +86,8 @@ def test_save_timer_reset_on_subsequent_save(hass, registry): def test_loading_saving_data(hass, registry): """Test that we load/save data correctly.""" orig_entry1 = registry.async_get_or_create('light', 'hue', '1234') - orig_entry2 = registry.async_get_or_create('light', 'hue', '5678') + orig_entry2 = registry.async_get_or_create( + 'light', 'hue', '5678', config_entry_id='mock-id') assert len(registry.entities) == 2 From 50321a29b5505b2260a96c4e92a67fe328af2459 Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Thu, 7 Jun 2018 20:25:26 +0200 Subject: [PATCH 032/144] Catch ConnectionError (fixes #14241) (#14748) --- .../components/media_player/yamaha.py | 81 ++++++++++--------- 1 file changed, 45 insertions(+), 36 deletions(-) diff --git a/homeassistant/components/media_player/yamaha.py b/homeassistant/components/media_player/yamaha.py index bb7942a2545..cf363458067 100644 --- a/homeassistant/components/media_player/yamaha.py +++ b/homeassistant/components/media_player/yamaha.py @@ -6,33 +6,45 @@ https://home-assistant.io/components/media_player.yamaha/ """ import logging +import requests import voluptuous as vol from homeassistant.components.media_player import ( + DOMAIN, MEDIA_PLAYER_SCHEMA, MEDIA_TYPE_MUSIC, PLATFORM_SCHEMA, + SUPPORT_NEXT_TRACK, SUPPORT_PAUSE, SUPPORT_PLAY, SUPPORT_PLAY_MEDIA, + SUPPORT_PREVIOUS_TRACK, SUPPORT_SELECT_SOURCE, SUPPORT_STOP, SUPPORT_TURN_OFF, SUPPORT_TURN_ON, SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET, - SUPPORT_SELECT_SOURCE, SUPPORT_PLAY_MEDIA, SUPPORT_PAUSE, SUPPORT_STOP, - SUPPORT_NEXT_TRACK, SUPPORT_PREVIOUS_TRACK, SUPPORT_PLAY, - MEDIA_TYPE_MUSIC, MEDIA_PLAYER_SCHEMA, DOMAIN, - MediaPlayerDevice, PLATFORM_SCHEMA) -from homeassistant.const import (CONF_NAME, CONF_HOST, STATE_OFF, STATE_ON, - STATE_PLAYING, STATE_IDLE, ATTR_ENTITY_ID) + MediaPlayerDevice) +from homeassistant.const import ( + ATTR_ENTITY_ID, CONF_HOST, CONF_NAME, STATE_IDLE, STATE_OFF, STATE_ON, + STATE_PLAYING) import homeassistant.helpers.config_validation as cv REQUIREMENTS = ['rxv==0.5.1'] _LOGGER = logging.getLogger(__name__) +ATTR_ENABLED = 'enabled' +ATTR_PORT = 'port' + +CONF_SOURCE_IGNORE = 'source_ignore' +CONF_SOURCE_NAMES = 'source_names' +CONF_ZONE_IGNORE = 'zone_ignore' +CONF_ZONE_NAMES = 'zone_names' + +DATA_YAMAHA = 'yamaha_known_receivers' +DEFAULT_NAME = "Yamaha Receiver" + +ENABLE_OUTPUT_SCHEMA = MEDIA_PLAYER_SCHEMA.extend({ + vol.Required(ATTR_ENABLED): cv.boolean, + vol.Required(ATTR_PORT): cv.string, +}) + +SERVICE_ENABLE_OUTPUT = 'yamaha_enable_output' + SUPPORT_YAMAHA = SUPPORT_VOLUME_SET | SUPPORT_VOLUME_MUTE | \ SUPPORT_TURN_ON | SUPPORT_TURN_OFF | SUPPORT_SELECT_SOURCE | SUPPORT_PLAY -CONF_SOURCE_NAMES = 'source_names' -CONF_SOURCE_IGNORE = 'source_ignore' -CONF_ZONE_NAMES = 'zone_names' -CONF_ZONE_IGNORE = 'zone_ignore' - -DEFAULT_NAME = 'Yamaha Receiver' -DATA_YAMAHA = 'yamaha_known_receivers' - PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, vol.Optional(CONF_HOST): cv.string, @@ -44,16 +56,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Optional(CONF_ZONE_NAMES, default={}): {cv.string: cv.string}, }) -SERVICE_ENABLE_OUTPUT = 'yamaha_enable_output' - -ATTR_PORT = 'port' -ATTR_ENABLED = 'enabled' - -ENABLE_OUTPUT_SCHEMA = MEDIA_PLAYER_SCHEMA.extend({ - vol.Required(ATTR_PORT): cv.string, - vol.Required(ATTR_ENABLED): cv.boolean -}) - def setup_platform(hass, config, add_devices, discovery_info=None): """Set up the Yamaha platform.""" @@ -80,7 +82,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): receivers = rxv.RXV( ctrl_url, model_name=model, friendly_name=name, unit_desc_url=desc_url).zone_controllers() - _LOGGER.info("Receivers: %s", receivers) + _LOGGER.debug("Receivers: %s", receivers) # when we are dynamically discovered config is empty zone_ignore = [] elif host is None: @@ -96,15 +98,15 @@ def setup_platform(hass, config, add_devices, discovery_info=None): if receiver.zone in zone_ignore: continue - device = YamahaDevice(name, receiver, source_ignore, - source_names, zone_names) + device = YamahaDevice( + name, receiver, source_ignore, source_names, zone_names) # Only add device if it's not already added if device.zone_id not in hass.data[DATA_YAMAHA]: hass.data[DATA_YAMAHA][device.zone_id] = device devices.append(device) else: - _LOGGER.debug('Ignoring duplicate receiver %s', name) + _LOGGER.debug("Ignoring duplicate receiver: %s", name) def service_handler(service): """Handle for services.""" @@ -130,8 +132,8 @@ def setup_platform(hass, config, add_devices, discovery_info=None): class YamahaDevice(MediaPlayerDevice): """Representation of a Yamaha device.""" - def __init__(self, name, receiver, source_ignore, - source_names, zone_names): + def __init__( + self, name, receiver, source_ignore, source_names, zone_names): """Initialize the Yamaha Receiver.""" self.receiver = receiver self._muted = False @@ -151,7 +153,12 @@ class YamahaDevice(MediaPlayerDevice): def update(self): """Get the latest details from the device.""" - self._play_status = self.receiver.play_status() + try: + self._play_status = self.receiver.play_status() + except requests.exceptions.ConnectionError: + _LOGGER.info("Receiver is offline: %s", self._name) + return + if self.receiver.on: if self._play_status is None: self._pwstate = STATE_ON @@ -231,11 +238,13 @@ class YamahaDevice(MediaPlayerDevice): supported_features = SUPPORT_YAMAHA supports = self._playback_support - mapping = {'play': (SUPPORT_PLAY | SUPPORT_PLAY_MEDIA), - 'pause': SUPPORT_PAUSE, - 'stop': SUPPORT_STOP, - 'skip_f': SUPPORT_NEXT_TRACK, - 'skip_r': SUPPORT_PREVIOUS_TRACK} + mapping = { + 'play': (SUPPORT_PLAY | SUPPORT_PLAY_MEDIA), + 'pause': SUPPORT_PAUSE, + 'stop': SUPPORT_STOP, + 'skip_f': SUPPORT_NEXT_TRACK, + 'skip_r': SUPPORT_PREVIOUS_TRACK, + } for attr, feature in mapping.items(): if getattr(supports, attr, False): supported_features |= feature From 90a51160c4c99a856a127e2872855cf2028979ca Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 7 Jun 2018 15:31:21 -0400 Subject: [PATCH 033/144] Don't run unnecessary methods in executor pool (#14853) * Don't run unnecessary methods in executor pool * Lint * Lint 2 --- homeassistant/components/axis.py | 3 +-- homeassistant/components/binary_sensor/vera.py | 4 ++-- homeassistant/components/binary_sensor/verisure.py | 1 + homeassistant/components/climate/vera.py | 8 ++------ homeassistant/components/cover/vera.py | 4 ++-- homeassistant/components/light/vera.py | 4 ++-- homeassistant/components/lock/vera.py | 4 ++-- homeassistant/components/sensor/vera.py | 4 ++-- homeassistant/components/sensor/verisure.py | 3 +++ homeassistant/components/switch/vera.py | 4 ++-- homeassistant/components/switch/verisure.py | 1 + homeassistant/components/vera.py | 4 +--- homeassistant/helpers/entity.py | 11 ++--------- 13 files changed, 23 insertions(+), 32 deletions(-) diff --git a/homeassistant/components/axis.py b/homeassistant/components/axis.py index fab7d98ed98..9906c61f269 100644 --- a/homeassistant/components/axis.py +++ b/homeassistant/components/axis.py @@ -272,8 +272,7 @@ class AxisDeviceEvent(Entity): def _update_callback(self): """Update the sensor's state, if needed.""" - self.update() - self.schedule_update_ha_state() + self.schedule_update_ha_state(True) @property def name(self): diff --git a/homeassistant/components/binary_sensor/vera.py b/homeassistant/components/binary_sensor/vera.py index e87886376bc..310e2289cbc 100644 --- a/homeassistant/components/binary_sensor/vera.py +++ b/homeassistant/components/binary_sensor/vera.py @@ -19,8 +19,8 @@ _LOGGER = logging.getLogger(__name__) def setup_platform(hass, config, add_devices, discovery_info=None): """Perform the setup for Vera controller devices.""" add_devices( - VeraBinarySensor(device, hass.data[VERA_CONTROLLER]) - for device in hass.data[VERA_DEVICES]['binary_sensor']) + [VeraBinarySensor(device, hass.data[VERA_CONTROLLER]) + for device in hass.data[VERA_DEVICES]['binary_sensor']], True) class VeraBinarySensor(VeraDevice, BinarySensorDevice): diff --git a/homeassistant/components/binary_sensor/verisure.py b/homeassistant/components/binary_sensor/verisure.py index 4a1b99f4b9b..7068d51f6a3 100644 --- a/homeassistant/components/binary_sensor/verisure.py +++ b/homeassistant/components/binary_sensor/verisure.py @@ -54,6 +54,7 @@ class VerisureDoorWindowSensor(BinarySensorDevice): "$.doorWindow.doorWindowDevice[?(@.deviceLabel=='%s')]", self._device_label) is not None + # pylint: disable=no-self-use def update(self): """Update the state of the sensor.""" hub.update_overview() diff --git a/homeassistant/components/climate/vera.py b/homeassistant/components/climate/vera.py index 6fb6bc0ff48..4deb4d9ea2e 100644 --- a/homeassistant/components/climate/vera.py +++ b/homeassistant/components/climate/vera.py @@ -32,8 +32,8 @@ SUPPORT_FLAGS = (SUPPORT_TARGET_TEMPERATURE | SUPPORT_OPERATION_MODE | def setup_platform(hass, config, add_devices_callback, discovery_info=None): """Set up of Vera thermostats.""" add_devices_callback( - VeraThermostat(device, hass.data[VERA_CONTROLLER]) for - device in hass.data[VERA_DEVICES]['climate']) + [VeraThermostat(device, hass.data[VERA_CONTROLLER]) for + device in hass.data[VERA_DEVICES]['climate']], True) class VeraThermostat(VeraDevice, ClimateDevice): @@ -101,10 +101,6 @@ class VeraThermostat(VeraDevice, ClimateDevice): if power: return convert(power, float, 0.0) - def update(self): - """Handle state updates.""" - self._state = self.vera_device.get_hvac_mode() - @property def temperature_unit(self): """Return the unit of measurement.""" diff --git a/homeassistant/components/cover/vera.py b/homeassistant/components/cover/vera.py index ff9ba6f762b..9b2e8f3aad0 100644 --- a/homeassistant/components/cover/vera.py +++ b/homeassistant/components/cover/vera.py @@ -19,8 +19,8 @@ _LOGGER = logging.getLogger(__name__) def setup_platform(hass, config, add_devices, discovery_info=None): """Set up the Vera covers.""" add_devices( - VeraCover(device, hass.data[VERA_CONTROLLER]) for - device in hass.data[VERA_DEVICES]['cover']) + [VeraCover(device, hass.data[VERA_CONTROLLER]) for + device in hass.data[VERA_DEVICES]['cover']], True) class VeraCover(VeraDevice, CoverDevice): diff --git a/homeassistant/components/light/vera.py b/homeassistant/components/light/vera.py index 6b12e69341d..7ace250b6ee 100644 --- a/homeassistant/components/light/vera.py +++ b/homeassistant/components/light/vera.py @@ -22,8 +22,8 @@ DEPENDENCIES = ['vera'] def setup_platform(hass, config, add_devices, discovery_info=None): """Set up the Vera lights.""" add_devices( - VeraLight(device, hass.data[VERA_CONTROLLER]) for - device in hass.data[VERA_DEVICES]['light']) + [VeraLight(device, hass.data[VERA_CONTROLLER]) for + device in hass.data[VERA_DEVICES]['light']], True) class VeraLight(VeraDevice, Light): diff --git a/homeassistant/components/lock/vera.py b/homeassistant/components/lock/vera.py index b3aae5e159f..e6e277cdee1 100644 --- a/homeassistant/components/lock/vera.py +++ b/homeassistant/components/lock/vera.py @@ -19,8 +19,8 @@ DEPENDENCIES = ['vera'] def setup_platform(hass, config, add_devices, discovery_info=None): """Find and return Vera locks.""" add_devices( - VeraLock(device, hass.data[VERA_CONTROLLER]) for - device in hass.data[VERA_DEVICES]['lock']) + [VeraLock(device, hass.data[VERA_CONTROLLER]) for + device in hass.data[VERA_DEVICES]['lock']], True) class VeraLock(VeraDevice, LockDevice): diff --git a/homeassistant/components/sensor/vera.py b/homeassistant/components/sensor/vera.py index eb8ccae768e..4fc92db1d90 100644 --- a/homeassistant/components/sensor/vera.py +++ b/homeassistant/components/sensor/vera.py @@ -25,8 +25,8 @@ SCAN_INTERVAL = timedelta(seconds=5) def setup_platform(hass, config, add_devices, discovery_info=None): """Set up the Vera controller devices.""" add_devices( - VeraSensor(device, hass.data[VERA_CONTROLLER]) - for device in hass.data[VERA_DEVICES]['sensor']) + [VeraSensor(device, hass.data[VERA_CONTROLLER]) + for device in hass.data[VERA_DEVICES]['sensor']], True) class VeraSensor(VeraDevice, Entity): diff --git a/homeassistant/components/sensor/verisure.py b/homeassistant/components/sensor/verisure.py index 5ab999ccabf..187a9bd7935 100644 --- a/homeassistant/components/sensor/verisure.py +++ b/homeassistant/components/sensor/verisure.py @@ -74,6 +74,7 @@ class VerisureThermometer(Entity): """Return the unit of measurement of this entity.""" return TEMP_CELSIUS + # pylint: disable=no-self-use def update(self): """Update the sensor.""" hub.update_overview() @@ -112,6 +113,7 @@ class VerisureHygrometer(Entity): """Return the unit of measurement of this entity.""" return '%' + # pylint: disable=no-self-use def update(self): """Update the sensor.""" hub.update_overview() @@ -150,6 +152,7 @@ class VerisureMouseDetection(Entity): """Return the unit of measurement of this entity.""" return 'Mice' + # pylint: disable=no-self-use def update(self): """Update the sensor.""" hub.update_overview() diff --git a/homeassistant/components/switch/vera.py b/homeassistant/components/switch/vera.py index d7c284e4ccf..82e2756c230 100644 --- a/homeassistant/components/switch/vera.py +++ b/homeassistant/components/switch/vera.py @@ -19,8 +19,8 @@ _LOGGER = logging.getLogger(__name__) def setup_platform(hass, config, add_devices, discovery_info=None): """Set up the Vera switches.""" add_devices( - VeraSwitch(device, hass.data[VERA_CONTROLLER]) for - device in hass.data[VERA_DEVICES]['switch']) + [VeraSwitch(device, hass.data[VERA_CONTROLLER]) for + device in hass.data[VERA_DEVICES]['switch']], True) class VeraSwitch(VeraDevice, SwitchDevice): diff --git a/homeassistant/components/switch/verisure.py b/homeassistant/components/switch/verisure.py index 810946a5058..4b126e5d332 100644 --- a/homeassistant/components/switch/verisure.py +++ b/homeassistant/components/switch/verisure.py @@ -72,6 +72,7 @@ class VerisureSmartplug(SwitchDevice): self._state = False self._change_timestamp = time() + # pylint: disable=no-self-use def update(self): """Get the latest date of the smartplug.""" hub.update_overview() diff --git a/homeassistant/components/vera.py b/homeassistant/components/vera.py index ebe92a2dcc2..2603f61eb75 100644 --- a/homeassistant/components/vera.py +++ b/homeassistant/components/vera.py @@ -148,12 +148,10 @@ class VeraDevice(Entity): slugify(vera_device.name), vera_device.device_id) self.controller.register(vera_device, self._update_callback) - self.update() def _update_callback(self, _device): """Update the state.""" - self.update() - self.schedule_update_ha_state() + self.schedule_update_ha_state(True) @property def name(self): diff --git a/homeassistant/helpers/entity.py b/homeassistant/helpers/entity.py index efaefc26184..85050b5736f 100644 --- a/homeassistant/helpers/entity.py +++ b/homeassistant/helpers/entity.py @@ -171,13 +171,6 @@ class Entity(object): """Flag supported features.""" return None - def update(self): - """Retrieve latest state. - - For asyncio use coroutine async_update. - """ - pass - # DO NOT OVERWRITE # These properties and methods are either managed by Home Assistant or they # are used to perform a very specific function. Overwriting these may @@ -320,10 +313,10 @@ class Entity(object): ) try: + # pylint: disable=no-member if hasattr(self, 'async_update'): - # pylint: disable=no-member yield from self.async_update() - else: + elif hasattr(self, 'update'): yield from self.hass.async_add_job(self.update) finally: self._update_staged = False From 1a7e8c88a3666a469488b08c1c9984684151b9d2 Mon Sep 17 00:00:00 2001 From: Sergiy Maysak Date: Thu, 7 Jun 2018 23:30:20 +0300 Subject: [PATCH 034/144] Wireless tags platform (#13495) * Initial version of wirelesstags platform support. * Pinned wirelesstagpy, generated requirements_all. * Fixed temperature units in imperial units systems, make binary events more tags specific. * Lowercased tag name during entity_id generation. * Fixed: tag_id template for tag_binary_events, support of light sensor for homebridge. * Minor style cleanup. * Removed state, define_name, icon. Reworked async arm/disarm update. Removed static attrs. Introduced available property. Custom events contains components name now. Cleaned dedundant items from schema definition. * Removed comment and beep duration from attributes. Minor cleanup of documentation comment. * Ignoring Wemo switches linked in iOS app. * Reworked passing data from platform to components using signals. --- .coveragerc | 3 + .../components/binary_sensor/wirelesstag.py | 214 +++++++++++++++ .../components/sensor/wirelesstag.py | 176 ++++++++++++ .../components/switch/wirelesstag.py | 118 ++++++++ homeassistant/components/wirelesstag.py | 256 ++++++++++++++++++ requirements_all.txt | 3 + 6 files changed, 770 insertions(+) create mode 100644 homeassistant/components/binary_sensor/wirelesstag.py create mode 100755 homeassistant/components/sensor/wirelesstag.py create mode 100644 homeassistant/components/switch/wirelesstag.py create mode 100644 homeassistant/components/wirelesstag.py diff --git a/.coveragerc b/.coveragerc index e38ceeba5ce..a7d222b33b2 100644 --- a/.coveragerc +++ b/.coveragerc @@ -311,6 +311,9 @@ omit = homeassistant/components/wink/* homeassistant/components/*/wink.py + homeassistant/components/wirelesstag.py + homeassistant/components/*/wirelesstag.py + homeassistant/components/xiaomi_aqara.py homeassistant/components/*/xiaomi_aqara.py diff --git a/homeassistant/components/binary_sensor/wirelesstag.py b/homeassistant/components/binary_sensor/wirelesstag.py new file mode 100644 index 00000000000..bfc2d44fc6e --- /dev/null +++ b/homeassistant/components/binary_sensor/wirelesstag.py @@ -0,0 +1,214 @@ +""" +Binary sensor support for Wireless Sensor Tags. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/binary_sensor.wirelesstag/ +""" +import logging + +import voluptuous as vol + +from homeassistant.core import callback +from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.components.binary_sensor import ( + BinarySensorDevice, PLATFORM_SCHEMA) +from homeassistant.components.wirelesstag import ( + DOMAIN as WIRELESSTAG_DOMAIN, + WIRELESSTAG_TYPE_13BIT, WIRELESSTAG_TYPE_WATER, + WIRELESSTAG_TYPE_ALSPRO, + WIRELESSTAG_TYPE_WEMO_DEVICE, + SIGNAL_BINARY_EVENT_UPDATE, + WirelessTagBaseSensor) +from homeassistant.const import ( + CONF_MONITORED_CONDITIONS, STATE_ON, STATE_OFF) +import homeassistant.helpers.config_validation as cv + +DEPENDENCIES = ['wirelesstag'] + +_LOGGER = logging.getLogger(__name__) + +# On means in range, Off means out of range +SENSOR_PRESENCE = 'presence' + +# On means motion detected, Off means cear +SENSOR_MOTION = 'motion' + +# On means open, Off means closed +SENSOR_DOOR = 'door' + +# On means temperature become too cold, Off means normal +SENSOR_COLD = 'cold' + +# On means hot, Off means normal +SENSOR_HEAT = 'heat' + +# On means too dry (humidity), Off means normal +SENSOR_DRY = 'dry' + +# On means too wet (humidity), Off means normal +SENSOR_WET = 'wet' + +# On means light detected, Off means no light +SENSOR_LIGHT = 'light' + +# On means moisture detected (wet), Off means no moisture (dry) +SENSOR_MOISTURE = 'moisture' + +# On means tag battery is low, Off means normal +SENSOR_BATTERY = 'low_battery' + +# Sensor types: Name, device_class, push notification type representing 'on', +# attr to check +SENSOR_TYPES = { + SENSOR_PRESENCE: ['Presence', 'presence', 'is_in_range', { + "on": "oor", + "off": "back_in_range" + }, 2], + SENSOR_MOTION: ['Motion', 'motion', 'is_moved', { + "on": "motion_detected", + }, 5], + SENSOR_DOOR: ['Door', 'door', 'is_door_open', { + "on": "door_opened", + "off": "door_closed" + }, 5], + SENSOR_COLD: ['Cold', 'cold', 'is_cold', { + "on": "temp_toolow", + "off": "temp_normal" + }, 4], + SENSOR_HEAT: ['Heat', 'heat', 'is_heat', { + "on": "temp_toohigh", + "off": "temp_normal" + }, 4], + SENSOR_DRY: ['Too dry', 'dry', 'is_too_dry', { + "on": "too_dry", + "off": "cap_normal" + }, 2], + SENSOR_WET: ['Too wet', 'wet', 'is_too_humid', { + "on": "too_humid", + "off": "cap_normal" + }, 2], + SENSOR_LIGHT: ['Light', 'light', 'is_light_on', { + "on": "too_bright", + "off": "light_normal" + }, 1], + SENSOR_MOISTURE: ['Leak', 'moisture', 'is_leaking', { + "on": "water_detected", + "off": "water_dried", + }, 1], + SENSOR_BATTERY: ['Low Battery', 'battery', 'is_battery_low', { + "on": "low_battery" + }, 3] +} + + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Required(CONF_MONITORED_CONDITIONS, default=[]): + vol.All(cv.ensure_list, [vol.In(SENSOR_TYPES)]), +}) + + +def setup_platform(hass, config, add_devices, discovery_info=None): + """Set up the platform for a WirelessTags.""" + platform = hass.data.get(WIRELESSTAG_DOMAIN) + + sensors = [] + tags = platform.tags + for tag in tags.values(): + allowed_sensor_types = WirelessTagBinarySensor.allowed_sensors(tag) + for sensor_type in config.get(CONF_MONITORED_CONDITIONS): + if sensor_type in allowed_sensor_types: + sensors.append(WirelessTagBinarySensor(platform, tag, + sensor_type)) + + add_devices(sensors, True) + hass.add_job(platform.install_push_notifications, sensors) + + +class WirelessTagBinarySensor(WirelessTagBaseSensor, BinarySensorDevice): + """A binary sensor implementation for WirelessTags.""" + + @classmethod + def allowed_sensors(cls, tag): + """Return list of allowed sensor types for specific tag type.""" + sensors_map = { + # 13-bit tag - allows everything but not light and moisture + WIRELESSTAG_TYPE_13BIT: [ + SENSOR_PRESENCE, SENSOR_BATTERY, + SENSOR_MOTION, SENSOR_DOOR, + SENSOR_COLD, SENSOR_HEAT, + SENSOR_DRY, SENSOR_WET], + + # Moister/water sensor - temperature and moisture only + WIRELESSTAG_TYPE_WATER: [ + SENSOR_PRESENCE, SENSOR_BATTERY, + SENSOR_COLD, SENSOR_HEAT, + SENSOR_MOISTURE], + + # ALS Pro: allows everything, but not moisture + WIRELESSTAG_TYPE_ALSPRO: [ + SENSOR_PRESENCE, SENSOR_BATTERY, + SENSOR_MOTION, SENSOR_DOOR, + SENSOR_COLD, SENSOR_HEAT, + SENSOR_DRY, SENSOR_WET, + SENSOR_LIGHT], + + # Wemo are power switches. + WIRELESSTAG_TYPE_WEMO_DEVICE: [SENSOR_PRESENCE] + } + + # allow everything if tag type is unknown + # (i just dont have full catalog of them :)) + tag_type = tag.tag_type + fullset = SENSOR_TYPES.keys() + return sensors_map[tag_type] if tag_type in sensors_map else fullset + + def __init__(self, api, tag, sensor_type): + """Initialize a binary sensor for a Wireless Sensor Tags.""" + super().__init__(api, tag) + self._sensor_type = sensor_type + self._name = '{0} {1}'.format(self._tag.name, + SENSOR_TYPES[self._sensor_type][0]) + self._device_class = SENSOR_TYPES[self._sensor_type][1] + self._tag_attr = SENSOR_TYPES[self._sensor_type][2] + self.binary_spec = SENSOR_TYPES[self._sensor_type][3] + self.tag_id_index_template = SENSOR_TYPES[self._sensor_type][4] + + async def async_added_to_hass(self): + """Register callbacks.""" + tag_id = self.tag_id + event_type = self.device_class + async_dispatcher_connect( + self.hass, + SIGNAL_BINARY_EVENT_UPDATE.format(tag_id, event_type), + self._on_binary_event_callback) + + @property + def is_on(self): + """Return True if the binary sensor is on.""" + return self._state == STATE_ON + + @property + def device_class(self): + """Return the class of the binary sensor.""" + return self._device_class + + @property + def principal_value(self): + """Return value of tag. + + Subclasses need override based on type of sensor. + """ + return ( + STATE_ON if getattr(self._tag, self._tag_attr, False) + else STATE_OFF) + + def updated_state_value(self): + """Use raw princial value.""" + return self.principal_value + + @callback + def _on_binary_event_callback(self, event): + """Update state from arrive push notification.""" + # state should be 'on' or 'off' + self._state = event.data.get('state') + self.async_schedule_update_ha_state() diff --git a/homeassistant/components/sensor/wirelesstag.py b/homeassistant/components/sensor/wirelesstag.py new file mode 100755 index 00000000000..c93da3c791f --- /dev/null +++ b/homeassistant/components/sensor/wirelesstag.py @@ -0,0 +1,176 @@ +""" +Sensor support for Wirelss Sensor Tags platform. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/sensor.wirelesstag/ +""" + +import logging +import voluptuous as vol + +from homeassistant.components.sensor import PLATFORM_SCHEMA +from homeassistant.core import callback +from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.const import ( + CONF_MONITORED_CONDITIONS) +from homeassistant.components.wirelesstag import ( + DOMAIN as WIRELESSTAG_DOMAIN, + WIRELESSTAG_TYPE_13BIT, WIRELESSTAG_TYPE_WATER, + WIRELESSTAG_TYPE_ALSPRO, + WIRELESSTAG_TYPE_WEMO_DEVICE, + SIGNAL_TAG_UPDATE, + WirelessTagBaseSensor) +import homeassistant.helpers.config_validation as cv +from homeassistant.const import TEMP_CELSIUS + +DEPENDENCIES = ['wirelesstag'] + +_LOGGER = logging.getLogger(__name__) + +SENSOR_TEMPERATURE = 'temperature' +SENSOR_HUMIDITY = 'humidity' +SENSOR_MOISTURE = 'moisture' +SENSOR_LIGHT = 'light' + +SENSOR_TYPES = { + SENSOR_TEMPERATURE: { + 'unit': TEMP_CELSIUS, + 'attr': 'temperature' + }, + SENSOR_HUMIDITY: { + 'unit': '%', + 'attr': 'humidity' + }, + SENSOR_MOISTURE: { + 'unit': '%', + 'attr': 'moisture' + }, + SENSOR_LIGHT: { + 'unit': 'lux', + 'attr': 'light' + } +} + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Required(CONF_MONITORED_CONDITIONS, default=[]): + vol.All(cv.ensure_list, [vol.In(SENSOR_TYPES)]), +}) + + +def setup_platform(hass, config, add_devices, discovery_info=None): + """Setup the sensor platform.""" + platform = hass.data.get(WIRELESSTAG_DOMAIN) + sensors = [] + tags = platform.tags + for tag in tags.values(): + for sensor_type in config.get(CONF_MONITORED_CONDITIONS): + if sensor_type in WirelessTagSensor.allowed_sensors(tag): + sensors.append(WirelessTagSensor( + platform, tag, sensor_type, hass.config)) + + add_devices(sensors, True) + + +class WirelessTagSensor(WirelessTagBaseSensor): + """Representation of a Sensor.""" + + @classmethod + def allowed_sensors(cls, tag): + """Return array of allowed sensor types for tag.""" + all_sensors = SENSOR_TYPES.keys() + sensors_per_tag_type = { + WIRELESSTAG_TYPE_13BIT: [ + SENSOR_TEMPERATURE, + SENSOR_HUMIDITY], + WIRELESSTAG_TYPE_WATER: [ + SENSOR_TEMPERATURE, + SENSOR_MOISTURE], + WIRELESSTAG_TYPE_ALSPRO: [ + SENSOR_TEMPERATURE, + SENSOR_HUMIDITY, + SENSOR_LIGHT], + WIRELESSTAG_TYPE_WEMO_DEVICE: [] + } + + tag_type = tag.tag_type + return ( + sensors_per_tag_type[tag_type] if tag_type in sensors_per_tag_type + else all_sensors) + + def __init__(self, api, tag, sensor_type, config): + """Constructor with platform(api), tag and hass sensor type.""" + super().__init__(api, tag) + + self._sensor_type = sensor_type + self._tag_attr = SENSOR_TYPES[self._sensor_type]['attr'] + self._unit_of_measurement = SENSOR_TYPES[self._sensor_type]['unit'] + self._name = self._tag.name + + # I want to see entity_id as: + # sensor.wirelesstag_bedroom_temperature + # and not as sensor.bedroom for temperature and + # sensor.bedroom_2 for humidity + self._entity_id = '{}.{}_{}_{}'.format('sensor', WIRELESSTAG_DOMAIN, + self.underscored_name, + self._sensor_type) + + async def async_added_to_hass(self): + """Register callbacks.""" + async_dispatcher_connect( + self.hass, + SIGNAL_TAG_UPDATE.format(self.tag_id), + self._update_tag_info_callback) + + @property + def entity_id(self): + """Overriden version.""" + return self._entity_id + + @property + def underscored_name(self): + """Provide name savvy to be used in entity_id name of self.""" + return self.name.lower().replace(" ", "_") + + @property + def state(self): + """Return the state of the sensor.""" + return self._state + + @property + def device_class(self): + """Return the class of the sensor.""" + return self._sensor_type + + @property + def unit_of_measurement(self): + """Return the unit of measurement.""" + return self._unit_of_measurement + + @property + def principal_value(self): + """Return sensor current value.""" + return getattr(self._tag, self._tag_attr, False) + + @callback + def _update_tag_info_callback(self, event): + """Handle push notification sent by tag manager.""" + if event.data.get('id') != self.tag_id: + return + + _LOGGER.info("Entity to update state: %s event data: %s", + self, event.data) + new_value = self.principal_value + try: + if self._sensor_type == SENSOR_TEMPERATURE: + new_value = event.data.get('temp') + elif (self._sensor_type == SENSOR_HUMIDITY or + self._sensor_type == SENSOR_MOISTURE): + new_value = event.data.get('cap') + elif self._sensor_type == SENSOR_LIGHT: + new_value = event.data.get('lux') + except Exception as error: # pylint: disable=W0703 + _LOGGER.info("Unable to update value of entity: \ + %s error: %s event: %s", self, error, event) + + self._state = self.decorate_value(new_value) + self.async_schedule_update_ha_state() diff --git a/homeassistant/components/switch/wirelesstag.py b/homeassistant/components/switch/wirelesstag.py new file mode 100644 index 00000000000..cce8c349a31 --- /dev/null +++ b/homeassistant/components/switch/wirelesstag.py @@ -0,0 +1,118 @@ +""" +Switch implementation for Wireless Sensor Tags (wirelesstag.net) platform. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/switch.wirelesstag/ +""" +import logging + +import voluptuous as vol + + +from homeassistant.components.wirelesstag import ( + DOMAIN as WIRELESSTAG_DOMAIN, + WIRELESSTAG_TYPE_13BIT, WIRELESSTAG_TYPE_WATER, + WIRELESSTAG_TYPE_ALSPRO, + WIRELESSTAG_TYPE_WEMO_DEVICE, + WirelessTagBaseSensor) +from homeassistant.components.switch import PLATFORM_SCHEMA, SwitchDevice +from homeassistant.const import ( + CONF_MONITORED_CONDITIONS) +import homeassistant.helpers.config_validation as cv + +DEPENDENCIES = ['wirelesstag'] + +_LOGGER = logging.getLogger(__name__) + +ARM_TEMPERATURE = 'temperature' +ARM_HUMIDITY = 'humidity' +ARM_MOTION = 'motion' +ARM_LIGHT = 'light' +ARM_MOISTURE = 'moisture' + +# Switch types: Name, tag sensor type +SWITCH_TYPES = { + ARM_TEMPERATURE: ['Arm Temperature', 'temperature'], + ARM_HUMIDITY: ['Arm Humidity', 'humidity'], + ARM_MOTION: ['Arm Motion', 'motion'], + ARM_LIGHT: ['Arm Light', 'light'], + ARM_MOISTURE: ['Arm Moisture', 'moisture'] +} + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Required(CONF_MONITORED_CONDITIONS, default=[]): + vol.All(cv.ensure_list, [vol.In(SWITCH_TYPES)]), +}) + + +def setup_platform(hass, config, add_devices, discovery_info=None): + """Set up switches for a Wireless Sensor Tags.""" + platform = hass.data.get(WIRELESSTAG_DOMAIN) + + switches = [] + tags = platform.load_tags() + for switch_type in config.get(CONF_MONITORED_CONDITIONS): + for _, tag in tags.items(): + if switch_type in WirelessTagSwitch.allowed_switches(tag): + switches.append(WirelessTagSwitch(platform, tag, switch_type)) + + add_devices(switches, True) + + +class WirelessTagSwitch(WirelessTagBaseSensor, SwitchDevice): + """A switch implementation for Wireless Sensor Tags.""" + + @classmethod + def allowed_switches(cls, tag): + """Return allowed switch types for wireless tag.""" + all_sensors = SWITCH_TYPES.keys() + sensors_per_tag_spec = { + WIRELESSTAG_TYPE_13BIT: [ + ARM_TEMPERATURE, ARM_HUMIDITY, ARM_MOTION], + WIRELESSTAG_TYPE_WATER: [ + ARM_TEMPERATURE, ARM_MOISTURE], + WIRELESSTAG_TYPE_ALSPRO: [ + ARM_TEMPERATURE, ARM_HUMIDITY, ARM_MOTION, ARM_LIGHT], + WIRELESSTAG_TYPE_WEMO_DEVICE: [] + } + + tag_type = tag.tag_type + + result = ( + sensors_per_tag_spec[tag_type] + if tag_type in sensors_per_tag_spec else all_sensors) + _LOGGER.info("Allowed switches: %s tag_type: %s", + str(result), tag_type) + + return result + + def __init__(self, api, tag, switch_type): + """Initialize a switch for Wireless Sensor Tag.""" + super().__init__(api, tag) + self._switch_type = switch_type + self.sensor_type = SWITCH_TYPES[self._switch_type][1] + self._name = '{} {}'.format(self._tag.name, + SWITCH_TYPES[self._switch_type][0]) + + def turn_on(self, **kwargs): + """Turn on the switch.""" + self._api.arm(self) + + def turn_off(self, **kwargs): + """Turn on the switch.""" + self._api.disarm(self) + + @property + def is_on(self) -> bool: + """Return True if entity is on.""" + return self._state + + def updated_state_value(self): + """Provide formatted value.""" + return self.principal_value + + @property + def principal_value(self): + """Provide actual value of switch.""" + attr_name = 'is_{}_sensor_armed'.format(self.sensor_type) + return getattr(self._tag, attr_name, False) diff --git a/homeassistant/components/wirelesstag.py b/homeassistant/components/wirelesstag.py new file mode 100644 index 00000000000..9fabcb1cd5a --- /dev/null +++ b/homeassistant/components/wirelesstag.py @@ -0,0 +1,256 @@ +""" +Wireless Sensor Tags platform support. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/wirelesstag/ +""" +import logging + +from requests.exceptions import HTTPError, ConnectTimeout +import voluptuous as vol +from homeassistant.const import ( + ATTR_BATTERY_LEVEL, ATTR_VOLTAGE, CONF_USERNAME, CONF_PASSWORD) +import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.entity import Entity +from homeassistant.helpers.dispatcher import ( + dispatcher_send) + +REQUIREMENTS = ['wirelesstagpy==0.3.0'] + +_LOGGER = logging.getLogger(__name__) + + +# straight of signal in dBm +ATTR_TAG_SIGNAL_STRAIGHT = 'signal_straight' +# indicates if tag is out of range or not +ATTR_TAG_OUT_OF_RANGE = 'out_of_range' +# number in percents from max power of tag receiver +ATTR_TAG_POWER_CONSUMPTION = 'power_consumption' + + +NOTIFICATION_ID = 'wirelesstag_notification' +NOTIFICATION_TITLE = "Wireless Sensor Tag Setup" + +DOMAIN = 'wirelesstag' +DEFAULT_ENTITY_NAMESPACE = 'wirelesstag' + +WIRELESSTAG_TYPE_13BIT = 13 +WIRELESSTAG_TYPE_ALSPRO = 26 +WIRELESSTAG_TYPE_WATER = 32 +WIRELESSTAG_TYPE_WEMO_DEVICE = 82 + +SIGNAL_TAG_UPDATE = 'wirelesstag.tag_info_updated_{}' +SIGNAL_BINARY_EVENT_UPDATE = 'wirelesstag.binary_event_updated_{}_{}' + +CONFIG_SCHEMA = vol.Schema({ + DOMAIN: vol.Schema({ + vol.Required(CONF_USERNAME): cv.string, + vol.Required(CONF_PASSWORD): cv.string, + }), +}, extra=vol.ALLOW_EXTRA) + + +class WirelessTagPlatform: + """Principal object to manage all registered in HA tags.""" + + def __init__(self, hass, api): + """Designated initializer for wirelesstags platform.""" + self.hass = hass + self.api = api + self.tags = {} + + def load_tags(self): + """Load tags from remote server.""" + self.tags = self.api.load_tags() + return self.tags + + def arm(self, switch): + """Arm entity sensor monitoring.""" + func_name = 'arm_{}'.format(switch.sensor_type) + arm_func = getattr(self.api, func_name) + if arm_func is not None: + arm_func(switch.tag_id) + + def disarm(self, switch): + """Disarm entity sensor monitoring.""" + func_name = 'disarm_{}'.format(switch.sensor_type) + disarm_func = getattr(self.api, func_name) + if disarm_func is not None: + disarm_func(switch.tag_id) + + # pylint: disable=no-self-use + def make_push_notitication(self, name, url, content): + """Factory for notification config.""" + from wirelesstagpy import NotificationConfig + return NotificationConfig(name, { + 'url': url, 'verb': 'POST', + 'content': content, 'disabled': False, 'nat': True}) + + def install_push_notifications(self, binary_sensors): + """Setup local push notification from tag manager.""" + _LOGGER.info("Registering local push notifications.") + configs = [] + + binary_url = self.binary_event_callback_url + for event in binary_sensors: + for state, name in event.binary_spec.items(): + content = ('{"type": "' + event.device_class + + '", "id":{' + str(event.tag_id_index_template) + + '}, "state": \"' + state + '\"}') + config = self.make_push_notitication(name, binary_url, content) + configs.append(config) + + content = ("{\"name\":\"{0}\",\"id\":{1},\"temp\":{2}," + + "\"cap\":{3},\"lux\":{4}}") + update_url = self.update_callback_url + update_config = self.make_push_notitication( + 'update', update_url, content) + configs.append(update_config) + + result = self.api.install_push_notification(0, configs, True) + if not result: + self.hass.components.persistent_notification.create( + "Error: failed to install local push notifications
", + title="Wireless Sensor Tag Setup Local Push Notifications", + notification_id="wirelesstag_failed_push_notification") + else: + _LOGGER.info("Installed push notifications for all tags.") + + @property + def update_callback_url(self): + """Return url for local push notifications(update event).""" + return '{}/api/events/wirelesstag_update_tags'.format( + self.hass.config.api.base_url) + + @property + def binary_event_callback_url(self): + """Return url for local push notifications(binary event).""" + return '{}/api/events/wirelesstag_binary_event'.format( + self.hass.config.api.base_url) + + def handle_update_tags_event(self, event): + """Main entry to handle push event from wireless tag manager.""" + _LOGGER.info("push notification for update arrived: %s", event) + dispatcher_send( + self.hass, + SIGNAL_TAG_UPDATE.format(event.data.get('id')), + event) + + def handle_binary_event(self, event): + """Handle push notifications for binary (on/off) events.""" + _LOGGER.info("Push notification for binary event arrived: %s", event) + try: + tag_id = event.data.get('id') + event_type = event.data.get('type') + dispatcher_send( + self.hass, + SIGNAL_BINARY_EVENT_UPDATE.format(tag_id, event_type), + event) + except Exception as ex: # pylint: disable=W0703 + _LOGGER.error("Unable to handle binary event:\ + %s error: %s", str(event), str(ex)) + + +def setup(hass, config): + """Set up the Wireless Sensor Tag component.""" + conf = config[DOMAIN] + username = conf.get(CONF_USERNAME) + password = conf.get(CONF_PASSWORD) + + try: + from wirelesstagpy import (WirelessTags, WirelessTagsException) + wirelesstags = WirelessTags(username=username, password=password) + + platform = WirelessTagPlatform(hass, wirelesstags) + platform.load_tags() + hass.data[DOMAIN] = platform + except (ConnectTimeout, HTTPError, WirelessTagsException) as ex: + _LOGGER.error("Unable to connect to wirelesstag.net service: %s", + str(ex)) + hass.components.persistent_notification.create( + "Error: {}
" + "Please restart hass after fixing this." + "".format(ex), + title=NOTIFICATION_TITLE, + notification_id=NOTIFICATION_ID) + return False + + # listen to custom events + hass.bus.listen('wirelesstag_update_tags', + hass.data[DOMAIN].handle_update_tags_event) + hass.bus.listen('wirelesstag_binary_event', + hass.data[DOMAIN].handle_binary_event) + + return True + + +class WirelessTagBaseSensor(Entity): + """Base class for HA implementation for Wireless Sensor Tag.""" + + def __init__(self, api, tag): + """Initialize a base sensor for Wireless Sensor Tag platform.""" + self._api = api + self._tag = tag + self._uuid = self._tag.uuid + self.tag_id = self._tag.tag_id + self._name = self._tag.name + self._state = None + + @property + def should_poll(self): + """Return the polling state.""" + return True + + @property + def name(self): + """Return the name of the sensor.""" + return self._name + + @property + def principal_value(self): + """Return base value. + + Subclasses need override based on type of sensor. + """ + return 0 + + def updated_state_value(self): + """Default implementation formats princial value.""" + return self.decorate_value(self.principal_value) + + # pylint: disable=no-self-use + def decorate_value(self, value): + """Decorate input value to be well presented for end user.""" + return '{:.1f}'.format(value) + + @property + def available(self): + """Return True if entity is available.""" + return self._tag.is_alive + + def update(self): + """Update state.""" + if not self.should_poll: + return + + updated_tags = self._api.load_tags() + updated_tag = updated_tags[self._uuid] + if updated_tag is None: + _LOGGER.error('Unable to update tag: "%s"', self.name) + return + + self._tag = updated_tag + self._state = self.updated_state_value() + + @property + def device_state_attributes(self): + """Return the state attributes.""" + return { + ATTR_BATTERY_LEVEL: self._tag.battery_remaining, + ATTR_VOLTAGE: '{:.2f}V'.format(self._tag.battery_volts), + ATTR_TAG_SIGNAL_STRAIGHT: '{}dBm'.format( + self._tag.signal_straight), + ATTR_TAG_OUT_OF_RANGE: not self._tag.is_in_range, + ATTR_TAG_POWER_CONSUMPTION: '{:.2f}%'.format( + self._tag.power_consumption) + } diff --git a/requirements_all.txt b/requirements_all.txt index d2b52d17961..6bbe2a0b79e 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1378,6 +1378,9 @@ websocket-client==0.37.0 # homeassistant.components.media_player.webostv websockets==3.2 +# homeassistant.components.wirelesstag +wirelesstagpy==0.3.0 + # homeassistant.components.zigbee xbee-helper==0.0.7 From d4cc806cd5cc408664341cbc15774a7c613e415b Mon Sep 17 00:00:00 2001 From: Sebastian Muszynski Date: Thu, 7 Jun 2018 22:55:18 +0200 Subject: [PATCH 035/144] Fix door/window sensor support of the Xiaomi Aqara LAN protocol V2 (Closes: #14775) (#14777) --- homeassistant/components/binary_sensor/xiaomi_aqara.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/binary_sensor/xiaomi_aqara.py b/homeassistant/components/binary_sensor/xiaomi_aqara.py index 3f8fc3dbb36..be5d9a689d1 100644 --- a/homeassistant/components/binary_sensor/xiaomi_aqara.py +++ b/homeassistant/components/binary_sensor/xiaomi_aqara.py @@ -28,7 +28,11 @@ def setup_platform(hass, config, add_devices, discovery_info=None): if model in ['motion', 'sensor_motion', 'sensor_motion.aq2']: devices.append(XiaomiMotionSensor(device, hass, gateway)) elif model in ['magnet', 'sensor_magnet', 'sensor_magnet.aq2']: - devices.append(XiaomiDoorSensor(device, gateway)) + if 'proto' not in device or int(device['proto'][0:1]) == 1: + data_key = 'status' + else: + data_key = 'window_status' + devices.append(XiaomiDoorSensor(device, data_key, gateway)) elif model == 'sensor_wleak.aq1': devices.append(XiaomiWaterLeakSensor(device, gateway)) elif model in ['smoke', 'sensor_smoke']: @@ -190,11 +194,11 @@ class XiaomiMotionSensor(XiaomiBinarySensor): class XiaomiDoorSensor(XiaomiBinarySensor): """Representation of a XiaomiDoorSensor.""" - def __init__(self, device, xiaomi_hub): + def __init__(self, device, data_key, xiaomi_hub): """Initialize the XiaomiDoorSensor.""" self._open_since = 0 XiaomiBinarySensor.__init__(self, device, 'Door Window Sensor', - xiaomi_hub, 'status', 'opening') + xiaomi_hub, data_key, 'opening') @property def device_state_attributes(self): From 87d55834be230170efca4f08a9a023191cd7ed60 Mon Sep 17 00:00:00 2001 From: Alexei Chetroi Date: Thu, 7 Jun 2018 16:56:07 -0400 Subject: [PATCH 036/144] zha: handle "step_with_on_off" cluster command in LevelListener. (#14756) --- homeassistant/components/binary_sensor/zha.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/binary_sensor/zha.py b/homeassistant/components/binary_sensor/zha.py index 6931355ca0e..224d694e0f5 100644 --- a/homeassistant/components/binary_sensor/zha.py +++ b/homeassistant/components/binary_sensor/zha.py @@ -187,8 +187,8 @@ class Switch(zha.Entity, BinarySensorDevice): if args[0] == 0xff: rate = 10 # Should read default move rate self._entity.move_level(-rate if args[0] else rate) - elif command_id == 0x0002: # step - # Step (technically shouldn't change on/off) + elif command_id in (0x0002, 0x0006): # step, -with_on_off + # Step (technically may change on/off) self._entity.move_level(-args[1] if args[0] else args[1]) def attribute_update(self, attrid, value): From 10317a0f71ffde47c4005ce8830b76e6bed4c989 Mon Sep 17 00:00:00 2001 From: Steve Edson Date: Thu, 7 Jun 2018 21:57:07 +0100 Subject: [PATCH 037/144] Rename Hive hub friendly name (#14747) Right now, "Hub Status" is very generic, I didn't know what component it was coming from, and the only way to tell was searching the source code to find the reference. --- homeassistant/components/sensor/hive.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/sensor/hive.py b/homeassistant/components/sensor/hive.py index 82816c83404..8c9409ef5ff 100644 --- a/homeassistant/components/sensor/hive.py +++ b/homeassistant/components/sensor/hive.py @@ -10,7 +10,7 @@ from homeassistant.helpers.entity import Entity DEPENDENCIES = ['hive'] -FRIENDLY_NAMES = {'Hub_OnlineStatus': 'Hub Status', +FRIENDLY_NAMES = {'Hub_OnlineStatus': 'Hive Hub Status', 'Hive_OutsideTemperature': 'Outside Temperature'} DEVICETYPE_ICONS = {'Hub_OnlineStatus': 'mdi:switch', 'Hive_OutsideTemperature': 'mdi:thermometer'} From fe018fd58c4037cdeb87bcce939edd5891680135 Mon Sep 17 00:00:00 2001 From: Dale Higgs Date: Thu, 7 Jun 2018 16:03:04 -0500 Subject: [PATCH 038/144] Add set_default_level to logger (#14703) * Add set_default_service to logger * Fix 2-line lint error * Add set_default_level to services.yaml * Add tests for set_default_level * Remove function and add else when setting default --- homeassistant/components/logger.py | 28 +++++++++++++++++++------- homeassistant/components/services.yaml | 6 ++++++ tests/components/test_logger.py | 27 +++++++++++++++++++++++++ 3 files changed, 54 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/logger.py b/homeassistant/components/logger.py index 6e8995a0444..daaffd0174c 100644 --- a/homeassistant/components/logger.py +++ b/homeassistant/components/logger.py @@ -15,6 +15,7 @@ DOMAIN = 'logger' DATA_LOGGER = 'logger' +SERVICE_SET_DEFAULT_LEVEL = 'set_default_level' SERVICE_SET_LEVEL = 'set_level' LOGSEVERITY = { @@ -31,8 +32,11 @@ LOGSEVERITY = { LOGGER_DEFAULT = 'default' LOGGER_LOGS = 'logs' +ATTR_LEVEL = 'level' + _VALID_LOG_LEVEL = vol.All(vol.Upper, vol.In(LOGSEVERITY)) +SERVICE_SET_DEFAULT_LEVEL_SCHEMA = vol.Schema({ATTR_LEVEL: _VALID_LOG_LEVEL}) SERVICE_SET_LEVEL_SCHEMA = vol.Schema({cv.string: _VALID_LOG_LEVEL}) CONFIG_SCHEMA = vol.Schema({ @@ -76,12 +80,9 @@ async def async_setup(hass, config): """Set up the logger component.""" logfilter = {} - # Set default log severity - logfilter[LOGGER_DEFAULT] = LOGSEVERITY['DEBUG'] - if LOGGER_DEFAULT in config.get(DOMAIN): - logfilter[LOGGER_DEFAULT] = LOGSEVERITY[ - config.get(DOMAIN)[LOGGER_DEFAULT] - ] + def set_default_log_level(level): + """Set the default log level for components.""" + logfilter[LOGGER_DEFAULT] = LOGSEVERITY[level] def set_log_levels(logpoints): """Set the specified log levels.""" @@ -103,6 +104,12 @@ async def async_setup(hass, config): ) ) + # Set default log severity + if LOGGER_DEFAULT in config.get(DOMAIN): + set_default_log_level(config.get(DOMAIN)[LOGGER_DEFAULT]) + else: + set_default_log_level('DEBUG') + logger = logging.getLogger('') logger.setLevel(logging.NOTSET) @@ -116,7 +123,14 @@ async def async_setup(hass, config): async def async_service_handler(service): """Handle logger services.""" - set_log_levels(service.data) + if service.service == SERVICE_SET_DEFAULT_LEVEL: + set_default_log_level(service.data.get(ATTR_LEVEL)) + else: + set_log_levels(service.data) + + hass.services.async_register( + DOMAIN, SERVICE_SET_DEFAULT_LEVEL, async_service_handler, + schema=SERVICE_SET_DEFAULT_LEVEL_SCHEMA) hass.services.async_register( DOMAIN, SERVICE_SET_LEVEL, async_service_handler, diff --git a/homeassistant/components/services.yaml b/homeassistant/components/services.yaml index c0279ef1d0f..19bf19a799a 100644 --- a/homeassistant/components/services.yaml +++ b/homeassistant/components/services.yaml @@ -175,6 +175,12 @@ ffmpeg: example: 'binary_sensor.ffmpeg_noise' logger: + set_default_level: + description: Set the default log level for components. + fields: + level: + description: Default severity level. Possible values are notset, debug, info, warn, warning, error, fatal, critical + example: 'debug' set_level: description: Set log level for components. diff --git a/tests/components/test_logger.py b/tests/components/test_logger.py index 61cb42e8bb5..a55a66c6505 100644 --- a/tests/components/test_logger.py +++ b/tests/components/test_logger.py @@ -10,6 +10,7 @@ from tests.common import get_test_home_assistant RECORD = namedtuple('record', ('name', 'levelno')) +NO_DEFAULT_CONFIG = {'logger': {}} NO_LOGS_CONFIG = {'logger': {'default': 'info'}} TEST_CONFIG = { 'logger': { @@ -99,3 +100,29 @@ class TestUpdater(unittest.TestCase): self.assert_logged('asdf', logging.DEBUG) self.assert_logged('dummy', logging.WARNING) + + def test_set_default_filter_empty_config(self): + """Test change default log level from empty configuration.""" + self.setup_logger(NO_DEFAULT_CONFIG) + + self.assert_logged('test', logging.DEBUG) + + self.hass.services.call( + logger.DOMAIN, 'set_default_level', {'level': 'warning'}) + self.hass.block_till_done() + + self.assert_not_logged('test', logging.DEBUG) + + def test_set_default_filter(self): + """Test change default log level with existing default.""" + self.setup_logger(TEST_CONFIG) + + self.assert_not_logged('asdf', logging.DEBUG) + self.assert_logged('dummy', logging.WARNING) + + self.hass.services.call( + logger.DOMAIN, 'set_default_level', {'level': 'debug'}) + self.hass.block_till_done() + + self.assert_logged('asdf', logging.DEBUG) + self.assert_logged('dummy', logging.WARNING) From 0748466ffcb86b85847eca3519445884f1ec4724 Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Thu, 7 Jun 2018 23:06:13 +0200 Subject: [PATCH 039/144] Zone - Hass configuration name is optional (#14449) * Hass configuration name is optional * Check explicitly if name is none * Reverted back to old logic for zones configured in configuration.yaml, where many zones can have the same name * New test to verify use case of allowing multiple zones having the same name * Fix too long line --- homeassistant/components/zone/__init__.py | 18 ++++++++---------- tests/components/zone/test_init.py | 19 +++++++++++-------- 2 files changed, 19 insertions(+), 18 deletions(-) diff --git a/homeassistant/components/zone/__init__.py b/homeassistant/components/zone/__init__.py index d3628fd57f3..c33a16c632e 100644 --- a/homeassistant/components/zone/__init__.py +++ b/homeassistant/components/zone/__init__.py @@ -45,27 +45,25 @@ PLATFORM_SCHEMA = vol.Schema({ async def async_setup(hass, config): """Setup configured zones as well as home assistant zone if necessary.""" - if DOMAIN not in hass.data: - hass.data[DOMAIN] = {} + hass.data[DOMAIN] = {} + entities = set() zone_entries = configured_zones(hass) for _, entry in config_per_platform(config, DOMAIN): - name = slugify(entry[CONF_NAME]) - if name not in zone_entries: + if slugify(entry[CONF_NAME]) not in zone_entries: zone = Zone(hass, entry[CONF_NAME], entry[CONF_LATITUDE], entry[CONF_LONGITUDE], entry.get(CONF_RADIUS), entry.get(CONF_ICON), entry.get(CONF_PASSIVE)) zone.entity_id = async_generate_entity_id( - ENTITY_ID_FORMAT, entry[CONF_NAME], None, hass) + ENTITY_ID_FORMAT, entry[CONF_NAME], entities) hass.async_add_job(zone.async_update_ha_state()) - hass.data[DOMAIN][name] = zone + entities.add(zone.entity_id) - if HOME_ZONE not in hass.data[DOMAIN] and HOME_ZONE not in zone_entries: - name = hass.config.location_name - zone = Zone(hass, name, hass.config.latitude, hass.config.longitude, + if ENTITY_ID_HOME not in entities and HOME_ZONE not in zone_entries: + zone = Zone(hass, hass.config.location_name, + hass.config.latitude, hass.config.longitude, DEFAULT_RADIUS, ICON_HOME, False) zone.entity_id = ENTITY_ID_HOME hass.async_add_job(zone.async_update_ha_state()) - hass.data[DOMAIN][slugify(name)] = zone return True diff --git a/tests/components/zone/test_init.py b/tests/components/zone/test_init.py index 1c698438f2c..c26b3375f3a 100644 --- a/tests/components/zone/test_init.py +++ b/tests/components/zone/test_init.py @@ -59,7 +59,6 @@ class TestComponentZone(unittest.TestCase): assert self.hass.config.latitude == state.attributes['latitude'] assert self.hass.config.longitude == state.attributes['longitude'] assert not state.attributes.get('passive', False) - assert 'test_home' in self.hass.data[zone.DOMAIN] def test_setup(self): """Test a successful setup.""" @@ -79,8 +78,6 @@ class TestComponentZone(unittest.TestCase): assert info['longitude'] == state.attributes['longitude'] assert info['radius'] == state.attributes['radius'] assert info['passive'] == state.attributes['passive'] - assert 'test_zone' in self.hass.data[zone.DOMAIN] - assert 'test_home' in self.hass.data[zone.DOMAIN] def test_setup_zone_skips_home_zone(self): """Test that zone named Home should override hass home zone.""" @@ -94,8 +91,17 @@ class TestComponentZone(unittest.TestCase): assert len(self.hass.states.entity_ids('zone')) == 1 state = self.hass.states.get('zone.home') assert info['name'] == state.name - assert 'home' in self.hass.data[zone.DOMAIN] - assert 'test_home' not in self.hass.data[zone.DOMAIN] + + def test_setup_name_can_be_same_on_multiple_zones(self): + """Test that zone named Home should override hass home zone.""" + info = { + 'name': 'Test Zone', + 'latitude': 1.1, + 'longitude': -2.2, + } + assert setup.setup_component( + self.hass, zone.DOMAIN, {'zone': [info, info]}) + assert len(self.hass.states.entity_ids('zone')) == 3 def test_setup_registered_zone_skips_home_zone(self): """Test that config entry named home should override hass home zone.""" @@ -105,7 +111,6 @@ class TestComponentZone(unittest.TestCase): entry.add_to_hass(self.hass) assert setup.setup_component(self.hass, zone.DOMAIN, {'zone': None}) assert len(self.hass.states.entity_ids('zone')) == 0 - assert not self.hass.data[zone.DOMAIN] def test_setup_registered_zone_skips_configured_zone(self): """Test if config entry will override configured zone.""" @@ -123,8 +128,6 @@ class TestComponentZone(unittest.TestCase): assert len(self.hass.states.entity_ids('zone')) == 1 state = self.hass.states.get('zone.test_zone') assert not state - assert 'test_zone' not in self.hass.data[zone.DOMAIN] - assert 'test_home' in self.hass.data[zone.DOMAIN] def test_active_zone_skips_passive_zones(self): """Test active and passive zones.""" From bb0068908dedec73a0b5e49ca290e4039b45bb2a Mon Sep 17 00:00:00 2001 From: Diogo Gomes Date: Fri, 8 Jun 2018 03:44:07 +0100 Subject: [PATCH 040/144] Fix unit conversion (#14730) * reviewing this code min_temp and max_temp are always present and always in celsius * revert change (preserve unit conversion) * revert (due to unit conversion) * self * clean * cleaner --- .../components/climate/generic_thermostat.py | 9 +++++---- homeassistant/components/climate/sensibo.py | 6 +++--- homeassistant/components/climate/tado.py | 16 ++++++---------- 3 files changed, 14 insertions(+), 17 deletions(-) diff --git a/homeassistant/components/climate/generic_thermostat.py b/homeassistant/components/climate/generic_thermostat.py index 6b7f6cb2afc..030a76626c6 100644 --- a/homeassistant/components/climate/generic_thermostat.py +++ b/homeassistant/components/climate/generic_thermostat.py @@ -14,8 +14,7 @@ from homeassistant.core import DOMAIN as HA_DOMAIN from homeassistant.components.climate import ( STATE_HEAT, STATE_COOL, STATE_IDLE, STATE_AUTO, ClimateDevice, ATTR_OPERATION_MODE, ATTR_AWAY_MODE, SUPPORT_OPERATION_MODE, - SUPPORT_AWAY_MODE, SUPPORT_TARGET_TEMPERATURE, PLATFORM_SCHEMA, - DEFAULT_MIN_TEMP, DEFAULT_MAX_TEMP) + SUPPORT_AWAY_MODE, SUPPORT_TARGET_TEMPERATURE, PLATFORM_SCHEMA) from homeassistant.const import ( ATTR_UNIT_OF_MEASUREMENT, STATE_ON, STATE_OFF, ATTR_TEMPERATURE, CONF_NAME, ATTR_ENTITY_ID, SERVICE_TURN_ON, SERVICE_TURN_OFF, @@ -268,7 +267,8 @@ class GenericThermostat(ClimateDevice): if self._min_temp: return self._min_temp - return DEFAULT_MIN_TEMP + # get default temp from super class + return super().min_temp @property def max_temp(self): @@ -277,7 +277,8 @@ class GenericThermostat(ClimateDevice): if self._max_temp: return self._max_temp - return DEFAULT_MAX_TEMP + # Get default temp from super class + return super().max_temp @asyncio.coroutine def _async_sensor_changed(self, entity_id, old_state, new_state): diff --git a/homeassistant/components/climate/sensibo.py b/homeassistant/components/climate/sensibo.py index b3fff0dd796..363653608e8 100644 --- a/homeassistant/components/climate/sensibo.py +++ b/homeassistant/components/climate/sensibo.py @@ -19,7 +19,7 @@ from homeassistant.components.climate import ( ATTR_CURRENT_HUMIDITY, ClimateDevice, DOMAIN, PLATFORM_SCHEMA, SUPPORT_TARGET_TEMPERATURE, SUPPORT_OPERATION_MODE, SUPPORT_FAN_MODE, SUPPORT_SWING_MODE, - SUPPORT_ON_OFF, DEFAULT_MIN_TEMP, DEFAULT_MAX_TEMP) + SUPPORT_ON_OFF) from homeassistant.exceptions import PlatformNotReady from homeassistant.helpers import config_validation as cv from homeassistant.helpers.aiohttp_client import async_get_clientsession @@ -246,13 +246,13 @@ class SensiboClimate(ClimateDevice): def min_temp(self): """Return the minimum temperature.""" return self._temperatures_list[0] \ - if self._temperatures_list else DEFAULT_MIN_TEMP + if self._temperatures_list else super().min_temp @property def max_temp(self): """Return the maximum temperature.""" return self._temperatures_list[-1] \ - if self._temperatures_list else DEFAULT_MAX_TEMP + if self._temperatures_list else super().max_temp @property def unique_id(self): diff --git a/homeassistant/components/climate/tado.py b/homeassistant/components/climate/tado.py index 59da425553a..b3734e020e0 100644 --- a/homeassistant/components/climate/tado.py +++ b/homeassistant/components/climate/tado.py @@ -8,8 +8,8 @@ import logging from homeassistant.const import (PRECISION_TENTHS, TEMP_CELSIUS) from homeassistant.components.climate import ( - ClimateDevice, SUPPORT_TARGET_TEMPERATURE, SUPPORT_OPERATION_MODE, - DEFAULT_MIN_TEMP, DEFAULT_MAX_TEMP) + ClimateDevice, SUPPORT_TARGET_TEMPERATURE, SUPPORT_OPERATION_MODE) +from homeassistant.util.temperature import convert as convert_temperature from homeassistant.const import ATTR_TEMPERATURE from homeassistant.components.tado import DATA_TADO @@ -231,18 +231,14 @@ class TadoClimate(ClimateDevice): @property def min_temp(self): """Return the minimum temperature.""" - if self._min_temp: - return self._min_temp - - return DEFAULT_MIN_TEMP + return convert_temperature(self._min_temp, self._unit, + self.hass.config.units.temperature_unit) @property def max_temp(self): """Return the maximum temperature.""" - if self._max_temp: - return self._max_temp - - return DEFAULT_MAX_TEMP + return convert_temperature(self._max_temp, self._unit, + self.hass.config.units.temperature_unit) def update(self): """Update the state of this climate device.""" From e3fba7912623e3ac9e45b76642fc728d76cf77ba Mon Sep 17 00:00:00 2001 From: Mal Curtis Date: Fri, 8 Jun 2018 17:45:21 +1200 Subject: [PATCH 041/144] Disable volume control for Onkyo when unavailable (Closes: #14774) (#14863) --- .../components/media_player/onkyo.py | 26 +++++++++++++++++-- 1 file changed, 24 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/media_player/onkyo.py b/homeassistant/components/media_player/onkyo.py index 71b74868544..92443ca2b42 100644 --- a/homeassistant/components/media_player/onkyo.py +++ b/homeassistant/components/media_player/onkyo.py @@ -32,6 +32,9 @@ SUPPORT_ONKYO = SUPPORT_VOLUME_SET | SUPPORT_VOLUME_MUTE | \ SUPPORT_VOLUME_STEP | SUPPORT_TURN_ON | SUPPORT_TURN_OFF | \ SUPPORT_SELECT_SOURCE | SUPPORT_PLAY +SUPPORT_ONKYO_WO_VOLUME = SUPPORT_TURN_ON | SUPPORT_TURN_OFF | \ + SUPPORT_SELECT_SOURCE | SUPPORT_PLAY + KNOWN_HOSTS = [] # type: List[str] DEFAULT_SOURCES = {'tv': 'TV', 'bd': 'Bluray', 'game': 'Game', 'aux1': 'Aux1', 'video1': 'Video 1', 'video2': 'Video 2', @@ -270,7 +273,8 @@ class OnkyoDeviceZone(OnkyoDevice): def __init__(self, zone, receiver, sources, name=None): """Initialize the Zone with the zone identifier.""" self._zone = zone - super().__init__(receiver, sources, name) + self._supports_volume = True + super(OnkyoDeviceZone, self).__init__(receiver, sources, name) def update(self): """Get the latest state from the device.""" @@ -289,9 +293,18 @@ class OnkyoDeviceZone(OnkyoDevice): current_source_raw = self.command( 'zone{}.selector=query'.format(self._zone)) + # If we received a source value, but not a volume value + # it's likely this zone permanently does not support volume. + if current_source_raw and not volume_raw: + self._supports_volume = False + if not (volume_raw and mute_raw and current_source_raw): return + # It's possible for some players to have zones set to HDMI with + # no sound control. In this case, the string `N/A` is returned. + self._supports_volume = isinstance(volume_raw[1], (float, int)) + # eiscp can return string or tuple. Make everything tuples. if isinstance(current_source_raw[1], str): current_source_tuples = \ @@ -307,7 +320,16 @@ class OnkyoDeviceZone(OnkyoDevice): self._current_source = '_'.join( [i for i in current_source_tuples[1]]) self._muted = bool(mute_raw[1] == 'on') - self._volume = volume_raw[1] / 80.0 + + if self._supports_volume: + self._volume = volume_raw[1] / 80.0 + + @property + def supported_features(self): + """Return media player features that are supported.""" + if self._supports_volume: + return SUPPORT_ONKYO + return SUPPORT_ONKYO_WO_VOLUME def turn_off(self): """Turn the media player off.""" From b657cff6ba9c8394f081dbed5f5b2d8c6888ab75 Mon Sep 17 00:00:00 2001 From: Anders Melchiorsen Date: Fri, 8 Jun 2018 07:46:34 +0200 Subject: [PATCH 042/144] Add netgear_lte component (#14687) * Add netgear_lte component * Improvements after review * Allow multiple notify targets * Require default notify target --- .coveragerc | 3 + homeassistant/components/netgear_lte.py | 86 +++++++++++++++++++ .../components/notify/netgear_lte.py | 45 ++++++++++ .../components/sensor/netgear_lte.py | 85 ++++++++++++++++++ requirements_all.txt | 3 + 5 files changed, 222 insertions(+) create mode 100644 homeassistant/components/netgear_lte.py create mode 100644 homeassistant/components/notify/netgear_lte.py create mode 100644 homeassistant/components/sensor/netgear_lte.py diff --git a/.coveragerc b/.coveragerc index a7d222b33b2..53dd7cdfe4e 100644 --- a/.coveragerc +++ b/.coveragerc @@ -201,6 +201,9 @@ omit = homeassistant/components/netatmo.py homeassistant/components/*/netatmo.py + homeassistant/components/netgear_lte.py + homeassistant/components/*/netgear_lte.py + homeassistant/components/octoprint.py homeassistant/components/*/octoprint.py diff --git a/homeassistant/components/netgear_lte.py b/homeassistant/components/netgear_lte.py new file mode 100644 index 00000000000..4887ea1aa67 --- /dev/null +++ b/homeassistant/components/netgear_lte.py @@ -0,0 +1,86 @@ +""" +Support for Netgear LTE modems. + +For more details about this component, please refer to the documentation at +https://home-assistant.io/components/netgear_lte/ +""" +import asyncio +from datetime import timedelta + +import voluptuous as vol +import attr + +from homeassistant.const import CONF_HOST, CONF_PASSWORD +import homeassistant.helpers.config_validation as cv +from homeassistant.util import Throttle + +REQUIREMENTS = ['eternalegypt==0.0.1'] + +MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=10) + +DOMAIN = 'netgear_lte' +DATA_KEY = 'netgear_lte' + +CONFIG_SCHEMA = vol.Schema({ + DOMAIN: vol.All(cv.ensure_list, [vol.Schema({ + vol.Required(CONF_HOST): cv.string, + vol.Required(CONF_PASSWORD): cv.string, + })]) +}, extra=vol.ALLOW_EXTRA) + + +@attr.s +class LTEData: + """Class for LTE state.""" + + eternalegypt = attr.ib() + unread_count = attr.ib(init=False) + usage = attr.ib(init=False) + + @Throttle(MIN_TIME_BETWEEN_UPDATES) + async def async_update(self): + """Call the API to update the data.""" + information = await self.eternalegypt.information() + self.unread_count = sum(1 for x in information.sms if x.unread) + self.usage = information.usage + + +@attr.s +class LTEHostData: + """Container for LTE states.""" + + hostdata = attr.ib(init=False, factory=dict) + + def get(self, config): + """Get the requested or the only hostdata value.""" + if CONF_HOST in config: + return self.hostdata.get(config[CONF_HOST]) + elif len(self.hostdata) == 1: + return next(iter(self.hostdata.values())) + + return None + + +async def async_setup(hass, config): + """Set up Netgear LTE component.""" + if DATA_KEY not in hass.data: + hass.data[DATA_KEY] = LTEHostData() + + tasks = [_setup_lte(hass, conf) for conf in config.get(DOMAIN, [])] + if tasks: + await asyncio.wait(tasks) + + return True + + +async def _setup_lte(hass, lte_config): + """Set up a Netgear LTE modem.""" + import eternalegypt + + host = lte_config[CONF_HOST] + password = lte_config[CONF_PASSWORD] + + eternalegypt = eternalegypt.LB2120(host, password) + lte_data = LTEData(eternalegypt) + await lte_data.async_update() + hass.data[DATA_KEY].hostdata[host] = lte_data diff --git a/homeassistant/components/notify/netgear_lte.py b/homeassistant/components/notify/netgear_lte.py new file mode 100644 index 00000000000..b4ed53b828d --- /dev/null +++ b/homeassistant/components/notify/netgear_lte.py @@ -0,0 +1,45 @@ +"""Netgear LTE platform for notify component. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/notify.netgear_lte/ +""" + +import voluptuous as vol +import attr + +from homeassistant.components.notify import ( + BaseNotificationService, ATTR_TARGET, PLATFORM_SCHEMA) +from homeassistant.const import CONF_HOST +import homeassistant.helpers.config_validation as cv + +from ..netgear_lte import DATA_KEY + + +DEPENDENCIES = ['netgear_lte'] + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Optional(CONF_HOST): cv.string, + vol.Required(ATTR_TARGET): vol.All(cv.ensure_list, [cv.string]), +}) + + +async def async_get_service(hass, config, discovery_info=None): + """Get the notification service.""" + lte_data = hass.data[DATA_KEY].get(config) + phone = config.get(ATTR_TARGET) + return NetgearNotifyService(lte_data, phone) + + +@attr.s +class NetgearNotifyService(BaseNotificationService): + """Implementation of a notification service.""" + + lte_data = attr.ib() + phone = attr.ib() + + async def async_send_message(self, message="", **kwargs): + """Send a message to a user.""" + targets = kwargs.get(ATTR_TARGET, self.phone) + if targets and message: + for target in targets: + await self.lte_data.eternalegypt.sms(target, message) diff --git a/homeassistant/components/sensor/netgear_lte.py b/homeassistant/components/sensor/netgear_lte.py new file mode 100644 index 00000000000..859435edbc9 --- /dev/null +++ b/homeassistant/components/sensor/netgear_lte.py @@ -0,0 +1,85 @@ +"""Netgear LTE sensors. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/sensor.netgear_lte/ +""" + +import voluptuous as vol +import attr + +from homeassistant.const import CONF_HOST, CONF_SENSORS +from homeassistant.components.sensor import PLATFORM_SCHEMA +from homeassistant.helpers.entity import Entity +import homeassistant.helpers.config_validation as cv + +from ..netgear_lte import DATA_KEY + +DEPENDENCIES = ['netgear_lte'] + +SENSOR_SMS = 'sms' +SENSOR_USAGE = 'usage' + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Optional(CONF_HOST): cv.string, + vol.Required(CONF_SENSORS): vol.All( + cv.ensure_list, [vol.In([SENSOR_SMS, SENSOR_USAGE])]) +}) + + +async def async_setup_platform( + hass, config, async_add_devices, discovery_info): + """Set up Netgear LTE sensor devices.""" + lte_data = hass.data[DATA_KEY].get(config) + + sensors = [] + for sensortype in config[CONF_SENSORS]: + if sensortype == SENSOR_SMS: + sensors.append(SMSSensor(lte_data)) + elif sensortype == SENSOR_USAGE: + sensors.append(UsageSensor(lte_data)) + + async_add_devices(sensors, True) + + +@attr.s +class LTESensor(Entity): + """Data usage sensor entity.""" + + lte_data = attr.ib() + + async def async_update(self): + """Update state.""" + await self.lte_data.async_update() + + +class SMSSensor(LTESensor): + """Unread SMS sensor entity.""" + + @property + def name(self): + """Return the name of the sensor.""" + return "Netgear LTE SMS" + + @property + def state(self): + """Return the state of the sensor.""" + return self.lte_data.unread_count + + +class UsageSensor(LTESensor): + """Data usage sensor entity.""" + + @property + def unit_of_measurement(self): + """Return the unit of measurement.""" + return "MiB" + + @property + def name(self): + """Return the name of the sensor.""" + return "Netgear LTE usage" + + @property + def state(self): + """Return the state of the sensor.""" + return round(self.lte_data.usage / 1024**2, 1) diff --git a/requirements_all.txt b/requirements_all.txt index 6bbe2a0b79e..f47bbbdf23e 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -291,6 +291,9 @@ enocean==0.40 # homeassistant.components.sensor.season ephem==3.7.6.0 +# homeassistant.components.netgear_lte +eternalegypt==0.0.1 + # homeassistant.components.keyboard_remote # evdev==0.6.1 From 6af995026b158b85f90ebe0b4e0abef4b9a60638 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 8 Jun 2018 16:50:19 -0400 Subject: [PATCH 043/144] Add support for new hass.io panel (#14873) --- homeassistant/components/hassio/__init__.py | 14 ++- homeassistant/components/panel_custom.py | 110 +++++++++++++++----- 2 files changed, 93 insertions(+), 31 deletions(-) diff --git a/homeassistant/components/hassio/__init__.py b/homeassistant/components/hassio/__init__.py index 0fbb2a57ca9..6ab86435371 100644 --- a/homeassistant/components/hassio/__init__.py +++ b/homeassistant/components/hassio/__init__.py @@ -171,14 +171,20 @@ def async_setup(hass, config): development_repo = config.get(DOMAIN, {}).get(CONF_FRONTEND_REPO) if development_repo is not None: hass.http.register_static_path( - '/api/hassio/app-es5', - os.path.join(development_repo, 'hassio/build-es5'), False) + '/api/hassio/app', + os.path.join(development_repo, 'hassio/build'), False) hass.http.register_view(HassIOView(host, websession)) if 'frontend' in hass.config.components: - yield from hass.components.frontend.async_register_built_in_panel( - 'hassio', 'Hass.io', 'hass:home-assistant') + yield from hass.components.panel_custom.async_register_panel( + frontend_url_path='hassio', + webcomponent_name='hassio-main', + sidebar_title='Hass.io', + sidebar_icon='hass:home-assistant', + js_url='/api/hassio/app/entrypoint.js', + embed_iframe=True, + ) if 'http' in config: yield from hassio.update_hass_api(config['http']) diff --git a/homeassistant/components/panel_custom.py b/homeassistant/components/panel_custom.py index 4659578ae27..0444e7a5b53 100644 --- a/homeassistant/components/panel_custom.py +++ b/homeassistant/components/panel_custom.py @@ -9,6 +9,7 @@ import os import voluptuous as vol +from homeassistant.loader import bind_hass import homeassistant.helpers.config_validation as cv DOMAIN = 'panel_custom' @@ -24,6 +25,9 @@ CONF_JS_URL = 'js_url' CONF_EMBED_IFRAME = 'embed_iframe' CONF_TRUST_EXTERNAL_SCRIPT = 'trust_external_script' +DEFAULT_EMBED_IFRAME = False +DEFAULT_TRUST_EXTERNAL = False + DEFAULT_ICON = 'mdi:bookmark' LEGACY_URL = '/api/panel_custom/{}' @@ -38,33 +42,99 @@ CONFIG_SCHEMA = vol.Schema({ vol.Optional(CONF_CONFIG): dict, vol.Optional(CONF_WEBCOMPONENT_PATH): cv.isfile, vol.Optional(CONF_JS_URL): cv.string, - vol.Optional(CONF_EMBED_IFRAME, default=False): cv.boolean, - vol.Optional(CONF_TRUST_EXTERNAL_SCRIPT, default=False): cv.boolean, + vol.Optional(CONF_EMBED_IFRAME, + default=DEFAULT_EMBED_IFRAME): cv.boolean, + vol.Optional(CONF_TRUST_EXTERNAL_SCRIPT, + default=DEFAULT_TRUST_EXTERNAL): cv.boolean, })]) }, extra=vol.ALLOW_EXTRA) _LOGGER = logging.getLogger(__name__) +@bind_hass +async def async_register_panel( + hass, + # The url to serve the panel + frontend_url_path, + # The webcomponent name that loads your panel + webcomponent_name, + # Title/icon for sidebar + sidebar_title=None, + sidebar_icon=None, + # HTML source of your panel + html_url=None, + # JS source of your panel + js_url=None, + # If your panel should be run inside an iframe + embed_iframe=DEFAULT_EMBED_IFRAME, + # Should user be asked for confirmation when loading external source + trust_external=DEFAULT_TRUST_EXTERNAL, + # Configuration to be passed to the panel + config=None): + """Register a new custom panel.""" + if js_url is None and html_url is None: + raise ValueError('Either js_url or html_url is required.') + elif js_url and html_url: + raise ValueError('Pass in either JS url or HTML url, not both.') + + if config is not None and not isinstance(config, dict): + raise ValueError('Config needs to be a dictionary.') + + custom_panel_config = { + 'name': webcomponent_name, + 'embed_iframe': embed_iframe, + 'trust_external': trust_external, + } + + if js_url is not None: + custom_panel_config['js_url'] = js_url + + if html_url is not None: + custom_panel_config['html_url'] = html_url + + if config is not None: + # Make copy because we're mutating it + config = dict(config) + else: + config = {} + + config['_panel_custom'] = custom_panel_config + + await hass.components.frontend.async_register_built_in_panel( + component_name='custom', + sidebar_title=sidebar_title, + sidebar_icon=sidebar_icon, + frontend_url_path=frontend_url_path, + config=config + ) + + async def async_setup(hass, config): """Initialize custom panel.""" success = False for panel in config.get(DOMAIN): - name = panel.get(CONF_COMPONENT_NAME) + name = panel[CONF_COMPONENT_NAME] + + kwargs = { + 'webcomponent_name': panel[CONF_COMPONENT_NAME], + 'frontend_url_path': panel.get(CONF_URL_PATH, name), + 'sidebar_title': panel.get(CONF_SIDEBAR_TITLE), + 'sidebar_icon': panel.get(CONF_SIDEBAR_ICON), + 'config': panel.get(CONF_CONFIG), + 'trust_external': panel[CONF_TRUST_EXTERNAL_SCRIPT], + 'embed_iframe': panel[CONF_EMBED_IFRAME], + } + panel_path = panel.get(CONF_WEBCOMPONENT_PATH) if panel_path is None: - panel_path = hass.config.path(PANEL_DIR, '{}.html'.format(name)) - - custom_panel_config = { - 'name': name, - 'embed_iframe': panel[CONF_EMBED_IFRAME], - 'trust_external': panel[CONF_TRUST_EXTERNAL_SCRIPT], - } + panel_path = hass.config.path( + PANEL_DIR, '{}.html'.format(name)) if CONF_JS_URL in panel: - custom_panel_config['js_url'] = panel[CONF_JS_URL] + kwargs['js_url'] = panel[CONF_JS_URL] elif not await hass.async_add_job(os.path.isfile, panel_path): _LOGGER.error('Unable to find webcomponent for %s: %s', @@ -74,23 +144,9 @@ async def async_setup(hass, config): else: url = LEGACY_URL.format(name) hass.http.register_static_path(url, panel_path) - custom_panel_config['html_url'] = LEGACY_URL.format(name) + kwargs['html_url'] = url - if CONF_CONFIG in panel: - # Make copy because we're mutating it - config = dict(panel[CONF_CONFIG]) - else: - config = {} - - config['_panel_custom'] = custom_panel_config - - await hass.components.frontend.async_register_built_in_panel( - component_name='custom', - sidebar_title=panel.get(CONF_SIDEBAR_TITLE), - sidebar_icon=panel.get(CONF_SIDEBAR_ICON), - frontend_url_path=panel.get(CONF_URL_PATH), - config=config - ) + await async_register_panel(hass, **kwargs) success = True From d3d9d9ebf2b4c2d5b618fcb354256b439c7f3feb Mon Sep 17 00:00:00 2001 From: Jason Hu Date: Fri, 8 Jun 2018 22:16:11 -0700 Subject: [PATCH 044/144] Add color_status sensor for Nest Protect (#14868) --- homeassistant/components/sensor/nest.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/sensor/nest.py b/homeassistant/components/sensor/nest.py index 00d18c7fe10..88464675c21 100644 --- a/homeassistant/components/sensor/nest.py +++ b/homeassistant/components/sensor/nest.py @@ -17,7 +17,11 @@ SENSOR_TYPES = ['humidity', 'operation_mode', 'hvac_state'] TEMP_SENSOR_TYPES = ['temperature', 'target'] -PROTECT_SENSOR_TYPES = ['co_status', 'smoke_status', 'battery_health'] +PROTECT_SENSOR_TYPES = ['co_status', + 'smoke_status', + 'battery_health', + # color_status: "gray", "green", "yellow", "red" + 'color_status'] STRUCTURE_SENSOR_TYPES = ['eta'] @@ -115,7 +119,8 @@ class NestBasicSensor(NestSensorDevice): if self.variable in VARIABLE_NAME_MAPPING: self._state = getattr(self.device, VARIABLE_NAME_MAPPING[self.variable]) - elif self.variable in PROTECT_SENSOR_TYPES: + elif self.variable in PROTECT_SENSOR_TYPES \ + and self.variable != 'color_status': # keep backward compatibility self._state = getattr(self.device, self.variable).capitalize() else: From f242418986401be898869f5c680a2da351405de1 Mon Sep 17 00:00:00 2001 From: Malte Franken Date: Sat, 9 Jun 2018 15:22:17 +1000 Subject: [PATCH 045/144] UVC camera platform handling unavailable NVR or cameras better (#14864) * fixed tests: using correct camera configuration now and error handling tests must be separated out to ensure that the setup_component call is actually executed * better error handling during setup; raising PlatformNotReady in likely recoverable cases; added tests --- homeassistant/components/camera/uvc.py | 26 ++++---- tests/components/camera/test_uvc.py | 90 +++++++++++++++++++------- 2 files changed, 81 insertions(+), 35 deletions(-) diff --git a/homeassistant/components/camera/uvc.py b/homeassistant/components/camera/uvc.py index 20dceb8a1c5..e992020e2b2 100644 --- a/homeassistant/components/camera/uvc.py +++ b/homeassistant/components/camera/uvc.py @@ -13,6 +13,7 @@ import voluptuous as vol from homeassistant.const import CONF_PORT from homeassistant.components.camera import Camera, PLATFORM_SCHEMA import homeassistant.helpers.config_validation as cv +from homeassistant.exceptions import PlatformNotReady REQUIREMENTS = ['uvcclient==0.10.1'] @@ -41,25 +42,26 @@ def setup_platform(hass, config, add_devices, discovery_info=None): port = config[CONF_PORT] from uvcclient import nvr - nvrconn = nvr.UVCRemote(addr, port, key) try: + # Exceptions may be raised in all method calls to the nvr library. + nvrconn = nvr.UVCRemote(addr, port, key) cameras = nvrconn.index() + + identifier = 'id' if nvrconn.server_version >= (3, 2, 0) else 'uuid' + # Filter out airCam models, which are not supported in the latest + # version of UnifiVideo and which are EOL by Ubiquiti + cameras = [ + camera for camera in cameras + if 'airCam' not in nvrconn.get_camera(camera[identifier])['model']] except nvr.NotAuthorized: _LOGGER.error("Authorization failure while connecting to NVR") return False - except nvr.NvrError: - _LOGGER.error("NVR refuses to talk to me") - return False + except nvr.NvrError as ex: + _LOGGER.error("NVR refuses to talk to me: %s", str(ex)) + raise PlatformNotReady except requests.exceptions.ConnectionError as ex: _LOGGER.error("Unable to connect to NVR: %s", str(ex)) - return False - - identifier = 'id' if nvrconn.server_version >= (3, 2, 0) else 'uuid' - # Filter out airCam models, which are not supported in the latest - # version of UnifiVideo and which are EOL by Ubiquiti - cameras = [ - camera for camera in cameras - if 'airCam' not in nvrconn.get_camera(camera[identifier])['model']] + raise PlatformNotReady add_devices([UnifiVideoCamera(nvrconn, camera[identifier], diff --git a/tests/components/camera/test_uvc.py b/tests/components/camera/test_uvc.py index dabad953bea..2de0782fd91 100644 --- a/tests/components/camera/test_uvc.py +++ b/tests/components/camera/test_uvc.py @@ -7,6 +7,7 @@ import requests from uvcclient import camera from uvcclient import nvr +from homeassistant.exceptions import PlatformNotReady from homeassistant.setup import setup_component from homeassistant.components.camera import uvc from tests.common import get_test_home_assistant @@ -34,21 +35,21 @@ class TestUVCSetup(unittest.TestCase): 'port': 123, 'key': 'secret', } - fake_cameras = [ + mock_cameras = [ {'uuid': 'one', 'name': 'Front', 'id': 'id1'}, {'uuid': 'two', 'name': 'Back', 'id': 'id2'}, {'uuid': 'three', 'name': 'Old AirCam', 'id': 'id3'}, ] - def fake_get_camera(uuid): - """Create a fake camera.""" + def mock_get_camera(uuid): + """Create a mock camera.""" if uuid == 'id3': return {'model': 'airCam'} else: return {'model': 'UVC'} - mock_remote.return_value.index.return_value = fake_cameras - mock_remote.return_value.get_camera.side_effect = fake_get_camera + mock_remote.return_value.index.return_value = mock_cameras + mock_remote.return_value.get_camera.side_effect = mock_get_camera mock_remote.return_value.server_version = (3, 2, 0) assert setup_component(self.hass, 'camera', {'camera': config}) @@ -71,11 +72,11 @@ class TestUVCSetup(unittest.TestCase): 'nvr': 'foo', 'key': 'secret', } - fake_cameras = [ + mock_cameras = [ {'uuid': 'one', 'name': 'Front', 'id': 'id1'}, {'uuid': 'two', 'name': 'Back', 'id': 'id2'}, ] - mock_remote.return_value.index.return_value = fake_cameras + mock_remote.return_value.index.return_value = mock_cameras mock_remote.return_value.get_camera.return_value = {'model': 'UVC'} mock_remote.return_value.server_version = (3, 2, 0) @@ -99,11 +100,11 @@ class TestUVCSetup(unittest.TestCase): 'nvr': 'foo', 'key': 'secret', } - fake_cameras = [ + mock_cameras = [ {'uuid': 'one', 'name': 'Front', 'id': 'id1'}, {'uuid': 'two', 'name': 'Back', 'id': 'id2'}, ] - mock_remote.return_value.index.return_value = fake_cameras + mock_remote.return_value.index.return_value = mock_cameras mock_remote.return_value.get_camera.return_value = {'model': 'UVC'} mock_remote.return_value.server_version = (3, 1, 3) @@ -133,19 +134,62 @@ class TestUVCSetup(unittest.TestCase): @mock.patch.object(uvc, 'UnifiVideoCamera') @mock.patch('uvcclient.nvr.UVCRemote') - def test_setup_nvr_errors(self, mock_remote, mock_uvc): - """Test for NVR errors.""" - errors = [nvr.NotAuthorized, nvr.NvrError, - requests.exceptions.ConnectionError] + def setup_nvr_errors_during_indexing(self, error, mock_remote, mock_uvc): + """Setup test for NVR errors during indexing.""" config = { 'platform': 'uvc', 'nvr': 'foo', 'key': 'secret', } - for error in errors: - mock_remote.return_value.index.side_effect = error - assert setup_component(self.hass, 'camera', config) - assert not mock_uvc.called + mock_remote.return_value.index.side_effect = error + assert setup_component(self.hass, 'camera', {'camera': config}) + assert not mock_uvc.called + + def test_setup_nvr_error_during_indexing_notauthorized(self): + """Test for error: nvr.NotAuthorized.""" + self.setup_nvr_errors_during_indexing(nvr.NotAuthorized) + + def test_setup_nvr_error_during_indexing_nvrerror(self): + """Test for error: nvr.NvrError.""" + self.setup_nvr_errors_during_indexing(nvr.NvrError) + self.assertRaises(PlatformNotReady) + + def test_setup_nvr_error_during_indexing_connectionerror(self): + """Test for error: requests.exceptions.ConnectionError.""" + self.setup_nvr_errors_during_indexing( + requests.exceptions.ConnectionError) + self.assertRaises(PlatformNotReady) + + @mock.patch.object(uvc, 'UnifiVideoCamera') + @mock.patch('uvcclient.nvr.UVCRemote.__init__') + def setup_nvr_errors_during_initialization(self, error, mock_remote, + mock_uvc): + """Setup test for NVR errors during initialization.""" + config = { + 'platform': 'uvc', + 'nvr': 'foo', + 'key': 'secret', + } + mock_remote.return_value = None + mock_remote.side_effect = error + assert setup_component(self.hass, 'camera', {'camera': config}) + assert not mock_remote.index.called + assert not mock_uvc.called + + def test_setup_nvr_error_during_initialization_notauthorized(self): + """Test for error: nvr.NotAuthorized.""" + self.setup_nvr_errors_during_initialization(nvr.NotAuthorized) + + def test_setup_nvr_error_during_initialization_nvrerror(self): + """Test for error: nvr.NvrError.""" + self.setup_nvr_errors_during_initialization(nvr.NvrError) + self.assertRaises(PlatformNotReady) + + def test_setup_nvr_error_during_initialization_connectionerror(self): + """Test for error: requests.exceptions.ConnectionError.""" + self.setup_nvr_errors_during_initialization( + requests.exceptions.ConnectionError) + self.assertRaises(PlatformNotReady) class TestUVC(unittest.TestCase): @@ -208,8 +252,8 @@ class TestUVC(unittest.TestCase): """Test the login tries.""" responses = [0] - def fake_login(*a): - """Fake login.""" + def mock_login(*a): + """Mock login.""" try: responses.pop(0) raise socket.error @@ -217,7 +261,7 @@ class TestUVC(unittest.TestCase): pass mock_store.return_value.get_camera_password.return_value = None - mock_camera.return_value.login.side_effect = fake_login + mock_camera.return_value.login.side_effect = mock_login self.uvc._login() self.assertEqual(2, mock_camera.call_count) self.assertEqual('host-b', self.uvc._connect_addr) @@ -263,8 +307,8 @@ class TestUVC(unittest.TestCase): """Test the re-authentication.""" responses = [0] - def fake_snapshot(): - """Fake snapshot.""" + def mock_snapshot(): + """Mock snapshot.""" try: responses.pop() raise camera.CameraAuthError() @@ -273,7 +317,7 @@ class TestUVC(unittest.TestCase): return 'image' self.uvc._camera = mock.MagicMock() - self.uvc._camera.get_snapshot.side_effect = fake_snapshot + self.uvc._camera.get_snapshot.side_effect = mock_snapshot with mock.patch.object(self.uvc, '_login') as mock_login: self.assertEqual('image', self.uvc.camera_image()) self.assertEqual(mock_login.call_count, 1) From 5f65f67f1e5cc6489b34aac7d3dcd75c1cd810d5 Mon Sep 17 00:00:00 2001 From: vandenberghev Date: Sat, 9 Jun 2018 12:37:06 +0200 Subject: [PATCH 046/144] Removed semicolon --- homeassistant/components/sensor/smappee.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/sensor/smappee.py b/homeassistant/components/sensor/smappee.py index 0263a1266c6..783c2aad469 100644 --- a/homeassistant/components/sensor/smappee.py +++ b/homeassistant/components/sensor/smappee.py @@ -189,7 +189,7 @@ class SmappeeSensor(Entity): data = self._smappee.sensor_consumption[self._location_id]\ .get(int(sensor_id)) if data: - tempdata = data.get('records'); + tempdata = data.get('records') if tempdata: consumption = tempdata[-1] _LOGGER.debug("%s (%s) %s", From d7b7370c82b84027dcf2b77ef1bcde8b3fc676a3 Mon Sep 17 00:00:00 2001 From: Ong Vairoj Date: Sat, 9 Jun 2018 06:22:34 -0700 Subject: [PATCH 047/144] Samsung TV can't turn off after idle period (#14587) When Samsung TV is idle for a period of time after issued a command, subsequent 'turn_off' command won't turn off the TV. The issue is seen in Samsung models with websocket as discussed in #12302. == Reproducible Steps 1. Turn on TV (either via HA or directly). 2. Issue some commands e.g. volume ups / downs. 3. Wait for ~1 minute. 4. Issue turn_off command via HA. TV won't turn off. 5. Issue subsequent turn off commands won't turn off TV still. 6. However, issue some other commands e.g. volume ups / downs multiple times in a row and then turn_off will turn off the TV. == Root Cause The underlying websocket connection opened by samsungctl get closed after some idle time. There was no retry mechanism so issued commands would intermittently fail but the subsequent one would succeed when `_remote` get recreated. With `turn_off()`, however, there is an additional call to `self.get_remote().close()` which indirectly caused new connection to be created and then closed immediately. This causes the component to stuck in failure mode when turn_off command is repeatly issued. == The Fix Recreate the connection and retry the command if connection is closed to avoid silent failures due to connection closed. Also set `_remote` to None after calling close() to put it in correct state. This fix eliminates intermittent command failure and failure mode in turn_off(). --- .../components/media_player/samsungtv.py | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/media_player/samsungtv.py b/homeassistant/components/media_player/samsungtv.py index 0b7fc3c078e..43e9abd96a6 100644 --- a/homeassistant/components/media_player/samsungtv.py +++ b/homeassistant/components/media_player/samsungtv.py @@ -155,16 +155,25 @@ class SamsungTVDevice(MediaPlayerDevice): _LOGGER.info("TV is powering off, not sending command: %s", key) return try: - self.get_remote().control(key) + # recreate connection if connection was dead + retry_count = 1 + for _ in range(retry_count + 1): + try: + self.get_remote().control(key) + break + except (self._exceptions_class.ConnectionClosed, + BrokenPipeError): + # BrokenPipe can occur when the commands is sent to fast + self._remote = None self._state = STATE_ON except (self._exceptions_class.UnhandledResponse, - self._exceptions_class.AccessDenied, BrokenPipeError): + self._exceptions_class.AccessDenied): # We got a response so it's on. - # BrokenPipe can occur when the commands is sent to fast self._state = STATE_ON self._remote = None + _LOGGER.debug("Failed sending command %s", key, exc_info=True) return - except (self._exceptions_class.ConnectionClosed, OSError): + except OSError: self._state = STATE_OFF self._remote = None if self._power_off_in_progress(): @@ -207,6 +216,7 @@ class SamsungTVDevice(MediaPlayerDevice): # Force closing of remote session to provide instant UI feedback try: self.get_remote().close() + self._remote = None except OSError: _LOGGER.debug("Could not establish connection.") From 5393b073fe6617cd717e969c3f470cf08e7541bd Mon Sep 17 00:00:00 2001 From: John Arild Berentsen Date: Sat, 9 Jun 2018 15:34:36 +0200 Subject: [PATCH 048/144] Discover Qubino ZMHTDx smart meter switches (#14884) --- homeassistant/components/zwave/discovery_schemas.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/zwave/discovery_schemas.py b/homeassistant/components/zwave/discovery_schemas.py index d38fbc7079c..fc2e7fc912d 100644 --- a/homeassistant/components/zwave/discovery_schemas.py +++ b/homeassistant/components/zwave/discovery_schemas.py @@ -213,6 +213,7 @@ DISCOVERY_SCHEMAS = [ }})}, {const.DISC_COMPONENT: 'switch', const.DISC_GENERIC_DEVICE_CLASS: [ + const.GENERIC_TYPE_METER, const.GENERIC_TYPE_SENSOR_ALARM, const.GENERIC_TYPE_SENSOR_BINARY, const.GENERIC_TYPE_SWITCH_BINARY, From bc0d0751b96e12f86c0bcf0fdd0e7790e1468652 Mon Sep 17 00:00:00 2001 From: hanzoh Date: Sat, 9 Jun 2018 16:12:42 +0200 Subject: [PATCH 049/144] Add missing mapping of RotaryHandleSensorIP states (#14885) --- homeassistant/components/sensor/homematic.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/homeassistant/components/sensor/homematic.py b/homeassistant/components/sensor/homematic.py index bdbc207a79c..60741a9f3c8 100644 --- a/homeassistant/components/sensor/homematic.py +++ b/homeassistant/components/sensor/homematic.py @@ -16,6 +16,9 @@ HM_STATE_HA_CAST = { 'RotaryHandleSensor': {0: 'closed', 1: 'tilted', 2: 'open'}, + 'RotaryHandleSensorIP': {0: 'closed', + 1: 'tilted', + 2: 'open'}, 'WaterSensor': {0: 'dry', 1: 'wet', 2: 'water'}, From 20caeb5383b4ca1f4a0140a105c19d4ea1bdf389 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ing=2E=20Jaroslav=20=C5=A0afka?= Date: Sun, 10 Jun 2018 08:31:42 +0200 Subject: [PATCH 050/144] Add entity registry support to media_player.snapcast (#14895) Unique id for client is generated from prefix 'snapcast_client_' and MAC address Unique id for group is generated from prefix 'snapcast_group_' and UUID provided by snapcast library --- homeassistant/components/media_player/snapcast.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/homeassistant/components/media_player/snapcast.py b/homeassistant/components/media_player/snapcast.py index 793800a3d22..ca7ff17a16a 100644 --- a/homeassistant/components/media_player/snapcast.py +++ b/homeassistant/components/media_player/snapcast.py @@ -104,6 +104,11 @@ class SnapcastGroupDevice(MediaPlayerDevice): 'unknown': STATE_UNKNOWN, }.get(self._group.stream_status, STATE_UNKNOWN) + @property + def unique_id(self): + """Return the ID of snapcast group.""" + return '{}{}'.format(GROUP_PREFIX, self._group.identifier) + @property def name(self): """Return the name of the device.""" @@ -185,6 +190,11 @@ class SnapcastClientDevice(MediaPlayerDevice): client.set_callback(self.schedule_update_ha_state) self._client = client + @property + def unique_id(self): + """Return the ID of this snapcast client.""" + return '{}{}'.format(CLIENT_PREFIX, self._client.identifier) + @property def name(self): """Return the name of the device.""" From f3e55ce330d6bf733360e9e46b30ab50fc01e566 Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Sun, 10 Jun 2018 08:39:48 +0200 Subject: [PATCH 051/144] Allow different identifiers for the CPU temperature (fixes #10104) (#14898) --- homeassistant/components/sensor/glances.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/sensor/glances.py b/homeassistant/components/sensor/glances.py index 0de87bd17ea..4fed3793c50 100644 --- a/homeassistant/components/sensor/glances.py +++ b/homeassistant/components/sensor/glances.py @@ -155,9 +155,9 @@ class GlancesSensor(Entity): self._state = value['processcount']['sleeping'] elif self.type == 'cpu_temp': for sensor in value['sensors']: - if sensor['label'] == 'CPU': + if sensor['label'] in ['CPU', "Package id 0", + "Physical id 0"]: self._state = sensor['value'] - self._state = None elif self.type == 'docker_active': count = 0 for container in value['docker']['containers']: From 8aca2e84dc81961296eea2fdacf672f068acdcbb Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Sun, 10 Jun 2018 02:23:07 -0600 Subject: [PATCH 052/144] Make RainMachine async (#14879) * Make RainMachine async * Updated requirements * Dispatcher adjustments * Small verbiage change * Member-requested changes * Style consistency * Updated requirements --- .../components/binary_sensor/rainmachine.py | 15 +- .../components/rainmachine/__init__.py | 88 +++++---- .../components/sensor/rainmachine.py | 15 +- .../components/switch/rainmachine.py | 170 +++++++++--------- requirements_all.txt | 2 +- 5 files changed, 156 insertions(+), 134 deletions(-) diff --git a/homeassistant/components/binary_sensor/rainmachine.py b/homeassistant/components/binary_sensor/rainmachine.py index 601a73298af..b2f44696fbd 100644 --- a/homeassistant/components/binary_sensor/rainmachine.py +++ b/homeassistant/components/binary_sensor/rainmachine.py @@ -8,7 +8,7 @@ import logging from homeassistant.components.binary_sensor import BinarySensorDevice from homeassistant.components.rainmachine import ( - BINARY_SENSORS, DATA_RAINMACHINE, DATA_UPDATE_TOPIC, TYPE_FREEZE, + BINARY_SENSORS, DATA_RAINMACHINE, SENSOR_UPDATE_TOPIC, TYPE_FREEZE, TYPE_FREEZE_PROTECTION, TYPE_HOT_DAYS, TYPE_HOURLY, TYPE_MONTH, TYPE_RAINDELAY, TYPE_RAINSENSOR, TYPE_WEEKDAY, RainMachineEntity) from homeassistant.const import CONF_MONITORED_CONDITIONS @@ -20,7 +20,8 @@ DEPENDENCIES = ['rainmachine'] _LOGGER = logging.getLogger(__name__) -def setup_platform(hass, config, add_devices, discovery_info=None): +async def async_setup_platform( + hass, config, async_add_devices, discovery_info=None): """Set up the RainMachine Switch platform.""" if discovery_info is None: return @@ -33,7 +34,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): binary_sensors.append( RainMachineBinarySensor(rainmachine, sensor_type, name, icon)) - add_devices(binary_sensors, True) + async_add_devices(binary_sensors, True) class RainMachineBinarySensor(RainMachineEntity, BinarySensorDevice): @@ -70,16 +71,16 @@ class RainMachineBinarySensor(RainMachineEntity, BinarySensorDevice): self.rainmachine.device_mac.replace(':', ''), self._sensor_type) @callback - def update_data(self): + def _update_data(self): """Update the state.""" self.async_schedule_update_ha_state(True) async def async_added_to_hass(self): """Register callbacks.""" - async_dispatcher_connect(self.hass, DATA_UPDATE_TOPIC, - self.update_data) + async_dispatcher_connect( + self.hass, SENSOR_UPDATE_TOPIC, self._update_data) - def update(self): + async def async_update(self): """Update the state.""" if self._sensor_type == TYPE_FREEZE: self._state = self.rainmachine.restrictions['current']['freeze'] diff --git a/homeassistant/components/rainmachine/__init__.py b/homeassistant/components/rainmachine/__init__.py index 7ee6b063720..38672dbc23b 100644 --- a/homeassistant/components/rainmachine/__init__.py +++ b/homeassistant/components/rainmachine/__init__.py @@ -13,12 +13,13 @@ from homeassistant.const import ( ATTR_ATTRIBUTION, CONF_BINARY_SENSORS, CONF_IP_ADDRESS, CONF_PASSWORD, CONF_PORT, CONF_SENSORS, CONF_SSL, CONF_MONITORED_CONDITIONS, CONF_SWITCHES) -from homeassistant.helpers import config_validation as cv, discovery -from homeassistant.helpers.dispatcher import dispatcher_send +from homeassistant.helpers import ( + aiohttp_client, config_validation as cv, discovery) +from homeassistant.helpers.dispatcher import async_dispatcher_send from homeassistant.helpers.entity import Entity -from homeassistant.helpers.event import track_time_interval +from homeassistant.helpers.event import async_track_time_interval -REQUIREMENTS = ['regenmaschine==0.4.2'] +REQUIREMENTS = ['regenmaschine==1.0.2'] _LOGGER = logging.getLogger(__name__) @@ -28,8 +29,9 @@ DOMAIN = 'rainmachine' NOTIFICATION_ID = 'rainmachine_notification' NOTIFICATION_TITLE = 'RainMachine Component Setup' -DATA_UPDATE_TOPIC = '{0}_data_update'.format(DOMAIN) PROGRAM_UPDATE_TOPIC = '{0}_program_update'.format(DOMAIN) +SENSOR_UPDATE_TOPIC = '{0}_data_update'.format(DOMAIN) +ZONE_UPDATE_TOPIC = '{0}_zone_update'.format(DOMAIN) CONF_PROGRAM_ID = 'program_id' CONF_ZONE_ID = 'zone_id' @@ -114,10 +116,10 @@ CONFIG_SCHEMA = vol.Schema( extra=vol.ALLOW_EXTRA) -def setup(hass, config): +async def async_setup(hass, config): """Set up the RainMachine component.""" - from regenmaschine import Authenticator, Client - from regenmaschine.exceptions import RainMachineError + from regenmaschine import Client + from regenmaschine.errors import RequestError conf = config[DOMAIN] ip_address = conf[CONF_IP_ADDRESS] @@ -126,17 +128,18 @@ def setup(hass, config): ssl = conf[CONF_SSL] try: - auth = Authenticator.create_local( - ip_address, password, port=port, https=ssl) - rainmachine = RainMachine(hass, Client(auth)) - rainmachine.update() + websession = aiohttp_client.async_get_clientsession(hass) + client = Client(ip_address, websession, port=port, ssl=ssl) + await client.authenticate(password) + rainmachine = RainMachine(client) + await rainmachine.async_update() hass.data[DATA_RAINMACHINE] = rainmachine - except RainMachineError as exc: - _LOGGER.error('An error occurred: %s', str(exc)) + except RequestError as err: + _LOGGER.error('An error occurred: %s', str(err)) hass.components.persistent_notification.create( 'Error: {0}
' 'You will need to restart hass after fixing.' - ''.format(exc), + ''.format(err), title=NOTIFICATION_TITLE, notification_id=NOTIFICATION_ID) return False @@ -146,36 +149,43 @@ def setup(hass, config): ('sensor', conf[CONF_SENSORS]), ('switch', conf[CONF_SWITCHES]), ]: - discovery.load_platform(hass, component, DOMAIN, schema, config) + hass.async_add_job( + discovery.async_load_platform(hass, component, DOMAIN, schema, + config)) - def refresh(event_time): - """Refresh RainMachine data.""" - _LOGGER.debug('Updating RainMachine data') - hass.data[DATA_RAINMACHINE].update() - dispatcher_send(hass, DATA_UPDATE_TOPIC) + async def refresh_sensors(event_time): + """Refresh RainMachine sensor data.""" + _LOGGER.debug('Updating RainMachine sensor data') + await rainmachine.async_update() + async_dispatcher_send(hass, SENSOR_UPDATE_TOPIC) - track_time_interval(hass, refresh, DEFAULT_SCAN_INTERVAL) + async_track_time_interval(hass, refresh_sensors, DEFAULT_SCAN_INTERVAL) - def start_program(service): + async def start_program(service): """Start a particular program.""" - rainmachine.client.programs.start(service.data[CONF_PROGRAM_ID]) + await rainmachine.client.programs.start(service.data[CONF_PROGRAM_ID]) + async_dispatcher_send(hass, PROGRAM_UPDATE_TOPIC) - def start_zone(service): + async def start_zone(service): """Start a particular zone for a certain amount of time.""" - rainmachine.client.zones.start(service.data[CONF_ZONE_ID], - service.data[CONF_ZONE_RUN_TIME]) + await rainmachine.client.zones.start(service.data[CONF_ZONE_ID], + service.data[CONF_ZONE_RUN_TIME]) + async_dispatcher_send(hass, ZONE_UPDATE_TOPIC) - def stop_all(service): + async def stop_all(service): """Stop all watering.""" - rainmachine.client.watering.stop_all() + await rainmachine.client.watering.stop_all() + async_dispatcher_send(hass, PROGRAM_UPDATE_TOPIC) - def stop_program(service): + async def stop_program(service): """Stop a program.""" - rainmachine.client.programs.stop(service.data[CONF_PROGRAM_ID]) + await rainmachine.client.programs.stop(service.data[CONF_PROGRAM_ID]) + async_dispatcher_send(hass, PROGRAM_UPDATE_TOPIC) - def stop_zone(service): + async def stop_zone(service): """Stop a zone.""" - rainmachine.client.zones.stop(service.data[CONF_ZONE_ID]) + await rainmachine.client.zones.stop(service.data[CONF_ZONE_ID]) + async_dispatcher_send(hass, ZONE_UPDATE_TOPIC) for service, method, schema in [ ('start_program', start_program, SERVICE_START_PROGRAM_SCHEMA), @@ -184,7 +194,7 @@ def setup(hass, config): ('stop_program', stop_program, SERVICE_STOP_PROGRAM_SCHEMA), ('stop_zone', stop_zone, SERVICE_STOP_ZONE_SCHEMA) ]: - hass.services.register(DOMAIN, service, method, schema=schema) + hass.services.async_register(DOMAIN, service, method, schema=schema) return True @@ -192,17 +202,17 @@ def setup(hass, config): class RainMachine(object): """Define a generic RainMachine object.""" - def __init__(self, hass, client): + def __init__(self, client): """Initialize.""" self.client = client - self.device_mac = self.client.provision.wifi()['macAddress'] + self.device_mac = self.client.mac self.restrictions = {} - def update(self): + async def async_update(self): """Update sensor/binary sensor data.""" self.restrictions.update({ - 'current': self.client.restrictions.current(), - 'global': self.client.restrictions.universal() + 'current': await self.client.restrictions.current(), + 'global': await self.client.restrictions.universal() }) diff --git a/homeassistant/components/sensor/rainmachine.py b/homeassistant/components/sensor/rainmachine.py index 8faf30acc38..f747a26df39 100644 --- a/homeassistant/components/sensor/rainmachine.py +++ b/homeassistant/components/sensor/rainmachine.py @@ -7,7 +7,7 @@ https://home-assistant.io/components/sensor.rainmachine/ import logging from homeassistant.components.rainmachine import ( - DATA_RAINMACHINE, DATA_UPDATE_TOPIC, SENSORS, RainMachineEntity) + DATA_RAINMACHINE, SENSOR_UPDATE_TOPIC, SENSORS, RainMachineEntity) from homeassistant.const import CONF_MONITORED_CONDITIONS from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_connect @@ -17,7 +17,8 @@ DEPENDENCIES = ['rainmachine'] _LOGGER = logging.getLogger(__name__) -def setup_platform(hass, config, add_devices, discovery_info=None): +async def async_setup_platform( + hass, config, async_add_devices, discovery_info=None): """Set up the RainMachine Switch platform.""" if discovery_info is None: return @@ -30,7 +31,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): sensors.append( RainMachineSensor(rainmachine, sensor_type, name, icon, unit)) - add_devices(sensors, True) + async_add_devices(sensors, True) class RainMachineSensor(RainMachineEntity): @@ -73,16 +74,16 @@ class RainMachineSensor(RainMachineEntity): return self._unit @callback - def update_data(self): + def _update_data(self): """Update the state.""" self.async_schedule_update_ha_state(True) async def async_added_to_hass(self): """Register callbacks.""" - async_dispatcher_connect(self.hass, DATA_UPDATE_TOPIC, - self.update_data) + async_dispatcher_connect( + self.hass, SENSOR_UPDATE_TOPIC, self._update_data) - def update(self): + async def async_update(self): """Update the sensor's state.""" self._state = self.rainmachine.restrictions['global'][ 'freezeProtectTemp'] diff --git a/homeassistant/components/switch/rainmachine.py b/homeassistant/components/switch/rainmachine.py index bdee64a3d54..b0cdf334cfa 100644 --- a/homeassistant/components/switch/rainmachine.py +++ b/homeassistant/components/switch/rainmachine.py @@ -8,12 +8,12 @@ import logging from homeassistant.components.rainmachine import ( CONF_ZONE_RUN_TIME, DATA_RAINMACHINE, DEFAULT_ZONE_RUN, - PROGRAM_UPDATE_TOPIC, RainMachineEntity) + PROGRAM_UPDATE_TOPIC, ZONE_UPDATE_TOPIC, RainMachineEntity) from homeassistant.const import ATTR_ID from homeassistant.components.switch import SwitchDevice from homeassistant.core import callback from homeassistant.helpers.dispatcher import ( - async_dispatcher_connect, dispatcher_send) + async_dispatcher_connect, async_dispatcher_send) DEPENDENCIES = ['rainmachine'] @@ -39,20 +39,11 @@ ATTR_VEGETATION_TYPE = 'vegetation_type' ATTR_ZONES = 'zones' DAYS = [ - 'Monday', - 'Tuesday', - 'Wednesday', - 'Thursday', - 'Friday', - 'Saturday', + 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday' ] -PROGRAM_STATUS_MAP = { - 0: 'Not Running', - 1: 'Running', - 2: 'Queued' -} +PROGRAM_STATUS_MAP = {0: 'Not Running', 1: 'Running', 2: 'Queued'} SOIL_TYPE_MAP = { 0: 'Not Set', @@ -108,7 +99,8 @@ VEGETATION_MAP = { } -def setup_platform(hass, config, add_devices, discovery_info=None): +async def async_setup_platform( + hass, config, async_add_devices, discovery_info=None): """Set up the RainMachine Switch platform.""" if discovery_info is None: return @@ -120,21 +112,24 @@ def setup_platform(hass, config, add_devices, discovery_info=None): rainmachine = hass.data[DATA_RAINMACHINE] entities = [] - for program in rainmachine.client.programs.all().get('programs', {}): + + programs = await rainmachine.client.programs.all() + for program in programs: if not program.get('active'): continue _LOGGER.debug('Adding program: %s', program) entities.append(RainMachineProgram(rainmachine, program)) - for zone in rainmachine.client.zones.all().get('zones', {}): + zones = await rainmachine.client.zones.all() + for zone in zones: if not zone.get('active'): continue _LOGGER.debug('Adding zone: %s', zone) entities.append(RainMachineZone(rainmachine, zone, zone_run_time)) - add_devices(entities, True) + async_add_devices(entities, True) class RainMachineSwitch(RainMachineEntity, SwitchDevice): @@ -163,10 +158,14 @@ class RainMachineSwitch(RainMachineEntity, SwitchDevice): def unique_id(self) -> str: """Return a unique, HASS-friendly identifier for this entity.""" return '{0}_{1}_{2}'.format( - self.rainmachine.device_mac.replace(':', ''), - self._switch_type, + self.rainmachine.device_mac.replace(':', ''), self._switch_type, self._rainmachine_entity_id) + @callback + def _program_updated(self): + """Update state, trigger updates.""" + self.async_schedule_update_ha_state(True) + class RainMachineProgram(RainMachineSwitch): """A RainMachine program.""" @@ -185,34 +184,42 @@ class RainMachineProgram(RainMachineSwitch): """Return a list of active zones associated with this program.""" return [z for z in self._obj['wateringTimes'] if z['active']] - def turn_off(self, **kwargs) -> None: + async def async_added_to_hass(self): + """Register callbacks.""" + async_dispatcher_connect( + self.hass, PROGRAM_UPDATE_TOPIC, self._program_updated) + + async def async_turn_off(self, **kwargs) -> None: """Turn the program off.""" - from regenmaschine.exceptions import RainMachineError + from regenmaschine.errors import RequestError try: - self.rainmachine.client.programs.stop(self._rainmachine_entity_id) - dispatcher_send(self.hass, PROGRAM_UPDATE_TOPIC) - except RainMachineError as exc_info: - _LOGGER.error('Unable to turn off program "%s"', self.unique_id) - _LOGGER.debug(exc_info) + await self.rainmachine.client.programs.stop( + self._rainmachine_entity_id) + async_dispatcher_send(self.hass, PROGRAM_UPDATE_TOPIC) + except RequestError as err: + _LOGGER.error( + 'Unable to turn off program "%s": %s', self.unique_id, + str(err)) - def turn_on(self, **kwargs) -> None: + async def async_turn_on(self, **kwargs) -> None: """Turn the program on.""" - from regenmaschine.exceptions import RainMachineError + from regenmaschine.errors import RequestError try: - self.rainmachine.client.programs.start(self._rainmachine_entity_id) - dispatcher_send(self.hass, PROGRAM_UPDATE_TOPIC) - except RainMachineError as exc_info: - _LOGGER.error('Unable to turn on program "%s"', self.unique_id) - _LOGGER.debug(exc_info) + await self.rainmachine.client.programs.start( + self._rainmachine_entity_id) + async_dispatcher_send(self.hass, PROGRAM_UPDATE_TOPIC) + except RequestError as err: + _LOGGER.error( + 'Unable to turn on program "%s": %s', self.unique_id, str(err)) - def update(self) -> None: + async def async_update(self) -> None: """Update info for the program.""" - from regenmaschine.exceptions import RainMachineError + from regenmaschine.errors import RequestError try: - self._obj = self.rainmachine.client.programs.get( + self._obj = await self.rainmachine.client.programs.get( self._rainmachine_entity_id) self._attrs.update({ @@ -221,10 +228,10 @@ class RainMachineProgram(RainMachineSwitch): ATTR_STATUS: PROGRAM_STATUS_MAP[self._obj.get('status')], ATTR_ZONES: ', '.join(z['name'] for z in self.zones) }) - except RainMachineError as exc_info: - _LOGGER.error('Unable to update info for program "%s"', - self.unique_id) - _LOGGER.debug(exc_info) + except RequestError as err: + _LOGGER.error( + 'Unable to update info for program "%s": %s', self.unique_id, + str(err)) class RainMachineZone(RainMachineSwitch): @@ -242,62 +249,65 @@ class RainMachineZone(RainMachineSwitch): """Return whether the zone is running.""" return bool(self._obj.get('state')) - @callback - def _program_updated(self): - """Update state, trigger updates.""" - self.async_schedule_update_ha_state(True) - async def async_added_to_hass(self): """Register callbacks.""" - async_dispatcher_connect(self.hass, PROGRAM_UPDATE_TOPIC, - self._program_updated) + async_dispatcher_connect( + self.hass, PROGRAM_UPDATE_TOPIC, self._program_updated) + async_dispatcher_connect( + self.hass, ZONE_UPDATE_TOPIC, self._program_updated) - def turn_off(self, **kwargs) -> None: + async def async_turn_off(self, **kwargs) -> None: """Turn the zone off.""" - from regenmaschine.exceptions import RainMachineError + from regenmaschine.errors import RequestError try: - self.rainmachine.client.zones.stop(self._rainmachine_entity_id) - except RainMachineError as exc_info: - _LOGGER.error('Unable to turn off zone "%s"', self.unique_id) - _LOGGER.debug(exc_info) + await self.rainmachine.client.zones.stop( + self._rainmachine_entity_id) + except RequestError as err: + _LOGGER.error( + 'Unable to turn off zone "%s": %s', self.unique_id, str(err)) - def turn_on(self, **kwargs) -> None: + async def async_turn_on(self, **kwargs) -> None: """Turn the zone on.""" - from regenmaschine.exceptions import RainMachineError + from regenmaschine.errors import RequestError try: - self.rainmachine.client.zones.start(self._rainmachine_entity_id, - self._run_time) - except RainMachineError as exc_info: - _LOGGER.error('Unable to turn on zone "%s"', self.unique_id) - _LOGGER.debug(exc_info) + await self.rainmachine.client.zones.start( + self._rainmachine_entity_id, self._run_time) + except RequestError as err: + _LOGGER.error( + 'Unable to turn on zone "%s": %s', self.unique_id, str(err)) - def update(self) -> None: + async def async_update(self) -> None: """Update info for the zone.""" - from regenmaschine.exceptions import RainMachineError + from regenmaschine.errors import RequestError try: - self._obj = self.rainmachine.client.zones.get( + self._obj = await self.rainmachine.client.zones.get( self._rainmachine_entity_id) - self._properties_json = self.rainmachine.client.zones.get( - self._rainmachine_entity_id, properties=True) + self._properties_json = await self.rainmachine.client.zones.get( + self._rainmachine_entity_id, details=True) self._attrs.update({ - ATTR_ID: self._obj['uid'], - ATTR_AREA: self._properties_json.get('waterSense').get('area'), - ATTR_CURRENT_CYCLE: self._obj.get('cycle'), + ATTR_ID: + self._obj['uid'], + ATTR_AREA: + self._properties_json.get('waterSense').get('area'), + ATTR_CURRENT_CYCLE: + self._obj.get('cycle'), ATTR_FIELD_CAPACITY: - self._properties_json.get( - 'waterSense').get('fieldCapacity'), - ATTR_NO_CYCLES: self._obj.get('noOfCycles'), + self._properties_json.get('waterSense') + .get('fieldCapacity'), + ATTR_NO_CYCLES: + self._obj.get('noOfCycles'), ATTR_PRECIP_RATE: - self._properties_json.get( - 'waterSense').get('precipitationRate'), - ATTR_RESTRICTIONS: self._obj.get('restriction'), - ATTR_SLOPE: SLOPE_TYPE_MAP.get( - self._properties_json.get('slope')), + self._properties_json.get('waterSense') + .get('precipitationRate'), + ATTR_RESTRICTIONS: + self._obj.get('restriction'), + ATTR_SLOPE: + SLOPE_TYPE_MAP.get(self._properties_json.get('slope')), ATTR_SOIL_TYPE: SOIL_TYPE_MAP.get(self._properties_json.get('sun')), ATTR_SPRINKLER_TYPE: @@ -308,7 +318,7 @@ class RainMachineZone(RainMachineSwitch): ATTR_VEGETATION_TYPE: VEGETATION_MAP.get(self._obj.get('type')), }) - except RainMachineError as exc_info: - _LOGGER.error('Unable to update info for zone "%s"', - self.unique_id) - _LOGGER.debug(exc_info) + except RequestError as err: + _LOGGER.error( + 'Unable to update info for zone "%s": %s', self.unique_id, + str(err)) diff --git a/requirements_all.txt b/requirements_all.txt index f47bbbdf23e..b941021d017 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1162,7 +1162,7 @@ raincloudy==0.0.4 # raspihats==2.2.3 # homeassistant.components.rainmachine -regenmaschine==0.4.2 +regenmaschine==1.0.2 # homeassistant.components.python_script restrictedpython==4.0b4 From ce7e9e36ddc7550d7022c8177b61fe81337ac119 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20S=C3=B8rensen?= Date: Sun, 10 Jun 2018 10:28:53 +0200 Subject: [PATCH 053/144] Add Uptime Robot sensor (#14631) * Added Uptime Robot sensor * added newline at the end and corrected doclink * Added changes form @cdce8p * Convert to binary_sensor * updated requirements * moved to correct dir * Update uptimerobot.py --- .coveragerc | 1 + .../components/binary_sensor/uptimerobot.py | 88 +++++++++++++++++++ requirements_all.txt | 3 + 3 files changed, 92 insertions(+) create mode 100644 homeassistant/components/binary_sensor/uptimerobot.py diff --git a/.coveragerc b/.coveragerc index 53dd7cdfe4e..4af9a767434 100644 --- a/.coveragerc +++ b/.coveragerc @@ -354,6 +354,7 @@ omit = homeassistant/components/binary_sensor/ping.py homeassistant/components/binary_sensor/rest.py homeassistant/components/binary_sensor/tapsaff.py + homeassistant/components/binary_sensor/uptimerobot.py homeassistant/components/browser.py homeassistant/components/calendar/caldav.py homeassistant/components/calendar/todoist.py diff --git a/homeassistant/components/binary_sensor/uptimerobot.py b/homeassistant/components/binary_sensor/uptimerobot.py new file mode 100644 index 00000000000..c50e5242256 --- /dev/null +++ b/homeassistant/components/binary_sensor/uptimerobot.py @@ -0,0 +1,88 @@ +"""A component to monitor Uptime Robot monitors. + +For more details about this component, please refer to the documentation at +https://www.home-assistant.io/components/binary_sensor.uptimerobot +""" +import logging + +import voluptuous as vol + +from homeassistant.components.binary_sensor import ( + BinarySensorDevice, PLATFORM_SCHEMA) +from homeassistant.const import CONF_API_KEY +import homeassistant.helpers.config_validation as cv + +REQUIREMENTS = ['pyuptimerobot==0.0.4'] + +_LOGGER = logging.getLogger(__name__) + +ATTR_TARGET = 'target' + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Required(CONF_API_KEY): cv.string, +}) + + +def setup_platform(hass, config, add_devices, discovery_info=None): + """Set up the Uptime Robot binary_sensors.""" + from pyuptimerobot import UptimeRobot + + up_robot = UptimeRobot() + apikey = config.get(CONF_API_KEY) + monitors = up_robot.getMonitors(apikey) + + devices = [] + if not monitors or monitors.get('stat') != 'ok': + _LOGGER.error('Error connecting to uptime robot.') + return + + for monitor in monitors['monitors']: + devices.append(UptimeRobotBinarySensor( + apikey, up_robot, monitorid=monitor['id'], + name=monitor['friendly_name'], target=monitor['url'])) + + add_devices(devices, True) + + +class UptimeRobotBinarySensor(BinarySensorDevice): + """Representation of a Uptime Robot binary_sensor.""" + + def __init__(self, apikey, up_robot, monitorid, name, target): + """Initialize the binary_sensor.""" + self._apikey = apikey + self._monitorid = str(monitorid) + self._name = name + self._target = target + self._up_robot = up_robot + self._state = None + + @property + def name(self): + """Return the name of the binary_sensor.""" + return self._name + + @property + def is_on(self): + """Return the state of the binary sensor.""" + return self._state + + @property + def device_class(self): + """Return the class of this device, from component DEVICE_CLASSES.""" + return 'connectivity' + + @property + def device_state_attributes(self): + """Return the state attributes of the binary_sensor.""" + return { + ATTR_TARGET: self._target, + } + + def update(self): + """Get the latest state of the binary_sensor.""" + monitor = self._up_robot.getMonitors(self._apikey, self._monitorid) + if not monitor or monitor.get('stat') != 'ok': + _LOGGER.debug("Failed to get new state, trying again later.") + return + status = monitor['monitors'][0]['status'] + self._state = 1 if status == 2 else 0 diff --git a/requirements_all.txt b/requirements_all.txt index b941021d017..0b1ef29eca4 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1119,6 +1119,9 @@ pyunifi==2.13 # homeassistant.components.upnp pyupnp-async==0.1.0.2 +# homeassistant.components.binary_sensor.uptimerobot +pyuptimerobot==0.0.4 + # homeassistant.components.keyboard # pyuserinput==0.1.11 From dc447a75c64b4d433c86fbac6a1881485ebe413a Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Sun, 10 Jun 2018 10:58:45 +0200 Subject: [PATCH 054/144] Upgrade pyuptimerobot to 0.0.5 --- .../components/binary_sensor/uptimerobot.py | 46 ++++++++++--------- requirements_all.txt | 2 +- 2 files changed, 26 insertions(+), 22 deletions(-) diff --git a/homeassistant/components/binary_sensor/uptimerobot.py b/homeassistant/components/binary_sensor/uptimerobot.py index c50e5242256..9e72d188c99 100644 --- a/homeassistant/components/binary_sensor/uptimerobot.py +++ b/homeassistant/components/binary_sensor/uptimerobot.py @@ -1,23 +1,26 @@ -"""A component to monitor Uptime Robot monitors. +""" +A platform that to monitor Uptime Robot monitors. -For more details about this component, please refer to the documentation at -https://www.home-assistant.io/components/binary_sensor.uptimerobot +For more details about this platform, please refer to the documentation at +https://www.home-assistant.io/components/binary_sensor.uptimerobot/ """ import logging import voluptuous as vol from homeassistant.components.binary_sensor import ( - BinarySensorDevice, PLATFORM_SCHEMA) -from homeassistant.const import CONF_API_KEY + PLATFORM_SCHEMA, BinarySensorDevice) +from homeassistant.const import ATTR_ATTRIBUTION, CONF_API_KEY import homeassistant.helpers.config_validation as cv -REQUIREMENTS = ['pyuptimerobot==0.0.4'] +REQUIREMENTS = ['pyuptimerobot==0.0.5'] _LOGGER = logging.getLogger(__name__) ATTR_TARGET = 'target' +CONF_ATTRIBUTION = "Data provided by Uptime Robot" + PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Required(CONF_API_KEY): cv.string, }) @@ -28,29 +31,29 @@ def setup_platform(hass, config, add_devices, discovery_info=None): from pyuptimerobot import UptimeRobot up_robot = UptimeRobot() - apikey = config.get(CONF_API_KEY) - monitors = up_robot.getMonitors(apikey) + api_key = config.get(CONF_API_KEY) + monitors = up_robot.getMonitors(api_key) devices = [] if not monitors or monitors.get('stat') != 'ok': - _LOGGER.error('Error connecting to uptime robot.') + _LOGGER.error("Error connecting to Uptime Robot") return for monitor in monitors['monitors']: devices.append(UptimeRobotBinarySensor( - apikey, up_robot, monitorid=monitor['id'], - name=monitor['friendly_name'], target=monitor['url'])) + api_key, up_robot, monitor['id'], monitor['friendly_name'], + monitor['url'])) add_devices(devices, True) class UptimeRobotBinarySensor(BinarySensorDevice): - """Representation of a Uptime Robot binary_sensor.""" + """Representation of a Uptime Robot binary sensor.""" - def __init__(self, apikey, up_robot, monitorid, name, target): - """Initialize the binary_sensor.""" - self._apikey = apikey - self._monitorid = str(monitorid) + def __init__(self, api_key, up_robot, monitor_id, name, target): + """Initialize Uptime Robot the binary sensor.""" + self._api_key = api_key + self._monitor_id = str(monitor_id) self._name = name self._target = target self._up_robot = up_robot @@ -58,7 +61,7 @@ class UptimeRobotBinarySensor(BinarySensorDevice): @property def name(self): - """Return the name of the binary_sensor.""" + """Return the name of the binary sensor.""" return self._name @property @@ -73,16 +76,17 @@ class UptimeRobotBinarySensor(BinarySensorDevice): @property def device_state_attributes(self): - """Return the state attributes of the binary_sensor.""" + """Return the state attributes of the binary sensor.""" return { + ATTR_ATTRIBUTION: CONF_ATTRIBUTION, ATTR_TARGET: self._target, } def update(self): - """Get the latest state of the binary_sensor.""" - monitor = self._up_robot.getMonitors(self._apikey, self._monitorid) + """Get the latest state of the binary sensor.""" + monitor = self._up_robot.getMonitors(self._api_key, self._monitor_id) if not monitor or monitor.get('stat') != 'ok': - _LOGGER.debug("Failed to get new state, trying again later.") + _LOGGER.warning("Failed to get new state") return status = monitor['monitors'][0]['status'] self._state = 1 if status == 2 else 0 diff --git a/requirements_all.txt b/requirements_all.txt index 0b1ef29eca4..9c48cea0d1c 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1120,7 +1120,7 @@ pyunifi==2.13 pyupnp-async==0.1.0.2 # homeassistant.components.binary_sensor.uptimerobot -pyuptimerobot==0.0.4 +pyuptimerobot==0.0.5 # homeassistant.components.keyboard # pyuserinput==0.1.11 From 5f4aa6d2ba1f0825773683ba92eb61f12ea2354d Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Sun, 10 Jun 2018 11:37:44 +0200 Subject: [PATCH 055/144] Upgrade python_opendata_transport to 0.1.3 (#14905) --- homeassistant/components/sensor/swiss_public_transport.py | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/sensor/swiss_public_transport.py b/homeassistant/components/sensor/swiss_public_transport.py index 928d84caa2b..72c6aa2e1a3 100644 --- a/homeassistant/components/sensor/swiss_public_transport.py +++ b/homeassistant/components/sensor/swiss_public_transport.py @@ -16,7 +16,7 @@ import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity import homeassistant.util.dt as dt_util -REQUIREMENTS = ['python_opendata_transport==0.1.0'] +REQUIREMENTS = ['python_opendata_transport==0.1.3'] _LOGGER = logging.getLogger(__name__) diff --git a/requirements_all.txt b/requirements_all.txt index 9c48cea0d1c..d721e18661f 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1090,7 +1090,7 @@ python-vlc==1.1.2 python-wink==1.7.3 # homeassistant.components.sensor.swiss_public_transport -python_opendata_transport==0.1.0 +python_opendata_transport==0.1.3 # homeassistant.components.zwave python_openzwave==0.4.3 From 54e87836f6a0e2212a692fbce424911cc9623799 Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Sun, 10 Jun 2018 11:37:58 +0200 Subject: [PATCH 056/144] Upgrade psutil to 5.4.6 (#14892) --- homeassistant/components/sensor/systemmonitor.py | 11 +++++------ requirements_all.txt | 2 +- 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/sensor/systemmonitor.py b/homeassistant/components/sensor/systemmonitor.py index 0b85de8e4f2..517ee6509f7 100644 --- a/homeassistant/components/sensor/systemmonitor.py +++ b/homeassistant/components/sensor/systemmonitor.py @@ -10,13 +10,12 @@ import os import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA -from homeassistant.const import ( - CONF_RESOURCES, STATE_OFF, STATE_ON, STATE_UNKNOWN, CONF_TYPE) +from homeassistant.const import CONF_RESOURCES, STATE_OFF, STATE_ON, CONF_TYPE from homeassistant.helpers.entity import Entity import homeassistant.helpers.config_validation as cv import homeassistant.util.dt as dt_util -REQUIREMENTS = ['psutil==5.4.5'] +REQUIREMENTS = ['psutil==5.4.6'] _LOGGER = logging.getLogger(__name__) @@ -157,19 +156,19 @@ class SystemMonitorSensor(Entity): counter = counters[self.argument][IO_COUNTER[self.type]] self._state = round(counter / 1024**2, 1) else: - self._state = STATE_UNKNOWN + self._state = None elif self.type == 'packets_out' or self.type == 'packets_in': counters = psutil.net_io_counters(pernic=True) if self.argument in counters: self._state = counters[self.argument][IO_COUNTER[self.type]] else: - self._state = STATE_UNKNOWN + self._state = None elif self.type == 'ipv4_address' or self.type == 'ipv6_address': addresses = psutil.net_if_addrs() if self.argument in addresses: self._state = addresses[self.argument][IF_ADDRS[self.type]][1] else: - self._state = STATE_UNKNOWN + self._state = None elif self.type == 'last_boot': self._state = dt_util.as_local( dt_util.utc_from_timestamp(psutil.boot_time()) diff --git a/requirements_all.txt b/requirements_all.txt index d721e18661f..df78c2b3747 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -672,7 +672,7 @@ proliphix==0.4.1 prometheus_client==0.1.0 # homeassistant.components.sensor.systemmonitor -psutil==5.4.5 +psutil==5.4.6 # homeassistant.components.wink pubnubsub-handler==1.0.2 From ce0ca7ff90345b45d4edc6ebec80e139696cdb5d Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Sun, 10 Jun 2018 11:38:11 +0200 Subject: [PATCH 057/144] Upgrade sendgrid to 5.4.0 (#14891) --- homeassistant/components/notify/sendgrid.py | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/notify/sendgrid.py b/homeassistant/components/notify/sendgrid.py index 89117397a53..b73f3a17ee7 100644 --- a/homeassistant/components/notify/sendgrid.py +++ b/homeassistant/components/notify/sendgrid.py @@ -14,7 +14,7 @@ from homeassistant.const import ( CONF_API_KEY, CONF_SENDER, CONF_RECIPIENT, CONTENT_TYPE_TEXT_PLAIN) import homeassistant.helpers.config_validation as cv -REQUIREMENTS = ['sendgrid==5.3.0'] +REQUIREMENTS = ['sendgrid==5.4.0'] _LOGGER = logging.getLogger(__name__) diff --git a/requirements_all.txt b/requirements_all.txt index df78c2b3747..17eb8455377 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1207,7 +1207,7 @@ schiene==0.22 scsgate==0.1.0 # homeassistant.components.notify.sendgrid -sendgrid==5.3.0 +sendgrid==5.4.0 # homeassistant.components.light.sensehat # homeassistant.components.sensor.sensehat From 703b4354e0a5e8e8a7d8a222ed2a19016b81bd3b Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Sun, 10 Jun 2018 11:38:23 +0200 Subject: [PATCH 058/144] Upgrade python-mystrom to 0.4.4 (#14889) --- homeassistant/components/light/mystrom.py | 14 +++++++------- homeassistant/components/switch/mystrom.py | 15 ++++++--------- requirements_all.txt | 2 +- 3 files changed, 14 insertions(+), 17 deletions(-) diff --git a/homeassistant/components/light/mystrom.py b/homeassistant/components/light/mystrom.py index 8d7fb807c6d..9abd96664f2 100644 --- a/homeassistant/components/light/mystrom.py +++ b/homeassistant/components/light/mystrom.py @@ -13,9 +13,9 @@ from homeassistant.components.light import ( Light, PLATFORM_SCHEMA, ATTR_BRIGHTNESS, SUPPORT_BRIGHTNESS, SUPPORT_EFFECT, ATTR_EFFECT, SUPPORT_FLASH, SUPPORT_COLOR, ATTR_HS_COLOR) -from homeassistant.const import CONF_HOST, CONF_MAC, CONF_NAME, STATE_UNKNOWN +from homeassistant.const import CONF_HOST, CONF_MAC, CONF_NAME -REQUIREMENTS = ['python-mystrom==0.4.2'] +REQUIREMENTS = ['python-mystrom==0.4.4'] _LOGGER = logging.getLogger(__name__) @@ -54,9 +54,9 @@ def setup_platform(hass, config, add_devices, discovery_info=None): try: if bulb.get_status()['type'] != 'rgblamp': _LOGGER.error("Device %s (%s) is not a myStrom bulb", host, mac) - return False + return except MyStromConnectionError: - _LOGGER.warning("myStrom bulb not online") + _LOGGER.warning("No route to device: %s", host) add_devices([MyStromLight(bulb, name)], True) @@ -107,7 +107,7 @@ class MyStromLight(Light): @property def is_on(self): """Return true if light is on.""" - return self._state['on'] if self._state is not None else STATE_UNKNOWN + return self._state['on'] if self._state is not None else None def turn_on(self, **kwargs): """Turn on the light.""" @@ -136,7 +136,7 @@ class MyStromLight(Light): if effect == EFFECT_RAINBOW: self._bulb.set_rainbow(30) except MyStromConnectionError: - _LOGGER.warning("myStrom bulb not online") + _LOGGER.warning("No route to device") def turn_off(self, **kwargs): """Turn off the bulb.""" @@ -163,5 +163,5 @@ class MyStromLight(Light): self._available = True except MyStromConnectionError: - _LOGGER.warning("myStrom bulb not online") + _LOGGER.warning("No route to device") self._available = False diff --git a/homeassistant/components/switch/mystrom.py b/homeassistant/components/switch/mystrom.py index 0a87d41d2fe..85fc546d00e 100644 --- a/homeassistant/components/switch/mystrom.py +++ b/homeassistant/components/switch/mystrom.py @@ -12,7 +12,7 @@ from homeassistant.components.switch import (SwitchDevice, PLATFORM_SCHEMA) from homeassistant.const import (CONF_NAME, CONF_HOST) import homeassistant.helpers.config_validation as cv -REQUIREMENTS = ['python-mystrom==0.4.2'] +REQUIREMENTS = ['python-mystrom==0.4.4'] DEFAULT_NAME = 'myStrom Switch' @@ -34,8 +34,8 @@ def setup_platform(hass, config, add_devices, discovery_info=None): try: MyStromPlug(host).get_status() except exceptions.MyStromConnectionError: - _LOGGER.error("No route to device '%s'", host) - return False + _LOGGER.error("No route to device: %s", host) + return add_devices([MyStromSwitch(name, host)]) @@ -74,8 +74,7 @@ class MyStromSwitch(SwitchDevice): try: self.plug.set_relay_on() except exceptions.MyStromConnectionError: - _LOGGER.error("No route to device '%s'. Is device offline?", - self._resource) + _LOGGER.error("No route to device: %s", self._resource) def turn_off(self, **kwargs): """Turn the switch off.""" @@ -83,8 +82,7 @@ class MyStromSwitch(SwitchDevice): try: self.plug.set_relay_off() except exceptions.MyStromConnectionError: - _LOGGER.error("No route to device '%s'. Is device offline?", - self._resource) + _LOGGER.error("No route to device: %s", self._resource) def update(self): """Get the latest data from the device and update the data.""" @@ -93,5 +91,4 @@ class MyStromSwitch(SwitchDevice): self.data = self.plug.get_status() except exceptions.MyStromConnectionError: self.data = {'power': 0, 'relay': False} - _LOGGER.error("No route to device '%s'. Is device offline?", - self._resource) + _LOGGER.error("No route to device: %s", self._resource) diff --git a/requirements_all.txt b/requirements_all.txt index 17eb8455377..7e91ccc59d3 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1045,7 +1045,7 @@ python-mpd2==1.0.0 # homeassistant.components.light.mystrom # homeassistant.components.switch.mystrom -python-mystrom==0.4.2 +python-mystrom==0.4.4 # homeassistant.components.nest python-nest==4.0.1 From 7d9ef97bdadbbd620a59fa94c5ee5fd088f5f3fe Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Sun, 10 Jun 2018 11:38:35 +0200 Subject: [PATCH 059/144] Upgrade pylast to 2.3.0 (#14888) --- homeassistant/components/sensor/lastfm.py | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/sensor/lastfm.py b/homeassistant/components/sensor/lastfm.py index 9fec4b4b5e3..5af81832523 100644 --- a/homeassistant/components/sensor/lastfm.py +++ b/homeassistant/components/sensor/lastfm.py @@ -13,7 +13,7 @@ from homeassistant.const import CONF_API_KEY import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity -REQUIREMENTS = ['pylast==2.2.0'] +REQUIREMENTS = ['pylast==2.3.0'] ATTR_LAST_PLAYED = 'last_played' ATTR_PLAY_COUNT = 'play_count' diff --git a/requirements_all.txt b/requirements_all.txt index 7e91ccc59d3..0a90e3b7a64 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -868,7 +868,7 @@ pykwb==0.0.8 pylacrosse==0.3.1 # homeassistant.components.sensor.lastfm -pylast==2.2.0 +pylast==2.3.0 # homeassistant.components.media_player.webostv # homeassistant.components.notify.webostv From 716ab0433f0cfaefebe5c1f93c70b488baac3445 Mon Sep 17 00:00:00 2001 From: Yevgeniy <33804747+sgttrs@users.noreply.github.com> Date: Sun, 10 Jun 2018 16:35:10 +0600 Subject: [PATCH 060/144] Added daily and hourly modes to Openweathermap (#14875) * Added daily and hourly modes Added wind speed and bearing to forecast * Fix mixed spaces and tabs * Fix lint * Fix pylint * Revert one attribution, order alphabetically --- .../components/weather/openweathermap.py | 74 ++++++++++++++----- 1 file changed, 54 insertions(+), 20 deletions(-) diff --git a/homeassistant/components/weather/openweathermap.py b/homeassistant/components/weather/openweathermap.py index 909f123b52c..8354757ff33 100644 --- a/homeassistant/components/weather/openweathermap.py +++ b/homeassistant/components/weather/openweathermap.py @@ -11,10 +11,10 @@ import voluptuous as vol from homeassistant.components.weather import ( ATTR_FORECAST_CONDITION, ATTR_FORECAST_PRECIPITATION, ATTR_FORECAST_TEMP, - ATTR_FORECAST_TIME, PLATFORM_SCHEMA, WeatherEntity) + ATTR_FORECAST_TEMP_LOW, ATTR_FORECAST_TIME, PLATFORM_SCHEMA, WeatherEntity) from homeassistant.const import ( - CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME, STATE_UNKNOWN, - TEMP_CELSIUS) + CONF_API_KEY, TEMP_CELSIUS, CONF_LATITUDE, CONF_LONGITUDE, CONF_MODE, + CONF_NAME, STATE_UNKNOWN) import homeassistant.helpers.config_validation as cv from homeassistant.util import Throttle @@ -22,20 +22,25 @@ REQUIREMENTS = ['pyowm==2.8.0'] _LOGGER = logging.getLogger(__name__) +ATTR_FORECAST_WIND_SPEED = 'wind_speed' +ATTR_FORECAST_WIND_BEARING = 'wind_bearing' + ATTRIBUTION = 'Data provided by OpenWeatherMap' +FORECAST_MODE = ['hourly', 'daily'] + DEFAULT_NAME = 'OpenWeatherMap' MIN_TIME_BETWEEN_FORECAST_UPDATES = timedelta(minutes=30) MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=10) CONDITION_CLASSES = { - 'cloudy': [804], + 'cloudy': [803, 804], 'fog': [701, 741], 'hail': [906], 'lightning': [210, 211, 212, 221], 'lightning-rainy': [200, 201, 202, 230, 231, 232], - 'partlycloudy': [801, 802, 803], + 'partlycloudy': [801, 802], 'pouring': [504, 314, 502, 503, 522], 'rainy': [300, 301, 302, 310, 311, 312, 313, 500, 501, 520, 521], 'snowy': [600, 601, 602, 611, 612, 620, 621, 622], @@ -51,6 +56,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Required(CONF_API_KEY): cv.string, vol.Optional(CONF_LATITUDE): cv.latitude, vol.Optional(CONF_LONGITUDE): cv.longitude, + vol.Optional(CONF_MODE, default='hourly'): vol.In(FORECAST_MODE), vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, }) @@ -62,6 +68,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): longitude = config.get(CONF_LONGITUDE, round(hass.config.longitude, 5)) latitude = config.get(CONF_LATITUDE, round(hass.config.latitude, 5)) name = config.get(CONF_NAME) + mode = config.get(CONF_MODE) try: owm = pyowm.OWM(config.get(CONF_API_KEY)) @@ -69,20 +76,21 @@ def setup_platform(hass, config, add_devices, discovery_info=None): _LOGGER.error("Error while connecting to OpenWeatherMap") return False - data = WeatherData(owm, latitude, longitude) + data = WeatherData(owm, latitude, longitude, mode) add_devices([OpenWeatherMapWeather( - name, data, hass.config.units.temperature_unit)], True) + name, data, hass.config.units.temperature_unit, mode)], True) class OpenWeatherMapWeather(WeatherEntity): """Implementation of an OpenWeatherMap sensor.""" - def __init__(self, name, owm, temperature_unit): + def __init__(self, name, owm, temperature_unit, mode): """Initialize the sensor.""" self._name = name self._owm = owm self._temperature_unit = temperature_unit + self._mode = mode self.data = None self.forecast_data = None @@ -140,15 +148,34 @@ class OpenWeatherMapWeather(WeatherEntity): """Return the forecast array.""" data = [] for entry in self.forecast_data.get_weathers(): - data.append({ - ATTR_FORECAST_TIME: entry.get_reference_time('unix') * 1000, - ATTR_FORECAST_TEMP: - entry.get_temperature('celsius').get('temp'), - ATTR_FORECAST_PRECIPITATION: entry.get_rain().get('3h'), - ATTR_FORECAST_CONDITION: - [k for k, v in CONDITION_CLASSES.items() - if entry.get_weather_code() in v][0] - }) + if self._mode == 'daily': + data.append({ + ATTR_FORECAST_TIME: + entry.get_reference_time('unix') * 1000, + ATTR_FORECAST_TEMP: + entry.get_temperature('celsius').get('day'), + ATTR_FORECAST_TEMP_LOW: + entry.get_temperature('celsius').get('night'), + ATTR_FORECAST_WIND_SPEED: + entry.get_wind().get('speed'), + ATTR_FORECAST_WIND_BEARING: + entry.get_wind().get('deg'), + ATTR_FORECAST_CONDITION: + [k for k, v in CONDITION_CLASSES.items() + if entry.get_weather_code() in v][0] + }) + else: + data.append({ + ATTR_FORECAST_TIME: + entry.get_reference_time('unix') * 1000, + ATTR_FORECAST_TEMP: + entry.get_temperature('celsius').get('temp'), + ATTR_FORECAST_PRECIPITATION: + entry.get_rain().get('3h'), + ATTR_FORECAST_CONDITION: + [k for k, v in CONDITION_CLASSES.items() + if entry.get_weather_code() in v][0] + }) return data def update(self): @@ -169,8 +196,9 @@ class OpenWeatherMapWeather(WeatherEntity): class WeatherData(object): """Get the latest data from OpenWeatherMap.""" - def __init__(self, owm, latitude, longitude): + def __init__(self, owm, latitude, longitude, mode): """Initialize the data object.""" + self._mode = mode self.owm = owm self.latitude = latitude self.longitude = longitude @@ -193,8 +221,14 @@ class WeatherData(object): from pyowm.exceptions.api_call_error import APICallError try: - fcd = self.owm.three_hours_forecast_at_coords( - self.latitude, self.longitude) + if self._mode == 'daily': + fcd = self.owm.daily_forecast_at_coords( + self.latitude, self.longitude, 15 + ) + else: + fcd = self.owm.three_hours_forecast_at_coords( + self.latitude, self.longitude + ) except APICallError: _LOGGER.error("Exception when calling OWM web API " "to update forecast") From b4e5695bbde406ff857acb8364b5c69e407510f3 Mon Sep 17 00:00:00 2001 From: Dan Klaffenbach Date: Sun, 10 Jun 2018 12:00:14 +0100 Subject: [PATCH 061/144] Bump to denonavr 0.7.3 (#14907) Closes #14792 See #14794 --- homeassistant/components/media_player/denonavr.py | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/media_player/denonavr.py b/homeassistant/components/media_player/denonavr.py index 74d3c5a0785..8cd47476058 100644 --- a/homeassistant/components/media_player/denonavr.py +++ b/homeassistant/components/media_player/denonavr.py @@ -20,7 +20,7 @@ from homeassistant.const import ( CONF_NAME, STATE_ON, CONF_ZONE, CONF_TIMEOUT) import homeassistant.helpers.config_validation as cv -REQUIREMENTS = ['denonavr==0.7.2'] +REQUIREMENTS = ['denonavr==0.7.3'] _LOGGER = logging.getLogger(__name__) diff --git a/requirements_all.txt b/requirements_all.txt index 0a90e3b7a64..a675d08b01c 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -249,7 +249,7 @@ defusedxml==0.5.0 deluge-client==1.4.0 # homeassistant.components.media_player.denonavr -denonavr==0.7.2 +denonavr==0.7.3 # homeassistant.components.media_player.directv directpy==0.5 From d5bbb6ffd2e4bd960116abc24a331ca429b3f0c5 Mon Sep 17 00:00:00 2001 From: Nate Clark Date: Sun, 10 Jun 2018 07:50:25 -0400 Subject: [PATCH 062/144] Add api_host option to Konnected config (#14896) --- homeassistant/components/konnected.py | 24 +++++++++++++++++++++--- 1 file changed, 21 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/konnected.py b/homeassistant/components/konnected.py index 70b66f84ae9..5b28b7b0999 100644 --- a/homeassistant/components/konnected.py +++ b/homeassistant/components/konnected.py @@ -31,6 +31,7 @@ REQUIREMENTS = ['konnected==0.1.2'] DOMAIN = 'konnected' CONF_ACTIVATION = 'activation' +CONF_API_HOST = 'api_host' STATE_LOW = 'low' STATE_HIGH = 'high' @@ -56,10 +57,12 @@ _SWITCH_SCHEMA = vol.All( }), cv.has_at_least_one_key(CONF_PIN, CONF_ZONE) ) +# pylint: disable=no-value-for-parameter CONFIG_SCHEMA = vol.Schema( { DOMAIN: vol.Schema({ vol.Required(CONF_ACCESS_TOKEN): cv.string, + vol.Optional(CONF_API_HOST): vol.Url(), vol.Required(CONF_DEVICES): [{ vol.Required(CONF_ID): cv.string, vol.Optional(CONF_BINARY_SENSORS): vol.All( @@ -87,7 +90,10 @@ async def async_setup(hass, config): access_token = cfg.get(CONF_ACCESS_TOKEN) if DOMAIN not in hass.data: - hass.data[DOMAIN] = {CONF_ACCESS_TOKEN: access_token} + hass.data[DOMAIN] = { + CONF_ACCESS_TOKEN: access_token, + CONF_API_HOST: cfg.get(CONF_API_HOST) + } def device_discovered(service, info): """Call when a Konnected device has been discovered.""" @@ -254,14 +260,26 @@ class KonnectedDevice(object): _LOGGER.debug('%s: current actuator config: %s', self.device_id, current_actuator_config) + desired_api_host = \ + self.hass.data[DOMAIN].get(CONF_API_HOST) or \ + self.hass.config.api.base_url + desired_api_endpoint = desired_api_host + ENDPOINT_ROOT + current_api_endpoint = self.status.get('endpoint') + + _LOGGER.debug('%s: desired api endpoint: %s', self.device_id, + desired_api_endpoint) + _LOGGER.debug('%s: current api endpoint: %s', self.device_id, + current_api_endpoint) + if (desired_sensor_configuration != current_sensor_configuration) or \ - (current_actuator_config != desired_actuator_config): + (current_actuator_config != desired_actuator_config) or \ + (current_api_endpoint != desired_api_endpoint): _LOGGER.debug('pushing settings to device %s', self.device_id) self.client.put_settings( desired_sensor_configuration, desired_actuator_config, self.hass.data[DOMAIN].get(CONF_ACCESS_TOKEN), - self.hass.config.api.base_url + ENDPOINT_ROOT + desired_api_endpoint ) From 1da30032a0e18661a45d16469484a924c48429a8 Mon Sep 17 00:00:00 2001 From: Ben Lebherz Date: Sun, 10 Jun 2018 15:38:55 +0200 Subject: [PATCH 063/144] Add support for the Unitymedia Horizon HD Recorder (#14275) * added new platform for the Unitymedia Horizon HD Recorder * improve connection handling of the horizon platform * remove unneeded parameters and fix spelling in the horizon platform * abort or raise exception if connection to the device could not be established * remove channel/source list and SELECT_SOURCE feature * remove useless type check after cast and use a try block instead * abort or raise exception if reconnect to device fails * remove protocol specific code and restructure sending logic accordingly * fix indentation to be pep8 complaint * remove unused methods/properties * fix unnecessary pylint commands and use a return to abert outside of setup_platform * directly access config values --- .coveragerc | 1 + .../components/media_player/horizon.py | 187 ++++++++++++++++++ requirements_all.txt | 3 + 3 files changed, 191 insertions(+) create mode 100644 homeassistant/components/media_player/horizon.py diff --git a/.coveragerc b/.coveragerc index 4af9a767434..5a78ec8093f 100644 --- a/.coveragerc +++ b/.coveragerc @@ -493,6 +493,7 @@ omit = homeassistant/components/media_player/frontier_silicon.py homeassistant/components/media_player/gpmdp.py homeassistant/components/media_player/gstreamer.py + homeassistant/components/media_player/horizon.py homeassistant/components/media_player/itunes.py homeassistant/components/media_player/kodi.py homeassistant/components/media_player/lg_netcast.py diff --git a/homeassistant/components/media_player/horizon.py b/homeassistant/components/media_player/horizon.py new file mode 100644 index 00000000000..4b0f9d0cf21 --- /dev/null +++ b/homeassistant/components/media_player/horizon.py @@ -0,0 +1,187 @@ +""" +Support for the Unitymedia Horizon HD Recorder. + +For more details about this platform, please refer to the documentation +https://home-assistant.io/components/media_player.horizon/ +""" + +from datetime import timedelta +import logging + +import voluptuous as vol + +from homeassistant.components.media_player import ( + MediaPlayerDevice, PLATFORM_SCHEMA, MEDIA_TYPE_CHANNEL, + SUPPORT_NEXT_TRACK, SUPPORT_TURN_ON, SUPPORT_TURN_OFF, SUPPORT_PAUSE, + SUPPORT_PLAY, SUPPORT_PLAY_MEDIA, SUPPORT_PREVIOUS_TRACK) +from homeassistant.const import (CONF_HOST, CONF_NAME, CONF_PORT, STATE_OFF, + STATE_PAUSED, STATE_PLAYING) +from homeassistant.exceptions import PlatformNotReady +import homeassistant.helpers.config_validation as cv +import homeassistant.util as util + +REQUIREMENTS = ['einder==0.3.1'] + +_LOGGER = logging.getLogger(__name__) + +DEFAULT_NAME = "Horizon" +DEFAULT_PORT = 5900 + +MIN_TIME_BETWEEN_FORCED_SCANS = timedelta(seconds=1) +MIN_TIME_BETWEEN_SCANS = timedelta(seconds=10) + +SUPPORT_HORIZON = SUPPORT_NEXT_TRACK | SUPPORT_PAUSE | SUPPORT_PLAY | \ + SUPPORT_PLAY_MEDIA | SUPPORT_PREVIOUS_TRACK | SUPPORT_TURN_ON | \ + SUPPORT_TURN_OFF + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Required(CONF_HOST): cv.string, + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, +}) + + +def setup_platform(hass, config, add_devices, discovery_info=None): + """Set up the Horizon platform.""" + from einder import Client, keys + from einder.exceptions import AuthenticationError + + host = config[CONF_HOST] + name = config[CONF_NAME] + port = config[CONF_PORT] + + try: + client = Client(host, port=port) + except AuthenticationError as msg: + _LOGGER.error("Authentication to %s at %s failed: %s", name, host, msg) + return + except OSError as msg: + # occurs if horizon box is offline + _LOGGER.error("Connection to %s at %s failed: %s", name, host, msg) + raise PlatformNotReady + + _LOGGER.info("Connection to %s at %s established", name, host) + + add_devices([HorizonDevice(client, name, keys)], True) + + +class HorizonDevice(MediaPlayerDevice): + """Representation of a Horizon HD Recorder.""" + + def __init__(self, client, name, keys): + """Initialize the remote.""" + self._client = client + self._name = name + self._state = None + self._keys = keys + + @property + def name(self): + """Return the name of the remote.""" + return self._name + + @property + def state(self): + """Return the state of the device.""" + return self._state + + @property + def supported_features(self): + """Flag media player features that are supported.""" + return SUPPORT_HORIZON + + @util.Throttle(MIN_TIME_BETWEEN_SCANS, MIN_TIME_BETWEEN_FORCED_SCANS) + def update(self): + """Update State using the media server running on the Horizon.""" + if self._client.is_powered_on(): + self._state = STATE_PLAYING + else: + self._state = STATE_OFF + + def turn_on(self): + """Turn the device on.""" + if self._state is STATE_OFF: + self._send_key(self._keys.POWER) + + def turn_off(self): + """Turn the device off.""" + if self._state is not STATE_OFF: + self._send_key(self._keys.POWER) + + def media_previous_track(self): + """Channel down.""" + self._send_key(self._keys.CHAN_DOWN) + self._state = STATE_PLAYING + + def media_next_track(self): + """Channel up.""" + self._send_key(self._keys.CHAN_UP) + self._state = STATE_PLAYING + + def media_play(self): + """Send play command.""" + self._send_key(self._keys.PAUSE) + self._state = STATE_PLAYING + + def media_pause(self): + """Send pause command.""" + self._send_key(self._keys.PAUSE) + self._state = STATE_PAUSED + + def media_play_pause(self): + """Send play/pause command.""" + self._send_key(self._keys.PAUSE) + if self._state == STATE_PAUSED: + self._state = STATE_PLAYING + else: + self._state = STATE_PAUSED + + def play_media(self, media_type, media_id, **kwargs): + """Play media / switch to channel.""" + if MEDIA_TYPE_CHANNEL == media_type: + try: + self._select_channel(int(media_id)) + self._state = STATE_PLAYING + except ValueError: + _LOGGER.error("Invalid channel: %s", media_id) + else: + _LOGGER.error("Invalid media type %s. Supported type: %s", + media_type, MEDIA_TYPE_CHANNEL) + + def _select_channel(self, channel): + """Select a channel (taken from einder library, thx).""" + self._send(channel=channel) + + def _send_key(self, key): + """Send a key to the Horizon device.""" + self._send(key=key) + + def _send(self, key=None, channel=None): + """Send a key to the Horizon device.""" + from einder.exceptions import AuthenticationError + + try: + if key: + self._client.send_key(key) + elif channel: + self._client.select_channel(channel) + except OSError as msg: + _LOGGER.error("%s disconnected: %s. Trying to reconnect...", + self._name, msg) + + # for reconnect, first gracefully disconnect + self._client.disconnect() + + try: + self._client.connect() + self._client.authorize() + except AuthenticationError as msg: + _LOGGER.error("Authentication to %s failed: %s", self._name, + msg) + return + except OSError as msg: + # occurs if horizon box is offline + _LOGGER.error("Reconnect to %s failed: %s", self._name, msg) + return + + self._send(key=key, channel=channel) diff --git a/requirements_all.txt b/requirements_all.txt index a675d08b01c..478a0cb5479 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -279,6 +279,9 @@ dsmr_parser==0.11 # homeassistant.components.sensor.dweet dweepy==0.3.0 +# homeassistant.components.media_player.horizon +einder==0.3.1 + # homeassistant.components.sensor.eliqonline eliqonline==1.0.14 From 1c561eaf0dd67d62ee33ec204e5ed07a1ecf3792 Mon Sep 17 00:00:00 2001 From: Matt Snyder Date: Sun, 10 Jun 2018 12:02:44 -0500 Subject: [PATCH 064/144] Add support for multiple Doorbird stations (#13994) --- homeassistant/components/camera/doorbird.py | 32 ++-- homeassistant/components/doorbird.py | 159 +++++++++++++++----- homeassistant/components/switch/doorbird.py | 29 ++-- 3 files changed, 157 insertions(+), 63 deletions(-) diff --git a/homeassistant/components/camera/doorbird.py b/homeassistant/components/camera/doorbird.py index 034ddc2fabb..6680258d95d 100644 --- a/homeassistant/components/camera/doorbird.py +++ b/homeassistant/components/camera/doorbird.py @@ -17,9 +17,9 @@ from homeassistant.helpers.aiohttp_client import async_get_clientsession DEPENDENCIES = ['doorbird'] -_CAMERA_LAST_VISITOR = "DoorBird Last Ring" -_CAMERA_LAST_MOTION = "DoorBird Last Motion" -_CAMERA_LIVE = "DoorBird Live" +_CAMERA_LAST_VISITOR = "{} Last Ring" +_CAMERA_LAST_MOTION = "{} Last Motion" +_CAMERA_LIVE = "{} Live" _LAST_VISITOR_INTERVAL = datetime.timedelta(minutes=1) _LAST_MOTION_INTERVAL = datetime.timedelta(minutes=1) _LIVE_INTERVAL = datetime.timedelta(seconds=1) @@ -30,16 +30,22 @@ _TIMEOUT = 10 # seconds @asyncio.coroutine def async_setup_platform(hass, config, async_add_devices, discovery_info=None): """Set up the DoorBird camera platform.""" - device = hass.data.get(DOORBIRD_DOMAIN) - async_add_devices([ - DoorBirdCamera(device.live_image_url, _CAMERA_LIVE, _LIVE_INTERVAL), - DoorBirdCamera( - device.history_image_url(1, 'doorbell'), _CAMERA_LAST_VISITOR, - _LAST_VISITOR_INTERVAL), - DoorBirdCamera( - device.history_image_url(1, 'motionsensor'), _CAMERA_LAST_MOTION, - _LAST_MOTION_INTERVAL), - ]) + for doorstation in hass.data[DOORBIRD_DOMAIN]: + device = doorstation.device + async_add_devices([ + DoorBirdCamera( + device.live_image_url, + _CAMERA_LIVE.format(doorstation.name), + _LIVE_INTERVAL), + DoorBirdCamera( + device.history_image_url(1, 'doorbell'), + _CAMERA_LAST_VISITOR.format(doorstation.name), + _LAST_VISITOR_INTERVAL), + DoorBirdCamera( + device.history_image_url(1, 'motionsensor'), + _CAMERA_LAST_MOTION.format(doorstation.name), + _LAST_MOTION_INTERVAL), + ]) class DoorBirdCamera(Camera): diff --git a/homeassistant/components/doorbird.py b/homeassistant/components/doorbird.py index 48f229b49ca..6cd820816e2 100644 --- a/homeassistant/components/doorbird.py +++ b/homeassistant/components/doorbird.py @@ -4,14 +4,16 @@ Support for DoorBird device. For more details about this component, please refer to the documentation at https://home-assistant.io/components/doorbird/ """ -import asyncio import logging +import asyncio import voluptuous as vol -from homeassistant.const import CONF_HOST, CONF_USERNAME, CONF_PASSWORD from homeassistant.components.http import HomeAssistantView +from homeassistant.const import CONF_HOST, CONF_USERNAME, \ + CONF_PASSWORD, CONF_NAME, CONF_DEVICES, CONF_MONITORED_CONDITIONS import homeassistant.helpers.config_validation as cv +from homeassistant.util import slugify REQUIREMENTS = ['DoorBirdPy==0.1.3'] @@ -24,60 +26,139 @@ API_URL = '/api/{}'.format(DOMAIN) CONF_DOORBELL_EVENTS = 'doorbell_events' CONF_CUSTOM_URL = 'hass_url_override' +DOORBELL_EVENT = 'doorbell' +MOTION_EVENT = 'motionsensor' + +# Sensor types: Name, device_class, event +SENSOR_TYPES = { + 'doorbell': ['Button', 'occupancy', DOORBELL_EVENT], + 'motion': ['Motion', 'motion', MOTION_EVENT], +} + +DEVICE_SCHEMA = vol.Schema({ + vol.Required(CONF_HOST): cv.string, + vol.Required(CONF_USERNAME): cv.string, + vol.Required(CONF_PASSWORD): cv.string, + vol.Optional(CONF_CUSTOM_URL): cv.string, + vol.Optional(CONF_NAME): cv.string, + vol.Optional(CONF_MONITORED_CONDITIONS, default=[]): + vol.All(cv.ensure_list, [vol.In(SENSOR_TYPES)]), +}) + CONFIG_SCHEMA = vol.Schema({ DOMAIN: vol.Schema({ - vol.Required(CONF_HOST): cv.string, - vol.Required(CONF_USERNAME): cv.string, - vol.Required(CONF_PASSWORD): cv.string, - vol.Optional(CONF_DOORBELL_EVENTS): cv.boolean, - vol.Optional(CONF_CUSTOM_URL): cv.string, - }) + vol.Required(CONF_DEVICES): vol.All(cv.ensure_list, [DEVICE_SCHEMA]) + }), }, extra=vol.ALLOW_EXTRA) -SENSOR_DOORBELL = 'doorbell' - def setup(hass, config): """Set up the DoorBird component.""" from doorbirdpy import DoorBird - device_ip = config[DOMAIN].get(CONF_HOST) - username = config[DOMAIN].get(CONF_USERNAME) - password = config[DOMAIN].get(CONF_PASSWORD) + # Provide an endpoint for the doorstations to call to trigger events + hass.http.register_view(DoorbirdRequestView()) - device = DoorBird(device_ip, username, password) - status = device.ready() + doorstations = [] - if status[0]: - _LOGGER.info("Connected to DoorBird at %s as %s", device_ip, username) - hass.data[DOMAIN] = device - elif status[1] == 401: - _LOGGER.error("Authorization rejected by DoorBird at %s", device_ip) - return False - else: - _LOGGER.error("Could not connect to DoorBird at %s: Error %s", - device_ip, str(status[1])) - return False + for index, doorstation_config in enumerate(config[DOMAIN][CONF_DEVICES]): + device_ip = doorstation_config.get(CONF_HOST) + username = doorstation_config.get(CONF_USERNAME) + password = doorstation_config.get(CONF_PASSWORD) + custom_url = doorstation_config.get(CONF_CUSTOM_URL) + events = doorstation_config.get(CONF_MONITORED_CONDITIONS) + name = (doorstation_config.get(CONF_NAME) + or 'DoorBird {}'.format(index + 1)) - if config[DOMAIN].get(CONF_DOORBELL_EVENTS): - # Provide an endpoint for the device to call to trigger events - hass.http.register_view(DoorbirdRequestView()) + device = DoorBird(device_ip, username, password) + status = device.ready() + + if status[0]: + _LOGGER.info("Connected to DoorBird at %s as %s", device_ip, + username) + doorstation = ConfiguredDoorbird(device, name, events, custom_url) + doorstations.append(doorstation) + elif status[1] == 401: + _LOGGER.error("Authorization rejected by DoorBird at %s", + device_ip) + return False + else: + _LOGGER.error("Could not connect to DoorBird at %s: Error %s", + device_ip, str(status[1])) + return False + + # SETUP EVENT SUBSCRIBERS + if events is not None: + # This will make HA the only service that receives events. + doorstation.device.reset_notifications() + + # Subscribe to doorbell or motion events + subscribe_events(hass, doorstation) + + hass.data[DOMAIN] = doorstations + + return True + + +def subscribe_events(hass, doorstation): + """Initialize the subscriber.""" + for sensor_type in doorstation.monitored_events: + name = '{} {}'.format(doorstation.name, + SENSOR_TYPES[sensor_type][0]) + event_type = SENSOR_TYPES[sensor_type][2] # Get the URL of this server hass_url = hass.config.api.base_url - # Override it if another is specified in the component configuration - if config[DOMAIN].get(CONF_CUSTOM_URL): - hass_url = config[DOMAIN].get(CONF_CUSTOM_URL) - _LOGGER.info("DoorBird will connect to this instance via %s", - hass_url) + # Override url if another is specified onth configuration + if doorstation.custom_url is not None: + hass_url = doorstation.custom_url - # This will make HA the only service that gets doorbell events - url = '{}{}/{}'.format(hass_url, API_URL, SENSOR_DOORBELL) - device.reset_notifications() - device.subscribe_notification(SENSOR_DOORBELL, url) + slug = slugify(name) - return True + url = '{}{}/{}'.format(hass_url, API_URL, slug) + + _LOGGER.info("DoorBird will connect to this instance via %s", + url) + + _LOGGER.info("You may use the following event name for automations" + ": %s_%s", DOMAIN, slug) + + doorstation.device.subscribe_notification(event_type, url) + + +class ConfiguredDoorbird(): + """Attach additional information to pass along with configured device.""" + + def __init__(self, device, name, events=None, custom_url=None): + """Initialize configured device.""" + self._name = name + self._device = device + self._custom_url = custom_url + self._monitored_events = events + + @property + def name(self): + """Custom device name.""" + return self._name + + @property + def device(self): + """The configured device.""" + return self._device + + @property + def custom_url(self): + """Custom url for device.""" + return self._custom_url + + @property + def monitored_events(self): + """Get monitored events.""" + if self._monitored_events is None: + return [] + + return self._monitored_events class DoorbirdRequestView(HomeAssistantView): @@ -93,5 +174,7 @@ class DoorbirdRequestView(HomeAssistantView): def get(self, request, sensor): """Respond to requests from the device.""" hass = request.app['hass'] + hass.bus.async_fire('{}_{}'.format(DOMAIN, sensor)) + return 'OK' diff --git a/homeassistant/components/switch/doorbird.py b/homeassistant/components/switch/doorbird.py index 9886b3a586d..92ba3640237 100644 --- a/homeassistant/components/switch/doorbird.py +++ b/homeassistant/components/switch/doorbird.py @@ -4,10 +4,10 @@ import logging import voluptuous as vol +import homeassistant.helpers.config_validation as cv from homeassistant.components.doorbird import DOMAIN as DOORBIRD_DOMAIN from homeassistant.components.switch import PLATFORM_SCHEMA, SwitchDevice from homeassistant.const import CONF_SWITCHES -import homeassistant.helpers.config_validation as cv DEPENDENCIES = ['doorbird'] @@ -15,7 +15,7 @@ _LOGGER = logging.getLogger(__name__) SWITCHES = { "open_door": { - "name": "Open Door", + "name": "{} Open Door", "icon": { True: "lock-open", False: "lock" @@ -23,7 +23,7 @@ SWITCHES = { "time": datetime.timedelta(seconds=3) }, "open_door_2": { - "name": "Open Door 2", + "name": "{} Open Door 2", "icon": { True: "lock-open", False: "lock" @@ -31,7 +31,7 @@ SWITCHES = { "time": datetime.timedelta(seconds=3) }, "light_on": { - "name": "Light On", + "name": "{} Light On", "icon": { True: "lightbulb-on", False: "lightbulb" @@ -48,31 +48,36 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ def setup_platform(hass, config, add_devices, discovery_info=None): """Set up the DoorBird switch platform.""" - device = hass.data.get(DOORBIRD_DOMAIN) - switches = [] - for switch in SWITCHES: - _LOGGER.debug("Adding DoorBird switch %s", SWITCHES[switch]["name"]) - switches.append(DoorBirdSwitch(device, switch)) + + for doorstation in hass.data[DOORBIRD_DOMAIN]: + + device = doorstation.device + + for switch in SWITCHES: + + _LOGGER.debug("Adding DoorBird switch %s", + SWITCHES[switch]["name"].format(doorstation.name)) + switches.append(DoorBirdSwitch(device, switch, doorstation.name)) add_devices(switches) - _LOGGER.info("Added DoorBird switches") class DoorBirdSwitch(SwitchDevice): """A relay in a DoorBird device.""" - def __init__(self, device, switch): + def __init__(self, device, switch, name): """Initialize a relay in a DoorBird device.""" self._device = device self._switch = switch + self._name = name self._state = False self._assume_off = datetime.datetime.min @property def name(self): """Return the name of the switch.""" - return SWITCHES[self._switch]["name"] + return SWITCHES[self._switch]["name"].format(self._name) @property def icon(self): From 576c806e86964d8bcb8dcec0f7a62e008512c8f7 Mon Sep 17 00:00:00 2001 From: Erik Eriksson <8228319+molobrakos@users.noreply.github.com> Date: Mon, 11 Jun 2018 15:29:04 +0200 Subject: [PATCH 065/144] Update mqtt_eventstream.py (#14923) * Update mqtt_eventstream.py Remove a line setting an internal state mqtt_eventstream.initialized to True since: 1. No other platform is doing this 2. This will create an annoying entity/item in the user interface which the user will have to explicitly hide * Update mqtt_eventstream.py --- homeassistant/components/mqtt_eventstream.py | 1 - 1 file changed, 1 deletion(-) diff --git a/homeassistant/components/mqtt_eventstream.py b/homeassistant/components/mqtt_eventstream.py index aa670578172..ea4463f5c23 100644 --- a/homeassistant/components/mqtt_eventstream.py +++ b/homeassistant/components/mqtt_eventstream.py @@ -116,5 +116,4 @@ def async_setup(hass, config): if sub_topic: yield from mqtt.async_subscribe(sub_topic, _event_receiver) - hass.states.async_set('{domain}.initialized'.format(domain=DOMAIN), True) return True From 30111ea417cdf44361910f82cf6576632d4935e9 Mon Sep 17 00:00:00 2001 From: Jason Hu Date: Mon, 11 Jun 2018 22:28:16 -0700 Subject: [PATCH 066/144] Upgrade python-nest, add security_state sensor, nest.set_mode service set ETA as well (#14901) --- homeassistant/components/nest.py | 35 ++++++++++++++++++++++--- homeassistant/components/sensor/nest.py | 2 +- homeassistant/components/services.yaml | 25 ++++++++++++++++++ requirements_all.txt | 2 +- 4 files changed, 58 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/nest.py b/homeassistant/components/nest.py index 16a0b80d1fd..739572223c6 100644 --- a/homeassistant/components/nest.py +++ b/homeassistant/components/nest.py @@ -7,6 +7,7 @@ https://home-assistant.io/components/nest/ from concurrent.futures import ThreadPoolExecutor import logging import socket +from datetime import datetime, timedelta import voluptuous as vol @@ -19,7 +20,7 @@ from homeassistant.helpers.dispatcher import async_dispatcher_send, \ async_dispatcher_connect from homeassistant.helpers.entity import Entity -REQUIREMENTS = ['python-nest==4.0.1'] +REQUIREMENTS = ['python-nest==4.0.2'] _CONFIGURING = {} _LOGGER = logging.getLogger(__name__) @@ -36,14 +37,23 @@ CONF_CLIENT_SECRET = 'client_secret' ATTR_HOME_MODE = 'home_mode' ATTR_STRUCTURE = 'structure' +ATTR_TRIP_ID = 'trip_id' +ATTR_ETA = 'eta' +ATTR_ETA_WINDOW = 'eta_window' + +HOME_MODE_AWAY = 'away' +HOME_MODE_HOME = 'home' SENSOR_SCHEMA = vol.Schema({ vol.Optional(CONF_MONITORED_CONDITIONS): vol.All(cv.ensure_list) }) AWAY_SCHEMA = vol.Schema({ - vol.Required(ATTR_HOME_MODE): cv.string, - vol.Optional(ATTR_STRUCTURE): vol.All(cv.ensure_list, cv.string) + vol.Required(ATTR_HOME_MODE): vol.In([HOME_MODE_AWAY, HOME_MODE_HOME]), + vol.Optional(ATTR_STRUCTURE): vol.All(cv.ensure_list, cv.string), + vol.Optional(ATTR_TRIP_ID): cv.string, + vol.Optional(ATTR_ETA): cv.time_period_str, + vol.Optional(ATTR_ETA_WINDOW): cv.time_period_str }) CONFIG_SCHEMA = vol.Schema({ @@ -148,7 +158,11 @@ async def async_setup_nest(hass, nest, config, pin=None): hass, component, DOMAIN, discovered, config) def set_mode(service): - """Set the home/away mode for a Nest structure.""" + """ + Set the home/away mode for a Nest structure. + + You can set optional eta information when set mode to away. + """ if ATTR_STRUCTURE in service.data: structures = service.data[ATTR_STRUCTURE] else: @@ -158,6 +172,19 @@ async def async_setup_nest(hass, nest, config, pin=None): if structure.name in structures: _LOGGER.info("Setting mode for %s", structure.name) structure.away = service.data[ATTR_HOME_MODE] + + if service.data[ATTR_HOME_MODE] == HOME_MODE_AWAY \ + and ATTR_ETA in service.data: + now = datetime.utcnow() + eta_begin = now + service.data[ATTR_ETA] + eta_window = service.data.get(ATTR_ETA_WINDOW, + timedelta(minutes=1)) + eta_end = eta_begin + eta_window + trip_id = service.data.get( + ATTR_TRIP_ID, "trip_{}".format(int(now.timestamp()))) + _LOGGER.info("Setting eta for %s, eta window starts at " + "%s ends at %s", trip_id, eta_begin, eta_end) + structure.set_eta(trip_id, eta_begin, eta_end) else: _LOGGER.error("Invalid structure %s", service.data[ATTR_STRUCTURE]) diff --git a/homeassistant/components/sensor/nest.py b/homeassistant/components/sensor/nest.py index 88464675c21..ea7a943881e 100644 --- a/homeassistant/components/sensor/nest.py +++ b/homeassistant/components/sensor/nest.py @@ -23,7 +23,7 @@ PROTECT_SENSOR_TYPES = ['co_status', # color_status: "gray", "green", "yellow", "red" 'color_status'] -STRUCTURE_SENSOR_TYPES = ['eta'] +STRUCTURE_SENSOR_TYPES = ['eta', 'security_state'] _VALID_SENSOR_TYPES = SENSOR_TYPES + TEMP_SENSOR_TYPES + PROTECT_SENSOR_TYPES \ + STRUCTURE_SENSOR_TYPES diff --git a/homeassistant/components/services.yaml b/homeassistant/components/services.yaml index 19bf19a799a..6b8bded59b8 100644 --- a/homeassistant/components/services.yaml +++ b/homeassistant/components/services.yaml @@ -576,3 +576,28 @@ shopping_list: name: description: The name of the item to mark as completed. example: Beer + +nest: + set_mode: + description: > + Set the home/away mode for a Nest structure. + Set to away mode will also set Estimated Arrival Time if provided. + Set ETA will cause the thermostat to begin warming or cooling the home before the user arrives. + After ETA set other Automation can read ETA sensor as a signal to prepare the home for + the user's arrival. + fields: + home_mode: + description: home or away + example: home + structure: + description: Optional structure name. Default set all structures managed by Home Assistant. + example: My Home + eta: + description: Optional Estimated Arrival Time from now. + example: 0:10 + eta_window: + description: Optional ETA window. Default is 1 minute. + example: 0:5 + trip_id: + description: Optional identity of a trip. Using the same trip_ID will update the estimation. + example: trip_back_home diff --git a/requirements_all.txt b/requirements_all.txt index 478a0cb5479..4b0ad5bc5ef 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1051,7 +1051,7 @@ python-mpd2==1.0.0 python-mystrom==0.4.4 # homeassistant.components.nest -python-nest==4.0.1 +python-nest==4.0.2 # homeassistant.components.device_tracker.nmap_tracker python-nmap==0.6.1 From be4776d039dfd9f2f98abbc0366c1fef6fc8d130 Mon Sep 17 00:00:00 2001 From: Ong Vairoj Date: Mon, 11 Jun 2018 22:33:21 -0700 Subject: [PATCH 067/144] Add more test cases for samsungtv (#14900) More test cases to cover retry logic added in 58a1c3839 --- .../components/media_player/test_samsungtv.py | 56 +++++++++++++++---- 1 file changed, 44 insertions(+), 12 deletions(-) diff --git a/tests/components/media_player/test_samsungtv.py b/tests/components/media_player/test_samsungtv.py index c3753eb53b5..b5baf8b078b 100644 --- a/tests/components/media_player/test_samsungtv.py +++ b/tests/components/media_player/test_samsungtv.py @@ -29,7 +29,15 @@ DISCOVERY_INFO = { } -class PackageException(Exception): +class AccessDenied(Exception): + """Dummy Exception.""" + + +class ConnectionClosed(Exception): + """Dummy Exception.""" + + +class UnhandledResponse(Exception): """Dummy Exception.""" @@ -45,9 +53,9 @@ class TestSamsungTv(unittest.TestCase): self.hass.block_till_done() self.device = SamsungTVDevice(**WORKING_CONFIG) self.device._exceptions_class = mock.Mock() - self.device._exceptions_class.UnhandledResponse = PackageException - self.device._exceptions_class.AccessDenied = PackageException - self.device._exceptions_class.ConnectionClosed = PackageException + self.device._exceptions_class.UnhandledResponse = UnhandledResponse + self.device._exceptions_class.AccessDenied = AccessDenied + self.device._exceptions_class.ConnectionClosed = ConnectionClosed def tearDown(self): """Tear down test data.""" @@ -123,22 +131,46 @@ class TestSamsungTv(unittest.TestCase): def test_send_key_broken_pipe(self): """Testing broken pipe Exception.""" _remote = mock.Mock() - self.device.get_remote = mock.Mock() _remote.control = mock.Mock( - side_effect=BrokenPipeError("Boom")) - self.device.get_remote.return_value = _remote - self.device.send_key("HELLO") + side_effect=BrokenPipeError('Boom')) + self.device.get_remote = mock.Mock(return_value=_remote) + self.device.send_key('HELLO') + self.assertIsNone(self.device._remote) + self.assertEqual(STATE_ON, self.device._state) + + def test_send_key_connection_closed_retry_succeed(self): + """Test retry on connection closed.""" + _remote = mock.Mock() + _remote.control = mock.Mock(side_effect=[ + self.device._exceptions_class.ConnectionClosed('Boom'), + mock.DEFAULT]) + self.device.get_remote = mock.Mock(return_value=_remote) + command = 'HELLO' + self.device.send_key(command) + self.assertEqual(STATE_ON, self.device._state) + # verify that _remote.control() get called twice because of retry logic + expected = [mock.call(command), + mock.call(command)] + self.assertEqual(expected, _remote.control.call_args_list) + + def test_send_key_unhandled_response(self): + """Testing unhandled response exception.""" + _remote = mock.Mock() + _remote.control = mock.Mock( + side_effect=self.device._exceptions_class.UnhandledResponse('Boom') + ) + self.device.get_remote = mock.Mock(return_value=_remote) + self.device.send_key('HELLO') self.assertIsNone(self.device._remote) self.assertEqual(STATE_ON, self.device._state) def test_send_key_os_error(self): """Testing broken pipe Exception.""" _remote = mock.Mock() - self.device.get_remote = mock.Mock() _remote.control = mock.Mock( - side_effect=OSError("Boom")) - self.device.get_remote.return_value = _remote - self.device.send_key("HELLO") + side_effect=OSError('Boom')) + self.device.get_remote = mock.Mock(return_value=_remote) + self.device.send_key('HELLO') self.assertIsNone(self.device._remote) self.assertEqual(STATE_OFF, self.device._state) From cdc5388dc9446fe49b8439319b596b32243bb095 Mon Sep 17 00:00:00 2001 From: Marcelo Moreira de Mello Date: Tue, 12 Jun 2018 02:01:26 -0400 Subject: [PATCH 068/144] Refactored Arlo component and enhanced Arlo API queries and times (#14823) * start arlo refactoring * Refactored Arlo Hub to avoid uncessary and duplicated GETs to Arlo API * Refactored Arlo camera component to avoid duplicate queries * Added debug and error messages when video is not found * Transformed Arlo Control Panel to Sync * Makes linter happy * Uses total_seconds() for scan_interval * Added callback and fixed scan_interval issue * Disable multiple tries and supported custom modes set in Arlo * Bump PyArlo version to 0.1.4 * Makes lint happy * Removed ArloHub object and added some tweaks * Fixed hub_refresh method * Makes lint happy * Ajusted async syntax and added callbacks decorators * Bump PyArlo version to 0.1.6 to include some enhacements * Refined code --- .../components/alarm_control_panel/arlo.py | 55 ++++++++++--------- homeassistant/components/arlo.py | 38 ++++++++++++- homeassistant/components/camera/arlo.py | 55 +++++++++---------- homeassistant/components/sensor/arlo.py | 50 ++++++++--------- requirements_all.txt | 2 +- 5 files changed, 116 insertions(+), 84 deletions(-) diff --git a/homeassistant/components/alarm_control_panel/arlo.py b/homeassistant/components/alarm_control_panel/arlo.py index 333bde9ee36..20887157cb4 100644 --- a/homeassistant/components/alarm_control_panel/arlo.py +++ b/homeassistant/components/alarm_control_panel/arlo.py @@ -4,15 +4,17 @@ Support for Arlo Alarm Control Panels. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/alarm_control_panel.arlo/ """ -import asyncio import logging import voluptuous as vol import homeassistant.helpers.config_validation as cv +from homeassistant.core import callback +from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.components.alarm_control_panel import ( AlarmControlPanel, PLATFORM_SCHEMA) -from homeassistant.components.arlo import (DATA_ARLO, CONF_ATTRIBUTION) +from homeassistant.components.arlo import ( + DATA_ARLO, CONF_ATTRIBUTION, SIGNAL_UPDATE_ARLO) from homeassistant.const import ( ATTR_ATTRIBUTION, STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME, STATE_ALARM_DISARMED) @@ -36,21 +38,20 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ }) -@asyncio.coroutine -def async_setup_platform(hass, config, async_add_devices, discovery_info=None): +def setup_platform(hass, config, add_devices, discovery_info=None): """Set up the Arlo Alarm Control Panels.""" - data = hass.data[DATA_ARLO] + arlo = hass.data[DATA_ARLO] - if not data.base_stations: + if not arlo.base_stations: return home_mode_name = config.get(CONF_HOME_MODE_NAME) away_mode_name = config.get(CONF_AWAY_MODE_NAME) base_stations = [] - for base_station in data.base_stations: + for base_station in arlo.base_stations: base_stations.append(ArloBaseStation(base_station, home_mode_name, away_mode_name)) - async_add_devices(base_stations, True) + add_devices(base_stations, True) class ArloBaseStation(AlarmControlPanel): @@ -68,6 +69,16 @@ class ArloBaseStation(AlarmControlPanel): """Return icon.""" return ICON + async def async_added_to_hass(self): + """Register callbacks.""" + async_dispatcher_connect( + self.hass, SIGNAL_UPDATE_ARLO, self._update_callback) + + @callback + def _update_callback(self): + """Call update method.""" + self.async_schedule_update_ha_state(True) + @property def state(self): """Return the state of the device.""" @@ -75,30 +86,22 @@ class ArloBaseStation(AlarmControlPanel): def update(self): """Update the state of the device.""" - # PyArlo sometimes returns None for mode. So retry 3 times before - # returning None. - num_retries = 3 - i = 0 - while i < num_retries: - mode = self._base_station.mode - if mode: - self._state = self._get_state_from_mode(mode) - return - i += 1 - self._state = None + _LOGGER.debug("Updating Arlo Alarm Control Panel %s", self.name) + mode = self._base_station.mode + if mode: + self._state = self._get_state_from_mode(mode) + else: + self._state = None - @asyncio.coroutine - def async_alarm_disarm(self, code=None): + async def async_alarm_disarm(self, code=None): """Send disarm command.""" self._base_station.mode = DISARMED - @asyncio.coroutine - def async_alarm_arm_away(self, code=None): + async def async_alarm_arm_away(self, code=None): """Send arm away command. Uses custom mode.""" self._base_station.mode = self._away_mode_name - @asyncio.coroutine - def async_alarm_arm_home(self, code=None): + async def async_alarm_arm_home(self, code=None): """Send arm home command. Uses custom mode.""" self._base_station.mode = self._home_mode_name @@ -125,4 +128,4 @@ class ArloBaseStation(AlarmControlPanel): return STATE_ALARM_ARMED_HOME elif mode == self._away_mode_name: return STATE_ALARM_ARMED_AWAY - return None + return mode diff --git a/homeassistant/components/arlo.py b/homeassistant/components/arlo.py index 7e51ec8c045..206ea4005e6 100644 --- a/homeassistant/components/arlo.py +++ b/homeassistant/components/arlo.py @@ -5,14 +5,18 @@ For more details about this component, please refer to the documentation at https://home-assistant.io/components/arlo/ """ import logging +from datetime import timedelta import voluptuous as vol from requests.exceptions import HTTPError, ConnectTimeout from homeassistant.helpers import config_validation as cv -from homeassistant.const import CONF_USERNAME, CONF_PASSWORD +from homeassistant.const import ( + CONF_USERNAME, CONF_PASSWORD, CONF_SCAN_INTERVAL) +from homeassistant.helpers.event import track_time_interval +from homeassistant.helpers.dispatcher import dispatcher_send -REQUIREMENTS = ['pyarlo==0.1.2'] +REQUIREMENTS = ['pyarlo==0.1.6'] _LOGGER = logging.getLogger(__name__) @@ -25,10 +29,16 @@ DOMAIN = 'arlo' NOTIFICATION_ID = 'arlo_notification' NOTIFICATION_TITLE = 'Arlo Component Setup' +SCAN_INTERVAL = timedelta(seconds=60) + +SIGNAL_UPDATE_ARLO = "arlo_update" + CONFIG_SCHEMA = vol.Schema({ DOMAIN: vol.Schema({ vol.Required(CONF_USERNAME): cv.string, vol.Required(CONF_PASSWORD): cv.string, + vol.Optional(CONF_SCAN_INTERVAL, default=SCAN_INTERVAL): + cv.time_period, }), }, extra=vol.ALLOW_EXTRA) @@ -38,6 +48,7 @@ def setup(hass, config): conf = config[DOMAIN] username = conf.get(CONF_USERNAME) password = conf.get(CONF_PASSWORD) + scan_interval = conf.get(CONF_SCAN_INTERVAL) try: from pyarlo import PyArlo @@ -45,7 +56,17 @@ def setup(hass, config): arlo = PyArlo(username, password, preload=False) if not arlo.is_connected: return False + + # assign refresh period to base station thread + arlo_base_station = next(( + station for station in arlo.base_stations), None) + + if arlo_base_station is None: + return False + + arlo_base_station.refresh_rate = scan_interval.total_seconds() hass.data[DATA_ARLO] = arlo + except (ConnectTimeout, HTTPError) as ex: _LOGGER.error("Unable to connect to Netgear Arlo: %s", str(ex)) hass.components.persistent_notification.create( @@ -55,4 +76,17 @@ def setup(hass, config): title=NOTIFICATION_TITLE, notification_id=NOTIFICATION_ID) return False + + def hub_refresh(event_time): + """Call ArloHub to refresh information.""" + _LOGGER.info("Updating Arlo Hub component") + hass.data[DATA_ARLO].update(update_cameras=True, + update_base_station=True) + dispatcher_send(hass, SIGNAL_UPDATE_ARLO) + + # register service + hass.services.register(DOMAIN, 'update', hub_refresh) + + # register scan interval for ArloHub + track_time_interval(hass, hub_refresh, scan_interval) return True diff --git a/homeassistant/components/camera/arlo.py b/homeassistant/components/camera/arlo.py index f3e70c2bdd7..1a98ade5518 100644 --- a/homeassistant/components/camera/arlo.py +++ b/homeassistant/components/camera/arlo.py @@ -4,23 +4,22 @@ Support for Netgear Arlo IP cameras. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/camera.arlo/ """ -import asyncio import logging -from datetime import timedelta import voluptuous as vol +from homeassistant.core import callback import homeassistant.helpers.config_validation as cv -from homeassistant.components.arlo import DEFAULT_BRAND, DATA_ARLO +from homeassistant.components.arlo import ( + DEFAULT_BRAND, DATA_ARLO, SIGNAL_UPDATE_ARLO) from homeassistant.components.camera import Camera, PLATFORM_SCHEMA from homeassistant.components.ffmpeg import DATA_FFMPEG from homeassistant.const import ATTR_BATTERY_LEVEL from homeassistant.helpers.aiohttp_client import async_aiohttp_proxy_stream +from homeassistant.helpers.dispatcher import async_dispatcher_connect _LOGGER = logging.getLogger(__name__) -SCAN_INTERVAL = timedelta(seconds=90) - ARLO_MODE_ARMED = 'armed' ARLO_MODE_DISARMED = 'disarmed' @@ -44,22 +43,19 @@ POWERSAVE_MODE_MAPPING = { } PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Optional(CONF_FFMPEG_ARGUMENTS): - cv.string, + vol.Optional(CONF_FFMPEG_ARGUMENTS): cv.string, }) def setup_platform(hass, config, add_devices, discovery_info=None): """Set up an Arlo IP Camera.""" - arlo = hass.data.get(DATA_ARLO) - if not arlo: - return False + arlo = hass.data[DATA_ARLO] cameras = [] for camera in arlo.cameras: cameras.append(ArloCam(hass, camera, config)) - add_devices(cameras, True) + add_devices(cameras) class ArloCam(Camera): @@ -74,31 +70,41 @@ class ArloCam(Camera): self._ffmpeg = hass.data[DATA_FFMPEG] self._ffmpeg_arguments = device_info.get(CONF_FFMPEG_ARGUMENTS) self._last_refresh = None - if self._camera.base_station: - self._camera.base_station.refresh_rate = \ - SCAN_INTERVAL.total_seconds() self.attrs = {} def camera_image(self): """Return a still image response from the camera.""" - return self._camera.last_image + return self._camera.last_image_from_cache - @asyncio.coroutine - def handle_async_mjpeg_stream(self, request): + async def async_added_to_hass(self): + """Register callbacks.""" + async_dispatcher_connect( + self.hass, SIGNAL_UPDATE_ARLO, self._update_callback) + + @callback + def _update_callback(self): + """Call update method.""" + self.async_schedule_update_ha_state() + + async def handle_async_mjpeg_stream(self, request): """Generate an HTTP MJPEG stream from the camera.""" from haffmpeg import CameraMjpeg video = self._camera.last_video if not video: + error_msg = \ + 'Video not found for {0}. Is it older than {1} days?'.format( + self.name, self._camera.min_days_vdo_cache) + _LOGGER.error(error_msg) return stream = CameraMjpeg(self._ffmpeg.binary, loop=self.hass.loop) - yield from stream.open_camera( + await stream.open_camera( video.video_url, extra_cmd=self._ffmpeg_arguments) - yield from async_aiohttp_proxy_stream( + await async_aiohttp_proxy_stream( self.hass, request, stream, 'multipart/x-mixed-replace;boundary=ffserver') - yield from stream.close() + await stream.close() @property def name(self): @@ -132,11 +138,6 @@ class ArloCam(Camera): """Return the camera brand.""" return DEFAULT_BRAND - @property - def should_poll(self): - """Camera should poll periodically.""" - return True - @property def motion_detection_enabled(self): """Return the camera motion detection status.""" @@ -164,7 +165,3 @@ class ArloCam(Camera): """Disable the motion detection in base station (Disarm).""" self._motion_status = False self.set_base_station_mode(ARLO_MODE_DISARMED) - - def update(self): - """Add an attribute-update task to the executor pool.""" - self._camera.update() diff --git a/homeassistant/components/sensor/arlo.py b/homeassistant/components/sensor/arlo.py index 97b7ac22909..18029691dc7 100644 --- a/homeassistant/components/sensor/arlo.py +++ b/homeassistant/components/sensor/arlo.py @@ -4,17 +4,17 @@ This component provides HA sensor for Netgear Arlo IP cameras. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/sensor.arlo/ """ -import asyncio import logging -from datetime import timedelta import voluptuous as vol +from homeassistant.core import callback import homeassistant.helpers.config_validation as cv from homeassistant.components.arlo import ( - CONF_ATTRIBUTION, DEFAULT_BRAND, DATA_ARLO) + CONF_ATTRIBUTION, DEFAULT_BRAND, DATA_ARLO, SIGNAL_UPDATE_ARLO) from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.const import (ATTR_ATTRIBUTION, CONF_MONITORED_CONDITIONS) +from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity import Entity from homeassistant.helpers.icon import icon_for_battery_level @@ -22,8 +22,6 @@ _LOGGER = logging.getLogger(__name__) DEPENDENCIES = ['arlo'] -SCAN_INTERVAL = timedelta(seconds=90) - # sensor_type [ description, unit, icon ] SENSOR_TYPES = { 'last_capture': ['Last', None, 'run-fast'], @@ -39,8 +37,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ }) -@asyncio.coroutine -def async_setup_platform(hass, config, async_add_devices, discovery_info=None): +def setup_platform(hass, config, add_devices, discovery_info=None): """Set up an Arlo IP sensor.""" arlo = hass.data.get(DATA_ARLO) if not arlo: @@ -50,24 +47,22 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None): for sensor_type in config.get(CONF_MONITORED_CONDITIONS): if sensor_type == 'total_cameras': sensors.append(ArloSensor( - hass, SENSOR_TYPES[sensor_type][0], arlo, sensor_type)) + SENSOR_TYPES[sensor_type][0], arlo, sensor_type)) else: for camera in arlo.cameras: name = '{0} {1}'.format( SENSOR_TYPES[sensor_type][0], camera.name) - sensors.append(ArloSensor(hass, name, camera, sensor_type)) + sensors.append(ArloSensor(name, camera, sensor_type)) - async_add_devices(sensors, True) + add_devices(sensors, True) class ArloSensor(Entity): """An implementation of a Netgear Arlo IP sensor.""" - def __init__(self, hass, name, device, sensor_type): + def __init__(self, name, device, sensor_type): """Initialize an Arlo sensor.""" - super().__init__() self._name = name - self._hass = hass self._data = device self._sensor_type = sensor_type self._state = None @@ -78,6 +73,16 @@ class ArloSensor(Entity): """Return the name of this camera.""" return self._name + async def async_added_to_hass(self): + """Register callbacks.""" + async_dispatcher_connect( + self.hass, SIGNAL_UPDATE_ARLO, self._update_callback) + + @callback + def _update_callback(self): + """Call update method.""" + self.async_schedule_update_ha_state(True) + @property def state(self): """Return the state of the sensor.""" @@ -98,18 +103,7 @@ class ArloSensor(Entity): def update(self): """Get the latest data and updates the state.""" - try: - base_station = self._data.base_station - except (AttributeError, IndexError): - return - - if not base_station: - return - - base_station.refresh_rate = SCAN_INTERVAL.total_seconds() - - self._data.update() - + _LOGGER.debug("Updating Arlo sensor %s", self.name) if self._sensor_type == 'total_cameras': self._state = len(self._data.cameras) @@ -118,9 +112,13 @@ class ArloSensor(Entity): elif self._sensor_type == 'last_capture': try: - video = self._data.videos()[0] + video = self._data.last_video self._state = video.created_at_pretty("%m-%d-%Y %H:%M:%S") except (AttributeError, IndexError): + error_msg = \ + 'Video not found for {0}. Older than {1} days?'.format( + self.name, self._data.min_days_vdo_cache) + _LOGGER.debug(error_msg) self._state = None elif self._sensor_type == 'battery_level': diff --git a/requirements_all.txt b/requirements_all.txt index 4b0ad5bc5ef..1a524c9fd0f 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -734,7 +734,7 @@ pyairvisual==1.0.0 pyalarmdotcom==0.3.2 # homeassistant.components.arlo -pyarlo==0.1.2 +pyarlo==0.1.6 # homeassistant.components.notify.xmpp pyasn1-modules==0.1.5 From c18033ba85eea69555cea5d67df7983b0d4f6db4 Mon Sep 17 00:00:00 2001 From: Jason Hu Date: Tue, 12 Jun 2018 00:32:13 -0700 Subject: [PATCH 069/144] Use cv.time_period instead of cv.time_period_str (#14938) --- homeassistant/components/nest.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/nest.py b/homeassistant/components/nest.py index 739572223c6..3ca1c483ee0 100644 --- a/homeassistant/components/nest.py +++ b/homeassistant/components/nest.py @@ -52,8 +52,8 @@ AWAY_SCHEMA = vol.Schema({ vol.Required(ATTR_HOME_MODE): vol.In([HOME_MODE_AWAY, HOME_MODE_HOME]), vol.Optional(ATTR_STRUCTURE): vol.All(cv.ensure_list, cv.string), vol.Optional(ATTR_TRIP_ID): cv.string, - vol.Optional(ATTR_ETA): cv.time_period_str, - vol.Optional(ATTR_ETA_WINDOW): cv.time_period_str + vol.Optional(ATTR_ETA): cv.time_period, + vol.Optional(ATTR_ETA_WINDOW): cv.time_period }) CONFIG_SCHEMA = vol.Schema({ From 6755ae2605c81f6ad978ddb4794ed59fe0de7324 Mon Sep 17 00:00:00 2001 From: Christoph Gerneth Date: Tue, 12 Jun 2018 12:36:02 +0200 Subject: [PATCH 070/144] Add support for KIWI Door Locks (#14485) * initial commit for kiwi door locks bugfixes improved attribute display flake8 more style adjustments * added session handling flake8 * added requirements_all reordered imports and flake8 attempt to pelase a very picky linter also pleasing pylint now :) * re-try the build * added kiwi.py to .coveragerc * reorganized datetime handling and attribute naming * created pypi package for door lock library * updated requirements_all.txt * code review changes * added async lock state reset for locking state * refactored lat/lon attribute updates * initial locked state changed from undefined to locked * refactored is_locked property check * handling authentication exception in setup_platform * added more check in setup_platform * code review changes: return type in setup_platform * fixed logging issue * event handling in main thread * updated kiwiki-client to version 0.1.1 * renamed alias e to exc --- .coveragerc | 1 + homeassistant/components/lock/kiwi.py | 110 ++++++++++++++++++++++++++ requirements_all.txt | 3 + 3 files changed, 114 insertions(+) create mode 100644 homeassistant/components/lock/kiwi.py diff --git a/.coveragerc b/.coveragerc index 5a78ec8093f..c4aea0e140a 100644 --- a/.coveragerc +++ b/.coveragerc @@ -470,6 +470,7 @@ omit = homeassistant/components/light/yeelightsunflower.py homeassistant/components/light/zengge.py homeassistant/components/lirc.py + homeassistant/components/lock/kiwi.py homeassistant/components/lock/lockitron.py homeassistant/components/lock/nello.py homeassistant/components/lock/nuki.py diff --git a/homeassistant/components/lock/kiwi.py b/homeassistant/components/lock/kiwi.py new file mode 100644 index 00000000000..78ea45525f2 --- /dev/null +++ b/homeassistant/components/lock/kiwi.py @@ -0,0 +1,110 @@ +""" +Support for the KIWI.KI lock platform. + +For more details about this platform, please refer to the documentation +https://home-assistant.io/components/lock.kiwi/ +""" +import logging + +import voluptuous as vol + +import homeassistant.helpers.config_validation as cv +from homeassistant.components.lock import (LockDevice, PLATFORM_SCHEMA) +from homeassistant.const import ( + CONF_PASSWORD, CONF_USERNAME, ATTR_ID, ATTR_LONGITUDE, ATTR_LATITUDE, + STATE_LOCKED, STATE_UNLOCKED) +from homeassistant.helpers.event import async_call_later +from homeassistant.core import callback + +REQUIREMENTS = ['kiwiki-client==0.1.1'] + +_LOGGER = logging.getLogger(__name__) + +ATTR_TYPE = 'hardware_type' +ATTR_PERMISSION = 'permission' +ATTR_CAN_INVITE = 'can_invite_others' + +UNLOCK_MAINTAIN_TIME = 5 + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Required(CONF_USERNAME): cv.string, + vol.Required(CONF_PASSWORD): cv.string +}) + + +def setup_platform(hass, config, add_devices, discovery_info=None): + """Set up the KIWI lock platform.""" + from kiwiki import KiwiClient, KiwiException + try: + kiwi = KiwiClient(config[CONF_USERNAME], config[CONF_PASSWORD]) + except KiwiException as exc: + _LOGGER.error(exc) + return + available_locks = kiwi.get_locks() + if not available_locks: + # No locks found; abort setup routine. + _LOGGER.info("No KIWI locks found in your account.") + return + add_devices([KiwiLock(lock, kiwi) for lock in available_locks], True) + + +class KiwiLock(LockDevice): + """Representation of a Kiwi lock.""" + + def __init__(self, kiwi_lock, client): + """Initialize the lock.""" + self._sensor = kiwi_lock + self._client = client + self.lock_id = kiwi_lock['sensor_id'] + self._state = STATE_LOCKED + + address = kiwi_lock.get('address') + address.update({ + ATTR_LATITUDE: address.pop('lat', None), + ATTR_LONGITUDE: address.pop('lng', None) + }) + + self._device_attrs = { + ATTR_ID: self.lock_id, + ATTR_TYPE: kiwi_lock.get('hardware_type'), + ATTR_PERMISSION: kiwi_lock.get('highest_permission'), + ATTR_CAN_INVITE: kiwi_lock.get('can_invite'), + **address + } + + @property + def name(self): + """Return the name of the lock.""" + name = self._sensor.get('name') + specifier = self._sensor['address'].get('specifier') + return name or specifier + + @property + def is_locked(self): + """Return true if lock is locked.""" + return self._state == STATE_LOCKED + + @property + def device_state_attributes(self): + """Return the device specific state attributes.""" + return self._device_attrs + + @callback + def clear_unlock_state(self, _): + """Clear unlock state automatically.""" + self._state = STATE_LOCKED + self.async_schedule_update_ha_state() + + def unlock(self, **kwargs): + """Unlock the device.""" + from kiwiki import KiwiException + try: + self._client.open_door(self.lock_id) + except KiwiException: + _LOGGER.error("failed to open door") + else: + self._state = STATE_UNLOCKED + self.hass.add_job( + async_call_later, self.hass, UNLOCK_MAINTAIN_TIME, + self.clear_unlock_state + ) diff --git a/requirements_all.txt b/requirements_all.txt index 1a524c9fd0f..aa346e66ef1 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -482,6 +482,9 @@ keyring==12.2.1 # homeassistant.scripts.keyring keyrings.alt==3.1 +# homeassistant.components.lock.kiwi +kiwiki-client==0.1.1 + # homeassistant.components.konnected konnected==0.1.2 From 89d008d1f3a6d20af1eaa6e134fd4f37140525f4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ing=2E=20Jaroslav=20=C5=A0afka?= Date: Tue, 12 Jun 2018 15:46:53 +0200 Subject: [PATCH 071/144] Fix snapcast uuid to be more unique (#14925) Current uuid is ok when using only 1 snapserver New uuid is needed when using multiple snapserver Because the client can connect to more snapservers and then uuid based on client MAC is not enough --- .../components/media_player/snapcast.py | 25 +++++++++++++------ 1 file changed, 18 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/media_player/snapcast.py b/homeassistant/components/media_player/snapcast.py index ca7ff17a16a..53a95f7924c 100644 --- a/homeassistant/components/media_player/snapcast.py +++ b/homeassistant/components/media_player/snapcast.py @@ -80,8 +80,11 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None): host, port) return - groups = [SnapcastGroupDevice(group) for group in server.groups] - clients = [SnapcastClientDevice(client) for client in server.clients] + # Note: Host part is needed, when using multiple snapservers + hpid = '{}:{}'.format(host, port) + + groups = [SnapcastGroupDevice(group, hpid) for group in server.groups] + clients = [SnapcastClientDevice(client, hpid) for client in server.clients] devices = groups + clients hass.data[DATA_KEY] = devices async_add_devices(devices) @@ -90,10 +93,12 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None): class SnapcastGroupDevice(MediaPlayerDevice): """Representation of a Snapcast group device.""" - def __init__(self, group): + def __init__(self, group, uid_part): """Initialize the Snapcast group device.""" group.set_callback(self.schedule_update_ha_state) self._group = group + self._uid = '{}{}_{}'.format(GROUP_PREFIX, uid_part, + self._group.identifier) @property def state(self): @@ -107,7 +112,7 @@ class SnapcastGroupDevice(MediaPlayerDevice): @property def unique_id(self): """Return the ID of snapcast group.""" - return '{}{}'.format(GROUP_PREFIX, self._group.identifier) + return self._uid @property def name(self): @@ -185,15 +190,21 @@ class SnapcastGroupDevice(MediaPlayerDevice): class SnapcastClientDevice(MediaPlayerDevice): """Representation of a Snapcast client device.""" - def __init__(self, client): + def __init__(self, client, uid_part): """Initialize the Snapcast client device.""" client.set_callback(self.schedule_update_ha_state) self._client = client + self._uid = '{}{}_{}'.format(CLIENT_PREFIX, uid_part, + self._client.identifier) @property def unique_id(self): - """Return the ID of this snapcast client.""" - return '{}{}'.format(CLIENT_PREFIX, self._client.identifier) + """ + Return the ID of this snapcast client. + + Note: Host part is needed, when using multiple snapservers + """ + return self._uid @property def name(self): From 3153b0c8fc94a177efd665782f7ce567b99b9522 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 12 Jun 2018 21:20:23 -0400 Subject: [PATCH 072/144] Bump frontend to 20180613.0 --- homeassistant/components/frontend/__init__.py | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/frontend/__init__.py b/homeassistant/components/frontend/__init__.py index d61b6f50a96..303c4846701 100644 --- a/homeassistant/components/frontend/__init__.py +++ b/homeassistant/components/frontend/__init__.py @@ -24,7 +24,7 @@ from homeassistant.core import callback from homeassistant.helpers.translation import async_get_translations from homeassistant.loader import bind_hass -REQUIREMENTS = ['home-assistant-frontend==20180607.0'] +REQUIREMENTS = ['home-assistant-frontend==20180613.0'] DOMAIN = 'frontend' DEPENDENCIES = ['api', 'websocket_api', 'http', 'system_log'] diff --git a/requirements_all.txt b/requirements_all.txt index aa346e66ef1..b6bb426a437 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -398,7 +398,7 @@ hipnotify==1.0.8 holidays==0.9.5 # homeassistant.components.frontend -home-assistant-frontend==20180607.0 +home-assistant-frontend==20180613.0 # homeassistant.components.homekit_controller # homekit==0.6 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 207b0a8545f..732aa85a37a 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -81,7 +81,7 @@ hbmqtt==0.9.2 holidays==0.9.5 # homeassistant.components.frontend -home-assistant-frontend==20180607.0 +home-assistant-frontend==20180613.0 # homeassistant.components.influxdb # homeassistant.components.sensor.influxdb From a3737930296957a3ad17d0a68ce4847e1718f2c1 Mon Sep 17 00:00:00 2001 From: Hate-Usernames Date: Wed, 13 Jun 2018 06:17:52 +0100 Subject: [PATCH 073/144] pytradfri 5.5.1: Improved 3rd party bulb support (#14887) * Bump pytradfri version * Update light component * Add tests * lint * Docstring typos * Blank line * lint * 5.5.1 * Fix tests on py3.5 --- homeassistant/components/light/tradfri.py | 116 +++-- homeassistant/components/tradfri.py | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 3 + script/gen_requirements_all.py | 1 + tests/components/light/test_tradfri.py | 548 ++++++++++++++++++++++ 6 files changed, 626 insertions(+), 46 deletions(-) create mode 100644 tests/components/light/test_tradfri.py diff --git a/homeassistant/components/light/tradfri.py b/homeassistant/components/light/tradfri.py index ab53c3669cb..c30745239ea 100644 --- a/homeassistant/components/light/tradfri.py +++ b/homeassistant/components/light/tradfri.py @@ -19,12 +19,16 @@ import homeassistant.util.color as color_util _LOGGER = logging.getLogger(__name__) +ATTR_DIMMER = 'dimmer' +ATTR_HUE = 'hue' +ATTR_SAT = 'saturation' ATTR_TRANSITION_TIME = 'transition_time' DEPENDENCIES = ['tradfri'] PLATFORM_SCHEMA = LIGHT_PLATFORM_SCHEMA IKEA = 'IKEA of Sweden' TRADFRI_LIGHT_MANAGER = 'Tradfri Light Manager' -SUPPORTED_FEATURES = (SUPPORT_BRIGHTNESS | SUPPORT_TRANSITION) +SUPPORTED_FEATURES = SUPPORT_TRANSITION +SUPPORTED_GROUP_FEATURES = SUPPORT_BRIGHTNESS | SUPPORT_TRANSITION async def async_setup_platform(hass, config, @@ -79,7 +83,7 @@ class TradfriGroup(Light): @property def supported_features(self): """Flag supported features.""" - return SUPPORTED_FEATURES + return SUPPORTED_GROUP_FEATURES @property def name(self): @@ -225,75 +229,97 @@ class TradfriLight(Light): """HS color of the light.""" if self._light_control.can_set_color: hsbxy = self._light_data.hsb_xy_color - hue = hsbxy[0] / (65535 / 360) - sat = hsbxy[1] / (65279 / 100) + hue = hsbxy[0] / (self._light_control.max_hue / 360) + sat = hsbxy[1] / (self._light_control.max_saturation / 100) if hue is not None and sat is not None: return hue, sat async def async_turn_off(self, **kwargs): """Instruct the light to turn off.""" - await self._api(self._light_control.set_state(False)) - - async def async_turn_on(self, **kwargs): - """Instruct the light to turn on.""" - params = {} + # This allows transitioning to off, but resets the brightness + # to 1 for the next set_state(True) command transition_time = None if ATTR_TRANSITION in kwargs: transition_time = int(kwargs[ATTR_TRANSITION]) * 10 - brightness = kwargs.get(ATTR_BRIGHTNESS) + dimmer_data = {ATTR_DIMMER: 0, ATTR_TRANSITION_TIME: + transition_time} + await self._api(self._light_control.set_dimmer(**dimmer_data)) + else: + await self._api(self._light_control.set_state(False)) - if brightness is not None: + async def async_turn_on(self, **kwargs): + """Instruct the light to turn on.""" + transition_time = None + if ATTR_TRANSITION in kwargs: + transition_time = int(kwargs[ATTR_TRANSITION]) * 10 + + dimmer_command = None + if ATTR_BRIGHTNESS in kwargs: + brightness = kwargs[ATTR_BRIGHTNESS] if brightness > 254: brightness = 254 elif brightness < 0: brightness = 0 + dimmer_data = {ATTR_DIMMER: brightness, ATTR_TRANSITION_TIME: + transition_time} + dimmer_command = self._light_control.set_dimmer(**dimmer_data) + transition_time = None + else: + dimmer_command = self._light_control.set_state(True) + color_command = None if ATTR_HS_COLOR in kwargs and self._light_control.can_set_color: - params[ATTR_BRIGHTNESS] = brightness - hue = int(kwargs[ATTR_HS_COLOR][0] * (65535 / 360)) - sat = int(kwargs[ATTR_HS_COLOR][1] * (65279 / 100)) - if brightness is None: - params[ATTR_TRANSITION_TIME] = transition_time - await self._api( - self._light_control.set_hsb(hue, sat, **params)) - return + hue = int(kwargs[ATTR_HS_COLOR][0] * + (self._light_control.max_hue / 360)) + sat = int(kwargs[ATTR_HS_COLOR][1] * + (self._light_control.max_saturation / 100)) + color_data = {ATTR_HUE: hue, ATTR_SAT: sat, ATTR_TRANSITION_TIME: + transition_time} + color_command = self._light_control.set_hsb(**color_data) + transition_time = None + temp_command = None if ATTR_COLOR_TEMP in kwargs and (self._light_control.can_set_temp or self._light_control.can_set_color): temp = kwargs[ATTR_COLOR_TEMP] - if temp > self.max_mireds: - temp = self.max_mireds - elif temp < self.min_mireds: - temp = self.min_mireds - - if brightness is None: - params[ATTR_TRANSITION_TIME] = transition_time # White Spectrum bulb - if (self._light_control.can_set_temp and - not self._light_control.can_set_color): - await self._api( - self._light_control.set_color_temp(temp, **params)) + if self._light_control.can_set_temp: + if temp > self.max_mireds: + temp = self.max_mireds + elif temp < self.min_mireds: + temp = self.min_mireds + temp_data = {ATTR_COLOR_TEMP: temp, ATTR_TRANSITION_TIME: + transition_time} + temp_command = self._light_control.set_color_temp(**temp_data) + transition_time = None # Color bulb (CWS) # color_temp needs to be set with hue/saturation - if self._light_control.can_set_color: - params[ATTR_BRIGHTNESS] = brightness + elif self._light_control.can_set_color: temp_k = color_util.color_temperature_mired_to_kelvin(temp) hs_color = color_util.color_temperature_to_hs(temp_k) - hue = int(hs_color[0] * (65535 / 360)) - sat = int(hs_color[1] * (65279 / 100)) - await self._api( - self._light_control.set_hsb(hue, sat, - **params)) + hue = int(hs_color[0] * (self._light_control.max_hue / 360)) + sat = int(hs_color[1] * + (self._light_control.max_saturation / 100)) + color_data = {ATTR_HUE: hue, ATTR_SAT: sat, + ATTR_TRANSITION_TIME: transition_time} + color_command = self._light_control.set_hsb(**color_data) + transition_time = None - if brightness is not None: - params[ATTR_TRANSITION_TIME] = transition_time - await self._api( - self._light_control.set_dimmer(brightness, - **params)) + # HSB can always be set, but color temp + brightness is bulb dependant + command = dimmer_command + if command is not None: + command += color_command else: - await self._api( - self._light_control.set_state(True)) + command = color_command + + if self._light_control.can_combine_commands: + await self._api(command + temp_command) + else: + if temp_command is not None: + await self._api(temp_command) + if command is not None: + await self._api(command) @callback def _async_start_observe(self, exc=None): @@ -324,6 +350,8 @@ class TradfriLight(Light): self._name = light.name self._features = SUPPORTED_FEATURES + if light.light_control.can_set_dimmer: + self._features |= SUPPORT_BRIGHTNESS if light.light_control.can_set_color: self._features |= SUPPORT_COLOR if light.light_control.can_set_temp: diff --git a/homeassistant/components/tradfri.py b/homeassistant/components/tradfri.py index 72d1b4c769f..9ed613abde0 100644 --- a/homeassistant/components/tradfri.py +++ b/homeassistant/components/tradfri.py @@ -15,7 +15,7 @@ from homeassistant.const import CONF_HOST from homeassistant.components.discovery import SERVICE_IKEA_TRADFRI from homeassistant.util.json import load_json, save_json -REQUIREMENTS = ['pytradfri[async]==5.4.2'] +REQUIREMENTS = ['pytradfri[async]==5.5.1'] DOMAIN = 'tradfri' GATEWAY_IDENTITY = 'homeassistant' diff --git a/requirements_all.txt b/requirements_all.txt index b6bb426a437..3b1096d36eb 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1117,7 +1117,7 @@ pytouchline==0.7 pytrackr==0.0.5 # homeassistant.components.tradfri -pytradfri[async]==5.4.2 +pytradfri[async]==5.5.1 # homeassistant.components.device_tracker.unifi pyunifi==2.13 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 732aa85a37a..9334d630429 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -158,6 +158,9 @@ python-forecastio==1.4.0 # homeassistant.components.sensor.whois pythonwhois==2.4.3 +# homeassistant.components.tradfri +pytradfri[async]==5.5.1 + # homeassistant.components.device_tracker.unifi pyunifi==2.13 diff --git a/script/gen_requirements_all.py b/script/gen_requirements_all.py index b5b636dc874..e770d902669 100755 --- a/script/gen_requirements_all.py +++ b/script/gen_requirements_all.py @@ -77,6 +77,7 @@ TEST_REQUIREMENTS = ( 'pynx584', 'pyqwikswitch', 'python-forecastio', + 'pytradfri\[async\]', 'pyunifi', 'pyupnp-async', 'pywebpush', diff --git a/tests/components/light/test_tradfri.py b/tests/components/light/test_tradfri.py new file mode 100644 index 00000000000..8ef5d17452a --- /dev/null +++ b/tests/components/light/test_tradfri.py @@ -0,0 +1,548 @@ +"""Tradfri lights platform tests.""" + +from copy import deepcopy +from unittest.mock import Mock, MagicMock, patch, PropertyMock + +import pytest +from pytradfri.device import Device, LightControl, Light +from pytradfri import RequestError + +from homeassistant.components import tradfri +from homeassistant.setup import async_setup_component + + +DEFAULT_TEST_FEATURES = {'can_set_dimmer': False, + 'can_set_color': False, + 'can_set_temp': False} +# [ +# {bulb features}, +# {turn_on arguments}, +# {expected result} +# ] +TURN_ON_TEST_CASES = [ + # Turn On + [ + {}, + {}, + {'state': 'on'}, + ], + # Brightness > 0 + [ + {'can_set_dimmer': True}, + {'brightness': 100}, + { + 'state': 'on', + 'brightness': 100 + } + ], + # Brightness == 0 + [ + {'can_set_dimmer': True}, + {'brightness': 0}, + { + 'brightness': 0 + } + ], + # Brightness < 0 + [ + {'can_set_dimmer': True}, + {'brightness': -1}, + { + 'brightness': 0 + } + ], + # Brightness > 254 + [ + {'can_set_dimmer': True}, + {'brightness': 1000}, + { + 'brightness': 254 + } + ], + # color_temp + [ + {'can_set_temp': True}, + {'color_temp': 250}, + {'color_temp': 250}, + ], + # color_temp < 250 + [ + {'can_set_temp': True}, + {'color_temp': 1}, + {'color_temp': 250}, + ], + # color_temp > 454 + [ + {'can_set_temp': True}, + {'color_temp': 1000}, + {'color_temp': 454}, + ], + # hs color + [ + {'can_set_color': True}, + {'hs_color': [300, 100]}, + { + 'state': 'on', + 'hs_color': [300, 100] + } + ], + # ct + brightness + [ + { + 'can_set_dimmer': True, + 'can_set_temp': True + }, + { + 'color_temp': 250, + 'brightness': 200 + }, + { + 'state': 'on', + 'color_temp': 250, + 'brightness': 200 + } + ], + # ct + brightness (no temp support) + [ + { + 'can_set_dimmer': True, + 'can_set_temp': False, + 'can_set_color': True + }, + { + 'color_temp': 250, + 'brightness': 200 + }, + { + 'state': 'on', + 'hs_color': [26.807, 34.869], + 'brightness': 200 + } + ], + # ct + brightness (no temp or color support) + [ + { + 'can_set_dimmer': True, + 'can_set_temp': False, + 'can_set_color': False + }, + { + 'color_temp': 250, + 'brightness': 200 + }, + { + 'state': 'on', + 'brightness': 200 + } + ], + # hs + brightness + [ + { + 'can_set_dimmer': True, + 'can_set_color': True + }, + { + 'hs_color': [300, 100], + 'brightness': 200 + }, + { + 'state': 'on', + 'hs_color': [300, 100], + 'brightness': 200 + } + ] +] + +# Result of transition is not tested, but data is passed to turn on service. +TRANSITION_CASES_FOR_TESTS = [None, 0, 1] + + +@pytest.fixture(autouse=True, scope='module') +def setup(request): + """Set up patches for pytradfri methods.""" + p_1 = patch('pytradfri.device.LightControl.raw', + new_callable=PropertyMock, + return_value=[{'mock': 'mock'}]) + p_2 = patch('pytradfri.device.LightControl.lights') + p_1.start() + p_2.start() + + def teardown(): + """Remove patches for pytradfri methods.""" + p_1.stop() + p_2.stop() + + request.addfinalizer(teardown) + + +@pytest.fixture +def mock_gateway(): + """Mock a Tradfri gateway.""" + def get_devices(): + """Return mock devices.""" + return gateway.mock_devices + + def get_groups(): + """Return mock groups.""" + return gateway.mock_groups + + gateway = Mock( + get_devices=get_devices, + get_groups=get_groups, + mock_devices=[], + mock_groups=[], + mock_responses=[] + ) + return gateway + + +@pytest.fixture +def mock_api(mock_gateway): + """Mock api.""" + async def api(self, command): + """Mock api function.""" + # Store the data for "real" command objects. + if(hasattr(command, '_data') and not isinstance(command, Mock)): + mock_gateway.mock_responses.append(command._data) + return command + return api + + +async def generate_psk(self, code): + """Mock psk.""" + return "mock" + + +async def setup_gateway(hass, mock_gateway, mock_api, + generate_psk=generate_psk, + known_hosts=None): + """Load the Tradfri platform with a mock gateway.""" + def request_config(_, callback, description, submit_caption, fields): + """Mock request_config.""" + hass.async_add_job(callback, {'security_code': 'mock'}) + + if known_hosts is None: + known_hosts = {} + + with patch('pytradfri.api.aiocoap_api.APIFactory.generate_psk', + generate_psk), \ + patch('pytradfri.api.aiocoap_api.APIFactory.request', mock_api), \ + patch('pytradfri.Gateway', return_value=mock_gateway), \ + patch.object(tradfri, 'load_json', return_value=known_hosts), \ + patch.object(hass.components.configurator, 'request_config', + request_config): + + await async_setup_component(hass, tradfri.DOMAIN, + { + tradfri.DOMAIN: { + 'host': 'mock-host', + 'allow_tradfri_groups': True + } + }) + await hass.async_block_till_done() + + +async def test_setup_gateway(hass, mock_gateway, mock_api): + """Test that the gateway can be setup without errors.""" + await setup_gateway(hass, mock_gateway, mock_api) + + +async def test_setup_gateway_known_host(hass, mock_gateway, mock_api): + """Test gateway setup with a known host.""" + await setup_gateway(hass, mock_gateway, mock_api, + known_hosts={ + 'mock-host': { + 'identity': 'mock', + 'key': 'mock-key' + } + }) + + +async def test_incorrect_security_code(hass, mock_gateway, mock_api): + """Test that an error is shown if the security code is incorrect.""" + async def psk_error(self, code): + """Raise RequestError when called.""" + raise RequestError + + with patch.object(hass.components.configurator, 'async_notify_errors') \ + as notify_error: + await setup_gateway(hass, mock_gateway, mock_api, + generate_psk=psk_error) + assert len(notify_error.mock_calls) > 0 + + +def mock_light(test_features={}, test_state={}, n=0): + """Mock a tradfri light.""" + mock_light_data = Mock( + **test_state + ) + + mock_light = Mock( + id='mock-light-id-{}'.format(n), + reachable=True, + observe=Mock(), + device_info=MagicMock() + ) + mock_light.name = 'tradfri_light_{}'.format(n) + + # Set supported features for the light. + features = {**DEFAULT_TEST_FEATURES, **test_features} + lc = LightControl(mock_light) + for k, v in features.items(): + setattr(lc, k, v) + # Store the initial state. + setattr(lc, 'lights', [mock_light_data]) + mock_light.light_control = lc + return mock_light + + +async def test_light(hass, mock_gateway, mock_api): + """Test that lights are correctly added.""" + features = { + 'can_set_dimmer': True, + 'can_set_color': True, + 'can_set_temp': True + } + + state = { + 'state': True, + 'dimmer': 100, + 'color_temp': 250, + 'hsb_xy_color': (100, 100, 100, 100, 100) + } + + mock_gateway.mock_devices.append( + mock_light(test_features=features, test_state=state) + ) + await setup_gateway(hass, mock_gateway, mock_api) + + lamp_1 = hass.states.get('light.tradfri_light_0') + assert lamp_1 is not None + assert lamp_1.state == 'on' + assert lamp_1.attributes['brightness'] == 100 + assert lamp_1.attributes['hs_color'] == (0.549, 0.153) + + +async def test_light_observed(hass, mock_gateway, mock_api): + """Test that lights are correctly observed.""" + light = mock_light() + mock_gateway.mock_devices.append(light) + await setup_gateway(hass, mock_gateway, mock_api) + assert len(light.observe.mock_calls) > 0 + + +async def test_light_available(hass, mock_gateway, mock_api): + """Test light available property.""" + light = mock_light({'state': True}, n=1) + light.reachable = True + + light2 = mock_light({'state': True}, n=2) + light2.reachable = False + + mock_gateway.mock_devices.append(light) + mock_gateway.mock_devices.append(light2) + await setup_gateway(hass, mock_gateway, mock_api) + + assert (hass.states.get('light.tradfri_light_1') + .state == 'on') + + assert (hass.states.get('light.tradfri_light_2') + .state == 'unavailable') + + +# Combine TURN_ON_TEST_CASES and TRANSITION_CASES_FOR_TESTS +ALL_TURN_ON_TEST_CASES = [ + ["test_features", "test_data", "expected_result", "id"], + [] +] + +idx = 1 +for tc in TURN_ON_TEST_CASES: + for trans in TRANSITION_CASES_FOR_TESTS: + case = deepcopy(tc) + if trans is not None: + case[1]['transition'] = trans + case.append(idx) + idx = idx + 1 + ALL_TURN_ON_TEST_CASES[1].append(case) + + +@pytest.mark.parametrize(*ALL_TURN_ON_TEST_CASES) +async def test_turn_on(hass, + mock_gateway, + mock_api, + test_features, + test_data, + expected_result, + id): + """Test turning on a light.""" + # Note pytradfri style, not hass. Values not really important. + initial_state = { + 'state': False, + 'dimmer': 0, + 'color_temp': 250, + 'hsb_xy_color': (100, 100, 100, 100, 100) + } + + # Setup the gateway with a mock light. + light = mock_light(test_features=test_features, + test_state=initial_state, + n=id) + mock_gateway.mock_devices.append(light) + await setup_gateway(hass, mock_gateway, mock_api) + + # Use the turn_on service call to change the light state. + await hass.services.async_call('light', 'turn_on', { + 'entity_id': 'light.tradfri_light_{}'.format(id), + **test_data + }, blocking=True) + await hass.async_block_till_done() + + # Check that the light is observed. + mock_func = light.observe + assert len(mock_func.mock_calls) > 0 + _, callkwargs = mock_func.call_args + assert 'callback' in callkwargs + # Callback function to refresh light state. + cb = callkwargs['callback'] + + responses = mock_gateway.mock_responses + # State on command data. + data = {'3311': [{'5850': 1}]} + # Add data for all sent commands. + for r in responses: + data['3311'][0] = {**data['3311'][0], **r['3311'][0]} + + # Use the callback function to update the light state. + dev = Device(data) + light_data = Light(dev, 0) + light.light_control.lights[0] = light_data + cb(light) + await hass.async_block_till_done() + + # Check that the state is correct. + states = hass.states.get('light.tradfri_light_{}'.format(id)) + for k, v in expected_result.items(): + if k == 'state': + assert states.state == v + else: + # Allow some rounding error in color conversions. + assert states.attributes[k] == pytest.approx(v, abs=0.01) + + +async def test_turn_off(hass, mock_gateway, mock_api): + """Test turning off a light.""" + state = { + 'state': True, + 'dimmer': 100, + } + + light = mock_light(test_state=state) + mock_gateway.mock_devices.append(light) + await setup_gateway(hass, mock_gateway, mock_api) + + # Use the turn_off service call to change the light state. + await hass.services.async_call('light', 'turn_off', { + 'entity_id': 'light.tradfri_light_0'}, blocking=True) + await hass.async_block_till_done() + + # Check that the light is observed. + mock_func = light.observe + assert len(mock_func.mock_calls) > 0 + _, callkwargs = mock_func.call_args + assert 'callback' in callkwargs + # Callback function to refresh light state. + cb = callkwargs['callback'] + + responses = mock_gateway.mock_responses + data = {'3311': [{}]} + # Add data for all sent commands. + for r in responses: + data['3311'][0] = {**data['3311'][0], **r['3311'][0]} + + # Use the callback function to update the light state. + dev = Device(data) + light_data = Light(dev, 0) + light.light_control.lights[0] = light_data + cb(light) + await hass.async_block_till_done() + + # Check that the state is correct. + states = hass.states.get('light.tradfri_light_0') + assert states.state == 'off' + + +def mock_group(test_state={}, n=0): + """Mock a Tradfri group.""" + default_state = { + 'state': False, + 'dimmer': 0, + } + + state = {**default_state, **test_state} + + mock_group = Mock( + member_ids=[], + observe=Mock(), + **state + ) + mock_group.name = 'tradfri_group_{}'.format(n) + return mock_group + + +async def test_group(hass, mock_gateway, mock_api): + """Test that groups are correctly added.""" + mock_gateway.mock_groups.append(mock_group()) + state = {'state': True, 'dimmer': 100} + mock_gateway.mock_groups.append(mock_group(state, 1)) + await setup_gateway(hass, mock_gateway, mock_api) + + group = hass.states.get('light.tradfri_group_0') + assert group is not None + assert group.state == 'off' + + group = hass.states.get('light.tradfri_group_1') + assert group is not None + assert group.state == 'on' + assert group.attributes['brightness'] == 100 + + +async def test_group_turn_on(hass, mock_gateway, mock_api): + """Test turning on a group.""" + group = mock_group() + group2 = mock_group(n=1) + group3 = mock_group(n=2) + mock_gateway.mock_groups.append(group) + mock_gateway.mock_groups.append(group2) + mock_gateway.mock_groups.append(group3) + await setup_gateway(hass, mock_gateway, mock_api) + + # Use the turn_off service call to change the light state. + await hass.services.async_call('light', 'turn_on', { + 'entity_id': 'light.tradfri_group_0'}, blocking=True) + await hass.services.async_call('light', 'turn_on', { + 'entity_id': 'light.tradfri_group_1', + 'brightness': 100}, blocking=True) + await hass.services.async_call('light', 'turn_on', { + 'entity_id': 'light.tradfri_group_2', + 'brightness': 100, + 'transition': 1}, blocking=True) + await hass.async_block_till_done() + + group.set_state.assert_called_with(1) + group2.set_dimmer.assert_called_with(100) + group3.set_dimmer.assert_called_with(100, transition_time=10) + + +async def test_group_turn_off(hass, mock_gateway, mock_api): + """Test turning off a group.""" + group = mock_group({'state': True}) + mock_gateway.mock_groups.append(group) + await setup_gateway(hass, mock_gateway, mock_api) + + # Use the turn_off service call to change the light state. + await hass.services.async_call('light', 'turn_off', { + 'entity_id': 'light.tradfri_group_0'}, blocking=True) + await hass.async_block_till_done() + + group.set_state.assert_called_with(0) From 2ac23c8be62e5e26d88cf1d872abdcd159e20215 Mon Sep 17 00:00:00 2001 From: Pawel Date: Wed, 13 Jun 2018 07:28:59 +0200 Subject: [PATCH 074/144] Epson projector support (#14841) * Epson projector support. Version based on external library * Epson projector support. Version based on external library * modified epson according to MartinHjelmare review. Added description of cmode to services.yaml * renamed EPSON_SCHEMA to epson_schema * removed method of getting cmode property * removed unnecessary checks change name of cmode service * renamed SERVICE_ATTR_CMODE to SERVICE_SELECT_CMODE --- .coveragerc | 1 + .../components/media_player/epson.py | 211 ++++++++++++++++++ .../components/media_player/services.yaml | 10 + requirements_all.txt | 3 + 4 files changed, 225 insertions(+) create mode 100644 homeassistant/components/media_player/epson.py diff --git a/.coveragerc b/.coveragerc index c4aea0e140a..693ca12d10e 100644 --- a/.coveragerc +++ b/.coveragerc @@ -490,6 +490,7 @@ omit = homeassistant/components/media_player/directv.py homeassistant/components/media_player/dunehd.py homeassistant/components/media_player/emby.py + homeassistant/components/media_player/epson.py homeassistant/components/media_player/firetv.py homeassistant/components/media_player/frontier_silicon.py homeassistant/components/media_player/gpmdp.py diff --git a/homeassistant/components/media_player/epson.py b/homeassistant/components/media_player/epson.py new file mode 100644 index 00000000000..b22234a4094 --- /dev/null +++ b/homeassistant/components/media_player/epson.py @@ -0,0 +1,211 @@ +""" +Support for Epson projector. + +For more details about this component, please refer to the documentation at +https://home-assistant.io/components/media_player.epson/ +""" +import logging +import voluptuous as vol + +from homeassistant.components.media_player import ( + DOMAIN, MEDIA_PLAYER_SCHEMA, PLATFORM_SCHEMA, SUPPORT_NEXT_TRACK, + SUPPORT_PREVIOUS_TRACK, SUPPORT_SELECT_SOURCE, SUPPORT_TURN_OFF, + SUPPORT_TURN_ON, SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_STEP, + MediaPlayerDevice) +from homeassistant.const import ( + ATTR_ENTITY_ID, CONF_HOST, CONF_NAME, CONF_PORT, CONF_SSL, STATE_OFF, + STATE_ON) +from homeassistant.helpers.aiohttp_client import async_get_clientsession +import homeassistant.helpers.config_validation as cv + +REQUIREMENTS = ['epson-projector==0.1.3'] + +DATA_EPSON = 'epson' +DEFAULT_NAME = 'EPSON Projector' + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Required(CONF_HOST): cv.string, + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + vol.Optional(CONF_PORT, default=80): cv.port, + vol.Optional(CONF_SSL, default=False): cv.boolean +}) + +SERVICE_SELECT_CMODE = 'epson_select_cmode' +ATTR_CMODE = 'cmode' +SUPPORT_CMODE = 33001 + +SUPPORT_EPSON = SUPPORT_TURN_ON | SUPPORT_TURN_OFF | SUPPORT_SELECT_SOURCE |\ + SUPPORT_CMODE | SUPPORT_VOLUME_MUTE | SUPPORT_VOLUME_STEP | \ + SUPPORT_NEXT_TRACK | SUPPORT_PREVIOUS_TRACK +_LOGGER = logging.getLogger(__name__) + + +async def async_setup_platform(hass, config, async_add_devices, + discovery_info=None): + """Set up the Epson media player platform.""" + if DATA_EPSON not in hass.data: + hass.data[DATA_EPSON] = [] + name = config.get(CONF_NAME) + host = config.get(CONF_HOST) + + epson = EpsonProjector(async_get_clientsession(hass, verify_ssl=False), + name, host, + config.get(CONF_PORT), config.get(CONF_SSL)) + hass.data[DATA_EPSON].append(epson) + async_add_devices([epson], update_before_add=True) + + async def async_service_handler(service): + """Handle for services.""" + entity_ids = service.data.get(ATTR_ENTITY_ID) + if entity_ids: + devices = [device for device in hass.data[DATA_EPSON] + if device.entity_id in entity_ids] + else: + devices = hass.data[DATA_EPSON] + for device in devices: + if service.service == SERVICE_SELECT_CMODE: + cmode = service.data.get(ATTR_CMODE) + await device.select_cmode(cmode) + await device.update() + from epson_projector.const import (CMODE_LIST_SET) + epson_schema = MEDIA_PLAYER_SCHEMA.extend({ + vol.Required(ATTR_CMODE): vol.All(cv.string, vol.Any(*CMODE_LIST_SET)) + }) + hass.services.async_register( + DOMAIN, SERVICE_SELECT_CMODE, async_service_handler, + schema=epson_schema) + + +class EpsonProjector(MediaPlayerDevice): + """Representation of Epson Projector Device.""" + + def __init__(self, websession, name, host, port, encryption): + """Initialize entity to control Epson projector.""" + self._name = name + import epson_projector as epson + from epson_projector.const import DEFAULT_SOURCES + self._projector = epson.Projector( + host, + websession=websession, + port=port) + self._cmode = None + self._source_list = list(DEFAULT_SOURCES.values()) + self._source = None + self._volume = None + self._state = None + + async def update(self): + """Update state of device.""" + from epson_projector.const import ( + EPSON_CODES, POWER, + CMODE, CMODE_LIST, SOURCE, VOLUME, + BUSY, SOURCE_LIST) + is_turned_on = await self._projector.get_property(POWER) + _LOGGER.debug("Project turn on/off status: %s", is_turned_on) + if is_turned_on and is_turned_on == EPSON_CODES[POWER]: + self._state = STATE_ON + cmode = await self._projector.get_property(CMODE) + self._cmode = CMODE_LIST.get(cmode, self._cmode) + source = await self._projector.get_property(SOURCE) + self._source = SOURCE_LIST.get(source, self._source) + volume = await self._projector.get_property(VOLUME) + if volume: + self._volume = volume + elif is_turned_on == BUSY: + self._state = STATE_ON + else: + self._state = STATE_OFF + + @property + def name(self): + """Return the name of the device.""" + return self._name + + @property + def state(self): + """Return the state of the device.""" + return self._state + + @property + def supported_features(self): + """Flag media player features that are supported.""" + return SUPPORT_EPSON + + async def async_turn_on(self): + """Turn on epson.""" + from epson_projector.const import TURN_ON + await self._projector.send_command(TURN_ON) + + async def async_turn_off(self): + """Turn off epson.""" + from epson_projector.const import TURN_OFF + await self._projector.send_command(TURN_OFF) + + @property + def source_list(self): + """List of available input sources.""" + return self._source_list + + @property + def source(self): + """Get current input sources.""" + return self._source + + @property + def volume_level(self): + """Return the volume level of the media player (0..1).""" + return self._volume + + async def select_cmode(self, cmode): + """Set color mode in Epson.""" + from epson_projector.const import (CMODE_LIST_SET) + await self._projector.send_command(CMODE_LIST_SET[cmode]) + + async def async_select_source(self, source): + """Select input source.""" + from epson_projector.const import INV_SOURCES + selected_source = INV_SOURCES[source] + await self._projector.send_command(selected_source) + + async def async_mute_volume(self, mute): + """Mute (true) or unmute (false) sound.""" + from epson_projector.const import MUTE + await self._projector.send_command(MUTE) + + async def async_volume_up(self): + """Increase volume.""" + from epson_projector.const import VOL_UP + await self._projector.send_command(VOL_UP) + + async def async_volume_down(self): + """Decrease volume.""" + from epson_projector.const import VOL_DOWN + await self._projector.send_command(VOL_DOWN) + + async def async_media_play(self): + """Play media via Epson.""" + from epson_projector.const import PLAY + await self._projector.send_command(PLAY) + + async def async_media_pause(self): + """Pause media via Epson.""" + from epson_projector.const import PAUSE + await self._projector.send_command(PAUSE) + + async def async_media_next_track(self): + """Skip to next.""" + from epson_projector.const import FAST + await self._projector.send_command(FAST) + + async def async_media_previous_track(self): + """Skip to previous.""" + from epson_projector.const import BACK + await self._projector.send_command(BACK) + + @property + def device_state_attributes(self): + """Return device specific state attributes.""" + attributes = {} + if self._cmode is not None: + attributes[ATTR_CMODE] = self._cmode + return attributes diff --git a/homeassistant/components/media_player/services.yaml b/homeassistant/components/media_player/services.yaml index 765f7e1f0f7..3c91f19469b 100644 --- a/homeassistant/components/media_player/services.yaml +++ b/homeassistant/components/media_player/services.yaml @@ -422,3 +422,13 @@ blackbird_set_all_zones: source: description: Name of source to switch to. example: 'Source 1' + +epson_select_cmode: + description: Select Color mode of Epson projector + fields: + entity_id: + description: Name of projector + example: 'media_player.epson_projector' + cmode: + description: Name of Cmode + example: 'cinema' diff --git a/requirements_all.txt b/requirements_all.txt index 3b1096d36eb..6ce31803335 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -294,6 +294,9 @@ enocean==0.40 # homeassistant.components.sensor.season ephem==3.7.6.0 +# homeassistant.components.media_player.epson +epson-projector==0.1.3 + # homeassistant.components.netgear_lte eternalegypt==0.0.1 From 176ef411de75728b99a2c3e88037b37d85ef2770 Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Tue, 12 Jun 2018 23:30:06 -0600 Subject: [PATCH 075/144] Add scan_interval to RainMachine (#14945) --- homeassistant/components/rainmachine/__init__.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/rainmachine/__init__.py b/homeassistant/components/rainmachine/__init__.py index 38672dbc23b..22fc427ccce 100644 --- a/homeassistant/components/rainmachine/__init__.py +++ b/homeassistant/components/rainmachine/__init__.py @@ -11,8 +11,8 @@ import voluptuous as vol from homeassistant.const import ( ATTR_ATTRIBUTION, CONF_BINARY_SENSORS, CONF_IP_ADDRESS, CONF_PASSWORD, - CONF_PORT, CONF_SENSORS, CONF_SSL, CONF_MONITORED_CONDITIONS, - CONF_SWITCHES) + CONF_PORT, CONF_SCAN_INTERVAL, CONF_SENSORS, CONF_SSL, + CONF_MONITORED_CONDITIONS, CONF_SWITCHES) from homeassistant.helpers import ( aiohttp_client, config_validation as cv, discovery) from homeassistant.helpers.dispatcher import async_dispatcher_send @@ -107,6 +107,8 @@ CONFIG_SCHEMA = vol.Schema( vol.Required(CONF_PASSWORD): cv.string, vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, vol.Optional(CONF_SSL, default=DEFAULT_SSL): cv.boolean, + vol.Optional(CONF_SCAN_INTERVAL, default=DEFAULT_SCAN_INTERVAL): + cv.time_period, vol.Optional(CONF_BINARY_SENSORS, default={}): BINARY_SENSOR_SCHEMA, vol.Optional(CONF_SENSORS, default={}): SENSOR_SCHEMA, @@ -159,7 +161,7 @@ async def async_setup(hass, config): await rainmachine.async_update() async_dispatcher_send(hass, SENSOR_UPDATE_TOPIC) - async_track_time_interval(hass, refresh_sensors, DEFAULT_SCAN_INTERVAL) + async_track_time_interval(hass, refresh_sensors, conf[CONF_SCAN_INTERVAL]) async def start_program(service): """Start a particular program.""" From fecce206a9ce3e869938478feb32ddede533dffd Mon Sep 17 00:00:00 2001 From: ArrayLabs Date: Wed, 13 Jun 2018 02:02:27 -0400 Subject: [PATCH 076/144] Myq update from 0.0.8 to 0.0.11 (#14947) * Update requirements_all.txt Update myq from 0.0.8 to 0.0.11 * Update myq.py Update myq from 0.0.8 to 0.0.11 --- homeassistant/components/cover/myq.py | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/cover/myq.py b/homeassistant/components/cover/myq.py index 1e2ec43181c..a4682172fee 100644 --- a/homeassistant/components/cover/myq.py +++ b/homeassistant/components/cover/myq.py @@ -13,7 +13,7 @@ from homeassistant.const import ( CONF_USERNAME, CONF_PASSWORD, CONF_TYPE, STATE_CLOSED) import homeassistant.helpers.config_validation as cv -REQUIREMENTS = ['pymyq==0.0.8'] +REQUIREMENTS = ['pymyq==0.0.11'] _LOGGER = logging.getLogger(__name__) diff --git a/requirements_all.txt b/requirements_all.txt index 6ce31803335..c1922f52bdf 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -917,7 +917,7 @@ pymonoprice==0.3 pymusiccast==0.1.6 # homeassistant.components.cover.myq -pymyq==0.0.8 +pymyq==0.0.11 # homeassistant.components.mysensors pymysensors==0.14.0 From cb646e48d04d34ade293bba6c258ee9ff07766ee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ville=20Skytt=C3=A4?= Date: Wed, 13 Jun 2018 14:08:39 +0300 Subject: [PATCH 077/144] Upgrade pylint to 1.9.2 (#14916) --- homeassistant/components/homekit_controller/__init__.py | 1 + requirements_test.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/homekit_controller/__init__.py b/homeassistant/components/homekit_controller/__init__.py index e36e7439e09..0883c5a3cc8 100644 --- a/homeassistant/components/homekit_controller/__init__.py +++ b/homeassistant/components/homekit_controller/__init__.py @@ -37,6 +37,7 @@ def homekit_http_send(self, message_body=None, encode_chunked=False): Appends an extra \r\n to the buffer. A message_body may be specified, to be appended to the request. """ + # pylint: disable=protected-access self._buffer.extend((b"", b"")) msg = b"\r\n".join(self._buffer) del self._buffer[:] diff --git a/requirements_test.txt b/requirements_test.txt index 0a4a0bcb5b0..e7e854110f1 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -8,7 +8,7 @@ flake8==3.5 mock-open==1.3.1 mypy==0.590 pydocstyle==1.1.1 -pylint==1.8.4 +pylint==1.9.2 pytest-aiohttp==0.3.0 pytest-cov==2.5.1 pytest-sugar==0.9.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 9334d630429..19796c3bab7 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -9,7 +9,7 @@ flake8==3.5 mock-open==1.3.1 mypy==0.590 pydocstyle==1.1.1 -pylint==1.8.4 +pylint==1.9.2 pytest-aiohttp==0.3.0 pytest-cov==2.5.1 pytest-sugar==0.9.1 From 65b0ec66150aeb7065501fea763357cd76fe6783 Mon Sep 17 00:00:00 2001 From: Vignesh Venkat Date: Wed, 13 Jun 2018 04:09:42 -0700 Subject: [PATCH 078/144] Update python-wink to 1.8.0 (#14894) * wink: Update to python-wink 1.8.0 This pulls in a patch to expose the GE Z-Wave in wall fan switch as a fan component instead of a light dimmer switch component. * Update requirements_all.txt --- homeassistant/components/wink/__init__.py | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/wink/__init__.py b/homeassistant/components/wink/__init__.py index 042943f7a3f..f3ec360462e 100644 --- a/homeassistant/components/wink/__init__.py +++ b/homeassistant/components/wink/__init__.py @@ -26,7 +26,7 @@ from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.event import track_time_interval from homeassistant.util.json import load_json, save_json -REQUIREMENTS = ['python-wink==1.7.3', 'pubnubsub-handler==1.0.2'] +REQUIREMENTS = ['python-wink==1.8.0', 'pubnubsub-handler==1.0.2'] _LOGGER = logging.getLogger(__name__) diff --git a/requirements_all.txt b/requirements_all.txt index c1922f52bdf..41b7f4036da 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1096,7 +1096,7 @@ python-velbus==2.0.11 python-vlc==1.1.2 # homeassistant.components.wink -python-wink==1.7.3 +python-wink==1.8.0 # homeassistant.components.sensor.swiss_public_transport python_opendata_transport==0.1.3 From 08adfd87f7df4f04d4d748fcf3a2f3dd79e15aa0 Mon Sep 17 00:00:00 2001 From: Marius Date: Wed, 13 Jun 2018 17:20:38 +0300 Subject: [PATCH 079/144] Add unique_id for mqtt binary sensor (#14929) * Added unique_id for mqtt binary sensor * Added missing mqtt message fire in test --- .../components/binary_sensor/mqtt.py | 18 +++++++++++++++--- tests/components/binary_sensor/test_mqtt.py | 19 +++++++++++++++++++ 2 files changed, 34 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/binary_sensor/mqtt.py b/homeassistant/components/binary_sensor/mqtt.py index e033355f655..d2533eb8f5b 100644 --- a/homeassistant/components/binary_sensor/mqtt.py +++ b/homeassistant/components/binary_sensor/mqtt.py @@ -6,6 +6,7 @@ https://home-assistant.io/components/binary_sensor.mqtt/ """ import asyncio import logging +from typing import Optional import voluptuous as vol @@ -24,7 +25,7 @@ import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) DEFAULT_NAME = 'MQTT Binary sensor' - +CONF_UNIQUE_ID = 'unique_id' DEFAULT_PAYLOAD_OFF = 'OFF' DEFAULT_PAYLOAD_ON = 'ON' DEFAULT_FORCE_UPDATE = False @@ -37,6 +38,9 @@ PLATFORM_SCHEMA = mqtt.MQTT_RO_PLATFORM_SCHEMA.extend({ vol.Optional(CONF_PAYLOAD_ON, default=DEFAULT_PAYLOAD_ON): cv.string, vol.Optional(CONF_DEVICE_CLASS): DEVICE_CLASSES_SCHEMA, vol.Optional(CONF_FORCE_UPDATE, default=DEFAULT_FORCE_UPDATE): cv.boolean, + # Integrations shouldn't never expose unique_id through configuration + # this here is an exception because MQTT is a msg transport, not a protocol + vol.Optional(CONF_UNIQUE_ID): cv.string, }).extend(mqtt.MQTT_AVAILABILITY_SCHEMA.schema) @@ -61,7 +65,8 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None): config.get(CONF_PAYLOAD_OFF), config.get(CONF_PAYLOAD_AVAILABLE), config.get(CONF_PAYLOAD_NOT_AVAILABLE), - value_template + value_template, + config.get(CONF_UNIQUE_ID), )]) @@ -70,7 +75,8 @@ class MqttBinarySensor(MqttAvailability, BinarySensorDevice): def __init__(self, name, state_topic, availability_topic, device_class, qos, force_update, payload_on, payload_off, payload_available, - payload_not_available, value_template): + payload_not_available, value_template, + unique_id: Optional[str]): """Initialize the MQTT binary sensor.""" super().__init__(availability_topic, qos, payload_available, payload_not_available) @@ -83,6 +89,7 @@ class MqttBinarySensor(MqttAvailability, BinarySensorDevice): self._qos = qos self._force_update = force_update self._template = value_template + self._unique_id = unique_id @asyncio.coroutine def async_added_to_hass(self): @@ -134,3 +141,8 @@ class MqttBinarySensor(MqttAvailability, BinarySensorDevice): def force_update(self): """Force update.""" return self._force_update + + @property + def unique_id(self): + """Return a unique ID.""" + return self._unique_id diff --git a/tests/components/binary_sensor/test_mqtt.py b/tests/components/binary_sensor/test_mqtt.py index 9b5cf7aa736..71eba2df950 100644 --- a/tests/components/binary_sensor/test_mqtt.py +++ b/tests/components/binary_sensor/test_mqtt.py @@ -77,6 +77,25 @@ class TestSensorMQTT(unittest.TestCase): state = self.hass.states.get('binary_sensor.test') self.assertIsNone(state) + def test_unique_id(self): + """Test unique id option only creates one sensor per unique_id.""" + assert setup_component(self.hass, binary_sensor.DOMAIN, { + binary_sensor.DOMAIN: [{ + 'platform': 'mqtt', + 'name': 'Test 1', + 'state_topic': 'test-topic', + 'unique_id': 'TOTALLY_UNIQUE' + }, { + 'platform': 'mqtt', + 'name': 'Test 2', + 'state_topic': 'test-topic', + 'unique_id': 'TOTALLY_UNIQUE' + }] + }) + fire_mqtt_message(self.hass, 'test-topic', 'payload') + self.hass.block_till_done() + assert len(self.hass.states.all()) == 1 + def test_availability_without_topic(self): """Test availability without defined availability topic.""" self.assertTrue(setup_component(self.hass, binary_sensor.DOMAIN, { From d549e26a9b6fc6158f3bf22a419a65ef4205a1e1 Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Wed, 13 Jun 2018 09:00:33 -0600 Subject: [PATCH 080/144] Make Yi platform async (#14944) * Conversion complete * Updated requirements * Got rid of 3.6-specific syntax * Removed more 3.6-specific syntax * Contributor-requested changes --- homeassistant/components/camera/yi.py | 109 ++++++++++++++------------ requirements_all.txt | 3 + 2 files changed, 62 insertions(+), 50 deletions(-) diff --git a/homeassistant/components/camera/yi.py b/homeassistant/components/camera/yi.py index 41fe816c479..868c5afb447 100644 --- a/homeassistant/components/camera/yi.py +++ b/homeassistant/components/camera/yi.py @@ -11,11 +11,13 @@ import voluptuous as vol from homeassistant.components.camera import Camera, PLATFORM_SCHEMA from homeassistant.components.ffmpeg import DATA_FFMPEG -from homeassistant.const import (CONF_HOST, CONF_NAME, CONF_PATH, - CONF_PASSWORD, CONF_PORT, CONF_USERNAME) +from homeassistant.const import ( + CONF_HOST, CONF_NAME, CONF_PATH, CONF_PASSWORD, CONF_PORT, CONF_USERNAME) from homeassistant.helpers import config_validation as cv from homeassistant.helpers.aiohttp_client import async_aiohttp_proxy_stream +from homeassistant.exceptions import PlatformNotReady +REQUIREMENTS = ['aioftp==0.10.1'] DEPENDENCIES = ['ffmpeg'] _LOGGER = logging.getLogger(__name__) @@ -38,12 +40,9 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ }) -async def async_setup_platform(hass, - config, - async_add_devices, - discovery_info=None): +async def async_setup_platform( + hass, config, async_add_devices, discovery_info=None): """Set up a Yi Camera.""" - _LOGGER.debug('Received configuration: %s', config) async_add_devices([YiCamera(hass, config)], True) @@ -54,71 +53,81 @@ class YiCamera(Camera): """Initialize.""" super().__init__() self._extra_arguments = config.get(CONF_FFMPEG_ARGUMENTS) + self._ftp = None self._last_image = None self._last_url = None self._manager = hass.data[DATA_FFMPEG] - self._name = config.get(CONF_NAME) - self.host = config.get(CONF_HOST) - self.port = config.get(CONF_PORT) - self.path = config.get(CONF_PATH) - self.user = config.get(CONF_USERNAME) - self.passwd = config.get(CONF_PASSWORD) + self._name = config[CONF_NAME] + self.host = config[CONF_HOST] + self.port = config[CONF_PORT] + self.path = config[CONF_PATH] + self.user = config[CONF_USERNAME] + self.passwd = config[CONF_PASSWORD] - @property - def name(self): - """Return the name of this camera.""" - return self._name + hass.async_add_job(self._connect_to_client) @property def brand(self): """Camera brand.""" return DEFAULT_BRAND - def get_latest_video_url(self): + @property + def name(self): + """Return the name of this camera.""" + return self._name + + async def _connect_to_client(self): + """Attempt to establish a connection via FTP.""" + from aioftp import Client, StatusCodeError + + ftp = Client() + try: + await ftp.connect(self.host) + await ftp.login(self.user, self.passwd) + self._ftp = ftp + except StatusCodeError as err: + raise PlatformNotReady(err) + + async def _get_latest_video_url(self): """Retrieve the latest video file from the customized Yi FTP server.""" - from ftplib import FTP, error_perm - - ftp = FTP(self.host) - try: - ftp.login(self.user, self.passwd) - except error_perm as exc: - _LOGGER.error('There was an error while logging into the camera') - _LOGGER.debug(exc) - return False + from aioftp import StatusCodeError try: - ftp.cwd(self.path) - except error_perm as exc: - _LOGGER.error('Unable to find path: %s', self.path) - _LOGGER.debug(exc) - return False + await self._ftp.change_directory(self.path) + dirs = [] + for path, attrs in await self._ftp.list(): + if attrs['type'] == 'dir' and '.' not in str(path): + dirs.append(path) + latest_dir = dirs[-1] + await self._ftp.change_directory(latest_dir) - dirs = [d for d in ftp.nlst() if '.' not in d] - if not dirs: - _LOGGER.warning("There don't appear to be any uploaded videos") - return False + videos = [] + for path, _ in await self._ftp.list(): + videos.append(path) + if not videos: + _LOGGER.info('Video folder "%s" empty; delaying', latest_dir) + return None - latest_dir = dirs[-1] - ftp.cwd(latest_dir) - videos = ftp.nlst() - if not videos: - _LOGGER.info('Video folder "%s" is empty; delaying', latest_dir) - return False - - return 'ftp://{0}:{1}@{2}:{3}{4}/{5}/{6}'.format( - self.user, self.passwd, self.host, self.port, self.path, - latest_dir, videos[-1]) + return 'ftp://{0}:{1}@{2}:{3}{4}/{5}/{6}'.format( + self.user, self.passwd, self.host, self.port, self.path, + latest_dir, videos[-1]) + except (ConnectionRefusedError, StatusCodeError) as err: + _LOGGER.error('Error while fetching video: %s', err) + return None async def async_camera_image(self): """Return a still image response from the camera.""" from haffmpeg import ImageFrame, IMAGE_JPEG - url = await self.hass.async_add_job(self.get_latest_video_url) + url = await self._get_latest_video_url() if url != self._last_url: ffmpeg = ImageFrame(self._manager.binary, loop=self.hass.loop) - self._last_image = await asyncio.shield(ffmpeg.get_image( - url, output_format=IMAGE_JPEG, - extra_cmd=self._extra_arguments), loop=self.hass.loop) + self._last_image = await asyncio.shield( + ffmpeg.get_image( + url, + output_format=IMAGE_JPEG, + extra_cmd=self._extra_arguments), + loop=self.hass.loop) self._last_url = url return self._last_image diff --git a/requirements_all.txt b/requirements_all.txt index 41b7f4036da..c180c3a055d 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -84,6 +84,9 @@ aiodns==1.1.1 # homeassistant.components.device_tracker.freebox aiofreepybox==0.0.3 +# homeassistant.components.camera.yi +aioftp==0.10.1 + # homeassistant.components.emulated_hue # homeassistant.components.http aiohttp_cors==0.7.0 From e014a84215e7bab47bf0adb37415fae39dd6c90c Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 13 Jun 2018 11:14:52 -0400 Subject: [PATCH 081/144] Nest config flow (#14921) * Move nest to dir based component * Add config flow for Nest * Load Nest platforms via config entry * Add tests for Nest config flow * Import existing access tokens as config entries * Lint * Update coverage * Update translation * Fix tests * Address strings * Use python-nest token resolution * Lint * Do not do I/O inside constructor * Lint * Update test requirements --- .coveragerc | 2 +- .../components/binary_sensor/nest.py | 69 ++++--- homeassistant/components/camera/__init__.py | 10 + homeassistant/components/camera/nest.py | 15 +- homeassistant/components/climate/__init__.py | 13 +- homeassistant/components/climate/nest.py | 18 +- .../components/nest/.translations/en.json | 33 ++++ .../components/{nest.py => nest/__init__.py} | 130 ++++--------- homeassistant/components/nest/config_flow.py | 154 ++++++++++++++++ homeassistant/components/nest/const.py | 2 + homeassistant/components/nest/local_auth.py | 45 +++++ homeassistant/components/nest/strings.json | 33 ++++ homeassistant/components/sensor/nest.py | 53 +++--- homeassistant/config_entries.py | 1 + homeassistant/data_entry_flow.py | 4 +- requirements_test_all.txt | 3 + script/gen_requirements_all.py | 1 + .../components/config/test_config_entries.py | 7 + tests/components/nest/__init__.py | 1 + tests/components/nest/test_config_flow.py | 174 ++++++++++++++++++ tests/components/nest/test_local_auth.py | 51 +++++ 21 files changed, 666 insertions(+), 153 deletions(-) create mode 100644 homeassistant/components/nest/.translations/en.json rename homeassistant/components/{nest.py => nest/__init__.py} (72%) create mode 100644 homeassistant/components/nest/config_flow.py create mode 100644 homeassistant/components/nest/const.py create mode 100644 homeassistant/components/nest/local_auth.py create mode 100644 homeassistant/components/nest/strings.json create mode 100644 tests/components/nest/__init__.py create mode 100644 tests/components/nest/test_config_flow.py create mode 100644 tests/components/nest/test_local_auth.py diff --git a/.coveragerc b/.coveragerc index 693ca12d10e..fa2ec6e9f27 100644 --- a/.coveragerc +++ b/.coveragerc @@ -195,7 +195,7 @@ omit = homeassistant/components/neato.py homeassistant/components/*/neato.py - homeassistant/components/nest.py + homeassistant/components/nest/__init__.py homeassistant/components/*/nest.py homeassistant/components/netatmo.py diff --git a/homeassistant/components/binary_sensor/nest.py b/homeassistant/components/binary_sensor/nest.py index 882ff142e8c..9da352e1268 100644 --- a/homeassistant/components/binary_sensor/nest.py +++ b/homeassistant/components/binary_sensor/nest.py @@ -8,7 +8,8 @@ from itertools import chain import logging from homeassistant.components.binary_sensor import BinarySensorDevice -from homeassistant.components.nest import DATA_NEST, NestSensorDevice +from homeassistant.components.nest import ( + DATA_NEST, DATA_NEST_CONFIG, CONF_BINARY_SENSORS, NestSensorDevice) from homeassistant.const import CONF_MONITORED_CONDITIONS DEPENDENCIES = ['nest'] @@ -56,12 +57,19 @@ _LOGGER = logging.getLogger(__name__) def setup_platform(hass, config, add_devices, discovery_info=None): - """Set up the Nest binary sensors.""" - if discovery_info is None: - return + """Set up the Nest binary sensors. + No longer used. + """ + + +async def async_setup_entry(hass, entry, async_add_devices): + """Set up a Nest binary sensor based on a config entry.""" nest = hass.data[DATA_NEST] + discovery_info = \ + hass.data.get(DATA_NEST_CONFIG, {}).get(CONF_BINARY_SENSORS, {}) + # Add all available binary sensors if no Nest binary sensor config is set if discovery_info == {}: conditions = _VALID_BINARY_SENSOR_TYPES @@ -76,32 +84,37 @@ def setup_platform(hass, config, add_devices, discovery_info=None): "for valid options.") _LOGGER.error(wstr) - sensors = [] - for structure in nest.structures(): - sensors += [NestBinarySensor(structure, None, variable) - for variable in conditions - if variable in STRUCTURE_BINARY_TYPES] - device_chain = chain(nest.thermostats(), - nest.smoke_co_alarms(), - nest.cameras()) - for structure, device in device_chain: - sensors += [NestBinarySensor(structure, device, variable) - for variable in conditions - if variable in BINARY_TYPES] - sensors += [NestBinarySensor(structure, device, variable) - for variable in conditions - if variable in CLIMATE_BINARY_TYPES - and device.is_thermostat] - - if device.is_camera: + def get_binary_sensors(): + """Get the Nest binary sensors.""" + sensors = [] + for structure in nest.structures(): + sensors += [NestBinarySensor(structure, None, variable) + for variable in conditions + if variable in STRUCTURE_BINARY_TYPES] + device_chain = chain(nest.thermostats(), + nest.smoke_co_alarms(), + nest.cameras()) + for structure, device in device_chain: sensors += [NestBinarySensor(structure, device, variable) for variable in conditions - if variable in CAMERA_BINARY_TYPES] - for activity_zone in device.activity_zones: - sensors += [NestActivityZoneSensor(structure, - device, - activity_zone)] - add_devices(sensors, True) + if variable in BINARY_TYPES] + sensors += [NestBinarySensor(structure, device, variable) + for variable in conditions + if variable in CLIMATE_BINARY_TYPES + and device.is_thermostat] + + if device.is_camera: + sensors += [NestBinarySensor(structure, device, variable) + for variable in conditions + if variable in CAMERA_BINARY_TYPES] + for activity_zone in device.activity_zones: + sensors += [NestActivityZoneSensor(structure, + device, + activity_zone)] + + return sensors + + async_add_devices(await hass.async_add_job(get_binary_sensors), True) class NestBinarySensor(NestSensorDevice, BinarySensorDevice): diff --git a/homeassistant/components/camera/__init__.py b/homeassistant/components/camera/__init__.py index 60f8979bb16..f2f4081fb6d 100644 --- a/homeassistant/components/camera/__init__.py +++ b/homeassistant/components/camera/__init__.py @@ -216,6 +216,16 @@ def async_setup(hass, config): return True +async def async_setup_entry(hass, entry): + """Setup a config entry.""" + return await hass.data[DOMAIN].async_setup_entry(entry) + + +async def async_unload_entry(hass, entry): + """Unload a config entry.""" + return await hass.data[DOMAIN].async_unload_entry(entry) + + class Camera(Entity): """The base class for camera entities.""" diff --git a/homeassistant/components/camera/nest.py b/homeassistant/components/camera/nest.py index 6ffb7ef8561..ab26df5caf0 100644 --- a/homeassistant/components/camera/nest.py +++ b/homeassistant/components/camera/nest.py @@ -23,14 +23,19 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({}) def setup_platform(hass, config, add_devices, discovery_info=None): - """Set up a Nest Cam.""" - if discovery_info is None: - return + """Set up a Nest Cam. - camera_devices = hass.data[nest.DATA_NEST].cameras() + No longer in use. + """ + + +async def async_setup_entry(hass, entry, async_add_devices): + """Set up a Nest sensor based on a config entry.""" + camera_devices = \ + await hass.async_add_job(hass.data[nest.DATA_NEST].cameras) cameras = [NestCamera(structure, device) for structure, device in camera_devices] - add_devices(cameras, True) + async_add_devices(cameras, True) class NestCamera(Camera): diff --git a/homeassistant/components/climate/__init__.py b/homeassistant/components/climate/__init__.py index ebe7cbbf2c1..a47edc5af42 100644 --- a/homeassistant/components/climate/__init__.py +++ b/homeassistant/components/climate/__init__.py @@ -246,7 +246,8 @@ def set_swing_mode(hass, swing_mode, entity_id=None): async def async_setup(hass, config): """Set up climate devices.""" - component = EntityComponent(_LOGGER, DOMAIN, hass, SCAN_INTERVAL) + component = hass.data[DOMAIN] = \ + EntityComponent(_LOGGER, DOMAIN, hass, SCAN_INTERVAL) await component.async_setup(config) async def async_away_mode_set_service(service): @@ -456,6 +457,16 @@ async def async_setup(hass, config): return True +async def async_setup_entry(hass, entry): + """Setup a config entry.""" + return await hass.data[DOMAIN].async_setup_entry(entry) + + +async def async_unload_entry(hass, entry): + """Unload a config entry.""" + return await hass.data[DOMAIN].async_unload_entry(entry) + + class ClimateDevice(Entity): """Representation of a climate device.""" diff --git a/homeassistant/components/climate/nest.py b/homeassistant/components/climate/nest.py index 696f1479c08..dc1f74613bc 100644 --- a/homeassistant/components/climate/nest.py +++ b/homeassistant/components/climate/nest.py @@ -32,16 +32,22 @@ NEST_MODE_HEAT_COOL = 'heat-cool' def setup_platform(hass, config, add_devices, discovery_info=None): - """Set up the Nest thermostat.""" - if discovery_info is None: - return + """Set up the Nest thermostat. + No longer in use. + """ + + +async def async_setup_entry(hass, entry, async_add_devices): + """Set up the Nest climate device based on a config entry.""" temp_unit = hass.config.units.temperature_unit - all_devices = [NestThermostat(structure, device, temp_unit) - for structure, device in hass.data[DATA_NEST].thermostats()] + thermostats = await hass.async_add_job(hass.data[DATA_NEST].thermostats) - add_devices(all_devices, True) + all_devices = [NestThermostat(structure, device, temp_unit) + for structure, device in thermostats] + + async_add_devices(all_devices, True) class NestThermostat(ClimateDevice): diff --git a/homeassistant/components/nest/.translations/en.json b/homeassistant/components/nest/.translations/en.json new file mode 100644 index 00000000000..cf448bb35e7 --- /dev/null +++ b/homeassistant/components/nest/.translations/en.json @@ -0,0 +1,33 @@ +{ + "config": { + "abort": { + "already_setup": "You can only configure a single Nest account.", + "authorize_url_fail": "Unknown error generating an authorize url.", + "authorize_url_timeout": "Timeout generating authorize url.", + "no_flows": "You need to configure Nest before being able to authenticate with it. [Please read the instructions](https://www.home-assistant.io/components/nest/)." + }, + "error": { + "internal_error": "Internal error validating code", + "invalid_code": "Invalid code", + "timeout": "Timeout validating code", + "unknown": "Unknown error validating code" + }, + "step": { + "init": { + "data": { + "flow_impl": "Provider" + }, + "description": "Pick via which authentication provider you want to authenticate with Nest.", + "title": "Authentication Provider" + }, + "link": { + "data": { + "code": "Pin code" + }, + "description": "To link your Nest account, [authorize your account]({url}).\n\nAfter authorization, copy-paste the provided pin code below.", + "title": "Link Nest Account" + } + }, + "title": "Nest" + } +} \ No newline at end of file diff --git a/homeassistant/components/nest.py b/homeassistant/components/nest/__init__.py similarity index 72% rename from homeassistant/components/nest.py rename to homeassistant/components/nest/__init__.py index 3ca1c483ee0..19d65061a89 100644 --- a/homeassistant/components/nest.py +++ b/homeassistant/components/nest/__init__.py @@ -6,6 +6,7 @@ https://home-assistant.io/components/nest/ """ from concurrent.futures import ThreadPoolExecutor import logging +import os.path import socket from datetime import datetime, timedelta @@ -15,19 +16,22 @@ from homeassistant.const import ( CONF_STRUCTURE, CONF_FILENAME, CONF_BINARY_SENSORS, CONF_SENSORS, CONF_MONITORED_CONDITIONS, EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP) -from homeassistant.helpers import discovery, config_validation as cv +from homeassistant.helpers import config_validation as cv from homeassistant.helpers.dispatcher import async_dispatcher_send, \ async_dispatcher_connect from homeassistant.helpers.entity import Entity +from .const import DOMAIN +from . import local_auth + REQUIREMENTS = ['python-nest==4.0.2'] _CONFIGURING = {} _LOGGER = logging.getLogger(__name__) -DOMAIN = 'nest' DATA_NEST = 'nest' +DATA_NEST_CONFIG = 'nest_config' SIGNAL_NEST_UPDATE = 'nest_update' @@ -86,76 +90,45 @@ async def async_nest_update_event_broker(hass, nest): return -async def async_request_configuration(nest, hass, config): - """Request configuration steps from the user.""" - configurator = hass.components.configurator - if 'nest' in _CONFIGURING: - _LOGGER.debug("configurator failed") - configurator.async_notify_errors( - _CONFIGURING['nest'], "Failed to configure, please try again.") +async def async_setup(hass, config): + """Set up Nest components.""" + if DOMAIN not in config: return - async def async_nest_config_callback(data): - """Run when the configuration callback is called.""" - _LOGGER.debug("configurator callback") - pin = data.get('pin') - if await async_setup_nest(hass, nest, config, pin=pin): - # start nest update event listener as we missed startup hook - hass.async_add_job(async_nest_update_event_broker, hass, nest) + conf = config[DOMAIN] - _CONFIGURING['nest'] = configurator.async_request_config( - "Nest", async_nest_config_callback, - description=('To configure Nest, click Request Authorization below, ' - 'log into your Nest account, ' - 'and then enter the resulting PIN'), - link_name='Request Authorization', - link_url=nest.authorize_url, - submit_caption="Confirm", - fields=[{'id': 'pin', 'name': 'Enter the PIN', 'type': ''}] - ) + local_auth.initialize(hass, conf[CONF_CLIENT_ID], conf[CONF_CLIENT_SECRET]) + + filename = config.get(CONF_FILENAME, NEST_CONFIG_FILE) + access_token_cache_file = hass.config.path(filename) + + if await hass.async_add_job(os.path.isfile, access_token_cache_file): + hass.async_add_job(hass.config_entries.flow.async_init( + DOMAIN, source='import', data={ + 'nest_conf_path': access_token_cache_file, + } + )) + + # Store config to be used during entry setup + hass.data[DATA_NEST_CONFIG] = conf + + return True -async def async_setup_nest(hass, nest, config, pin=None): - """Set up the Nest devices.""" - from nest.nest import AuthorizationError, APIError - if pin is not None: - _LOGGER.debug("pin acquired, requesting access token") - error_message = None - try: - nest.request_token(pin) - except AuthorizationError as auth_error: - error_message = "Nest authorization failed: {}".format(auth_error) - except APIError as api_error: - error_message = "Failed to call Nest API: {}".format(api_error) +async def async_setup_entry(hass, entry): + """Setup Nest from a config entry.""" + from nest import Nest - if error_message is not None: - _LOGGER.warning(error_message) - hass.components.configurator.async_notify_errors( - _CONFIGURING['nest'], error_message) - return False - - if nest.access_token is None: - _LOGGER.debug("no access_token, requesting configuration") - await async_request_configuration(nest, hass, config) - return False - - if 'nest' in _CONFIGURING: - _LOGGER.debug("configuration done") - configurator = hass.components.configurator - configurator.async_request_done(_CONFIGURING.pop('nest')) + nest = Nest(access_token=entry.data['tokens']['access_token']) _LOGGER.debug("proceeding with setup") - conf = config[DOMAIN] + conf = hass.data.get(DATA_NEST_CONFIG, {}) hass.data[DATA_NEST] = NestDevice(hass, conf, nest) + await hass.async_add_job(hass.data[DATA_NEST].initialize) - for component, discovered in [ - ('climate', {}), - ('camera', {}), - ('sensor', conf.get(CONF_SENSORS, {})), - ('binary_sensor', conf.get(CONF_BINARY_SENSORS, {}))]: - _LOGGER.debug("proceeding with discovery -- %s", component) - hass.async_add_job(discovery.async_load_platform, - hass, component, DOMAIN, discovered, config) + for component in 'climate', 'camera', 'sensor', 'binary_sensor': + hass.async_add_job(hass.config_entries.async_forward_entry_setup( + entry, component)) def set_mode(service): """ @@ -210,29 +183,6 @@ async def async_setup_nest(hass, nest, config, pin=None): return True -async def async_setup(hass, config): - """Set up Nest components.""" - from nest import Nest - - if 'nest' in _CONFIGURING: - return - - conf = config[DOMAIN] - client_id = conf[CONF_CLIENT_ID] - client_secret = conf[CONF_CLIENT_SECRET] - filename = config.get(CONF_FILENAME, NEST_CONFIG_FILE) - - access_token_cache_file = hass.config.path(filename) - - nest = Nest( - access_token_cache_file=access_token_cache_file, - client_id=client_id, client_secret=client_secret) - - await async_setup_nest(hass, nest, config) - - return True - - class NestDevice(object): """Structure Nest functions for hass.""" @@ -240,12 +190,12 @@ class NestDevice(object): """Init Nest Devices.""" self.hass = hass self.nest = nest + self.local_structure = conf.get(CONF_STRUCTURE) - if CONF_STRUCTURE not in conf: - self.local_structure = [s.name for s in nest.structures] - else: - self.local_structure = conf[CONF_STRUCTURE] - _LOGGER.debug("Structures to include: %s", self.local_structure) + def initialize(self): + """Initialize Nest.""" + if self.local_structure is None: + self.local_structure = [s.name for s in self.nest.structures] def structures(self): """Generate a list of structures.""" diff --git a/homeassistant/components/nest/config_flow.py b/homeassistant/components/nest/config_flow.py new file mode 100644 index 00000000000..ee83598235c --- /dev/null +++ b/homeassistant/components/nest/config_flow.py @@ -0,0 +1,154 @@ +"""Config flow to configure Nest.""" +import asyncio +from collections import OrderedDict +import logging + +import async_timeout +import voluptuous as vol + +from homeassistant import config_entries, data_entry_flow +from homeassistant.core import callback +from homeassistant.exceptions import HomeAssistantError +from homeassistant.util.json import load_json + +from .const import DOMAIN + + +DATA_FLOW_IMPL = 'nest_flow_implementation' +_LOGGER = logging.getLogger(__name__) + + +@callback +def register_flow_implementation(hass, domain, name, gen_authorize_url, + convert_code): + """Register a flow implementation. + + domain: Domain of the component responsible for the implementation. + name: Name of the component. + gen_authorize_url: Coroutine function to generate the authorize url. + convert_code: Coroutine function to convert a code to an access token. + """ + if DATA_FLOW_IMPL not in hass.data: + hass.data[DATA_FLOW_IMPL] = OrderedDict() + + hass.data[DATA_FLOW_IMPL][domain] = { + 'domain': domain, + 'name': name, + 'gen_authorize_url': gen_authorize_url, + 'convert_code': convert_code, + } + + +class NestAuthError(HomeAssistantError): + """Base class for Nest auth errors.""" + + +class CodeInvalid(NestAuthError): + """Raised when invalid authorization code.""" + + +@config_entries.HANDLERS.register(DOMAIN) +class NestFlowHandler(data_entry_flow.FlowHandler): + """Handle a Nest config flow.""" + + VERSION = 1 + + def __init__(self): + """Initialize the Nest config flow.""" + self.flow_impl = None + + async def async_step_init(self, user_input=None): + """Handle a flow start.""" + flows = self.hass.data.get(DATA_FLOW_IMPL, {}) + + if self.hass.config_entries.async_entries(DOMAIN): + return self.async_abort(reason='already_setup') + + elif not flows: + return self.async_abort(reason='no_flows') + + elif len(flows) == 1: + self.flow_impl = list(flows)[0] + return await self.async_step_link() + + elif user_input is not None: + self.flow_impl = user_input['flow_impl'] + return await self.async_step_link() + + return self.async_show_form( + step_id='init', + data_schema=vol.Schema({ + vol.Required('flow_impl'): vol.In(list(flows)) + }) + ) + + async def async_step_link(self, user_input=None): + """Attempt to link with the Nest account. + + Route the user to a website to authenticate with Nest. Depending on + implementation type we expect a pin or an external component to + deliver the authentication code. + """ + flow = self.hass.data[DATA_FLOW_IMPL][self.flow_impl] + + errors = {} + + if user_input is not None: + try: + with async_timeout.timeout(10): + tokens = await flow['convert_code'](user_input['code']) + return self._entry_from_tokens( + 'Nest (via {})'.format(flow['name']), flow, tokens) + + except asyncio.TimeoutError: + errors['code'] = 'timeout' + except CodeInvalid: + errors['code'] = 'invalid_code' + except NestAuthError: + errors['code'] = 'unknown' + except Exception: # pylint: disable=broad-except + errors['code'] = 'internal_error' + _LOGGER.exception("Unexpected error resolving code") + + try: + with async_timeout.timeout(10): + url = await flow['gen_authorize_url'](self.flow_id) + except asyncio.TimeoutError: + return self.async_abort(reason='authorize_url_timeout') + except Exception: # pylint: disable=broad-except + _LOGGER.exception("Unexpected error generating auth url") + return self.async_abort(reason='authorize_url_fail') + + return self.async_show_form( + step_id='link', + description_placeholders={ + 'url': url + }, + data_schema=vol.Schema({ + vol.Required('code'): str, + }), + errors=errors, + ) + + async def async_step_import(self, info): + """Import existing auth from Nest.""" + if self.hass.config_entries.async_entries(DOMAIN): + return self.async_abort(reason='already_setup') + + flow = self.hass.data[DATA_FLOW_IMPL][DOMAIN] + tokens = await self.hass.async_add_job( + load_json, info['nest_conf_path']) + + return self._entry_from_tokens( + 'Nest (import from configuration.yaml)', flow, tokens) + + @callback + def _entry_from_tokens(self, title, flow, tokens): + """Create an entry from tokens.""" + return self.async_create_entry( + title=title, + data={ + 'tokens': tokens, + 'impl_domain': flow['domain'], + }, + ) diff --git a/homeassistant/components/nest/const.py b/homeassistant/components/nest/const.py new file mode 100644 index 00000000000..835918f6a04 --- /dev/null +++ b/homeassistant/components/nest/const.py @@ -0,0 +1,2 @@ +"""Constants used by the Nest component.""" +DOMAIN = 'nest' diff --git a/homeassistant/components/nest/local_auth.py b/homeassistant/components/nest/local_auth.py new file mode 100644 index 00000000000..5ab10cc2a5e --- /dev/null +++ b/homeassistant/components/nest/local_auth.py @@ -0,0 +1,45 @@ +"""Local Nest authentication.""" +import asyncio +from functools import partial + +from homeassistant.core import callback +from . import config_flow +from .const import DOMAIN + + +@callback +def initialize(hass, client_id, client_secret): + """Initialize a local auth provider.""" + config_flow.register_flow_implementation( + hass, DOMAIN, 'local', partial(generate_auth_url, client_id), + partial(resolve_auth_code, hass, client_id, client_secret) + ) + + +async def generate_auth_url(client_id, flow_id): + """Generate an authorize url.""" + from nest.nest import AUTHORIZE_URL + return AUTHORIZE_URL.format(client_id, flow_id) + + +async def resolve_auth_code(hass, client_id, client_secret, code): + """Resolve an authorization code.""" + from nest.nest import NestAuth, AuthorizationError + + result = asyncio.Future() + auth = NestAuth( + client_id=client_id, + client_secret=client_secret, + auth_callback=result.set_result, + ) + auth.pin = code + + try: + await hass.async_add_job(auth.login) + return await result + except AuthorizationError as err: + if err.response.status_code == 401: + raise config_flow.CodeInvalid() + else: + raise config_flow.NestAuthError('Unknown error: {} ({})'.format( + err, err.response.status_code)) diff --git a/homeassistant/components/nest/strings.json b/homeassistant/components/nest/strings.json new file mode 100644 index 00000000000..5a70e3fd48d --- /dev/null +++ b/homeassistant/components/nest/strings.json @@ -0,0 +1,33 @@ +{ + "config": { + "title": "Nest", + "step": { + "init": { + "title": "Authentication Provider", + "description": "Pick via which authentication provider you want to authenticate with Nest.", + "data": { + "flow_impl": "Provider" + } + }, + "link": { + "title": "Link Nest Account", + "description": "To link your Nest account, [authorize your account]({url}).\n\nAfter authorization, copy-paste the provided pin code below.", + "data": { + "code": "Pin code" + } + } + }, + "error": { + "timeout": "Timeout validating code", + "invalid_code": "Invalid code", + "unknown": "Unknown error validating code", + "internal_error": "Internal error validating code" + }, + "abort": { + "already_setup": "You can only configure a single Nest account.", + "no_flows": "You need to configure Nest before being able to authenticate with it. [Please read the instructions](https://www.home-assistant.io/components/nest/).", + "authorize_url_timeout": "Timeout generating authorize url.", + "authorize_url_fail": "Unknown error generating an authorize url." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/sensor/nest.py b/homeassistant/components/sensor/nest.py index ea7a943881e..bf1b3f65c4a 100644 --- a/homeassistant/components/sensor/nest.py +++ b/homeassistant/components/sensor/nest.py @@ -6,7 +6,8 @@ https://home-assistant.io/components/sensor.nest/ """ import logging -from homeassistant.components.nest import DATA_NEST, NestSensorDevice +from homeassistant.components.nest import ( + DATA_NEST, DATA_NEST_CONFIG, CONF_SENSORS, NestSensorDevice) from homeassistant.const import ( TEMP_CELSIUS, TEMP_FAHRENHEIT, CONF_MONITORED_CONDITIONS, DEVICE_CLASS_TEMPERATURE, DEVICE_CLASS_HUMIDITY) @@ -51,12 +52,18 @@ _LOGGER = logging.getLogger(__name__) def setup_platform(hass, config, add_devices, discovery_info=None): - """Set up the Nest Sensor.""" - if discovery_info is None: - return + """Set up the Nest Sensor. + No longer used. + """ + + +async def async_setup_entry(hass, entry, async_add_devices): + """Set up a Nest sensor based on a config entry.""" nest = hass.data[DATA_NEST] + discovery_info = hass.data.get(DATA_NEST_CONFIG, {}).get(CONF_SENSORS, {}) + # Add all available sensors if no Nest sensor config is set if discovery_info == {}: conditions = _VALID_SENSOR_TYPES @@ -77,26 +84,30 @@ def setup_platform(hass, config, add_devices, discovery_info=None): "binary_sensor.nest/ for valid options.") _LOGGER.error(wstr) - all_sensors = [] - for structure in nest.structures(): - all_sensors += [NestBasicSensor(structure, None, variable) - for variable in conditions - if variable in STRUCTURE_SENSOR_TYPES] + def get_sensors(): + """Get the Nest sensors.""" + all_sensors = [] + for structure in nest.structures(): + all_sensors += [NestBasicSensor(structure, None, variable) + for variable in conditions + if variable in STRUCTURE_SENSOR_TYPES] - for structure, device in nest.thermostats(): - all_sensors += [NestBasicSensor(structure, device, variable) - for variable in conditions - if variable in SENSOR_TYPES] - all_sensors += [NestTempSensor(structure, device, variable) - for variable in conditions - if variable in TEMP_SENSOR_TYPES] + for structure, device in nest.thermostats(): + all_sensors += [NestBasicSensor(structure, device, variable) + for variable in conditions + if variable in SENSOR_TYPES] + all_sensors += [NestTempSensor(structure, device, variable) + for variable in conditions + if variable in TEMP_SENSOR_TYPES] - for structure, device in nest.smoke_co_alarms(): - all_sensors += [NestBasicSensor(structure, device, variable) - for variable in conditions - if variable in PROTECT_SENSOR_TYPES] + for structure, device in nest.smoke_co_alarms(): + all_sensors += [NestBasicSensor(structure, device, variable) + for variable in conditions + if variable in PROTECT_SENSOR_TYPES] - add_devices(all_sensors, True) + return all_sensors + + async_add_devices(await hass.async_add_job(get_sensors), True) class NestBasicSensor(NestSensorDevice): diff --git a/homeassistant/config_entries.py b/homeassistant/config_entries.py index 8a73e424fb5..7826e26b960 100644 --- a/homeassistant/config_entries.py +++ b/homeassistant/config_entries.py @@ -129,6 +129,7 @@ HANDLERS = Registry() FLOWS = [ 'deconz', 'hue', + 'nest', 'zone', ] diff --git a/homeassistant/data_entry_flow.py b/homeassistant/data_entry_flow.py index 5095297e795..3b0f264fd40 100644 --- a/homeassistant/data_entry_flow.py +++ b/homeassistant/data_entry_flow.py @@ -132,7 +132,8 @@ class FlowHandler: VERSION = 1 @callback - def async_show_form(self, *, step_id, data_schema=None, errors=None): + def async_show_form(self, *, step_id, data_schema=None, errors=None, + description_placeholders=None): """Return the definition of a form to gather user input.""" return { 'type': RESULT_TYPE_FORM, @@ -141,6 +142,7 @@ class FlowHandler: 'step_id': step_id, 'data_schema': data_schema, 'errors': errors, + 'description_placeholders': description_placeholders, } @callback diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 19796c3bab7..af4f8feb753 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -155,6 +155,9 @@ pyqwikswitch==0.8 # homeassistant.components.weather.darksky python-forecastio==1.4.0 +# homeassistant.components.nest +python-nest==4.0.2 + # homeassistant.components.sensor.whois pythonwhois==2.4.3 diff --git a/script/gen_requirements_all.py b/script/gen_requirements_all.py index e770d902669..7bf87c74de7 100755 --- a/script/gen_requirements_all.py +++ b/script/gen_requirements_all.py @@ -77,6 +77,7 @@ TEST_REQUIREMENTS = ( 'pynx584', 'pyqwikswitch', 'python-forecastio', + 'python-nest', 'pytradfri\[async\]', 'pyunifi', 'pyupnp-async', diff --git a/tests/components/config/test_config_entries.py b/tests/components/config/test_config_entries.py index 84d15578e13..82c747da01c 100644 --- a/tests/components/config/test_config_entries.py +++ b/tests/components/config/test_config_entries.py @@ -110,6 +110,9 @@ def test_initialize_flow(hass, client): return self.async_show_form( step_id='init', data_schema=schema, + description_placeholders={ + 'url': 'https://example.com', + }, errors={ 'username': 'Should be unique.' } @@ -140,6 +143,9 @@ def test_initialize_flow(hass, client): 'type': 'string' } ], + 'description_placeholders': { + 'url': 'https://example.com', + }, 'errors': { 'username': 'Should be unique.' } @@ -242,6 +248,7 @@ def test_two_step_flow(hass, client): 'type': 'string' } ], + 'description_placeholders': None, 'errors': None } diff --git a/tests/components/nest/__init__.py b/tests/components/nest/__init__.py new file mode 100644 index 00000000000..313cfccc761 --- /dev/null +++ b/tests/components/nest/__init__.py @@ -0,0 +1 @@ +"""Tests for the Nest component.""" diff --git a/tests/components/nest/test_config_flow.py b/tests/components/nest/test_config_flow.py new file mode 100644 index 00000000000..9692d5ce129 --- /dev/null +++ b/tests/components/nest/test_config_flow.py @@ -0,0 +1,174 @@ +"""Tests for the Nest config flow.""" +import asyncio +from unittest.mock import Mock, patch + +from homeassistant import data_entry_flow +from homeassistant.components.nest import config_flow + +from tests.common import mock_coro + + +async def test_abort_if_no_implementation_registered(hass): + """Test we abort if no implementation is registered.""" + flow = config_flow.NestFlowHandler() + flow.hass = hass + result = await flow.async_step_init() + + assert result['type'] == data_entry_flow.RESULT_TYPE_ABORT + assert result['reason'] == 'no_flows' + + +async def test_abort_if_already_setup(hass): + """Test we abort if Nest is already setup.""" + flow = config_flow.NestFlowHandler() + flow.hass = hass + + with patch.object(hass.config_entries, 'async_entries', return_value=[{}]): + result = await flow.async_step_init() + + assert result['type'] == data_entry_flow.RESULT_TYPE_ABORT + assert result['reason'] == 'already_setup' + + +async def test_full_flow_implementation(hass): + """Test registering an implementation and finishing flow works.""" + gen_authorize_url = Mock(return_value=mock_coro('https://example.com')) + convert_code = Mock(return_value=mock_coro({'access_token': 'yoo'})) + config_flow.register_flow_implementation( + hass, 'test', 'Test', gen_authorize_url, convert_code) + config_flow.register_flow_implementation( + hass, 'test-other', 'Test Other', None, None) + + flow = config_flow.NestFlowHandler() + flow.hass = hass + result = await flow.async_step_init() + assert result['type'] == data_entry_flow.RESULT_TYPE_FORM + assert result['step_id'] == 'init' + + result = await flow.async_step_init({'flow_impl': 'test'}) + assert result['type'] == data_entry_flow.RESULT_TYPE_FORM + assert result['step_id'] == 'link' + assert result['description_placeholders'] == { + 'url': 'https://example.com', + } + + result = await flow.async_step_link({'code': '123ABC'}) + assert result['type'] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result['data']['tokens'] == {'access_token': 'yoo'} + assert result['data']['impl_domain'] == 'test' + assert result['title'] == 'Nest (via Test)' + + +async def test_not_pick_implementation_if_only_one(hass): + """Test we allow picking implementation if we have two.""" + gen_authorize_url = Mock(return_value=mock_coro('https://example.com')) + config_flow.register_flow_implementation( + hass, 'test', 'Test', gen_authorize_url, None) + + flow = config_flow.NestFlowHandler() + flow.hass = hass + result = await flow.async_step_init() + assert result['type'] == data_entry_flow.RESULT_TYPE_FORM + assert result['step_id'] == 'link' + + +async def test_abort_if_timeout_generating_auth_url(hass): + """Test we abort if generating authorize url fails.""" + gen_authorize_url = Mock(side_effect=asyncio.TimeoutError) + config_flow.register_flow_implementation( + hass, 'test', 'Test', gen_authorize_url, None) + + flow = config_flow.NestFlowHandler() + flow.hass = hass + result = await flow.async_step_init() + assert result['type'] == data_entry_flow.RESULT_TYPE_ABORT + assert result['reason'] == 'authorize_url_timeout' + + +async def test_abort_if_exception_generating_auth_url(hass): + """Test we abort if generating authorize url blows up.""" + gen_authorize_url = Mock(side_effect=ValueError) + config_flow.register_flow_implementation( + hass, 'test', 'Test', gen_authorize_url, None) + + flow = config_flow.NestFlowHandler() + flow.hass = hass + result = await flow.async_step_init() + assert result['type'] == data_entry_flow.RESULT_TYPE_ABORT + assert result['reason'] == 'authorize_url_fail' + + +async def test_verify_code_timeout(hass): + """Test verify code timing out.""" + gen_authorize_url = Mock(return_value=mock_coro('https://example.com')) + convert_code = Mock(side_effect=asyncio.TimeoutError) + config_flow.register_flow_implementation( + hass, 'test', 'Test', gen_authorize_url, convert_code) + + flow = config_flow.NestFlowHandler() + flow.hass = hass + result = await flow.async_step_init() + assert result['type'] == data_entry_flow.RESULT_TYPE_FORM + assert result['step_id'] == 'link' + + result = await flow.async_step_link({'code': '123ABC'}) + assert result['type'] == data_entry_flow.RESULT_TYPE_FORM + assert result['step_id'] == 'link' + assert result['errors'] == {'code': 'timeout'} + + +async def test_verify_code_invalid(hass): + """Test verify code invalid.""" + gen_authorize_url = Mock(return_value=mock_coro('https://example.com')) + convert_code = Mock(side_effect=config_flow.CodeInvalid) + config_flow.register_flow_implementation( + hass, 'test', 'Test', gen_authorize_url, convert_code) + + flow = config_flow.NestFlowHandler() + flow.hass = hass + result = await flow.async_step_init() + assert result['type'] == data_entry_flow.RESULT_TYPE_FORM + assert result['step_id'] == 'link' + + result = await flow.async_step_link({'code': '123ABC'}) + assert result['type'] == data_entry_flow.RESULT_TYPE_FORM + assert result['step_id'] == 'link' + assert result['errors'] == {'code': 'invalid_code'} + + +async def test_verify_code_unknown_error(hass): + """Test verify code unknown error.""" + gen_authorize_url = Mock(return_value=mock_coro('https://example.com')) + convert_code = Mock(side_effect=config_flow.NestAuthError) + config_flow.register_flow_implementation( + hass, 'test', 'Test', gen_authorize_url, convert_code) + + flow = config_flow.NestFlowHandler() + flow.hass = hass + result = await flow.async_step_init() + assert result['type'] == data_entry_flow.RESULT_TYPE_FORM + assert result['step_id'] == 'link' + + result = await flow.async_step_link({'code': '123ABC'}) + assert result['type'] == data_entry_flow.RESULT_TYPE_FORM + assert result['step_id'] == 'link' + assert result['errors'] == {'code': 'unknown'} + + +async def test_verify_code_exception(hass): + """Test verify code blows up.""" + gen_authorize_url = Mock(return_value=mock_coro('https://example.com')) + convert_code = Mock(side_effect=ValueError) + config_flow.register_flow_implementation( + hass, 'test', 'Test', gen_authorize_url, convert_code) + + flow = config_flow.NestFlowHandler() + flow.hass = hass + result = await flow.async_step_init() + assert result['type'] == data_entry_flow.RESULT_TYPE_FORM + assert result['step_id'] == 'link' + + result = await flow.async_step_link({'code': '123ABC'}) + assert result['type'] == data_entry_flow.RESULT_TYPE_FORM + assert result['step_id'] == 'link' + assert result['errors'] == {'code': 'internal_error'} diff --git a/tests/components/nest/test_local_auth.py b/tests/components/nest/test_local_auth.py new file mode 100644 index 00000000000..44a5299b33d --- /dev/null +++ b/tests/components/nest/test_local_auth.py @@ -0,0 +1,51 @@ +"""Test Nest local auth.""" +from homeassistant.components.nest import const, config_flow, local_auth +from urllib.parse import parse_qsl + +import pytest + +import requests_mock as rmock + + +@pytest.fixture +def registered_flow(hass): + """Mock a registered flow.""" + local_auth.initialize(hass, 'TEST-CLIENT-ID', 'TEST-CLIENT-SECRET') + return hass.data[config_flow.DATA_FLOW_IMPL][const.DOMAIN] + + +async def test_generate_auth_url(registered_flow): + """Test generating an auth url. + + Mainly testing that it doesn't blow up. + """ + url = await registered_flow['gen_authorize_url']('TEST-FLOW-ID') + assert url is not None + + +async def test_convert_code(requests_mock, registered_flow): + """Test converting a code.""" + from nest.nest import ACCESS_TOKEN_URL + + def token_matcher(request): + """Match a fetch token request.""" + if request.url != ACCESS_TOKEN_URL: + return None + + assert dict(parse_qsl(request.text)) == { + 'client_id': 'TEST-CLIENT-ID', + 'client_secret': 'TEST-CLIENT-SECRET', + 'code': 'TEST-CODE', + 'grant_type': 'authorization_code' + } + + return rmock.create_response(request, json={ + 'access_token': 'TEST-ACCESS-TOKEN' + }) + + requests_mock.add_matcher(token_matcher) + + tokens = await registered_flow['convert_code']('TEST-CODE') + assert tokens == { + 'access_token': 'TEST-ACCESS-TOKEN' + } From cccd0deb659f83b429b1231cc6d68e6b66a19496 Mon Sep 17 00:00:00 2001 From: Robin Date: Wed, 13 Jun 2018 20:02:46 +0100 Subject: [PATCH 082/144] Fix Facebox face data parsing (#14951) * Adds parse_faces * Update facebox.py --- .../components/image_processing/facebox.py | 40 +++++++++++++---- .../image_processing/test_facebox.py | 44 ++++++++++++++----- 2 files changed, 65 insertions(+), 19 deletions(-) diff --git a/homeassistant/components/image_processing/facebox.py b/homeassistant/components/image_processing/facebox.py index 81b43c1f8e0..f556b62e935 100644 --- a/homeassistant/components/image_processing/facebox.py +++ b/homeassistant/components/image_processing/facebox.py @@ -10,16 +10,22 @@ import logging import requests import voluptuous as vol +from homeassistant.const import ATTR_NAME from homeassistant.core import split_entity_id import homeassistant.helpers.config_validation as cv from homeassistant.components.image_processing import ( - PLATFORM_SCHEMA, ImageProcessingFaceEntity, CONF_SOURCE, CONF_ENTITY_ID, - CONF_NAME) + PLATFORM_SCHEMA, ImageProcessingFaceEntity, ATTR_CONFIDENCE, CONF_SOURCE, + CONF_ENTITY_ID, CONF_NAME) from homeassistant.const import (CONF_IP_ADDRESS, CONF_PORT) _LOGGER = logging.getLogger(__name__) +ATTR_BOUNDING_BOX = 'bounding_box' +ATTR_IMAGE_ID = 'image_id' +ATTR_MATCHED = 'matched' CLASSIFIER = 'facebox' +TIMEOUT = 9 + PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Required(CONF_IP_ADDRESS): cv.string, @@ -30,7 +36,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ def encode_image(image): """base64 encode an image stream.""" base64_img = base64.b64encode(image).decode('ascii') - return {"base64": base64_img} + return base64_img def get_matched_faces(faces): @@ -39,6 +45,24 @@ def get_matched_faces(faces): for face in faces if face['matched']} +def parse_faces(api_faces): + """Parse the API face data into the format required.""" + known_faces = [] + for entry in api_faces: + face = {} + if entry['matched']: # This data is only in matched faces. + face[ATTR_NAME] = entry['name'] + face[ATTR_IMAGE_ID] = entry['id'] + else: # Lets be explicit. + face[ATTR_NAME] = None + face[ATTR_IMAGE_ID] = None + face[ATTR_CONFIDENCE] = round(100.0*entry['confidence'], 2) + face[ATTR_MATCHED] = entry['matched'] + face[ATTR_BOUNDING_BOX] = entry['rect'] + known_faces.append(face) + return known_faces + + def setup_platform(hass, config, add_devices, discovery_info=None): """Set up the classifier.""" entities = [] @@ -74,18 +98,18 @@ class FaceClassifyEntity(ImageProcessingFaceEntity): try: response = requests.post( self._url, - json=encode_image(image), - timeout=9 + json={"base64": encode_image(image)}, + timeout=TIMEOUT ).json() except requests.exceptions.ConnectionError: _LOGGER.error("ConnectionError: Is %s running?", CLASSIFIER) response['success'] = False if response['success']: - faces = response['faces'] - total = response['facesCount'] - self.process_faces(faces, total) + total_faces = response['facesCount'] + faces = parse_faces(response['faces']) self._matched = get_matched_faces(faces) + self.process_faces(faces, total_faces) else: self.total_faces = None diff --git a/tests/components/image_processing/test_facebox.py b/tests/components/image_processing/test_facebox.py index cdc19a3d8d1..9449ebf5f71 100644 --- a/tests/components/image_processing/test_facebox.py +++ b/tests/components/image_processing/test_facebox.py @@ -7,7 +7,7 @@ import requests_mock from homeassistant.core import callback from homeassistant.const import ( - ATTR_ENTITY_ID, CONF_FRIENDLY_NAME, + ATTR_ENTITY_ID, ATTR_NAME, CONF_FRIENDLY_NAME, CONF_IP_ADDRESS, CONF_PORT, STATE_UNKNOWN) from homeassistant.setup import async_setup_component import homeassistant.components.image_processing as ip @@ -16,6 +16,7 @@ import homeassistant.components.image_processing.facebox as fb MOCK_IP = '192.168.0.1' MOCK_PORT = '8080' +# Mock data returned by the facebox API. MOCK_FACE = {'confidence': 0.5812028911604818, 'id': 'john.jpg', 'matched': True, @@ -28,6 +29,20 @@ MOCK_JSON = {"facesCount": 1, "faces": [MOCK_FACE] } +# Faces data after parsing. +PARSED_FACES = [{ATTR_NAME: 'John Lennon', + fb.ATTR_IMAGE_ID: 'john.jpg', + fb.ATTR_CONFIDENCE: 58.12, + fb.ATTR_MATCHED: True, + fb.ATTR_BOUNDING_BOX: { + 'height': 75, + 'left': 63, + 'top': 262, + 'width': 74}, + }] + +MATCHED_FACES = {'John Lennon': 58.12} + VALID_ENTITY_ID = 'image_processing.facebox_demo_camera' VALID_CONFIG = { ip.DOMAIN: { @@ -45,12 +60,14 @@ VALID_CONFIG = { def test_encode_image(): """Test that binary data is encoded correctly.""" - assert fb.encode_image(b'test')["base64"] == 'dGVzdA==' + assert fb.encode_image(b'test') == 'dGVzdA==' -def test_get_matched_faces(): - """Test that matched faces are parsed correctly.""" - assert fb.get_matched_faces([MOCK_FACE]) == {MOCK_FACE['name']: 0.58} +def test_parse_faces(): + """Test parsing of raw face data, and generation of matched_faces.""" + parsed_faces = fb.parse_faces(MOCK_JSON['faces']) + assert parsed_faces == PARSED_FACES + assert fb.get_matched_faces(parsed_faces) == MATCHED_FACES @pytest.fixture @@ -92,16 +109,21 @@ async def test_process_image(hass, mock_image): state = hass.states.get(VALID_ENTITY_ID) assert state.state == '1' - assert state.attributes.get('matched_faces') == {MOCK_FACE['name']: 0.58} + assert state.attributes.get('matched_faces') == MATCHED_FACES - MOCK_FACE[ATTR_ENTITY_ID] = VALID_ENTITY_ID # Update. - assert state.attributes.get('faces') == [MOCK_FACE] + PARSED_FACES[0][ATTR_ENTITY_ID] = VALID_ENTITY_ID # Update. + assert state.attributes.get('faces') == PARSED_FACES assert state.attributes.get(CONF_FRIENDLY_NAME) == 'facebox demo_camera' assert len(face_events) == 1 - assert face_events[0].data['name'] == MOCK_FACE['name'] - assert face_events[0].data['confidence'] == MOCK_FACE['confidence'] - assert face_events[0].data['entity_id'] == VALID_ENTITY_ID + assert face_events[0].data[ATTR_NAME] == PARSED_FACES[0][ATTR_NAME] + assert (face_events[0].data[fb.ATTR_CONFIDENCE] + == PARSED_FACES[0][fb.ATTR_CONFIDENCE]) + assert face_events[0].data[ATTR_ENTITY_ID] == VALID_ENTITY_ID + assert (face_events[0].data[fb.ATTR_IMAGE_ID] == + PARSED_FACES[0][fb.ATTR_IMAGE_ID]) + assert (face_events[0].data[fb.ATTR_BOUNDING_BOX] == + PARSED_FACES[0][fb.ATTR_BOUNDING_BOX]) async def test_connection_error(hass, mock_image): From cdd111df497df5d1763e1358178f13490c104f89 Mon Sep 17 00:00:00 2001 From: Nick Whyte Date: Thu, 14 Jun 2018 21:56:04 +1000 Subject: [PATCH 083/144] Add sensor.nsw_fuel_station component (#14757) * Add sensor.nsw_fuel_station component * bump dependency * PR Changes * flake8 * Use MockPrice * Fix requirements * Fix tests * line length * wip * Handle errors and show persistent notification * update tests * Address @MartinHjelmare's comments * Fetch station name from API * Update tests * Update requirements * Address comments --- .coveragerc | 1 + CODEOWNERS | 1 + .../components/sensor/nsw_fuel_station.py | 174 ++++++++++++++++++ requirements_all.txt | 3 + .../sensor/test_nsw_fuel_station.py | 117 ++++++++++++ 5 files changed, 296 insertions(+) create mode 100644 homeassistant/components/sensor/nsw_fuel_station.py create mode 100644 tests/components/sensor/test_nsw_fuel_station.py diff --git a/.coveragerc b/.coveragerc index fa2ec6e9f27..38c88c4748c 100644 --- a/.coveragerc +++ b/.coveragerc @@ -655,6 +655,7 @@ omit = homeassistant/components/sensor/nederlandse_spoorwegen.py homeassistant/components/sensor/netdata.py homeassistant/components/sensor/neurio_energy.py + homeassistant/components/sensor/nsw_fuel_station.py homeassistant/components/sensor/nut.py homeassistant/components/sensor/nzbget.py homeassistant/components/sensor/ohmconnect.py diff --git a/CODEOWNERS b/CODEOWNERS index 0da8353e5aa..556791b879c 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -70,6 +70,7 @@ homeassistant/components/sensor/filter.py @dgomes homeassistant/components/sensor/gearbest.py @HerrHofrat homeassistant/components/sensor/irish_rail_transport.py @ttroy50 homeassistant/components/sensor/miflora.py @danielhiversen @ChristianKuehnel +homeassistant/components/sensor/nsw_fuel_station.py @nickw444 homeassistant/components/sensor/pollen.py @bachya homeassistant/components/sensor/qnap.py @colinodell homeassistant/components/sensor/sma.py @kellerza diff --git a/homeassistant/components/sensor/nsw_fuel_station.py b/homeassistant/components/sensor/nsw_fuel_station.py new file mode 100644 index 00000000000..2440dac3204 --- /dev/null +++ b/homeassistant/components/sensor/nsw_fuel_station.py @@ -0,0 +1,174 @@ +""" +Sensor platform to display the current fuel prices at a NSW fuel station. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/sensor.nsw_fuel_station/ +""" +import datetime +import logging +from typing import Optional + +import voluptuous as vol + +import homeassistant.helpers.config_validation as cv +from homeassistant.components.light import PLATFORM_SCHEMA +from homeassistant.const import ATTR_ATTRIBUTION +from homeassistant.helpers.entity import Entity +from homeassistant.util import Throttle + +REQUIREMENTS = ['nsw-fuel-api-client==1.0.10'] + +_LOGGER = logging.getLogger(__name__) + +ATTR_STATION_ID = 'station_id' +ATTR_STATION_NAME = 'station_name' + +CONF_STATION_ID = 'station_id' +CONF_FUEL_TYPES = 'fuel_types' +CONF_ALLOWED_FUEL_TYPES = ["E10", "U91", "E85", "P95", "P98", "DL", + "PDL", "B20", "LPG", "CNG", "EV"] +CONF_DEFAULT_FUEL_TYPES = ["E10", "U91"] +CONF_ATTRIBUTION = "Data provided by NSW Government FuelCheck" + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Required(CONF_STATION_ID): cv.positive_int, + vol.Optional(CONF_FUEL_TYPES, default=CONF_DEFAULT_FUEL_TYPES): + vol.All(cv.ensure_list, [vol.In(CONF_ALLOWED_FUEL_TYPES)]), +}) + +MIN_TIME_BETWEEN_UPDATES = datetime.timedelta(hours=1) + +NOTIFICATION_ID = 'nsw_fuel_station_notification' +NOTIFICATION_TITLE = 'NSW Fuel Station Sensor Setup' + + +def setup_platform(hass, config, add_devices, discovery_info=None): + """Set up the NSW Fuel Station sensor.""" + from nsw_fuel import FuelCheckClient + + station_id = config[CONF_STATION_ID] + fuel_types = config[CONF_FUEL_TYPES] + + client = FuelCheckClient() + station_data = StationPriceData(client, station_id) + station_data.update() + + if station_data.error is not None: + message = ( + 'Error: {}. Check the logs for additional information.' + ).format(station_data.error) + + hass.components.persistent_notification.create( + message, + title=NOTIFICATION_TITLE, + notification_id=NOTIFICATION_ID) + return + + available_fuel_types = station_data.get_available_fuel_types() + + add_devices([ + StationPriceSensor(station_data, fuel_type) + for fuel_type in fuel_types + if fuel_type in available_fuel_types + ]) + + +class StationPriceData(object): + """An object to store and fetch the latest data for a given station.""" + + def __init__(self, client, station_id: int) -> None: + """Initialize the sensor.""" + self.station_id = station_id + self._client = client + self._data = None + self._reference_data = None + self.error = None + self._station_name = None + + @Throttle(MIN_TIME_BETWEEN_UPDATES) + def update(self): + """Update the internal data using the API client.""" + from nsw_fuel import FuelCheckError + + if self._reference_data is None: + try: + self._reference_data = self._client.get_reference_data() + except FuelCheckError as exc: + self.error = str(exc) + _LOGGER.error( + 'Failed to fetch NSW Fuel station reference data. %s', exc) + return + + try: + self._data = self._client.get_fuel_prices_for_station( + self.station_id) + except FuelCheckError as exc: + self.error = str(exc) + _LOGGER.error( + 'Failed to fetch NSW Fuel station price data. %s', exc) + + def for_fuel_type(self, fuel_type: str): + """Return the price of the given fuel type.""" + if self._data is None: + return None + return next((price for price + in self._data if price.fuel_type == fuel_type), None) + + def get_available_fuel_types(self): + """Return the available fuel types for the station.""" + return [price.fuel_type for price in self._data] + + def get_station_name(self) -> str: + """Return the name of the station.""" + if self._station_name is None: + name = None + if self._reference_data is not None: + name = next((station.name for station + in self._reference_data.stations + if station.code == self.station_id), None) + + self._station_name = name or 'station {}'.format(self.station_id) + + return self._station_name + + +class StationPriceSensor(Entity): + """Implementation of a sensor that reports the fuel price for a station.""" + + def __init__(self, station_data: StationPriceData, fuel_type: str): + """Initialize the sensor.""" + self._station_data = station_data + self._fuel_type = fuel_type + + @property + def name(self) -> str: + """Return the name of the sensor.""" + return '{} {}'.format( + self._station_data.get_station_name(), self._fuel_type) + + @property + def state(self) -> Optional[float]: + """Return the state of the sensor.""" + price_info = self._station_data.for_fuel_type(self._fuel_type) + if price_info: + return price_info.price + + return None + + @property + def device_state_attributes(self) -> dict: + """Return the state attributes of the device.""" + return { + ATTR_STATION_ID: self._station_data.station_id, + ATTR_STATION_NAME: self._station_data.get_station_name(), + ATTR_ATTRIBUTION: CONF_ATTRIBUTION + } + + @property + def unit_of_measurement(self) -> str: + """Return the units of measurement.""" + return '¢/L' + + def update(self): + """Update current conditions.""" + self._station_data.update() diff --git a/requirements_all.txt b/requirements_all.txt index c180c3a055d..dabecdacb2f 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -597,6 +597,9 @@ neurio==0.3.1 # homeassistant.components.sensor.nederlandse_spoorwegen nsapi==2.7.4 +# homeassistant.components.sensor.nsw_fuel_station +nsw-fuel-api-client==1.0.10 + # homeassistant.components.nuheat nuheat==0.3.0 diff --git a/tests/components/sensor/test_nsw_fuel_station.py b/tests/components/sensor/test_nsw_fuel_station.py new file mode 100644 index 00000000000..1ee314d9eee --- /dev/null +++ b/tests/components/sensor/test_nsw_fuel_station.py @@ -0,0 +1,117 @@ +"""The tests for the NSW Fuel Station sensor platform.""" +import unittest +from unittest.mock import patch + +from homeassistant.components import sensor +from homeassistant.setup import setup_component +from tests.common import ( + get_test_home_assistant, assert_setup_component, MockDependency) + +VALID_CONFIG = { + 'platform': 'nsw_fuel_station', + 'station_id': 350, + 'fuel_types': ['E10', 'P95'], +} + + +class MockPrice(): + """Mock Price implementation.""" + + def __init__(self, price, fuel_type, last_updated, + price_unit, station_code): + """Initialize a mock price instance.""" + self.price = price + self.fuel_type = fuel_type + self.last_updated = last_updated + self.price_unit = price_unit + self.station_code = station_code + + +class MockStation(): + """Mock Station implementation.""" + + def __init__(self, name, code): + """Initialize a mock Station instance.""" + self.name = name + self.code = code + + +class MockGetReferenceDataResponse(): + """Mock GetReferenceDataResponse implementation.""" + + def __init__(self, stations): + """Initialize a mock GetReferenceDataResponse instance.""" + self.stations = stations + + +class FuelCheckClientMock(): + """Mock FuelCheckClient implementation.""" + + def get_fuel_prices_for_station(self, station): + """Return a fake fuel prices response.""" + return [ + MockPrice( + price=150.0, + fuel_type='P95', + last_updated=None, + price_unit=None, + station_code=350 + ), + MockPrice( + price=140.0, + fuel_type='E10', + last_updated=None, + price_unit=None, + station_code=350 + ) + ] + + def get_reference_data(self): + """Return a fake reference data response.""" + return MockGetReferenceDataResponse( + stations=[ + MockStation(code=350, name="My Fake Station") + ] + ) + + +class TestNSWFuelStation(unittest.TestCase): + """Test the NSW Fuel Station sensor platform.""" + + def setUp(self): + """Set up things to be run when tests are started.""" + self.hass = get_test_home_assistant() + self.config = VALID_CONFIG + + def tearDown(self): + """Stop everything that was started.""" + self.hass.stop() + + @MockDependency('nsw_fuel') + @patch('nsw_fuel.FuelCheckClient', new=FuelCheckClientMock) + def test_setup(self, mock_nsw_fuel): + """Test the setup with custom settings.""" + with assert_setup_component(1, sensor.DOMAIN): + self.assertTrue(setup_component(self.hass, sensor.DOMAIN, { + 'sensor': VALID_CONFIG})) + + fake_entities = [ + 'my_fake_station_p95', + 'my_fake_station_e10' + ] + + for entity_id in fake_entities: + state = self.hass.states.get('sensor.{}'.format(entity_id)) + self.assertIsNotNone(state) + + @MockDependency('nsw_fuel') + @patch('nsw_fuel.FuelCheckClient', new=FuelCheckClientMock) + def test_sensor_values(self, mock_nsw_fuel): + """Test retrieval of sensor values.""" + self.assertTrue(setup_component( + self.hass, sensor.DOMAIN, {'sensor': VALID_CONFIG})) + + self.assertEqual('140.0', self.hass.states.get( + 'sensor.my_fake_station_e10').state) + self.assertEqual('150.0', self.hass.states.get( + 'sensor.my_fake_station_p95').state) From 0e7d284c839e9144be6b2d47de1143bc7430c8f7 Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Thu, 14 Jun 2018 07:30:47 -0600 Subject: [PATCH 084/144] Make AirVisual platform async + other adjustments (#14943) * Changes complete * Updated requirements * Add support for scan_interval * Small style update * Owner-requested changes --- homeassistant/components/sensor/airvisual.py | 334 +++++++++---------- requirements_all.txt | 2 +- 2 files changed, 165 insertions(+), 171 deletions(-) diff --git a/homeassistant/components/sensor/airvisual.py b/homeassistant/components/sensor/airvisual.py index b4007c8d744..0002274833f 100644 --- a/homeassistant/components/sensor/airvisual.py +++ b/homeassistant/components/sensor/airvisual.py @@ -9,16 +9,16 @@ from datetime import timedelta import voluptuous as vol -import homeassistant.helpers.config_validation as cv from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.const import ( ATTR_ATTRIBUTION, ATTR_LATITUDE, ATTR_LONGITUDE, CONF_API_KEY, - CONF_LATITUDE, CONF_LONGITUDE, CONF_MONITORED_CONDITIONS, CONF_STATE, - CONF_SHOW_ON_MAP, CONF_RADIUS) + CONF_LATITUDE, CONF_LONGITUDE, CONF_MONITORED_CONDITIONS, + CONF_SCAN_INTERVAL, CONF_STATE, CONF_SHOW_ON_MAP) +from homeassistant.helpers import aiohttp_client, config_validation as cv from homeassistant.helpers.entity import Entity from homeassistant.util import Throttle -REQUIREMENTS = ['pyairvisual==1.0.0'] +REQUIREMENTS = ['pyairvisual==2.0.1'] _LOGGER = getLogger(__name__) ATTR_CITY = 'city' @@ -29,135 +29,173 @@ ATTR_REGION = 'region' CONF_CITY = 'city' CONF_COUNTRY = 'country' -CONF_ATTRIBUTION = "Data provided by AirVisual" + +DEFAULT_ATTRIBUTION = "Data provided by AirVisual" +DEFAULT_SCAN_INTERVAL = timedelta(minutes=10) MASS_PARTS_PER_MILLION = 'ppm' MASS_PARTS_PER_BILLION = 'ppb' VOLUME_MICROGRAMS_PER_CUBIC_METER = 'µg/m3' -MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=10) - -SENSOR_TYPES = [ - ('AirPollutionLevelSensor', 'Air Pollution Level', 'mdi:scale'), - ('AirQualityIndexSensor', 'Air Quality Index', 'mdi:format-list-numbers'), - ('MainPollutantSensor', 'Main Pollutant', 'mdi:chemical-weapon'), +SENSOR_TYPE_LEVEL = 'air_pollution_level' +SENSOR_TYPE_AQI = 'air_quality_index' +SENSOR_TYPE_POLLUTANT = 'main_pollutant' +SENSORS = [ + (SENSOR_TYPE_LEVEL, 'Air Pollution Level', 'mdi:scale', None), + (SENSOR_TYPE_AQI, 'Air Quality Index', 'mdi:format-list-numbers', 'AQI'), + (SENSOR_TYPE_POLLUTANT, 'Main Pollutant', 'mdi:chemical-weapon', None), ] -POLLUTANT_LEVEL_MAPPING = [ - {'label': 'Good', 'minimum': 0, 'maximum': 50}, - {'label': 'Moderate', 'minimum': 51, 'maximum': 100}, - {'label': 'Unhealthy for sensitive group', 'minimum': 101, 'maximum': 150}, - {'label': 'Unhealthy', 'minimum': 151, 'maximum': 200}, - {'label': 'Very Unhealthy', 'minimum': 201, 'maximum': 300}, - {'label': 'Hazardous', 'minimum': 301, 'maximum': 10000} -] +POLLUTANT_LEVEL_MAPPING = [{ + 'label': 'Good', + 'minimum': 0, + 'maximum': 50 +}, { + 'label': 'Moderate', + 'minimum': 51, + 'maximum': 100 +}, { + 'label': 'Unhealthy for sensitive group', + 'minimum': 101, + 'maximum': 150 +}, { + 'label': 'Unhealthy', + 'minimum': 151, + 'maximum': 200 +}, { + 'label': 'Very Unhealthy', + 'minimum': 201, + 'maximum': 300 +}, { + 'label': 'Hazardous', + 'minimum': 301, + 'maximum': 10000 +}] POLLUTANT_MAPPING = { - 'co': {'label': 'Carbon Monoxide', 'unit': MASS_PARTS_PER_MILLION}, - 'n2': {'label': 'Nitrogen Dioxide', 'unit': MASS_PARTS_PER_BILLION}, - 'o3': {'label': 'Ozone', 'unit': MASS_PARTS_PER_BILLION}, - 'p1': {'label': 'PM10', 'unit': VOLUME_MICROGRAMS_PER_CUBIC_METER}, - 'p2': {'label': 'PM2.5', 'unit': VOLUME_MICROGRAMS_PER_CUBIC_METER}, - 's2': {'label': 'Sulfur Dioxide', 'unit': MASS_PARTS_PER_BILLION}, + 'co': { + 'label': 'Carbon Monoxide', + 'unit': MASS_PARTS_PER_MILLION + }, + 'n2': { + 'label': 'Nitrogen Dioxide', + 'unit': MASS_PARTS_PER_BILLION + }, + 'o3': { + 'label': 'Ozone', + 'unit': MASS_PARTS_PER_BILLION + }, + 'p1': { + 'label': 'PM10', + 'unit': VOLUME_MICROGRAMS_PER_CUBIC_METER + }, + 'p2': { + 'label': 'PM2.5', + 'unit': VOLUME_MICROGRAMS_PER_CUBIC_METER + }, + 's2': { + 'label': 'Sulfur Dioxide', + 'unit': MASS_PARTS_PER_BILLION + }, } SENSOR_LOCALES = {'cn': 'Chinese', 'us': 'U.S.'} PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Required(CONF_API_KEY): cv.string, - vol.Required(CONF_MONITORED_CONDITIONS): + vol.Required(CONF_MONITORED_CONDITIONS, default=list(SENSOR_LOCALES)): vol.All(cv.ensure_list, [vol.In(SENSOR_LOCALES)]), - vol.Optional(CONF_CITY): cv.string, - vol.Optional(CONF_COUNTRY): cv.string, - vol.Optional(CONF_LATITUDE): cv.latitude, - vol.Optional(CONF_LONGITUDE): cv.longitude, - vol.Optional(CONF_RADIUS, default=1000): cv.positive_int, + vol.Inclusive(CONF_CITY, 'city'): cv.string, + vol.Inclusive(CONF_COUNTRY, 'city'): cv.string, + vol.Inclusive(CONF_LATITUDE, 'coords'): cv.latitude, + vol.Inclusive(CONF_LONGITUDE, 'coords'): cv.longitude, vol.Optional(CONF_SHOW_ON_MAP, default=True): cv.boolean, - vol.Optional(CONF_STATE): cv.string, + vol.Inclusive(CONF_STATE, 'city'): cv.string, + vol.Optional(CONF_SCAN_INTERVAL, default=DEFAULT_SCAN_INTERVAL): + cv.time_period }) -def setup_platform(hass, config, add_devices, discovery_info=None): +async def async_setup_platform( + hass, config, async_add_devices, discovery_info=None): """Configure the platform and add the sensors.""" from pyairvisual import Client - classes = { - 'AirPollutionLevelSensor': AirPollutionLevelSensor, - 'AirQualityIndexSensor': AirQualityIndexSensor, - 'MainPollutantSensor': MainPollutantSensor - } - - api_key = config.get(CONF_API_KEY) - monitored_locales = config.get(CONF_MONITORED_CONDITIONS) - latitude = config.get(CONF_LATITUDE, hass.config.latitude) - longitude = config.get(CONF_LONGITUDE, hass.config.longitude) - radius = config.get(CONF_RADIUS) city = config.get(CONF_CITY) state = config.get(CONF_STATE) country = config.get(CONF_COUNTRY) - show_on_map = config.get(CONF_SHOW_ON_MAP) + + latitude = config.get(CONF_LATITUDE, hass.config.latitude) + longitude = config.get(CONF_LONGITUDE, hass.config.longitude) + + websession = aiohttp_client.async_get_clientsession(hass) if city and state and country: _LOGGER.debug( "Using city, state, and country: %s, %s, %s", city, state, country) location_id = ','.join((city, state, country)) data = AirVisualData( - Client(api_key), city=city, state=state, country=country, - show_on_map=show_on_map) + Client(config[CONF_API_KEY], websession), + city=city, + state=state, + country=country, + show_on_map=config[CONF_SHOW_ON_MAP], + scan_interval=config[CONF_SCAN_INTERVAL]) else: _LOGGER.debug( "Using latitude and longitude: %s, %s", latitude, longitude) location_id = ','.join((str(latitude), str(longitude))) data = AirVisualData( - Client(api_key), latitude=latitude, longitude=longitude, - radius=radius, show_on_map=show_on_map) + Client(config[CONF_API_KEY], websession), + latitude=latitude, + longitude=longitude, + show_on_map=config[CONF_SHOW_ON_MAP], + scan_interval=config[CONF_SCAN_INTERVAL]) - data.update() + await data.async_update() sensors = [] - for locale in monitored_locales: - for sensor_class, name, icon in SENSOR_TYPES: - sensors.append(classes[sensor_class]( - data, - name, - icon, - locale, - location_id - )) + for locale in config[CONF_MONITORED_CONDITIONS]: + for kind, name, icon, unit in SENSORS: + sensors.append( + AirVisualSensor( + data, kind, name, icon, unit, locale, location_id)) - add_devices(sensors, True) + async_add_devices(sensors, True) -class AirVisualBaseSensor(Entity): - """Define a base class for all of our sensors.""" +class AirVisualSensor(Entity): + """Define an AirVisual sensor.""" - def __init__(self, data, name, icon, locale, entity_id): - """Initialize the sensor.""" - self.data = data - self._attrs = {} + def __init__(self, airvisual, kind, name, icon, unit, locale, location_id): + """Initialize.""" + self._attrs = {ATTR_ATTRIBUTION: DEFAULT_ATTRIBUTION} self._icon = icon self._locale = locale + self._location_id = location_id self._name = name self._state = None - self._entity_id = entity_id - self._unit = None + self._type = kind + self._unit = unit + self.airvisual = airvisual @property def device_state_attributes(self): """Return the device state attributes.""" - self._attrs.update({ - ATTR_ATTRIBUTION: CONF_ATTRIBUTION, - }) - - if self.data.show_on_map: - self._attrs[ATTR_LATITUDE] = self.data.latitude - self._attrs[ATTR_LONGITUDE] = self.data.longitude + if self.airvisual.show_on_map: + self._attrs[ATTR_LATITUDE] = self.airvisual.latitude + self._attrs[ATTR_LONGITUDE] = self.airvisual.longitude else: - self._attrs['lati'] = self.data.latitude - self._attrs['long'] = self.data.longitude + self._attrs['lati'] = self.airvisual.latitude + self._attrs['long'] = self.airvisual.longitude return self._attrs + @property + def available(self): + """Return True if entity is available.""" + return bool(self.airvisual.pollution_info) + @property def icon(self): """Return the icon.""" @@ -173,127 +211,83 @@ class AirVisualBaseSensor(Entity): """Return the state.""" return self._state - -class AirPollutionLevelSensor(AirVisualBaseSensor): - """Define a sensor to measure air pollution level.""" - @property def unique_id(self): """Return a unique, HASS-friendly identifier for this entity.""" - return '{0}_pollution_level'.format(self._entity_id) - - def update(self): - """Update the status of the sensor.""" - self.data.update() - - aqi = self.data.pollution_info.get('aqi{0}'.format(self._locale)) - try: - [level] = [ - i for i in POLLUTANT_LEVEL_MAPPING - if i['minimum'] <= aqi <= i['maximum'] - ] - self._state = level.get('label') - except TypeError: - self._state = None - except ValueError: - self._state = None - - -class AirQualityIndexSensor(AirVisualBaseSensor): - """Define a sensor to measure AQI.""" - - @property - def unique_id(self): - """Return a unique, HASS-friendly identifier for this entity.""" - return '{0}_aqi'.format(self._entity_id) + return '{0}_{1}_{2}'.format( + self._location_id, self._locale, self._type) @property def unit_of_measurement(self): """Return the unit the value is expressed in.""" - return 'AQI' + return self._unit - def update(self): - """Update the status of the sensor.""" - self.data.update() + async def async_update(self): + """Update the sensor.""" + await self.airvisual.async_update() + data = self.airvisual.pollution_info - self._state = self.data.pollution_info.get( - 'aqi{0}'.format(self._locale)) + if not data: + return - -class MainPollutantSensor(AirVisualBaseSensor): - """Define a sensor to the main pollutant of an area.""" - - def __init__(self, data, name, icon, locale, entity_id): - """Initialize the sensor.""" - super().__init__(data, name, icon, locale, entity_id) - self._symbol = None - self._unit = None - - @property - def unique_id(self): - """Return a unique, HASS-friendly identifier for this entity.""" - return '{0}_main_pollutant'.format(self._entity_id) - - def update(self): - """Update the status of the sensor.""" - self.data.update() - - symbol = self.data.pollution_info.get('main{0}'.format(self._locale)) - pollution_info = POLLUTANT_MAPPING.get(symbol, {}) - self._state = pollution_info.get('label') - self._unit = pollution_info.get('unit') - self._symbol = symbol - - self._attrs.update({ - ATTR_POLLUTANT_SYMBOL: self._symbol, - ATTR_POLLUTANT_UNIT: self._unit - }) + if self._type == SENSOR_TYPE_LEVEL: + aqi = data['aqi{0}'.format(self._locale)] + [level] = [ + i for i in POLLUTANT_LEVEL_MAPPING + if i['minimum'] <= aqi <= i['maximum'] + ] + self._state = level['label'] + elif self._type == SENSOR_TYPE_AQI: + self._state = data['aqi{0}'.format(self._locale)] + elif self._type == SENSOR_TYPE_POLLUTANT: + symbol = data['main{0}'.format(self._locale)] + self._state = POLLUTANT_MAPPING[symbol]['label'] + self._attrs.update({ + ATTR_POLLUTANT_SYMBOL: symbol, + ATTR_POLLUTANT_UNIT: POLLUTANT_MAPPING[symbol]['unit'] + }) class AirVisualData(object): """Define an object to hold sensor data.""" def __init__(self, client, **kwargs): - """Initialize the AirVisual data element.""" + """Initialize.""" self._client = client - self.attrs = {} - self.pollution_info = None - self.city = kwargs.get(CONF_CITY) - self.state = kwargs.get(CONF_STATE) self.country = kwargs.get(CONF_COUNTRY) - self.latitude = kwargs.get(CONF_LATITUDE) self.longitude = kwargs.get(CONF_LONGITUDE) - self._radius = kwargs.get(CONF_RADIUS) - + self.pollution_info = {} self.show_on_map = kwargs.get(CONF_SHOW_ON_MAP) + self.state = kwargs.get(CONF_STATE) - @Throttle(MIN_TIME_BETWEEN_UPDATES) - def update(self): - """Update with new AirVisual data.""" - from pyairvisual.exceptions import HTTPError + self.async_update = Throttle( + kwargs[CONF_SCAN_INTERVAL])(self._async_update) + + async def _async_update(self): + """Update AirVisual data.""" + from pyairvisual.errors import AirVisualError try: if self.city and self.state and self.country: - resp = self._client.city( - self.city, self.state, self.country).get('data') - self.longitude, self.latitude = resp.get('location').get( - 'coordinates') + resp = await self._client.data.city( + self.city, self.state, self.country) + self.longitude, self.latitude = resp['location']['coordinates'] else: - resp = self._client.nearest_city( - self.latitude, self.longitude, self._radius).get('data') + resp = await self._client.data.nearest_city( + self.latitude, self.longitude) + _LOGGER.debug("New data retrieved: %s", resp) - self.pollution_info = resp.get('current', {}).get('pollution', {}) + self.pollution_info = resp['current']['pollution'] + except AirVisualError as err: + if self.city and self.state and self.country: + location = (self.city, self.state, self.country) + else: + location = (self.latitude, self.longitude) - self.attrs = { - ATTR_CITY: resp.get('city'), - ATTR_REGION: resp.get('state'), - ATTR_COUNTRY: resp.get('country') - } - except HTTPError as exc_info: - _LOGGER.error("Unable to retrieve data on this location: %s", - self.__dict__) - _LOGGER.debug(exc_info) + _LOGGER.error( + "Can't retrieve data for location: %s (%s)", location, + err) self.pollution_info = {} diff --git a/requirements_all.txt b/requirements_all.txt index dabecdacb2f..8a075631792 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -740,7 +740,7 @@ py_ryobi_gdo==0.0.10 pyads==2.2.6 # homeassistant.components.sensor.airvisual -pyairvisual==1.0.0 +pyairvisual==2.0.1 # homeassistant.components.alarm_control_panel.alarmdotcom pyalarmdotcom==0.3.2 From c36c3f0d64d6aa9b799c13e632c6f229fff6f096 Mon Sep 17 00:00:00 2001 From: "ruohan.chen" Date: Thu, 14 Jun 2018 21:47:17 +0800 Subject: [PATCH 085/144] Add support for ZhongHong HVAC Controllers (#14552) * first blood for ZhongHong HVAC Controller * add requirements * requirements_all.txt updated * add zhong_hong.py to coveragerc * add comments * unique_id add platform name * zhong_hong_hvac version bump to 1.0.1 * improve some coding style to match the project standard * zhong_hong_hvac version bump to 1.0.4 * zhong_hong_hvac version require 1.0.7 * update requirements by script/gen_requirements_all.py * zhong_hong_hvac version bump to 1.0.8 * fix startup problem * remove unused import * zhong_hong_hvac version bump to 1.0.9 - operation_mode: cold -> cool * start hub listen event when all climate entities is ready * use dispatcher to setup hub * var name change SIGNAL_DEVICE_SETTED_UP -> SIGNAL_DEVICE_ADDED * async problem fix * bugfix: set_operation_mode forget to use upper case * stringify the exception instead of print full stack of traceback * avoid to call str(exception) explicity * remove unnecessary try...except clause * remove unused import --- .coveragerc | 1 + .../components/climate/zhong_hong.py | 217 ++++++++++++++++++ requirements_all.txt | 3 + 3 files changed, 221 insertions(+) create mode 100644 homeassistant/components/climate/zhong_hong.py diff --git a/.coveragerc b/.coveragerc index 38c88c4748c..5a8f26e34da 100644 --- a/.coveragerc +++ b/.coveragerc @@ -385,6 +385,7 @@ omit = homeassistant/components/climate/sensibo.py homeassistant/components/climate/touchline.py homeassistant/components/climate/venstar.py + homeassistant/components/climate/zhong_hong.py homeassistant/components/cover/garadget.py homeassistant/components/cover/gogogate2.py homeassistant/components/cover/homematic.py diff --git a/homeassistant/components/climate/zhong_hong.py b/homeassistant/components/climate/zhong_hong.py new file mode 100644 index 00000000000..7ff19871ee7 --- /dev/null +++ b/homeassistant/components/climate/zhong_hong.py @@ -0,0 +1,217 @@ +""" +Support for ZhongHong HVAC Controller. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/climate.zhong_hong/ +""" +import logging + +import voluptuous as vol + +from homeassistant.components.climate import ( + ATTR_OPERATION_MODE, PLATFORM_SCHEMA, STATE_COOL, STATE_DRY, + STATE_FAN_ONLY, STATE_HEAT, SUPPORT_FAN_MODE, SUPPORT_ON_OFF, + SUPPORT_OPERATION_MODE, SUPPORT_TARGET_TEMPERATURE, ClimateDevice) +from homeassistant.const import (ATTR_TEMPERATURE, CONF_HOST, CONF_PORT, + EVENT_HOMEASSISTANT_STOP, TEMP_CELSIUS) +import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.dispatcher import (async_dispatcher_connect, + async_dispatcher_send) + +_LOGGER = logging.getLogger(__name__) + +CONF_GATEWAY_ADDRRESS = 'gateway_address' + +REQUIREMENTS = ['zhong_hong_hvac==1.0.9'] +SIGNAL_DEVICE_ADDED = 'zhong_hong_device_added' +SIGNAL_ZHONG_HONG_HUB_START = 'zhong_hong_hub_start' + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Required(CONF_HOST): + cv.string, + vol.Optional(CONF_PORT, default=9999): + vol.Coerce(int), + vol.Optional(CONF_GATEWAY_ADDRRESS, default=1): + vol.Coerce(int), +}) + + +def setup_platform(hass, config, add_devices, discovery_info=None): + """Set up the ZhongHong HVAC platform.""" + from zhong_hong_hvac.hub import ZhongHongGateway + host = config.get(CONF_HOST) + port = config.get(CONF_PORT) + gw_addr = config.get(CONF_GATEWAY_ADDRRESS) + hub = ZhongHongGateway(host, port, gw_addr) + devices = [ + ZhongHongClimate(hub, addr_out, addr_in) + for (addr_out, addr_in) in hub.discovery_ac() + ] + + _LOGGER.debug("We got %s zhong_hong climate devices", len(devices)) + + hub_is_initialized = False + + async def startup(): + """Start hub socket after all climate entity is setted up.""" + nonlocal hub_is_initialized + if not all([device.is_initialized for device in devices]): + return + + if hub_is_initialized: + return + + _LOGGER.debug("zhong_hong hub start listen event") + await hass.async_add_job(hub.start_listen) + await hass.async_add_job(hub.query_all_status) + hub_is_initialized = True + + async_dispatcher_connect(hass, SIGNAL_DEVICE_ADDED, startup) + + # add devices after SIGNAL_DEVICE_SETTED_UP event is listend + add_devices(devices) + + def stop_listen(event): + """Stop ZhongHongHub socket.""" + hub.stop_listen() + + hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, stop_listen) + + +class ZhongHongClimate(ClimateDevice): + """Representation of a ZhongHong controller support HVAC.""" + + def __init__(self, hub, addr_out, addr_in): + """Set up the ZhongHong climate devices.""" + from zhong_hong_hvac.hvac import HVAC + self._device = HVAC(hub, addr_out, addr_in) + self._hub = hub + self._current_operation = None + self._current_temperature = None + self._target_temperature = None + self._current_fan_mode = None + self._is_on = None + self.is_initialized = False + + async def async_added_to_hass(self): + """Register callbacks.""" + self._device.register_update_callback(self._after_update) + self.is_initialized = True + async_dispatcher_send(self.hass, SIGNAL_DEVICE_ADDED) + + def _after_update(self, climate): + """Callback to update state.""" + _LOGGER.debug("async update ha state") + if self._device.current_operation: + self._current_operation = self._device.current_operation.lower() + if self._device.current_temperature: + self._current_temperature = self._device.current_temperature + if self._device.current_fan_mode: + self._current_fan_mode = self._device.current_fan_mode + if self._device.target_temperature: + self._target_temperature = self._device.target_temperature + self._is_on = self._device.is_on + self.schedule_update_ha_state() + + @property + def should_poll(self): + """Return the polling state.""" + return False + + @property + def name(self): + """Return the name of the thermostat, if any.""" + return self.unique_id + + @property + def unique_id(self): + """Return the unique ID of the HVAC.""" + return "zhong_hong_hvac_{}_{}".format(self._device.addr_out, + self._device.addr_in) + + @property + def supported_features(self): + """Return the list of supported features.""" + return (SUPPORT_TARGET_TEMPERATURE | SUPPORT_FAN_MODE + | SUPPORT_OPERATION_MODE | SUPPORT_ON_OFF) + + @property + def temperature_unit(self): + """Return the unit of measurement used by the platform.""" + return TEMP_CELSIUS + + @property + def current_operation(self): + """Return current operation ie. heat, cool, idle.""" + return self._current_operation + + @property + def operation_list(self): + """Return the list of available operation modes.""" + return [STATE_COOL, STATE_HEAT, STATE_DRY, STATE_FAN_ONLY] + + @property + def current_temperature(self): + """Return the current temperature.""" + return self._current_temperature + + @property + def target_temperature(self): + """Return the temperature we try to reach.""" + return self._target_temperature + + @property + def target_temperature_step(self): + """Return the supported step of target temperature.""" + return 1 + + @property + def is_on(self): + """Return true if on.""" + return self._device.is_on + + @property + def current_fan_mode(self): + """Return the fan setting.""" + return self._current_fan_mode + + @property + def fan_list(self): + """Return the list of available fan modes.""" + return self._device.fan_list + + @property + def min_temp(self): + """Return the minimum temperature.""" + return self._device.min_temp + + @property + def max_temp(self): + """Return the maximum temperature.""" + return self._device.max_temp + + def turn_on(self): + """Turn on ac.""" + return self._device.turn_on() + + def turn_off(self): + """Turn off ac.""" + return self._device.turn_off() + + def set_temperature(self, **kwargs): + """Set new target temperature.""" + temperature = kwargs.get(ATTR_TEMPERATURE) + if temperature is not None: + self._device.set_temperature(temperature) + + operation_mode = kwargs.get(ATTR_OPERATION_MODE) + if operation_mode is not None: + self.set_operation_mode(operation_mode) + + def set_operation_mode(self, operation_mode): + """Set new target operation mode.""" + self._device.set_operation_mode(operation_mode.upper()) + + def set_fan_mode(self, fan_mode): + """Set new target fan mode.""" + self._device.set_fan_mode(fan_mode) diff --git a/requirements_all.txt b/requirements_all.txt index 8a075631792..f144f3e2a2b 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1441,6 +1441,9 @@ zengge==0.2 # homeassistant.components.zeroconf zeroconf==0.20.0 +# homeassistant.components.climate.zhong_hong +zhong_hong_hvac==1.0.9 + # homeassistant.components.media_player.ziggo_mediabox_xl ziggo-mediabox-xl==1.0.0 From b2440a6d9514530ff974eef280d05fe5c0c608eb Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 14 Jun 2018 11:57:09 -0400 Subject: [PATCH 086/144] Fix tests (#14959) * Fix tests * Lint --- requirements_test.txt | 6 +++--- requirements_test_all.txt | 6 +++--- tests/components/test_feedreader.py | 10 ++++++---- 3 files changed, 12 insertions(+), 10 deletions(-) diff --git a/requirements_test.txt b/requirements_test.txt index e7e854110f1..7ee0e166cf2 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -1,7 +1,7 @@ # linters such as flake8 and pylint should be pinned, as new releases # make new things fail. Manually update these pins when pulling in a # new version -asynctest>=0.11.1 +asynctest==0.12.1 coveralls==1.2.0 flake8-docstrings==1.0.3 flake8==3.5 @@ -12,6 +12,6 @@ pylint==1.9.2 pytest-aiohttp==0.3.0 pytest-cov==2.5.1 pytest-sugar==0.9.1 -pytest-timeout>=1.2.1 -pytest==3.4.2 +pytest-timeout==1.3.0 +pytest==3.6.1 requests_mock==1.5 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index af4f8feb753..82adbcc0733 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2,7 +2,7 @@ # linters such as flake8 and pylint should be pinned, as new releases # make new things fail. Manually update these pins when pulling in a # new version -asynctest>=0.11.1 +asynctest==0.12.1 coveralls==1.2.0 flake8-docstrings==1.0.3 flake8==3.5 @@ -13,8 +13,8 @@ pylint==1.9.2 pytest-aiohttp==0.3.0 pytest-cov==2.5.1 pytest-sugar==0.9.1 -pytest-timeout>=1.2.1 -pytest==3.4.2 +pytest-timeout==1.3.0 +pytest==3.6.1 requests_mock==1.5 diff --git a/tests/components/test_feedreader.py b/tests/components/test_feedreader.py index c20b297017c..336d19664b4 100644 --- a/tests/components/test_feedreader.py +++ b/tests/components/test_feedreader.py @@ -1,6 +1,6 @@ """The tests for the feedreader component.""" import time -from datetime import datetime, timedelta +from datetime import timedelta import unittest from genericpath import exists @@ -118,9 +118,11 @@ class TestFeedreaderComponent(unittest.TestCase): assert events[0].data.description == "Description 1" assert events[0].data.link == "http://www.example.com/link/1" assert events[0].data.id == "GUID 1" - assert datetime.fromtimestamp( - time.mktime(events[0].data.published_parsed)) == \ - datetime(2018, 4, 30, 5, 10, 0) + assert events[0].data.published_parsed.tm_year == 2018 + assert events[0].data.published_parsed.tm_mon == 4 + assert events[0].data.published_parsed.tm_mday == 30 + assert events[0].data.published_parsed.tm_hour == 5 + assert events[0].data.published_parsed.tm_min == 10 assert manager.last_update_successful is True def test_feed_updates(self): From c8e0de19b6295376827d73ea81c1662749a49da2 Mon Sep 17 00:00:00 2001 From: Benedict Aas Date: Thu, 14 Jun 2018 19:06:49 +0100 Subject: [PATCH 087/144] add relative time option to simulated sensors (#14038) By default simulated sensors are relative to when they're activated, instead we make this togglable with this new option 'relative_to_epoch', and instead they become relative to 1970-01-01 00:00:00. --- homeassistant/components/sensor/simulated.py | 19 ++++++++++++++++--- tests/components/sensor/test_simulated.py | 6 ++++-- 2 files changed, 20 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/sensor/simulated.py b/homeassistant/components/sensor/simulated.py index 9f114cf2c56..419ca7c13fb 100644 --- a/homeassistant/components/sensor/simulated.py +++ b/homeassistant/components/sensor/simulated.py @@ -7,6 +7,7 @@ https://home-assistant.io/components/sensor.simulated/ import logging import math from random import Random +from datetime import datetime import voluptuous as vol @@ -25,6 +26,7 @@ CONF_PERIOD = 'period' CONF_PHASE = 'phase' CONF_SEED = 'seed' CONF_UNIT = 'unit' +CONF_RELATIVE_TO_EPOCH = 'relative_to_epoch' DEFAULT_AMP = 1 DEFAULT_FWHM = 0 @@ -34,6 +36,7 @@ DEFAULT_PERIOD = 60 DEFAULT_PHASE = 0 DEFAULT_SEED = 999 DEFAULT_UNIT = 'value' +DEFAULT_RELATIVE_TO_EPOCH = True ICON = 'mdi:chart-line' @@ -46,6 +49,8 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Optional(CONF_PHASE, default=DEFAULT_PHASE): vol.Coerce(float), vol.Optional(CONF_SEED, default=DEFAULT_SEED): cv.positive_int, vol.Optional(CONF_UNIT, default=DEFAULT_UNIT): cv.string, + vol.Optional(CONF_RELATIVE_TO_EPOCH, default=DEFAULT_RELATIVE_TO_EPOCH): + cv.boolean, }) @@ -59,15 +64,18 @@ def setup_platform(hass, config, add_devices, discovery_info=None): phase = config.get(CONF_PHASE) fwhm = config.get(CONF_FWHM) seed = config.get(CONF_SEED) + relative_to_epoch = config.get(CONF_RELATIVE_TO_EPOCH) - sensor = SimulatedSensor(name, unit, amp, mean, period, phase, fwhm, seed) + sensor = SimulatedSensor(name, unit, amp, mean, period, phase, fwhm, seed, + relative_to_epoch) add_devices([sensor], True) class SimulatedSensor(Entity): """Class for simulated sensor.""" - def __init__(self, name, unit, amp, mean, period, phase, fwhm, seed): + def __init__(self, name, unit, amp, mean, period, phase, fwhm, seed, + relative_to_epoch): """Init the class.""" self._name = name self._unit = unit @@ -78,7 +86,11 @@ class SimulatedSensor(Entity): self._fwhm = fwhm self._seed = seed self._random = Random(seed) # A local seeded Random - self._start_time = dt_util.utcnow() + self._start_time = ( + datetime(1970, 1, 1, tzinfo=dt_util.UTC) if relative_to_epoch + else dt_util.utcnow() + ) + self._relative_to_epoch = relative_to_epoch self._state = None def time_delta(self): @@ -136,5 +148,6 @@ class SimulatedSensor(Entity): 'phase': self._phase, 'spread': self._fwhm, 'seed': self._seed, + 'relative_to_epoch': self._relative_to_epoch, } return attr diff --git a/tests/components/sensor/test_simulated.py b/tests/components/sensor/test_simulated.py index d226c79cff5..50552baa33e 100644 --- a/tests/components/sensor/test_simulated.py +++ b/tests/components/sensor/test_simulated.py @@ -5,8 +5,8 @@ from tests.common import get_test_home_assistant from homeassistant.components.sensor.simulated import ( CONF_AMP, CONF_FWHM, CONF_MEAN, CONF_PERIOD, CONF_PHASE, CONF_SEED, - CONF_UNIT, DEFAULT_AMP, DEFAULT_FWHM, DEFAULT_MEAN, DEFAULT_NAME, - DEFAULT_PHASE, DEFAULT_SEED) + CONF_UNIT, CONF_RELATIVE_TO_EPOCH, DEFAULT_AMP, DEFAULT_FWHM, DEFAULT_MEAN, + DEFAULT_NAME, DEFAULT_PHASE, DEFAULT_SEED, DEFAULT_RELATIVE_TO_EPOCH) from homeassistant.const import CONF_FRIENDLY_NAME from homeassistant.setup import setup_component @@ -42,3 +42,5 @@ class TestSimulatedSensor(unittest.TestCase): assert state.attributes.get(CONF_PHASE) == DEFAULT_PHASE assert state.attributes.get(CONF_FWHM) == DEFAULT_FWHM assert state.attributes.get(CONF_SEED) == DEFAULT_SEED + assert state.attributes.get( + CONF_RELATIVE_TO_EPOCH) == DEFAULT_RELATIVE_TO_EPOCH From 2c6e6c2a6fb89d0c602349ad474cb9a861afa42f Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 14 Jun 2018 15:17:54 -0400 Subject: [PATCH 088/144] Add config entry for Sonos + Cast (#14955) * Add config entry for Sonos * Lint * Use add_job * Add Cast config entry * Lint * Rename DOMAIN import * Mock pychromecast in test --- .coveragerc | 8 +- .../components/cast/.translations/en.json | 15 +++ homeassistant/components/cast/__init__.py | 30 +++++ homeassistant/components/cast/strings.json | 15 +++ homeassistant/components/discovery.py | 4 +- .../components/media_player/__init__.py | 10 ++ homeassistant/components/media_player/cast.py | 23 +++- .../components/media_player/sonos.py | 25 +++- .../components/sonos/.translations/en.json | 15 +++ homeassistant/components/sonos/__init__.py | 29 +++++ homeassistant/components/sonos/strings.json | 15 +++ homeassistant/config_entries.py | 2 + homeassistant/helpers/config_entry_flow.py | 85 +++++++++++++ requirements_all.txt | 4 +- requirements_test_all.txt | 2 +- tests/components/cast/__init__.py | 1 + tests/components/cast/test_init.py | 22 ++++ tests/components/sonos/__init__.py | 1 + tests/components/sonos/test_init.py | 20 +++ tests/helpers/test_config_entry_flow.py | 116 ++++++++++++++++++ 20 files changed, 432 insertions(+), 10 deletions(-) create mode 100644 homeassistant/components/cast/.translations/en.json create mode 100644 homeassistant/components/cast/__init__.py create mode 100644 homeassistant/components/cast/strings.json create mode 100644 homeassistant/components/sonos/.translations/en.json create mode 100644 homeassistant/components/sonos/__init__.py create mode 100644 homeassistant/components/sonos/strings.json create mode 100644 homeassistant/helpers/config_entry_flow.py create mode 100644 tests/components/cast/__init__.py create mode 100644 tests/components/cast/test_init.py create mode 100644 tests/components/sonos/__init__.py create mode 100644 tests/components/sonos/test_init.py create mode 100644 tests/helpers/test_config_entry_flow.py diff --git a/.coveragerc b/.coveragerc index 5a8f26e34da..e7d6d2a404a 100644 --- a/.coveragerc +++ b/.coveragerc @@ -61,6 +61,9 @@ omit = homeassistant/components/coinbase.py homeassistant/components/sensor/coinbase.py + homeassistant/components/cast/* + homeassistant/components/*/cast.py + homeassistant/components/comfoconnect.py homeassistant/components/*/comfoconnect.py @@ -252,6 +255,9 @@ omit = homeassistant/components/smappee.py homeassistant/components/*/smappee.py + homeassistant/components/sonos/__init__.py + homeassistant/components/*/sonos.py + homeassistant/components/tado.py homeassistant/components/*/tado.py @@ -482,7 +488,6 @@ omit = homeassistant/components/media_player/aquostv.py homeassistant/components/media_player/bluesound.py homeassistant/components/media_player/braviatv.py - homeassistant/components/media_player/cast.py homeassistant/components/media_player/channels.py homeassistant/components/media_player/clementine.py homeassistant/components/media_player/cmus.py @@ -518,7 +523,6 @@ omit = homeassistant/components/media_player/russound_rnet.py homeassistant/components/media_player/snapcast.py homeassistant/components/media_player/songpal.py - homeassistant/components/media_player/sonos.py homeassistant/components/media_player/spotify.py homeassistant/components/media_player/squeezebox.py homeassistant/components/media_player/ue_smart_radio.py diff --git a/homeassistant/components/cast/.translations/en.json b/homeassistant/components/cast/.translations/en.json new file mode 100644 index 00000000000..55d79a7d560 --- /dev/null +++ b/homeassistant/components/cast/.translations/en.json @@ -0,0 +1,15 @@ +{ + "config": { + "abort": { + "no_devices_found": "No Google Cast devices found on the network.", + "single_instance_allowed": "Only a single configuration of Google Cast is necessary." + }, + "step": { + "confirm": { + "description": "Do you want to setup Google Cast?", + "title": "Google Cast" + } + }, + "title": "Google Cast" + } +} \ No newline at end of file diff --git a/homeassistant/components/cast/__init__.py b/homeassistant/components/cast/__init__.py new file mode 100644 index 00000000000..a4ee25f0915 --- /dev/null +++ b/homeassistant/components/cast/__init__.py @@ -0,0 +1,30 @@ +"""Component to embed Google Cast.""" +from homeassistant.helpers import config_entry_flow + + +DOMAIN = 'cast' +REQUIREMENTS = ['pychromecast==2.1.0'] + + +async def async_setup(hass, config): + """Set up the Cast component.""" + hass.data[DOMAIN] = config.get(DOMAIN, {}) + return True + + +async def async_setup_entry(hass, entry): + """Set up Cast from a config entry.""" + hass.async_add_job(hass.config_entries.async_forward_entry_setup( + entry, 'media_player')) + return True + + +async def _async_has_devices(hass): + """Return if there are devices that can be discovered.""" + from pychromecast.discovery import discover_chromecasts + + return await hass.async_add_job(discover_chromecasts) + + +config_entry_flow.register_discovery_flow( + DOMAIN, 'Google Cast', _async_has_devices) diff --git a/homeassistant/components/cast/strings.json b/homeassistant/components/cast/strings.json new file mode 100644 index 00000000000..7f480de0e8b --- /dev/null +++ b/homeassistant/components/cast/strings.json @@ -0,0 +1,15 @@ +{ + "config": { + "title": "Google Cast", + "step": { + "confirm": { + "title": "Google Cast", + "description": "Do you want to setup Google Cast?" + } + }, + "abort": { + "single_instance_allowed": "Only a single configuration of Google Cast is necessary.", + "no_devices_found": "No Google Cast devices found on the network." + } + } +} diff --git a/homeassistant/components/discovery.py b/homeassistant/components/discovery.py index 00d4291539b..d7041865892 100644 --- a/homeassistant/components/discovery.py +++ b/homeassistant/components/discovery.py @@ -46,7 +46,9 @@ SERVICE_HOMEKIT = 'homekit' CONFIG_ENTRY_HANDLERS = { SERVICE_DECONZ: 'deconz', + 'google_cast': 'cast', SERVICE_HUE: 'hue', + 'sonos': 'sonos', } SERVICE_HANDLERS = { @@ -64,11 +66,9 @@ SERVICE_HANDLERS = { SERVICE_SABNZBD: ('sabnzbd', None), SERVICE_SAMSUNG_PRINTER: ('sensor', 'syncthru'), SERVICE_KONNECTED: ('konnected', None), - 'google_cast': ('media_player', 'cast'), 'panasonic_viera': ('media_player', 'panasonic_viera'), 'plex_mediaserver': ('media_player', 'plex'), 'roku': ('media_player', 'roku'), - 'sonos': ('media_player', 'sonos'), 'yamaha': ('media_player', 'yamaha'), 'logitech_mediaserver': ('media_player', 'squeezebox'), 'directv': ('media_player', 'directv'), diff --git a/homeassistant/components/media_player/__init__.py b/homeassistant/components/media_player/__init__.py index 7452b7dd186..d963deba7b5 100644 --- a/homeassistant/components/media_player/__init__.py +++ b/homeassistant/components/media_player/__init__.py @@ -456,6 +456,16 @@ async def async_setup(hass, config): return True +async def async_setup_entry(hass, entry): + """Setup a config entry.""" + return await hass.data[DOMAIN].async_setup_entry(entry) + + +async def async_unload_entry(hass, entry): + """Unload a config entry.""" + return await hass.data[DOMAIN].async_unload_entry(entry) + + class MediaPlayerDevice(Entity): """ABC for media player devices.""" diff --git a/homeassistant/components/media_player/cast.py b/homeassistant/components/media_player/cast.py index a9bea9e4c1d..eced0dbbe25 100644 --- a/homeassistant/components/media_player/cast.py +++ b/homeassistant/components/media_player/cast.py @@ -17,6 +17,7 @@ from homeassistant.helpers.typing import HomeAssistantType, ConfigType from homeassistant.core import callback from homeassistant.helpers.dispatcher import (dispatcher_send, async_dispatcher_connect) +from homeassistant.components.cast import DOMAIN as CAST_DOMAIN from homeassistant.components.media_player import ( MEDIA_TYPE_MUSIC, MEDIA_TYPE_TVSHOW, MEDIA_TYPE_MOVIE, SUPPORT_NEXT_TRACK, SUPPORT_PAUSE, SUPPORT_PLAY_MEDIA, SUPPORT_PREVIOUS_TRACK, @@ -28,7 +29,7 @@ from homeassistant.const import ( import homeassistant.helpers.config_validation as cv import homeassistant.util.dt as dt_util -REQUIREMENTS = ['pychromecast==2.1.0'] +DEPENDENCIES = ('cast',) _LOGGER = logging.getLogger(__name__) @@ -186,6 +187,26 @@ def _async_create_cast_device(hass: HomeAssistantType, async def async_setup_platform(hass: HomeAssistantType, config: ConfigType, async_add_devices, discovery_info=None): + """Set up thet Cast platform. + + Deprecated. + """ + _LOGGER.warning( + 'Setting configuration for Cast via platform is deprecated. ' + 'Configure via Cast component instead.') + await _async_setup_platform( + hass, config, async_add_devices, discovery_info) + + +async def async_setup_entry(hass, config_entry, async_add_devices): + """Set up Cast from a config entry.""" + await _async_setup_platform( + hass, hass.data[CAST_DOMAIN].get('media_player', {}), + async_add_devices, None) + + +async def _async_setup_platform(hass: HomeAssistantType, config: ConfigType, + async_add_devices, discovery_info): """Set up the cast platform.""" import pychromecast diff --git a/homeassistant/components/media_player/sonos.py b/homeassistant/components/media_player/sonos.py index 0f536e1edfb..da0ad24b135 100644 --- a/homeassistant/components/media_player/sonos.py +++ b/homeassistant/components/media_player/sonos.py @@ -20,13 +20,14 @@ from homeassistant.components.media_player import ( SUPPORT_PLAY_MEDIA, SUPPORT_PREVIOUS_TRACK, SUPPORT_SEEK, SUPPORT_SELECT_SOURCE, SUPPORT_SHUFFLE_SET, SUPPORT_STOP, SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET, MediaPlayerDevice) +from homeassistant.components.sonos import DOMAIN as SONOS_DOMAIN from homeassistant.const import ( ATTR_ENTITY_ID, ATTR_TIME, CONF_HOSTS, STATE_IDLE, STATE_OFF, STATE_PAUSED, STATE_PLAYING) import homeassistant.helpers.config_validation as cv from homeassistant.util.dt import utcnow -REQUIREMENTS = ['SoCo==0.14'] +DEPENDENCIES = ('sonos',) _LOGGER = logging.getLogger(__name__) @@ -49,7 +50,7 @@ SERVICE_CLEAR_TIMER = 'sonos_clear_sleep_timer' SERVICE_UPDATE_ALARM = 'sonos_update_alarm' SERVICE_SET_OPTION = 'sonos_set_option' -DATA_SONOS = 'sonos' +DATA_SONOS = 'sonos_devices' SOURCE_LINEIN = 'Line-in' SOURCE_TV = 'TV' @@ -118,6 +119,26 @@ class SonosData: def setup_platform(hass, config, add_devices, discovery_info=None): + """Set up the Sonos platform. + + Deprecated. + """ + _LOGGER.warning('Loading Sonos via platform config is deprecated.') + _setup_platform(hass, config, add_devices, discovery_info) + + +async def async_setup_entry(hass, config_entry, async_add_devices): + """Set up Sonos from a config entry.""" + def add_devices(devices, update_before_add=False): + """Sync version of async add devices.""" + hass.add_job(async_add_devices, devices, update_before_add) + + hass.add_job(_setup_platform, hass, + hass.data[SONOS_DOMAIN].get('media_player', {}), + add_devices, None) + + +def _setup_platform(hass, config, add_devices, discovery_info): """Set up the Sonos platform.""" import soco import soco.events diff --git a/homeassistant/components/sonos/.translations/en.json b/homeassistant/components/sonos/.translations/en.json new file mode 100644 index 00000000000..c7aae4302f6 --- /dev/null +++ b/homeassistant/components/sonos/.translations/en.json @@ -0,0 +1,15 @@ +{ + "config": { + "abort": { + "no_devices_found": "No Sonos devices found on the network.", + "single_instance_allowed": "Only a single configuration of Sonos is necessary." + }, + "step": { + "confirm": { + "description": "Do you want to setup Sonos?", + "title": "Sonos" + } + }, + "title": "Sonos" + } +} \ No newline at end of file diff --git a/homeassistant/components/sonos/__init__.py b/homeassistant/components/sonos/__init__.py new file mode 100644 index 00000000000..7c3de210768 --- /dev/null +++ b/homeassistant/components/sonos/__init__.py @@ -0,0 +1,29 @@ +"""Component to embed Sonos.""" +from homeassistant.helpers import config_entry_flow + + +DOMAIN = 'sonos' +REQUIREMENTS = ['SoCo==0.14'] + + +async def async_setup(hass, config): + """Set up the Sonos component.""" + hass.data[DOMAIN] = config.get(DOMAIN, {}) + return True + + +async def async_setup_entry(hass, entry): + """Set up Sonos from a config entry.""" + hass.async_add_job(hass.config_entries.async_forward_entry_setup( + entry, 'media_player')) + return True + + +async def _async_has_devices(hass): + """Return if there are devices that can be discovered.""" + import soco + + return await hass.async_add_job(soco.discover) + + +config_entry_flow.register_discovery_flow(DOMAIN, 'Sonos', _async_has_devices) diff --git a/homeassistant/components/sonos/strings.json b/homeassistant/components/sonos/strings.json new file mode 100644 index 00000000000..4aa68712d59 --- /dev/null +++ b/homeassistant/components/sonos/strings.json @@ -0,0 +1,15 @@ +{ + "config": { + "title": "Sonos", + "step": { + "confirm": { + "title": "Sonos", + "description": "Do you want to setup Sonos?" + } + }, + "abort": { + "single_instance_allowed": "Only a single configuration of Sonos is necessary.", + "no_devices_found": "No Sonos devices found on the network." + } + } +} diff --git a/homeassistant/config_entries.py b/homeassistant/config_entries.py index 7826e26b960..504c0850a93 100644 --- a/homeassistant/config_entries.py +++ b/homeassistant/config_entries.py @@ -127,9 +127,11 @@ _LOGGER = logging.getLogger(__name__) HANDLERS = Registry() # Components that have config flows. In future we will auto-generate this list. FLOWS = [ + 'cast', 'deconz', 'hue', 'nest', + 'sonos', 'zone', ] diff --git a/homeassistant/helpers/config_entry_flow.py b/homeassistant/helpers/config_entry_flow.py new file mode 100644 index 00000000000..2a4ec2966df --- /dev/null +++ b/homeassistant/helpers/config_entry_flow.py @@ -0,0 +1,85 @@ +"""Helpers for data entry flows for config entries.""" +from functools import partial + +from homeassistant.core import callback +from homeassistant import config_entries, data_entry_flow + + +def register_discovery_flow(domain, title, discovery_function): + """Register flow for discovered integrations that not require auth.""" + config_entries.HANDLERS.register(domain)( + partial(DiscoveryFlowHandler, domain, title, discovery_function)) + + +class DiscoveryFlowHandler(data_entry_flow.FlowHandler): + """Handle a discovery config flow.""" + + VERSION = 1 + + def __init__(self, domain, title, discovery_function): + """Initialize the discovery config flow.""" + self._domain = domain + self._title = title + self._discovery_function = discovery_function + + async def async_step_init(self, user_input=None): + """Handle a flow initialized by the user.""" + if self._async_current_entries(): + return self.async_abort( + reason='single_instance_allowed' + ) + + # Get current discovered entries. + in_progress = self._async_in_progress() + + has_devices = in_progress + if not has_devices: + has_devices = await self.hass.async_add_job( + self._discovery_function, self.hass) + + if not has_devices: + return self.async_abort( + reason='no_devices_found' + ) + + # Cancel the discovered one. + for flow in in_progress: + self.hass.config_entries.flow.async_abort(flow['flow_id']) + + return self.async_create_entry( + title=self._title, + data={}, + ) + + async def async_step_confirm(self, user_input=None): + """Confirm setup.""" + if user_input is not None: + return self.async_create_entry( + title=self._title, + data={}, + ) + + return self.async_show_form( + step_id='confirm', + ) + + async def async_step_discovery(self, discovery_info): + """Handle a flow initialized by discovery.""" + if self._async_in_progress() or self._async_current_entries(): + return self.async_abort( + reason='single_instance_allowed' + ) + + return await self.async_step_confirm() + + @callback + def _async_current_entries(self): + """Return current entries.""" + return self.hass.config_entries.async_entries(self._domain) + + @callback + def _async_in_progress(self): + """Return other in progress flows for current domain.""" + return [flw for flw in self.hass.config_entries.flow.async_progress() + if flw['handler'] == self._domain and + flw['flow_id'] != self.flow_id] diff --git a/requirements_all.txt b/requirements_all.txt index f144f3e2a2b..79db358942d 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -54,7 +54,7 @@ PyXiaomiGateway==0.9.5 # homeassistant.components.remember_the_milk RtmAPI==0.7.0 -# homeassistant.components.media_player.sonos +# homeassistant.components.sonos SoCo==0.14 # homeassistant.components.sensor.travisci @@ -773,7 +773,7 @@ pyblackbird==0.5 # homeassistant.components.media_player.channels pychannels==1.0.0 -# homeassistant.components.media_player.cast +# homeassistant.components.cast pychromecast==2.1.0 # homeassistant.components.media_player.cmus diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 82adbcc0733..34928b4f111 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -24,7 +24,7 @@ HAP-python==2.2.2 # homeassistant.components.notify.html5 PyJWT==1.6.0 -# homeassistant.components.media_player.sonos +# homeassistant.components.sonos SoCo==0.14 # homeassistant.components.device_tracker.automatic diff --git a/tests/components/cast/__init__.py b/tests/components/cast/__init__.py new file mode 100644 index 00000000000..7e904dce00a --- /dev/null +++ b/tests/components/cast/__init__.py @@ -0,0 +1 @@ +"""Tests for the Cast component.""" diff --git a/tests/components/cast/test_init.py b/tests/components/cast/test_init.py new file mode 100644 index 00000000000..260856c6742 --- /dev/null +++ b/tests/components/cast/test_init.py @@ -0,0 +1,22 @@ +"""Tests for the Cast config flow.""" +from unittest.mock import patch + +from homeassistant import data_entry_flow +from homeassistant.components import cast + +from tests.common import MockDependency, mock_coro + + +async def test_creating_entry_sets_up_media_player(hass): + """Test setting up Cast loads the media player.""" + with patch('homeassistant.components.media_player.cast.async_setup_entry', + return_value=mock_coro(True)) as mock_setup, \ + MockDependency('pychromecast', 'discovery'), \ + patch('pychromecast.discovery.discover_chromecasts', + return_value=True): + result = await hass.config_entries.flow.async_init(cast.DOMAIN) + assert result['type'] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + + await hass.async_block_till_done() + + assert len(mock_setup.mock_calls) == 1 diff --git a/tests/components/sonos/__init__.py b/tests/components/sonos/__init__.py new file mode 100644 index 00000000000..878e0c17318 --- /dev/null +++ b/tests/components/sonos/__init__.py @@ -0,0 +1 @@ +"""Tests for the Sonos component.""" diff --git a/tests/components/sonos/test_init.py b/tests/components/sonos/test_init.py new file mode 100644 index 00000000000..2cbc2360fd4 --- /dev/null +++ b/tests/components/sonos/test_init.py @@ -0,0 +1,20 @@ +"""Tests for the Sonos config flow.""" +from unittest.mock import patch + +from homeassistant import data_entry_flow +from homeassistant.components import sonos + +from tests.common import mock_coro + + +async def test_creating_entry_sets_up_media_player(hass): + """Test setting up Sonos loads the media player.""" + with patch('homeassistant.components.media_player.sonos.async_setup_entry', + return_value=mock_coro(True)) as mock_setup, \ + patch('soco.discover', return_value=True): + result = await hass.config_entries.flow.async_init(sonos.DOMAIN) + assert result['type'] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + + await hass.async_block_till_done() + + assert len(mock_setup.mock_calls) == 1 diff --git a/tests/helpers/test_config_entry_flow.py b/tests/helpers/test_config_entry_flow.py new file mode 100644 index 00000000000..d3f13ac4302 --- /dev/null +++ b/tests/helpers/test_config_entry_flow.py @@ -0,0 +1,116 @@ +"""Tests for the Config Entry Flow helper.""" +from unittest.mock import patch + +import pytest + +from homeassistant import config_entries, data_entry_flow, loader +from homeassistant.helpers import config_entry_flow +from tests.common import MockConfigEntry, MockModule + + +@pytest.fixture +def flow_conf(hass): + """Register a handler.""" + handler_conf = { + 'discovered': False, + } + + async def has_discovered_devices(hass): + """Mock if we have discovered devices.""" + return handler_conf['discovered'] + + with patch.dict(config_entries.HANDLERS): + config_entry_flow.register_discovery_flow( + 'test', 'Test', has_discovered_devices) + yield handler_conf + + +async def test_single_entry_allowed(hass, flow_conf): + """Test only a single entry is allowed.""" + flow = config_entries.HANDLERS['test']() + flow.hass = hass + + MockConfigEntry(domain='test').add_to_hass(hass) + result = await flow.async_step_init() + + assert result['type'] == data_entry_flow.RESULT_TYPE_ABORT + assert result['reason'] == 'single_instance_allowed' + + +async def test_user_no_devices_found(hass, flow_conf): + """Test if no devices found.""" + flow = config_entries.HANDLERS['test']() + flow.hass = hass + + result = await flow.async_step_init() + + assert result['type'] == data_entry_flow.RESULT_TYPE_ABORT + assert result['reason'] == 'no_devices_found' + + +async def test_user_no_confirmation(hass, flow_conf): + """Test user requires no confirmation to setup.""" + flow = config_entries.HANDLERS['test']() + flow.hass = hass + flow_conf['discovered'] = True + + result = await flow.async_step_init() + + assert result['type'] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + + +async def test_discovery_single_instance(hass, flow_conf): + """Test we ask for confirmation via discovery.""" + flow = config_entries.HANDLERS['test']() + flow.hass = hass + + MockConfigEntry(domain='test').add_to_hass(hass) + result = await flow.async_step_discovery({}) + + assert result['type'] == data_entry_flow.RESULT_TYPE_ABORT + assert result['reason'] == 'single_instance_allowed' + + +async def test_discovery_confirmation(hass, flow_conf): + """Test we ask for confirmation via discovery.""" + flow = config_entries.HANDLERS['test']() + flow.hass = hass + + result = await flow.async_step_discovery({}) + + assert result['type'] == data_entry_flow.RESULT_TYPE_FORM + assert result['step_id'] == 'confirm' + + result = await flow.async_step_confirm({}) + assert result['type'] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + + +async def test_multiple_discoveries(hass, flow_conf): + """Test we only create one instance for multiple discoveries.""" + loader.set_component(hass, 'test', MockModule('test')) + + result = await hass.config_entries.flow.async_init( + 'test', source=data_entry_flow.SOURCE_DISCOVERY, data={}) + assert result['type'] == data_entry_flow.RESULT_TYPE_FORM + + # Second discovery + result = await hass.config_entries.flow.async_init( + 'test', source=data_entry_flow.SOURCE_DISCOVERY, data={}) + assert result['type'] == data_entry_flow.RESULT_TYPE_ABORT + + +async def test_user_init_trumps_discovery(hass, flow_conf): + """Test a user initialized one will finish and cancel discovered one.""" + loader.set_component(hass, 'test', MockModule('test')) + + # Discovery starts flow + result = await hass.config_entries.flow.async_init( + 'test', source=data_entry_flow.SOURCE_DISCOVERY, data={}) + assert result['type'] == data_entry_flow.RESULT_TYPE_FORM + + # User starts flow + result = await hass.config_entries.flow.async_init('test', data={}) + assert result['type'] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + + # Discovery flow has been aborted + assert len(hass.config_entries.flow.async_progress()) == 0 From 1128104281a2080a0463f29efbb9912aef8d3603 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 15 Jun 2018 10:59:13 -0400 Subject: [PATCH 089/144] Adhere to scan_interval in platforms when setup via config entry (#14969) --- homeassistant/helpers/entity_component.py | 3 ++- tests/common.py | 5 ++++- tests/helpers/test_entity_component.py | 6 +++++- 3 files changed, 11 insertions(+), 3 deletions(-) diff --git a/homeassistant/helpers/entity_component.py b/homeassistant/helpers/entity_component.py index c82ae2a46f0..4ac3a147296 100644 --- a/homeassistant/helpers/entity_component.py +++ b/homeassistant/helpers/entity_component.py @@ -108,7 +108,8 @@ class EntityComponent(object): raise ValueError('Config entry has already been setup!') self._platforms[key] = self._async_init_entity_platform( - platform_type, platform + platform_type, platform, + scan_interval=getattr(platform, 'SCAN_INTERVAL', None), ) return await self._platforms[key].async_setup_entry(config_entry) diff --git a/tests/common.py b/tests/common.py index f53d1c2be2b..556935a6ac1 100644 --- a/tests/common.py +++ b/tests/common.py @@ -373,13 +373,16 @@ class MockPlatform(object): # pylint: disable=invalid-name def __init__(self, setup_platform=None, dependencies=None, platform_schema=None, async_setup_platform=None, - async_setup_entry=None): + async_setup_entry=None, scan_interval=None): """Initialize the platform.""" self.DEPENDENCIES = dependencies or [] if platform_schema is not None: self.PLATFORM_SCHEMA = platform_schema + if scan_interval is not None: + self.SCAN_INTERVAL = scan_interval + if setup_platform is not None: # We run this in executor, wrap it in function self.setup_platform = lambda *args: setup_platform(*args) diff --git a/tests/helpers/test_entity_component.py b/tests/helpers/test_entity_component.py index 504f31cc987..b4910723c8d 100644 --- a/tests/helpers/test_entity_component.py +++ b/tests/helpers/test_entity_component.py @@ -346,7 +346,8 @@ async def test_setup_entry(hass): mock_setup_entry = Mock(return_value=mock_coro(True)) loader.set_component( hass, 'test_domain.entry_domain', - MockPlatform(async_setup_entry=mock_setup_entry)) + MockPlatform(async_setup_entry=mock_setup_entry, + scan_interval=timedelta(seconds=5))) component = EntityComponent(_LOGGER, DOMAIN, hass) entry = MockConfigEntry(domain='entry_domain') @@ -357,6 +358,9 @@ async def test_setup_entry(hass): assert p_hass is hass assert p_entry is entry + assert component._platforms[entry.entry_id].scan_interval == \ + timedelta(seconds=5) + async def test_setup_entry_platform_not_exist(hass): """Test setup entry fails if platform doesnt exist.""" From 3cd4cb741ccb051e2720ca52b84bd27556c576f5 Mon Sep 17 00:00:00 2001 From: Thibault Cohen Date: Fri, 15 Jun 2018 11:16:31 -0400 Subject: [PATCH 090/144] Add Calendar API endpoint to get events (#14702) * Add Calendar API endpoint to get events * Set default event color * Fix PR comments * Fix PR comments * Fix PR comments * Remote local.py file * Use iso 8601 * Fix lint * Fix PR comments * Fix PR comments * Add Support for todoist and demo calendar * Todoist events are allday events * Add calendar demo api endpoint test * Register only one api endpoint for calendar * Rename demo calendar --- homeassistant/components/calendar/__init__.py | 56 +++++++++++++++---- homeassistant/components/calendar/caldav.py | 35 +++++++++++- homeassistant/components/calendar/demo.py | 22 +++++++- homeassistant/components/calendar/google.py | 40 +++++++++++-- homeassistant/components/calendar/todoist.py | 29 ++++++++++ tests/components/calendar/test_caldav.py | 17 +++--- tests/components/calendar/test_demo.py | 24 ++++++++ tests/components/calendar/test_google.py | 13 +++-- 8 files changed, 202 insertions(+), 34 deletions(-) create mode 100644 tests/components/calendar/test_demo.py diff --git a/homeassistant/components/calendar/__init__.py b/homeassistant/components/calendar/__init__.py index 5198381b976..f5e1d581891 100644 --- a/homeassistant/components/calendar/__init__.py +++ b/homeassistant/components/calendar/__init__.py @@ -9,6 +9,8 @@ import logging from datetime import timedelta import re +from aiohttp import web + from homeassistant.components.google import ( CONF_OFFSET, CONF_DEVICE_ID, CONF_NAME) from homeassistant.const import STATE_OFF, STATE_ON @@ -18,11 +20,15 @@ from homeassistant.helpers.entity import Entity, generate_entity_id from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.template import DATE_STR_FORMAT from homeassistant.util import dt +from homeassistant.components import http + _LOGGER = logging.getLogger(__name__) DOMAIN = 'calendar' +DEPENDENCIES = ['http'] + ENTITY_ID_FORMAT = DOMAIN + '.{}' SCAN_INTERVAL = timedelta(seconds=60) @@ -34,6 +40,8 @@ def async_setup(hass, config): component = EntityComponent( _LOGGER, DOMAIN, hass, SCAN_INTERVAL, DOMAIN) + hass.http.register_view(CalendarEventView(component)) + yield from component.async_setup(config) return True @@ -42,6 +50,14 @@ DEFAULT_CONF_TRACK_NEW = True DEFAULT_CONF_OFFSET = '!!' +def get_date(date): + """Get the dateTime from date or dateTime as a local.""" + if 'date' in date: + return dt.start_of_local_day(dt.dt.datetime.combine( + dt.parse_date(date['date']), dt.dt.time.min)) + return dt.as_local(dt.parse_datetime(date['dateTime'])) + + # pylint: disable=too-many-instance-attributes class CalendarEventDevice(Entity): """A calendar event device.""" @@ -144,15 +160,8 @@ class CalendarEventDevice(Entity): self.cleanup() return - def _get_date(date): - """Get the dateTime from date or dateTime as a local.""" - if 'date' in date: - return dt.start_of_local_day(dt.dt.datetime.combine( - dt.parse_date(date['date']), dt.dt.time.min)) - return dt.as_local(dt.parse_datetime(date['dateTime'])) - - start = _get_date(self.data.event['start']) - end = _get_date(self.data.event['end']) + start = get_date(self.data.event['start']) + end = get_date(self.data.event['end']) summary = self.data.event.get('summary', '') @@ -176,10 +185,37 @@ class CalendarEventDevice(Entity): # cleanup the string so we don't have a bunch of double+ spaces self._cal_data['message'] = re.sub(' +', '', summary).strip() - self._cal_data['offset_time'] = offset_time self._cal_data['location'] = self.data.event.get('location', '') self._cal_data['description'] = self.data.event.get('description', '') self._cal_data['start'] = start self._cal_data['end'] = end self._cal_data['all_day'] = 'date' in self.data.event['start'] + + +class CalendarEventView(http.HomeAssistantView): + """View to retrieve calendar content.""" + + url = '/api/calendar/{entity_id}' + name = 'api:calendar' + + def __init__(self, component): + """Initialize calendar view.""" + self.component = component + + async def get(self, request, entity_id): + """Return calendar events.""" + entity = self.component.get_entity('calendar.' + entity_id) + start = request.query.get('start') + end = request.query.get('end') + if None in (start, end, entity): + return web.Response(status=400) + try: + start_date = dt.parse_datetime(start) + end_date = dt.parse_datetime(end) + except (ValueError, AttributeError): + return web.Response(status=400) + event_list = await entity.async_get_events(request.app['hass'], + start_date, + end_date) + return self.json(event_list) diff --git a/homeassistant/components/calendar/caldav.py b/homeassistant/components/calendar/caldav.py index 6f92891c551..9c30d1481f8 100644 --- a/homeassistant/components/calendar/caldav.py +++ b/homeassistant/components/calendar/caldav.py @@ -11,7 +11,7 @@ import re import voluptuous as vol from homeassistant.components.calendar import ( - PLATFORM_SCHEMA, CalendarEventDevice) + PLATFORM_SCHEMA, CalendarEventDevice, get_date) from homeassistant.const import ( CONF_NAME, CONF_PASSWORD, CONF_URL, CONF_USERNAME) import homeassistant.helpers.config_validation as cv @@ -92,7 +92,7 @@ def setup_platform(hass, config, add_devices, disc_info=None): if not config.get(CONF_CUSTOM_CALENDARS): device_data = { CONF_NAME: calendar.name, - CONF_DEVICE_ID: calendar.name + CONF_DEVICE_ID: calendar.name, } calendar_devices.append( WebDavCalendarEventDevice(hass, device_data, calendar) @@ -120,6 +120,10 @@ class WebDavCalendarEventDevice(CalendarEventDevice): attributes = super().device_state_attributes return attributes + async def async_get_events(self, hass, start_date, end_date): + """Get all events in a specific time frame.""" + return await self.data.async_get_events(hass, start_date, end_date) + class WebDavCalendarData(object): """Class to utilize the calendar dav client object to get next event.""" @@ -131,6 +135,33 @@ class WebDavCalendarData(object): self.search = search self.event = None + async def async_get_events(self, hass, start_date, end_date): + """Get all events in a specific time frame.""" + # Get event list from the current calendar + vevent_list = await hass.async_add_job(self.calendar.date_search, + start_date, end_date) + event_list = [] + for event in vevent_list: + vevent = event.instance.vevent + uid = None + if hasattr(vevent, 'uid'): + uid = vevent.uid.value + data = { + "uid": uid, + "title": vevent.summary.value, + "start": self.get_hass_date(vevent.dtstart.value), + "end": self.get_hass_date(self.get_end_date(vevent)), + "location": self.get_attr_value(vevent, "location"), + "description": self.get_attr_value(vevent, "description"), + } + + data['start'] = get_date(data['start']).isoformat() + data['end'] = get_date(data['end']).isoformat() + + event_list.append(data) + + return event_list + @Throttle(MIN_TIME_BETWEEN_UPDATES) def update(self): """Get the latest data.""" diff --git a/homeassistant/components/calendar/demo.py b/homeassistant/components/calendar/demo.py index 7823f03c85e..5ddd9fe8e3d 100644 --- a/homeassistant/components/calendar/demo.py +++ b/homeassistant/components/calendar/demo.py @@ -4,8 +4,10 @@ Demo platform that has two fake binary sensors. For more details about this platform, please refer to the documentation https://home-assistant.io/components/demo/ """ +import copy + import homeassistant.util.dt as dt_util -from homeassistant.components.calendar import CalendarEventDevice +from homeassistant.components.calendar import CalendarEventDevice, get_date from homeassistant.components.google import CONF_DEVICE_ID, CONF_NAME @@ -16,12 +18,12 @@ def setup_platform(hass, config, add_devices, discovery_info=None): add_devices([ DemoGoogleCalendar(hass, calendar_data_future, { CONF_NAME: 'Future Event', - CONF_DEVICE_ID: 'future_event', + CONF_DEVICE_ID: 'calendar_1', }), DemoGoogleCalendar(hass, calendar_data_current, { CONF_NAME: 'Current Event', - CONF_DEVICE_ID: 'current_event', + CONF_DEVICE_ID: 'calendar_2', }), ]) @@ -29,11 +31,21 @@ def setup_platform(hass, config, add_devices, discovery_info=None): class DemoGoogleCalendarData(object): """Representation of a Demo Calendar element.""" + event = {} + # pylint: disable=no-self-use def update(self): """Return true so entity knows we have new data.""" return True + async def async_get_events(self, hass, start_date, end_date): + """Get all events in a specific time frame.""" + event = copy.copy(self.event) + event['title'] = event['summary'] + event['start'] = get_date(event['start']).isoformat() + event['end'] = get_date(event['end']).isoformat() + return [event] + class DemoGoogleCalendarDataFuture(DemoGoogleCalendarData): """Representation of a Demo Calendar for a future event.""" @@ -80,3 +92,7 @@ class DemoGoogleCalendar(CalendarEventDevice): """Initialize Google Calendar but without the API calls.""" self.data = calendar_data super().__init__(hass, data) + + async def async_get_events(self, hass, start_date, end_date): + """Get all events in a specific time frame.""" + return await self.data.async_get_events(hass, start_date, end_date) diff --git a/homeassistant/components/calendar/google.py b/homeassistant/components/calendar/google.py index 6c26c65ebe7..da76530a36d 100644 --- a/homeassistant/components/calendar/google.py +++ b/homeassistant/components/calendar/google.py @@ -51,6 +51,10 @@ class GoogleCalendarEventDevice(CalendarEventDevice): super().__init__(hass, data) + async def async_get_events(self, hass, start_date, end_date): + """Get all events in a specific time frame.""" + return await self.data.async_get_events(hass, start_date, end_date) + class GoogleCalendarData(object): """Class to utilize calendar service object to get next event.""" @@ -64,9 +68,7 @@ class GoogleCalendarData(object): self.ignore_availability = ignore_availability self.event = None - @Throttle(MIN_TIME_BETWEEN_UPDATES) - def update(self): - """Get the latest data.""" + def _prepare_query(self): from httplib2 import ServerNotFoundError try: @@ -74,13 +76,41 @@ class GoogleCalendarData(object): except ServerNotFoundError: _LOGGER.warning("Unable to connect to Google, using cached data") return False - params = dict(DEFAULT_GOOGLE_SEARCH_PARAMS) - params['timeMin'] = dt.now().isoformat('T') params['calendarId'] = self.calendar_id if self.search: params['q'] = self.search + return service, params + + async def async_get_events(self, hass, start_date, end_date): + """Get all events in a specific time frame.""" + service, params = await hass.async_add_job(self._prepare_query) + params['timeMin'] = start_date.isoformat('T') + params['timeMax'] = end_date.isoformat('T') + + # pylint: disable=no-member + events = await hass.async_add_job(service.events) + # pylint: enable=no-member + result = await hass.async_add_job(events.list(**params).execute) + + items = result.get('items', []) + event_list = [] + for item in items: + if (not self.ignore_availability + and 'transparency' in item.keys()): + if item['transparency'] == 'opaque': + event_list.append(item) + else: + event_list.append(item) + return event_list + + @Throttle(MIN_TIME_BETWEEN_UPDATES) + def update(self): + """Get the latest data.""" + service, params = self._prepare_query() + params['timeMin'] = dt.now().isoformat('T') + events = service.events() # pylint: disable=no-member result = events.list(**params).execute() diff --git a/homeassistant/components/calendar/todoist.py b/homeassistant/components/calendar/todoist.py index b70e44456db..71a6a17de10 100644 --- a/homeassistant/components/calendar/todoist.py +++ b/homeassistant/components/calendar/todoist.py @@ -257,6 +257,10 @@ class TodoistProjectDevice(CalendarEventDevice): super().cleanup() self._cal_data[ALL_TASKS] = [] + async def async_get_events(self, hass, start_date, end_date): + """Get all events in a specific time frame.""" + return await self.data.async_get_events(hass, start_date, end_date) + @property def device_state_attributes(self): """Return the device state attributes.""" @@ -485,6 +489,31 @@ class TodoistProjectData(object): continue return event + async def async_get_events(self, hass, start_date, end_date): + """Get all tasks in a specific time frame.""" + if self._id is None: + project_task_data = [ + task for task in self._api.state[TASKS] + if not self._project_id_whitelist or + task[PROJECT_ID] in self._project_id_whitelist] + else: + project_task_data = self._api.projects.get_data(self._id)[TASKS] + + events = [] + time_format = '%a %d %b %Y %H:%M:%S %z' + for task in project_task_data: + due_date = datetime.strptime(task['due_date_utc'], time_format) + if due_date > start_date and due_date < end_date: + event = { + 'uid': task['id'], + 'title': task['content'], + 'start': due_date.isoformat(), + 'end': due_date.isoformat(), + 'allDay': True, + } + events.append(event) + return events + @Throttle(MIN_TIME_BETWEEN_UPDATES) def update(self): """Get the latest data.""" diff --git a/tests/components/calendar/test_caldav.py b/tests/components/calendar/test_caldav.py index 11dd0cb9635..c5dadbc56ea 100644 --- a/tests/components/calendar/test_caldav.py +++ b/tests/components/calendar/test_caldav.py @@ -19,7 +19,7 @@ _LOGGER = logging.getLogger(__name__) DEVICE_DATA = { "name": "Private Calendar", - "device_id": "Private Calendar" + "device_id": "Private Calendar", } EVENTS = [ @@ -163,6 +163,7 @@ class TestComponentsWebDavCalendar(unittest.TestCase): def setUp(self): """Set up things to be run when tests are started.""" self.hass = get_test_home_assistant() + self.hass.http = Mock() self.calendar = _mock_calendar("Private") # pylint: disable=invalid-name @@ -255,7 +256,7 @@ class TestComponentsWebDavCalendar(unittest.TestCase): "start_time": "2017-11-27 17:00:00", "end_time": "2017-11-27 18:00:00", "location": "Hamburg", - "description": "Surprisingly rainy" + "description": "Surprisingly rainy", }) @patch('homeassistant.util.dt.now', return_value=_local_datetime(17, 30)) @@ -274,7 +275,7 @@ class TestComponentsWebDavCalendar(unittest.TestCase): "start_time": "2017-11-27 17:00:00", "end_time": "2017-11-27 18:00:00", "location": "Hamburg", - "description": "Surprisingly rainy" + "description": "Surprisingly rainy", }) @patch('homeassistant.util.dt.now', return_value=_local_datetime(17, 00)) @@ -293,7 +294,7 @@ class TestComponentsWebDavCalendar(unittest.TestCase): "start_time": "2017-11-27 16:30:00", "description": "Sunny day", "end_time": "2017-11-27 17:30:00", - "location": "San Francisco" + "location": "San Francisco", }) @patch('homeassistant.util.dt.now', return_value=_local_datetime(8, 30)) @@ -311,7 +312,7 @@ class TestComponentsWebDavCalendar(unittest.TestCase): "start_time": "2017-11-27 10:00:00", "end_time": "2017-11-27 11:00:00", "location": "Hamburg", - "description": "Surprisingly shiny" + "description": "Surprisingly shiny", }) @patch('homeassistant.util.dt.now', return_value=_local_datetime(12, 00)) @@ -332,7 +333,7 @@ class TestComponentsWebDavCalendar(unittest.TestCase): "start_time": "2017-11-27 17:00:00", "end_time": "2017-11-27 18:00:00", "location": "Hamburg", - "description": "Surprisingly rainy" + "description": "Surprisingly rainy", }) @patch('homeassistant.util.dt.now', return_value=_local_datetime(12, 00)) @@ -353,7 +354,7 @@ class TestComponentsWebDavCalendar(unittest.TestCase): "start_time": "2017-11-27 17:00:00", "end_time": "2017-11-27 18:00:00", "location": "Hamburg", - "description": "Surprisingly rainy" + "description": "Surprisingly rainy", }) @patch('homeassistant.util.dt.now', return_value=_local_datetime(20, 00)) @@ -395,5 +396,5 @@ class TestComponentsWebDavCalendar(unittest.TestCase): "start_time": "2017-11-27 00:00:00", "end_time": "2017-11-28 00:00:00", "location": "Hamburg", - "description": "What a beautiful day" + "description": "What a beautiful day", }) diff --git a/tests/components/calendar/test_demo.py b/tests/components/calendar/test_demo.py new file mode 100644 index 00000000000..50ac63121b1 --- /dev/null +++ b/tests/components/calendar/test_demo.py @@ -0,0 +1,24 @@ +"""The tests for the demo calendar component.""" +from datetime import timedelta + +from homeassistant.bootstrap import async_setup_component +import homeassistant.util.dt as dt_util + + +async def test_api_calendar_demo_view(hass, aiohttp_client): + """Test the calendar demo view.""" + await async_setup_component(hass, 'calendar', + {'calendar': {'platform': 'demo'}}) + client = await aiohttp_client(hass.http.app) + response = await client.get( + '/api/calendar/calendar_2') + assert response.status == 400 + start = dt_util.now() + end = start + timedelta(days=1) + response = await client.get( + '/api/calendar/calendar_1?start={}&end={}'.format(start.isoformat(), + end.isoformat())) + assert response.status == 200 + events = await response.json() + assert events[0]['summary'] == 'Future Event' + assert events[0]['title'] == 'Future Event' diff --git a/tests/components/calendar/test_google.py b/tests/components/calendar/test_google.py index 9f94ea9f44c..d176cd758b4 100644 --- a/tests/components/calendar/test_google.py +++ b/tests/components/calendar/test_google.py @@ -27,6 +27,7 @@ class TestComponentsGoogleCalendar(unittest.TestCase): def setUp(self): """Setup things to be run when tests are started.""" self.hass = get_test_home_assistant() + self.hass.http = Mock() # Set our timezone to CST/Regina so we can check calculations # This keeps UTC-6 all year round @@ -99,7 +100,7 @@ class TestComponentsGoogleCalendar(unittest.TestCase): 'start_time': '{} 00:00:00'.format(event['start']['date']), 'end_time': '{} 00:00:00'.format(event['end']['date']), 'location': event['location'], - 'description': event['description'] + 'description': event['description'], }) @patch('homeassistant.components.calendar.google.GoogleCalendarData') @@ -160,7 +161,7 @@ class TestComponentsGoogleCalendar(unittest.TestCase): (one_hour_from_now + dt_util.dt.timedelta(minutes=60)) .strftime(DATE_STR_FORMAT), 'location': '', - 'description': '' + 'description': '', }) @patch('homeassistant.components.calendar.google.GoogleCalendarData') @@ -222,7 +223,7 @@ class TestComponentsGoogleCalendar(unittest.TestCase): (middle_of_event + dt_util.dt.timedelta(minutes=60)) .strftime(DATE_STR_FORMAT), 'location': '', - 'description': '' + 'description': '', }) @patch('homeassistant.components.calendar.google.GoogleCalendarData') @@ -285,7 +286,7 @@ class TestComponentsGoogleCalendar(unittest.TestCase): (middle_of_event + dt_util.dt.timedelta(minutes=60)) .strftime(DATE_STR_FORMAT), 'location': '', - 'description': '' + 'description': '', }) @pytest.mark.skip @@ -352,7 +353,7 @@ class TestComponentsGoogleCalendar(unittest.TestCase): 'start_time': '{} 06:00:00'.format(event['start']['date']), 'end_time': '{} 06:00:00'.format(event['end']['date']), 'location': event['location'], - 'description': event['description'] + 'description': event['description'], }) @patch('homeassistant.components.calendar.google.GoogleCalendarData') @@ -419,7 +420,7 @@ class TestComponentsGoogleCalendar(unittest.TestCase): 'start_time': '{} 00:00:00'.format(event['start']['date']), 'end_time': '{} 00:00:00'.format(event['end']['date']), 'location': event['location'], - 'description': event['description'] + 'description': event['description'], }) @MockDependency("httplib2") From f744a29d9d950fcf6e60525fc31ad18f1a700159 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 15 Jun 2018 13:37:46 -0400 Subject: [PATCH 091/144] Add calendar panel, add tests (#14973) --- homeassistant/components/calendar/__init__.py | 46 +++++++++++++++---- homeassistant/components/calendar/demo.py | 4 +- tests/components/calendar/test_demo.py | 23 ---------- tests/components/calendar/test_init.py | 37 +++++++++++++++ 4 files changed, 75 insertions(+), 35 deletions(-) diff --git a/homeassistant/components/calendar/__init__.py b/homeassistant/components/calendar/__init__.py index f5e1d581891..65e5e33c7c1 100644 --- a/homeassistant/components/calendar/__init__.py +++ b/homeassistant/components/calendar/__init__.py @@ -4,7 +4,6 @@ Support for Google Calendar event device sensors. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/calendar/ """ -import asyncio import logging from datetime import timedelta import re @@ -34,15 +33,18 @@ ENTITY_ID_FORMAT = DOMAIN + '.{}' SCAN_INTERVAL = timedelta(seconds=60) -@asyncio.coroutine -def async_setup(hass, config): +async def async_setup(hass, config): """Track states and offer events for calendars.""" component = EntityComponent( _LOGGER, DOMAIN, hass, SCAN_INTERVAL, DOMAIN) + hass.http.register_view(CalendarListView(component)) hass.http.register_view(CalendarEventView(component)) - yield from component.async_setup(config) + await hass.components.frontend.async_register_built_in_panel( + 'calendar', 'calendar', 'hass:calendar') + + await component.async_setup(config) return True @@ -196,8 +198,8 @@ class CalendarEventDevice(Entity): class CalendarEventView(http.HomeAssistantView): """View to retrieve calendar content.""" - url = '/api/calendar/{entity_id}' - name = 'api:calendar' + url = '/api/calendars/{entity_id}' + name = 'api:calendars:calendar' def __init__(self, component): """Initialize calendar view.""" @@ -205,7 +207,7 @@ class CalendarEventView(http.HomeAssistantView): async def get(self, request, entity_id): """Return calendar events.""" - entity = self.component.get_entity('calendar.' + entity_id) + entity = self.component.get_entity(entity_id) start = request.query.get('start') end = request.query.get('end') if None in (start, end, entity): @@ -215,7 +217,31 @@ class CalendarEventView(http.HomeAssistantView): end_date = dt.parse_datetime(end) except (ValueError, AttributeError): return web.Response(status=400) - event_list = await entity.async_get_events(request.app['hass'], - start_date, - end_date) + event_list = await entity.async_get_events( + request.app['hass'], start_date, end_date) return self.json(event_list) + + +class CalendarListView(http.HomeAssistantView): + """View to retrieve calendar list.""" + + url = '/api/calendars' + name = "api:calendars" + + def __init__(self, component): + """Initialize calendar view.""" + self.component = component + + async def get(self, request): + """Retrieve calendar list.""" + get_state = request.app['hass'].states.get + calendar_list = [] + + for entity in self.component.entities: + state = get_state(entity.entity_id) + calendar_list.append({ + "name": state.name, + "entity_id": entity.entity_id, + }) + + return self.json(sorted(calendar_list, key=lambda x: x['name'])) diff --git a/homeassistant/components/calendar/demo.py b/homeassistant/components/calendar/demo.py index 5ddd9fe8e3d..53129d3316c 100644 --- a/homeassistant/components/calendar/demo.py +++ b/homeassistant/components/calendar/demo.py @@ -17,12 +17,12 @@ def setup_platform(hass, config, add_devices, discovery_info=None): calendar_data_current = DemoGoogleCalendarDataCurrent() add_devices([ DemoGoogleCalendar(hass, calendar_data_future, { - CONF_NAME: 'Future Event', + CONF_NAME: 'Calendar 1', CONF_DEVICE_ID: 'calendar_1', }), DemoGoogleCalendar(hass, calendar_data_current, { - CONF_NAME: 'Current Event', + CONF_NAME: 'Calendar 2', CONF_DEVICE_ID: 'calendar_2', }), ]) diff --git a/tests/components/calendar/test_demo.py b/tests/components/calendar/test_demo.py index 50ac63121b1..09c6a06a54e 100644 --- a/tests/components/calendar/test_demo.py +++ b/tests/components/calendar/test_demo.py @@ -1,24 +1 @@ """The tests for the demo calendar component.""" -from datetime import timedelta - -from homeassistant.bootstrap import async_setup_component -import homeassistant.util.dt as dt_util - - -async def test_api_calendar_demo_view(hass, aiohttp_client): - """Test the calendar demo view.""" - await async_setup_component(hass, 'calendar', - {'calendar': {'platform': 'demo'}}) - client = await aiohttp_client(hass.http.app) - response = await client.get( - '/api/calendar/calendar_2') - assert response.status == 400 - start = dt_util.now() - end = start + timedelta(days=1) - response = await client.get( - '/api/calendar/calendar_1?start={}&end={}'.format(start.isoformat(), - end.isoformat())) - assert response.status == 200 - events = await response.json() - assert events[0]['summary'] == 'Future Event' - assert events[0]['title'] == 'Future Event' diff --git a/tests/components/calendar/test_init.py b/tests/components/calendar/test_init.py index 164c3f57f52..a5f6a751b46 100644 --- a/tests/components/calendar/test_init.py +++ b/tests/components/calendar/test_init.py @@ -1 +1,38 @@ """The tests for the calendar component.""" +from datetime import timedelta + +from homeassistant.bootstrap import async_setup_component +import homeassistant.util.dt as dt_util + + +async def test_events_http_api(hass, aiohttp_client): + """Test the calendar demo view.""" + await async_setup_component(hass, 'calendar', + {'calendar': {'platform': 'demo'}}) + client = await aiohttp_client(hass.http.app) + response = await client.get( + '/api/calendars/calendar.calendar_2') + assert response.status == 400 + start = dt_util.now() + end = start + timedelta(days=1) + response = await client.get( + '/api/calendars/calendar.calendar_1?start={}&end={}'.format( + start.isoformat(), end.isoformat())) + assert response.status == 200 + events = await response.json() + assert events[0]['summary'] == 'Future Event' + assert events[0]['title'] == 'Future Event' + + +async def test_calendars_http_api(hass, aiohttp_client): + """Test the calendar demo view.""" + await async_setup_component(hass, 'calendar', + {'calendar': {'platform': 'demo'}}) + client = await aiohttp_client(hass.http.app) + response = await client.get('/api/calendars') + assert response.status == 200 + data = await response.json() + assert data == [ + {'entity_id': 'calendar.calendar_1', 'name': 'Calendar 1'}, + {'entity_id': 'calendar.calendar_2', 'name': 'Calendar 2'} + ] From 47a344f3a1e1d5f1745a32e5c777ef3e88b12e97 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 15 Jun 2018 13:46:31 -0400 Subject: [PATCH 092/144] Bump frontend to 20180615.0 --- homeassistant/components/frontend/__init__.py | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/frontend/__init__.py b/homeassistant/components/frontend/__init__.py index 303c4846701..0c425ccd3b1 100644 --- a/homeassistant/components/frontend/__init__.py +++ b/homeassistant/components/frontend/__init__.py @@ -24,7 +24,7 @@ from homeassistant.core import callback from homeassistant.helpers.translation import async_get_translations from homeassistant.loader import bind_hass -REQUIREMENTS = ['home-assistant-frontend==20180613.0'] +REQUIREMENTS = ['home-assistant-frontend==20180615.0'] DOMAIN = 'frontend' DEPENDENCIES = ['api', 'websocket_api', 'http', 'system_log'] diff --git a/requirements_all.txt b/requirements_all.txt index 79db358942d..90cd28d61fc 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -404,7 +404,7 @@ hipnotify==1.0.8 holidays==0.9.5 # homeassistant.components.frontend -home-assistant-frontend==20180613.0 +home-assistant-frontend==20180615.0 # homeassistant.components.homekit_controller # homekit==0.6 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 34928b4f111..02f079dd9a6 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -81,7 +81,7 @@ hbmqtt==0.9.2 holidays==0.9.5 # homeassistant.components.frontend -home-assistant-frontend==20180613.0 +home-assistant-frontend==20180615.0 # homeassistant.components.influxdb # homeassistant.components.sensor.influxdb From c9174708365877ccfb00a79df4f1ca848421ee5e Mon Sep 17 00:00:00 2001 From: Sriram Vaidyanathan Date: Fri, 15 Jun 2018 23:57:52 +0530 Subject: [PATCH 093/144] Xiaomi Cameras - multiple models (#14244) * Added support for Xiaofang Camera * Added entry for Xiaofang 1080p camera * Code fix * Minor comment fix * Updated coveragerc for Xiaomi cameras * Added Xiaomi Camera Added Xiaomi Camera to accommodate multiple models like Yi, Xiaofang, etc. * Minor code fix * Minor code fix * Added model property * Update xiaomi.py * Minor code fix * Update xiaomi.py * Update xiaomi.py * Minor code fix * Package requirement fix due to Version conflict * To fix conflicts * Update package_constraints.txt * Minor fix * Update xiaomi.py * Update xiaomi.py Changes made per comment * Update xiaomi.py * Don't update on add. --- .coveragerc | 1 + homeassistant/components/camera/xiaomi.py | 166 ++++++++++++++++++++++ 2 files changed, 167 insertions(+) create mode 100644 homeassistant/components/camera/xiaomi.py diff --git a/.coveragerc b/.coveragerc index e7d6d2a404a..d059d62b5f3 100644 --- a/.coveragerc +++ b/.coveragerc @@ -376,6 +376,7 @@ omit = homeassistant/components/camera/rpi_camera.py homeassistant/components/camera/synology.py homeassistant/components/camera/xeoma.py + homeassistant/components/camera/xiaomi.py homeassistant/components/camera/yi.py homeassistant/components/climate/econet.py homeassistant/components/climate/ephember.py diff --git a/homeassistant/components/camera/xiaomi.py b/homeassistant/components/camera/xiaomi.py new file mode 100644 index 00000000000..c18a3649e7b --- /dev/null +++ b/homeassistant/components/camera/xiaomi.py @@ -0,0 +1,166 @@ +""" +This component provides support for Xiaomi Cameras. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/camera.xiaomi/ +""" +import asyncio +import logging + +import voluptuous as vol + +from homeassistant.components.camera import Camera, PLATFORM_SCHEMA +from homeassistant.components.ffmpeg import DATA_FFMPEG +from homeassistant.const import (CONF_HOST, CONF_NAME, CONF_PATH, + CONF_PASSWORD, CONF_PORT, CONF_USERNAME) +from homeassistant.helpers import config_validation as cv +from homeassistant.helpers.aiohttp_client import async_aiohttp_proxy_stream + +DEPENDENCIES = ['ffmpeg'] +_LOGGER = logging.getLogger(__name__) + +DEFAULT_BRAND = 'Xiaomi Home Camera' +DEFAULT_PATH = '/media/mmcblk0p1/record' +DEFAULT_PORT = 21 +DEFAULT_USERNAME = 'root' + +CONF_FFMPEG_ARGUMENTS = 'ffmpeg_arguments' +CONF_MODEL = 'model' + +MODEL_YI = 'yi' +MODEL_XIAOFANG = 'xiaofang' + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Required(CONF_NAME): cv.string, + vol.Required(CONF_HOST): cv.string, + vol.Required(CONF_MODEL): vol.Any(MODEL_YI, + MODEL_XIAOFANG), + vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.string, + vol.Optional(CONF_PATH, default=DEFAULT_PATH): cv.string, + vol.Optional(CONF_USERNAME, default=DEFAULT_USERNAME): cv.string, + vol.Required(CONF_PASSWORD): cv.string, + vol.Optional(CONF_FFMPEG_ARGUMENTS): cv.string +}) + + +async def async_setup_platform(hass, + config, + async_add_devices, + discovery_info=None): + """Set up a Xiaomi Camera.""" + _LOGGER.debug('Received configuration for model %s', config[CONF_MODEL]) + async_add_devices([XiaomiCamera(hass, config)]) + + +class XiaomiCamera(Camera): + """Define an implementation of a Xiaomi Camera.""" + + def __init__(self, hass, config): + """Initialize.""" + super().__init__() + self._extra_arguments = config.get(CONF_FFMPEG_ARGUMENTS) + self._last_image = None + self._last_url = None + self._manager = hass.data[DATA_FFMPEG] + self._name = config[CONF_NAME] + self.host = config[CONF_HOST] + self._model = config[CONF_MODEL] + self.port = config[CONF_PORT] + self.path = config[CONF_PATH] + self.user = config[CONF_USERNAME] + self.passwd = config[CONF_PASSWORD] + + @property + def name(self): + """Return the name of this camera.""" + return self._name + + @property + def brand(self): + """Return the camera brand.""" + return DEFAULT_BRAND + + @property + def model(self): + """Return the camera model.""" + return self._model + + def get_latest_video_url(self): + """Retrieve the latest video file from the Xiaomi Camera FTP server.""" + from ftplib import FTP, error_perm + + ftp = FTP(self.host) + try: + ftp.login(self.user, self.passwd) + except error_perm as exc: + _LOGGER.error('Camera login failed: %s', exc) + return False + + try: + ftp.cwd(self.path) + except error_perm as exc: + _LOGGER.error('Unable to find path: %s - %s', self.path, exc) + return False + + dirs = [d for d in ftp.nlst() if '.' not in d] + if not dirs: + if self._model == MODEL_YI: + _LOGGER.warning("There don't appear to be any uploaded videos") + return False + elif self._model == MODEL_XIAOFANG: + _LOGGER.warning("There don't appear to be any folders") + return False + + first_dir = dirs[-1] + try: + ftp.cwd(first_dir) + except error_perm as exc: + _LOGGER.error('Unable to find path: %s - %s', first_dir, exc) + return False + + dirs = [d for d in ftp.nlst() if '.' not in d] + if not dirs: + _LOGGER.warning("There don't appear to be any uploaded videos") + return False + + latest_dir = dirs[-1] + ftp.cwd(latest_dir) + videos = [v for v in ftp.nlst() if '.tmp' not in v] + if not videos: + _LOGGER.info('Video folder "%s" is empty; delaying', latest_dir) + return False + + if self._model == MODEL_XIAOFANG: + video = videos[-2] + else: + video = videos[-1] + + return 'ftp://{0}:{1}@{2}:{3}{4}/{5}'.format( + self.user, self.passwd, self.host, self.port, ftp.pwd(), video) + + async def async_camera_image(self): + """Return a still image response from the camera.""" + from haffmpeg import ImageFrame, IMAGE_JPEG + + url = await self.hass.async_add_job(self.get_latest_video_url) + if url != self._last_url: + ffmpeg = ImageFrame(self._manager.binary, loop=self.hass.loop) + self._last_image = await asyncio.shield(ffmpeg.get_image( + url, output_format=IMAGE_JPEG, + extra_cmd=self._extra_arguments), loop=self.hass.loop) + self._last_url = url + + return self._last_image + + async def handle_async_mjpeg_stream(self, request): + """Generate an HTTP MJPEG stream from the camera.""" + from haffmpeg import CameraMjpeg + + stream = CameraMjpeg(self._manager.binary, loop=self.hass.loop) + await stream.open_camera( + self._last_url, extra_cmd=self._extra_arguments) + + await async_aiohttp_proxy_stream( + self.hass, request, stream, + 'multipart/x-mixed-replace;boundary=ffserver') + await stream.close() From 940577e105bb855fa6ac6d15558f9c57471612ea Mon Sep 17 00:00:00 2001 From: Albert Lee Date: Fri, 15 Jun 2018 14:30:35 -0400 Subject: [PATCH 094/144] Fix binary_sensor.skybell state update when there are no events (#14927) --- homeassistant/components/binary_sensor/skybell.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/binary_sensor/skybell.py b/homeassistant/components/binary_sensor/skybell.py index 734f8e03375..44cad11e3f0 100644 --- a/homeassistant/components/binary_sensor/skybell.py +++ b/homeassistant/components/binary_sensor/skybell.py @@ -94,4 +94,4 @@ class SkybellBinarySensor(SkybellDevice, BinarySensorDevice): self._state = bool(event and event.get('id') != self._event.get('id')) - self._event = event + self._event = event or {} From ac13a2736b26abcafc80f348edba53ff7dc8749d Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Fri, 15 Jun 2018 20:31:22 +0200 Subject: [PATCH 095/144] Deconz make groups configurable (#14704) * Make groups configurable * Config flow and tests in place * Fix too long line --- .../components/deconz/.translations/en.json | 3 ++- homeassistant/components/deconz/config_flow.py | 9 ++++++++- homeassistant/components/deconz/const.py | 1 + homeassistant/components/deconz/strings.json | 3 ++- homeassistant/components/light/deconz.py | 6 +++++- tests/components/deconz/test_config_flow.py | 18 ++++++++++++------ tests/components/light/test_deconz.py | 18 ++++++++++++++++-- 7 files changed, 46 insertions(+), 12 deletions(-) diff --git a/homeassistant/components/deconz/.translations/en.json b/homeassistant/components/deconz/.translations/en.json index a2f90e49e3a..465c6c1e0e8 100644 --- a/homeassistant/components/deconz/.translations/en.json +++ b/homeassistant/components/deconz/.translations/en.json @@ -23,7 +23,8 @@ "options": { "title": "Extra configuration options for deCONZ", "data": { - "allow_clip_sensor": "Allow importing virtual sensors" + "allow_clip_sensor": "Allow importing virtual sensors", + "allow_deconz_groups": "Allow importing deCONZ groups" } } }, diff --git a/homeassistant/components/deconz/config_flow.py b/homeassistant/components/deconz/config_flow.py index cb7c3aad7fd..27fb6987f8c 100644 --- a/homeassistant/components/deconz/config_flow.py +++ b/homeassistant/components/deconz/config_flow.py @@ -8,7 +8,9 @@ from homeassistant.const import CONF_API_KEY, CONF_HOST, CONF_PORT from homeassistant.helpers import aiohttp_client from homeassistant.util.json import load_json -from .const import CONF_ALLOW_CLIP_SENSOR, CONFIG_FILE, DOMAIN +from .const import ( + CONF_ALLOW_DECONZ_GROUPS, CONF_ALLOW_CLIP_SENSOR, CONFIG_FILE, DOMAIN) + CONF_BRIDGEID = 'bridgeid' @@ -94,12 +96,15 @@ class DeconzFlowHandler(data_entry_flow.FlowHandler): """Extra options for deCONZ. CONF_CLIP_SENSOR -- Allow user to choose if they want clip sensors. + CONF_DECONZ_GROUPS -- Allow user to choose if they want deCONZ groups. """ from pydeconz.utils import async_get_bridgeid if user_input is not None: self.deconz_config[CONF_ALLOW_CLIP_SENSOR] = \ user_input[CONF_ALLOW_CLIP_SENSOR] + self.deconz_config[CONF_ALLOW_DECONZ_GROUPS] = \ + user_input[CONF_ALLOW_DECONZ_GROUPS] if CONF_BRIDGEID not in self.deconz_config: session = aiohttp_client.async_get_clientsession(self.hass) @@ -115,6 +120,7 @@ class DeconzFlowHandler(data_entry_flow.FlowHandler): step_id='options', data_schema=vol.Schema({ vol.Optional(CONF_ALLOW_CLIP_SENSOR): bool, + vol.Optional(CONF_ALLOW_DECONZ_GROUPS): bool, }), ) @@ -158,6 +164,7 @@ class DeconzFlowHandler(data_entry_flow.FlowHandler): return await self.async_step_link() self.deconz_config[CONF_ALLOW_CLIP_SENSOR] = True + self.deconz_config[CONF_ALLOW_DECONZ_GROUPS] = True return self.async_create_entry( title='deCONZ-' + self.deconz_config[CONF_BRIDGEID], data=self.deconz_config diff --git a/homeassistant/components/deconz/const.py b/homeassistant/components/deconz/const.py index 43f3c6441da..f7aa4c7a430 100644 --- a/homeassistant/components/deconz/const.py +++ b/homeassistant/components/deconz/const.py @@ -10,3 +10,4 @@ DATA_DECONZ_ID = 'deconz_entities' DATA_DECONZ_UNSUB = 'deconz_dispatchers' CONF_ALLOW_CLIP_SENSOR = 'allow_clip_sensor' +CONF_ALLOW_DECONZ_GROUPS = 'allow_deconz_groups' diff --git a/homeassistant/components/deconz/strings.json b/homeassistant/components/deconz/strings.json index cabe58694d2..09549a300a0 100644 --- a/homeassistant/components/deconz/strings.json +++ b/homeassistant/components/deconz/strings.json @@ -16,7 +16,8 @@ "options": { "title": "Extra configuration options for deCONZ", "data":{ - "allow_clip_sensor": "Allow importing virtual sensors" + "allow_clip_sensor": "Allow importing virtual sensors", + "allow_deconz_groups": "Allow importing deCONZ groups" } } }, diff --git a/homeassistant/components/light/deconz.py b/homeassistant/components/light/deconz.py index 916e60c00b1..a4593a72617 100644 --- a/homeassistant/components/light/deconz.py +++ b/homeassistant/components/light/deconz.py @@ -6,6 +6,7 @@ https://home-assistant.io/components/light.deconz/ """ from homeassistant.components.deconz import ( DOMAIN as DATA_DECONZ, DATA_DECONZ_ID, DATA_DECONZ_UNSUB) +from homeassistant.components.deconz.const import CONF_ALLOW_DECONZ_GROUPS from homeassistant.components.light import ( ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, ATTR_EFFECT, ATTR_FLASH, ATTR_HS_COLOR, ATTR_TRANSITION, EFFECT_COLORLOOP, FLASH_LONG, FLASH_SHORT, @@ -33,6 +34,7 @@ async def async_setup_entry(hass, config_entry, async_add_devices): for light in lights: entities.append(DeconzLight(light)) async_add_devices(entities, True) + hass.data[DATA_DECONZ_UNSUB].append( async_dispatcher_connect(hass, 'deconz_new_light', async_add_light)) @@ -40,10 +42,12 @@ async def async_setup_entry(hass, config_entry, async_add_devices): def async_add_group(groups): """Add group from deCONZ.""" entities = [] + allow_group = config_entry.data.get(CONF_ALLOW_DECONZ_GROUPS, True) for group in groups: - if group.lights: + if group.lights and allow_group: entities.append(DeconzLight(group)) async_add_devices(entities, True) + hass.data[DATA_DECONZ_UNSUB].append( async_dispatcher_connect(hass, 'deconz_new_group', async_add_group)) diff --git a/tests/components/deconz/test_config_flow.py b/tests/components/deconz/test_config_flow.py index df3310f3d6f..111cfbe9697 100644 --- a/tests/components/deconz/test_config_flow.py +++ b/tests/components/deconz/test_config_flow.py @@ -23,7 +23,7 @@ async def test_flow_works(hass, aioclient_mock): await flow.async_step_init() await flow.async_step_link(user_input={}) result = await flow.async_step_options( - user_input={'allow_clip_sensor': True}) + user_input={'allow_clip_sensor': True, 'allow_deconz_groups': True}) assert result['type'] == 'create_entry' assert result['title'] == 'deCONZ-id' @@ -32,7 +32,8 @@ async def test_flow_works(hass, aioclient_mock): 'host': '1.2.3.4', 'port': 80, 'api_key': '1234567890ABCDEF', - 'allow_clip_sensor': True + 'allow_clip_sensor': True, + 'allow_deconz_groups': True } @@ -149,6 +150,7 @@ async def test_bridge_discovery_config_file(hass): 'port': 80, 'serial': 'id' }) + assert result['type'] == 'create_entry' assert result['title'] == 'deCONZ-id' assert result['data'] == { @@ -156,7 +158,8 @@ async def test_bridge_discovery_config_file(hass): 'host': '1.2.3.4', 'port': 80, 'api_key': '1234567890ABCDEF', - 'allow_clip_sensor': True + 'allow_clip_sensor': True, + 'allow_deconz_groups': True } @@ -217,6 +220,7 @@ async def test_import_with_api_key(hass): 'port': 80, 'api_key': '1234567890ABCDEF' }) + assert result['type'] == 'create_entry' assert result['title'] == 'deCONZ-id' assert result['data'] == { @@ -224,7 +228,8 @@ async def test_import_with_api_key(hass): 'host': '1.2.3.4', 'port': 80, 'api_key': '1234567890ABCDEF', - 'allow_clip_sensor': True + 'allow_clip_sensor': True, + 'allow_deconz_groups': True } @@ -238,7 +243,7 @@ async def test_options(hass, aioclient_mock): 'port': 80, 'api_key': '1234567890ABCDEF'} result = await flow.async_step_options( - user_input={'allow_clip_sensor': False}) + user_input={'allow_clip_sensor': False, 'allow_deconz_groups': False}) assert result['type'] == 'create_entry' assert result['title'] == 'deCONZ-id' assert result['data'] == { @@ -246,5 +251,6 @@ async def test_options(hass, aioclient_mock): 'host': '1.2.3.4', 'port': 80, 'api_key': '1234567890ABCDEF', - 'allow_clip_sensor': False + 'allow_clip_sensor': False, + 'allow_deconz_groups': False } diff --git a/tests/components/light/test_deconz.py b/tests/components/light/test_deconz.py index 2608d77ce2a..d7d609f820e 100644 --- a/tests/components/light/test_deconz.py +++ b/tests/components/light/test_deconz.py @@ -38,7 +38,7 @@ GROUP = { } -async def setup_bridge(hass, data): +async def setup_bridge(hass, data, allow_deconz_groups=True): """Load the deCONZ light platform.""" from pydeconz import DeconzSession loop = Mock() @@ -53,7 +53,9 @@ async def setup_bridge(hass, data): hass.data[deconz.DATA_DECONZ_UNSUB] = [] hass.data[deconz.DATA_DECONZ_ID] = {} config_entry = config_entries.ConfigEntry( - 1, deconz.DOMAIN, 'Mock Title', {'host': 'mock-host'}, 'test') + 1, deconz.DOMAIN, 'Mock Title', + {'host': 'mock-host', 'allow_deconz_groups': allow_deconz_groups}, + 'test') await hass.config_entries.async_forward_entry_setup(config_entry, 'light') # To flush out the service call to update the group await hass.async_block_till_done() @@ -98,3 +100,15 @@ async def test_add_new_group(hass): async_dispatcher_send(hass, 'deconz_new_group', [group]) await hass.async_block_till_done() assert "light.name" in hass.data[deconz.DATA_DECONZ_ID] + + +async def test_do_not_add_deconz_groups(hass): + """Test that clip sensors can be ignored.""" + data = {} + await setup_bridge(hass, data, allow_deconz_groups=False) + group = Mock() + group.name = 'name' + group.register_async_callback = Mock() + async_dispatcher_send(hass, 'deconz_new_group', [group]) + await hass.async_block_till_done() + assert len(hass.data[deconz.DATA_DECONZ_ID]) == 0 From 8a777f6e7865b6d383663774fc9a05ac56b0c163 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 15 Jun 2018 15:19:58 -0400 Subject: [PATCH 096/144] Show notification when user configures Nest client_id/secret (#14970) * Show notification when user configures Nest client_id/secret * Lint --- homeassistant/components/hue/__init__.py | 3 +- homeassistant/components/nest/__init__.py | 12 +++-- homeassistant/components/nest/config_flow.py | 10 ++++- homeassistant/config_entries.py | 5 ++- homeassistant/data_entry_flow.py | 1 + tests/components/nest/test_config_flow.py | 46 +++++++++++++++++++- 6 files changed, 65 insertions(+), 12 deletions(-) diff --git a/homeassistant/components/hue/__init__.py b/homeassistant/components/hue/__init__.py index 251d8cba095..dbd86ef31f3 100644 --- a/homeassistant/components/hue/__init__.py +++ b/homeassistant/components/hue/__init__.py @@ -9,6 +9,7 @@ import logging import voluptuous as vol +from homeassistant import data_entry_flow from homeassistant.const import CONF_FILENAME, CONF_HOST from homeassistant.helpers import aiohttp_client, config_validation as cv @@ -107,7 +108,7 @@ async def async_setup(hass, config): # deadlock: creating a config entry will set up the component but the # setup would block till the entry is created! hass.async_add_job(hass.config_entries.flow.async_init( - DOMAIN, source='import', data={ + DOMAIN, source=data_entry_flow.SOURCE_IMPORT, data={ 'host': bridge_conf[CONF_HOST], 'path': bridge_conf[CONF_FILENAME], } diff --git a/homeassistant/components/nest/__init__.py b/homeassistant/components/nest/__init__.py index 19d65061a89..bd74897371a 100644 --- a/homeassistant/components/nest/__init__.py +++ b/homeassistant/components/nest/__init__.py @@ -6,7 +6,6 @@ https://home-assistant.io/components/nest/ """ from concurrent.futures import ThreadPoolExecutor import logging -import os.path import socket from datetime import datetime, timedelta @@ -102,12 +101,11 @@ async def async_setup(hass, config): filename = config.get(CONF_FILENAME, NEST_CONFIG_FILE) access_token_cache_file = hass.config.path(filename) - if await hass.async_add_job(os.path.isfile, access_token_cache_file): - hass.async_add_job(hass.config_entries.flow.async_init( - DOMAIN, source='import', data={ - 'nest_conf_path': access_token_cache_file, - } - )) + hass.async_add_job(hass.config_entries.flow.async_init( + DOMAIN, source='import', data={ + 'nest_conf_path': access_token_cache_file, + } + )) # Store config to be used during entry setup hass.data[DATA_NEST_CONFIG] = conf diff --git a/homeassistant/components/nest/config_flow.py b/homeassistant/components/nest/config_flow.py index ee83598235c..b5c095f34b8 100644 --- a/homeassistant/components/nest/config_flow.py +++ b/homeassistant/components/nest/config_flow.py @@ -2,6 +2,7 @@ import asyncio from collections import OrderedDict import logging +import os import async_timeout import voluptuous as vol @@ -135,9 +136,14 @@ class NestFlowHandler(data_entry_flow.FlowHandler): if self.hass.config_entries.async_entries(DOMAIN): return self.async_abort(reason='already_setup') + config_path = info['nest_conf_path'] + + if not await self.hass.async_add_job(os.path.isfile, config_path): + self.flow_impl = DOMAIN + return await self.async_step_link() + flow = self.hass.data[DATA_FLOW_IMPL][DOMAIN] - tokens = await self.hass.async_add_job( - load_json, info['nest_conf_path']) + tokens = await self.hass.async_add_job(load_json, config_path) return self._entry_from_tokens( 'Nest (import from configuration.yaml)', flow, tokens) diff --git a/homeassistant/config_entries.py b/homeassistant/config_entries.py index 504c0850a93..4fbbbb77b79 100644 --- a/homeassistant/config_entries.py +++ b/homeassistant/config_entries.py @@ -146,7 +146,10 @@ ENTRY_STATE_NOT_LOADED = 'not_loaded' ENTRY_STATE_FAILED_UNLOAD = 'failed_unload' DISCOVERY_NOTIFICATION_ID = 'config_entry_discovery' -DISCOVERY_SOURCES = (data_entry_flow.SOURCE_DISCOVERY,) +DISCOVERY_SOURCES = ( + data_entry_flow.SOURCE_DISCOVERY, + data_entry_flow.SOURCE_IMPORT, +) class ConfigEntry: diff --git a/homeassistant/data_entry_flow.py b/homeassistant/data_entry_flow.py index 3b0f264fd40..e51ba4d9718 100644 --- a/homeassistant/data_entry_flow.py +++ b/homeassistant/data_entry_flow.py @@ -9,6 +9,7 @@ _LOGGER = logging.getLogger(__name__) SOURCE_USER = 'user' SOURCE_DISCOVERY = 'discovery' +SOURCE_IMPORT = 'import' RESULT_TYPE_FORM = 'form' RESULT_TYPE_CREATE_ENTRY = 'create_entry' diff --git a/tests/components/nest/test_config_flow.py b/tests/components/nest/test_config_flow.py index 9692d5ce129..e80d18a9862 100644 --- a/tests/components/nest/test_config_flow.py +++ b/tests/components/nest/test_config_flow.py @@ -3,7 +3,8 @@ import asyncio from unittest.mock import Mock, patch from homeassistant import data_entry_flow -from homeassistant.components.nest import config_flow +from homeassistant.setup import async_setup_component +from homeassistant.components.nest import config_flow, DOMAIN from tests.common import mock_coro @@ -172,3 +173,46 @@ async def test_verify_code_exception(hass): assert result['type'] == data_entry_flow.RESULT_TYPE_FORM assert result['step_id'] == 'link' assert result['errors'] == {'code': 'internal_error'} + + +async def test_step_import(hass): + """Test that we trigger import when configuring with client.""" + with patch('os.path.isfile', return_value=False): + assert await async_setup_component(hass, DOMAIN, { + DOMAIN: { + 'client_id': 'bla', + 'client_secret': 'bla', + }, + }) + await hass.async_block_till_done() + + flow = hass.config_entries.flow.async_progress()[0] + result = await hass.config_entries.flow.async_configure(flow['flow_id']) + + assert result['type'] == data_entry_flow.RESULT_TYPE_FORM + assert result['step_id'] == 'link' + + +async def test_step_import_with_token_cache(hass): + """Test that we import existing token cache.""" + with patch('os.path.isfile', return_value=True), \ + patch('homeassistant.components.nest.config_flow.load_json', + return_value={'access_token': 'yo'}), \ + patch('homeassistant.components.nest.async_setup_entry', + return_value=mock_coro(True)): + assert await async_setup_component(hass, DOMAIN, { + DOMAIN: { + 'client_id': 'bla', + 'client_secret': 'bla', + }, + }) + await hass.async_block_till_done() + + entry = hass.config_entries.async_entries(DOMAIN)[0] + + assert entry.data == { + 'impl_domain': 'nest', + 'tokens': { + 'access_token': 'yo' + } + } From 9efa31ef9f392bdc9f8831b6602b245acab0a0de Mon Sep 17 00:00:00 2001 From: John Mihalic Date: Fri, 15 Jun 2018 15:24:09 -0400 Subject: [PATCH 097/144] Eight Sleep add REM type, Update async syntax, Catch API quirks (#14937) --- .../components/binary_sensor/eight_sleep.py | 8 +-- homeassistant/components/eight_sleep.py | 38 +++++------ .../components/sensor/eight_sleep.py | 66 ++++++++++++------- requirements_all.txt | 2 +- 4 files changed, 60 insertions(+), 54 deletions(-) diff --git a/homeassistant/components/binary_sensor/eight_sleep.py b/homeassistant/components/binary_sensor/eight_sleep.py index a6d4476f047..40ca491e1f3 100644 --- a/homeassistant/components/binary_sensor/eight_sleep.py +++ b/homeassistant/components/binary_sensor/eight_sleep.py @@ -5,7 +5,6 @@ For more details about this platform, please refer to the documentation at https://home-assistant.io/components/binary_sensor.eight_sleep/ """ import logging -import asyncio from homeassistant.components.binary_sensor import BinarySensorDevice from homeassistant.components.eight_sleep import ( @@ -16,8 +15,8 @@ _LOGGER = logging.getLogger(__name__) DEPENDENCIES = ['eight_sleep'] -@asyncio.coroutine -def async_setup_platform(hass, config, async_add_devices, discovery_info=None): +async def async_setup_platform(hass, config, async_add_devices, + discovery_info=None): """Set up the eight sleep binary sensor.""" if discovery_info is None: return @@ -63,7 +62,6 @@ class EightHeatSensor(EightSleepHeatEntity, BinarySensorDevice): """Return true if the binary sensor is on.""" return self._state - @asyncio.coroutine - def async_update(self): + async def async_update(self): """Retrieve latest state.""" self._state = self._usrobj.bed_presence diff --git a/homeassistant/components/eight_sleep.py b/homeassistant/components/eight_sleep.py index 3478d5cd08e..704eab1846b 100644 --- a/homeassistant/components/eight_sleep.py +++ b/homeassistant/components/eight_sleep.py @@ -4,7 +4,6 @@ Support for Eight smart mattress covers and mattresses. For more details about this component, please refer to the documentation at https://home-assistant.io/components/eight_sleep/ """ -import asyncio import logging from datetime import timedelta @@ -22,7 +21,7 @@ from homeassistant.helpers.entity import Entity from homeassistant.helpers.event import async_track_point_in_utc_time from homeassistant.util.dt import utcnow -REQUIREMENTS = ['pyeight==0.0.8'] +REQUIREMENTS = ['pyeight==0.0.9'] _LOGGER = logging.getLogger(__name__) @@ -86,8 +85,7 @@ CONFIG_SCHEMA = vol.Schema({ }, extra=vol.ALLOW_EXTRA) -@asyncio.coroutine -def async_setup(hass, config): +async def async_setup(hass, config): """Set up the Eight Sleep component.""" from pyeight.eight import EightSleep @@ -107,31 +105,29 @@ def async_setup(hass, config): hass.data[DATA_EIGHT] = eight # Authenticate, build sensors - success = yield from eight.start() + success = await eight.start() if not success: # Authentication failed, cannot continue return False - @asyncio.coroutine - def async_update_heat_data(now): + async def async_update_heat_data(now): """Update heat data from eight in HEAT_SCAN_INTERVAL.""" - yield from eight.update_device_data() + await eight.update_device_data() async_dispatcher_send(hass, SIGNAL_UPDATE_HEAT) async_track_point_in_utc_time( hass, async_update_heat_data, utcnow() + HEAT_SCAN_INTERVAL) - @asyncio.coroutine - def async_update_user_data(now): + async def async_update_user_data(now): """Update user data from eight in USER_SCAN_INTERVAL.""" - yield from eight.update_user_data() + await eight.update_user_data() async_dispatcher_send(hass, SIGNAL_UPDATE_USER) async_track_point_in_utc_time( hass, async_update_user_data, utcnow() + USER_SCAN_INTERVAL) - yield from async_update_heat_data(None) - yield from async_update_user_data(None) + await async_update_heat_data(None) + await async_update_user_data(None) # Load sub components sensors = [] @@ -157,8 +153,7 @@ def async_setup(hass, config): CONF_BINARY_SENSORS: binary_sensors, }, config)) - @asyncio.coroutine - def async_service_handler(service): + async def async_service_handler(service): """Handle eight sleep service calls.""" params = service.data.copy() @@ -170,7 +165,7 @@ def async_setup(hass, config): side = sens.split('_')[1] userid = eight.fetch_userid(side) usrobj = eight.users[userid] - yield from usrobj.set_heating_level(target, duration) + await usrobj.set_heating_level(target, duration) async_dispatcher_send(hass, SIGNAL_UPDATE_HEAT) @@ -179,10 +174,9 @@ def async_setup(hass, config): DOMAIN, SERVICE_HEAT_SET, async_service_handler, schema=SERVICE_EIGHT_SCHEMA) - @asyncio.coroutine - def stop_eight(event): + async def stop_eight(event): """Handle stopping eight api session.""" - yield from eight.stop() + await eight.stop() hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, stop_eight) @@ -196,8 +190,7 @@ class EightSleepUserEntity(Entity): """Initialize the data object.""" self._eight = eight - @asyncio.coroutine - def async_added_to_hass(self): + async def async_added_to_hass(self): """Register update dispatcher.""" @callback def async_eight_user_update(): @@ -220,8 +213,7 @@ class EightSleepHeatEntity(Entity): """Initialize the data object.""" self._eight = eight - @asyncio.coroutine - def async_added_to_hass(self): + async def async_added_to_hass(self): """Register update dispatcher.""" @callback def async_eight_heat_update(): diff --git a/homeassistant/components/sensor/eight_sleep.py b/homeassistant/components/sensor/eight_sleep.py index e0a42fdb6a8..fd7c1aee3ae 100644 --- a/homeassistant/components/sensor/eight_sleep.py +++ b/homeassistant/components/sensor/eight_sleep.py @@ -5,7 +5,6 @@ For more details about this platform, please refer to the documentation at https://home-assistant.io/components/sensor.eight_sleep/ """ import logging -import asyncio from homeassistant.components.eight_sleep import ( DATA_EIGHT, EightSleepHeatEntity, EightSleepUserEntity, @@ -24,20 +23,20 @@ ATTR_AVG_HEART_RATE = 'Average Heart Rate' ATTR_SLEEP_DUR = 'Time Slept' ATTR_LIGHT_PERC = 'Light Sleep %' ATTR_DEEP_PERC = 'Deep Sleep %' +ATTR_REM_PERC = 'REM Sleep %' ATTR_TNT = 'Tosses & Turns' ATTR_SLEEP_STAGE = 'Sleep Stage' ATTR_TARGET_HEAT = 'Target Heating Level' ATTR_ACTIVE_HEAT = 'Heating Active' ATTR_DURATION_HEAT = 'Heating Time Remaining' -ATTR_LAST_SEEN = 'Last In Bed' ATTR_PROCESSING = 'Processing' ATTR_SESSION_START = 'Session Start' _LOGGER = logging.getLogger(__name__) -@asyncio.coroutine -def async_setup_platform(hass, config, async_add_devices, discovery_info=None): +async def async_setup_platform(hass, config, async_add_devices, + discovery_info=None): """Set up the eight sleep sensors.""" if discovery_info is None: return @@ -98,8 +97,7 @@ class EightHeatSensor(EightSleepHeatEntity): """Return the unit the value is expressed in.""" return '%' - @asyncio.coroutine - def async_update(self): + async def async_update(self): """Retrieve latest state.""" _LOGGER.debug("Updating Heat sensor: %s", self._sensor) self._state = self._usrobj.heating_level @@ -110,7 +108,6 @@ class EightHeatSensor(EightSleepHeatEntity): state_attr = {ATTR_TARGET_HEAT: self._usrobj.target_heating_level} state_attr[ATTR_ACTIVE_HEAT] = self._usrobj.now_heating state_attr[ATTR_DURATION_HEAT] = self._usrobj.heating_remaining - state_attr[ATTR_LAST_SEEN] = self._usrobj.last_seen return state_attr @@ -164,8 +161,7 @@ class EightUserSensor(EightSleepUserEntity): if 'bed_temp' in self._sensor: return 'mdi:thermometer' - @asyncio.coroutine - def async_update(self): + async def async_update(self): """Retrieve latest state.""" _LOGGER.debug("Updating User sensor: %s", self._sensor) if 'current' in self._sensor: @@ -176,10 +172,13 @@ class EightUserSensor(EightSleepUserEntity): self._attr = self._usrobj.last_values elif 'bed_temp' in self._sensor: temp = self._usrobj.current_values['bed_temp'] - if self._units == 'si': - self._state = round(temp, 2) - else: - self._state = round((temp*1.8)+32, 2) + try: + if self._units == 'si': + self._state = round(temp, 2) + else: + self._state = round((temp*1.8)+32, 2) + except TypeError: + self._state = None elif 'sleep_stage' in self._sensor: self._state = self._usrobj.current_values['stage'] @@ -208,12 +207,27 @@ class EightUserSensor(EightSleepUserEntity): except ZeroDivisionError: state_attr[ATTR_DEEP_PERC] = 0 - if self._units == 'si': - room_temp = round(self._attr['room_temp'], 2) - bed_temp = round(self._attr['bed_temp'], 2) - else: - room_temp = round((self._attr['room_temp']*1.8)+32, 2) - bed_temp = round((self._attr['bed_temp']*1.8)+32, 2) + try: + state_attr[ATTR_REM_PERC] = round(( + self._attr['breakdown']['rem'] / sleep_time) * 100, 2) + except ZeroDivisionError: + state_attr[ATTR_REM_PERC] = 0 + + try: + if self._units == 'si': + room_temp = round(self._attr['room_temp'], 2) + else: + room_temp = round((self._attr['room_temp']*1.8)+32, 2) + except TypeError: + room_temp = None + + try: + if self._units == 'si': + bed_temp = round(self._attr['bed_temp'], 2) + else: + bed_temp = round((self._attr['bed_temp']*1.8)+32, 2) + except TypeError: + bed_temp = None if 'current' in self._sensor_root: state_attr[ATTR_RESP_RATE] = round(self._attr['resp_rate'], 2) @@ -255,15 +269,17 @@ class EightRoomSensor(EightSleepUserEntity): """Return the state of the sensor.""" return self._state - @asyncio.coroutine - def async_update(self): + async def async_update(self): """Retrieve latest state.""" _LOGGER.debug("Updating Room sensor: %s", self._sensor) temp = self._eight.room_temperature() - if self._units == 'si': - self._state = round(temp, 2) - else: - self._state = round((temp*1.8)+32, 2) + try: + if self._units == 'si': + self._state = round(temp, 2) + else: + self._state = round((temp*1.8)+32, 2) + except TypeError: + self._state = None @property def unit_of_measurement(self): diff --git a/requirements_all.txt b/requirements_all.txt index 90cd28d61fc..921bbf8fd46 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -811,7 +811,7 @@ pyeconet==0.0.5 pyedimax==0.1 # homeassistant.components.eight_sleep -pyeight==0.0.8 +pyeight==0.0.9 # homeassistant.components.media_player.emby pyemby==1.5 From d0cbbe6141fe7b54843212ef6ceb45d1f47b9b65 Mon Sep 17 00:00:00 2001 From: c727 Date: Fri, 15 Jun 2018 23:09:01 +0200 Subject: [PATCH 098/144] Return ISO formated datetime in forecast (#14975) * Return ISO formated datetime in forecast * Lint --- homeassistant/components/weather/ecobee.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/weather/ecobee.py b/homeassistant/components/weather/ecobee.py index 80ee4c29fbe..59737c578a5 100644 --- a/homeassistant/components/weather/ecobee.py +++ b/homeassistant/components/weather/ecobee.py @@ -4,6 +4,7 @@ Support for displaying weather info from Ecobee API. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/weather.ecobee/ """ +from datetime import datetime from homeassistant.components import ecobee from homeassistant.components.weather import ( WeatherEntity, ATTR_FORECAST_CONDITION, ATTR_FORECAST_TEMP, @@ -134,8 +135,10 @@ class EcobeeWeather(WeatherEntity): try: forecasts = [] for day in self.weather['forecasts']: + date_time = datetime.strptime(day['dateTime'], + '%Y-%m-%d %H:%M:%S').isoformat() forecast = { - ATTR_FORECAST_TIME: day['dateTime'], + ATTR_FORECAST_TIME: date_time, ATTR_FORECAST_CONDITION: day['condition'], ATTR_FORECAST_TEMP: float(day['tempHigh']) / 10, } From 4bd7a7eee35a9bf718c08e8e49ac694cd07f9273 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ville=20Skytt=C3=A4?= Date: Sat, 16 Jun 2018 01:15:46 +0300 Subject: [PATCH 099/144] Remove inline pylint disables for messages disabled in pylintrc (#14978) --- homeassistant/components/axis.py | 2 +- homeassistant/components/binary_sensor/command_line.py | 1 - homeassistant/components/binary_sensor/gc100.py | 1 - homeassistant/components/binary_sensor/isy994.py | 3 --- homeassistant/components/binary_sensor/knx.py | 1 - homeassistant/components/binary_sensor/netatmo.py | 1 - homeassistant/components/binary_sensor/octoprint.py | 1 - homeassistant/components/binary_sensor/pilight.py | 1 - homeassistant/components/binary_sensor/raspihats.py | 1 - homeassistant/components/binary_sensor/rpi_gpio.py | 1 - homeassistant/components/binary_sensor/trend.py | 1 - homeassistant/components/binary_sensor/wemo.py | 2 +- homeassistant/components/bloomsky.py | 1 - homeassistant/components/calendar/__init__.py | 2 -- homeassistant/components/camera/__init__.py | 1 - homeassistant/components/camera/bloomsky.py | 1 - homeassistant/components/camera/foscam.py | 1 - homeassistant/components/camera/generic.py | 1 - homeassistant/components/camera/mjpeg.py | 1 - homeassistant/components/camera/netatmo.py | 1 - homeassistant/components/camera/zoneminder.py | 1 - homeassistant/components/climate/knx.py | 1 - homeassistant/components/climate/wink.py | 1 - homeassistant/components/cover/isy994.py | 1 - homeassistant/components/cover/knx.py | 2 -- homeassistant/components/cover/lutron.py | 1 - homeassistant/components/cover/lutron_caseta.py | 1 - homeassistant/components/cover/rpi_gpio.py | 1 - homeassistant/components/device_tracker/actiontec.py | 1 - homeassistant/components/device_tracker/aruba.py | 1 - homeassistant/components/device_tracker/asuswrt.py | 1 - homeassistant/components/device_tracker/bt_home_hub_5.py | 1 - homeassistant/components/device_tracker/ddwrt.py | 1 - homeassistant/components/device_tracker/huawei_router.py | 1 - homeassistant/components/device_tracker/sky_hub.py | 1 - homeassistant/components/device_tracker/snmp.py | 1 - homeassistant/components/device_tracker/thomson.py | 1 - homeassistant/components/device_tracker/unifi_direct.py | 1 - homeassistant/components/ecobee.py | 3 +-- homeassistant/components/fan/demo.py | 1 - homeassistant/components/fan/isy994.py | 1 - homeassistant/components/fan/xiaomi_miio.py | 1 - homeassistant/components/ios.py | 2 +- homeassistant/components/isy994.py | 1 - homeassistant/components/light/blinksticklight.py | 1 - homeassistant/components/light/isy994.py | 1 - homeassistant/components/light/knx.py | 1 - homeassistant/components/light/lifx_legacy.py | 2 -- homeassistant/components/light/lutron.py | 1 - homeassistant/components/light/lutron_caseta.py | 1 - homeassistant/components/light/tellstick.py | 1 - homeassistant/components/light/tikteck.py | 1 - homeassistant/components/light/vera.py | 1 - homeassistant/components/light/xiaomi_miio.py | 1 - homeassistant/components/light/zengge.py | 1 - homeassistant/components/lock/demo.py | 1 - homeassistant/components/lock/isy994.py | 1 - homeassistant/components/lock/lockitron.py | 1 - homeassistant/components/lock/nello.py | 1 - homeassistant/components/lock/nuki.py | 1 - homeassistant/components/lock/sesame.py | 1 - homeassistant/components/lock/volvooncall.py | 1 - homeassistant/components/logbook.py | 1 - homeassistant/components/media_player/aquostv.py | 2 -- homeassistant/components/media_player/blackbird.py | 1 - homeassistant/components/media_player/braviatv.py | 1 - homeassistant/components/media_player/channels.py | 1 - homeassistant/components/media_player/clementine.py | 1 - homeassistant/components/media_player/demo.py | 1 - homeassistant/components/media_player/dunehd.py | 1 - homeassistant/components/media_player/firetv.py | 1 - homeassistant/components/media_player/frontier_silicon.py | 1 - homeassistant/components/media_player/gpmdp.py | 1 - homeassistant/components/media_player/gstreamer.py | 1 - homeassistant/components/media_player/lg_netcast.py | 1 - homeassistant/components/media_player/monoprice.py | 1 - homeassistant/components/media_player/mpchc.py | 1 - homeassistant/components/media_player/mpd.py | 1 - homeassistant/components/media_player/openhome.py | 1 - homeassistant/components/media_player/panasonic_viera.py | 1 - homeassistant/components/media_player/pandora.py | 1 - homeassistant/components/media_player/philips_js.py | 1 - homeassistant/components/media_player/samsungtv.py | 1 - homeassistant/components/media_player/snapcast.py | 1 - homeassistant/components/media_player/vlc.py | 1 - homeassistant/components/media_player/webostv.py | 2 -- homeassistant/components/modbus.py | 4 ++-- homeassistant/components/notify/message_bird.py | 1 - homeassistant/components/notify/mysensors.py | 2 -- homeassistant/components/notify/nfandroidtv.py | 1 - homeassistant/components/notify/pushbullet.py | 1 - homeassistant/components/nuimo_controller.py | 1 - homeassistant/components/raspihats.py | 1 - homeassistant/components/remote/demo.py | 1 - homeassistant/components/remote/itach.py | 1 - homeassistant/components/scene/lifx_cloud.py | 1 - homeassistant/components/sensor/bloomsky.py | 1 - homeassistant/components/sensor/broadlink.py | 1 - homeassistant/components/sensor/citybikes.py | 1 - homeassistant/components/sensor/command_line.py | 1 - homeassistant/components/sensor/crimereports.py | 1 - homeassistant/components/sensor/deluge.py | 1 - homeassistant/components/sensor/demo.py | 1 - homeassistant/components/sensor/eddystone_temperature.py | 1 - homeassistant/components/sensor/fedex.py | 1 - homeassistant/components/sensor/fints.py | 1 - homeassistant/components/sensor/fitbit.py | 2 -- homeassistant/components/sensor/haveibeenpwned.py | 1 - homeassistant/components/sensor/hp_ilo.py | 1 - homeassistant/components/sensor/isy994.py | 1 - homeassistant/components/sensor/kira.py | 2 +- homeassistant/components/sensor/knx.py | 1 - homeassistant/components/sensor/lastfm.py | 1 - homeassistant/components/sensor/mold_indicator.py | 1 - homeassistant/components/sensor/mopar.py | 1 - homeassistant/components/sensor/mvglive.py | 1 - homeassistant/components/sensor/nzbget.py | 1 - homeassistant/components/sensor/octoprint.py | 1 - homeassistant/components/sensor/ohmconnect.py | 1 - homeassistant/components/sensor/onewire.py | 1 - homeassistant/components/sensor/opensky.py | 1 - homeassistant/components/sensor/pilight.py | 1 - homeassistant/components/sensor/plex.py | 1 - homeassistant/components/sensor/postnl.py | 1 - homeassistant/components/sensor/pyload.py | 1 - homeassistant/components/sensor/qnap.py | 1 - homeassistant/components/sensor/skybeacon.py | 1 - homeassistant/components/sensor/spotcrime.py | 1 - homeassistant/components/sensor/steam_online.py | 1 - homeassistant/components/sensor/supervisord.py | 1 - homeassistant/components/sensor/systemmonitor.py | 1 - homeassistant/components/sensor/tellstick.py | 1 - homeassistant/components/sensor/temper.py | 1 - homeassistant/components/sensor/template.py | 1 - homeassistant/components/sensor/torque.py | 1 - homeassistant/components/sensor/twitch.py | 1 - homeassistant/components/sensor/ups.py | 1 - homeassistant/components/sensor/worldtidesinfo.py | 1 - homeassistant/components/sensor/xbox_live.py | 1 - homeassistant/components/sensor/xiaomi_miio.py | 1 - homeassistant/components/sleepiq.py | 1 - homeassistant/components/switch/bbb_gpio.py | 1 - homeassistant/components/switch/command_line.py | 1 - homeassistant/components/switch/deluge.py | 1 - homeassistant/components/switch/demo.py | 1 - homeassistant/components/switch/dlink.py | 1 - homeassistant/components/switch/edimax.py | 1 - homeassistant/components/switch/flux.py | 1 - homeassistant/components/switch/gc100.py | 1 - homeassistant/components/switch/isy994.py | 1 - homeassistant/components/switch/kankun.py | 1 - homeassistant/components/switch/knx.py | 1 - homeassistant/components/switch/lutron_caseta.py | 1 - homeassistant/components/switch/orvibo.py | 1 - homeassistant/components/switch/pulseaudio_loopback.py | 1 - homeassistant/components/switch/raspihats.py | 1 - homeassistant/components/switch/rest.py | 1 - homeassistant/components/switch/rpi_gpio.py | 1 - homeassistant/components/switch/rpi_rf.py | 2 +- homeassistant/components/switch/tellstick.py | 1 - homeassistant/components/switch/telnet.py | 1 - homeassistant/components/switch/template.py | 1 - homeassistant/components/switch/tplink.py | 1 - homeassistant/components/switch/transmission.py | 1 - homeassistant/components/switch/wemo.py | 2 +- homeassistant/components/switch/xiaomi_miio.py | 1 - homeassistant/components/vera.py | 2 +- homeassistant/components/wemo.py | 2 +- homeassistant/components/wink/__init__.py | 1 - homeassistant/core.py | 2 +- homeassistant/util/dt.py | 2 +- 171 files changed, 13 insertions(+), 182 deletions(-) diff --git a/homeassistant/components/axis.py b/homeassistant/components/axis.py index 9906c61f269..71894364f91 100644 --- a/homeassistant/components/axis.py +++ b/homeassistant/components/axis.py @@ -145,7 +145,7 @@ def request_configuration(hass, config, name, host, serialnumber): def setup(hass, config): """Set up for Axis devices.""" - def _shutdown(call): # pylint: disable=unused-argument + def _shutdown(call): """Stop the event stream on shutdown.""" for serialnumber, device in AXIS_DEVICES.items(): _LOGGER.info("Stopping event stream for %s.", serialnumber) diff --git a/homeassistant/components/binary_sensor/command_line.py b/homeassistant/components/binary_sensor/command_line.py index 2289ad5d906..480786b2c2c 100644 --- a/homeassistant/components/binary_sensor/command_line.py +++ b/homeassistant/components/binary_sensor/command_line.py @@ -35,7 +35,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ }) -# pylint: disable=unused-argument def setup_platform(hass, config, add_devices, discovery_info=None): """Set up the Command line Binary Sensor.""" name = config.get(CONF_NAME) diff --git a/homeassistant/components/binary_sensor/gc100.py b/homeassistant/components/binary_sensor/gc100.py index c17e6b50911..767be2874e6 100644 --- a/homeassistant/components/binary_sensor/gc100.py +++ b/homeassistant/components/binary_sensor/gc100.py @@ -23,7 +23,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ }) -# pylint: disable=unused-argument def setup_platform(hass, config, add_devices, discovery_info=None): """Set up the GC100 devices.""" binary_sensors = [] diff --git a/homeassistant/components/binary_sensor/isy994.py b/homeassistant/components/binary_sensor/isy994.py index 09f1739cba7..a80e4db747d 100644 --- a/homeassistant/components/binary_sensor/isy994.py +++ b/homeassistant/components/binary_sensor/isy994.py @@ -28,7 +28,6 @@ ISY_DEVICE_TYPES = { } -# pylint: disable=unused-argument def setup_platform(hass, config: ConfigType, add_devices: Callable[[list], None], discovery_info=None): """Set up the ISY994 binary sensor platform.""" @@ -299,7 +298,6 @@ class ISYBinarySensorHeartbeat(ISYDevice, BinarySensorDevice): # No heartbeat timer is active pass - # pylint: disable=unused-argument @callback def timer_elapsed(now) -> None: """Heartbeat missed; set state to indicate dead battery.""" @@ -314,7 +312,6 @@ class ISYBinarySensorHeartbeat(ISYDevice, BinarySensorDevice): self._heartbeat_timer = async_track_point_in_utc_time( self.hass, timer_elapsed, point_in_time) - # pylint: disable=unused-argument def on_update(self, event: object) -> None: """Ignore node status updates. diff --git a/homeassistant/components/binary_sensor/knx.py b/homeassistant/components/binary_sensor/knx.py index 834186b8b18..e6b28047cb8 100644 --- a/homeassistant/components/binary_sensor/knx.py +++ b/homeassistant/components/binary_sensor/knx.py @@ -115,7 +115,6 @@ class KNXBinarySensor(BinarySensorDevice): """Register callbacks to update hass after device was changed.""" async def after_update_callback(device): """Call after device was updated.""" - # pylint: disable=unused-argument await self.async_update_ha_state() self.device.register_device_updated_cb(after_update_callback) diff --git a/homeassistant/components/binary_sensor/netatmo.py b/homeassistant/components/binary_sensor/netatmo.py index 10fc2ccc3ff..7c3a3e1dd30 100644 --- a/homeassistant/components/binary_sensor/netatmo.py +++ b/homeassistant/components/binary_sensor/netatmo.py @@ -57,7 +57,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ }) -# pylint: disable=unused-argument def setup_platform(hass, config, add_devices, discovery_info=None): """Set up the access to Netatmo binary sensor.""" netatmo = hass.components.netatmo diff --git a/homeassistant/components/binary_sensor/octoprint.py b/homeassistant/components/binary_sensor/octoprint.py index 265fcec66fa..1a1967b9014 100644 --- a/homeassistant/components/binary_sensor/octoprint.py +++ b/homeassistant/components/binary_sensor/octoprint.py @@ -33,7 +33,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ }) -# pylint: disable=unused-argument def setup_platform(hass, config, add_devices, discovery_info=None): """Set up the available OctoPrint binary sensors.""" octoprint_api = hass.data[DOMAIN]["api"] diff --git a/homeassistant/components/binary_sensor/pilight.py b/homeassistant/components/binary_sensor/pilight.py index d2c46c795a8..69dc3b83485 100644 --- a/homeassistant/components/binary_sensor/pilight.py +++ b/homeassistant/components/binary_sensor/pilight.py @@ -44,7 +44,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ }) -# pylint: disable=unused-argument def setup_platform(hass, config, add_devices, discovery_info=None): """Set up Pilight Binary Sensor.""" disarm = config.get(CONF_DISARM_AFTER_TRIGGER) diff --git a/homeassistant/components/binary_sensor/raspihats.py b/homeassistant/components/binary_sensor/raspihats.py index 9d489a59711..9ab56a5a20d 100644 --- a/homeassistant/components/binary_sensor/raspihats.py +++ b/homeassistant/components/binary_sensor/raspihats.py @@ -42,7 +42,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ }) -# pylint: disable=unused-argument def setup_platform(hass, config, add_devices, discovery_info=None): """Set up the raspihats binary_sensor devices.""" I2CHatBinarySensor.I2C_HATS_MANAGER = hass.data[I2C_HATS_MANAGER] diff --git a/homeassistant/components/binary_sensor/rpi_gpio.py b/homeassistant/components/binary_sensor/rpi_gpio.py index 2322b1bf498..e1e06ce57b9 100644 --- a/homeassistant/components/binary_sensor/rpi_gpio.py +++ b/homeassistant/components/binary_sensor/rpi_gpio.py @@ -39,7 +39,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ }) -# pylint: disable=unused-argument def setup_platform(hass, config, add_devices, discovery_info=None): """Set up the Raspberry PI GPIO devices.""" pull_mode = config.get(CONF_PULL_MODE) diff --git a/homeassistant/components/binary_sensor/trend.py b/homeassistant/components/binary_sensor/trend.py index 5405a6a77ba..dcdd312ce81 100644 --- a/homeassistant/components/binary_sensor/trend.py +++ b/homeassistant/components/binary_sensor/trend.py @@ -57,7 +57,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ }) -# pylint: disable=unused-argument def setup_platform(hass, config, add_devices, discovery_info=None): """Set up the trend sensors.""" sensors = [] diff --git a/homeassistant/components/binary_sensor/wemo.py b/homeassistant/components/binary_sensor/wemo.py index 30a7e291401..d3c78597c70 100644 --- a/homeassistant/components/binary_sensor/wemo.py +++ b/homeassistant/components/binary_sensor/wemo.py @@ -13,7 +13,7 @@ DEPENDENCIES = ['wemo'] _LOGGER = logging.getLogger(__name__) -# pylint: disable=unused-argument, too-many-function-args +# pylint: disable=too-many-function-args def setup_platform(hass, config, add_devices_callback, discovery_info=None): """Register discovered WeMo binary sensors.""" import pywemo.discovery as discovery diff --git a/homeassistant/components/bloomsky.py b/homeassistant/components/bloomsky.py index f04e0af7be9..bc9d3acf54f 100644 --- a/homeassistant/components/bloomsky.py +++ b/homeassistant/components/bloomsky.py @@ -34,7 +34,6 @@ CONFIG_SCHEMA = vol.Schema({ }, extra=vol.ALLOW_EXTRA) -# pylint: disable=unused-argument def setup(hass, config): """Set up the BloomSky component.""" api_key = config[DOMAIN][CONF_API_KEY] diff --git a/homeassistant/components/calendar/__init__.py b/homeassistant/components/calendar/__init__.py index 65e5e33c7c1..9716e46bc03 100644 --- a/homeassistant/components/calendar/__init__.py +++ b/homeassistant/components/calendar/__init__.py @@ -60,7 +60,6 @@ def get_date(date): return dt.as_local(dt.parse_datetime(date['dateTime'])) -# pylint: disable=too-many-instance-attributes class CalendarEventDevice(Entity): """A calendar event device.""" @@ -68,7 +67,6 @@ class CalendarEventDevice(Entity): # with an update() method data = None - # pylint: disable=too-many-arguments def __init__(self, hass, data): """Create the Calendar Event Device.""" self._name = data.get(CONF_NAME) diff --git a/homeassistant/components/camera/__init__.py b/homeassistant/components/camera/__init__.py index f2f4081fb6d..c41020c3faf 100644 --- a/homeassistant/components/camera/__init__.py +++ b/homeassistant/components/camera/__init__.py @@ -1,4 +1,3 @@ -# pylint: disable=too-many-lines """ Component to interface with cameras. diff --git a/homeassistant/components/camera/bloomsky.py b/homeassistant/components/camera/bloomsky.py index ef70692215d..77528992674 100644 --- a/homeassistant/components/camera/bloomsky.py +++ b/homeassistant/components/camera/bloomsky.py @@ -13,7 +13,6 @@ from homeassistant.components.camera import Camera DEPENDENCIES = ['bloomsky'] -# pylint: disable=unused-argument def setup_platform(hass, config, add_devices, discovery_info=None): """Set up access to BloomSky cameras.""" bloomsky = hass.components.bloomsky diff --git a/homeassistant/components/camera/foscam.py b/homeassistant/components/camera/foscam.py index 15db83d345a..4ea733139a9 100644 --- a/homeassistant/components/camera/foscam.py +++ b/homeassistant/components/camera/foscam.py @@ -33,7 +33,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ }) -# pylint: disable=unused-argument def setup_platform(hass, config, add_devices, discovery_info=None): """Set up a Foscam IP Camera.""" add_devices([FoscamCam(config)]) diff --git a/homeassistant/components/camera/generic.py b/homeassistant/components/camera/generic.py index e11bd599e45..911c14e7232 100644 --- a/homeassistant/components/camera/generic.py +++ b/homeassistant/components/camera/generic.py @@ -46,7 +46,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ @asyncio.coroutine -# pylint: disable=unused-argument def async_setup_platform(hass, config, async_add_devices, discovery_info=None): """Set up a generic IP Camera.""" async_add_devices([GenericCamera(hass, config)]) diff --git a/homeassistant/components/camera/mjpeg.py b/homeassistant/components/camera/mjpeg.py index 35d30104f6e..a5ed0cdc02c 100644 --- a/homeassistant/components/camera/mjpeg.py +++ b/homeassistant/components/camera/mjpeg.py @@ -42,7 +42,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ @asyncio.coroutine -# pylint: disable=unused-argument def async_setup_platform(hass, config, async_add_devices, discovery_info=None): """Set up a MJPEG IP Camera.""" if discovery_info: diff --git a/homeassistant/components/camera/netatmo.py b/homeassistant/components/camera/netatmo.py index 5b8effd5dcc..34a78e19f9f 100644 --- a/homeassistant/components/camera/netatmo.py +++ b/homeassistant/components/camera/netatmo.py @@ -29,7 +29,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ }) -# pylint: disable=unused-argument def setup_platform(hass, config, add_devices, discovery_info=None): """Set up access to Netatmo cameras.""" netatmo = hass.components.netatmo diff --git a/homeassistant/components/camera/zoneminder.py b/homeassistant/components/camera/zoneminder.py index a98e3ef066f..90ef08c24fe 100644 --- a/homeassistant/components/camera/zoneminder.py +++ b/homeassistant/components/camera/zoneminder.py @@ -49,7 +49,6 @@ def _get_image_url(hass, monitor, mode): @asyncio.coroutine -# pylint: disable=unused-argument def async_setup_platform(hass, config, async_add_devices, discovery_info=None): """Set up the ZoneMinder cameras.""" cameras = [] diff --git a/homeassistant/components/climate/knx.py b/homeassistant/components/climate/knx.py index 5ce6cc2fa7a..f53cf2491dc 100644 --- a/homeassistant/components/climate/knx.py +++ b/homeassistant/components/climate/knx.py @@ -136,7 +136,6 @@ class KNXClimate(ClimateDevice): """Register callbacks to update hass after device was changed.""" async def after_update_callback(device): """Call after device was updated.""" - # pylint: disable=unused-argument await self.async_update_ha_state() self.device.register_device_updated_cb(after_update_callback) diff --git a/homeassistant/components/climate/wink.py b/homeassistant/components/climate/wink.py index c67e032c149..12a6960f833 100644 --- a/homeassistant/components/climate/wink.py +++ b/homeassistant/components/climate/wink.py @@ -84,7 +84,6 @@ def setup_platform(hass, config, add_devices, discovery_info=None): add_devices([WinkWaterHeater(water_heater, hass)]) -# pylint: disable=abstract-method class WinkThermostat(WinkDevice, ClimateDevice): """Representation of a Wink thermostat.""" diff --git a/homeassistant/components/cover/isy994.py b/homeassistant/components/cover/isy994.py index 82ca60e84e6..743a36d41d5 100644 --- a/homeassistant/components/cover/isy994.py +++ b/homeassistant/components/cover/isy994.py @@ -25,7 +25,6 @@ VALUE_TO_STATE = { } -# pylint: disable=unused-argument def setup_platform(hass, config: ConfigType, add_devices: Callable[[list], None], discovery_info=None): """Set up the ISY994 cover platform.""" diff --git a/homeassistant/components/cover/knx.py b/homeassistant/components/cover/knx.py index 83668924268..7bb20e4cf1f 100644 --- a/homeassistant/components/cover/knx.py +++ b/homeassistant/components/cover/knx.py @@ -107,7 +107,6 @@ class KNXCover(CoverDevice): """Register callbacks to update hass after device was changed.""" async def after_update_callback(device): """Call after device was updated.""" - # pylint: disable=unused-argument await self.async_update_ha_state() self.device.register_device_updated_cb(after_update_callback) @@ -197,7 +196,6 @@ class KNXCover(CoverDevice): @callback def auto_updater_hook(self, now): """Call for the autoupdater.""" - # pylint: disable=unused-argument self.async_schedule_update_ha_state() if self.device.position_reached(): self.stop_auto_updater() diff --git a/homeassistant/components/cover/lutron.py b/homeassistant/components/cover/lutron.py index 4e38681a310..599bdb1ceba 100644 --- a/homeassistant/components/cover/lutron.py +++ b/homeassistant/components/cover/lutron.py @@ -17,7 +17,6 @@ _LOGGER = logging.getLogger(__name__) DEPENDENCIES = ['lutron'] -# pylint: disable=unused-argument def setup_platform(hass, config, add_devices, discovery_info=None): """Set up the Lutron shades.""" devs = [] diff --git a/homeassistant/components/cover/lutron_caseta.py b/homeassistant/components/cover/lutron_caseta.py index 6ad9b093ed8..1ed502e0f7f 100644 --- a/homeassistant/components/cover/lutron_caseta.py +++ b/homeassistant/components/cover/lutron_caseta.py @@ -18,7 +18,6 @@ _LOGGER = logging.getLogger(__name__) DEPENDENCIES = ['lutron_caseta'] -# pylint: disable=unused-argument @asyncio.coroutine def async_setup_platform(hass, config, async_add_devices, discovery_info=None): """Set up the Lutron Caseta shades as a cover device.""" diff --git a/homeassistant/components/cover/rpi_gpio.py b/homeassistant/components/cover/rpi_gpio.py index 49666139330..384f96f3f52 100644 --- a/homeassistant/components/cover/rpi_gpio.py +++ b/homeassistant/components/cover/rpi_gpio.py @@ -54,7 +54,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ }) -# pylint: disable=unused-argument def setup_platform(hass, config, add_devices, discovery_info=None): """Set up the RPi cover platform.""" relay_time = config.get(CONF_RELAY_TIME) diff --git a/homeassistant/components/device_tracker/actiontec.py b/homeassistant/components/device_tracker/actiontec.py index 781e486a40e..72d9992c60f 100644 --- a/homeassistant/components/device_tracker/actiontec.py +++ b/homeassistant/components/device_tracker/actiontec.py @@ -31,7 +31,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ }) -# pylint: disable=unused-argument def get_scanner(hass, config): """Validate the configuration and return an Actiontec scanner.""" scanner = ActiontecDeviceScanner(config[DOMAIN]) diff --git a/homeassistant/components/device_tracker/aruba.py b/homeassistant/components/device_tracker/aruba.py index 79d8806fe22..92ef78f60f3 100644 --- a/homeassistant/components/device_tracker/aruba.py +++ b/homeassistant/components/device_tracker/aruba.py @@ -30,7 +30,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ }) -# pylint: disable=unused-argument def get_scanner(hass, config): """Validate the configuration and return a Aruba scanner.""" scanner = ArubaDeviceScanner(config[DOMAIN]) diff --git a/homeassistant/components/device_tracker/asuswrt.py b/homeassistant/components/device_tracker/asuswrt.py index 7e9b10e9241..5cb7e283c99 100644 --- a/homeassistant/components/device_tracker/asuswrt.py +++ b/homeassistant/components/device_tracker/asuswrt.py @@ -78,7 +78,6 @@ _ARP_REGEX = re.compile( r'.*') -# pylint: disable=unused-argument def get_scanner(hass, config): """Validate the configuration and return an ASUS-WRT scanner.""" scanner = AsusWrtDeviceScanner(config[DOMAIN]) diff --git a/homeassistant/components/device_tracker/bt_home_hub_5.py b/homeassistant/components/device_tracker/bt_home_hub_5.py index a3b5bcac77c..707850d2215 100644 --- a/homeassistant/components/device_tracker/bt_home_hub_5.py +++ b/homeassistant/components/device_tracker/bt_home_hub_5.py @@ -26,7 +26,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ }) -# pylint: disable=unused-argument def get_scanner(hass, config): """Return a BT Home Hub 5 scanner if successful.""" scanner = BTHomeHub5DeviceScanner(config[DOMAIN]) diff --git a/homeassistant/components/device_tracker/ddwrt.py b/homeassistant/components/device_tracker/ddwrt.py index 3d36a1b428c..3e17fdd3329 100644 --- a/homeassistant/components/device_tracker/ddwrt.py +++ b/homeassistant/components/device_tracker/ddwrt.py @@ -27,7 +27,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ }) -# pylint: disable=unused-argument def get_scanner(hass, config): """Validate the configuration and return a DD-WRT scanner.""" try: diff --git a/homeassistant/components/device_tracker/huawei_router.py b/homeassistant/components/device_tracker/huawei_router.py index 775075b8a4a..804269e6228 100644 --- a/homeassistant/components/device_tracker/huawei_router.py +++ b/homeassistant/components/device_tracker/huawei_router.py @@ -26,7 +26,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ }) -# pylint: disable=unused-argument def get_scanner(hass, config): """Validate the configuration and return a HUAWEI scanner.""" scanner = HuaweiDeviceScanner(config[DOMAIN]) diff --git a/homeassistant/components/device_tracker/sky_hub.py b/homeassistant/components/device_tracker/sky_hub.py index c48c9bd029b..0c289ce9a82 100644 --- a/homeassistant/components/device_tracker/sky_hub.py +++ b/homeassistant/components/device_tracker/sky_hub.py @@ -23,7 +23,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ }) -# pylint: disable=unused-argument def get_scanner(hass, config): """Return a Sky Hub scanner if successful.""" scanner = SkyHubDeviceScanner(config[DOMAIN]) diff --git a/homeassistant/components/device_tracker/snmp.py b/homeassistant/components/device_tracker/snmp.py index c9c27fb2bfa..3d57cb108e2 100644 --- a/homeassistant/components/device_tracker/snmp.py +++ b/homeassistant/components/device_tracker/snmp.py @@ -34,7 +34,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ }) -# pylint: disable=unused-argument def get_scanner(hass, config): """Validate the configuration and return an SNMP scanner.""" scanner = SnmpScanner(config[DOMAIN]) diff --git a/homeassistant/components/device_tracker/thomson.py b/homeassistant/components/device_tracker/thomson.py index 3fa161e467d..8a56fcee702 100644 --- a/homeassistant/components/device_tracker/thomson.py +++ b/homeassistant/components/device_tracker/thomson.py @@ -33,7 +33,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ }) -# pylint: disable=unused-argument def get_scanner(hass, config): """Validate the configuration and return a THOMSON scanner.""" scanner = ThomsonDeviceScanner(config[DOMAIN]) diff --git a/homeassistant/components/device_tracker/unifi_direct.py b/homeassistant/components/device_tracker/unifi_direct.py index 168ab04ec6f..c3c4a48bb82 100644 --- a/homeassistant/components/device_tracker/unifi_direct.py +++ b/homeassistant/components/device_tracker/unifi_direct.py @@ -33,7 +33,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ }) -# pylint: disable=unused-argument def get_scanner(hass, config): """Validate the configuration and return a Unifi direct scanner.""" scanner = UnifiDeviceScanner(config[DOMAIN]) diff --git a/homeassistant/components/ecobee.py b/homeassistant/components/ecobee.py index 9c29cea704c..22348dcc297 100644 --- a/homeassistant/components/ecobee.py +++ b/homeassistant/components/ecobee.py @@ -48,7 +48,6 @@ def request_configuration(network, hass, config): return - # pylint: disable=unused-argument def ecobee_configuration_callback(callback_data): """Handle configuration callbacks.""" network.request_tokens() @@ -106,7 +105,7 @@ def setup(hass, config): Will automatically load thermostat and sensor components to support devices discovered on the network. """ - # pylint: disable=global-statement, import-error + # pylint: disable=import-error global NETWORK if 'ecobee' in _CONFIGURING: diff --git a/homeassistant/components/fan/demo.py b/homeassistant/components/fan/demo.py index b328ebb3101..c03c492c834 100644 --- a/homeassistant/components/fan/demo.py +++ b/homeassistant/components/fan/demo.py @@ -13,7 +13,6 @@ FULL_SUPPORT = SUPPORT_SET_SPEED | SUPPORT_OSCILLATE | SUPPORT_DIRECTION LIMITED_SUPPORT = SUPPORT_SET_SPEED -# pylint: disable=unused-argument def setup_platform(hass, config, add_devices_callback, discovery_info=None): """Set up the demo fan platform.""" add_devices_callback([ diff --git a/homeassistant/components/fan/isy994.py b/homeassistant/components/fan/isy994.py index 847ca3b325b..97a5f9c3bd6 100644 --- a/homeassistant/components/fan/isy994.py +++ b/homeassistant/components/fan/isy994.py @@ -30,7 +30,6 @@ for key in VALUE_TO_STATE: STATE_TO_VALUE[VALUE_TO_STATE[key]] = key -# pylint: disable=unused-argument def setup_platform(hass, config: ConfigType, add_devices: Callable[[list], None], discovery_info=None): """Set up the ISY994 fan platform.""" diff --git a/homeassistant/components/fan/xiaomi_miio.py b/homeassistant/components/fan/xiaomi_miio.py index 2f00de08005..1616d388816 100644 --- a/homeassistant/components/fan/xiaomi_miio.py +++ b/homeassistant/components/fan/xiaomi_miio.py @@ -314,7 +314,6 @@ SERVICE_TO_METHOD = { } -# pylint: disable=unused-argument async def async_setup_platform(hass, config, async_add_devices, discovery_info=None): """Set up the miio fan device from config.""" diff --git a/homeassistant/components/ios.py b/homeassistant/components/ios.py index fe3c934659b..249f147847c 100644 --- a/homeassistant/components/ios.py +++ b/homeassistant/components/ios.py @@ -203,7 +203,7 @@ def device_name_for_push_id(push_id): def setup(hass, config): """Set up the iOS component.""" - # pylint: disable=global-statement, import-error + # pylint: disable=import-error global CONFIG_FILE global CONFIG_FILE_PATH diff --git a/homeassistant/components/isy994.py b/homeassistant/components/isy994.py index ecabcd36a85..90ab41cf98b 100644 --- a/homeassistant/components/isy994.py +++ b/homeassistant/components/isy994.py @@ -425,7 +425,6 @@ class ISYDevice(Entity): self._control_handler = self._node.controlEvents.subscribe( self.on_control) - # pylint: disable=unused-argument def on_update(self, event: object) -> None: """Handle the update event from the ISY994 Node.""" self.schedule_update_ha_state() diff --git a/homeassistant/components/light/blinksticklight.py b/homeassistant/components/light/blinksticklight.py index 18a6b4ae266..bca587074b0 100644 --- a/homeassistant/components/light/blinksticklight.py +++ b/homeassistant/components/light/blinksticklight.py @@ -31,7 +31,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ }) -# pylint: disable=unused-argument def setup_platform(hass, config, add_devices, discovery_info=None): """Set up Blinkstick device specified by serial number.""" from blinkstick import blinkstick diff --git a/homeassistant/components/light/isy994.py b/homeassistant/components/light/isy994.py index d2ed865892e..ce358d0a974 100644 --- a/homeassistant/components/light/isy994.py +++ b/homeassistant/components/light/isy994.py @@ -15,7 +15,6 @@ from homeassistant.helpers.typing import ConfigType _LOGGER = logging.getLogger(__name__) -# pylint: disable=unused-argument def setup_platform(hass, config: ConfigType, add_devices: Callable[[list], None], discovery_info=None): """Set up the ISY994 light platform.""" diff --git a/homeassistant/components/light/knx.py b/homeassistant/components/light/knx.py index 18446951735..8fa2b56d1d2 100644 --- a/homeassistant/components/light/knx.py +++ b/homeassistant/components/light/knx.py @@ -88,7 +88,6 @@ class KNXLight(Light): """Register callbacks to update hass after device was changed.""" async def after_update_callback(device): """Call after device was updated.""" - # pylint: disable=unused-argument await self.async_update_ha_state() self.device.register_device_updated_cb(after_update_callback) diff --git a/homeassistant/components/light/lifx_legacy.py b/homeassistant/components/light/lifx_legacy.py index 490eeb6ecab..182d7536dc4 100644 --- a/homeassistant/components/light/lifx_legacy.py +++ b/homeassistant/components/light/lifx_legacy.py @@ -45,7 +45,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ }) -# pylint: disable=unused-argument def setup_platform(hass, config, add_devices, discovery_info=None): """Set up the LIFX platform.""" server_addr = config.get(CONF_SERVER) @@ -118,7 +117,6 @@ class LIFX(object): bulb.set_power(power) bulb.schedule_update_ha_state() - # pylint: disable=unused-argument def poll(self, now): """Set up polling for the light.""" self.probe() diff --git a/homeassistant/components/light/lutron.py b/homeassistant/components/light/lutron.py index 34d6cba7cb8..24744110c6f 100644 --- a/homeassistant/components/light/lutron.py +++ b/homeassistant/components/light/lutron.py @@ -16,7 +16,6 @@ _LOGGER = logging.getLogger(__name__) DEPENDENCIES = ['lutron'] -# pylint: disable=unused-argument def setup_platform(hass, config, add_devices, discovery_info=None): """Set up the Lutron lights.""" devs = [] diff --git a/homeassistant/components/light/lutron_caseta.py b/homeassistant/components/light/lutron_caseta.py index e4e1baf6c58..09f0a337cc3 100644 --- a/homeassistant/components/light/lutron_caseta.py +++ b/homeassistant/components/light/lutron_caseta.py @@ -19,7 +19,6 @@ _LOGGER = logging.getLogger(__name__) DEPENDENCIES = ['lutron_caseta'] -# pylint: disable=unused-argument @asyncio.coroutine def async_setup_platform(hass, config, async_add_devices, discovery_info=None): """Set up the Lutron Caseta lights.""" diff --git a/homeassistant/components/light/tellstick.py b/homeassistant/components/light/tellstick.py index 1bf7d632af5..44e5e40b3b7 100644 --- a/homeassistant/components/light/tellstick.py +++ b/homeassistant/components/light/tellstick.py @@ -15,7 +15,6 @@ from homeassistant.components.tellstick import ( SUPPORT_TELLSTICK = SUPPORT_BRIGHTNESS -# pylint: disable=unused-argument def setup_platform(hass, config, add_devices, discovery_info=None): """Set up the Tellstick lights.""" if (discovery_info is None or diff --git a/homeassistant/components/light/tikteck.py b/homeassistant/components/light/tikteck.py index 2079638f7f1..c21da57ea96 100644 --- a/homeassistant/components/light/tikteck.py +++ b/homeassistant/components/light/tikteck.py @@ -31,7 +31,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ }) -# pylint: disable=unused-argument def setup_platform(hass, config, add_devices, discovery_info=None): """Set up the Tikteck platform.""" lights = [] diff --git a/homeassistant/components/light/vera.py b/homeassistant/components/light/vera.py index 7ace250b6ee..e62ffaecdff 100644 --- a/homeassistant/components/light/vera.py +++ b/homeassistant/components/light/vera.py @@ -18,7 +18,6 @@ _LOGGER = logging.getLogger(__name__) DEPENDENCIES = ['vera'] -# pylint: disable=unused-argument def setup_platform(hass, config, add_devices, discovery_info=None): """Set up the Vera lights.""" add_devices( diff --git a/homeassistant/components/light/xiaomi_miio.py b/homeassistant/components/light/xiaomi_miio.py index cba15f6df9f..fbb8dd66f01 100644 --- a/homeassistant/components/light/xiaomi_miio.py +++ b/homeassistant/components/light/xiaomi_miio.py @@ -100,7 +100,6 @@ SERVICE_TO_METHOD = { } -# pylint: disable=unused-argument async def async_setup_platform(hass, config, async_add_devices, discovery_info=None): """Set up the light from config.""" diff --git a/homeassistant/components/light/zengge.py b/homeassistant/components/light/zengge.py index 3c77f2d8449..35d2bf2388c 100644 --- a/homeassistant/components/light/zengge.py +++ b/homeassistant/components/light/zengge.py @@ -30,7 +30,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ }) -# pylint: disable=unused-argument def setup_platform(hass, config, add_devices, discovery_info=None): """Set up the Zengge platform.""" lights = [] diff --git a/homeassistant/components/lock/demo.py b/homeassistant/components/lock/demo.py index d561dd333ab..8da53a9ef11 100644 --- a/homeassistant/components/lock/demo.py +++ b/homeassistant/components/lock/demo.py @@ -8,7 +8,6 @@ from homeassistant.components.lock import LockDevice, SUPPORT_OPEN from homeassistant.const import (STATE_LOCKED, STATE_UNLOCKED) -# pylint: disable=unused-argument def setup_platform(hass, config, add_devices, discovery_info=None): """Set up the Demo lock platform.""" add_devices([ diff --git a/homeassistant/components/lock/isy994.py b/homeassistant/components/lock/isy994.py index 50371fdc9ae..79e4308dbda 100644 --- a/homeassistant/components/lock/isy994.py +++ b/homeassistant/components/lock/isy994.py @@ -21,7 +21,6 @@ VALUE_TO_STATE = { } -# pylint: disable=unused-argument def setup_platform(hass, config: ConfigType, add_devices: Callable[[list], None], discovery_info=None): """Set up the ISY994 lock platform.""" diff --git a/homeassistant/components/lock/lockitron.py b/homeassistant/components/lock/lockitron.py index ea79848f60c..6bf445ba477 100644 --- a/homeassistant/components/lock/lockitron.py +++ b/homeassistant/components/lock/lockitron.py @@ -26,7 +26,6 @@ API_STATE_URL = BASE_URL + '/v2/locks/{}?access_token={}' API_ACTION_URL = BASE_URL + '/v2/locks/{}?access_token={}&state={}' -# pylint: disable=unused-argument def setup_platform(hass, config, add_devices, discovery_info=None): """Set up the Lockitron platform.""" access_token = config.get(CONF_ACCESS_TOKEN) diff --git a/homeassistant/components/lock/nello.py b/homeassistant/components/lock/nello.py index 04030c92425..f67243415c5 100644 --- a/homeassistant/components/lock/nello.py +++ b/homeassistant/components/lock/nello.py @@ -27,7 +27,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ }) -# pylint: disable=unused-argument def setup_platform(hass, config, add_devices, discovery_info=None): """Set up the Nello lock platform.""" from pynello import Nello diff --git a/homeassistant/components/lock/nuki.py b/homeassistant/components/lock/nuki.py index 4fe05279919..536c8f2abeb 100644 --- a/homeassistant/components/lock/nuki.py +++ b/homeassistant/components/lock/nuki.py @@ -50,7 +50,6 @@ UNLATCH_SERVICE_SCHEMA = vol.Schema({ }) -# pylint: disable=unused-argument def setup_platform(hass, config, add_devices, discovery_info=None): """Set up the Nuki lock platform.""" from pynuki import NukiBridge diff --git a/homeassistant/components/lock/sesame.py b/homeassistant/components/lock/sesame.py index 5bc40435486..09f7266d15c 100644 --- a/homeassistant/components/lock/sesame.py +++ b/homeassistant/components/lock/sesame.py @@ -24,7 +24,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ }) -# pylint: disable=unused-argument def setup_platform( hass, config: ConfigType, add_devices: Callable[[list], None], discovery_info=None): diff --git a/homeassistant/components/lock/volvooncall.py b/homeassistant/components/lock/volvooncall.py index ab1d2fabefe..b6e7383b138 100644 --- a/homeassistant/components/lock/volvooncall.py +++ b/homeassistant/components/lock/volvooncall.py @@ -12,7 +12,6 @@ from homeassistant.components.volvooncall import VolvoEntity _LOGGER = logging.getLogger(__name__) -# pylint: disable=unused-argument def setup_platform(hass, config, add_devices, discovery_info=None): """Set up the Volvo On Call lock.""" if discovery_info is None: diff --git a/homeassistant/components/logbook.py b/homeassistant/components/logbook.py index bcfae533abf..e2d02acc61c 100644 --- a/homeassistant/components/logbook.py +++ b/homeassistant/components/logbook.py @@ -372,7 +372,6 @@ def _exclude_events(events, config): return filtered_events -# pylint: disable=too-many-return-statements def _entry_message_from_state(domain, state): """Convert a state to a message for the logbook.""" # We pass domain in so we don't have to split entity_id again diff --git a/homeassistant/components/media_player/aquostv.py b/homeassistant/components/media_player/aquostv.py index 6933286f0fe..93daf5b2f89 100644 --- a/homeassistant/components/media_player/aquostv.py +++ b/homeassistant/components/media_player/aquostv.py @@ -59,7 +59,6 @@ SOURCES = {0: 'TV / Antenna', 8: 'PC_IN'} -# pylint: disable=unused-argument def setup_platform(hass, config, add_devices, discovery_info=None): """Set up the Sharp Aquos TV platform.""" import sharp_aquos_rc @@ -104,7 +103,6 @@ def _retry(func): return wrapper -# pylint: disable=abstract-method class SharpAquosTVDevice(MediaPlayerDevice): """Representation of a Aquos TV.""" diff --git a/homeassistant/components/media_player/blackbird.py b/homeassistant/components/media_player/blackbird.py index 1c976f5eecd..3d8e1fde687 100644 --- a/homeassistant/components/media_player/blackbird.py +++ b/homeassistant/components/media_player/blackbird.py @@ -61,7 +61,6 @@ PLATFORM_SCHEMA = vol.All( })) -# pylint: disable=unused-argument def setup_platform(hass, config, add_devices, discovery_info=None): """Set up the Monoprice Blackbird 4k 8x8 HDBaseT Matrix platform.""" if DATA_BLACKBIRD not in hass.data: diff --git a/homeassistant/components/media_player/braviatv.py b/homeassistant/components/media_player/braviatv.py index f0cc93a8b0f..727bda3be3f 100644 --- a/homeassistant/components/media_player/braviatv.py +++ b/homeassistant/components/media_player/braviatv.py @@ -60,7 +60,6 @@ def _get_mac_address(ip_address): return None -# pylint: disable=unused-argument def setup_platform(hass, config, add_devices, discovery_info=None): """Set up the Sony Bravia TV platform.""" host = config.get(CONF_HOST) diff --git a/homeassistant/components/media_player/channels.py b/homeassistant/components/media_player/channels.py index 6b41ace6ce2..41713e0c5bc 100644 --- a/homeassistant/components/media_player/channels.py +++ b/homeassistant/components/media_player/channels.py @@ -105,7 +105,6 @@ def setup_platform(hass, config, add_devices, discovery_info=None): class ChannelsPlayer(MediaPlayerDevice): """Representation of a Channels instance.""" - # pylint: disable=too-many-public-methods def __init__(self, name, host, port): """Initialize the Channels app.""" from pychannels import Channels diff --git a/homeassistant/components/media_player/clementine.py b/homeassistant/components/media_player/clementine.py index 6847b87e54f..1ee18576ab8 100644 --- a/homeassistant/components/media_player/clementine.py +++ b/homeassistant/components/media_player/clementine.py @@ -43,7 +43,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ }) -# pylint: disable=unused-argument def setup_platform(hass, config, add_devices, discovery_info=None): """Set up the Clementine platform.""" from clementineremote import ClementineRemote diff --git a/homeassistant/components/media_player/demo.py b/homeassistant/components/media_player/demo.py index 2c74feae847..405c220c877 100644 --- a/homeassistant/components/media_player/demo.py +++ b/homeassistant/components/media_player/demo.py @@ -14,7 +14,6 @@ from homeassistant.const import STATE_OFF, STATE_PAUSED, STATE_PLAYING import homeassistant.util.dt as dt_util -# pylint: disable=unused-argument def setup_platform(hass, config, add_devices, discovery_info=None): """Set up the media player demo platform.""" add_devices([ diff --git a/homeassistant/components/media_player/dunehd.py b/homeassistant/components/media_player/dunehd.py index efa5e7e6079..ed20ac25cf9 100644 --- a/homeassistant/components/media_player/dunehd.py +++ b/homeassistant/components/media_player/dunehd.py @@ -32,7 +32,6 @@ DUNEHD_PLAYER_SUPPORT = \ SUPPORT_PLAY -# pylint: disable=unused-argument def setup_platform(hass, config, add_devices, discovery_info=None): """Set up the DuneHD media player platform.""" from pdunehd import DuneHDPlayer diff --git a/homeassistant/components/media_player/firetv.py b/homeassistant/components/media_player/firetv.py index 9d66ae77eef..157db2c44d3 100644 --- a/homeassistant/components/media_player/firetv.py +++ b/homeassistant/components/media_player/firetv.py @@ -43,7 +43,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ }) -# pylint: disable=unused-argument def setup_platform(hass, config, add_devices, discovery_info=None): """Set up the FireTV platform.""" name = config.get(CONF_NAME) diff --git a/homeassistant/components/media_player/frontier_silicon.py b/homeassistant/components/media_player/frontier_silicon.py index 6d95ea675fb..ab594f47e14 100644 --- a/homeassistant/components/media_player/frontier_silicon.py +++ b/homeassistant/components/media_player/frontier_silicon.py @@ -41,7 +41,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ }) -# pylint: disable=unused-argument @asyncio.coroutine def async_setup_platform(hass, config, async_add_devices, discovery_info=None): """Set up the Frontier Silicon platform.""" diff --git a/homeassistant/components/media_player/gpmdp.py b/homeassistant/components/media_player/gpmdp.py index 2f116abebc3..4a0ec1fa87f 100644 --- a/homeassistant/components/media_player/gpmdp.py +++ b/homeassistant/components/media_player/gpmdp.py @@ -59,7 +59,6 @@ def request_configuration(hass, config, url, add_devices_callback): 'method': 'connect', 'arguments': ['Home Assistant']})) - # pylint: disable=unused-argument def gpmdp_configuration_callback(callback_data): """Handle configuration changes.""" while True: diff --git a/homeassistant/components/media_player/gstreamer.py b/homeassistant/components/media_player/gstreamer.py index 064ca68ea95..91cd8d19cc4 100644 --- a/homeassistant/components/media_player/gstreamer.py +++ b/homeassistant/components/media_player/gstreamer.py @@ -34,7 +34,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ }) -# pylint: disable=unused-argument def setup_platform(hass, config, add_devices, discovery_info=None): """Set up the Gstreamer platform.""" from gsp import GstreamerPlayer diff --git a/homeassistant/components/media_player/lg_netcast.py b/homeassistant/components/media_player/lg_netcast.py index edbd6546cca..8c98844cf93 100644 --- a/homeassistant/components/media_player/lg_netcast.py +++ b/homeassistant/components/media_player/lg_netcast.py @@ -43,7 +43,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ }) -# pylint: disable=unused-argument def setup_platform(hass, config, add_devices, discovery_info=None): """Set up the LG TV platform.""" from pylgnetcast import LgNetCastClient diff --git a/homeassistant/components/media_player/monoprice.py b/homeassistant/components/media_player/monoprice.py index 44d19ac6860..a951356500f 100644 --- a/homeassistant/components/media_player/monoprice.py +++ b/homeassistant/components/media_player/monoprice.py @@ -55,7 +55,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ }) -# pylint: disable=unused-argument def setup_platform(hass, config, add_devices, discovery_info=None): """Set up the Monoprice 6-zone amplifier platform.""" port = config.get(CONF_PORT) diff --git a/homeassistant/components/media_player/mpchc.py b/homeassistant/components/media_player/mpchc.py index a375a585ad4..ad8dd0bf056 100644 --- a/homeassistant/components/media_player/mpchc.py +++ b/homeassistant/components/media_player/mpchc.py @@ -35,7 +35,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ }) -# pylint: disable=unused-argument def setup_platform(hass, config, add_devices, discovery_info=None): """Set up the MPC-HC platform.""" name = config.get(CONF_NAME) diff --git a/homeassistant/components/media_player/mpd.py b/homeassistant/components/media_player/mpd.py index 04dd1ac5f2e..73417e5f25d 100644 --- a/homeassistant/components/media_player/mpd.py +++ b/homeassistant/components/media_player/mpd.py @@ -46,7 +46,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ }) -# pylint: disable=unused-argument def setup_platform(hass, config, add_devices, discovery_info=None): """Set up the MPD platform.""" host = config.get(CONF_HOST) diff --git a/homeassistant/components/media_player/openhome.py b/homeassistant/components/media_player/openhome.py index 5e30f9783c7..5d9c7bd14c5 100644 --- a/homeassistant/components/media_player/openhome.py +++ b/homeassistant/components/media_player/openhome.py @@ -25,7 +25,6 @@ _LOGGER = logging.getLogger(__name__) DEVICES = [] -# pylint: disable=unused-argument def setup_platform(hass, config, add_devices, discovery_info=None): """Set up the Openhome platform.""" from openhomedevice.Device import Device diff --git a/homeassistant/components/media_player/panasonic_viera.py b/homeassistant/components/media_player/panasonic_viera.py index db60de922d9..549071fde8e 100644 --- a/homeassistant/components/media_player/panasonic_viera.py +++ b/homeassistant/components/media_player/panasonic_viera.py @@ -42,7 +42,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ }) -# pylint: disable=unused-argument def setup_platform(hass, config, add_devices, discovery_info=None): """Set up the Panasonic Viera TV platform.""" from panasonic_viera import RemoteControl diff --git a/homeassistant/components/media_player/pandora.py b/homeassistant/components/media_player/pandora.py index d66811eed66..a47db7f633c 100644 --- a/homeassistant/components/media_player/pandora.py +++ b/homeassistant/components/media_player/pandora.py @@ -43,7 +43,6 @@ CURRENT_SONG_PATTERN = re.compile(r'"(.*?)"\s+by\s+"(.*?)"\son\s+"(.*?)"', STATION_PATTERN = re.compile(r'Station\s"(.+?)"', re.MULTILINE) -# pylint: disable=unused-argument def setup_platform(hass, config, add_devices, discovery_info=None): """Set up the Pandora media player platform.""" if not _pianobar_exists(): diff --git a/homeassistant/components/media_player/philips_js.py b/homeassistant/components/media_player/philips_js.py index 01d63e0b6c8..be0c0527f1b 100644 --- a/homeassistant/components/media_player/philips_js.py +++ b/homeassistant/components/media_player/philips_js.py @@ -48,7 +48,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ }) -# pylint: disable=unused-argument def setup_platform(hass, config, add_devices, discovery_info=None): """Set up the Philips TV platform.""" import haphilipsjs diff --git a/homeassistant/components/media_player/samsungtv.py b/homeassistant/components/media_player/samsungtv.py index 43e9abd96a6..15a2b41795e 100644 --- a/homeassistant/components/media_player/samsungtv.py +++ b/homeassistant/components/media_player/samsungtv.py @@ -47,7 +47,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ }) -# pylint: disable=unused-argument def setup_platform(hass, config, add_devices, discovery_info=None): """Set up the Samsung TV platform.""" known_devices = hass.data.get(KNOWN_DEVICES_KEY) diff --git a/homeassistant/components/media_player/snapcast.py b/homeassistant/components/media_player/snapcast.py index 53a95f7924c..a880d3c920d 100644 --- a/homeassistant/components/media_player/snapcast.py +++ b/homeassistant/components/media_player/snapcast.py @@ -46,7 +46,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ }) -# pylint: disable=unused-argument @asyncio.coroutine def async_setup_platform(hass, config, async_add_devices, discovery_info=None): """Set up the Snapcast platform.""" diff --git a/homeassistant/components/media_player/vlc.py b/homeassistant/components/media_player/vlc.py index abd8252d813..45e1a91c510 100644 --- a/homeassistant/components/media_player/vlc.py +++ b/homeassistant/components/media_player/vlc.py @@ -34,7 +34,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ }) -# pylint: disable=unused-argument def setup_platform(hass, config, add_devices, discovery_info=None): """Set up the vlc platform.""" add_devices([VlcDevice(config.get(CONF_NAME, DEFAULT_NAME), diff --git a/homeassistant/components/media_player/webostv.py b/homeassistant/components/media_player/webostv.py index c3426e45404..42d0ae85ab3 100644 --- a/homeassistant/components/media_player/webostv.py +++ b/homeassistant/components/media_player/webostv.py @@ -61,7 +61,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ }) -# pylint: disable=unused-argument def setup_platform(hass, config, add_devices, discovery_info=None): """Set up the LG WebOS TV platform.""" if discovery_info is not None: @@ -139,7 +138,6 @@ def request_configuration( _CONFIGURING[host], 'Failed to pair, please try again.') return - # pylint: disable=unused-argument def lgtv_configuration_callback(data): """Handle actions when configuration callback is called.""" setup_tv(host, name, customize, config, timeout, hass, diff --git a/homeassistant/components/modbus.py b/homeassistant/components/modbus.py index a928c0d3aca..fe46c858b51 100644 --- a/homeassistant/components/modbus.py +++ b/homeassistant/components/modbus.py @@ -75,11 +75,11 @@ HUB = None def setup(hass, config): """Set up Modbus component.""" # Modbus connection type - # pylint: disable=global-statement, import-error + # pylint: disable=import-error client_type = config[DOMAIN][CONF_TYPE] # Connect to Modbus network - # pylint: disable=global-statement, import-error + # pylint: disable=import-error if client_type == 'serial': from pymodbus.client.sync import ModbusSerialClient as ModbusClient diff --git a/homeassistant/components/notify/message_bird.py b/homeassistant/components/notify/message_bird.py index b20abb52efc..fa747ccba88 100644 --- a/homeassistant/components/notify/message_bird.py +++ b/homeassistant/components/notify/message_bird.py @@ -24,7 +24,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ }) -# pylint: disable=unused-argument def get_service(hass, config, discovery_info=None): """Get the MessageBird notification service.""" import messagebird diff --git a/homeassistant/components/notify/mysensors.py b/homeassistant/components/notify/mysensors.py index 1374779c5f0..db568514dea 100644 --- a/homeassistant/components/notify/mysensors.py +++ b/homeassistant/components/notify/mysensors.py @@ -36,8 +36,6 @@ class MySensorsNotificationDevice(mysensors.MySensorsDevice): class MySensorsNotificationService(BaseNotificationService): """Implement a MySensors notification service.""" - # pylint: disable=too-few-public-methods - def __init__(self, hass): """Initialize the service.""" self.devices = mysensors.get_mysensors_devices(hass, DOMAIN) diff --git a/homeassistant/components/notify/nfandroidtv.py b/homeassistant/components/notify/nfandroidtv.py index 1fa8f1dab78..044a037cc29 100644 --- a/homeassistant/components/notify/nfandroidtv.py +++ b/homeassistant/components/notify/nfandroidtv.py @@ -86,7 +86,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ }) -# pylint: disable=unused-argument def get_service(hass, config, discovery_info=None): """Get the Notifications for Android TV notification service.""" remoteip = config.get(CONF_IP) diff --git a/homeassistant/components/notify/pushbullet.py b/homeassistant/components/notify/pushbullet.py index 37edb6709a7..a94cf4f1055 100644 --- a/homeassistant/components/notify/pushbullet.py +++ b/homeassistant/components/notify/pushbullet.py @@ -29,7 +29,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ }) -# pylint: disable=unused-argument def get_service(hass, config, discovery_info=None): """Get the Pushbullet notification service.""" from pushbullet import PushBullet diff --git a/homeassistant/components/nuimo_controller.py b/homeassistant/components/nuimo_controller.py index ffd7a799413..25e8a230224 100644 --- a/homeassistant/components/nuimo_controller.py +++ b/homeassistant/components/nuimo_controller.py @@ -97,7 +97,6 @@ class NuimoThread(threading.Thread): self._nuimo.disconnect() self._nuimo = None - # pylint: disable=unused-argument def stop(self, event): """Terminate Thread by unsetting flag.""" _LOGGER.debug('Stopping thread for Nuimo %s', self._mac) diff --git a/homeassistant/components/raspihats.py b/homeassistant/components/raspihats.py index 3bc45eab34e..41480c09a32 100644 --- a/homeassistant/components/raspihats.py +++ b/homeassistant/components/raspihats.py @@ -34,7 +34,6 @@ I2C_HAT_NAMES = [ I2C_HATS_MANAGER = 'I2CH_MNG' -# pylint: disable=unused-argument def setup(hass, config): """Set up the raspihats component.""" hass.data[I2C_HATS_MANAGER] = I2CHatsManager() diff --git a/homeassistant/components/remote/demo.py b/homeassistant/components/remote/demo.py index bc67c1646b2..d959d74574f 100644 --- a/homeassistant/components/remote/demo.py +++ b/homeassistant/components/remote/demo.py @@ -8,7 +8,6 @@ from homeassistant.components.remote import RemoteDevice from homeassistant.const import DEVICE_DEFAULT_NAME -# pylint: disable=unused-argument def setup_platform(hass, config, add_devices_callback, discovery_info=None): """Set up the demo remotes.""" add_devices_callback([ diff --git a/homeassistant/components/remote/itach.py b/homeassistant/components/remote/itach.py index 8b91e5356b4..78d277ca65f 100644 --- a/homeassistant/components/remote/itach.py +++ b/homeassistant/components/remote/itach.py @@ -44,7 +44,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ }) -# pylint: disable=unused-argument def setup_platform(hass, config, add_devices, discovery_info=None): """Set up the ITach connection and devices.""" import pyitachip2ir diff --git a/homeassistant/components/scene/lifx_cloud.py b/homeassistant/components/scene/lifx_cloud.py index ffbb10cba4e..6fe91d0acd2 100644 --- a/homeassistant/components/scene/lifx_cloud.py +++ b/homeassistant/components/scene/lifx_cloud.py @@ -29,7 +29,6 @@ PLATFORM_SCHEMA = vol.Schema({ }) -# pylint: disable=unused-argument @asyncio.coroutine def async_setup_platform(hass, config, async_add_devices, discovery_info=None): """Set up the scenes stored in the LIFX Cloud.""" diff --git a/homeassistant/components/sensor/bloomsky.py b/homeassistant/components/sensor/bloomsky.py index b460498c901..d33796d04cc 100644 --- a/homeassistant/components/sensor/bloomsky.py +++ b/homeassistant/components/sensor/bloomsky.py @@ -41,7 +41,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ }) -# pylint: disable=unused-argument def setup_platform(hass, config, add_devices, discovery_info=None): """Set up the available BloomSky weather sensors.""" bloomsky = hass.components.bloomsky diff --git a/homeassistant/components/sensor/broadlink.py b/homeassistant/components/sensor/broadlink.py index 9376687cf13..8806fae5974 100644 --- a/homeassistant/components/sensor/broadlink.py +++ b/homeassistant/components/sensor/broadlink.py @@ -47,7 +47,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ }) -# pylint: disable=unused-argument def setup_platform(hass, config, add_devices, discovery_info=None): """Set up the Broadlink device sensors.""" host = config.get(CONF_HOST) diff --git a/homeassistant/components/sensor/citybikes.py b/homeassistant/components/sensor/citybikes.py index a8bc441b722..24f8ea7e6a9 100644 --- a/homeassistant/components/sensor/citybikes.py +++ b/homeassistant/components/sensor/citybikes.py @@ -125,7 +125,6 @@ def async_citybikes_request(hass, uri, schema): raise CityBikesRequestError -# pylint: disable=unused-argument @asyncio.coroutine def async_setup_platform(hass, config, async_add_devices, discovery_info=None): diff --git a/homeassistant/components/sensor/command_line.py b/homeassistant/components/sensor/command_line.py index f326a57b137..4a26a1dc9fc 100644 --- a/homeassistant/components/sensor/command_line.py +++ b/homeassistant/components/sensor/command_line.py @@ -35,7 +35,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ }) -# pylint: disable=unused-argument def setup_platform(hass, config, add_devices, discovery_info=None): """Set up the Command Sensor.""" name = config.get(CONF_NAME) diff --git a/homeassistant/components/sensor/crimereports.py b/homeassistant/components/sensor/crimereports.py index a2d7315a314..adf7e3c0fa9 100644 --- a/homeassistant/components/sensor/crimereports.py +++ b/homeassistant/components/sensor/crimereports.py @@ -41,7 +41,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ }) -# pylint: disable=unused-argument def setup_platform(hass, config, add_devices, discovery_info=None): """Set up the Crime Reports platform.""" latitude = config.get(CONF_LATITUDE, hass.config.latitude) diff --git a/homeassistant/components/sensor/deluge.py b/homeassistant/components/sensor/deluge.py index 8acbda74d7d..b9109f6428c 100644 --- a/homeassistant/components/sensor/deluge.py +++ b/homeassistant/components/sensor/deluge.py @@ -42,7 +42,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ }) -# pylint: disable=unused-argument def setup_platform(hass, config, add_devices, discovery_info=None): """Set up the Deluge sensors.""" from deluge_client import DelugeRPCClient diff --git a/homeassistant/components/sensor/demo.py b/homeassistant/components/sensor/demo.py index 325d3e0ae58..15cc0ec46ae 100644 --- a/homeassistant/components/sensor/demo.py +++ b/homeassistant/components/sensor/demo.py @@ -10,7 +10,6 @@ from homeassistant.const import ( from homeassistant.helpers.entity import Entity -# pylint: disable=unused-argument def setup_platform(hass, config, add_devices, discovery_info=None): """Set up the Demo sensors.""" add_devices([ diff --git a/homeassistant/components/sensor/eddystone_temperature.py b/homeassistant/components/sensor/eddystone_temperature.py index 2c8ad4781d0..978b8db669a 100644 --- a/homeassistant/components/sensor/eddystone_temperature.py +++ b/homeassistant/components/sensor/eddystone_temperature.py @@ -39,7 +39,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ }) -# pylint: disable=unused-argument def setup_platform(hass, config, add_devices, discovery_info=None): """Validate configuration, create devices and start monitoring thread.""" bt_device_id = config.get("bt_device_id") diff --git a/homeassistant/components/sensor/fedex.py b/homeassistant/components/sensor/fedex.py index f86de1d865c..991588f07f3 100644 --- a/homeassistant/components/sensor/fedex.py +++ b/homeassistant/components/sensor/fedex.py @@ -41,7 +41,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ }) -# pylint: disable=unused-argument def setup_platform(hass, config, add_devices, discovery_info=None): """Set up the Fedex platform.""" import fedexdeliverymanager diff --git a/homeassistant/components/sensor/fints.py b/homeassistant/components/sensor/fints.py index 798f74bb654..13129919139 100644 --- a/homeassistant/components/sensor/fints.py +++ b/homeassistant/components/sensor/fints.py @@ -50,7 +50,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ }) -# pylint: disable=unused-argument def setup_platform(hass, config, add_devices, discovery_info=None): """Set up the sensors. diff --git a/homeassistant/components/sensor/fitbit.py b/homeassistant/components/sensor/fitbit.py index 8d64a8d8229..f312d1f22cc 100644 --- a/homeassistant/components/sensor/fitbit.py +++ b/homeassistant/components/sensor/fitbit.py @@ -156,7 +156,6 @@ def request_app_setup(hass, config, add_devices, config_path, """Assist user with configuring the Fitbit dev application.""" configurator = hass.components.configurator - # pylint: disable=unused-argument def fitbit_configuration_callback(callback_data): """Handle configuration updates.""" config_path = hass.config.path(FITBIT_CONFIG_FILE) @@ -202,7 +201,6 @@ def request_oauth_completion(hass): return - # pylint: disable=unused-argument def fitbit_configuration_callback(callback_data): """Handle configuration updates.""" diff --git a/homeassistant/components/sensor/haveibeenpwned.py b/homeassistant/components/sensor/haveibeenpwned.py index 3b041127a5b..c1fe7ab4880 100644 --- a/homeassistant/components/sensor/haveibeenpwned.py +++ b/homeassistant/components/sensor/haveibeenpwned.py @@ -35,7 +35,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ }) -# pylint: disable=unused-argument def setup_platform(hass, config, add_devices, discovery_info=None): """Set up the HaveIBeenPwned sensor.""" emails = config.get(CONF_EMAIL) diff --git a/homeassistant/components/sensor/hp_ilo.py b/homeassistant/components/sensor/hp_ilo.py index 922ed04a8d9..acd10fe08af 100644 --- a/homeassistant/components/sensor/hp_ilo.py +++ b/homeassistant/components/sensor/hp_ilo.py @@ -59,7 +59,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ }) -# pylint: disable=unused-argument def setup_platform(hass, config, add_devices, discovery_info=None): """Set up the HP ILO sensor.""" hostname = config.get(CONF_HOST) diff --git a/homeassistant/components/sensor/isy994.py b/homeassistant/components/sensor/isy994.py index ecf7bc0b8c2..ca8c19bbc7a 100644 --- a/homeassistant/components/sensor/isy994.py +++ b/homeassistant/components/sensor/isy994.py @@ -235,7 +235,6 @@ UOM_TO_STATES = { } -# pylint: disable=unused-argument def setup_platform(hass, config: ConfigType, add_devices: Callable[[list], None], discovery_info=None): """Set up the ISY994 sensor platform.""" diff --git a/homeassistant/components/sensor/kira.py b/homeassistant/components/sensor/kira.py index b5d3073ea9a..74a1bd19d34 100644 --- a/homeassistant/components/sensor/kira.py +++ b/homeassistant/components/sensor/kira.py @@ -18,7 +18,7 @@ ICON = 'mdi:remote' CONF_SENSOR = 'sensor' -# pylint: disable=unused-argument, too-many-function-args +# pylint: disable=too-many-function-args def setup_platform(hass, config, add_devices, discovery_info=None): """Set up a Kira sensor.""" if discovery_info is not None: diff --git a/homeassistant/components/sensor/knx.py b/homeassistant/components/sensor/knx.py index 8eeb75fb0f1..925b16cb4c7 100644 --- a/homeassistant/components/sensor/knx.py +++ b/homeassistant/components/sensor/knx.py @@ -73,7 +73,6 @@ class KNXSensor(Entity): """Register callbacks to update hass after device was changed.""" async def after_update_callback(device): """Call after device was updated.""" - # pylint: disable=unused-argument await self.async_update_ha_state() self.device.register_device_updated_cb(after_update_callback) diff --git a/homeassistant/components/sensor/lastfm.py b/homeassistant/components/sensor/lastfm.py index 5af81832523..ee9ab146c87 100644 --- a/homeassistant/components/sensor/lastfm.py +++ b/homeassistant/components/sensor/lastfm.py @@ -29,7 +29,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ }) -# pylint: disable=unused-argument def setup_platform(hass, config, add_devices, discovery_info=None): """Set up the Last.fm platform.""" import pylast as lastfm diff --git a/homeassistant/components/sensor/mold_indicator.py b/homeassistant/components/sensor/mold_indicator.py index 057718400c4..2822ce01dca 100644 --- a/homeassistant/components/sensor/mold_indicator.py +++ b/homeassistant/components/sensor/mold_indicator.py @@ -41,7 +41,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ }) -# pylint: disable=unused-argument def setup_platform(hass, config, add_devices, discovery_info=None): """Set up MoldIndicator sensor.""" name = config.get(CONF_NAME, DEFAULT_NAME) diff --git a/homeassistant/components/sensor/mopar.py b/homeassistant/components/sensor/mopar.py index 99ea4ef6135..3e1887cfd59 100644 --- a/homeassistant/components/sensor/mopar.py +++ b/homeassistant/components/sensor/mopar.py @@ -41,7 +41,6 @@ REMOTE_COMMAND_SCHEMA = vol.Schema({ }) -# pylint: disable=unused-argument def setup_platform(hass, config, add_devices, discovery_info=None): """Set up the Mopar platform.""" import motorparts diff --git a/homeassistant/components/sensor/mvglive.py b/homeassistant/components/sensor/mvglive.py index 46d79c1121b..81c7173e4d0 100644 --- a/homeassistant/components/sensor/mvglive.py +++ b/homeassistant/components/sensor/mvglive.py @@ -72,7 +72,6 @@ def setup_platform(hass, config, add_devices, discovery_info=None): add_devices(sensors, True) -# pylint: disable=too-few-public-methods class MVGLiveSensor(Entity): """Implementation of an MVG Live sensor.""" diff --git a/homeassistant/components/sensor/nzbget.py b/homeassistant/components/sensor/nzbget.py index b140d02af04..0fa6362ad05 100644 --- a/homeassistant/components/sensor/nzbget.py +++ b/homeassistant/components/sensor/nzbget.py @@ -50,7 +50,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ }) -# pylint: disable=unused-argument def setup_platform(hass, config, add_devices, discovery_info=None): """Set up the NZBGet sensors.""" host = config.get(CONF_HOST) diff --git a/homeassistant/components/sensor/octoprint.py b/homeassistant/components/sensor/octoprint.py index 8a800e8616c..20d00267dee 100644 --- a/homeassistant/components/sensor/octoprint.py +++ b/homeassistant/components/sensor/octoprint.py @@ -38,7 +38,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ }) -# pylint: disable=unused-argument def setup_platform(hass, config, add_devices, discovery_info=None): """Set up the available OctoPrint sensors.""" octoprint_api = hass.data[DOMAIN]["api"] diff --git a/homeassistant/components/sensor/ohmconnect.py b/homeassistant/components/sensor/ohmconnect.py index ff465b3617c..d323a21a521 100644 --- a/homeassistant/components/sensor/ohmconnect.py +++ b/homeassistant/components/sensor/ohmconnect.py @@ -31,7 +31,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ }) -# pylint: disable=unused-argument def setup_platform(hass, config, add_devices, discovery_info=None): """Set up the OhmConnect sensor.""" name = config.get(CONF_NAME) diff --git a/homeassistant/components/sensor/onewire.py b/homeassistant/components/sensor/onewire.py index 43105d54e38..95ad5f1713d 100644 --- a/homeassistant/components/sensor/onewire.py +++ b/homeassistant/components/sensor/onewire.py @@ -47,7 +47,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ }) -# pylint: disable=unused-argument def setup_platform(hass, config, add_devices, discovery_info=None): """Set up the one wire Sensors.""" base_dir = config.get(CONF_MOUNT_DIR) diff --git a/homeassistant/components/sensor/opensky.py b/homeassistant/components/sensor/opensky.py index bd071ace578..af0491cc26c 100644 --- a/homeassistant/components/sensor/opensky.py +++ b/homeassistant/components/sensor/opensky.py @@ -50,7 +50,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ }) -# pylint: disable=unused-argument def setup_platform(hass, config, add_devices, discovery_info=None): """Set up the Open Sky platform.""" latitude = config.get(CONF_LATITUDE, hass.config.latitude) diff --git a/homeassistant/components/sensor/pilight.py b/homeassistant/components/sensor/pilight.py index 596887998ec..9784cc3dc4c 100644 --- a/homeassistant/components/sensor/pilight.py +++ b/homeassistant/components/sensor/pilight.py @@ -30,7 +30,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ }) -# pylint: disable=unused-argument def setup_platform(hass, config, add_devices, discovery_info=None): """Set up Pilight Sensor.""" add_devices([PilightSensor( diff --git a/homeassistant/components/sensor/plex.py b/homeassistant/components/sensor/plex.py index b61e1bce0da..5aa156a0ac6 100644 --- a/homeassistant/components/sensor/plex.py +++ b/homeassistant/components/sensor/plex.py @@ -41,7 +41,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ }) -# pylint: disable=unused-argument def setup_platform(hass, config, add_devices, discovery_info=None): """Set up the Plex sensor.""" name = config.get(CONF_NAME) diff --git a/homeassistant/components/sensor/postnl.py b/homeassistant/components/sensor/postnl.py index 63a9c1d67d5..0e296fa56bd 100644 --- a/homeassistant/components/sensor/postnl.py +++ b/homeassistant/components/sensor/postnl.py @@ -35,7 +35,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ }) -# pylint: disable=unused-argument def setup_platform(hass, config, add_devices, discovery_info=None): """Set up the PostNL sensor platform.""" from postnl_api import PostNL_API, UnauthorizedException diff --git a/homeassistant/components/sensor/pyload.py b/homeassistant/components/sensor/pyload.py index 9e1c0875169..cc4ce1e6448 100644 --- a/homeassistant/components/sensor/pyload.py +++ b/homeassistant/components/sensor/pyload.py @@ -43,7 +43,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ }) -# pylint: disable=unused-argument def setup_platform(hass, config, add_devices, discovery_info=None): """Set up the pyLoad sensors.""" host = config.get(CONF_HOST) diff --git a/homeassistant/components/sensor/qnap.py b/homeassistant/components/sensor/qnap.py index 7dd795d8f8d..3d9704875c9 100644 --- a/homeassistant/components/sensor/qnap.py +++ b/homeassistant/components/sensor/qnap.py @@ -102,7 +102,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ }) -# pylint: disable=unused-argument def setup_platform(hass, config, add_devices, discovery_info=None): """Set up the QNAP NAS sensor.""" api = QNAPStatsAPI(config) diff --git a/homeassistant/components/sensor/skybeacon.py b/homeassistant/components/sensor/skybeacon.py index 61933614a74..53cbaab19a5 100644 --- a/homeassistant/components/sensor/skybeacon.py +++ b/homeassistant/components/sensor/skybeacon.py @@ -39,7 +39,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ }) -# pylint: disable=unused-argument def setup_platform(hass, config, add_devices, discovery_info=None): """Set up the Skybeacon sensor.""" # pylint: disable=unreachable diff --git a/homeassistant/components/sensor/spotcrime.py b/homeassistant/components/sensor/spotcrime.py index 08177c9a7b9..daa520f2ede 100644 --- a/homeassistant/components/sensor/spotcrime.py +++ b/homeassistant/components/sensor/spotcrime.py @@ -44,7 +44,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ }) -# pylint: disable=unused-argument def setup_platform(hass, config, add_devices, discovery_info=None): """Set up the Crime Reports platform.""" latitude = config.get(CONF_LATITUDE, hass.config.latitude) diff --git a/homeassistant/components/sensor/steam_online.py b/homeassistant/components/sensor/steam_online.py index 88cb786e66d..e22e1594b55 100644 --- a/homeassistant/components/sensor/steam_online.py +++ b/homeassistant/components/sensor/steam_online.py @@ -36,7 +36,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ }) -# pylint: disable=unused-argument def setup_platform(hass, config, add_devices, discovery_info=None): """Set up the Steam platform.""" import steam as steamod diff --git a/homeassistant/components/sensor/supervisord.py b/homeassistant/components/sensor/supervisord.py index fd0c6292de2..5a302462bbf 100644 --- a/homeassistant/components/sensor/supervisord.py +++ b/homeassistant/components/sensor/supervisord.py @@ -26,7 +26,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ }) -# pylint: disable=unused-argument def setup_platform(hass, config, add_devices, discovery_info=None): """Set up the Supervisord platform.""" url = config.get(CONF_URL) diff --git a/homeassistant/components/sensor/systemmonitor.py b/homeassistant/components/sensor/systemmonitor.py index 517ee6509f7..1883ee89d4e 100644 --- a/homeassistant/components/sensor/systemmonitor.py +++ b/homeassistant/components/sensor/systemmonitor.py @@ -67,7 +67,6 @@ IF_ADDRS = { } -# pylint: disable=unused-argument def setup_platform(hass, config, add_devices, discovery_info=None): """Set up the system monitor sensors.""" dev = [] diff --git a/homeassistant/components/sensor/tellstick.py b/homeassistant/components/sensor/tellstick.py index 8355add47e9..de929aa0942 100644 --- a/homeassistant/components/sensor/tellstick.py +++ b/homeassistant/components/sensor/tellstick.py @@ -37,7 +37,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ }) -# pylint: disable=unused-argument def setup_platform(hass, config, add_devices, discovery_info=None): """Set up the Tellstick sensors.""" import tellcore.telldus as telldus diff --git a/homeassistant/components/sensor/temper.py b/homeassistant/components/sensor/temper.py index 973e07d9cf3..f0a3e15834c 100644 --- a/homeassistant/components/sensor/temper.py +++ b/homeassistant/components/sensor/temper.py @@ -33,7 +33,6 @@ def get_temper_devices(): return TemperHandler().get_devices() -# pylint: disable=unused-argument def setup_platform(hass, config, add_devices, discovery_info=None): """Set up the Temper sensors.""" temp_unit = hass.config.units.temperature_unit diff --git a/homeassistant/components/sensor/template.py b/homeassistant/components/sensor/template.py index 65f49998dbf..23c7c13f0ed 100644 --- a/homeassistant/components/sensor/template.py +++ b/homeassistant/components/sensor/template.py @@ -42,7 +42,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ @asyncio.coroutine -# pylint: disable=unused-argument def async_setup_platform(hass, config, async_add_devices, discovery_info=None): """Set up the template sensors.""" sensors = [] diff --git a/homeassistant/components/sensor/torque.py b/homeassistant/components/sensor/torque.py index 98fad475d52..4ed1b5907cf 100644 --- a/homeassistant/components/sensor/torque.py +++ b/homeassistant/components/sensor/torque.py @@ -46,7 +46,6 @@ def convert_pid(value): return int(value, 16) -# pylint: disable=unused-argument def setup_platform(hass, config, add_devices, discovery_info=None): """Set up the Torque platform.""" vehicle = config.get(CONF_NAME) diff --git a/homeassistant/components/sensor/twitch.py b/homeassistant/components/sensor/twitch.py index b3e227aea72..250911b49b1 100644 --- a/homeassistant/components/sensor/twitch.py +++ b/homeassistant/components/sensor/twitch.py @@ -31,7 +31,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ }) -# pylint: disable=unused-argument def setup_platform(hass, config, add_devices, discovery_info=None): """Set up the Twitch platform.""" channels = config.get(CONF_CHANNELS, []) diff --git a/homeassistant/components/sensor/ups.py b/homeassistant/components/sensor/ups.py index c51ae67475f..a864df384ad 100644 --- a/homeassistant/components/sensor/ups.py +++ b/homeassistant/components/sensor/ups.py @@ -38,7 +38,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ }) -# pylint: disable=unused-argument def setup_platform(hass, config, add_devices, discovery_info=None): """Set up the UPS platform.""" import upsmychoice diff --git a/homeassistant/components/sensor/worldtidesinfo.py b/homeassistant/components/sensor/worldtidesinfo.py index 8884d790eed..05d61173da0 100644 --- a/homeassistant/components/sensor/worldtidesinfo.py +++ b/homeassistant/components/sensor/worldtidesinfo.py @@ -31,7 +31,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ }) -# pylint: disable=unused-argument def setup_platform(hass, config, add_devices, discovery_info=None): """Set up the WorldTidesInfo sensor.""" name = config.get(CONF_NAME) diff --git a/homeassistant/components/sensor/xbox_live.py b/homeassistant/components/sensor/xbox_live.py index 0c7b8b48f62..250c74ee493 100644 --- a/homeassistant/components/sensor/xbox_live.py +++ b/homeassistant/components/sensor/xbox_live.py @@ -27,7 +27,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ }) -# pylint: disable=unused-argument def setup_platform(hass, config, add_devices, discovery_info=None): """Set up the Xbox platform.""" from xboxapi import xbox_api diff --git a/homeassistant/components/sensor/xiaomi_miio.py b/homeassistant/components/sensor/xiaomi_miio.py index f7bc9488cc5..a70d701fac6 100644 --- a/homeassistant/components/sensor/xiaomi_miio.py +++ b/homeassistant/components/sensor/xiaomi_miio.py @@ -36,7 +36,6 @@ ATTR_MODEL = 'model' SUCCESS = ['ok'] -# pylint: disable=unused-argument async def async_setup_platform(hass, config, async_add_devices, discovery_info=None): """Set up the sensor from config.""" diff --git a/homeassistant/components/sleepiq.py b/homeassistant/components/sleepiq.py index 3b74b79b36b..df36eef2f9e 100644 --- a/homeassistant/components/sleepiq.py +++ b/homeassistant/components/sleepiq.py @@ -51,7 +51,6 @@ def setup(hass, config): Will automatically load sensor components to support devices discovered on the account. """ - # pylint: disable=global-statement global DATA from sleepyq import Sleepyq diff --git a/homeassistant/components/switch/bbb_gpio.py b/homeassistant/components/switch/bbb_gpio.py index 6dc5df4ffe3..5412f559b73 100644 --- a/homeassistant/components/switch/bbb_gpio.py +++ b/homeassistant/components/switch/bbb_gpio.py @@ -34,7 +34,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ }) -# pylint: disable=unused-argument def setup_platform(hass, config, add_devices, discovery_info=None): """Set up the BeagleBone Black GPIO devices.""" pins = config.get(CONF_PINS) diff --git a/homeassistant/components/switch/command_line.py b/homeassistant/components/switch/command_line.py index 478b1c6e9ad..127c7578940 100644 --- a/homeassistant/components/switch/command_line.py +++ b/homeassistant/components/switch/command_line.py @@ -32,7 +32,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ }) -# pylint: disable=unused-argument def setup_platform(hass, config, add_devices, discovery_info=None): """Find and return switches controlled by shell commands.""" devices = config.get(CONF_SWITCHES, {}) diff --git a/homeassistant/components/switch/deluge.py b/homeassistant/components/switch/deluge.py index da0b3bf3228..c71c3865f5d 100644 --- a/homeassistant/components/switch/deluge.py +++ b/homeassistant/components/switch/deluge.py @@ -32,7 +32,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ }) -# pylint: disable=unused-argument def setup_platform(hass, config, add_devices, discovery_info=None): """Set up the Deluge switch.""" from deluge_client import DelugeRPCClient diff --git a/homeassistant/components/switch/demo.py b/homeassistant/components/switch/demo.py index 83b8ae796bb..7e22f962330 100644 --- a/homeassistant/components/switch/demo.py +++ b/homeassistant/components/switch/demo.py @@ -8,7 +8,6 @@ from homeassistant.components.switch import SwitchDevice from homeassistant.const import DEVICE_DEFAULT_NAME -# pylint: disable=unused-argument def setup_platform(hass, config, add_devices_callback, discovery_info=None): """Set up the demo switches.""" add_devices_callback([ diff --git a/homeassistant/components/switch/dlink.py b/homeassistant/components/switch/dlink.py index 5d727e72138..1c7253c4ec3 100644 --- a/homeassistant/components/switch/dlink.py +++ b/homeassistant/components/switch/dlink.py @@ -36,7 +36,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ }) -# pylint: disable=unused-argument def setup_platform(hass, config, add_devices, discovery_info=None): """Set up a D-Link Smart Plug.""" from pyW215.pyW215 import SmartPlug diff --git a/homeassistant/components/switch/edimax.py b/homeassistant/components/switch/edimax.py index 40ebb54b603..9cd7c484886 100644 --- a/homeassistant/components/switch/edimax.py +++ b/homeassistant/components/switch/edimax.py @@ -29,7 +29,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ }) -# pylint: disable=unused-argument def setup_platform(hass, config, add_devices, discovery_info=None): """Find and return Edimax Smart Plugs.""" from pyedimax.smartplug import SmartPlug diff --git a/homeassistant/components/switch/flux.py b/homeassistant/components/switch/flux.py index 21689dcca0f..f57843cdaa0 100644 --- a/homeassistant/components/switch/flux.py +++ b/homeassistant/components/switch/flux.py @@ -95,7 +95,6 @@ def set_lights_rgb(hass, lights, rgb, transition): transition=transition) -# pylint: disable=unused-argument def setup_platform(hass, config, add_devices, discovery_info=None): """Set up the Flux switches.""" name = config.get(CONF_NAME) diff --git a/homeassistant/components/switch/gc100.py b/homeassistant/components/switch/gc100.py index f4175926aa0..54c3b5e942a 100644 --- a/homeassistant/components/switch/gc100.py +++ b/homeassistant/components/switch/gc100.py @@ -23,7 +23,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ }) -# pylint: disable=unused-argument def setup_platform(hass, config, add_devices, discovery_info=None): """Set up the GC100 devices.""" switches = [] diff --git a/homeassistant/components/switch/isy994.py b/homeassistant/components/switch/isy994.py index efdda6ed40c..3d29c53bd7c 100644 --- a/homeassistant/components/switch/isy994.py +++ b/homeassistant/components/switch/isy994.py @@ -15,7 +15,6 @@ from homeassistant.helpers.typing import ConfigType # noqa _LOGGER = logging.getLogger(__name__) -# pylint: disable=unused-argument def setup_platform(hass, config: ConfigType, add_devices: Callable[[list], None], discovery_info=None): """Set up the ISY994 switch platform.""" diff --git a/homeassistant/components/switch/kankun.py b/homeassistant/components/switch/kankun.py index 88a07b68cd9..c830e2299f6 100644 --- a/homeassistant/components/switch/kankun.py +++ b/homeassistant/components/switch/kankun.py @@ -34,7 +34,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ }) -# pylint: disable=unused-argument def setup_platform(hass, config, add_devices_callback, discovery_info=None): """Set up Kankun Wifi switches.""" switches = config.get('switches', {}) diff --git a/homeassistant/components/switch/knx.py b/homeassistant/components/switch/knx.py index a96f96a9c5c..c13631ca5e6 100644 --- a/homeassistant/components/switch/knx.py +++ b/homeassistant/components/switch/knx.py @@ -72,7 +72,6 @@ class KNXSwitch(SwitchDevice): """Register callbacks to update hass after device was changed.""" async def after_update_callback(device): """Call after device was updated.""" - # pylint: disable=unused-argument await self.async_update_ha_state() self.device.register_device_updated_cb(after_update_callback) diff --git a/homeassistant/components/switch/lutron_caseta.py b/homeassistant/components/switch/lutron_caseta.py index da36c76f41d..f5e7cf2836f 100644 --- a/homeassistant/components/switch/lutron_caseta.py +++ b/homeassistant/components/switch/lutron_caseta.py @@ -16,7 +16,6 @@ _LOGGER = logging.getLogger(__name__) DEPENDENCIES = ['lutron_caseta'] -# pylint: disable=unused-argument @asyncio.coroutine def async_setup_platform(hass, config, async_add_devices, discovery_info=None): """Set up Lutron switch.""" diff --git a/homeassistant/components/switch/orvibo.py b/homeassistant/components/switch/orvibo.py index e039a29809d..fdb4752f594 100644 --- a/homeassistant/components/switch/orvibo.py +++ b/homeassistant/components/switch/orvibo.py @@ -31,7 +31,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ }) -# pylint: disable=unused-argument def setup_platform(hass, config, add_devices_callback, discovery_info=None): """Set up S20 switches.""" from orvibo.s20 import discover, S20, S20Exception diff --git a/homeassistant/components/switch/pulseaudio_loopback.py b/homeassistant/components/switch/pulseaudio_loopback.py index 007e74e14fd..e25368f3c5c 100644 --- a/homeassistant/components/switch/pulseaudio_loopback.py +++ b/homeassistant/components/switch/pulseaudio_loopback.py @@ -54,7 +54,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ }) -# pylint: disable=unused-argument def setup_platform(hass, config, add_devices, discovery_info=None): """Read in all of our configuration, and initialize the loopback switch.""" name = config.get(CONF_NAME) diff --git a/homeassistant/components/switch/raspihats.py b/homeassistant/components/switch/raspihats.py index 7be3a6f0baa..7173ad35daf 100644 --- a/homeassistant/components/switch/raspihats.py +++ b/homeassistant/components/switch/raspihats.py @@ -39,7 +39,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ }) -# pylint: disable=unused-argument def setup_platform(hass, config, add_devices, discovery_info=None): """Set up the raspihats switch devices.""" I2CHatSwitch.I2C_HATS_MANAGER = hass.data[I2C_HATS_MANAGER] diff --git a/homeassistant/components/switch/rest.py b/homeassistant/components/switch/rest.py index 9c589d1d95b..914408406a9 100644 --- a/homeassistant/components/switch/rest.py +++ b/homeassistant/components/switch/rest.py @@ -47,7 +47,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ }) -# pylint: disable=unused-argument @asyncio.coroutine def async_setup_platform(hass, config, async_add_devices, discovery_info=None): """Set up the RESTful switch.""" diff --git a/homeassistant/components/switch/rpi_gpio.py b/homeassistant/components/switch/rpi_gpio.py index ac38da1c6a7..26de2a78e18 100644 --- a/homeassistant/components/switch/rpi_gpio.py +++ b/homeassistant/components/switch/rpi_gpio.py @@ -34,7 +34,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ }) -# pylint: disable=unused-argument def setup_platform(hass, config, add_devices, discovery_info=None): """Set up the Raspberry PI GPIO devices.""" invert_logic = config.get(CONF_INVERT_LOGIC) diff --git a/homeassistant/components/switch/rpi_rf.py b/homeassistant/components/switch/rpi_rf.py index 40200f05806..62c92ad2d96 100644 --- a/homeassistant/components/switch/rpi_rf.py +++ b/homeassistant/components/switch/rpi_rf.py @@ -44,7 +44,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ }) -# pylint: disable=unused-argument, import-error, no-member +# pylint: disable=import-error, no-member def setup_platform(hass, config, add_devices, discovery_info=None): """Find and return switches controlled by a generic RF device via GPIO.""" import rpi_rf diff --git a/homeassistant/components/switch/tellstick.py b/homeassistant/components/switch/tellstick.py index ae19e77c2e5..5f7930a8a7c 100644 --- a/homeassistant/components/switch/tellstick.py +++ b/homeassistant/components/switch/tellstick.py @@ -10,7 +10,6 @@ from homeassistant.components.tellstick import ( from homeassistant.helpers.entity import ToggleEntity -# pylint: disable=unused-argument def setup_platform(hass, config, add_devices, discovery_info=None): """Set up Tellstick switches.""" if (discovery_info is None or diff --git a/homeassistant/components/switch/telnet.py b/homeassistant/components/switch/telnet.py index c3a608b9692..381f2ec9bec 100644 --- a/homeassistant/components/switch/telnet.py +++ b/homeassistant/components/switch/telnet.py @@ -38,7 +38,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ SCAN_INTERVAL = timedelta(seconds=10) -# pylint: disable=unused-argument def setup_platform(hass, config, add_devices, discovery_info=None): """Find and return switches controlled by telnet commands.""" devices = config.get(CONF_SWITCHES, {}) diff --git a/homeassistant/components/switch/template.py b/homeassistant/components/switch/template.py index 93ebf98e9ac..a6fa8241940 100644 --- a/homeassistant/components/switch/template.py +++ b/homeassistant/components/switch/template.py @@ -44,7 +44,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ @asyncio.coroutine -# pylint: disable=unused-argument def async_setup_platform(hass, config, async_add_devices, discovery_info=None): """Set up the Template switch.""" switches = [] diff --git a/homeassistant/components/switch/tplink.py b/homeassistant/components/switch/tplink.py index 1eca5284f76..cd2a0f189fc 100644 --- a/homeassistant/components/switch/tplink.py +++ b/homeassistant/components/switch/tplink.py @@ -32,7 +32,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ }) -# pylint: disable=unused-argument def setup_platform(hass, config, add_devices, discovery_info=None): """Set up the TPLink switch platform.""" from pyHS100 import SmartPlug diff --git a/homeassistant/components/switch/transmission.py b/homeassistant/components/switch/transmission.py index 840fdae44d9..ffe285a23f3 100644 --- a/homeassistant/components/switch/transmission.py +++ b/homeassistant/components/switch/transmission.py @@ -31,7 +31,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ }) -# pylint: disable=unused-argument def setup_platform(hass, config, add_devices, discovery_info=None): """Set up the Transmission switch.""" import transmissionrpc diff --git a/homeassistant/components/switch/wemo.py b/homeassistant/components/switch/wemo.py index 4f06f941558..569566bcbfb 100644 --- a/homeassistant/components/switch/wemo.py +++ b/homeassistant/components/switch/wemo.py @@ -33,7 +33,7 @@ WEMO_OFF = 0 WEMO_STANDBY = 8 -# pylint: disable=unused-argument, too-many-function-args +# pylint: disable=too-many-function-args def setup_platform(hass, config, add_devices_callback, discovery_info=None): """Set up discovered WeMo switches.""" import pywemo.discovery as discovery diff --git a/homeassistant/components/switch/xiaomi_miio.py b/homeassistant/components/switch/xiaomi_miio.py index b0d251822b0..1e11b844fdf 100644 --- a/homeassistant/components/switch/xiaomi_miio.py +++ b/homeassistant/components/switch/xiaomi_miio.py @@ -97,7 +97,6 @@ SERVICE_TO_METHOD = { } -# pylint: disable=unused-argument async def async_setup_platform(hass, config, async_add_devices, discovery_info=None): """Set up the switch from config.""" diff --git a/homeassistant/components/vera.py b/homeassistant/components/vera.py index 2603f61eb75..cbbf279bb8c 100644 --- a/homeassistant/components/vera.py +++ b/homeassistant/components/vera.py @@ -53,7 +53,7 @@ VERA_COMPONENTS = [ ] -# pylint: disable=unused-argument, too-many-function-args +# pylint: disable=too-many-function-args def setup(hass, base_config): """Set up for Vera devices.""" import pyvera as veraApi diff --git a/homeassistant/components/wemo.py b/homeassistant/components/wemo.py index 9929b64be7d..15b75b2f7a8 100644 --- a/homeassistant/components/wemo.py +++ b/homeassistant/components/wemo.py @@ -44,7 +44,7 @@ CONFIG_SCHEMA = vol.Schema({ }, extra=vol.ALLOW_EXTRA) -# pylint: disable=unused-argument, too-many-function-args +# pylint: disable=too-many-function-args def setup(hass, config): """Set up for WeMo devices.""" import pywemo diff --git a/homeassistant/components/wink/__init__.py b/homeassistant/components/wink/__init__.py index f3ec360462e..e4dfc17246a 100644 --- a/homeassistant/components/wink/__init__.py +++ b/homeassistant/components/wink/__init__.py @@ -210,7 +210,6 @@ def _request_oauth_completion(hass, config): "Failed to register, please try again.") return - # pylint: disable=unused-argument def wink_configuration_callback(callback_data): """Call setup again.""" setup(hass, config) diff --git a/homeassistant/core.py b/homeassistant/core.py index bc3b598180c..5e6dcd81310 100644 --- a/homeassistant/core.py +++ b/homeassistant/core.py @@ -4,7 +4,7 @@ Core components of Home Assistant. Home Assistant is a Home Automation framework for observing the state of entities and react to changes. """ -# pylint: disable=unused-import, too-many-lines +# pylint: disable=unused-import import asyncio from concurrent.futures import ThreadPoolExecutor import enum diff --git a/homeassistant/util/dt.py b/homeassistant/util/dt.py index 7b5b996a3a3..cd440783cc3 100644 --- a/homeassistant/util/dt.py +++ b/homeassistant/util/dt.py @@ -27,7 +27,7 @@ def set_default_time_zone(time_zone: dt.tzinfo) -> None: Async friendly. """ - global DEFAULT_TIME_ZONE # pylint: disable=global-statement + global DEFAULT_TIME_ZONE # NOTE: Remove in the future in favour of typing assert isinstance(time_zone, dt.tzinfo) From 2ec295a6f8bdfa8d01fef2eb9baa7835160204b0 Mon Sep 17 00:00:00 2001 From: Johan Bloemberg Date: Sat, 16 Jun 2018 00:26:48 +0200 Subject: [PATCH 100/144] Add availability to Rflink entities. (#14977) --- homeassistant/components/rflink.py | 31 ++++++++++++++++++++ tests/components/sensor/test_rflink.py | 40 +++++++++++++++++++++++++- 2 files changed, 70 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/rflink.py b/homeassistant/components/rflink.py index 87e2a7a2331..272a5b868ec 100644 --- a/homeassistant/components/rflink.py +++ b/homeassistant/components/rflink.py @@ -20,6 +20,8 @@ from homeassistant.exceptions import HomeAssistantError import homeassistant.helpers.config_validation as cv from homeassistant.helpers.deprecation import get_deprecated from homeassistant.helpers.entity import Entity +from homeassistant.helpers.dispatcher import ( + async_dispatcher_send, async_dispatcher_connect) REQUIREMENTS = ['rflink==0.0.37'] @@ -65,6 +67,8 @@ DOMAIN = 'rflink' SERVICE_SEND_COMMAND = 'send_command' +SIGNAL_AVAILABILITY = 'rflink_device_available' + DEVICE_DEFAULTS_SCHEMA = vol.Schema({ vol.Optional(CONF_FIRE_EVENT, default=False): cv.boolean, vol.Optional(CONF_SIGNAL_REPETITIONS, @@ -185,6 +189,8 @@ def async_setup(hass, config): # Reset protocol binding before starting reconnect RflinkCommand.set_rflink_protocol(None) + async_dispatcher_send(hass, SIGNAL_AVAILABILITY, False) + # If HA is not stopping, initiate new connection if hass.state != CoreState.stopping: _LOGGER.warning('disconnected from Rflink, reconnecting') @@ -219,9 +225,16 @@ def async_setup(hass, config): _LOGGER.exception( "Error connecting to Rflink, reconnecting in %s", reconnect_interval) + # Connection to Rflink device is lost, make entities unavailable + async_dispatcher_send(hass, SIGNAL_AVAILABILITY, False) + hass.loop.call_later(reconnect_interval, reconnect, exc) return + # There is a valid connection to a Rflink device now so + # mark entities as available + async_dispatcher_send(hass, SIGNAL_AVAILABILITY, True) + # Bind protocol to command class to allow entities to send commands RflinkCommand.set_rflink_protocol( protocol, config[DOMAIN][CONF_WAIT_FOR_ACK]) @@ -244,6 +257,7 @@ class RflinkDevice(Entity): platform = None _state = STATE_UNKNOWN + _available = True def __init__(self, device_id, hass, name=None, aliases=None, group=True, group_aliases=None, nogroup_aliases=None, fire_event=False, @@ -305,6 +319,23 @@ class RflinkDevice(Entity): """Assume device state until first device event sets state.""" return self._state is STATE_UNKNOWN + @property + def available(self): + """Return True if entity is available.""" + return self._available + + @callback + def set_availability(self, availability): + """Update availability state.""" + self._available = availability + self.async_schedule_update_ha_state() + + @asyncio.coroutine + def async_added_to_hass(self): + """Register update callback.""" + async_dispatcher_connect(self.hass, SIGNAL_AVAILABILITY, + self.set_availability) + class RflinkCommand(RflinkDevice): """Singleton class to make Rflink command interface available to entities. diff --git a/tests/components/sensor/test_rflink.py b/tests/components/sensor/test_rflink.py index a99d14cc735..a250a75ab99 100644 --- a/tests/components/sensor/test_rflink.py +++ b/tests/components/sensor/test_rflink.py @@ -8,6 +8,9 @@ automatic sensor creation. import asyncio from ..test_rflink import mock_rflink +from homeassistant.components.rflink import ( + CONF_RECONNECT_INTERVAL) +from homeassistant.const import STATE_UNKNOWN DOMAIN = 'sensor' @@ -32,7 +35,7 @@ CONFIG = { def test_default_setup(hass, monkeypatch): """Test all basic functionality of the rflink sensor component.""" # setup mocking rflink module - event_callback, create, _, _ = yield from mock_rflink( + event_callback, create, _, disconnect_callback = yield from mock_rflink( hass, CONFIG, DOMAIN, monkeypatch) # make sure arguments are passed @@ -100,3 +103,38 @@ def test_disable_automatic_add(hass, monkeypatch): # make sure new device is not added assert not hass.states.get('sensor.test2') + + +@asyncio.coroutine +def test_entity_availability(hass, monkeypatch): + """If Rflink device is disconnected, entities should become unavailable.""" + # Make sure Rflink mock does not 'recover' to quickly from the + # disconnect or else the unavailability cannot be measured + config = CONFIG + failures = [True, True] + config[CONF_RECONNECT_INTERVAL] = 60 + + # Create platform and entities + event_callback, create, _, disconnect_callback = yield from mock_rflink( + hass, config, DOMAIN, monkeypatch, failures=failures) + + # Entities are available by default + assert hass.states.get('sensor.test').state == STATE_UNKNOWN + + # Mock a disconnect of the Rflink device + disconnect_callback() + + # Wait for dispatch events to propagate + yield from hass.async_block_till_done() + + # Entity should be unavailable + assert hass.states.get('sensor.test').state == 'unavailable' + + # Reconnect the Rflink device + disconnect_callback() + + # Wait for dispatch events to propagate + yield from hass.async_block_till_done() + + # Entities should be available again + assert hass.states.get('sensor.test').state == STATE_UNKNOWN From 2839f0ff5fec87a7c636a9713d6c340f351e86c1 Mon Sep 17 00:00:00 2001 From: Marcelo Moreira de Mello Date: Sat, 16 Jun 2018 02:58:39 -0400 Subject: [PATCH 101/144] Upgrade ring_doorbell to 0.2.1 to fix oauth issues (#14984) * Upgraded to ring_doorbell to 0.2.1 to fix oauth issues * Updated unittest to cover Ring oauth --- homeassistant/components/ring.py | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/binary_sensor/test_ring.py | 2 ++ tests/components/sensor/test_ring.py | 2 ++ tests/components/test_ring.py | 2 ++ tests/fixtures/ring_oauth.json | 8 ++++++++ 7 files changed, 17 insertions(+), 3 deletions(-) create mode 100644 tests/fixtures/ring_oauth.json diff --git a/homeassistant/components/ring.py b/homeassistant/components/ring.py index 1a15e22fca0..3bfa1372fab 100644 --- a/homeassistant/components/ring.py +++ b/homeassistant/components/ring.py @@ -12,7 +12,7 @@ import voluptuous as vol import homeassistant.helpers.config_validation as cv from homeassistant.const import CONF_USERNAME, CONF_PASSWORD -REQUIREMENTS = ['ring_doorbell==0.1.8'] +REQUIREMENTS = ['ring_doorbell==0.2.1'] _LOGGER = logging.getLogger(__name__) diff --git a/requirements_all.txt b/requirements_all.txt index 921bbf8fd46..8d31dd7e267 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1189,7 +1189,7 @@ restrictedpython==4.0b4 rflink==0.0.37 # homeassistant.components.ring -ring_doorbell==0.1.8 +ring_doorbell==0.2.1 # homeassistant.components.notify.rocketchat rocketchat-API==0.6.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 02f079dd9a6..1b32efe9577 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -180,7 +180,7 @@ restrictedpython==4.0b4 rflink==0.0.37 # homeassistant.components.ring -ring_doorbell==0.1.8 +ring_doorbell==0.2.1 # homeassistant.components.media_player.yamaha rxv==0.5.1 diff --git a/tests/components/binary_sensor/test_ring.py b/tests/components/binary_sensor/test_ring.py index 889282b56dd..e557050ae48 100644 --- a/tests/components/binary_sensor/test_ring.py +++ b/tests/components/binary_sensor/test_ring.py @@ -44,6 +44,8 @@ class TestRingBinarySensorSetup(unittest.TestCase): @requests_mock.Mocker() def test_binary_sensor(self, mock): """Test the Ring sensor class and methods.""" + mock.post('https://oauth.ring.com/oauth/token', + text=load_fixture('ring_oauth.json')) mock.post('https://api.ring.com/clients_api/session', text=load_fixture('ring_session.json')) mock.get('https://api.ring.com/clients_api/ring_devices', diff --git a/tests/components/sensor/test_ring.py b/tests/components/sensor/test_ring.py index 0cce0ea681d..4d34018ce52 100644 --- a/tests/components/sensor/test_ring.py +++ b/tests/components/sensor/test_ring.py @@ -51,6 +51,8 @@ class TestRingSensorSetup(unittest.TestCase): @requests_mock.Mocker() def test_sensor(self, mock): """Test the Ring sensor class and methods.""" + mock.post('https://oauth.ring.com/oauth/token', + text=load_fixture('ring_oauth.json')) mock.post('https://api.ring.com/clients_api/session', text=load_fixture('ring_session.json')) mock.get('https://api.ring.com/clients_api/ring_devices', diff --git a/tests/components/test_ring.py b/tests/components/test_ring.py index 3837ec13061..7b974686a4e 100644 --- a/tests/components/test_ring.py +++ b/tests/components/test_ring.py @@ -42,6 +42,8 @@ class TestRing(unittest.TestCase): @requests_mock.Mocker() def test_setup(self, mock): """Test the setup.""" + mock.post('https://oauth.ring.com/oauth/token', + text=load_fixture('ring_oauth.json')) mock.post('https://api.ring.com/clients_api/session', text=load_fixture('ring_session.json')) response = ring.setup(self.hass, self.config) diff --git a/tests/fixtures/ring_oauth.json b/tests/fixtures/ring_oauth.json new file mode 100644 index 00000000000..5e69ddde065 --- /dev/null +++ b/tests/fixtures/ring_oauth.json @@ -0,0 +1,8 @@ +{ + "access_token": "eyJ0eWfvEQwqfJNKyQ9999", + "token_type": "bearer", + "expires_in": 3600, + "refresh_token": "67695a26bdefc1ac8999", + "scope": "client", + "created_at": 1529099870 +} From 7d9bce2153943a4d951b4b38bebe1ca369185458 Mon Sep 17 00:00:00 2001 From: cdce8p <30130371+cdce8p@users.noreply.github.com> Date: Sat, 16 Jun 2018 12:55:32 +0200 Subject: [PATCH 102/144] Fix extended package support (#14980) * Fix package recurive merge bug * Fixed extended package support --- homeassistant/config.py | 68 +++++++++++++++++++---------------------- tests/test_config.py | 12 ++++++-- 2 files changed, 41 insertions(+), 39 deletions(-) diff --git a/homeassistant/config.py b/homeassistant/config.py index 44bf542f7cd..2906f07a307 100644 --- a/homeassistant/config.py +++ b/homeassistant/config.py @@ -548,15 +548,15 @@ def _identify_config_schema(module): return '', schema -def _recursive_merge(pack_name, comp_name, config, conf, package): +def _recursive_merge(conf, package): """Merge package into conf, recursively.""" + error = False for key, pack_conf in package.items(): if isinstance(pack_conf, dict): if not pack_conf: continue conf[key] = conf.get(key, OrderedDict()) - _recursive_merge(pack_name, comp_name, config, - conf=conf[key], package=pack_conf) + error = _recursive_merge(conf=conf[key], package=pack_conf) elif isinstance(pack_conf, list): if not pack_conf: @@ -566,11 +566,10 @@ def _recursive_merge(pack_name, comp_name, config, conf, package): else: if conf.get(key) is not None: - _log_pkg_error( - pack_name, comp_name, config, - 'has keys that are defined multiple times') + return key else: conf[key] = pack_conf + return error def merge_packages_config(hass, config, packages, @@ -605,39 +604,34 @@ def merge_packages_config(hass, config, packages, config[comp_name].extend(cv.ensure_list(comp_conf)) continue - if merge_type == 'dict': - if comp_conf is None: - comp_conf = OrderedDict() + if comp_conf is None: + comp_conf = OrderedDict() - if not isinstance(comp_conf, dict): - _log_pkg_error( - pack_name, comp_name, config, - "cannot be merged. Expected a dict.") - continue - - if comp_name not in config: - config[comp_name] = OrderedDict() - - if not isinstance(config[comp_name], dict): - _log_pkg_error( - pack_name, comp_name, config, - "cannot be merged. Dict expected in main config.") - continue - - for key, val in comp_conf.items(): - if key in config[comp_name]: - _log_pkg_error(pack_name, comp_name, config, - "duplicate key '{}'".format(key)) - continue - config[comp_name][key] = val - continue - - # The last merge type are sections that require recursive merging - if comp_name in config: - _recursive_merge(pack_name, comp_name, config, - conf=config[comp_name], package=comp_conf) + if not isinstance(comp_conf, dict): + _log_pkg_error( + pack_name, comp_name, config, + "cannot be merged. Expected a dict.") continue - config[comp_name] = comp_conf + + if comp_name not in config or config[comp_name] is None: + config[comp_name] = OrderedDict() + + if not isinstance(config[comp_name], dict): + _log_pkg_error( + pack_name, comp_name, config, + "cannot be merged. Dict expected in main config.") + continue + if not isinstance(comp_conf, dict): + _log_pkg_error( + pack_name, comp_name, config, + "cannot be merged. Dict expected in package.") + continue + + error = _recursive_merge(conf=config[comp_name], + package=comp_conf) + if error: + _log_pkg_error(pack_name, comp_name, config, + "has duplicate key '{}'".format(error)) return config diff --git a/tests/test_config.py b/tests/test_config.py index d22d6b2acfd..717a3f62ec9 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -589,7 +589,7 @@ def test_merge(merge_log_err, hass): assert len(config['input_boolean']) == 2 assert len(config['input_select']) == 1 assert len(config['light']) == 3 - assert config['wake_on_lan'] is None + assert isinstance(config['wake_on_lan'], OrderedDict) def test_merge_try_falsy(merge_log_err, hass): @@ -656,6 +656,14 @@ def test_merge_type_mismatch(merge_log_err, hass): def test_merge_once_only_keys(merge_log_err, hass): """Test if we have a merge for a comp that may occur only once. Keys.""" + packages = {'pack_2': {'api': None}} + config = { + config_util.CONF_CORE: {config_util.CONF_PACKAGES: packages}, + 'api': None, + } + config_util.merge_packages_config(hass, config, packages) + assert config['api'] == OrderedDict() + packages = {'pack_2': {'api': { 'key_3': 3, }}} @@ -755,7 +763,7 @@ def test_merge_duplicate_keys(merge_log_err, hass): } config = { config_util.CONF_CORE: {config_util.CONF_PACKAGES: packages}, - 'input_select': {'ib1': None}, + 'input_select': {'ib1': 1}, } config_util.merge_packages_config(hass, config, packages) From 17308a273089f92cdd4697952d1240c0583fcf93 Mon Sep 17 00:00:00 2001 From: Marcelo Moreira de Mello Date: Sat, 16 Jun 2018 06:57:27 -0400 Subject: [PATCH 103/144] Upgraded PyArlo to 0.1.7 (#14987) --- homeassistant/components/arlo.py | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/arlo.py b/homeassistant/components/arlo.py index 206ea4005e6..cd2c13ad292 100644 --- a/homeassistant/components/arlo.py +++ b/homeassistant/components/arlo.py @@ -16,7 +16,7 @@ from homeassistant.const import ( from homeassistant.helpers.event import track_time_interval from homeassistant.helpers.dispatcher import dispatcher_send -REQUIREMENTS = ['pyarlo==0.1.6'] +REQUIREMENTS = ['pyarlo==0.1.7'] _LOGGER = logging.getLogger(__name__) diff --git a/requirements_all.txt b/requirements_all.txt index 8d31dd7e267..d9d2c347c51 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -746,7 +746,7 @@ pyairvisual==2.0.1 pyalarmdotcom==0.3.2 # homeassistant.components.arlo -pyarlo==0.1.6 +pyarlo==0.1.7 # homeassistant.components.notify.xmpp pyasn1-modules==0.1.5 From ff4da05267d1114a54bfe6fdf1e7d98928b08c4e Mon Sep 17 00:00:00 2001 From: Marcelo Moreira de Mello Date: Sat, 16 Jun 2018 06:57:58 -0400 Subject: [PATCH 104/144] Upgraded python-amcrest to 1.2.3 (#14988) --- homeassistant/components/amcrest.py | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/amcrest.py b/homeassistant/components/amcrest.py index d0e470e3f8e..820ca41ad2e 100644 --- a/homeassistant/components/amcrest.py +++ b/homeassistant/components/amcrest.py @@ -18,7 +18,7 @@ from homeassistant.const import ( from homeassistant.helpers import discovery import homeassistant.helpers.config_validation as cv -REQUIREMENTS = ['amcrest==1.2.2'] +REQUIREMENTS = ['amcrest==1.2.3'] DEPENDENCIES = ['ffmpeg'] _LOGGER = logging.getLogger(__name__) diff --git a/requirements_all.txt b/requirements_all.txt index d9d2c347c51..f52ce86f1ea 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -113,7 +113,7 @@ alarmdecoder==1.13.2 alpha_vantage==2.0.0 # homeassistant.components.amcrest -amcrest==1.2.2 +amcrest==1.2.3 # homeassistant.components.media_player.anthemav anthemav==1.1.8 From 3ee8f58fdf30e664d814af3a91e0c93dd448e9e9 Mon Sep 17 00:00:00 2001 From: Marcelo Moreira de Mello Date: Sat, 16 Jun 2018 06:58:18 -0400 Subject: [PATCH 105/144] Upgraded RainCloudy to version 0.0.5 (#14986) --- homeassistant/components/raincloud.py | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/raincloud.py b/homeassistant/components/raincloud.py index 308a945e942..a04f4926b76 100644 --- a/homeassistant/components/raincloud.py +++ b/homeassistant/components/raincloud.py @@ -19,7 +19,7 @@ from homeassistant.helpers.dispatcher import ( from homeassistant.helpers.entity import Entity from homeassistant.helpers.event import track_time_interval -REQUIREMENTS = ['raincloudy==0.0.4'] +REQUIREMENTS = ['raincloudy==0.0.5'] _LOGGER = logging.getLogger(__name__) diff --git a/requirements_all.txt b/requirements_all.txt index f52ce86f1ea..fdf4dc56f98 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1174,7 +1174,7 @@ rachiopy==0.1.2 radiotherm==1.3 # homeassistant.components.raincloud -raincloudy==0.0.4 +raincloudy==0.0.5 # homeassistant.components.raspihats # raspihats==2.2.3 From 0b114f075567ed18c5cd3ac1010db55113131249 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sat, 16 Jun 2018 10:48:41 -0400 Subject: [PATCH 106/144] Do not mount deps folder when running in virtual env (#14993) * Do not mount deps folder when inside virtual env * Add tests * Fix package test --- homeassistant/bootstrap.py | 29 ++++++----------- homeassistant/scripts/__init__.py | 11 +++++-- homeassistant/util/package.py | 26 +++------------ tests/test_bootstrap.py | 54 ++++++++++++++++++++++++++++++- tests/util/test_package.py | 18 ++--------- 5 files changed, 79 insertions(+), 59 deletions(-) diff --git a/homeassistant/bootstrap.py b/homeassistant/bootstrap.py index a405362d368..b108ac805e9 100644 --- a/homeassistant/bootstrap.py +++ b/homeassistant/bootstrap.py @@ -1,5 +1,4 @@ """Provide methods to bootstrap a Home Assistant instance.""" -import asyncio import logging import logging.handlers import os @@ -17,7 +16,7 @@ from homeassistant.components import persistent_notification from homeassistant.const import EVENT_HOMEASSISTANT_CLOSE from homeassistant.setup import async_setup_component from homeassistant.util.logging import AsyncHandler -from homeassistant.util.package import async_get_user_site, get_user_site +from homeassistant.util.package import async_get_user_site, is_virtual_env from homeassistant.util.yaml import clear_secret_cache from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.signal import async_register_signal_handling @@ -53,8 +52,9 @@ def from_config_dict(config: Dict[str, Any], if config_dir is not None: config_dir = os.path.abspath(config_dir) hass.config.config_dir = config_dir - hass.loop.run_until_complete( - async_mount_local_lib_path(config_dir, hass.loop)) + if not is_virtual_env(): + hass.loop.run_until_complete( + async_mount_local_lib_path(config_dir)) # run task hass = hass.loop.run_until_complete( @@ -197,7 +197,9 @@ async def async_from_config_file(config_path: str, # Set config dir to directory holding config file config_dir = os.path.abspath(os.path.dirname(config_path)) hass.config.config_dir = config_dir - await async_mount_local_lib_path(config_dir, hass.loop) + + if not is_virtual_env(): + await async_mount_local_lib_path(config_dir) async_enable_logging(hass, verbose, log_rotate_days, log_file, log_no_color) @@ -211,9 +213,8 @@ async def async_from_config_file(config_path: str, finally: clear_secret_cache() - hass = await async_from_config_dict( + return await async_from_config_dict( config_dict, hass, enable_log=False, skip_pip=skip_pip) - return hass @core.callback @@ -308,23 +309,13 @@ def async_enable_logging(hass: core.HomeAssistant, "Unable to setup error log %s (access denied)", err_log_path) -def mount_local_lib_path(config_dir: str) -> str: - """Add local library to Python Path.""" - deps_dir = os.path.join(config_dir, 'deps') - lib_dir = get_user_site(deps_dir) - if lib_dir not in sys.path: - sys.path.insert(0, lib_dir) - return deps_dir - - -async def async_mount_local_lib_path(config_dir: str, - loop: asyncio.AbstractEventLoop) -> str: +async def async_mount_local_lib_path(config_dir: str) -> str: """Add local library to Python Path. This function is a coroutine. """ deps_dir = os.path.join(config_dir, 'deps') - lib_dir = await async_get_user_site(deps_dir, loop=loop) + lib_dir = await async_get_user_site(deps_dir) if lib_dir not in sys.path: sys.path.insert(0, lib_dir) return deps_dir diff --git a/homeassistant/scripts/__init__.py b/homeassistant/scripts/__init__.py index 815a5c8e55f..7aba3b2561c 100644 --- a/homeassistant/scripts/__init__.py +++ b/homeassistant/scripts/__init__.py @@ -1,5 +1,6 @@ """Home Assistant command line scripts.""" import argparse +import asyncio import importlib import logging import os @@ -7,10 +8,10 @@ import sys from typing import List -from homeassistant.bootstrap import mount_local_lib_path +from homeassistant.bootstrap import async_mount_local_lib_path from homeassistant.config import get_default_config_dir from homeassistant import requirements -from homeassistant.util.package import install_package +from homeassistant.util.package import install_package, is_virtual_env def run(args: List) -> int: @@ -38,7 +39,11 @@ def run(args: List) -> int: script = importlib.import_module('homeassistant.scripts.' + args[0]) config_dir = extract_config_dir() - mount_local_lib_path(config_dir) + + if not is_virtual_env(): + asyncio.get_event_loop().run_until_complete( + async_mount_local_lib_path(config_dir)) + pip_kwargs = requirements.pip_kwargs(config_dir) logging.basicConfig(stream=sys.stdout, level=logging.INFO) diff --git a/homeassistant/util/package.py b/homeassistant/util/package.py index a2f707c54f5..d1d398020de 100644 --- a/homeassistant/util/package.py +++ b/homeassistant/util/package.py @@ -77,32 +77,16 @@ def check_package_exists(package: str) -> bool: return any(dist in req for dist in env[req.project_name]) -def _get_user_site(deps_dir: str) -> tuple: - """Get arguments and environment for subprocess used in get_user_site.""" - env = os.environ.copy() - env['PYTHONUSERBASE'] = os.path.abspath(deps_dir) - args = [sys.executable, '-m', 'site', '--user-site'] - return args, env - - -def get_user_site(deps_dir: str) -> str: - """Return user local library path.""" - args, env = _get_user_site(deps_dir) - process = Popen(args, stdin=PIPE, stdout=PIPE, stderr=PIPE, env=env) - stdout, _ = process.communicate() - lib_dir = stdout.decode().strip() - return lib_dir - - -async def async_get_user_site(deps_dir: str, - loop: asyncio.AbstractEventLoop) -> str: +async def async_get_user_site(deps_dir: str) -> str: """Return user local library path. This function is a coroutine. """ - args, env = _get_user_site(deps_dir) + env = os.environ.copy() + env['PYTHONUSERBASE'] = os.path.abspath(deps_dir) + args = [sys.executable, '-m', 'site', '--user-site'] process = await asyncio.create_subprocess_exec( - *args, loop=loop, stdin=asyncio.subprocess.PIPE, + *args, stdin=asyncio.subprocess.PIPE, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.DEVNULL, env=env) stdout, _ = await process.communicate() diff --git a/tests/test_bootstrap.py b/tests/test_bootstrap.py index 3e4d4739779..e329f835f84 100644 --- a/tests/test_bootstrap.py +++ b/tests/test_bootstrap.py @@ -9,7 +9,7 @@ import homeassistant.config as config_util from homeassistant import bootstrap import homeassistant.util.dt as dt_util -from tests.common import patch_yaml_files, get_test_config_dir +from tests.common import patch_yaml_files, get_test_config_dir, mock_coro ORIG_TIMEZONE = dt_util.DEFAULT_TIME_ZONE VERSION_PATH = os.path.join(get_test_config_dir(), config_util.VERSION_FILE) @@ -52,3 +52,55 @@ def test_home_assistant_core_config_validation(hass): } }, hass) assert result is None + + +def test_from_config_dict_not_mount_deps_folder(loop): + """Test that we do not mount the deps folder inside from_config_dict.""" + with patch('homeassistant.bootstrap.is_virtual_env', return_value=False), \ + patch('homeassistant.core.HomeAssistant', + return_value=Mock(loop=loop)), \ + patch('homeassistant.bootstrap.async_mount_local_lib_path', + return_value=mock_coro()) as mock_mount, \ + patch('homeassistant.bootstrap.async_from_config_dict', + return_value=mock_coro()): + + bootstrap.from_config_dict({}, config_dir='.') + assert len(mock_mount.mock_calls) == 1 + + with patch('homeassistant.bootstrap.is_virtual_env', return_value=True), \ + patch('homeassistant.core.HomeAssistant', + return_value=Mock(loop=loop)), \ + patch('homeassistant.bootstrap.async_mount_local_lib_path', + return_value=mock_coro()) as mock_mount, \ + patch('homeassistant.bootstrap.async_from_config_dict', + return_value=mock_coro()): + + bootstrap.from_config_dict({}, config_dir='.') + assert len(mock_mount.mock_calls) == 0 + + +async def test_async_from_config_file_not_mount_deps_folder(loop): + """Test that we not mount the deps folder inside async_from_config_file.""" + hass = Mock(async_add_job=Mock(side_effect=lambda *args: mock_coro())) + + with patch('homeassistant.bootstrap.is_virtual_env', return_value=False), \ + patch('homeassistant.bootstrap.async_enable_logging', + return_value=mock_coro()), \ + patch('homeassistant.bootstrap.async_mount_local_lib_path', + return_value=mock_coro()) as mock_mount, \ + patch('homeassistant.bootstrap.async_from_config_dict', + return_value=mock_coro()): + + await bootstrap.async_from_config_file('mock-path', hass) + assert len(mock_mount.mock_calls) == 1 + + with patch('homeassistant.bootstrap.is_virtual_env', return_value=True), \ + patch('homeassistant.bootstrap.async_enable_logging', + return_value=mock_coro()), \ + patch('homeassistant.bootstrap.async_mount_local_lib_path', + return_value=mock_coro()) as mock_mount, \ + patch('homeassistant.bootstrap.async_from_config_dict', + return_value=mock_coro()): + + await bootstrap.async_from_config_file('mock-path', hass) + assert len(mock_mount.mock_calls) == 0 diff --git a/tests/util/test_package.py b/tests/util/test_package.py index 33db052f45a..ab9f9f0ad2c 100644 --- a/tests/util/test_package.py +++ b/tests/util/test_package.py @@ -201,20 +201,8 @@ def test_check_package_zip(): assert not package.check_package_exists(TEST_ZIP_REQ) -def test_get_user_site(deps_dir, lib_dir, mock_popen, mock_env_copy): - """Test get user site directory.""" - env = mock_env_copy() - env['PYTHONUSERBASE'] = os.path.abspath(deps_dir) - args = [sys.executable, '-m', 'site', '--user-site'] - ret = package.get_user_site(deps_dir) - assert mock_popen.call_count == 1 - assert mock_popen.call_args == call( - args, stdin=PIPE, stdout=PIPE, stderr=PIPE, env=env) - assert ret == lib_dir - - @asyncio.coroutine -def test_async_get_user_site(hass, mock_env_copy): +def test_async_get_user_site(mock_env_copy): """Test async get user site directory.""" deps_dir = '/deps_dir' env = mock_env_copy() @@ -222,10 +210,10 @@ def test_async_get_user_site(hass, mock_env_copy): args = [sys.executable, '-m', 'site', '--user-site'] with patch('homeassistant.util.package.asyncio.create_subprocess_exec', return_value=mock_async_subprocess()) as popen_mock: - ret = yield from package.async_get_user_site(deps_dir, hass.loop) + ret = yield from package.async_get_user_site(deps_dir) assert popen_mock.call_count == 1 assert popen_mock.call_args == call( - *args, loop=hass.loop, stdin=asyncio.subprocess.PIPE, + *args, stdin=asyncio.subprocess.PIPE, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.DEVNULL, env=env) assert ret == os.path.join(deps_dir, 'lib_dir') From abf07b60f07d33266bf49186750c81880056002a Mon Sep 17 00:00:00 2001 From: Jason Hu Date: Sat, 16 Jun 2018 07:49:11 -0700 Subject: [PATCH 107/144] Refactoring camera component to use async/await syntax. (#14990) * Refactoring camera component to use async/await syntax Also updated camera demo platform to encourage use of async * Code review --- homeassistant/components/camera/__init__.py | 36 +++++++++------------ homeassistant/components/camera/demo.py | 5 +-- 2 files changed, 19 insertions(+), 22 deletions(-) diff --git a/homeassistant/components/camera/__init__.py b/homeassistant/components/camera/__init__.py index c41020c3faf..ebda09de20c 100644 --- a/homeassistant/components/camera/__init__.py +++ b/homeassistant/components/camera/__init__.py @@ -96,6 +96,7 @@ def disable_motion_detection(hass, entity_id=None): @bind_hass +@callback def async_snapshot(hass, filename, entity_id=None): """Make a snapshot from a camera.""" data = {ATTR_ENTITY_ID: entity_id} if entity_id else {} @@ -128,8 +129,7 @@ async def async_get_image(hass, entity_id, timeout=10): raise HomeAssistantError('Unable to get image') -@asyncio.coroutine -def async_setup(hass, config): +async def async_setup(hass, config): """Set up the camera component.""" component = hass.data[DOMAIN] = \ EntityComponent(_LOGGER, DOMAIN, hass, SCAN_INTERVAL) @@ -141,7 +141,7 @@ def async_setup(hass, config): SCHEMA_WS_CAMERA_THUMBNAIL ) - yield from component.async_setup(config) + await component.async_setup(config) @callback def update_tokens(time): @@ -153,27 +153,25 @@ def async_setup(hass, config): hass.helpers.event.async_track_time_interval( update_tokens, TOKEN_CHANGE_INTERVAL) - @asyncio.coroutine - def async_handle_camera_service(service): + async def async_handle_camera_service(service): """Handle calls to the camera services.""" target_cameras = component.async_extract_from_service(service) update_tasks = [] for camera in target_cameras: if service.service == SERVICE_ENABLE_MOTION: - yield from camera.async_enable_motion_detection() + await camera.async_enable_motion_detection() elif service.service == SERVICE_DISABLE_MOTION: - yield from camera.async_disable_motion_detection() + await camera.async_disable_motion_detection() if not camera.should_poll: continue update_tasks.append(camera.async_update_ha_state(True)) if update_tasks: - yield from asyncio.wait(update_tasks, loop=hass.loop) + await asyncio.wait(update_tasks, loop=hass.loop) - @asyncio.coroutine - def async_handle_snapshot_service(service): + async def async_handle_snapshot_service(service): """Handle snapshot services calls.""" target_cameras = component.async_extract_from_service(service) filename = service.data[ATTR_FILENAME] @@ -189,7 +187,7 @@ def async_setup(hass, config): "Can't write %s, no access to path!", snapshot_file) continue - image = yield from camera.async_camera_image() + image = await camera.async_camera_image() def _write_image(to_file, image_data): """Executor helper to write image.""" @@ -197,7 +195,7 @@ def async_setup(hass, config): img_file.write(image_data) try: - yield from hass.async_add_job( + await hass.async_add_job( _write_image, snapshot_file, image) except OSError as err: _LOGGER.error("Can't write image to file: %s", err) @@ -274,6 +272,7 @@ class Camera(Entity): """Return bytes of camera image.""" raise NotImplementedError() + @callback def async_camera_image(self): """Return bytes of camera image. @@ -397,8 +396,7 @@ class CameraView(HomeAssistantView): """Initialize a basic camera view.""" self.component = component - @asyncio.coroutine - def get(self, request, entity_id): + async def get(self, request, entity_id): """Start a GET request.""" camera = self.component.get_entity(entity_id) @@ -412,11 +410,10 @@ class CameraView(HomeAssistantView): if not authenticated: return web.Response(status=401) - response = yield from self.handle(request, camera) + response = await self.handle(request, camera) return response - @asyncio.coroutine - def handle(self, request, camera): + async def handle(self, request, camera): """Handle the camera request.""" raise NotImplementedError() @@ -427,12 +424,11 @@ class CameraImageView(CameraView): url = '/api/camera_proxy/{entity_id}' name = 'api:camera:image' - @asyncio.coroutine - def handle(self, request, camera): + async def handle(self, request, camera): """Serve camera image.""" with suppress(asyncio.CancelledError, asyncio.TimeoutError): with async_timeout.timeout(10, loop=request.app['hass'].loop): - image = yield from camera.async_camera_image() + image = await camera.async_camera_image() if image: return web.Response(body=image, diff --git a/homeassistant/components/camera/demo.py b/homeassistant/components/camera/demo.py index d009f156e9d..3c1477d1828 100644 --- a/homeassistant/components/camera/demo.py +++ b/homeassistant/components/camera/demo.py @@ -12,9 +12,10 @@ from homeassistant.components.camera import Camera _LOGGER = logging.getLogger(__name__) -def setup_platform(hass, config, add_devices, discovery_info=None): +async def async_setup_platform(hass, config, async_add_devices, + discovery_info=None): """Set up the Demo camera platform.""" - add_devices([ + async_add_devices([ DemoCamera(hass, config, 'Demo camera') ]) From 87f9f1733570d2325ad478c0abc970cc216be644 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sat, 16 Jun 2018 10:51:07 -0400 Subject: [PATCH 108/144] Version bump to 0.72.0b0 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 5644c3d0a1f..d9446952f00 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -2,7 +2,7 @@ """Constants used by Home Assistant components.""" MAJOR_VERSION = 0 MINOR_VERSION = 72 -PATCH_VERSION = '0.dev0' +PATCH_VERSION = '0b0' __short_version__ = '{}.{}'.format(MAJOR_VERSION, MINOR_VERSION) __version__ = '{}.{}'.format(__short_version__, PATCH_VERSION) REQUIRED_PYTHON_VER = (3, 5, 3) From 1f50e335fa26ad396d5537b0cd63f23b92036ff1 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sat, 16 Jun 2018 17:32:49 -0400 Subject: [PATCH 109/144] Bump frontend to 20180616.0 --- homeassistant/components/frontend/__init__.py | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/frontend/__init__.py b/homeassistant/components/frontend/__init__.py index 0c425ccd3b1..af3459d0b19 100644 --- a/homeassistant/components/frontend/__init__.py +++ b/homeassistant/components/frontend/__init__.py @@ -24,7 +24,7 @@ from homeassistant.core import callback from homeassistant.helpers.translation import async_get_translations from homeassistant.loader import bind_hass -REQUIREMENTS = ['home-assistant-frontend==20180615.0'] +REQUIREMENTS = ['home-assistant-frontend==20180616.0'] DOMAIN = 'frontend' DEPENDENCIES = ['api', 'websocket_api', 'http', 'system_log'] diff --git a/requirements_all.txt b/requirements_all.txt index fdf4dc56f98..55038299bc0 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -404,7 +404,7 @@ hipnotify==1.0.8 holidays==0.9.5 # homeassistant.components.frontend -home-assistant-frontend==20180615.0 +home-assistant-frontend==20180616.0 # homeassistant.components.homekit_controller # homekit==0.6 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 1b32efe9577..03023966d95 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -81,7 +81,7 @@ hbmqtt==0.9.2 holidays==0.9.5 # homeassistant.components.frontend -home-assistant-frontend==20180615.0 +home-assistant-frontend==20180616.0 # homeassistant.components.influxdb # homeassistant.components.sensor.influxdb From bdf625764043d8b5156621dbed97e44e8ad0a626 Mon Sep 17 00:00:00 2001 From: Sebastian Muszynski Date: Sat, 16 Jun 2018 21:53:25 +0200 Subject: [PATCH 110/144] Remove load power attribute for channel USB (#14996) * Remove load power attribute for channel USB * Fix format --- homeassistant/components/switch/xiaomi_miio.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/switch/xiaomi_miio.py b/homeassistant/components/switch/xiaomi_miio.py index 1e11b844fdf..37b16f44ea8 100644 --- a/homeassistant/components/switch/xiaomi_miio.py +++ b/homeassistant/components/switch/xiaomi_miio.py @@ -421,8 +421,11 @@ class ChuangMiPlugSwitch(XiaomiPlugGenericSwitch): self._device_features = FEATURE_FLAGS_PLUG_V3 self._state_attrs.update({ ATTR_WIFI_LED: None, - ATTR_LOAD_POWER: None, }) + if self._channel_usb is False: + self._state_attrs.update({ + ATTR_LOAD_POWER: None, + }) async def async_turn_on(self, **kwargs): """Turn a channel on.""" @@ -476,7 +479,7 @@ class ChuangMiPlugSwitch(XiaomiPlugGenericSwitch): if state.wifi_led: self._state_attrs[ATTR_WIFI_LED] = state.wifi_led - if state.load_power: + if self._channel_usb is False and state.load_power: self._state_attrs[ATTR_LOAD_POWER] = state.load_power except DeviceException as ex: From a0139081159fa697479d997d4d6f55f1b5521f84 Mon Sep 17 00:00:00 2001 From: Andrey Date: Sat, 16 Jun 2018 22:52:23 +0300 Subject: [PATCH 111/144] Switch to own packaged version of spotipy (#14997) --- homeassistant/components/media_player/spotify.py | 4 +--- requirements_all.txt | 6 +++--- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/media_player/spotify.py b/homeassistant/components/media_player/spotify.py index 963258f1861..73ec8a175b1 100644 --- a/homeassistant/components/media_player/spotify.py +++ b/homeassistant/components/media_player/spotify.py @@ -20,9 +20,7 @@ from homeassistant.const import ( CONF_NAME, STATE_PLAYING, STATE_PAUSED, STATE_IDLE, STATE_UNKNOWN) import homeassistant.helpers.config_validation as cv -COMMIT = '544614f4b1d508201d363e84e871f86c90aa26b2' -REQUIREMENTS = ['https://github.com/happyleavesaoc/spotipy/' - 'archive/%s.zip#spotipy==2.4.4' % COMMIT] +REQUIREMENTS = ['spotipy-homeassistant==2.4.4.dev1'] DEPENDENCIES = ['http'] diff --git a/requirements_all.txt b/requirements_all.txt index 55038299bc0..69a6f01dbd5 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -424,9 +424,6 @@ httplib2==0.10.3 # homeassistant.components.media_player.braviatv https://github.com/aparraga/braviarc/archive/0.3.7.zip#braviarc==0.3.7 -# homeassistant.components.media_player.spotify -https://github.com/happyleavesaoc/spotipy/archive/544614f4b1d508201d363e84e871f86c90aa26b2.zip#spotipy==2.4.4 - # homeassistant.components.neato https://github.com/jabesq/pybotvac/archive/v0.0.5.zip#pybotvac==0.0.5 @@ -1281,6 +1278,9 @@ speedtest-cli==2.0.2 # homeassistant.components.sensor.spotcrime spotcrime==1.0.3 +# homeassistant.components.media_player.spotify +spotipy-homeassistant==2.4.4.dev1 + # homeassistant.components.recorder # homeassistant.scripts.db_migrator # homeassistant.components.sensor.sql From 8e185bc300ef5c92bea70c0967c2715fda982a77 Mon Sep 17 00:00:00 2001 From: Teemu R Date: Sat, 16 Jun 2018 21:52:03 +0200 Subject: [PATCH 112/144] Bump pyhs100 version (#15001) Fixes #13925 --- homeassistant/components/light/tplink.py | 2 +- homeassistant/components/switch/tplink.py | 2 +- requirements_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/light/tplink.py b/homeassistant/components/light/tplink.py index 4101eab2150..d7544cb6c5a 100644 --- a/homeassistant/components/light/tplink.py +++ b/homeassistant/components/light/tplink.py @@ -19,7 +19,7 @@ from homeassistant.util.color import \ from homeassistant.util.color import ( color_temperature_kelvin_to_mired as kelvin_to_mired) -REQUIREMENTS = ['pyHS100==0.3.0'] +REQUIREMENTS = ['pyHS100==0.3.1'] _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/switch/tplink.py b/homeassistant/components/switch/tplink.py index cd2a0f189fc..46682d87356 100644 --- a/homeassistant/components/switch/tplink.py +++ b/homeassistant/components/switch/tplink.py @@ -14,7 +14,7 @@ from homeassistant.components.switch import ( from homeassistant.const import (CONF_HOST, CONF_NAME, ATTR_VOLTAGE) import homeassistant.helpers.config_validation as cv -REQUIREMENTS = ['pyHS100==0.3.0'] +REQUIREMENTS = ['pyHS100==0.3.1'] _LOGGER = logging.getLogger(__name__) diff --git a/requirements_all.txt b/requirements_all.txt index 69a6f01dbd5..af5e9c6c787 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -719,7 +719,7 @@ pyCEC==0.4.13 # homeassistant.components.light.tplink # homeassistant.components.switch.tplink -pyHS100==0.3.0 +pyHS100==0.3.1 # homeassistant.components.rfxtrx pyRFXtrx==0.22.1 From 5d82f48c020f104a7848e1e1ca7969aa442b0469 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sat, 16 Jun 2018 17:12:03 -0400 Subject: [PATCH 113/144] Add experimental UI backend (#15002) * Add experimental UI * Add test * Lint --- homeassistant/components/frontend/__init__.py | 31 ++++++++++++++++--- tests/components/test_frontend.py | 19 ++++++++++++ 2 files changed, 46 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/frontend/__init__.py b/homeassistant/components/frontend/__init__.py index af3459d0b19..0f77b9e0adc 100644 --- a/homeassistant/components/frontend/__init__.py +++ b/homeassistant/components/frontend/__init__.py @@ -23,6 +23,7 @@ from homeassistant.const import CONF_NAME, EVENT_THEMES_UPDATED from homeassistant.core import callback from homeassistant.helpers.translation import async_get_translations from homeassistant.loader import bind_hass +from homeassistant.util.yaml import load_yaml REQUIREMENTS = ['home-assistant-frontend==20180616.0'] @@ -105,6 +106,10 @@ SCHEMA_GET_TRANSLATIONS = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend({ vol.Required('type'): WS_TYPE_GET_TRANSLATIONS, vol.Required('language'): str, }) +WS_TYPE_GET_EXPERIMENTAL_UI = 'frontend/experimental_ui' +SCHEMA_GET_EXPERIMENTAL_UI = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend({ + vol.Required('type'): WS_TYPE_GET_EXPERIMENTAL_UI, +}) class Panel: @@ -210,6 +215,9 @@ async def async_setup(hass, config): hass.components.websocket_api.async_register_command( WS_TYPE_GET_TRANSLATIONS, websocket_get_translations, SCHEMA_GET_TRANSLATIONS) + hass.components.websocket_api.async_register_command( + WS_TYPE_GET_EXPERIMENTAL_UI, websocket_experimental_config, + SCHEMA_GET_EXPERIMENTAL_UI) hass.http.register_view(ManifestJSONView) conf = config.get(DOMAIN, {}) @@ -254,10 +262,11 @@ async def async_setup(hass, config): """Finalize setup of a panel.""" panel.async_register_index_routes(hass.http.app.router, index_view) - await asyncio.wait([ - async_register_built_in_panel(hass, panel) - for panel in ('dev-event', 'dev-info', 'dev-service', 'dev-state', - 'dev-template', 'dev-mqtt', 'kiosk')], loop=hass.loop) + await asyncio.wait( + [async_register_built_in_panel(hass, panel) for panel in ( + 'dev-event', 'dev-info', 'dev-service', 'dev-state', + 'dev-template', 'dev-mqtt', 'kiosk', 'experimental-ui')], + loop=hass.loop) hass.data[DATA_FINALIZE_PANEL] = async_finalize_panel @@ -488,3 +497,17 @@ def websocket_get_translations(hass, connection, msg): )) hass.async_add_job(send_translations()) + + +def websocket_experimental_config(hass, connection, msg): + """Send experimental UI config over websocket config.""" + async def send_exp_config(): + """Send experimental frontend config.""" + config = await hass.async_add_job( + load_yaml, hass.config.path('experimental-ui.yaml')) + + connection.send_message_outside(websocket_api.result_message( + msg['id'], config + )) + + hass.async_add_job(send_exp_config()) diff --git a/tests/components/test_frontend.py b/tests/components/test_frontend.py index 2f118f24ef0..cb0c72e9edd 100644 --- a/tests/components/test_frontend.py +++ b/tests/components/test_frontend.py @@ -278,3 +278,22 @@ async def test_get_translations(hass, hass_ws_client): assert msg['type'] == wapi.TYPE_RESULT assert msg['success'] assert msg['result'] == {'resources': {'lang': 'nl'}} + + +async def test_experimental_ui(hass, hass_ws_client): + """Test experimental_ui command.""" + await async_setup_component(hass, 'frontend') + client = await hass_ws_client(hass) + + with patch('homeassistant.components.frontend.load_yaml', + return_value={'hello': 'world'}): + await client.send_json({ + 'id': 5, + 'type': 'frontend/experimental_ui', + }) + msg = await client.receive_json() + + assert msg['id'] == 5 + assert msg['type'] == wapi.TYPE_RESULT + assert msg['success'] + assert msg['result'] == {'hello': 'world'} From 65970a22480f4154726ea8db39363ffa7273376e Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sat, 16 Jun 2018 17:36:35 -0400 Subject: [PATCH 114/144] Version bump to 0.72.0b1 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index d9446952f00..dd32c0e5be7 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -2,7 +2,7 @@ """Constants used by Home Assistant components.""" MAJOR_VERSION = 0 MINOR_VERSION = 72 -PATCH_VERSION = '0b0' +PATCH_VERSION = '0b1' __short_version__ = '{}.{}'.format(MAJOR_VERSION, MINOR_VERSION) __version__ = '{}.{}'.format(__short_version__, PATCH_VERSION) REQUIRED_PYTHON_VER = (3, 5, 3) From 7238205adb144cd042a9081f8c7d487c13aa3150 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sat, 16 Jun 2018 22:35:19 -0400 Subject: [PATCH 115/144] Frontend bump to 20180617.0 --- homeassistant/components/frontend/__init__.py | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/frontend/__init__.py b/homeassistant/components/frontend/__init__.py index 0f77b9e0adc..25aa0da0a3e 100644 --- a/homeassistant/components/frontend/__init__.py +++ b/homeassistant/components/frontend/__init__.py @@ -25,7 +25,7 @@ from homeassistant.helpers.translation import async_get_translations from homeassistant.loader import bind_hass from homeassistant.util.yaml import load_yaml -REQUIREMENTS = ['home-assistant-frontend==20180616.0'] +REQUIREMENTS = ['home-assistant-frontend==20180617.0'] DOMAIN = 'frontend' DEPENDENCIES = ['api', 'websocket_api', 'http', 'system_log'] diff --git a/requirements_all.txt b/requirements_all.txt index af5e9c6c787..d860112c7f8 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -404,7 +404,7 @@ hipnotify==1.0.8 holidays==0.9.5 # homeassistant.components.frontend -home-assistant-frontend==20180616.0 +home-assistant-frontend==20180617.0 # homeassistant.components.homekit_controller # homekit==0.6 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 03023966d95..a2245c02cf1 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -81,7 +81,7 @@ hbmqtt==0.9.2 holidays==0.9.5 # homeassistant.components.frontend -home-assistant-frontend==20180616.0 +home-assistant-frontend==20180617.0 # homeassistant.components.influxdb # homeassistant.components.sensor.influxdb From 471d6e45eba1e08ecc3d876e6291a77868b8b2e5 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sat, 16 Jun 2018 22:37:13 -0400 Subject: [PATCH 116/144] Version bump to 0.72.0b2 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index dd32c0e5be7..562247a14c0 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -2,7 +2,7 @@ """Constants used by Home Assistant components.""" MAJOR_VERSION = 0 MINOR_VERSION = 72 -PATCH_VERSION = '0b1' +PATCH_VERSION = '0b2' __short_version__ = '{}.{}'.format(MAJOR_VERSION, MINOR_VERSION) __version__ = '{}.{}'.format(__short_version__, PATCH_VERSION) REQUIRED_PYTHON_VER = (3, 5, 3) From da3695dccc99f3cb18d47d11f5eefe2ea833618c Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Sun, 17 Jun 2018 19:33:04 +0200 Subject: [PATCH 117/144] Update test_http.py --- tests/components/hassio/test_http.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/components/hassio/test_http.py b/tests/components/hassio/test_http.py index ac90deb9f73..5f2c9c009c3 100644 --- a/tests/components/hassio/test_http.py +++ b/tests/components/hassio/test_http.py @@ -61,7 +61,7 @@ def test_forward_request_no_auth_for_panel(hassio_client, build_type): '_create_response') as mresp: mresp.return_value = 'response' resp = yield from hassio_client.get( - '/api/hassio/app-{}'.format(build_type)) + '/api/hassio/{}'.format(build_type)) # Check we got right response assert resp.status == 200 From 1642502a706042d7bc350d40eb8ba04b40a25890 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sun, 17 Jun 2018 23:03:29 -0400 Subject: [PATCH 118/144] Update translations --- .../components/cast/.translations/ca.json | 15 +++++++++ .../components/cast/.translations/ko.json | 15 +++++++++ .../components/cast/.translations/no.json | 15 +++++++++ .../components/cast/.translations/pl.json | 15 +++++++++ .../components/cast/.translations/ru.json | 15 +++++++++ .../components/cast/.translations/sv.json | 15 +++++++++ .../components/cast/.translations/vi.json | 15 +++++++++ .../cast/.translations/zh-Hans.json | 15 +++++++++ .../components/deconz/.translations/bg.json | 1 + .../components/deconz/.translations/ca.json | 33 +++++++++++++++++++ .../components/deconz/.translations/cs.json | 32 ++++++++++++++++++ .../components/deconz/.translations/en.json | 4 +-- .../components/deconz/.translations/fr.json | 32 ++++++++++++++++++ .../components/deconz/.translations/hu.json | 6 +++- .../components/deconz/.translations/it.json | 26 +++++++++++++++ .../components/deconz/.translations/ko.json | 11 +++++-- .../components/deconz/.translations/lb.json | 6 ++++ .../components/deconz/.translations/no.json | 7 ++++ .../components/deconz/.translations/pl.json | 6 ++++ .../deconz/.translations/pt-BR.json | 32 ++++++++++++++++++ .../components/deconz/.translations/pt.json | 29 ++++++++++++++-- .../components/deconz/.translations/ru.json | 7 ++++ .../components/deconz/.translations/sl.json | 6 ++++ .../components/deconz/.translations/sv.json | 33 +++++++++++++++++++ .../components/deconz/.translations/vi.json | 26 +++++++++++++++ .../deconz/.translations/zh-Hans.json | 7 ++++ .../deconz/.translations/zh-Hant.json | 7 ++++ .../components/hue/.translations/ca.json | 29 ++++++++++++++++ .../components/hue/.translations/cs.json | 29 ++++++++++++++++ .../components/hue/.translations/en.json | 2 +- .../components/hue/.translations/fr.json | 29 ++++++++++++++++ .../components/hue/.translations/hu.json | 3 +- .../components/hue/.translations/it.json | 21 +++++++++++- .../components/hue/.translations/pt-BR.json | 29 ++++++++++++++++ .../components/hue/.translations/pt.json | 24 ++++++++++++++ .../components/hue/.translations/sv.json | 29 ++++++++++++++++ .../components/hue/.translations/vi.json | 17 ++++++++++ .../components/nest/.translations/ca.json | 33 +++++++++++++++++++ .../components/nest/.translations/ko.json | 33 +++++++++++++++++++ .../components/nest/.translations/no.json | 33 +++++++++++++++++++ .../components/nest/.translations/pl.json | 33 +++++++++++++++++++ .../components/nest/.translations/ru.json | 33 +++++++++++++++++++ .../components/nest/.translations/sv.json | 33 +++++++++++++++++++ .../components/nest/.translations/vi.json | 22 +++++++++++++ .../nest/.translations/zh-Hans.json | 33 +++++++++++++++++++ .../sensor/.translations/season.ca.json | 8 +++++ .../sensor/.translations/season.fr.json | 8 +++++ .../sensor/.translations/season.pt-BR.json | 8 +++++ .../components/sonos/.translations/ca.json | 15 +++++++++ .../components/sonos/.translations/ko.json | 15 +++++++++ .../components/sonos/.translations/no.json | 15 +++++++++ .../components/sonos/.translations/pl.json | 15 +++++++++ .../components/sonos/.translations/ru.json | 15 +++++++++ .../components/sonos/.translations/sv.json | 15 +++++++++ .../components/sonos/.translations/vi.json | 15 +++++++++ .../sonos/.translations/zh-Hans.json | 15 +++++++++ .../components/zone/.translations/bg.json | 21 ++++++++++++ .../components/zone/.translations/ca.json | 21 ++++++++++++ .../components/zone/.translations/cs.json | 21 ++++++++++++ .../components/zone/.translations/fr.json | 21 ++++++++++++ .../components/zone/.translations/hu.json | 21 ++++++++++++ .../components/zone/.translations/it.json | 21 ++++++++++++ .../components/zone/.translations/ko.json | 2 +- .../components/zone/.translations/pt-BR.json | 21 ++++++++++++ .../components/zone/.translations/pt.json | 3 +- .../components/zone/.translations/sl.json | 21 ++++++++++++ .../components/zone/.translations/sv.json | 21 ++++++++++++ .../components/zone/.translations/vi.json | 21 ++++++++++++ .../zone/.translations/zh-Hant.json | 21 ++++++++++++ homeassistant/config_entries.py | 3 ++ 70 files changed, 1267 insertions(+), 12 deletions(-) create mode 100644 homeassistant/components/cast/.translations/ca.json create mode 100644 homeassistant/components/cast/.translations/ko.json create mode 100644 homeassistant/components/cast/.translations/no.json create mode 100644 homeassistant/components/cast/.translations/pl.json create mode 100644 homeassistant/components/cast/.translations/ru.json create mode 100644 homeassistant/components/cast/.translations/sv.json create mode 100644 homeassistant/components/cast/.translations/vi.json create mode 100644 homeassistant/components/cast/.translations/zh-Hans.json create mode 100644 homeassistant/components/deconz/.translations/ca.json create mode 100644 homeassistant/components/deconz/.translations/cs.json create mode 100644 homeassistant/components/deconz/.translations/fr.json create mode 100644 homeassistant/components/deconz/.translations/it.json create mode 100644 homeassistant/components/deconz/.translations/pt-BR.json create mode 100644 homeassistant/components/deconz/.translations/sv.json create mode 100644 homeassistant/components/deconz/.translations/vi.json create mode 100644 homeassistant/components/hue/.translations/ca.json create mode 100644 homeassistant/components/hue/.translations/cs.json create mode 100644 homeassistant/components/hue/.translations/fr.json create mode 100644 homeassistant/components/hue/.translations/pt-BR.json create mode 100644 homeassistant/components/hue/.translations/sv.json create mode 100644 homeassistant/components/hue/.translations/vi.json create mode 100644 homeassistant/components/nest/.translations/ca.json create mode 100644 homeassistant/components/nest/.translations/ko.json create mode 100644 homeassistant/components/nest/.translations/no.json create mode 100644 homeassistant/components/nest/.translations/pl.json create mode 100644 homeassistant/components/nest/.translations/ru.json create mode 100644 homeassistant/components/nest/.translations/sv.json create mode 100644 homeassistant/components/nest/.translations/vi.json create mode 100644 homeassistant/components/nest/.translations/zh-Hans.json create mode 100644 homeassistant/components/sensor/.translations/season.ca.json create mode 100644 homeassistant/components/sensor/.translations/season.fr.json create mode 100644 homeassistant/components/sensor/.translations/season.pt-BR.json create mode 100644 homeassistant/components/sonos/.translations/ca.json create mode 100644 homeassistant/components/sonos/.translations/ko.json create mode 100644 homeassistant/components/sonos/.translations/no.json create mode 100644 homeassistant/components/sonos/.translations/pl.json create mode 100644 homeassistant/components/sonos/.translations/ru.json create mode 100644 homeassistant/components/sonos/.translations/sv.json create mode 100644 homeassistant/components/sonos/.translations/vi.json create mode 100644 homeassistant/components/sonos/.translations/zh-Hans.json create mode 100644 homeassistant/components/zone/.translations/bg.json create mode 100644 homeassistant/components/zone/.translations/ca.json create mode 100644 homeassistant/components/zone/.translations/cs.json create mode 100644 homeassistant/components/zone/.translations/fr.json create mode 100644 homeassistant/components/zone/.translations/hu.json create mode 100644 homeassistant/components/zone/.translations/it.json create mode 100644 homeassistant/components/zone/.translations/pt-BR.json create mode 100644 homeassistant/components/zone/.translations/sl.json create mode 100644 homeassistant/components/zone/.translations/sv.json create mode 100644 homeassistant/components/zone/.translations/vi.json create mode 100644 homeassistant/components/zone/.translations/zh-Hant.json diff --git a/homeassistant/components/cast/.translations/ca.json b/homeassistant/components/cast/.translations/ca.json new file mode 100644 index 00000000000..e65e00f8624 --- /dev/null +++ b/homeassistant/components/cast/.translations/ca.json @@ -0,0 +1,15 @@ +{ + "config": { + "abort": { + "no_devices_found": "No s'han trobat dispositius de Google Cast a la xarxa.", + "single_instance_allowed": "Nom\u00e9s cal una \u00fanica configuraci\u00f3 de Google Cast." + }, + "step": { + "confirm": { + "description": "Voleu configurar Google Cast?", + "title": "Google Cast" + } + }, + "title": "Google Cast" + } +} \ No newline at end of file diff --git a/homeassistant/components/cast/.translations/ko.json b/homeassistant/components/cast/.translations/ko.json new file mode 100644 index 00000000000..2be2a69c171 --- /dev/null +++ b/homeassistant/components/cast/.translations/ko.json @@ -0,0 +1,15 @@ +{ + "config": { + "abort": { + "no_devices_found": "Googgle Cast \uc7a5\uce58\uac00 \ub124\ud2b8\uc6cc\ud06c\uc5d0\uc11c \ubc1c\uacac\ub418\uc9c0 \uc54a\uc558\uc2b5\ub2c8\ub2e4.", + "single_instance_allowed": "Google Cast\uc758 \ub2e8\uc77c \uad6c\uc131 \ub9cc \ud544\uc694\ud569\ub2c8\ub2e4." + }, + "step": { + "confirm": { + "description": "Google Cast\ub97c \uc124\uc815 \ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?", + "title": "Google Cast" + } + }, + "title": "Google Cast" + } +} \ No newline at end of file diff --git a/homeassistant/components/cast/.translations/no.json b/homeassistant/components/cast/.translations/no.json new file mode 100644 index 00000000000..d36c929e721 --- /dev/null +++ b/homeassistant/components/cast/.translations/no.json @@ -0,0 +1,15 @@ +{ + "config": { + "abort": { + "no_devices_found": "Ingen Google Cast enheter funnet p\u00e5 nettverket.", + "single_instance_allowed": "Kun en enkelt konfigurasjon av Google Cast er n\u00f8dvendig." + }, + "step": { + "confirm": { + "description": "\u00d8nsker du \u00e5 sette opp Google Cast?", + "title": "Google Cast" + } + }, + "title": "Google Cast" + } +} \ No newline at end of file diff --git a/homeassistant/components/cast/.translations/pl.json b/homeassistant/components/cast/.translations/pl.json new file mode 100644 index 00000000000..c4399f95def --- /dev/null +++ b/homeassistant/components/cast/.translations/pl.json @@ -0,0 +1,15 @@ +{ + "config": { + "abort": { + "no_devices_found": "Nie znaleziono w sieci urz\u0105dze\u0144 Google Cast.", + "single_instance_allowed": "Wymagana jest tylko jedna konfiguracja Google Cast." + }, + "step": { + "confirm": { + "description": "Czy chcesz skonfigurowa\u0107 Google Cast?", + "title": "Google Cast" + } + }, + "title": "Google Cast" + } +} \ No newline at end of file diff --git a/homeassistant/components/cast/.translations/ru.json b/homeassistant/components/cast/.translations/ru.json new file mode 100644 index 00000000000..9c9353da37e --- /dev/null +++ b/homeassistant/components/cast/.translations/ru.json @@ -0,0 +1,15 @@ +{ + "config": { + "abort": { + "no_devices_found": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 Google Cast \u043d\u0435 \u043d\u0430\u0439\u0434\u0435\u043d\u044b.", + "single_instance_allowed": "\u041d\u0435\u043e\u0431\u0445\u043e\u0434\u0438\u043c\u0430 \u0442\u043e\u043b\u044c\u043a\u043e \u043e\u0434\u043d\u0430 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f Google Cast." + }, + "step": { + "confirm": { + "description": "\u0412\u044b \u0445\u043e\u0442\u0438\u0442\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c Google Cast?", + "title": "Google Cast" + } + }, + "title": "Google Cast" + } +} \ No newline at end of file diff --git a/homeassistant/components/cast/.translations/sv.json b/homeassistant/components/cast/.translations/sv.json new file mode 100644 index 00000000000..aea55058d10 --- /dev/null +++ b/homeassistant/components/cast/.translations/sv.json @@ -0,0 +1,15 @@ +{ + "config": { + "abort": { + "no_devices_found": "Inga Google Cast-enheter hittades i n\u00e4tverket.", + "single_instance_allowed": "Endast en enda konfiguration av Google Cast \u00e4r n\u00f6dv\u00e4ndig." + }, + "step": { + "confirm": { + "description": "Vill du konfigurera Google Cast?", + "title": "Google Cast" + } + }, + "title": "Google Cast" + } +} \ No newline at end of file diff --git a/homeassistant/components/cast/.translations/vi.json b/homeassistant/components/cast/.translations/vi.json new file mode 100644 index 00000000000..2f2982293cf --- /dev/null +++ b/homeassistant/components/cast/.translations/vi.json @@ -0,0 +1,15 @@ +{ + "config": { + "abort": { + "no_devices_found": "Kh\u00f4ng t\u00ecm th\u1ea5y thi\u1ebft b\u1ecb Google Cast n\u00e0o tr\u00ean m\u1ea1ng.", + "single_instance_allowed": "Ch\u1ec9 c\u1ea7n m\u1ed9t c\u1ea5u h\u00ecnh duy nh\u1ea5t c\u1ee7a Google Cast l\u00e0 \u0111\u1ee7." + }, + "step": { + "confirm": { + "description": "B\u1ea1n c\u00f3 mu\u1ed1n thi\u1ebft l\u1eadp Google Cast kh\u00f4ng?", + "title": "Google Cast" + } + }, + "title": "Google Cast" + } +} \ No newline at end of file diff --git a/homeassistant/components/cast/.translations/zh-Hans.json b/homeassistant/components/cast/.translations/zh-Hans.json new file mode 100644 index 00000000000..4a844d3d4dd --- /dev/null +++ b/homeassistant/components/cast/.translations/zh-Hans.json @@ -0,0 +1,15 @@ +{ + "config": { + "abort": { + "no_devices_found": "\u6ca1\u6709\u5728\u7f51\u7edc\u4e0a\u627e\u5230 Google Cast \u8bbe\u5907\u3002", + "single_instance_allowed": "\u53ea\u6709\u4e00\u6b21 Google Cast \u914d\u7f6e\u662f\u5fc5\u8981\u7684\u3002" + }, + "step": { + "confirm": { + "description": "\u60a8\u60f3\u8981\u914d\u7f6e Google Cast \u5417\uff1f", + "title": "Google Cast" + } + }, + "title": "Google Cast" + } +} \ No newline at end of file diff --git a/homeassistant/components/deconz/.translations/bg.json b/homeassistant/components/deconz/.translations/bg.json index 91727cae257..2ea65762063 100644 --- a/homeassistant/components/deconz/.translations/bg.json +++ b/homeassistant/components/deconz/.translations/bg.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "already_configured": "\u041c\u043e\u0441\u0442\u044a\u0442 \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d", "no_bridges": "\u041d\u0435 \u0441\u0430 \u043e\u0442\u043a\u0440\u0438\u0442\u0438 \u043c\u043e\u0441\u0442\u043e\u0432\u0435 deCONZ", "one_instance_only": "\u041a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442 \u043f\u043e\u0434\u0434\u044a\u0440\u0436\u0430 \u0441\u0430\u043c\u043e \u0435\u0434\u043d\u043e \u043a\u043e\u043f\u0438\u0435 \u043d\u0430 deCONZ" }, diff --git a/homeassistant/components/deconz/.translations/ca.json b/homeassistant/components/deconz/.translations/ca.json new file mode 100644 index 00000000000..0a9e6fdee3f --- /dev/null +++ b/homeassistant/components/deconz/.translations/ca.json @@ -0,0 +1,33 @@ +{ + "config": { + "abort": { + "already_configured": "L'enlla\u00e7 ja est\u00e0 configurat", + "no_bridges": "No s'han descobert enlla\u00e7os amb deCONZ", + "one_instance_only": "El component nom\u00e9s admet una inst\u00e0ncia deCONZ" + }, + "error": { + "no_key": "No s'ha pogut obtenir una clau API" + }, + "step": { + "init": { + "data": { + "host": "Amfitri\u00f3", + "port": "Port (predeterminat: '80')" + }, + "title": "Definiu la passarel\u00b7la deCONZ" + }, + "link": { + "description": "Desbloqueja la teva passarel\u00b7la d'enlla\u00e7 deCONZ per a registrar-te amb Home Assistant.\n\n1. V\u00e9s a la configuraci\u00f3 del sistema deCONZ\n2. Prem el bot\u00f3 \"Desbloquejar passarel\u00b7la\"", + "title": "Vincular amb deCONZ" + }, + "options": { + "data": { + "allow_clip_sensor": "Permet la importaci\u00f3 de sensors virtuals", + "allow_deconz_groups": "Permet la importaci\u00f3 de grups deCONZ" + }, + "title": "Opcions de configuraci\u00f3 addicionals per deCONZ" + } + }, + "title": "deCONZ" + } +} \ No newline at end of file diff --git a/homeassistant/components/deconz/.translations/cs.json b/homeassistant/components/deconz/.translations/cs.json new file mode 100644 index 00000000000..0721cac3321 --- /dev/null +++ b/homeassistant/components/deconz/.translations/cs.json @@ -0,0 +1,32 @@ +{ + "config": { + "abort": { + "already_configured": "P\u0159emost\u011bn\u00ed je ji\u017e nakonfigurov\u00e1no", + "no_bridges": "\u017d\u00e1dn\u00e9 deCONZ p\u0159emost\u011bn\u00ed nebyly nalezeny", + "one_instance_only": "Komponent podporuje pouze jednu instanci deCONZ" + }, + "error": { + "no_key": "Nelze z\u00edskat kl\u00ed\u010d API" + }, + "step": { + "init": { + "data": { + "host": "Hostitel", + "port": "Port (v\u00fdchoz\u00ed hodnota: '80')" + }, + "title": "Definujte br\u00e1nu deCONZ" + }, + "link": { + "description": "Odemkn\u011bte br\u00e1nu deCONZ, pro registraci v Home Assistant. \n\n 1. P\u0159ejd\u011bte do nastaven\u00ed syst\u00e9mu deCONZ \n 2. Stiskn\u011bte tla\u010d\u00edtko \"Unlock Gateway\"", + "title": "Propojit s deCONZ" + }, + "options": { + "data": { + "allow_clip_sensor": "Povolit import virtu\u00e1ln\u00edch \u010didel" + }, + "title": "Dal\u0161\u00ed mo\u017enosti konfigurace pro deCONZ" + } + }, + "title": "Br\u00e1na deCONZ Zigbee" + } +} \ No newline at end of file diff --git a/homeassistant/components/deconz/.translations/en.json b/homeassistant/components/deconz/.translations/en.json index 465c6c1e0e8..f55f64ca430 100644 --- a/homeassistant/components/deconz/.translations/en.json +++ b/homeassistant/components/deconz/.translations/en.json @@ -21,11 +21,11 @@ "title": "Link with deCONZ" }, "options": { - "title": "Extra configuration options for deCONZ", "data": { "allow_clip_sensor": "Allow importing virtual sensors", "allow_deconz_groups": "Allow importing deCONZ groups" - } + }, + "title": "Extra configuration options for deCONZ" } }, "title": "deCONZ Zigbee gateway" diff --git a/homeassistant/components/deconz/.translations/fr.json b/homeassistant/components/deconz/.translations/fr.json new file mode 100644 index 00000000000..02f174cd59f --- /dev/null +++ b/homeassistant/components/deconz/.translations/fr.json @@ -0,0 +1,32 @@ +{ + "config": { + "abort": { + "already_configured": "Ce pont est d\u00e9j\u00e0 configur\u00e9", + "no_bridges": "Aucun pont deCONZ n'a \u00e9t\u00e9 d\u00e9couvert", + "one_instance_only": "Le composant prend uniquement en charge une instance deCONZ" + }, + "error": { + "no_key": "Impossible d'obtenir une cl\u00e9 d'API" + }, + "step": { + "init": { + "data": { + "host": "H\u00f4te", + "port": "Port (valeur par d\u00e9faut : 80)" + }, + "title": "Initialiser la passerelle deCONZ" + }, + "link": { + "description": "D\u00e9verrouillez votre passerelle deCONZ pour vous enregistrer aupr\u00e8s de Home Assistant. \n\n 1. Acc\u00e9dez aux param\u00e8tres du syst\u00e8me deCONZ \n 2. Cliquez sur \"D\u00e9verrouiller la passerelle\"", + "title": "Lien vers deCONZ" + }, + "options": { + "data": { + "allow_clip_sensor": "Autoriser l'importation de capteurs virtuels" + }, + "title": "Options de configuration suppl\u00e9mentaires pour deCONZ" + } + }, + "title": "Passerelle deCONZ Zigbee" + } +} \ No newline at end of file diff --git a/homeassistant/components/deconz/.translations/hu.json b/homeassistant/components/deconz/.translations/hu.json index 42aab9c6d7e..c1fd76c5035 100644 --- a/homeassistant/components/deconz/.translations/hu.json +++ b/homeassistant/components/deconz/.translations/hu.json @@ -1,6 +1,8 @@ { "config": { "abort": { + "already_configured": "A bridge m\u00e1r konfigur\u00e1lva van", + "no_bridges": "Nem tal\u00e1ltam deCONZ bridget", "one_instance_only": "Ez a komponens csak egy deCONZ egys\u00e9get t\u00e1mogat" }, "error": { @@ -11,9 +13,11 @@ "data": { "host": "H\u00e1zigazda (Host)", "port": "Port (alap\u00e9rtelmezett \u00e9rt\u00e9k: '80')" - } + }, + "title": "deCONZ \u00e1tj\u00e1r\u00f3 megad\u00e1sa" }, "link": { + "description": "Oldja fel a deCONZ \u00e1tj\u00e1r\u00f3t a Home Assistant-ban val\u00f3 regisztr\u00e1l\u00e1shoz.\n\n1. Menjen a deCONZ rendszer be\u00e1ll\u00edt\u00e1sokhoz\n2. Nyomja meg az \"\u00c1tj\u00e1r\u00f3 felold\u00e1sa\" gombot", "title": "Kapcsol\u00f3d\u00e1s a deCONZ-hoz" } }, diff --git a/homeassistant/components/deconz/.translations/it.json b/homeassistant/components/deconz/.translations/it.json new file mode 100644 index 00000000000..6fc7158b882 --- /dev/null +++ b/homeassistant/components/deconz/.translations/it.json @@ -0,0 +1,26 @@ +{ + "config": { + "abort": { + "already_configured": "Il Bridge \u00e8 gi\u00e0 configurato", + "no_bridges": "Nessun bridge deCONZ rilevato", + "one_instance_only": "Il componente supporto solo un'istanza di deCONZ" + }, + "error": { + "no_key": "Impossibile ottenere una API key" + }, + "step": { + "init": { + "data": { + "host": "Host", + "port": "Porta (valore di default: '80')" + }, + "title": "Definisci il gateway deCONZ" + }, + "link": { + "description": "Sblocca il tuo gateway deCONZ per registrarlo in Home Assistant.\n\n1. Vai nelle impostazioni di sistema di deCONZ\n2. Premi il bottone \"Unlock Gateway\"", + "title": "Collega con deCONZ" + } + }, + "title": "deCONZ" + } +} \ No newline at end of file diff --git a/homeassistant/components/deconz/.translations/ko.json b/homeassistant/components/deconz/.translations/ko.json index d6de1028218..9c5ffa19257 100644 --- a/homeassistant/components/deconz/.translations/ko.json +++ b/homeassistant/components/deconz/.translations/ko.json @@ -18,9 +18,16 @@ }, "link": { "description": "deCONZ \uac8c\uc774\ud2b8\uc6e8\uc774\ub97c \uc5b8\ub77d\ud558\uc5ec Home Assistant \uc5d0 \uc5f0\uacb0\ud558\uae30\n\n1. deCONZ \uc2dc\uc2a4\ud15c \uc124\uc815\uc73c\ub85c \uc774\ub3d9\ud558\uc138\uc694\n2. \"Unlock Gateway\" \ubc84\ud2bc\uc744 \ub204\ub974\uc138\uc694 ", - "title": "deCONZ \uc640 \uc5f0\uacb0" + "title": "deCONZ\uc640 \uc5f0\uacb0" + }, + "options": { + "data": { + "allow_clip_sensor": "\uac00\uc0c1 \uc13c\uc11c \uac00\uc838\uc624\uae30 \ud5c8\uc6a9", + "allow_deconz_groups": "deCONZ \ub0b4\uc6a9 \uac00\uc838\uc624\uae30 \ud5c8\uc6a9" + }, + "title": "deCONZ\ub97c \uc704\ud55c \ucd94\uac00 \uad6c\uc131 \uc635\uc158" } }, - "title": "deCONZ" + "title": "deCONZ Zigbee \uac8c\uc774\ud2b8\uc6e8\uc774" } } \ No newline at end of file diff --git a/homeassistant/components/deconz/.translations/lb.json b/homeassistant/components/deconz/.translations/lb.json index 2a9dfc5e543..46190d23926 100644 --- a/homeassistant/components/deconz/.translations/lb.json +++ b/homeassistant/components/deconz/.translations/lb.json @@ -19,6 +19,12 @@ "link": { "description": "Entsperrt \u00e4r deCONZ gateway fir se mat Home Assistant ze registr\u00e9ieren.\n\n1. Gidd op\u00a0deCONZ System Astellungen\n2. Dr\u00e9ckt \"Unlock\" Gateway Kn\u00e4ppchen", "title": "Link mat deCONZ" + }, + "options": { + "data": { + "allow_clip_sensor": "Erlaabt den Import vun virtuellen Sensoren" + }, + "title": "Extra Konfiguratiouns Optiounen fir deCONZ" } }, "title": "deCONZ" diff --git a/homeassistant/components/deconz/.translations/no.json b/homeassistant/components/deconz/.translations/no.json index 25e3b0b7d68..55518b7da53 100644 --- a/homeassistant/components/deconz/.translations/no.json +++ b/homeassistant/components/deconz/.translations/no.json @@ -19,6 +19,13 @@ "link": { "description": "L\u00e5s opp deCONZ-gatewayen din for \u00e5 registrere deg med Home Assistant. \n\n 1. G\u00e5 til deCONZ-systeminnstillinger \n 2. Trykk p\u00e5 \"L\u00e5s opp gateway\" knappen", "title": "Koble til deCONZ" + }, + "options": { + "data": { + "allow_clip_sensor": "Tillat import av virtuelle sensorer", + "allow_deconz_groups": "Tillat import av deCONZ grupper" + }, + "title": "Ekstra konfigurasjonsalternativer for deCONZ" } }, "title": "deCONZ" diff --git a/homeassistant/components/deconz/.translations/pl.json b/homeassistant/components/deconz/.translations/pl.json index bb7488fcbec..461e8b185ee 100644 --- a/homeassistant/components/deconz/.translations/pl.json +++ b/homeassistant/components/deconz/.translations/pl.json @@ -19,6 +19,12 @@ "link": { "description": "Odblokuj bramk\u0119 deCONZ, aby zarejestrowa\u0107 j\u0105 w Home Assistant. \n\n 1. Przejd\u017a do ustawie\u0144 systemu deCONZ \n 2. Naci\u015bnij przycisk \"Odblokuj bramk\u0119\"", "title": "Po\u0142\u0105cz z deCONZ" + }, + "options": { + "data": { + "allow_clip_sensor": "Zezwalaj na importowanie wirtualnych sensor\u00f3w" + }, + "title": "Dodatkowe opcje konfiguracji dla deCONZ" } }, "title": "deCONZ" diff --git a/homeassistant/components/deconz/.translations/pt-BR.json b/homeassistant/components/deconz/.translations/pt-BR.json new file mode 100644 index 00000000000..065c51aee21 --- /dev/null +++ b/homeassistant/components/deconz/.translations/pt-BR.json @@ -0,0 +1,32 @@ +{ + "config": { + "abort": { + "already_configured": "A ponte j\u00e1 est\u00e1 configurada", + "no_bridges": "N\u00e3o h\u00e1 pontes de deCONZ descobertas", + "one_instance_only": "Componente suporta apenas uma inst\u00e2ncia deCONZ" + }, + "error": { + "no_key": "N\u00e3o foi poss\u00edvel obter uma chave de API" + }, + "step": { + "init": { + "data": { + "host": "Hospedeiro", + "port": "Porta (valor padr\u00e3o: '80')" + }, + "title": "Defina o gateway deCONZ" + }, + "link": { + "description": "Desbloqueie o seu gateway deCONZ para se registar no Home Assistant. \n\n 1. V\u00e1 para as configura\u00e7\u00f5es do sistema deCONZ \n 2. Pressione o bot\u00e3o \"Desbloquear Gateway\"", + "title": "Linkar com deCONZ" + }, + "options": { + "data": { + "allow_clip_sensor": "Permitir a importa\u00e7\u00e3o de sensores virtuais" + }, + "title": "Op\u00e7\u00f5es extras de configura\u00e7\u00e3o para deCONZ" + } + }, + "title": "Gateway deCONZ Zigbee" + } +} \ No newline at end of file diff --git a/homeassistant/components/deconz/.translations/pt.json b/homeassistant/components/deconz/.translations/pt.json index 2a00c698691..6ccbfe9f217 100644 --- a/homeassistant/components/deconz/.translations/pt.json +++ b/homeassistant/components/deconz/.translations/pt.json @@ -1,7 +1,32 @@ { "config": { "abort": { - "already_configured": "Bridge j\u00e1 est\u00e1 configurada" - } + "already_configured": "Bridge j\u00e1 est\u00e1 configurada", + "no_bridges": "Nenhum deCONZ descoberto", + "one_instance_only": "Componente suporta apenas uma conex\u00e3o deCONZ" + }, + "error": { + "no_key": "N\u00e3o foi poss\u00edvel obter uma chave de API" + }, + "step": { + "init": { + "data": { + "host": "Servidor", + "port": "Porta (por omiss\u00e3o: '80')" + }, + "title": "Defina o gateway deCONZ" + }, + "link": { + "description": "Desbloqueie o seu gateway deCONZ para se registar no Home Assistant. \n\n 1. V\u00e1 para as configura\u00e7\u00f5es do sistema deCONZ \n 2. Pressione o bot\u00e3o \"Desbloquear Gateway\"", + "title": "Link com deCONZ" + }, + "options": { + "data": { + "allow_clip_sensor": "Permitir a importa\u00e7\u00e3o de sensores virtuais" + }, + "title": "Op\u00e7\u00f5es extra de configura\u00e7\u00e3o para deCONZ" + } + }, + "title": "deCONZ" } } \ No newline at end of file diff --git a/homeassistant/components/deconz/.translations/ru.json b/homeassistant/components/deconz/.translations/ru.json index b0dc6a8a4a8..56490f67cb3 100644 --- a/homeassistant/components/deconz/.translations/ru.json +++ b/homeassistant/components/deconz/.translations/ru.json @@ -19,6 +19,13 @@ "link": { "description": "\u0420\u0430\u0437\u0431\u043b\u043e\u043a\u0438\u0440\u0443\u0439\u0442\u0435 \u0448\u043b\u044e\u0437 deCONZ \u0434\u043b\u044f \u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0430\u0446\u0438\u0438 \u0432 Home Assistant:\n\n1. \u041f\u0435\u0440\u0435\u0439\u0434\u0438\u0442\u0435 \u043a \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430\u043c \u0441\u0438\u0441\u0442\u0435\u043c\u044b deCONZ\n2. \u041d\u0430\u0436\u043c\u0438\u0442\u0435 \u043a\u043d\u043e\u043f\u043a\u0443 \u00ab\u0420\u0430\u0437\u0431\u043b\u043e\u043a\u0438\u0440\u043e\u0432\u0430\u0442\u044c \u0448\u043b\u044e\u0437\u00bb", "title": "\u0421\u0432\u044f\u0437\u044c \u0441 deCONZ" + }, + "options": { + "data": { + "allow_clip_sensor": "\u0420\u0430\u0437\u0440\u0435\u0448\u0438\u0442\u044c \u0438\u043c\u043f\u043e\u0440\u0442 \u0432\u0438\u0440\u0442\u0443\u0430\u043b\u044c\u043d\u044b\u0445 \u0434\u0430\u0442\u0447\u0438\u043a\u043e\u0432", + "allow_deconz_groups": "\u0420\u0430\u0437\u0440\u0435\u0448\u0438\u0442\u044c \u0438\u043c\u043f\u043e\u0440\u0442 \u0433\u0440\u0443\u043f\u043f deCONZ" + }, + "title": "\u0414\u043e\u043f\u043e\u043b\u043d\u0438\u0442\u0435\u043b\u044c\u043d\u044b\u0435 \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u044b \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u0438 \u0434\u043b\u044f deCONZ" } }, "title": "deCONZ" diff --git a/homeassistant/components/deconz/.translations/sl.json b/homeassistant/components/deconz/.translations/sl.json index b738002b273..59c5577c96b 100644 --- a/homeassistant/components/deconz/.translations/sl.json +++ b/homeassistant/components/deconz/.translations/sl.json @@ -19,6 +19,12 @@ "link": { "description": "Odklenite va\u0161 deCONZ gateway za registracijo z Home Assistant-om. \n1. Pojdite v deCONT sistemske nastavitve\n2. Pritisnite tipko \"odkleni prehod\"", "title": "Povezava z deCONZ" + }, + "options": { + "data": { + "allow_clip_sensor": "Dovoli uvoz virtualnih senzorjev" + }, + "title": "Dodatne mo\u017enosti konfiguracije za deCONZ" } }, "title": "deCONZ" diff --git a/homeassistant/components/deconz/.translations/sv.json b/homeassistant/components/deconz/.translations/sv.json new file mode 100644 index 00000000000..88cf8742acd --- /dev/null +++ b/homeassistant/components/deconz/.translations/sv.json @@ -0,0 +1,33 @@ +{ + "config": { + "abort": { + "already_configured": "Bryggan \u00e4r redan konfigurerad", + "no_bridges": "Inga deCONZ-bryggor uppt\u00e4cktes", + "one_instance_only": "Komponenten st\u00f6djer endast en deCONZ-instans" + }, + "error": { + "no_key": "Det gick inte att ta emot en API-nyckel" + }, + "step": { + "init": { + "data": { + "host": "V\u00e4rd", + "port": "Port (standardv\u00e4rde: '80')" + }, + "title": "Definiera deCONZ-gatewaye" + }, + "link": { + "description": "L\u00e5s upp din deCONZ-gateway f\u00f6r att registrera dig med Home Assistant. \n\n 1. G\u00e5 till deCONZ-systeminst\u00e4llningarna \n 2. Tryck p\u00e5 \"L\u00e5s upp gateway\"-knappen", + "title": "L\u00e4nka med deCONZ" + }, + "options": { + "data": { + "allow_clip_sensor": "Till\u00e5t import av virtuella sensorer", + "allow_deconz_groups": "Till\u00e5t import av deCONZ-grupper" + }, + "title": "Extra konfigurationsalternativ f\u00f6r deCONZ" + } + }, + "title": "deCONZ" + } +} \ No newline at end of file diff --git a/homeassistant/components/deconz/.translations/vi.json b/homeassistant/components/deconz/.translations/vi.json new file mode 100644 index 00000000000..00f1d9be57f --- /dev/null +++ b/homeassistant/components/deconz/.translations/vi.json @@ -0,0 +1,26 @@ +{ + "config": { + "abort": { + "already_configured": "C\u1ea7u \u0111\u00e3 \u0111\u01b0\u1ee3c c\u1ea5u h\u00ecnh", + "no_bridges": "Kh\u00f4ng t\u00ecm th\u1ea5y c\u1ea7u deCONZ n\u00e0o", + "one_instance_only": "Th\u00e0nh ph\u1ea7n ch\u1ec9 h\u1ed7 tr\u1ee3 m\u1ed9t c\u00e1 th\u1ec3 deCONZ" + }, + "error": { + "no_key": "Kh\u00f4ng th\u1ec3 l\u1ea5y kh\u00f3a API" + }, + "step": { + "init": { + "data": { + "port": "C\u1ed5ng (gi\u00e1 tr\u1ecb m\u1eb7c \u0111\u1ecbnh: '80')" + } + }, + "options": { + "data": { + "allow_clip_sensor": "Cho ph\u00e9p nh\u1eadp c\u1ea3m bi\u1ebfn \u1ea3o", + "allow_deconz_groups": "Cho ph\u00e9p nh\u1eadp c\u00e1c nh\u00f3m deCONZ" + }, + "title": "T\u00f9y ch\u1ecdn c\u1ea5u h\u00ecnh b\u1ed5 sung cho deCONZ" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/deconz/.translations/zh-Hans.json b/homeassistant/components/deconz/.translations/zh-Hans.json index f41b5b5111c..2e5a216c77d 100644 --- a/homeassistant/components/deconz/.translations/zh-Hans.json +++ b/homeassistant/components/deconz/.translations/zh-Hans.json @@ -19,6 +19,13 @@ "link": { "description": "\u89e3\u9501\u60a8\u7684 deCONZ \u7f51\u5173\u4ee5\u6ce8\u518c\u5230 Home Assistant\u3002 \n\n 1. \u524d\u5f80 deCONZ \u7cfb\u7edf\u8bbe\u7f6e\n 2. \u70b9\u51fb\u201c\u89e3\u9501\u7f51\u5173\u201d\u6309\u94ae", "title": "\u8fde\u63a5 deCONZ" + }, + "options": { + "data": { + "allow_clip_sensor": "\u5141\u8bb8\u5bfc\u5165\u865a\u62df\u4f20\u611f\u5668", + "allow_deconz_groups": "\u5141\u8bb8\u5bfc\u5165 deCONZ \u7fa4\u7ec4" + }, + "title": "deCONZ \u7684\u9644\u52a0\u914d\u7f6e\u9879" } }, "title": "deCONZ" diff --git a/homeassistant/components/deconz/.translations/zh-Hant.json b/homeassistant/components/deconz/.translations/zh-Hant.json index 33be3846eb8..17cbe87f1e8 100644 --- a/homeassistant/components/deconz/.translations/zh-Hant.json +++ b/homeassistant/components/deconz/.translations/zh-Hant.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "already_configured": "Bridge \u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", "no_bridges": "\u672a\u641c\u5c0b\u5230 deCONZ Bridfe", "one_instance_only": "\u7d44\u4ef6\u50c5\u652f\u63f4\u4e00\u7d44 deCONZ \u5be6\u4f8b" }, @@ -18,6 +19,12 @@ "link": { "description": "\u89e3\u9664 deCONZ \u7db2\u95dc\u9396\u5b9a\uff0c\u4ee5\u65bc Home Assistant \u9032\u884c\u8a3b\u518a\u3002\n\n1. \u9032\u5165 deCONZ \u7cfb\u7d71\u8a2d\u5b9a\n2. \u6309\u4e0b\u300c\u89e3\u9664\u7db2\u95dc\u9396\u5b9a\uff08Unlock Gateway\uff09\u300d\u6309\u9215", "title": "\u9023\u7d50\u81f3 deCONZ" + }, + "options": { + "data": { + "allow_clip_sensor": "\u5141\u8a31\u532f\u5165\u865b\u64ec\u611f\u61c9\u5668" + }, + "title": "deCONZ \u9644\u52a0\u8a2d\u5b9a\u9078\u9805" } }, "title": "deCONZ" diff --git a/homeassistant/components/hue/.translations/ca.json b/homeassistant/components/hue/.translations/ca.json new file mode 100644 index 00000000000..6c41eed5467 --- /dev/null +++ b/homeassistant/components/hue/.translations/ca.json @@ -0,0 +1,29 @@ +{ + "config": { + "abort": { + "all_configured": "Tots els enlla\u00e7os Philips Hue ja estan configurats", + "already_configured": "L'enlla\u00e7 ja est\u00e0 configurat", + "cannot_connect": "No es pot connectar amb l'enlla\u00e7", + "discover_timeout": "No s'han pogut descobrir enlla\u00e7os Hue", + "no_bridges": "No s'han trobat enlla\u00e7os Philips Hue", + "unknown": "S'ha produ\u00eft un error desconegut" + }, + "error": { + "linking": "S'ha produ\u00eft un error desconegut al vincular.", + "register_failed": "No s'ha pogut registrar, torneu-ho a provar" + }, + "step": { + "init": { + "data": { + "host": "Amfitri\u00f3" + }, + "title": "Tria l'enlla\u00e7 Hue" + }, + "link": { + "description": "Premeu el bot\u00f3 de l'ella\u00e7 per registrar Philips Hue amb Home Assistant. \n\n ![Ubicaci\u00f3 del bot\u00f3 al pont](/static/images/config_philips_hue.jpg)", + "title": "Vincular concentrador" + } + }, + "title": "Philips Hue" + } +} \ No newline at end of file diff --git a/homeassistant/components/hue/.translations/cs.json b/homeassistant/components/hue/.translations/cs.json new file mode 100644 index 00000000000..35c423b1a03 --- /dev/null +++ b/homeassistant/components/hue/.translations/cs.json @@ -0,0 +1,29 @@ +{ + "config": { + "abort": { + "all_configured": "V\u0161echny Philips Hue p\u0159emost\u011bn\u00ed jsou ji\u017e nakonfigurov\u00e1ny", + "already_configured": "P\u0159emost\u011bn\u00ed je ji\u017e nakonfigurov\u00e1no", + "cannot_connect": "Nelze se p\u0159ipojit k p\u0159emost\u011bn\u00ed", + "discover_timeout": "Nelze nal\u00e9zt p\u0159emost\u011bn\u00ed Hue", + "no_bridges": "Nebyly nalezeny \u017e\u00e1dn\u00e9 p\u0159emost\u011bn\u00ed Philips Hue", + "unknown": "Do\u0161lo k nezn\u00e1m\u00e9 chyb\u011b" + }, + "error": { + "linking": "Do\u0161lo k nezn\u00e1m\u00e9 chyb\u011b propojen\u00ed.", + "register_failed": "Registrace se nezda\u0159ila, zkuste to pros\u00edm znovu" + }, + "step": { + "init": { + "data": { + "host": "Hostitel" + }, + "title": "Vybrat Hue p\u0159emost\u011bn\u00ed" + }, + "link": { + "description": "Stiskn\u011bte tla\u010d\u00edtko na p\u0159emost\u011bn\u00ed k registraci Philips Hue v Home Assistant.\n\n! [Um\u00edst\u011bn\u00ed tla\u010d\u00edtka na p\u0159emost\u011bn\u00ed] (/ static/images/config_philips_hue.jpg)", + "title": "P\u0159ipojit Hub" + } + }, + "title": "Philips Hue p\u0159emost\u011bn\u00ed" + } +} \ No newline at end of file diff --git a/homeassistant/components/hue/.translations/en.json b/homeassistant/components/hue/.translations/en.json index b0459ec3916..cea8d8be10a 100644 --- a/homeassistant/components/hue/.translations/en.json +++ b/homeassistant/components/hue/.translations/en.json @@ -24,6 +24,6 @@ "title": "Link Hub" } }, - "title": "Philips Hue Bridge" + "title": "Philips Hue" } } \ No newline at end of file diff --git a/homeassistant/components/hue/.translations/fr.json b/homeassistant/components/hue/.translations/fr.json new file mode 100644 index 00000000000..73613f237da --- /dev/null +++ b/homeassistant/components/hue/.translations/fr.json @@ -0,0 +1,29 @@ +{ + "config": { + "abort": { + "all_configured": "Tous les ponts Philips Hue sont d\u00e9j\u00e0 configur\u00e9s", + "already_configured": "Ce pont est d\u00e9j\u00e0 configur\u00e9", + "cannot_connect": "Connexion au pont impossible", + "discover_timeout": "D\u00e9tection de ponts Philips Hue impossible", + "no_bridges": "Aucun pont Philips Hue n'a \u00e9t\u00e9 d\u00e9couvert", + "unknown": "Une erreur inconnue s'est produite" + }, + "error": { + "linking": "Une erreur inconnue s'est produite lors de la liaison entre le pont et Home Assistant", + "register_failed": "\u00c9chec d'enregistrement. Veuillez r\u00e9essayer." + }, + "step": { + "init": { + "data": { + "host": "H\u00f4te" + }, + "title": "Choisissez le pont Philips Hue" + }, + "link": { + "description": "Appuyez sur le bouton du pont pour lier Philips Hue avec Home Assistant. \n\n ![Emplacement du bouton sur le pont] (/static/images/config_philips_hue.jpg)", + "title": "Hub de liaison" + } + }, + "title": "Pont Philips Hue" + } +} \ No newline at end of file diff --git a/homeassistant/components/hue/.translations/hu.json b/homeassistant/components/hue/.translations/hu.json index a4032dcbcfc..be6548f59a0 100644 --- a/homeassistant/components/hue/.translations/hu.json +++ b/homeassistant/components/hue/.translations/hu.json @@ -2,7 +2,7 @@ "config": { "abort": { "all_configured": "M\u00e1r minden Philips Hue bridge konfigur\u00e1lt", - "already_configured": "A bridge m\u00e1r konfigur\u00e1lt", + "already_configured": "A bridge m\u00e1r konfigur\u00e1lva van", "cannot_connect": "Nem siker\u00fclt csatlakozni a bridge-hez.", "discover_timeout": "Nem tal\u00e1ltam a Hue bridget", "no_bridges": "Nem tal\u00e1ltam Philips Hue bridget", @@ -20,6 +20,7 @@ "title": "V\u00e1lassz Hue bridge-t" }, "link": { + "description": "Nyomja meg a gombot a bridge-en a Philips Hue Home Assistant-ben val\u00f3 regisztr\u00e1l\u00e1s\u00e1hoz.\n\n![Location of button on bridge](/static/images/config_philips_hue.jpg)", "title": "Kapcsol\u00f3d\u00e1s a hubhoz" } }, diff --git a/homeassistant/components/hue/.translations/it.json b/homeassistant/components/hue/.translations/it.json index 2c7a8c1924d..a9f2a732127 100644 --- a/homeassistant/components/hue/.translations/it.json +++ b/homeassistant/components/hue/.translations/it.json @@ -2,8 +2,27 @@ "config": { "abort": { "all_configured": "Tutti i bridge Philips Hue sono gi\u00e0 configurati", + "already_configured": "Il Bridge \u00e8 gi\u00e0 configurato", + "cannot_connect": "Impossibile connettersi al bridge", "discover_timeout": "Impossibile trovare i bridge Hue", - "no_bridges": "Nessun bridge Hue di Philips trovato" + "no_bridges": "Nessun bridge Hue di Philips trovato", + "unknown": "Si \u00e8 verificato un errore" + }, + "error": { + "linking": "Si \u00e8 verificato un errore sconosciuto in fase di collegamento.", + "register_failed": "Errore in fase di registrazione, riprova" + }, + "step": { + "init": { + "data": { + "host": "Host" + }, + "title": "Selezione il bridge Hue" + }, + "link": { + "description": "Premi il pulsante sul bridge per registrare Philips Hue con Home Assistant\n\n![Posizione del pulsante sul bridge](/static/images/config_philips_hue.jpg)", + "title": "Collega Hub" + } }, "title": "Philips Hue Bridge" } diff --git a/homeassistant/components/hue/.translations/pt-BR.json b/homeassistant/components/hue/.translations/pt-BR.json new file mode 100644 index 00000000000..5c6e409245c --- /dev/null +++ b/homeassistant/components/hue/.translations/pt-BR.json @@ -0,0 +1,29 @@ +{ + "config": { + "abort": { + "all_configured": "Todas as pontes Philips Hue j\u00e1 est\u00e3o configuradas", + "already_configured": "A ponte j\u00e1 est\u00e1 configurada", + "cannot_connect": "N\u00e3o \u00e9 poss\u00edvel conectar-se \u00e0 ponte", + "discover_timeout": "Incapaz de descobrir pontes Hue", + "no_bridges": "N\u00e3o h\u00e1 pontes Philips Hue descobertas", + "unknown": "Ocorreu um erro desconhecido" + }, + "error": { + "linking": "Ocorreu um erro de liga\u00e7\u00e3o desconhecido.", + "register_failed": "Falhou ao registrar, por favor tente novamente" + }, + "step": { + "init": { + "data": { + "host": "Hospedeiro" + }, + "title": "Escolha a ponte Hue" + }, + "link": { + "description": "Pressione o bot\u00e3o na ponte para registrar o Philips Hue com o Home Assistant. \n\n ![Localiza\u00e7\u00e3o do bot\u00e3o na ponte] (/static/images/config_philips_hue.jpg)", + "title": "Hub de links" + } + }, + "title": "Philips Hue" + } +} \ No newline at end of file diff --git a/homeassistant/components/hue/.translations/pt.json b/homeassistant/components/hue/.translations/pt.json index 8c4c45f9c89..f7988d82d8c 100644 --- a/homeassistant/components/hue/.translations/pt.json +++ b/homeassistant/components/hue/.translations/pt.json @@ -1,5 +1,29 @@ { "config": { + "abort": { + "all_configured": "Todas os Philips Hue j\u00e1 est\u00e3o configuradas", + "already_configured": "Hue j\u00e1 est\u00e1 configurado", + "cannot_connect": "N\u00e3o foi poss\u00edvel se conectar", + "discover_timeout": "Nenhum Hue bridge descoberto", + "no_bridges": "Nenhum Philips Hue descoberto", + "unknown": "Ocorreu um erro desconhecido" + }, + "error": { + "linking": "Ocorreu um erro de liga\u00e7\u00e3o desconhecido.", + "register_failed": "Falha ao registrar, por favor, tente novamente" + }, + "step": { + "init": { + "data": { + "host": "Servidor" + }, + "title": "Hue bridge" + }, + "link": { + "description": "Pressione o bot\u00e3o no Philips Hue para registrar com o Home Assistant. \n\n ! [Localiza\u00e7\u00e3o do bot\u00e3o] (/ static / images / config_philips_hue.jpg)", + "title": "Link Hub" + } + }, "title": "" } } \ No newline at end of file diff --git a/homeassistant/components/hue/.translations/sv.json b/homeassistant/components/hue/.translations/sv.json new file mode 100644 index 00000000000..efbcfa544f5 --- /dev/null +++ b/homeassistant/components/hue/.translations/sv.json @@ -0,0 +1,29 @@ +{ + "config": { + "abort": { + "all_configured": "Alla Philips Hue-bryggor \u00e4r redan konfigurerade", + "already_configured": "Bryggan \u00e4r redan konfigurerad", + "cannot_connect": "Det gick inte att ansluta till bryggan", + "discover_timeout": "Det gick inte att uppt\u00e4cka n\u00e5gra Hue-bryggor", + "no_bridges": "Inga Philips Hue-bryggor uppt\u00e4cktes", + "unknown": "Ett ok\u00e4nt fel intr\u00e4ffade" + }, + "error": { + "linking": "Ett ok\u00e4nt l\u00e4nkningsfel intr\u00e4ffade.", + "register_failed": "Misslyckades med att registrera, v\u00e4nligen f\u00f6rs\u00f6k igen" + }, + "step": { + "init": { + "data": { + "host": "V\u00e4rd" + }, + "title": "V\u00e4lj Hue-brygga" + }, + "link": { + "description": "Tryck p\u00e5 knappen p\u00e5 bryggan f\u00f6r att registrera Philips Hue med Home Assistant. \n\n ! [Placering av knapp p\u00e5 brygga] (/ static / images / config_philips_hue.jpg)", + "title": "L\u00e4nka hub" + } + }, + "title": "Philips Hue Brygga" + } +} \ No newline at end of file diff --git a/homeassistant/components/hue/.translations/vi.json b/homeassistant/components/hue/.translations/vi.json new file mode 100644 index 00000000000..5cbd0c4aebf --- /dev/null +++ b/homeassistant/components/hue/.translations/vi.json @@ -0,0 +1,17 @@ +{ + "config": { + "abort": { + "all_configured": "T\u1ea5t c\u1ea3 c\u00e1c c\u1ea7u Philips Hue \u0111\u00e3 \u0111\u01b0\u1ee3c c\u1ea5u h\u00ecnh", + "unknown": "X\u1ea3y ra l\u1ed7i kh\u00f4ng x\u00e1c \u0111\u1ecbnh \u0111\u01b0\u1ee3c" + }, + "error": { + "linking": "\u0110\u00e3 x\u1ea3y ra l\u1ed7i li\u00ean k\u1ebft kh\u00f4ng x\u00e1c \u0111\u1ecbnh.", + "register_failed": "Kh\u00f4ng th\u1ec3 \u0111\u0103ng k\u00fd, vui l\u00f2ng th\u1eed l\u1ea1i" + }, + "step": { + "link": { + "title": "Li\u00ean k\u1ebft Hub" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nest/.translations/ca.json b/homeassistant/components/nest/.translations/ca.json new file mode 100644 index 00000000000..2fb17916aee --- /dev/null +++ b/homeassistant/components/nest/.translations/ca.json @@ -0,0 +1,33 @@ +{ + "config": { + "abort": { + "already_setup": "Nom\u00e9s podeu configurar un \u00fanic compte Nest.", + "authorize_url_fail": "S'ha produ\u00eft un error desconegut al generar l'URL d'autoritzaci\u00f3.", + "authorize_url_timeout": "Temps d'espera generant l'URL d'autoritzaci\u00f3 esgotat.", + "no_flows": "Necessiteu configurar Nest abans de poder autenticar-vos-hi. [Llegiu les instruccions](https://www.home-assistant.io/components/nest/)." + }, + "error": { + "internal_error": "Error intern al validar el codi", + "invalid_code": "Codi inv\u00e0lid", + "timeout": "Temps d'espera de validaci\u00f3 del codi esgotat", + "unknown": "Error desconegut al validar el codi" + }, + "step": { + "init": { + "data": { + "flow_impl": "Prove\u00efdor" + }, + "description": "Trieu a trav\u00e9s de quin prove\u00efdor d'autenticaci\u00f3 us voleu autenticar amb Nest.", + "title": "Prove\u00efdor d'autenticaci\u00f3" + }, + "link": { + "data": { + "code": "Codi pin" + }, + "description": "Per enlla\u00e7ar el vostre compte de Nest, [autoritzeu el vostre compte] ({url}). \n\nDespr\u00e9s de l'autoritzaci\u00f3, copieu i enganxeu el codi pin que es mostra a sota.", + "title": "Enlla\u00e7ar compte de Nest" + } + }, + "title": "Nest" + } +} \ No newline at end of file diff --git a/homeassistant/components/nest/.translations/ko.json b/homeassistant/components/nest/.translations/ko.json new file mode 100644 index 00000000000..0caa70aeff2 --- /dev/null +++ b/homeassistant/components/nest/.translations/ko.json @@ -0,0 +1,33 @@ +{ + "config": { + "abort": { + "already_setup": "\ud558\ub098\uc758 Nest \uacc4\uc815\ub9cc \uad6c\uc131 \ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4.", + "authorize_url_fail": "\uc778\uc99d url \uc0dd\uc131\uc5d0 \uc54c \uc218 \uc5c6\ub294 \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4.", + "authorize_url_timeout": "\uc778\uc99d url \uc0dd\uc131 \uc2dc\uac04\uc774 \ucd08\uacfc\ub418\uc5c8\uc2b5\ub2c8\ub2e4.", + "no_flows": "Nest \ub97c \uc778\uc99d\ud558\uae30 \uc804\uc5d0 Nest \ub97c \uad6c\uc131\ud574\uc57c \ud569\ub2c8\ub2e4. [\uc548\ub0b4](https://www.home-assistant.io/components/nest/)\ub97c \uc77d\uc5b4\ubcf4\uc138\uc694." + }, + "error": { + "internal_error": "\ucf54\ub4dc \uc720\ud6a8\uc131 \uac80\uc0ac\uc5d0 \ub0b4\ubd80 \uc624\ub958 \ubc1c\uc0dd", + "invalid_code": "\uc798\ubabb\ub41c \ucf54\ub4dc", + "timeout": "\ucf54\ub4dc \uc720\ud6a8\uc131 \uac80\uc0ac \uc2dc\uac04 \ucd08\uacfc", + "unknown": "\ucf54\ub4dc \uc720\ud6a8\uc131 \uac80\uc0ac\uc5d0 \uc54c \uc218 \uc5c6\ub294 \uc624\ub958 \ubc1c\uc0dd" + }, + "step": { + "init": { + "data": { + "flow_impl": "\uacf5\uae09\uc790" + }, + "description": "Nest\ub85c \uc778\uc99d\ud558\ub824\ub294 \uc778\uc99d \uacf5\uae09\uc790\ub97c \uc120\ud0dd\ud574\uc8fc\uc138\uc694.", + "title": "\uc778\uc99d \uacf5\uae09\uc790" + }, + "link": { + "data": { + "code": "\ud540 \ucf54\ub4dc" + }, + "description": "Nest \uacc4\uc815\uc744 \uc5f0\uacb0\ud558\ub824\uba74, [\uacc4\uc815 \uc5f0\uacb0 \uc2b9\uc778]({url})\uc744 \ud574\uc8fc\uc138\uc694.\n\n\uc2b9\uc778 \ud6c4, \uc544\ub798\uc758 \ud540 \ucf54\ub4dc\ub97c \ubcf5\uc0ac\ud558\uc5ec \ubd99\uc5ec\ub123\uc73c\uc138\uc694.", + "title": "Nest \uacc4\uc815 \uc5f0\uacb0" + } + }, + "title": "Nest" + } +} \ No newline at end of file diff --git a/homeassistant/components/nest/.translations/no.json b/homeassistant/components/nest/.translations/no.json new file mode 100644 index 00000000000..03cf1a82b81 --- /dev/null +++ b/homeassistant/components/nest/.translations/no.json @@ -0,0 +1,33 @@ +{ + "config": { + "abort": { + "already_setup": "Du kan bare konfigurere en enkelt Nest konto.", + "authorize_url_fail": "Ukjent feil ved generering av autoriseringsadresse.", + "authorize_url_timeout": "Tidsavbrudd ved generering av autoriseringsadresse.", + "no_flows": "Du m\u00e5 konfigurere Nest f\u00f8r du kan autentisere med den. [Vennligst les instruksjonene](https://www.home-assistant.io/components/nest/)." + }, + "error": { + "internal_error": "Intern feil ved validering av kode", + "invalid_code": "Ugyldig kode", + "timeout": "Tidsavbrudd ved validering av kode", + "unknown": "Ukjent feil ved validering av kode" + }, + "step": { + "init": { + "data": { + "flow_impl": "Tilbyder" + }, + "description": "Velg via hvilken autentiseringstilbyder du vil godkjenne med Nest.", + "title": "Autentiseringstilbyder" + }, + "link": { + "data": { + "code": "PIN kode" + }, + "description": "For \u00e5 koble din Nest-konto, [autoriser kontoen din]({url}). \n\n Etter godkjenning, kopier og lim inn den oppgitte PIN koden nedenfor.", + "title": "Koble til Nest konto" + } + }, + "title": "Nest" + } +} \ No newline at end of file diff --git a/homeassistant/components/nest/.translations/pl.json b/homeassistant/components/nest/.translations/pl.json new file mode 100644 index 00000000000..c03b2eff0fa --- /dev/null +++ b/homeassistant/components/nest/.translations/pl.json @@ -0,0 +1,33 @@ +{ + "config": { + "abort": { + "already_setup": "Mo\u017cesz skonfigurowa\u0107 tylko jedno konto Nest.", + "authorize_url_fail": "Nieznany b\u0142\u0105d podczas generowania url autoryzacji.", + "authorize_url_timeout": "Min\u0105\u0142 limit czasu generowania url autoryzacji.", + "no_flows": "Musisz skonfigurowa\u0107 Nest, zanim b\u0119dziesz m\u00f3g\u0142 wykona\u0107 uwierzytelnienie. [Przeczytaj instrukcje](https://www.home-assistant.io/components/nest/)." + }, + "error": { + "internal_error": "Wewn\u0119trzny b\u0142\u0105d sprawdzania poprawno\u015bci kodu", + "invalid_code": "Nieprawid\u0142owy kod", + "timeout": "Min\u0105\u0142 limit czasu sprawdzania poprawno\u015bci kodu", + "unknown": "Nieznany b\u0142\u0105d sprawdzania poprawno\u015bci kodu" + }, + "step": { + "init": { + "data": { + "flow_impl": "Dostawca" + }, + "description": "Wybierz, kt\u00f3rego dostawc\u0119 uwierzytelnienia chcesz u\u017cywa\u0107 z Nest.", + "title": "Dostawca uwierzytelnienia" + }, + "link": { + "data": { + "code": "Kod PIN" + }, + "description": "Aby po\u0142\u0105czy\u0107 z kontem Nest, [wykonaj autoryzacj\u0119]({url}). \n\n Po autoryzacji skopiuj i wklej podany kod PIN poni\u017cej.", + "title": "Po\u0142\u0105cz z kontem Nest" + } + }, + "title": "Nest" + } +} \ No newline at end of file diff --git a/homeassistant/components/nest/.translations/ru.json b/homeassistant/components/nest/.translations/ru.json new file mode 100644 index 00000000000..0f7b9b8dd71 --- /dev/null +++ b/homeassistant/components/nest/.translations/ru.json @@ -0,0 +1,33 @@ +{ + "config": { + "abort": { + "already_setup": "\u0412\u044b \u043c\u043e\u0436\u0435\u0442\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c \u0442\u043e\u043b\u044c\u043a\u043e \u043e\u0434\u043d\u0443 \u0443\u0447\u0435\u0442\u043d\u0443\u044e \u0437\u0430\u043f\u0438\u0441\u044c Nest.", + "authorize_url_fail": "\u041d\u0435\u0438\u0437\u0432\u0435\u0441\u0442\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430 \u043f\u0440\u0438 \u0433\u0435\u043d\u0435\u0440\u0430\u0446\u0438\u0438 \u0441\u0441\u044b\u043b\u043a\u0438 \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0438\u0438.", + "authorize_url_timeout": "\u0418\u0441\u0442\u0435\u043a\u043b\u043e \u0432\u0440\u0435\u043c\u044f \u0433\u0435\u043d\u0435\u0440\u0430\u0446\u0438\u0438 \u0441\u0441\u044b\u043b\u043a\u0438 \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0438\u0438.", + "no_flows": "\u0412\u0430\u043c \u043d\u0443\u0436\u043d\u043e \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c Nest \u043f\u0435\u0440\u0435\u0434 \u0442\u0435\u043c, \u043a\u0430\u043a \u0432\u044b\u043f\u043e\u043b\u043d\u0438\u0442\u044c \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044e. [\u041f\u043e\u0436\u0430\u043b\u0443\u0439\u0441\u0442\u0430, \u043e\u0437\u043d\u0430\u043a\u043e\u043c\u044c\u0442\u0435\u0441\u044c \u0441 \u0438\u043d\u0441\u0442\u0440\u0443\u043a\u0446\u0438\u044f\u043c\u0438](https://www.home-assistant.io/components/nest/)." + }, + "error": { + "internal_error": "\u0412\u043d\u0443\u0442\u0440\u0435\u043d\u043d\u044f\u044f \u043e\u0448\u0438\u0431\u043a\u0430 \u043f\u0440\u043e\u0432\u0435\u0440\u043a\u0438 \u043a\u043e\u0434\u0430", + "invalid_code": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 \u043a\u043e\u0434", + "timeout": "\u0418\u0441\u0442\u0435\u043a\u043b\u043e \u0432\u0440\u0435\u043c\u044f \u043f\u0440\u043e\u0432\u0435\u0440\u043a\u0438 \u043a\u043e\u0434\u0430.", + "unknown": "\u041d\u0435\u0438\u0437\u0432\u0435\u0441\u0442\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430 \u043f\u0440\u043e\u0432\u0435\u0440\u043a\u0438 \u043a\u043e\u0434\u0430" + }, + "step": { + "init": { + "data": { + "flow_impl": "\u041f\u0440\u043e\u0432\u0430\u0439\u0434\u0435\u0440" + }, + "description": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435, \u0447\u0435\u0440\u0435\u0437 \u043a\u0430\u043a\u043e\u0433\u043e \u043f\u0440\u043e\u0432\u0430\u0439\u0434\u0435\u0440\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438 \u0432\u044b \u0445\u043e\u0442\u0438\u0442\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0438\u0442\u044c \u0432\u0445\u043e\u0434 \u0432 Nest.", + "title": "\u041f\u0440\u043e\u0432\u0430\u0439\u0434\u0435\u0440 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438" + }, + "link": { + "data": { + "code": "\u041f\u0438\u043d-\u043a\u043e\u0434" + }, + "description": " [\u0410\u0432\u0442\u043e\u0440\u0438\u0437\u0443\u0439\u0442\u0435\u0441\u044c]({url}), \u0447\u0442\u043e\u0431\u044b \u043f\u0440\u0438\u0432\u044f\u0437\u0430\u0442\u044c \u0441\u0432\u043e\u044e \u0443\u0447\u0435\u0442\u043d\u0443\u044e \u0437\u0430\u043f\u0438\u0441\u044c Nest. \n\n \u041f\u043e\u0441\u043b\u0435 \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0438\u0438 \u0441\u043a\u043e\u043f\u0438\u0440\u0443\u0439\u0442\u0435 \u043f\u0440\u0438\u043b\u0430\u0433\u0430\u0435\u043c\u044b\u0439 \u043f\u0438\u043d-\u043a\u043e\u0434.", + "title": "\u041f\u0440\u0438\u0432\u044f\u0437\u0430\u0442\u044c \u0443\u0447\u0435\u0442\u043d\u0443\u044e \u0437\u0430\u043f\u0438\u0441\u044c Nest" + } + }, + "title": "Nest" + } +} \ No newline at end of file diff --git a/homeassistant/components/nest/.translations/sv.json b/homeassistant/components/nest/.translations/sv.json new file mode 100644 index 00000000000..721f891219d --- /dev/null +++ b/homeassistant/components/nest/.translations/sv.json @@ -0,0 +1,33 @@ +{ + "config": { + "abort": { + "already_setup": "Du kan endast konfigurera ett Nest-konto.", + "authorize_url_fail": "Ok\u00e4nt fel vid generering av autentisieringsadress.", + "authorize_url_timeout": "Timeout vid generering av en autentisieringsadress.", + "no_flows": "Du m\u00e5ste konfigurera Nest innan du kan autentisera med det. [V\u00e4nligen l\u00e4s instruktionerna] (https://www.home-assistant.io/components/nest/)." + }, + "error": { + "internal_error": "Internt fel vid validering av kod", + "invalid_code": "Ogiltig kod", + "timeout": "Timeout vid valididering av kod", + "unknown": "Ok\u00e4nt fel vid validering av kod" + }, + "step": { + "init": { + "data": { + "flow_impl": "Leverant\u00f6r" + }, + "description": "V\u00e4lj den autentiseringsleverant\u00f6r som du vill autentisera med mot Nest.", + "title": "Autentiseringsleverant\u00f6r" + }, + "link": { + "data": { + "code": "Pin-kod" + }, + "description": "F\u00f6r att l\u00e4nka ditt Nest-konto, [autentisiera ditt konto]({url}). \n\nEfter autentisiering, klipp och klistra in den angivna pin-koden nedan.", + "title": "L\u00e4nka Nest-konto" + } + }, + "title": "Nest" + } +} \ No newline at end of file diff --git a/homeassistant/components/nest/.translations/vi.json b/homeassistant/components/nest/.translations/vi.json new file mode 100644 index 00000000000..996c6c68eae --- /dev/null +++ b/homeassistant/components/nest/.translations/vi.json @@ -0,0 +1,22 @@ +{ + "config": { + "error": { + "internal_error": "M\u00e3 x\u00e1c th\u1ef1c l\u1ed7i n\u1ed9i b\u1ed9", + "invalid_code": "M\u00e3 kh\u00f4ng h\u1ee3p l\u1ec7", + "timeout": "M\u00e3 x\u00e1c th\u1ef1c h\u1ebft th\u1eddi gian ch\u1edd", + "unknown": "M\u00e3 x\u00e1c th\u1ef1c l\u1ed7i kh\u00f4ng x\u00e1c \u0111\u1ecbnh" + }, + "step": { + "init": { + "data": { + "flow_impl": "Nh\u00e0 cung c\u1ea5p" + }, + "title": "Nh\u00e0 cung c\u1ea5p x\u00e1c th\u1ef1c" + }, + "link": { + "title": "Li\u00ean k\u1ebft t\u00e0i kho\u1ea3n Nest" + } + }, + "title": "Nest" + } +} \ No newline at end of file diff --git a/homeassistant/components/nest/.translations/zh-Hans.json b/homeassistant/components/nest/.translations/zh-Hans.json new file mode 100644 index 00000000000..05ba5bdf155 --- /dev/null +++ b/homeassistant/components/nest/.translations/zh-Hans.json @@ -0,0 +1,33 @@ +{ + "config": { + "abort": { + "already_setup": "\u60a8\u53ea\u80fd\u914d\u7f6e\u4e00\u4e2a Nest \u5e10\u6237\u3002", + "authorize_url_fail": "\u751f\u6210\u6388\u6743\u7f51\u5740\u65f6\u53d1\u751f\u672a\u77e5\u9519\u8bef\u3002", + "authorize_url_timeout": "\u751f\u6210\u6388\u6743\u7f51\u5740\u8d85\u65f6\u3002", + "no_flows": "\u60a8\u9700\u8981\u5148\u914d\u7f6e Nest\uff0c\u7136\u540e\u624d\u80fd\u5bf9\u5176\u8fdb\u884c\u6388\u6743\u3002 [\u8bf7\u9605\u8bfb\u8bf4\u660e](https://www.home-assistant.io/components/nest/)\u3002" + }, + "error": { + "internal_error": "\u9a8c\u8bc1\u4ee3\u7801\u65f6\u53d1\u751f\u5185\u90e8\u9519\u8bef", + "invalid_code": "\u65e0\u6548\u4ee3\u7801", + "timeout": "\u4ee3\u7801\u9a8c\u8bc1\u8d85\u65f6", + "unknown": "\u9a8c\u8bc1\u4ee3\u7801\u65f6\u53d1\u751f\u672a\u77e5\u9519\u8bef" + }, + "step": { + "init": { + "data": { + "flow_impl": "\u63d0\u4f9b\u8005" + }, + "description": "\u9009\u62e9\u60a8\u60f3\u901a\u8fc7\u54ea\u4e2a\u6388\u6743\u63d0\u4f9b\u8005\u4e0e Nest \u8fdb\u884c\u6388\u6743\u3002", + "title": "\u6388\u6743\u63d0\u4f9b\u8005" + }, + "link": { + "data": { + "code": "PIN \u7801" + }, + "description": "\u8981\u5173\u8054 Nest \u5e10\u6237\uff0c\u8bf7[\u6388\u6743\u5e10\u6237]({url})\u3002\n\n\u5b8c\u6210\u6388\u6743\u540e\uff0c\u5728\u4e0b\u9762\u7c98\u8d34\u83b7\u5f97\u7684 PIN \u7801\u3002", + "title": "\u5173\u8054 Nest \u5e10\u6237" + } + }, + "title": "Nest" + } +} \ No newline at end of file diff --git a/homeassistant/components/sensor/.translations/season.ca.json b/homeassistant/components/sensor/.translations/season.ca.json new file mode 100644 index 00000000000..9bce187ec65 --- /dev/null +++ b/homeassistant/components/sensor/.translations/season.ca.json @@ -0,0 +1,8 @@ +{ + "state": { + "autumn": "Tardor", + "spring": "Primavera", + "summer": "Estiu", + "winter": "Hivern" + } +} \ No newline at end of file diff --git a/homeassistant/components/sensor/.translations/season.fr.json b/homeassistant/components/sensor/.translations/season.fr.json new file mode 100644 index 00000000000..ec9f9657428 --- /dev/null +++ b/homeassistant/components/sensor/.translations/season.fr.json @@ -0,0 +1,8 @@ +{ + "state": { + "autumn": "Automne", + "spring": "Printemps", + "summer": "\u00c9t\u00e9", + "winter": "Hiver" + } +} \ No newline at end of file diff --git a/homeassistant/components/sensor/.translations/season.pt-BR.json b/homeassistant/components/sensor/.translations/season.pt-BR.json new file mode 100644 index 00000000000..fde45ad6c8e --- /dev/null +++ b/homeassistant/components/sensor/.translations/season.pt-BR.json @@ -0,0 +1,8 @@ +{ + "state": { + "autumn": "Outono", + "spring": "Primavera", + "summer": "Ver\u00e3o", + "winter": "Inverno" + } +} \ No newline at end of file diff --git a/homeassistant/components/sonos/.translations/ca.json b/homeassistant/components/sonos/.translations/ca.json new file mode 100644 index 00000000000..9a745784b25 --- /dev/null +++ b/homeassistant/components/sonos/.translations/ca.json @@ -0,0 +1,15 @@ +{ + "config": { + "abort": { + "no_devices_found": "No s'han trobat dispositius Sonos a la xarxa.", + "single_instance_allowed": "Nom\u00e9s cal una \u00fanica configuraci\u00f3 de Sonos." + }, + "step": { + "confirm": { + "description": "Voleu configurar Sonos?", + "title": "Sonos" + } + }, + "title": "Sonos" + } +} \ No newline at end of file diff --git a/homeassistant/components/sonos/.translations/ko.json b/homeassistant/components/sonos/.translations/ko.json new file mode 100644 index 00000000000..5453e4322cd --- /dev/null +++ b/homeassistant/components/sonos/.translations/ko.json @@ -0,0 +1,15 @@ +{ + "config": { + "abort": { + "no_devices_found": "Sonos \uc7a5\uce58\uac00 \ub124\ud2b8\uc6cc\ud06c\uc5d0\uc11c \ubc1c\uacac\ub418\uc9c0 \uc54a\uc558\uc2b5\ub2c8\ub2e4.", + "single_instance_allowed": "Sonos\uc758 \ub2e8\uc77c \uad6c\uc131 \ub9cc \ud544\uc694\ud569\ub2c8\ub2e4." + }, + "step": { + "confirm": { + "description": "Sonos\ub97c \uc124\uc815 \ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?", + "title": "Sonos" + } + }, + "title": "Sonos" + } +} \ No newline at end of file diff --git a/homeassistant/components/sonos/.translations/no.json b/homeassistant/components/sonos/.translations/no.json new file mode 100644 index 00000000000..c837abad499 --- /dev/null +++ b/homeassistant/components/sonos/.translations/no.json @@ -0,0 +1,15 @@ +{ + "config": { + "abort": { + "no_devices_found": "Ingen Sonos enheter funnet p\u00e5 nettverket.", + "single_instance_allowed": "Kun en enkelt konfigurasjon av Sonos er n\u00f8dvendig." + }, + "step": { + "confirm": { + "description": "\u00d8nsker du \u00e5 sette opp Sonos?", + "title": "Sonos" + } + }, + "title": "Sonos" + } +} \ No newline at end of file diff --git a/homeassistant/components/sonos/.translations/pl.json b/homeassistant/components/sonos/.translations/pl.json new file mode 100644 index 00000000000..2a0c526b9a6 --- /dev/null +++ b/homeassistant/components/sonos/.translations/pl.json @@ -0,0 +1,15 @@ +{ + "config": { + "abort": { + "no_devices_found": "Nie znaleziono w sieci urz\u0105dze\u0144 Sonos.", + "single_instance_allowed": "Wymagana jest tylko jedna konfiguracja Sonos." + }, + "step": { + "confirm": { + "description": "Chcesz skonfigurowa\u0107 Sonos?", + "title": "Sonos" + } + }, + "title": "Sonos" + } +} \ No newline at end of file diff --git a/homeassistant/components/sonos/.translations/ru.json b/homeassistant/components/sonos/.translations/ru.json new file mode 100644 index 00000000000..63b6bd87c20 --- /dev/null +++ b/homeassistant/components/sonos/.translations/ru.json @@ -0,0 +1,15 @@ +{ + "config": { + "abort": { + "no_devices_found": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 Sonos \u043d\u0435 \u043d\u0430\u0439\u0434\u0435\u043d\u044b.", + "single_instance_allowed": "\u041d\u0435\u043e\u0431\u0445\u043e\u0434\u0438\u043c\u0430 \u0442\u043e\u043b\u044c\u043a\u043e \u043e\u0434\u043d\u0430 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f Sonos." + }, + "step": { + "confirm": { + "description": "\u0412\u044b \u0445\u043e\u0442\u0438\u0442\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c Sonos?", + "title": "Sonos" + } + }, + "title": "Sonos" + } +} \ No newline at end of file diff --git a/homeassistant/components/sonos/.translations/sv.json b/homeassistant/components/sonos/.translations/sv.json new file mode 100644 index 00000000000..756fe8a7483 --- /dev/null +++ b/homeassistant/components/sonos/.translations/sv.json @@ -0,0 +1,15 @@ +{ + "config": { + "abort": { + "no_devices_found": "Inga Sonos-enheter hittades i n\u00e4tverket.", + "single_instance_allowed": "Endast en enda konfiguration av Sonos \u00e4r n\u00f6dv\u00e4ndig." + }, + "step": { + "confirm": { + "description": "Vill du konfigurera Sonos?", + "title": "Sonos" + } + }, + "title": "Sonos" + } +} \ No newline at end of file diff --git a/homeassistant/components/sonos/.translations/vi.json b/homeassistant/components/sonos/.translations/vi.json new file mode 100644 index 00000000000..ebeb1a8b07c --- /dev/null +++ b/homeassistant/components/sonos/.translations/vi.json @@ -0,0 +1,15 @@ +{ + "config": { + "abort": { + "no_devices_found": "Kh\u00f4ng t\u00ecm th\u1ea5y thi\u1ebft b\u1ecb Sonos n\u00e0o tr\u00ean m\u1ea1ng.", + "single_instance_allowed": "Ch\u1ec9 c\u1ea7n m\u1ed9t c\u1ea5u h\u00ecnh duy nh\u1ea5t c\u1ee7a Sonos l\u00e0 \u0111\u1ee7." + }, + "step": { + "confirm": { + "description": "B\u1ea1n c\u00f3 mu\u1ed1n thi\u1ebft l\u1eadp Sonos kh\u00f4ng?", + "title": "Sonos" + } + }, + "title": "Sonos" + } +} \ No newline at end of file diff --git a/homeassistant/components/sonos/.translations/zh-Hans.json b/homeassistant/components/sonos/.translations/zh-Hans.json new file mode 100644 index 00000000000..17c1e78d3e8 --- /dev/null +++ b/homeassistant/components/sonos/.translations/zh-Hans.json @@ -0,0 +1,15 @@ +{ + "config": { + "abort": { + "no_devices_found": "\u6ca1\u6709\u5728\u7f51\u7edc\u4e0a\u627e\u5230 Sonos \u8bbe\u5907\u3002", + "single_instance_allowed": "\u53ea\u6709\u4e00\u6b21 Sonos \u914d\u7f6e\u662f\u5fc5\u8981\u7684\u3002" + }, + "step": { + "confirm": { + "description": "\u60a8\u60f3\u8981\u914d\u7f6e Sonos \u5417\uff1f", + "title": "Sonos" + } + }, + "title": "Sonos" + } +} \ No newline at end of file diff --git a/homeassistant/components/zone/.translations/bg.json b/homeassistant/components/zone/.translations/bg.json new file mode 100644 index 00000000000..5770058c5eb --- /dev/null +++ b/homeassistant/components/zone/.translations/bg.json @@ -0,0 +1,21 @@ +{ + "config": { + "error": { + "name_exists": "\u0418\u043c\u0435\u0442\u043e \u0432\u0435\u0447\u0435 \u0441\u044a\u0449\u0435\u0441\u0442\u0432\u0443\u0432\u0430" + }, + "step": { + "init": { + "data": { + "icon": "\u0418\u043a\u043e\u043d\u0430", + "latitude": "\u0428\u0438\u0440\u0438\u043d\u0430", + "longitude": "\u0414\u044a\u043b\u0436\u0438\u043d\u0430", + "name": "\u0418\u043c\u0435", + "passive": "\u041f\u0430\u0441\u0438\u0432\u043d\u0430", + "radius": "\u0420\u0430\u0434\u0438\u0443\u0441" + }, + "title": "\u041e\u043f\u0440\u0435\u0434\u0435\u043b\u0435\u0442\u0435 \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u0438\u0442\u0435 \u043d\u0430 \u0437\u043e\u043d\u0430\u0442\u0430" + } + }, + "title": "\u0417\u043e\u043d\u0430" + } +} \ No newline at end of file diff --git a/homeassistant/components/zone/.translations/ca.json b/homeassistant/components/zone/.translations/ca.json new file mode 100644 index 00000000000..1676c8f3906 --- /dev/null +++ b/homeassistant/components/zone/.translations/ca.json @@ -0,0 +1,21 @@ +{ + "config": { + "error": { + "name_exists": "El nom ja existeix" + }, + "step": { + "init": { + "data": { + "icon": "Icona", + "latitude": "Latitud", + "longitude": "Longitud", + "name": "Nom", + "passive": "Passiu", + "radius": "Radi" + }, + "title": "Defineix els par\u00e0metres de la zona" + } + }, + "title": "Zona" + } +} \ No newline at end of file diff --git a/homeassistant/components/zone/.translations/cs.json b/homeassistant/components/zone/.translations/cs.json new file mode 100644 index 00000000000..a521377e5e0 --- /dev/null +++ b/homeassistant/components/zone/.translations/cs.json @@ -0,0 +1,21 @@ +{ + "config": { + "error": { + "name_exists": "N\u00e1zev ji\u017e existuje" + }, + "step": { + "init": { + "data": { + "icon": "Ikona", + "latitude": "Zem\u011bpisn\u00e1 \u0161\u00ed\u0159ka", + "longitude": "Zem\u011bpisn\u00e1 d\u00e9lka", + "name": "N\u00e1zev", + "passive": "Pasivn\u00ed", + "radius": "Polom\u011br" + }, + "title": "Definujte parametry z\u00f3ny" + } + }, + "title": "Z\u00f3na" + } +} \ No newline at end of file diff --git a/homeassistant/components/zone/.translations/fr.json b/homeassistant/components/zone/.translations/fr.json new file mode 100644 index 00000000000..eb02aba7b50 --- /dev/null +++ b/homeassistant/components/zone/.translations/fr.json @@ -0,0 +1,21 @@ +{ + "config": { + "error": { + "name_exists": "Ce nom est d\u00e9j\u00e0 utilis\u00e9" + }, + "step": { + "init": { + "data": { + "icon": "Ic\u00f4ne", + "latitude": "Latitude", + "longitude": "Longitude", + "name": "Nom", + "passive": "Passif", + "radius": "Rayon" + }, + "title": "D\u00e9finir les param\u00e8tres de la zone" + } + }, + "title": "Zone" + } +} \ No newline at end of file diff --git a/homeassistant/components/zone/.translations/hu.json b/homeassistant/components/zone/.translations/hu.json new file mode 100644 index 00000000000..0181f688c27 --- /dev/null +++ b/homeassistant/components/zone/.translations/hu.json @@ -0,0 +1,21 @@ +{ + "config": { + "error": { + "name_exists": "A n\u00e9v m\u00e1r l\u00e9tezik" + }, + "step": { + "init": { + "data": { + "icon": "Ikon", + "latitude": "Sz\u00e9less\u00e9g", + "longitude": "Hossz\u00fas\u00e1g", + "name": "N\u00e9v", + "passive": "Passz\u00edv", + "radius": "Sug\u00e1r" + }, + "title": "Z\u00f3na param\u00e9terek megad\u00e1sa" + } + }, + "title": "Z\u00f3na" + } +} \ No newline at end of file diff --git a/homeassistant/components/zone/.translations/it.json b/homeassistant/components/zone/.translations/it.json new file mode 100644 index 00000000000..4490124510f --- /dev/null +++ b/homeassistant/components/zone/.translations/it.json @@ -0,0 +1,21 @@ +{ + "config": { + "error": { + "name_exists": "Il nome \u00e8 gi\u00e0 esistente" + }, + "step": { + "init": { + "data": { + "icon": "Icona", + "latitude": "Latitudine", + "longitude": "Logitudine", + "name": "Nome", + "passive": "Passiva", + "radius": "Raggio" + }, + "title": "Imposta i parametri della zona" + } + }, + "title": "Zona" + } +} \ No newline at end of file diff --git a/homeassistant/components/zone/.translations/ko.json b/homeassistant/components/zone/.translations/ko.json index 364f8f3cc77..421f079a67e 100644 --- a/homeassistant/components/zone/.translations/ko.json +++ b/homeassistant/components/zone/.translations/ko.json @@ -13,7 +13,7 @@ "passive": "\uc790\ub3d9\ud654 \uc804\uc6a9", "radius": "\ubc18\uacbd" }, - "title": "\uad6c\uc5ed \ub9e4\uac1c \ubcc0\uc218 \uc815\uc758" + "title": "\uad6c\uc5ed \uc124\uc815" } }, "title": "\uad6c\uc5ed" diff --git a/homeassistant/components/zone/.translations/pt-BR.json b/homeassistant/components/zone/.translations/pt-BR.json new file mode 100644 index 00000000000..f2a41b0b267 --- /dev/null +++ b/homeassistant/components/zone/.translations/pt-BR.json @@ -0,0 +1,21 @@ +{ + "config": { + "error": { + "name_exists": "O nome j\u00e1 existe" + }, + "step": { + "init": { + "data": { + "icon": "\u00cdcone", + "latitude": "Latitude", + "longitude": "Longitude", + "name": "Nome", + "passive": "Passivo", + "radius": "Raio" + }, + "title": "Definir par\u00e2metros da zona" + } + }, + "title": "Zona" + } +} \ No newline at end of file diff --git a/homeassistant/components/zone/.translations/pt.json b/homeassistant/components/zone/.translations/pt.json index a4ced557805..2c3292e58c1 100644 --- a/homeassistant/components/zone/.translations/pt.json +++ b/homeassistant/components/zone/.translations/pt.json @@ -12,7 +12,8 @@ "name": "Nome", "passive": "Passivo", "radius": "Raio" - } + }, + "title": "Definir os par\u00e2metros da zona" } }, "title": "Zona" diff --git a/homeassistant/components/zone/.translations/sl.json b/homeassistant/components/zone/.translations/sl.json new file mode 100644 index 00000000000..1885cb5d2c8 --- /dev/null +++ b/homeassistant/components/zone/.translations/sl.json @@ -0,0 +1,21 @@ +{ + "config": { + "error": { + "name_exists": "Ime \u017ee obstaja" + }, + "step": { + "init": { + "data": { + "icon": "Ikona", + "latitude": "Zemljepisna \u0161irina", + "longitude": "Zemljepisna dol\u017eina", + "name": "Ime", + "passive": "Pasivno", + "radius": "Radij" + }, + "title": "Dolo\u010dite parametre obmo\u010dja" + } + }, + "title": "Obmo\u010dje" + } +} \ No newline at end of file diff --git a/homeassistant/components/zone/.translations/sv.json b/homeassistant/components/zone/.translations/sv.json new file mode 100644 index 00000000000..55c5bcf7127 --- /dev/null +++ b/homeassistant/components/zone/.translations/sv.json @@ -0,0 +1,21 @@ +{ + "config": { + "error": { + "name_exists": "Namnet finns redan" + }, + "step": { + "init": { + "data": { + "icon": "Ikon", + "latitude": "Latitud", + "longitude": "Longitud", + "name": "Namn", + "passive": "Passiv", + "radius": "Radie" + }, + "title": "Definiera zonparametrar" + } + }, + "title": "Zon" + } +} \ No newline at end of file diff --git a/homeassistant/components/zone/.translations/vi.json b/homeassistant/components/zone/.translations/vi.json new file mode 100644 index 00000000000..7217944bd6b --- /dev/null +++ b/homeassistant/components/zone/.translations/vi.json @@ -0,0 +1,21 @@ +{ + "config": { + "error": { + "name_exists": "T\u00ean \u0111\u00e3 t\u1ed3n t\u1ea1i" + }, + "step": { + "init": { + "data": { + "icon": "Bi\u1ec3u t\u01b0\u1ee3ng", + "latitude": "V\u0129 \u0111\u1ed9", + "longitude": "Kinh \u0111\u1ed9", + "name": "T\u00ean", + "passive": "Th\u1ee5 \u0111\u1ed9ng", + "radius": "B\u00e1n k\u00ednh" + }, + "title": "X\u00e1c \u0111\u1ecbnh tham s\u1ed1 v\u00f9ng" + } + }, + "title": "V\u00f9ng" + } +} \ No newline at end of file diff --git a/homeassistant/components/zone/.translations/zh-Hant.json b/homeassistant/components/zone/.translations/zh-Hant.json new file mode 100644 index 00000000000..12c1141397d --- /dev/null +++ b/homeassistant/components/zone/.translations/zh-Hant.json @@ -0,0 +1,21 @@ +{ + "config": { + "error": { + "name_exists": "\u8a72\u540d\u7a31\u5df2\u5b58\u5728" + }, + "step": { + "init": { + "data": { + "icon": "\u5716\u793a", + "latitude": "\u7def\u5ea6", + "longitude": "\u7d93\u5ea6", + "name": "\u540d\u7a31", + "passive": "\u88ab\u52d5", + "radius": "\u534a\u5f91" + }, + "title": "\u5b9a\u7fa9\u5340\u57df\u53c3\u6578" + } + }, + "title": "\u5340\u57df" + } +} \ No newline at end of file diff --git a/homeassistant/config_entries.py b/homeassistant/config_entries.py index 4fbbbb77b79..db2912d7b42 100644 --- a/homeassistant/config_entries.py +++ b/homeassistant/config_entries.py @@ -151,6 +151,8 @@ DISCOVERY_SOURCES = ( data_entry_flow.SOURCE_IMPORT, ) +EVENT_FLOW_DISCOVERED = 'config_entry_discovered' + class ConfigEntry: """Hold a configuration entry.""" @@ -404,6 +406,7 @@ class ConfigEntries: # Create notification. if source in DISCOVERY_SOURCES: + self.hass.bus.async_fire(EVENT_FLOW_DISCOVERED) self.hass.components.persistent_notification.async_create( title='New devices discovered', message=("We have discovered new devices on your network. " From 86c6b4d8e3bfb728f4917e17fcb2f06f1faa3efd Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Sun, 17 Jun 2018 20:34:47 +0200 Subject: [PATCH 119/144] Fix panel URL authentication for Hass.io (#15024) * Update http.py * Update http.py * fix tests * Update test_http.py --- homeassistant/components/hassio/http.py | 2 +- tests/components/hassio/test_http.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/hassio/http.py b/homeassistant/components/hassio/http.py index bb4f8219a33..c51d45cc339 100644 --- a/homeassistant/components/hassio/http.py +++ b/homeassistant/components/hassio/http.py @@ -36,7 +36,7 @@ NO_TIMEOUT = { } NO_AUTH = { - re.compile(r'^app-(es5|latest)/.+$'), + re.compile(r'^app/.*$'), re.compile(r'^addons/[^/]*/logo$') } diff --git a/tests/components/hassio/test_http.py b/tests/components/hassio/test_http.py index 5f2c9c009c3..ce260225097 100644 --- a/tests/components/hassio/test_http.py +++ b/tests/components/hassio/test_http.py @@ -47,8 +47,8 @@ def test_auth_required_forward_request(hassio_client): @asyncio.coroutine @pytest.mark.parametrize( 'build_type', [ - 'es5/index.html', 'es5/hassio-app.html', 'latest/index.html', - 'latest/hassio-app.html', 'es5/some-chunk.js', 'es5/app.js', + 'app/index.html', 'app/hassio-app.html', 'app/index.html', + 'app/hassio-app.html', 'app/some-chunk.js', 'app/app.js', ]) def test_forward_request_no_auth_for_panel(hassio_client, build_type): """Test no auth needed for .""" From 5a3ea74a2614bf1b9203e864d34d33d291af138e Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 18 Jun 2018 09:58:16 -0400 Subject: [PATCH 120/144] Bump frontend to 20180618.0 --- homeassistant/components/frontend/__init__.py | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/frontend/__init__.py b/homeassistant/components/frontend/__init__.py index 25aa0da0a3e..2c9b68bf079 100644 --- a/homeassistant/components/frontend/__init__.py +++ b/homeassistant/components/frontend/__init__.py @@ -25,7 +25,7 @@ from homeassistant.helpers.translation import async_get_translations from homeassistant.loader import bind_hass from homeassistant.util.yaml import load_yaml -REQUIREMENTS = ['home-assistant-frontend==20180617.0'] +REQUIREMENTS = ['home-assistant-frontend==20180618.0'] DOMAIN = 'frontend' DEPENDENCIES = ['api', 'websocket_api', 'http', 'system_log'] diff --git a/requirements_all.txt b/requirements_all.txt index d860112c7f8..e2507fa8cbe 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -404,7 +404,7 @@ hipnotify==1.0.8 holidays==0.9.5 # homeassistant.components.frontend -home-assistant-frontend==20180617.0 +home-assistant-frontend==20180618.0 # homeassistant.components.homekit_controller # homekit==0.6 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index a2245c02cf1..d4a24fbc084 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -81,7 +81,7 @@ hbmqtt==0.9.2 holidays==0.9.5 # homeassistant.components.frontend -home-assistant-frontend==20180617.0 +home-assistant-frontend==20180618.0 # homeassistant.components.influxdb # homeassistant.components.sensor.influxdb From ef39bca52eb24622d79bed0e3bdc09b6b14eebe4 Mon Sep 17 00:00:00 2001 From: Martin Hjelmare Date: Mon, 18 Jun 2018 15:21:41 +0200 Subject: [PATCH 121/144] Fix linode I/O in state property (#15010) * Fix linode I/O in state property * Move update of all attrs to update --- .../components/binary_sensor/linode.py | 33 ++++++++--------- homeassistant/components/switch/linode.py | 35 ++++++++++--------- 2 files changed, 35 insertions(+), 33 deletions(-) diff --git a/homeassistant/components/binary_sensor/linode.py b/homeassistant/components/binary_sensor/linode.py index 8af0318373d..d4fc60696cd 100644 --- a/homeassistant/components/binary_sensor/linode.py +++ b/homeassistant/components/binary_sensor/linode.py @@ -52,19 +52,18 @@ class LinodeBinarySensor(BinarySensorDevice): self._node_id = node_id self._state = None self.data = None + self._attrs = {} + self._name = None @property def name(self): """Return the name of the sensor.""" - if self.data is not None: - return self.data.label + return self._name @property def is_on(self): """Return true if the binary sensor is on.""" - if self.data is not None: - return self.data.status == 'running' - return False + return self._state @property def device_class(self): @@ -74,8 +73,18 @@ class LinodeBinarySensor(BinarySensorDevice): @property def device_state_attributes(self): """Return the state attributes of the Linode Node.""" - if self.data: - return { + return self._attrs + + def update(self): + """Update state of sensor.""" + self._linode.update() + if self._linode.data is not None: + for node in self._linode.data: + if node.id == self._node_id: + self.data = node + if self.data is not None: + self._state = self.data.status == 'running' + self._attrs = { ATTR_CREATED: self.data.created, ATTR_NODE_ID: self.data.id, ATTR_NODE_NAME: self.data.label, @@ -85,12 +94,4 @@ class LinodeBinarySensor(BinarySensorDevice): ATTR_REGION: self.data.region.country, ATTR_VCPUS: self.data.specs.vcpus, } - return {} - - def update(self): - """Update state of sensor.""" - self._linode.update() - if self._linode.data is not None: - for node in self._linode.data: - if node.id == self._node_id: - self.data = node + self._name = self.data.label diff --git a/homeassistant/components/switch/linode.py b/homeassistant/components/switch/linode.py index 91177e32116..43f4bdc31b4 100644 --- a/homeassistant/components/switch/linode.py +++ b/homeassistant/components/switch/linode.py @@ -51,35 +51,23 @@ class LinodeSwitch(SwitchDevice): self._node_id = node_id self.data = None self._state = None + self._attrs = {} + self._name = None @property def name(self): """Return the name of the switch.""" - if self.data is not None: - return self.data.label + return self._name @property def is_on(self): """Return true if switch is on.""" - if self.data is not None: - return self.data.status == 'running' - return False + return self._state @property def device_state_attributes(self): """Return the state attributes of the Linode Node.""" - if self.data: - return { - ATTR_CREATED: self.data.created, - ATTR_NODE_ID: self.data.id, - ATTR_NODE_NAME: self.data.label, - ATTR_IPV4_ADDRESS: self.data.ipv4, - ATTR_IPV6_ADDRESS: self.data.ipv6, - ATTR_MEMORY: self.data.specs.memory, - ATTR_REGION: self.data.region.country, - ATTR_VCPUS: self.data.specs.vcpus, - } - return {} + return self._attrs def turn_on(self, **kwargs): """Boot-up the Node.""" @@ -98,3 +86,16 @@ class LinodeSwitch(SwitchDevice): for node in self._linode.data: if node.id == self._node_id: self.data = node + if self.data is not None: + self._state = self.data.status == 'running' + self._attrs = { + ATTR_CREATED: self.data.created, + ATTR_NODE_ID: self.data.id, + ATTR_NODE_NAME: self.data.label, + ATTR_IPV4_ADDRESS: self.data.ipv4, + ATTR_IPV6_ADDRESS: self.data.ipv6, + ATTR_MEMORY: self.data.specs.memory, + ATTR_REGION: self.data.region.country, + ATTR_VCPUS: self.data.specs.vcpus, + } + self._name = self.data.label From e29dfa8609d555b8e69c74ebf7f829f6ff431d53 Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Mon, 18 Jun 2018 02:24:11 +0200 Subject: [PATCH 122/144] Upgrade aiohttp to 3.3.2 (#15025) --- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- setup.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index c69e9eb4af4..5e7386242ba 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -1,4 +1,4 @@ -aiohttp==3.3.0 +aiohttp==3.3.2 astral==1.6.1 async_timeout==3.0.0 attrs==18.1.0 diff --git a/requirements_all.txt b/requirements_all.txt index e2507fa8cbe..bbf74004be4 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1,5 +1,5 @@ # Home Assistant core -aiohttp==3.3.0 +aiohttp==3.3.2 astral==1.6.1 async_timeout==3.0.0 attrs==18.1.0 diff --git a/setup.py b/setup.py index a4d15feb7fc..f914e032fd7 100755 --- a/setup.py +++ b/setup.py @@ -42,7 +42,7 @@ DOWNLOAD_URL = '{}/archive/{}.zip'.format(GITHUB_URL, hass_const.__version__) PACKAGES = find_packages(exclude=['tests', 'tests.*']) REQUIRES = [ - 'aiohttp==3.3.0', + 'aiohttp==3.3.2', 'astral==1.6.1', 'async_timeout==3.0.0', 'attrs==18.1.0', From e0cea2d18d25ed6dda440a0c185af6aabf5c3ed3 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sun, 17 Jun 2018 23:55:35 -0400 Subject: [PATCH 123/144] Make zone entries work without radius (#15032) --- homeassistant/components/zone/__init__.py | 4 ++-- tests/components/zone/test_init.py | 1 - 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/zone/__init__.py b/homeassistant/components/zone/__init__.py index c33a16c632e..ee19e00266c 100644 --- a/homeassistant/components/zone/__init__.py +++ b/homeassistant/components/zone/__init__.py @@ -73,8 +73,8 @@ async def async_setup_entry(hass, config_entry): entry = config_entry.data name = entry[CONF_NAME] zone = Zone(hass, name, entry[CONF_LATITUDE], entry[CONF_LONGITUDE], - entry.get(CONF_RADIUS), entry.get(CONF_ICON), - entry.get(CONF_PASSIVE)) + entry.get(CONF_RADIUS, DEFAULT_RADIUS), entry.get(CONF_ICON), + entry.get(CONF_PASSIVE, DEFAULT_PASSIVE)) zone.entity_id = async_generate_entity_id( ENTITY_ID_FORMAT, name, None, hass) hass.async_add_job(zone.async_update_ha_state()) diff --git a/tests/components/zone/test_init.py b/tests/components/zone/test_init.py index c26b3375f3a..92dee05818d 100644 --- a/tests/components/zone/test_init.py +++ b/tests/components/zone/test_init.py @@ -17,7 +17,6 @@ async def test_setup_entry_successful(hass): zone.CONF_NAME: 'Test Zone', zone.CONF_LATITUDE: 1.1, zone.CONF_LONGITUDE: -2.2, - zone.CONF_RADIUS: 250, zone.CONF_RADIUS: True } hass.data[zone.DOMAIN] = {} From 60179a1cbb4597a9a37a2a801dbc69f5c991ee8a Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Mon, 18 Jun 2018 15:22:52 +0200 Subject: [PATCH 124/144] Bugfix empty entity lists (#15035) * Bugfix empty entity lists * Add tests * Update test_entity_platform.py * Update entity_platform.py --- homeassistant/helpers/entity_platform.py | 4 ++++ tests/helpers/test_entity_platform.py | 10 ++++++++++ 2 files changed, 14 insertions(+) diff --git a/homeassistant/helpers/entity_platform.py b/homeassistant/helpers/entity_platform.py index ab6c3a084c0..472a88888d8 100644 --- a/homeassistant/helpers/entity_platform.py +++ b/homeassistant/helpers/entity_platform.py @@ -216,6 +216,10 @@ class EntityPlatform(object): component_entities, registry) for entity in new_entities] + # No entities for processing + if not tasks: + return + await asyncio.wait(tasks, loop=self.hass.loop) self.async_entities_added_callback() diff --git a/tests/helpers/test_entity_platform.py b/tests/helpers/test_entity_platform.py index 9fa178022dc..2d2f148189f 100644 --- a/tests/helpers/test_entity_platform.py +++ b/tests/helpers/test_entity_platform.py @@ -592,3 +592,13 @@ async def test_reset_cancels_retry_setup(hass): assert len(mock_call_later.return_value.mock_calls) == 1 assert ent_platform._async_cancel_retry_setup is None + + +@asyncio.coroutine +def test_not_fails_with_adding_empty_entities_(hass): + """Test for not fails on empty entities list.""" + component = EntityComponent(_LOGGER, DOMAIN, hass) + + yield from component.async_add_entities([]) + + assert len(hass.states.async_entity_ids()) == 0 From ef5b2a2492439969a167782f4280e0ca24f632bc Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 18 Jun 2018 10:00:24 -0400 Subject: [PATCH 125/144] Version bump to 0.72.0b3 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 562247a14c0..72f018ad366 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -2,7 +2,7 @@ """Constants used by Home Assistant components.""" MAJOR_VERSION = 0 MINOR_VERSION = 72 -PATCH_VERSION = '0b2' +PATCH_VERSION = '0b3' __short_version__ = '{}.{}'.format(MAJOR_VERSION, MINOR_VERSION) __version__ = '{}.{}'.format(__short_version__, PATCH_VERSION) REQUIRED_PYTHON_VER = (3, 5, 3) From 9800b74a6de576390f6c6818b5e9ef588671587d Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 18 Jun 2018 10:00:47 -0400 Subject: [PATCH 126/144] Version bump to 0.72.0b4 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 72f018ad366..7682234233e 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -2,7 +2,7 @@ """Constants used by Home Assistant components.""" MAJOR_VERSION = 0 MINOR_VERSION = 72 -PATCH_VERSION = '0b3' +PATCH_VERSION = '0b4' __short_version__ = '{}.{}'.format(MAJOR_VERSION, MINOR_VERSION) __version__ = '{}.{}'.format(__short_version__, PATCH_VERSION) REQUIRED_PYTHON_VER = (3, 5, 3) From 3b4f7b4f5de5be5dbca49014fcc07cd310f1e56c Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 19 Jun 2018 10:56:33 -0400 Subject: [PATCH 127/144] Update frontend to 20180619.0 --- homeassistant/components/frontend/__init__.py | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/frontend/__init__.py b/homeassistant/components/frontend/__init__.py index 2c9b68bf079..9af1a7af3be 100644 --- a/homeassistant/components/frontend/__init__.py +++ b/homeassistant/components/frontend/__init__.py @@ -25,7 +25,7 @@ from homeassistant.helpers.translation import async_get_translations from homeassistant.loader import bind_hass from homeassistant.util.yaml import load_yaml -REQUIREMENTS = ['home-assistant-frontend==20180618.0'] +REQUIREMENTS = ['home-assistant-frontend==20180619.0'] DOMAIN = 'frontend' DEPENDENCIES = ['api', 'websocket_api', 'http', 'system_log'] diff --git a/requirements_all.txt b/requirements_all.txt index bbf74004be4..766ab10671c 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -404,7 +404,7 @@ hipnotify==1.0.8 holidays==0.9.5 # homeassistant.components.frontend -home-assistant-frontend==20180618.0 +home-assistant-frontend==20180619.0 # homeassistant.components.homekit_controller # homekit==0.6 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index d4a24fbc084..b32da6fc9f2 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -81,7 +81,7 @@ hbmqtt==0.9.2 holidays==0.9.5 # homeassistant.components.frontend -home-assistant-frontend==20180618.0 +home-assistant-frontend==20180619.0 # homeassistant.components.influxdb # homeassistant.components.sensor.influxdb From 659616a4eb285d089a41632bb65abeec66eb506d Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 19 Jun 2018 10:58:57 -0400 Subject: [PATCH 128/144] Version bump to 0.72.0b5 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 7682234233e..98179b8502e 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -2,7 +2,7 @@ """Constants used by Home Assistant components.""" MAJOR_VERSION = 0 MINOR_VERSION = 72 -PATCH_VERSION = '0b4' +PATCH_VERSION = '0b5' __short_version__ = '{}.{}'.format(MAJOR_VERSION, MINOR_VERSION) __version__ = '{}.{}'.format(__short_version__, PATCH_VERSION) REQUIRED_PYTHON_VER = (3, 5, 3) From 49845d9398f642ec20e77c62a7487724b050a9bf Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 20 Jun 2018 15:13:08 -0400 Subject: [PATCH 129/144] Rename experimental UI to lovelace (#15065) * Rename experimental UI to lovelace * Bump frontend to 20180620.0 --- homeassistant/components/frontend/__init__.py | 42 ++++++++++------- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/frontend/__init__.py | 1 + .../test_init.py} | 45 +++++++++++++++++-- 5 files changed, 72 insertions(+), 20 deletions(-) create mode 100644 tests/components/frontend/__init__.py rename tests/components/{test_frontend.py => frontend/test_init.py} (86%) diff --git a/homeassistant/components/frontend/__init__.py b/homeassistant/components/frontend/__init__.py index 9af1a7af3be..b2cac55bd77 100644 --- a/homeassistant/components/frontend/__init__.py +++ b/homeassistant/components/frontend/__init__.py @@ -21,11 +21,12 @@ from homeassistant.components import websocket_api from homeassistant.config import find_config_file, load_yaml_config_file from homeassistant.const import CONF_NAME, EVENT_THEMES_UPDATED from homeassistant.core import callback +from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.translation import async_get_translations from homeassistant.loader import bind_hass from homeassistant.util.yaml import load_yaml -REQUIREMENTS = ['home-assistant-frontend==20180619.0'] +REQUIREMENTS = ['home-assistant-frontend==20180620.0'] DOMAIN = 'frontend' DEPENDENCIES = ['api', 'websocket_api', 'http', 'system_log'] @@ -106,9 +107,9 @@ SCHEMA_GET_TRANSLATIONS = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend({ vol.Required('type'): WS_TYPE_GET_TRANSLATIONS, vol.Required('language'): str, }) -WS_TYPE_GET_EXPERIMENTAL_UI = 'frontend/experimental_ui' -SCHEMA_GET_EXPERIMENTAL_UI = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend({ - vol.Required('type'): WS_TYPE_GET_EXPERIMENTAL_UI, +WS_TYPE_GET_LOVELACE_UI = 'frontend/lovelace_config' +SCHEMA_GET_LOVELACE_UI = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend({ + vol.Required('type'): WS_TYPE_GET_LOVELACE_UI, }) @@ -216,8 +217,8 @@ async def async_setup(hass, config): WS_TYPE_GET_TRANSLATIONS, websocket_get_translations, SCHEMA_GET_TRANSLATIONS) hass.components.websocket_api.async_register_command( - WS_TYPE_GET_EXPERIMENTAL_UI, websocket_experimental_config, - SCHEMA_GET_EXPERIMENTAL_UI) + WS_TYPE_GET_LOVELACE_UI, websocket_lovelace_config, + SCHEMA_GET_LOVELACE_UI) hass.http.register_view(ManifestJSONView) conf = config.get(DOMAIN, {}) @@ -265,7 +266,7 @@ async def async_setup(hass, config): await asyncio.wait( [async_register_built_in_panel(hass, panel) for panel in ( 'dev-event', 'dev-info', 'dev-service', 'dev-state', - 'dev-template', 'dev-mqtt', 'kiosk', 'experimental-ui')], + 'dev-template', 'dev-mqtt', 'kiosk', 'lovelace')], loop=hass.loop) hass.data[DATA_FINALIZE_PANEL] = async_finalize_panel @@ -499,15 +500,26 @@ def websocket_get_translations(hass, connection, msg): hass.async_add_job(send_translations()) -def websocket_experimental_config(hass, connection, msg): - """Send experimental UI config over websocket config.""" +def websocket_lovelace_config(hass, connection, msg): + """Send lovelace UI config over websocket config.""" async def send_exp_config(): - """Send experimental frontend config.""" - config = await hass.async_add_job( - load_yaml, hass.config.path('experimental-ui.yaml')) + """Send lovelace frontend config.""" + error = None + try: + config = await hass.async_add_job( + load_yaml, hass.config.path('ui-lovelace.yaml')) + message = websocket_api.result_message( + msg['id'], config + ) + except FileNotFoundError: + error = ('file_not_found', + 'Could not find ui-lovelace.yaml in your config dir.') + except HomeAssistantError as err: + error = 'load_error', str(err) - connection.send_message_outside(websocket_api.result_message( - msg['id'], config - )) + if error is not None: + message = websocket_api.error_message(msg['id'], *error) + + connection.send_message_outside(message) hass.async_add_job(send_exp_config()) diff --git a/requirements_all.txt b/requirements_all.txt index 766ab10671c..56896c9b6da 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -404,7 +404,7 @@ hipnotify==1.0.8 holidays==0.9.5 # homeassistant.components.frontend -home-assistant-frontend==20180619.0 +home-assistant-frontend==20180620.0 # homeassistant.components.homekit_controller # homekit==0.6 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index b32da6fc9f2..177796961a5 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -81,7 +81,7 @@ hbmqtt==0.9.2 holidays==0.9.5 # homeassistant.components.frontend -home-assistant-frontend==20180619.0 +home-assistant-frontend==20180620.0 # homeassistant.components.influxdb # homeassistant.components.sensor.influxdb diff --git a/tests/components/frontend/__init__.py b/tests/components/frontend/__init__.py new file mode 100644 index 00000000000..991a74dee7a --- /dev/null +++ b/tests/components/frontend/__init__.py @@ -0,0 +1 @@ +"""Tests for the frontend component.""" diff --git a/tests/components/test_frontend.py b/tests/components/frontend/test_init.py similarity index 86% rename from tests/components/test_frontend.py rename to tests/components/frontend/test_init.py index cb0c72e9edd..2125668facb 100644 --- a/tests/components/test_frontend.py +++ b/tests/components/frontend/test_init.py @@ -5,6 +5,7 @@ from unittest.mock import patch import pytest +from homeassistant.exceptions import HomeAssistantError from homeassistant.setup import async_setup_component from homeassistant.components.frontend import ( DOMAIN, CONF_JS_VERSION, CONF_THEMES, CONF_EXTRA_HTML_URL, @@ -280,8 +281,8 @@ async def test_get_translations(hass, hass_ws_client): assert msg['result'] == {'resources': {'lang': 'nl'}} -async def test_experimental_ui(hass, hass_ws_client): - """Test experimental_ui command.""" +async def test_lovelace_ui(hass, hass_ws_client): + """Test lovelace_ui command.""" await async_setup_component(hass, 'frontend') client = await hass_ws_client(hass) @@ -289,7 +290,7 @@ async def test_experimental_ui(hass, hass_ws_client): return_value={'hello': 'world'}): await client.send_json({ 'id': 5, - 'type': 'frontend/experimental_ui', + 'type': 'frontend/lovelace_config', }) msg = await client.receive_json() @@ -297,3 +298,41 @@ async def test_experimental_ui(hass, hass_ws_client): assert msg['type'] == wapi.TYPE_RESULT assert msg['success'] assert msg['result'] == {'hello': 'world'} + + +async def test_lovelace_ui_not_found(hass, hass_ws_client): + """Test lovelace_ui command cannot find file.""" + await async_setup_component(hass, 'frontend') + client = await hass_ws_client(hass) + + with patch('homeassistant.components.frontend.load_yaml', + side_effect=FileNotFoundError): + await client.send_json({ + 'id': 5, + 'type': 'frontend/lovelace_config', + }) + msg = await client.receive_json() + + assert msg['id'] == 5 + assert msg['type'] == wapi.TYPE_RESULT + assert msg['success'] is False + assert msg['error']['code'] == 'file_not_found' + + +async def test_lovelace_ui_load_err(hass, hass_ws_client): + """Test lovelace_ui command cannot find file.""" + await async_setup_component(hass, 'frontend') + client = await hass_ws_client(hass) + + with patch('homeassistant.components.frontend.load_yaml', + side_effect=HomeAssistantError): + await client.send_json({ + 'id': 5, + 'type': 'frontend/lovelace_config', + }) + msg = await client.receive_json() + + assert msg['id'] == 5 + assert msg['type'] == wapi.TYPE_RESULT + assert msg['success'] is False + assert msg['error']['code'] == 'load_error' From c84f1d7d33b72f11fd765369b5ae67384e8863f1 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 20 Jun 2018 15:13:33 -0400 Subject: [PATCH 130/144] Version bump to 0.72.0b6 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 98179b8502e..091bd907b93 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -2,7 +2,7 @@ """Constants used by Home Assistant components.""" MAJOR_VERSION = 0 MINOR_VERSION = 72 -PATCH_VERSION = '0b5' +PATCH_VERSION = '0b6' __short_version__ = '{}.{}'.format(MAJOR_VERSION, MINOR_VERSION) __version__ = '{}.{}'.format(__short_version__, PATCH_VERSION) REQUIRED_PYTHON_VER = (3, 5, 3) From e98e7e2751510fa9b2ea6da7b0cad7f1afdc76a2 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 21 Jun 2018 14:57:08 -0400 Subject: [PATCH 131/144] Update frontend to 20180621.0 --- homeassistant/components/frontend/__init__.py | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/frontend/__init__.py b/homeassistant/components/frontend/__init__.py index b2cac55bd77..9200f4d78f6 100644 --- a/homeassistant/components/frontend/__init__.py +++ b/homeassistant/components/frontend/__init__.py @@ -26,7 +26,7 @@ from homeassistant.helpers.translation import async_get_translations from homeassistant.loader import bind_hass from homeassistant.util.yaml import load_yaml -REQUIREMENTS = ['home-assistant-frontend==20180620.0'] +REQUIREMENTS = ['home-assistant-frontend==20180621.0'] DOMAIN = 'frontend' DEPENDENCIES = ['api', 'websocket_api', 'http', 'system_log'] diff --git a/requirements_all.txt b/requirements_all.txt index 56896c9b6da..c0c85012526 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -404,7 +404,7 @@ hipnotify==1.0.8 holidays==0.9.5 # homeassistant.components.frontend -home-assistant-frontend==20180620.0 +home-assistant-frontend==20180621.0 # homeassistant.components.homekit_controller # homekit==0.6 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 177796961a5..8f09c4d7195 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -81,7 +81,7 @@ hbmqtt==0.9.2 holidays==0.9.5 # homeassistant.components.frontend -home-assistant-frontend==20180620.0 +home-assistant-frontend==20180621.0 # homeassistant.components.influxdb # homeassistant.components.sensor.influxdb From 4b5d578c08d9fd69e8f1455152a4483a98e551f2 Mon Sep 17 00:00:00 2001 From: Tom Harris Date: Wed, 20 Jun 2018 20:44:05 -0500 Subject: [PATCH 132/144] X10 (#14741) * Implement X10 * Add X10 after add_device_callback * Ref device by id not hex and add x10OnOffSwitch name * X10 services and add sensor device * Correctly reference X10_HOUSECODE_SCHEMA * Log adding of X10 devices * Add X10 All Units Off, All Lights On and All Lights Off devices * Correct ref to X10 states vs devices * Add X10 All Units Off, All Lights On and All Lights Off devices * Correct X10 config * Debug x10 device additions * Config x10 from bool to housecode char * Pass PLM to X10 device create * Remove PLM to call to add_x10_device * Unconfuse x10 config and method names * Correct spelling of x10_all_lights_off_housecode * Bump insteonplm to 0.10.0 to support X10 --- .../components/insteon_plm/__init__.py | 111 +++++++++++++++++- .../components/insteon_plm/services.yaml | 18 +++ .../components/switch/insteon_plm.py | 3 +- requirements_all.txt | 2 +- 4 files changed, 128 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/insteon_plm/__init__.py b/homeassistant/components/insteon_plm/__init__.py index b86f80cbee7..b2f7c8b6655 100644 --- a/homeassistant/components/insteon_plm/__init__.py +++ b/homeassistant/components/insteon_plm/__init__.py @@ -17,7 +17,7 @@ import homeassistant.helpers.config_validation as cv from homeassistant.helpers import discovery from homeassistant.helpers.entity import Entity -REQUIREMENTS = ['insteonplm==0.9.2'] +REQUIREMENTS = ['insteonplm==0.10.0'] _LOGGER = logging.getLogger(__name__) @@ -29,17 +29,31 @@ CONF_CAT = 'cat' CONF_SUBCAT = 'subcat' CONF_FIRMWARE = 'firmware' CONF_PRODUCT_KEY = 'product_key' +CONF_X10 = 'x10_devices' +CONF_HOUSECODE = 'housecode' +CONF_UNITCODE = 'unitcode' +CONF_DIM_STEPS = 'dim_steps' +CONF_X10_ALL_UNITS_OFF = 'x10_all_units_off' +CONF_X10_ALL_LIGHTS_ON = 'x10_all_lights_on' +CONF_X10_ALL_LIGHTS_OFF = 'x10_all_lights_off' SRV_ADD_ALL_LINK = 'add_all_link' SRV_DEL_ALL_LINK = 'delete_all_link' SRV_LOAD_ALDB = 'load_all_link_database' SRV_PRINT_ALDB = 'print_all_link_database' SRV_PRINT_IM_ALDB = 'print_im_all_link_database' +SRV_X10_ALL_UNITS_OFF = 'x10_all_units_off' +SRV_X10_ALL_LIGHTS_OFF = 'x10_all_lights_off' +SRV_X10_ALL_LIGHTS_ON = 'x10_all_lights_on' SRV_ALL_LINK_GROUP = 'group' SRV_ALL_LINK_MODE = 'mode' SRV_LOAD_DB_RELOAD = 'reload' SRV_CONTROLLER = 'controller' SRV_RESPONDER = 'responder' +SRV_HOUSECODE = 'housecode' + +HOUSECODES = ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', + 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p'] CONF_DEVICE_OVERRIDE_SCHEMA = vol.All( cv.deprecated(CONF_PLATFORM), vol.Schema({ @@ -51,11 +65,24 @@ CONF_DEVICE_OVERRIDE_SCHEMA = vol.All( vol.Optional(CONF_PLATFORM): cv.string, })) +CONF_X10_SCHEMA = vol.All( + vol.Schema({ + vol.Required(CONF_HOUSECODE): cv.string, + vol.Required(CONF_UNITCODE): vol.Range(min=1, max=16), + vol.Required(CONF_PLATFORM): cv.string, + vol.Optional(CONF_DIM_STEPS): vol.Range(min=2, max=255) + })) + CONFIG_SCHEMA = vol.Schema({ DOMAIN: vol.Schema({ vol.Required(CONF_PORT): cv.string, vol.Optional(CONF_OVERRIDE): vol.All( - cv.ensure_list_csv, [CONF_DEVICE_OVERRIDE_SCHEMA]) + cv.ensure_list_csv, [CONF_DEVICE_OVERRIDE_SCHEMA]), + vol.Optional(CONF_X10_ALL_UNITS_OFF): vol.In(HOUSECODES), + vol.Optional(CONF_X10_ALL_LIGHTS_ON): vol.In(HOUSECODES), + vol.Optional(CONF_X10_ALL_LIGHTS_OFF): vol.In(HOUSECODES), + vol.Optional(CONF_X10): vol.All( + cv.ensure_list_csv, [CONF_X10_SCHEMA]) }) }, extra=vol.ALLOW_EXTRA) @@ -77,6 +104,10 @@ PRINT_ALDB_SCHEMA = vol.Schema({ vol.Required(CONF_ENTITY_ID): cv.entity_id, }) +X10_HOUSECODE_SCHEMA = vol.Schema({ + vol.Required(SRV_HOUSECODE): vol.In(HOUSECODES), + }) + @asyncio.coroutine def async_setup(hass, config): @@ -89,6 +120,10 @@ def async_setup(hass, config): conf = config[DOMAIN] port = conf.get(CONF_PORT) overrides = conf.get(CONF_OVERRIDE, []) + x10_devices = conf.get(CONF_X10, []) + x10_all_units_off_housecode = conf.get(CONF_X10_ALL_UNITS_OFF) + x10_all_lights_on_housecode = conf.get(CONF_X10_ALL_LIGHTS_ON) + x10_all_lights_off_housecode = conf.get(CONF_X10_ALL_LIGHTS_OFF) @callback def async_plm_new_device(device): @@ -106,7 +141,7 @@ def async_setup(hass, config): hass.async_add_job( discovery.async_load_platform( hass, platform, DOMAIN, - discovered={'address': device.address.hex, + discovered={'address': device.address.id, 'state_key': state_key}, hass_config=config)) @@ -151,6 +186,21 @@ def async_setup(hass, config): # Furture direction is to create an INSTEON control panel. print_aldb_to_log(plm.aldb) + def x10_all_units_off(service): + """Send the X10 All Units Off command.""" + housecode = service.data.get(SRV_HOUSECODE) + plm.x10_all_units_off(housecode) + + def x10_all_lights_off(service): + """Send the X10 All Lights Off command.""" + housecode = service.data.get(SRV_HOUSECODE) + plm.x10_all_lights_off(housecode) + + def x10_all_lights_on(service): + """Send the X10 All Lights On command.""" + housecode = service.data.get(SRV_HOUSECODE) + plm.x10_all_lights_on(housecode) + def _register_services(): hass.services.register(DOMAIN, SRV_ADD_ALL_LINK, add_all_link, schema=ADD_ALL_LINK_SCHEMA) @@ -162,6 +212,15 @@ def async_setup(hass, config): schema=PRINT_ALDB_SCHEMA) hass.services.register(DOMAIN, SRV_PRINT_IM_ALDB, print_im_aldb, schema=None) + hass.services.register(DOMAIN, SRV_X10_ALL_UNITS_OFF, + x10_all_units_off, + schema=X10_HOUSECODE_SCHEMA) + hass.services.register(DOMAIN, SRV_X10_ALL_LIGHTS_OFF, + x10_all_lights_off, + schema=X10_HOUSECODE_SCHEMA) + hass.services.register(DOMAIN, SRV_X10_ALL_LIGHTS_ON, + x10_all_lights_on, + schema=X10_HOUSECODE_SCHEMA) _LOGGER.debug("Insteon_plm Services registered") _LOGGER.info("Looking for PLM on %s", port) @@ -192,6 +251,36 @@ def async_setup(hass, config): hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, conn.close) plm.devices.add_device_callback(async_plm_new_device) + + if x10_all_units_off_housecode: + device = plm.add_x10_device(x10_all_units_off_housecode, + 20, + 'allunitsoff') + if x10_all_lights_on_housecode: + device = plm.add_x10_device(x10_all_lights_on_housecode, + 21, + 'alllightson') + if x10_all_lights_off_housecode: + device = plm.add_x10_device(x10_all_lights_off_housecode, + 22, + 'alllightsoff') + for device in x10_devices: + housecode = device.get(CONF_HOUSECODE) + unitcode = device.get(CONF_UNITCODE) + x10_type = 'onoff' + steps = device.get(CONF_DIM_STEPS, 22) + if device.get(CONF_PLATFORM) == 'light': + x10_type = 'dimmable' + elif device.get(CONF_PLATFORM) == 'binary_sensor': + x10_type = 'sensor' + _LOGGER.debug("Adding X10 device to insteonplm: %s %d %s", + housecode, unitcode, x10_type) + device = plm.add_x10_device(housecode, + unitcode, + x10_type) + if device and hasattr(device.states[0x01], 'steps'): + device.states[0x01].steps = steps + hass.async_add_job(_register_services) return True @@ -219,6 +308,13 @@ class IPDB(object): IoLincSensor, LeakSensorDryWet) + from insteonplm.states.x10 import (X10DimmableSwitch, + X10OnOffSwitch, + X10OnOffSensor, + X10AllUnitsOffSensor, + X10AllLightsOnSensor, + X10AllLightsOffSensor) + self.states = [State(OnOffSwitch_OutletTop, 'switch'), State(OnOffSwitch_OutletBottom, 'switch'), State(OpenClosedRelay, 'switch'), @@ -231,7 +327,14 @@ class IPDB(object): State(VariableSensor, 'sensor'), State(DimmableSwitch_Fan, 'fan'), - State(DimmableSwitch, 'light')] + State(DimmableSwitch, 'light'), + + State(X10DimmableSwitch, 'light'), + State(X10OnOffSwitch, 'switch'), + State(X10OnOffSensor, 'binary_sensor'), + State(X10AllUnitsOffSensor, 'binary_sensor'), + State(X10AllLightsOnSensor, 'binary_sensor'), + State(X10AllLightsOffSensor, 'binary_sensor')] def __len__(self): """Return the number of INSTEON state types mapped to HA platforms.""" diff --git a/homeassistant/components/insteon_plm/services.yaml b/homeassistant/components/insteon_plm/services.yaml index 9ea53c10fbf..4d87d7881bf 100644 --- a/homeassistant/components/insteon_plm/services.yaml +++ b/homeassistant/components/insteon_plm/services.yaml @@ -30,3 +30,21 @@ print_all_link_database: example: 'light.1a2b3c' print_im_all_link_database: description: Print the All-Link Database for the INSTEON Modem (IM). +x10_all_units_off: + description: Send X10 All Units Off command + fields: + housecode: + description: X10 house code + example: c +x10_all_lights_on: + description: Send X10 All Lights On command + fields: + housecode: + description: X10 house code + example: c +x10_all_lights_off: + description: Send X10 All Lights Off command + fields: + housecode: + description: X10 house code + example: c diff --git a/homeassistant/components/switch/insteon_plm.py b/homeassistant/components/switch/insteon_plm.py index be562e9d909..42b4829f64e 100644 --- a/homeassistant/components/switch/insteon_plm.py +++ b/homeassistant/components/switch/insteon_plm.py @@ -30,7 +30,8 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None): device.address.hex, device.states[state_key].name) new_entity = None - if state_name in ['lightOnOff', 'outletTopOnOff', 'outletBottomOnOff']: + if state_name in ['lightOnOff', 'outletTopOnOff', 'outletBottomOnOff', + 'x10OnOffSwitch']: new_entity = InsteonPLMSwitchDevice(device, state_key) elif state_name == 'openClosedRelay': new_entity = InsteonPLMOpenClosedDevice(device, state_key) diff --git a/requirements_all.txt b/requirements_all.txt index c0c85012526..35f45bbd5b0 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -464,7 +464,7 @@ influxdb==5.0.0 insteonlocal==0.53 # homeassistant.components.insteon_plm -insteonplm==0.9.2 +insteonplm==0.10.0 # homeassistant.components.sensor.iperf3 iperf3==0.1.10 From 617647c5fd1705848dd2bf425de9d7ace3a42103 Mon Sep 17 00:00:00 2001 From: Bob Clough Date: Thu, 21 Jun 2018 19:59:03 +0100 Subject: [PATCH 133/144] Fix MQTT Light with RGB and Brightness (#15053) * Fix MQTT Light with RGB and Brightness When an MQTT light is given an RGB and Brightness topic, the RGB is scaled by the brightness *as well* as the brightness being set This causes 255,0,0 at 50% brightness to be sent as 127,0,0 at 50% brightness, which ends up as 63,0,0 after the RGB bulb has applied its brightness scaling. Fixes the same issue in mqtt, mqtt-json and mqtt-template. Related Issue: #13725 * Add comment to mqtt_json as well --- homeassistant/components/light/mqtt.py | 11 +++++++++-- homeassistant/components/light/mqtt_json.py | 11 ++++++++--- homeassistant/components/light/mqtt_template.py | 11 +++++++++-- tests/components/light/test_mqtt.py | 14 +++++++------- tests/components/light/test_mqtt_json.py | 4 ++-- 5 files changed, 35 insertions(+), 16 deletions(-) diff --git a/homeassistant/components/light/mqtt.py b/homeassistant/components/light/mqtt.py index 97a4cc8c137..c0e363f85d6 100644 --- a/homeassistant/components/light/mqtt.py +++ b/homeassistant/components/light/mqtt.py @@ -442,8 +442,15 @@ class MqttLight(MqttAvailability, Light): self._topic[CONF_RGB_COMMAND_TOPIC] is not None: hs_color = kwargs[ATTR_HS_COLOR] - brightness = kwargs.get( - ATTR_BRIGHTNESS, self._brightness if self._brightness else 255) + + # If there's a brightness topic set, we don't want to scale the RGB + # values given using the brightness. + if self._topic[CONF_BRIGHTNESS_COMMAND_TOPIC] is not None: + brightness = 255 + else: + brightness = kwargs.get( + ATTR_BRIGHTNESS, self._brightness if self._brightness else + 255) rgb = color_util.color_hsv_to_RGB( hs_color[0], hs_color[1], brightness / 255 * 100) tpl = self._templates[CONF_RGB_COMMAND_TEMPLATE] diff --git a/homeassistant/components/light/mqtt_json.py b/homeassistant/components/light/mqtt_json.py index 14f5ee7a9b9..705e106fdff 100644 --- a/homeassistant/components/light/mqtt_json.py +++ b/homeassistant/components/light/mqtt_json.py @@ -345,9 +345,14 @@ class MqttJson(MqttAvailability, Light): hs_color = kwargs[ATTR_HS_COLOR] message['color'] = {} if self._rgb: - brightness = kwargs.get( - ATTR_BRIGHTNESS, - self._brightness if self._brightness else 255) + # If there's a brightness topic set, we don't want to scale the + # RGB values given using the brightness. + if self._brightness is not None: + brightness = 255 + else: + brightness = kwargs.get( + ATTR_BRIGHTNESS, + self._brightness if self._brightness else 255) rgb = color_util.color_hsv_to_RGB( hs_color[0], hs_color[1], brightness / 255 * 100) message['color']['r'] = rgb[0] diff --git a/homeassistant/components/light/mqtt_template.py b/homeassistant/components/light/mqtt_template.py index e32c13fc5b6..f6b3fbe8b70 100644 --- a/homeassistant/components/light/mqtt_template.py +++ b/homeassistant/components/light/mqtt_template.py @@ -317,8 +317,15 @@ class MqttTemplate(MqttAvailability, Light): if ATTR_HS_COLOR in kwargs: hs_color = kwargs[ATTR_HS_COLOR] - brightness = kwargs.get( - ATTR_BRIGHTNESS, self._brightness if self._brightness else 255) + + # If there's a brightness topic set, we don't want to scale the RGB + # values given using the brightness. + if self._templates[CONF_BRIGHTNESS_TEMPLATE] is not None: + brightness = 255 + else: + brightness = kwargs.get( + ATTR_BRIGHTNESS, self._brightness if self._brightness else + 255) rgb = color_util.color_hsv_to_RGB( hs_color[0], hs_color[1], brightness / 255 * 100) values['red'] = rgb[0] diff --git a/tests/components/light/test_mqtt.py b/tests/components/light/test_mqtt.py index 8b51adb2187..49bcd8a73ec 100644 --- a/tests/components/light/test_mqtt.py +++ b/tests/components/light/test_mqtt.py @@ -523,24 +523,24 @@ class TestLightMQTT(unittest.TestCase): self.mock_publish.reset_mock() light.turn_on(self.hass, 'light.test', brightness=50, xy_color=[0.123, 0.123]) - light.turn_on(self.hass, 'light.test', rgb_color=[75, 75, 75], + light.turn_on(self.hass, 'light.test', rgb_color=[255, 128, 0], white_value=80) self.hass.block_till_done() self.mock_publish.async_publish.assert_has_calls([ mock.call('test_light_rgb/set', 'on', 2, False), - mock.call('test_light_rgb/rgb/set', '50,50,50', 2, False), + mock.call('test_light_rgb/rgb/set', '255,128,0', 2, False), mock.call('test_light_rgb/brightness/set', 50, 2, False), mock.call('test_light_rgb/white_value/set', 80, 2, False), - mock.call('test_light_rgb/xy/set', '0.323,0.329', 2, False), + mock.call('test_light_rgb/xy/set', '0.14,0.131', 2, False), ], any_order=True) state = self.hass.states.get('light.test') self.assertEqual(STATE_ON, state.state) - self.assertEqual((255, 255, 255), state.attributes['rgb_color']) + self.assertEqual((255, 128, 0), state.attributes['rgb_color']) self.assertEqual(50, state.attributes['brightness']) self.assertEqual(80, state.attributes['white_value']) - self.assertEqual((0.323, 0.329), state.attributes['xy_color']) + self.assertEqual((0.611, 0.375), state.attributes['xy_color']) def test_sending_mqtt_rgb_command_with_template(self): """Test the sending of RGB command with template.""" @@ -808,11 +808,11 @@ class TestLightMQTT(unittest.TestCase): # Turn on w/ just a color to insure brightness gets # added and sent. - light.turn_on(self.hass, 'light.test', rgb_color=[75, 75, 75]) + light.turn_on(self.hass, 'light.test', rgb_color=[255, 128, 0]) self.hass.block_till_done() self.mock_publish.async_publish.assert_has_calls([ - mock.call('test_light/rgb', '50,50,50', 0, False), + mock.call('test_light/rgb', '255,128,0', 0, False), mock.call('test_light/bright', 50, 0, False) ], any_order=True) diff --git a/tests/components/light/test_mqtt_json.py b/tests/components/light/test_mqtt_json.py index 275fb42ede9..af560bff9c3 100644 --- a/tests/components/light/test_mqtt_json.py +++ b/tests/components/light/test_mqtt_json.py @@ -381,8 +381,8 @@ class TestLightMQTTJSON(unittest.TestCase): self.assertEqual(50, message_json["brightness"]) self.assertEqual({ 'r': 0, - 'g': 50, - 'b': 4, + 'g': 255, + 'b': 21, }, message_json["color"]) self.assertEqual("ON", message_json["state"]) From 302717e8a1b6d6e0fdce24258303573a50c4195a Mon Sep 17 00:00:00 2001 From: Daniel Shokouhi Date: Wed, 20 Jun 2018 18:46:15 -0700 Subject: [PATCH 134/144] Update Neato Library And Reduce Cloud Calls (#15072) * Update Neato library to 0.0.6 and reduce the amount of calls to the cloud * Remove file commited in error * Lint --- homeassistant/components/camera/neato.py | 2 +- homeassistant/components/neato.py | 6 +++--- homeassistant/components/switch/neato.py | 3 +++ homeassistant/components/vacuum/neato.py | 4 +++- requirements_all.txt | 2 +- 5 files changed, 11 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/camera/neato.py b/homeassistant/components/camera/neato.py index 33bd00caa6b..689129e1067 100644 --- a/homeassistant/components/camera/neato.py +++ b/homeassistant/components/camera/neato.py @@ -45,7 +45,7 @@ class NeatoCleaningMap(Camera): self.update() return self._image - @Throttle(timedelta(seconds=10)) + @Throttle(timedelta(seconds=60)) def update(self): """Check the contents of the map list.""" self.neato.update_robots() diff --git a/homeassistant/components/neato.py b/homeassistant/components/neato.py index 7402bb18843..c6a3dcf9c9a 100644 --- a/homeassistant/components/neato.py +++ b/homeassistant/components/neato.py @@ -17,8 +17,8 @@ from homeassistant.util import Throttle _LOGGER = logging.getLogger(__name__) -REQUIREMENTS = ['https://github.com/jabesq/pybotvac/archive/v0.0.5.zip' - '#pybotvac==0.0.5'] +REQUIREMENTS = ['https://github.com/jabesq/pybotvac/archive/v0.0.6.zip' + '#pybotvac==0.0.6'] DOMAIN = 'neato' NEATO_ROBOTS = 'neato_robots' @@ -122,7 +122,7 @@ class NeatoHub(object): _LOGGER.error("Unable to connect to Neato API") return False - @Throttle(timedelta(seconds=1)) + @Throttle(timedelta(seconds=60)) def update_robots(self): """Update the robot states.""" _LOGGER.debug("Running HUB.update_robots %s", diff --git a/homeassistant/components/switch/neato.py b/homeassistant/components/switch/neato.py index a797abb47fc..1d149383f6f 100644 --- a/homeassistant/components/switch/neato.py +++ b/homeassistant/components/switch/neato.py @@ -5,10 +5,12 @@ For more details about this platform, please refer to the documentation at https://home-assistant.io/components/switch.neato/ """ import logging +from datetime import timedelta import requests from homeassistant.const import STATE_OFF, STATE_ON from homeassistant.helpers.entity import ToggleEntity from homeassistant.components.neato import NEATO_ROBOTS, NEATO_LOGIN +from homeassistant.util import Throttle _LOGGER = logging.getLogger(__name__) @@ -50,6 +52,7 @@ class NeatoConnectedSwitch(ToggleEntity): self._schedule_state = None self._clean_state = None + @Throttle(timedelta(seconds=60)) def update(self): """Update the states of Neato switches.""" _LOGGER.debug("Running switch update") diff --git a/homeassistant/components/vacuum/neato.py b/homeassistant/components/vacuum/neato.py index 9eba34cea32..128bece8494 100644 --- a/homeassistant/components/vacuum/neato.py +++ b/homeassistant/components/vacuum/neato.py @@ -5,7 +5,7 @@ For more details about this platform, please refer to the documentation at https://home-assistant.io/components/vacuum.neato/ """ import logging - +from datetime import timedelta import requests from homeassistant.const import STATE_OFF, STATE_ON @@ -15,6 +15,7 @@ from homeassistant.components.vacuum import ( SUPPORT_MAP, ATTR_STATUS, ATTR_BATTERY_LEVEL, ATTR_BATTERY_ICON) from homeassistant.components.neato import ( NEATO_ROBOTS, NEATO_LOGIN, NEATO_MAP_DATA, ACTION, ERRORS, MODE, ALERTS) +from homeassistant.util import Throttle _LOGGER = logging.getLogger(__name__) @@ -62,6 +63,7 @@ class NeatoConnectedVacuum(VacuumDevice): self.clean_suspension_charge_count = None self.clean_suspension_time = None + @Throttle(timedelta(seconds=60)) def update(self): """Update the states of Neato Vacuums.""" _LOGGER.debug("Running Neato Vacuums update") diff --git a/requirements_all.txt b/requirements_all.txt index 35f45bbd5b0..fb365b26651 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -425,7 +425,7 @@ httplib2==0.10.3 https://github.com/aparraga/braviarc/archive/0.3.7.zip#braviarc==0.3.7 # homeassistant.components.neato -https://github.com/jabesq/pybotvac/archive/v0.0.5.zip#pybotvac==0.0.5 +https://github.com/jabesq/pybotvac/archive/v0.0.6.zip#pybotvac==0.0.6 # homeassistant.components.switch.anel_pwrctrl https://github.com/mweinelt/anel-pwrctrl/archive/ed26e8830e28a2bfa4260a9002db23ce3e7e63d7.zip#anel_pwrctrl==0.0.1 From a4b843eb2d053c92a1cdc9000f64b54e93b0e517 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 21 Jun 2018 15:02:29 -0400 Subject: [PATCH 135/144] Version bump to 0.72.0b7 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 091bd907b93..efed01d409e 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -2,7 +2,7 @@ """Constants used by Home Assistant components.""" MAJOR_VERSION = 0 MINOR_VERSION = 72 -PATCH_VERSION = '0b6' +PATCH_VERSION = '0b7' __short_version__ = '{}.{}'.format(MAJOR_VERSION, MINOR_VERSION) __version__ = '{}.{}'.format(__short_version__, PATCH_VERSION) REQUIRED_PYTHON_VER = (3, 5, 3) From 0df99f8762a03f51402ba4dd45b7d26d4a3ef15b Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 21 Jun 2018 17:15:16 -0400 Subject: [PATCH 136/144] Bump frontend to 20180621.1 --- homeassistant/components/frontend/__init__.py | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/frontend/__init__.py b/homeassistant/components/frontend/__init__.py index 9200f4d78f6..d8497f9c790 100644 --- a/homeassistant/components/frontend/__init__.py +++ b/homeassistant/components/frontend/__init__.py @@ -26,7 +26,7 @@ from homeassistant.helpers.translation import async_get_translations from homeassistant.loader import bind_hass from homeassistant.util.yaml import load_yaml -REQUIREMENTS = ['home-assistant-frontend==20180621.0'] +REQUIREMENTS = ['home-assistant-frontend==20180621.1'] DOMAIN = 'frontend' DEPENDENCIES = ['api', 'websocket_api', 'http', 'system_log'] diff --git a/requirements_all.txt b/requirements_all.txt index fb365b26651..7c03f3465c3 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -404,7 +404,7 @@ hipnotify==1.0.8 holidays==0.9.5 # homeassistant.components.frontend -home-assistant-frontend==20180621.0 +home-assistant-frontend==20180621.1 # homeassistant.components.homekit_controller # homekit==0.6 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 8f09c4d7195..f6762e1faab 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -81,7 +81,7 @@ hbmqtt==0.9.2 holidays==0.9.5 # homeassistant.components.frontend -home-assistant-frontend==20180621.0 +home-assistant-frontend==20180621.1 # homeassistant.components.influxdb # homeassistant.components.sensor.influxdb From 35b609dd8b75f8b443b5ce17f1cec18162d8ab42 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 21 Jun 2018 17:27:01 -0400 Subject: [PATCH 137/144] Allow writing commit with version bump --- script/version_bump.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/script/version_bump.py b/script/version_bump.py index 59060a7075b..eb61420a600 100755 --- a/script/version_bump.py +++ b/script/version_bump.py @@ -2,6 +2,7 @@ """Helper script to bump the current version.""" import argparse import re +import subprocess from packaging.version import Version @@ -117,12 +118,20 @@ def main(): help="The type of the bump the version to.", choices=['beta', 'dev', 'patch', 'minor'], ) + parser.add_argument( + '--commit', action='store_true', + help='Create a version bump commit.') arguments = parser.parse_args() current = Version(const.__version__) bumped = bump_version(current, arguments.type) assert bumped > current, 'BUG! New version is not newer than old version' write_version(bumped) + if not arguments.commit: + return + + subprocess.run(['git', 'commit', '-am', f'Bumped version to {bumped}']) + def test_bump_version(): """Make sure it all works.""" From 6e5a2a77ab27ef605c81f6071edb237bcdfb7d67 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 21 Jun 2018 17:27:08 -0400 Subject: [PATCH 138/144] Bumped version to 0.72.0b8 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index efed01d409e..e9b72a70f1b 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -2,7 +2,7 @@ """Constants used by Home Assistant components.""" MAJOR_VERSION = 0 MINOR_VERSION = 72 -PATCH_VERSION = '0b7' +PATCH_VERSION = '0b8' __short_version__ = '{}.{}'.format(MAJOR_VERSION, MINOR_VERSION) __version__ = '{}.{}'.format(__short_version__, PATCH_VERSION) REQUIRED_PYTHON_VER = (3, 5, 3) From 6456f66b476ba3bf3c333b0cd7c1b2599eb7ed46 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 21 Jun 2018 17:38:44 -0400 Subject: [PATCH 139/144] Frontend bump to 20180621.2 --- homeassistant/components/frontend/__init__.py | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/frontend/__init__.py b/homeassistant/components/frontend/__init__.py index d8497f9c790..89353b56098 100644 --- a/homeassistant/components/frontend/__init__.py +++ b/homeassistant/components/frontend/__init__.py @@ -26,7 +26,7 @@ from homeassistant.helpers.translation import async_get_translations from homeassistant.loader import bind_hass from homeassistant.util.yaml import load_yaml -REQUIREMENTS = ['home-assistant-frontend==20180621.1'] +REQUIREMENTS = ['home-assistant-frontend==20180621.2'] DOMAIN = 'frontend' DEPENDENCIES = ['api', 'websocket_api', 'http', 'system_log'] diff --git a/requirements_all.txt b/requirements_all.txt index 7c03f3465c3..83b8052f78f 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -404,7 +404,7 @@ hipnotify==1.0.8 holidays==0.9.5 # homeassistant.components.frontend -home-assistant-frontend==20180621.1 +home-assistant-frontend==20180621.2 # homeassistant.components.homekit_controller # homekit==0.6 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index f6762e1faab..a3925262572 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -81,7 +81,7 @@ hbmqtt==0.9.2 holidays==0.9.5 # homeassistant.components.frontend -home-assistant-frontend==20180621.1 +home-assistant-frontend==20180621.2 # homeassistant.components.influxdb # homeassistant.components.sensor.influxdb From 0ea2d99910b6defba4ce1ec53cf89cdd49efcf7e Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 21 Jun 2018 17:39:02 -0400 Subject: [PATCH 140/144] Bumped version to 0.72.0b9 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index e9b72a70f1b..7feb5d8bdac 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -2,7 +2,7 @@ """Constants used by Home Assistant components.""" MAJOR_VERSION = 0 MINOR_VERSION = 72 -PATCH_VERSION = '0b8' +PATCH_VERSION = '0b9' __short_version__ = '{}.{}'.format(MAJOR_VERSION, MINOR_VERSION) __version__ = '{}.{}'.format(__short_version__, PATCH_VERSION) REQUIRED_PYTHON_VER = (3, 5, 3) From 124495dd84f5c41284647308f115cad075098c7b Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 22 Jun 2018 10:24:04 -0400 Subject: [PATCH 141/144] Update frontend to 20180622.0 --- homeassistant/components/frontend/__init__.py | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/frontend/__init__.py b/homeassistant/components/frontend/__init__.py index 89353b56098..9c9fdd137e2 100644 --- a/homeassistant/components/frontend/__init__.py +++ b/homeassistant/components/frontend/__init__.py @@ -26,7 +26,7 @@ from homeassistant.helpers.translation import async_get_translations from homeassistant.loader import bind_hass from homeassistant.util.yaml import load_yaml -REQUIREMENTS = ['home-assistant-frontend==20180621.2'] +REQUIREMENTS = ['home-assistant-frontend==20180622.0'] DOMAIN = 'frontend' DEPENDENCIES = ['api', 'websocket_api', 'http', 'system_log'] diff --git a/requirements_all.txt b/requirements_all.txt index 83b8052f78f..54f3a89e089 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -404,7 +404,7 @@ hipnotify==1.0.8 holidays==0.9.5 # homeassistant.components.frontend -home-assistant-frontend==20180621.2 +home-assistant-frontend==20180622.0 # homeassistant.components.homekit_controller # homekit==0.6 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index a3925262572..c8194a8382f 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -81,7 +81,7 @@ hbmqtt==0.9.2 holidays==0.9.5 # homeassistant.components.frontend -home-assistant-frontend==20180621.2 +home-assistant-frontend==20180622.0 # homeassistant.components.influxdb # homeassistant.components.sensor.influxdb From 7325847fa951b729a5145e3d0762322d0d035273 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 22 Jun 2018 10:24:45 -0400 Subject: [PATCH 142/144] Bumped version to 0.72.0 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 7feb5d8bdac..a22605c37f4 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -2,7 +2,7 @@ """Constants used by Home Assistant components.""" MAJOR_VERSION = 0 MINOR_VERSION = 72 -PATCH_VERSION = '0b9' +PATCH_VERSION = '0' __short_version__ = '{}.{}'.format(MAJOR_VERSION, MINOR_VERSION) __version__ = '{}.{}'.format(__short_version__, PATCH_VERSION) REQUIRED_PYTHON_VER = (3, 5, 3) From a02d7989d5dc6c69ed80ab2282797be9f8a3acc5 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 22 Jun 2018 11:07:26 -0400 Subject: [PATCH 143/144] Use older syntax for version bump --- script/version_bump.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/script/version_bump.py b/script/version_bump.py index eb61420a600..e324b231d06 100755 --- a/script/version_bump.py +++ b/script/version_bump.py @@ -130,7 +130,8 @@ def main(): if not arguments.commit: return - subprocess.run(['git', 'commit', '-am', f'Bumped version to {bumped}']) + subprocess.run([ + 'git', 'commit', '-am', 'Bumped version to {}'.format(bumped)]) def test_bump_version(): From 66110a7d57ffa52a303980595a34a4199f4a2b66 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 22 Jun 2018 12:46:45 -0400 Subject: [PATCH 144/144] Bump frontend to 20180622.1 --- homeassistant/components/frontend/__init__.py | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/frontend/__init__.py b/homeassistant/components/frontend/__init__.py index 9c9fdd137e2..3d2231ab43b 100644 --- a/homeassistant/components/frontend/__init__.py +++ b/homeassistant/components/frontend/__init__.py @@ -26,7 +26,7 @@ from homeassistant.helpers.translation import async_get_translations from homeassistant.loader import bind_hass from homeassistant.util.yaml import load_yaml -REQUIREMENTS = ['home-assistant-frontend==20180622.0'] +REQUIREMENTS = ['home-assistant-frontend==20180622.1'] DOMAIN = 'frontend' DEPENDENCIES = ['api', 'websocket_api', 'http', 'system_log'] diff --git a/requirements_all.txt b/requirements_all.txt index 54f3a89e089..52a5e052560 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -404,7 +404,7 @@ hipnotify==1.0.8 holidays==0.9.5 # homeassistant.components.frontend -home-assistant-frontend==20180622.0 +home-assistant-frontend==20180622.1 # homeassistant.components.homekit_controller # homekit==0.6 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index c8194a8382f..a38c7f259b4 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -81,7 +81,7 @@ hbmqtt==0.9.2 holidays==0.9.5 # homeassistant.components.frontend -home-assistant-frontend==20180622.0 +home-assistant-frontend==20180622.1 # homeassistant.components.influxdb # homeassistant.components.sensor.influxdb