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 51595d19b1ac33..c59798d16d7242 100644 --- a/homeassistant/components/sensor/smappee.py +++ b/homeassistant/components/sensor/smappee.py @@ -21,17 +21,17 @@ '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 4c9757b3260fc7..5644c3d0a1f2f6 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 00000000000000..ef4f1b349d72a5 --- /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 fd2bb5b4f5a1e5..fcd1e3726e9405 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 00000000000000..7df6166a2b6a18 --- /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 72a4cfdfbaaa8b..ebdcdc6ca70a20 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 ae3a4e0be72328..2090f5227093dc 100644 --- a/homeassistant/components/xiaomi_aqara.py +++ b/homeassistant/components/xiaomi_aqara.py @@ -23,7 +23,7 @@ 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 fcd1e3726e9405..00ed2f88cb79d6 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 68a9da55ae4f9d..7fa8d5b3fe84f7 100644 --- a/homeassistant/components/media_player/kodi.py +++ b/homeassistant/components/media_player/kodi.py @@ -393,7 +393,7 @@ def state(self): 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 75b8a1f72bd9bb..0c57c98c0af3ab 100644 --- a/homeassistant/components/sensor/moon.py +++ b/homeassistant/components/sensor/moon.py @@ -4,7 +4,6 @@ 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 @@ }) -@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 @@ def icon(self): """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 027c12569a609a..8e8c784e68b941 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_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 @@ }) -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) + + session = async_get_clientsession(hass) + pi_hole = PiHoleData(PiHole( + host, hass.loop, session, location=location, tls=use_tls, + verify_tls=verify_tls)) - api = PiHoleAPI('{}/{}'.format(host, location), use_ssl, verify_ssl) + await pi_hole.async_update() - sensors = [PiHoleSensor(hass, api, name, condition) + 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 00ed2f88cb79d6..59cec2c1e6acf8 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 5b84962144d5b0..0263a1266c6196 100644 --- a/homeassistant/components/sensor/smappee.py +++ b/homeassistant/components/sensor/smappee.py @@ -189,8 +189,10 @@ def update(self): 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 c5769253657c61..5d6e1453124c09 100644 --- a/homeassistant/components/device_tracker/xiaomi_miio.py +++ b/homeassistant/components/device_tracker/xiaomi_miio.py @@ -20,7 +20,7 @@ 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 2acc3895f3e5a4..2f00de08005813 100644 --- a/homeassistant/components/fan/xiaomi_miio.py +++ b/homeassistant/components/fan/xiaomi_miio.py @@ -49,7 +49,7 @@ '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 24eab7ebd4ad1c..cba15f6df9f7ec 100644 --- a/homeassistant/components/light/xiaomi_miio.py +++ b/homeassistant/components/light/xiaomi_miio.py @@ -42,7 +42,7 @@ '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 e731d421e69f8c..8a3e51b55b32b7 100644 --- a/homeassistant/components/remote/xiaomi_miio.py +++ b/homeassistant/components/remote/xiaomi_miio.py @@ -22,7 +22,7 @@ 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 066dc384007f5b..f7bc9488cc5e44 100644 --- a/homeassistant/components/sensor/xiaomi_miio.py +++ b/homeassistant/components/sensor/xiaomi_miio.py @@ -25,7 +25,7 @@ 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 149acd76c07c00..b0d251822b0b1c 100644 --- a/homeassistant/components/switch/xiaomi_miio.py +++ b/homeassistant/components/switch/xiaomi_miio.py @@ -39,7 +39,7 @@ '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 620014a1baee71..f6789d78b9ae97 100644 --- a/homeassistant/components/vacuum/xiaomi_miio.py +++ b/homeassistant/components/vacuum/xiaomi_miio.py @@ -19,7 +19,7 @@ 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 59cec2c1e6acf8..9e7d73b053b390 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 5a8800d9583f2a..b907d4b4217b37 100644 --- a/homeassistant/components/config/__init__.py +++ b/homeassistant/components/config/__init__.py @@ -21,7 +21,7 @@ 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 45c35dcdd2a186..0fbb2a57ca95f0 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 c27e394ce28e52..7ee1c70487fe1d 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 1ea0b586d33665..bcfae533abfc9b 100644 --- a/homeassistant/components/logbook.py +++ b/homeassistant/components/logbook.py @@ -100,7 +100,7 @@ def log_message(service): 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 5dad77f64cef3b..3f2f9ded22a8cc 100644 --- a/homeassistant/components/frontend/__init__.py +++ b/homeassistant/components/frontend/__init__.py @@ -5,7 +5,6 @@ https://home-assistant.io/components/frontend/ """ import asyncio -import hashlib import json import logging import os @@ -30,8 +29,6 @@ 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 @@ }) -class AbstractPanel: +class Panel: """Abstract class for panels.""" # Name of the webcomponent @@ -113,30 +110,20 @@ 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 + def __init__(self, component_name, sidebar_title, sidebar_icon, + frontend_url_path, config): + """Initialize a built-in panel.""" + self.component_name = component_name + self.sidebar_title = sidebar_title + self.sidebar_icon = sidebar_icon + self.frontend_url_path = frontend_url_path or component_name + self.config = config @callback def async_register_index_routes(self, router, index_view): @@ -147,19 +134,7 @@ def async_register_index_routes(self, router, index_view): '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.""" - self.component_name = component_name - self.sidebar_title = sidebar_title - self.sidebar_icon = sidebar_icon - self.frontend_url_path = frontend_url_path or component_name - self.config = config - + @callback def to_response(self, hass, request): """Panel as dictionary.""" return { @@ -171,95 +146,25 @@ def to_response(self, hass, request): } -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. - - 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 panel.frontend_url_path in panels: + _LOGGER.warning("Overwriting component %s", panel.frontend_url_path) + + 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 @@ def get_template(self, latest): 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 zcmd6MhgVZe)Ni;-6A(cGmENT*RgoeRK&pU*7NvI*dJ9zrk=~0GK|xv|^n@OeCOsk$ zAfZW-X5dQizQg_2x7J(h{R1y+ot$&_%?Y@ZqGfeD8xm8&)8Y{09(-#Ur=bKmll= z_IPII4Fb{Jyu8Rj*}1m?BBhU}t{UY61tk^Vy?-?A<^d9gjkX3DxC6gq)s`?2hEAbq6#V6-yvZJV8$jMaGD)F$}%1A!WTfx9>3qt$J(P8IW+PHD4Q)9;w{J7R3Q z{)u^emnnY7+C;R5fvm^vmZ&M{-N)LGq_*})+aJkQ{I|A-+FE|M?Vc*)5VDaP9kW4y zy+=;-c9kQ^{%7|85B~3)Mn|CT7h2nX0OPfHa7ETw3DAN;0ryxG#W4Hw<_FWMTtQEF zQM(Js1>*gb=`k9H)c`GUtCTkxN}`jR+Nq|Oc$`p-$j9ad*1P1zM;=#HPW^&~{|WH=9c=qdn$q*iYn7O&bO!nyXsp#M(Z4O@>f?B| zXge4pxw@&z&Lj8=nbi$y^S=AW37jrb4@qJ+`=7wxoM8B?@xgYvSKaV$AMAo+^;S-) zK{?HPw3_Q#wpYQQLPh>mf=#X!V8jV4hX~=j(uawZb<2m#r`JF^qA#R;VM3kK#s&B3 zREYP$Gjpj`-@12oUpTshKv&fHo`Xqmx#gj!SSq3-k9xeG8a&)I)rtCnZ0wJ13eXiE zmlRE=XX^X~2jxyvFH;=Wr=dogFYnOj@GiX+20aP0k(r6CU>#y($a|(tMTi{b$s%6a zStT(HUcWu6fc*C$yEVXc>c+jaH4^Kl>>A0H>FaaG;t<=i4H1Y|!;jOnEXpP2GXnz35R&Y9Y zorwWq(EY_WDT>hiJuKgGdE3wG=T!GQ3eXe7JtrekC)%4oA8IcSst7o-%I!uD|9!^B z+CO389>od@j*U6%MwsiIet9?S%8#V1D}~=+=${(7slw5Fn;8U4D$-nk6j*u@SRK_J z%n&P^2ux9${6RSE5Pnaa?1|;|+cj*qrBt2yXyxp0z$%>5|3)??CpE2RdpyY;10^MA znNN+^pt_kDnK+oy`@^j%zE}avFk^&gN0bzpK1szaJI&PvnChU+^&*@r@rl!a65tvq>x`^ zuU+hgWw@a-hMC>oPxa4cu_=op{2{mQisNzvag2;FlV?mBg2m?Ph;E!<9V`n=_tRbv zz<#Y6MdWn0!={6nAYAEO>sI9@bNXFN!TB7xkT#jZQ$D-9<+ZvmBvUQ>Y0lIsGV?IQ ztvlz4haA#}F^Shb%0Tx^rO@Cm(yx^GtCSrWBK>fy`!hB(M7HxShtxJQT1l0$RcHC# zYFLXS-6wbhr%h25@lJA*jB#(*)%&;arDb};^|f%-h%eopx^_g;erZ(Xu;z}h2q2s6 zgIU%>^*(JxUTcyiuYN*-=Av8w9Ja(!hsG-a3!;$KrV$jUUgn=Mv*=w9@52x`KiAD> zE{89EbIG>8rJwx|E$yFC!@xWk+Ka8&vQx#0NP2(tY%wAwi6M&D4Rro0a6@PKwm=qP zI(!yGq%^pf1=Y8?dJeQa>2Kn3YAR-+2JASAxa4dg=vip@jp%KAZGnf7T)6OCbaueA zdTwZ0h3HnLkrPF*Wjw8*rvIA7(jjE%cbA;ow$wg*4Ni31*60g{c9Tle3w5QQ9#AAX z6d%OH57U)uNC&FH>~uMfED`aa831oF6j25Q4ZZ-3z*NXosQ zi~g0Z?h}{Hq&wv&p$eveI~9%usZIICuJ+?Z0Tx`&#mw~Cye%3*12+=C&JVHDWzq13 zBI9`JYUN(j)zv3ir!CWs(KVd*4L*?Tg0-CA$lst1@=ai{G9>FTZLfI9Rp1vaU3WQa z4`b7g_e|~(og{Re5i?8BU3?nN)BSb#GpfKXIe-0nbC&tX9;xehBHV7?>t`>3J~~Ki zq$j#rsRcXf|$5?#i^E46_k2wNlNM#ujPakLx@ACm1#xaDvrvXnV0pm z-{!07=9iEUi!HyQ@9*MN0|j%sf)_3%m}O;oy|2uFZX3Jylx+5TH7jQJ4htmEi;qK6 zV{wQE`}qmSFNvp{@D>(p;>Vid)_j(Nn||+V z{hV;j-w^L?#Mhu(i5WU#fT#uEj&v4p^%EiiAsnNYKlDyIS`l@p)XTuwZssUf+KV4j zcBDf#fdan-g=&$2H`F;!NsYR-Ck6(`_eI;{4k;<_#l?oL_D zyqqh>E7yy%SSaWNMP2MeGnmnwgW)Hlm|5`MKb#DWwv!RMsYMT}*VV-A-eE{ex*Q`b zt)JHY)>-Glzd|{Ac76u90U<87_63=2Rw=nII{_1P4Kq6eMtLmWgLs`kU*1f}x;QV* zpzkkNTfkWJGSmJheeMD#qJFl#qP2cKt{2jR+gT;J$i zVC$bL@bRzb{W*Kh0D$!5FMdpV@sWB)aCTlCyrJudlz)_Y(4-I&b>xlaCaqYO*Fr~2p)GAT;L7S)>HT$5) z%VQ`n0FXYk+4&r#A`PQWl#C?D6gk@9y=ji`_F{;LVrEV$9f-1wim$A(P)WVIvQdx8 z?{|%GHo~<{`B^?avupF#x_Q>O94YHdorzo1vz0Wh5A}GQN^6Osh4PyN=^{S9WdxIA zO^Odahki@nhkx$!9S9H5+-6X(>0p#O{&KqakL$ljEK`0hBCzz`+Vl;WiQA~bi!TGk zoayWFE&bVu%xBJlcKgo71|M_}GJa-W#n&z}Fao%fk%$^BG?e4mT7#hk?mUl@9P`mdbw% zNPUD9jnwW}j4ii*W%X{1wjN%HD~20e5ohT`T>KV|73?iHjmN}%D4ft?N$BZ!N+O{K ziwpQ&M!AaxDPvvfxK6fB1-Qns%KU41gSMi}&(|@l?&6m3ryQT}xDsOe;xUQtk;^Pv zc#NVMbo)aZmZV*pS#(V4I#?ORoNMeC?p3b zaqtb+Z})cb1~X2_Id?$T)iu2WK4NAE4~cz>xbQBpwvD(NaG@Z^SLE0?r4w2m_7y`M zRymk8I?p+*Y`?YU0@t6|74$#+a7UJZeGVyu8A4Z}oR4Xc`d2e5P|rV0^c8EuaLvkz z1##V3K=ySLw5&?uwBx78Xp6DQu_3eDO|7P{!F`}+mX&~V!ve)LML6Y6BC1D=Sbvy9 zluqdZQ4(bloE+&>k7Y9)EZps#d7L=o?tE^)&|d^x@ReVSn0ekzH19>Sa8}?GFV8xy zDO6E#$N(V6&sj>8Bnu9cmW`NC@n0&X$H@)L?6uH^iW0Yy9)Lrg!hS1Buc-JfDTsy9 zK=#4djZ!gE&i6Q?a8u*DK{+%G1;BD9OTAP)O?2l*EoR(SP1^24l2JOoz|-m5r>*DE z=AoTZc}k)i3vNt+1YK?JV4=2^eX(CPziy5<-mMa{gE>dEiX?&= zQ{|}oti)AT-q*XPk&A5IQMK?+ayeOkR3!h{kn!%TN@mND*Km9>ZbM$Uu#yVu`t>ve z>&B8vcoQv?_1L+5oJNz}>h#@XE_h8@7(BxsnkJn(FlgvYLfMfGWJ~|$E0t9EUY4~kHNl1Juev=PUN;3YS^?i zv(2TS_4-}wl3OLu=;kt+ElB@n;ViEYBxtHhQj5i&k>`3$o0aKk2s&ugIeL-YF%7b z+Q@Z@nNBoTR*_ES5kXGd6QPlpHf0&a$o#-zh0b#C8;y%Axj%V-;~Q&W_yj30pKon!kW+u+x#jOhzKQT1&G*Z`RWtl8&628wiI*h< zj`?f*Uee+5Dg+(*9mt`GtdrOK=tGPCVe^-7`AFCwG| zLx%>)+hs$FGtUH=t*`u>SpAL>#YFJMv=vwt>L3@fQh~n9QHexD8S$VW)5j+3nf-@v z{Va{^E6f*z{py8_W2y)&z&LcZnOq_ixpABd?h_93SUxgkvTSQ_yYWmNqO=K~m^S)& zacrVdj?4`T-SINII3uu>G~a*s-6_oMO*L7-%PAiLY0osIWpE3M()^s&A38?iCy)$4 z;=~DCH-XU3U&AoU1v2BZ+L``Tc!rJ~bWYg8K-gQDEq>wnW{rx>q8|C1V-~I$vjn%n z#zP+sfn;`w0Yq}YW4WO))z5FmuikS#wV(VJAZlfG`dsiAm*bOREN63AO-JaXnDlef zz4le;^?^}LM3I3c*#xh#``pq~Wug~Edb-YylZurKrbwNnMDHpAP2YKDxDvj45SXmL zJH(&BTr1tWi(8hf;RygzNYL>8Q_0ts@+AGLygF{!>+1dbkhY8cX=m!EgE4Bze-}yc zkZ~=yqq2=sAwA_B;fo=cKGGjtbuoK_hr(kQ68lGQfli8rk~q{xvcCD)Khdl`V=#Ad z)bvPVzOHhP3({akDsl4|_kCsSi$P^>-GoAr5YS)Hddw^nL#ws9a2~Ele3ii}(#l&O8yHw40%!pXHHD&SDxuO&5X( zm^nEPHx$vk-b^^FrSj}T24}fGFk(O~lB8mNedwmPe`GlOHs{B~Pa8%P&-JRsG&j@7-$+c>iS;=GGWe->Xy`cyvP+q)eTowAOy0fo!wU-IyK`#PRztnfj~zAyn+0 zPP@dUZgWoD`Khbi(A$<9Z`{+iQ5p+?)`8X8+AYowps0}EXblYAnV#i2gZ6%!PSl0JxEs(@t zNK?JqHvvMvCha7hsd_nL#Z3lT&^$Htu%c9HOlx)$h9up3n+|#8F^LK}j{Kr+F;h*m z?c#30M4ua8O?IR=h0lLKo2*X!R@hHes<+acGDcSfi=F3;HgG;udXtn{w2?VO`fXlc zr&`;2A25Zs_1LPlH|}|pb%XEzW7!u}Bd5dJO6Mj!ezeDrC$Yh8OBsoPmq4T`0Voy$ zP&|v}Xq$o^`eMBQTdAu-hWD-=~rAX88z>s_aYR%e?cDXo9za^&)K>{_cGf)Z9e{?hpEtz-%eN`GG zJ}-5=_cd1vg}JG*TTq4D3c1PBNkNrk;?Ds~u0=I$olQEVV`39HA8OPh_tf+E_Ei3) zg``}|@w?->h&1Ly$`{7c#HZa(~$EcfyqnDH4LA`TI*)>#NU~ za6B+NDppXZLuQTQ$LxQ{)6MFNF`d7xOhcYXVkRA;tH}XI^LaHP2em<+br*4@mz0h$ zHRMsug`#aaG(aSi5_HkVG;4vbM9B3hVPprCUOKw&U2zpj58)*8pHF^sh%W9@l5Mk0>%Rixyavs_aKP|BpM7g~xFL2f zivYacjzt|QAPK0=#9Dh*ROhsjzujQGuAs}PE6AIDC`Kv&<-?V~#uO_fPx?I%4i;`=48%m3AA1~rD73MSJXqMWHl@g-b079>d|Nf$j8+4@ zQUEr)WF%HTuQ~dq#yEZc&ts@{-3V!*UF+}JW|3>E{Mv?&o}@BdcLxd5+k-)#V|iG{XtV4dSO?L^NA3^po!EuTd#8b8YgD+z3!Y?Z`(Vq>nhicRR7@A zYbO$1lYVboHh4rXabV^yXd~!4v^yQpHgWY=h4-1G1t6Y(%?|8K&&>B<+%z1ydOB|D zFdMbegwJH7$;pfPccF(2Kq^$ydW`Zk@ciWfe?eO2yZ+u>S(l!p%r3QF4Zik=jW!bB z22)TRm`9G>Fi@zl+E^3y1hk?;J3MK0FaVDe)h?{ZP1Xkp5JQV3u5T%r`J%@iUCJph z8P`^5$rTp|VvGHGrre9PBIeGP`?S_3)^|>?rd-UrS~L!L-XR#DWeI1u#wHHgtyPL3Umms7!9wZ%UF?1UVEpp!k`0Np**WFI}H@N zo{3zmRcw73$#a>+-LD!=EFC*bfGyn3&6+ze?kme_Pl#og&3poO1d!mN`&tH|2-GSJ z^&4;bSb1oBqh5gew*XyC)trPcu8|`r4hmUOT}8Cf0{(5i*)Z48-|}$F4BGH`+%Ko~ z>@Tq4DQWXTERo~NA%7sJf&bf2k-F}=;VUuU5y{d7Om}g)1@of@^~~lMkyyD&bLzD} zl0Z2Vftrti7PCrI4)8-RPwrZe{4FLfG6C8j=i5Eb%FkbA#)A%jT%6Y26vMkAWfaLo^y{CDv2>)Sd<35SF+>TUFm zPKwvnMa_kK18qi2r@JYqHNL*3&0+Jfd9Lyj+G0+Wyi3eW2ARPEr;)7C0sN(B$@Ubz zwGSKblC*_8IlF$FRV*ly5^Zog-q5WQS5pVH&T5o-%{CG`2V-Z|;a{sMv&bx2J4kpR zo}f`L>qkp{syqH48(qGLFR))VNYCjPz&TMT_|cTMdoD)EUF5hE4MWN7%i zn58G!OWK+O*6QT*V=kSI_{pET2}VCrYg6NunvotWMrjp62grNm(Zn!0fY06>Cj|Xn zY{1Gy(A|*5?X|vYZFBK#oZcX=QCR2e-nn!E1acmJhDQRslqx^|ELU zZ<|NKdPj}hvsg*&GPqZ6^Tuzh$+%!w(wowC&Of`H>>)G`=7&XCh>5O100eNdt$&n< z4BO4C?=6$x7%)0!Z^uP2UgB&x$kB}#GQ4Yo94X$orS<)~>zc)U^@&yHWt7cPrd)ep zHqf+Mo`4LP*ZMBOY-dOv1C(>WX30)o3+IOvpi4UZK3b@~na}lP0PJ;^*Oor#QMy9= zBUQxR#8tI36c^-j#m-py>MUtW)+)5}7=v&boxe#o&v#oM3SLVD`z zqJaHVQ5Z~se#xEBEjhq+(ctYfsjXB;;oJ0Ch4{D!@jEmD->g!vbuQMbGM6{ yXNBrcSJAaTbMB(mN$WYG{|`QC{wKm~vekBx7j#)hES)l!1Wh$PaOG30xBmyu;i?t@ diff --git a/tests/components/test_frontend.py b/tests/components/test_frontend.py index 657497b868bf34..2f83d923e2bd92 100644 --- a/tests/components/test_frontend.py +++ b/tests/components/test_frontend.py @@ -8,7 +8,7 @@ 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 fd0e30ccebc408..10fc2ccc3ff542 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 bf2dfe39bd8b55..5b8effd5dcc0c5 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 49452662fc43e1..a4b921037dbe4d 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 @@ def get_module_names(self): @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 44a54c9551261f..a635d1820db083 100644 --- a/homeassistant/components/netatmo.py +++ b/homeassistant/components/netatmo.py @@ -16,9 +16,7 @@ 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 @@ 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 @@ def get_camera_type(self, camera=None, home=None, cid=None): @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 f09e1d4f395464..191e587feafd1d 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 @@ def get_module_names(self): @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 9e7d73b053b390..87256c8eb7a3b0 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 93d56a97c4273e..5c1d9a576a6852 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 @@ def get(self, request): @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 be40bf7d010752..d44ac138e4171f 100644 --- a/homeassistant/components/media_player/xiaomi_tv.py +++ b/homeassistant/components/media_player/xiaomi_tv.py @@ -13,7 +13,7 @@ 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 87256c8eb7a3b0..24c2df99ebb7f9 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 9dac0b48bc2c3a..9f114cf2c56a33 100644 --- a/homeassistant/components/sensor/simulated.py +++ b/homeassistant/components/sensor/simulated.py @@ -100,7 +100,7 @@ def signal_calc(self): 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 3bfccc629fdd52..d226c79cff507a 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 @@ def test_default_config(self): '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 dfbbb232efca86..c8958d98178dab 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 839da8c9d53331..fa3ca31c770725 100755 --- a/homeassistant/components/climate/fritzbox.py +++ b/homeassistant/components/climate/fritzbox.py @@ -13,21 +13,27 @@ 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 @@ def current_temperature(self): @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 @@ def set_temperature(self, **kwargs): @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 @@ def set_operation_mode(self, operation_mode): 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 00000000000000..ccffef9e547b9e --- /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 c8958d98178dab..c51f6bc37cbca0 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 00000000000000..67957ca99b9f68 --- /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 69447b81cd427e..00d4291539b936 100644 --- a/homeassistant/components/discovery.py +++ b/homeassistant/components/discovery.py @@ -84,6 +84,7 @@ '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 24c2df99ebb7f9..8673f4aa22f504 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 4b9a2c89da0bab..c594bf1f99e16a 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.""" - - url = '/api/config/entity_registry/{entity_id}' - name = 'api:config:entity_registry:entity' +@callback +def websocket_get_entity(hass, connection, msg): + """Handle get entity registry entry command. - 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( + msg['entity_id'], name=msg['name']) + connection.send_message_outside(websocket_api.result_message( + msg['id'], _entry_dict(entry) + )) - entry = registry.async_update_entity(entity_id, **data) - return self.json(_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 fd7c69994776d8..1591b8da1d2c34 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 3f2f9ded22a8cc..f9ace910481e19 100644 --- a/homeassistant/components/frontend/__init__.py +++ b/homeassistant/components/frontend/__init__.py @@ -96,6 +96,15 @@ 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 @@ def async_finalize_panel(panel): 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 @@ def get(self, request): # pylint: disable=no-self-use 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 11094acd3e2a00..e16e5524f95456 100644 --- a/homeassistant/components/websocket_api.py +++ b/homeassistant/components/websocket_api.py @@ -38,6 +38,7 @@ 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 @@ def handle_hass_stop(event): '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 2f83d923e2bd92..2f118f24ef0933 100644 --- a/tests/components/test_frontend.py +++ b/tests/components/test_frontend.py @@ -11,6 +11,19 @@ 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() -@asyncio.coroutine -def test_themes_set_theme_wrong_name(hass, mock_http_client_with_themes): + 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' + + +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) -@asyncio.coroutine -def test_themes_reload_themes(hass, mock_http_client_with_themes): + await client.send_json({ + 'id': 5, + 'type': 'frontend/get_themes', + }) + + msg = await client.receive_json() + + assert msg['result']['default_theme'] == 'default' + + +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', + }) -@asyncio.coroutine -def test_missing_themes(mock_http_client): + msg = await client.receive_json() + + assert msg['result']['themes'] == {'sad': {'primary-color': 'blue'}} + assert msg['result']['default_theme'] == 'default' + + +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 cff103142b0b92..fbd8584a7d14ae 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 aa1d2d9eff049e..d71419ba79e69e 100644 --- a/homeassistant/components/sensor/gearbest.py +++ b/homeassistant/components/sensor/gearbest.py @@ -16,7 +16,7 @@ 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 8673f4aa22f504..81f60e47c6d592 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 ebdcdc6ca70a20..3f8fc3dbb3645a 100644 --- a/homeassistant/components/binary_sensor/xiaomi_aqara.py +++ b/homeassistant/components/binary_sensor/xiaomi_aqara.py @@ -330,7 +330,7 @@ def parse_data(self, data, raw_data): 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 f9ace910481e19..d61b6f50a964d2 100644 --- a/homeassistant/components/frontend/__init__.py +++ b/homeassistant/components/frontend/__init__.py @@ -24,7 +24,7 @@ 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 81f60e47c6d592..0449a695b663fd 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 47f54954cd2199..207b0a8545f478 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 c8c7e0d809b5a9..1e565054637766 100644 --- a/tests/components/test_init.py +++ b/tests/components/test_init.py @@ -74,30 +74,6 @@ def test_toggle(self): 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 fc9e91c93d7523..f8873894a01bf0 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 3ba95407fec15c..e29289722e89b8 100644 --- a/homeassistant/components/notify/mastodon.py +++ b/homeassistant/components/notify/mastodon.py @@ -13,7 +13,7 @@ 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 0449a695b663fd..c68ec249fc8a44 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 dff5ccd42acff2..421356f07bc632 100644 --- a/homeassistant/components/light/lifx.py +++ b/homeassistant/components/light/lifx.py @@ -30,7 +30,7 @@ _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 @@ async def service_handler(service): 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 @@ async def register_new_device(self, device): # 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 @@ def supported_features(self): @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 @@ async def default_effect(self, **kwargs): 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 @@ async def set_color(self, ack, hsbk, kwargs, duration=0): 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 c68ec249fc8a44..ce34f2662eb70e 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 20a1a473ba8bf9..7452b7dd186983 100644 --- a/homeassistant/components/media_player/__init__.py +++ b/homeassistant/components/media_player/__init__.py @@ -57,6 +57,7 @@ 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_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_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 @@ 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_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_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_service_handler(service): 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 @@ def source_list(self): """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 @@ def async_select_source(self, source): """ 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 @@ def support_select_source(self): """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 22fe1d005f711f..2c74feae847809 100644 --- a/homeassistant/components/media_player/demo.py +++ b/homeassistant/components/media_player/demo.py @@ -8,8 +8,8 @@ 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 @@ def __init__(self, name): 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 @@ def shuffle(self): """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 @@ def set_shuffle(self, shuffle): 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 0a6c413a688b88..765f7e1f0f76f5 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 c51f6bc37cbca0..e38ceeba5cefb3 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 00000000000000..246cf3a96c28ea --- /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 ce34f2662eb70e..76492dee899b63 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 e76dc24d9ddcb2..c69e9eb4af41fa 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 76492dee899b63..d2b52d17961321 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 4390b980f9e948..a4d15feb7fc324 100755 --- a/setup.py +++ b/setup.py @@ -42,7 +42,7 @@ 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 1d98a5733f7054..5397daeb784cfd 100644 --- a/homeassistant/components/climate/mqtt.py +++ b/homeassistant/components/climate/mqtt.py @@ -17,7 +17,7 @@ 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_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 @@ 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 @@ def __init__(self, hass, name, topic, value_templates, qos, retain, 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 @@ def supported_features(self): 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 663393503aca86..255d482d584394 100644 --- a/tests/components/climate/test_mqtt.py +++ b/tests/components/climate/test_mqtt.py @@ -9,9 +9,9 @@ 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 @@ def test_setup_params(self): 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 @@ def test_set_with_templates(self): 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 00a7e49840e159..ab6c3a084c07c2 100644 --- a/homeassistant/helpers/entity_platform.py +++ b/homeassistant/helpers/entity_platform.py @@ -260,9 +260,15 @@ async def _async_add_entity(self, entity, update_before_add, 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 35cc1015aaf722..4a2cd5fa50c31d 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 @@ def async_generate_entity_id(self, domain, suggested_object_id): @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 @@ def async_get_or_create(self, domain, platform, unique_id, *, 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 @@ async def _async_load(self): 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 @@ async def _async_save(self): 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 4e09f9576f2c61..9fa178022dc43c 100644 --- a/tests/helpers/test_entity_platform.py +++ b/tests/helpers/test_entity_platform.py @@ -16,7 +16,7 @@ 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 492b97f63873b9..6808206243f8d1 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 | 75 +++++++++++-------- 1 file changed, 42 insertions(+), 33 deletions(-) diff --git a/homeassistant/components/media_player/yamaha.py b/homeassistant/components/media_player/yamaha.py index bb7942a2545ee2..cf36345806745e 100644 --- a/homeassistant/components/media_player/yamaha.py +++ b/homeassistant/components/media_player/yamaha.py @@ -6,32 +6,44 @@ """ 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__) -SUPPORT_YAMAHA = SUPPORT_VOLUME_SET | SUPPORT_VOLUME_MUTE | \ - SUPPORT_TURN_ON | SUPPORT_TURN_OFF | SUPPORT_SELECT_SOURCE | SUPPORT_PLAY +ATTR_ENABLED = 'enabled' +ATTR_PORT = 'port' -CONF_SOURCE_NAMES = 'source_names' CONF_SOURCE_IGNORE = 'source_ignore' -CONF_ZONE_NAMES = 'zone_names' +CONF_SOURCE_NAMES = 'source_names' CONF_ZONE_IGNORE = 'zone_ignore' +CONF_ZONE_NAMES = 'zone_names' -DEFAULT_NAME = 'Yamaha Receiver' 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 PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, @@ -44,16 +56,6 @@ 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 service_handler(service): 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 @@ def __init__(self, name, receiver, source_ignore, 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 @@ def supported_features(self): 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 fab7d98ed98399..9906c61f2694cf 100644 --- a/homeassistant/components/axis.py +++ b/homeassistant/components/axis.py @@ -272,8 +272,7 @@ def __init__(self, event_config): 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 e87886376bc307..310e2289cbc9bf 100644 --- a/homeassistant/components/binary_sensor/vera.py +++ b/homeassistant/components/binary_sensor/vera.py @@ -19,8 +19,8 @@ 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 4a1b99f4b9bc9a..7068d51f6a3632 100644 --- a/homeassistant/components/binary_sensor/verisure.py +++ b/homeassistant/components/binary_sensor/verisure.py @@ -54,6 +54,7 @@ def available(self): "$.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 6fb6bc0ff48410..4deb4d9ea2ea4a 100644 --- a/homeassistant/components/climate/vera.py +++ b/homeassistant/components/climate/vera.py @@ -32,8 +32,8 @@ 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 @@ def current_power_w(self): 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 ff9ba6f762b861..9b2e8f3aad02b1 100644 --- a/homeassistant/components/cover/vera.py +++ b/homeassistant/components/cover/vera.py @@ -19,8 +19,8 @@ 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 6b12e69341d2cc..7ace250b6eeee5 100644 --- a/homeassistant/components/light/vera.py +++ b/homeassistant/components/light/vera.py @@ -22,8 +22,8 @@ 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 b3aae5e159fafc..e6e277cdee1417 100644 --- a/homeassistant/components/lock/vera.py +++ b/homeassistant/components/lock/vera.py @@ -19,8 +19,8 @@ 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 eb8ccae768e204..4fc92db1d90092 100644 --- a/homeassistant/components/sensor/vera.py +++ b/homeassistant/components/sensor/vera.py @@ -25,8 +25,8 @@ 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 5ab999ccabfea7..187a9bd7935c2e 100644 --- a/homeassistant/components/sensor/verisure.py +++ b/homeassistant/components/sensor/verisure.py @@ -74,6 +74,7 @@ def unit_of_measurement(self): """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 @@ def unit_of_measurement(self): """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 @@ def unit_of_measurement(self): """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 d7c284e4ccf515..82e2756c23054e 100644 --- a/homeassistant/components/switch/vera.py +++ b/homeassistant/components/switch/vera.py @@ -19,8 +19,8 @@ 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 810946a505883b..4b126e5d3320be 100644 --- a/homeassistant/components/switch/verisure.py +++ b/homeassistant/components/switch/verisure.py @@ -72,6 +72,7 @@ def turn_off(self, **kwargs): 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 ebe92a2dcc2c1d..2603f61eb751ba 100644 --- a/homeassistant/components/vera.py +++ b/homeassistant/components/vera.py @@ -148,12 +148,10 @@ def __init__(self, vera_device, controller): 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 efaefc26184644..85050b5736f4ec 100644 --- a/homeassistant/helpers/entity.py +++ b/homeassistant/helpers/entity.py @@ -171,13 +171,6 @@ def supported_features(self) -> int: """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 @@ def async_device_update(self, warning=True): ) 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 e38ceeba5cefb3..a7d222b33b293b 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 00000000000000..bfc2d44fc6e8bd --- /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 00000000000000..c93da3c791f107 --- /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 00000000000000..cce8c349a31448 --- /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 00000000000000..9fabcb1cd5aefb --- /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 d2b52d17961321..6bbe2a0b79e89b 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 3f8fc3dbb3645a..be5d9a689d1194 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 @@ def parse_data(self, data, raw_data): 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 6931355ca0e207..224d694e0f5b63 100644 --- a/homeassistant/components/binary_sensor/zha.py +++ b/homeassistant/components/binary_sensor/zha.py @@ -187,8 +187,8 @@ def cluster_command(self, tsn, command_id, args): 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 82816c83404717..8c9409ef5ffc7b 100644 --- a/homeassistant/components/sensor/hive.py +++ b/homeassistant/components/sensor/hive.py @@ -10,7 +10,7 @@ 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 6e8995a0444cd7..daaffd0174c7ac 100644 --- a/homeassistant/components/logger.py +++ b/homeassistant/components/logger.py @@ -15,6 +15,7 @@ DATA_LOGGER = 'logger' +SERVICE_SET_DEFAULT_LEVEL = 'set_default_level' SERVICE_SET_LEVEL = 'set_level' LOGSEVERITY = { @@ -31,8 +32,11 @@ 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 @@ def set_log_levels(logpoints): ) ) + # 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 @@ def set_log_levels(logpoints): 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 c0279ef1d0f28e..19bf19a799a266 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 61cb42e8bb5bcd..a55a66c6505fe5 100644 --- a/tests/components/test_logger.py +++ b/tests/components/test_logger.py @@ -10,6 +10,7 @@ RECORD = namedtuple('record', ('name', 'levelno')) +NO_DEFAULT_CONFIG = {'logger': {}} NO_LOGS_CONFIG = {'logger': {'default': 'info'}} TEST_CONFIG = { 'logger': { @@ -99,3 +100,29 @@ def test_set_filter(self): 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 d3628fd57f3bbe..c33a16c632e629 100644 --- a/homeassistant/components/zone/__init__.py +++ b/homeassistant/components/zone/__init__.py @@ -45,27 +45,25 @@ 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 1c698438f2c410..c26b3375f3ace4 100644 --- a/tests/components/zone/test_init.py +++ b/tests/components/zone/test_init.py @@ -59,7 +59,6 @@ def test_setup_no_zones_still_adds_home_zone(self): 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 @@ def test_setup(self): 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 @@ def test_setup_zone_skips_home_zone(self): 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 @@ def test_setup_registered_zone_skips_home_zone(self): 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 @@ def test_setup_registered_zone_skips_configured_zone(self): 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 6b7f6cb2afc91a..030a76626c6e2a 100644 --- a/homeassistant/components/climate/generic_thermostat.py +++ b/homeassistant/components/climate/generic_thermostat.py @@ -14,8 +14,7 @@ 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 @@ def min_temp(self): 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 @@ def max_temp(self): 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 b3fff0dd796d33..363653608e86ca 100644 --- a/homeassistant/components/climate/sensibo.py +++ b/homeassistant/components/climate/sensibo.py @@ -19,7 +19,7 @@ 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 @@ def is_on(self): 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 59da425553a2b9..b3734e020e00e2 100644 --- a/homeassistant/components/climate/tado.py +++ b/homeassistant/components/climate/tado.py @@ -8,8 +8,8 @@ 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 @@ def set_operation_mode(self, readable_operation_mode): @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 71b74868544aff..92443ca2b42d99 100644 --- a/homeassistant/components/media_player/onkyo.py +++ b/homeassistant/components/media_player/onkyo.py @@ -32,6 +32,9 @@ 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 @@ def update(self): 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 @@ def update(self): 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 a7d222b33b293b..53dd7cdfe4e69b 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 00000000000000..4887ea1aa67596 --- /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 00000000000000..b4ed53b828d6b3 --- /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 00000000000000..859435edbc99ea --- /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 6bbe2a0b79e89b..f47bbbdf23e818 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 0fbb2a57ca95f0..6ab86435371f1b 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 4659578ae27e9d..0444e7a5b5305c 100644 --- a/homeassistant/components/panel_custom.py +++ b/homeassistant/components/panel_custom.py @@ -9,6 +9,7 @@ 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_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 @@ 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) - - 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 + kwargs['html_url'] = url - 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 00d18c7fe105d2..88464675c21587 100644 --- a/homeassistant/components/sensor/nest.py +++ b/homeassistant/components/sensor/nest.py @@ -17,7 +17,11 @@ 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 @@ def update(self): 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 20dceb8a1c5da2..e992020e2b275a 100644 --- a/homeassistant/components/camera/uvc.py +++ b/homeassistant/components/camera/uvc.py @@ -13,6 +13,7 @@ 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 dabad953bea13a..2de0782fd9109d 100644 --- a/tests/components/camera/test_uvc.py +++ b/tests/components/camera/test_uvc.py @@ -7,6 +7,7 @@ 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 @@ def test_setup_full_config(self, mock_uvc, mock_remote): '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 @@ def test_setup_partial_config(self, mock_uvc, mock_remote): '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 @@ def test_setup_partial_config_v31x(self, mock_uvc, mock_remote): '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 @@ def test_setup_incomplete_config(self, mock_uvc): @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 @@ def test_login_tries_both_addrs_and_caches(self, mock_camera, mock_store): """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 @@ def fake_login(*a): 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 @@ def test_camera_image_reauths(self): """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 @@ def fake_snapshot(): 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 0263a1266c6196..783c2aad4693ae 100644 --- a/homeassistant/components/sensor/smappee.py +++ b/homeassistant/components/sensor/smappee.py @@ -189,7 +189,7 @@ def update(self): 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 0b7fc3c078e811..43e9abd96a668e 100644 --- a/homeassistant/components/media_player/samsungtv.py +++ b/homeassistant/components/media_player/samsungtv.py @@ -155,16 +155,25 @@ def send_key(self, key): _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 @@ def turn_off(self): # 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 d38fbc7079c306..fc2e7fc912d6c7 100644 --- a/homeassistant/components/zwave/discovery_schemas.py +++ b/homeassistant/components/zwave/discovery_schemas.py @@ -213,6 +213,7 @@ }})}, {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 bdbc207a79ca9f..60741a9f3c8475 100644 --- a/homeassistant/components/sensor/homematic.py +++ b/homeassistant/components/sensor/homematic.py @@ -16,6 +16,9 @@ '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 793800a3d2259e..ca7ff17a16ab93 100644 --- a/homeassistant/components/media_player/snapcast.py +++ b/homeassistant/components/media_player/snapcast.py @@ -104,6 +104,11 @@ def state(self): '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 @@ def __init__(self, client): 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 0de87bd17eac84..4fed3793c50c00 100644 --- a/homeassistant/components/sensor/glances.py +++ b/homeassistant/components/sensor/glances.py @@ -155,9 +155,9 @@ def update(self): 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 601a73298af0de..b2f44696fbdc25 100644 --- a/homeassistant/components/binary_sensor/rainmachine.py +++ b/homeassistant/components/binary_sensor/rainmachine.py @@ -8,7 +8,7 @@ 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 @@ _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 @@ def unique_id(self) -> str: 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 7ee6b06372008f..38672dbc23b8d9 100644 --- a/homeassistant/components/rainmachine/__init__.py +++ b/homeassistant/components/rainmachine/__init__.py @@ -13,12 +13,13 @@ 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 @@ 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 @@ 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 stop_zone(service): ('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 stop_zone(service): 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 8faf30acc3899d..f747a26df397b4 100644 --- a/homeassistant/components/sensor/rainmachine.py +++ b/homeassistant/components/sensor/rainmachine.py @@ -7,7 +7,7 @@ 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 @@ _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 @@ def unit_of_measurement(self): 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 bdee64a3d54ac0..b0cdf334cfa127 100644 --- a/homeassistant/components/switch/rainmachine.py +++ b/homeassistant/components/switch/rainmachine.py @@ -8,12 +8,12 @@ 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_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 @@ } -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 @@ def is_enabled(self) -> bool: 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 @@ def zones(self) -> list: """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 @@ def update(self) -> None: 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 @@ def is_on(self) -> bool: """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 @@ def update(self) -> None: 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 f47bbbdf23e818..b941021d017ab2 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 53dd7cdfe4e69b..4af9a767434523 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 00000000000000..c50e52422568e8 --- /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 b941021d017ab2..0b1ef29eca4396 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 c50e52422568e8..9e72d188c99552 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 @@ def __init__(self, apikey, up_robot, monitorid, name, target): @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 @@ def device_class(self): @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 0b1ef29eca4396..9c48cea0d1c45a 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 928d84caa2b295..72c6aa2e1a3dfd 100644 --- a/homeassistant/components/sensor/swiss_public_transport.py +++ b/homeassistant/components/sensor/swiss_public_transport.py @@ -16,7 +16,7 @@ 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 9c48cea0d1c45a..d721e18661fb69 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 0b85de8e4f23dd..517ee6509f76a1 100644 --- a/homeassistant/components/sensor/systemmonitor.py +++ b/homeassistant/components/sensor/systemmonitor.py @@ -10,13 +10,12 @@ 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 @@ def update(self): 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 d721e18661fb69..df78c2b3747cc1 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 89117397a5336a..b73f3a17ee74d9 100644 --- a/homeassistant/components/notify/sendgrid.py +++ b/homeassistant/components/notify/sendgrid.py @@ -14,7 +14,7 @@ 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 df78c2b3747cc1..17eb8455377013 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 8d7fb807c6dbb9..9abd96664f26ad 100644 --- a/homeassistant/components/light/mystrom.py +++ b/homeassistant/components/light/mystrom.py @@ -13,9 +13,9 @@ 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 @@ def effect_list(self): @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 @@ def turn_on(self, **kwargs): 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 @@ def update(self): 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 0a87d41d2fefa1..85fc546d00e591 100644 --- a/homeassistant/components/switch/mystrom.py +++ b/homeassistant/components/switch/mystrom.py @@ -12,7 +12,7 @@ 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 @@ def turn_on(self, **kwargs): 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 @@ def turn_off(self, **kwargs): 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 @@ def update(self): 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 17eb8455377013..7e91ccc59d3821 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 9fec4b4b5e3651..5af81832523f1d 100644 --- a/homeassistant/components/sensor/lastfm.py +++ b/homeassistant/components/sensor/lastfm.py @@ -13,7 +13,7 @@ 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 7e91ccc59d3821..0a90e3b7a64261 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 909f123b52c28b..8354757ff33eed 100644 --- a/homeassistant/components/weather/openweathermap.py +++ b/homeassistant/components/weather/openweathermap.py @@ -11,10 +11,10 @@ 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 @@ _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 @@ 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 @@ def forecast(self): """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 @@ def update(self): 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 @@ def update_forecast(self): 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 74d3c5a0785fd3..8cd47476058f35 100644 --- a/homeassistant/components/media_player/denonavr.py +++ b/homeassistant/components/media_player/denonavr.py @@ -20,7 +20,7 @@ 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 0a90e3b7a64261..a675d08b01c5e8 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 70b66f84ae95f5..5b28b7b0999740 100644 --- a/homeassistant/components/konnected.py +++ b/homeassistant/components/konnected.py @@ -31,6 +31,7 @@ DOMAIN = 'konnected' CONF_ACTIVATION = 'activation' +CONF_API_HOST = 'api_host' STATE_LOW = 'low' STATE_HIGH = 'high' @@ -56,10 +57,12 @@ }), 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 @@ def sync_device_config(self): _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 4af9a767434523..5a78ec8093f28d 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 00000000000000..4b0f9d0cf21ae5 --- /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 a675d08b01c5e8..478a0cb54794cd 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 034ddc2fabbe0a..6680258d95d253 100644 --- a/homeassistant/components/camera/doorbird.py +++ b/homeassistant/components/camera/doorbird.py @@ -17,9 +17,9 @@ 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 @@ @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 48f229b49cad8c..6cd820816e2fef 100644 --- a/homeassistant/components/doorbird.py +++ b/homeassistant/components/doorbird.py @@ -4,14 +4,16 @@ 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 @@ 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()) + + doorstations = [] + + 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)) + + 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 - device = DoorBird(device_ip, username, password) - status = device.ready() + return True - 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 - if config[DOMAIN].get(CONF_DOORBELL_EVENTS): - # Provide an endpoint for the device to call to trigger events - hass.http.register_view(DoorbirdRequestView()) +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) + + 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) - return True + +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 9886b3a586d04a..92ba3640237edb 100644 --- a/homeassistant/components/switch/doorbird.py +++ b/homeassistant/components/switch/doorbird.py @@ -4,10 +4,10 @@ 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 @@ SWITCHES = { "open_door": { - "name": "Open Door", + "name": "{} Open Door", "icon": { True: "lock-open", False: "lock" @@ -23,7 +23,7 @@ "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 @@ "time": datetime.timedelta(seconds=3) }, "light_on": { - "name": "Light On", + "name": "{} Light On", "icon": { True: "lightbulb-on", False: "lightbulb" @@ -48,31 +48,36 @@ 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 aa670578172374..ea4463f5c2347e 100644 --- a/homeassistant/components/mqtt_eventstream.py +++ b/homeassistant/components/mqtt_eventstream.py @@ -116,5 +116,4 @@ def _event_receiver(topic, payload, qos): 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 16a0b80d1fd33d..739572223c61ec 100644 --- a/homeassistant/components/nest.py +++ b/homeassistant/components/nest.py @@ -7,6 +7,7 @@ from concurrent.futures import ThreadPoolExecutor import logging import socket +from datetime import datetime, timedelta import voluptuous as vol @@ -19,7 +20,7 @@ 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 @@ 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 @@ def set_mode(service): 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 88464675c21587..ea7a943881e00a 100644 --- a/homeassistant/components/sensor/nest.py +++ b/homeassistant/components/sensor/nest.py @@ -23,7 +23,7 @@ # 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 19bf19a799a266..6b8bded59b83a8 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 478a0cb54794cd..4b0ad5bc5ef7f7 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 c3753eb53b52b7..b5baf8b078b6ff 100644 --- a/tests/components/media_player/test_samsungtv.py +++ b/tests/components/media_player/test_samsungtv.py @@ -29,7 +29,15 @@ } -class PackageException(Exception): +class AccessDenied(Exception): + """Dummy Exception.""" + + +class ConnectionClosed(Exception): + """Dummy Exception.""" + + +class UnhandledResponse(Exception): """Dummy Exception.""" @@ -45,9 +53,9 @@ def setUp(self, samsung_mock, wol_mock): 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 @@ def test_send_key(self): 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 | 57 ++++++++++--------- homeassistant/components/arlo.py | 38 ++++++++++++- homeassistant/components/camera/arlo.py | 55 +++++++++--------- homeassistant/components/sensor/arlo.py | 50 ++++++++-------- requirements_all.txt | 2 +- 5 files changed, 117 insertions(+), 85 deletions(-) diff --git a/homeassistant/components/alarm_control_panel/arlo.py b/homeassistant/components/alarm_control_panel/arlo.py index 333bde9ee36a75..20887157cb4dff 100644 --- a/homeassistant/components/alarm_control_panel/arlo.py +++ b/homeassistant/components/alarm_control_panel/arlo.py @@ -4,15 +4,17 @@ 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 @@ }) -@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 @@ def icon(self): """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 @@ def state(self): 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 - - @asyncio.coroutine - def async_alarm_disarm(self, code=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 + + 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 @@ def _get_state_from_mode(self, mode): 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 7e51ec8c045e17..206ea4005e6bae 100644 --- a/homeassistant/components/arlo.py +++ b/homeassistant/components/arlo.py @@ -5,14 +5,18 @@ 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 @@ 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 f3e70c2bdd74c9..1a98ade55183ea 100644 --- a/homeassistant/components/camera/arlo.py +++ b/homeassistant/components/camera/arlo.py @@ -4,23 +4,22 @@ 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 @@ } 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 @@ def __init__(self, hass, camera, device_info): 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 + + 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() - @asyncio.coroutine - def handle_async_mjpeg_stream(self, request): + 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 @@ def brand(self): """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 @@ def disable_motion_detection(self): """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 97b7ac2290940d..18029691dc7bae 100644 --- a/homeassistant/components/sensor/arlo.py +++ b/homeassistant/components/sensor/arlo.py @@ -4,17 +4,17 @@ 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 @@ DEPENDENCIES = ['arlo'] -SCAN_INTERVAL = timedelta(seconds=90) - # sensor_type [ description, unit, icon ] SENSOR_TYPES = { 'last_capture': ['Last', None, 'run-fast'], @@ -39,8 +37,7 @@ }) -@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 @@ def name(self): """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 @@ def unit_of_measurement(self): 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 @@ def update(self): 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 4b0ad5bc5ef7f7..1a524c9fd0fcbd 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 739572223c61ec..3ca1c483ee097d 100644 --- a/homeassistant/components/nest.py +++ b/homeassistant/components/nest.py @@ -52,8 +52,8 @@ 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 5a78ec8093f28d..c4aea0e140a37a 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 00000000000000..78ea45525f284c --- /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 1a524c9fd0fcbd..aa346e66ef134e 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 ca7ff17a16ab93..53a95f7924c7aa 100644 --- a/homeassistant/components/media_player/snapcast.py +++ b/homeassistant/components/media_player/snapcast.py @@ -80,8 +80,11 @@ def _handle_service(service): 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 _handle_service(service): 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 @@ def state(self): @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 @@ def async_restore(self): 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 d61b6f50a964d2..303c4846701f87 100644 --- a/homeassistant/components/frontend/__init__.py +++ b/homeassistant/components/frontend/__init__.py @@ -24,7 +24,7 @@ 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 aa346e66ef134e..b6bb426a437a0f 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 207b0a8545f478..732aa85a37a43f 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 ab53c3669cb722..c30745239ea05c 100644 --- a/homeassistant/components/light/tradfri.py +++ b/homeassistant/components/light/tradfri.py @@ -19,12 +19,16 @@ _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 @@ def unique_id(self): @property def supported_features(self): """Flag supported features.""" - return SUPPORTED_FEATURES + return SUPPORTED_GROUP_FEATURES @property def name(self): @@ -225,75 +229,97 @@ def hs_color(self): """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)) + # 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 + + 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)) async def async_turn_on(self, **kwargs): """Instruct the light to turn on.""" - params = {} transition_time = None if ATTR_TRANSITION in kwargs: transition_time = int(kwargs[ATTR_TRANSITION]) * 10 - brightness = kwargs.get(ATTR_BRIGHTNESS) - - if brightness is not None: + 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)) - - if brightness is not None: - params[ATTR_TRANSITION_TIME] = transition_time - await self._api( - self._light_control.set_dimmer(brightness, - **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 + + # 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 @@ def _refresh(self, 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 72d1b4c769f1f6..9ed613abde04ea 100644 --- a/homeassistant/components/tradfri.py +++ b/homeassistant/components/tradfri.py @@ -15,7 +15,7 @@ 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 b6bb426a437a0f..3b1096d36eb41a 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 732aa85a37a43f..9334d630429432 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 b5b636dc8745d1..e770d90266986a 100755 --- a/script/gen_requirements_all.py +++ b/script/gen_requirements_all.py @@ -77,6 +77,7 @@ '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 00000000000000..8ef5d17452a639 --- /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 c4aea0e140a37a..693ca12d10e49f 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 00000000000000..b22234a40940fa --- /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 765f7e1f0f76f5..3c91f19469b2b3 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 3b1096d36eb41a..6ce31803335892 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 38672dbc23b8d9..22fc427ccce969 100644 --- a/homeassistant/components/rainmachine/__init__.py +++ b/homeassistant/components/rainmachine/__init__.py @@ -11,8 +11,8 @@ 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 @@ 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 refresh_sensors(event_time): 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 1e2ec43181cf78..a4682172feee46 100644 --- a/homeassistant/components/cover/myq.py +++ b/homeassistant/components/cover/myq.py @@ -13,7 +13,7 @@ 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 6ce31803335892..c1922f52bdf70d 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 e36e7439e09d63..0883c5a3cc85b0 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 0a4a0bcb5b04c7..e7e854110f1dd1 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 9334d630429432..19796c3bab7af2 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 042943f7a3f273..f3ec360462e191 100644 --- a/homeassistant/components/wink/__init__.py +++ b/homeassistant/components/wink/__init__.py @@ -26,7 +26,7 @@ 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 c1922f52bdf70d..41b7f4036dabb8 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 e033355f655313..d2533eb8f5b478 100644 --- a/homeassistant/components/binary_sensor/mqtt.py +++ b/homeassistant/components/binary_sensor/mqtt.py @@ -6,6 +6,7 @@ """ import asyncio import logging +from typing import Optional import voluptuous as vol @@ -24,7 +25,7 @@ _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 @@ 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 @@ def __init__(self, name, state_topic, availability_topic, device_class, 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 @@ def device_class(self): 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 9b5cf7aa736eeb..71eba2df95039f 100644 --- a/tests/components/binary_sensor/test_mqtt.py +++ b/tests/components/binary_sensor/test_mqtt.py @@ -77,6 +77,25 @@ def test_invalid_device_class(self): 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 | 111 ++++++++++++++------------ requirements_all.txt | 3 + 2 files changed, 63 insertions(+), 51 deletions(-) diff --git a/homeassistant/components/camera/yi.py b/homeassistant/components/camera/yi.py index 41fe816c4799c6..868c5afb4473c5 100644 --- a/homeassistant/components/camera/yi.py +++ b/homeassistant/components/camera/yi.py @@ -11,11 +11,13 @@ 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 @@ }) -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 @@ def __init__(self, hass, config): """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): - """Retrieve the latest video file from the customized Yi FTP server.""" - from ftplib import FTP, error_perm + @property + def name(self): + """Return the name of this camera.""" + return self._name - ftp = FTP(self.host) + async def _connect_to_client(self): + """Attempt to establish a connection via FTP.""" + from aioftp import Client, StatusCodeError + + ftp = Client() 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 + 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 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 - - 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 = 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]) + 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) + + 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 + + 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 41b7f4036dabb8..c180c3a055d026 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 | 16 +- .../components/nest/.translations/en.json | 33 ++++ .../components/{nest.py => nest/__init__.py} | 136 +++++--------- 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 | 59 +++--- 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, 671 insertions(+), 158 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 693ca12d10e49f..fa2ec6e9f2724c 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 882ff142e8c147..9da352e1268bff 100644 --- a/homeassistant/components/binary_sensor/nest.py +++ b/homeassistant/components/binary_sensor/nest.py @@ -8,7 +8,8 @@ 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 @@ 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 60f8979bb16d91..f2f4081fb6dc2b 100644 --- a/homeassistant/components/camera/__init__.py +++ b/homeassistant/components/camera/__init__.py @@ -216,6 +216,16 @@ def _write_image(to_file, image_data): 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 6ffb7ef85619f8..ab26df5caf00da 100644 --- a/homeassistant/components/camera/nest.py +++ b/homeassistant/components/camera/nest.py @@ -23,14 +23,19 @@ 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 ebe7cbbf2c1c4a..a47edc5af42632 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_on_off_service(service): 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 696f1479c08861..dc1f74613bcb46 100644 --- a/homeassistant/components/climate/nest.py +++ b/homeassistant/components/climate/nest.py @@ -32,16 +32,22 @@ 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 + thermostats = await hass.async_add_job(hass.data[DATA_NEST].thermostats) + all_devices = [NestThermostat(structure, device, temp_unit) - for structure, device in hass.data[DATA_NEST].thermostats()] + for structure, device in thermostats] - add_devices(all_devices, True) + 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 00000000000000..cf448bb35e7273 --- /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 3ca1c483ee097d..19d65061a896f2 100644 --- a/homeassistant/components/nest.py +++ b/homeassistant/components/nest/__init__.py @@ -6,6 +6,7 @@ """ from concurrent.futures import ThreadPoolExecutor import logging +import os.path import socket from datetime import datetime, timedelta @@ -15,19 +16,22 @@ 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) - - _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': ''}] - ) - - -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) - - 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 + conf = config[DOMAIN] + + 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 - if 'nest' in _CONFIGURING: - _LOGGER.debug("configuration done") - configurator = hass.components.configurator - configurator.async_request_done(_CONFIGURING.pop('nest')) + +async def async_setup_entry(hass, entry): + """Setup Nest from a config entry.""" + from nest import 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 @@ def shut_down(event): 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 @@ def __init__(self, hass, conf, nest): """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 00000000000000..ee83598235cdb8 --- /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 00000000000000..835918f6a048fd --- /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 00000000000000..5ab10cc2a5e25c --- /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 00000000000000..5a70e3fd48d6c4 --- /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 ea7a943881e00a..bf1b3f65c4a9fc 100644 --- a/homeassistant/components/sensor/nest.py +++ b/homeassistant/components/sensor/nest.py @@ -6,7 +6,8 @@ """ 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 @@ 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] - - 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] - - add_devices(all_sensors, True) + 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.smoke_co_alarms(): + all_sensors += [NestBasicSensor(structure, device, variable) + for variable in conditions + if variable in PROTECT_SENSOR_TYPES] + + 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 8a73e424fb5133..7826e26b960c0a 100644 --- a/homeassistant/config_entries.py +++ b/homeassistant/config_entries.py @@ -129,6 +129,7 @@ async def async_step_discovery(info): FLOWS = [ 'deconz', 'hue', + 'nest', 'zone', ] diff --git a/homeassistant/data_entry_flow.py b/homeassistant/data_entry_flow.py index 5095297e79543d..3b0f264fd407ef 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 @@ def async_show_form(self, *, step_id, data_schema=None, errors=None): '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 19796c3bab7af2..af4f8feb753149 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 e770d90266986a..7bf87c74de7266 100755 --- a/script/gen_requirements_all.py +++ b/script/gen_requirements_all.py @@ -77,6 +77,7 @@ '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 84d15578e13d99..82c747da01c750 100644 --- a/tests/components/config/test_config_entries.py +++ b/tests/components/config/test_config_entries.py @@ -110,6 +110,9 @@ def async_step_init(self, user_input=None): 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 async_step_init(self, user_input=None): 'type': 'string' } ], + 'description_placeholders': { + 'url': 'https://example.com', + }, 'errors': { 'username': 'Should be unique.' } @@ -242,6 +248,7 @@ def async_step_account(self, user_input=None): '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 00000000000000..313cfccc76169d --- /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 00000000000000..9692d5ce129d95 --- /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 00000000000000..44a5299b33dbd3 --- /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 81b43c1f8e0ce1..f556b62e935425 100644 --- a/homeassistant/components/image_processing/facebox.py +++ b/homeassistant/components/image_processing/facebox.py @@ -10,16 +10,22 @@ 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 @@ 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 @@ def process_image(self, image): 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 cdc19a3d8d1bfa..9449ebf5f71de7 100644 --- a/tests/components/image_processing/test_facebox.py +++ b/tests/components/image_processing/test_facebox.py @@ -7,7 +7,7 @@ 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 @@ 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 @@ "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 @@ 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 @@ def mock_face_event(event): 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 fa2ec6e9f2724c..38c88c4748cecc 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 0da8353e5aa84d..556791b879c64e 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 00000000000000..2440dac3204e6f --- /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 c180c3a055d026..dabecdacb2f506 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 00000000000000..1ee314d9eee094 --- /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 | 344 +++++++++---------- requirements_all.txt | 2 +- 2 files changed, 170 insertions(+), 176 deletions(-) diff --git a/homeassistant/components/sensor/airvisual.py b/homeassistant/components/sensor/airvisual.py index b4007c8d7440fb..0002274833ff20 100644 --- a/homeassistant/components/sensor/airvisual.py +++ b/homeassistant/components/sensor/airvisual.py @@ -9,16 +9,16 @@ 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 @@ 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 - )) - - add_devices(sensors, True) - - -class AirVisualBaseSensor(Entity): - """Define a base class for all of our sensors.""" - - def __init__(self, data, name, icon, locale, entity_id): - """Initialize the sensor.""" - self.data = data - self._attrs = {} + 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)) + + async_add_devices(sensors, True) + + +class AirVisualSensor(Entity): + """Define an AirVisual sensor.""" + + 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 @@ def state(self): """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' - - def update(self): - """Update the status of the sensor.""" - self.data.update() - - self._state = self.data.pollution_info.get( - 'aqi{0}'.format(self._locale)) - + return self._unit -class MainPollutantSensor(AirVisualBaseSensor): - """Define a sensor to the main pollutant of an area.""" + async def async_update(self): + """Update the sensor.""" + await self.airvisual.async_update() + data = self.airvisual.pollution_info - 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 + if not data: + return - @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) + + self.async_update = Throttle( + kwargs[CONF_SCAN_INTERVAL])(self._async_update) - @Throttle(MIN_TIME_BETWEEN_UPDATES) - def update(self): - """Update with new AirVisual data.""" - from pyairvisual.exceptions import HTTPError + 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.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) + 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) + + _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 dabecdacb2f506..8a075631792f5c 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 38c88c4748cecc..5a8f26e34daef4 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 00000000000000..7ff19871ee7bd5 --- /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 8a075631792f5c..f144f3e2a2b201 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 e7e854110f1dd1..7ee0e166cf2839 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 af4f8feb753149..82adbcc0733949 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 c20b297017c027..336d19664b42ff 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 @@ def test_feed(self): 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 9f114cf2c56a33..419ca7c13fb175 100644 --- a/homeassistant/components/sensor/simulated.py +++ b/homeassistant/components/sensor/simulated.py @@ -7,6 +7,7 @@ import logging import math from random import Random +from datetime import datetime import voluptuous as vol @@ -25,6 +26,7 @@ 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_PHASE = 0 DEFAULT_SEED = 999 DEFAULT_UNIT = 'value' +DEFAULT_RELATIVE_TO_EPOCH = True ICON = 'mdi:chart-line' @@ -46,6 +49,8 @@ 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 @@ def __init__(self, name, unit, amp, mean, period, phase, fwhm, seed): 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 @@ def device_state_attributes(self): '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 d226c79cff507a..50552baa33e24c 100644 --- a/tests/components/sensor/test_simulated.py +++ b/tests/components/sensor/test_simulated.py @@ -5,8 +5,8 @@ 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 @@ def test_default_config(self): 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 5a8f26e34daef4..e7d6d2a404a0b1 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 00000000000000..55d79a7d560a9b --- /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 00000000000000..a4ee25f0915c91 --- /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 00000000000000..7f480de0e8bea3 --- /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 00d4291539b936..d7041865892aa5 100644 --- a/homeassistant/components/discovery.py +++ b/homeassistant/components/discovery.py @@ -46,7 +46,9 @@ CONFIG_ENTRY_HANDLERS = { SERVICE_DECONZ: 'deconz', + 'google_cast': 'cast', SERVICE_HUE: 'hue', + 'sonos': 'sonos', } SERVICE_HANDLERS = { @@ -64,11 +66,9 @@ 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 7452b7dd186983..d963deba7b55e7 100644 --- a/homeassistant/components/media_player/__init__.py +++ b/homeassistant/components/media_player/__init__.py @@ -456,6 +456,16 @@ async def async_service_handler(service): 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 a9bea9e4c1d137..eced0dbbe25bb2 100644 --- a/homeassistant/components/media_player/cast.py +++ b/homeassistant/components/media_player/cast.py @@ -17,6 +17,7 @@ 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 @@ 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 0f536e1edfb5ab..da0ad24b135591 100644 --- a/homeassistant/components/media_player/sonos.py +++ b/homeassistant/components/media_player/sonos.py @@ -20,13 +20,14 @@ 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_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 @@ def __init__(self): 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 00000000000000..c7aae4302f6bea --- /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 00000000000000..7c3de210768152 --- /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 00000000000000..4aa68712d599e7 --- /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 7826e26b960c0a..504c0850a939f6 100644 --- a/homeassistant/config_entries.py +++ b/homeassistant/config_entries.py @@ -127,9 +127,11 @@ async def async_step_discovery(info): 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 00000000000000..2a4ec2966df967 --- /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 f144f3e2a2b201..79db358942d5b3 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 82adbcc0733949..34928b4f111b07 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 00000000000000..7e904dce00af63 --- /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 00000000000000..260856c6742e2c --- /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 00000000000000..878e0c17318946 --- /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 00000000000000..2cbc2360fd44d9 --- /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 00000000000000..d3f13ac43021bc --- /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 c82ae2a46f0831..4ac3a147296f5b 100644 --- a/homeassistant/helpers/entity_component.py +++ b/homeassistant/helpers/entity_component.py @@ -108,7 +108,8 @@ async def async_setup_entry(self, config_entry): 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 f53d1c2be2ba51..556935a6ac173a 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 504f31cc9875c8..b4910723c8dfa8 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 5198381b9767a3..f5e1d581891a4d 100644 --- a/homeassistant/components/calendar/__init__.py +++ b/homeassistant/components/calendar/__init__.py @@ -9,6 +9,8 @@ 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_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 @@ def async_setup(hass, config): 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 @@ def update(self): 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 @@ def _get_date(date): # 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 6f92891c551d76..9c30d1481f8af9 100644 --- a/homeassistant/components/calendar/caldav.py +++ b/homeassistant/components/calendar/caldav.py @@ -11,7 +11,7 @@ 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 @@ def device_state_attributes(self): 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 @@ def __init__(self, calendar, include_all_day, search): 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 7823f03c85ecf4..5ddd9fe8e3d939 100644 --- a/homeassistant/components/calendar/demo.py +++ b/homeassistant/components/calendar/demo.py @@ -4,8 +4,10 @@ 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 @@ def __init__(self, hass, calendar_data, data): """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 6c26c65ebe77fd..da76530a36d634 100644 --- a/homeassistant/components/calendar/google.py +++ b/homeassistant/components/calendar/google.py @@ -51,6 +51,10 @@ def __init__(self, hass, calendar_service, 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) + class GoogleCalendarData(object): """Class to utilize calendar service object to get next event.""" @@ -64,9 +68,7 @@ def __init__(self, calendar_service, calendar_id, search, 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 @@ def update(self): 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 b70e44456db822..71a6a17de107a8 100644 --- a/homeassistant/components/calendar/todoist.py +++ b/homeassistant/components/calendar/todoist.py @@ -257,6 +257,10 @@ def cleanup(self): 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 @@ def select_best_task(project_tasks): 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 11dd0cb963535d..c5dadbc56eaca2 100644 --- a/tests/components/calendar/test_caldav.py +++ b/tests/components/calendar/test_caldav.py @@ -19,7 +19,7 @@ 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 @@ def test_ongoing_event(self, mock_now): "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 @@ def test_just_ended_event(self, mock_now): "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 @@ def test_ongoing_event_different_tz(self, mock_now): "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 @@ def test_ongoing_event_with_offset(self, mock_now): "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 @@ def test_matching_filter(self, mock_now): "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 @@ def test_matching_filter_real_regexp(self, mock_now): "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 @@ def test_all_day_event_returned(self, mock_now): "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 00000000000000..50ac63121b1290 --- /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 9f94ea9f44c370..d176cd758b43a9 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 @@ def test_all_day_event(self, mock_next_event): '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 @@ def test_future_event(self, mock_next_event): (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 @@ def test_in_progress_event(self, mock_next_event): (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 @@ def test_offset_in_progress_event(self, mock_next_event): (middle_of_event + dt_util.dt.timedelta(minutes=60)) .strftime(DATE_STR_FORMAT), 'location': '', - 'description': '' + 'description': '', }) @pytest.mark.skip @@ -352,7 +353,7 @@ def test_all_day_offset_in_progress_event(self, mock_next_event): '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 @@ def test_all_day_offset_event(self, mock_next_event): '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 f5e1d581891a4d..65e5e33c7c1f68 100644 --- a/homeassistant/components/calendar/__init__.py +++ b/homeassistant/components/calendar/__init__.py @@ -4,7 +4,6 @@ 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 @@ 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 @@ def update(self): 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 @@ def __init__(self, component): 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 @@ async def get(self, request, entity_id): 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 5ddd9fe8e3d939..53129d3316cf60 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 50ac63121b1290..09c6a06a54ec0c 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 164c3f57f52495..a5f6a751b46f2e 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 303c4846701f87..0c425ccd3b1e8d 100644 --- a/homeassistant/components/frontend/__init__.py +++ b/homeassistant/components/frontend/__init__.py @@ -24,7 +24,7 @@ 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 79db358942d5b3..90cd28d61fc105 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 34928b4f111b07..02f079dd9a69c3 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 e7d6d2a404a0b1..d059d62b5f31a9 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 00000000000000..c18a3649e7bbaa --- /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 734f8e03375e5f..44cad11e3f0979 100644 --- a/homeassistant/components/binary_sensor/skybell.py +++ b/homeassistant/components/binary_sensor/skybell.py @@ -94,4 +94,4 @@ def update(self): 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 a2f90e49e3ad36..465c6c1e0e86d1 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 cb7c3aad7fdc6a..27fb6987f8c24c 100644 --- a/homeassistant/components/deconz/config_flow.py +++ b/homeassistant/components/deconz/config_flow.py @@ -8,7 +8,9 @@ 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 @@ async def async_step_options(self, user_input=None): """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 @@ async def async_step_options(self, user_input=None): 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 @@ async def async_step_import(self, import_config): 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 43f3c6441da0de..f7aa4c7a43057d 100644 --- a/homeassistant/components/deconz/const.py +++ b/homeassistant/components/deconz/const.py @@ -10,3 +10,4 @@ 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 cabe58694d2a76..09549a300a0d3f 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 916e60c00b1b9c..a4593a72617bbf 100644 --- a/homeassistant/components/light/deconz.py +++ b/homeassistant/components/light/deconz.py @@ -6,6 +6,7 @@ """ 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 @@ def async_add_light(lights): 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 @@ def async_add_light(lights): 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 df3310f3d6f233..111cfbe969763e 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 2608d77ce2ae68..d7d609f820eb5f 100644 --- a/tests/components/light/test_deconz.py +++ b/tests/components/light/test_deconz.py @@ -38,7 +38,7 @@ } -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 251d8cba095bec..dbd86ef31f344d 100644 --- a/homeassistant/components/hue/__init__.py +++ b/homeassistant/components/hue/__init__.py @@ -9,6 +9,7 @@ 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 19d65061a896f2..bd74897371ad05 100644 --- a/homeassistant/components/nest/__init__.py +++ b/homeassistant/components/nest/__init__.py @@ -6,7 +6,6 @@ """ 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 ee83598235cdb8..b5c095f34b8065 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 @@ async def async_step_import(self, info): 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 504c0850a939f6..4fbbbb77b794b4 100644 --- a/homeassistant/config_entries.py +++ b/homeassistant/config_entries.py @@ -146,7 +146,10 @@ async def async_step_discovery(info): 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 3b0f264fd407ef..e51ba4d97186f1 100644 --- a/homeassistant/data_entry_flow.py +++ b/homeassistant/data_entry_flow.py @@ -9,6 +9,7 @@ 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 9692d5ce129d95..e80d18a98626e7 100644 --- a/tests/components/nest/test_config_flow.py +++ b/tests/components/nest/test_config_flow.py @@ -3,7 +3,8 @@ 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 a6d4476f047e20..40ca491e1f3cc0 100644 --- a/homeassistant/components/binary_sensor/eight_sleep.py +++ b/homeassistant/components/binary_sensor/eight_sleep.py @@ -5,7 +5,6 @@ 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 @@ 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 @@ def is_on(self): """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 3478d5cd08e7e4..704eab1846bb73 100644 --- a/homeassistant/components/eight_sleep.py +++ b/homeassistant/components/eight_sleep.py @@ -4,7 +4,6 @@ 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.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 @@ }, 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_update_user_data(now): 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_service_handler(service): 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_service_handler(service): 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 @@ def __init__(self, eight): """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 @@ def __init__(self, eight): """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 e0a42fdb6a8dea..fd7c1aee3ae67f 100644 --- a/homeassistant/components/sensor/eight_sleep.py +++ b/homeassistant/components/sensor/eight_sleep.py @@ -5,7 +5,6 @@ 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_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 @@ def unit_of_measurement(self): """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 @@ def device_state_attributes(self): 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 @@ def icon(self): 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 @@ def async_update(self): 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 @@ def device_state_attributes(self): 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 @@ def state(self): """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 90cd28d61fc105..921bbf8fd46bfd 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 80ee4c29fbe880..59737c578a5915 100644 --- a/homeassistant/components/weather/ecobee.py +++ b/homeassistant/components/weather/ecobee.py @@ -4,6 +4,7 @@ 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 @@ def forecast(self): 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 9906c61f2694cf..71894364f91440 100644 --- a/homeassistant/components/axis.py +++ b/homeassistant/components/axis.py @@ -145,7 +145,7 @@ def configuration_callback(callback_data): 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 2289ad5d9064ec..480786b2c2c8a7 100644 --- a/homeassistant/components/binary_sensor/command_line.py +++ b/homeassistant/components/binary_sensor/command_line.py @@ -35,7 +35,6 @@ }) -# 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 c17e6b50911401..767be2874e6ab8 100644 --- a/homeassistant/components/binary_sensor/gc100.py +++ b/homeassistant/components/binary_sensor/gc100.py @@ -23,7 +23,6 @@ }) -# 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 09f1739cba7804..a80e4db747d505 100644 --- a/homeassistant/components/binary_sensor/isy994.py +++ b/homeassistant/components/binary_sensor/isy994.py @@ -28,7 +28,6 @@ } -# 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 @@ def _restart_timer(self): # 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 @@ def timer_elapsed(now) -> None: 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 834186b8b185e1..e6b28047cb8f2f 100644 --- a/homeassistant/components/binary_sensor/knx.py +++ b/homeassistant/components/binary_sensor/knx.py @@ -115,7 +115,6 @@ def async_register_callbacks(self): """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 10fc2ccc3ff542..7c3a3e1dd306b9 100644 --- a/homeassistant/components/binary_sensor/netatmo.py +++ b/homeassistant/components/binary_sensor/netatmo.py @@ -57,7 +57,6 @@ }) -# 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 265fcec66fa9c7..1a1967b9014a0b 100644 --- a/homeassistant/components/binary_sensor/octoprint.py +++ b/homeassistant/components/binary_sensor/octoprint.py @@ -33,7 +33,6 @@ }) -# 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 d2c46c795a8530..69dc3b834855c1 100644 --- a/homeassistant/components/binary_sensor/pilight.py +++ b/homeassistant/components/binary_sensor/pilight.py @@ -44,7 +44,6 @@ }) -# 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 9d489a59711a3f..9ab56a5a20da75 100644 --- a/homeassistant/components/binary_sensor/raspihats.py +++ b/homeassistant/components/binary_sensor/raspihats.py @@ -42,7 +42,6 @@ }) -# 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 2322b1bf49845b..e1e06ce57b9612 100644 --- a/homeassistant/components/binary_sensor/rpi_gpio.py +++ b/homeassistant/components/binary_sensor/rpi_gpio.py @@ -39,7 +39,6 @@ }) -# 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 5405a6a77ba57d..dcdd312ce814f7 100644 --- a/homeassistant/components/binary_sensor/trend.py +++ b/homeassistant/components/binary_sensor/trend.py @@ -57,7 +57,6 @@ }) -# 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 30a7e291401bc0..d3c78597c70bc7 100644 --- a/homeassistant/components/binary_sensor/wemo.py +++ b/homeassistant/components/binary_sensor/wemo.py @@ -13,7 +13,7 @@ _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 f04e0af7be9aba..bc9d3acf54fe60 100644 --- a/homeassistant/components/bloomsky.py +++ b/homeassistant/components/bloomsky.py @@ -34,7 +34,6 @@ }, 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 65e5e33c7c1f68..9716e46bc032af 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 f2f4081fb6dc2b..c41020c3faf112 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 ef70692215dfbd..775289926745e6 100644 --- a/homeassistant/components/camera/bloomsky.py +++ b/homeassistant/components/camera/bloomsky.py @@ -13,7 +13,6 @@ 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 15db83d345a93e..4ea733139a90b6 100644 --- a/homeassistant/components/camera/foscam.py +++ b/homeassistant/components/camera/foscam.py @@ -33,7 +33,6 @@ }) -# 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 e11bd599e45e70..911c14e72325b0 100644 --- a/homeassistant/components/camera/generic.py +++ b/homeassistant/components/camera/generic.py @@ -46,7 +46,6 @@ @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 35d30104f6e66d..a5ed0cdc02c492 100644 --- a/homeassistant/components/camera/mjpeg.py +++ b/homeassistant/components/camera/mjpeg.py @@ -42,7 +42,6 @@ @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 5b8effd5dcc0c5..34a78e19f9fce1 100644 --- a/homeassistant/components/camera/netatmo.py +++ b/homeassistant/components/camera/netatmo.py @@ -29,7 +29,6 @@ }) -# 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 a98e3ef066fbee..90ef08c24feba2 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 5ce6cc2fa7af0a..f53cf2491dc5c5 100644 --- a/homeassistant/components/climate/knx.py +++ b/homeassistant/components/climate/knx.py @@ -136,7 +136,6 @@ def async_register_callbacks(self): """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 c67e032c14947d..12a6960f8334b7 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 82ca60e84e6c91..743a36d41d5084 100644 --- a/homeassistant/components/cover/isy994.py +++ b/homeassistant/components/cover/isy994.py @@ -25,7 +25,6 @@ } -# 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 83668924268e0d..7bb20e4cf1f2cf 100644 --- a/homeassistant/components/cover/knx.py +++ b/homeassistant/components/cover/knx.py @@ -107,7 +107,6 @@ def async_register_callbacks(self): """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 @@ def stop_auto_updater(self): @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 4e38681a310f3c..599bdb1cebab7f 100644 --- a/homeassistant/components/cover/lutron.py +++ b/homeassistant/components/cover/lutron.py @@ -17,7 +17,6 @@ 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 6ad9b093ed84ae..1ed502e0f7f8af 100644 --- a/homeassistant/components/cover/lutron_caseta.py +++ b/homeassistant/components/cover/lutron_caseta.py @@ -18,7 +18,6 @@ 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 49666139330c5c..384f96f3f52339 100644 --- a/homeassistant/components/cover/rpi_gpio.py +++ b/homeassistant/components/cover/rpi_gpio.py @@ -54,7 +54,6 @@ }) -# 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 781e486a40e83f..72d9992c60f3e2 100644 --- a/homeassistant/components/device_tracker/actiontec.py +++ b/homeassistant/components/device_tracker/actiontec.py @@ -31,7 +31,6 @@ }) -# 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 79d8806fe22bb1..92ef78f60f3b0b 100644 --- a/homeassistant/components/device_tracker/aruba.py +++ b/homeassistant/components/device_tracker/aruba.py @@ -30,7 +30,6 @@ }) -# 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 7e9b10e9241aa1..5cb7e283c99721 100644 --- a/homeassistant/components/device_tracker/asuswrt.py +++ b/homeassistant/components/device_tracker/asuswrt.py @@ -78,7 +78,6 @@ 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 a3b5bcac77c824..707850d2215c1e 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 @@ }) -# 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 3d36a1b428c024..3e17fdd332948b 100644 --- a/homeassistant/components/device_tracker/ddwrt.py +++ b/homeassistant/components/device_tracker/ddwrt.py @@ -27,7 +27,6 @@ }) -# 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 775075b8a4aae9..804269e62280c5 100644 --- a/homeassistant/components/device_tracker/huawei_router.py +++ b/homeassistant/components/device_tracker/huawei_router.py @@ -26,7 +26,6 @@ }) -# 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 c48c9bd029b94e..0c289ce9a82e55 100644 --- a/homeassistant/components/device_tracker/sky_hub.py +++ b/homeassistant/components/device_tracker/sky_hub.py @@ -23,7 +23,6 @@ }) -# 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 c9c27fb2bfa848..3d57cb108e243c 100644 --- a/homeassistant/components/device_tracker/snmp.py +++ b/homeassistant/components/device_tracker/snmp.py @@ -34,7 +34,6 @@ }) -# 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 3fa161e467dec4..8a56fcee7024b5 100644 --- a/homeassistant/components/device_tracker/thomson.py +++ b/homeassistant/components/device_tracker/thomson.py @@ -33,7 +33,6 @@ }) -# 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 168ab04ec6f14a..c3c4a48bb826ff 100644 --- a/homeassistant/components/device_tracker/unifi_direct.py +++ b/homeassistant/components/device_tracker/unifi_direct.py @@ -33,7 +33,6 @@ }) -# 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 9c29cea704c325..22348dcc297abb 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 b328ebb310174b..c03c492c834a11 100644 --- a/homeassistant/components/fan/demo.py +++ b/homeassistant/components/fan/demo.py @@ -13,7 +13,6 @@ 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 847ca3b325b2bc..97a5f9c3bd69d2 100644 --- a/homeassistant/components/fan/isy994.py +++ b/homeassistant/components/fan/isy994.py @@ -30,7 +30,6 @@ 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 2f00de08005813..1616d38881626d 100644 --- a/homeassistant/components/fan/xiaomi_miio.py +++ b/homeassistant/components/fan/xiaomi_miio.py @@ -314,7 +314,6 @@ } -# 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 fe3c934659b927..249f147847c08b 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 ecabcd36a85391..90ab41cf98b7e0 100644 --- a/homeassistant/components/isy994.py +++ b/homeassistant/components/isy994.py @@ -425,7 +425,6 @@ def async_added_to_hass(self) -> None: 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 18a6b4ae266d99..bca587074b01c4 100644 --- a/homeassistant/components/light/blinksticklight.py +++ b/homeassistant/components/light/blinksticklight.py @@ -31,7 +31,6 @@ }) -# 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 d2ed865892e6f5..ce358d0a974e5e 100644 --- a/homeassistant/components/light/isy994.py +++ b/homeassistant/components/light/isy994.py @@ -15,7 +15,6 @@ _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 18446951735bf2..8fa2b56d1d2d1e 100644 --- a/homeassistant/components/light/knx.py +++ b/homeassistant/components/light/knx.py @@ -88,7 +88,6 @@ def async_register_callbacks(self): """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 490eeb6ecaba17..182d7536dc4544 100644 --- a/homeassistant/components/light/lifx_legacy.py +++ b/homeassistant/components/light/lifx_legacy.py @@ -45,7 +45,6 @@ }) -# 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 @@ def on_power(self, ipaddr, power): 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 34d6cba7cb8b09..24744110c6fd98 100644 --- a/homeassistant/components/light/lutron.py +++ b/homeassistant/components/light/lutron.py @@ -16,7 +16,6 @@ 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 e4e1baf6c582d2..09f0a337cc343a 100644 --- a/homeassistant/components/light/lutron_caseta.py +++ b/homeassistant/components/light/lutron_caseta.py @@ -19,7 +19,6 @@ 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 1bf7d632af5fc1..44e5e40b3b79cc 100644 --- a/homeassistant/components/light/tellstick.py +++ b/homeassistant/components/light/tellstick.py @@ -15,7 +15,6 @@ 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 2079638f7f1046..c21da57ea96f1a 100644 --- a/homeassistant/components/light/tikteck.py +++ b/homeassistant/components/light/tikteck.py @@ -31,7 +31,6 @@ }) -# 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 7ace250b6eeee5..e62ffaecdff92f 100644 --- a/homeassistant/components/light/vera.py +++ b/homeassistant/components/light/vera.py @@ -18,7 +18,6 @@ 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 cba15f6df9f7ec..fbb8dd66f013d8 100644 --- a/homeassistant/components/light/xiaomi_miio.py +++ b/homeassistant/components/light/xiaomi_miio.py @@ -100,7 +100,6 @@ } -# 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 3c77f2d8449cac..35d2bf2388cd3b 100644 --- a/homeassistant/components/light/zengge.py +++ b/homeassistant/components/light/zengge.py @@ -30,7 +30,6 @@ }) -# 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 d561dd333ab315..8da53a9ef11fba 100644 --- a/homeassistant/components/lock/demo.py +++ b/homeassistant/components/lock/demo.py @@ -8,7 +8,6 @@ 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 50371fdc9ae8fb..79e4308dbda114 100644 --- a/homeassistant/components/lock/isy994.py +++ b/homeassistant/components/lock/isy994.py @@ -21,7 +21,6 @@ } -# 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 ea79848f60ce11..6bf445ba477529 100644 --- a/homeassistant/components/lock/lockitron.py +++ b/homeassistant/components/lock/lockitron.py @@ -26,7 +26,6 @@ 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 04030c92425774..f67243415c50f8 100644 --- a/homeassistant/components/lock/nello.py +++ b/homeassistant/components/lock/nello.py @@ -27,7 +27,6 @@ }) -# 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 4fe05279919a60..536c8f2abeb794 100644 --- a/homeassistant/components/lock/nuki.py +++ b/homeassistant/components/lock/nuki.py @@ -50,7 +50,6 @@ }) -# 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 5bc404354860f7..09f7266d15c6a1 100644 --- a/homeassistant/components/lock/sesame.py +++ b/homeassistant/components/lock/sesame.py @@ -24,7 +24,6 @@ }) -# 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 ab1d2fabefe1c5..b6e7383b138251 100644 --- a/homeassistant/components/lock/volvooncall.py +++ b/homeassistant/components/lock/volvooncall.py @@ -12,7 +12,6 @@ _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 bcfae533abfc9b..e2d02acc61c019 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 6933286f0fe584..93daf5b2f893d6 100644 --- a/homeassistant/components/media_player/aquostv.py +++ b/homeassistant/components/media_player/aquostv.py @@ -59,7 +59,6 @@ 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 wrapper(obj, *args, **kwargs): 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 1c976f5eecd32b..3d8e1fde687f74 100644 --- a/homeassistant/components/media_player/blackbird.py +++ b/homeassistant/components/media_player/blackbird.py @@ -61,7 +61,6 @@ })) -# 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 f0cc93a8b0f3f2..727bda3be3f181 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 6b41ace6ce21b5..41713e0c5bc6f6 100644 --- a/homeassistant/components/media_player/channels.py +++ b/homeassistant/components/media_player/channels.py @@ -105,7 +105,6 @@ def service_handler(service): 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 6847b87e54f7c7..1ee18576ab8c85 100644 --- a/homeassistant/components/media_player/clementine.py +++ b/homeassistant/components/media_player/clementine.py @@ -43,7 +43,6 @@ }) -# 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 2c74feae847809..405c220c8770a3 100644 --- a/homeassistant/components/media_player/demo.py +++ b/homeassistant/components/media_player/demo.py @@ -14,7 +14,6 @@ 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 efa5e7e607983d..ed20ac25cf90eb 100644 --- a/homeassistant/components/media_player/dunehd.py +++ b/homeassistant/components/media_player/dunehd.py @@ -32,7 +32,6 @@ 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 9d66ae77eeff06..157db2c44d3a35 100644 --- a/homeassistant/components/media_player/firetv.py +++ b/homeassistant/components/media_player/firetv.py @@ -43,7 +43,6 @@ }) -# 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 6d95ea675fb812..ab594f47e14d62 100644 --- a/homeassistant/components/media_player/frontier_silicon.py +++ b/homeassistant/components/media_player/frontier_silicon.py @@ -41,7 +41,6 @@ }) -# 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 2f116abebc3311..4a0ec1fa87f4aa 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 064ca68ea9561c..91cd8d19cc4a59 100644 --- a/homeassistant/components/media_player/gstreamer.py +++ b/homeassistant/components/media_player/gstreamer.py @@ -34,7 +34,6 @@ }) -# 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 edbd6546cca9f5..8c98844cf9358a 100644 --- a/homeassistant/components/media_player/lg_netcast.py +++ b/homeassistant/components/media_player/lg_netcast.py @@ -43,7 +43,6 @@ }) -# 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 44d19ac6860391..a951356500f082 100644 --- a/homeassistant/components/media_player/monoprice.py +++ b/homeassistant/components/media_player/monoprice.py @@ -55,7 +55,6 @@ }) -# 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 a375a585ad40db..ad8dd0bf0564f0 100644 --- a/homeassistant/components/media_player/mpchc.py +++ b/homeassistant/components/media_player/mpchc.py @@ -35,7 +35,6 @@ }) -# 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 04dd1ac5f2e1ef..73417e5f25d7f0 100644 --- a/homeassistant/components/media_player/mpd.py +++ b/homeassistant/components/media_player/mpd.py @@ -46,7 +46,6 @@ }) -# 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 5e30f9783c7582..5d9c7bd14c578a 100644 --- a/homeassistant/components/media_player/openhome.py +++ b/homeassistant/components/media_player/openhome.py @@ -25,7 +25,6 @@ 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 db60de922d998f..549071fde8e5d4 100644 --- a/homeassistant/components/media_player/panasonic_viera.py +++ b/homeassistant/components/media_player/panasonic_viera.py @@ -42,7 +42,6 @@ }) -# 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 d66811eed661f1..a47db7f633c4a6 100644 --- a/homeassistant/components/media_player/pandora.py +++ b/homeassistant/components/media_player/pandora.py @@ -43,7 +43,6 @@ 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 01d63e0b6c845d..be0c0527f1bc9f 100644 --- a/homeassistant/components/media_player/philips_js.py +++ b/homeassistant/components/media_player/philips_js.py @@ -48,7 +48,6 @@ }) -# 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 43e9abd96a668e..15a2b41795e8c7 100644 --- a/homeassistant/components/media_player/samsungtv.py +++ b/homeassistant/components/media_player/samsungtv.py @@ -47,7 +47,6 @@ }) -# 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 53a95f7924c7aa..a880d3c920d150 100644 --- a/homeassistant/components/media_player/snapcast.py +++ b/homeassistant/components/media_player/snapcast.py @@ -46,7 +46,6 @@ }) -# 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 abd8252d813c44..45e1a91c510fda 100644 --- a/homeassistant/components/media_player/vlc.py +++ b/homeassistant/components/media_player/vlc.py @@ -34,7 +34,6 @@ }) -# 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 c3426e454048f5..42d0ae85ab3c8e 100644 --- a/homeassistant/components/media_player/webostv.py +++ b/homeassistant/components/media_player/webostv.py @@ -61,7 +61,6 @@ }) -# 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 a928c0d3aca031..fe46c858b5119f 100644 --- a/homeassistant/components/modbus.py +++ b/homeassistant/components/modbus.py @@ -75,11 +75,11 @@ 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 b20abb52efc5c3..fa747ccba88dee 100644 --- a/homeassistant/components/notify/message_bird.py +++ b/homeassistant/components/notify/message_bird.py @@ -24,7 +24,6 @@ }) -# 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 1374779c5f0417..db568514dea25a 100644 --- a/homeassistant/components/notify/mysensors.py +++ b/homeassistant/components/notify/mysensors.py @@ -36,8 +36,6 @@ def __repr__(self): 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 1fa8f1dab78b63..044a037cc2978b 100644 --- a/homeassistant/components/notify/nfandroidtv.py +++ b/homeassistant/components/notify/nfandroidtv.py @@ -86,7 +86,6 @@ }) -# 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 37edb6709a74d5..a94cf4f105528d 100644 --- a/homeassistant/components/notify/pushbullet.py +++ b/homeassistant/components/notify/pushbullet.py @@ -29,7 +29,6 @@ }) -# 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 ffd7a799413b58..25e8a230224833 100644 --- a/homeassistant/components/nuimo_controller.py +++ b/homeassistant/components/nuimo_controller.py @@ -97,7 +97,6 @@ def run(self): 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 3bc45eab34ece4..41480c09a32352 100644 --- a/homeassistant/components/raspihats.py +++ b/homeassistant/components/raspihats.py @@ -34,7 +34,6 @@ 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 bc67c1646b27b8..d959d74574f3b9 100644 --- a/homeassistant/components/remote/demo.py +++ b/homeassistant/components/remote/demo.py @@ -8,7 +8,6 @@ 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 8b91e5356b416d..78d277ca65fd3a 100644 --- a/homeassistant/components/remote/itach.py +++ b/homeassistant/components/remote/itach.py @@ -44,7 +44,6 @@ }) -# 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 ffbb10cba4eca1..6fe91d0acd2e25 100644 --- a/homeassistant/components/scene/lifx_cloud.py +++ b/homeassistant/components/scene/lifx_cloud.py @@ -29,7 +29,6 @@ }) -# 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 b460498c901e44..d33796d04ccc77 100644 --- a/homeassistant/components/sensor/bloomsky.py +++ b/homeassistant/components/sensor/bloomsky.py @@ -41,7 +41,6 @@ }) -# 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 9376687cf131fa..8806fae5974f77 100644 --- a/homeassistant/components/sensor/broadlink.py +++ b/homeassistant/components/sensor/broadlink.py @@ -47,7 +47,6 @@ }) -# 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 a8bc441b722fab..24f8ea7e6a94f6 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 f326a57b137fd2..4a26a1dc9fc771 100644 --- a/homeassistant/components/sensor/command_line.py +++ b/homeassistant/components/sensor/command_line.py @@ -35,7 +35,6 @@ }) -# 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 a2d7315a314cc5..adf7e3c0fa9719 100644 --- a/homeassistant/components/sensor/crimereports.py +++ b/homeassistant/components/sensor/crimereports.py @@ -41,7 +41,6 @@ }) -# 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 8acbda74d7d461..b9109f6428c1df 100644 --- a/homeassistant/components/sensor/deluge.py +++ b/homeassistant/components/sensor/deluge.py @@ -42,7 +42,6 @@ }) -# 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 325d3e0ae58050..15cc0ec46aebe4 100644 --- a/homeassistant/components/sensor/demo.py +++ b/homeassistant/components/sensor/demo.py @@ -10,7 +10,6 @@ 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 2c8ad4781d003d..978b8db669ac77 100644 --- a/homeassistant/components/sensor/eddystone_temperature.py +++ b/homeassistant/components/sensor/eddystone_temperature.py @@ -39,7 +39,6 @@ }) -# 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 f86de1d865c964..991588f07f326c 100644 --- a/homeassistant/components/sensor/fedex.py +++ b/homeassistant/components/sensor/fedex.py @@ -41,7 +41,6 @@ }) -# 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 798f74bb6548a8..1312991913939a 100644 --- a/homeassistant/components/sensor/fints.py +++ b/homeassistant/components/sensor/fints.py @@ -50,7 +50,6 @@ }) -# 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 8d64a8d8229d0b..f312d1f22cc1e1 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 3b041127a5b18f..c1fe7ab4880fab 100644 --- a/homeassistant/components/sensor/haveibeenpwned.py +++ b/homeassistant/components/sensor/haveibeenpwned.py @@ -35,7 +35,6 @@ }) -# 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 922ed04a8d9e2a..acd10fe08afb3a 100644 --- a/homeassistant/components/sensor/hp_ilo.py +++ b/homeassistant/components/sensor/hp_ilo.py @@ -59,7 +59,6 @@ }) -# 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 ecf7bc0b8c2665..ca8c19bbc7a193 100644 --- a/homeassistant/components/sensor/isy994.py +++ b/homeassistant/components/sensor/isy994.py @@ -235,7 +235,6 @@ } -# 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 b5d3073ea9a06f..74a1bd19d34428 100644 --- a/homeassistant/components/sensor/kira.py +++ b/homeassistant/components/sensor/kira.py @@ -18,7 +18,7 @@ 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 8eeb75fb0f17d6..925b16cb4c7d20 100644 --- a/homeassistant/components/sensor/knx.py +++ b/homeassistant/components/sensor/knx.py @@ -73,7 +73,6 @@ def async_register_callbacks(self): """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 5af81832523f1d..ee9ab146c87fcb 100644 --- a/homeassistant/components/sensor/lastfm.py +++ b/homeassistant/components/sensor/lastfm.py @@ -29,7 +29,6 @@ }) -# 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 057718400c4b59..2822ce01dca456 100644 --- a/homeassistant/components/sensor/mold_indicator.py +++ b/homeassistant/components/sensor/mold_indicator.py @@ -41,7 +41,6 @@ }) -# 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 99ea4ef6135ad5..3e1887cfd598b5 100644 --- a/homeassistant/components/sensor/mopar.py +++ b/homeassistant/components/sensor/mopar.py @@ -41,7 +41,6 @@ }) -# 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 46d79c1121ba84..81c7173e4d0386 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 b140d02af04587..0fa6362ad051ff 100644 --- a/homeassistant/components/sensor/nzbget.py +++ b/homeassistant/components/sensor/nzbget.py @@ -50,7 +50,6 @@ }) -# 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 8a800e8616cc73..20d00267deef3a 100644 --- a/homeassistant/components/sensor/octoprint.py +++ b/homeassistant/components/sensor/octoprint.py @@ -38,7 +38,6 @@ }) -# 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 ff465b3617c2e3..d323a21a521096 100644 --- a/homeassistant/components/sensor/ohmconnect.py +++ b/homeassistant/components/sensor/ohmconnect.py @@ -31,7 +31,6 @@ }) -# 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 43105d54e38dfb..95ad5f1713d460 100644 --- a/homeassistant/components/sensor/onewire.py +++ b/homeassistant/components/sensor/onewire.py @@ -47,7 +47,6 @@ }) -# 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 bd071ace57854c..af0491cc26cf96 100644 --- a/homeassistant/components/sensor/opensky.py +++ b/homeassistant/components/sensor/opensky.py @@ -50,7 +50,6 @@ }) -# 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 596887998ecd55..9784cc3dc4ca35 100644 --- a/homeassistant/components/sensor/pilight.py +++ b/homeassistant/components/sensor/pilight.py @@ -30,7 +30,6 @@ }) -# 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 b61e1bce0da05b..5aa156a0ac6df7 100644 --- a/homeassistant/components/sensor/plex.py +++ b/homeassistant/components/sensor/plex.py @@ -41,7 +41,6 @@ }) -# 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 63a9c1d67d5ae2..0e296fa56bd614 100644 --- a/homeassistant/components/sensor/postnl.py +++ b/homeassistant/components/sensor/postnl.py @@ -35,7 +35,6 @@ }) -# 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 9e1c0875169197..cc4ce1e64485e6 100644 --- a/homeassistant/components/sensor/pyload.py +++ b/homeassistant/components/sensor/pyload.py @@ -43,7 +43,6 @@ }) -# 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 7dd795d8f8d36b..3d9704875c9cc5 100644 --- a/homeassistant/components/sensor/qnap.py +++ b/homeassistant/components/sensor/qnap.py @@ -102,7 +102,6 @@ }) -# 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 61933614a7471d..53cbaab19a58eb 100644 --- a/homeassistant/components/sensor/skybeacon.py +++ b/homeassistant/components/sensor/skybeacon.py @@ -39,7 +39,6 @@ }) -# 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 08177c9a7b9200..daa520f2ede077 100644 --- a/homeassistant/components/sensor/spotcrime.py +++ b/homeassistant/components/sensor/spotcrime.py @@ -44,7 +44,6 @@ }) -# 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 88cb786e66d8d9..e22e1594b55479 100644 --- a/homeassistant/components/sensor/steam_online.py +++ b/homeassistant/components/sensor/steam_online.py @@ -36,7 +36,6 @@ }) -# 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 fd0c6292de2bb4..5a302462bbf188 100644 --- a/homeassistant/components/sensor/supervisord.py +++ b/homeassistant/components/sensor/supervisord.py @@ -26,7 +26,6 @@ }) -# 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 517ee6509f76a1..1883ee89d4e2f6 100644 --- a/homeassistant/components/sensor/systemmonitor.py +++ b/homeassistant/components/sensor/systemmonitor.py @@ -67,7 +67,6 @@ } -# 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 8355add47e92a5..de929aa094272b 100644 --- a/homeassistant/components/sensor/tellstick.py +++ b/homeassistant/components/sensor/tellstick.py @@ -37,7 +37,6 @@ }) -# 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 973e07d9cf3435..f0a3e15834cf3e 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 65f49998dbf9fa..23c7c13f0edec1 100644 --- a/homeassistant/components/sensor/template.py +++ b/homeassistant/components/sensor/template.py @@ -42,7 +42,6 @@ @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 98fad475d52a2e..4ed1b5907cf424 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 b3e227aea72247..250911b49b1096 100644 --- a/homeassistant/components/sensor/twitch.py +++ b/homeassistant/components/sensor/twitch.py @@ -31,7 +31,6 @@ }) -# 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 c51ae67475fe70..a864df384ad011 100644 --- a/homeassistant/components/sensor/ups.py +++ b/homeassistant/components/sensor/ups.py @@ -38,7 +38,6 @@ }) -# 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 8884d790eed036..05d61173da00f1 100644 --- a/homeassistant/components/sensor/worldtidesinfo.py +++ b/homeassistant/components/sensor/worldtidesinfo.py @@ -31,7 +31,6 @@ }) -# 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 0c7b8b48f624ce..250c74ee4933c4 100644 --- a/homeassistant/components/sensor/xbox_live.py +++ b/homeassistant/components/sensor/xbox_live.py @@ -27,7 +27,6 @@ }) -# 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 f7bc9488cc5e44..a70d701fac639b 100644 --- a/homeassistant/components/sensor/xiaomi_miio.py +++ b/homeassistant/components/sensor/xiaomi_miio.py @@ -36,7 +36,6 @@ 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 3b74b79b36b018..df36eef2f9ef9a 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 6dc5df4ffe3513..5412f559b7391a 100644 --- a/homeassistant/components/switch/bbb_gpio.py +++ b/homeassistant/components/switch/bbb_gpio.py @@ -34,7 +34,6 @@ }) -# 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 478b1c6e9adf58..127c7578940f40 100644 --- a/homeassistant/components/switch/command_line.py +++ b/homeassistant/components/switch/command_line.py @@ -32,7 +32,6 @@ }) -# 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 da0b3bf3228966..c71c3865f5dc19 100644 --- a/homeassistant/components/switch/deluge.py +++ b/homeassistant/components/switch/deluge.py @@ -32,7 +32,6 @@ }) -# 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 83b8ae796bb058..7e22f962330d5b 100644 --- a/homeassistant/components/switch/demo.py +++ b/homeassistant/components/switch/demo.py @@ -8,7 +8,6 @@ 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 5d727e72138db7..1c7253c4ec378f 100644 --- a/homeassistant/components/switch/dlink.py +++ b/homeassistant/components/switch/dlink.py @@ -36,7 +36,6 @@ }) -# 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 40ebb54b603074..9cd7c48488649d 100644 --- a/homeassistant/components/switch/edimax.py +++ b/homeassistant/components/switch/edimax.py @@ -29,7 +29,6 @@ }) -# 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 21689dcca0fa5a..f57843cdaa0f7f 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 f4175926aa0610..54c3b5e942aeae 100644 --- a/homeassistant/components/switch/gc100.py +++ b/homeassistant/components/switch/gc100.py @@ -23,7 +23,6 @@ }) -# 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 efdda6ed40cb79..3d29c53bd7cb08 100644 --- a/homeassistant/components/switch/isy994.py +++ b/homeassistant/components/switch/isy994.py @@ -15,7 +15,6 @@ _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 88a07b68cd94cb..c830e2299f6ce4 100644 --- a/homeassistant/components/switch/kankun.py +++ b/homeassistant/components/switch/kankun.py @@ -34,7 +34,6 @@ }) -# 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 a96f96a9c5c242..c13631ca5e67c2 100644 --- a/homeassistant/components/switch/knx.py +++ b/homeassistant/components/switch/knx.py @@ -72,7 +72,6 @@ def async_register_callbacks(self): """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 da36c76f41dbce..f5e7cf2836f892 100644 --- a/homeassistant/components/switch/lutron_caseta.py +++ b/homeassistant/components/switch/lutron_caseta.py @@ -16,7 +16,6 @@ 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 e039a29809d4ed..fdb4752f594432 100644 --- a/homeassistant/components/switch/orvibo.py +++ b/homeassistant/components/switch/orvibo.py @@ -31,7 +31,6 @@ }) -# 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 007e74e14fd37c..e25368f3c5cffc 100644 --- a/homeassistant/components/switch/pulseaudio_loopback.py +++ b/homeassistant/components/switch/pulseaudio_loopback.py @@ -54,7 +54,6 @@ }) -# 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 7be3a6f0baafe6..7173ad35dafbfd 100644 --- a/homeassistant/components/switch/raspihats.py +++ b/homeassistant/components/switch/raspihats.py @@ -39,7 +39,6 @@ }) -# 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 9c589d1d95b9eb..914408406a9e80 100644 --- a/homeassistant/components/switch/rest.py +++ b/homeassistant/components/switch/rest.py @@ -47,7 +47,6 @@ }) -# 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 ac38da1c6a7b9d..26de2a78e1899a 100644 --- a/homeassistant/components/switch/rpi_gpio.py +++ b/homeassistant/components/switch/rpi_gpio.py @@ -34,7 +34,6 @@ }) -# 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 40200f05806343..62c92ad2d968c6 100644 --- a/homeassistant/components/switch/rpi_rf.py +++ b/homeassistant/components/switch/rpi_rf.py @@ -44,7 +44,7 @@ }) -# 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 ae19e77c2e5a92..5f7930a8a7c47a 100644 --- a/homeassistant/components/switch/tellstick.py +++ b/homeassistant/components/switch/tellstick.py @@ -10,7 +10,6 @@ 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 c3a608b96924a1..381f2ec9bec829 100644 --- a/homeassistant/components/switch/telnet.py +++ b/homeassistant/components/switch/telnet.py @@ -38,7 +38,6 @@ 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 93ebf98e9ace67..a6fa8241940b1c 100644 --- a/homeassistant/components/switch/template.py +++ b/homeassistant/components/switch/template.py @@ -44,7 +44,6 @@ @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 1eca5284f76f2d..cd2a0f189fc625 100644 --- a/homeassistant/components/switch/tplink.py +++ b/homeassistant/components/switch/tplink.py @@ -32,7 +32,6 @@ }) -# 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 840fdae44d935e..ffe285a23f3942 100644 --- a/homeassistant/components/switch/transmission.py +++ b/homeassistant/components/switch/transmission.py @@ -31,7 +31,6 @@ }) -# 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 4f06f94155831a..569566bcbfb6f4 100644 --- a/homeassistant/components/switch/wemo.py +++ b/homeassistant/components/switch/wemo.py @@ -33,7 +33,7 @@ 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 b0d251822b0b1c..1e11b844fdf56c 100644 --- a/homeassistant/components/switch/xiaomi_miio.py +++ b/homeassistant/components/switch/xiaomi_miio.py @@ -97,7 +97,6 @@ } -# 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 2603f61eb751ba..cbbf279bb8c8d8 100644 --- a/homeassistant/components/vera.py +++ b/homeassistant/components/vera.py @@ -53,7 +53,7 @@ ] -# 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 9929b64be7dacc..15b75b2f7a82b4 100644 --- a/homeassistant/components/wemo.py +++ b/homeassistant/components/wemo.py @@ -44,7 +44,7 @@ }, 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 f3ec360462e191..e4dfc17246a427 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 bc3b598180c0b3..5e6dcd81310b0d 100644 --- a/homeassistant/core.py +++ b/homeassistant/core.py @@ -4,7 +4,7 @@ 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 7b5b996a3a3555..cd440783cc3e01 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 87e2a7a2331eb0..272a5b868ec8b0 100644 --- a/homeassistant/components/rflink.py +++ b/homeassistant/components/rflink.py @@ -20,6 +20,8 @@ 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 @@ 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 reconnect(exc=None): # 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 connect(): _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 @@ def assumed_state(self): """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 a99d14cc735007..a250a75ab99912 100644 --- a/tests/components/sensor/test_rflink.py +++ b/tests/components/sensor/test_rflink.py @@ -8,6 +8,9 @@ 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 @@ 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 1a15e22fca08c1..3bfa1372fabae5 100644 --- a/homeassistant/components/ring.py +++ b/homeassistant/components/ring.py @@ -12,7 +12,7 @@ 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 921bbf8fd46bfd..8d31dd7e267e62 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 02f079dd9a69c3..1b32efe9577b8c 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 889282b56dd98f..e557050ae48782 100644 --- a/tests/components/binary_sensor/test_ring.py +++ b/tests/components/binary_sensor/test_ring.py @@ -44,6 +44,8 @@ def tearDown(self): @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 0cce0ea681d318..4d34018ce52c85 100644 --- a/tests/components/sensor/test_ring.py +++ b/tests/components/sensor/test_ring.py @@ -51,6 +51,8 @@ def tearDown(self): @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 3837ec130611e7..7b974686a4e132 100644 --- a/tests/components/test_ring.py +++ b/tests/components/test_ring.py @@ -42,6 +42,8 @@ def tearDown(self): # pylint: disable=invalid-name @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 00000000000000..5e69ddde065272 --- /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 44bf542f7cd082..2906f07a307c0f 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 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 + 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 - # 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 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 - config[comp_name] = comp_conf + 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 d22d6b2acfd1a9..717a3f62ec9a33 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 206ea4005e6bae..cd2c13ad292bd3 100644 --- a/homeassistant/components/arlo.py +++ b/homeassistant/components/arlo.py @@ -16,7 +16,7 @@ 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 8d31dd7e267e62..d9d2c347c51d1a 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 d0e470e3f8ec44..820ca41ad2e73f 100644 --- a/homeassistant/components/amcrest.py +++ b/homeassistant/components/amcrest.py @@ -18,7 +18,7 @@ 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 d9d2c347c51d1a..f52ce86f1ea7c4 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 308a945e942e26..a04f4926b76ebb 100644 --- a/homeassistant/components/raincloud.py +++ b/homeassistant/components/raincloud.py @@ -19,7 +19,7 @@ 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 f52ce86f1ea7c4..fdf4dc56f98415 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 a405362d368d8b..b108ac805e9f67 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.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 @@ async def async_stop_async_handler(event): "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 815a5c8e55f630..7aba3b2561cbaa 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 @@ 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 a2f707c54f5032..d1d398020dee17 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 3e4d47397799a7..e329f835f84b71 100644 --- a/tests/test_bootstrap.py +++ b/tests/test_bootstrap.py @@ -9,7 +9,7 @@ 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 33db052f45acae..ab9f9f0ad2c5e3 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 c41020c3faf112..ebda09de20cd35 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 update_tokens(time): 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_handle_snapshot_service(service): "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 _write_image(to_file, image_data): 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 @@ def camera_image(self): """Return bytes of camera image.""" raise NotImplementedError() + @callback def async_camera_image(self): """Return bytes of camera image. @@ -397,8 +396,7 @@ def __init__(self, component): """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 @@ def get(self, request, entity_id): 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 d009f156e9d225..3c1477d1828658 100644 --- a/homeassistant/components/camera/demo.py +++ b/homeassistant/components/camera/demo.py @@ -12,9 +12,10 @@ _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 5644c3d0a1f2f6..d9446952f002c7 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 0c425ccd3b1e8d..af3459d0b19878 100644 --- a/homeassistant/components/frontend/__init__.py +++ b/homeassistant/components/frontend/__init__.py @@ -24,7 +24,7 @@ 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 fdf4dc56f98415..55038299bc01a4 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 1b32efe9577b8c..03023966d958a6 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 1e11b844fdf56c..37b16f44ea8eec 100644 --- a/homeassistant/components/switch/xiaomi_miio.py +++ b/homeassistant/components/switch/xiaomi_miio.py @@ -421,8 +421,11 @@ def __init__(self, name, plug, model, unique_id, channel_usb): 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 @@ async def async_update(self): 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 963258f1861df6..73ec8a175b1f17 100644 --- a/homeassistant/components/media_player/spotify.py +++ b/homeassistant/components/media_player/spotify.py @@ -20,9 +20,7 @@ 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 55038299bc01a4..69a6f01dbd5e1d 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 4101eab2150298..d7544cb6c5a2ee 100644 --- a/homeassistant/components/light/tplink.py +++ b/homeassistant/components/light/tplink.py @@ -19,7 +19,7 @@ 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 cd2a0f189fc625..46682d87356c6f 100644 --- a/homeassistant/components/switch/tplink.py +++ b/homeassistant/components/switch/tplink.py @@ -14,7 +14,7 @@ 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 69a6f01dbd5e1d..af5e9c6c787eac 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 af3459d0b19878..0f77b9e0adcf49 100644 --- a/homeassistant/components/frontend/__init__.py +++ b/homeassistant/components/frontend/__init__.py @@ -23,6 +23,7 @@ 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 @@ 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 @@ def async_finalize_panel(panel): """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 @@ async def send_translations(): )) 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 2f118f24ef0933..cb0c72e9edd1a9 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 d9446952f002c7..dd32c0e5be7d81 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 0f77b9e0adcf49..25aa0da0a3e27f 100644 --- a/homeassistant/components/frontend/__init__.py +++ b/homeassistant/components/frontend/__init__.py @@ -25,7 +25,7 @@ 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 af5e9c6c787eac..d860112c7f8f7a 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 03023966d958a6..a2245c02cf1434 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 dd32c0e5be7d81..562247a14c06ea 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 ac90deb9f737d1..5f2c9c009c33d4 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 00000000000000..e65e00f8624b69 --- /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 00000000000000..2be2a69c171327 --- /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 00000000000000..d36c929e7211b5 --- /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 00000000000000..c4399f95defe81 --- /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 00000000000000..9c9353da37e3da --- /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 00000000000000..aea55058d108f7 --- /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 00000000000000..2f2982293cfdac --- /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 00000000000000..4a844d3d4dd84a --- /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 91727cae257009..2ea6576206375d 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 00000000000000..0a9e6fdee3f68e --- /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 00000000000000..0721cac3321bfc --- /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 465c6c1e0e86d1..f55f64ca43094a 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 00000000000000..02f174cd59f746 --- /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 42aab9c6d7e56f..c1fd76c5035fc2 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 00000000000000..6fc7158b88269c --- /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 d6de1028218dee..9c5ffa19257f3b 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 2a9dfc5e5438dd..46190d23926b8c 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 25e3b0b7d68c40..55518b7da532ae 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 bb7488fcbec1e9..461e8b185eebeb 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 00000000000000..065c51aee21cdc --- /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 2a00c69869140e..6ccbfe9f217d56 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 b0dc6a8a4a85f4..56490f67cb3dc6 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 b738002b273d64..59c5577c96b5a8 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 00000000000000..88cf8742acde8c --- /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 00000000000000..00f1d9be57f07e --- /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 f41b5b5111c2be..2e5a216c77ddec 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 33be3846eb8290..17cbe87f1e8f9b 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 00000000000000..6c41eed5467ac9 --- /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 00000000000000..35c423b1a03420 --- /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 b0459ec39163ab..cea8d8be10af34 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 00000000000000..73613f237dac3b --- /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 a4032dcbcfc215..be6548f59a0e91 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 2c7a8c1924d6db..a9f2a732127a23 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 00000000000000..5c6e409245c7e9 --- /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 8c4c45f9c897c8..f7988d82d8ce3b 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 00000000000000..efbcfa544f5d81 --- /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 00000000000000..5cbd0c4aebfbd0 --- /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 00000000000000..2fb17916aee81b --- /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 00000000000000..0caa70aeff2853 --- /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 00000000000000..03cf1a82b813bf --- /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 00000000000000..c03b2eff0fabd0 --- /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 00000000000000..0f7b9b8dd719c2 --- /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 00000000000000..721f891219daa5 --- /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 00000000000000..996c6c68eae9e3 --- /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 00000000000000..05ba5bdf15525a --- /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 00000000000000..9bce187ec65d91 --- /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 00000000000000..ec9f9657428917 --- /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 00000000000000..fde45ad6c8efa0 --- /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 00000000000000..9a745784b25fd2 --- /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 00000000000000..5453e4322cd094 --- /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 00000000000000..c837abad499db4 --- /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 00000000000000..2a0c526b9a64ac --- /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 00000000000000..63b6bd87c20b47 --- /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 00000000000000..756fe8a74832d2 --- /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 00000000000000..ebeb1a8b07ce31 --- /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 00000000000000..17c1e78d3e8922 --- /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 00000000000000..5770058c5ebc4f --- /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 00000000000000..1676c8f390627a --- /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 00000000000000..a521377e5e0a59 --- /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 00000000000000..eb02aba7b50c05 --- /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 00000000000000..0181f688c27d0d --- /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 00000000000000..4490124510fa4d --- /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 364f8f3cc77f3a..421f079a67ea48 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 00000000000000..f2a41b0b26785c --- /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 a4ced557805661..2c3292e58c192d 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 00000000000000..1885cb5d2c86bd --- /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 00000000000000..55c5bcf712721c --- /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 00000000000000..7217944bd6b631 --- /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 00000000000000..12c1141397d7ef --- /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 4fbbbb77b794b4..db2912d7b42297 100644 --- a/homeassistant/config_entries.py +++ b/homeassistant/config_entries.py @@ -151,6 +151,8 @@ async def async_step_discovery(info): data_entry_flow.SOURCE_IMPORT, ) +EVENT_FLOW_DISCOVERED = 'config_entry_discovered' + class ConfigEntry: """Hold a configuration entry.""" @@ -404,6 +406,7 @@ async def _async_create_flow(self, handler, *, source, data): # 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 bb4f8219a333bd..c51d45cc3396eb 100644 --- a/homeassistant/components/hassio/http.py +++ b/homeassistant/components/hassio/http.py @@ -36,7 +36,7 @@ } 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 5f2c9c009c33d4..ce260225097a12 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 25aa0da0a3e27f..2c9b68bf079bd9 100644 --- a/homeassistant/components/frontend/__init__.py +++ b/homeassistant/components/frontend/__init__.py @@ -25,7 +25,7 @@ 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 d860112c7f8f7a..e2507fa8cbec4a 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 a2245c02cf1434..d4a24fbc0846d0 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 8af0318373d5a0..d4fc60696cdafd 100644 --- a/homeassistant/components/binary_sensor/linode.py +++ b/homeassistant/components/binary_sensor/linode.py @@ -52,19 +52,18 @@ def __init__(self, li, node_id): 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 @@ def device_class(self): @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 @@ def device_state_attributes(self): 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 91177e321169ab..43f4bdc31b4b72 100644 --- a/homeassistant/components/switch/linode.py +++ b/homeassistant/components/switch/linode.py @@ -51,35 +51,23 @@ def __init__(self, li, node_id): 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 @@ def update(self): 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 c69e9eb4af41fa..5e7386242baa1c 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 e2507fa8cbec4a..bbf74004be4a04 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 a4d15feb7fc324..f914e032fd7325 100755 --- a/setup.py +++ b/setup.py @@ -42,7 +42,7 @@ 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 c33a16c632e629..ee19e00266c7fc 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 c26b3375f3ace4..92dee05818dedb 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 ab6c3a084c07c2..472a88888d88f5 100644 --- a/homeassistant/helpers/entity_platform.py +++ b/homeassistant/helpers/entity_platform.py @@ -216,6 +216,10 @@ async def async_add_entities(self, new_entities, update_before_add=False): 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 9fa178022dc43c..2d2f148189f683 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 562247a14c06ea..72f018ad366809 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 72f018ad366809..7682234233edbb 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 2c9b68bf079bd9..9af1a7af3bedda 100644 --- a/homeassistant/components/frontend/__init__.py +++ b/homeassistant/components/frontend/__init__.py @@ -25,7 +25,7 @@ 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 bbf74004be4a04..766ab10671c130 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 d4a24fbc0846d0..b32da6fc9f28a3 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 7682234233edbb..98179b8502e71c 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 | 44 +++++++++++------- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/frontend/__init__.py | 1 + .../test_init.py} | 45 +++++++++++++++++-- 5 files changed, 73 insertions(+), 21 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 9af1a7af3bedda..b2cac55bd77421 100644 --- a/homeassistant/components/frontend/__init__.py +++ b/homeassistant/components/frontend/__init__.py @@ -21,11 +21,12 @@ 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 @@ 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 @@ def async_finalize_panel(panel): 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 @@ async def send_translations(): 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')) - - connection.send_message_outside(websocket_api.result_message( - msg['id'], config - )) + """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) + + 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 766ab10671c130..56896c9b6da0a4 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 b32da6fc9f28a3..177796961a5bac 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 00000000000000..991a74dee7a1c7 --- /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 cb0c72e9edd1a9..2125668facb8a9 100644 --- a/tests/components/test_frontend.py +++ b/tests/components/frontend/test_init.py @@ -5,6 +5,7 @@ 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 98179b8502e71c..091bd907b93dc1 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 b2cac55bd77421..9200f4d78f65e7 100644 --- a/homeassistant/components/frontend/__init__.py +++ b/homeassistant/components/frontend/__init__.py @@ -26,7 +26,7 @@ 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 56896c9b6da0a4..c0c85012526e14 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 177796961a5bac..8f09c4d7195d96 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 b86f80cbee788d..b2f7c8b66551bd 100644 --- a/homeassistant/components/insteon_plm/__init__.py +++ b/homeassistant/components/insteon_plm/__init__.py @@ -17,7 +17,7 @@ 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_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 @@ 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 @@ 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_plm_new_device(device): 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 print_im_aldb(service): # 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 _register_services(): 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 _register_services(): 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 @@ def __init__(self): 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 @@ def __init__(self): 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 9ea53c10fbf1af..4d87d7881bf666 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 be562e9d909d67..42b4829f64ecd0 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 c0c85012526e14..35f45bbd5b0cd1 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 97a4cc8c137ea4..c0e363f85d6d40 100644 --- a/homeassistant/components/light/mqtt.py +++ b/homeassistant/components/light/mqtt.py @@ -442,8 +442,15 @@ async def async_turn_on(self, **kwargs): 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 14f5ee7a9b9142..705e106fdff8be 100644 --- a/homeassistant/components/light/mqtt_json.py +++ b/homeassistant/components/light/mqtt_json.py @@ -345,9 +345,14 @@ async def async_turn_on(self, **kwargs): 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 e32c13fc5b6eff..f6b3fbe8b70799 100644 --- a/homeassistant/components/light/mqtt_template.py +++ b/homeassistant/components/light/mqtt_template.py @@ -317,8 +317,15 @@ async def async_turn_on(self, **kwargs): 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 8b51adb2187399..49bcd8a73ecc0c 100644 --- a/tests/components/light/test_mqtt.py +++ b/tests/components/light/test_mqtt.py @@ -523,24 +523,24 @@ def test_sending_mqtt_commands_and_optimistic(self): \ 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 @@ def test_on_command_brightness(self): # 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 275fb42ede917c..af560bff9c3224 100644 --- a/tests/components/light/test_mqtt_json.py +++ b/tests/components/light/test_mqtt_json.py @@ -381,8 +381,8 @@ def test_sending_mqtt_commands_and_optimistic(self): \ 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 33bd00caa6bf68..689129e1067ff9 100644 --- a/homeassistant/components/camera/neato.py +++ b/homeassistant/components/camera/neato.py @@ -45,7 +45,7 @@ def camera_image(self): 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 7402bb18843ad2..c6a3dcf9c9a605 100644 --- a/homeassistant/components/neato.py +++ b/homeassistant/components/neato.py @@ -17,8 +17,8 @@ _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 @@ def login(self): _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 a797abb47fcf9d..1d149383f6fad6 100644 --- a/homeassistant/components/switch/neato.py +++ b/homeassistant/components/switch/neato.py @@ -5,10 +5,12 @@ 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 @@ def __init__(self, hass, robot, switch_type): 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 9eba34cea321b3..128bece8494274 100644 --- a/homeassistant/components/vacuum/neato.py +++ b/homeassistant/components/vacuum/neato.py @@ -5,7 +5,7 @@ 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 @@ 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 @@ def __init__(self, hass, robot): 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 35f45bbd5b0cd1..fb365b26651889 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 091bd907b93dc1..efed01d409e2fe 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 9200f4d78f65e7..d8497f9c7900df 100644 --- a/homeassistant/components/frontend/__init__.py +++ b/homeassistant/components/frontend/__init__.py @@ -26,7 +26,7 @@ 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 fb365b26651889..7c03f3465c3636 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 8f09c4d7195d96..f6762e1faabf48 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 59060a7075b024..eb61420a600839 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 efed01d409e2fe..e9b72a70f1b137 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 d8497f9c7900df..89353b56098b7f 100644 --- a/homeassistant/components/frontend/__init__.py +++ b/homeassistant/components/frontend/__init__.py @@ -26,7 +26,7 @@ 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 7c03f3465c3636..83b8052f78f9fb 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 f6762e1faabf48..a3925262572400 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 e9b72a70f1b137..7feb5d8bdac2c1 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 89353b56098b7f..9c9fdd137e2e4c 100644 --- a/homeassistant/components/frontend/__init__.py +++ b/homeassistant/components/frontend/__init__.py @@ -26,7 +26,7 @@ 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 83b8052f78f9fb..54f3a89e089221 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 a3925262572400..c8194a8382f4a7 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 7feb5d8bdac2c1..a22605c37f49a4 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 eb61420a600839..e324b231d0667d 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 9c9fdd137e2e4c..3d2231ab43b440 100644 --- a/homeassistant/components/frontend/__init__.py +++ b/homeassistant/components/frontend/__init__.py @@ -26,7 +26,7 @@ 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 54f3a89e089221..52a5e0525604de 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 c8194a8382f4a7..a38c7f259b4782 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