From 6ccb83584e3da104f65fa915bc48a761a6d507af Mon Sep 17 00:00:00 2001 From: Johann Kellerman Date: Sat, 21 Apr 2018 08:34:42 +0200 Subject: [PATCH 001/155] Qwikswitch binary sensors (#14008) --- .../components/binary_sensor/qwikswitch.py | 70 +++++++++++++++++++ homeassistant/components/qwikswitch.py | 44 ++++++++---- homeassistant/components/sensor/qwikswitch.py | 12 ++-- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- .../{sensor => }/test_qwikswitch.py | 70 +++++++++++++------ 6 files changed, 160 insertions(+), 40 deletions(-) create mode 100644 homeassistant/components/binary_sensor/qwikswitch.py rename tests/components/{sensor => }/test_qwikswitch.py (55%) diff --git a/homeassistant/components/binary_sensor/qwikswitch.py b/homeassistant/components/binary_sensor/qwikswitch.py new file mode 100644 index 00000000000..067021b0c7a --- /dev/null +++ b/homeassistant/components/binary_sensor/qwikswitch.py @@ -0,0 +1,70 @@ +""" +Support for Qwikswitch Binary Sensors. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/binary_sensor.qwikswitch/ +""" +import logging + +from homeassistant.components.binary_sensor import BinarySensorDevice +from homeassistant.components.qwikswitch import QSEntity, DOMAIN as QWIKSWITCH +from homeassistant.core import callback + +DEPENDENCIES = [QWIKSWITCH] + +_LOGGER = logging.getLogger(__name__) + + +async def async_setup_platform(hass, _, add_devices, discovery_info=None): + """Add binary sensor from the main Qwikswitch component.""" + if discovery_info is None: + return + + qsusb = hass.data[QWIKSWITCH] + _LOGGER.debug("Setup qwikswitch.binary_sensor %s, %s", + qsusb, discovery_info) + devs = [QSBinarySensor(sensor) for sensor in discovery_info[QWIKSWITCH]] + add_devices(devs) + + +class QSBinarySensor(QSEntity, BinarySensorDevice): + """Sensor based on a Qwikswitch relay/dimmer module.""" + + _val = False + + def __init__(self, sensor): + """Initialize the sensor.""" + from pyqwikswitch import SENSORS + + super().__init__(sensor['id'], sensor['name']) + self.channel = sensor['channel'] + sensor_type = sensor['type'] + + self._decode, _ = SENSORS[sensor_type] + self._invert = not sensor.get('invert', False) + self._class = sensor.get('class', 'door') + + @callback + def update_packet(self, packet): + """Receive update packet from QSUSB.""" + val = self._decode(packet, channel=self.channel) + _LOGGER.debug("Update %s (%s:%s) decoded as %s: %s", + self.entity_id, self.qsid, self.channel, val, packet) + if val is not None: + self._val = bool(val) + self.async_schedule_update_ha_state() + + @property + def is_on(self): + """Check if device is on (non-zero).""" + return self._val == self._invert + + @property + def unique_id(self): + """Return a unique identifier for this sensor.""" + return "qs{}:{}".format(self.qsid, self.channel) + + @property + def device_class(self): + """Return the class of this sensor.""" + return self._class diff --git a/homeassistant/components/qwikswitch.py b/homeassistant/components/qwikswitch.py index 3dc16f513dc..f26318fa7a9 100644 --- a/homeassistant/components/qwikswitch.py +++ b/homeassistant/components/qwikswitch.py @@ -8,17 +8,18 @@ import logging import voluptuous as vol +from homeassistant.components.binary_sensor import DEVICE_CLASSES_SCHEMA +from homeassistant.components.light import ATTR_BRIGHTNESS from homeassistant.const import ( - EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP, CONF_URL, - CONF_SENSORS, CONF_SWITCHES) + CONF_SENSORS, CONF_SWITCHES, CONF_URL, EVENT_HOMEASSISTANT_START, + EVENT_HOMEASSISTANT_STOP) from homeassistant.core import callback from homeassistant.helpers.aiohttp_client import async_get_clientsession +import homeassistant.helpers.config_validation as cv from homeassistant.helpers.discovery import load_platform from homeassistant.helpers.entity import Entity -from homeassistant.components.light import ATTR_BRIGHTNESS -import homeassistant.helpers.config_validation as cv -REQUIREMENTS = ['pyqwikswitch==0.71'] +REQUIREMENTS = ['pyqwikswitch==0.8'] _LOGGER = logging.getLogger(__name__) @@ -28,6 +29,7 @@ CONF_DIMMER_ADJUST = 'dimmer_adjust' CONF_BUTTON_EVENTS = 'button_events' CV_DIM_VALUE = vol.All(vol.Coerce(float), vol.Range(min=1, max=3)) + CONFIG_SCHEMA = vol.Schema({ DOMAIN: vol.Schema({ vol.Required(CONF_URL, default='http://127.0.0.1:2020'): @@ -40,6 +42,8 @@ CONFIG_SCHEMA = vol.Schema({ vol.Optional('channel', default=1): int, vol.Required('name'): str, vol.Required('type'): str, + vol.Optional('class'): DEVICE_CLASSES_SCHEMA, + vol.Optional('invert'): bool })]), vol.Optional(CONF_SWITCHES, default=[]): vol.All( cv.ensure_list, [str]) @@ -115,7 +119,7 @@ class QSToggleEntity(QSEntity): async def async_setup(hass, config): """Qwiskswitch component setup.""" from pyqwikswitch.async_ import QSUsb - from pyqwikswitch import CMD_BUTTONS, QS_CMD, QS_ID, QSType + from pyqwikswitch import CMD_BUTTONS, QS_CMD, QS_ID, QSType, SENSORS # Add cmd's to in /&listen packets will fire events # By default only buttons of type [TOGGLE,SCENE EXE,LEVEL] @@ -143,22 +147,39 @@ async def async_setup(hass, config): hass.data[DOMAIN] = qsusb - _new = {'switch': [], 'light': [], 'sensor': sensors} + comps = {'switch': [], 'light': [], 'sensor': [], 'binary_sensor': []} + + try: + for sens in sensors: + _, _type = SENSORS[sens['type']] + if _type is bool: + comps['binary_sensor'].append(sens) + continue + comps['sensor'].append(sens) + for _key in ('invert', 'class'): + if _key in sens: + _LOGGER.warning( + "%s should only be used for binary_sensors: %s", + _key, sens) + + except KeyError: + _LOGGER.warning("Sensor validation failed") + for qsid, dev in qsusb.devices.items(): if qsid in switches: if dev.qstype != QSType.relay: _LOGGER.warning( "You specified a switch that is not a relay %s", qsid) continue - _new['switch'].append(qsid) + comps['switch'].append(qsid) elif dev.qstype in (QSType.relay, QSType.dimmer): - _new['light'].append(qsid) + comps['light'].append(qsid) else: _LOGGER.warning("Ignored unknown QSUSB device: %s", dev) continue # Load platforms - for comp_name, comp_conf in _new.items(): + for comp_name, comp_conf in comps.items(): if comp_conf: load_platform(hass, comp_name, DOMAIN, {DOMAIN: comp_conf}, config) @@ -190,9 +211,8 @@ async def async_setup(hass, config): @callback def async_stop(_): - """Stop the listener queue and clean up.""" + """Stop the listener.""" hass.data[DOMAIN].stop() - _LOGGER.info("Waiting for long poll to QSUSB to time out (max 30sec)") hass.bus.async_listen(EVENT_HOMEASSISTANT_STOP, async_stop) diff --git a/homeassistant/components/sensor/qwikswitch.py b/homeassistant/components/sensor/qwikswitch.py index ebd5f5254d4..1497b4ad5cc 100644 --- a/homeassistant/components/sensor/qwikswitch.py +++ b/homeassistant/components/sensor/qwikswitch.py @@ -36,18 +36,18 @@ class QSSensor(QSEntity): super().__init__(sensor['id'], sensor['name']) self.channel = sensor['channel'] - self.sensor_type = sensor['type'] + sensor_type = sensor['type'] - self._decode, self.unit = SENSORS[self.sensor_type] + self._decode, self.unit = SENSORS[sensor_type] if isinstance(self.unit, type): - self.unit = "{}:{}".format(self.sensor_type, self.channel) + self.unit = "{}:{}".format(sensor_type, self.channel) @callback def update_packet(self, packet): """Receive update packet from QSUSB.""" - val = self._decode(packet.get('data'), channel=self.channel) - _LOGGER.debug("Update %s (%s) decoded as %s: %s: %s", - self.entity_id, self.qsid, val, self.channel, packet) + val = self._decode(packet, channel=self.channel) + _LOGGER.debug("Update %s (%s:%s) decoded as %s: %s", + self.entity_id, self.qsid, self.channel, val, packet) if val is not None: self._val = val self.async_schedule_update_ha_state() diff --git a/requirements_all.txt b/requirements_all.txt index aeb5b84811e..bc3724d0930 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -898,7 +898,7 @@ pyowm==2.8.0 pypollencom==1.1.2 # homeassistant.components.qwikswitch -pyqwikswitch==0.71 +pyqwikswitch==0.8 # homeassistant.components.rainbird pyrainbird==0.1.3 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 0d371996e36..cf4aa2e1b3c 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -149,7 +149,7 @@ pymonoprice==0.3 pynx584==0.4 # homeassistant.components.qwikswitch -pyqwikswitch==0.71 +pyqwikswitch==0.8 # homeassistant.components.sensor.darksky # homeassistant.components.weather.darksky diff --git a/tests/components/sensor/test_qwikswitch.py b/tests/components/test_qwikswitch.py similarity index 55% rename from tests/components/sensor/test_qwikswitch.py rename to tests/components/test_qwikswitch.py index d9dfe072fc0..76655f32816 100644 --- a/tests/components/sensor/test_qwikswitch.py +++ b/tests/components/test_qwikswitch.py @@ -13,17 +13,19 @@ _LOGGER = logging.getLogger(__name__) class AiohttpClientMockResponseList(list): - """List that fires an event on empty pop, for aiohttp Mocker.""" + """Return multiple values for aiohttp Mocker. + + aoihttp mocker uses decode to fetch the next value. + """ def decode(self, _): """Return next item from list.""" try: - res = list.pop(self) + res = list.pop(self, 0) _LOGGER.debug("MockResponseList popped %s: %s", res, self) return res except IndexError: - _LOGGER.debug("MockResponseList empty") - return "" + raise AssertionError("MockResponseList empty") async def wait_till_empty(self, hass): """Wait until empty.""" @@ -52,8 +54,8 @@ def aioclient_mock(): yield mock_session -async def test_sensor_device(hass, aioclient_mock): - """Test a sensor device.""" +async def test_binary_sensor_device(hass, aioclient_mock): + """Test a binary sensor device.""" config = { 'qwikswitch': { 'sensors': { @@ -67,21 +69,49 @@ async def test_sensor_device(hass, aioclient_mock): await async_setup_component(hass, QWIKSWITCH, config) await hass.async_block_till_done() - state_obj = hass.states.get('sensor.s1') - assert state_obj - assert state_obj.state == 'None' + state_obj = hass.states.get('binary_sensor.s1') + assert state_obj.state == 'off' + hass.bus.async_fire(EVENT_HOMEASSISTANT_START) - LISTEN.append( # Close - """{"id":"@a00001","cmd":"","data":"4e0e1601","rssi":"61%"}""") + LISTEN.append('{"id":"@a00001","cmd":"","data":"4e0e1601","rssi":"61%"}') + LISTEN.append('') # Will cause a sleep await hass.async_block_till_done() - state_obj = hass.states.get('sensor.s1') - assert state_obj.state == 'True' + state_obj = hass.states.get('binary_sensor.s1') + assert state_obj.state == 'on' - # Causes a 30second delay: can be uncommented when upstream library - # allows cancellation of asyncio.sleep(30) on failed packet ("") - # LISTEN.append( # Open - # """{"id":"@a00001","cmd":"","data":"4e0e1701","rssi":"61%"}""") - # await LISTEN.wait_till_empty(hass) - # state_obj = hass.states.get('sensor.s1') - # assert state_obj.state == 'False' + LISTEN.append('{"id":"@a00001","cmd":"","data":"4e0e1701","rssi":"61%"}') + hass.data[QWIKSWITCH]._sleep_task.cancel() + await LISTEN.wait_till_empty(hass) + state_obj = hass.states.get('binary_sensor.s1') + assert state_obj.state == 'off' + + +async def test_sensor_device(hass, aioclient_mock): + """Test a sensor device.""" + config = { + 'qwikswitch': { + 'sensors': { + 'name': 'ss1', + 'id': '@a00001', + 'channel': 1, + 'type': 'qwikcord', + } + } + } + await async_setup_component(hass, QWIKSWITCH, config) + await hass.async_block_till_done() + + state_obj = hass.states.get('sensor.ss1') + assert state_obj.state == 'None' + + hass.bus.async_fire(EVENT_HOMEASSISTANT_START) + + LISTEN.append( + '{"id":"@a00001","name":"ss1","type":"rel",' + '"val":"4733800001a00000"}') + LISTEN.append('') # Will cause a sleep + await LISTEN.wait_till_empty(hass) # await hass.async_block_till_done() + + state_obj = hass.states.get('sensor.ss1') + assert state_obj.state == 'None' From cb490780c9cf9f10f1c883a7474c0ef5d3ed2ef8 Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Sat, 21 Apr 2018 02:16:52 -0600 Subject: [PATCH 002/155] Pollen.com: Added attributes on top 3 allergens (#14018) --- homeassistant/components/sensor/pollen.py | 26 +++++++++++++++++------ 1 file changed, 20 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/sensor/pollen.py b/homeassistant/components/sensor/pollen.py index b55c60f6e7c..1ef5a27cf3d 100644 --- a/homeassistant/components/sensor/pollen.py +++ b/homeassistant/components/sensor/pollen.py @@ -160,7 +160,7 @@ class BaseSensor(Entity): def __init__(self, data, data_params, name, icon, unique_id): """Initialize the sensor.""" - self._attrs = {} + self._attrs = {ATTR_ATTRIBUTION: DEFAULT_ATTRIBUTION} self._icon = icon self._name = name self._data_params = data_params @@ -172,7 +172,6 @@ class BaseSensor(Entity): @property def device_state_attributes(self): """Return the device state attributes.""" - self._attrs.update({ATTR_ATTRIBUTION: DEFAULT_ATTRIBUTION}) return self._attrs @property @@ -254,10 +253,25 @@ class AllergyIndexSensor(BaseSensor): i['label'] for i in RATING_MAPPING if i['minimum'] <= period['Index'] <= i['maximum'] ] - self._attrs[ATTR_ALLERGEN_GENUS] = period['Triggers'][0]['Genus'] - self._attrs[ATTR_ALLERGEN_NAME] = period['Triggers'][0]['Name'] - self._attrs[ATTR_ALLERGEN_TYPE] = period['Triggers'][0][ - 'PlantType'] + + for i in range(3): + index = i + 1 + try: + data = period['Triggers'][i] + self._attrs['{0}_{1}'.format( + ATTR_ALLERGEN_GENUS, index)] = data['Genus'] + self._attrs['{0}_{1}'.format( + ATTR_ALLERGEN_NAME, index)] = data['Name'] + self._attrs['{0}_{1}'.format( + ATTR_ALLERGEN_TYPE, index)] = data['PlantType'] + except IndexError: + self._attrs['{0}_{1}'.format( + ATTR_ALLERGEN_GENUS, index)] = None + self._attrs['{0}_{1}'.format( + ATTR_ALLERGEN_NAME, index)] = None + self._attrs['{0}_{1}'.format( + ATTR_ALLERGEN_TYPE, index)] = None + self._attrs[ATTR_RATING] = rating except KeyError: From f12ff6f2970e43b19a23517ff88a6713081a079b Mon Sep 17 00:00:00 2001 From: Jon Maddox Date: Sat, 21 Apr 2018 04:20:33 -0400 Subject: [PATCH 003/155] Expose the condition code on condition sensors (#14011) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * expose the condition code on condition sensors * :lipstick: * like thisss duh * add test for condition_code * It’s a string --- homeassistant/components/sensor/yweather.py | 9 ++++++--- tests/components/sensor/test_yweather.py | 2 ++ 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/sensor/yweather.py b/homeassistant/components/sensor/yweather.py index df18e086ddd..db66419e54a 100644 --- a/homeassistant/components/sensor/yweather.py +++ b/homeassistant/components/sensor/yweather.py @@ -131,9 +131,12 @@ class YahooWeatherSensor(Entity): @property def device_state_attributes(self): """Return the state attributes.""" - return { - ATTR_ATTRIBUTION: CONF_ATTRIBUTION, - } + attrs = {ATTR_ATTRIBUTION: CONF_ATTRIBUTION} + + if self._code is not None and "weather" in self._type: + attrs['condition_code'] = self._code + + return attrs def update(self): """Get the latest data from Yahoo! and updates the states.""" diff --git a/tests/components/sensor/test_yweather.py b/tests/components/sensor/test_yweather.py index 88b94906a35..aeee47bfa80 100644 --- a/tests/components/sensor/test_yweather.py +++ b/tests/components/sensor/test_yweather.py @@ -162,6 +162,8 @@ class TestWeather(unittest.TestCase): state = self.hass.states.get('sensor.yweather_condition') assert state is not None self.assertEqual(state.state, 'Mostly Cloudy') + self.assertEqual(state.attributes.get('condition_code'), + '28') self.assertEqual(state.attributes.get('friendly_name'), 'Yweather Condition') From 4c23a61853b74e865e4972dc02a65d7aa46fccfe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20H=C3=B8yer=20Iversen?= Date: Sat, 21 Apr 2018 10:54:11 +0200 Subject: [PATCH 004/155] upgrade rfxtrx lib, dimming support for Lighting3 (#14026) --- homeassistant/components/rfxtrx.py | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/rfxtrx.py b/homeassistant/components/rfxtrx.py index d6873a0bd91..2e96ec64d97 100644 --- a/homeassistant/components/rfxtrx.py +++ b/homeassistant/components/rfxtrx.py @@ -20,7 +20,7 @@ from homeassistant.const import ( ) from homeassistant.helpers.entity import Entity -REQUIREMENTS = ['pyRFXtrx==0.22.0'] +REQUIREMENTS = ['pyRFXtrx==0.22.1'] DOMAIN = 'rfxtrx' diff --git a/requirements_all.txt b/requirements_all.txt index bc3724d0930..d6f811ba68c 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -677,7 +677,7 @@ pyCEC==0.4.13 pyHS100==0.3.0 # homeassistant.components.rfxtrx -pyRFXtrx==0.22.0 +pyRFXtrx==0.22.1 # homeassistant.components.sensor.tibber pyTibber==0.4.1 From 51f55bddb7c1633c540b5e89da41cb705a6a3723 Mon Sep 17 00:00:00 2001 From: Matt Schmitt Date: Sat, 21 Apr 2018 10:16:46 -0400 Subject: [PATCH 005/155] HomeKit Alarm Control Panel Code Exception Fix (#14025) * Catch exception for KeyError * Use get and added test --- .../components/homekit/type_security_systems.py | 2 +- .../components/homekit/test_type_security_systems.py | 12 ++++++++++-- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/homekit/type_security_systems.py b/homeassistant/components/homekit/type_security_systems.py index 6b8457a3aa5..0762e0f25f9 100644 --- a/homeassistant/components/homekit/type_security_systems.py +++ b/homeassistant/components/homekit/type_security_systems.py @@ -30,7 +30,7 @@ class SecuritySystem(HomeAccessory): def __init__(self, *args, config): """Initialize a SecuritySystem accessory object.""" super().__init__(*args, category=CATEGORY_ALARM_SYSTEM) - self._alarm_code = config[ATTR_CODE] + self._alarm_code = config.get(ATTR_CODE) self.flag_target_state = False serv_alarm = add_preload_service(self, SERV_SECURITY_SYSTEM) diff --git a/tests/components/homekit/test_type_security_systems.py b/tests/components/homekit/test_type_security_systems.py index ec538ce4b50..9c1ff0faf1a 100644 --- a/tests/components/homekit/test_type_security_systems.py +++ b/tests/components/homekit/test_type_security_systems.py @@ -109,8 +109,16 @@ class TestHomekitSecuritySystems(unittest.TestCase): acc = SecuritySystem(self.hass, 'SecuritySystem', acp, 2, config={ATTR_CODE: None}) - acc.run() - + # Set from HomeKit + acc.char_target_state.client_update_value(0) + self.hass.block_till_done() + self.assertEqual( + self.events[0].data[ATTR_SERVICE], 'alarm_arm_home') + self.assertNotIn(ATTR_CODE, self.events[0].data[ATTR_SERVICE_DATA]) + self.assertEqual(acc.char_target_state.value, 0) + + acc = SecuritySystem(self.hass, 'SecuritySystem', acp, + 2, config={}) # Set from HomeKit acc.char_target_state.client_update_value(0) self.hass.block_till_done() From c2bee496e2a83a05116c5e4c637f00aeec2109b6 Mon Sep 17 00:00:00 2001 From: Ryan Bahm Date: Sat, 21 Apr 2018 23:42:18 -0700 Subject: [PATCH 006/155] Add Accuracy to Google Location Sharing (#14039) * Update locationsharinglib to 1.2.1 and add accuracy. * Change indents to match HA style --- homeassistant/components/device_tracker/google_maps.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/device_tracker/google_maps.py b/homeassistant/components/device_tracker/google_maps.py index d1e59293365..b594b23dbeb 100644 --- a/homeassistant/components/device_tracker/google_maps.py +++ b/homeassistant/components/device_tracker/google_maps.py @@ -79,5 +79,6 @@ class GoogleMapsScanner(object): gps=(person.latitude, person.longitude), picture=person.picture_url, source_type=SOURCE_TYPE_GPS, + gps_accuracy=person.accuracy, attributes=attrs ) From 86374ad80927fe8a4054fc750ca96132d83c901b Mon Sep 17 00:00:00 2001 From: David Broadfoot Date: Sun, 22 Apr 2018 20:54:48 +1000 Subject: [PATCH 007/155] bump gogogate2 version (#14044) * bump gogogate2 version * Update - requirements_all --- homeassistant/components/cover/gogogate2.py | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/cover/gogogate2.py b/homeassistant/components/cover/gogogate2.py index 99da248b094..688df62ca6a 100644 --- a/homeassistant/components/cover/gogogate2.py +++ b/homeassistant/components/cover/gogogate2.py @@ -15,7 +15,7 @@ from homeassistant.const import ( CONF_IP_ADDRESS, CONF_NAME) import homeassistant.helpers.config_validation as cv -REQUIREMENTS = ['pygogogate2==0.0.3'] +REQUIREMENTS = ['pygogogate2==0.0.7'] _LOGGER = logging.getLogger(__name__) diff --git a/requirements_all.txt b/requirements_all.txt index d6f811ba68c..8efac1f8ec7 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -778,7 +778,7 @@ pyfritzhome==0.3.7 pyfttt==0.3 # homeassistant.components.cover.gogogate2 -pygogogate2==0.0.3 +pygogogate2==0.0.7 # homeassistant.components.remote.harmony pyharmony==1.0.20 From 1fbc6508715c214e9b3a3077959846324a6423b1 Mon Sep 17 00:00:00 2001 From: Stijn Tintel Date: Sun, 22 Apr 2018 13:55:45 +0300 Subject: [PATCH 008/155] device_tracker.ubus: catch ConnectionError (#14045) When an OpenWrt device monitored via ubus is offline, this causes the log to be flooded with several exceptions. Avoid this by catching requests.exceptions.ConnectionError in addition to requests.exceptions.Timeout. Signed-off-by: Stijn Tintel --- homeassistant/components/device_tracker/ubus.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/device_tracker/ubus.py b/homeassistant/components/device_tracker/ubus.py index 3d7ef5cef6e..f265014657b 100644 --- a/homeassistant/components/device_tracker/ubus.py +++ b/homeassistant/components/device_tracker/ubus.py @@ -207,7 +207,7 @@ def _req_json_rpc(url, session_id, rpcmethod, subsystem, method, **params): try: res = requests.post(url, data=data, timeout=5) - except requests.exceptions.Timeout: + except (requests.exceptions.ConnectionError, requests.exceptions.Timeout): return if res.status_code == 200: From 5d3471269aa2b40525aed7aa1d85447bf71926cd Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sun, 22 Apr 2018 15:00:24 -0400 Subject: [PATCH 009/155] Show a notification when a config entry is discovered (#14022) * Show a notification when a config entry is discovered * update comment * Inline functions * Lint --- homeassistant/config_entries.py | 24 ++++++++++++++++++++- homeassistant/data_entry_flow.py | 2 +- tests/conftest.py | 2 +- tests/test_config_entries.py | 36 ++++++++++++++++++++++++++++++++ tests/test_data_entry_flow.py | 2 +- 5 files changed, 62 insertions(+), 4 deletions(-) diff --git a/homeassistant/config_entries.py b/homeassistant/config_entries.py index 46bb2f7bfe2..b159f01c72b 100644 --- a/homeassistant/config_entries.py +++ b/homeassistant/config_entries.py @@ -141,6 +141,9 @@ ENTRY_STATE_SETUP_ERROR = 'setup_error' ENTRY_STATE_NOT_LOADED = 'not_loaded' ENTRY_STATE_FAILED_UNLOAD = 'failed_unload' +DISCOVERY_NOTIFICATION_ID = 'config_entry_discovery' +DISCOVERY_SOURCES = (data_entry_flow.SOURCE_DISCOVERY,) + class ConfigEntry: """Hold a configuration entry.""" @@ -362,9 +365,19 @@ class ConfigEntries: await async_setup_component( self.hass, entry.domain, self._hass_config) + # Return Entry if they not from a discovery request + if result['source'] not in DISCOVERY_SOURCES: + return entry + + # If no discovery config entries in progress, remove notification. + if not any(ent['source'] in DISCOVERY_SOURCES for ent + in self.hass.config_entries.flow.async_progress()): + self.hass.components.persistent_notification.async_dismiss( + DISCOVERY_NOTIFICATION_ID) + return entry - async def _async_create_flow(self, handler): + async def _async_create_flow(self, handler, *, source, data): """Create a flow for specified handler. Handler key is the domain of the component that we want to setup. @@ -379,6 +392,15 @@ class ConfigEntries: await async_process_deps_reqs( self.hass, self._hass_config, handler, component) + # Create notification. + if source in DISCOVERY_SOURCES: + self.hass.components.persistent_notification.async_create( + title='New devices discovered', + message=("We have discovered new devices on your network. " + "[Check it out](/config/integrations)"), + notification_id=DISCOVERY_NOTIFICATION_ID + ) + return handler() @callback diff --git a/homeassistant/data_entry_flow.py b/homeassistant/data_entry_flow.py index cadec3f3d69..8eb18a3a7e7 100644 --- a/homeassistant/data_entry_flow.py +++ b/homeassistant/data_entry_flow.py @@ -52,7 +52,7 @@ class FlowManager: async def async_init(self, handler, *, source=SOURCE_USER, data=None): """Start a configuration flow.""" - flow = await self._async_create_flow(handler) + flow = await self._async_create_flow(handler, source=source, data=data) flow.hass = self.hass flow.handler = handler flow.flow_id = uuid.uuid4().hex diff --git a/tests/conftest.py b/tests/conftest.py index 269d460ebb6..73e69605eae 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -20,7 +20,7 @@ if os.environ.get('UVLOOP') == '1': import uvloop asyncio.set_event_loop_policy(uvloop.EventLoopPolicy()) -logging.basicConfig() +logging.basicConfig(level=logging.INFO) logging.getLogger('sqlalchemy.engine').setLevel(logging.INFO) diff --git a/tests/test_config_entries.py b/tests/test_config_entries.py index 94b1dcb47da..b46909d7732 100644 --- a/tests/test_config_entries.py +++ b/tests/test_config_entries.py @@ -245,3 +245,39 @@ async def test_forward_entry_does_not_setup_entry_if_setup_fails(hass): await hass.config_entries.async_forward_entry_setup(entry, 'forwarded') assert len(mock_setup.mock_calls) == 1 assert len(mock_setup_entry.mock_calls) == 0 + + +async def test_discovery_notification(hass): + """Test that we create/dismiss a notification when source is discovery.""" + await async_setup_component(hass, 'persistent_notification', {}) + + class TestFlow(data_entry_flow.FlowHandler): + VERSION = 5 + + async def async_step_discovery(self, user_input=None): + if user_input is not None: + return self.async_create_entry( + title='Test Title', + data={ + 'token': 'abcd' + } + ) + return self.async_show_form( + step_id='discovery', + ) + + with patch.dict(config_entries.HANDLERS, {'test': TestFlow}): + result = await hass.config_entries.flow.async_init( + 'test', source=data_entry_flow.SOURCE_DISCOVERY) + + await hass.async_block_till_done() + state = hass.states.get('persistent_notification.config_entry_discovery') + assert state is not None + + result = await hass.config_entries.flow.async_configure( + result['flow_id'], {}) + assert result['type'] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + + await hass.async_block_till_done() + state = hass.states.get('persistent_notification.config_entry_discovery') + assert state is None diff --git a/tests/test_data_entry_flow.py b/tests/test_data_entry_flow.py index 2767e206c30..6d3e41436c5 100644 --- a/tests/test_data_entry_flow.py +++ b/tests/test_data_entry_flow.py @@ -12,7 +12,7 @@ def manager(): handlers = Registry() entries = [] - async def async_create_flow(handler_name): + async def async_create_flow(handler_name, *, source, data): handler = handlers.get(handler_name) if handler is None: From 7f634c6ed060061e7a99f9463012e63d3972839d Mon Sep 17 00:00:00 2001 From: Otto Winter Date: Sun, 22 Apr 2018 22:32:15 +0200 Subject: [PATCH 010/155] Revert cast platform polling mode (#14027) --- homeassistant/components/media_player/cast.py | 64 +++----------- tests/components/media_player/test_cast.py | 85 +------------------ 2 files changed, 13 insertions(+), 136 deletions(-) diff --git a/homeassistant/components/media_player/cast.py b/homeassistant/components/media_player/cast.py index 30d4bd166d0..632ab4214b8 100644 --- a/homeassistant/components/media_player/cast.py +++ b/homeassistant/components/media_player/cast.py @@ -288,8 +288,7 @@ class CastDevice(MediaPlayerDevice): self._chromecast = None # type: Optional[pychromecast.Chromecast] self.cast_status = None self.media_status = None - self.media_status_position = None - self.media_status_position_received = None + self.media_status_received = None self._available = False # type: bool self._status_listener = None # type: Optional[CastStatusListener] @@ -362,26 +361,10 @@ class CastDevice(MediaPlayerDevice): self._chromecast = None self.cast_status = None self.media_status = None - self.media_status_position = None - self.media_status_position_received = None + self.media_status_received = None self._status_listener.invalidate() self._status_listener = None - def update(self): - """Periodically update the properties. - - Even though we receive callbacks for most state changes, some 3rd party - apps don't always send them. Better poll every now and then if the - chromecast is active (i.e. an app is running). - """ - if not self._available: - # Not connected or not available. - return - - if self._chromecast.media_controller.is_active: - # We can only update status if the media namespace is active - self._chromecast.media_controller.update_status() - # ========== Callbacks ========== def new_cast_status(self, cast_status): """Handle updates of the cast status.""" @@ -390,36 +373,8 @@ class CastDevice(MediaPlayerDevice): def new_media_status(self, media_status): """Handle updates of the media status.""" - # Only use media position for playing/paused, - # and for normal playback rate - if (media_status is None or - abs(media_status.playback_rate - 1) > 0.01 or - not (media_status.player_is_playing or - media_status.player_is_paused)): - self.media_status_position = None - self.media_status_position_received = None - else: - # Avoid unnecessary state attribute updates if player_state and - # calculated position stay the same - now = dt_util.utcnow() - do_update = \ - (self.media_status is None or - self.media_status_position is None or - self.media_status.player_state != media_status.player_state) - if not do_update: - if media_status.player_is_playing: - elapsed = now - self.media_status_position_received - do_update = abs(media_status.current_time - - (self.media_status_position + - elapsed.total_seconds())) > 1 - else: - do_update = \ - self.media_status_position != media_status.current_time - if do_update: - self.media_status_position = media_status.current_time - self.media_status_position_received = now - self.media_status = media_status + self.media_status_received = dt_util.utcnow() self.schedule_update_ha_state() def new_connection_status(self, connection_status): @@ -496,8 +451,8 @@ class CastDevice(MediaPlayerDevice): # ========== Properties ========== @property def should_poll(self): - """Polling needed for cast integration, see async_update.""" - return True + """No polling needed.""" + return False @property def name(self): @@ -625,7 +580,12 @@ class CastDevice(MediaPlayerDevice): @property def media_position(self): """Position of current playing media in seconds.""" - return self.media_status_position + if self.media_status is None or \ + not (self.media_status.player_is_playing or + self.media_status.player_is_paused or + self.media_status.player_is_idle): + return None + return self.media_status.current_time @property def media_position_updated_at(self): @@ -633,7 +593,7 @@ class CastDevice(MediaPlayerDevice): Returns value from homeassistant.util.dt.utcnow(). """ - return self.media_status_position_received + return self.media_status_received @property def unique_id(self) -> Optional[str]: diff --git a/tests/components/media_player/test_cast.py b/tests/components/media_player/test_cast.py index 0c0f3906dc2..ee69ec1c85d 100644 --- a/tests/components/media_player/test_cast.py +++ b/tests/components/media_player/test_cast.py @@ -1,7 +1,6 @@ """The tests for the Cast Media player platform.""" # pylint: disable=protected-access import asyncio -import datetime as dt from typing import Optional from unittest.mock import patch, MagicMock, Mock from uuid import UUID @@ -15,8 +14,7 @@ from homeassistant.components.media_player.cast import ChromecastInfo from homeassistant.const import EVENT_HOMEASSISTANT_STOP from homeassistant.helpers.dispatcher import async_dispatcher_connect, \ async_dispatcher_send -from homeassistant.components.media_player import cast, \ - ATTR_MEDIA_POSITION, ATTR_MEDIA_POSITION_UPDATED_AT +from homeassistant.components.media_player import cast from homeassistant.setup import async_setup_component @@ -288,8 +286,6 @@ async def test_entity_media_states(hass: HomeAssistantType): assert entity.unique_id == full_info.uuid media_status = MagicMock(images=None) - media_status.current_time = 0 - media_status.playback_rate = 1 media_status.player_is_playing = True entity.new_media_status(media_status) await hass.async_block_till_done() @@ -324,85 +320,6 @@ async def test_entity_media_states(hass: HomeAssistantType): assert state.state == 'unknown' -async def test_entity_media_position(hass: HomeAssistantType): - """Test various entity media states.""" - info = get_fake_chromecast_info() - full_info = attr.evolve(info, model_name='google home', - friendly_name='Speaker', uuid=FakeUUID) - - with patch('pychromecast.dial.get_device_status', - return_value=full_info): - chromecast, entity = await async_setup_media_player_cast(hass, info) - - media_status = MagicMock(images=None) - media_status.current_time = 10 - media_status.playback_rate = 1 - media_status.player_is_playing = True - media_status.player_is_paused = False - media_status.player_is_idle = False - now = dt.datetime.now(dt.timezone.utc) - with patch('homeassistant.util.dt.utcnow', return_value=now): - entity.new_media_status(media_status) - await hass.async_block_till_done() - state = hass.states.get('media_player.speaker') - assert state.attributes[ATTR_MEDIA_POSITION] == 10 - assert state.attributes[ATTR_MEDIA_POSITION_UPDATED_AT] == now - - media_status.current_time = 15 - now_plus_5 = now + dt.timedelta(seconds=5) - with patch('homeassistant.util.dt.utcnow', return_value=now_plus_5): - entity.new_media_status(media_status) - await hass.async_block_till_done() - state = hass.states.get('media_player.speaker') - assert state.attributes[ATTR_MEDIA_POSITION] == 10 - assert state.attributes[ATTR_MEDIA_POSITION_UPDATED_AT] == now - - media_status.current_time = 20 - with patch('homeassistant.util.dt.utcnow', return_value=now_plus_5): - entity.new_media_status(media_status) - await hass.async_block_till_done() - state = hass.states.get('media_player.speaker') - assert state.attributes[ATTR_MEDIA_POSITION] == 20 - assert state.attributes[ATTR_MEDIA_POSITION_UPDATED_AT] == now_plus_5 - - media_status.current_time = 25 - now_plus_10 = now + dt.timedelta(seconds=10) - media_status.player_is_playing = False - media_status.player_is_paused = True - with patch('homeassistant.util.dt.utcnow', return_value=now_plus_10): - entity.new_media_status(media_status) - await hass.async_block_till_done() - state = hass.states.get('media_player.speaker') - assert state.attributes[ATTR_MEDIA_POSITION] == 25 - assert state.attributes[ATTR_MEDIA_POSITION_UPDATED_AT] == now_plus_10 - - now_plus_15 = now + dt.timedelta(seconds=15) - with patch('homeassistant.util.dt.utcnow', return_value=now_plus_15): - entity.new_media_status(media_status) - await hass.async_block_till_done() - state = hass.states.get('media_player.speaker') - assert state.attributes[ATTR_MEDIA_POSITION] == 25 - assert state.attributes[ATTR_MEDIA_POSITION_UPDATED_AT] == now_plus_10 - - media_status.current_time = 30 - now_plus_20 = now + dt.timedelta(seconds=20) - with patch('homeassistant.util.dt.utcnow', return_value=now_plus_20): - entity.new_media_status(media_status) - await hass.async_block_till_done() - state = hass.states.get('media_player.speaker') - assert state.attributes[ATTR_MEDIA_POSITION] == 30 - assert state.attributes[ATTR_MEDIA_POSITION_UPDATED_AT] == now_plus_20 - - media_status.player_is_paused = False - media_status.player_is_idle = True - with patch('homeassistant.util.dt.utcnow', return_value=now_plus_20): - entity.new_media_status(media_status) - await hass.async_block_till_done() - state = hass.states.get('media_player.speaker') - assert ATTR_MEDIA_POSITION not in state.attributes - assert ATTR_MEDIA_POSITION_UPDATED_AT not in state.attributes - - async def test_switched_host(hass: HomeAssistantType): """Test cast device listens for changed hosts and disconnects old cast.""" info = get_fake_chromecast_info() From e4cb3af76d15567606a147da7d2a3c6c99556199 Mon Sep 17 00:00:00 2001 From: Matthew Garrett Date: Sun, 22 Apr 2018 13:38:01 -0700 Subject: [PATCH 011/155] Handle HomeKit configuration failure more cleanly (#14041) * Handle HomeKit configuration failure more cleanly Add support for handling cases where HomeKit configuration fails, and give the user more information about what to do. * Don't consume the exception for a homekit.UnknownError If we get an UnknownError then we should alert the user but also still generate the backtrace so there's actually something for them to file in a bug report. --- .../components/homekit_controller/__init__.py | 27 ++++++++++++++++--- requirements_all.txt | 2 +- 2 files changed, 25 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/homekit_controller/__init__.py b/homeassistant/components/homekit_controller/__init__.py index c33edd07918..164e7d50e4d 100644 --- a/homeassistant/components/homekit_controller/__init__.py +++ b/homeassistant/components/homekit_controller/__init__.py @@ -14,7 +14,7 @@ from homeassistant.components.discovery import SERVICE_HOMEKIT from homeassistant.helpers import discovery from homeassistant.helpers.entity import Entity -REQUIREMENTS = ['homekit==0.5'] +REQUIREMENTS = ['homekit==0.6'] DOMAIN = 'homekit_controller' HOMEKIT_DIR = '.homekit' @@ -133,10 +133,31 @@ class HKDevice(): import homekit pairing_id = str(uuid.uuid4()) code = callback_data.get('code').strip() - self.pairing_data = homekit.perform_pair_setup( - self.conn, code, pairing_id) + try: + self.pairing_data = homekit.perform_pair_setup(self.conn, code, + pairing_id) + except homekit.exception.UnavailableError: + error_msg = "This accessory is already paired to another device. \ + Please reset the accessory and try again." + _configurator = self.hass.data[DOMAIN+self.hkid] + self.configurator.notify_errors(_configurator, error_msg) + return + except homekit.exception.AuthenticationError: + error_msg = "Incorrect HomeKit code for {}. Please check it and \ + try again.".format(self.model) + _configurator = self.hass.data[DOMAIN+self.hkid] + self.configurator.notify_errors(_configurator, error_msg) + return + except homekit.exception.UnknownError: + error_msg = "Received an unknown error. Please file a bug." + _configurator = self.hass.data[DOMAIN+self.hkid] + self.configurator.notify_errors(_configurator, error_msg) + raise + if self.pairing_data is not None: homekit.save_pairing(self.pairing_file, self.pairing_data) + _configurator = self.hass.data[DOMAIN+self.hkid] + self.configurator.request_done(_configurator) self.accessory_setup() else: error_msg = "Unable to pair, please try again" diff --git a/requirements_all.txt b/requirements_all.txt index 8efac1f8ec7..7946a0d0955 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -389,7 +389,7 @@ holidays==0.9.4 home-assistant-frontend==20180420.0 # homeassistant.components.homekit_controller -# homekit==0.5 +# homekit==0.6 # homeassistant.components.homematicip_cloud homematicip==0.8 From 5fe40530215d376640ecbb7c57bcf30fc7200a8b Mon Sep 17 00:00:00 2001 From: Mark Coombes Date: Mon, 23 Apr 2018 07:52:39 -0400 Subject: [PATCH 012/155] Update device classes for contact sensor HomeKit (#14051) --- homeassistant/components/homekit/const.py | 3 +++ homeassistant/components/homekit/type_sensors.py | 6 +++++- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/homekit/const.py b/homeassistant/components/homekit/const.py index 1c498b4b3b9..59444c75421 100644 --- a/homeassistant/components/homekit/const.py +++ b/homeassistant/components/homekit/const.py @@ -102,6 +102,8 @@ PROP_CELSIUS = {'minValue': -273, 'maxValue': 999} # #### Device Class #### DEVICE_CLASS_CO2 = 'co2' +DEVICE_CLASS_DOOR = 'door' +DEVICE_CLASS_GARAGE_DOOR = 'garage_door' DEVICE_CLASS_GAS = 'gas' DEVICE_CLASS_HUMIDITY = 'humidity' DEVICE_CLASS_LIGHT = 'light' @@ -112,3 +114,4 @@ DEVICE_CLASS_OPENING = 'opening' DEVICE_CLASS_PM25 = 'pm25' DEVICE_CLASS_SMOKE = 'smoke' DEVICE_CLASS_TEMPERATURE = 'temperature' +DEVICE_CLASS_WINDOW = 'window' diff --git a/homeassistant/components/homekit/type_sensors.py b/homeassistant/components/homekit/type_sensors.py index 6aa8d92c0af..7d7bbc5edd6 100644 --- a/homeassistant/components/homekit/type_sensors.py +++ b/homeassistant/components/homekit/type_sensors.py @@ -20,6 +20,7 @@ from .const import ( DEVICE_CLASS_MOTION, SERV_MOTION_SENSOR, CHAR_MOTION_DETECTED, DEVICE_CLASS_OCCUPANCY, SERV_OCCUPANCY_SENSOR, CHAR_OCCUPANCY_DETECTED, DEVICE_CLASS_OPENING, SERV_CONTACT_SENSOR, CHAR_CONTACT_SENSOR_STATE, + DEVICE_CLASS_DOOR, DEVICE_CLASS_GARAGE_DOOR, DEVICE_CLASS_WINDOW, DEVICE_CLASS_SMOKE, SERV_SMOKE_SENSOR, CHAR_SMOKE_DETECTED) from .util import ( convert_to_float, temperature_to_homekit, density_to_air_quality) @@ -29,13 +30,16 @@ _LOGGER = logging.getLogger(__name__) BINARY_SENSOR_SERVICE_MAP = { DEVICE_CLASS_CO2: (SERV_CARBON_DIOXIDE_SENSOR, CHAR_CARBON_DIOXIDE_DETECTED), + DEVICE_CLASS_DOOR: (SERV_CONTACT_SENSOR, CHAR_CONTACT_SENSOR_STATE), + DEVICE_CLASS_GARAGE_DOOR: (SERV_CONTACT_SENSOR, CHAR_CONTACT_SENSOR_STATE), DEVICE_CLASS_GAS: (SERV_CARBON_MONOXIDE_SENSOR, CHAR_CARBON_MONOXIDE_DETECTED), DEVICE_CLASS_MOISTURE: (SERV_LEAK_SENSOR, CHAR_LEAK_DETECTED), DEVICE_CLASS_MOTION: (SERV_MOTION_SENSOR, CHAR_MOTION_DETECTED), DEVICE_CLASS_OCCUPANCY: (SERV_OCCUPANCY_SENSOR, CHAR_OCCUPANCY_DETECTED), DEVICE_CLASS_OPENING: (SERV_CONTACT_SENSOR, CHAR_CONTACT_SENSOR_STATE), - DEVICE_CLASS_SMOKE: (SERV_SMOKE_SENSOR, CHAR_SMOKE_DETECTED)} + DEVICE_CLASS_SMOKE: (SERV_SMOKE_SENSOR, CHAR_SMOKE_DETECTED), + DEVICE_CLASS_WINDOW: (SERV_CONTACT_SENSOR, CHAR_CONTACT_SENSOR_STATE)} @TYPES.register('TemperatureSensor') From 8a10fcd9852411cb3e209965e0286c540498f7c4 Mon Sep 17 00:00:00 2001 From: Kane610 Date: Mon, 23 Apr 2018 18:00:16 +0200 Subject: [PATCH 013/155] deCONZ use forward entry setup (#13990) * Use forward entry setup with light platform * Move sensor to forward entry setup * Use forward entry setup with binary sensors * Use forward entry setup with scene platform * Remove import of unused functionality * Move deconz setup in to setup entry Create initial negative tests for setup entry * Fix hound comment * Improved tests * Add test for scene platform * Add test for binary sensor platform * Add test for light platform * Add test for light platform * Add test for sensor platform * Fix hound comment * More asserts on sensor types --- .../components/binary_sensor/__init__.py | 7 +- .../components/binary_sensor/deconz.py | 8 +- homeassistant/components/deconz/__init__.py | 31 +++---- homeassistant/components/light/deconz.py | 8 +- homeassistant/components/scene/__init__.py | 7 +- homeassistant/components/scene/deconz.py | 8 +- homeassistant/components/sensor/__init__.py | 7 +- homeassistant/components/sensor/deconz.py | 8 +- tests/components/binary_sensor/test_deconz.py | 55 +++++++++++++ tests/components/deconz/test_init.py | 42 +++++++++- tests/components/light/test_deconz.py | 74 +++++++++++++++++ tests/components/scene/test_deconz.py | 57 +++++++++++++ tests/components/sensor/test_deconz.py | 82 +++++++++++++++++++ 13 files changed, 358 insertions(+), 36 deletions(-) create mode 100644 tests/components/binary_sensor/test_deconz.py create mode 100644 tests/components/light/test_deconz.py create mode 100644 tests/components/scene/test_deconz.py create mode 100644 tests/components/sensor/test_deconz.py diff --git a/homeassistant/components/binary_sensor/__init__.py b/homeassistant/components/binary_sensor/__init__.py index ad475be76ca..ee2a0ce712d 100644 --- a/homeassistant/components/binary_sensor/__init__.py +++ b/homeassistant/components/binary_sensor/__init__.py @@ -50,13 +50,18 @@ DEVICE_CLASSES_SCHEMA = vol.All(vol.Lower, vol.In(DEVICE_CLASSES)) async def async_setup(hass, config): """Track states and offer events for binary sensors.""" - component = EntityComponent( + component = hass.data[DOMAIN] = EntityComponent( logging.getLogger(__name__), DOMAIN, hass, SCAN_INTERVAL) await component.async_setup(config) return True +async def async_setup_entry(hass, entry): + """Setup a config entry.""" + return await hass.data[DOMAIN].async_setup_entry(entry) + + # pylint: disable=no-self-use class BinarySensorDevice(Entity): """Represent a binary sensor.""" diff --git a/homeassistant/components/binary_sensor/deconz.py b/homeassistant/components/binary_sensor/deconz.py index ef3ec506e3a..a9a3e28f4be 100644 --- a/homeassistant/components/binary_sensor/deconz.py +++ b/homeassistant/components/binary_sensor/deconz.py @@ -15,10 +15,12 @@ DEPENDENCIES = ['deconz'] async def async_setup_platform(hass, config, async_add_devices, discovery_info=None): - """Set up the deCONZ binary sensor.""" - if discovery_info is None: - return + """Old way of setting up deCONZ binary sensors.""" + pass + +async def async_setup_entry(hass, config_entry, async_add_devices): + """Set up the deCONZ binary sensor.""" from pydeconz.sensor import DECONZ_BINARY_SENSOR sensors = hass.data[DATA_DECONZ].sensors entities = [] diff --git a/homeassistant/components/deconz/__init__.py b/homeassistant/components/deconz/__init__.py index 064725eda95..d68edac9e59 100644 --- a/homeassistant/components/deconz/__init__.py +++ b/homeassistant/components/deconz/__init__.py @@ -9,8 +9,7 @@ import voluptuous as vol from homeassistant.const import ( CONF_API_KEY, CONF_HOST, CONF_PORT, EVENT_HOMEASSISTANT_STOP) from homeassistant.core import callback -from homeassistant.helpers import ( - aiohttp_client, discovery, config_validation as cv) +from homeassistant.helpers import aiohttp_client, config_validation as cv from homeassistant.util.json import load_json # Loading the config flow file will register the flow @@ -58,28 +57,20 @@ async def async_setup(hass, config): return True -async def async_setup_entry(hass, entry): - """Set up a deCONZ bridge for a config entry.""" - if DOMAIN in hass.data: - _LOGGER.error( - "Config entry failed since one deCONZ instance already exists") - return False - result = await async_setup_deconz(hass, None, entry.data) - if result: - return True - return False - - -async def async_setup_deconz(hass, config, deconz_config): - """Set up a deCONZ session. +async def async_setup_entry(hass, config_entry): + """Set up a deCONZ bridge for a config entry. Load config, group, light and sensor data for server information. Start websocket for push notification of state changes from deCONZ. """ - _LOGGER.debug("deCONZ config %s", deconz_config) from pydeconz import DeconzSession + if DOMAIN in hass.data: + _LOGGER.error( + "Config entry failed since one deCONZ instance already exists") + return False + session = aiohttp_client.async_get_clientsession(hass) - deconz = DeconzSession(hass.loop, session, **deconz_config) + deconz = DeconzSession(hass.loop, session, **config_entry.data) result = await deconz.async_load_parameters() if result is False: _LOGGER.error("Failed to communicate with deCONZ") @@ -89,8 +80,8 @@ async def async_setup_deconz(hass, config, deconz_config): hass.data[DATA_DECONZ_ID] = {} for component in ['binary_sensor', 'light', 'scene', 'sensor']: - hass.async_add_job(discovery.async_load_platform( - hass, component, DOMAIN, {}, config)) + hass.async_add_job(hass.config_entries.async_forward_entry_setup( + config_entry, component)) deconz.start() async def async_configure(call): diff --git a/homeassistant/components/light/deconz.py b/homeassistant/components/light/deconz.py index 020f43d9935..36ad572a263 100644 --- a/homeassistant/components/light/deconz.py +++ b/homeassistant/components/light/deconz.py @@ -19,10 +19,12 @@ DEPENDENCIES = ['deconz'] async def async_setup_platform(hass, config, async_add_devices, discovery_info=None): - """Set up the deCONZ light.""" - if discovery_info is None: - return + """Old way of setting up deCONZ lights.""" + pass + +async def async_setup_entry(hass, config_entry, async_add_devices): + """Set up the deCONZ lights from a config entry.""" lights = hass.data[DATA_DECONZ].lights groups = hass.data[DATA_DECONZ].groups entities = [] diff --git a/homeassistant/components/scene/__init__.py b/homeassistant/components/scene/__init__.py index 8f0b9d5c7ab..a3e3a5b38a7 100644 --- a/homeassistant/components/scene/__init__.py +++ b/homeassistant/components/scene/__init__.py @@ -71,7 +71,7 @@ def activate(hass, entity_id=None): async def async_setup(hass, config): """Set up the scenes.""" logger = logging.getLogger(__name__) - component = EntityComponent(logger, DOMAIN, hass) + component = hass.data[DOMAIN] = EntityComponent(logger, DOMAIN, hass) await component.async_setup(config) @@ -90,6 +90,11 @@ async def async_setup(hass, config): return True +async def async_setup_entry(hass, entry): + """Setup a config entry.""" + return await hass.data[DOMAIN].async_setup_entry(entry) + + class Scene(Entity): """A scene is a group of entities and the states we want them to be.""" diff --git a/homeassistant/components/scene/deconz.py b/homeassistant/components/scene/deconz.py index dffc7720776..3eb73736717 100644 --- a/homeassistant/components/scene/deconz.py +++ b/homeassistant/components/scene/deconz.py @@ -13,10 +13,12 @@ DEPENDENCIES = ['deconz'] async def async_setup_platform(hass, config, async_add_devices, discovery_info=None): - """Set up scenes for deCONZ component.""" - if discovery_info is None: - return + """Old way of setting up deCONZ scenes.""" + pass + +async def async_setup_entry(hass, config_entry, async_add_devices): + """Set up scenes for deCONZ component.""" scenes = hass.data[DATA_DECONZ].scenes entities = [] diff --git a/homeassistant/components/sensor/__init__.py b/homeassistant/components/sensor/__init__.py index 2bc35a034f4..2887d32b987 100644 --- a/homeassistant/components/sensor/__init__.py +++ b/homeassistant/components/sensor/__init__.py @@ -31,8 +31,13 @@ DEVICE_CLASSES_SCHEMA = vol.All(vol.Lower, vol.In(DEVICE_CLASSES)) async def async_setup(hass, config): """Track states and offer events for sensors.""" - component = EntityComponent( + component = hass.data[DOMAIN] = EntityComponent( _LOGGER, DOMAIN, hass, SCAN_INTERVAL) await component.async_setup(config) return True + + +async def async_setup_entry(hass, entry): + """Setup a config entry.""" + return await hass.data[DOMAIN].async_setup_entry(entry) diff --git a/homeassistant/components/sensor/deconz.py b/homeassistant/components/sensor/deconz.py index e569c5578ac..dc28a181aa0 100644 --- a/homeassistant/components/sensor/deconz.py +++ b/homeassistant/components/sensor/deconz.py @@ -22,10 +22,12 @@ ATTR_EVENT_ID = 'event_id' async def async_setup_platform(hass, config, async_add_devices, discovery_info=None): - """Set up the deCONZ sensors.""" - if discovery_info is None: - return + """Old way of setting up deCONZ sensors.""" + pass + +async def async_setup_entry(hass, config_entry, async_add_devices): + """Set up the deCONZ sensors.""" from pydeconz.sensor import DECONZ_SENSOR, SWITCH as DECONZ_REMOTE sensors = hass.data[DATA_DECONZ].sensors entities = [] diff --git a/tests/components/binary_sensor/test_deconz.py b/tests/components/binary_sensor/test_deconz.py new file mode 100644 index 00000000000..84ed059e97e --- /dev/null +++ b/tests/components/binary_sensor/test_deconz.py @@ -0,0 +1,55 @@ +"""deCONZ binary sensor platform tests.""" +from unittest.mock import Mock, patch + +from homeassistant import config_entries +from homeassistant.components import deconz + +from tests.common import mock_coro + + +SENSOR = { + "1": { + "id": "Sensor 1 id", + "name": "Sensor 1 name", + "type": "ZHAPresence", + "state": {"presence": False}, + "config": {} + } +} + + +async def setup_bridge(hass, data): + """Load the deCONZ binary sensor platform.""" + from pydeconz import DeconzSession + loop = Mock() + session = Mock() + entry = Mock() + entry.data = {'host': '1.2.3.4', 'port': 80, 'api_key': '1234567890ABCDEF'} + bridge = DeconzSession(loop, session, **entry.data) + with patch('pydeconz.DeconzSession.async_get_state', + return_value=mock_coro(data)): + await bridge.async_load_parameters() + hass.data[deconz.DOMAIN] = bridge + hass.data[deconz.DATA_DECONZ_ID] = {} + config_entry = config_entries.ConfigEntry( + 1, deconz.DOMAIN, 'Mock Title', {'host': 'mock-host'}, 'test') + await hass.config_entries.async_forward_entry_setup( + config_entry, 'binary_sensor') + # To flush out the service call to update the group + await hass.async_block_till_done() + + +async def test_no_binary_sensors(hass): + """Test the update_lights function with some lights.""" + data = {} + await setup_bridge(hass, data) + assert len(hass.data[deconz.DATA_DECONZ_ID]) == 0 + assert len(hass.states.async_all()) == 0 + + +async def test_binary_sensors(hass): + """Test the update_lights function with some lights.""" + data = {"sensors": SENSOR} + await setup_bridge(hass, data) + assert "binary_sensor.sensor_1_name" in hass.data[deconz.DATA_DECONZ_ID] + assert len(hass.states.async_all()) == 1 diff --git a/tests/components/deconz/test_init.py b/tests/components/deconz/test_init.py index cbc8a373972..ce231e3d162 100644 --- a/tests/components/deconz/test_init.py +++ b/tests/components/deconz/test_init.py @@ -1,9 +1,11 @@ """Test deCONZ component setup process.""" -from unittest.mock import patch +from unittest.mock import Mock, patch from homeassistant.setup import async_setup_component from homeassistant.components import deconz +from tests.common import mock_coro + async def test_config_with_host_passed_to_config_entry(hass): """Test that configured options for a host are loaded via config entry.""" @@ -67,3 +69,41 @@ async def test_config_discovery(hass): assert await async_setup_component(hass, deconz.DOMAIN, {}) is True # No flow started assert len(mock_config_entries.flow.mock_calls) == 0 + + +async def test_setup_entry_already_registered_bridge(hass): + """Test setup entry doesn't allow more than one instance of deCONZ.""" + hass.data[deconz.DOMAIN] = True + assert await deconz.async_setup_entry(hass, {}) is False + + +async def test_setup_entry_no_available_bridge(hass): + """Test setup entry fails if deCONZ is not available.""" + entry = Mock() + entry.data = {'host': '1.2.3.4', 'port': 80, 'api_key': '1234567890ABCDEF'} + with patch('pydeconz.DeconzSession.async_load_parameters', + return_value=mock_coro(False)): + assert await deconz.async_setup_entry(hass, entry) is False + + +async def test_setup_entry_successful(hass): + """Test setup entry is successful.""" + entry = Mock() + entry.data = {'host': '1.2.3.4', 'port': 80, 'api_key': '1234567890ABCDEF'} + with patch.object(hass, 'async_add_job') as mock_add_job, \ + patch.object(hass, 'config_entries') as mock_config_entries, \ + patch('pydeconz.DeconzSession.async_load_parameters', + return_value=mock_coro(True)): + assert await deconz.async_setup_entry(hass, entry) is True + assert hass.data[deconz.DOMAIN] + assert hass.data[deconz.DATA_DECONZ_ID] == {} + assert len(mock_add_job.mock_calls) == 4 + assert len(mock_config_entries.async_forward_entry_setup.mock_calls) == 4 + assert mock_config_entries.async_forward_entry_setup.mock_calls[0][1] == \ + (entry, 'binary_sensor') + assert mock_config_entries.async_forward_entry_setup.mock_calls[1][1] == \ + (entry, 'light') + assert mock_config_entries.async_forward_entry_setup.mock_calls[2][1] == \ + (entry, 'scene') + assert mock_config_entries.async_forward_entry_setup.mock_calls[3][1] == \ + (entry, 'sensor') diff --git a/tests/components/light/test_deconz.py b/tests/components/light/test_deconz.py new file mode 100644 index 00000000000..d907697354e --- /dev/null +++ b/tests/components/light/test_deconz.py @@ -0,0 +1,74 @@ +"""deCONZ light platform tests.""" +from unittest.mock import Mock, patch + +from homeassistant import config_entries +from homeassistant.components import deconz + +from tests.common import mock_coro + + +LIGHT = { + "1": { + "id": "Light 1 id", + "name": "Light 1 name", + "state": {} + } +} + +GROUP = { + "1": { + "id": "Group 1 id", + "name": "Group 1 name", + "state": {}, + "action": {}, + "scenes": [], + "lights": [ + "1", + "2" + ] + }, + "2": { + "id": "Group 2 id", + "name": "Group 2 name", + "state": {}, + "action": {}, + "scenes": [] + }, +} + + +async def setup_bridge(hass, data): + """Load the deCONZ light platform.""" + from pydeconz import DeconzSession + loop = Mock() + session = Mock() + entry = Mock() + entry.data = {'host': '1.2.3.4', 'port': 80, 'api_key': '1234567890ABCDEF'} + bridge = DeconzSession(loop, session, **entry.data) + with patch('pydeconz.DeconzSession.async_get_state', + return_value=mock_coro(data)): + await bridge.async_load_parameters() + hass.data[deconz.DOMAIN] = bridge + hass.data[deconz.DATA_DECONZ_ID] = {} + config_entry = config_entries.ConfigEntry( + 1, deconz.DOMAIN, 'Mock Title', {'host': 'mock-host'}, '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() + + +async def test_no_lights_or_groups(hass): + """Test the update_lights function with some lights.""" + data = {} + await setup_bridge(hass, data) + assert len(hass.data[deconz.DATA_DECONZ_ID]) == 0 + assert len(hass.states.async_all()) == 0 + + +async def test_lights_and_groups(hass): + """Test the update_lights function with some lights.""" + await setup_bridge(hass, {"lights": LIGHT, "groups": GROUP}) + assert "light.light_1_name" in hass.data[deconz.DATA_DECONZ_ID] + assert "light.group_1_name" in hass.data[deconz.DATA_DECONZ_ID] + assert "light.group_2_name" not in hass.data[deconz.DATA_DECONZ_ID] + assert len(hass.states.async_all()) == 3 diff --git a/tests/components/scene/test_deconz.py b/tests/components/scene/test_deconz.py new file mode 100644 index 00000000000..53f25808be2 --- /dev/null +++ b/tests/components/scene/test_deconz.py @@ -0,0 +1,57 @@ +"""deCONZ scenes platform tests.""" +from unittest.mock import Mock, patch + +from homeassistant import config_entries +from homeassistant.components import deconz + +from tests.common import mock_coro + + +GROUP = { + "1": { + "id": "Group 1 id", + "name": "Group 1 name", + "state": {}, + "action": {}, + "scenes": [{ + "id": "1", + "name": "Scene 1" + }], + } +} + + +async def setup_bridge(hass, data): + """Load the deCONZ scene platform.""" + from pydeconz import DeconzSession + loop = Mock() + session = Mock() + entry = Mock() + entry.data = {'host': '1.2.3.4', 'port': 80, 'api_key': '1234567890ABCDEF'} + bridge = DeconzSession(loop, session, **entry.data) + with patch('pydeconz.DeconzSession.async_get_state', + return_value=mock_coro(data)): + await bridge.async_load_parameters() + hass.data[deconz.DOMAIN] = bridge + hass.data[deconz.DATA_DECONZ_ID] = {} + config_entry = config_entries.ConfigEntry( + 1, deconz.DOMAIN, 'Mock Title', {'host': 'mock-host'}, 'test') + await hass.config_entries.async_forward_entry_setup(config_entry, 'scene') + # To flush out the service call to update the group + await hass.async_block_till_done() + + +async def test_no_scenes(hass): + """Test the update_lights function with some lights.""" + data = {} + await setup_bridge(hass, data) + assert len(hass.data[deconz.DATA_DECONZ_ID]) == 0 + assert len(hass.states.async_all()) == 0 + + +async def test_scenes(hass): + """Test the update_lights function with some lights.""" + data = {"groups": GROUP} + await setup_bridge(hass, data) + assert "scene.group_1_name_scene_1" in hass.data[deconz.DATA_DECONZ_ID] + assert len(hass.states.async_all()) == 1 diff --git a/tests/components/sensor/test_deconz.py b/tests/components/sensor/test_deconz.py new file mode 100644 index 00000000000..b70fb396686 --- /dev/null +++ b/tests/components/sensor/test_deconz.py @@ -0,0 +1,82 @@ +"""deCONZ sensor platform tests.""" +from unittest.mock import Mock, patch + +from homeassistant import config_entries +from homeassistant.components import deconz + +from tests.common import mock_coro + + +SENSOR = { + "1": { + "id": "Sensor 1 id", + "name": "Sensor 1 name", + "type": "ZHATemperature", + "state": {"temperature": False}, + "config": {} + }, + "2": { + "id": "Sensor 2 id", + "name": "Sensor 2 name", + "type": "ZHAPresence", + "state": {"presence": False}, + "config": {} + }, + "3": { + "id": "Sensor 3 id", + "name": "Sensor 3 name", + "type": "ZHASwitch", + "state": {"buttonevent": 1000}, + "config": {} + }, + "4": { + "id": "Sensor 4 id", + "name": "Sensor 4 name", + "type": "ZHASwitch", + "state": {"buttonevent": 1000}, + "config": {"battery": 100} + } +} + + +async def setup_bridge(hass, data): + """Load the deCONZ sensor platform.""" + from pydeconz import DeconzSession + loop = Mock() + session = Mock() + entry = Mock() + entry.data = {'host': '1.2.3.4', 'port': 80, 'api_key': '1234567890ABCDEF'} + bridge = DeconzSession(loop, session, **entry.data) + with patch('pydeconz.DeconzSession.async_get_state', + return_value=mock_coro(data)): + await bridge.async_load_parameters() + hass.data[deconz.DOMAIN] = bridge + hass.data[deconz.DATA_DECONZ_ID] = {} + config_entry = config_entries.ConfigEntry( + 1, deconz.DOMAIN, 'Mock Title', {'host': 'mock-host'}, 'test') + await hass.config_entries.async_forward_entry_setup(config_entry, 'sensor') + # To flush out the service call to update the group + await hass.async_block_till_done() + + +async def test_no_sensors(hass): + """Test the update_lights function with some lights.""" + data = {} + await setup_bridge(hass, data) + assert len(hass.data[deconz.DATA_DECONZ_ID]) == 0 + assert len(hass.states.async_all()) == 0 + + +async def test_binary_sensors(hass): + """Test the update_lights function with some lights.""" + data = {"sensors": SENSOR} + await setup_bridge(hass, data) + assert "sensor.sensor_1_name" in hass.data[deconz.DATA_DECONZ_ID] + assert "sensor.sensor_2_name" not in hass.data[deconz.DATA_DECONZ_ID] + assert "sensor.sensor_3_name" not in hass.data[deconz.DATA_DECONZ_ID] + assert "sensor.sensor_3_name_battery_level" not in \ + hass.data[deconz.DATA_DECONZ_ID] + assert "sensor.sensor_4_name" not in hass.data[deconz.DATA_DECONZ_ID] + assert "sensor.sensor_4_name_battery_level" in \ + hass.data[deconz.DATA_DECONZ_ID] + assert len(hass.states.async_all()) == 2 From 5ed73fecd3e67e381573bad8b9f03c5d8259a193 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 23 Apr 2018 13:47:06 -0400 Subject: [PATCH 014/155] Order the output of the automation editor (#14019) * Order the output of the automation editor * Lint --- homeassistant/components/config/automation.py | 34 +++++++- tests/components/config/test_automation.py | 83 +++++++++++++++++++ 2 files changed, 116 insertions(+), 1 deletion(-) create mode 100644 tests/components/config/test_automation.py diff --git a/homeassistant/components/config/automation.py b/homeassistant/components/config/automation.py index 6ede91e9b66..1e260854687 100644 --- a/homeassistant/components/config/automation.py +++ b/homeassistant/components/config/automation.py @@ -1,6 +1,8 @@ """Provide configuration end points for Automations.""" import asyncio +from collections import OrderedDict +from homeassistant.const import CONF_ID from homeassistant.components.config import EditIdBasedConfigView from homeassistant.components.automation import ( PLATFORM_SCHEMA, DOMAIN, async_reload) @@ -13,8 +15,38 @@ CONFIG_PATH = 'automations.yaml' @asyncio.coroutine def async_setup(hass): """Set up the Automation config API.""" - hass.http.register_view(EditIdBasedConfigView( + hass.http.register_view(EditAutomationConfigView( DOMAIN, 'config', CONFIG_PATH, cv.string, PLATFORM_SCHEMA, post_write_hook=async_reload )) return True + + +class EditAutomationConfigView(EditIdBasedConfigView): + """Edit automation config.""" + + def _write_value(self, hass, data, config_key, new_value): + """Set value.""" + index = None + for index, cur_value in enumerate(data): + if cur_value[CONF_ID] == config_key: + break + else: + cur_value = OrderedDict() + cur_value[CONF_ID] = config_key + index = len(data) + data.append(cur_value) + + # Iterate through some keys that we want to have ordered in the output + updated_value = OrderedDict() + for key in ('id', 'alias', 'trigger', 'condition', 'action'): + if key in cur_value: + updated_value[key] = cur_value[key] + if key in new_value: + updated_value[key] = new_value[key] + + # We cover all current fields above, but just in case we start + # supporting more fields in the future. + updated_value.update(cur_value) + updated_value.update(new_value) + data[index] = updated_value diff --git a/tests/components/config/test_automation.py b/tests/components/config/test_automation.py new file mode 100644 index 00000000000..327283e74aa --- /dev/null +++ b/tests/components/config/test_automation.py @@ -0,0 +1,83 @@ +"""Test Automation config panel.""" +import json +from unittest.mock import patch + +from homeassistant.bootstrap import async_setup_component +from homeassistant.components import config + + +async def test_get_device_config(hass, aiohttp_client): + """Test getting device config.""" + with patch.object(config, 'SECTIONS', ['automation']): + await async_setup_component(hass, 'config', {}) + + client = await aiohttp_client(hass.http.app) + + def mock_read(path): + """Mock reading data.""" + return [ + { + 'id': 'sun', + }, + { + 'id': 'moon', + } + ] + + with patch('homeassistant.components.config._read', mock_read): + resp = await client.get( + '/api/config/automation/config/moon') + + assert resp.status == 200 + result = await resp.json() + + assert result == {'id': 'moon'} + + +async def test_update_device_config(hass, aiohttp_client): + """Test updating device config.""" + with patch.object(config, 'SECTIONS', ['automation']): + await async_setup_component(hass, 'config', {}) + + client = await aiohttp_client(hass.http.app) + + orig_data = [ + { + 'id': 'sun', + }, + { + 'id': 'moon', + } + ] + + def mock_read(path): + """Mock reading data.""" + return orig_data + + written = [] + + def mock_write(path, data): + """Mock writing data.""" + written.append(data) + + with patch('homeassistant.components.config._read', mock_read), \ + patch('homeassistant.components.config._write', mock_write): + resp = await client.post( + '/api/config/automation/config/moon', data=json.dumps({ + 'trigger': [], + 'action': [], + 'condition': [], + })) + + assert resp.status == 200 + result = await resp.json() + assert result == {'result': 'ok'} + + assert list(orig_data[1]) == ['id', 'trigger', 'condition', 'action'] + assert orig_data[1] == { + 'id': 'moon', + 'trigger': [], + 'condition': [], + 'action': [], + } + assert written[0] == orig_data From 31554e8368d7ce9e62831fddbefce22d2873dd4c Mon Sep 17 00:00:00 2001 From: John Mihalic Date: Mon, 23 Apr 2018 16:43:59 -0400 Subject: [PATCH 015/155] Bump pyEight version to update API & reduce connection issues (#14058) --- homeassistant/components/eight_sleep.py | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/eight_sleep.py b/homeassistant/components/eight_sleep.py index 7ae4ec862bb..3478d5cd08e 100644 --- a/homeassistant/components/eight_sleep.py +++ b/homeassistant/components/eight_sleep.py @@ -22,7 +22,7 @@ from homeassistant.helpers.entity import Entity from homeassistant.helpers.event import async_track_point_in_utc_time from homeassistant.util.dt import utcnow -REQUIREMENTS = ['pyeight==0.0.7'] +REQUIREMENTS = ['pyeight==0.0.8'] _LOGGER = logging.getLogger(__name__) diff --git a/requirements_all.txt b/requirements_all.txt index 7946a0d0955..9f398a37bdf 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -754,7 +754,7 @@ pyeconet==0.0.5 pyedimax==0.1 # homeassistant.components.eight_sleep -pyeight==0.0.7 +pyeight==0.0.8 # homeassistant.components.media_player.emby pyemby==1.5 From ca29224846e7ed8a71d5f8d504007eaabdabaf4c Mon Sep 17 00:00:00 2001 From: thelittlefireman Date: Tue, 24 Apr 2018 18:46:17 +0200 Subject: [PATCH 016/155] Bump locationsharinglib to 1.2.2 (#14070) * Bump locationsharinglib to 1.2.2 * Bump locationsharinglib to 1.2.2 --- homeassistant/components/device_tracker/google_maps.py | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/device_tracker/google_maps.py b/homeassistant/components/device_tracker/google_maps.py index b594b23dbeb..1d0058ed229 100644 --- a/homeassistant/components/device_tracker/google_maps.py +++ b/homeassistant/components/device_tracker/google_maps.py @@ -19,7 +19,7 @@ from homeassistant.util import slugify _LOGGER = logging.getLogger(__name__) -REQUIREMENTS = ['locationsharinglib==1.2.1'] +REQUIREMENTS = ['locationsharinglib==1.2.2'] CREDENTIALS_FILE = '.google_maps_location_sharing.cookies' diff --git a/requirements_all.txt b/requirements_all.txt index 9f398a37bdf..86450c529d5 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -503,7 +503,7 @@ liveboxplaytv==2.0.2 lmnotify==0.0.4 # homeassistant.components.device_tracker.google_maps -locationsharinglib==1.2.1 +locationsharinglib==1.2.2 # homeassistant.components.sensor.luftdaten luftdaten==0.1.3 From 18137733f91d9a2f8d67133eab91bbe8508b9da1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20H=C3=B8yer=20Iversen?= Date: Wed, 25 Apr 2018 04:45:16 +0200 Subject: [PATCH 017/155] Upgrade broadlink lib (#14074) --- homeassistant/components/sensor/broadlink.py | 2 +- homeassistant/components/switch/broadlink.py | 2 +- requirements_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/sensor/broadlink.py b/homeassistant/components/sensor/broadlink.py index 5182ba4530e..9376687cf13 100644 --- a/homeassistant/components/sensor/broadlink.py +++ b/homeassistant/components/sensor/broadlink.py @@ -19,7 +19,7 @@ from homeassistant.helpers.entity import Entity from homeassistant.util import Throttle import homeassistant.helpers.config_validation as cv -REQUIREMENTS = ['broadlink==0.8.0'] +REQUIREMENTS = ['broadlink==0.9.0'] _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/switch/broadlink.py b/homeassistant/components/switch/broadlink.py index 50c334b1f09..46002112177 100644 --- a/homeassistant/components/switch/broadlink.py +++ b/homeassistant/components/switch/broadlink.py @@ -22,7 +22,7 @@ import homeassistant.helpers.config_validation as cv from homeassistant.util import Throttle, slugify from homeassistant.util.dt import utcnow -REQUIREMENTS = ['broadlink==0.8.0'] +REQUIREMENTS = ['broadlink==0.9.0'] _LOGGER = logging.getLogger(__name__) diff --git a/requirements_all.txt b/requirements_all.txt index 86450c529d5..c5c206f9a62 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -180,7 +180,7 @@ botocore==1.7.34 # homeassistant.components.sensor.broadlink # homeassistant.components.switch.broadlink -broadlink==0.8.0 +broadlink==0.9.0 # homeassistant.components.device_tracker.bluetooth_tracker bt_proximity==0.1.2 From 4e97954bbed1a7cc8443bf6f7fa7ef68ebc0103a Mon Sep 17 00:00:00 2001 From: stephanerosi Date: Wed, 25 Apr 2018 04:45:57 +0200 Subject: [PATCH 018/155] Remove excessive debugging in webostv module (#14056) --- .../components/media_player/webostv.py | 35 +++++++++---------- 1 file changed, 16 insertions(+), 19 deletions(-) diff --git a/homeassistant/components/media_player/webostv.py b/homeassistant/components/media_player/webostv.py index d7682a611b9..c3426e45404 100644 --- a/homeassistant/components/media_player/webostv.py +++ b/homeassistant/components/media_player/webostv.py @@ -352,33 +352,30 @@ class LgWebOSDevice(MediaPlayerDevice): if media_type == MEDIA_TYPE_CHANNEL: _LOGGER.debug("Searching channel...") partial_match_channel_id = None + perfect_match_channel_id = None for channel in self._client.get_channels(): - _LOGGER.debug( - "Checking channel number <%s>, name <%s>, id <%s>...", - channel['channelNumber'], - channel['channelName'], - channel['channelId']) if media_id == channel['channelNumber']: - _LOGGER.debug( - "Perfect match on channel number: switching!") - self._client.set_channel(channel['channelId']) - return + perfect_match_channel_id = channel['channelId'] + continue elif media_id.lower() == channel['channelName'].lower(): - _LOGGER.debug( - "Perfect match on channel name: switching!") - self._client.set_channel(channel['channelId']) - return + perfect_match_channel_id = channel['channelId'] + continue elif media_id.lower() in channel['channelName'].lower(): - _LOGGER.debug( - "Partial match on channel name: saving it...") partial_match_channel_id = channel['channelId'] - if partial_match_channel_id is not None: - _LOGGER.debug( - "Using partial match on channel name: switching!") + if perfect_match_channel_id is not None: + _LOGGER.info( + "Switching to channel <%s> with perfect match", + perfect_match_channel_id) + self._client.set_channel(perfect_match_channel_id) + elif partial_match_channel_id is not None: + _LOGGER.info( + "Switching to channel <%s> with partial match", + partial_match_channel_id) self._client.set_channel(partial_match_channel_id) - return + + return def media_play(self): """Send play command.""" From 75fffb6a860e01fe0828723f761e96ebd0ae4536 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 24 Apr 2018 23:18:28 -0400 Subject: [PATCH 019/155] Bump frontend to 20180425.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 87ca8bd2a28..ba487a935a2 100644 --- a/homeassistant/components/frontend/__init__.py +++ b/homeassistant/components/frontend/__init__.py @@ -24,7 +24,7 @@ from homeassistant.core import callback from homeassistant.helpers.translation import async_get_translations from homeassistant.loader import bind_hass -REQUIREMENTS = ['home-assistant-frontend==20180420.0'] +REQUIREMENTS = ['home-assistant-frontend==20180425.0'] DOMAIN = 'frontend' DEPENDENCIES = ['api', 'websocket_api', 'http', 'system_log'] diff --git a/requirements_all.txt b/requirements_all.txt index c5c206f9a62..29783c7ab16 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -386,7 +386,7 @@ hipnotify==1.0.8 holidays==0.9.4 # homeassistant.components.frontend -home-assistant-frontend==20180420.0 +home-assistant-frontend==20180425.0 # homeassistant.components.homekit_controller # homekit==0.6 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index cf4aa2e1b3c..779b304f490 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -81,7 +81,7 @@ hbmqtt==0.9.1 holidays==0.9.4 # homeassistant.components.frontend -home-assistant-frontend==20180420.0 +home-assistant-frontend==20180425.0 # homeassistant.components.influxdb # homeassistant.components.sensor.influxdb From 0a0d34d394e0f460cc772d79834a5aaf8ac907de Mon Sep 17 00:00:00 2001 From: Kerwin Bryant Date: Wed, 25 Apr 2018 13:05:00 +0800 Subject: [PATCH 020/155] Support new Xiaomi Aqara device model names and LAN protocol 2.0 (#13540) --- .../components/binary_sensor/xiaomi_aqara.py | 29 ++++++++++++------- .../components/light/xiaomi_aqara.py | 2 +- .../components/sensor/xiaomi_aqara.py | 4 +-- .../components/switch/xiaomi_aqara.py | 8 +++-- 4 files changed, 28 insertions(+), 15 deletions(-) diff --git a/homeassistant/components/binary_sensor/xiaomi_aqara.py b/homeassistant/components/binary_sensor/xiaomi_aqara.py index 2ed0de66b18..49f716b9eb7 100644 --- a/homeassistant/components/binary_sensor/xiaomi_aqara.py +++ b/homeassistant/components/binary_sensor/xiaomi_aqara.py @@ -25,30 +25,35 @@ def setup_platform(hass, config, add_devices, discovery_info=None): for (_, gateway) in hass.data[PY_XIAOMI_GATEWAY].gateways.items(): for device in gateway.devices['binary_sensor']: model = device['model'] - if model in ['motion', 'sensor_motion.aq2']: + if model in ['motion', 'sensor_motion', 'sensor_motion.aq2']: devices.append(XiaomiMotionSensor(device, hass, gateway)) - elif model in ['magnet', 'sensor_magnet.aq2']: + elif model in ['magnet', 'sensor_magnet', 'sensor_magnet.aq2']: devices.append(XiaomiDoorSensor(device, gateway)) elif model == 'sensor_wleak.aq1': devices.append(XiaomiWaterLeakSensor(device, gateway)) - elif model == 'smoke': + elif model in ['smoke', 'sensor_smoke']: devices.append(XiaomiSmokeSensor(device, gateway)) - elif model == 'natgas': + elif model in ['natgas', 'sensor_natgas']: devices.append(XiaomiNatgasSensor(device, gateway)) - elif model in ['switch', 'sensor_switch.aq2', 'sensor_switch.aq3']: - devices.append(XiaomiButton(device, 'Switch', 'status', + elif model in ['switch', 'sensor_switch', + 'sensor_switch.aq2', 'sensor_switch.aq3']: + if 'proto' not in device or int(device['proto'][0:1]) == 1: + data_key = 'status' + else: + data_key = 'channel_0' + devices.append(XiaomiButton(device, 'Switch', data_key, hass, gateway)) - elif model == '86sw1': + elif model in ['86sw1', 'sensor_86sw1.aq1']: devices.append(XiaomiButton(device, 'Wall Switch', 'channel_0', hass, gateway)) - elif model == '86sw2': + elif model in ['86sw2', 'sensor_86sw2.aq1']: devices.append(XiaomiButton(device, 'Wall Switch (Left)', 'channel_0', hass, gateway)) devices.append(XiaomiButton(device, 'Wall Switch (Right)', 'channel_1', hass, gateway)) devices.append(XiaomiButton(device, 'Wall Switch (Both)', 'dual_channel', hass, gateway)) - elif model == 'cube': + elif model in ['cube', 'sensor_cube']: devices.append(XiaomiCube(device, hass, gateway)) add_devices(devices) @@ -129,8 +134,12 @@ class XiaomiMotionSensor(XiaomiBinarySensor): """Initialize the XiaomiMotionSensor.""" self._hass = hass self._no_motion_since = 0 + if 'proto' not in device or int(device['proto'][0:1]) == 1: + data_key = 'status' + else: + data_key = 'motion_status' XiaomiBinarySensor.__init__(self, device, 'Motion Sensor', xiaomi_hub, - 'status', 'motion') + data_key, 'motion') @property def device_state_attributes(self): diff --git a/homeassistant/components/light/xiaomi_aqara.py b/homeassistant/components/light/xiaomi_aqara.py index 125e791829f..37ae60e3494 100644 --- a/homeassistant/components/light/xiaomi_aqara.py +++ b/homeassistant/components/light/xiaomi_aqara.py @@ -18,7 +18,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): for (_, gateway) in hass.data[PY_XIAOMI_GATEWAY].gateways.items(): for device in gateway.devices['light']: model = device['model'] - if model == 'gateway': + if model in ['gateway', 'gateway.v3']: devices.append(XiaomiGatewayLight(device, 'Gateway Light', gateway)) add_devices(devices) diff --git a/homeassistant/components/sensor/xiaomi_aqara.py b/homeassistant/components/sensor/xiaomi_aqara.py index 33bbdc32308..497a3915154 100644 --- a/homeassistant/components/sensor/xiaomi_aqara.py +++ b/homeassistant/components/sensor/xiaomi_aqara.py @@ -26,7 +26,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): 'temperature', gateway)) devices.append(XiaomiSensor(device, 'Humidity', 'humidity', gateway)) - elif device['model'] == 'weather.v1': + elif device['model'] in ['weather', 'weather.v1']: devices.append(XiaomiSensor(device, 'Temperature', 'temperature', gateway)) devices.append(XiaomiSensor(device, 'Humidity', @@ -36,7 +36,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): elif device['model'] == 'sensor_motion.aq2': devices.append(XiaomiSensor(device, 'Illumination', 'lux', gateway)) - elif device['model'] == 'gateway': + elif device['model'] in ['gateway', 'gateway.v3', 'acpartner.v3']: devices.append(XiaomiSensor(device, 'Illumination', 'illumination', gateway)) add_devices(devices) diff --git a/homeassistant/components/switch/xiaomi_aqara.py b/homeassistant/components/switch/xiaomi_aqara.py index 939fc70660a..4c44d6b2592 100644 --- a/homeassistant/components/switch/xiaomi_aqara.py +++ b/homeassistant/components/switch/xiaomi_aqara.py @@ -26,7 +26,11 @@ def setup_platform(hass, config, add_devices, discovery_info=None): for device in gateway.devices['switch']: model = device['model'] if model == 'plug': - devices.append(XiaomiGenericSwitch(device, "Plug", 'status', + if 'proto' not in device or int(device['proto'][0:1]) == 1: + data_key = 'status' + else: + data_key = 'channel_0' + devices.append(XiaomiGenericSwitch(device, "Plug", data_key, True, gateway)) elif model in ['ctrl_neutral1', 'ctrl_neutral1.aq1']: devices.append(XiaomiGenericSwitch(device, 'Wall Switch', @@ -52,7 +56,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): 'Wall Switch LN Right', 'channel_1', False, gateway)) - elif model in ['86plug', 'ctrl_86plug.aq1']: + elif model in ['86plug', 'ctrl_86plug', 'ctrl_86plug.aq1']: devices.append(XiaomiGenericSwitch(device, 'Wall Plug', 'status', True, gateway)) add_devices(devices) From 558b659f7caad4027e5d696dfa4d581cf5240a41 Mon Sep 17 00:00:00 2001 From: Thijs de Jong Date: Wed, 25 Apr 2018 07:09:45 +0200 Subject: [PATCH 021/155] Add devices to Tahoma (#14075) --- homeassistant/components/cover/tahoma.py | 2 ++ homeassistant/components/sensor/tahoma.py | 5 +++++ homeassistant/components/tahoma.py | 2 ++ 3 files changed, 9 insertions(+) diff --git a/homeassistant/components/cover/tahoma.py b/homeassistant/components/cover/tahoma.py index c99076de851..20625143daf 100644 --- a/homeassistant/components/cover/tahoma.py +++ b/homeassistant/components/cover/tahoma.py @@ -79,5 +79,7 @@ class TahomaCover(TahomaDevice, CoverDevice): if self.tahoma_device.type == \ 'io:RollerShutterWithLowSpeedManagementIOComponent': self.apply_action('setPosition', 'secured') + elif self.tahoma_device.type == 'rts:BlindRTSComponent': + self.apply_action('my') else: self.apply_action('stopIdentify') diff --git a/homeassistant/components/sensor/tahoma.py b/homeassistant/components/sensor/tahoma.py index 39d1cbc75a3..cafa942f65b 100644 --- a/homeassistant/components/sensor/tahoma.py +++ b/homeassistant/components/sensor/tahoma.py @@ -46,6 +46,8 @@ class TahomaSensor(TahomaDevice, Entity): """Return the unit of measurement of this entity, if any.""" if self.tahoma_device.type == 'Temperature Sensor': return None + elif self.tahoma_device.type == 'io:SomfyContactIOSystemSensor': + return None elif self.tahoma_device.type == 'io:LightIOSystemSensor': return 'lux' elif self.tahoma_device.type == 'Humidity Sensor': @@ -57,3 +59,6 @@ class TahomaSensor(TahomaDevice, Entity): if self.tahoma_device.type == 'io:LightIOSystemSensor': self.current_value = self.tahoma_device.active_states[ 'core:LuminanceState'] + if self.tahoma_device.type == 'io:SomfyContactIOSystemSensor': + self.current_value = self.tahoma_device.active_states[ + 'core:ContactState'] diff --git a/homeassistant/components/tahoma.py b/homeassistant/components/tahoma.py index 055e3f410ea..9848d20094c 100644 --- a/homeassistant/components/tahoma.py +++ b/homeassistant/components/tahoma.py @@ -38,6 +38,8 @@ TAHOMA_COMPONENTS = [ TAHOMA_TYPES = { 'rts:RollerShutterRTSComponent': 'cover', 'rts:CurtainRTSComponent': 'cover', + 'rts:BlindRTSComponent': 'cover', + 'rts:VenetianBlindRTSComponent': 'cover', 'io:RollerShutterWithLowSpeedManagementIOComponent': 'cover', 'io:RollerShutterVeluxIOComponent': 'cover', 'io:RollerShutterGenericIOComponent': 'cover', From f23f9465d3072a46e4031e9de250674caf99381d Mon Sep 17 00:00:00 2001 From: Mitko Masarliev Date: Wed, 25 Apr 2018 13:33:47 +0300 Subject: [PATCH 022/155] New sensor domain expiry (#14067) * domain expiry * domain expiry * domain expiry * scan interval * change host to domain --- .coveragerc | 1 + .../components/sensor/domain_expiry.py | 76 +++++++++++++++++++ requirements_all.txt | 3 + 3 files changed, 80 insertions(+) create mode 100644 homeassistant/components/sensor/domain_expiry.py diff --git a/.coveragerc b/.coveragerc index eae6498cd0a..452dbec7559 100644 --- a/.coveragerc +++ b/.coveragerc @@ -574,6 +574,7 @@ omit = homeassistant/components/sensor/discogs.py homeassistant/components/sensor/dnsip.py homeassistant/components/sensor/dovado.py + homeassistant/components/sensor/domain_expiry.py homeassistant/components/sensor/dte_energy_bridge.py homeassistant/components/sensor/dublin_bus_transport.py homeassistant/components/sensor/dwd_weather_warnings.py diff --git a/homeassistant/components/sensor/domain_expiry.py b/homeassistant/components/sensor/domain_expiry.py new file mode 100644 index 00000000000..9364ce041f2 --- /dev/null +++ b/homeassistant/components/sensor/domain_expiry.py @@ -0,0 +1,76 @@ +""" +Counter for the days till domain will expire. + +For more details about this sensor please refer to the documentation at +https://home-assistant.io/components/sensor.domain_expiry/ +""" +import logging +from datetime import datetime, timedelta + +import voluptuous as vol + +import homeassistant.helpers.config_validation as cv +from homeassistant.components.sensor import PLATFORM_SCHEMA +from homeassistant.const import (CONF_NAME, CONF_DOMAIN) +from homeassistant.helpers.entity import Entity + +REQUIREMENTS = ['python-whois==0.6.9'] + +_LOGGER = logging.getLogger(__name__) + +DEFAULT_NAME = 'Domain Expiry' + +SCAN_INTERVAL = timedelta(hours=24) + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Required(CONF_DOMAIN): cv.string, + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string +}) + + +def setup_platform(hass, config, add_devices, discovery_info=None): + """Set up domain expiry sensor.""" + server_name = config.get(CONF_DOMAIN) + sensor_name = config.get(CONF_NAME) + + add_devices([DomainExpiry(sensor_name, server_name)], True) + + +class DomainExpiry(Entity): + """Implementation of the domain expiry sensor.""" + + def __init__(self, sensor_name, server_name): + """Initialize the sensor.""" + self.server_name = server_name + self._name = sensor_name + self._state = None + + @property + def name(self): + """Return the name of the sensor.""" + return self._name + + @property + def unit_of_measurement(self): + """Return the unit this state is expressed in.""" + return 'days' + + @property + def state(self): + """Return the state of the sensor.""" + return self._state + + @property + def icon(self): + """Icon to use in the frontend, if any.""" + return 'mdi:earth' + + def update(self): + """Fetch the domain information.""" + import whois + domain = whois.whois(self.server_name) + if isinstance(domain.expiration_date, datetime): + expiry = domain.expiration_date - datetime.today() + self._state = expiry.days + else: + _LOGGER.error("Cannot get expiry date for %s", self.server_name) diff --git a/requirements_all.txt b/requirements_all.txt index 29783c7ab16..f4810116a40 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1035,6 +1035,9 @@ python-velbus==2.0.11 # homeassistant.components.media_player.vlc python-vlc==1.1.2 +# homeassistant.components.sensor.domain_expiry +python-whois==0.6.9 + # homeassistant.components.wink python-wink==1.7.3 From a94864c86f2230ce0aa0c270f6aa7760f5033800 Mon Sep 17 00:00:00 2001 From: c727 Date: Wed, 25 Apr 2018 12:37:57 +0200 Subject: [PATCH 023/155] Modify weather components for "new" frontend card (#14076) * Enable weather condition for all forecasts (OWM) * Remove entity_picture from BR * Remove summary texts from Dark Sky * Update test_darksky.py --- .../components/weather/buienradar.py | 9 -------- homeassistant/components/weather/darksky.py | 22 ------------------- .../components/weather/openweathermap.py | 8 +++---- tests/components/weather/test_darksky.py | 3 --- 4 files changed, 3 insertions(+), 39 deletions(-) diff --git a/homeassistant/components/weather/buienradar.py b/homeassistant/components/weather/buienradar.py index a49a1664eec..bf1864a9c0f 100644 --- a/homeassistant/components/weather/buienradar.py +++ b/homeassistant/components/weather/buienradar.py @@ -121,15 +121,6 @@ class BrWeather(WeatherEntity): if conditions: return conditions.get(ccode) - @property - def entity_picture(self): - """Return the entity picture to use in the frontend, if any.""" - from buienradar.buienradar import (IMAGE) - - if self._data and self._data.condition: - return self._data.condition.get(IMAGE, None) - return None - @property def temperature(self): """Return the current temperature.""" diff --git a/homeassistant/components/weather/darksky.py b/homeassistant/components/weather/darksky.py index 52aa8c46046..f0712542ea5 100644 --- a/homeassistant/components/weather/darksky.py +++ b/homeassistant/components/weather/darksky.py @@ -25,9 +25,6 @@ _LOGGER = logging.getLogger(__name__) ATTRIBUTION = "Powered by Dark Sky" -ATTR_DAILY_FORECAST_SUMMARY = 'daily_forecast_summary' -ATTR_HOURLY_FORECAST_SUMMARY = 'hourly_forecast_summary' - CONF_UNITS = 'units' DEFAULT_NAME = 'Dark Sky' @@ -122,25 +119,6 @@ class DarkSkyWeather(WeatherEntity): ATTR_FORECAST_TEMP: entry.d.get('temperature')} for entry in self._ds_hourly.data] - @property - def hourly_forecast_summary(self): - """Return a summary of the hourly forecast.""" - return self._ds_hourly.summary - - @property - def daily_forecast_summary(self): - """Return a summary of the daily forecast.""" - return self._ds_daily.summary - - @property - def device_state_attributes(self): - """Return the state attributes.""" - attrs = { - ATTR_DAILY_FORECAST_SUMMARY: self.daily_forecast_summary, - ATTR_HOURLY_FORECAST_SUMMARY: self.hourly_forecast_summary - } - return attrs - def update(self): """Get the latest data from Dark Sky.""" self._dark_sky.update() diff --git a/homeassistant/components/weather/openweathermap.py b/homeassistant/components/weather/openweathermap.py index c8a1bdf8f68..a8e26d39cb3 100644 --- a/homeassistant/components/weather/openweathermap.py +++ b/homeassistant/components/weather/openweathermap.py @@ -28,7 +28,6 @@ DEFAULT_NAME = 'OpenWeatherMap' MIN_TIME_BETWEEN_FORECAST_UPDATES = timedelta(minutes=30) MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=10) -MIN_OFFSET_BETWEEN_FORECAST_CONDITIONS = 3 CONDITION_CLASSES = { 'cloudy': [804], @@ -144,12 +143,11 @@ class OpenWeatherMapWeather(WeatherEntity): data.append({ ATTR_FORECAST_TIME: entry.get_reference_time('unix') * 1000, ATTR_FORECAST_TEMP: - entry.get_temperature('celsius').get('temp') - }) - if (len(data) - 1) % MIN_OFFSET_BETWEEN_FORECAST_CONDITIONS == 0: - data[len(data) - 1][ATTR_FORECAST_CONDITION] = \ + entry.get_temperature('celsius').get('temp'), + ATTR_FORECAST_CONDITION: [k for k, v in CONDITION_CLASSES.items() if entry.get_weather_code() in v][0] + }) return data def update(self): diff --git a/tests/components/weather/test_darksky.py b/tests/components/weather/test_darksky.py index 787aca2ca17..7faa033e0a8 100644 --- a/tests/components/weather/test_darksky.py +++ b/tests/components/weather/test_darksky.py @@ -49,6 +49,3 @@ class TestDarkSky(unittest.TestCase): state = self.hass.states.get('weather.test') self.assertEqual(state.state, 'Clear') - self.assertEqual(state.attributes['daily_forecast_summary'], - 'No precipitation throughout the week, with ' - 'temperatures falling to 66°F on Thursday.') From 241a0793bb804dc00bb9a92ceec02a783dde7059 Mon Sep 17 00:00:00 2001 From: Anders Melchiorsen Date: Wed, 25 Apr 2018 20:31:42 +0200 Subject: [PATCH 024/155] Add Sonos device attribute with grouping information (#13553) --- homeassistant/components/media_player/sonos.py | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/media_player/sonos.py b/homeassistant/components/media_player/sonos.py index b10c761d532..cc10355abe8 100644 --- a/homeassistant/components/media_player/sonos.py +++ b/homeassistant/components/media_player/sonos.py @@ -67,7 +67,7 @@ ATTR_WITH_GROUP = 'with_group' ATTR_NIGHT_SOUND = 'night_sound' ATTR_SPEECH_ENHANCE = 'speech_enhance' -ATTR_IS_COORDINATOR = 'is_coordinator' +ATTR_SONOS_GROUP = 'sonos_group' UPNP_ERRORS_TO_IGNORE = ['701', '711'] @@ -340,6 +340,7 @@ class SonosDevice(MediaPlayerDevice): self._play_mode = None self._name = None self._coordinator = None + self._sonos_group = None self._status = None self._media_duration = None self._media_position = None @@ -688,7 +689,14 @@ class SonosDevice(MediaPlayerDevice): if p.uid != coordinator_uid] if self.unique_id == coordinator_uid: + sonos_group = [] + for uid in (coordinator_uid, *slave_uids): + entity = _get_entity_from_soco_uid(self.hass, uid) + if entity: + sonos_group.append(entity.entity_id) + self._coordinator = None + self._sonos_group = sonos_group self.schedule_update_ha_state() for slave_uid in slave_uids: @@ -696,6 +704,7 @@ class SonosDevice(MediaPlayerDevice): if slave: # pylint: disable=protected-access slave._coordinator = self + slave._sonos_group = sonos_group slave.schedule_update_ha_state() @property @@ -1038,7 +1047,7 @@ class SonosDevice(MediaPlayerDevice): @property def device_state_attributes(self): """Return device specific state attributes.""" - attributes = {ATTR_IS_COORDINATOR: self.is_coordinator} + attributes = {ATTR_SONOS_GROUP: self._sonos_group} if self._night_sound is not None: attributes[ATTR_NIGHT_SOUND] = self._night_sound From 8c2dedab52bba516e92347dc1e0809538f626d52 Mon Sep 17 00:00:00 2001 From: Mattias Welponer Date: Wed, 25 Apr 2018 21:57:44 +0200 Subject: [PATCH 025/155] Re-implement HomematicIP cloud to async (#13468) * Recode to async version of homematicip-rest-api * Remove blank line * Cleanup of access point status class * Fix to loong line * Fix import errors * Bugfix missing wait the _retry_task for sleep command * Update comment * Updates after review * Small updates of logging and property name * Fix DOMAIN and revert back to lowercase snakecase strings * Fix intention and tripple double quotes * Fix travis build * Remove unnecessary state attributes * Fix optional name in configuration * Further reduction of state attributes --- homeassistant/components/homematicip_cloud.py | 222 ++++++++++-------- .../components/sensor/homematicip_cloud.py | 141 +++-------- requirements_all.txt | 2 +- 3 files changed, 159 insertions(+), 206 deletions(-) diff --git a/homeassistant/components/homematicip_cloud.py b/homeassistant/components/homematicip_cloud.py index 180d6943d8a..0ed9fe22e27 100644 --- a/homeassistant/components/homematicip_cloud.py +++ b/homeassistant/components/homematicip_cloud.py @@ -5,143 +5,181 @@ For more details about this component, please refer to the documentation at https://home-assistant.io/components/homematicip_cloud/ """ +import asyncio import logging -from socket import timeout - import voluptuous as vol -from homeassistant.core import callback +from homeassistant.const import EVENT_HOMEASSISTANT_STOP +from homeassistant.helpers.aiohttp_client import async_get_clientsession import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.dispatcher import (dispatcher_send, - async_dispatcher_connect) -from homeassistant.helpers.discovery import load_platform +from homeassistant.helpers.discovery import async_load_platform from homeassistant.helpers.entity import Entity -REQUIREMENTS = ['homematicip==0.8'] +REQUIREMENTS = ['homematicip==0.9.2.4'] _LOGGER = logging.getLogger(__name__) DOMAIN = 'homematicip_cloud' +COMPONENTS = [ + 'sensor' +] + CONF_NAME = 'name' CONF_ACCESSPOINT = 'accesspoint' CONF_AUTHTOKEN = 'authtoken' CONFIG_SCHEMA = vol.Schema({ - vol.Optional(DOMAIN): [vol.Schema({ - vol.Optional(CONF_NAME, default=''): cv.string, + vol.Optional(DOMAIN, default=[]): vol.All(cv.ensure_list, [vol.Schema({ + vol.Optional(CONF_NAME): vol.Any(cv.string), vol.Required(CONF_ACCESSPOINT): cv.string, vol.Required(CONF_AUTHTOKEN): cv.string, - })], + })]), }, extra=vol.ALLOW_EXTRA) -EVENT_HOME_CHANGED = 'homematicip_home_changed' -EVENT_DEVICE_CHANGED = 'homematicip_device_changed' -EVENT_GROUP_CHANGED = 'homematicip_group_changed' -EVENT_SECURITY_CHANGED = 'homematicip_security_changed' -EVENT_JOURNAL_CHANGED = 'homematicip_journal_changed' +HMIP_ACCESS_POINT = 'Access Point' +HMIP_HUB = 'HmIP-HUB' ATTR_HOME_ID = 'home_id' -ATTR_HOME_LABEL = 'home_label' +ATTR_HOME_NAME = 'home_name' ATTR_DEVICE_ID = 'device_id' ATTR_DEVICE_LABEL = 'device_label' ATTR_STATUS_UPDATE = 'status_update' ATTR_FIRMWARE_STATE = 'firmware_state' +ATTR_UNREACHABLE = 'unreachable' ATTR_LOW_BATTERY = 'low_battery' +ATTR_MODEL_TYPE = 'model_type' +ATTR_GROUP_TYPE = 'group_type' +ATTR_DEVICE_RSSI = 'device_rssi' +ATTR_DUTY_CYCLE = 'duty_cycle' +ATTR_CONNECTED = 'connected' ATTR_SABOTAGE = 'sabotage' -ATTR_RSSI = 'rssi' -ATTR_TYPE = 'type' +ATTR_OPERATION_LOCK = 'operation_lock' -def setup(hass, config): +async def async_setup(hass, config): """Set up the HomematicIP component.""" - # pylint: disable=import-error, no-name-in-module - from homematicip.home import Home + from homematicip.base.base_connection import HmipConnectionError hass.data.setdefault(DOMAIN, {}) - homes = hass.data[DOMAIN] accesspoints = config.get(DOMAIN, []) - - def _update_event(events): - """Handle incoming HomeMaticIP events.""" - for event in events: - etype = event['eventType'] - edata = event['data'] - if etype == 'DEVICE_CHANGED': - dispatcher_send(hass, EVENT_DEVICE_CHANGED, edata.id) - elif etype == 'GROUP_CHANGED': - dispatcher_send(hass, EVENT_GROUP_CHANGED, edata.id) - elif etype == 'HOME_CHANGED': - dispatcher_send(hass, EVENT_HOME_CHANGED, edata.id) - elif etype == 'JOURNAL_CHANGED': - dispatcher_send(hass, EVENT_SECURITY_CHANGED, edata.id) - return True - - for device in accesspoints: - name = device.get(CONF_NAME) - accesspoint = device.get(CONF_ACCESSPOINT) - authtoken = device.get(CONF_AUTHTOKEN) - - home = Home() - if name.lower() == 'none': - name = '' - home.label = name + for conf in accesspoints: + _websession = async_get_clientsession(hass) + _hmip = HomematicipConnector(hass, conf, _websession) try: - home.set_auth_token(authtoken) - home.init(accesspoint) - if home.get_current_state(): - _LOGGER.info("Connection to HMIP established") - else: - _LOGGER.warning("Connection to HMIP could not be established") - return False - except timeout: - _LOGGER.warning("Connection to HMIP could not be established") + await _hmip.init() + except HmipConnectionError: + _LOGGER.error('Failed to connect to the HomematicIP server, %s.', + conf.get(CONF_ACCESSPOINT)) return False - homes[home.id] = home - home.onEvent += _update_event - home.enable_events() - _LOGGER.info('HUB name: %s, id: %s', home.label, home.id) - for component in ['sensor']: - load_platform(hass, component, DOMAIN, {'homeid': home.id}, config) + home = _hmip.home + home.name = conf.get(CONF_NAME) + home.label = HMIP_ACCESS_POINT + home.modelType = HMIP_HUB + hass.data[DOMAIN][home.id] = home + _LOGGER.info('Connected to the HomematicIP server, %s.', + conf.get(CONF_ACCESSPOINT)) + homeid = {ATTR_HOME_ID: home.id} + for component in COMPONENTS: + hass.async_add_job(async_load_platform(hass, component, DOMAIN, + homeid, config)) + + hass.loop.create_task(_hmip.connect()) return True +class HomematicipConnector: + """Manages HomematicIP http and websocket connection.""" + + def __init__(self, hass, config, websession): + """Initialize HomematicIP cloud connection.""" + from homematicip.async.home import AsyncHome + self._hass = hass + self._ws_close_requested = False + self._retry_task = None + self._tries = 0 + self._accesspoint = config.get(CONF_ACCESSPOINT) + _authtoken = config.get(CONF_AUTHTOKEN) + + self.home = AsyncHome(hass.loop, websession) + self.home.set_auth_token(_authtoken) + + hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, self.close()) + + async def init(self): + """Initialize connection.""" + await self.home.init(self._accesspoint) + await self.home.get_current_state() + + async def _handle_connection(self): + """Handle websocket connection.""" + from homematicip.base.base_connection import HmipConnectionError + + await self.home.get_current_state() + hmip_events = await self.home.enable_events() + try: + await hmip_events + except HmipConnectionError: + return + + async def connect(self): + """Start websocket connection.""" + self._tries = 0 + while True: + await self._handle_connection() + if self._ws_close_requested: + break + self._ws_close_requested = False + self._tries += 1 + try: + self._retry_task = self._hass.async_add_job(asyncio.sleep( + 2 ** min(9, self._tries), loop=self._hass.loop)) + await self._retry_task + except asyncio.CancelledError: + break + _LOGGER.info('Reconnect (%s) to the HomematicIP cloud server.', + self._tries) + + async def close(self): + """Close the websocket connection.""" + self._ws_close_requested = True + if self._retry_task is not None: + self._retry_task.cancel() + await self.home.disable_events() + _LOGGER.info("Closed connection to HomematicIP cloud server.") + + class HomematicipGenericDevice(Entity): """Representation of an HomematicIP generic device.""" - def __init__(self, home, device): + def __init__(self, home, device, post=None): """Initialize the generic device.""" self._home = home self._device = device + self.post = post + _LOGGER.info('Setting up %s (%s)', self.name, + self._device.modelType) async def async_added_to_hass(self): """Register callbacks.""" - async_dispatcher_connect( - self.hass, EVENT_DEVICE_CHANGED, self._device_changed) + self._device.on_update(self._device_changed) - @callback - def _device_changed(self, deviceid): + def _device_changed(self, json, **kwargs): """Handle device state changes.""" - if deviceid is None or deviceid == self._device.id: - _LOGGER.debug('Event device %s', self._device.label) - self.async_schedule_update_ha_state() - - def _name(self, addon=''): - """Return the name of the device.""" - name = '' - if self._home.label != '': - name += self._home.label + ' ' - name += self._device.label - if addon != '': - name += ' ' + addon - return name + _LOGGER.debug('Event %s (%s)', self.name, self._device.modelType) + self.async_schedule_update_ha_state() @property def name(self): """Return the name of the generic device.""" - return self._name() + name = self._device.label + if self._home.name is not None: + name = "{} {}".format(self._home.name, name) + if self.post is not None: + name = "{} {}".format(name, self.post) + return name @property def should_poll(self): @@ -153,24 +191,10 @@ class HomematicipGenericDevice(Entity): """Device available.""" return not self._device.unreach - def _generic_state_attributes(self): - """Return the state attributes of the generic device.""" - laststatus = '' - if self._device.lastStatusUpdate is not None: - laststatus = self._device.lastStatusUpdate.isoformat() - return { - ATTR_HOME_LABEL: self._home.label, - ATTR_DEVICE_LABEL: self._device.label, - ATTR_HOME_ID: self._device.homeId, - ATTR_DEVICE_ID: self._device.id.lower(), - ATTR_STATUS_UPDATE: laststatus, - ATTR_FIRMWARE_STATE: self._device.updateState.lower(), - ATTR_LOW_BATTERY: self._device.lowBat, - ATTR_RSSI: self._device.rssiDeviceValue, - ATTR_TYPE: self._device.modelType - } - @property def device_state_attributes(self): """Return the state attributes of the generic device.""" - return self._generic_state_attributes() + return { + ATTR_LOW_BATTERY: self._device.lowBat, + ATTR_MODEL_TYPE: self._device.modelType + } diff --git a/homeassistant/components/sensor/homematicip_cloud.py b/homeassistant/components/sensor/homematicip_cloud.py index 1a37aa1ad4e..aa350f7be5d 100644 --- a/homeassistant/components/sensor/homematicip_cloud.py +++ b/homeassistant/components/sensor/homematicip_cloud.py @@ -7,13 +7,10 @@ https://home-assistant.io/components/sensor.homematicip_cloud/ import logging -from homeassistant.core import callback -from homeassistant.helpers.entity import Entity -from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.components.homematicip_cloud import ( - HomematicipGenericDevice, DOMAIN, EVENT_HOME_CHANGED, - ATTR_HOME_LABEL, ATTR_HOME_ID, ATTR_LOW_BATTERY, ATTR_RSSI) -from homeassistant.const import TEMP_CELSIUS, STATE_OK + HomematicipGenericDevice, DOMAIN as HOMEMATICIP_CLOUD_DOMAIN, + ATTR_HOME_ID) +from homeassistant.const import TEMP_CELSIUS _LOGGER = logging.getLogger(__name__) @@ -21,68 +18,49 @@ DEPENDENCIES = ['homematicip_cloud'] ATTR_VALVE_STATE = 'valve_state' ATTR_VALVE_POSITION = 'valve_position' +ATTR_TEMPERATURE = 'temperature' ATTR_TEMPERATURE_OFFSET = 'temperature_offset' +ATTR_HUMIDITY = 'humidity' HMIP_UPTODATE = 'up_to_date' HMIP_VALVE_DONE = 'adaption_done' HMIP_SABOTAGE = 'sabotage' +STATE_OK = 'ok' STATE_LOW_BATTERY = 'low_battery' STATE_SABOTAGE = 'sabotage' -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 HomematicIP sensors devices.""" - # pylint: disable=import-error, no-name-in-module from homematicip.device import ( HeatingThermostat, TemperatureHumiditySensorWithoutDisplay, TemperatureHumiditySensorDisplay) - homeid = discovery_info['homeid'] - home = hass.data[DOMAIN][homeid] - devices = [HomematicipAccesspoint(home)] + if discovery_info is None: + return + home = hass.data[HOMEMATICIP_CLOUD_DOMAIN][discovery_info[ATTR_HOME_ID]] + devices = [HomematicipAccesspointStatus(home)] for device in home.devices: - devices.append(HomematicipDeviceStatus(home, device)) if isinstance(device, HeatingThermostat): devices.append(HomematicipHeatingThermostat(home, device)) - if isinstance(device, TemperatureHumiditySensorWithoutDisplay): - devices.append(HomematicipSensorThermometer(home, device)) - devices.append(HomematicipSensorHumidity(home, device)) - if isinstance(device, TemperatureHumiditySensorDisplay): - devices.append(HomematicipSensorThermometer(home, device)) - devices.append(HomematicipSensorHumidity(home, device)) + if isinstance(device, (TemperatureHumiditySensorDisplay, + TemperatureHumiditySensorWithoutDisplay)): + devices.append(HomematicipTemperatureSensor(home, device)) + devices.append(HomematicipHumiditySensor(home, device)) - if home.devices: - add_devices(devices) + if devices: + async_add_devices(devices) -class HomematicipAccesspoint(Entity): +class HomematicipAccesspointStatus(HomematicipGenericDevice): """Representation of an HomeMaticIP access point.""" def __init__(self, home): - """Initialize the access point sensor.""" - self._home = home - _LOGGER.debug('Setting up access point %s', home.label) - - async def async_added_to_hass(self): - """Register callbacks.""" - async_dispatcher_connect( - self.hass, EVENT_HOME_CHANGED, self._home_changed) - - @callback - def _home_changed(self, deviceid): - """Handle device state changes.""" - if deviceid is None or deviceid == self._home.id: - _LOGGER.debug('Event home %s', self._home.label) - self.async_schedule_update_ha_state() - - @property - def name(self): - """Return the name of the access point device.""" - if self._home.label == '': - return 'Access Point Status' - return '{} Access Point Status'.format(self._home.label) + """Initialize access point device.""" + super().__init__(home, home) @property def icon(self): @@ -102,24 +80,15 @@ class HomematicipAccesspoint(Entity): @property def device_state_attributes(self): """Return the state attributes of the access point.""" - return { - ATTR_HOME_LABEL: self._home.label, - ATTR_HOME_ID: self._home.id, - } + return {} class HomematicipDeviceStatus(HomematicipGenericDevice): """Representation of an HomematicIP device status.""" def __init__(self, home, device): - """Initialize the device.""" - super().__init__(home, device) - _LOGGER.debug('Setting up sensor device status: %s', device.label) - - @property - def name(self): - """Return the name of the device.""" - return self._name('Status') + """Initialize generic status device.""" + super().__init__(home, device, 'Status') @property def icon(self): @@ -150,9 +119,8 @@ class HomematicipHeatingThermostat(HomematicipGenericDevice): """MomematicIP heating thermostat representation.""" def __init__(self, home, device): - """"Initialize heating thermostat.""" - super().__init__(home, device) - _LOGGER.debug('Setting up heating thermostat device: %s', device.label) + """Initialize heating thermostat device.""" + super().__init__(home, device, 'Heating') @property def icon(self): @@ -173,34 +141,18 @@ class HomematicipHeatingThermostat(HomematicipGenericDevice): """Return the unit this state is expressed in.""" return '%' - @property - def device_state_attributes(self): - """Return the state attributes.""" - return { - ATTR_VALVE_STATE: self._device.valveState.lower(), - ATTR_TEMPERATURE_OFFSET: self._device.temperatureOffset, - ATTR_LOW_BATTERY: self._device.lowBat, - ATTR_RSSI: self._device.rssiDeviceValue - } - -class HomematicipSensorHumidity(HomematicipGenericDevice): - """MomematicIP thermometer device.""" +class HomematicipHumiditySensor(HomematicipGenericDevice): + """MomematicIP humidity device.""" def __init__(self, home, device): - """"Initialize the thermometer device.""" - super().__init__(home, device) - _LOGGER.debug('Setting up humidity device: %s', device.label) - - @property - def name(self): - """Return the name of the device.""" - return self._name('Humidity') + """Initialize the thermometer device.""" + super().__init__(home, device, 'Humidity') @property def icon(self): """Return the icon.""" - return 'mdi:water' + return 'mdi:water-percent' @property def state(self): @@ -212,27 +164,13 @@ class HomematicipSensorHumidity(HomematicipGenericDevice): """Return the unit this state is expressed in.""" return '%' - @property - def device_state_attributes(self): - """Return the state attributes.""" - return { - ATTR_LOW_BATTERY: self._device.lowBat, - ATTR_RSSI: self._device.rssiDeviceValue, - } - -class HomematicipSensorThermometer(HomematicipGenericDevice): - """MomematicIP thermometer device.""" +class HomematicipTemperatureSensor(HomematicipGenericDevice): + """MomematicIP the thermometer device.""" def __init__(self, home, device): - """"Initialize the thermometer device.""" - super().__init__(home, device) - _LOGGER.debug('Setting up thermometer device: %s', device.label) - - @property - def name(self): - """Return the name of the device.""" - return self._name('Temperature') + """Initialize the thermometer device.""" + super().__init__(home, device, 'Temperature') @property def icon(self): @@ -248,12 +186,3 @@ class HomematicipSensorThermometer(HomematicipGenericDevice): def unit_of_measurement(self): """Return the unit this state is expressed in.""" return TEMP_CELSIUS - - @property - def device_state_attributes(self): - """Return the state attributes.""" - return { - ATTR_TEMPERATURE_OFFSET: self._device.temperatureOffset, - ATTR_LOW_BATTERY: self._device.lowBat, - ATTR_RSSI: self._device.rssiDeviceValue, - } diff --git a/requirements_all.txt b/requirements_all.txt index f4810116a40..12e178502c4 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -392,7 +392,7 @@ home-assistant-frontend==20180425.0 # homekit==0.6 # homeassistant.components.homematicip_cloud -homematicip==0.8 +homematicip==0.9.2.4 # homeassistant.components.camera.onvif http://github.com/tgaugry/suds-passworddigest-py3/archive/86fc50e39b4d2b8997481967d6a7fe1c57118999.zip#suds-passworddigest-py3==0.1.2a From d7f77354906944adc07cb5373a3a784d861b8486 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20H=C3=B8yer=20Iversen?= Date: Thu, 26 Apr 2018 09:49:35 +0200 Subject: [PATCH 026/155] Fix timezone issue when calculating min/max values in tibber #14009 (#14080) * fix timezone issue in tibber #14009 * remove debug print --- homeassistant/components/sensor/tibber.py | 30 +++++++++++------------ 1 file changed, 14 insertions(+), 16 deletions(-) diff --git a/homeassistant/components/sensor/tibber.py b/homeassistant/components/sensor/tibber.py index 4fb378ac227..42568a6b9ad 100644 --- a/homeassistant/components/sensor/tibber.py +++ b/homeassistant/components/sensor/tibber.py @@ -60,7 +60,7 @@ class TibberSensor(Entity): """Initialize the sensor.""" self._tibber_home = tibber_home self._last_updated = None - self._newest_data_timestamp = None + self._last_data_timestamp = None self._state = None self._is_available = False self._device_state_attributes = {} @@ -70,13 +70,13 @@ class TibberSensor(Entity): async def async_update(self): """Get the latest data and updates the states.""" - now = dt_util.utcnow() + now = dt_util.now() if self._tibber_home.current_price_total and self._last_updated and \ - self._last_updated.hour == now.hour and self._newest_data_timestamp: + self._last_updated.hour == now.hour and self._last_data_timestamp: return - if (not self._newest_data_timestamp or - (self._newest_data_timestamp - now).total_seconds()/3600 < 12 + if (not self._last_data_timestamp or + (self._last_data_timestamp - now).total_seconds()/3600 < 12 or not self._is_available): _LOGGER.debug("Asking for new data.") await self._fetch_data() @@ -135,24 +135,22 @@ class TibberSensor(Entity): def _update_current_price(self): state = None - max_price = None - min_price = None - now = dt_util.utcnow() + max_price = 0 + min_price = 10000 + now = dt_util.now() for key, price_total in self._tibber_home.price_total.items(): - price_time = dt_util.as_utc(dt_util.parse_datetime(key)) + price_time = dt_util.as_local(dt_util.parse_datetime(key)) price_total = round(price_total, 3) time_diff = (now - price_time).total_seconds()/60 - if (not self._newest_data_timestamp or - price_time > self._newest_data_timestamp): - self._newest_data_timestamp = price_time + if (not self._last_data_timestamp or + price_time > self._last_data_timestamp): + self._last_data_timestamp = price_time if 0 <= time_diff < 60: state = price_total self._last_updated = price_time if now.date() == price_time.date(): - if max_price is None or price_total > max_price: - max_price = price_total - if min_price is None or price_total < min_price: - min_price = price_total + max_price = max(max_price, price_total) + min_price = min(min_price, price_total) self._state = state self._device_state_attributes['max_price'] = max_price self._device_state_attributes['min_price'] = min_price From 47e143d5a16832d5854a26a430544c580dc8703e Mon Sep 17 00:00:00 2001 From: Daniel Perna Date: Thu, 26 Apr 2018 19:30:28 +0200 Subject: [PATCH 027/155] Update pyhomematic to 0.1.42 (#14095) * Updated pyhomematic to 0.1.42 * Updated pyhomematic to 0.1.42 --- homeassistant/components/homematic/__init__.py | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/homematic/__init__.py b/homeassistant/components/homematic/__init__.py index 1528943a7f9..0291cc28fed 100644 --- a/homeassistant/components/homematic/__init__.py +++ b/homeassistant/components/homematic/__init__.py @@ -20,7 +20,7 @@ from homeassistant.helpers.entity import Entity import homeassistant.helpers.config_validation as cv from homeassistant.loader import bind_hass -REQUIREMENTS = ['pyhomematic==0.1.41'] +REQUIREMENTS = ['pyhomematic==0.1.42'] DOMAIN = 'homematic' _LOGGER = logging.getLogger(__name__) diff --git a/requirements_all.txt b/requirements_all.txt index 12e178502c4..8650ec153db 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -790,7 +790,7 @@ pyhik==0.1.8 pyhiveapi==0.2.14 # homeassistant.components.homematic -pyhomematic==0.1.41 +pyhomematic==0.1.42 # homeassistant.components.sensor.hydroquebec pyhydroquebec==2.2.2 From ff7b51259e0b3adf80f8be71be594f2c3d0028ac Mon Sep 17 00:00:00 2001 From: GotoCode Date: Thu, 26 Apr 2018 20:35:29 +0300 Subject: [PATCH 028/155] Updated list of AWS regions for Amazon Polly (#14097) Fixes #14052 --- homeassistant/components/tts/amazon_polly.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/tts/amazon_polly.py b/homeassistant/components/tts/amazon_polly.py index d7cf0f1f2d1..46c1a24caa0 100644 --- a/homeassistant/components/tts/amazon_polly.py +++ b/homeassistant/components/tts/amazon_polly.py @@ -20,7 +20,11 @@ CONF_PROFILE_NAME = 'profile_name' ATTR_CREDENTIALS = 'credentials' DEFAULT_REGION = 'us-east-1' -SUPPORTED_REGIONS = ['us-east-1', 'us-east-2', 'us-west-2', 'eu-west-1'] +SUPPORTED_REGIONS = ['us-east-1', 'us-east-2', 'us-west-1', 'us-west-2', + 'ca-central-1', 'eu-west-1', 'eu-central-1', 'eu-west-2', + 'eu-west-3', 'ap-southeast-1', 'ap-southeast-2', + 'ap-northeast-2', 'ap-northeast-1', 'ap-south-1', + 'sa-east-1'] CONF_VOICE = 'voice' CONF_OUTPUT_FORMAT = 'output_format' From 3e18078700e97d7380828a31cfd40ff1d201c432 Mon Sep 17 00:00:00 2001 From: Robin Date: Thu, 26 Apr 2018 20:01:58 +0100 Subject: [PATCH 029/155] Adds update file_path service to local_file camera (#13976) * WIP: Add update_file service to local_file camera * Add event on update * Update local_file.py * Update services.yaml * Fix indent * Update test_local_file.py * Update test_local_file.py * Update test_local_file.py * Update test_local_file.py * Update test_local_file.py * Update test_local_file.py * Update test_local_file.py * Update test_local_file.py * Update test_local_file.py * Update test_local_file.py * Update test_local_file.py * Update test_local_file.py * Update local_file.py * Update test_local_file.py * Update local_file.py * Adds file_path to device_state_attributes * Update test_local_file.py * Update test_local_file.py * Update test_local_file.py * Update test_local_file.py * Update local_file.py * Update test_local_file.py * fixed test_update_file_path * Update local_file.py * Update test_local_file.py * Update test_local_file.py * Update services.yaml * Update local_file.py * Update local_file.py * Update test_local_file.py * Update local_file.py --- homeassistant/components/camera/local_file.py | 47 ++++++++++++++++--- homeassistant/components/camera/services.yaml | 11 ++++- tests/components/camera/test_local_file.py | 37 +++++++++++++++ 3 files changed, 87 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/camera/local_file.py b/homeassistant/components/camera/local_file.py index 95d24c7d42e..95eade48568 100644 --- a/homeassistant/components/camera/local_file.py +++ b/homeassistant/components/camera/local_file.py @@ -11,31 +11,44 @@ import os import voluptuous as vol from homeassistant.const import CONF_NAME -from homeassistant.components.camera import Camera, PLATFORM_SCHEMA +from homeassistant.components.camera import ( + Camera, CAMERA_SERVICE_SCHEMA, DOMAIN, PLATFORM_SCHEMA) from homeassistant.helpers import config_validation as cv _LOGGER = logging.getLogger(__name__) CONF_FILE_PATH = 'file_path' - DEFAULT_NAME = 'Local File' +SERVICE_UPDATE_FILE_PATH = 'local_file_update_file_path' PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Required(CONF_FILE_PATH): cv.string, vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string }) +CAMERA_SERVICE_UPDATE_FILE_PATH = CAMERA_SERVICE_SCHEMA.extend({ + vol.Required(CONF_FILE_PATH): cv.string +}) + def setup_platform(hass, config, add_devices, discovery_info=None): """Set up the Camera that works with local files.""" file_path = config[CONF_FILE_PATH] + camera = LocalFile(config[CONF_NAME], file_path) - # check filepath given is readable - if not os.access(file_path, os.R_OK): - _LOGGER.warning("Could not read camera %s image from file: %s", - config[CONF_NAME], file_path) + def update_file_path_service(call): + """Update the file path.""" + file_path = call.data.get(CONF_FILE_PATH) + camera.update_file_path(file_path) + return True - add_devices([LocalFile(config[CONF_NAME], file_path)]) + hass.services.register( + DOMAIN, + SERVICE_UPDATE_FILE_PATH, + update_file_path_service, + schema=CAMERA_SERVICE_UPDATE_FILE_PATH) + + add_devices([camera]) class LocalFile(Camera): @@ -46,6 +59,7 @@ class LocalFile(Camera): super().__init__() self._name = name + self.check_file_path_access(file_path) self._file_path = file_path # Set content type of local file content, _ = mimetypes.guess_type(file_path) @@ -61,7 +75,26 @@ class LocalFile(Camera): _LOGGER.warning("Could not read camera %s image from file: %s", self._name, self._file_path) + def check_file_path_access(self, file_path): + """Check that filepath given is readable.""" + if not os.access(file_path, os.R_OK): + _LOGGER.warning("Could not read camera %s image from file: %s", + self._name, file_path) + + def update_file_path(self, file_path): + """Update the file_path.""" + self.check_file_path_access(file_path) + self._file_path = file_path + self.schedule_update_ha_state() + @property def name(self): """Return the name of this camera.""" return self._name + + @property + def device_state_attributes(self): + """Return the camera state attributes.""" + return { + 'file_path': self._file_path, + } diff --git a/homeassistant/components/camera/services.yaml b/homeassistant/components/camera/services.yaml index b548f3d1ada..544fd0e6b8a 100644 --- a/homeassistant/components/camera/services.yaml +++ b/homeassistant/components/camera/services.yaml @@ -24,6 +24,16 @@ snapshot: description: Template of a Filename. Variable is entity_id. example: '/tmp/snapshot_{{ entity_id }}' +local_file_update_file_path: + description: Update the file_path for a local_file camera. + fields: + entity_id: + description: Name(s) of entities to update. + example: 'camera.local_file' + file_path: + description: Path to the new image file. + example: '/images/newimage.jpg' + onvif_ptz: description: Pan/Tilt/Zoom service for ONVIF camera. fields: @@ -39,4 +49,3 @@ onvif_ptz: zoom: description: "Zoom. Allowed values: ZOOM_IN, ZOOM_OUT" example: "ZOOM_IN" - diff --git a/tests/components/camera/test_local_file.py b/tests/components/camera/test_local_file.py index 1098c8c9233..40517ea1298 100644 --- a/tests/components/camera/test_local_file.py +++ b/tests/components/camera/test_local_file.py @@ -6,6 +6,9 @@ from unittest import mock # https://bugs.python.org/issue23004 from mock_open import MockOpen +from homeassistant.components.camera import DOMAIN +from homeassistant.components.camera.local_file import ( + SERVICE_UPDATE_FILE_PATH) from homeassistant.setup import async_setup_component from tests.common import mock_registry @@ -115,3 +118,37 @@ def test_camera_content_type(hass, aiohttp_client): assert resp_4.content_type == 'image/jpeg' body = yield from resp_4.text() assert body == image + + +async def test_update_file_path(hass): + """Test update_file_path service.""" + # Setup platform + + mock_registry(hass) + + with mock.patch('os.path.isfile', mock.Mock(return_value=True)), \ + mock.patch('os.access', mock.Mock(return_value=True)): + await async_setup_component(hass, 'camera', { + 'camera': { + 'platform': 'local_file', + 'file_path': 'mock/path.jpg' + } + }) + + # Fetch state and check motion detection attribute + state = hass.states.get('camera.local_file') + assert state.attributes.get('friendly_name') == 'Local File' + assert state.attributes.get('file_path') == 'mock/path.jpg' + + service_data = { + "entity_id": 'camera.local_file', + "file_path": 'new/path.jpg' + } + + await hass.services.async_call(DOMAIN, + SERVICE_UPDATE_FILE_PATH, + service_data) + await hass.async_block_till_done() + + state = hass.states.get('camera.local_file') + assert state.attributes.get('file_path') == 'new/path.jpg' From f5de2b9e5b7d5dc2c75acfa598b580e9d32f1c9b Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 26 Apr 2018 16:39:14 -0400 Subject: [PATCH 030/155] Bump frontend to 20180426 --- 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 ba487a935a2..4a181c00c02 100644 --- a/homeassistant/components/frontend/__init__.py +++ b/homeassistant/components/frontend/__init__.py @@ -24,7 +24,7 @@ from homeassistant.core import callback from homeassistant.helpers.translation import async_get_translations from homeassistant.loader import bind_hass -REQUIREMENTS = ['home-assistant-frontend==20180425.0'] +REQUIREMENTS = ['home-assistant-frontend==20180426.0'] DOMAIN = 'frontend' DEPENDENCIES = ['api', 'websocket_api', 'http', 'system_log'] diff --git a/requirements_all.txt b/requirements_all.txt index 8650ec153db..91b6c71eaa1 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -386,7 +386,7 @@ hipnotify==1.0.8 holidays==0.9.4 # homeassistant.components.frontend -home-assistant-frontend==20180425.0 +home-assistant-frontend==20180426.0 # homeassistant.components.homekit_controller # homekit==0.6 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 779b304f490..876aba4574d 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -81,7 +81,7 @@ hbmqtt==0.9.1 holidays==0.9.4 # homeassistant.components.frontend -home-assistant-frontend==20180425.0 +home-assistant-frontend==20180426.0 # homeassistant.components.influxdb # homeassistant.components.sensor.influxdb From 4b06392442a177e5c0aa5e8cb4f8cfe3383affae Mon Sep 17 00:00:00 2001 From: Kane610 Date: Thu, 26 Apr 2018 23:59:22 +0200 Subject: [PATCH 031/155] Zone component config entry support (#14059) * Initial commit * Add error handling to config flow Change unique identifyer to name Clean up hound comments * Ensure hass home zone is created with correct entity id Fix failing tests * Fix rest of tests * Move zone tests to zone folder Create config flow tests * Add possibility to unload entry * Use hass.data instead of globas * Don't calculate configures zones every loop iteration * No need to know about home zone during setup of entry * Only use name as title * Don't cache hass home zone * Add new tests for setup and setup entry * Break out functionality from init to zone.py * Make hass home zone be created directly * Make sure that config flow doesn't override hass home zone * A newline was missing in const * Configured zones shall not be imported Removed config flow import functionality Improved tests --- .../components/device_tracker/__init__.py | 3 +- .../components/device_tracker/icloud.py | 2 +- .../components/zone/.translations/en.json | 21 ++++ homeassistant/components/zone/__init__.py | 93 ++++++++++++++++ homeassistant/components/zone/config_flow.py | 56 ++++++++++ homeassistant/components/zone/const.py | 5 + homeassistant/components/zone/strings.json | 21 ++++ homeassistant/components/{ => zone}/zone.py | 70 +----------- homeassistant/config_entries.py | 1 + homeassistant/helpers/condition.py | 4 +- tests/components/zone/__init__.py | 1 + tests/components/zone/test_config_flow.py | 55 ++++++++++ .../{test_zone.py => zone/test_init.py} | 102 +++++++++++++++--- 13 files changed, 351 insertions(+), 83 deletions(-) create mode 100644 homeassistant/components/zone/.translations/en.json create mode 100644 homeassistant/components/zone/__init__.py create mode 100644 homeassistant/components/zone/config_flow.py create mode 100644 homeassistant/components/zone/const.py create mode 100644 homeassistant/components/zone/strings.json rename homeassistant/components/{ => zone}/zone.py (57%) create mode 100644 tests/components/zone/__init__.py create mode 100644 tests/components/zone/test_config_flow.py rename tests/components/{test_zone.py => zone/test_init.py} (55%) diff --git a/homeassistant/components/device_tracker/__init__.py b/homeassistant/components/device_tracker/__init__.py index b24f7784faf..2f068481953 100644 --- a/homeassistant/components/device_tracker/__init__.py +++ b/homeassistant/components/device_tracker/__init__.py @@ -15,6 +15,7 @@ from homeassistant.setup import async_prepare_setup_platform from homeassistant.core import callback from homeassistant.loader import bind_hass from homeassistant.components import group, zone +from homeassistant.components.zone.zone import async_active_zone from homeassistant.config import load_yaml_config_file, async_log_exception from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import config_per_platform, discovery @@ -541,7 +542,7 @@ class Device(Entity): elif self.location_name: self._state = self.location_name elif self.gps is not None and self.source_type == SOURCE_TYPE_GPS: - zone_state = zone.async_active_zone( + zone_state = async_active_zone( self.hass, self.gps[0], self.gps[1], self.gps_accuracy) if zone_state is None: self._state = STATE_NOT_HOME diff --git a/homeassistant/components/device_tracker/icloud.py b/homeassistant/components/device_tracker/icloud.py index 781e3674550..5d40f5d533a 100644 --- a/homeassistant/components/device_tracker/icloud.py +++ b/homeassistant/components/device_tracker/icloud.py @@ -13,7 +13,7 @@ import voluptuous as vol from homeassistant.const import CONF_USERNAME, CONF_PASSWORD from homeassistant.components.device_tracker import ( PLATFORM_SCHEMA, DOMAIN, ATTR_ATTRIBUTES, ENTITY_ID_FORMAT, DeviceScanner) -from homeassistant.components.zone import active_zone +from homeassistant.components.zone.zone import active_zone from homeassistant.helpers.event import track_utc_time_change import homeassistant.helpers.config_validation as cv from homeassistant.util import slugify diff --git a/homeassistant/components/zone/.translations/en.json b/homeassistant/components/zone/.translations/en.json new file mode 100644 index 00000000000..ff2c7c07c14 --- /dev/null +++ b/homeassistant/components/zone/.translations/en.json @@ -0,0 +1,21 @@ +{ + "config": { + "title": "Zone", + "step": { + "init": { + "title": "Define zone parameters", + "data": { + "name": "Name", + "latitude": "Latitude", + "longitude": "Longitude", + "radius": "Radius", + "passive": "Passive", + "icon": "Icon" + } + } + }, + "error": { + "name_exists": "Name already exists" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/zone/__init__.py b/homeassistant/components/zone/__init__.py new file mode 100644 index 00000000000..d3628fd57f3 --- /dev/null +++ b/homeassistant/components/zone/__init__.py @@ -0,0 +1,93 @@ +""" +Support for the definition of zones. + +For more details about this component, please refer to the documentation at +https://home-assistant.io/components/zone/ +""" + +import logging + +import voluptuous as vol + +import homeassistant.helpers.config_validation as cv +from homeassistant.const import ( + CONF_NAME, CONF_LATITUDE, CONF_LONGITUDE, CONF_ICON, CONF_RADIUS) +from homeassistant.helpers import config_per_platform +from homeassistant.helpers.entity import async_generate_entity_id +from homeassistant.util import slugify + +from .config_flow import configured_zones +from .const import CONF_PASSIVE, DOMAIN, HOME_ZONE +from .zone import Zone + +_LOGGER = logging.getLogger(__name__) + +DEFAULT_NAME = 'Unnamed zone' +DEFAULT_PASSIVE = False +DEFAULT_RADIUS = 100 + +ENTITY_ID_FORMAT = 'zone.{}' +ENTITY_ID_HOME = ENTITY_ID_FORMAT.format(HOME_ZONE) + +ICON_HOME = 'mdi:home' +ICON_IMPORT = 'mdi:import' + +# The config that zone accepts is the same as if it has platforms. +PLATFORM_SCHEMA = vol.Schema({ + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + vol.Required(CONF_LATITUDE): cv.latitude, + vol.Required(CONF_LONGITUDE): cv.longitude, + vol.Optional(CONF_RADIUS, default=DEFAULT_RADIUS): vol.Coerce(float), + vol.Optional(CONF_PASSIVE, default=DEFAULT_PASSIVE): cv.boolean, + vol.Optional(CONF_ICON): cv.icon, +}, extra=vol.ALLOW_EXTRA) + + +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] = {} + zone_entries = configured_zones(hass) + for _, entry in config_per_platform(config, DOMAIN): + name = slugify(entry[CONF_NAME]) + if 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) + hass.async_add_job(zone.async_update_ha_state()) + hass.data[DOMAIN][name] = zone + + 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, + 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 + + +async def async_setup_entry(hass, config_entry): + """Set up zone as 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)) + zone.entity_id = async_generate_entity_id( + ENTITY_ID_FORMAT, name, None, hass) + hass.async_add_job(zone.async_update_ha_state()) + hass.data[DOMAIN][slugify(name)] = zone + return True + + +async def async_unload_entry(hass, config_entry): + """Unload a config entry.""" + zones = hass.data[DOMAIN] + name = slugify(config_entry.data[CONF_NAME]) + zone = zones.pop(name) + await zone.async_remove() + return True diff --git a/homeassistant/components/zone/config_flow.py b/homeassistant/components/zone/config_flow.py new file mode 100644 index 00000000000..5ec955a48d9 --- /dev/null +++ b/homeassistant/components/zone/config_flow.py @@ -0,0 +1,56 @@ +"""Config flow to configure zone component.""" + +import voluptuous as vol + +import homeassistant.helpers.config_validation as cv +from homeassistant import config_entries, data_entry_flow +from homeassistant.const import ( + CONF_NAME, CONF_LATITUDE, CONF_LONGITUDE, CONF_ICON, CONF_RADIUS) +from homeassistant.core import callback +from homeassistant.util import slugify + +from .const import CONF_PASSIVE, DOMAIN, HOME_ZONE + + +@callback +def configured_zones(hass): + """Return a set of the configured hosts.""" + return set((slugify(entry.data[CONF_NAME])) for + entry in hass.config_entries.async_entries(DOMAIN)) + + +@config_entries.HANDLERS.register(DOMAIN) +class ZoneFlowHandler(data_entry_flow.FlowHandler): + """Zone config flow.""" + + VERSION = 1 + + def __init__(self): + """Initialize zone configuration flow.""" + pass + + async def async_step_init(self, user_input=None): + """Handle a flow start.""" + errors = {} + + if user_input is not None: + name = slugify(user_input[CONF_NAME]) + if name not in configured_zones(self.hass) and name != HOME_ZONE: + return self.async_create_entry( + title=user_input[CONF_NAME], + data=user_input, + ) + errors['base'] = 'name_exists' + + return self.async_show_form( + step_id='init', + data_schema=vol.Schema({ + vol.Required(CONF_NAME): str, + vol.Required(CONF_LATITUDE): cv.latitude, + vol.Required(CONF_LONGITUDE): cv.longitude, + vol.Optional(CONF_RADIUS): vol.Coerce(float), + vol.Optional(CONF_ICON): str, + vol.Optional(CONF_PASSIVE): bool, + }), + errors=errors, + ) diff --git a/homeassistant/components/zone/const.py b/homeassistant/components/zone/const.py new file mode 100644 index 00000000000..b69ba67302a --- /dev/null +++ b/homeassistant/components/zone/const.py @@ -0,0 +1,5 @@ +"""Constants for the zone component.""" + +CONF_PASSIVE = 'passive' +DOMAIN = 'zone' +HOME_ZONE = 'home' diff --git a/homeassistant/components/zone/strings.json b/homeassistant/components/zone/strings.json new file mode 100644 index 00000000000..ff2c7c07c14 --- /dev/null +++ b/homeassistant/components/zone/strings.json @@ -0,0 +1,21 @@ +{ + "config": { + "title": "Zone", + "step": { + "init": { + "title": "Define zone parameters", + "data": { + "name": "Name", + "latitude": "Latitude", + "longitude": "Longitude", + "radius": "Radius", + "passive": "Passive", + "icon": "Icon" + } + } + }, + "error": { + "name_exists": "Name already exists" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/zone.py b/homeassistant/components/zone/zone.py similarity index 57% rename from homeassistant/components/zone.py rename to homeassistant/components/zone/zone.py index b1a94f3809c..b7c2e9ee858 100644 --- a/homeassistant/components/zone.py +++ b/homeassistant/components/zone/zone.py @@ -1,54 +1,18 @@ -""" -Support for the definition of zones. +"""Component entity and functionality.""" -For more details about this component, please refer to the documentation at -https://home-assistant.io/components/zone/ -""" -import asyncio -import logging - -import voluptuous as vol - -import homeassistant.helpers.config_validation as cv -from homeassistant.const import ( - ATTR_HIDDEN, ATTR_LATITUDE, ATTR_LONGITUDE, CONF_NAME, CONF_LATITUDE, - CONF_LONGITUDE, CONF_ICON, CONF_RADIUS) +from homeassistant.const import ATTR_HIDDEN, ATTR_LATITUDE, ATTR_LONGITUDE +from homeassistant.helpers.entity import Entity from homeassistant.loader import bind_hass -from homeassistant.helpers import config_per_platform -from homeassistant.helpers.entity import Entity, async_generate_entity_id from homeassistant.util.async_ import run_callback_threadsafe from homeassistant.util.location import distance -_LOGGER = logging.getLogger(__name__) +from .const import DOMAIN ATTR_PASSIVE = 'passive' ATTR_RADIUS = 'radius' -CONF_PASSIVE = 'passive' - -DEFAULT_NAME = 'Unnamed zone' -DEFAULT_PASSIVE = False -DEFAULT_RADIUS = 100 -DOMAIN = 'zone' - -ENTITY_ID_FORMAT = 'zone.{}' -ENTITY_ID_HOME = ENTITY_ID_FORMAT.format('home') - -ICON_HOME = 'mdi:home' -ICON_IMPORT = 'mdi:import' - STATE = 'zoning' -# The config that zone accepts is the same as if it has platforms. -PLATFORM_SCHEMA = vol.Schema({ - vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, - vol.Required(CONF_LATITUDE): cv.latitude, - vol.Required(CONF_LONGITUDE): cv.longitude, - vol.Optional(CONF_RADIUS, default=DEFAULT_RADIUS): vol.Coerce(float), - vol.Optional(CONF_PASSIVE, default=DEFAULT_PASSIVE): cv.boolean, - vol.Optional(CONF_ICON): cv.icon, -}, extra=vol.ALLOW_EXTRA) - @bind_hass def active_zone(hass, latitude, longitude, radius=0): @@ -104,32 +68,6 @@ def in_zone(zone, latitude, longitude, radius=0): return zone_dist - radius < zone.attributes[ATTR_RADIUS] -@asyncio.coroutine -def async_setup(hass, config): - """Set up the zone.""" - entities = set() - tasks = [] - for _, entry in config_per_platform(config, DOMAIN): - name = entry.get(CONF_NAME) - zone = Zone(hass, 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, name, entities) - tasks.append(zone.async_update_ha_state()) - entities.add(zone.entity_id) - - if ENTITY_ID_HOME not in entities: - zone = Zone(hass, hass.config.location_name, - hass.config.latitude, hass.config.longitude, - DEFAULT_RADIUS, ICON_HOME, False) - zone.entity_id = ENTITY_ID_HOME - tasks.append(zone.async_update_ha_state()) - - yield from asyncio.wait(tasks, loop=hass.loop) - return True - - class Zone(Entity): """Representation of a Zone.""" diff --git a/homeassistant/config_entries.py b/homeassistant/config_entries.py index b159f01c72b..c23d53f2735 100644 --- a/homeassistant/config_entries.py +++ b/homeassistant/config_entries.py @@ -129,6 +129,7 @@ HANDLERS = Registry() FLOWS = [ 'deconz', 'hue', + 'zone', ] diff --git a/homeassistant/helpers/condition.py b/homeassistant/helpers/condition.py index f8f841cc449..cb577e8a9c7 100644 --- a/homeassistant/helpers/condition.py +++ b/homeassistant/helpers/condition.py @@ -393,8 +393,8 @@ def zone(hass, zone_ent, entity): if latitude is None or longitude is None: return False - return zone_cmp.in_zone(zone_ent, latitude, longitude, - entity.attributes.get(ATTR_GPS_ACCURACY, 0)) + return zone_cmp.zone.in_zone(zone_ent, latitude, longitude, + entity.attributes.get(ATTR_GPS_ACCURACY, 0)) def zone_from_config(config, config_validation=True): diff --git a/tests/components/zone/__init__.py b/tests/components/zone/__init__.py new file mode 100644 index 00000000000..2ba325fce81 --- /dev/null +++ b/tests/components/zone/__init__.py @@ -0,0 +1 @@ +"""Tests for the zone component.""" diff --git a/tests/components/zone/test_config_flow.py b/tests/components/zone/test_config_flow.py new file mode 100644 index 00000000000..d8ee6f7c5c0 --- /dev/null +++ b/tests/components/zone/test_config_flow.py @@ -0,0 +1,55 @@ +"""Tests for zone config flow.""" + +from homeassistant.components.zone import config_flow +from homeassistant.components.zone.const import CONF_PASSIVE, DOMAIN, HOME_ZONE +from homeassistant.const import ( + CONF_NAME, CONF_LATITUDE, CONF_LONGITUDE, CONF_ICON, CONF_RADIUS) + +from tests.common import MockConfigEntry + + +async def test_flow_works(hass): + """Test that config flow works.""" + flow = config_flow.ZoneFlowHandler() + flow.hass = hass + + result = await flow.async_step_init(user_input={ + CONF_NAME: 'Name', + CONF_LATITUDE: '1.1', + CONF_LONGITUDE: '2.2', + CONF_RADIUS: '100', + CONF_ICON: 'mdi:home', + CONF_PASSIVE: True + }) + + assert result['type'] == 'create_entry' + assert result['title'] == 'Name' + assert result['data'] == { + CONF_NAME: 'Name', + CONF_LATITUDE: '1.1', + CONF_LONGITUDE: '2.2', + CONF_RADIUS: '100', + CONF_ICON: 'mdi:home', + CONF_PASSIVE: True + } + + +async def test_flow_requires_unique_name(hass): + """Test that config flow verifies that each zones name is unique.""" + MockConfigEntry(domain=DOMAIN, data={ + CONF_NAME: 'Name' + }).add_to_hass(hass) + flow = config_flow.ZoneFlowHandler() + flow.hass = hass + + result = await flow.async_step_init(user_input={CONF_NAME: 'Name'}) + assert result['errors'] == {'base': 'name_exists'} + + +async def test_flow_requires_name_different_from_home(hass): + """Test that config flow verifies that each zones name is unique.""" + flow = config_flow.ZoneFlowHandler() + flow.hass = hass + + result = await flow.async_step_init(user_input={CONF_NAME: HOME_ZONE}) + assert result['errors'] == {'base': 'name_exists'} diff --git a/tests/components/test_zone.py b/tests/components/zone/test_init.py similarity index 55% rename from tests/components/test_zone.py rename to tests/components/zone/test_init.py index 0ea84324362..1c698438f2c 100644 --- a/tests/components/test_zone.py +++ b/tests/components/zone/test_init.py @@ -1,10 +1,42 @@ """Test zone component.""" + import unittest +from unittest.mock import Mock from homeassistant import setup from homeassistant.components import zone from tests.common import get_test_home_assistant +from tests.common import MockConfigEntry + + +async def test_setup_entry_successful(hass): + """Test setup entry is successful.""" + entry = Mock() + entry.data = { + 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] = {} + assert await zone.async_setup_entry(hass, entry) is True + assert 'test_zone' in hass.data[zone.DOMAIN] + + +async def test_unload_entry_successful(hass): + """Test unload entry is successful.""" + entry = Mock() + entry.data = { + zone.CONF_NAME: 'Test Zone', + zone.CONF_LATITUDE: 1.1, + zone.CONF_LONGITUDE: -2.2 + } + hass.data[zone.DOMAIN] = {} + assert await zone.async_setup_entry(hass, entry) is True + assert await zone.async_unload_entry(hass, entry) is True + assert not hass.data[zone.DOMAIN] class TestComponentZone(unittest.TestCase): @@ -20,18 +52,17 @@ class TestComponentZone(unittest.TestCase): def test_setup_no_zones_still_adds_home_zone(self): """Test if no config is passed in we still get the home zone.""" - assert setup.setup_component(self.hass, zone.DOMAIN, - {'zone': None}) - + assert setup.setup_component(self.hass, zone.DOMAIN, {'zone': None}) assert len(self.hass.states.entity_ids('zone')) == 1 state = self.hass.states.get('zone.home') assert self.hass.config.location_name == state.name 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 setup.""" + """Test a successful setup.""" info = { 'name': 'Test Zone', 'latitude': 32.880837, @@ -39,16 +70,61 @@ class TestComponentZone(unittest.TestCase): 'radius': 250, 'passive': True } - assert setup.setup_component(self.hass, zone.DOMAIN, { - 'zone': info - }) + assert setup.setup_component(self.hass, zone.DOMAIN, {'zone': info}) + assert len(self.hass.states.entity_ids('zone')) == 2 state = self.hass.states.get('zone.test_zone') assert info['name'] == state.name assert info['latitude'] == state.attributes['latitude'] 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.""" + info = { + 'name': 'Home', + 'latitude': 1.1, + 'longitude': -2.2, + } + assert setup.setup_component(self.hass, zone.DOMAIN, {'zone': info}) + + 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_registered_zone_skips_home_zone(self): + """Test that config entry named home should override hass home zone.""" + entry = MockConfigEntry(domain=zone.DOMAIN, data={ + zone.CONF_NAME: 'home' + }) + 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.""" + entry = MockConfigEntry(domain=zone.DOMAIN, data={ + zone.CONF_NAME: 'Test Zone' + }) + entry.add_to_hass(self.hass) + info = { + 'name': 'Test Zone', + 'latitude': 1.1, + 'longitude': -2.2, + } + assert setup.setup_component(self.hass, zone.DOMAIN, {'zone': info}) + + 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.""" @@ -64,7 +140,7 @@ class TestComponentZone(unittest.TestCase): ] }) self.hass.block_till_done() - active = zone.active_zone(self.hass, 32.880600, -117.237561) + active = zone.zone.active_zone(self.hass, 32.880600, -117.237561) assert active is None def test_active_zone_skips_passive_zones_2(self): @@ -80,7 +156,7 @@ class TestComponentZone(unittest.TestCase): ] }) self.hass.block_till_done() - active = zone.active_zone(self.hass, 32.880700, -117.237561) + active = zone.zone.active_zone(self.hass, 32.880700, -117.237561) assert 'zone.active_zone' == active.entity_id def test_active_zone_prefers_smaller_zone_if_same_distance(self): @@ -104,7 +180,7 @@ class TestComponentZone(unittest.TestCase): ] }) - active = zone.active_zone(self.hass, latitude, longitude) + active = zone.zone.active_zone(self.hass, latitude, longitude) assert 'zone.small_zone' == active.entity_id def test_active_zone_prefers_smaller_zone_if_same_distance_2(self): @@ -122,7 +198,7 @@ class TestComponentZone(unittest.TestCase): ] }) - active = zone.active_zone(self.hass, latitude, longitude) + active = zone.zone.active_zone(self.hass, latitude, longitude) assert 'zone.smallest_zone' == active.entity_id def test_in_zone_works_for_passive_zones(self): @@ -141,5 +217,5 @@ class TestComponentZone(unittest.TestCase): ] }) - assert zone.in_zone(self.hass.states.get('zone.passive_zone'), - latitude, longitude) + assert zone.zone.in_zone(self.hass.states.get('zone.passive_zone'), + latitude, longitude) From 9d1f9fe20490672eca410dbd6b2d64534805fbd5 Mon Sep 17 00:00:00 2001 From: Otto Winter Date: Fri, 27 Apr 2018 13:15:45 +0200 Subject: [PATCH 032/155] Improve MQTT topic validation (#14099) * Improve MQTT topic validation * Fix test * Improve length check --- homeassistant/components/mqtt/__init__.py | 56 ++++++++++++++++++----- tests/components/mqtt/test_init.py | 52 +++++++++++++++++++-- 2 files changed, 93 insertions(+), 15 deletions(-) diff --git a/homeassistant/components/mqtt/__init__.py b/homeassistant/components/mqtt/__init__.py index b81a4fc16a7..55d99a0817e 100644 --- a/homeassistant/components/mqtt/__init__.py +++ b/homeassistant/components/mqtt/__init__.py @@ -90,22 +90,52 @@ ATTR_RETAIN = CONF_RETAIN MAX_RECONNECT_WAIT = 300 # seconds -def valid_subscribe_topic(value: Any, invalid_chars='\0') -> str: - """Validate that we can subscribe using this MQTT topic.""" +def valid_topic(value: Any) -> str: + """Validate that this is a valid topic name/filter.""" value = cv.string(value) - if all(c not in value for c in invalid_chars): - return vol.Length(min=1, max=65535)(value) - raise vol.Invalid('Invalid MQTT topic name') + try: + raw_value = value.encode('utf-8') + except UnicodeError: + raise vol.Invalid("MQTT topic name/filter must be valid UTF-8 string.") + if not raw_value: + raise vol.Invalid("MQTT topic name/filter must not be empty.") + if len(raw_value) > 65535: + raise vol.Invalid("MQTT topic name/filter must not be longer than " + "65535 encoded bytes.") + if '\0' in value: + raise vol.Invalid("MQTT topic name/filter must not contain null " + "character.") + return value + + +def valid_subscribe_topic(value: Any) -> str: + """Validate that we can subscribe using this MQTT topic.""" + value = valid_topic(value) + for i in (i for i, c in enumerate(value) if c == '+'): + if (i > 0 and value[i - 1] != '/') or \ + (i < len(value) - 1 and value[i + 1] != '/'): + raise vol.Invalid("Single-level wildcard must occupy an entire " + "level of the filter") + + index = value.find('#') + if index != -1: + if index != len(value) - 1: + # If there are multiple wildcards, this will also trigger + raise vol.Invalid("Multi-level wildcard must be the last " + "character in the topic filter.") + if len(value) > 1 and value[index - 1] != '/': + raise vol.Invalid("Multi-level wildcard must be after a topic " + "level separator.") + + return value def valid_publish_topic(value: Any) -> str: """Validate that we can publish using this MQTT topic.""" - return valid_subscribe_topic(value, invalid_chars='#+\0') - - -def valid_discovery_topic(value: Any) -> str: - """Validate a discovery topic.""" - return valid_subscribe_topic(value, invalid_chars='#+\0/') + value = valid_topic(value) + if '+' in value or '#' in value: + raise vol.Invalid("Wildcards can not be used in topic names") + return value _VALID_QOS_SCHEMA = vol.All(vol.Coerce(int), vol.In([0, 1, 2])) @@ -143,8 +173,10 @@ CONFIG_SCHEMA = vol.Schema({ vol.Optional(CONF_WILL_MESSAGE): MQTT_WILL_BIRTH_SCHEMA, vol.Optional(CONF_BIRTH_MESSAGE): MQTT_WILL_BIRTH_SCHEMA, vol.Optional(CONF_DISCOVERY, default=DEFAULT_DISCOVERY): cv.boolean, + # discovery_prefix must be a valid publish topic because if no + # state topic is specified, it will be created with the given prefix. vol.Optional(CONF_DISCOVERY_PREFIX, - default=DEFAULT_DISCOVERY_PREFIX): valid_discovery_topic, + default=DEFAULT_DISCOVERY_PREFIX): valid_publish_topic, }), }, extra=vol.ALLOW_EXTRA) diff --git a/tests/components/mqtt/test_init.py b/tests/components/mqtt/test_init.py index b25479bb75a..05c5de71b8c 100644 --- a/tests/components/mqtt/test_init.py +++ b/tests/components/mqtt/test_init.py @@ -131,10 +131,56 @@ class TestMQTTComponent(unittest.TestCase): self.hass.data['mqtt'].async_publish.call_args[0][2], 2) self.assertFalse(self.hass.data['mqtt'].async_publish.call_args[0][3]) - def test_invalid_mqtt_topics(self): - """Test invalid topics.""" + def test_validate_topic(self): + """Test topic name/filter validation.""" + # Invalid UTF-8, must not contain U+D800 to U+DFFF. + self.assertRaises(vol.Invalid, mqtt.valid_topic, '\ud800') + self.assertRaises(vol.Invalid, mqtt.valid_topic, '\udfff') + # Topic MUST NOT be empty + self.assertRaises(vol.Invalid, mqtt.valid_topic, '') + # Topic MUST NOT be longer than 65535 encoded bytes. + self.assertRaises(vol.Invalid, mqtt.valid_topic, 'ü' * 32768) + # UTF-8 MUST NOT include null character + self.assertRaises(vol.Invalid, mqtt.valid_topic, 'bad\0one') + + # Topics "SHOULD NOT" include these special characters + # (not MUST NOT, RFC2119). The receiver MAY close the connection. + mqtt.valid_topic('\u0001') + mqtt.valid_topic('\u001F') + mqtt.valid_topic('\u009F') + mqtt.valid_topic('\u009F') + mqtt.valid_topic('\uffff') + + def test_validate_subscribe_topic(self): + """Test invalid subscribe topics.""" + mqtt.valid_subscribe_topic('#') + mqtt.valid_subscribe_topic('sport/#') + self.assertRaises(vol.Invalid, mqtt.valid_subscribe_topic, 'sport/#/') + self.assertRaises(vol.Invalid, mqtt.valid_subscribe_topic, 'foo/bar#') + self.assertRaises(vol.Invalid, mqtt.valid_subscribe_topic, 'foo/#/bar') + + mqtt.valid_subscribe_topic('+') + mqtt.valid_subscribe_topic('+/tennis/#') + self.assertRaises(vol.Invalid, mqtt.valid_subscribe_topic, 'sport+') + self.assertRaises(vol.Invalid, mqtt.valid_subscribe_topic, 'sport+/') + self.assertRaises(vol.Invalid, mqtt.valid_subscribe_topic, 'sport/+1') + self.assertRaises(vol.Invalid, mqtt.valid_subscribe_topic, 'sport/+#') + self.assertRaises(vol.Invalid, mqtt.valid_subscribe_topic, 'bad+topic') + mqtt.valid_subscribe_topic('sport/+/player1') + mqtt.valid_subscribe_topic('/finance') + mqtt.valid_subscribe_topic('+/+') + mqtt.valid_subscribe_topic('$SYS/#') + + def test_validate_publish_topic(self): + """Test invalid publish topics.""" + self.assertRaises(vol.Invalid, mqtt.valid_publish_topic, 'pub+') + self.assertRaises(vol.Invalid, mqtt.valid_publish_topic, 'pub/+') + self.assertRaises(vol.Invalid, mqtt.valid_publish_topic, '1#') self.assertRaises(vol.Invalid, mqtt.valid_publish_topic, 'bad+topic') - self.assertRaises(vol.Invalid, mqtt.valid_subscribe_topic, 'bad\0one') + mqtt.valid_publish_topic('//') + + # Topic names beginning with $ SHOULD NOT be used, but can + mqtt.valid_publish_topic('$SYS/') # pylint: disable=invalid-name From 0b350993b5a9cb1fd2a8229bc47e7149e18e9dba Mon Sep 17 00:00:00 2001 From: Anders Melchiorsen Date: Fri, 27 Apr 2018 13:18:58 +0200 Subject: [PATCH 033/155] Improve precision of Hue color state (#14113) --- homeassistant/components/light/hue.py | 20 +++++--------------- tests/components/light/test_hue.py | 17 ++--------------- 2 files changed, 7 insertions(+), 30 deletions(-) diff --git a/homeassistant/components/light/hue.py b/homeassistant/components/light/hue.py index 6eb8de99c99..6b4908b02d4 100644 --- a/homeassistant/components/light/hue.py +++ b/homeassistant/components/light/hue.py @@ -242,26 +242,16 @@ class HueLight(Light): @property def hs_color(self): """Return the hs color value.""" - # pylint: disable=redefined-outer-name mode = self._color_mode - - if mode not in ('hs', 'xy'): - return - source = self.light.action if self.is_group else self.light.state - hue = source.get('hue') - sat = source.get('sat') + if mode == 'xy' and 'xy' in source: + return color.color_xy_to_hs(*source['xy']) - # Sometimes the state will not include valid hue/sat values. - # Reported as issue 13434 - if hue is not None and sat is not None: - return hue / 65535 * 360, sat / 255 * 100 + if mode == 'hs' and 'hue' in source and 'sat' in source: + return source['hue'] / 65535 * 360, source['sat'] / 255 * 100 - if 'xy' not in source: - return None - - return color.color_xy_to_hs(*source['xy']) + return None @property def color_temp(self): diff --git a/tests/components/light/test_hue.py b/tests/components/light/test_hue.py index 712cd17a7c7..d36548e1e91 100644 --- a/tests/components/light/test_hue.py +++ b/tests/components/light/test_hue.py @@ -237,7 +237,7 @@ async def test_lights(hass, mock_bridge): assert lamp_1 is not None assert lamp_1.state == 'on' assert lamp_1.attributes['brightness'] == 144 - assert lamp_1.attributes['hs_color'] == (71.896, 83.137) + assert lamp_1.attributes['hs_color'] == (36.067, 69.804) lamp_2 = hass.states.get('light.hue_lamp_2') assert lamp_2 is not None @@ -253,7 +253,7 @@ async def test_lights_color_mode(hass, mock_bridge): assert lamp_1 is not None assert lamp_1.state == 'on' assert lamp_1.attributes['brightness'] == 144 - assert lamp_1.attributes['hs_color'] == (71.896, 83.137) + assert lamp_1.attributes['hs_color'] == (36.067, 69.804) assert 'color_temp' not in lamp_1.attributes new_light1_on = LIGHT_1_ON.copy() @@ -668,19 +668,6 @@ def test_hs_color(): 'colormode': 'xy', 'hue': 1234, 'sat': 123, - }), - request_bridge_update=None, - bridge=Mock(), - is_group=False, - ) - - assert light.hs_color == (1234 / 65535 * 360, 123 / 255 * 100) - - light = hue_light.HueLight( - light=Mock(state={ - 'colormode': 'xy', - 'hue': None, - 'sat': 123, 'xy': [0.4, 0.5] }), request_bridge_update=None, From 7e39a5c4d50cf5754f5f32a84870ca57a5778b02 Mon Sep 17 00:00:00 2001 From: Matthew Garrett Date: Fri, 27 Apr 2018 13:39:07 -0700 Subject: [PATCH 034/155] Change Eufy brightness handling (#14111) Eufy device state isn't reported if the bulb is off, so avoid stamping on the previous values if the bulb isn't going to give us useful information. In addition, improve handling of bulb turn on if we aren't provided with a brightness - this should avoid the bulb tending to end up with a brightness of 1 after power cycling. --- homeassistant/components/light/eufy.py | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/light/eufy.py b/homeassistant/components/light/eufy.py index a66e219c1a8..6f0a8816eea 100644 --- a/homeassistant/components/light/eufy.py +++ b/homeassistant/components/light/eufy.py @@ -61,13 +61,14 @@ class EufyLight(Light): def update(self): """Synchronise state from the bulb.""" self._bulb.update() - self._brightness = self._bulb.brightness - self._temp = self._bulb.temperature - if self._bulb.colors: - self._colormode = True - self._hs = color_util.color_RGB_to_hs(*self._bulb.colors) - else: - self._colormode = False + if self._bulb.power: + self._brightness = self._bulb.brightness + self._temp = self._bulb.temperature + if self._bulb.colors: + self._colormode = True + self._hs = color_util.color_RGB_to_hs(*self._bulb.colors) + else: + self._colormode = False self._state = self._bulb.power @property @@ -130,7 +131,9 @@ class EufyLight(Light): if brightness is not None: brightness = int(brightness * 100 / 255) else: - brightness = max(1, self._brightness) + if self._brightness is None: + self._brightness = 100 + brightness = self._brightness if colortemp is not None: self._colormode = False From 58ae8d91f931151d3d2dbc2338d4831c52b1ec84 Mon Sep 17 00:00:00 2001 From: Sebastian Muszynski Date: Sat, 28 Apr 2018 12:35:19 +0200 Subject: [PATCH 035/155] Fix the optional friendly name of the Yeelight (Closes: #14088) (#14110) --- homeassistant/components/light/yeelight.py | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/light/yeelight.py b/homeassistant/components/light/yeelight.py index d6d860cbd9e..202c6ac594d 100644 --- a/homeassistant/components/light/yeelight.py +++ b/homeassistant/components/light/yeelight.py @@ -32,16 +32,17 @@ LEGACY_DEVICE_TYPE_MAP = { 'ceiling1': 'ceiling', } -CONF_TRANSITION = 'transition' +DEFAULT_NAME = 'Yeelight' DEFAULT_TRANSITION = 350 +CONF_TRANSITION = 'transition' CONF_SAVE_ON_CHANGE = 'save_on_change' CONF_MODE_MUSIC = 'use_music_mode' DATA_KEY = 'light.yeelight' DEVICE_SCHEMA = vol.Schema({ - vol.Optional(CONF_NAME): cv.string, + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, vol.Optional(CONF_TRANSITION, default=DEFAULT_TRANSITION): cv.positive_int, vol.Optional(CONF_MODE_MUSIC, default=False): cv.boolean, vol.Optional(CONF_SAVE_ON_CHANGE, default=True): cv.boolean, @@ -136,20 +137,18 @@ def setup_platform(hass, config, add_devices, discovery_info=None): # Not using hostname, as it seems to vary. name = "yeelight_%s_%s" % (device_type, discovery_info['properties']['mac']) - device = {'name': name, 'ipaddr': discovery_info['host']} + host = discovery_info['host'] + device = {'name': name, 'ipaddr': host} light = YeelightLight(device, DEVICE_SCHEMA({})) lights.append(light) - hass.data[DATA_KEY][name] = light + hass.data[DATA_KEY][host] = light else: - for ipaddr, device_config in config[CONF_DEVICES].items(): - name = device_config[CONF_NAME] - _LOGGER.debug("Adding configured %s", name) - - device = {'name': name, 'ipaddr': ipaddr} + for host, device_config in config[CONF_DEVICES].items(): + device = {'name': device_config[CONF_NAME], 'ipaddr': host} light = YeelightLight(device, device_config) lights.append(light) - hass.data[DATA_KEY][name] = light + hass.data[DATA_KEY][host] = light add_devices(lights, True) From 2749ca4ef4cec399d8d1d1b5158d21995dbb3d77 Mon Sep 17 00:00:00 2001 From: Colin O'Dell Date: Sat, 28 Apr 2018 05:39:45 -0500 Subject: [PATCH 036/155] Update QNAP lib to 0.2.6; handle null temps gracefully (#14117) There's one particular QNAP model which sometimes return empty/null temperatures for certain disks. This commit ensures that this model can be integrated with HASS without causing KeyErrors or other exceptions - if this edge case is hit, the sensor will simply show `0` instead. --- homeassistant/components/sensor/qnap.py | 4 ++-- requirements_all.txt | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/sensor/qnap.py b/homeassistant/components/sensor/qnap.py index 629a5f6a0ee..b3ca054f88f 100644 --- a/homeassistant/components/sensor/qnap.py +++ b/homeassistant/components/sensor/qnap.py @@ -17,7 +17,7 @@ from homeassistant.const import ( from homeassistant.util import Throttle import homeassistant.helpers.config_validation as cv -REQUIREMENTS = ['qnapstats==0.2.5'] +REQUIREMENTS = ['qnapstats==0.2.6'] _LOGGER = logging.getLogger(__name__) @@ -352,7 +352,7 @@ class QNAPDriveSensor(QNAPSensor): return data['health'] if self.var_id == 'drive_temp': - return int(data['temp_c']) + return int(data['temp_c']) if data['temp_c'] is not None else 0 @property def name(self): diff --git a/requirements_all.txt b/requirements_all.txt index 91b6c71eaa1..b277e638bd9 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1099,7 +1099,7 @@ pyxeoma==1.4.0 pyzabbix==0.7.4 # homeassistant.components.sensor.qnap -qnapstats==0.2.5 +qnapstats==0.2.6 # homeassistant.components.switch.rachio rachiopy==0.1.2 From 00706ad90c901b7ac503d6c0db1d3781ae664379 Mon Sep 17 00:00:00 2001 From: ratcash Date: Sat, 28 Apr 2018 13:35:51 +0200 Subject: [PATCH 037/155] Support Xiaomi Mijia Bluetooth Wireless Temperature and Humidity Sensor (#13955) --- homeassistant/components/sensor/mitemp_bt.py | 172 +++++++++++++++++++ requirements_all.txt | 3 + 2 files changed, 175 insertions(+) create mode 100644 homeassistant/components/sensor/mitemp_bt.py diff --git a/homeassistant/components/sensor/mitemp_bt.py b/homeassistant/components/sensor/mitemp_bt.py new file mode 100644 index 00000000000..3628765293b --- /dev/null +++ b/homeassistant/components/sensor/mitemp_bt.py @@ -0,0 +1,172 @@ +""" +Support for Xiaomi Mi Temp BLE environmental sensor. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/sensor.mitemp_bt/ +""" +import logging + +import voluptuous as vol + +from homeassistant.components.sensor import PLATFORM_SCHEMA +from homeassistant.helpers.entity import Entity +import homeassistant.helpers.config_validation as cv +from homeassistant.const import ( + CONF_FORCE_UPDATE, CONF_MONITORED_CONDITIONS, CONF_NAME, CONF_MAC +) + + +REQUIREMENTS = ['mitemp_bt==0.0.1'] + +_LOGGER = logging.getLogger(__name__) + +CONF_ADAPTER = 'adapter' +CONF_CACHE = 'cache_value' +CONF_MEDIAN = 'median' +CONF_RETRIES = 'retries' +CONF_TIMEOUT = 'timeout' + +DEFAULT_ADAPTER = 'hci0' +DEFAULT_UPDATE_INTERVAL = 300 +DEFAULT_FORCE_UPDATE = False +DEFAULT_MEDIAN = 3 +DEFAULT_NAME = 'MiTemp BT' +DEFAULT_RETRIES = 2 +DEFAULT_TIMEOUT = 10 + + +# Sensor types are defined like: Name, units +SENSOR_TYPES = { + 'temperature': ['Temperature', '°C'], + 'humidity': ['Humidity', '%'], + 'battery': ['Battery', '%'], +} + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Required(CONF_MAC): cv.string, + vol.Optional(CONF_MONITORED_CONDITIONS, default=list(SENSOR_TYPES)): + vol.All(cv.ensure_list, [vol.In(SENSOR_TYPES)]), + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + vol.Optional(CONF_MEDIAN, default=DEFAULT_MEDIAN): cv.positive_int, + vol.Optional(CONF_FORCE_UPDATE, default=DEFAULT_FORCE_UPDATE): cv.boolean, + vol.Optional(CONF_TIMEOUT, default=DEFAULT_TIMEOUT): cv.positive_int, + vol.Optional(CONF_RETRIES, default=DEFAULT_RETRIES): cv.positive_int, + vol.Optional(CONF_CACHE, default=DEFAULT_UPDATE_INTERVAL): cv.positive_int, + vol.Optional(CONF_ADAPTER, default=DEFAULT_ADAPTER): cv.string, +}) + + +def setup_platform(hass, config, add_devices, discovery_info=None): + """Set up the MiTempBt sensor.""" + from mitemp_bt import mitemp_bt_poller + try: + import bluepy.btle # noqa: F401 # pylint: disable=unused-variable + from btlewrap import BluepyBackend + backend = BluepyBackend + except ImportError: + from btlewrap import GatttoolBackend + backend = GatttoolBackend + _LOGGER.debug('MiTempBt is using %s backend.', backend.__name__) + + cache = config.get(CONF_CACHE) + poller = mitemp_bt_poller.MiTempBtPoller( + config.get(CONF_MAC), cache_timeout=cache, + adapter=config.get(CONF_ADAPTER), backend=backend) + force_update = config.get(CONF_FORCE_UPDATE) + median = config.get(CONF_MEDIAN) + poller.ble_timeout = config.get(CONF_TIMEOUT) + poller.retries = config.get(CONF_RETRIES) + + devs = [] + + for parameter in config[CONF_MONITORED_CONDITIONS]: + name = SENSOR_TYPES[parameter][0] + unit = SENSOR_TYPES[parameter][1] + + prefix = config.get(CONF_NAME) + if prefix: + name = "{} {}".format(prefix, name) + + devs.append(MiTempBtSensor( + poller, parameter, name, unit, force_update, median)) + + add_devices(devs) + + +class MiTempBtSensor(Entity): + """Implementing the MiTempBt sensor.""" + + def __init__(self, poller, parameter, name, unit, force_update, median): + """Initialize the sensor.""" + self.poller = poller + self.parameter = parameter + self._unit = unit + self._name = name + self._state = None + self.data = [] + self._force_update = force_update + # Median is used to filter out outliers. median of 3 will filter + # single outliers, while median of 5 will filter double outliers + # Use median_count = 1 if no filtering is required. + self.median_count = median + + @property + def name(self): + """Return the name of the sensor.""" + return self._name + + @property + def state(self): + """Return the state of the sensor.""" + return self._state + + @property + def unit_of_measurement(self): + """Return the units of measurement.""" + return self._unit + + @property + def force_update(self): + """Force update.""" + return self._force_update + + def update(self): + """ + Update current conditions. + + This uses a rolling median over 3 values to filter out outliers. + """ + from btlewrap.base import BluetoothBackendException + try: + _LOGGER.debug("Polling data for %s", self.name) + data = self.poller.parameter_value(self.parameter) + except IOError as ioerr: + _LOGGER.warning("Polling error %s", ioerr) + return + except BluetoothBackendException as bterror: + _LOGGER.warning("Polling error %s", bterror) + return + + if data is not None: + _LOGGER.debug("%s = %s", self.name, data) + self.data.append(data) + else: + _LOGGER.warning("Did not receive any data from Mi Temp sensor %s", + self.name) + # Remove old data from median list or set sensor value to None + # if no data is available anymore + if self.data: + self.data = self.data[1:] + else: + self._state = None + return + + if len(self.data) > self.median_count: + self.data = self.data[1:] + + if len(self.data) == self.median_count: + median = sorted(self.data)[int((self.median_count - 1) / 2)] + _LOGGER.debug("Median is: %s", median) + self._state = median + else: + _LOGGER.debug("Not yet enough data for median calculation") diff --git a/requirements_all.txt b/requirements_all.txt index b277e638bd9..fe5901d6577 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -527,6 +527,9 @@ mficlient==0.3.0 # homeassistant.components.sensor.miflora miflora==0.4.0 +# homeassistant.components.sensor.mitemp_bt +mitemp_bt==0.0.1 + # homeassistant.components.sensor.mopar motorparts==1.0.2 From 1d41321f8f7c9fd35796aad4f6ba37b34e20c064 Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Sat, 28 Apr 2018 14:03:09 +0200 Subject: [PATCH 038/155] Upgrade colorlog to 3.1.4 (#14132) --- homeassistant/scripts/check_config.py | 4 ++-- requirements_all.txt | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/scripts/check_config.py b/homeassistant/scripts/check_config.py index 8c78602f3d0..4375d973a0b 100644 --- a/homeassistant/scripts/check_config.py +++ b/homeassistant/scripts/check_config.py @@ -21,7 +21,7 @@ from homeassistant.config import ( import homeassistant.util.yaml as yaml from homeassistant.exceptions import HomeAssistantError -REQUIREMENTS = ('colorlog==3.1.2',) +REQUIREMENTS = ('colorlog==3.1.4',) if system() == 'Windows': # Ensure colorama installed for colorlog on Windows REQUIREMENTS += ('colorama<=1',) @@ -58,7 +58,7 @@ def color(the_color, *args, reset=None): def run(script_args: List) -> int: """Handle ensure config commandline script.""" parser = argparse.ArgumentParser( - description=("Check Home Assistant configuration.")) + description="Check Home Assistant configuration.") parser.add_argument( '--script', choices=['check_config']) parser.add_argument( diff --git a/requirements_all.txt b/requirements_all.txt index fe5901d6577..d6cb477ab51 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -202,7 +202,7 @@ coinbase==2.1.0 coinmarketcap==4.2.1 # homeassistant.scripts.check_config -colorlog==3.1.2 +colorlog==3.1.4 # homeassistant.components.alarm_control_panel.concord232 # homeassistant.components.binary_sensor.concord232 From 8bc497ba1d8b588fcb165b8c7b4773741ed47d32 Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Sat, 28 Apr 2018 07:46:58 -0600 Subject: [PATCH 039/155] Move RainMachine to component/hub model (#14085) * Moves RainMachine to component/hub model * Updated requirements * Updated coverage * Hound violations * Collaborator-requested changes * Small formatting updates * Removed references to remote API * Collaborator-requested changes * Collaborator-requested changes * Fixed attribution --- .coveragerc | 4 +- homeassistant/components/rainmachine.py | 71 ++++++++ .../components/switch/rainmachine.py | 167 +++++------------- requirements_all.txt | 2 +- 4 files changed, 117 insertions(+), 127 deletions(-) create mode 100644 homeassistant/components/rainmachine.py diff --git a/.coveragerc b/.coveragerc index 452dbec7559..c1c879aef09 100644 --- a/.coveragerc +++ b/.coveragerc @@ -208,6 +208,9 @@ omit = homeassistant/components/raincloud.py homeassistant/components/*/raincloud.py + homeassistant/components/rainmachine.py + homeassistant/components/*/rainmachine.py + homeassistant/components/raspihats.py homeassistant/components/*/raspihats.py @@ -711,7 +714,6 @@ omit = homeassistant/components/switch/orvibo.py homeassistant/components/switch/pulseaudio_loopback.py homeassistant/components/switch/rainbird.py - homeassistant/components/switch/rainmachine.py homeassistant/components/switch/rest.py homeassistant/components/switch/rpi_rf.py homeassistant/components/switch/snmp.py diff --git a/homeassistant/components/rainmachine.py b/homeassistant/components/rainmachine.py new file mode 100644 index 00000000000..4c8b8a1114f --- /dev/null +++ b/homeassistant/components/rainmachine.py @@ -0,0 +1,71 @@ +""" +This component provides support for RainMachine sprinkler controllers. + +For more details about this component, please refer to the documentation at +https://home-assistant.io/components/rainmachine/ +""" +import logging +from datetime import timedelta + +import voluptuous as vol +from requests.exceptions import ConnectTimeout + +from homeassistant.helpers import config_validation as cv +from homeassistant.const import ( + CONF_IP_ADDRESS, CONF_PASSWORD, CONF_PORT, CONF_SSL) + +REQUIREMENTS = ['regenmaschine==0.4.1'] + +_LOGGER = logging.getLogger(__name__) + +DATA_RAINMACHINE = 'data_rainmachine' +DOMAIN = 'rainmachine' + +NOTIFICATION_ID = 'rainmachine_notification' +NOTIFICATION_TITLE = 'RainMachine Component Setup' + +DEFAULT_ATTRIBUTION = 'Data provided by Green Electronics LLC' +DEFAULT_PORT = 8080 +DEFAULT_SSL = True + +MIN_SCAN_TIME = timedelta(seconds=1) +MIN_SCAN_TIME_FORCED = timedelta(milliseconds=100) + +CONFIG_SCHEMA = vol.Schema( + { + DOMAIN: vol.Schema({ + vol.Required(CONF_IP_ADDRESS): cv.string, + vol.Required(CONF_PASSWORD): cv.string, + vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, + vol.Optional(CONF_SSL, default=DEFAULT_SSL): cv.boolean, + }) + }, + extra=vol.ALLOW_EXTRA) + + +def setup(hass, config): + """Set up the RainMachine component.""" + from regenmaschine import Authenticator, Client + from regenmaschine.exceptions import HTTPError + + conf = config[DOMAIN] + ip_address = conf[CONF_IP_ADDRESS] + password = conf[CONF_PASSWORD] + port = conf[CONF_PORT] + ssl = conf[CONF_SSL] + + try: + auth = Authenticator.create_local( + ip_address, password, port=port, https=ssl) + client = Client(auth) + hass.data[DATA_RAINMACHINE] = client + except (HTTPError, ConnectTimeout, UnboundLocalError) as exc_info: + _LOGGER.error('An error occurred: %s', str(exc_info)) + hass.components.persistent_notification.create( + 'Error: {0}
' + 'You will need to restart hass after fixing.' + ''.format(exc_info), + title=NOTIFICATION_TITLE, + notification_id=NOTIFICATION_ID) + return False + return True diff --git a/homeassistant/components/switch/rainmachine.py b/homeassistant/components/switch/rainmachine.py index 99d41bdd9c3..cdada7ce274 100644 --- a/homeassistant/components/switch/rainmachine.py +++ b/homeassistant/components/switch/rainmachine.py @@ -1,130 +1,61 @@ """Implements a RainMachine sprinkler controller for Home Assistant.""" -from datetime import timedelta from logging import getLogger import voluptuous as vol import homeassistant.helpers.config_validation as cv -from homeassistant.components.switch import SwitchDevice -from homeassistant.const import ( - ATTR_ATTRIBUTION, ATTR_DEVICE_CLASS, CONF_EMAIL, CONF_IP_ADDRESS, - CONF_PASSWORD, CONF_PLATFORM, CONF_PORT, CONF_SCAN_INTERVAL, CONF_SSL) +from homeassistant.components.rainmachine import ( + DATA_RAINMACHINE, DEFAULT_ATTRIBUTION, MIN_SCAN_TIME, MIN_SCAN_TIME_FORCED) +from homeassistant.components.switch import PLATFORM_SCHEMA, SwitchDevice +from homeassistant.const import ATTR_ATTRIBUTION, ATTR_DEVICE_CLASS from homeassistant.util import Throttle _LOGGER = getLogger(__name__) -REQUIREMENTS = ['regenmaschine==0.4.1'] +DEPENDENCIES = ['rainmachine'] ATTR_CYCLES = 'cycles' ATTR_TOTAL_DURATION = 'total_duration' CONF_ZONE_RUN_TIME = 'zone_run_time' -DEFAULT_PORT = 8080 -DEFAULT_SSL = True DEFAULT_ZONE_RUN_SECONDS = 60 * 10 -MIN_SCAN_TIME_LOCAL = timedelta(seconds=1) -MIN_SCAN_TIME_REMOTE = timedelta(seconds=5) -MIN_SCAN_TIME_FORCED = timedelta(milliseconds=100) - -PLATFORM_SCHEMA = vol.Schema( - vol.All( - cv.has_at_least_one_key(CONF_IP_ADDRESS, CONF_EMAIL), - { - vol.Required(CONF_PLATFORM): cv.string, - vol.Optional(CONF_SCAN_INTERVAL): cv.time_period, - vol.Exclusive(CONF_IP_ADDRESS, 'auth'): cv.string, - vol.Exclusive(CONF_EMAIL, 'auth'): - vol.Email(), # pylint: disable=no-value-for-parameter - 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_ZONE_RUN_TIME, default=DEFAULT_ZONE_RUN_SECONDS): - cv.positive_int - }), - extra=vol.ALLOW_EXTRA) +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Optional(CONF_ZONE_RUN_TIME, default=DEFAULT_ZONE_RUN_SECONDS): + cv.positive_int +}) def setup_platform(hass, config, add_devices, discovery_info=None): """Set this component up under its platform.""" - import regenmaschine as rm + client = hass.data.get(DATA_RAINMACHINE) + device_name = client.provision.device_name()['name'] + device_mac = client.provision.wifi()['macAddress'] - _LOGGER.debug('Config data: %s', config) + _LOGGER.debug('Config received: %s', config) - ip_address = config.get(CONF_IP_ADDRESS, None) - email_address = config.get(CONF_EMAIL, None) - password = config[CONF_PASSWORD] zone_run_time = config[CONF_ZONE_RUN_TIME] - try: - if ip_address: - _LOGGER.debug('Configuring local API') + entities = [] + for program in client.programs.all().get('programs', {}): + if not program.get('active'): + continue - port = config[CONF_PORT] - ssl = config[CONF_SSL] - auth = rm.Authenticator.create_local( - ip_address, password, port=port, https=ssl) - elif email_address: - _LOGGER.debug('Configuring remote API') - auth = rm.Authenticator.create_remote(email_address, password) + _LOGGER.debug('Adding program: %s', program) + entities.append( + RainMachineProgram(client, device_name, device_mac, program)) - _LOGGER.debug('Querying against: %s', auth.url) + for zone in client.zones.all().get('zones', {}): + if not zone.get('active'): + continue - client = rm.Client(auth) - device_name = client.provision.device_name()['name'] - device_mac = client.provision.wifi()['macAddress'] + _LOGGER.debug('Adding zone: %s', zone) + entities.append( + RainMachineZone(client, device_name, device_mac, zone, + zone_run_time)) - entities = [] - for program in client.programs.all().get('programs', {}): - if not program.get('active'): - continue - - _LOGGER.debug('Adding program: %s', program) - entities.append( - RainMachineProgram(client, device_name, device_mac, program)) - - for zone in client.zones.all().get('zones', {}): - if not zone.get('active'): - continue - - _LOGGER.debug('Adding zone: %s', zone) - entities.append( - RainMachineZone(client, device_name, device_mac, zone, - zone_run_time)) - - add_devices(entities) - except rm.exceptions.HTTPError as exc_info: - _LOGGER.error('An HTTP error occurred while talking with RainMachine') - _LOGGER.debug(exc_info) - return False - except UnboundLocalError as exc_info: - _LOGGER.error('Could not authenticate against RainMachine') - _LOGGER.debug(exc_info) - return False - - -def aware_throttle(api_type): - """Create an API type-aware throttler.""" - _decorator = None - if api_type == 'local': - - @Throttle(MIN_SCAN_TIME_LOCAL, MIN_SCAN_TIME_FORCED) - def decorator(function): - """Create a local API throttler.""" - return function - - _decorator = decorator - else: - - @Throttle(MIN_SCAN_TIME_REMOTE, MIN_SCAN_TIME_FORCED) - def decorator(function): - """Create a remote API throttler.""" - return function - - _decorator = decorator - - return _decorator + add_devices(entities, True) class RainMachineEntity(SwitchDevice): @@ -135,19 +66,24 @@ class RainMachineEntity(SwitchDevice): self._api_type = 'remote' if client.auth.using_remote_api else 'local' self._client = client self._entity_json = entity_json + self.device_mac = device_mac self.device_name = device_name self._attrs = { - ATTR_ATTRIBUTION: '© RainMachine', + ATTR_ATTRIBUTION: DEFAULT_ATTRIBUTION, ATTR_DEVICE_CLASS: self.device_name } @property def device_state_attributes(self) -> dict: """Return the state attributes.""" - if self._client: - return self._attrs + return self._attrs + + @property + def icon(self) -> str: + """Return the icon.""" + return 'mdi:water' @property def is_enabled(self) -> bool: @@ -159,27 +95,6 @@ class RainMachineEntity(SwitchDevice): """Return the RainMachine ID for this entity.""" return self._entity_json.get('uid') - @aware_throttle('local') - def _local_update(self) -> None: - """Call an update with scan times appropriate for the local API.""" - self._update() - - @aware_throttle('remote') - def _remote_update(self) -> None: - """Call an update with scan times appropriate for the remote API.""" - self._update() - - def _update(self) -> None: # pylint: disable=no-self-use - """Logic for update method, regardless of API type.""" - raise NotImplementedError() - - def update(self) -> None: - """Determine how the entity updates itself.""" - if self._api_type == 'remote': - self._remote_update() - else: - self._local_update() - class RainMachineProgram(RainMachineEntity): """A RainMachine program.""" @@ -192,7 +107,7 @@ class RainMachineProgram(RainMachineEntity): @property def name(self) -> str: """Return the name of the program.""" - return 'Program: {}'.format(self._entity_json.get('name')) + return 'Program: {0}'.format(self._entity_json.get('name')) @property def unique_id(self) -> str: @@ -224,7 +139,8 @@ class RainMachineProgram(RainMachineEntity): _LOGGER.error('Unable to turn on program "%s"', self.unique_id) _LOGGER.debug(exc_info) - def _update(self) -> None: + @Throttle(MIN_SCAN_TIME, MIN_SCAN_TIME_FORCED) + def update(self) -> None: """Update info for the program.""" import regenmaschine.exceptions as exceptions @@ -258,7 +174,7 @@ class RainMachineZone(RainMachineEntity): @property def name(self) -> str: """Return the name of the zone.""" - return 'Zone: {}'.format(self._entity_json.get('name')) + return 'Zone: {0}'.format(self._entity_json.get('name')) @property def unique_id(self) -> str: @@ -287,7 +203,8 @@ class RainMachineZone(RainMachineEntity): _LOGGER.error('Unable to turn on zone "%s"', self.unique_id) _LOGGER.debug(exc_info) - def _update(self) -> None: + @Throttle(MIN_SCAN_TIME, MIN_SCAN_TIME_FORCED) + def update(self) -> None: """Update info for the zone.""" import regenmaschine.exceptions as exceptions diff --git a/requirements_all.txt b/requirements_all.txt index d6cb477ab51..f6fdf81a2e4 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1116,7 +1116,7 @@ raincloudy==0.0.4 # homeassistant.components.raspihats # raspihats==2.2.3 -# homeassistant.components.switch.rainmachine +# homeassistant.components.rainmachine regenmaschine==0.4.1 # homeassistant.components.python_script From c78e8eb578846356fd629ae989c7d8fbcd754711 Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Sat, 28 Apr 2018 17:14:34 +0200 Subject: [PATCH 040/155] Add support for light sensors with 'lx' unit to HomeKit (#14131) * add support for light sensors with lx unit * add test for light sensor with 'lx' unit --- homeassistant/components/homekit/__init__.py | 2 +- tests/components/homekit/test_get_accessories.py | 7 +++++++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/homekit/__init__.py b/homeassistant/components/homekit/__init__.py index 24c6dfa8a76..025ef4069e9 100644 --- a/homeassistant/components/homekit/__init__.py +++ b/homeassistant/components/homekit/__init__.py @@ -126,7 +126,7 @@ def get_accessory(hass, state, aid, config): or DEVICE_CLASS_CO2 in state.entity_id: a_type = 'CarbonDioxideSensor' elif device_class == DEVICE_CLASS_LIGHT or unit == 'lm' or \ - unit == 'lux': + unit == 'lux' or unit == 'lx': a_type = 'LightSensor' elif state.domain == 'switch' or state.domain == 'remote' \ diff --git a/tests/components/homekit/test_get_accessories.py b/tests/components/homekit/test_get_accessories.py index c26982e170b..76736ce45ad 100644 --- a/tests/components/homekit/test_get_accessories.py +++ b/tests/components/homekit/test_get_accessories.py @@ -125,6 +125,13 @@ class TestGetAccessories(unittest.TestCase): {ATTR_UNIT_OF_MEASUREMENT: 'lux'}) get_accessory(None, state, 2, {}) + def test_light_sensor_unit_lx(self): + """Test light sensor with lx as unit.""" + with patch.dict(TYPES, {'LightSensor': self.mock_type}): + state = State('sensor.light', '900', + {ATTR_UNIT_OF_MEASUREMENT: 'lx'}) + get_accessory(None, state, 2, {}) + def test_binary_sensor(self): """Test binary sensor with opening class.""" with patch.dict(TYPES, {'BinarySensor': self.mock_type}): From ea5c336ab4bb7e8479481525ca4180093ae273e7 Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Sat, 28 Apr 2018 19:21:37 +0200 Subject: [PATCH 041/155] Upgrade restrictedpython to 4.0b3 (#14140) --- homeassistant/components/python_script.py | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/python_script.py b/homeassistant/components/python_script.py index dedc39ef3a2..1d33740d4a4 100644 --- a/homeassistant/components/python_script.py +++ b/homeassistant/components/python_script.py @@ -18,7 +18,7 @@ from homeassistant.loader import bind_hass from homeassistant.util import sanitize_filename import homeassistant.util.dt as dt_util -REQUIREMENTS = ['restrictedpython==4.0b2'] +REQUIREMENTS = ['restrictedpython==4.0b3'] _LOGGER = logging.getLogger(__name__) diff --git a/requirements_all.txt b/requirements_all.txt index f6fdf81a2e4..dc4997d49ec 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1120,7 +1120,7 @@ raincloudy==0.0.4 regenmaschine==0.4.1 # homeassistant.components.python_script -restrictedpython==4.0b2 +restrictedpython==4.0b3 # homeassistant.components.rflink rflink==0.0.37 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 876aba4574d..36e8df39bd3 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -168,7 +168,7 @@ pyupnp-async==0.1.0.1 pywebpush==1.6.0 # homeassistant.components.python_script -restrictedpython==4.0b2 +restrictedpython==4.0b3 # homeassistant.components.rflink rflink==0.0.37 From 8d87b9fed5437f8e619a997c286f7269c331104b Mon Sep 17 00:00:00 2001 From: Marcus Date: Sun, 29 Apr 2018 04:39:21 +1000 Subject: [PATCH 042/155] Logitech Pop support for emulated_hue component (#12833) * Update hue_api.py add dummy group handler for logitech-pop * Update __init__.py add HueGroupView for logitech pop * Update __init__.py removed whitespace on blankline * fix line limit and space * fix indents * fix more docstring and formatting issues. * fix more whitespace issues * Fix pylint issue --- .../components/emulated_hue/__init__.py | 3 ++- .../components/emulated_hue/hue_api.py | 23 +++++++++++++++++++ 2 files changed, 25 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/emulated_hue/__init__.py b/homeassistant/components/emulated_hue/__init__.py index fa558cf299f..fd7f7147fdb 100644 --- a/homeassistant/components/emulated_hue/__init__.py +++ b/homeassistant/components/emulated_hue/__init__.py @@ -20,7 +20,7 @@ import homeassistant.helpers.config_validation as cv from homeassistant.util.json import load_json, save_json from .hue_api import ( HueUsernameView, HueAllLightsStateView, HueOneLightStateView, - HueOneLightChangeView) + HueOneLightChangeView, HueGroupView) from .upnp import DescriptionXmlView, UPNPResponderThread DOMAIN = 'emulated_hue' @@ -104,6 +104,7 @@ def setup(hass, yaml_config): server.register_view(HueAllLightsStateView(config)) server.register_view(HueOneLightStateView(config)) server.register_view(HueOneLightChangeView(config)) + server.register_view(HueGroupView(config)) upnp_listener = UPNPResponderThread( config.host_ip_addr, config.listen_port, diff --git a/homeassistant/components/emulated_hue/hue_api.py b/homeassistant/components/emulated_hue/hue_api.py index 5d97ef3cea4..2b74984e4ca 100644 --- a/homeassistant/components/emulated_hue/hue_api.py +++ b/homeassistant/components/emulated_hue/hue_api.py @@ -51,6 +51,29 @@ class HueUsernameView(HomeAssistantView): return self.json([{'success': {'username': '12345678901234567890'}}]) +class HueGroupView(HomeAssistantView): + """Group handler to get Logitech Pop working.""" + + url = '/api/{username}/groups/0/action' + name = 'emulated_hue:groups:state' + requires_auth = False + + def __init__(self, config): + """Initialize the instance of the view.""" + self.config = config + + @core.callback + def put(self, request, username): + """Process a request to make the Logitech Pop working.""" + return self.json([{ + 'error': { + 'address': '/groups/0/action/scene', + 'type': 7, + 'description': 'invalid value, dummy for parameter, scene' + } + }]) + + class HueAllLightsStateView(HomeAssistantView): """Handle requests for getting and setting info about entities.""" From b352b761f386fe007ffb33409c2f2514f10180b3 Mon Sep 17 00:00:00 2001 From: Charles Garwood Date: Sat, 28 Apr 2018 15:05:27 -0400 Subject: [PATCH 043/155] Bump pyvizio to 0.0.3 (#14147) * Bumping pyvizio version * Bump pyvizio version --- homeassistant/components/media_player/vizio.py | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/media_player/vizio.py b/homeassistant/components/media_player/vizio.py index 64d1f642e6e..381482a4839 100644 --- a/homeassistant/components/media_player/vizio.py +++ b/homeassistant/components/media_player/vizio.py @@ -19,7 +19,7 @@ from homeassistant.const import ( from homeassistant.helpers import config_validation as cv import homeassistant.util as util -REQUIREMENTS = ['pyvizio==0.0.2'] +REQUIREMENTS = ['pyvizio==0.0.3'] _LOGGER = logging.getLogger(__name__) diff --git a/requirements_all.txt b/requirements_all.txt index dc4997d49ec..baf907c0459 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1084,7 +1084,7 @@ pyvera==0.2.42 pyvesync==0.1.1 # homeassistant.components.media_player.vizio -pyvizio==0.0.2 +pyvizio==0.0.3 # homeassistant.components.velux pyvlx==0.1.3 From 93fe61bf13652639ece8b44f88feb9fe01088bd3 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sat, 28 Apr 2018 17:09:38 -0400 Subject: [PATCH 044/155] System log: make firing event optional (#14102) * Syste log: make firing event optional * Add test * Lint * Doc string --- .../components/system_log/__init__.py | 12 +- tests/components/test_system_log.py | 142 ++++++++++-------- 2 files changed, 88 insertions(+), 66 deletions(-) diff --git a/homeassistant/components/system_log/__init__.py b/homeassistant/components/system_log/__init__.py index 1dad1f3a1eb..5994184d815 100644 --- a/homeassistant/components/system_log/__init__.py +++ b/homeassistant/components/system_log/__init__.py @@ -19,12 +19,14 @@ import homeassistant.helpers.config_validation as cv from homeassistant.const import EVENT_HOMEASSISTANT_STOP CONF_MAX_ENTRIES = 'max_entries' +CONF_FIRE_EVENT = 'fire_event' CONF_MESSAGE = 'message' CONF_LEVEL = 'level' CONF_LOGGER = 'logger' DATA_SYSTEM_LOG = 'system_log' DEFAULT_MAX_ENTRIES = 50 +DEFAULT_FIRE_EVENT = False DEPENDENCIES = ['http'] DOMAIN = 'system_log' @@ -37,6 +39,7 @@ CONFIG_SCHEMA = vol.Schema({ DOMAIN: vol.Schema({ vol.Optional(CONF_MAX_ENTRIES, default=DEFAULT_MAX_ENTRIES): cv.positive_int, + vol.Optional(CONF_FIRE_EVENT, default=DEFAULT_FIRE_EVENT): cv.boolean, }), }, extra=vol.ALLOW_EXTRA) @@ -97,11 +100,12 @@ def _exception_as_string(exc_info): class LogErrorHandler(logging.Handler): """Log handler for error messages.""" - def __init__(self, hass, maxlen): + def __init__(self, hass, maxlen, fire_event): """Initialize a new LogErrorHandler.""" super().__init__() self.hass = hass self.records = deque(maxlen=maxlen) + self.fire_event = fire_event def _create_entry(self, record, call_stack): return { @@ -130,7 +134,8 @@ class LogErrorHandler(logging.Handler): entry = self._create_entry(record, stack) self.records.appendleft(entry) - self.hass.bus.fire(EVENT_SYSTEM_LOG, entry) + if self.fire_event: + self.hass.bus.fire(EVENT_SYSTEM_LOG, entry) @asyncio.coroutine @@ -140,7 +145,8 @@ def async_setup(hass, config): if conf is None: conf = CONFIG_SCHEMA({DOMAIN: {}})[DOMAIN] - handler = LogErrorHandler(hass, conf.get(CONF_MAX_ENTRIES)) + handler = LogErrorHandler(hass, conf[CONF_MAX_ENTRIES], + conf[CONF_FIRE_EVENT]) logging.getLogger().addHandler(handler) hass.http.register_view(AllErrorsView(handler)) diff --git a/tests/components/test_system_log.py b/tests/components/test_system_log.py index c440ef9c30c..59e99e5c1b5 100644 --- a/tests/components/test_system_log.py +++ b/tests/components/test_system_log.py @@ -1,33 +1,26 @@ """Test system log component.""" -import asyncio import logging from unittest.mock import MagicMock, patch -import pytest - from homeassistant.core import callback from homeassistant.bootstrap import async_setup_component from homeassistant.components import system_log _LOGGER = logging.getLogger('test_logger') +BASIC_CONFIG = { + 'system_log': { + 'max_entries': 2, + } +} -@pytest.fixture(autouse=True) -@asyncio.coroutine -def setup_test_case(hass, aiohttp_client): - """Setup system_log component before test case.""" - config = {'system_log': {'max_entries': 2}} - yield from async_setup_component(hass, system_log.DOMAIN, config) - - -@asyncio.coroutine -def get_error_log(hass, aiohttp_client, expected_count): +async def get_error_log(hass, aiohttp_client, expected_count): """Fetch all entries from system_log via the API.""" - client = yield from aiohttp_client(hass.http.app) - resp = yield from client.get('/api/error/all') + client = await aiohttp_client(hass.http.app) + resp = await client.get('/api/error/all') assert resp.status == 200 - data = yield from resp.json() + data = await resp.json() assert len(data) == expected_count return data @@ -52,43 +45,43 @@ def get_frame(name): return (name, None, None, None) -@asyncio.coroutine -def test_normal_logs(hass, aiohttp_client): +async def test_normal_logs(hass, aiohttp_client): """Test that debug and info are not logged.""" + await async_setup_component(hass, system_log.DOMAIN, BASIC_CONFIG) _LOGGER.debug('debug') _LOGGER.info('info') # Assert done by get_error_log - yield from get_error_log(hass, aiohttp_client, 0) + await get_error_log(hass, aiohttp_client, 0) -@asyncio.coroutine -def test_exception(hass, aiohttp_client): +async def test_exception(hass, aiohttp_client): """Test that exceptions are logged and retrieved correctly.""" + await async_setup_component(hass, system_log.DOMAIN, BASIC_CONFIG) _generate_and_log_exception('exception message', 'log message') - log = (yield from get_error_log(hass, aiohttp_client, 1))[0] + log = (await get_error_log(hass, aiohttp_client, 1))[0] assert_log(log, 'exception message', 'log message', 'ERROR') -@asyncio.coroutine -def test_warning(hass, aiohttp_client): +async def test_warning(hass, aiohttp_client): """Test that warning are logged and retrieved correctly.""" + await async_setup_component(hass, system_log.DOMAIN, BASIC_CONFIG) _LOGGER.warning('warning message') - log = (yield from get_error_log(hass, aiohttp_client, 1))[0] + log = (await get_error_log(hass, aiohttp_client, 1))[0] assert_log(log, '', 'warning message', 'WARNING') -@asyncio.coroutine -def test_error(hass, aiohttp_client): +async def test_error(hass, aiohttp_client): """Test that errors are logged and retrieved correctly.""" + await async_setup_component(hass, system_log.DOMAIN, BASIC_CONFIG) _LOGGER.error('error message') - log = (yield from get_error_log(hass, aiohttp_client, 1))[0] + log = (await get_error_log(hass, aiohttp_client, 1))[0] assert_log(log, '', 'error message', 'ERROR') -@asyncio.coroutine -def test_error_posted_as_event(hass, aiohttp_client): - """Test that error are posted as events.""" +async def test_config_not_fire_event(hass): + """Test that errors are not posted as events with default config.""" + await async_setup_component(hass, system_log.DOMAIN, BASIC_CONFIG) events = [] @callback @@ -99,77 +92,100 @@ def test_error_posted_as_event(hass, aiohttp_client): hass.bus.async_listen(system_log.EVENT_SYSTEM_LOG, event_listener) _LOGGER.error('error message') - yield from hass.async_block_till_done() + await hass.async_block_till_done() + + assert len(events) == 0 + + +async def test_error_posted_as_event(hass): + """Test that error are posted as events.""" + await async_setup_component(hass, system_log.DOMAIN, { + 'system_log': { + 'max_entries': 2, + 'fire_event': True, + } + }) + events = [] + + @callback + def event_listener(event): + """Listen to events of type system_log_event.""" + events.append(event) + + hass.bus.async_listen(system_log.EVENT_SYSTEM_LOG, event_listener) + + _LOGGER.error('error message') + await hass.async_block_till_done() assert len(events) == 1 assert_log(events[0].data, '', 'error message', 'ERROR') -@asyncio.coroutine -def test_critical(hass, aiohttp_client): +async def test_critical(hass, aiohttp_client): """Test that critical are logged and retrieved correctly.""" + await async_setup_component(hass, system_log.DOMAIN, BASIC_CONFIG) _LOGGER.critical('critical message') - log = (yield from get_error_log(hass, aiohttp_client, 1))[0] + log = (await get_error_log(hass, aiohttp_client, 1))[0] assert_log(log, '', 'critical message', 'CRITICAL') -@asyncio.coroutine -def test_remove_older_logs(hass, aiohttp_client): +async def test_remove_older_logs(hass, aiohttp_client): """Test that older logs are rotated out.""" + await async_setup_component(hass, system_log.DOMAIN, BASIC_CONFIG) _LOGGER.error('error message 1') _LOGGER.error('error message 2') _LOGGER.error('error message 3') - log = yield from get_error_log(hass, aiohttp_client, 2) + log = await get_error_log(hass, aiohttp_client, 2) assert_log(log[0], '', 'error message 3', 'ERROR') assert_log(log[1], '', 'error message 2', 'ERROR') -@asyncio.coroutine -def test_clear_logs(hass, aiohttp_client): +async def test_clear_logs(hass, aiohttp_client): """Test that the log can be cleared via a service call.""" + await async_setup_component(hass, system_log.DOMAIN, BASIC_CONFIG) _LOGGER.error('error message') hass.async_add_job( hass.services.async_call( system_log.DOMAIN, system_log.SERVICE_CLEAR, {})) - yield from hass.async_block_till_done() + await hass.async_block_till_done() # Assert done by get_error_log - yield from get_error_log(hass, aiohttp_client, 0) + await get_error_log(hass, aiohttp_client, 0) -@asyncio.coroutine -def test_write_log(hass): +async def test_write_log(hass): """Test that error propagates to logger.""" + await async_setup_component(hass, system_log.DOMAIN, BASIC_CONFIG) logger = MagicMock() with patch('logging.getLogger', return_value=logger) as mock_logging: hass.async_add_job( hass.services.async_call( system_log.DOMAIN, system_log.SERVICE_WRITE, {'message': 'test_message'})) - yield from hass.async_block_till_done() + await hass.async_block_till_done() mock_logging.assert_called_once_with( 'homeassistant.components.system_log.external') assert logger.method_calls[0] == ('error', ('test_message',)) -@asyncio.coroutine -def test_write_choose_logger(hass): +async def test_write_choose_logger(hass): """Test that correct logger is chosen.""" + await async_setup_component(hass, system_log.DOMAIN, BASIC_CONFIG) with patch('logging.getLogger') as mock_logging: hass.async_add_job( hass.services.async_call( system_log.DOMAIN, system_log.SERVICE_WRITE, {'message': 'test_message', 'logger': 'myLogger'})) - yield from hass.async_block_till_done() + await hass.async_block_till_done() mock_logging.assert_called_once_with( 'myLogger') -@asyncio.coroutine -def test_write_choose_level(hass): +async def test_write_choose_level(hass): """Test that correct logger is chosen.""" + await async_setup_component(hass, system_log.DOMAIN, BASIC_CONFIG) logger = MagicMock() with patch('logging.getLogger', return_value=logger): hass.async_add_job( @@ -177,17 +193,17 @@ def test_write_choose_level(hass): system_log.DOMAIN, system_log.SERVICE_WRITE, {'message': 'test_message', 'level': 'debug'})) - yield from hass.async_block_till_done() + await hass.async_block_till_done() assert logger.method_calls[0] == ('debug', ('test_message',)) -@asyncio.coroutine -def test_unknown_path(hass, aiohttp_client): +async def test_unknown_path(hass, aiohttp_client): """Test error logged from unknown path.""" + await async_setup_component(hass, system_log.DOMAIN, BASIC_CONFIG) _LOGGER.findCaller = MagicMock( return_value=('unknown_path', 0, None, None)) _LOGGER.error('error message') - log = (yield from get_error_log(hass, aiohttp_client, 1))[0] + log = (await get_error_log(hass, aiohttp_client, 1))[0] assert log['source'] == 'unknown_path' @@ -206,31 +222,31 @@ def log_error_from_test_path(path): _LOGGER.error('error message') -@asyncio.coroutine -def test_homeassistant_path(hass, aiohttp_client): +async def test_homeassistant_path(hass, aiohttp_client): """Test error logged from homeassistant path.""" + await async_setup_component(hass, system_log.DOMAIN, BASIC_CONFIG) with patch('homeassistant.components.system_log.HOMEASSISTANT_PATH', new=['venv_path/homeassistant']): log_error_from_test_path( 'venv_path/homeassistant/component/component.py') - log = (yield from get_error_log(hass, aiohttp_client, 1))[0] + log = (await get_error_log(hass, aiohttp_client, 1))[0] assert log['source'] == 'component/component.py' -@asyncio.coroutine -def test_config_path(hass, aiohttp_client): +async def test_config_path(hass, aiohttp_client): """Test error logged from config path.""" + await async_setup_component(hass, system_log.DOMAIN, BASIC_CONFIG) with patch.object(hass.config, 'config_dir', new='config'): log_error_from_test_path('config/custom_component/test.py') - log = (yield from get_error_log(hass, aiohttp_client, 1))[0] + log = (await get_error_log(hass, aiohttp_client, 1))[0] assert log['source'] == 'custom_component/test.py' -@asyncio.coroutine -def test_netdisco_path(hass, aiohttp_client): +async def test_netdisco_path(hass, aiohttp_client): """Test error logged from netdisco path.""" + await async_setup_component(hass, system_log.DOMAIN, BASIC_CONFIG) with patch.dict('sys.modules', netdisco=MagicMock(__path__=['venv_path/netdisco'])): log_error_from_test_path('venv_path/netdisco/disco_component.py') - log = (yield from get_error_log(hass, aiohttp_client, 1))[0] + log = (await get_error_log(hass, aiohttp_client, 1))[0] assert log['source'] == 'disco_component.py' From e6d4501ee360c79716f5b50f2c8e557d33f77b54 Mon Sep 17 00:00:00 2001 From: Anders Melchiorsen Date: Sat, 28 Apr 2018 23:12:11 +0200 Subject: [PATCH 045/155] Fix color setting of tplink lights (#14108) --- homeassistant/components/light/tplink.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/light/tplink.py b/homeassistant/components/light/tplink.py index 0bbec010282..4101eab2150 100644 --- a/homeassistant/components/light/tplink.py +++ b/homeassistant/components/light/tplink.py @@ -11,8 +11,8 @@ import voluptuous as vol from homeassistant.const import (CONF_HOST, CONF_NAME) from homeassistant.components.light import ( - Light, ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, ATTR_KELVIN, ATTR_HS_COLOR, - SUPPORT_BRIGHTNESS, SUPPORT_COLOR_TEMP, SUPPORT_COLOR, PLATFORM_SCHEMA) + Light, ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, ATTR_HS_COLOR, SUPPORT_BRIGHTNESS, + SUPPORT_COLOR_TEMP, SUPPORT_COLOR, PLATFORM_SCHEMA) import homeassistant.helpers.config_validation as cv from homeassistant.util.color import \ color_temperature_mired_to_kelvin as mired_to_kelvin @@ -90,15 +90,15 @@ class TPLinkSmartBulb(Light): if ATTR_COLOR_TEMP in kwargs: self.smartbulb.color_temp = \ mired_to_kelvin(kwargs[ATTR_COLOR_TEMP]) - if ATTR_KELVIN in kwargs: - self.smartbulb.color_temp = kwargs[ATTR_KELVIN] - if ATTR_BRIGHTNESS in kwargs: - brightness = kwargs.get(ATTR_BRIGHTNESS, self.brightness or 255) - self.smartbulb.brightness = brightness_to_percentage(brightness) + + brightness = brightness_to_percentage( + kwargs.get(ATTR_BRIGHTNESS, self.brightness or 255)) if ATTR_HS_COLOR in kwargs: hue, sat = kwargs.get(ATTR_HS_COLOR) - hsv = (hue, sat, 100) + hsv = (int(hue), int(sat), brightness) self.smartbulb.hsv = hsv + elif ATTR_BRIGHTNESS in kwargs: + self.smartbulb.brightness = brightness def turn_off(self, **kwargs): """Turn the light off.""" From 7bdd4dd9603a603afd4964f365c4ea012a10adbf Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Sat, 28 Apr 2018 23:15:32 +0200 Subject: [PATCH 046/155] Upgrade pylast to 2.2.0 (#14139) --- 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 9d305973ecf..9fec4b4b5e3 100644 --- a/homeassistant/components/sensor/lastfm.py +++ b/homeassistant/components/sensor/lastfm.py @@ -13,7 +13,7 @@ from homeassistant.const import CONF_API_KEY import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity -REQUIREMENTS = ['pylast==2.1.0'] +REQUIREMENTS = ['pylast==2.2.0'] ATTR_LAST_PLAYED = 'last_played' ATTR_PLAY_COUNT = 'play_count' diff --git a/requirements_all.txt b/requirements_all.txt index baf907c0459..6eb9f91f0e2 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -823,7 +823,7 @@ pykwb==0.0.8 pylacrosse==0.3.1 # homeassistant.components.sensor.lastfm -pylast==2.1.0 +pylast==2.2.0 # homeassistant.components.media_player.webostv # homeassistant.components.notify.webostv From 95f2ad2299363cb8943b1ad1b423acc6cc607a74 Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Sat, 28 Apr 2018 23:16:01 +0200 Subject: [PATCH 047/155] Upgrade sqlalchemy to 1.2.7 (#14138) --- homeassistant/components/recorder/__init__.py | 2 +- homeassistant/components/sensor/sql.py | 12 ++++++------ requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 9 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/recorder/__init__.py b/homeassistant/components/recorder/__init__.py index 64e2b85f611..8e69c2cfcd8 100644 --- a/homeassistant/components/recorder/__init__.py +++ b/homeassistant/components/recorder/__init__.py @@ -35,7 +35,7 @@ from . import migration, purge from .const import DATA_INSTANCE from .util import session_scope -REQUIREMENTS = ['sqlalchemy==1.2.6'] +REQUIREMENTS = ['sqlalchemy==1.2.7'] _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/sensor/sql.py b/homeassistant/components/sensor/sql.py index eeca31fa36b..7d18bb3f049 100644 --- a/homeassistant/components/sensor/sql.py +++ b/homeassistant/components/sensor/sql.py @@ -19,11 +19,11 @@ from homeassistant.helpers.entity import Entity _LOGGER = logging.getLogger(__name__) -REQUIREMENTS = ['sqlalchemy==1.2.6'] +REQUIREMENTS = ['sqlalchemy==1.2.7'] +CONF_COLUMN_NAME = 'column' CONF_QUERIES = 'queries' CONF_QUERY = 'query' -CONF_COLUMN_NAME = 'column' def validate_sql_select(value): @@ -34,9 +34,9 @@ def validate_sql_select(value): _QUERY_SCHEME = vol.Schema({ + vol.Required(CONF_COLUMN_NAME): cv.string, vol.Required(CONF_NAME): cv.string, vol.Required(CONF_QUERY): vol.All(cv.string, validate_sql_select), - vol.Required(CONF_COLUMN_NAME): cv.string, vol.Optional(CONF_UNIT_OF_MEASUREMENT): cv.string, vol.Optional(CONF_VALUE_TEMPLATE): cv.template, }) @@ -48,7 +48,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ def setup_platform(hass, config, add_devices, discovery_info=None): - """Setup the sensor platform.""" + """Set up the SQL sensor platform.""" db_url = config.get(CONF_DB_URL, None) if not db_url: db_url = DEFAULT_URL.format( @@ -90,10 +90,10 @@ def setup_platform(hass, config, add_devices, discovery_info=None): class SQLSensor(Entity): - """An SQL sensor.""" + """Representation of an SQL sensor.""" def __init__(self, name, sessmaker, query, column, unit, value_template): - """Initialize SQL sensor.""" + """Initialize the SQL sensor.""" self._name = name if "LIMIT" in query: self._query = query diff --git a/requirements_all.txt b/requirements_all.txt index 6eb9f91f0e2..d8ee0286c69 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1218,7 +1218,7 @@ spotcrime==1.0.3 # homeassistant.components.recorder # homeassistant.scripts.db_migrator # homeassistant.components.sensor.sql -sqlalchemy==1.2.6 +sqlalchemy==1.2.7 # homeassistant.components.statsd statsd==3.2.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 36e8df39bd3..5d6fe9e2f63 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -188,7 +188,7 @@ somecomfort==0.5.2 # homeassistant.components.recorder # homeassistant.scripts.db_migrator # homeassistant.components.sensor.sql -sqlalchemy==1.2.6 +sqlalchemy==1.2.7 # homeassistant.components.statsd statsd==3.2.1 From 449085313b63b0a1896636c7455459647be6b9ed Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Sat, 28 Apr 2018 23:16:34 +0200 Subject: [PATCH 048/155] Upgrade tapsaff to 0.2.0 (#14137) --- homeassistant/components/binary_sensor/tapsaff.py | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/binary_sensor/tapsaff.py b/homeassistant/components/binary_sensor/tapsaff.py index 09d28b96f72..c0f6ca3f112 100644 --- a/homeassistant/components/binary_sensor/tapsaff.py +++ b/homeassistant/components/binary_sensor/tapsaff.py @@ -14,7 +14,7 @@ from homeassistant.components.binary_sensor import ( from homeassistant.const import CONF_NAME import homeassistant.helpers.config_validation as cv -REQUIREMENTS = ['tapsaff==0.1.3'] +REQUIREMENTS = ['tapsaff==0.2.0'] _LOGGER = logging.getLogger(__name__) diff --git a/requirements_all.txt b/requirements_all.txt index d8ee0286c69..f85c44a2965 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1236,7 +1236,7 @@ tahoma-api==0.0.13 tank_utility==1.4.0 # homeassistant.components.binary_sensor.tapsaff -tapsaff==0.1.3 +tapsaff==0.2.0 # homeassistant.components.tellstick tellcore-net==0.4 From 9a9161477fbdece538882625d906785423ff335a Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Sat, 28 Apr 2018 23:16:51 +0200 Subject: [PATCH 049/155] Upgrade python-telegram-bot to 10.0.2 (#14144) --- homeassistant/components/telegram_bot/__init__.py | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/telegram_bot/__init__.py b/homeassistant/components/telegram_bot/__init__.py index e43640e4df2..af0fe5bd572 100644 --- a/homeassistant/components/telegram_bot/__init__.py +++ b/homeassistant/components/telegram_bot/__init__.py @@ -22,7 +22,7 @@ import homeassistant.helpers.config_validation as cv from homeassistant.exceptions import TemplateError from homeassistant.setup import async_prepare_setup_platform -REQUIREMENTS = ['python-telegram-bot==10.0.1'] +REQUIREMENTS = ['python-telegram-bot==10.0.2'] _LOGGER = logging.getLogger(__name__) diff --git a/requirements_all.txt b/requirements_all.txt index f85c44a2965..87c6aeff8c2 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1027,7 +1027,7 @@ python-synology==0.1.0 python-tado==0.2.3 # homeassistant.components.telegram_bot -python-telegram-bot==10.0.1 +python-telegram-bot==10.0.2 # homeassistant.components.sensor.twitch python-twitch==1.3.0 From 84f163252aece208703597164d9b4fd042bb8f77 Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Sat, 28 Apr 2018 23:17:10 +0200 Subject: [PATCH 050/155] Upgrade youtube_dl to 2018.04.25 (#14136) --- homeassistant/components/media_extractor.py | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/media_extractor.py b/homeassistant/components/media_extractor.py index b5fd26b0bcb..fe6ebe8e618 100644 --- a/homeassistant/components/media_extractor.py +++ b/homeassistant/components/media_extractor.py @@ -14,7 +14,7 @@ from homeassistant.components.media_player import ( SERVICE_PLAY_MEDIA) from homeassistant.helpers import config_validation as cv -REQUIREMENTS = ['youtube_dl==2018.04.16'] +REQUIREMENTS = ['youtube_dl==2018.04.25'] _LOGGER = logging.getLogger(__name__) diff --git a/requirements_all.txt b/requirements_all.txt index 87c6aeff8c2..ec2182fd24f 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1364,7 +1364,7 @@ yeelight==0.4.0 yeelightsunflower==0.0.10 # homeassistant.components.media_extractor -youtube_dl==2018.04.16 +youtube_dl==2018.04.25 # homeassistant.components.light.zengge zengge==0.2 From 2091f86e25285af2860b2709fec0c2f5aef172bb Mon Sep 17 00:00:00 2001 From: Matt Schmitt Date: Sat, 28 Apr 2018 17:17:30 -0400 Subject: [PATCH 051/155] Clean up HomeKit accessory information characteristics (#14114) * Update accessory information characteristics * Add firmware revision characteristic --- .../components/homekit/accessories.py | 21 ++++++++++----- homeassistant/components/homekit/const.py | 8 +++--- tests/components/homekit/test_accessories.py | 26 +++++++++++-------- 3 files changed, 34 insertions(+), 21 deletions(-) diff --git a/homeassistant/components/homekit/accessories.py b/homeassistant/components/homekit/accessories.py index d9b90a77d68..c7703b221d8 100644 --- a/homeassistant/components/homekit/accessories.py +++ b/homeassistant/components/homekit/accessories.py @@ -7,15 +7,17 @@ import logging from pyhap.accessory import Accessory, Bridge, Category from pyhap.accessory_driver import AccessoryDriver +from homeassistant.const import __version__ from homeassistant.core import callback as ha_callback +from homeassistant.core import split_entity_id from homeassistant.helpers.event import ( async_track_state_change, track_point_in_utc_time) from homeassistant.util import dt as dt_util from .const import ( - DEBOUNCE_TIMEOUT, BRIDGE_MODEL, BRIDGE_NAME, MANUFACTURER, - SERV_ACCESSORY_INFO, CHAR_MANUFACTURER, - CHAR_MODEL, CHAR_NAME, CHAR_SERIAL_NUMBER) + DEBOUNCE_TIMEOUT, BRIDGE_MODEL, BRIDGE_NAME, BRIDGE_SERIAL_NUMBER, + MANUFACTURER, SERV_ACCESSORY_INFO, CHAR_FIRMWARE_REVISION, + CHAR_MANUFACTURER, CHAR_MODEL, CHAR_NAME, CHAR_SERIAL_NUMBER) from .util import ( show_setup_message, dismiss_setup_message) @@ -84,14 +86,17 @@ def setup_char(char_name, service, value=None, properties=None, callback=None): return char -def set_accessory_info(acc, name, model, manufacturer=MANUFACTURER, - serial_number='0000'): +def set_accessory_info(acc, name, model, serial_number, + manufacturer=MANUFACTURER, + firmware_revision=__version__): """Set the default accessory information.""" service = acc.get_service(SERV_ACCESSORY_INFO) service.get_characteristic(CHAR_NAME).set_value(name) service.get_characteristic(CHAR_MODEL).set_value(model) service.get_characteristic(CHAR_MANUFACTURER).set_value(manufacturer) service.get_characteristic(CHAR_SERIAL_NUMBER).set_value(serial_number) + service.get_characteristic(CHAR_FIRMWARE_REVISION) \ + .set_value(firmware_revision) class HomeAccessory(Accessory): @@ -100,7 +105,8 @@ class HomeAccessory(Accessory): def __init__(self, hass, name, entity_id, aid, category): """Initialize a Accessory object.""" super().__init__(name, aid=aid) - set_accessory_info(self, name, model=entity_id) + domain = split_entity_id(entity_id)[0].replace("_", " ").title() + set_accessory_info(self, name, model=domain, serial_number=entity_id) self.category = getattr(Category, category, Category.OTHER) self.entity_id = entity_id self.hass = hass @@ -137,7 +143,8 @@ class HomeBridge(Bridge): def __init__(self, hass, name=BRIDGE_NAME): """Initialize a Bridge object.""" super().__init__(name) - set_accessory_info(self, name, model=BRIDGE_MODEL) + set_accessory_info(self, name, model=BRIDGE_MODEL, + serial_number=BRIDGE_SERIAL_NUMBER) self.hass = hass def _set_services(self): diff --git a/homeassistant/components/homekit/const.py b/homeassistant/components/homekit/const.py index 59444c75421..9c9f60eef94 100644 --- a/homeassistant/components/homekit/const.py +++ b/homeassistant/components/homekit/const.py @@ -18,9 +18,10 @@ DEFAULT_PORT = 51827 SERVICE_HOMEKIT_START = 'start' # #### STRING CONSTANTS #### -BRIDGE_MODEL = 'homekit.bridge' -BRIDGE_NAME = 'Home Assistant' -MANUFACTURER = 'HomeAssistant' +BRIDGE_MODEL = 'Bridge' +BRIDGE_NAME = 'Home Assistant Bridge' +BRIDGE_SERIAL_NUMBER = 'homekit.bridge' +MANUFACTURER = 'Home Assistant' # #### Categories #### CATEGORY_ALARM_SYSTEM = 'ALARM_SYSTEM' @@ -74,6 +75,7 @@ CHAR_CURRENT_POSITION = 'CurrentPosition' # Int | [0, 100] CHAR_CURRENT_HUMIDITY = 'CurrentRelativeHumidity' # percent CHAR_CURRENT_SECURITY_STATE = 'SecuritySystemCurrentState' CHAR_CURRENT_TEMPERATURE = 'CurrentTemperature' +CHAR_FIRMWARE_REVISION = 'FirmwareRevision' CHAR_HEATING_THRESHOLD_TEMPERATURE = 'HeatingThresholdTemperature' CHAR_HUE = 'Hue' # arcdegress | [0, 360] CHAR_LEAK_DETECTED = 'LeakDetected' diff --git a/tests/components/homekit/test_accessories.py b/tests/components/homekit/test_accessories.py index f8e026483aa..3df76185a51 100644 --- a/tests/components/homekit/test_accessories.py +++ b/tests/components/homekit/test_accessories.py @@ -10,8 +10,8 @@ from homeassistant.components.homekit.accessories import ( add_preload_service, set_accessory_info, debounce, HomeAccessory, HomeBridge, HomeDriver) from homeassistant.components.homekit.const import ( - BRIDGE_MODEL, BRIDGE_NAME, SERV_ACCESSORY_INFO, - CHAR_MANUFACTURER, CHAR_MODEL, CHAR_NAME, CHAR_SERIAL_NUMBER) + BRIDGE_MODEL, BRIDGE_NAME, SERV_ACCESSORY_INFO, CHAR_FIRMWARE_REVISION, + CHAR_MANUFACTURER, CHAR_MODEL, CHAR_NAME, CHAR_SERIAL_NUMBER, MANUFACTURER) from homeassistant.const import ATTR_NOW, EVENT_TIME_CHANGED import homeassistant.util.dt as dt_util @@ -92,26 +92,30 @@ class TestAccessories(unittest.TestCase): """Test setting the basic accessory information.""" # Test HomeAccessory acc = HomeAccessory('HA', 'Home Accessory', 'homekit.accessory', 2, '') - set_accessory_info(acc, 'name', 'model', 'manufacturer', '0000') + set_accessory_info(acc, 'name', 'model', '0000', MANUFACTURER, '1.2.3') serv = acc.get_service(SERV_ACCESSORY_INFO) self.assertEqual(serv.get_characteristic(CHAR_NAME).value, 'name') self.assertEqual(serv.get_characteristic(CHAR_MODEL).value, 'model') - self.assertEqual( - serv.get_characteristic(CHAR_MANUFACTURER).value, 'manufacturer') self.assertEqual( serv.get_characteristic(CHAR_SERIAL_NUMBER).value, '0000') + self.assertEqual( + serv.get_characteristic(CHAR_MANUFACTURER).value, MANUFACTURER) + self.assertEqual( + serv.get_characteristic(CHAR_FIRMWARE_REVISION).value, '1.2.3') # Test HomeBridge acc = HomeBridge('hass') - set_accessory_info(acc, 'name', 'model', 'manufacturer', '0000') + set_accessory_info(acc, 'name', 'model', '0000', MANUFACTURER, '1.2.3') serv = acc.get_service(SERV_ACCESSORY_INFO) self.assertEqual(serv.get_characteristic(CHAR_MODEL).value, 'model') - self.assertEqual( - serv.get_characteristic(CHAR_MANUFACTURER).value, 'manufacturer') self.assertEqual( serv.get_characteristic(CHAR_SERIAL_NUMBER).value, '0000') + self.assertEqual( + serv.get_characteristic(CHAR_MANUFACTURER).value, MANUFACTURER) + self.assertEqual( + serv.get_characteristic(CHAR_FIRMWARE_REVISION).value, '1.2.3') def test_home_accessory(self): """Test HomeAccessory class.""" @@ -124,7 +128,7 @@ class TestAccessories(unittest.TestCase): self.assertEqual(len(acc.services), 1) serv = acc.services[0] # SERV_ACCESSORY_INFO self.assertEqual( - serv.get_characteristic(CHAR_MODEL).value, 'homekit.accessory') + serv.get_characteristic(CHAR_MODEL).value, 'Homekit') hass.states.set('homekit.accessory', 'on') hass.block_till_done() @@ -132,13 +136,13 @@ class TestAccessories(unittest.TestCase): hass.states.set('homekit.accessory', 'off') hass.block_till_done() - acc = HomeAccessory('hass', 'test_name', 'test_model', 2, '') + acc = HomeAccessory('hass', 'test_name', 'test_model.demo', 2, '') self.assertEqual(acc.display_name, 'test_name') self.assertEqual(acc.aid, 2) self.assertEqual(len(acc.services), 1) serv = acc.services[0] # SERV_ACCESSORY_INFO self.assertEqual( - serv.get_characteristic(CHAR_MODEL).value, 'test_model') + serv.get_characteristic(CHAR_MODEL).value, 'Test Model') hass.stop() From 4205dc0f7c6fa227e1b76d88ccf171dc149aae2b Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Sat, 28 Apr 2018 23:17:38 +0200 Subject: [PATCH 052/155] Upgrade psutil to 5.4.5 (#14135) --- homeassistant/components/sensor/systemmonitor.py | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/sensor/systemmonitor.py b/homeassistant/components/sensor/systemmonitor.py index 2f970796fe1..0b85de8e4f2 100644 --- a/homeassistant/components/sensor/systemmonitor.py +++ b/homeassistant/components/sensor/systemmonitor.py @@ -16,7 +16,7 @@ from homeassistant.helpers.entity import Entity import homeassistant.helpers.config_validation as cv import homeassistant.util.dt as dt_util -REQUIREMENTS = ['psutil==5.4.3'] +REQUIREMENTS = ['psutil==5.4.5'] _LOGGER = logging.getLogger(__name__) diff --git a/requirements_all.txt b/requirements_all.txt index ec2182fd24f..e9bd0d71de1 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -642,7 +642,7 @@ proliphix==0.4.1 prometheus_client==0.1.0 # homeassistant.components.sensor.systemmonitor -psutil==5.4.3 +psutil==5.4.5 # homeassistant.components.wink pubnubsub-handler==1.0.2 From 07f94eaa928bb91f18de4ec5be6fc4599bd28992 Mon Sep 17 00:00:00 2001 From: Gabe Date: Sat, 28 Apr 2018 17:12:40 -0500 Subject: [PATCH 053/155] Fixed datetime values (#14153) --- homeassistant/components/sensor/sql.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/homeassistant/components/sensor/sql.py b/homeassistant/components/sensor/sql.py index 7d18bb3f049..b7ece1bdb87 100644 --- a/homeassistant/components/sensor/sql.py +++ b/homeassistant/components/sensor/sql.py @@ -5,6 +5,7 @@ For more details about this platform, please refer to the documentation at https://home-assistant.io/components/sensor.sql/ """ import decimal +import datetime import logging import voluptuous as vol @@ -145,6 +146,8 @@ class SQLSensor(Entity): for key, value in res.items(): if isinstance(value, decimal.Decimal): value = float(value) + if isinstance(value, datetime.date): + value = str(value) self._attributes[key] = value except sqlalchemy.exc.SQLAlchemyError as err: _LOGGER.error("Error executing query %s: %s", self._query, err) From 44ddc6ba62e624ace7870791a1452f67332f17e0 Mon Sep 17 00:00:00 2001 From: engrbm87 Date: Sun, 29 Apr 2018 02:16:22 +0400 Subject: [PATCH 054/155] deluge-components-update (#14016) --- homeassistant/components/sensor/deluge.py | 27 ++++++++++++++----- homeassistant/components/switch/deluge.py | 32 +++++++++++++++++------ requirements_all.txt | 2 +- 3 files changed, 45 insertions(+), 16 deletions(-) diff --git a/homeassistant/components/sensor/deluge.py b/homeassistant/components/sensor/deluge.py index f4793867d4c..8acbda74d7d 100644 --- a/homeassistant/components/sensor/deluge.py +++ b/homeassistant/components/sensor/deluge.py @@ -14,8 +14,9 @@ from homeassistant.const import ( CONF_HOST, CONF_PASSWORD, CONF_USERNAME, CONF_NAME, CONF_PORT, CONF_MONITORED_VARIABLES, STATE_IDLE) from homeassistant.helpers.entity import Entity +from homeassistant.exceptions import PlatformNotReady -REQUIREMENTS = ['deluge-client==1.0.5'] +REQUIREMENTS = ['deluge-client==1.4.0'] _LOGGER = logging.getLogger(__name__) _THROTTLED_REFRESH = None @@ -24,7 +25,6 @@ DEFAULT_NAME = 'Deluge' DEFAULT_PORT = 58846 DHT_UPLOAD = 1000 DHT_DOWNLOAD = 1000 - SENSOR_TYPES = { 'current_status': ['Status', None], 'download_speed': ['Down Speed', 'kB/s'], @@ -58,8 +58,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): deluge_api.connect() except ConnectionRefusedError: _LOGGER.error("Connection to Deluge Daemon failed") - return - + raise PlatformNotReady dev = [] for variable in config[CONF_MONITORED_VARIABLES]: dev.append(DelugeSensor(variable, deluge_api, name)) @@ -79,6 +78,7 @@ class DelugeSensor(Entity): self._state = None self._unit_of_measurement = SENSOR_TYPES[sensor_type][1] self.data = None + self._available = False @property def name(self): @@ -90,6 +90,11 @@ class DelugeSensor(Entity): """Return the state of the sensor.""" return self._state + @property + def available(self): + """Return true if device is available.""" + return self._available + @property def unit_of_measurement(self): """Return the unit of measurement of this entity, if any.""" @@ -97,9 +102,17 @@ class DelugeSensor(Entity): def update(self): """Get the latest data from Deluge and updates the state.""" - self.data = self.client.call('core.get_session_status', - ['upload_rate', 'download_rate', - 'dht_upload_rate', 'dht_download_rate']) + from deluge_client import FailedToReconnectException + try: + self.data = self.client.call('core.get_session_status', + ['upload_rate', 'download_rate', + 'dht_upload_rate', + 'dht_download_rate']) + self._available = True + except FailedToReconnectException: + _LOGGER.error("Connection to Deluge Daemon Lost") + self._available = False + return upload = self.data[b'upload_rate'] - self.data[b'dht_upload_rate'] download = self.data[b'download_rate'] - self.data[ diff --git a/homeassistant/components/switch/deluge.py b/homeassistant/components/switch/deluge.py index 30287a2669e..da0b3bf3228 100644 --- a/homeassistant/components/switch/deluge.py +++ b/homeassistant/components/switch/deluge.py @@ -9,15 +9,16 @@ import logging import voluptuous as vol from homeassistant.components.switch import PLATFORM_SCHEMA +from homeassistant.exceptions import PlatformNotReady from homeassistant.const import ( CONF_HOST, CONF_NAME, CONF_PORT, CONF_PASSWORD, CONF_USERNAME, STATE_OFF, STATE_ON) from homeassistant.helpers.entity import ToggleEntity import homeassistant.helpers.config_validation as cv -REQUIREMENTS = ['deluge-client==1.0.5'] +REQUIREMENTS = ['deluge-client==1.4.0'] -_LOGGING = logging.getLogger(__name__) +_LOGGER = logging.getLogger(__name__) DEFAULT_NAME = 'Deluge Switch' DEFAULT_PORT = 58846 @@ -46,8 +47,8 @@ def setup_platform(hass, config, add_devices, discovery_info=None): try: deluge_api.connect() except ConnectionRefusedError: - _LOGGING.error("Connection to Deluge Daemon failed") - return + _LOGGER.error("Connection to Deluge Daemon failed") + raise PlatformNotReady add_devices([DelugeSwitch(deluge_api, name)]) @@ -60,6 +61,7 @@ class DelugeSwitch(ToggleEntity): self._name = name self.deluge_client = deluge_client self._state = STATE_OFF + self._available = False @property def name(self): @@ -76,18 +78,32 @@ class DelugeSwitch(ToggleEntity): """Return true if device is on.""" return self._state == STATE_ON + @property + def available(self): + """Return true if device is available.""" + return self._available + def turn_on(self, **kwargs): """Turn the device on.""" - self.deluge_client.call('core.resume_all_torrents') + torrent_ids = self.deluge_client.call('core.get_session_state') + self.deluge_client.call('core.resume_torrent', torrent_ids) def turn_off(self, **kwargs): """Turn the device off.""" - self.deluge_client.call('core.pause_all_torrents') + torrent_ids = self.deluge_client.call('core.get_session_state') + self.deluge_client.call('core.pause_torrent', torrent_ids) def update(self): """Get the latest data from deluge and updates the state.""" - torrent_list = self.deluge_client.call('core.get_torrents_status', {}, - ['paused']) + from deluge_client import FailedToReconnectException + try: + torrent_list = self.deluge_client.call('core.get_torrents_status', + {}, ['paused']) + self._available = True + except FailedToReconnectException: + _LOGGER.error("Connection to Deluge Daemon Lost") + self._available = False + return for torrent in torrent_list.values(): item = torrent.popitem() if not item[1]: diff --git a/requirements_all.txt b/requirements_all.txt index e9bd0d71de1..f0be99705c1 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -243,7 +243,7 @@ defusedxml==0.5.0 # homeassistant.components.sensor.deluge # homeassistant.components.switch.deluge -deluge-client==1.0.5 +deluge-client==1.4.0 # homeassistant.components.media_player.denonavr denonavr==0.6.1 From a0b14c29137a4643243b14c7851a9e7b45edfe7c Mon Sep 17 00:00:00 2001 From: Otto Winter Date: Sun, 29 Apr 2018 00:33:10 +0200 Subject: [PATCH 055/155] Light mqtt_json: Add HS color support (#14029) * Light mqtt_json HS color support * Lint * Catch float ValueError --- homeassistant/components/light/mqtt_json.py | 27 ++++++++++++++-- tests/components/light/test_mqtt_json.py | 36 +++++++++++++++++++++ 2 files changed, 60 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/light/mqtt_json.py b/homeassistant/components/light/mqtt_json.py index 20e49e40bae..a0bfc5a0787 100644 --- a/homeassistant/components/light/mqtt_json.py +++ b/homeassistant/components/light/mqtt_json.py @@ -44,12 +44,14 @@ DEFAULT_OPTIMISTIC = False DEFAULT_RGB = False DEFAULT_WHITE_VALUE = False DEFAULT_XY = False +DEFAULT_HS = False DEFAULT_BRIGHTNESS_SCALE = 255 CONF_EFFECT_LIST = 'effect_list' CONF_FLASH_TIME_LONG = 'flash_time_long' CONF_FLASH_TIME_SHORT = 'flash_time_short' +CONF_HS = 'hs' # Stealing some of these from the base MQTT configs. PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ @@ -72,6 +74,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Optional(CONF_STATE_TOPIC): mqtt.valid_subscribe_topic, vol.Optional(CONF_WHITE_VALUE, default=DEFAULT_WHITE_VALUE): cv.boolean, vol.Optional(CONF_XY, default=DEFAULT_XY): cv.boolean, + vol.Optional(CONF_HS, default=DEFAULT_HS): cv.boolean, vol.Required(CONF_COMMAND_TOPIC): mqtt.valid_publish_topic, }).extend(mqtt.MQTT_AVAILABILITY_SCHEMA.schema) @@ -99,6 +102,7 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None): config.get(CONF_RGB), config.get(CONF_WHITE_VALUE), config.get(CONF_XY), + config.get(CONF_HS), { key: config.get(key) for key in ( CONF_FLASH_TIME_SHORT, @@ -116,7 +120,7 @@ class MqttJson(MqttAvailability, Light): """Representation of a MQTT JSON light.""" def __init__(self, name, effect_list, topic, qos, retain, optimistic, - brightness, color_temp, effect, rgb, white_value, xy, + brightness, color_temp, effect, rgb, white_value, xy, hs, flash_times, availability_topic, payload_available, payload_not_available, brightness_scale): """Initialize MQTT JSON light.""" @@ -131,6 +135,7 @@ class MqttJson(MqttAvailability, Light): self._state = False self._rgb = rgb self._xy = xy + self._hs_support = hs if brightness: self._brightness = 255 else: @@ -146,7 +151,7 @@ class MqttJson(MqttAvailability, Light): else: self._effect = None - if rgb or xy: + if hs or rgb or xy: self._hs = [0, 0] else: self._hs = None @@ -166,6 +171,7 @@ class MqttJson(MqttAvailability, Light): self._supported_features |= (effect and SUPPORT_EFFECT) self._supported_features |= (white_value and SUPPORT_WHITE_VALUE) self._supported_features |= (xy and SUPPORT_COLOR) + self._supported_features |= (hs and SUPPORT_COLOR) @asyncio.coroutine def async_added_to_hass(self): @@ -193,6 +199,7 @@ class MqttJson(MqttAvailability, Light): pass except ValueError: _LOGGER.warning("Invalid RGB color value received") + try: x_color = float(values['color']['x']) y_color = float(values['color']['y']) @@ -203,6 +210,16 @@ class MqttJson(MqttAvailability, Light): except ValueError: _LOGGER.warning("Invalid XY color value received") + try: + hue = float(values['color']['h']) + saturation = float(values['color']['s']) + + self._hs = (hue, saturation) + except KeyError: + pass + except ValueError: + _LOGGER.warning("Invalid HS color value received") + if self._brightness is not None: try: self._brightness = int(values['brightness'] / @@ -309,7 +326,8 @@ class MqttJson(MqttAvailability, Light): message = {'state': 'ON'} - if ATTR_HS_COLOR in kwargs and (self._rgb or self._xy): + if ATTR_HS_COLOR in kwargs and (self._hs_support + or self._rgb or self._xy): hs_color = kwargs[ATTR_HS_COLOR] message['color'] = {} if self._rgb: @@ -325,6 +343,9 @@ class MqttJson(MqttAvailability, Light): xy_color = color_util.color_hs_to_xy(*kwargs[ATTR_HS_COLOR]) message['color']['x'] = xy_color[0] message['color']['y'] = xy_color[1] + if self._hs_support: + message['color']['h'] = hs_color[0] + message['color']['s'] = hs_color[1] if self._optimistic: self._hs = kwargs[ATTR_HS_COLOR] diff --git a/tests/components/light/test_mqtt_json.py b/tests/components/light/test_mqtt_json.py index d6835b00be0..5bae1061b7f 100644 --- a/tests/components/light/test_mqtt_json.py +++ b/tests/components/light/test_mqtt_json.py @@ -146,6 +146,7 @@ class TestLightMQTTJSON(unittest.TestCase): self.assertIsNone(state.attributes.get('effect')) self.assertIsNone(state.attributes.get('white_value')) self.assertIsNone(state.attributes.get('xy_color')) + self.assertIsNone(state.attributes.get('hs_color')) fire_mqtt_message(self.hass, 'test_light_rgb', '{"state":"ON"}') self.hass.block_till_done() @@ -158,6 +159,7 @@ class TestLightMQTTJSON(unittest.TestCase): self.assertIsNone(state.attributes.get('effect')) self.assertIsNone(state.attributes.get('white_value')) self.assertIsNone(state.attributes.get('xy_color')) + self.assertIsNone(state.attributes.get('hs_color')) def test_controlling_state_via_topic(self): \ # pylint: disable=invalid-name @@ -174,6 +176,7 @@ class TestLightMQTTJSON(unittest.TestCase): 'rgb': True, 'white_value': True, 'xy': True, + 'hs': True, 'qos': '0' } }) @@ -187,6 +190,7 @@ class TestLightMQTTJSON(unittest.TestCase): self.assertIsNone(state.attributes.get('effect')) self.assertIsNone(state.attributes.get('white_value')) self.assertIsNone(state.attributes.get('xy_color')) + self.assertIsNone(state.attributes.get('hs_color')) self.assertFalse(state.attributes.get(ATTR_ASSUMED_STATE)) # Turn on the light, full white @@ -207,6 +211,7 @@ class TestLightMQTTJSON(unittest.TestCase): self.assertEqual('colorloop', state.attributes.get('effect')) self.assertEqual(150, state.attributes.get('white_value')) self.assertEqual((0.323, 0.329), state.attributes.get('xy_color')) + self.assertEqual((0.0, 0.0), state.attributes.get('hs_color')) # Turn the light off fire_mqtt_message(self.hass, 'test_light_rgb', '{"state":"OFF"}') @@ -243,6 +248,15 @@ class TestLightMQTTJSON(unittest.TestCase): self.assertEqual((0.141, 0.14), light_state.attributes.get('xy_color')) + fire_mqtt_message(self.hass, 'test_light_rgb', + '{"state":"ON",' + '"color":{"h":180,"s":50}}') + self.hass.block_till_done() + + light_state = self.hass.states.get('light.test') + self.assertEqual((180.0, 50.0), + light_state.attributes.get('hs_color')) + fire_mqtt_message(self.hass, 'test_light_rgb', '{"state":"ON",' '"color_temp":155}') @@ -361,6 +375,28 @@ class TestLightMQTTJSON(unittest.TestCase): self.assertEqual(50, state.attributes['brightness']) self.assertEqual((125, 100), state.attributes['hs_color']) + def test_sending_hs_color(self): + """Test light.turn_on with hs color sends hs color parameters.""" + assert setup_component(self.hass, light.DOMAIN, { + light.DOMAIN: { + 'platform': 'mqtt_json', + 'name': 'test', + 'command_topic': 'test_light_rgb/set', + 'hs': True, + } + }) + + light.turn_on(self.hass, 'light.test', hs_color=(180.0, 50.0)) + self.hass.block_till_done() + + message_json = json.loads( + self.mock_publish.async_publish.mock_calls[0][1][1]) + self.assertEqual("ON", message_json["state"]) + self.assertEqual({ + 'h': 180.0, + 's': 50.0, + }, message_json["color"]) + def test_flash_short_and_long(self): \ # pylint: disable=invalid-name """Test for flash length being sent when included.""" From a4bf42104489c2ee4558596ae19b106812dafbcb Mon Sep 17 00:00:00 2001 From: Otto Winter Date: Sun, 29 Apr 2018 01:26:20 +0200 Subject: [PATCH 056/155] Convert more files to async/await syntax (#14142) * Move more files to async/await syntax * Attempt Work around pylint bug Using lazytox :P --- homeassistant/bootstrap.py | 68 +++++++++--------- homeassistant/components/api.py | 50 ++++++------- .../components/device_tracker/gpslogger.py | 9 ++- homeassistant/components/dialogflow.py | 16 ++--- homeassistant/components/fan/mqtt.py | 31 ++++---- .../components/google_assistant/__init__.py | 10 ++- .../components/google_assistant/auth.py | 4 +- .../components/google_assistant/http.py | 8 +-- homeassistant/components/group/__init__.py | 71 ++++++++----------- homeassistant/components/history.py | 14 ++-- homeassistant/components/history_graph.py | 6 +- homeassistant/components/input_boolean.py | 25 +++---- homeassistant/components/light/mqtt_json.py | 19 +++-- homeassistant/components/logbook.py | 12 ++-- homeassistant/components/logger.py | 7 +- homeassistant/components/map.py | 7 +- .../components/media_player/__init__.py | 46 +++++------- .../components/media_player/universal.py | 20 +++--- homeassistant/components/recorder/__init__.py | 8 +-- homeassistant/components/sensor/mqtt.py | 15 ++-- .../components/sensor/wunderground.py | 7 +- homeassistant/components/switch/mqtt.py | 18 ++--- homeassistant/components/timer/__init__.py | 40 +++++------ homeassistant/components/tts/google.py | 12 ++-- homeassistant/components/updater.py | 24 +++---- homeassistant/util/logging.py | 7 +- tests/test_bootstrap.py | 2 +- 27 files changed, 229 insertions(+), 327 deletions(-) diff --git a/homeassistant/bootstrap.py b/homeassistant/bootstrap.py index e0962568a66..0abe5a7811e 100644 --- a/homeassistant/bootstrap.py +++ b/homeassistant/bootstrap.py @@ -67,16 +67,15 @@ def from_config_dict(config: Dict[str, Any], return hass -@asyncio.coroutine -def async_from_config_dict(config: Dict[str, Any], - hass: core.HomeAssistant, - config_dir: Optional[str] = None, - enable_log: bool = True, - verbose: bool = False, - skip_pip: bool = False, - log_rotate_days: Any = None, - log_file: Any = None, - log_no_color: bool = False) \ +async def async_from_config_dict(config: Dict[str, Any], + hass: core.HomeAssistant, + config_dir: Optional[str] = None, + enable_log: bool = True, + verbose: bool = False, + skip_pip: bool = False, + log_rotate_days: Any = None, + log_file: Any = None, + log_no_color: bool = False) \ -> Optional[core.HomeAssistant]: """Try to configure Home Assistant from a configuration dictionary. @@ -92,12 +91,12 @@ def async_from_config_dict(config: Dict[str, Any], core_config = config.get(core.DOMAIN, {}) try: - yield from conf_util.async_process_ha_core_config(hass, core_config) + await conf_util.async_process_ha_core_config(hass, core_config) except vol.Invalid as ex: conf_util.async_log_exception(ex, 'homeassistant', core_config, hass) return None - yield from hass.async_add_job(conf_util.process_ha_config_upgrade, hass) + await hass.async_add_job(conf_util.process_ha_config_upgrade, hass) hass.config.skip_pip = skip_pip if skip_pip: @@ -105,7 +104,7 @@ def async_from_config_dict(config: Dict[str, Any], "This may cause issues") if not loader.PREPARED: - yield from hass.async_add_job(loader.prepare, hass) + await hass.async_add_job(loader.prepare, hass) # Make a copy because we are mutating it. config = OrderedDict(config) @@ -120,7 +119,7 @@ def async_from_config_dict(config: Dict[str, Any], config[key] = {} hass.config_entries = config_entries.ConfigEntries(hass, config) - yield from hass.config_entries.async_load() + await hass.config_entries.async_load() # Filter out the repeating and common config section [homeassistant] components = set(key.split(' ')[0] for key in config.keys() @@ -129,13 +128,13 @@ def async_from_config_dict(config: Dict[str, Any], # setup components # pylint: disable=not-an-iterable - res = yield from core_components.async_setup(hass, config) + res = await core_components.async_setup(hass, config) if not res: _LOGGER.error("Home Assistant core failed to initialize. " "further initialization aborted") return hass - yield from persistent_notification.async_setup(hass, config) + await persistent_notification.async_setup(hass, config) _LOGGER.info("Home Assistant core initialized") @@ -145,7 +144,7 @@ def async_from_config_dict(config: Dict[str, Any], continue hass.async_add_job(async_setup_component(hass, component, config)) - yield from hass.async_block_till_done() + await hass.async_block_till_done() # stage 2 for component in components: @@ -153,7 +152,7 @@ def async_from_config_dict(config: Dict[str, Any], continue hass.async_add_job(async_setup_component(hass, component, config)) - yield from hass.async_block_till_done() + await hass.async_block_till_done() stop = time() _LOGGER.info("Home Assistant initialized in %.2fs", stop-start) @@ -187,14 +186,13 @@ def from_config_file(config_path: str, return hass -@asyncio.coroutine -def async_from_config_file(config_path: str, - hass: core.HomeAssistant, - verbose: bool = False, - skip_pip: bool = True, - log_rotate_days: Any = None, - log_file: Any = None, - log_no_color: bool = False): +async def async_from_config_file(config_path: str, + hass: core.HomeAssistant, + verbose: bool = False, + skip_pip: bool = True, + log_rotate_days: Any = None, + log_file: Any = None, + log_no_color: bool = False): """Read the configuration file and try to start all the functionality. Will add functionality to 'hass' parameter. @@ -203,13 +201,13 @@ 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 - yield from async_mount_local_lib_path(config_dir, hass.loop) + await async_mount_local_lib_path(config_dir, hass.loop) async_enable_logging(hass, verbose, log_rotate_days, log_file, log_no_color) try: - config_dict = yield from hass.async_add_job( + config_dict = await hass.async_add_job( conf_util.load_yaml_config_file, config_path) except HomeAssistantError as err: _LOGGER.error("Error loading %s: %s", config_path, err) @@ -217,7 +215,7 @@ def async_from_config_file(config_path: str, finally: clear_secret_cache() - hass = yield from async_from_config_dict( + hass = await async_from_config_dict( config_dict, hass, enable_log=False, skip_pip=skip_pip) return hass @@ -294,11 +292,10 @@ def async_enable_logging(hass: core.HomeAssistant, async_handler = AsyncHandler(hass.loop, err_handler) - @asyncio.coroutine - def async_stop_async_handler(event): + async def async_stop_async_handler(event): """Cleanup async handler.""" logging.getLogger('').removeHandler(async_handler) - yield from async_handler.async_close(blocking=True) + await async_handler.async_close(blocking=True) hass.bus.async_listen_once( EVENT_HOMEASSISTANT_CLOSE, async_stop_async_handler) @@ -323,15 +320,14 @@ def mount_local_lib_path(config_dir: str) -> str: return deps_dir -@asyncio.coroutine -def async_mount_local_lib_path(config_dir: str, - loop: asyncio.AbstractEventLoop) -> str: +async def async_mount_local_lib_path(config_dir: str, + loop: asyncio.AbstractEventLoop) -> str: """Add local library to Python Path. This function is a coroutine. """ deps_dir = os.path.join(config_dir, 'deps') - lib_dir = yield from async_get_user_site(deps_dir, loop=loop) + lib_dir = await async_get_user_site(deps_dir, loop=loop) if lib_dir not in sys.path: sys.path.insert(0, lib_dir) return deps_dir diff --git a/homeassistant/components/api.py b/homeassistant/components/api.py index 6fdf0c027a4..83e05dae641 100644 --- a/homeassistant/components/api.py +++ b/homeassistant/components/api.py @@ -76,8 +76,7 @@ class APIEventStream(HomeAssistantView): url = URL_API_STREAM name = "api:stream" - @asyncio.coroutine - def get(self, request): + async def get(self, request): """Provide a streaming interface for the event bus.""" # pylint: disable=no-self-use hass = request.app['hass'] @@ -88,8 +87,7 @@ class APIEventStream(HomeAssistantView): if restrict: restrict = restrict.split(',') + [EVENT_HOMEASSISTANT_STOP] - @asyncio.coroutine - def forward_events(event): + async def forward_events(event): """Forward events to the open request.""" if event.event_type == EVENT_TIME_CHANGED: return @@ -104,11 +102,11 @@ class APIEventStream(HomeAssistantView): else: data = json.dumps(event, cls=rem.JSONEncoder) - yield from to_write.put(data) + await to_write.put(data) response = web.StreamResponse() response.content_type = 'text/event-stream' - yield from response.prepare(request) + await response.prepare(request) unsub_stream = hass.bus.async_listen(MATCH_ALL, forward_events) @@ -116,13 +114,13 @@ class APIEventStream(HomeAssistantView): _LOGGER.debug('STREAM %s ATTACHED', id(stop_obj)) # Fire off one message so browsers fire open event right away - yield from to_write.put(STREAM_PING_PAYLOAD) + await to_write.put(STREAM_PING_PAYLOAD) while True: try: with async_timeout.timeout(STREAM_PING_INTERVAL, loop=hass.loop): - payload = yield from to_write.get() + payload = await to_write.get() if payload is stop_obj: break @@ -130,9 +128,9 @@ class APIEventStream(HomeAssistantView): msg = "data: {}\n\n".format(payload) _LOGGER.debug('STREAM %s WRITING %s', id(stop_obj), msg.strip()) - yield from response.write(msg.encode("UTF-8")) + await response.write(msg.encode("UTF-8")) except asyncio.TimeoutError: - yield from to_write.put(STREAM_PING_PAYLOAD) + await to_write.put(STREAM_PING_PAYLOAD) except asyncio.CancelledError: _LOGGER.debug('STREAM %s ABORT', id(stop_obj)) @@ -200,12 +198,11 @@ class APIEntityStateView(HomeAssistantView): return self.json(state) return self.json_message('Entity not found', HTTP_NOT_FOUND) - @asyncio.coroutine - def post(self, request, entity_id): + async def post(self, request, entity_id): """Update state of entity.""" hass = request.app['hass'] try: - data = yield from request.json() + data = await request.json() except ValueError: return self.json_message('Invalid JSON specified', HTTP_BAD_REQUEST) @@ -257,10 +254,9 @@ class APIEventView(HomeAssistantView): url = '/api/events/{event_type}' name = "api:event" - @asyncio.coroutine - def post(self, request, event_type): + async def post(self, request, event_type): """Fire events.""" - body = yield from request.text() + body = await request.text() try: event_data = json.loads(body) if body else None except ValueError: @@ -292,10 +288,9 @@ class APIServicesView(HomeAssistantView): url = URL_API_SERVICES name = "api:services" - @asyncio.coroutine - def get(self, request): + async def get(self, request): """Get registered services.""" - services = yield from async_services_json(request.app['hass']) + services = await async_services_json(request.app['hass']) return self.json(services) @@ -305,14 +300,13 @@ class APIDomainServicesView(HomeAssistantView): url = "/api/services/{domain}/{service}" name = "api:domain-services" - @asyncio.coroutine - def post(self, request, domain, service): + async def post(self, request, domain, service): """Call a service. Returns a list of changed states. """ hass = request.app['hass'] - body = yield from request.text() + body = await request.text() try: data = json.loads(body) if body else None except ValueError: @@ -320,7 +314,7 @@ class APIDomainServicesView(HomeAssistantView): HTTP_BAD_REQUEST) with AsyncTrackStates(hass) as changed_states: - yield from hass.services.async_call(domain, service, data, True) + await hass.services.async_call(domain, service, data, True) return self.json(changed_states) @@ -343,11 +337,10 @@ class APITemplateView(HomeAssistantView): url = URL_API_TEMPLATE name = "api:template" - @asyncio.coroutine - def post(self, request): + async def post(self, request): """Render a template.""" try: - data = yield from request.json() + data = await request.json() tpl = template.Template(data['template'], request.app['hass']) return tpl.async_render(data.get('variables')) except (ValueError, TemplateError) as ex: @@ -366,10 +359,9 @@ class APIErrorLog(HomeAssistantView): return await self.file(request, request.app['hass'].data[DATA_LOGGING]) -@asyncio.coroutine -def async_services_json(hass): +async def async_services_json(hass): """Generate services data to JSONify.""" - descriptions = yield from async_get_all_descriptions(hass) + descriptions = await async_get_all_descriptions(hass) return [{"domain": key, "services": value} for key, value in descriptions.items()] diff --git a/homeassistant/components/device_tracker/gpslogger.py b/homeassistant/components/device_tracker/gpslogger.py index 1952e6d676d..68ea9ac88ae 100644 --- a/homeassistant/components/device_tracker/gpslogger.py +++ b/homeassistant/components/device_tracker/gpslogger.py @@ -4,7 +4,6 @@ Support for the GPSLogger platform. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/device_tracker.gpslogger/ """ -import asyncio import logging from hmac import compare_digest @@ -22,6 +21,7 @@ from homeassistant.components.http import ( from homeassistant.components.device_tracker import ( # NOQA DOMAIN, PLATFORM_SCHEMA ) +from homeassistant.helpers.typing import HomeAssistantType, ConfigType _LOGGER = logging.getLogger(__name__) @@ -32,8 +32,8 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ }) -@asyncio.coroutine -def async_setup_scanner(hass, config, async_see, discovery_info=None): +async def async_setup_scanner(hass: HomeAssistantType, config: ConfigType, + async_see, discovery_info=None): """Set up an endpoint for the GPSLogger application.""" hass.http.register_view(GPSLoggerView(async_see, config)) @@ -54,8 +54,7 @@ class GPSLoggerView(HomeAssistantView): # password is set self.requires_auth = self._password is None - @asyncio.coroutine - def get(self, request: Request): + async def get(self, request: Request): """Handle for GPSLogger message received as GET.""" hass = request.app['hass'] data = request.query diff --git a/homeassistant/components/dialogflow.py b/homeassistant/components/dialogflow.py index 63205c5479c..7a0918aab25 100644 --- a/homeassistant/components/dialogflow.py +++ b/homeassistant/components/dialogflow.py @@ -4,7 +4,6 @@ Support for Dialogflow webhook. For more details about this component, please refer to the documentation at https://home-assistant.io/components/dialogflow/ """ -import asyncio import logging import voluptuous as vol @@ -37,8 +36,7 @@ class DialogFlowError(HomeAssistantError): """Raised when a DialogFlow error happens.""" -@asyncio.coroutine -def async_setup(hass, config): +async def async_setup(hass, config): """Set up Dialogflow component.""" hass.http.register_view(DialogflowIntentsView) @@ -51,16 +49,15 @@ class DialogflowIntentsView(HomeAssistantView): url = INTENTS_API_ENDPOINT name = 'api:dialogflow' - @asyncio.coroutine - def post(self, request): + async def post(self, request): """Handle Dialogflow.""" hass = request.app['hass'] - message = yield from request.json() + message = await request.json() _LOGGER.debug("Received Dialogflow request: %s", message) try: - response = yield from async_handle_message(hass, message) + response = await async_handle_message(hass, message) return b'' if response is None else self.json(response) except DialogFlowError as err: @@ -93,8 +90,7 @@ def dialogflow_error_response(hass, message, error): return dialogflow_response.as_dict() -@asyncio.coroutine -def async_handle_message(hass, message): +async def async_handle_message(hass, message): """Handle a DialogFlow message.""" req = message.get('result') action_incomplete = req['actionIncomplete'] @@ -110,7 +106,7 @@ def async_handle_message(hass, message): raise DialogFlowError( "You have not defined an action in your Dialogflow intent.") - intent_response = yield from intent.async_handle( + intent_response = await intent.async_handle( hass, DOMAIN, action, {key: {'value': value} for key, value in parameters.items()}) diff --git a/homeassistant/components/fan/mqtt.py b/homeassistant/components/fan/mqtt.py index 95ff587c613..6fa506edec6 100644 --- a/homeassistant/components/fan/mqtt.py +++ b/homeassistant/components/fan/mqtt.py @@ -4,7 +4,6 @@ Support for MQTT fans. For more details about this platform, please refer to the documentation https://home-assistant.io/components/fan.mqtt/ """ -import asyncio import logging import voluptuous as vol @@ -19,6 +18,7 @@ from homeassistant.components.mqtt import ( CONF_PAYLOAD_AVAILABLE, CONF_PAYLOAD_NOT_AVAILABLE, CONF_QOS, CONF_RETAIN, MqttAvailability) import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.typing import HomeAssistantType, ConfigType from homeassistant.components.fan import (SPEED_LOW, SPEED_MEDIUM, SPEED_HIGH, FanEntity, SUPPORT_SET_SPEED, SUPPORT_OSCILLATE, @@ -77,8 +77,8 @@ PLATFORM_SCHEMA = mqtt.MQTT_RW_PLATFORM_SCHEMA.extend({ }).extend(mqtt.MQTT_AVAILABILITY_SCHEMA.schema) -@asyncio.coroutine -def async_setup_platform(hass, config, async_add_devices, discovery_info=None): +async def async_setup_platform(hass: HomeAssistantType, config: ConfigType, + async_add_devices, discovery_info=None): """Set up the MQTT fan platform.""" if discovery_info is not None: config = PLATFORM_SCHEMA(discovery_info) @@ -149,10 +149,9 @@ class MqttFan(MqttAvailability, FanEntity): self._supported_features |= (topic[CONF_SPEED_STATE_TOPIC] is not None and SUPPORT_SET_SPEED) - @asyncio.coroutine - def async_added_to_hass(self): + async def async_added_to_hass(self): """Subscribe to MQTT events.""" - yield from super().async_added_to_hass() + await super().async_added_to_hass() templates = {} for key, tpl in list(self._templates.items()): @@ -173,7 +172,7 @@ class MqttFan(MqttAvailability, FanEntity): self.async_schedule_update_ha_state() if self._topic[CONF_STATE_TOPIC] is not None: - yield from mqtt.async_subscribe( + await mqtt.async_subscribe( self.hass, self._topic[CONF_STATE_TOPIC], state_received, self._qos) @@ -190,7 +189,7 @@ class MqttFan(MqttAvailability, FanEntity): self.async_schedule_update_ha_state() if self._topic[CONF_SPEED_STATE_TOPIC] is not None: - yield from mqtt.async_subscribe( + await mqtt.async_subscribe( self.hass, self._topic[CONF_SPEED_STATE_TOPIC], speed_received, self._qos) self._speed = SPEED_OFF @@ -206,7 +205,7 @@ class MqttFan(MqttAvailability, FanEntity): self.async_schedule_update_ha_state() if self._topic[CONF_OSCILLATION_STATE_TOPIC] is not None: - yield from mqtt.async_subscribe( + await mqtt.async_subscribe( self.hass, self._topic[CONF_OSCILLATION_STATE_TOPIC], oscillation_received, self._qos) self._oscillation = False @@ -251,8 +250,7 @@ class MqttFan(MqttAvailability, FanEntity): """Return the oscillation state.""" return self._oscillation - @asyncio.coroutine - def async_turn_on(self, speed: str = None, **kwargs) -> None: + async def async_turn_on(self, speed: str = None, **kwargs) -> None: """Turn on the entity. This method is a coroutine. @@ -261,10 +259,9 @@ class MqttFan(MqttAvailability, FanEntity): self.hass, self._topic[CONF_COMMAND_TOPIC], self._payload[STATE_ON], self._qos, self._retain) if speed: - yield from self.async_set_speed(speed) + await self.async_set_speed(speed) - @asyncio.coroutine - def async_turn_off(self, **kwargs) -> None: + async def async_turn_off(self, **kwargs) -> None: """Turn off the entity. This method is a coroutine. @@ -273,8 +270,7 @@ class MqttFan(MqttAvailability, FanEntity): self.hass, self._topic[CONF_COMMAND_TOPIC], self._payload[STATE_OFF], self._qos, self._retain) - @asyncio.coroutine - def async_set_speed(self, speed: str) -> None: + async def async_set_speed(self, speed: str) -> None: """Set the speed of the fan. This method is a coroutine. @@ -299,8 +295,7 @@ class MqttFan(MqttAvailability, FanEntity): self._speed = speed self.async_schedule_update_ha_state() - @asyncio.coroutine - def async_oscillate(self, oscillating: bool) -> None: + async def async_oscillate(self, oscillating: bool) -> None: """Set oscillation. This method is a coroutine. diff --git a/homeassistant/components/google_assistant/__init__.py b/homeassistant/components/google_assistant/__init__.py index 676654c2c91..1c6d11a7c99 100644 --- a/homeassistant/components/google_assistant/__init__.py +++ b/homeassistant/components/google_assistant/__init__.py @@ -70,8 +70,7 @@ def request_sync(hass): hass.services.call(DOMAIN, SERVICE_REQUEST_SYNC) -@asyncio.coroutine -def async_setup(hass: HomeAssistant, yaml_config: Dict[str, Any]): +async def async_setup(hass: HomeAssistant, yaml_config: Dict[str, Any]): """Activate Google Actions component.""" config = yaml_config.get(DOMAIN, {}) agent_user_id = config.get(CONF_AGENT_USER_ID) @@ -79,20 +78,19 @@ def async_setup(hass: HomeAssistant, yaml_config: Dict[str, Any]): hass.http.register_view(GoogleAssistantAuthView(hass, config)) async_register_http(hass, config) - @asyncio.coroutine - def request_sync_service_handler(call): + async def request_sync_service_handler(call): """Handle request sync service calls.""" websession = async_get_clientsession(hass) try: with async_timeout.timeout(5, loop=hass.loop): - res = yield from websession.post( + res = await websession.post( REQUEST_SYNC_BASE_URL, params={'key': api_key}, json={'agent_user_id': agent_user_id}) _LOGGER.info("Submitted request_sync request to Google") res.raise_for_status() except aiohttp.ClientResponseError: - body = yield from res.read() + body = await res.read() _LOGGER.error( 'request_sync request failed: %d %s', res.status, body) except (asyncio.TimeoutError, aiohttp.ClientError): diff --git a/homeassistant/components/google_assistant/auth.py b/homeassistant/components/google_assistant/auth.py index 1ed27403797..a21dd0e6738 100644 --- a/homeassistant/components/google_assistant/auth.py +++ b/homeassistant/components/google_assistant/auth.py @@ -1,6 +1,5 @@ """Google Assistant OAuth View.""" -import asyncio import logging # Typing imports @@ -44,8 +43,7 @@ class GoogleAssistantAuthView(HomeAssistantView): self.client_id = cfg.get(CONF_CLIENT_ID) self.access_token = cfg.get(CONF_ACCESS_TOKEN) - @asyncio.coroutine - def get(self, request: Request) -> Response: + async def get(self, request: Request) -> Response: """Handle oauth token request.""" query = request.query redirect_uri = query.get('redirect_uri') diff --git a/homeassistant/components/google_assistant/http.py b/homeassistant/components/google_assistant/http.py index 0caea3aadf4..0ea5f7d9fa4 100644 --- a/homeassistant/components/google_assistant/http.py +++ b/homeassistant/components/google_assistant/http.py @@ -4,7 +4,6 @@ Support for Google Actions Smart Home Control. For more details about this component, please refer to the documentation at https://home-assistant.io/components/google_assistant/ """ -import asyncio import logging from aiohttp.hdrs import AUTHORIZATION @@ -77,14 +76,13 @@ class GoogleAssistantView(HomeAssistantView): self.access_token = access_token self.gass_config = gass_config - @asyncio.coroutine - def post(self, request: Request) -> Response: + async def post(self, request: Request) -> Response: """Handle Google Assistant requests.""" auth = request.headers.get(AUTHORIZATION, None) if 'Bearer {}'.format(self.access_token) != auth: return self.json_message("missing authorization", status_code=401) - message = yield from request.json() # type: dict - result = yield from async_handle_message( + message = await request.json() # type: dict + result = await async_handle_message( request.app['hass'], self.gass_config, message) return self.json(result) diff --git a/homeassistant/components/group/__init__.py b/homeassistant/components/group/__init__.py index 67ad8066aff..f70a2d29351 100644 --- a/homeassistant/components/group/__init__.py +++ b/homeassistant/components/group/__init__.py @@ -245,34 +245,31 @@ def get_entity_ids(hass, entity_id, domain_filter=None): if ent_id.startswith(domain_filter)] -@asyncio.coroutine -def async_setup(hass, config): +async def async_setup(hass, config): """Set up all groups found defined in the configuration.""" component = hass.data.get(DOMAIN) if component is None: component = hass.data[DOMAIN] = EntityComponent(_LOGGER, DOMAIN, hass) - yield from _async_process_config(hass, config, component) + await _async_process_config(hass, config, component) - @asyncio.coroutine - def reload_service_handler(service): + async def reload_service_handler(service): """Remove all user-defined groups and load new ones from config.""" auto = list(filter(lambda e: not e.user_defined, component.entities)) - conf = yield from component.async_prepare_reload() + conf = await component.async_prepare_reload() if conf is None: return - yield from _async_process_config(hass, conf, component) + await _async_process_config(hass, conf, component) - yield from component.async_add_entities(auto) + await component.async_add_entities(auto) hass.services.async_register( DOMAIN, SERVICE_RELOAD, reload_service_handler, schema=RELOAD_SERVICE_SCHEMA) - @asyncio.coroutine - def groups_service_handler(service): + async def groups_service_handler(service): """Handle dynamic group service functions.""" object_id = service.data[ATTR_OBJECT_ID] entity_id = ENTITY_ID_FORMAT.format(object_id) @@ -287,7 +284,7 @@ def async_setup(hass, config): ATTR_VISIBLE, ATTR_ICON, ATTR_VIEW, ATTR_CONTROL ) if service.data.get(attr) is not None} - yield from Group.async_create_group( + await Group.async_create_group( hass, service.data.get(ATTR_NAME, object_id), object_id=object_id, entity_ids=entity_ids, @@ -308,11 +305,11 @@ def async_setup(hass, config): if ATTR_ADD_ENTITIES in service.data: delta = service.data[ATTR_ADD_ENTITIES] entity_ids = set(group.tracking) | set(delta) - yield from group.async_update_tracked_entity_ids(entity_ids) + await group.async_update_tracked_entity_ids(entity_ids) if ATTR_ENTITIES in service.data: entity_ids = service.data[ATTR_ENTITIES] - yield from group.async_update_tracked_entity_ids(entity_ids) + await group.async_update_tracked_entity_ids(entity_ids) if ATTR_NAME in service.data: group.name = service.data[ATTR_NAME] @@ -335,13 +332,13 @@ def async_setup(hass, config): need_update = True if need_update: - yield from group.async_update_ha_state() + await group.async_update_ha_state() return # remove group if service.service == SERVICE_REMOVE: - yield from component.async_remove_entity(entity_id) + await component.async_remove_entity(entity_id) hass.services.async_register( DOMAIN, SERVICE_SET, groups_service_handler, @@ -351,8 +348,7 @@ def async_setup(hass, config): DOMAIN, SERVICE_REMOVE, groups_service_handler, schema=REMOVE_SERVICE_SCHEMA) - @asyncio.coroutine - def visibility_service_handler(service): + async def visibility_service_handler(service): """Change visibility of a group.""" visible = service.data.get(ATTR_VISIBLE) @@ -363,7 +359,7 @@ def async_setup(hass, config): tasks.append(group.async_update_ha_state()) if tasks: - yield from asyncio.wait(tasks, loop=hass.loop) + await asyncio.wait(tasks, loop=hass.loop) hass.services.async_register( DOMAIN, SERVICE_SET_VISIBILITY, visibility_service_handler, @@ -372,8 +368,7 @@ def async_setup(hass, config): return True -@asyncio.coroutine -def _async_process_config(hass, config, component): +async def _async_process_config(hass, config, component): """Process group configuration.""" for object_id, conf in config.get(DOMAIN, {}).items(): name = conf.get(CONF_NAME, object_id) @@ -384,7 +379,7 @@ def _async_process_config(hass, config, component): # Don't create tasks and await them all. The order is important as # groups get a number based on creation order. - yield from Group.async_create_group( + await Group.async_create_group( hass, name, entity_ids, icon=icon, view=view, control=control, object_id=object_id) @@ -428,10 +423,9 @@ class Group(Entity): hass.loop).result() @staticmethod - @asyncio.coroutine - def async_create_group(hass, name, entity_ids=None, user_defined=True, - visible=True, icon=None, view=False, control=None, - object_id=None): + async def async_create_group(hass, name, entity_ids=None, + user_defined=True, visible=True, icon=None, + view=False, control=None, object_id=None): """Initialize a group. This method must be run in the event loop. @@ -453,7 +447,7 @@ class Group(Entity): component = hass.data[DOMAIN] = \ EntityComponent(_LOGGER, DOMAIN, hass) - yield from component.async_add_entities([group], True) + await component.async_add_entities([group], True) return group @@ -520,17 +514,16 @@ class Group(Entity): self.async_update_tracked_entity_ids(entity_ids), self.hass.loop ).result() - @asyncio.coroutine - def async_update_tracked_entity_ids(self, entity_ids): + async def async_update_tracked_entity_ids(self, entity_ids): """Update the member entity IDs. This method must be run in the event loop. """ - yield from self.async_stop() + await self.async_stop() self.tracking = tuple(ent_id.lower() for ent_id in entity_ids) self.group_on, self.group_off = None, None - yield from self.async_update_ha_state(True) + await self.async_update_ha_state(True) self.async_start() @callback @@ -544,8 +537,7 @@ class Group(Entity): self.hass, self.tracking, self._async_state_changed_listener ) - @asyncio.coroutine - def async_stop(self): + async def async_stop(self): """Unregister the group from Home Assistant. This method must be run in the event loop. @@ -554,27 +546,24 @@ class Group(Entity): self._async_unsub_state_changed() self._async_unsub_state_changed = None - @asyncio.coroutine - def async_update(self): + async def async_update(self): """Query all members and determine current group state.""" self._state = STATE_UNKNOWN self._async_update_group_state() - @asyncio.coroutine - def async_added_to_hass(self): + async def async_added_to_hass(self): """Callback when added to HASS.""" if self.tracking: self.async_start() - @asyncio.coroutine - def async_will_remove_from_hass(self): + async def async_will_remove_from_hass(self): """Callback when removed from HASS.""" if self._async_unsub_state_changed: self._async_unsub_state_changed() self._async_unsub_state_changed = None - @asyncio.coroutine - def _async_state_changed_listener(self, entity_id, old_state, new_state): + async def _async_state_changed_listener(self, entity_id, old_state, + new_state): """Respond to a member state changing. This method must be run in the event loop. @@ -584,7 +573,7 @@ class Group(Entity): return self._async_update_group_state(new_state) - yield from self.async_update_ha_state() + await self.async_update_ha_state() @property def _tracking_states(self): diff --git a/homeassistant/components/history.py b/homeassistant/components/history.py index b5ac37b1451..c27e394ce28 100644 --- a/homeassistant/components/history.py +++ b/homeassistant/components/history.py @@ -4,7 +4,6 @@ Provide pre-made queries on top of the recorder component. For more details about this component, please refer to the documentation at https://home-assistant.io/components/history/ """ -import asyncio from collections import defaultdict from datetime import timedelta from itertools import groupby @@ -259,8 +258,7 @@ def get_state(hass, utc_point_in_time, entity_id, run=None): return states[0] if states else None -@asyncio.coroutine -def async_setup(hass, config): +async def async_setup(hass, config): """Set up the history hooks.""" filters = Filters() conf = config.get(DOMAIN, {}) @@ -275,7 +273,7 @@ def async_setup(hass, config): use_include_order = conf.get(CONF_ORDER) hass.http.register_view(HistoryPeriodView(filters, use_include_order)) - yield from hass.components.frontend.async_register_built_in_panel( + await hass.components.frontend.async_register_built_in_panel( 'history', 'history', 'mdi:poll-box') return True @@ -293,8 +291,7 @@ class HistoryPeriodView(HomeAssistantView): self.filters = filters self.use_include_order = use_include_order - @asyncio.coroutine - def get(self, request, datetime=None): + async def get(self, request, datetime=None): """Return history over a period of time.""" timer_start = time.perf_counter() if datetime: @@ -330,7 +327,7 @@ class HistoryPeriodView(HomeAssistantView): hass = request.app['hass'] - result = yield from hass.async_add_job( + result = await hass.async_add_job( get_significant_states, hass, start_time, end_time, entity_ids, self.filters, include_start_time_state) result = list(result.values()) @@ -353,8 +350,7 @@ class HistoryPeriodView(HomeAssistantView): sorted_result.extend(result) result = sorted_result - response = yield from hass.async_add_job(self.json, result) - return response + return await hass.async_add_job(self.json, result) class Filters(object): diff --git a/homeassistant/components/history_graph.py b/homeassistant/components/history_graph.py index e6977d60c30..fa7d615dce2 100644 --- a/homeassistant/components/history_graph.py +++ b/homeassistant/components/history_graph.py @@ -4,7 +4,6 @@ Support to graphs card in the UI. For more details about this component, please refer to the documentation at https://home-assistant.io/components/history_graph/ """ -import asyncio import logging import voluptuous as vol @@ -39,8 +38,7 @@ CONFIG_SCHEMA = vol.Schema({ }, extra=vol.ALLOW_EXTRA) -@asyncio.coroutine -def async_setup(hass, config): +async def async_setup(hass, config): """Load graph configurations.""" component = EntityComponent( _LOGGER, DOMAIN, hass) @@ -51,7 +49,7 @@ def async_setup(hass, config): graph = HistoryGraphEntity(name, cfg) graphs.append(graph) - yield from component.async_add_entities(graphs) + await component.async_add_entities(graphs) return True diff --git a/homeassistant/components/input_boolean.py b/homeassistant/components/input_boolean.py index 56761b5af4e..9c8435614a2 100644 --- a/homeassistant/components/input_boolean.py +++ b/homeassistant/components/input_boolean.py @@ -65,8 +65,7 @@ def toggle(hass, entity_id): hass.services.call(DOMAIN, SERVICE_TOGGLE, {ATTR_ENTITY_ID: entity_id}) -@asyncio.coroutine -def async_setup(hass, config): +async def async_setup(hass, config): """Set up an input boolean.""" component = EntityComponent(_LOGGER, DOMAIN, hass) @@ -85,8 +84,7 @@ def async_setup(hass, config): if not entities: return False - @asyncio.coroutine - def async_handler_service(service): + async def async_handler_service(service): """Handle a calls to the input boolean services.""" target_inputs = component.async_extract_from_service(service) @@ -99,7 +97,7 @@ def async_setup(hass, config): tasks = [getattr(input_b, attr)() for input_b in target_inputs] if tasks: - yield from asyncio.wait(tasks, loop=hass.loop) + await asyncio.wait(tasks, loop=hass.loop) hass.services.async_register( DOMAIN, SERVICE_TURN_OFF, async_handler_service, @@ -111,7 +109,7 @@ def async_setup(hass, config): DOMAIN, SERVICE_TOGGLE, async_handler_service, schema=SERVICE_SCHEMA) - yield from component.async_add_entities(entities) + await component.async_add_entities(entities) return True @@ -145,24 +143,21 @@ class InputBoolean(ToggleEntity): """Return true if entity is on.""" return self._state - @asyncio.coroutine - def async_added_to_hass(self): + async def async_added_to_hass(self): """Call when entity about to be added to hass.""" # If not None, we got an initial value. if self._state is not None: return - state = yield from async_get_last_state(self.hass, self.entity_id) + state = await async_get_last_state(self.hass, self.entity_id) self._state = state and state.state == STATE_ON - @asyncio.coroutine - def async_turn_on(self, **kwargs): + async def async_turn_on(self, **kwargs): """Turn the entity on.""" self._state = True - yield from self.async_update_ha_state() + await self.async_update_ha_state() - @asyncio.coroutine - def async_turn_off(self, **kwargs): + async def async_turn_off(self, **kwargs): """Turn the entity off.""" self._state = False - yield from self.async_update_ha_state() + await self.async_update_ha_state() diff --git a/homeassistant/components/light/mqtt_json.py b/homeassistant/components/light/mqtt_json.py index a0bfc5a0787..ca5c76e905f 100644 --- a/homeassistant/components/light/mqtt_json.py +++ b/homeassistant/components/light/mqtt_json.py @@ -4,7 +4,6 @@ Support for MQTT JSON lights. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/light.mqtt_json/ """ -import asyncio import logging import json import voluptuous as vol @@ -26,6 +25,7 @@ from homeassistant.components.mqtt import ( CONF_PAYLOAD_AVAILABLE, CONF_PAYLOAD_NOT_AVAILABLE, CONF_QOS, CONF_RETAIN, MqttAvailability) import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.typing import HomeAssistantType, ConfigType import homeassistant.util.color as color_util _LOGGER = logging.getLogger(__name__) @@ -79,8 +79,8 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ }).extend(mqtt.MQTT_AVAILABILITY_SCHEMA.schema) -@asyncio.coroutine -def async_setup_platform(hass, config, async_add_devices, discovery_info=None): +async def async_setup_platform(hass: HomeAssistantType, config: ConfigType, + async_add_devices, discovery_info=None): """Set up a MQTT JSON Light.""" if discovery_info is not None: config = PLATFORM_SCHEMA(discovery_info) @@ -173,10 +173,9 @@ class MqttJson(MqttAvailability, Light): self._supported_features |= (xy and SUPPORT_COLOR) self._supported_features |= (hs and SUPPORT_COLOR) - @asyncio.coroutine - def async_added_to_hass(self): + async def async_added_to_hass(self): """Subscribe to MQTT events.""" - yield from super().async_added_to_hass() + await super().async_added_to_hass() @callback def state_received(topic, payload, qos): @@ -257,7 +256,7 @@ class MqttJson(MqttAvailability, Light): self.async_schedule_update_ha_state() if self._topic[CONF_STATE_TOPIC] is not None: - yield from mqtt.async_subscribe( + await mqtt.async_subscribe( self.hass, self._topic[CONF_STATE_TOPIC], state_received, self._qos) @@ -316,8 +315,7 @@ class MqttJson(MqttAvailability, Light): """Flag supported features.""" return self._supported_features - @asyncio.coroutine - def async_turn_on(self, **kwargs): + async def async_turn_on(self, **kwargs): """Turn the device on. This method is a coroutine. @@ -404,8 +402,7 @@ class MqttJson(MqttAvailability, Light): if should_update: self.async_schedule_update_ha_state() - @asyncio.coroutine - def async_turn_off(self, **kwargs): + async def async_turn_off(self, **kwargs): """Turn the device off. This method is a coroutine. diff --git a/homeassistant/components/logbook.py b/homeassistant/components/logbook.py index 1c3e8ed1f19..8bab6fe0440 100644 --- a/homeassistant/components/logbook.py +++ b/homeassistant/components/logbook.py @@ -4,7 +4,6 @@ Event parser and human readable log generator. For more details about this component, please refer to the documentation at https://home-assistant.io/components/logbook/ """ -import asyncio import logging from datetime import timedelta from itertools import groupby @@ -88,8 +87,7 @@ def async_log_entry(hass, name, message, domain=None, entity_id=None): hass.bus.async_fire(EVENT_LOGBOOK_ENTRY, data) -@asyncio.coroutine -def setup(hass, config): +async def setup(hass, config): """Listen for download events to download files.""" @callback def log_message(service): @@ -105,7 +103,7 @@ def setup(hass, config): hass.http.register_view(LogbookView(config.get(DOMAIN, {}))) - yield from hass.components.frontend.async_register_built_in_panel( + await hass.components.frontend.async_register_built_in_panel( 'logbook', 'logbook', 'mdi:format-list-bulleted-type') hass.services.async_register( @@ -124,8 +122,7 @@ class LogbookView(HomeAssistantView): """Initialize the logbook view.""" self.config = config - @asyncio.coroutine - def get(self, request, datetime=None): + async def get(self, request, datetime=None): """Retrieve logbook entries.""" if datetime: datetime = dt_util.parse_datetime(datetime) @@ -144,8 +141,7 @@ class LogbookView(HomeAssistantView): return self.json(list( _get_events(hass, self.config, start_day, end_day))) - response = yield from hass.async_add_job(json_events) - return response + return await hass.async_add_job(json_events) class Entry(object): diff --git a/homeassistant/components/logger.py b/homeassistant/components/logger.py index c2309401977..6e8995a0444 100644 --- a/homeassistant/components/logger.py +++ b/homeassistant/components/logger.py @@ -4,7 +4,6 @@ Component that will help set the level of logging for components. For more details about this component, please refer to the documentation at https://home-assistant.io/components/logger/ """ -import asyncio import logging from collections import OrderedDict @@ -73,8 +72,7 @@ class HomeAssistantLogFilter(logging.Filter): return record.levelno >= default -@asyncio.coroutine -def async_setup(hass, config): +async def async_setup(hass, config): """Set up the logger component.""" logfilter = {} @@ -116,8 +114,7 @@ def async_setup(hass, config): if LOGGER_LOGS in config.get(DOMAIN): set_log_levels(config.get(DOMAIN)[LOGGER_LOGS]) - @asyncio.coroutine - def async_service_handler(service): + async def async_service_handler(service): """Handle logger services.""" set_log_levels(service.data) diff --git a/homeassistant/components/map.py b/homeassistant/components/map.py index b8293f64fc0..30cb00af69e 100644 --- a/homeassistant/components/map.py +++ b/homeassistant/components/map.py @@ -4,14 +4,11 @@ Provides a map panel for showing device locations. For more details about this component, please refer to the documentation at https://home-assistant.io/components/map/ """ -import asyncio - DOMAIN = 'map' -@asyncio.coroutine -def async_setup(hass, config): +async def async_setup(hass, config): """Register the built-in map panel.""" - yield from hass.components.frontend.async_register_built_in_panel( + await hass.components.frontend.async_register_built_in_panel( 'map', 'map', 'mdi:account-location') return True diff --git a/homeassistant/components/media_player/__init__.py b/homeassistant/components/media_player/__init__.py index 615c758cd1a..20fd3b875c8 100644 --- a/homeassistant/components/media_player/__init__.py +++ b/homeassistant/components/media_player/__init__.py @@ -361,18 +361,16 @@ def set_shuffle(hass, shuffle, entity_id=None): hass.services.call(DOMAIN, SERVICE_SHUFFLE_SET, data) -@asyncio.coroutine -def async_setup(hass, config): +async def async_setup(hass, config): """Track states and offer events for media_players.""" component = EntityComponent( logging.getLogger(__name__), DOMAIN, hass, SCAN_INTERVAL) hass.http.register_view(MediaPlayerImageView(component)) - yield from component.async_setup(config) + await component.async_setup(config) - @asyncio.coroutine - def async_service_handler(service): + async def async_service_handler(service): """Map services to methods on MediaPlayerDevice.""" method = SERVICE_TO_METHOD.get(service.service) if not method: @@ -400,13 +398,13 @@ def async_setup(hass, config): update_tasks = [] for player in target_players: - yield from getattr(player, method['method'])(**params) + await getattr(player, method['method'])(**params) if not player.should_poll: continue update_tasks.append(player.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) for service in SERVICE_TO_METHOD: schema = SERVICE_TO_METHOD[service].get( @@ -490,14 +488,13 @@ class MediaPlayerDevice(Entity): return None - @asyncio.coroutine - def async_get_media_image(self): + async def async_get_media_image(self): """Fetch media image of current playing image.""" url = self.media_image_url if url is None: return None, None - return (yield from _async_fetch_image(self.hass, url)) + return await _async_fetch_image(self.hass, url) @property def media_title(self): @@ -808,34 +805,31 @@ class MediaPlayerDevice(Entity): return self.async_turn_on() return self.async_turn_off() - @asyncio.coroutine - def async_volume_up(self): + async def async_volume_up(self): """Turn volume up for media player. This method is a coroutine. """ if hasattr(self, 'volume_up'): # pylint: disable=no-member - yield from self.hass.async_add_job(self.volume_up) + await self.hass.async_add_job(self.volume_up) return if self.volume_level < 1: - yield from self.async_set_volume_level( - min(1, self.volume_level + .1)) + await self.async_set_volume_level(min(1, self.volume_level + .1)) - @asyncio.coroutine - def async_volume_down(self): + async def async_volume_down(self): """Turn volume down for media player. This method is a coroutine. """ if hasattr(self, 'volume_down'): # pylint: disable=no-member - yield from self.hass.async_add_job(self.volume_down) + await self.hass.async_add_job(self.volume_down) return if self.volume_level > 0: - yield from self.async_set_volume_level( + await self.async_set_volume_level( max(0, self.volume_level - .1)) def async_media_play_pause(self): @@ -879,8 +873,7 @@ class MediaPlayerDevice(Entity): return state_attr -@asyncio.coroutine -def _async_fetch_image(hass, url): +async def _async_fetch_image(hass, url): """Fetch image. Images are cached in memory (the images are typically 10-100kB in size). @@ -891,7 +884,7 @@ def _async_fetch_image(hass, url): if url not in cache_images: cache_images[url] = {CACHE_LOCK: asyncio.Lock(loop=hass.loop)} - with (yield from cache_images[url][CACHE_LOCK]): + async with cache_images[url][CACHE_LOCK]: if CACHE_CONTENT in cache_images[url]: return cache_images[url][CACHE_CONTENT] @@ -899,10 +892,10 @@ def _async_fetch_image(hass, url): websession = async_get_clientsession(hass) try: with async_timeout.timeout(10, loop=hass.loop): - response = yield from websession.get(url) + response = await websession.get(url) if response.status == 200: - content = yield from response.read() + content = await response.read() content_type = response.headers.get(CONTENT_TYPE) if content_type: content_type = content_type.split(';')[0] @@ -928,8 +921,7 @@ class MediaPlayerImageView(HomeAssistantView): """Initialize a media player view.""" self.component = component - @asyncio.coroutine - def get(self, request, entity_id): + async def get(self, request, entity_id): """Start a get request.""" player = self.component.get_entity(entity_id) if player is None: @@ -942,7 +934,7 @@ class MediaPlayerImageView(HomeAssistantView): if not authenticated: return web.Response(status=401) - data, content_type = yield from player.async_get_media_image() + data, content_type = await player.async_get_media_image() if data is None: return web.Response(status=500) diff --git a/homeassistant/components/media_player/universal.py b/homeassistant/components/media_player/universal.py index 27a0714527d..fa4f03f1179 100644 --- a/homeassistant/components/media_player/universal.py +++ b/homeassistant/components/media_player/universal.py @@ -4,7 +4,6 @@ Combination of multiple media players into one for a universal controller. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/media_player.universal/ """ -import asyncio import logging # pylint: disable=import-error from copy import copy @@ -63,8 +62,8 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ }, extra=vol.REMOVE_EXTRA) -@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 universal media players.""" player = UniversalMediaPlayer( hass, @@ -99,8 +98,7 @@ class UniversalMediaPlayer(MediaPlayerDevice): if state_template is not None: self._state_template.hass = hass - @asyncio.coroutine - def async_added_to_hass(self): + async def async_added_to_hass(self): """Subscribe to children and template state changes. This method must be run in the event loop and returns a coroutine. @@ -144,15 +142,14 @@ class UniversalMediaPlayer(MediaPlayerDevice): active_child = self._child_state return active_child.attributes.get(attr_name) if active_child else None - @asyncio.coroutine - def _async_call_service(self, service_name, service_data=None, - allow_override=False): + async def _async_call_service(self, service_name, service_data=None, + allow_override=False): """Call either a specified or active child's service.""" if service_data is None: service_data = {} if allow_override and service_name in self._cmds: - yield from async_call_from_config( + await async_call_from_config( self.hass, self._cmds[service_name], variables=service_data, blocking=True, validate_config=False) @@ -165,7 +162,7 @@ class UniversalMediaPlayer(MediaPlayerDevice): service_data[ATTR_ENTITY_ID] = active_child.entity_id - yield from self.hass.services.async_call( + await self.hass.services.async_call( DOMAIN, service_name, service_data, blocking=True) @property @@ -506,8 +503,7 @@ class UniversalMediaPlayer(MediaPlayerDevice): return self._async_call_service( SERVICE_SHUFFLE_SET, data, allow_override=True) - @asyncio.coroutine - def async_update(self): + async def async_update(self): """Update state in HA.""" for child_name in self._children: child_state = self.hass.states.get(child_name) diff --git a/homeassistant/components/recorder/__init__.py b/homeassistant/components/recorder/__init__.py index 8e69c2cfcd8..9b5bea043f4 100644 --- a/homeassistant/components/recorder/__init__.py +++ b/homeassistant/components/recorder/__init__.py @@ -111,8 +111,7 @@ def run_information(hass, point_in_time: Optional[datetime] = None): return res -@asyncio.coroutine -def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: +async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up the recorder.""" conf = config.get(DOMAIN, {}) keep_days = conf.get(CONF_PURGE_KEEP_DAYS) @@ -131,8 +130,7 @@ def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: instance.async_initialize() instance.start() - @asyncio.coroutine - def async_handle_purge_service(service): + async def async_handle_purge_service(service): """Handle calls to the purge service.""" instance.do_adhoc_purge(**service.data) @@ -140,7 +138,7 @@ def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: DOMAIN, SERVICE_PURGE, async_handle_purge_service, schema=SERVICE_PURGE_SCHEMA) - return (yield from instance.async_db_ready) + return await instance.async_db_ready PurgeTask = namedtuple('PurgeTask', ['keep_days', 'repack']) diff --git a/homeassistant/components/sensor/mqtt.py b/homeassistant/components/sensor/mqtt.py index c4f64e9e015..d7d66a3a145 100644 --- a/homeassistant/components/sensor/mqtt.py +++ b/homeassistant/components/sensor/mqtt.py @@ -4,7 +4,6 @@ Support for MQTT sensors. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/sensor.mqtt/ """ -import asyncio import logging import json from datetime import timedelta @@ -22,6 +21,7 @@ from homeassistant.const import ( from homeassistant.helpers.entity import Entity import homeassistant.components.mqtt as mqtt import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.typing import HomeAssistantType, ConfigType from homeassistant.helpers.event import async_track_point_in_utc_time from homeassistant.util import dt as dt_util @@ -48,8 +48,8 @@ PLATFORM_SCHEMA = mqtt.MQTT_RO_PLATFORM_SCHEMA.extend({ }).extend(mqtt.MQTT_AVAILABILITY_SCHEMA.schema) -@asyncio.coroutine -def async_setup_platform(hass, config, async_add_devices, discovery_info=None): +async def async_setup_platform(hass: HomeAssistantType, config: ConfigType, + async_add_devices, discovery_info=None): """Set up MQTT Sensor.""" if discovery_info is not None: config = PLATFORM_SCHEMA(discovery_info) @@ -100,10 +100,9 @@ class MqttSensor(MqttAvailability, Entity): self._unique_id = unique_id self._attributes = None - @asyncio.coroutine - def async_added_to_hass(self): + async def async_added_to_hass(self): """Subscribe to MQTT events.""" - yield from super().async_added_to_hass() + await super().async_added_to_hass() @callback def message_received(topic, payload, qos): @@ -142,8 +141,8 @@ class MqttSensor(MqttAvailability, Entity): self._state = payload self.async_schedule_update_ha_state() - yield from mqtt.async_subscribe( - self.hass, self._state_topic, message_received, self._qos) + await mqtt.async_subscribe(self.hass, self._state_topic, + message_received, self._qos) @callback def value_is_expired(self, *_): diff --git a/homeassistant/components/sensor/wunderground.py b/homeassistant/components/sensor/wunderground.py index 7938b17e4d6..bbee167d4b0 100644 --- a/homeassistant/components/sensor/wunderground.py +++ b/homeassistant/components/sensor/wunderground.py @@ -639,9 +639,8 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ ADDED_ENTITY_IDS_KEY = 'wunderground_added_entity_ids' -@asyncio.coroutine -def async_setup_platform(hass: HomeAssistantType, config: ConfigType, - async_add_devices, discovery_info=None): +async def async_setup_platform(hass: HomeAssistantType, config: ConfigType, + async_add_devices, discovery_info=None): """Set up the WUnderground sensor.""" hass.data.setdefault(ADDED_ENTITY_IDS_KEY, set()) @@ -656,7 +655,7 @@ def async_setup_platform(hass: HomeAssistantType, config: ConfigType, for variable in config[CONF_MONITORED_CONDITIONS]: sensors.append(WUndergroundSensor(hass, rest, variable, namespace)) - yield from rest.async_update() + await rest.async_update() if not rest.data: raise PlatformNotReady diff --git a/homeassistant/components/switch/mqtt.py b/homeassistant/components/switch/mqtt.py index f3bd0bef012..15dc6f1d0f4 100644 --- a/homeassistant/components/switch/mqtt.py +++ b/homeassistant/components/switch/mqtt.py @@ -4,7 +4,6 @@ Support for MQTT switches. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/switch.mqtt/ """ -import asyncio import logging import voluptuous as vol @@ -39,8 +38,8 @@ PLATFORM_SCHEMA = mqtt.MQTT_RW_PLATFORM_SCHEMA.extend({ }).extend(mqtt.MQTT_AVAILABILITY_SCHEMA.schema) -@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 MQTT switch.""" if discovery_info is not None: config = PLATFORM_SCHEMA(discovery_info) @@ -88,10 +87,9 @@ class MqttSwitch(MqttAvailability, SwitchDevice): self._optimistic = optimistic self._template = value_template - @asyncio.coroutine - def async_added_to_hass(self): + async def async_added_to_hass(self): """Subscribe to MQTT events.""" - yield from super().async_added_to_hass() + await super().async_added_to_hass() @callback def state_message_received(topic, payload, qos): @@ -110,7 +108,7 @@ class MqttSwitch(MqttAvailability, SwitchDevice): # Force into optimistic mode. self._optimistic = True else: - yield from mqtt.async_subscribe( + await mqtt.async_subscribe( self.hass, self._state_topic, state_message_received, self._qos) @@ -139,8 +137,7 @@ class MqttSwitch(MqttAvailability, SwitchDevice): """Return the icon.""" return self._icon - @asyncio.coroutine - def async_turn_on(self, **kwargs): + async def async_turn_on(self, **kwargs): """Turn the device on. This method is a coroutine. @@ -153,8 +150,7 @@ class MqttSwitch(MqttAvailability, SwitchDevice): self._state = True self.async_schedule_update_ha_state() - @asyncio.coroutine - def async_turn_off(self, **kwargs): + async def async_turn_off(self, **kwargs): """Turn the device off. This method is a coroutine. diff --git a/homeassistant/components/timer/__init__.py b/homeassistant/components/timer/__init__.py index 84d2d3f349d..5a363e84d7b 100644 --- a/homeassistant/components/timer/__init__.py +++ b/homeassistant/components/timer/__init__.py @@ -122,8 +122,7 @@ def async_finish(hass, entity_id): DOMAIN, SERVICE_FINISH, {ATTR_ENTITY_ID: entity_id})) -@asyncio.coroutine -def async_setup(hass, config): +async def async_setup(hass, config): """Set up a timer.""" component = EntityComponent(_LOGGER, DOMAIN, hass) @@ -142,8 +141,7 @@ def async_setup(hass, config): if not entities: return False - @asyncio.coroutine - def async_handler_service(service): + async def async_handler_service(service): """Handle a call to the timer services.""" target_timers = component.async_extract_from_service(service) @@ -162,7 +160,7 @@ def async_setup(hass, config): timer.async_start(service.data.get(ATTR_DURATION)) ) if tasks: - yield from asyncio.wait(tasks, loop=hass.loop) + await asyncio.wait(tasks, loop=hass.loop) hass.services.async_register( DOMAIN, SERVICE_START, async_handler_service, @@ -177,7 +175,7 @@ def async_setup(hass, config): DOMAIN, SERVICE_FINISH, async_handler_service, schema=SERVICE_SCHEMA) - yield from component.async_add_entities(entities) + await component.async_add_entities(entities) return True @@ -224,19 +222,17 @@ class Timer(Entity): ATTR_REMAINING: str(self._remaining) } - @asyncio.coroutine - def async_added_to_hass(self): + async def async_added_to_hass(self): """Call when entity is about to be added to Home Assistant.""" # If not None, we got an initial value. if self._state is not None: return restore_state = self._hass.helpers.restore_state - state = yield from restore_state.async_get_last_state(self.entity_id) + state = await restore_state.async_get_last_state(self.entity_id) self._state = state and state.state == state - @asyncio.coroutine - def async_start(self, duration): + async def async_start(self, duration): """Start a timer.""" if self._listener: self._listener() @@ -260,10 +256,9 @@ class Timer(Entity): self._listener = async_track_point_in_utc_time(self._hass, self.async_finished, self._end) - yield from self.async_update_ha_state() + await self.async_update_ha_state() - @asyncio.coroutine - def async_pause(self): + async def async_pause(self): """Pause a timer.""" if self._listener is None: return @@ -273,10 +268,9 @@ class Timer(Entity): self._remaining = self._end - dt_util.utcnow() self._state = STATUS_PAUSED self._end = None - yield from self.async_update_ha_state() + await self.async_update_ha_state() - @asyncio.coroutine - def async_cancel(self): + async def async_cancel(self): """Cancel a timer.""" if self._listener: self._listener() @@ -286,10 +280,9 @@ class Timer(Entity): self._remaining = timedelta() self._hass.bus.async_fire(EVENT_TIMER_CANCELLED, {"entity_id": self.entity_id}) - yield from self.async_update_ha_state() + await self.async_update_ha_state() - @asyncio.coroutine - def async_finish(self): + async def async_finish(self): """Reset and updates the states, fire finished event.""" if self._state != STATUS_ACTIVE: return @@ -299,10 +292,9 @@ class Timer(Entity): self._remaining = timedelta() self._hass.bus.async_fire(EVENT_TIMER_FINISHED, {"entity_id": self.entity_id}) - yield from self.async_update_ha_state() + await self.async_update_ha_state() - @asyncio.coroutine - def async_finished(self, time): + async def async_finished(self, time): """Reset and updates the states, fire finished event.""" if self._state != STATUS_ACTIVE: return @@ -312,4 +304,4 @@ class Timer(Entity): self._remaining = timedelta() self._hass.bus.async_fire(EVENT_TIMER_FINISHED, {"entity_id": self.entity_id}) - yield from self.async_update_ha_state() + await self.async_update_ha_state() diff --git a/homeassistant/components/tts/google.py b/homeassistant/components/tts/google.py index 084a7229212..bf03ec1adad 100644 --- a/homeassistant/components/tts/google.py +++ b/homeassistant/components/tts/google.py @@ -39,8 +39,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ }) -@asyncio.coroutine -def async_get_engine(hass, config): +async def async_get_engine(hass, config): """Set up Google speech component.""" return GoogleProvider(hass, config[CONF_LANG]) @@ -70,8 +69,7 @@ class GoogleProvider(Provider): """Return list of supported languages.""" return SUPPORT_LANGUAGES - @asyncio.coroutine - def async_get_tts_audio(self, message, language, options=None): + async def async_get_tts_audio(self, message, language, options=None): """Load TTS from google.""" from gtts_token import gtts_token @@ -81,7 +79,7 @@ class GoogleProvider(Provider): data = b'' for idx, part in enumerate(message_parts): - part_token = yield from self.hass.async_add_job( + part_token = await self.hass.async_add_job( token.calculate_token, part) url_param = { @@ -97,7 +95,7 @@ class GoogleProvider(Provider): try: with async_timeout.timeout(10, loop=self.hass.loop): - request = yield from websession.get( + request = await websession.get( GOOGLE_SPEECH_URL, params=url_param, headers=self.headers ) @@ -106,7 +104,7 @@ class GoogleProvider(Provider): _LOGGER.error("Error %d on load url %s", request.status, request.url) return (None, None) - data += yield from request.read() + data += await request.read() except (asyncio.TimeoutError, aiohttp.ClientError): _LOGGER.error("Timeout for google speech.") diff --git a/homeassistant/components/updater.py b/homeassistant/components/updater.py index f7bf9774e42..9ccf280ed04 100644 --- a/homeassistant/components/updater.py +++ b/homeassistant/components/updater.py @@ -72,8 +72,7 @@ def _load_uuid(hass, filename=UPDATER_UUID_FILE): return _create_uuid(hass, filename) -@asyncio.coroutine -def async_setup(hass, config): +async def async_setup(hass, config): """Set up the updater component.""" if 'dev' in current_version: # This component only makes sense in release versions @@ -81,16 +80,15 @@ def async_setup(hass, config): config = config.get(DOMAIN, {}) if config.get(CONF_REPORTING): - huuid = yield from hass.async_add_job(_load_uuid, hass) + huuid = await hass.async_add_job(_load_uuid, hass) else: huuid = None include_components = config.get(CONF_COMPONENT_REPORTING) - @asyncio.coroutine - def check_new_version(now): + async def check_new_version(now): """Check if a new version is available and report if one is.""" - result = yield from get_newest_version(hass, huuid, include_components) + result = await get_newest_version(hass, huuid, include_components) if result is None: return @@ -125,8 +123,7 @@ def async_setup(hass, config): return True -@asyncio.coroutine -def get_system_info(hass, include_components): +async def get_system_info(hass, include_components): """Return info about the system.""" info_object = { 'arch': platform.machine(), @@ -151,7 +148,7 @@ def get_system_info(hass, include_components): info_object['os_version'] = platform.release() elif platform.system() == 'Linux': import distro - linux_dist = yield from hass.async_add_job( + linux_dist = await hass.async_add_job( distro.linux_distribution, False) info_object['distribution'] = linux_dist[0] info_object['os_version'] = linux_dist[1] @@ -160,11 +157,10 @@ def get_system_info(hass, include_components): return info_object -@asyncio.coroutine -def get_newest_version(hass, huuid, include_components): +async def get_newest_version(hass, huuid, include_components): """Get the newest Home Assistant version.""" if huuid: - info_object = yield from get_system_info(hass, include_components) + info_object = await get_system_info(hass, include_components) info_object['huuid'] = huuid else: info_object = {} @@ -172,7 +168,7 @@ def get_newest_version(hass, huuid, include_components): session = async_get_clientsession(hass) try: with async_timeout.timeout(5, loop=hass.loop): - req = yield from session.post(UPDATER_URL, json=info_object) + req = await session.post(UPDATER_URL, json=info_object) _LOGGER.info(("Submitted analytics to Home Assistant servers. " "Information submitted includes %s"), info_object) except (asyncio.TimeoutError, aiohttp.ClientError): @@ -181,7 +177,7 @@ def get_newest_version(hass, huuid, include_components): return None try: - res = yield from req.json() + res = await req.json() except ValueError: _LOGGER.error("Received invalid JSON from Home Assistant Update") return None diff --git a/homeassistant/util/logging.py b/homeassistant/util/logging.py index f7306cae98b..10b43445184 100644 --- a/homeassistant/util/logging.py +++ b/homeassistant/util/logging.py @@ -49,17 +49,16 @@ class AsyncHandler(object): """Wrap close to handler.""" self.emit(None) - @asyncio.coroutine - def async_close(self, blocking=False): + async def async_close(self, blocking=False): """Close the handler. When blocking=True, will wait till closed. """ - yield from self._queue.put(None) + await self._queue.put(None) if blocking: while self._thread.is_alive(): - yield from asyncio.sleep(0, loop=self.loop) + await asyncio.sleep(0, loop=self.loop) def emit(self, record): """Process a record.""" diff --git a/tests/test_bootstrap.py b/tests/test_bootstrap.py index c109ae30aad..3e4d4739779 100644 --- a/tests/test_bootstrap.py +++ b/tests/test_bootstrap.py @@ -40,9 +40,9 @@ def test_from_config_file(hass): assert components == hass.config.components -@asyncio.coroutine @patch('homeassistant.bootstrap.async_enable_logging', Mock()) @patch('homeassistant.bootstrap.async_register_signal_handling', Mock()) +@asyncio.coroutine def test_home_assistant_core_config_validation(hass): """Test if we pass in wrong information for HA conf.""" # Extensive HA conf validation testing is done From fd038b6de9299c063cd707952bd1a775c3d2ca44 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sat, 28 Apr 2018 20:15:00 -0400 Subject: [PATCH 057/155] Disable eliqonline requirement (#14156) * Disable eliqonline requirement * Disable pylint import error --- homeassistant/components/sensor/eliqonline.py | 3 ++- requirements_all.txt | 3 --- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/sensor/eliqonline.py b/homeassistant/components/sensor/eliqonline.py index 3e736ed719f..23c397053c5 100644 --- a/homeassistant/components/sensor/eliqonline.py +++ b/homeassistant/components/sensor/eliqonline.py @@ -14,7 +14,8 @@ from homeassistant.const import (CONF_ACCESS_TOKEN, CONF_NAME, STATE_UNKNOWN) from homeassistant.helpers.entity import Entity import homeassistant.helpers.config_validation as cv -REQUIREMENTS = ['eliqonline==1.0.13'] +# pylint: disable=import-error, no-member +REQUIREMENTS = [] # ['eliqonline==1.0.13'] - package disappeared _LOGGER = logging.getLogger(__name__) diff --git a/requirements_all.txt b/requirements_all.txt index f0be99705c1..a3519f29283 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -276,9 +276,6 @@ dsmr_parser==0.11 # homeassistant.components.sensor.dweet dweepy==0.3.0 -# homeassistant.components.sensor.eliqonline -eliqonline==1.0.13 - # homeassistant.components.enocean enocean==0.40 From ef48a7ca2c2ce938cd5bf8669cdf02b3535d67d7 Mon Sep 17 00:00:00 2001 From: Matthew Garrett Date: Sun, 29 Apr 2018 00:46:36 -0700 Subject: [PATCH 058/155] Fix Python 3.6 compatibility for HomeKit controller (#14160) Python 3.6's http client passes an additional argument to _send_output, so add that to the function definition. --- homeassistant/components/homekit_controller/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/homekit_controller/__init__.py b/homeassistant/components/homekit_controller/__init__.py index 164e7d50e4d..e36e7439e09 100644 --- a/homeassistant/components/homekit_controller/__init__.py +++ b/homeassistant/components/homekit_controller/__init__.py @@ -31,7 +31,7 @@ KNOWN_DEVICES = "{}-devices".format(DOMAIN) _LOGGER = logging.getLogger(__name__) -def homekit_http_send(self, message_body=None): +def homekit_http_send(self, message_body=None, encode_chunked=False): r"""Send the currently buffered request and clear the buffer. Appends an extra \r\n to the buffer. From 3fd4987baf3a11b7caba7f97227ac9f96dd882a9 Mon Sep 17 00:00:00 2001 From: Kane610 Date: Sun, 29 Apr 2018 16:16:20 +0200 Subject: [PATCH 059/155] deCONZ allow unloading of config entry (#14115) * Working but incomplete * Remove events on unload * Add unload test * Fix failing sensor test * Improve unload test * Move DeconzEvent to init * Fix visual under-indentation --- .../components/binary_sensor/__init__.py | 5 ++ homeassistant/components/deconz/__init__.py | 55 +++++++++++++++++-- homeassistant/components/deconz/const.py | 1 + homeassistant/components/scene/__init__.py | 5 ++ homeassistant/components/sensor/__init__.py | 5 ++ homeassistant/components/sensor/deconz.py | 29 +--------- tests/components/deconz/test_init.py | 16 ++++++ tests/components/sensor/test_deconz.py | 1 + 8 files changed, 86 insertions(+), 31 deletions(-) diff --git a/homeassistant/components/binary_sensor/__init__.py b/homeassistant/components/binary_sensor/__init__.py index ee2a0ce712d..d72211d5ad1 100644 --- a/homeassistant/components/binary_sensor/__init__.py +++ b/homeassistant/components/binary_sensor/__init__.py @@ -62,6 +62,11 @@ async def async_setup_entry(hass, 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) + + # pylint: disable=no-self-use class BinarySensorDevice(Entity): """Represent a binary sensor.""" diff --git a/homeassistant/components/deconz/__init__.py b/homeassistant/components/deconz/__init__.py index d68edac9e59..75414598693 100644 --- a/homeassistant/components/deconz/__init__.py +++ b/homeassistant/components/deconz/__init__.py @@ -7,14 +7,17 @@ https://home-assistant.io/components/deconz/ import voluptuous as vol from homeassistant.const import ( - CONF_API_KEY, CONF_HOST, CONF_PORT, EVENT_HOMEASSISTANT_STOP) -from homeassistant.core import callback + CONF_API_KEY, CONF_EVENT, CONF_HOST, + CONF_ID, CONF_PORT, EVENT_HOMEASSISTANT_STOP) +from homeassistant.core import EventOrigin, callback from homeassistant.helpers import aiohttp_client, config_validation as cv +from homeassistant.util import slugify from homeassistant.util.json import load_json # Loading the config flow file will register the flow from .config_flow import configured_hosts -from .const import CONFIG_FILE, DATA_DECONZ_ID, DOMAIN, _LOGGER +from .const import ( + CONFIG_FILE, DATA_DECONZ_EVENT, DATA_DECONZ_ID, DOMAIN, _LOGGER) REQUIREMENTS = ['pydeconz==36'] @@ -26,6 +29,8 @@ CONFIG_SCHEMA = vol.Schema({ }) }, extra=vol.ALLOW_EXTRA) +SERVICE_DECONZ = 'configure' + SERVICE_FIELD = 'field' SERVICE_ENTITY = 'entity' SERVICE_DATA = 'data' @@ -64,6 +69,7 @@ async def async_setup_entry(hass, config_entry): Start websocket for push notification of state changes from deCONZ. """ from pydeconz import DeconzSession + from pydeconz.sensor import SWITCH as DECONZ_REMOTE if DOMAIN in hass.data: _LOGGER.error( "Config entry failed since one deCONZ instance already exists") @@ -82,6 +88,11 @@ async def async_setup_entry(hass, config_entry): for component in ['binary_sensor', 'light', 'scene', 'sensor']: hass.async_add_job(hass.config_entries.async_forward_entry_setup( config_entry, component)) + + hass.data[DATA_DECONZ_EVENT] = [DeconzEvent( + hass, sensor) for sensor in deconz.sensors.values() + if sensor.type in DECONZ_REMOTE] + deconz.start() async def async_configure(call): @@ -112,7 +123,7 @@ async def async_setup_entry(hass, config_entry): return await deconz.async_put_state(field, data) hass.services.async_register( - DOMAIN, 'configure', async_configure, schema=SERVICE_SCHEMA) + DOMAIN, SERVICE_DECONZ, async_configure, schema=SERVICE_SCHEMA) @callback def deconz_shutdown(event): @@ -127,3 +138,39 @@ async def async_setup_entry(hass, config_entry): hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, deconz_shutdown) return True + + +async def async_unload_entry(hass, config_entry): + """Unload deCONZ config entry.""" + deconz = hass.data.pop(DOMAIN) + hass.services.async_remove(DOMAIN, SERVICE_DECONZ) + deconz.close() + for component in ['binary_sensor', 'light', 'scene', 'sensor']: + await hass.config_entries.async_forward_entry_unload( + config_entry, component) + hass.data[DATA_DECONZ_EVENT] = [] + hass.data[DATA_DECONZ_ID] = [] + return True + + +class DeconzEvent(object): + """When you want signals instead of entities. + + Stateless sensors such as remotes are expected to generate an event + instead of a sensor entity in hass. + """ + + def __init__(self, hass, device): + """Register callback that will be used for signals.""" + self._hass = hass + self._device = device + self._device.register_async_callback(self.async_update_callback) + self._event = 'deconz_{}'.format(CONF_EVENT) + self._id = slugify(self._device.name) + + @callback + def async_update_callback(self, reason): + """Fire the event if reason is that state is updated.""" + if reason['state']: + data = {CONF_ID: self._id, CONF_EVENT: self._device.state} + self._hass.bus.async_fire(self._event, data, EventOrigin.remote) diff --git a/homeassistant/components/deconz/const.py b/homeassistant/components/deconz/const.py index c5820c971f6..e6d393c8ee7 100644 --- a/homeassistant/components/deconz/const.py +++ b/homeassistant/components/deconz/const.py @@ -5,4 +5,5 @@ _LOGGER = logging.getLogger('homeassistant.components.deconz') DOMAIN = 'deconz' CONFIG_FILE = 'deconz.conf' +DATA_DECONZ_EVENT = 'deconz_events' DATA_DECONZ_ID = 'deconz_entities' diff --git a/homeassistant/components/scene/__init__.py b/homeassistant/components/scene/__init__.py index a3e3a5b38a7..2394d538f2f 100644 --- a/homeassistant/components/scene/__init__.py +++ b/homeassistant/components/scene/__init__.py @@ -95,6 +95,11 @@ async def async_setup_entry(hass, 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 Scene(Entity): """A scene is a group of entities and the states we want them to be.""" diff --git a/homeassistant/components/sensor/__init__.py b/homeassistant/components/sensor/__init__.py index 2887d32b987..bed1850b34d 100644 --- a/homeassistant/components/sensor/__init__.py +++ b/homeassistant/components/sensor/__init__.py @@ -41,3 +41,8 @@ async def async_setup(hass, config): 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) diff --git a/homeassistant/components/sensor/deconz.py b/homeassistant/components/sensor/deconz.py index dc28a181aa0..69be7f52d6c 100644 --- a/homeassistant/components/sensor/deconz.py +++ b/homeassistant/components/sensor/deconz.py @@ -6,9 +6,8 @@ https://home-assistant.io/components/sensor.deconz/ """ from homeassistant.components.deconz import ( DOMAIN as DATA_DECONZ, DATA_DECONZ_ID) -from homeassistant.const import ( - ATTR_BATTERY_LEVEL, ATTR_VOLTAGE, CONF_EVENT, CONF_ID) -from homeassistant.core import EventOrigin, callback +from homeassistant.const import ATTR_BATTERY_LEVEL, ATTR_VOLTAGE +from homeassistant.core import callback from homeassistant.helpers.entity import Entity from homeassistant.helpers.icon import icon_for_battery_level from homeassistant.util import slugify @@ -35,7 +34,6 @@ async def async_setup_entry(hass, config_entry, async_add_devices): for sensor in sensors.values(): if sensor and sensor.type in DECONZ_SENSOR: if sensor.type in DECONZ_REMOTE: - DeconzEvent(hass, sensor) if sensor.battery: entities.append(DeconzBattery(sensor)) else: @@ -184,26 +182,3 @@ class DeconzBattery(Entity): ATTR_EVENT_ID: slugify(self._device.name), } return attr - - -class DeconzEvent(object): - """When you want signals instead of entities. - - Stateless sensors such as remotes are expected to generate an event - instead of a sensor entity in hass. - """ - - def __init__(self, hass, device): - """Register callback that will be used for signals.""" - self._hass = hass - self._device = device - self._device.register_async_callback(self.async_update_callback) - self._event = 'deconz_{}'.format(CONF_EVENT) - self._id = slugify(self._device.name) - - @callback - def async_update_callback(self, reason): - """Fire the event if reason is that state is updated.""" - if reason['state']: - data = {CONF_ID: self._id, CONF_EVENT: self._device.state} - self._hass.bus.async_fire(self._event, data, EventOrigin.remote) diff --git a/tests/components/deconz/test_init.py b/tests/components/deconz/test_init.py index ce231e3d162..b09edf42a87 100644 --- a/tests/components/deconz/test_init.py +++ b/tests/components/deconz/test_init.py @@ -107,3 +107,19 @@ async def test_setup_entry_successful(hass): (entry, 'scene') assert mock_config_entries.async_forward_entry_setup.mock_calls[3][1] == \ (entry, 'sensor') + + +async def test_unload_entry(hass): + """Test being able to unload an entry.""" + entry = Mock() + entry.data = {'host': '1.2.3.4', 'port': 80, 'api_key': '1234567890ABCDEF'} + with patch('pydeconz.DeconzSession.async_load_parameters', + return_value=mock_coro(True)): + assert await deconz.async_setup_entry(hass, entry) is True + assert deconz.DATA_DECONZ_EVENT in hass.data + hass.data[deconz.DATA_DECONZ_EVENT].append(Mock()) + hass.data[deconz.DATA_DECONZ_ID] = {'id': 'deconzid'} + assert await deconz.async_unload_entry(hass, entry) + assert deconz.DOMAIN not in hass.data + assert len(hass.data[deconz.DATA_DECONZ_EVENT]) == 0 + assert len(hass.data[deconz.DATA_DECONZ_ID]) == 0 diff --git a/tests/components/sensor/test_deconz.py b/tests/components/sensor/test_deconz.py index b70fb396686..d6c026e88bd 100644 --- a/tests/components/sensor/test_deconz.py +++ b/tests/components/sensor/test_deconz.py @@ -51,6 +51,7 @@ async def setup_bridge(hass, data): return_value=mock_coro(data)): await bridge.async_load_parameters() hass.data[deconz.DOMAIN] = bridge + hass.data[deconz.DATA_DECONZ_EVENT] = [] hass.data[deconz.DATA_DECONZ_ID] = {} config_entry = config_entries.ConfigEntry( 1, deconz.DOMAIN, 'Mock Title', {'host': 'mock-host'}, 'test') From d352dee9b7d0b7b9c39d7d6d512e74a636753783 Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Sun, 29 Apr 2018 16:21:46 +0200 Subject: [PATCH 060/155] Upgrade netdisco to 1.4.0 (#14152) --- homeassistant/components/discovery.py | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/discovery.py b/homeassistant/components/discovery.py index f0ebcba8366..69d0f4796ff 100644 --- a/homeassistant/components/discovery.py +++ b/homeassistant/components/discovery.py @@ -21,7 +21,7 @@ from homeassistant.helpers.event import async_track_point_in_utc_time from homeassistant.helpers.discovery import async_load_platform, async_discover import homeassistant.util.dt as dt_util -REQUIREMENTS = ['netdisco==1.3.1'] +REQUIREMENTS = ['netdisco==1.4.0'] DOMAIN = 'discovery' diff --git a/requirements_all.txt b/requirements_all.txt index a3519f29283..74835077054 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -550,7 +550,7 @@ nad_receiver==0.0.9 nanoleaf==0.4.1 # homeassistant.components.discovery -netdisco==1.3.1 +netdisco==1.4.0 # homeassistant.components.sensor.neurio_energy neurio==0.3.1 From 8e7f500f28332d218dcfa1b316279d5822127d95 Mon Sep 17 00:00:00 2001 From: escoand Date: Sun, 29 Apr 2018 17:50:49 +0200 Subject: [PATCH 061/155] Add precipitation to OpenWeatherMap forecast (#13971) * add initial precipitation support * move attr to component * remove blank line * add forecast attributes to platform and update demo * add tests * break long lines * calc lower temp correctly * move all new attributes to component * convert temp low only when existing --- homeassistant/components/weather/__init__.py | 7 +++++++ homeassistant/components/weather/buienradar.py | 6 ++---- homeassistant/components/weather/demo.py | 18 ++++++++++++++---- homeassistant/components/weather/ecobee.py | 5 ++--- .../components/weather/openweathermap.py | 5 +++-- homeassistant/components/weather/yweather.py | 5 ++--- tests/components/weather/test_weather.py | 12 +++++++++++- 7 files changed, 41 insertions(+), 17 deletions(-) diff --git a/homeassistant/components/weather/__init__.py b/homeassistant/components/weather/__init__.py index b200d634ba9..467a106a6a2 100644 --- a/homeassistant/components/weather/__init__.py +++ b/homeassistant/components/weather/__init__.py @@ -22,7 +22,10 @@ ENTITY_ID_FORMAT = DOMAIN + '.{}' ATTR_CONDITION_CLASS = 'condition_class' ATTR_FORECAST = 'forecast' +ATTR_FORECAST_CONDITION = 'condition' +ATTR_FORECAST_PRECIPITATION = 'precipitation' ATTR_FORECAST_TEMP = 'temperature' +ATTR_FORECAST_TEMP_LOW = 'templow' ATTR_FORECAST_TIME = 'datetime' ATTR_WEATHER_ATTRIBUTION = 'attribution' ATTR_WEATHER_HUMIDITY = 'humidity' @@ -144,6 +147,10 @@ class WeatherEntity(Entity): forecast_entry[ATTR_FORECAST_TEMP] = show_temp( self.hass, forecast_entry[ATTR_FORECAST_TEMP], self.temperature_unit, self.precision) + if ATTR_FORECAST_TEMP_LOW in forecast_entry: + forecast_entry[ATTR_FORECAST_TEMP_LOW] = show_temp( + self.hass, forecast_entry[ATTR_FORECAST_TEMP_LOW], + self.temperature_unit, self.precision) forecast.append(forecast_entry) data[ATTR_FORECAST] = forecast diff --git a/homeassistant/components/weather/buienradar.py b/homeassistant/components/weather/buienradar.py index bf1864a9c0f..9b9707e87f6 100644 --- a/homeassistant/components/weather/buienradar.py +++ b/homeassistant/components/weather/buienradar.py @@ -10,7 +10,8 @@ import asyncio import voluptuous as vol from homeassistant.components.weather import ( - WeatherEntity, PLATFORM_SCHEMA, ATTR_FORECAST_TEMP, ATTR_FORECAST_TIME) + WeatherEntity, PLATFORM_SCHEMA, ATTR_FORECAST_CONDITION, + 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 import config_validation as cv @@ -28,9 +29,6 @@ DEFAULT_TIMEFRAME = 60 CONF_FORECAST = 'forecast' -ATTR_FORECAST_CONDITION = 'condition' -ATTR_FORECAST_TEMP_LOW = 'templow' - CONDITION_CLASSES = { 'cloudy': ['c', 'p'], diff --git a/homeassistant/components/weather/demo.py b/homeassistant/components/weather/demo.py index 02e07996213..fffdf03d07d 100644 --- a/homeassistant/components/weather/demo.py +++ b/homeassistant/components/weather/demo.py @@ -7,7 +7,8 @@ https://home-assistant.io/components/demo/ from datetime import datetime, timedelta from homeassistant.components.weather import ( - WeatherEntity, ATTR_FORECAST_TEMP, ATTR_FORECAST_TIME) + WeatherEntity, ATTR_FORECAST_CONDITION, ATTR_FORECAST_PRECIPITATION, + ATTR_FORECAST_TEMP, ATTR_FORECAST_TEMP_LOW, ATTR_FORECAST_TIME) from homeassistant.const import (TEMP_CELSIUS, TEMP_FAHRENHEIT) CONDITION_CLASSES = { @@ -32,9 +33,15 @@ def setup_platform(hass, config, add_devices, discovery_info=None): """Set up the Demo weather.""" add_devices([ DemoWeather('South', 'Sunshine', 21.6414, 92, 1099, 0.5, TEMP_CELSIUS, - [22, 19, 15, 12, 14, 18, 21]), + [['rainy', 1, 22, 15], ['rainy', 5, 19, 8], + ['cloudy', 0, 15, 9], ['sunny', 0, 12, 6], + ['partlycloudy', 2, 14, 7], ['rainy', 15, 18, 7], + ['fog', 0.2, 21, 12]]), DemoWeather('North', 'Shower rain', -12, 54, 987, 4.8, TEMP_FAHRENHEIT, - [-10, -13, -18, -23, -19, -14, -9]) + [['snowy', 2, -10, -15], ['partlycloudy', 1, -13, -14], + ['sunny', 0, -18, -22], ['sunny', 0.1, -23, -23], + ['snowy', 4, -19, -20], ['sunny', 0.3, -14, -19], + ['sunny', 0, -9, -12]]) ]) @@ -108,7 +115,10 @@ class DemoWeather(WeatherEntity): for entry in self._forecast: data_dict = { ATTR_FORECAST_TIME: reftime.isoformat(), - ATTR_FORECAST_TEMP: entry + ATTR_FORECAST_CONDITION: entry[0], + ATTR_FORECAST_PRECIPITATION: entry[1], + ATTR_FORECAST_TEMP: entry[2], + ATTR_FORECAST_TEMP_LOW: entry[3] } reftime = reftime + timedelta(hours=4) forecast_data.append(data_dict) diff --git a/homeassistant/components/weather/ecobee.py b/homeassistant/components/weather/ecobee.py index 379f5c1211b..80ee4c29fbe 100644 --- a/homeassistant/components/weather/ecobee.py +++ b/homeassistant/components/weather/ecobee.py @@ -6,14 +6,13 @@ https://home-assistant.io/components/weather.ecobee/ """ from homeassistant.components import ecobee from homeassistant.components.weather import ( - WeatherEntity, ATTR_FORECAST_TEMP, ATTR_FORECAST_TIME) + WeatherEntity, ATTR_FORECAST_CONDITION, ATTR_FORECAST_TEMP, + ATTR_FORECAST_TEMP_LOW, ATTR_FORECAST_TIME) from homeassistant.const import (TEMP_FAHRENHEIT) DEPENDENCIES = ['ecobee'] -ATTR_FORECAST_CONDITION = 'condition' -ATTR_FORECAST_TEMP_LOW = 'templow' ATTR_FORECAST_TEMP_HIGH = 'temphigh' ATTR_FORECAST_PRESSURE = 'pressure' ATTR_FORECAST_VISIBILITY = 'visibility' diff --git a/homeassistant/components/weather/openweathermap.py b/homeassistant/components/weather/openweathermap.py index a8e26d39cb3..909f123b52c 100644 --- a/homeassistant/components/weather/openweathermap.py +++ b/homeassistant/components/weather/openweathermap.py @@ -10,7 +10,8 @@ import logging import voluptuous as vol from homeassistant.components.weather import ( - ATTR_FORECAST_TEMP, ATTR_FORECAST_TIME, PLATFORM_SCHEMA, WeatherEntity) + ATTR_FORECAST_CONDITION, ATTR_FORECAST_PRECIPITATION, ATTR_FORECAST_TEMP, + ATTR_FORECAST_TIME, PLATFORM_SCHEMA, WeatherEntity) from homeassistant.const import ( CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME, STATE_UNKNOWN, TEMP_CELSIUS) @@ -21,7 +22,6 @@ REQUIREMENTS = ['pyowm==2.8.0'] _LOGGER = logging.getLogger(__name__) -ATTR_FORECAST_CONDITION = 'condition' ATTRIBUTION = 'Data provided by OpenWeatherMap' DEFAULT_NAME = 'OpenWeatherMap' @@ -144,6 +144,7 @@ class OpenWeatherMapWeather(WeatherEntity): 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] diff --git a/homeassistant/components/weather/yweather.py b/homeassistant/components/weather/yweather.py index 5987cf7621f..f9befece5a4 100644 --- a/homeassistant/components/weather/yweather.py +++ b/homeassistant/components/weather/yweather.py @@ -10,7 +10,8 @@ import logging import voluptuous as vol from homeassistant.components.weather import ( - ATTR_FORECAST_TEMP, ATTR_FORECAST_TIME, PLATFORM_SCHEMA, WeatherEntity) + ATTR_FORECAST_CONDITION, ATTR_FORECAST_TEMP, ATTR_FORECAST_TEMP_LOW, + ATTR_FORECAST_TIME, PLATFORM_SCHEMA, WeatherEntity) from homeassistant.const import CONF_NAME, STATE_UNKNOWN, TEMP_CELSIUS import homeassistant.helpers.config_validation as cv @@ -20,10 +21,8 @@ _LOGGER = logging.getLogger(__name__) DATA_CONDITION = 'yahoo_condition' -ATTR_FORECAST_CONDITION = 'condition' ATTRIBUTION = "Weather details provided by Yahoo! Inc." -ATTR_FORECAST_TEMP_LOW = 'templow' CONF_WOEID = 'woeid' diff --git a/tests/components/weather/test_weather.py b/tests/components/weather/test_weather.py index 9d22b1ad0ae..a88e9979551 100644 --- a/tests/components/weather/test_weather.py +++ b/tests/components/weather/test_weather.py @@ -5,7 +5,8 @@ from homeassistant.components import weather from homeassistant.components.weather import ( ATTR_WEATHER_ATTRIBUTION, ATTR_WEATHER_HUMIDITY, ATTR_WEATHER_OZONE, ATTR_WEATHER_PRESSURE, ATTR_WEATHER_TEMPERATURE, ATTR_WEATHER_WIND_BEARING, - ATTR_WEATHER_WIND_SPEED, ATTR_FORECAST, ATTR_FORECAST_TEMP) + ATTR_WEATHER_WIND_SPEED, ATTR_FORECAST, ATTR_FORECAST_CONDITION, + ATTR_FORECAST_PRECIPITATION, ATTR_FORECAST_TEMP, ATTR_FORECAST_TEMP_LOW) from homeassistant.util.unit_system import METRIC_SYSTEM from homeassistant.setup import setup_component @@ -45,8 +46,17 @@ class TestWeather(unittest.TestCase): assert data.get(ATTR_WEATHER_OZONE) is None assert data.get(ATTR_WEATHER_ATTRIBUTION) == \ 'Powered by Home Assistant' + assert data.get(ATTR_FORECAST)[0].get(ATTR_FORECAST_CONDITION) == \ + 'rainy' + assert data.get(ATTR_FORECAST)[0].get(ATTR_FORECAST_PRECIPITATION) == 1 assert data.get(ATTR_FORECAST)[0].get(ATTR_FORECAST_TEMP) == 22 + assert data.get(ATTR_FORECAST)[0].get(ATTR_FORECAST_TEMP_LOW) == 15 + assert data.get(ATTR_FORECAST)[6].get(ATTR_FORECAST_CONDITION) == \ + 'fog' + assert data.get(ATTR_FORECAST)[6].get(ATTR_FORECAST_PRECIPITATION) \ + == 0.2 assert data.get(ATTR_FORECAST)[6].get(ATTR_FORECAST_TEMP) == 21 + assert data.get(ATTR_FORECAST)[6].get(ATTR_FORECAST_TEMP_LOW) == 12 assert len(data.get(ATTR_FORECAST)) == 7 def test_temperature_convert(self): From 113bdc493a89314b0263c67dcfb1ba07942f4935 Mon Sep 17 00:00:00 2001 From: Hate-Usernames Date: Sun, 29 Apr 2018 16:54:44 +0100 Subject: [PATCH 062/155] Allow transitioning to colour temp for tradfri (#14157) --- homeassistant/components/light/tradfri.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/homeassistant/components/light/tradfri.py b/homeassistant/components/light/tradfri.py index 95082bb4d19..ab53c3669cb 100644 --- a/homeassistant/components/light/tradfri.py +++ b/homeassistant/components/light/tradfri.py @@ -253,6 +253,8 @@ class TradfriLight(Light): 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 From 74320306a1195969c21dcecd2bd78cdde51f7398 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sun, 29 Apr 2018 10:22:28 -0400 Subject: [PATCH 063/155] Add mitemp_bt to coverage --- .coveragerc | 1 + 1 file changed, 1 insertion(+) diff --git a/.coveragerc b/.coveragerc index c1c879aef09..e7aa9a2b4d2 100644 --- a/.coveragerc +++ b/.coveragerc @@ -619,6 +619,7 @@ omit = homeassistant/components/sensor/lyft.py homeassistant/components/sensor/metoffice.py homeassistant/components/sensor/miflora.py + homeassistant/components/sensor/mitemp_bt.py homeassistant/components/sensor/modem_callerid.py homeassistant/components/sensor/mopar.py homeassistant/components/sensor/mqtt_room.py From 4c0024fd972ace494fe365cff57e887a66d445af Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sun, 29 Apr 2018 14:15:39 -0400 Subject: [PATCH 064/155] Another coverage fix --- .coveragerc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.coveragerc b/.coveragerc index e7aa9a2b4d2..1852d7d7365 100644 --- a/.coveragerc +++ b/.coveragerc @@ -29,7 +29,7 @@ omit = homeassistant/components/arduino.py homeassistant/components/*/arduino.py - homeassistant/components/bmw_connected_drive.py + homeassistant/components/bmw_connected_drive/*.py homeassistant/components/*/bmw_connected_drive.py homeassistant/components/android_ip_webcam.py From aa8bd3714388478273709a7c9b7f2ec291054f68 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jens=20=C3=98stergaard=20Nielsen?= Date: Sun, 29 Apr 2018 20:57:57 +0200 Subject: [PATCH 065/155] Added update_interval to maxcube (#14143) --- homeassistant/components/maxcube.py | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/maxcube.py b/homeassistant/components/maxcube.py index cf5091fc308..bca7a1b4ab7 100644 --- a/homeassistant/components/maxcube.py +++ b/homeassistant/components/maxcube.py @@ -13,7 +13,7 @@ import voluptuous as vol import homeassistant.helpers.config_validation as cv from homeassistant.helpers.discovery import load_platform -from homeassistant.const import CONF_HOST, CONF_PORT +from homeassistant.const import CONF_HOST, CONF_PORT, CONF_SCAN_INTERVAL REQUIREMENTS = ['maxcube-api==0.1.0'] @@ -32,6 +32,7 @@ CONF_GATEWAYS = 'gateways' CONFIG_GATEWAY = vol.Schema({ vol.Required(CONF_HOST): cv.string, vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, + vol.Optional(CONF_SCAN_INTERVAL, default=300): cv.time_period, }) CONFIG_SCHEMA = vol.Schema({ @@ -54,10 +55,11 @@ def setup(hass, config): for gateway in gateways: host = gateway[CONF_HOST] port = gateway[CONF_PORT] + scan_interval = gateway[CONF_SCAN_INTERVAL].total_seconds() try: cube = MaxCube(MaxCubeConnection(host, port)) - hass.data[DATA_KEY][host] = MaxCubeHandle(cube) + hass.data[DATA_KEY][host] = MaxCubeHandle(cube, scan_interval) except timeout as ex: _LOGGER.error("Unable to connect to Max!Cube gateway: %s", str(ex)) hass.components.persistent_notification.create( @@ -80,9 +82,10 @@ def setup(hass, config): class MaxCubeHandle(object): """Keep the cube instance in one place and centralize the update.""" - def __init__(self, cube): + def __init__(self, cube, scan_interval): """Initialize the Cube Handle.""" self.cube = cube + self.scan_interval = scan_interval self.mutex = Lock() self._updatets = time.time() @@ -90,8 +93,8 @@ class MaxCubeHandle(object): """Pull the latest data from the MAX! Cube.""" # Acquire mutex to prevent simultaneous update from multiple threads with self.mutex: - # Only update every 60s - if (time.time() - self._updatets) >= 60: + # Only update every update_interval + if (time.time() - self._updatets) >= self.scan_interval: _LOGGER.debug("Updating") try: From 30d987f59ff8f321b7c9f1d9baa1fc3870b15d79 Mon Sep 17 00:00:00 2001 From: Anders Melchiorsen Date: Mon, 30 Apr 2018 00:49:19 +0200 Subject: [PATCH 066/155] Revert Hue color state to be xy-based (#14154) --- homeassistant/components/light/hue.py | 5 +---- tests/components/light/test_hue.py | 13 ------------- 2 files changed, 1 insertion(+), 17 deletions(-) diff --git a/homeassistant/components/light/hue.py b/homeassistant/components/light/hue.py index 6b4908b02d4..9f662718514 100644 --- a/homeassistant/components/light/hue.py +++ b/homeassistant/components/light/hue.py @@ -245,12 +245,9 @@ class HueLight(Light): mode = self._color_mode source = self.light.action if self.is_group else self.light.state - if mode == 'xy' and 'xy' in source: + if mode in ('xy', 'hs'): return color.color_xy_to_hs(*source['xy']) - if mode == 'hs' and 'hue' in source and 'sat' in source: - return source['hue'] / 65535 * 360, source['sat'] / 255 * 100 - return None @property diff --git a/tests/components/light/test_hue.py b/tests/components/light/test_hue.py index d36548e1e91..8f5b52ea6de 100644 --- a/tests/components/light/test_hue.py +++ b/tests/components/light/test_hue.py @@ -650,19 +650,6 @@ def test_hs_color(): assert light.hs_color is None - light = hue_light.HueLight( - light=Mock(state={ - 'colormode': 'hs', - 'hue': 1234, - 'sat': 123, - }), - request_bridge_update=None, - bridge=Mock(), - is_group=False, - ) - - assert light.hs_color == (1234 / 65535 * 360, 123 / 255 * 100) - light = hue_light.HueLight( light=Mock(state={ 'colormode': 'xy', From 02a12a0bb4c87ee2406983a2459d9a98947b6f9d Mon Sep 17 00:00:00 2001 From: Russell Cloran Date: Sun, 29 Apr 2018 23:31:27 -0700 Subject: [PATCH 067/155] zha: Support remotes/buttons (#12528) --- homeassistant/components/binary_sensor/zha.py | 152 +++++++++++++++++- homeassistant/components/zha/__init__.py | 97 +++++++---- homeassistant/components/zha/const.py | 19 ++- 3 files changed, 232 insertions(+), 36 deletions(-) diff --git a/homeassistant/components/binary_sensor/zha.py b/homeassistant/components/binary_sensor/zha.py index bf038a62465..e1e6689d1eb 100644 --- a/homeassistant/components/binary_sensor/zha.py +++ b/homeassistant/components/binary_sensor/zha.py @@ -31,12 +31,21 @@ async def async_setup_platform(hass, config, async_add_devices, if discovery_info is None: return + from zigpy.zcl.clusters.general import OnOff from zigpy.zcl.clusters.security import IasZone + if IasZone.cluster_id in discovery_info['in_clusters']: + await _async_setup_iaszone(hass, config, async_add_devices, + discovery_info) + elif OnOff.cluster_id in discovery_info['out_clusters']: + await _async_setup_remote(hass, config, async_add_devices, + discovery_info) - in_clusters = discovery_info['in_clusters'] +async def _async_setup_iaszone(hass, config, async_add_devices, + discovery_info): device_class = None - cluster = in_clusters[IasZone.cluster_id] + from zigpy.zcl.clusters.security import IasZone + cluster = discovery_info['in_clusters'][IasZone.cluster_id] if discovery_info['new_join']: await cluster.bind() ieee = cluster.endpoint.device.application.ieee @@ -53,8 +62,34 @@ async def async_setup_platform(hass, config, async_add_devices, async_add_devices([sensor], update_before_add=True) +async def _async_setup_remote(hass, config, async_add_devices, discovery_info): + + async def safe(coro): + """Run coro, catching ZigBee delivery errors, and ignoring them.""" + import zigpy.exceptions + try: + await coro + except zigpy.exceptions.DeliveryError as exc: + _LOGGER.warning("Ignoring error during setup: %s", exc) + + if discovery_info['new_join']: + from zigpy.zcl.clusters.general import OnOff, LevelControl + out_clusters = discovery_info['out_clusters'] + if OnOff.cluster_id in out_clusters: + cluster = out_clusters[OnOff.cluster_id] + await safe(cluster.bind()) + await safe(cluster.configure_reporting(0, 0, 600, 1)) + if LevelControl.cluster_id in out_clusters: + cluster = out_clusters[LevelControl.cluster_id] + await safe(cluster.bind()) + await safe(cluster.configure_reporting(0, 1, 600, 1)) + + sensor = Switch(**discovery_info) + async_add_devices([sensor], update_before_add=True) + + class BinarySensor(zha.Entity, BinarySensorDevice): - """THe ZHA Binary Sensor.""" + """The ZHA Binary Sensor.""" _domain = DOMAIN @@ -102,3 +137,114 @@ class BinarySensor(zha.Entity, BinarySensorDevice): state = result.get('zone_status', self._state) if isinstance(state, (int, uint16_t)): self._state = result.get('zone_status', self._state) & 3 + + +class Switch(zha.Entity, BinarySensorDevice): + """ZHA switch/remote controller/button.""" + + _domain = DOMAIN + + class OnOffListener: + """Listener for the OnOff ZigBee cluster.""" + + def __init__(self, entity): + """Initialize OnOffListener.""" + self._entity = entity + + def cluster_command(self, tsn, command_id, args): + """Handle commands received to this cluster.""" + if command_id in (0x0000, 0x0040): + self._entity.set_state(False) + elif command_id in (0x0001, 0x0041, 0x0042): + self._entity.set_state(True) + elif command_id == 0x0002: + self._entity.set_state(not self._entity.is_on) + + def attribute_updated(self, attrid, value): + """Handle attribute updates on this cluster.""" + if attrid == 0: + self._entity.set_state(value) + self._entity.schedule_update_ha_state() + + def zdo_command(self, *args, **kwargs): + """Handle ZDO commands on this cluster.""" + pass + + class LevelListener: + """Listener for the LevelControl ZigBee cluster.""" + + def __init__(self, entity): + """Initialize LevelListener.""" + self._entity = entity + + def cluster_command(self, tsn, command_id, args): + """Handle commands received to this cluster.""" + if command_id in (0x0000, 0x0004): # move_to_level, -with_on_off + self._entity.set_level(args[0]) + elif command_id in (0x0001, 0x0005): # move, -with_on_off + # We should dim slowly -- for now, just step once + rate = args[1] + 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) + self._entity.move_level(-args[1] if args[0] else args[1]) + + def attribute_update(self, attrid, value): + """Handle attribute updates on this cluster.""" + if attrid == 0: + self._entity.set_level(value) + + def zdo_command(self, *args, **kwargs): + """Handle ZDO commands on this cluster.""" + pass + + def __init__(self, **kwargs): + """Initialize Switch.""" + self._state = True + self._level = 255 + from zigpy.zcl.clusters import general + self._out_listeners = { + general.OnOff.cluster_id: self.OnOffListener(self), + general.LevelControl.cluster_id: self.LevelListener(self), + } + super().__init__(**kwargs) + + @property + def is_on(self) -> bool: + """Return true if the binary sensor is on.""" + return self._state + + @property + def device_state_attributes(self): + """Return the device state attributes.""" + return {'level': self._state and self._level or 0} + + def move_level(self, change): + """Increment the level, setting state if appropriate.""" + if not self._state and change > 0: + self._level = 0 + self._level = min(255, max(0, self._level + change)) + self._state = bool(self._level) + self.schedule_update_ha_state() + + def set_level(self, level): + """Set the level, setting state if appropriate.""" + self._level = level + self._state = bool(self._level) + self.schedule_update_ha_state() + + def set_state(self, state): + """Set the state.""" + self._state = state + if self._level == 0: + self._level = 255 + self.schedule_update_ha_state() + + async def async_update(self): + """Retrieve latest state.""" + from zigpy.zcl.clusters.general import OnOff + result = await zha.safe_read( + self._endpoint.out_clusters[OnOff.cluster_id], ['on_off']) + self._state = result.get('on_off', self._state) diff --git a/homeassistant/components/zha/__init__.py b/homeassistant/components/zha/__init__.py index 73c1fdf9075..dc9cb26462d 100644 --- a/homeassistant/components/zha/__init__.py +++ b/homeassistant/components/zha/__init__.py @@ -221,44 +221,78 @@ class ApplicationListener: self._config, ) - for cluster_id, cluster in endpoint.in_clusters.items(): - cluster_type = type(cluster) - if cluster_id in profile_clusters[0]: - continue - if cluster_type not in zha_const.SINGLE_CLUSTER_DEVICE_CLASS: - continue + for cluster in endpoint.in_clusters.values(): + await self._attempt_single_cluster_device( + endpoint, + cluster, + profile_clusters[0], + device_key, + zha_const.SINGLE_INPUT_CLUSTER_DEVICE_CLASS, + 'in_clusters', + discovered_info, + join, + ) - component = zha_const.SINGLE_CLUSTER_DEVICE_CLASS[cluster_type] - cluster_key = "{}-{}".format(device_key, cluster_id) - discovery_info = { - 'application_listener': self, - 'endpoint': endpoint, - 'in_clusters': {cluster.cluster_id: cluster}, - 'out_clusters': {}, - 'new_join': join, - 'unique_id': cluster_key, - 'entity_suffix': '_{}'.format(cluster_id), - } - discovery_info.update(discovered_info) - self._hass.data[DISCOVERY_KEY][cluster_key] = discovery_info - - await discovery.async_load_platform( - self._hass, - component, - DOMAIN, - {'discovery_key': cluster_key}, - self._config, + for cluster in endpoint.out_clusters.values(): + await self._attempt_single_cluster_device( + endpoint, + cluster, + profile_clusters[1], + device_key, + zha_const.SINGLE_OUTPUT_CLUSTER_DEVICE_CLASS, + 'out_clusters', + discovered_info, + join, ) def register_entity(self, ieee, entity_obj): """Record the creation of a hass entity associated with ieee.""" self._device_registry[ieee].append(entity_obj) + async def _attempt_single_cluster_device(self, endpoint, cluster, + profile_clusters, device_key, + device_classes, discovery_attr, + entity_info, is_new_join): + """Try to set up an entity from a "bare" cluster.""" + if cluster.cluster_id in profile_clusters: + return + # pylint: disable=unidiomatic-typecheck + if type(cluster) not in device_classes: + return + + component = device_classes[type(cluster)] + cluster_key = "{}-{}".format(device_key, cluster.cluster_id) + discovery_info = { + 'application_listener': self, + 'endpoint': endpoint, + 'in_clusters': {}, + 'out_clusters': {}, + 'new_join': is_new_join, + 'unique_id': cluster_key, + 'entity_suffix': '_{}'.format(cluster.cluster_id), + } + discovery_info[discovery_attr] = {cluster.cluster_id: cluster} + discovery_info.update(entity_info) + self._hass.data[DISCOVERY_KEY][cluster_key] = discovery_info + + await discovery.async_load_platform( + self._hass, + component, + DOMAIN, + {'discovery_key': cluster_key}, + self._config, + ) + class Entity(entity.Entity): """A base class for ZHA entities.""" _domain = None # Must be overridden by subclasses + # Normally the entity itself is the listener. Base classes may set this to + # a dict of cluster ID -> listener to receive messages for specific + # clusters separately + _in_listeners = {} + _out_listeners = {} def __init__(self, endpoint, in_clusters, out_clusters, manufacturer, model, application_listener, unique_id, **kwargs): @@ -287,10 +321,11 @@ class Entity(entity.Entity): kwargs.get('entity_suffix', ''), ) - for cluster in in_clusters.values(): - cluster.add_listener(self) - for cluster in out_clusters.values(): - cluster.add_listener(self) + for cluster_id, cluster in in_clusters.items(): + cluster.add_listener(self._in_listeners.get(cluster_id, self)) + for cluster_id, cluster in out_clusters.items(): + cluster.add_listener(self._out_listeners.get(cluster_id, self)) + self._endpoint = endpoint self._in_clusters = in_clusters self._out_clusters = out_clusters @@ -379,7 +414,7 @@ async def safe_read(cluster, attributes): try: result, _ = await cluster.read_attributes( attributes, - allow_cache=False, + allow_cache=True, ) return result except Exception: # pylint: disable=broad-except diff --git a/homeassistant/components/zha/const.py b/homeassistant/components/zha/const.py index 4fe3581d5b2..36eb4d55c97 100644 --- a/homeassistant/components/zha/const.py +++ b/homeassistant/components/zha/const.py @@ -1,7 +1,8 @@ """All constants related to the ZHA component.""" DEVICE_CLASS = {} -SINGLE_CLUSTER_DEVICE_CLASS = {} +SINGLE_INPUT_CLUSTER_DEVICE_CLASS = {} +SINGLE_OUTPUT_CLUSTER_DEVICE_CLASS = {} COMPONENT_CLUSTERS = {} @@ -15,11 +16,17 @@ def populate_data(): from zigpy.profiles import PROFILES, zha, zll DEVICE_CLASS[zha.PROFILE_ID] = { + zha.DeviceType.ON_OFF_SWITCH: 'binary_sensor', + zha.DeviceType.LEVEL_CONTROL_SWITCH: 'binary_sensor', + zha.DeviceType.REMOTE_CONTROL: 'binary_sensor', zha.DeviceType.SMART_PLUG: 'switch', zha.DeviceType.ON_OFF_LIGHT: 'light', zha.DeviceType.DIMMABLE_LIGHT: 'light', zha.DeviceType.COLOR_DIMMABLE_LIGHT: 'light', + zha.DeviceType.ON_OFF_LIGHT_SWITCH: 'binary_sensor', + zha.DeviceType.DIMMER_SWITCH: 'binary_sensor', + zha.DeviceType.COLOR_DIMMER_SWITCH: 'binary_sensor', } DEVICE_CLASS[zll.PROFILE_ID] = { zll.DeviceType.ON_OFF_LIGHT: 'light', @@ -29,15 +36,23 @@ def populate_data(): zll.DeviceType.COLOR_LIGHT: 'light', zll.DeviceType.EXTENDED_COLOR_LIGHT: 'light', zll.DeviceType.COLOR_TEMPERATURE_LIGHT: 'light', + zll.DeviceType.COLOR_CONTROLLER: 'binary_sensor', + zll.DeviceType.COLOR_SCENE_CONTROLLER: 'binary_sensor', + zll.DeviceType.CONTROLLER: 'binary_sensor', + zll.DeviceType.SCENE_CONTROLLER: 'binary_sensor', + zll.DeviceType.ON_OFF_SENSOR: 'binary_sensor', } - SINGLE_CLUSTER_DEVICE_CLASS.update({ + SINGLE_INPUT_CLUSTER_DEVICE_CLASS.update({ zcl.clusters.general.OnOff: 'switch', zcl.clusters.measurement.RelativeHumidity: 'sensor', zcl.clusters.measurement.TemperatureMeasurement: 'sensor', zcl.clusters.security.IasZone: 'binary_sensor', zcl.clusters.hvac.Fan: 'fan', }) + SINGLE_OUTPUT_CLUSTER_DEVICE_CLASS.update({ + zcl.clusters.general.OnOff: 'binary_sensor', + }) # A map of hass components to all Zigbee clusters it could use for profile_id, classes in DEVICE_CLASS.items(): From d7eced95fa59da1a1efcd767f87acd9f8e650404 Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Mon, 30 Apr 2018 09:28:00 +0200 Subject: [PATCH 068/155] Upgrade numpy to 1.14.3 (#14187) --- homeassistant/components/binary_sensor/trend.py | 2 +- homeassistant/components/image_processing/opencv.py | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/binary_sensor/trend.py b/homeassistant/components/binary_sensor/trend.py index 9b4598f3c42..5405a6a77ba 100644 --- a/homeassistant/components/binary_sensor/trend.py +++ b/homeassistant/components/binary_sensor/trend.py @@ -23,7 +23,7 @@ from homeassistant.helpers.entity import generate_entity_id from homeassistant.helpers.event import async_track_state_change from homeassistant.util import utcnow -REQUIREMENTS = ['numpy==1.14.2'] +REQUIREMENTS = ['numpy==1.14.3'] _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/image_processing/opencv.py b/homeassistant/components/image_processing/opencv.py index 18e74966a59..c3e34b4d42b 100644 --- a/homeassistant/components/image_processing/opencv.py +++ b/homeassistant/components/image_processing/opencv.py @@ -16,7 +16,7 @@ from homeassistant.components.image_processing import ( from homeassistant.core import split_entity_id import homeassistant.helpers.config_validation as cv -REQUIREMENTS = ['numpy==1.14.2'] +REQUIREMENTS = ['numpy==1.14.3'] _LOGGER = logging.getLogger(__name__) diff --git a/requirements_all.txt b/requirements_all.txt index 74835077054..796f80c3bd0 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -563,7 +563,7 @@ nuheat==0.3.0 # homeassistant.components.binary_sensor.trend # homeassistant.components.image_processing.opencv -numpy==1.14.2 +numpy==1.14.3 # homeassistant.components.google oauth2client==4.0.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 5d6fe9e2f63..28265bdb5f1 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -99,7 +99,7 @@ mficlient==0.3.0 # homeassistant.components.binary_sensor.trend # homeassistant.components.image_processing.opencv -numpy==1.14.2 +numpy==1.14.3 # homeassistant.components.mqtt # homeassistant.components.shiftr From 76c9c0179b5a0ffae6637d10bc58a974519053b4 Mon Sep 17 00:00:00 2001 From: Otto Winter Date: Mon, 30 Apr 2018 14:46:44 +0200 Subject: [PATCH 069/155] Improve chromecast disconnection logic (#14190) * Attempt Cast Fix * Cleanup --- homeassistant/components/media_player/cast.py | 26 +++++++++++++------ tests/components/media_player/test_cast.py | 16 +++++++++--- 2 files changed, 30 insertions(+), 12 deletions(-) diff --git a/homeassistant/components/media_player/cast.py b/homeassistant/components/media_player/cast.py index 632ab4214b8..a9bea9e4c1d 100644 --- a/homeassistant/components/media_player/cast.py +++ b/homeassistant/components/media_player/cast.py @@ -306,13 +306,18 @@ class CastDevice(MediaPlayerDevice): _LOGGER.debug("Discovered chromecast with same UUID: %s", discover) self.hass.async_add_job(self.async_set_cast_info(discover)) + async def async_stop(event): + """Disconnect socket on Home Assistant stop.""" + await self._async_disconnect() + async_dispatcher_connect(self.hass, SIGNAL_CAST_DISCOVERED, async_cast_discovered) + self.hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, async_stop) self.hass.async_add_job(self.async_set_cast_info(self._cast_info)) async def async_will_remove_from_hass(self) -> None: """Disconnect Chromecast object when removed.""" - self._async_disconnect() + await self._async_disconnect() if self._cast_info.uuid is not None: # Remove the entity from the added casts so that it can dynamically # be re-added again. @@ -328,7 +333,7 @@ class CastDevice(MediaPlayerDevice): if old_cast_info.host_port == cast_info.host_port: # Nothing connection-related updated return - self._async_disconnect() + await self._async_disconnect() # Failed connection will unfortunately never raise an exception, it # will instead just try connecting indefinitely. @@ -348,22 +353,27 @@ class CastDevice(MediaPlayerDevice): _LOGGER.debug("Connection successful!") self.async_schedule_update_ha_state() - @callback - def _async_disconnect(self): + async def _async_disconnect(self): """Disconnect Chromecast object if it is set.""" if self._chromecast is None: # Can't disconnect if not connected. return - _LOGGER.debug("Disconnecting from previous chromecast socket.") + _LOGGER.debug("Disconnecting from chromecast socket.") self._available = False - self._chromecast.disconnect(blocking=False) + self.async_schedule_update_ha_state() + + await self.hass.async_add_job(self._chromecast.disconnect) + # Invalidate some attributes self._chromecast = None self.cast_status = None self.media_status = None self.media_status_received = None - self._status_listener.invalidate() - self._status_listener = None + if self._status_listener is not None: + self._status_listener.invalidate() + self._status_listener = None + + self.async_schedule_update_ha_state() # ========== Callbacks ========== def new_cast_status(self, cast_status): diff --git a/tests/components/media_player/test_cast.py b/tests/components/media_player/test_cast.py index ee69ec1c85d..41cf6749b71 100644 --- a/tests/components/media_player/test_cast.py +++ b/tests/components/media_player/test_cast.py @@ -346,8 +346,16 @@ async def test_switched_host(hass: HomeAssistantType): async_dispatcher_send(hass, cast.SIGNAL_CAST_DISCOVERED, changed) await hass.async_block_till_done() assert get_chromecast.call_count == 1 - chromecast.disconnect.assert_called_once_with(blocking=False) + assert chromecast.disconnect.call_count == 1 - hass.bus.async_fire(EVENT_HOMEASSISTANT_STOP) - await hass.async_block_till_done() - chromecast.disconnect.assert_called_once_with(blocking=False) + +async def test_disconnect_on_stop(hass: HomeAssistantType): + """Test cast device disconnects socket on stop.""" + info = get_fake_chromecast_info() + + with patch('pychromecast.dial.get_device_status', return_value=info): + chromecast, _ = await async_setup_media_player_cast(hass, info) + + hass.bus.async_fire(EVENT_HOMEASSISTANT_STOP) + await hass.async_block_till_done() + assert chromecast.disconnect.call_count == 1 From 46c260fd8512d7c34fe4c2482cd4aa49d369061b Mon Sep 17 00:00:00 2001 From: cdce8p <30130371+cdce8p@users.noreply.github.com> Date: Mon, 30 Apr 2018 14:58:17 +0200 Subject: [PATCH 070/155] Added CONF_IP_ADDRESS to HomeKit (#14163) --- homeassistant/components/homekit/__init__.py | 16 +++++++---- tests/components/homekit/test_homekit.py | 28 +++++++++++++------- 2 files changed, 30 insertions(+), 14 deletions(-) diff --git a/homeassistant/components/homekit/__init__.py b/homeassistant/components/homekit/__init__.py index 025ef4069e9..4984cfee959 100644 --- a/homeassistant/components/homekit/__init__.py +++ b/homeassistant/components/homekit/__init__.py @@ -3,6 +3,7 @@ For more details about this platform, please refer to the documentation at https://home-assistant.io/components/homekit/ """ +import ipaddress import logging from zlib import adler32 @@ -12,8 +13,8 @@ from homeassistant.components.cover import ( SUPPORT_CLOSE, SUPPORT_OPEN, SUPPORT_SET_POSITION) from homeassistant.const import ( ATTR_SUPPORTED_FEATURES, ATTR_UNIT_OF_MEASUREMENT, - ATTR_DEVICE_CLASS, CONF_PORT, TEMP_CELSIUS, TEMP_FAHRENHEIT, - EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP) + ATTR_DEVICE_CLASS, CONF_IP_ADDRESS, CONF_PORT, TEMP_CELSIUS, + TEMP_FAHRENHEIT, EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP) import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entityfilter import FILTER_SCHEMA from homeassistant.util import get_local_ip @@ -35,6 +36,8 @@ REQUIREMENTS = ['HAP-python==1.1.9'] CONFIG_SCHEMA = vol.Schema({ DOMAIN: vol.All({ vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, + vol.Optional(CONF_IP_ADDRESS): + vol.All(ipaddress.ip_address, cv.string), vol.Optional(CONF_AUTO_START, default=DEFAULT_AUTO_START): cv.boolean, vol.Optional(CONF_FILTER, default={}): FILTER_SCHEMA, vol.Optional(CONF_ENTITY_CONFIG, default={}): validate_entity_config, @@ -48,11 +51,12 @@ async def async_setup(hass, config): conf = config[DOMAIN] port = conf[CONF_PORT] + ip_address = conf.get(CONF_IP_ADDRESS) auto_start = conf[CONF_AUTO_START] entity_filter = conf[CONF_FILTER] entity_config = conf[CONF_ENTITY_CONFIG] - homekit = HomeKit(hass, port, entity_filter, entity_config) + homekit = HomeKit(hass, port, ip_address, entity_filter, entity_config) homekit.setup() if auto_start: @@ -151,10 +155,11 @@ def generate_aid(entity_id): class HomeKit(): """Class to handle all actions between HomeKit and Home Assistant.""" - def __init__(self, hass, port, entity_filter, entity_config): + def __init__(self, hass, port, ip_address, entity_filter, entity_config): """Initialize a HomeKit object.""" self.hass = hass self._port = port + self._ip_address = ip_address self._filter = entity_filter self._config = entity_config self.started = False @@ -169,9 +174,10 @@ class HomeKit(): self.hass.bus.async_listen_once( EVENT_HOMEASSISTANT_STOP, self.stop) + ip_addr = self._ip_address or get_local_ip() path = self.hass.config.path(HOMEKIT_FILE) self.bridge = HomeBridge(self.hass) - self.driver = HomeDriver(self.bridge, self._port, get_local_ip(), path) + self.driver = HomeDriver(self.bridge, self._port, ip_addr, path) def add_bridge_accessory(self, state): """Try adding accessory to bridge if configured beforehand.""" diff --git a/tests/components/homekit/test_homekit.py b/tests/components/homekit/test_homekit.py index d1ad232d279..7ae37becbd5 100644 --- a/tests/components/homekit/test_homekit.py +++ b/tests/components/homekit/test_homekit.py @@ -11,7 +11,8 @@ from homeassistant.components.homekit.const import ( DEFAULT_PORT, SERVICE_HOMEKIT_START) from homeassistant.helpers.entityfilter import generate_filter from homeassistant.const import ( - CONF_PORT, EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP) + CONF_IP_ADDRESS, CONF_PORT, + EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP) from tests.common import get_test_home_assistant from tests.components.homekit.test_accessories import patch_debounce @@ -59,7 +60,7 @@ class TestHomeKit(unittest.TestCase): self.hass, DOMAIN, {DOMAIN: {}})) self.assertEqual(mock_homekit.mock_calls, [ - call(self.hass, DEFAULT_PORT, ANY, {}), + call(self.hass, DEFAULT_PORT, None, ANY, {}), call().setup()]) # Test auto start enabled @@ -74,7 +75,8 @@ class TestHomeKit(unittest.TestCase): """Test async_setup with auto start disabled and test service calls.""" mock_homekit.return_value = homekit = Mock() - config = {DOMAIN: {CONF_AUTO_START: False, CONF_PORT: 11111}} + config = {DOMAIN: {CONF_AUTO_START: False, CONF_PORT: 11111, + CONF_IP_ADDRESS: '172.0.0.0'}} self.assertTrue(setup.setup_component( self.hass, DOMAIN, config)) @@ -82,7 +84,7 @@ class TestHomeKit(unittest.TestCase): self.hass.block_till_done() self.assertEqual(mock_homekit.mock_calls, [ - call(self.hass, 11111, ANY, {}), + call(self.hass, 11111, '172.0.0.0', ANY, {}), call().setup()]) # Test start call with driver stopped. @@ -101,7 +103,7 @@ class TestHomeKit(unittest.TestCase): def test_homekit_setup(self): """Test setup of bridge and driver.""" - homekit = HomeKit(self.hass, DEFAULT_PORT, {}, {}) + homekit = HomeKit(self.hass, DEFAULT_PORT, None, {}, {}) with patch(PATH_HOMEKIT + '.accessories.HomeDriver') as mock_driver, \ patch('homeassistant.util.get_local_ip') as mock_ip: @@ -117,9 +119,17 @@ class TestHomeKit(unittest.TestCase): self.assertEqual( self.hass.bus.listeners.get(EVENT_HOMEASSISTANT_STOP), 1) + def test_homekit_setup_ip_address(self): + """Test setup with given IP address.""" + homekit = HomeKit(self.hass, DEFAULT_PORT, '172.0.0.0', {}, {}) + + with patch(PATH_HOMEKIT + '.accessories.HomeDriver') as mock_driver: + homekit.setup() + mock_driver.assert_called_with(ANY, DEFAULT_PORT, '172.0.0.0', ANY) + def test_homekit_add_accessory(self): """Add accessory if config exists and get_acc returns an accessory.""" - homekit = HomeKit(self.hass, None, lambda entity_id: True, {}) + homekit = HomeKit(self.hass, None, None, lambda entity_id: True, {}) homekit.bridge = HomeBridge(self.hass) with patch(PATH_HOMEKIT + '.accessories.HomeBridge.add_accessory') \ @@ -142,7 +152,7 @@ class TestHomeKit(unittest.TestCase): def test_homekit_entity_filter(self): """Test the entity filter.""" entity_filter = generate_filter(['cover'], ['demo.test'], [], []) - homekit = HomeKit(self.hass, None, entity_filter, {}) + homekit = HomeKit(self.hass, None, None, entity_filter, {}) with patch(PATH_HOMEKIT + '.get_accessory') as mock_get_acc: mock_get_acc.return_value = None @@ -162,7 +172,7 @@ class TestHomeKit(unittest.TestCase): @patch(PATH_HOMEKIT + '.HomeKit.add_bridge_accessory') def test_homekit_start(self, mock_add_bridge_acc, mock_show_setup_msg): """Test HomeKit start method.""" - homekit = HomeKit(self.hass, None, {}, {'cover.demo': {}}) + homekit = HomeKit(self.hass, None, None, {}, {'cover.demo': {}}) homekit.bridge = HomeBridge(self.hass) homekit.driver = Mock() @@ -184,7 +194,7 @@ class TestHomeKit(unittest.TestCase): def test_homekit_stop(self): """Test HomeKit stop method.""" - homekit = HomeKit(None, None, None, None) + homekit = HomeKit(None, None, None, None, None) homekit.driver = Mock() # Test if started = False From 5dcad89a0d58ed40993b6b8d27a5d5d2daee63b7 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 30 Apr 2018 09:18:18 -0400 Subject: [PATCH 071/155] Do not sync entities with an empty name (#14181) --- .../components/google_assistant/smart_home.py | 11 +++++--- .../google_assistant/test_smart_home.py | 26 +++++++++++++++++++ 2 files changed, 34 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/google_assistant/smart_home.py b/homeassistant/components/google_assistant/smart_home.py index 7e746d48bed..27d993aee76 100644 --- a/homeassistant/components/google_assistant/smart_home.py +++ b/homeassistant/components/google_assistant/smart_home.py @@ -102,18 +102,23 @@ class _GoogleEntity: if state.state == STATE_UNAVAILABLE: return None + entity_config = self.config.entity_config.get(state.entity_id, {}) + name = (entity_config.get(CONF_NAME) or state.name).strip() + + # If an empty string + if not name: + return None + traits = self.traits() # Found no supported traits for this entity if not traits: return None - entity_config = self.config.entity_config.get(state.entity_id, {}) - device = { 'id': state.entity_id, 'name': { - 'name': entity_config.get(CONF_NAME) or state.name + 'name': name }, 'attributes': {}, 'traits': [trait.name for trait in traits], diff --git a/tests/components/google_assistant/test_smart_home.py b/tests/components/google_assistant/test_smart_home.py index e284b026ad8..cdaf4200c97 100644 --- a/tests/components/google_assistant/test_smart_home.py +++ b/tests/components/google_assistant/test_smart_home.py @@ -286,3 +286,29 @@ async def test_unavailable_state_doesnt_sync(hass): 'devices': [] } } + + +async def test_empty_name_doesnt_sync(hass): + """Test that an entity with empty name does not sync over.""" + light = DemoLight( + None, ' ', + state=False, + ) + light.hass = hass + light.entity_id = 'light.demo_light' + await light.async_update_ha_state() + + result = await sh.async_handle_message(hass, BASIC_CONFIG, { + "requestId": REQ_ID, + "inputs": [{ + "intent": "action.devices.SYNC" + }] + }) + + assert result == { + 'requestId': REQ_ID, + 'payload': { + 'agentUserId': 'test-agent', + 'devices': [] + } + } From 853a16938b98ea94463f1c5866da38b2d0832a18 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 30 Apr 2018 09:56:42 -0400 Subject: [PATCH 072/155] Fix poorly formatted automations (#14196) --- homeassistant/components/config/automation.py | 8 ++- tests/components/config/test_automation.py | 67 +++++++++++++++++-- 2 files changed, 67 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/config/automation.py b/homeassistant/components/config/automation.py index 1e260854687..223159eb415 100644 --- a/homeassistant/components/config/automation.py +++ b/homeassistant/components/config/automation.py @@ -1,6 +1,7 @@ """Provide configuration end points for Automations.""" import asyncio from collections import OrderedDict +import uuid from homeassistant.const import CONF_ID from homeassistant.components.config import EditIdBasedConfigView @@ -29,7 +30,12 @@ class EditAutomationConfigView(EditIdBasedConfigView): """Set value.""" index = None for index, cur_value in enumerate(data): - if cur_value[CONF_ID] == config_key: + # When people copy paste their automations to the config file, + # they sometimes forget to add IDs. Fix it here. + if CONF_ID not in cur_value: + cur_value[CONF_ID] = uuid.uuid4().hex + + elif cur_value[CONF_ID] == config_key: break else: cur_value = OrderedDict() diff --git a/tests/components/config/test_automation.py b/tests/components/config/test_automation.py index 327283e74aa..2c888dd2dd2 100644 --- a/tests/components/config/test_automation.py +++ b/tests/components/config/test_automation.py @@ -42,13 +42,13 @@ async def test_update_device_config(hass, aiohttp_client): client = await aiohttp_client(hass.http.app) orig_data = [ - { - 'id': 'sun', - }, - { - 'id': 'moon', - } - ] + { + 'id': 'sun', + }, + { + 'id': 'moon', + } + ] def mock_read(path): """Mock reading data.""" @@ -81,3 +81,56 @@ async def test_update_device_config(hass, aiohttp_client): 'action': [], } assert written[0] == orig_data + + +async def test_bad_formatted_automations(hass, aiohttp_client): + """Test that we handle automations without ID.""" + with patch.object(config, 'SECTIONS', ['automation']): + await async_setup_component(hass, 'config', {}) + + client = await aiohttp_client(hass.http.app) + + orig_data = [ + { + # No ID + 'action': { + 'event': 'hello' + } + }, + { + 'id': 'moon', + } + ] + + def mock_read(path): + """Mock reading data.""" + return orig_data + + written = [] + + def mock_write(path, data): + """Mock writing data.""" + written.append(data) + + with patch('homeassistant.components.config._read', mock_read), \ + patch('homeassistant.components.config._write', mock_write): + resp = await client.post( + '/api/config/automation/config/moon', data=json.dumps({ + 'trigger': [], + 'action': [], + 'condition': [], + })) + + assert resp.status == 200 + result = await resp.json() + assert result == {'result': 'ok'} + + # Verify ID added to orig_data + assert 'id' in orig_data[0] + + assert orig_data[1] == { + 'id': 'moon', + 'trigger': [], + 'condition': [], + 'action': [], + } From eceece866d5bde62d44eb8d214be820d11e71959 Mon Sep 17 00:00:00 2001 From: Mahesh Subramaniya Date: Mon, 30 Apr 2018 10:48:51 -0500 Subject: [PATCH 073/155] Updating darksky default update interval to 5 mins (#14195) With Darksky allowing only 1000 API requests per day, 2 minutes retry seems to be bit closer to running over the limit and actually it did for 5 days in my account. Hence proposing a change to 5 minutes to keep the API happy and also it doesn't hurt to check the weather for every 5 mins than 2 mins someone lives in Jupiter :-P --- homeassistant/components/sensor/darksky.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/sensor/darksky.py b/homeassistant/components/sensor/darksky.py index 7d535c5f1d9..ac09de9c699 100644 --- a/homeassistant/components/sensor/darksky.py +++ b/homeassistant/components/sensor/darksky.py @@ -146,7 +146,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ 'Latitude and longitude must exist together'): cv.latitude, vol.Inclusive(CONF_LONGITUDE, 'coordinates', 'Latitude and longitude must exist together'): cv.longitude, - vol.Optional(CONF_UPDATE_INTERVAL, default=timedelta(seconds=120)): ( + vol.Optional(CONF_UPDATE_INTERVAL, default=timedelta(seconds=300)): ( vol.All(cv.time_period, cv.positive_timedelta)), vol.Optional(CONF_FORECAST): vol.All(cv.ensure_list, [vol.Range(min=1, max=7)]), From 6e0a3abf66d89833394d55b2ac25af4068ab2a01 Mon Sep 17 00:00:00 2001 From: mvn23 Date: Mon, 30 Apr 2018 19:27:45 +0200 Subject: [PATCH 074/155] Fix TypeError on round(self.humidity) (fixes #13116) (#14174) * Fix TypeError on round(self.humidity) Some weather platforms postpone the first data fetch for a while on init. As a result round(self.humidity is called before it is assigned a value, producing an error. This is a fix for that. * Rewrite to avoid false negative evaluation As per the suggestion from @OttoWinter, rewrite to avoid matching e.g. 0.0 as false. --- homeassistant/components/weather/__init__.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/weather/__init__.py b/homeassistant/components/weather/__init__.py index 467a106a6a2..c36c960c4fc 100644 --- a/homeassistant/components/weather/__init__.py +++ b/homeassistant/components/weather/__init__.py @@ -113,9 +113,12 @@ class WeatherEntity(Entity): ATTR_WEATHER_TEMPERATURE: show_temp( self.hass, self.temperature, self.temperature_unit, self.precision), - ATTR_WEATHER_HUMIDITY: round(self.humidity) } + humidity = self.humidity + if humidity is not None: + data[ATTR_WEATHER_HUMIDITY] = round(humidity) + ozone = self.ozone if ozone is not None: data[ATTR_WEATHER_OZONE] = ozone From 12dff5baa8ce643d2ffa996f1202970c335c32f2 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 30 Apr 2018 15:05:29 -0400 Subject: [PATCH 075/155] Add room hint support to Google Assistant cloud (#14180) --- homeassistant/components/cloud/__init__.py | 4 +++- tests/components/cloud/test_iot.py | 4 +++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/cloud/__init__.py b/homeassistant/components/cloud/__init__.py index e73d043d366..8c1a9751c19 100644 --- a/homeassistant/components/cloud/__init__.py +++ b/homeassistant/components/cloud/__init__.py @@ -21,6 +21,7 @@ from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.util import dt as dt_util from homeassistant.components.alexa import smart_home as alexa_sh from homeassistant.components.google_assistant import helpers as ga_h +from homeassistant.components.google_assistant import const as ga_c from . import http_api, iot from .const import CONFIG_DIR, DOMAIN, SERVERS @@ -52,7 +53,8 @@ ALEXA_ENTITY_SCHEMA = vol.Schema({ GOOGLE_ENTITY_SCHEMA = vol.Schema({ vol.Optional(CONF_NAME): cv.string, - vol.Optional(CONF_ALIASES): vol.All(cv.ensure_list, [cv.string]) + vol.Optional(CONF_ALIASES): vol.All(cv.ensure_list, [cv.string]), + vol.Optional(ga_c.CONF_ROOM_HINT): cv.string, }) ASSISTANT_SCHEMA = vol.Schema({ diff --git a/tests/components/cloud/test_iot.py b/tests/components/cloud/test_iot.py index f4ae81ad2f2..81b1e315085 100644 --- a/tests/components/cloud/test_iot.py +++ b/tests/components/cloud/test_iot.py @@ -318,7 +318,8 @@ def test_handler_google_actions(hass): 'entity_config': { 'switch.test': { 'name': 'Config name', - 'aliases': 'Config alias' + 'aliases': 'Config alias', + 'room': 'living room' } } } @@ -347,6 +348,7 @@ def test_handler_google_actions(hass): assert device['name']['name'] == 'Config name' assert device['name']['nicknames'] == ['Config alias'] assert device['type'] == 'action.devices.types.SWITCH' + assert device['roomHint'] == 'living room' async def test_refresh_token_expired(hass): From 626d6df545de8cc71128992181e9d09d024aee9e Mon Sep 17 00:00:00 2001 From: Philipp Schmitt Date: Tue, 1 May 2018 10:14:33 +0200 Subject: [PATCH 076/155] Update CODEOWNERS (#14214) --- CODEOWNERS | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CODEOWNERS b/CODEOWNERS index 528716e174d..a62ed67db66 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -54,8 +54,11 @@ homeassistant/components/device_tracker/tile.py @bachya homeassistant/components/history_graph.py @andrey-git homeassistant/components/light/tplink.py @rytilahti homeassistant/components/light/yeelight.py @rytilahti +homeassistant/components/lock/nello.py @pschmitt +homeassistant/components/lock/nuki.py @pschmitt homeassistant/components/media_player/emby.py @mezz64 homeassistant/components/media_player/kodi.py @armills +homeassistant/components/media_player/liveboxplaytv.py @pschmitt homeassistant/components/media_player/mediaroom.py @dgomes homeassistant/components/media_player/monoprice.py @etsinko homeassistant/components/media_player/sonos.py @amelchio @@ -77,6 +80,7 @@ homeassistant/components/sensor/upnp.py @dgomes homeassistant/components/sensor/waqi.py @andrey-git homeassistant/components/switch/rainmachine.py @bachya homeassistant/components/switch/tplink.py @rytilahti +homeassistant/components/vacuum/roomba.py @pschmitt homeassistant/components/xiaomi_aqara.py @danielhiversen @syssi homeassistant/components/*/axis.py @kane610 From a4e0c9c251095a171538ca5475c5a14c4e4e4211 Mon Sep 17 00:00:00 2001 From: Diogo Gomes Date: Tue, 1 May 2018 13:51:47 +0100 Subject: [PATCH 077/155] Fixes #14169 (Upgrade pyupnp-async to 0.1.0.2) (#14210) * Fixes #14169 (upstream version bump) * bump pyupnp-async version --- homeassistant/components/upnp.py | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/upnp.py b/homeassistant/components/upnp.py index dd611090c22..26a59746aea 100644 --- a/homeassistant/components/upnp.py +++ b/homeassistant/components/upnp.py @@ -15,7 +15,7 @@ from homeassistant.helpers import config_validation as cv from homeassistant.helpers import discovery from homeassistant.util import get_local_ip -REQUIREMENTS = ['pyupnp-async==0.1.0.1'] +REQUIREMENTS = ['pyupnp-async==0.1.0.2'] DEPENDENCIES = ['http'] _LOGGER = logging.getLogger(__name__) diff --git a/requirements_all.txt b/requirements_all.txt index 796f80c3bd0..93bf26f5239 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1069,7 +1069,7 @@ pytradfri[async]==5.4.2 pyunifi==2.13 # homeassistant.components.upnp -pyupnp-async==0.1.0.1 +pyupnp-async==0.1.0.2 # homeassistant.components.keyboard # pyuserinput==0.1.11 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 28265bdb5f1..a5835392c4a 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -162,7 +162,7 @@ pythonwhois==2.4.3 pyunifi==2.13 # homeassistant.components.upnp -pyupnp-async==0.1.0.1 +pyupnp-async==0.1.0.2 # homeassistant.components.notify.html5 pywebpush==1.6.0 From 9d4d1c82335e4c40d8502e30bd11c01b177e15d2 Mon Sep 17 00:00:00 2001 From: Russell Cloran Date: Tue, 1 May 2018 05:55:25 -0700 Subject: [PATCH 078/155] zha: Clean up binary_sensor listener registration/state updates (#14197) - Instead of registering listeners in the entity __init__, do it in async_added_to_hass to avoid errors updating an entity which isn't fully set up yet - Change from schedule_update_ha_state to async_schedule_update_ha_state --- homeassistant/components/binary_sensor/zha.py | 9 +++---- homeassistant/components/sensor/zha.py | 2 +- homeassistant/components/zha/__init__.py | 26 ++++++++++++------- 3 files changed, 21 insertions(+), 16 deletions(-) diff --git a/homeassistant/components/binary_sensor/zha.py b/homeassistant/components/binary_sensor/zha.py index e1e6689d1eb..756323f41d9 100644 --- a/homeassistant/components/binary_sensor/zha.py +++ b/homeassistant/components/binary_sensor/zha.py @@ -164,7 +164,6 @@ class Switch(zha.Entity, BinarySensorDevice): """Handle attribute updates on this cluster.""" if attrid == 0: self._entity.set_state(value) - self._entity.schedule_update_ha_state() def zdo_command(self, *args, **kwargs): """Handle ZDO commands on this cluster.""" @@ -202,6 +201,7 @@ class Switch(zha.Entity, BinarySensorDevice): def __init__(self, **kwargs): """Initialize Switch.""" + super().__init__(**kwargs) self._state = True self._level = 255 from zigpy.zcl.clusters import general @@ -209,7 +209,6 @@ class Switch(zha.Entity, BinarySensorDevice): general.OnOff.cluster_id: self.OnOffListener(self), general.LevelControl.cluster_id: self.LevelListener(self), } - super().__init__(**kwargs) @property def is_on(self) -> bool: @@ -227,20 +226,20 @@ class Switch(zha.Entity, BinarySensorDevice): self._level = 0 self._level = min(255, max(0, self._level + change)) self._state = bool(self._level) - self.schedule_update_ha_state() + self.async_schedule_update_ha_state() def set_level(self, level): """Set the level, setting state if appropriate.""" self._level = level self._state = bool(self._level) - self.schedule_update_ha_state() + self.async_schedule_update_ha_state() def set_state(self, state): """Set the state.""" self._state = state if self._level == 0: self._level = 255 - self.schedule_update_ha_state() + self.async_schedule_update_ha_state() async def async_update(self): """Retrieve latest state.""" diff --git a/homeassistant/components/sensor/zha.py b/homeassistant/components/sensor/zha.py index 36cdca2e638..d856ed1a17e 100644 --- a/homeassistant/components/sensor/zha.py +++ b/homeassistant/components/sensor/zha.py @@ -71,7 +71,7 @@ class Sensor(zha.Entity): _LOGGER.debug("Attribute updated: %s %s %s", self, attribute, value) if attribute == self.value_attribute: self._state = value - self.schedule_update_ha_state() + self.async_schedule_update_ha_state() class TemperatureSensor(Sensor): diff --git a/homeassistant/components/zha/__init__.py b/homeassistant/components/zha/__init__.py index dc9cb26462d..9b66c4c6ded 100644 --- a/homeassistant/components/zha/__init__.py +++ b/homeassistant/components/zha/__init__.py @@ -288,11 +288,6 @@ class Entity(entity.Entity): """A base class for ZHA entities.""" _domain = None # Must be overridden by subclasses - # Normally the entity itself is the listener. Base classes may set this to - # a dict of cluster ID -> listener to receive messages for specific - # clusters separately - _in_listeners = {} - _out_listeners = {} def __init__(self, endpoint, in_clusters, out_clusters, manufacturer, model, application_listener, unique_id, **kwargs): @@ -321,19 +316,30 @@ class Entity(entity.Entity): kwargs.get('entity_suffix', ''), ) - for cluster_id, cluster in in_clusters.items(): - cluster.add_listener(self._in_listeners.get(cluster_id, self)) - for cluster_id, cluster in out_clusters.items(): - cluster.add_listener(self._out_listeners.get(cluster_id, self)) - self._endpoint = endpoint self._in_clusters = in_clusters self._out_clusters = out_clusters self._state = ha_const.STATE_UNKNOWN self._unique_id = unique_id + # Normally the entity itself is the listener. Sub-classes may set this + # to a dict of cluster ID -> listener to receive messages for specific + # clusters separately + self._in_listeners = {} + self._out_listeners = {} + application_listener.register_entity(ieee, self) + async def async_added_to_hass(self): + """Callback once the entity is added to hass. + + It is now safe to update the entity state + """ + for cluster_id, cluster in self._in_clusters.items(): + cluster.add_listener(self._in_listeners.get(cluster_id, self)) + for cluster_id, cluster in self._out_clusters.items(): + cluster.add_listener(self._out_listeners.get(cluster_id, self)) + @property def unique_id(self) -> str: """Return a unique ID.""" From b994c10d7f35cbf5f35c2f65ac713b4fa3067d6a Mon Sep 17 00:00:00 2001 From: sander76 Date: Tue, 1 May 2018 17:01:13 +0200 Subject: [PATCH 079/155] HomematicIP cloud: Add logic to check accesspoint connection state (#14203) * Add logic to check accesspoint connection state * lint * changes as per @balloobs comments. * pylint fix --- homeassistant/components/homematicip_cloud.py | 60 ++++++++++++++++++- 1 file changed, 59 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/homematicip_cloud.py b/homeassistant/components/homematicip_cloud.py index 0ed9fe22e27..0b15d7a3dfe 100644 --- a/homeassistant/components/homematicip_cloud.py +++ b/homeassistant/components/homematicip_cloud.py @@ -7,13 +7,15 @@ https://home-assistant.io/components/homematicip_cloud/ import asyncio import logging + import voluptuous as vol +import homeassistant.helpers.config_validation as cv from homeassistant.const import EVENT_HOMEASSISTANT_STOP from homeassistant.helpers.aiohttp_client import async_get_clientsession -import homeassistant.helpers.config_validation as cv from homeassistant.helpers.discovery import async_load_platform from homeassistant.helpers.entity import Entity +from homeassistant.core import callback REQUIREMENTS = ['homematicip==0.9.2.4'] @@ -96,6 +98,7 @@ class HomematicipConnector: def __init__(self, hass, config, websession): """Initialize HomematicIP cloud connection.""" from homematicip.async.home import AsyncHome + self._hass = hass self._ws_close_requested = False self._retry_task = None @@ -106,6 +109,9 @@ class HomematicipConnector: self.home = AsyncHome(hass.loop, websession) self.home.set_auth_token(_authtoken) + self.home.on_update(self.async_update) + self._accesspoint_connected = True + hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, self.close()) async def init(self): @@ -113,6 +119,58 @@ class HomematicipConnector: await self.home.init(self._accesspoint) await self.home.get_current_state() + @callback + def async_update(self, *args, **kwargs): + """Async update the home device. + + Triggered when the hmip HOME_CHANGED event has fired. + There are several occasions for this event to happen. + We are only interested to check whether the access point + is still connected. If not, device state changes cannot + be forwarded to hass. So if access point is disconnected all devices + are set to unavailable. + """ + if not self.home.connected: + _LOGGER.error( + "HMIP access point has lost connection with the cloud") + self._accesspoint_connected = False + self.set_all_to_unavailable() + elif not self._accesspoint_connected: + # Explicitly getting an update as device states might have + # changed during access point disconnect.""" + + job = self._hass.async_add_job(self.get_state()) + job.add_done_callback(self.get_state_finished) + + async def get_state(self): + """Update hmip state and tell hass.""" + await self.home.get_current_state() + self.update_all() + + def get_state_finished(self, future): + """Execute when get_state coroutine has finished.""" + from homematicip.base.base_connection import HmipConnectionError + + try: + future.result() + except HmipConnectionError: + # Somehow connection could not recover. Will disconnect and + # so reconnect loop is taking over. + _LOGGER.error( + "updating state after himp access point reconnect failed.") + self._hass.async_add_job(self.home.disable_events()) + + def set_all_to_unavailable(self): + """Set all devices to unavailable and tell Hass.""" + for device in self.home.devices: + device.unreach = True + self.update_all() + + def update_all(self): + """Signal all devices to update their state.""" + for device in self.home.devices: + device.fire_update_event() + async def _handle_connection(self): """Handle websocket connection.""" from homematicip.base.base_connection import HmipConnectionError From cdd45e78783037dfa6449eb91a7dafb2c9ae0c44 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 1 May 2018 12:20:41 -0400 Subject: [PATCH 080/155] Foundation for users (#13968) * Add initial user foundation to Home Assistant * Address comments * Address comments * Allow non-ascii passwords * One more utf-8 hmac compare digest * Add new line --- homeassistant/auth.py | 505 ++++++++++++++++++ homeassistant/auth_providers/__init__.py | 1 + .../auth_providers/insecure_example.py | 116 ++++ homeassistant/components/auth/__init__.py | 344 ++++++++++++ homeassistant/components/auth/client.py | 63 +++ homeassistant/components/http/auth.py | 43 +- homeassistant/config.py | 11 +- homeassistant/config_entries.py | 6 +- homeassistant/const.py | 1 + homeassistant/data_entry_flow.py | 2 +- homeassistant/helpers/data_entry_flow.py | 70 ++- pylintrc | 4 + tests/auth_providers/__init__.py | 1 + tests/auth_providers/test_insecure_example.py | 89 +++ tests/common.py | 34 +- tests/components/auth/__init__.py | 38 ++ tests/components/auth/test_client.py | 70 +++ tests/components/auth/test_init.py | 53 ++ tests/components/auth/test_init_link_user.py | 150 ++++++ tests/components/auth/test_init_login_flow.py | 66 +++ tests/components/http/test_init.py | 7 +- tests/test_auth.py | 159 ++++++ 22 files changed, 1774 insertions(+), 59 deletions(-) create mode 100644 homeassistant/auth.py create mode 100644 homeassistant/auth_providers/__init__.py create mode 100644 homeassistant/auth_providers/insecure_example.py create mode 100644 homeassistant/components/auth/__init__.py create mode 100644 homeassistant/components/auth/client.py create mode 100644 tests/auth_providers/__init__.py create mode 100644 tests/auth_providers/test_insecure_example.py create mode 100644 tests/components/auth/__init__.py create mode 100644 tests/components/auth/test_client.py create mode 100644 tests/components/auth/test_init.py create mode 100644 tests/components/auth/test_init_link_user.py create mode 100644 tests/components/auth/test_init_login_flow.py create mode 100644 tests/test_auth.py diff --git a/homeassistant/auth.py b/homeassistant/auth.py new file mode 100644 index 00000000000..55de9309954 --- /dev/null +++ b/homeassistant/auth.py @@ -0,0 +1,505 @@ +"""Provide an authentication layer for Home Assistant.""" +import asyncio +import binascii +from collections import OrderedDict +from datetime import datetime, timedelta +import os +import importlib +import logging +import uuid + +import attr +import voluptuous as vol +from voluptuous.humanize import humanize_error + +from homeassistant import data_entry_flow, requirements +from homeassistant.core import callback +from homeassistant.const import CONF_TYPE, CONF_NAME, CONF_ID +from homeassistant.exceptions import HomeAssistantError +from homeassistant.util.decorator import Registry +from homeassistant.util import dt as dt_util + + +_LOGGER = logging.getLogger(__name__) + + +AUTH_PROVIDERS = Registry() + +AUTH_PROVIDER_SCHEMA = vol.Schema({ + vol.Required(CONF_TYPE): str, + vol.Optional(CONF_NAME): str, + # Specify ID if you have two auth providers for same type. + vol.Optional(CONF_ID): str, +}, extra=vol.ALLOW_EXTRA) + +ACCESS_TOKEN_EXPIRATION = timedelta(minutes=30) +DATA_REQS = 'auth_reqs_processed' + + +class AuthError(HomeAssistantError): + """Generic authentication error.""" + + +class InvalidUser(AuthError): + """Raised when an invalid user has been specified.""" + + +class InvalidPassword(AuthError): + """Raised when an invalid password has been supplied.""" + + +class UnknownError(AuthError): + """When an unknown error occurs.""" + + +def generate_secret(entropy=32): + """Generate a secret. + + Backport of secrets.token_hex from Python 3.6 + + Event loop friendly. + """ + return binascii.hexlify(os.urandom(entropy)).decode('ascii') + + +class AuthProvider: + """Provider of user authentication.""" + + DEFAULT_TITLE = 'Unnamed auth provider' + + initialized = False + + def __init__(self, store, config): + """Initialize an auth provider.""" + self.store = store + self.config = config + + @property + def id(self): # pylint: disable=invalid-name + """Return id of the auth provider. + + Optional, can be None. + """ + return self.config.get(CONF_ID) + + @property + def type(self): + """Return type of the provider.""" + return self.config[CONF_TYPE] + + @property + def name(self): + """Return the name of the auth provider.""" + return self.config.get(CONF_NAME, self.DEFAULT_TITLE) + + async def async_credentials(self): + """Return all credentials of this provider.""" + return await self.store.credentials_for_provider(self.type, self.id) + + @callback + def async_create_credentials(self, data): + """Create credentials.""" + return Credentials( + auth_provider_type=self.type, + auth_provider_id=self.id, + data=data, + ) + + # Implement by extending class + + async def async_initialize(self): + """Initialize the auth provider. + + Optional. + """ + + async def async_credential_flow(self): + """Return the data flow for logging in with auth provider.""" + raise NotImplementedError + + async def async_get_or_create_credentials(self, flow_result): + """Get credentials based on the flow result.""" + raise NotImplementedError + + async def async_user_meta_for_credentials(self, credentials): + """Return extra user metadata for credentials. + + Will be used to populate info when creating a new user. + """ + return {} + + +@attr.s(slots=True) +class User: + """A user.""" + + id = attr.ib(type=str, default=attr.Factory(lambda: uuid.uuid4().hex)) + is_owner = attr.ib(type=bool, default=False) + is_active = attr.ib(type=bool, default=False) + name = attr.ib(type=str, default=None) + # For persisting and see if saved? + # store = attr.ib(type=AuthStore, default=None) + + # List of credentials of a user. + credentials = attr.ib(type=list, default=attr.Factory(list)) + + # Tokens associated with a user. + refresh_tokens = attr.ib(type=dict, default=attr.Factory(dict)) + + def as_dict(self): + """Convert user object to a dictionary.""" + return { + 'id': self.id, + 'is_owner': self.is_owner, + 'is_active': self.is_active, + 'name': self.name, + } + + +@attr.s(slots=True) +class RefreshToken: + """RefreshToken for a user to grant new access tokens.""" + + user = attr.ib(type=User) + client_id = attr.ib(type=str) + id = attr.ib(type=str, default=attr.Factory(lambda: uuid.uuid4().hex)) + created_at = attr.ib(type=datetime, default=attr.Factory(dt_util.utcnow)) + access_token_expiration = attr.ib(type=timedelta, + default=ACCESS_TOKEN_EXPIRATION) + token = attr.ib(type=str, + default=attr.Factory(lambda: generate_secret(64))) + access_tokens = attr.ib(type=list, default=attr.Factory(list)) + + +@attr.s(slots=True) +class AccessToken: + """Access token to access the API. + + These will only ever be stored in memory and not be persisted. + """ + + refresh_token = attr.ib(type=RefreshToken) + created_at = attr.ib(type=datetime, default=attr.Factory(dt_util.utcnow)) + token = attr.ib(type=str, + default=attr.Factory(generate_secret)) + + @property + def expires(self): + """Return datetime when this token expires.""" + return self.created_at + self.refresh_token.access_token_expiration + + +@attr.s(slots=True) +class Credentials: + """Credentials for a user on an auth provider.""" + + auth_provider_type = attr.ib(type=str) + auth_provider_id = attr.ib(type=str) + + # Allow the auth provider to store data to represent their auth. + data = attr.ib(type=dict) + + id = attr.ib(type=str, default=attr.Factory(lambda: uuid.uuid4().hex)) + is_new = attr.ib(type=bool, default=True) + + +@attr.s(slots=True) +class Client: + """Client that interacts with Home Assistant on behalf of a user.""" + + name = attr.ib(type=str) + id = attr.ib(type=str, default=attr.Factory(lambda: uuid.uuid4().hex)) + secret = attr.ib(type=str, default=attr.Factory(generate_secret)) + + +async def load_auth_provider_module(hass, provider): + """Load an auth provider.""" + try: + module = importlib.import_module( + 'homeassistant.auth_providers.{}'.format(provider)) + except ImportError: + _LOGGER.warning('Unable to find auth provider %s', provider) + return None + + if hass.config.skip_pip or not hasattr(module, 'REQUIREMENTS'): + return module + + processed = hass.data.get(DATA_REQS) + + if processed is None: + processed = hass.data[DATA_REQS] = set() + elif provider in processed: + return module + + req_success = await requirements.async_process_requirements( + hass, 'auth provider {}'.format(provider), module.REQUIREMENTS) + + if not req_success: + return None + + return module + + +async def auth_manager_from_config(hass, provider_configs): + """Initialize an auth manager from config.""" + store = AuthStore(hass) + if provider_configs: + providers = await asyncio.gather( + *[_auth_provider_from_config(hass, store, config) + for config in provider_configs]) + else: + providers = [] + # So returned auth providers are in same order as config + provider_hash = OrderedDict() + for provider in providers: + if provider is None: + continue + + key = (provider.type, provider.id) + + if key in provider_hash: + _LOGGER.error( + 'Found duplicate provider: %s. Please add unique IDs if you ' + 'want to have the same provider twice.', key) + continue + + provider_hash[key] = provider + manager = AuthManager(hass, store, provider_hash) + return manager + + +async def _auth_provider_from_config(hass, store, config): + """Initialize an auth provider from a config.""" + provider_name = config[CONF_TYPE] + module = await load_auth_provider_module(hass, provider_name) + + if module is None: + return None + + try: + config = module.CONFIG_SCHEMA(config) + except vol.Invalid as err: + _LOGGER.error('Invalid configuration for auth provider %s: %s', + provider_name, humanize_error(config, err)) + return None + + return AUTH_PROVIDERS[provider_name](store, config) + + +class AuthManager: + """Manage the authentication for Home Assistant.""" + + def __init__(self, hass, store, providers): + """Initialize the auth manager.""" + self._store = store + self._providers = providers + self.login_flow = data_entry_flow.FlowManager( + hass, self._async_create_login_flow, + self._async_finish_login_flow) + self.access_tokens = {} + + @property + def async_auth_providers(self): + """Return a list of available auth providers.""" + return self._providers.values() + + async def async_get_user(self, user_id): + """Retrieve a user.""" + return await self._store.async_get_user(user_id) + + async def async_get_or_create_user(self, credentials): + """Get or create a user.""" + return await self._store.async_get_or_create_user( + credentials, self._async_get_auth_provider(credentials)) + + async def async_link_user(self, user, credentials): + """Link credentials to an existing user.""" + await self._store.async_link_user(user, credentials) + + async def async_remove_user(self, user): + """Remove a user.""" + await self._store.async_remove_user(user) + + async def async_create_refresh_token(self, user, client_id): + """Create a new refresh token for a user.""" + return await self._store.async_create_refresh_token(user, client_id) + + async def async_get_refresh_token(self, token): + """Get refresh token by token.""" + return await self._store.async_get_refresh_token(token) + + @callback + def async_create_access_token(self, refresh_token): + """Create a new access token.""" + access_token = AccessToken(refresh_token) + self.access_tokens[access_token.token] = access_token + return access_token + + @callback + def async_get_access_token(self, token): + """Get an access token.""" + return self.access_tokens.get(token) + + async def async_create_client(self, name): + """Create a new client.""" + return await self._store.async_create_client(name) + + async def async_get_client(self, client_id): + """Get a client.""" + return await self._store.async_get_client(client_id) + + async def _async_create_login_flow(self, handler, *, source, data): + """Create a login flow.""" + auth_provider = self._providers[handler] + + if not auth_provider.initialized: + auth_provider.initialized = True + await auth_provider.async_initialize() + + return await auth_provider.async_credential_flow() + + async def _async_finish_login_flow(self, result): + """Result of a credential login flow.""" + auth_provider = self._providers[result['handler']] + return await auth_provider.async_get_or_create_credentials( + result['data']) + + @callback + def _async_get_auth_provider(self, credentials): + """Helper to get auth provider from a set of credentials.""" + auth_provider_key = (credentials.auth_provider_type, + credentials.auth_provider_id) + return self._providers[auth_provider_key] + + +class AuthStore: + """Stores authentication info. + + Any mutation to an object should happen inside the auth store. + + The auth store is lazy. It won't load the data from disk until a method is + called that needs it. + """ + + def __init__(self, hass): + """Initialize the auth store.""" + self.hass = hass + self.users = None + self.clients = None + self._load_lock = asyncio.Lock(loop=hass.loop) + + async def credentials_for_provider(self, provider_type, provider_id): + """Return credentials for specific auth provider type and id.""" + if self.users is None: + await self.async_load() + + return [ + credentials + for user in self.users.values() + for credentials in user.credentials + if (credentials.auth_provider_type == provider_type and + credentials.auth_provider_id == provider_id) + ] + + async def async_get_user(self, user_id): + """Retrieve a user.""" + if self.users is None: + await self.async_load() + + return self.users.get(user_id) + + async def async_get_or_create_user(self, credentials, auth_provider): + """Get or create a new user for given credentials. + + If link_user is passed in, the credentials will be linked to the passed + in user if the credentials are new. + """ + if self.users is None: + await self.async_load() + + # New credentials, store in user + if credentials.is_new: + info = await auth_provider.async_user_meta_for_credentials( + credentials) + # Make owner and activate user if it's the first user. + if self.users: + is_owner = False + is_active = False + else: + is_owner = True + is_active = True + + new_user = User( + is_owner=is_owner, + is_active=is_active, + name=info.get('name'), + ) + self.users[new_user.id] = new_user + await self.async_link_user(new_user, credentials) + return new_user + + for user in self.users.values(): + for creds in user.credentials: + if (creds.auth_provider_type == credentials.auth_provider_type + and creds.auth_provider_id == + credentials.auth_provider_id): + return user + + raise ValueError('We got credentials with ID but found no user') + + async def async_link_user(self, user, credentials): + """Add credentials to an existing user.""" + user.credentials.append(credentials) + await self.async_save() + credentials.is_new = False + + async def async_remove_user(self, user): + """Remove a user.""" + self.users.pop(user.id) + await self.async_save() + + async def async_create_refresh_token(self, user, client_id): + """Create a new token for a user.""" + refresh_token = RefreshToken(user, client_id) + user.refresh_tokens[refresh_token.token] = refresh_token + await self.async_save() + return refresh_token + + async def async_get_refresh_token(self, token): + """Get refresh token by token.""" + if self.users is None: + await self.async_load() + + for user in self.users.values(): + refresh_token = user.refresh_tokens.get(token) + if refresh_token is not None: + return refresh_token + + return None + + async def async_create_client(self, name): + """Create a new client.""" + if self.clients is None: + await self.async_load() + + client = Client(name) + self.clients[client.id] = client + await self.async_save() + return client + + async def async_get_client(self, client_id): + """Get a client.""" + if self.clients is None: + await self.async_load() + + return self.clients.get(client_id) + + async def async_load(self): + """Load the users.""" + async with self._load_lock: + self.users = {} + self.clients = {} + + async def async_save(self): + """Save users.""" + pass diff --git a/homeassistant/auth_providers/__init__.py b/homeassistant/auth_providers/__init__.py new file mode 100644 index 00000000000..4705e7580ca --- /dev/null +++ b/homeassistant/auth_providers/__init__.py @@ -0,0 +1 @@ +"""Auth providers for Home Assistant.""" diff --git a/homeassistant/auth_providers/insecure_example.py b/homeassistant/auth_providers/insecure_example.py new file mode 100644 index 00000000000..8538e8c2f3e --- /dev/null +++ b/homeassistant/auth_providers/insecure_example.py @@ -0,0 +1,116 @@ +"""Example auth provider.""" +from collections import OrderedDict +import hmac + +import voluptuous as vol + +from homeassistant import auth, data_entry_flow +from homeassistant.core import callback + + +USER_SCHEMA = vol.Schema({ + vol.Required('username'): str, + vol.Required('password'): str, + vol.Optional('name'): str, +}) + + +CONFIG_SCHEMA = auth.AUTH_PROVIDER_SCHEMA.extend({ + vol.Required('users'): [USER_SCHEMA] +}, extra=vol.PREVENT_EXTRA) + + +@auth.AUTH_PROVIDERS.register('insecure_example') +class ExampleAuthProvider(auth.AuthProvider): + """Example auth provider based on hardcoded usernames and passwords.""" + + async def async_credential_flow(self): + """Return a flow to login.""" + return LoginFlow(self) + + @callback + def async_validate_login(self, username, password): + """Helper to validate a username and password.""" + user = None + + # Compare all users to avoid timing attacks. + for usr in self.config['users']: + if hmac.compare_digest(username.encode('utf-8'), + usr['username'].encode('utf-8')): + user = usr + + if user is None: + # Do one more compare to make timing the same as if user was found. + hmac.compare_digest(password.encode('utf-8'), + password.encode('utf-8')) + raise auth.InvalidUser + + if not hmac.compare_digest(user['password'].encode('utf-8'), + password.encode('utf-8')): + raise auth.InvalidPassword + + async def async_get_or_create_credentials(self, flow_result): + """Get credentials based on the flow result.""" + username = flow_result['username'] + password = flow_result['password'] + + self.async_validate_login(username, password) + + for credential in await self.async_credentials(): + if credential.data['username'] == username: + return credential + + # Create new credentials. + return self.async_create_credentials({ + 'username': username + }) + + async def async_user_meta_for_credentials(self, credentials): + """Return extra user metadata for credentials. + + Will be used to populate info when creating a new user. + """ + username = credentials.data['username'] + + for user in self.config['users']: + if user['username'] == username: + return { + 'name': user.get('name') + } + + return {} + + +class LoginFlow(data_entry_flow.FlowHandler): + """Handler for the login flow.""" + + def __init__(self, auth_provider): + """Initialize the login flow.""" + self._auth_provider = auth_provider + + async def async_step_init(self, user_input=None): + """Handle the step of the form.""" + errors = {} + + if user_input is not None: + try: + self._auth_provider.async_validate_login( + user_input['username'], user_input['password']) + except (auth.InvalidUser, auth.InvalidPassword): + errors['base'] = 'invalid_auth' + + if not errors: + return self.async_create_entry( + title=self._auth_provider.name, + data=user_input + ) + + schema = OrderedDict() + schema['username'] = str + schema['password'] = str + + return self.async_show_form( + step_id='init', + data_schema=vol.Schema(schema), + errors=errors, + ) diff --git a/homeassistant/components/auth/__init__.py b/homeassistant/components/auth/__init__.py new file mode 100644 index 00000000000..d4b4b0f4591 --- /dev/null +++ b/homeassistant/components/auth/__init__.py @@ -0,0 +1,344 @@ +"""Component to allow users to login and get tokens. + +All requests will require passing in a valid client ID and secret via HTTP +Basic Auth. + +# GET /auth/providers + +Return a list of auth providers. Example: + +[ + { + "name": "Local", + "id": null, + "type": "local_provider", + } +] + +# POST /auth/login_flow + +Create a login flow. Will return the first step of the flow. + +Pass in parameter 'handler' to specify the auth provider to use. Auth providers +are identified by type and id. + +{ + "handler": ["local_provider", null] +} + +Return value will be a step in a data entry flow. See the docs for data entry +flow for details. + +{ + "data_schema": [ + {"name": "username", "type": "string"}, + {"name": "password", "type": "string"} + ], + "errors": {}, + "flow_id": "8f7e42faab604bcab7ac43c44ca34d58", + "handler": ["insecure_example", null], + "step_id": "init", + "type": "form" +} + +# POST /auth/login_flow/{flow_id} + +Progress the flow. Most flows will be 1 page, but could optionally add extra +login challenges, like TFA. Once the flow has finished, the returned step will +have type "create_entry" and "result" key will contain an authorization code. + +{ + "flow_id": "8f7e42faab604bcab7ac43c44ca34d58", + "handler": ["insecure_example", null], + "result": "411ee2f916e648d691e937ae9344681e", + "source": "user", + "title": "Example", + "type": "create_entry", + "version": 1 +} + +# POST /auth/token + +This is an OAuth2 endpoint for granting tokens. We currently support the grant +types "authorization_code" and "refresh_token". Because we follow the OAuth2 +spec, data should be send in formatted as x-www-form-urlencoded. Examples will +be in JSON as it's more readable. + +## Grant type authorization_code + +Exchange the authorization code retrieved from the login flow for tokens. + +{ + "grant_type": "authorization_code", + "code": "411ee2f916e648d691e937ae9344681e" +} + +Return value will be the access and refresh tokens. The access token will have +a limited expiration. New access tokens can be requested using the refresh +token. + +{ + "access_token": "ABCDEFGH", + "expires_in": 1800, + "refresh_token": "IJKLMNOPQRST", + "token_type": "Bearer" +} + +## Grant type refresh_token + +Request a new access token using a refresh token. + +{ + "grant_type": "refresh_token", + "refresh_token": "IJKLMNOPQRST" +} + +Return value will be a new access token. The access token will have +a limited expiration. + +{ + "access_token": "ABCDEFGH", + "expires_in": 1800, + "token_type": "Bearer" +} +""" +import logging +import uuid + +import aiohttp.web +import voluptuous as vol + +from homeassistant import data_entry_flow +from homeassistant.core import callback +from homeassistant.helpers.data_entry_flow import ( + FlowManagerIndexView, FlowManagerResourceView) +from homeassistant.components.http.view import HomeAssistantView +from homeassistant.components.http.data_validator import RequestDataValidator + +from .client import verify_client + +DOMAIN = 'auth' +DEPENDENCIES = ['http'] +_LOGGER = logging.getLogger(__name__) + + +async def async_setup(hass, config): + """Component to allow users to login.""" + store_credentials, retrieve_credentials = _create_cred_store() + + hass.http.register_view(AuthProvidersView) + hass.http.register_view(LoginFlowIndexView(hass.auth.login_flow)) + hass.http.register_view( + LoginFlowResourceView(hass.auth.login_flow, store_credentials)) + hass.http.register_view(GrantTokenView(retrieve_credentials)) + hass.http.register_view(LinkUserView(retrieve_credentials)) + + return True + + +class AuthProvidersView(HomeAssistantView): + """View to get available auth providers.""" + + url = '/auth/providers' + name = 'api:auth:providers' + requires_auth = False + + @verify_client + async def get(self, request, client_id): + """Get available auth providers.""" + return self.json([{ + 'name': provider.name, + 'id': provider.id, + 'type': provider.type, + } for provider in request.app['hass'].auth.async_auth_providers]) + + +class LoginFlowIndexView(FlowManagerIndexView): + """View to create a config flow.""" + + url = '/auth/login_flow' + name = 'api:auth:login_flow' + requires_auth = False + + async def get(self, request): + """Do not allow index of flows in progress.""" + return aiohttp.web.Response(status=405) + + # pylint: disable=arguments-differ + @verify_client + async def post(self, request, client_id): + """Create a new login flow.""" + # pylint: disable=no-value-for-parameter + return await super().post(request) + + +class LoginFlowResourceView(FlowManagerResourceView): + """View to interact with the flow manager.""" + + url = '/auth/login_flow/{flow_id}' + name = 'api:auth:login_flow:resource' + requires_auth = False + + def __init__(self, flow_mgr, store_credentials): + """Initialize the login flow resource view.""" + super().__init__(flow_mgr) + self._store_credentials = store_credentials + + # pylint: disable=arguments-differ + async def get(self, request): + """Do not allow getting status of a flow in progress.""" + return self.json_message('Invalid flow specified', 404) + + # pylint: disable=arguments-differ + @verify_client + @RequestDataValidator(vol.Schema(dict), allow_empty=True) + async def post(self, request, client_id, flow_id, data): + """Handle progressing a login flow request.""" + try: + result = await self._flow_mgr.async_configure(flow_id, data) + except data_entry_flow.UnknownFlow: + return self.json_message('Invalid flow specified', 404) + except vol.Invalid: + return self.json_message('User input malformed', 400) + + if result['type'] != data_entry_flow.RESULT_TYPE_CREATE_ENTRY: + return self.json(self._prepare_result_json(result)) + + result.pop('data') + result['result'] = self._store_credentials(client_id, result['result']) + + return self.json(result) + + +class GrantTokenView(HomeAssistantView): + """View to grant tokens.""" + + url = '/auth/token' + name = 'api:auth:token' + requires_auth = False + + def __init__(self, retrieve_credentials): + """Initialize the grant token view.""" + self._retrieve_credentials = retrieve_credentials + + @verify_client + async def post(self, request, client_id): + """Grant a token.""" + hass = request.app['hass'] + data = await request.post() + grant_type = data.get('grant_type') + + if grant_type == 'authorization_code': + return await self._async_handle_auth_code( + hass, client_id, data) + + elif grant_type == 'refresh_token': + return await self._async_handle_refresh_token( + hass, client_id, data) + + return self.json({ + 'error': 'unsupported_grant_type', + }, status_code=400) + + async def _async_handle_auth_code(self, hass, client_id, data): + """Handle authorization code request.""" + code = data.get('code') + + if code is None: + return self.json({ + 'error': 'invalid_request', + }, status_code=400) + + credentials = self._retrieve_credentials(client_id, code) + + if credentials is None: + return self.json({ + 'error': 'invalid_request', + }, status_code=400) + + user = await hass.auth.async_get_or_create_user(credentials) + refresh_token = await hass.auth.async_create_refresh_token(user, + client_id) + access_token = hass.auth.async_create_access_token(refresh_token) + + return self.json({ + 'access_token': access_token.token, + 'token_type': 'Bearer', + 'refresh_token': refresh_token.token, + 'expires_in': + int(refresh_token.access_token_expiration.total_seconds()), + }) + + async def _async_handle_refresh_token(self, hass, client_id, data): + """Handle authorization code request.""" + token = data.get('refresh_token') + + if token is None: + return self.json({ + 'error': 'invalid_request', + }, status_code=400) + + refresh_token = await hass.auth.async_get_refresh_token(token) + + if refresh_token is None or refresh_token.client_id != client_id: + return self.json({ + 'error': 'invalid_grant', + }, status_code=400) + + access_token = hass.auth.async_create_access_token(refresh_token) + + return self.json({ + 'access_token': access_token.token, + 'token_type': 'Bearer', + 'expires_in': + int(refresh_token.access_token_expiration.total_seconds()), + }) + + +class LinkUserView(HomeAssistantView): + """View to link existing users to new credentials.""" + + url = '/auth/link_user' + name = 'api:auth:link_user' + + def __init__(self, retrieve_credentials): + """Initialize the link user view.""" + self._retrieve_credentials = retrieve_credentials + + @RequestDataValidator(vol.Schema({ + 'code': str, + 'client_id': str, + })) + async def post(self, request, data): + """Link a user.""" + hass = request.app['hass'] + user = request['hass_user'] + + credentials = self._retrieve_credentials( + data['client_id'], data['code']) + + if credentials is None: + return self.json_message('Invalid code', status_code=400) + + await hass.auth.async_link_user(user, credentials) + return self.json_message('User linked') + + +@callback +def _create_cred_store(): + """Create a credential store.""" + temp_credentials = {} + + @callback + def store_credentials(client_id, credentials): + """Store credentials and return a code to retrieve it.""" + code = uuid.uuid4().hex + temp_credentials[(client_id, code)] = credentials + return code + + @callback + def retrieve_credentials(client_id, code): + """Retrieve credentials.""" + return temp_credentials.pop((client_id, code), None) + + return store_credentials, retrieve_credentials diff --git a/homeassistant/components/auth/client.py b/homeassistant/components/auth/client.py new file mode 100644 index 00000000000..28d72aefe0f --- /dev/null +++ b/homeassistant/components/auth/client.py @@ -0,0 +1,63 @@ +"""Helpers to resolve client ID/secret.""" +import base64 +from functools import wraps +import hmac + +import aiohttp.hdrs + + +def verify_client(method): + """Decorator to verify client id/secret on requests.""" + @wraps(method) + async def wrapper(view, request, *args, **kwargs): + """Verify client id/secret before doing request.""" + client_id = await _verify_client(request) + + if client_id is None: + return view.json({ + 'error': 'invalid_client', + }, status_code=401) + + return await method( + view, request, *args, client_id=client_id, **kwargs) + + return wrapper + + +async def _verify_client(request): + """Method to verify the client id/secret in consistent time. + + By using a consistent time for looking up client id and comparing the + secret, we prevent attacks by malicious actors trying different client ids + and are able to derive from the time it takes to process the request if + they guessed the client id correctly. + """ + if aiohttp.hdrs.AUTHORIZATION not in request.headers: + return None + + auth_type, auth_value = \ + request.headers.get(aiohttp.hdrs.AUTHORIZATION).split(' ', 1) + + if auth_type != 'Basic': + return None + + decoded = base64.b64decode(auth_value).decode('utf-8') + try: + client_id, client_secret = decoded.split(':', 1) + except ValueError: + # If no ':' in decoded + return None + + client = await request.app['hass'].auth.async_get_client(client_id) + + if client is None: + # Still do a compare so we run same time as if a client was found. + hmac.compare_digest(client_secret.encode('utf-8'), + client_secret.encode('utf-8')) + return None + + if hmac.compare_digest(client_secret.encode('utf-8'), + client.secret.encode('utf-8')): + return client_id + + return None diff --git a/homeassistant/components/http/auth.py b/homeassistant/components/http/auth.py index 65c70c37bd2..5558063c5c4 100644 --- a/homeassistant/components/http/auth.py +++ b/homeassistant/components/http/auth.py @@ -32,17 +32,19 @@ def setup_auth(app, trusted_networks, api_password): if (HTTP_HEADER_HA_AUTH in request.headers and hmac.compare_digest( - api_password, request.headers[HTTP_HEADER_HA_AUTH])): + api_password.encode('utf-8'), + request.headers[HTTP_HEADER_HA_AUTH].encode('utf-8'))): # A valid auth header has been set authenticated = True elif (DATA_API_PASSWORD in request.query and - hmac.compare_digest(api_password, - request.query[DATA_API_PASSWORD])): + hmac.compare_digest( + api_password.encode('utf-8'), + request.query[DATA_API_PASSWORD].encode('utf-8'))): authenticated = True elif (hdrs.AUTHORIZATION in request.headers and - validate_authorization_header(api_password, request)): + await async_validate_auth_header(api_password, request)): authenticated = True elif _is_trusted_ip(request, trusted_networks): @@ -70,23 +72,38 @@ def _is_trusted_ip(request, trusted_networks): def validate_password(request, api_password): """Test if password is valid.""" return hmac.compare_digest( - api_password, request.app['hass'].http.api_password) + api_password.encode('utf-8'), + request.app['hass'].http.api_password.encode('utf-8')) -def validate_authorization_header(api_password, request): +async def async_validate_auth_header(api_password, request): """Test an authorization header if valid password.""" if hdrs.AUTHORIZATION not in request.headers: return False - auth_type, auth = request.headers.get(hdrs.AUTHORIZATION).split(' ', 1) + auth_type, auth_val = request.headers.get(hdrs.AUTHORIZATION).split(' ', 1) - if auth_type != 'Basic': + if auth_type == 'Basic': + decoded = base64.b64decode(auth_val).decode('utf-8') + try: + username, password = decoded.split(':', 1) + except ValueError: + # If no ':' in decoded + return False + + if username != 'homeassistant': + return False + + return hmac.compare_digest(api_password.encode('utf-8'), + password.encode('utf-8')) + + if auth_type != 'Bearer': return False - decoded = base64.b64decode(auth).decode('utf-8') - username, password = decoded.split(':', 1) - - if username != 'homeassistant': + hass = request.app['hass'] + access_token = hass.auth.async_get_access_token(auth_val) + if access_token is None: return False - return hmac.compare_digest(api_password, password) + request['hass_user'] = access_token.refresh_token.user + return True diff --git a/homeassistant/config.py b/homeassistant/config.py index 28936ae12e9..2c440485e49 100644 --- a/homeassistant/config.py +++ b/homeassistant/config.py @@ -12,13 +12,14 @@ from typing import Any, List, Tuple # NOQA import voluptuous as vol from voluptuous.humanize import humanize_error +from homeassistant import auth from homeassistant.const import ( ATTR_FRIENDLY_NAME, ATTR_HIDDEN, ATTR_ASSUMED_STATE, CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME, CONF_PACKAGES, CONF_UNIT_SYSTEM, CONF_TIME_ZONE, CONF_ELEVATION, CONF_UNIT_SYSTEM_METRIC, CONF_UNIT_SYSTEM_IMPERIAL, CONF_TEMPERATURE_UNIT, TEMP_CELSIUS, __version__, CONF_CUSTOMIZE, CONF_CUSTOMIZE_DOMAIN, CONF_CUSTOMIZE_GLOB, - CONF_WHITELIST_EXTERNAL_DIRS) + CONF_WHITELIST_EXTERNAL_DIRS, CONF_AUTH_PROVIDERS) from homeassistant.core import callback, DOMAIN as CONF_CORE from homeassistant.exceptions import HomeAssistantError from homeassistant.loader import get_component, get_platform @@ -157,6 +158,8 @@ CORE_CONFIG_SCHEMA = CUSTOMIZE_CONFIG_SCHEMA.extend({ # pylint: disable=no-value-for-parameter vol.All(cv.ensure_list, [vol.IsDir()]), vol.Optional(CONF_PACKAGES, default={}): PACKAGES_CONFIG_SCHEMA, + vol.Optional(CONF_AUTH_PROVIDERS): + vol.All(cv.ensure_list, [auth.AUTH_PROVIDER_SCHEMA]) }) @@ -394,6 +397,12 @@ async def async_process_ha_core_config(hass, config): This method is a coroutine. """ config = CORE_CONFIG_SCHEMA(config) + + # Only load auth during startup. + if not hasattr(hass, 'auth'): + hass.auth = await auth.auth_manager_from_config( + hass, config.get(CONF_AUTH_PROVIDERS, [])) + hac = hass.config def set_time_zone(time_zone_str): diff --git a/homeassistant/config_entries.py b/homeassistant/config_entries.py index c23d53f2735..1350cd7d76a 100644 --- a/homeassistant/config_entries.py +++ b/homeassistant/config_entries.py @@ -260,7 +260,7 @@ class ConfigEntries: """Initialize the entry manager.""" self.hass = hass self.flow = data_entry_flow.FlowManager( - hass, self._async_create_flow, self._async_save_entry) + hass, self._async_create_flow, self._async_finish_flow) self._hass_config = hass_config self._entries = None self._sched_save = None @@ -345,8 +345,8 @@ class ConfigEntries: return await entry.async_unload( self.hass, component=getattr(self.hass.components, component)) - async def _async_save_entry(self, result): - """Add an entry.""" + async def _async_finish_flow(self, result): + """Finish a config flow and add an entry.""" entry = ConfigEntry( version=result['version'], domain=result['handler'], diff --git a/homeassistant/const.py b/homeassistant/const.py index 43380d00a2d..2e96e2f29c0 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -30,6 +30,7 @@ CONF_API_KEY = 'api_key' CONF_API_VERSION = 'api_version' CONF_AT = 'at' CONF_AUTHENTICATION = 'authentication' +CONF_AUTH_PROVIDERS = 'auth_providers' CONF_BASE = 'base' CONF_BEFORE = 'before' CONF_BELOW = 'below' diff --git a/homeassistant/data_entry_flow.py b/homeassistant/data_entry_flow.py index 8eb18a3a7e7..e9580aba273 100644 --- a/homeassistant/data_entry_flow.py +++ b/homeassistant/data_entry_flow.py @@ -67,7 +67,7 @@ class FlowManager: return await self._async_handle_step(flow, step, data) async def async_configure(self, flow_id, user_input=None): - """Start or continue a configuration flow.""" + """Continue a configuration flow.""" flow = self._progress.get(flow_id) if flow is None: diff --git a/homeassistant/helpers/data_entry_flow.py b/homeassistant/helpers/data_entry_flow.py index a8aca2fd2e9..913e90a859d 100644 --- a/homeassistant/helpers/data_entry_flow.py +++ b/homeassistant/helpers/data_entry_flow.py @@ -7,40 +7,40 @@ from homeassistant.components.http import HomeAssistantView from homeassistant.components.http.data_validator import RequestDataValidator -def _prepare_json(result): - """Convert result for JSON.""" - if result['type'] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY: - data = result.copy() - data.pop('result') - data.pop('data') - return data - - elif result['type'] != data_entry_flow.RESULT_TYPE_FORM: - return result - - import voluptuous_serialize - - data = result.copy() - - schema = data['data_schema'] - if schema is None: - data['data_schema'] = [] - else: - data['data_schema'] = voluptuous_serialize.convert(schema) - - return data - - -class FlowManagerIndexView(HomeAssistantView): - """View to create config flows.""" +class _BaseFlowManagerView(HomeAssistantView): + """Foundation for flow manager views.""" def __init__(self, flow_mgr): """Initialize the flow manager index view.""" self._flow_mgr = flow_mgr - async def get(self, request): - """List flows that are in progress.""" - return self.json(self._flow_mgr.async_progress()) + # pylint: disable=no-self-use + def _prepare_result_json(self, result): + """Convert result to JSON.""" + if result['type'] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY: + data = result.copy() + data.pop('result') + data.pop('data') + return data + + elif result['type'] != data_entry_flow.RESULT_TYPE_FORM: + return result + + import voluptuous_serialize + + data = result.copy() + + schema = data['data_schema'] + if schema is None: + data['data_schema'] = [] + else: + data['data_schema'] = voluptuous_serialize.convert(schema) + + return data + + +class FlowManagerIndexView(_BaseFlowManagerView): + """View to create config flows.""" @RequestDataValidator(vol.Schema({ vol.Required('handler'): vol.Any(str, list), @@ -59,18 +59,14 @@ class FlowManagerIndexView(HomeAssistantView): except data_entry_flow.UnknownStep: return self.json_message('Handler does not support init', 400) - result = _prepare_json(result) + result = self._prepare_result_json(result) return self.json(result) -class FlowManagerResourceView(HomeAssistantView): +class FlowManagerResourceView(_BaseFlowManagerView): """View to interact with the flow manager.""" - def __init__(self, flow_mgr): - """Initialize the flow manager resource view.""" - self._flow_mgr = flow_mgr - async def get(self, request, flow_id): """Get the current state of a data_entry_flow.""" try: @@ -78,7 +74,7 @@ class FlowManagerResourceView(HomeAssistantView): except data_entry_flow.UnknownFlow: return self.json_message('Invalid flow specified', 404) - result = _prepare_json(result) + result = self._prepare_result_json(result) return self.json(result) @@ -92,7 +88,7 @@ class FlowManagerResourceView(HomeAssistantView): except vol.Invalid: return self.json_message('User input malformed', 400) - result = _prepare_json(result) + result = self._prepare_result_json(result) return self.json(result) diff --git a/pylintrc b/pylintrc index 85a44782af1..df839b379b5 100644 --- a/pylintrc +++ b/pylintrc @@ -41,3 +41,7 @@ disable= [EXCEPTIONS] overgeneral-exceptions=Exception,HomeAssistantError + +# For attrs +[typecheck] +ignored-classes=_CountingAttr diff --git a/tests/auth_providers/__init__.py b/tests/auth_providers/__init__.py new file mode 100644 index 00000000000..dd1b58639b1 --- /dev/null +++ b/tests/auth_providers/__init__.py @@ -0,0 +1 @@ +"""Tests for the auth providers.""" diff --git a/tests/auth_providers/test_insecure_example.py b/tests/auth_providers/test_insecure_example.py new file mode 100644 index 00000000000..92fc2974e27 --- /dev/null +++ b/tests/auth_providers/test_insecure_example.py @@ -0,0 +1,89 @@ +"""Tests for the insecure example auth provider.""" +from unittest.mock import Mock +import uuid + +import pytest + +from homeassistant import auth +from homeassistant.auth_providers import insecure_example + +from tests.common import mock_coro + + +@pytest.fixture +def store(): + """Mock store.""" + return auth.AuthStore(Mock()) + + +@pytest.fixture +def provider(store): + """Mock provider.""" + return insecure_example.ExampleAuthProvider(store, { + 'type': 'insecure_example', + 'users': [ + { + 'username': 'user-test', + 'password': 'password-test', + }, + { + 'username': '🎉', + 'password': '😎', + } + ] + }) + + +async def test_create_new_credential(provider): + """Test that we create a new credential.""" + credentials = await provider.async_get_or_create_credentials({ + 'username': 'user-test', + 'password': 'password-test', + }) + assert credentials.is_new is True + + +async def test_match_existing_credentials(store, provider): + """See if we match existing users.""" + existing = auth.Credentials( + id=uuid.uuid4(), + auth_provider_type='insecure_example', + auth_provider_id=None, + data={ + 'username': 'user-test' + }, + is_new=False, + ) + store.credentials_for_provider = Mock(return_value=mock_coro([existing])) + credentials = await provider.async_get_or_create_credentials({ + 'username': 'user-test', + 'password': 'password-test', + }) + assert credentials is existing + + +async def test_verify_username(provider): + """Test we raise if incorrect user specified.""" + with pytest.raises(auth.InvalidUser): + await provider.async_get_or_create_credentials({ + 'username': 'non-existing-user', + 'password': 'password-test', + }) + + +async def test_verify_password(provider): + """Test we raise if incorrect user specified.""" + with pytest.raises(auth.InvalidPassword): + await provider.async_get_or_create_credentials({ + 'username': 'user-test', + 'password': 'incorrect-password', + }) + + +async def test_utf_8_username_password(provider): + """Test that we create a new credential.""" + credentials = await provider.async_get_or_create_credentials({ + 'username': '🎉', + 'password': '😎', + }) + assert credentials.is_new is True diff --git a/tests/common.py b/tests/common.py index 67fd8bab23f..b04abda7c28 100644 --- a/tests/common.py +++ b/tests/common.py @@ -10,7 +10,8 @@ import logging import threading from contextlib import contextmanager -from homeassistant import core as ha, loader, data_entry_flow, config_entries +from homeassistant import ( + auth, core as ha, loader, data_entry_flow, config_entries) from homeassistant.setup import setup_component, async_setup_component from homeassistant.config import async_process_component_config from homeassistant.helpers import ( @@ -113,6 +114,9 @@ def async_test_home_assistant(loop): hass.config_entries = config_entries.ConfigEntries(hass, {}) hass.config_entries._entries = [] hass.config.async_load = Mock() + store = auth.AuthStore(hass) + hass.auth = auth.AuthManager(hass, store, {}) + ensure_auth_manager_loaded(hass.auth) INSTANCES.append(hass) orig_async_add_job = hass.async_add_job @@ -303,6 +307,34 @@ def mock_registry(hass, mock_entries=None): return registry +class MockUser(auth.User): + """Mock a user in Home Assistant.""" + + def __init__(self, id='mock-id', is_owner=True, is_active=True, + name='Mock User'): + """Initialize mock user.""" + super().__init__(id, is_owner, is_active, name) + + def add_to_hass(self, hass): + """Test helper to add entry to hass.""" + return self.add_to_auth_manager(hass.auth) + + def add_to_auth_manager(self, auth_mgr): + """Test helper to add entry to hass.""" + auth_mgr._store.users[self.id] = self + return self + + +@ha.callback +def ensure_auth_manager_loaded(auth_mgr): + """Ensure an auth manager is considered loaded.""" + store = auth_mgr._store + if store.clients is None: + store.clients = {} + if store.users is None: + store.users = {} + + class MockModule(object): """Representation of a fake module.""" diff --git a/tests/components/auth/__init__.py b/tests/components/auth/__init__.py new file mode 100644 index 00000000000..3e5a59e8386 --- /dev/null +++ b/tests/components/auth/__init__.py @@ -0,0 +1,38 @@ +"""Tests for the auth component.""" +from aiohttp.helpers import BasicAuth + +from homeassistant import auth +from homeassistant.setup import async_setup_component + +from tests.common import ensure_auth_manager_loaded + + +BASE_CONFIG = [{ + 'name': 'Example', + 'type': 'insecure_example', + 'users': [{ + 'username': 'test-user', + 'password': 'test-pass', + 'name': 'Test Name' + }] +}] +CLIENT_ID = 'test-id' +CLIENT_SECRET = 'test-secret' +CLIENT_AUTH = BasicAuth(CLIENT_ID, CLIENT_SECRET) + + +async def async_setup_auth(hass, aiohttp_client, provider_configs=BASE_CONFIG, + setup_api=False): + """Helper to setup authentication and create a HTTP client.""" + hass.auth = await auth.auth_manager_from_config(hass, provider_configs) + ensure_auth_manager_loaded(hass.auth) + await async_setup_component(hass, 'auth', { + 'http': { + 'api_password': 'bla' + } + }) + client = auth.Client('Test Client', CLIENT_ID, CLIENT_SECRET) + hass.auth._store.clients[client.id] = client + if setup_api: + await async_setup_component(hass, 'api', {}) + return await aiohttp_client(hass.http.app) diff --git a/tests/components/auth/test_client.py b/tests/components/auth/test_client.py new file mode 100644 index 00000000000..2995a6ac81a --- /dev/null +++ b/tests/components/auth/test_client.py @@ -0,0 +1,70 @@ +"""Tests for the client validator.""" +from aiohttp.helpers import BasicAuth +import pytest + +from homeassistant.setup import async_setup_component +from homeassistant.components.auth.client import verify_client +from homeassistant.components.http.view import HomeAssistantView + +from . import async_setup_auth + + +@pytest.fixture +def mock_view(hass): + """Register a view that verifies client id/secret.""" + hass.loop.run_until_complete(async_setup_component(hass, 'http', {})) + + clients = [] + + class ClientView(HomeAssistantView): + url = '/' + name = 'bla' + + @verify_client + async def get(self, request, client_id): + """Handle GET request.""" + clients.append(client_id) + + hass.http.register_view(ClientView) + return clients + + +async def test_verify_client(hass, aiohttp_client, mock_view): + """Test that verify client can extract client auth from a request.""" + http_client = await async_setup_auth(hass, aiohttp_client) + client = await hass.auth.async_create_client('Hello') + + resp = await http_client.get('/', auth=BasicAuth(client.id, client.secret)) + assert resp.status == 200 + assert mock_view == [client.id] + + +async def test_verify_client_no_auth_header(hass, aiohttp_client, mock_view): + """Test that verify client will decline unknown client id.""" + http_client = await async_setup_auth(hass, aiohttp_client) + + resp = await http_client.get('/') + assert resp.status == 401 + assert mock_view == [] + + +async def test_verify_client_invalid_client_id(hass, aiohttp_client, + mock_view): + """Test that verify client will decline unknown client id.""" + http_client = await async_setup_auth(hass, aiohttp_client) + client = await hass.auth.async_create_client('Hello') + + resp = await http_client.get('/', auth=BasicAuth('invalid', client.secret)) + assert resp.status == 401 + assert mock_view == [] + + +async def test_verify_client_invalid_client_secret(hass, aiohttp_client, + mock_view): + """Test that verify client will decline incorrect client secret.""" + http_client = await async_setup_auth(hass, aiohttp_client) + client = await hass.auth.async_create_client('Hello') + + resp = await http_client.get('/', auth=BasicAuth(client.id, 'invalid')) + assert resp.status == 401 + assert mock_view == [] diff --git a/tests/components/auth/test_init.py b/tests/components/auth/test_init.py new file mode 100644 index 00000000000..5d9bf6b98cc --- /dev/null +++ b/tests/components/auth/test_init.py @@ -0,0 +1,53 @@ +"""Integration tests for the auth component.""" +from . import async_setup_auth, CLIENT_AUTH + + +async def test_login_new_user_and_refresh_token(hass, aiohttp_client): + """Test logging in with new user and refreshing tokens.""" + client = await async_setup_auth(hass, aiohttp_client, setup_api=True) + resp = await client.post('/auth/login_flow', json={ + 'handler': ['insecure_example', None] + }, auth=CLIENT_AUTH) + assert resp.status == 200 + step = await resp.json() + + resp = await client.post( + '/auth/login_flow/{}'.format(step['flow_id']), json={ + 'username': 'test-user', + 'password': 'test-pass', + }, auth=CLIENT_AUTH) + + assert resp.status == 200 + step = await resp.json() + code = step['result'] + + # Exchange code for tokens + resp = await client.post('/auth/token', data={ + 'grant_type': 'authorization_code', + 'code': code + }, auth=CLIENT_AUTH) + + assert resp.status == 200 + tokens = await resp.json() + + assert hass.auth.async_get_access_token(tokens['access_token']) is not None + + # Use refresh token to get more tokens. + resp = await client.post('/auth/token', data={ + 'grant_type': 'refresh_token', + 'refresh_token': tokens['refresh_token'] + }, auth=CLIENT_AUTH) + + assert resp.status == 200 + tokens = await resp.json() + assert 'refresh_token' not in tokens + assert hass.auth.async_get_access_token(tokens['access_token']) is not None + + # Test using access token to hit API. + resp = await client.get('/api/') + assert resp.status == 401 + + resp = await client.get('/api/', headers={ + 'authorization': 'Bearer {}'.format(tokens['access_token']) + }) + assert resp.status == 200 diff --git a/tests/components/auth/test_init_link_user.py b/tests/components/auth/test_init_link_user.py new file mode 100644 index 00000000000..44695bce202 --- /dev/null +++ b/tests/components/auth/test_init_link_user.py @@ -0,0 +1,150 @@ +"""Tests for the link user flow.""" +from . import async_setup_auth, CLIENT_AUTH, CLIENT_ID + + +async def async_get_code(hass, aiohttp_client): + """Helper for link user tests that returns authorization code.""" + config = [{ + 'name': 'Example', + 'type': 'insecure_example', + 'users': [{ + 'username': 'test-user', + 'password': 'test-pass', + 'name': 'Test Name' + }] + }, { + 'name': 'Example', + 'id': '2nd auth', + 'type': 'insecure_example', + 'users': [{ + 'username': '2nd-user', + 'password': '2nd-pass', + 'name': '2nd Name' + }] + }] + client = await async_setup_auth(hass, aiohttp_client, config) + + resp = await client.post('/auth/login_flow', json={ + 'handler': ['insecure_example', None] + }, auth=CLIENT_AUTH) + assert resp.status == 200 + step = await resp.json() + + resp = await client.post( + '/auth/login_flow/{}'.format(step['flow_id']), json={ + 'username': 'test-user', + 'password': 'test-pass', + }, auth=CLIENT_AUTH) + + assert resp.status == 200 + step = await resp.json() + code = step['result'] + + # Exchange code for tokens + resp = await client.post('/auth/token', data={ + 'grant_type': 'authorization_code', + 'code': code + }, auth=CLIENT_AUTH) + + assert resp.status == 200 + tokens = await resp.json() + + access_token = hass.auth.async_get_access_token(tokens['access_token']) + assert access_token is not None + user = access_token.refresh_token.user + assert len(user.credentials) == 1 + + # Now authenticate with the 2nd flow + resp = await client.post('/auth/login_flow', json={ + 'handler': ['insecure_example', '2nd auth'] + }, auth=CLIENT_AUTH) + assert resp.status == 200 + step = await resp.json() + + resp = await client.post( + '/auth/login_flow/{}'.format(step['flow_id']), json={ + 'username': '2nd-user', + 'password': '2nd-pass', + }, auth=CLIENT_AUTH) + + assert resp.status == 200 + step = await resp.json() + + return { + 'user': user, + 'code': step['result'], + 'client': client, + 'tokens': tokens, + } + + +async def test_link_user(hass, aiohttp_client): + """Test linking a user to new credentials.""" + info = await async_get_code(hass, aiohttp_client) + client = info['client'] + code = info['code'] + tokens = info['tokens'] + + # Link user + resp = await client.post('/auth/link_user', json={ + 'client_id': CLIENT_ID, + 'code': code + }, headers={ + 'authorization': 'Bearer {}'.format(tokens['access_token']) + }) + + assert resp.status == 200 + assert len(info['user'].credentials) == 2 + + +async def test_link_user_invalid_client_id(hass, aiohttp_client): + """Test linking a user to new credentials.""" + info = await async_get_code(hass, aiohttp_client) + client = info['client'] + code = info['code'] + tokens = info['tokens'] + + # Link user + resp = await client.post('/auth/link_user', json={ + 'client_id': 'invalid', + 'code': code + }, headers={ + 'authorization': 'Bearer {}'.format(tokens['access_token']) + }) + + assert resp.status == 400 + assert len(info['user'].credentials) == 1 + + +async def test_link_user_invalid_code(hass, aiohttp_client): + """Test linking a user to new credentials.""" + info = await async_get_code(hass, aiohttp_client) + client = info['client'] + tokens = info['tokens'] + + # Link user + resp = await client.post('/auth/link_user', json={ + 'client_id': CLIENT_ID, + 'code': 'invalid' + }, headers={ + 'authorization': 'Bearer {}'.format(tokens['access_token']) + }) + + assert resp.status == 400 + assert len(info['user'].credentials) == 1 + + +async def test_link_user_invalid_auth(hass, aiohttp_client): + """Test linking a user to new credentials.""" + info = await async_get_code(hass, aiohttp_client) + client = info['client'] + code = info['code'] + + # Link user + resp = await client.post('/auth/link_user', json={ + 'client_id': CLIENT_ID, + 'code': code, + }, headers={'authorization': 'Bearer invalid'}) + + assert resp.status == 401 + assert len(info['user'].credentials) == 1 diff --git a/tests/components/auth/test_init_login_flow.py b/tests/components/auth/test_init_login_flow.py new file mode 100644 index 00000000000..96fece6506b --- /dev/null +++ b/tests/components/auth/test_init_login_flow.py @@ -0,0 +1,66 @@ +"""Tests for the login flow.""" +from aiohttp.helpers import BasicAuth + +from . import async_setup_auth, CLIENT_AUTH + + +async def test_fetch_auth_providers(hass, aiohttp_client): + """Test fetching auth providers.""" + client = await async_setup_auth(hass, aiohttp_client) + resp = await client.get('/auth/providers', auth=CLIENT_AUTH) + assert await resp.json() == [{ + 'name': 'Example', + 'type': 'insecure_example', + 'id': None + }] + + +async def test_fetch_auth_providers_require_valid_client(hass, aiohttp_client): + """Test fetching auth providers.""" + client = await async_setup_auth(hass, aiohttp_client) + resp = await client.get('/auth/providers', + auth=BasicAuth('invalid', 'bla')) + assert resp.status == 401 + + +async def test_cannot_get_flows_in_progress(hass, aiohttp_client): + """Test we cannot get flows in progress.""" + client = await async_setup_auth(hass, aiohttp_client, []) + resp = await client.get('/auth/login_flow') + assert resp.status == 405 + + +async def test_invalid_username_password(hass, aiohttp_client): + """Test we cannot get flows in progress.""" + client = await async_setup_auth(hass, aiohttp_client) + resp = await client.post('/auth/login_flow', json={ + 'handler': ['insecure_example', None] + }, auth=CLIENT_AUTH) + assert resp.status == 200 + step = await resp.json() + + # Incorrect username + resp = await client.post( + '/auth/login_flow/{}'.format(step['flow_id']), json={ + 'username': 'wrong-user', + 'password': 'test-pass', + }, auth=CLIENT_AUTH) + + assert resp.status == 200 + step = await resp.json() + + assert step['step_id'] == 'init' + assert step['errors']['base'] == 'invalid_auth' + + # Incorrect password + resp = await client.post( + '/auth/login_flow/{}'.format(step['flow_id']), json={ + 'username': 'test-user', + 'password': 'wrong-pass', + }, auth=CLIENT_AUTH) + + assert resp.status == 200 + step = await resp.json() + + assert step['step_id'] == 'init' + assert step['errors']['base'] == 'invalid_auth' diff --git a/tests/components/http/test_init.py b/tests/components/http/test_init.py index c02e203444f..d5368032a37 100644 --- a/tests/components/http/test_init.py +++ b/tests/components/http/test_init.py @@ -1,4 +1,6 @@ """The tests for the Home Assistant HTTP component.""" +import logging + from homeassistant.setup import async_setup_component import homeassistant.components.http as http @@ -76,14 +78,13 @@ async def test_api_no_base_url(hass): async def test_not_log_password(hass, aiohttp_client, caplog): """Test access with password doesn't get logged.""" - result = await async_setup_component(hass, 'api', { + assert await async_setup_component(hass, 'api', { 'http': { http.CONF_API_PASSWORD: 'some-pass' } }) - assert result - client = await aiohttp_client(hass.http.app) + logging.getLogger('aiohttp.access').setLevel(logging.INFO) resp = await client.get('/api/', params={ 'api_password': 'some-pass' diff --git a/tests/test_auth.py b/tests/test_auth.py new file mode 100644 index 00000000000..4bbf218fd23 --- /dev/null +++ b/tests/test_auth.py @@ -0,0 +1,159 @@ +"""Tests for the Home Assistant auth module.""" +from unittest.mock import Mock + +import pytest + +from homeassistant import auth, data_entry_flow +from tests.common import MockUser, ensure_auth_manager_loaded + + +@pytest.fixture +def mock_hass(): + """Hass mock with minimum amount of data set to make it work with auth.""" + hass = Mock() + hass.config.skip_pip = True + return hass + + +async def test_auth_manager_from_config_validates_config_and_id(mock_hass): + """Test get auth providers.""" + manager = await auth.auth_manager_from_config(mock_hass, [{ + 'name': 'Test Name', + 'type': 'insecure_example', + 'users': [], + }, { + 'name': 'Invalid config because no users', + 'type': 'insecure_example', + 'id': 'invalid_config', + }, { + 'name': 'Test Name 2', + 'type': 'insecure_example', + 'id': 'another', + 'users': [], + }, { + 'name': 'Wrong because duplicate ID', + 'type': 'insecure_example', + 'id': 'another', + 'users': [], + }]) + + providers = [{ + 'name': provider.name, + 'id': provider.id, + 'type': provider.type, + } for provider in manager.async_auth_providers] + assert providers == [{ + 'name': 'Test Name', + 'type': 'insecure_example', + 'id': None, + }, { + 'name': 'Test Name 2', + 'type': 'insecure_example', + 'id': 'another', + }] + + +async def test_create_new_user(mock_hass): + """Test creating new user.""" + manager = await auth.auth_manager_from_config(mock_hass, [{ + 'type': 'insecure_example', + 'users': [{ + 'username': 'test-user', + 'password': 'test-pass', + 'name': 'Test Name' + }] + }]) + + step = await manager.login_flow.async_init(('insecure_example', None)) + assert step['type'] == data_entry_flow.RESULT_TYPE_FORM + + step = await manager.login_flow.async_configure(step['flow_id'], { + 'username': 'test-user', + 'password': 'test-pass', + }) + assert step['type'] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + credentials = step['result'] + user = await manager.async_get_or_create_user(credentials) + assert user is not None + assert user.is_owner is True + assert user.name == 'Test Name' + + +async def test_login_as_existing_user(mock_hass): + """Test login as existing user.""" + manager = await auth.auth_manager_from_config(mock_hass, [{ + 'type': 'insecure_example', + 'users': [{ + 'username': 'test-user', + 'password': 'test-pass', + 'name': 'Test Name' + }] + }]) + ensure_auth_manager_loaded(manager) + + # Add fake user with credentials for example auth provider. + user = MockUser( + id='mock-user', + is_owner=False, + is_active=False, + name='Paulus', + ).add_to_auth_manager(manager) + user.credentials.append(auth.Credentials( + id='mock-id', + auth_provider_type='insecure_example', + auth_provider_id=None, + data={'username': 'test-user'}, + is_new=False, + )) + + step = await manager.login_flow.async_init(('insecure_example', None)) + assert step['type'] == data_entry_flow.RESULT_TYPE_FORM + + step = await manager.login_flow.async_configure(step['flow_id'], { + 'username': 'test-user', + 'password': 'test-pass', + }) + assert step['type'] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + credentials = step['result'] + + user = await manager.async_get_or_create_user(credentials) + assert user is not None + assert user.id == 'mock-user' + assert user.is_owner is False + assert user.is_active is False + assert user.name == 'Paulus' + + +async def test_linking_user_to_two_auth_providers(mock_hass): + """Test linking user to two auth providers.""" + manager = await auth.auth_manager_from_config(mock_hass, [{ + 'type': 'insecure_example', + 'users': [{ + 'username': 'test-user', + 'password': 'test-pass', + }] + }, { + 'type': 'insecure_example', + 'id': 'another-provider', + 'users': [{ + 'username': 'another-user', + 'password': 'another-password', + }] + }]) + + step = await manager.login_flow.async_init(('insecure_example', None)) + step = await manager.login_flow.async_configure(step['flow_id'], { + 'username': 'test-user', + 'password': 'test-pass', + }) + user = await manager.async_get_or_create_user(step['result']) + assert user is not None + + step = await manager.login_flow.async_init(('insecure_example', + 'another-provider')) + step = await manager.login_flow.async_configure(step['flow_id'], { + 'username': 'another-user', + 'password': 'another-password', + }) + await manager.async_link_user(user, step['result']) + assert len(user.credentials) == 2 From d82693b4606360afaacfa6a995d94121d4b1cc97 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 1 May 2018 13:35:23 -0400 Subject: [PATCH 081/155] Allow easy extension of websocket API (#14186) * Allow easy extension of websocket API * Lint * Move panel test to frontend * Register websocket commands * Simplify test * Lint --- homeassistant/components/frontend/__init__.py | 24 +- homeassistant/components/websocket_api.py | 231 +++++++++--------- tests/components/conftest.py | 22 ++ tests/components/test_frontend.py | 24 ++ tests/components/test_websocket_api.py | 56 ++--- 5 files changed, 203 insertions(+), 154 deletions(-) create mode 100644 tests/components/conftest.py diff --git a/homeassistant/components/frontend/__init__.py b/homeassistant/components/frontend/__init__.py index 4a181c00c02..564ba286b96 100644 --- a/homeassistant/components/frontend/__init__.py +++ b/homeassistant/components/frontend/__init__.py @@ -16,8 +16,9 @@ import voluptuous as vol import jinja2 import homeassistant.helpers.config_validation as cv -from homeassistant.components.http import HomeAssistantView +from homeassistant.components.http.view import HomeAssistantView from homeassistant.components.http.const import KEY_AUTHENTICATED +from homeassistant.components import websocket_api from homeassistant.config import find_config_file, load_yaml_config_file from homeassistant.const import CONF_NAME, EVENT_THEMES_UPDATED from homeassistant.core import callback @@ -94,6 +95,10 @@ SERVICE_RELOAD_THEMES = 'reload_themes' SERVICE_SET_THEME_SCHEMA = vol.Schema({ vol.Required(CONF_NAME): cv.string, }) +WS_TYPE_GET_PANELS = 'get_panels' +SCHEMA_GET_PANELS = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend({ + vol.Required('type'): WS_TYPE_GET_PANELS, +}) class AbstractPanel: @@ -291,6 +296,8 @@ def add_manifest_json_key(key, val): @asyncio.coroutine def async_setup(hass, config): """Set up the serving of the frontend.""" + hass.components.websocket_api.async_register_command( + WS_TYPE_GET_PANELS, websocket_handle_get_panels, SCHEMA_GET_PANELS) hass.http.register_view(ManifestJSONView) conf = config.get(DOMAIN, {}) @@ -597,3 +604,18 @@ def _is_latest(js_option, request): useragent = request.headers.get('User-Agent') return useragent and hass_frontend.version(useragent) + + +def websocket_handle_get_panels(hass, connection, msg): + """Handle get panels command. + + Async friendly. + """ + panels = { + panel: + connection.hass.data[DATA_PANELS][panel].to_response( + connection.hass, connection.request) + for panel in connection.hass.data[DATA_PANELS]} + + connection.to_write.put_nowait(websocket_api.result_message( + msg['id'], panels)) diff --git a/homeassistant/components/websocket_api.py b/homeassistant/components/websocket_api.py index 1e23ad19897..84c92631572 100644 --- a/homeassistant/components/websocket_api.py +++ b/homeassistant/components/websocket_api.py @@ -18,8 +18,8 @@ from voluptuous.humanize import humanize_error from homeassistant.const import ( MATCH_ALL, EVENT_TIME_CHANGED, EVENT_HOMEASSISTANT_STOP, __version__) -from homeassistant.components import frontend from homeassistant.core import callback +from homeassistant.loader import bind_hass from homeassistant.remote import JSONEncoder from homeassistant.helpers import config_validation as cv from homeassistant.helpers.service import async_get_all_descriptions @@ -46,7 +46,6 @@ TYPE_AUTH_REQUIRED = 'auth_required' TYPE_CALL_SERVICE = 'call_service' TYPE_EVENT = 'event' TYPE_GET_CONFIG = 'get_config' -TYPE_GET_PANELS = 'get_panels' TYPE_GET_SERVICES = 'get_services' TYPE_GET_STATES = 'get_states' TYPE_PING = 'ping' @@ -64,62 +63,56 @@ AUTH_MESSAGE_SCHEMA = vol.Schema({ vol.Required('api_password'): str, }) -SUBSCRIBE_EVENTS_MESSAGE_SCHEMA = vol.Schema({ +# Minimal requirements of a message +MINIMAL_MESSAGE_SCHEMA = vol.Schema({ vol.Required('id'): cv.positive_int, + vol.Required('type'): cv.string, +}, extra=vol.ALLOW_EXTRA) +# Base schema to extend by message handlers +BASE_COMMAND_MESSAGE_SCHEMA = vol.Schema({ + vol.Required('id'): cv.positive_int, +}) + + +SCHEMA_SUBSCRIBE_EVENTS = BASE_COMMAND_MESSAGE_SCHEMA.extend({ vol.Required('type'): TYPE_SUBSCRIBE_EVENTS, vol.Optional('event_type', default=MATCH_ALL): str, }) -UNSUBSCRIBE_EVENTS_MESSAGE_SCHEMA = vol.Schema({ - vol.Required('id'): cv.positive_int, + +SCHEMA_UNSUBSCRIBE_EVENTS = BASE_COMMAND_MESSAGE_SCHEMA.extend({ vol.Required('type'): TYPE_UNSUBSCRIBE_EVENTS, vol.Required('subscription'): cv.positive_int, }) -CALL_SERVICE_MESSAGE_SCHEMA = vol.Schema({ - vol.Required('id'): cv.positive_int, + +SCHEMA_CALL_SERVICE = BASE_COMMAND_MESSAGE_SCHEMA.extend({ vol.Required('type'): TYPE_CALL_SERVICE, vol.Required('domain'): str, vol.Required('service'): str, vol.Optional('service_data'): dict }) -GET_STATES_MESSAGE_SCHEMA = vol.Schema({ - vol.Required('id'): cv.positive_int, + +SCHEMA_GET_STATES = BASE_COMMAND_MESSAGE_SCHEMA.extend({ vol.Required('type'): TYPE_GET_STATES, }) -GET_SERVICES_MESSAGE_SCHEMA = vol.Schema({ - vol.Required('id'): cv.positive_int, + +SCHEMA_GET_SERVICES = BASE_COMMAND_MESSAGE_SCHEMA.extend({ vol.Required('type'): TYPE_GET_SERVICES, }) -GET_CONFIG_MESSAGE_SCHEMA = vol.Schema({ - vol.Required('id'): cv.positive_int, + +SCHEMA_GET_CONFIG = BASE_COMMAND_MESSAGE_SCHEMA.extend({ vol.Required('type'): TYPE_GET_CONFIG, }) -GET_PANELS_MESSAGE_SCHEMA = vol.Schema({ - vol.Required('id'): cv.positive_int, - vol.Required('type'): TYPE_GET_PANELS, -}) -PING_MESSAGE_SCHEMA = vol.Schema({ - vol.Required('id'): cv.positive_int, +SCHEMA_PING = BASE_COMMAND_MESSAGE_SCHEMA.extend({ vol.Required('type'): TYPE_PING, }) -BASE_COMMAND_MESSAGE_SCHEMA = vol.Schema({ - vol.Required('id'): cv.positive_int, - vol.Required('type'): vol.Any(TYPE_CALL_SERVICE, - TYPE_SUBSCRIBE_EVENTS, - TYPE_UNSUBSCRIBE_EVENTS, - TYPE_GET_STATES, - TYPE_GET_SERVICES, - TYPE_GET_CONFIG, - TYPE_GET_PANELS, - TYPE_PING) -}, extra=vol.ALLOW_EXTRA) # Define the possible errors that occur when connections are cancelled. # Originally, this was just asyncio.CancelledError, but issue #9546 showed @@ -191,9 +184,36 @@ def result_message(iden, result=None): } +@bind_hass +@callback +def async_register_command(hass, command, handler, schema): + """Register a websocket command.""" + handlers = hass.data.get(DOMAIN) + if handlers is None: + handlers = hass.data[DOMAIN] = {} + handlers[command] = (handler, schema) + + async def async_setup(hass, config): """Initialize the websocket API.""" hass.http.register_view(WebsocketAPIView) + + async_register_command(hass, TYPE_SUBSCRIBE_EVENTS, + handle_subscribe_events, SCHEMA_SUBSCRIBE_EVENTS) + async_register_command(hass, TYPE_UNSUBSCRIBE_EVENTS, + handle_unsubscribe_events, + SCHEMA_UNSUBSCRIBE_EVENTS) + async_register_command(hass, TYPE_CALL_SERVICE, + handle_call_service, SCHEMA_CALL_SERVICE) + async_register_command(hass, TYPE_GET_STATES, + handle_get_states, SCHEMA_GET_STATES) + async_register_command(hass, TYPE_GET_SERVICES, + handle_get_services, SCHEMA_GET_SERVICES) + async_register_command(hass, TYPE_GET_CONFIG, + handle_get_config, SCHEMA_GET_CONFIG) + async_register_command(hass, TYPE_PING, + handle_ping, SCHEMA_PING) + return True @@ -316,10 +336,11 @@ class ActiveConnection: msg = await wsock.receive_json() last_id = 0 + handlers = self.hass.data[DOMAIN] while msg: self.debug("Received", msg) - msg = BASE_COMMAND_MESSAGE_SCHEMA(msg) + msg = MINIMAL_MESSAGE_SCHEMA(msg) cur_id = msg['id'] if cur_id <= last_id: @@ -327,9 +348,13 @@ class ActiveConnection: cur_id, ERR_ID_REUSE, 'Identifier values have to increase.')) + elif msg['type'] not in handlers: + # Unknown command + break + else: - handler_name = 'handle_{}'.format(msg['type']) - getattr(self, handler_name)(msg) + handler, schema = handlers[msg['type']] + handler(self.hass, self, schema(msg)) last_id = cur_id msg = await wsock.receive_json() @@ -403,109 +428,89 @@ class ActiveConnection: return wsock - def handle_subscribe_events(self, msg): - """Handle subscribe events command. - Async friendly. - """ - msg = SUBSCRIBE_EVENTS_MESSAGE_SCHEMA(msg) +def handle_subscribe_events(hass, connection, msg): + """Handle subscribe events command. - async def forward_events(event): - """Forward events to websocket.""" - if event.event_type == EVENT_TIME_CHANGED: - return + Async friendly. + """ + async def forward_events(event): + """Forward events to websocket.""" + if event.event_type == EVENT_TIME_CHANGED: + return - self.send_message_outside(event_message(msg['id'], event)) + connection.send_message_outside(event_message(msg['id'], event)) - self.event_listeners[msg['id']] = self.hass.bus.async_listen( - msg['event_type'], forward_events) + connection.event_listeners[msg['id']] = hass.bus.async_listen( + msg['event_type'], forward_events) - self.to_write.put_nowait(result_message(msg['id'])) + connection.to_write.put_nowait(result_message(msg['id'])) - def handle_unsubscribe_events(self, msg): - """Handle unsubscribe events command. - Async friendly. - """ - msg = UNSUBSCRIBE_EVENTS_MESSAGE_SCHEMA(msg) +def handle_unsubscribe_events(hass, connection, msg): + """Handle unsubscribe events command. - subscription = msg['subscription'] + Async friendly. + """ + subscription = msg['subscription'] - if subscription in self.event_listeners: - self.event_listeners.pop(subscription)() - self.to_write.put_nowait(result_message(msg['id'])) - else: - self.to_write.put_nowait(error_message( - msg['id'], ERR_NOT_FOUND, - 'Subscription not found.')) + if subscription in connection.event_listeners: + connection.event_listeners.pop(subscription)() + connection.to_write.put_nowait(result_message(msg['id'])) + else: + connection.to_write.put_nowait(error_message( + msg['id'], ERR_NOT_FOUND, 'Subscription not found.')) - def handle_call_service(self, msg): - """Handle call service command. - Async friendly. - """ - msg = CALL_SERVICE_MESSAGE_SCHEMA(msg) +def handle_call_service(hass, connection, msg): + """Handle call service command. - async def call_service_helper(msg): - """Call a service and fire complete message.""" - await self.hass.services.async_call( - msg['domain'], msg['service'], msg.get('service_data'), True) - self.send_message_outside(result_message(msg['id'])) + Async friendly. + """ + async def call_service_helper(msg): + """Call a service and fire complete message.""" + await hass.services.async_call( + msg['domain'], msg['service'], msg.get('service_data'), True) + connection.send_message_outside(result_message(msg['id'])) - self.hass.async_add_job(call_service_helper(msg)) + hass.async_add_job(call_service_helper(msg)) - def handle_get_states(self, msg): - """Handle get states command. - Async friendly. - """ - msg = GET_STATES_MESSAGE_SCHEMA(msg) +def handle_get_states(hass, connection, msg): + """Handle get states command. - self.to_write.put_nowait(result_message( - msg['id'], self.hass.states.async_all())) + Async friendly. + """ + connection.to_write.put_nowait(result_message( + msg['id'], hass.states.async_all())) - def handle_get_services(self, msg): - """Handle get services command. - Async friendly. - """ - msg = GET_SERVICES_MESSAGE_SCHEMA(msg) +def handle_get_services(hass, connection, msg): + """Handle get services command. - async def get_services_helper(msg): - """Get available services and fire complete message.""" - descriptions = await async_get_all_descriptions(self.hass) - self.send_message_outside(result_message(msg['id'], descriptions)) + Async friendly. + """ + async def get_services_helper(msg): + """Get available services and fire complete message.""" + descriptions = await async_get_all_descriptions(hass) + connection.send_message_outside( + result_message(msg['id'], descriptions)) - self.hass.async_add_job(get_services_helper(msg)) + hass.async_add_job(get_services_helper(msg)) - def handle_get_config(self, msg): - """Handle get config command. - Async friendly. - """ - msg = GET_CONFIG_MESSAGE_SCHEMA(msg) +def handle_get_config(hass, connection, msg): + """Handle get config command. - self.to_write.put_nowait(result_message( - msg['id'], self.hass.config.as_dict())) + Async friendly. + """ + connection.to_write.put_nowait(result_message( + msg['id'], hass.config.as_dict())) - def handle_get_panels(self, msg): - """Handle get panels command. - Async friendly. - """ - msg = GET_PANELS_MESSAGE_SCHEMA(msg) - panels = { - panel: - self.hass.data[frontend.DATA_PANELS][panel].to_response( - self.hass, self.request) - for panel in self.hass.data[frontend.DATA_PANELS]} +def handle_ping(hass, connection, msg): + """Handle ping command. - self.to_write.put_nowait(result_message( - msg['id'], panels)) - - def handle_ping(self, msg): - """Handle ping command. - - Async friendly. - """ - self.to_write.put_nowait(pong_message(msg['id'])) + Async friendly. + """ + connection.to_write.put_nowait(pong_message(msg['id'])) diff --git a/tests/components/conftest.py b/tests/components/conftest.py new file mode 100644 index 00000000000..53caeb80783 --- /dev/null +++ b/tests/components/conftest.py @@ -0,0 +1,22 @@ +"""Fixtures for component testing.""" +import pytest + +from homeassistant.setup import async_setup_component + + +@pytest.fixture +def hass_ws_client(aiohttp_client): + """Websocket client fixture connected to websocket server.""" + async def create_client(hass): + """Create a websocket client.""" + wapi = hass.components.websocket_api + assert await async_setup_component(hass, 'websocket_api') + + client = await aiohttp_client(hass.http.app) + websocket = await client.ws_connect(wapi.URL) + auth_ok = await websocket.receive_json() + assert auth_ok['type'] == wapi.TYPE_AUTH_OK + + return websocket + + return create_client diff --git a/tests/components/test_frontend.py b/tests/components/test_frontend.py index c742e215738..973544495d7 100644 --- a/tests/components/test_frontend.py +++ b/tests/components/test_frontend.py @@ -9,6 +9,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) +from homeassistant.components import websocket_api as wapi @pytest.fixture @@ -189,3 +190,26 @@ def test_panel_without_path(hass): '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') + await hass.components.frontend.async_register_built_in_panel( + 'map', 'Map', 'mdi:account-location') + + client = await hass_ws_client(hass) + await client.send_json({ + 'id': 5, + 'type': 'get_panels', + }) + + msg = await client.receive_json() + + assert msg['id'] == 5 + assert msg['type'] == wapi.TYPE_RESULT + assert msg['success'] + assert msg['result']['map']['component_name'] == 'map' + assert msg['result']['map']['url_path'] == 'map' + assert msg['result']['map']['icon'] == 'mdi:account-location' + assert msg['result']['map']['title'] == 'Map' diff --git a/tests/components/test_websocket_api.py b/tests/components/test_websocket_api.py index 4deccf65209..0a130e507d4 100644 --- a/tests/components/test_websocket_api.py +++ b/tests/components/test_websocket_api.py @@ -7,7 +7,7 @@ from async_timeout import timeout import pytest from homeassistant.core import callback -from homeassistant.components import websocket_api as wapi, frontend +from homeassistant.components import websocket_api as wapi from homeassistant.setup import async_setup_component from tests.common import mock_coro @@ -16,20 +16,9 @@ API_PASSWORD = 'test1234' @pytest.fixture -def websocket_client(loop, hass, aiohttp_client): - """Websocket client fixture connected to websocket server.""" - assert loop.run_until_complete( - async_setup_component(hass, 'websocket_api')) - - client = loop.run_until_complete(aiohttp_client(hass.http.app)) - ws = loop.run_until_complete(client.ws_connect(wapi.URL)) - auth_ok = loop.run_until_complete(ws.receive_json()) - assert auth_ok['type'] == wapi.TYPE_AUTH_OK - - yield ws - - if not ws.closed: - loop.run_until_complete(ws.close()) +def websocket_client(hass, hass_ws_client): + """Create a websocket client.""" + return hass.loop.run_until_complete(hass_ws_client(hass)) @pytest.fixture @@ -289,31 +278,6 @@ def test_get_config(hass, websocket_client): assert msg['result'] == hass.config.as_dict() -@asyncio.coroutine -def test_get_panels(hass, websocket_client): - """Test get_panels command.""" - yield from hass.components.frontend.async_register_built_in_panel( - 'map', 'Map', 'mdi:account-location') - hass.data[frontend.DATA_JS_VERSION] = 'es5' - yield from websocket_client.send_json({ - 'id': 5, - 'type': wapi.TYPE_GET_PANELS, - }) - - msg = yield from websocket_client.receive_json() - assert msg['id'] == 5 - assert msg['type'] == wapi.TYPE_RESULT - assert msg['success'] - assert msg['result'] == {'map': { - 'component_name': 'map', - 'url_path': 'map', - 'config': None, - 'url': None, - 'icon': 'mdi:account-location', - 'title': 'Map', - }} - - @asyncio.coroutine def test_ping(websocket_client): """Test get_panels command.""" @@ -337,3 +301,15 @@ def test_pending_msg_overflow(hass, mock_low_queue, websocket_client): }) msg = yield from websocket_client.receive() assert msg.type == WSMsgType.close + + +@asyncio.coroutine +def test_unknown_command(websocket_client): + """Test get_panels command.""" + yield from websocket_client.send_json({ + 'id': 5, + 'type': 'unknown_command', + }) + + msg = yield from websocket_client.receive() + assert msg.type == WSMsgType.close From e78497789b5ec7f87dc1d956e2a873ed7c9f84f1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20H=C3=B6rrle?= Date: Tue, 1 May 2018 20:13:35 +0200 Subject: [PATCH 082/155] Change the divisor for total consumption output (#14215) According to my observations, the "switch_energy" value displayed by Pyfritzhome is the sum of Wh over the last week since measurement. As a result, the correct divisor for representing output as kWh would be 1000 instead of 10000. --- homeassistant/components/switch/fritzbox.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/switch/fritzbox.py b/homeassistant/components/switch/fritzbox.py index c8313b0dfef..65a1aa6aabc 100755 --- a/homeassistant/components/switch/fritzbox.py +++ b/homeassistant/components/switch/fritzbox.py @@ -87,7 +87,7 @@ class FritzboxSwitch(SwitchDevice): if self._device.has_powermeter: attrs[ATTR_TOTAL_CONSUMPTION] = "{:.3f}".format( - (self._device.energy or 0.0) / 100000) + (self._device.energy or 0.0) / 1000) attrs[ATTR_TOTAL_CONSUMPTION_UNIT] = \ ATTR_TOTAL_CONSUMPTION_UNIT_VALUE if self._device.has_temperature_sensor: From b0cccbfd9f2fc4e7699303e672bd41b98be3123a Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Tue, 1 May 2018 20:14:28 +0200 Subject: [PATCH 083/155] Upgrade mypy to 0.590 (#14207) --- requirements_test.txt | 2 +- requirements_test_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/requirements_test.txt b/requirements_test.txt index 38b716406fd..6d5f68615be 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -6,7 +6,7 @@ coveralls==1.2.0 flake8-docstrings==1.0.3 flake8==3.5 mock-open==1.3.1 -mypy==0.580 +mypy==0.590 pydocstyle==1.1.1 pylint==1.8.3 pytest-aiohttp==0.3.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index a5835392c4a..0605b3d2e24 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -7,7 +7,7 @@ coveralls==1.2.0 flake8-docstrings==1.0.3 flake8==3.5 mock-open==1.3.1 -mypy==0.580 +mypy==0.590 pydocstyle==1.1.1 pylint==1.8.3 pytest-aiohttp==0.3.0 From 9bc8f6649b1c7dd8da23cafced0a8502a6d11864 Mon Sep 17 00:00:00 2001 From: Otto Winter Date: Tue, 1 May 2018 20:32:44 +0200 Subject: [PATCH 084/155] Template Sensor add device_class support (#14034) * Template Sensor Device Class Support * Lint * Add tests --- homeassistant/components/sensor/template.py | 19 ++++++++--- tests/components/sensor/test_template.py | 37 +++++++++++++++++++++ 2 files changed, 52 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/sensor/template.py b/homeassistant/components/sensor/template.py index 1cd43262513..65f49998dbf 100644 --- a/homeassistant/components/sensor/template.py +++ b/homeassistant/components/sensor/template.py @@ -6,16 +6,18 @@ https://home-assistant.io/components/sensor.template/ """ import asyncio import logging +from typing import Optional import voluptuous as vol from homeassistant.core import callback -from homeassistant.components.sensor import ENTITY_ID_FORMAT, PLATFORM_SCHEMA +from homeassistant.components.sensor import ENTITY_ID_FORMAT, \ + PLATFORM_SCHEMA, DEVICE_CLASSES_SCHEMA from homeassistant.const import ( ATTR_FRIENDLY_NAME, ATTR_UNIT_OF_MEASUREMENT, CONF_VALUE_TEMPLATE, CONF_ICON_TEMPLATE, CONF_ENTITY_PICTURE_TEMPLATE, ATTR_ENTITY_ID, CONF_SENSORS, EVENT_HOMEASSISTANT_START, CONF_FRIENDLY_NAME_TEMPLATE, - MATCH_ALL) + MATCH_ALL, CONF_DEVICE_CLASS) from homeassistant.exceptions import TemplateError import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity, async_generate_entity_id @@ -30,6 +32,7 @@ SENSOR_SCHEMA = vol.Schema({ vol.Optional(CONF_FRIENDLY_NAME_TEMPLATE): cv.template, vol.Optional(ATTR_FRIENDLY_NAME): cv.string, vol.Optional(ATTR_UNIT_OF_MEASUREMENT): cv.string, + vol.Optional(CONF_DEVICE_CLASS): DEVICE_CLASSES_SCHEMA, vol.Optional(ATTR_ENTITY_ID): cv.entity_ids }) @@ -52,6 +55,7 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None): friendly_name = device_config.get(ATTR_FRIENDLY_NAME, device) friendly_name_template = device_config.get(CONF_FRIENDLY_NAME_TEMPLATE) unit_of_measurement = device_config.get(ATTR_UNIT_OF_MEASUREMENT) + device_class = device_config.get(CONF_DEVICE_CLASS) entity_ids = set() manual_entity_ids = device_config.get(ATTR_ENTITY_ID) @@ -86,7 +90,8 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None): state_template, icon_template, entity_picture_template, - entity_ids) + entity_ids, + device_class) ) if not sensors: _LOGGER.error("No sensors added") @@ -101,7 +106,7 @@ class SensorTemplate(Entity): def __init__(self, hass, device_id, friendly_name, friendly_name_template, unit_of_measurement, state_template, icon_template, - entity_picture_template, entity_ids): + entity_picture_template, entity_ids, device_class): """Initialize the sensor.""" self.hass = hass self.entity_id = async_generate_entity_id(ENTITY_ID_FORMAT, device_id, @@ -116,6 +121,7 @@ class SensorTemplate(Entity): self._icon = None self._entity_picture = None self._entities = entity_ids + self._device_class = device_class @asyncio.coroutine def async_added_to_hass(self): @@ -151,6 +157,11 @@ class SensorTemplate(Entity): """Return the icon to use in the frontend, if any.""" return self._icon + @property + def device_class(self) -> Optional[str]: + """Return the device class of the sensor.""" + return self._device_class + @property def entity_picture(self): """Return the entity_picture to use in the frontend, if any.""" diff --git a/tests/components/sensor/test_template.py b/tests/components/sensor/test_template.py index b05fc90bfe4..f8d912f24dd 100644 --- a/tests/components/sensor/test_template.py +++ b/tests/components/sensor/test_template.py @@ -267,3 +267,40 @@ class TestTemplateSensor: self.hass.block_till_done() assert self.hass.states.all() == [] + + def test_setup_invalid_device_class(self): + """"Test setup with invalid device_class.""" + with assert_setup_component(0): + assert setup_component(self.hass, 'sensor', { + 'sensor': { + 'platform': 'template', + 'sensors': { + 'test': { + 'value_template': '{{ foo }}', + 'device_class': 'foobarnotreal', + }, + }, + } + }) + + def test_setup_valid_device_class(self): + """"Test setup with valid device_class.""" + with assert_setup_component(1): + assert setup_component(self.hass, 'sensor', { + 'sensor': { + 'platform': 'template', + 'sensors': { + 'test1': { + 'value_template': '{{ foo }}', + 'device_class': 'temperature', + }, + 'test2': {'value_template': '{{ foo }}'}, + } + } + }) + self.hass.block_till_done() + + state = self.hass.states.get('sensor.test1') + assert state.attributes['device_class'] == 'temperature' + state = self.hass.states.get('sensor.test2') + assert 'device_class' not in state.attributes From b00f771541f9982cbbe9e684f2ed8c9068ac428e Mon Sep 17 00:00:00 2001 From: Ruben Date: Tue, 1 May 2018 20:40:48 +0200 Subject: [PATCH 085/155] Add more parameters for DSMR sensor (#13967) * Add more parameters for DSMR component * Add suiting icon for power failure * Add suiting icon for swells & sags * Fix tab indentation -> spaces * Fix too long lines (PEP8) --- homeassistant/components/sensor/dsmr.py | 93 +++++++++++++++++++++++-- 1 file changed, 86 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/sensor/dsmr.py b/homeassistant/components/sensor/dsmr.py index cea29d437ae..d7982f1c9db 100644 --- a/homeassistant/components/sensor/dsmr.py +++ b/homeassistant/components/sensor/dsmr.py @@ -31,6 +31,8 @@ DOMAIN = 'dsmr' ICON_GAS = 'mdi:fire' ICON_POWER = 'mdi:flash' +ICON_POWER_FAILURE = 'mdi:flash-off' +ICON_SWELL_SAG = 'mdi:pulse' # Smart meter sends telegram every 10 seconds MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=10) @@ -61,13 +63,86 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None): # Define list of name,obis mappings to generate entities obis_mapping = [ - ['Power Consumption', obis_ref.CURRENT_ELECTRICITY_USAGE], - ['Power Production', obis_ref.CURRENT_ELECTRICITY_DELIVERY], - ['Power Tariff', obis_ref.ELECTRICITY_ACTIVE_TARIFF], - ['Power Consumption (low)', obis_ref.ELECTRICITY_USED_TARIFF_1], - ['Power Consumption (normal)', obis_ref.ELECTRICITY_USED_TARIFF_2], - ['Power Production (low)', obis_ref.ELECTRICITY_DELIVERED_TARIFF_1], - ['Power Production (normal)', obis_ref.ELECTRICITY_DELIVERED_TARIFF_2], + [ + 'Power Consumption', + obis_ref.CURRENT_ELECTRICITY_USAGE + ], + [ + 'Power Production', + obis_ref.CURRENT_ELECTRICITY_DELIVERY + ], + [ + 'Power Tariff', + obis_ref.ELECTRICITY_ACTIVE_TARIFF + ], + [ + 'Power Consumption (low)', + obis_ref.ELECTRICITY_USED_TARIFF_1 + ], + [ + 'Power Consumption (normal)', + obis_ref.ELECTRICITY_USED_TARIFF_2 + ], + [ + 'Power Production (low)', + obis_ref.ELECTRICITY_DELIVERED_TARIFF_1 + ], + [ + 'Power Production (normal)', + obis_ref.ELECTRICITY_DELIVERED_TARIFF_2 + ], + [ + 'Power Consumption Phase L1', + obis_ref.INSTANTANEOUS_ACTIVE_POWER_L1_POSITIVE + ], + [ + 'Power Consumption Phase L2', + obis_ref.INSTANTANEOUS_ACTIVE_POWER_L2_POSITIVE + ], + [ + 'Power Consumption Phase L3', + obis_ref.INSTANTANEOUS_ACTIVE_POWER_L3_POSITIVE + ], + [ + 'Power Production Phase L1', + obis_ref.INSTANTANEOUS_ACTIVE_POWER_L1_NEGATIVE + ], + [ + 'Power Production Phase L2', + obis_ref.INSTANTANEOUS_ACTIVE_POWER_L2_NEGATIVE + ], + [ + 'Power Production Phase L3', + obis_ref.INSTANTANEOUS_ACTIVE_POWER_L3_NEGATIVE + ], + [ + 'Long Power Failure Count', + obis_ref.LONG_POWER_FAILURE_COUNT + ], + [ + 'Voltage Sags Phase L1', + obis_ref.VOLTAGE_SAG_L1_COUNT + ], + [ + 'Voltage Sags Phase L2', + obis_ref.VOLTAGE_SAG_L2_COUNT + ], + [ + 'Voltage Sags Phase L3', + obis_ref.VOLTAGE_SAG_L3_COUNT + ], + [ + 'Voltage Swells Phase L1', + obis_ref.VOLTAGE_SWELL_L1_COUNT + ], + [ + 'Voltage Swells Phase L2', + obis_ref.VOLTAGE_SWELL_L2_COUNT + ], + [ + 'Voltage Swells Phase L3', + obis_ref.VOLTAGE_SWELL_L3_COUNT + ], ] # Generate device entities @@ -174,6 +249,10 @@ class DSMREntity(Entity): @property def icon(self): """Icon to use in the frontend, if any.""" + if 'Sags' in self._name or 'Swells' in self.name: + return ICON_SWELL_SAG + if 'Failure' in self._name: + return ICON_POWER_FAILURE if 'Power' in self._name: return ICON_POWER elif 'Gas' in self._name: From bf53cbe08d4908ab37063f35bdcb97cb682823b0 Mon Sep 17 00:00:00 2001 From: blackwind Date: Tue, 1 May 2018 12:41:36 -0600 Subject: [PATCH 086/155] Support setting explicit mute value for Panasonic Viera TV (#13954) * Use module's methods instead of API calls * Use module's methods instead of API calls for media commands --- .../components/media_player/panasonic_viera.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/media_player/panasonic_viera.py b/homeassistant/components/media_player/panasonic_viera.py index 39e5f81b71d..4a25fa1bf67 100644 --- a/homeassistant/components/media_player/panasonic_viera.py +++ b/homeassistant/components/media_player/panasonic_viera.py @@ -138,20 +138,20 @@ class PanasonicVieraTVDevice(MediaPlayerDevice): def turn_off(self): """Turn off media player.""" if self._state != STATE_OFF: - self.send_key('NRC_POWER-ONOFF') + self._remote.turn_off() self._state = STATE_OFF def volume_up(self): """Volume up the media player.""" - self.send_key('NRC_VOLUP-ONOFF') + self._remote.volume_up() def volume_down(self): """Volume down media player.""" - self.send_key('NRC_VOLDOWN-ONOFF') + self._remote.volume_down() def mute_volume(self, mute): """Send mute command.""" - self.send_key('NRC_MUTE-ONOFF') + self._remote.set_mute(mute) def set_volume_level(self, volume): """Set volume level, range 0..1.""" @@ -172,20 +172,20 @@ class PanasonicVieraTVDevice(MediaPlayerDevice): def media_play(self): """Send play command.""" self._playing = True - self.send_key('NRC_PLAY-ONOFF') + self._remote.media_play() def media_pause(self): """Send media pause command to media player.""" self._playing = False - self.send_key('NRC_PAUSE-ONOFF') + self._remote.media_pause() def media_next_track(self): """Send next track command.""" - self.send_key('NRC_FF-ONOFF') + self._remote.media_next_track() def media_previous_track(self): """Send the previous track command.""" - self.send_key('NRC_REW-ONOFF') + self._remote.media_previous_track() def play_media(self, media_type, media_id, **kwargs): """Play media.""" From 38560cda1c1be7e800f2956903eab4a1f7ce8072 Mon Sep 17 00:00:00 2001 From: NovapaX Date: Tue, 1 May 2018 20:49:33 +0200 Subject: [PATCH 087/155] Allow to set a desired update interval for camera_proxy_stream view (#13350) * allow to set a desired update interval for camera_proxy_stream view * lint * refactor into a seperate method. Keep the handle_async_mjpeg_stream method to be overridden by platforms so they can keep proxying the direct streams from the camera * change descriptions * consolidate * lint * travis * async/await and force min stream interval for fallback stream. * guard clause. Let the method raise error on interval. * is is not = * what to except when you're excepting * raise ValueError, remove unnecessary 500 response --- homeassistant/components/camera/__init__.py | 53 +++++++++++++++------ 1 file changed, 39 insertions(+), 14 deletions(-) diff --git a/homeassistant/components/camera/__init__.py b/homeassistant/components/camera/__init__.py index 5321ec3d860..1fa89bc2241 100644 --- a/homeassistant/components/camera/__init__.py +++ b/homeassistant/components/camera/__init__.py @@ -53,6 +53,9 @@ ENTITY_IMAGE_URL = '/api/camera_proxy/{0}?token={1}' TOKEN_CHANGE_INTERVAL = timedelta(minutes=5) _RND = SystemRandom() +FALLBACK_STREAM_INTERVAL = 1 # seconds +MIN_STREAM_INTERVAL = 0.5 # seconds + CAMERA_SERVICE_SCHEMA = vol.Schema({ vol.Optional(ATTR_ENTITY_ID): cv.entity_ids, }) @@ -252,19 +255,21 @@ class Camera(Entity): """ return self.hass.async_add_job(self.camera_image) - @asyncio.coroutine - def handle_async_mjpeg_stream(self, request): + async def handle_async_still_stream(self, request, interval): """Generate an HTTP MJPEG stream from camera images. This method must be run in the event loop. """ - response = web.StreamResponse() + if interval < MIN_STREAM_INTERVAL: + raise ValueError("Stream interval must be be > {}" + .format(MIN_STREAM_INTERVAL)) + response = web.StreamResponse() response.content_type = ('multipart/x-mixed-replace; ' 'boundary=--frameboundary') - yield from response.prepare(request) + await response.prepare(request) - async def write(img_bytes): + async def write_to_mjpeg_stream(img_bytes): """Write image to stream.""" await response.write(bytes( '--frameboundary\r\n' @@ -277,21 +282,21 @@ class Camera(Entity): try: while True: - img_bytes = yield from self.async_camera_image() + img_bytes = await self.async_camera_image() if not img_bytes: break if img_bytes and img_bytes != last_image: - yield from write(img_bytes) + await write_to_mjpeg_stream(img_bytes) # Chrome seems to always ignore first picture, # print it twice. if last_image is None: - yield from write(img_bytes) + await write_to_mjpeg_stream(img_bytes) last_image = img_bytes - yield from asyncio.sleep(.5) + await asyncio.sleep(interval) except asyncio.CancelledError: _LOGGER.debug("Stream closed by frontend.") @@ -299,7 +304,17 @@ class Camera(Entity): finally: if response is not None: - yield from response.write_eof() + await response.write_eof() + + async def handle_async_mjpeg_stream(self, request): + """Serve an HTTP MJPEG stream from the camera. + + This method can be overridden by camera plaforms to proxy + a direct stream from the camera. + This method must be run in the event loop. + """ + await self.handle_async_still_stream(request, + FALLBACK_STREAM_INTERVAL) @property def state(self): @@ -411,7 +426,17 @@ class CameraMjpegStream(CameraView): url = '/api/camera_proxy_stream/{entity_id}' name = 'api:camera:stream' - @asyncio.coroutine - def handle(self, request, camera): - """Serve camera image.""" - yield from camera.handle_async_mjpeg_stream(request) + async def handle(self, request, camera): + """Serve camera stream, possibly with interval.""" + interval = request.query.get('interval') + if interval is None: + await camera.handle_async_mjpeg_stream(request) + return + + try: + # Compose camera stream from stills + interval = float(request.query.get('interval')) + await camera.handle_async_still_stream(request, interval) + return + except ValueError: + return web.Response(status=400) From 5d96751168dc2b9a9dc43ed338d35045a113fa75 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ville=20Skytt=C3=A4?= Date: Tue, 1 May 2018 20:54:06 +0200 Subject: [PATCH 088/155] panasonic_viera: Provide unique_id from SSDP UDN, if available (#13541) --- .../components/media_player/panasonic_viera.py | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/media_player/panasonic_viera.py b/homeassistant/components/media_player/panasonic_viera.py index 4a25fa1bf67..db60de922d9 100644 --- a/homeassistant/components/media_player/panasonic_viera.py +++ b/homeassistant/components/media_player/panasonic_viera.py @@ -56,8 +56,13 @@ def setup_platform(hass, config, add_devices, discovery_info=None): name = discovery_info.get('name') host = discovery_info.get('host') port = discovery_info.get('port') + udn = discovery_info.get('udn') + if udn and udn.startswith('uuid:'): + uuid = udn[len('uuid:'):] + else: + uuid = None remote = RemoteControl(host, port) - add_devices([PanasonicVieraTVDevice(mac, name, remote)]) + add_devices([PanasonicVieraTVDevice(mac, name, remote, uuid)]) return True host = config.get(CONF_HOST) @@ -70,19 +75,25 @@ def setup_platform(hass, config, add_devices, discovery_info=None): class PanasonicVieraTVDevice(MediaPlayerDevice): """Representation of a Panasonic Viera TV.""" - def __init__(self, mac, name, remote): + def __init__(self, mac, name, remote, uuid=None): """Initialize the Panasonic device.""" import wakeonlan # Save a reference to the imported class self._wol = wakeonlan self._mac = mac self._name = name + self._uuid = uuid self._muted = False self._playing = True self._state = STATE_UNKNOWN self._remote = remote self._volume = 0 + @property + def unique_id(self) -> str: + """Return the unique ID of this Viera TV.""" + return self._uuid + def update(self): """Retrieve the latest data.""" try: From 83d300fd11e95aaf802e062d14ca0d67b10bb566 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 1 May 2018 14:57:30 -0400 Subject: [PATCH 089/155] Custom component loading cleanup (#14211) * Clean up custom component loading * Fix some tests * Fix some stuff * Make imports work again * Fix tests * Remove debug print * Lint --- homeassistant/bootstrap.py | 8 +- .../components/automation/__init__.py | 16 +- .../components/binary_sensor/bloomsky.py | 3 +- .../components/binary_sensor/netatmo.py | 3 +- .../components/binary_sensor/wemo.py | 7 +- homeassistant/components/camera/bloomsky.py | 3 +- homeassistant/components/camera/netatmo.py | 3 +- homeassistant/components/climate/netatmo.py | 3 +- .../components/device_sun_light_trigger.py | 25 ++- .../components/device_tracker/__init__.py | 7 +- .../components/image_processing/__init__.py | 5 +- homeassistant/components/light/wemo.py | 3 +- homeassistant/components/microsoft_face.py | 3 +- homeassistant/components/mqtt_eventstream.py | 7 +- homeassistant/components/mysensors.py | 7 +- homeassistant/components/scene/__init__.py | 14 +- homeassistant/components/sensor/bloomsky.py | 3 +- homeassistant/components/sensor/netatmo.py | 3 +- homeassistant/components/zwave/__init__.py | 2 +- homeassistant/config.py | 9 +- homeassistant/helpers/config_validation.py | 14 -- homeassistant/helpers/service.py | 8 +- homeassistant/helpers/template.py | 6 +- homeassistant/helpers/translation.py | 6 +- homeassistant/loader.py | 201 +++++++----------- homeassistant/scripts/check_config.py | 28 +-- homeassistant/setup.py | 8 +- tests/common.py | 6 +- .../climate/test_generic_thermostat.py | 2 +- .../components/config/test_config_entries.py | 12 +- .../components/device_tracker/test_asuswrt.py | 2 +- tests/components/device_tracker/test_init.py | 10 +- tests/components/light/test_init.py | 6 +- tests/components/scene/test_init.py | 2 +- tests/components/switch/test_flux.py | 32 +-- tests/components/switch/test_init.py | 6 +- .../test_device_sun_light_trigger.py | 4 +- tests/helpers/test_config_validation.py | 20 -- tests/helpers/test_discovery.py | 8 +- tests/helpers/test_entity_component.py | 30 +-- tests/helpers/test_entity_platform.py | 12 +- tests/helpers/test_service.py | 6 +- tests/helpers/test_translation.py | 6 +- tests/scripts/test_check_config.py | 3 - tests/test_config.py | 26 +-- tests/test_config_entries.py | 21 +- tests/test_loader.py | 31 +-- tests/test_requirements.py | 6 +- tests/test_setup.py | 49 +++-- .../custom_components/test_standalone.py | 2 +- 50 files changed, 315 insertions(+), 392 deletions(-) diff --git a/homeassistant/bootstrap.py b/homeassistant/bootstrap.py index 0abe5a7811e..826cc563e82 100644 --- a/homeassistant/bootstrap.py +++ b/homeassistant/bootstrap.py @@ -12,8 +12,7 @@ from typing import Any, Optional, Dict import voluptuous as vol from homeassistant import ( - core, config as conf_util, config_entries, loader, - components as core_components) + core, config as conf_util, config_entries, components as core_components) from homeassistant.components import persistent_notification from homeassistant.const import EVENT_HOMEASSISTANT_CLOSE from homeassistant.setup import async_setup_component @@ -103,15 +102,12 @@ async def async_from_config_dict(config: Dict[str, Any], _LOGGER.warning("Skipping pip installation of required modules. " "This may cause issues") - if not loader.PREPARED: - await hass.async_add_job(loader.prepare, hass) - # Make a copy because we are mutating it. config = OrderedDict(config) # Merge packages conf_util.merge_packages_config( - config, core_config.get(conf_util.CONF_PACKAGES, {})) + hass, config, core_config.get(conf_util.CONF_PACKAGES, {})) # Ensure we have no None values after merge for key, value in config.items(): diff --git a/homeassistant/components/automation/__init__.py b/homeassistant/components/automation/__init__.py index 8c490754f40..2f510fd33d6 100644 --- a/homeassistant/components/automation/__init__.py +++ b/homeassistant/components/automation/__init__.py @@ -6,6 +6,7 @@ https://home-assistant.io/components/automation/ """ import asyncio from functools import partial +import importlib import logging import voluptuous as vol @@ -22,7 +23,6 @@ from homeassistant.helpers import extract_domain_configs, script, condition from homeassistant.helpers.entity import ToggleEntity from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.restore_state import async_get_last_state -from homeassistant.loader import get_platform from homeassistant.util.dt import utcnow import homeassistant.helpers.config_validation as cv @@ -58,12 +58,14 @@ _LOGGER = logging.getLogger(__name__) def _platform_validator(config): """Validate it is a valid platform.""" - platform = get_platform(DOMAIN, config[CONF_PLATFORM]) + try: + platform = importlib.import_module( + 'homeassistant.components.automation.{}'.format( + config[CONF_PLATFORM])) + except ImportError: + raise vol.Invalid('Invalid platform specified') from None - if not hasattr(platform, 'TRIGGER_SCHEMA'): - return config - - return getattr(platform, 'TRIGGER_SCHEMA')(config) + return platform.TRIGGER_SCHEMA(config) _TRIGGER_SCHEMA = vol.All( @@ -71,7 +73,7 @@ _TRIGGER_SCHEMA = vol.All( [ vol.All( vol.Schema({ - vol.Required(CONF_PLATFORM): cv.platform_validator(DOMAIN) + vol.Required(CONF_PLATFORM): str }, extra=vol.ALLOW_EXTRA), _platform_validator ), diff --git a/homeassistant/components/binary_sensor/bloomsky.py b/homeassistant/components/binary_sensor/bloomsky.py index 53f148fe97f..3080cc65532 100644 --- a/homeassistant/components/binary_sensor/bloomsky.py +++ b/homeassistant/components/binary_sensor/bloomsky.py @@ -11,7 +11,6 @@ import voluptuous as vol from homeassistant.components.binary_sensor import ( BinarySensorDevice, PLATFORM_SCHEMA) from homeassistant.const import CONF_MONITORED_CONDITIONS -from homeassistant.loader import get_component import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) @@ -31,7 +30,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ def setup_platform(hass, config, add_devices, discovery_info=None): """Set up the available BloomSky weather binary sensors.""" - bloomsky = get_component('bloomsky') + bloomsky = hass.components.bloomsky # Default needed in case of discovery sensors = config.get(CONF_MONITORED_CONDITIONS, SENSOR_TYPES) diff --git a/homeassistant/components/binary_sensor/netatmo.py b/homeassistant/components/binary_sensor/netatmo.py index 7997e4e60db..fd0e30ccebc 100644 --- a/homeassistant/components/binary_sensor/netatmo.py +++ b/homeassistant/components/binary_sensor/netatmo.py @@ -13,7 +13,6 @@ import voluptuous as vol from homeassistant.components.binary_sensor import ( BinarySensorDevice, PLATFORM_SCHEMA) from homeassistant.components.netatmo import CameraData -from homeassistant.loader import get_component from homeassistant.const import CONF_TIMEOUT from homeassistant.helpers import config_validation as cv @@ -61,7 +60,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ # pylint: disable=unused-argument def setup_platform(hass, config, add_devices, discovery_info=None): """Set up the access to Netatmo binary sensor.""" - netatmo = get_component('netatmo') + netatmo = hass.components.netatmo home = config.get(CONF_HOME) timeout = config.get(CONF_TIMEOUT) if timeout is None: diff --git a/homeassistant/components/binary_sensor/wemo.py b/homeassistant/components/binary_sensor/wemo.py index cc1f602d871..30a7e291401 100644 --- a/homeassistant/components/binary_sensor/wemo.py +++ b/homeassistant/components/binary_sensor/wemo.py @@ -7,7 +7,6 @@ https://home-assistant.io/components/binary_sensor.wemo/ import logging from homeassistant.components.binary_sensor import BinarySensorDevice -from homeassistant.loader import get_component DEPENDENCIES = ['wemo'] @@ -25,18 +24,18 @@ def setup_platform(hass, config, add_devices_callback, discovery_info=None): device = discovery.device_from_description(location, mac) if device: - add_devices_callback([WemoBinarySensor(device)]) + add_devices_callback([WemoBinarySensor(hass, device)]) class WemoBinarySensor(BinarySensorDevice): """Representation a WeMo binary sensor.""" - def __init__(self, device): + def __init__(self, hass, device): """Initialize the WeMo sensor.""" self.wemo = device self._state = None - wemo = get_component('wemo') + wemo = hass.components.wemo wemo.SUBSCRIPTION_REGISTRY.register(self.wemo) wemo.SUBSCRIPTION_REGISTRY.on(self.wemo, None, self._update_callback) diff --git a/homeassistant/components/camera/bloomsky.py b/homeassistant/components/camera/bloomsky.py index c3b4775b593..ef70692215d 100644 --- a/homeassistant/components/camera/bloomsky.py +++ b/homeassistant/components/camera/bloomsky.py @@ -9,7 +9,6 @@ import logging import requests from homeassistant.components.camera import Camera -from homeassistant.loader import get_component DEPENDENCIES = ['bloomsky'] @@ -17,7 +16,7 @@ DEPENDENCIES = ['bloomsky'] # pylint: disable=unused-argument def setup_platform(hass, config, add_devices, discovery_info=None): """Set up access to BloomSky cameras.""" - bloomsky = get_component('bloomsky') + bloomsky = hass.components.bloomsky for device in bloomsky.BLOOMSKY.devices.values(): add_devices([BloomSkyCamera(bloomsky.BLOOMSKY, device)]) diff --git a/homeassistant/components/camera/netatmo.py b/homeassistant/components/camera/netatmo.py index 48f2710ce2e..bf2dfe39bd8 100644 --- a/homeassistant/components/camera/netatmo.py +++ b/homeassistant/components/camera/netatmo.py @@ -12,7 +12,6 @@ import voluptuous as vol from homeassistant.const import CONF_VERIFY_SSL from homeassistant.components.netatmo import CameraData from homeassistant.components.camera import (Camera, PLATFORM_SCHEMA) -from homeassistant.loader import get_component from homeassistant.helpers import config_validation as cv DEPENDENCIES = ['netatmo'] @@ -33,7 +32,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ # pylint: disable=unused-argument def setup_platform(hass, config, add_devices, discovery_info=None): """Set up access to Netatmo cameras.""" - netatmo = get_component('netatmo') + netatmo = hass.components.netatmo home = config.get(CONF_HOME) verify_ssl = config.get(CONF_VERIFY_SSL, True) import lnetatmo diff --git a/homeassistant/components/climate/netatmo.py b/homeassistant/components/climate/netatmo.py index 5d54b39e773..49452662fc4 100644 --- a/homeassistant/components/climate/netatmo.py +++ b/homeassistant/components/climate/netatmo.py @@ -13,7 +13,6 @@ from homeassistant.components.climate import ( STATE_HEAT, STATE_IDLE, ClimateDevice, PLATFORM_SCHEMA, SUPPORT_TARGET_TEMPERATURE, SUPPORT_OPERATION_MODE, SUPPORT_AWAY_MODE) from homeassistant.util import Throttle -from homeassistant.loader import get_component import homeassistant.helpers.config_validation as cv DEPENDENCIES = ['netatmo'] @@ -42,7 +41,7 @@ SUPPORT_FLAGS = (SUPPORT_TARGET_TEMPERATURE | SUPPORT_OPERATION_MODE | def setup_platform(hass, config, add_devices, discovery_info=None): """Set up the NetAtmo Thermostat.""" - netatmo = get_component('netatmo') + netatmo = hass.components.netatmo device = config.get(CONF_RELAY) import lnetatmo diff --git a/homeassistant/components/device_sun_light_trigger.py b/homeassistant/components/device_sun_light_trigger.py index a1297c5c118..641ade7308b 100644 --- a/homeassistant/components/device_sun_light_trigger.py +++ b/homeassistant/components/device_sun_light_trigger.py @@ -16,7 +16,6 @@ from homeassistant.const import STATE_HOME, STATE_NOT_HOME from homeassistant.helpers.event import ( async_track_point_in_utc_time, async_track_state_change) from homeassistant.helpers.sun import is_up, get_astral_event_next -from homeassistant.loader import get_component import homeassistant.helpers.config_validation as cv DOMAIN = 'device_sun_light_trigger' @@ -48,9 +47,9 @@ CONFIG_SCHEMA = vol.Schema({ def async_setup(hass, config): """Set up the triggers to control lights based on device presence.""" logger = logging.getLogger(__name__) - device_tracker = get_component('device_tracker') - group = get_component('group') - light = get_component('light') + device_tracker = hass.components.device_tracker + group = hass.components.group + light = hass.components.light conf = config[DOMAIN] disable_turn_off = conf.get(CONF_DISABLE_TURN_OFF) light_group = conf.get(CONF_LIGHT_GROUP, light.ENTITY_ID_ALL_LIGHTS) @@ -58,14 +57,14 @@ def async_setup(hass, config): device_group = conf.get( CONF_DEVICE_GROUP, device_tracker.ENTITY_ID_ALL_DEVICES) device_entity_ids = group.get_entity_ids( - hass, device_group, device_tracker.DOMAIN) + device_group, device_tracker.DOMAIN) if not device_entity_ids: logger.error("No devices found to track") return False # Get the light IDs from the specified group - light_ids = group.get_entity_ids(hass, light_group, light.DOMAIN) + light_ids = group.get_entity_ids(light_group, light.DOMAIN) if not light_ids: logger.error("No lights found to turn on") @@ -85,9 +84,9 @@ def async_setup(hass, config): def async_turn_on_before_sunset(light_id): """Turn on lights.""" - if not device_tracker.is_on(hass) or light.is_on(hass, light_id): + if not device_tracker.is_on() or light.is_on(light_id): return - light.async_turn_on(hass, light_id, + light.async_turn_on(light_id, transition=LIGHT_TRANSITION_TIME.seconds, profile=light_profile) @@ -129,7 +128,7 @@ def async_setup(hass, config): @callback def check_light_on_dev_state_change(entity, old_state, new_state): """Handle tracked device state changes.""" - lights_are_on = group.is_on(hass, light_group) + lights_are_on = group.is_on(light_group) light_needed = not (lights_are_on or is_up(hass)) # These variables are needed for the elif check @@ -139,7 +138,7 @@ def async_setup(hass, config): # Do we need lights? if light_needed: logger.info("Home coming event for %s. Turning lights on", entity) - light.async_turn_on(hass, light_ids, profile=light_profile) + light.async_turn_on(light_ids, profile=light_profile) # Are we in the time span were we would turn on the lights # if someone would be home? @@ -152,7 +151,7 @@ def async_setup(hass, config): # when the fading in started and turn it on if so for index, light_id in enumerate(light_ids): if now > start_point + index * LIGHT_TRANSITION_TIME: - light.async_turn_on(hass, light_id) + light.async_turn_on(light_id) else: # If this light didn't happen to be turned on yet so @@ -169,12 +168,12 @@ def async_setup(hass, config): @callback def turn_off_lights_when_all_leave(entity, old_state, new_state): """Handle device group state change.""" - if not group.is_on(hass, light_group): + if not group.is_on(light_group): return logger.info( "Everyone has left but there are lights on. Turning them off") - light.async_turn_off(hass, light_ids) + light.async_turn_off(light_ids) async_track_state_change( hass, device_group, turn_off_lights_when_all_leave, diff --git a/homeassistant/components/device_tracker/__init__.py b/homeassistant/components/device_tracker/__init__.py index 2f068481953..e1dd52a28ea 100644 --- a/homeassistant/components/device_tracker/__init__.py +++ b/homeassistant/components/device_tracker/__init__.py @@ -24,7 +24,6 @@ from homeassistant.helpers.event import async_track_time_interval from homeassistant.helpers.restore_state import async_get_last_state from homeassistant.helpers.typing import GPSType, ConfigType, HomeAssistantType import homeassistant.helpers.config_validation as cv -from homeassistant.loader import get_component import homeassistant.util as util from homeassistant.util.async_ import run_coroutine_threadsafe import homeassistant.util.dt as dt_util @@ -322,7 +321,7 @@ class DeviceTracker(object): # During init, we ignore the group if self.group and self.track_new: self.group.async_set_group( - self.hass, util.slugify(GROUP_NAME_ALL_DEVICES), visible=False, + util.slugify(GROUP_NAME_ALL_DEVICES), visible=False, name=GROUP_NAME_ALL_DEVICES, add=[device.entity_id]) self.hass.bus.async_fire(EVENT_NEW_DEVICE, { @@ -357,9 +356,9 @@ class DeviceTracker(object): entity_ids = [dev.entity_id for dev in self.devices.values() if dev.track] - self.group = get_component('group') + self.group = self.hass.components.group self.group.async_set_group( - self.hass, util.slugify(GROUP_NAME_ALL_DEVICES), visible=False, + util.slugify(GROUP_NAME_ALL_DEVICES), visible=False, name=GROUP_NAME_ALL_DEVICES, entity_ids=entity_ids) @callback diff --git a/homeassistant/components/image_processing/__init__.py b/homeassistant/components/image_processing/__init__.py index 061fd5d7074..de195ce0165 100644 --- a/homeassistant/components/image_processing/__init__.py +++ b/homeassistant/components/image_processing/__init__.py @@ -17,7 +17,6 @@ from homeassistant.exceptions import HomeAssistantError from homeassistant.loader import bind_hass from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity_component import EntityComponent -from homeassistant.loader import get_component _LOGGER = logging.getLogger(__name__) @@ -121,12 +120,12 @@ class ImageProcessingEntity(Entity): This method is a coroutine. """ - camera = get_component('camera') + camera = self.hass.components.camera image = None try: image = yield from camera.async_get_image( - self.hass, self.camera_entity, timeout=self.timeout) + self.camera_entity, timeout=self.timeout) except HomeAssistantError as err: _LOGGER.error("Error on receive image from entity: %s", err) diff --git a/homeassistant/components/light/wemo.py b/homeassistant/components/light/wemo.py index d0575105235..fcf3d2f7a7d 100644 --- a/homeassistant/components/light/wemo.py +++ b/homeassistant/components/light/wemo.py @@ -12,7 +12,6 @@ import homeassistant.util as util from homeassistant.components.light import ( Light, ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, ATTR_HS_COLOR, ATTR_TRANSITION, SUPPORT_BRIGHTNESS, SUPPORT_COLOR_TEMP, SUPPORT_COLOR, SUPPORT_TRANSITION) -from homeassistant.loader import get_component import homeassistant.util.color as color_util DEPENDENCIES = ['wemo'] @@ -151,7 +150,7 @@ class WemoDimmer(Light): @asyncio.coroutine def async_added_to_hass(self): """Register update callback.""" - wemo = get_component('wemo') + wemo = self.hass.components.wemo # The register method uses a threading condition, so call via executor. # and yield from to wait until the task is done. yield from self.hass.async_add_job( diff --git a/homeassistant/components/microsoft_face.py b/homeassistant/components/microsoft_face.py index 5a0bf2af1c4..e99d8d4a5f6 100644 --- a/homeassistant/components/microsoft_face.py +++ b/homeassistant/components/microsoft_face.py @@ -18,7 +18,6 @@ from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.aiohttp_client import async_get_clientsession import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity -from homeassistant.loader import get_component from homeassistant.util import slugify _LOGGER = logging.getLogger(__name__) @@ -231,7 +230,7 @@ def async_setup(hass, config): p_id = face.store[g_id].get(service.data[ATTR_PERSON]) camera_entity = service.data[ATTR_CAMERA_ENTITY] - camera = get_component('camera') + camera = hass.components.camera try: image = yield from camera.async_get_image(hass, camera_entity) diff --git a/homeassistant/components/mqtt_eventstream.py b/homeassistant/components/mqtt_eventstream.py index 6f6cb312f2b..aa670578172 100644 --- a/homeassistant/components/mqtt_eventstream.py +++ b/homeassistant/components/mqtt_eventstream.py @@ -10,7 +10,6 @@ import json import voluptuous as vol from homeassistant.core import callback -import homeassistant.loader as loader from homeassistant.components.mqtt import ( valid_publish_topic, valid_subscribe_topic) from homeassistant.const import ( @@ -42,7 +41,7 @@ CONFIG_SCHEMA = vol.Schema({ @asyncio.coroutine def async_setup(hass, config): """Set up the MQTT eventstream component.""" - mqtt = loader.get_component('mqtt') + mqtt = hass.components.mqtt conf = config.get(DOMAIN, {}) pub_topic = conf.get(CONF_PUBLISH_TOPIC) sub_topic = conf.get(CONF_SUBSCRIBE_TOPIC) @@ -82,7 +81,7 @@ def async_setup(hass, config): event_info = {'event_type': event.event_type, 'event_data': event.data} msg = json.dumps(event_info, cls=JSONEncoder) - mqtt.async_publish(hass, pub_topic, msg) + mqtt.async_publish(pub_topic, msg) # Only listen for local events if you are going to publish them. if pub_topic: @@ -115,7 +114,7 @@ def async_setup(hass, config): # Only subscribe if you specified a topic. if sub_topic: - yield from mqtt.async_subscribe(hass, sub_topic, _event_receiver) + yield from mqtt.async_subscribe(sub_topic, _event_receiver) hass.states.async_set('{domain}.initialized'.format(domain=DOMAIN), True) return True diff --git a/homeassistant/components/mysensors.py b/homeassistant/components/mysensors.py index 17c9129a31d..9b394457973 100644 --- a/homeassistant/components/mysensors.py +++ b/homeassistant/components/mysensors.py @@ -24,7 +24,6 @@ import homeassistant.helpers.config_validation as cv from homeassistant.helpers.dispatcher import ( async_dispatcher_connect, dispatcher_send) from homeassistant.helpers.entity import Entity -from homeassistant.loader import get_component from homeassistant.setup import setup_component REQUIREMENTS = ['pymysensors==0.11.1'] @@ -294,16 +293,16 @@ def setup(hass, config): if device == MQTT_COMPONENT: if not setup_component(hass, MQTT_COMPONENT, config): return - mqtt = get_component(MQTT_COMPONENT) + mqtt = hass.components.mqtt retain = config[DOMAIN].get(CONF_RETAIN) def pub_callback(topic, payload, qos, retain): """Call MQTT publish function.""" - mqtt.publish(hass, topic, payload, qos, retain) + mqtt.publish(topic, payload, qos, retain) def sub_callback(topic, sub_cb, qos): """Call MQTT subscribe function.""" - mqtt.subscribe(hass, topic, sub_cb, qos) + mqtt.subscribe(topic, sub_cb, qos) gateway = mysensors.MQTTGateway( pub_callback, sub_callback, event_callback=None, persistence=persistence, diff --git a/homeassistant/components/scene/__init__.py b/homeassistant/components/scene/__init__.py index 2394d538f2f..7b76836555c 100644 --- a/homeassistant/components/scene/__init__.py +++ b/homeassistant/components/scene/__init__.py @@ -5,6 +5,7 @@ For more details about this component, please refer to the documentation at https://home-assistant.io/components/scene/ """ import asyncio +import importlib import logging import voluptuous as vol @@ -16,7 +17,6 @@ import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.state import HASS_DOMAIN -from homeassistant.loader import get_platform DOMAIN = 'scene' STATE = 'scening' @@ -34,20 +34,24 @@ def _hass_domain_validator(config): def _platform_validator(config): """Validate it is a valid platform.""" - p_name = config[CONF_PLATFORM] - platform = get_platform(DOMAIN, p_name) + try: + platform = importlib.import_module( + 'homeassistant.components.scene.{}'.format( + config[CONF_PLATFORM])) + except ImportError: + raise vol.Invalid('Invalid platform specified') from None if not hasattr(platform, 'PLATFORM_SCHEMA'): return config - return getattr(platform, 'PLATFORM_SCHEMA')(config) + return platform.PLATFORM_SCHEMA(config) PLATFORM_SCHEMA = vol.Schema( vol.All( _hass_domain_validator, vol.Schema({ - vol.Required(CONF_PLATFORM): cv.platform_validator(DOMAIN) + vol.Required(CONF_PLATFORM): str }, extra=vol.ALLOW_EXTRA), _platform_validator ), extra=vol.ALLOW_EXTRA) diff --git a/homeassistant/components/sensor/bloomsky.py b/homeassistant/components/sensor/bloomsky.py index ce44abdb087..b460498c901 100644 --- a/homeassistant/components/sensor/bloomsky.py +++ b/homeassistant/components/sensor/bloomsky.py @@ -11,7 +11,6 @@ import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.const import (TEMP_FAHRENHEIT, CONF_MONITORED_CONDITIONS) from homeassistant.helpers.entity import Entity -from homeassistant.loader import get_component import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) @@ -45,7 +44,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ # pylint: disable=unused-argument def setup_platform(hass, config, add_devices, discovery_info=None): """Set up the available BloomSky weather sensors.""" - bloomsky = get_component('bloomsky') + bloomsky = hass.components.bloomsky # Default needed in case of discovery sensors = config.get(CONF_MONITORED_CONDITIONS, SENSOR_TYPES) diff --git a/homeassistant/components/sensor/netatmo.py b/homeassistant/components/sensor/netatmo.py index 4dddaf45aa4..4aeba082e55 100644 --- a/homeassistant/components/sensor/netatmo.py +++ b/homeassistant/components/sensor/netatmo.py @@ -13,7 +13,6 @@ from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.const import TEMP_CELSIUS, STATE_UNKNOWN from homeassistant.helpers.entity import Entity from homeassistant.util import Throttle -from homeassistant.loader import get_component import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) @@ -64,7 +63,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ def setup_platform(hass, config, add_devices, discovery_info=None): """Set up the available Netatmo weather sensors.""" - netatmo = get_component('netatmo') + netatmo = hass.components.netatmo data = NetAtmoData(netatmo.NETATMO_AUTH, config.get(CONF_STATION, None)) dev = [] diff --git a/homeassistant/components/zwave/__init__.py b/homeassistant/components/zwave/__init__.py index 02d2b574592..d56b4bc91b4 100644 --- a/homeassistant/components/zwave/__init__.py +++ b/homeassistant/components/zwave/__init__.py @@ -788,7 +788,7 @@ class ZWaveDeviceEntityValues(): if polling_intensity: self.primary.enable_poll(polling_intensity) - platform = get_platform(component, DOMAIN) + platform = get_platform(self._hass, component, DOMAIN) device = platform.get_device( node=self._node, values=self, node_config=node_config, hass=self._hass) diff --git a/homeassistant/config.py b/homeassistant/config.py index 2c440485e49..d69704a7032 100644 --- a/homeassistant/config.py +++ b/homeassistant/config.py @@ -548,7 +548,8 @@ def _identify_config_schema(module): return '', schema -def merge_packages_config(config, packages, _log_pkg_error=_log_pkg_error): +def merge_packages_config(hass, config, packages, + _log_pkg_error=_log_pkg_error): """Merge packages into the top-level configuration. Mutate config.""" # pylint: disable=too-many-nested-blocks PACKAGES_CONFIG_SCHEMA(packages) @@ -556,7 +557,7 @@ def merge_packages_config(config, packages, _log_pkg_error=_log_pkg_error): for comp_name, comp_conf in pack_conf.items(): if comp_name == CONF_CORE: continue - component = get_component(comp_name) + component = get_component(hass, comp_name) if component is None: _log_pkg_error(pack_name, comp_name, config, "does not exist") @@ -625,7 +626,7 @@ def async_process_component_config(hass, config, domain): This method must be run in the event loop. """ - component = get_component(domain) + component = get_component(hass, domain) if hasattr(component, 'CONFIG_SCHEMA'): try: @@ -651,7 +652,7 @@ def async_process_component_config(hass, config, domain): platforms.append(p_validated) continue - platform = get_platform(domain, p_name) + platform = get_platform(hass, domain, p_name) if platform is None: continue diff --git a/homeassistant/helpers/config_validation.py b/homeassistant/helpers/config_validation.py index 4b7c58f6e66..8177999cc94 100644 --- a/homeassistant/helpers/config_validation.py +++ b/homeassistant/helpers/config_validation.py @@ -12,7 +12,6 @@ from typing import Any, Union, TypeVar, Callable, Sequence, Dict import voluptuous as vol -from homeassistant.loader import get_platform from homeassistant.const import ( CONF_PLATFORM, CONF_SCAN_INTERVAL, TEMP_CELSIUS, TEMP_FAHRENHEIT, CONF_ALIAS, CONF_ENTITY_ID, CONF_VALUE_TEMPLATE, WEEKDAYS, @@ -283,19 +282,6 @@ def match_all(value): return value -def platform_validator(domain): - """Validate if platform exists for given domain.""" - def validator(value): - """Test if platform exists.""" - if value is None: - raise vol.Invalid('platform cannot be None') - if get_platform(domain, str(value)): - return value - raise vol.Invalid( - 'platform {} does not exist for {}'.format(value, domain)) - return validator - - def positive_timedelta(value: timedelta) -> timedelta: """Validate timedelta is positive.""" if value < timedelta(0): diff --git a/homeassistant/helpers/service.py b/homeassistant/helpers/service.py index 3595b258f12..9114a4db941 100644 --- a/homeassistant/helpers/service.py +++ b/homeassistant/helpers/service.py @@ -92,7 +92,7 @@ def extract_entity_ids(hass, service_call, expand_group=True): if not (service_call.data and ATTR_ENTITY_ID in service_call.data): return [] - group = get_component('group') + group = hass.components.group # Entity ID attr can be a list or a string service_ent_id = service_call.data[ATTR_ENTITY_ID] @@ -100,10 +100,10 @@ def extract_entity_ids(hass, service_call, expand_group=True): if expand_group: if isinstance(service_ent_id, str): - return group.expand_entity_ids(hass, [service_ent_id]) + return group.expand_entity_ids([service_ent_id]) return [ent_id for ent_id in - group.expand_entity_ids(hass, service_ent_id)] + group.expand_entity_ids(service_ent_id)] else: @@ -128,7 +128,7 @@ async def async_get_all_descriptions(hass): import homeassistant.components as components component_path = path.dirname(components.__file__) else: - component_path = path.dirname(get_component(domain).__file__) + component_path = path.dirname(get_component(hass, domain).__file__) return path.join(component_path, 'services.yaml') def load_services_files(yaml_files): diff --git a/homeassistant/helpers/template.py b/homeassistant/helpers/template.py index 3a24de6b39c..f523726c388 100644 --- a/homeassistant/helpers/template.py +++ b/homeassistant/helpers/template.py @@ -16,7 +16,7 @@ from homeassistant.const import ( from homeassistant.core import State, valid_entity_id from homeassistant.exceptions import TemplateError from homeassistant.helpers import location as loc_helper -from homeassistant.loader import bind_hass, get_component +from homeassistant.loader import bind_hass from homeassistant.util import convert from homeassistant.util import dt as dt_util from homeassistant.util import location as loc_util @@ -349,10 +349,10 @@ class TemplateMethods(object): else: gr_entity_id = str(entities) - group = get_component('group') + group = self._hass.components.group states = [self._hass.states.get(entity_id) for entity_id - in group.expand_entity_ids(self._hass, [gr_entity_id])] + in group.expand_entity_ids([gr_entity_id])] return _wrap_state(loc_helper.closest(latitude, longitude, states)) diff --git a/homeassistant/helpers/translation.py b/homeassistant/helpers/translation.py index 26cb34ede8c..f1335f73346 100644 --- a/homeassistant/helpers/translation.py +++ b/homeassistant/helpers/translation.py @@ -30,14 +30,14 @@ def flatten(data): return recursive_flatten('', data) -def component_translation_file(component, language): +def component_translation_file(hass, component, language): """Return the translation json file location for a component.""" if '.' in component: name = component.split('.', 1)[1] else: name = component - module = get_component(component) + module = get_component(hass, component) component_path = path.dirname(module.__file__) # If loading translations for the package root, (__init__.py), the @@ -97,7 +97,7 @@ async def async_get_component_resources(hass, language): missing_files = {} for component in missing_components: missing_files[component] = component_translation_file( - component, language) + hass, component, language) # Load missing files if missing_files: diff --git a/homeassistant/loader.py b/homeassistant/loader.py index a3ce2a13f56..322870952f2 100644 --- a/homeassistant/loader.py +++ b/homeassistant/loader.py @@ -6,15 +6,13 @@ documentation as possible to keep it understandable. Components can be accessed via hass.components.switch from your code. If you want to retrieve a platform that is part of a component, you should -call get_component('switch.your_platform'). In both cases the config directory -is checked to see if it contains a user provided version. If not available it -will check the built-in components and platforms. +call get_component(hass, 'switch.your_platform'). In both cases the config +directory is checked to see if it contains a user provided version. If not +available it will check the built-in components and platforms. """ import functools as ft import importlib import logging -import os -import pkgutil import sys from types import ModuleType @@ -42,135 +40,94 @@ _COMPONENT_CACHE = {} # type: Dict[str, ModuleType] _LOGGER = logging.getLogger(__name__) -def prepare(hass: 'HomeAssistant'): - """Prepare the loading of components. - - This method needs to run in an executor. - """ - global PREPARED # pylint: disable=global-statement - - # Load the built-in components - import homeassistant.components as components - - AVAILABLE_COMPONENTS.clear() - - AVAILABLE_COMPONENTS.extend( - item[1] for item in - pkgutil.iter_modules(components.__path__, 'homeassistant.components.')) - - # Look for available custom components - custom_path = hass.config.path("custom_components") - - if os.path.isdir(custom_path): - # Ensure we can load custom components using Pythons import - sys.path.insert(0, hass.config.config_dir) - - # We cannot use the same approach as for built-in components because - # custom components might only contain a platform for a component. - # ie custom_components/switch/some_platform.py. Using pkgutil would - # not give us the switch component (and neither should it). - - # Assumption: the custom_components dir only contains directories or - # python components. If this assumption is not true, HA won't break, - # just might output more errors. - for fil in os.listdir(custom_path): - if fil == '__pycache__': - continue - elif os.path.isdir(os.path.join(custom_path, fil)): - AVAILABLE_COMPONENTS.append('custom_components.{}'.format(fil)) - else: - # For files we will strip out .py extension - AVAILABLE_COMPONENTS.append( - 'custom_components.{}'.format(fil[0:-3])) - - PREPARED = True +DATA_KEY = 'components' +PATH_CUSTOM_COMPONENTS = 'custom_components' +PACKAGE_COMPONENTS = 'homeassistant.components' -def set_component(comp_name: str, component: ModuleType) -> None: +def set_component(hass, comp_name: str, component: ModuleType) -> None: """Set a component in the cache. Async friendly. """ - _check_prepared() - - _COMPONENT_CACHE[comp_name] = component + cache = hass.data.get(DATA_KEY) + if cache is None: + cache = hass.data[DATA_KEY] = {} + cache[comp_name] = component -def get_platform(domain: str, platform: str) -> Optional[ModuleType]: +def get_platform(hass, domain: str, platform: str) -> Optional[ModuleType]: """Try to load specified platform. Async friendly. """ - return get_component(PLATFORM_FORMAT.format(domain, platform)) + return get_component(hass, PLATFORM_FORMAT.format(domain, platform)) -def get_component(comp_name) -> Optional[ModuleType]: - """Try to load specified component. +def get_component(hass, comp_or_platform): + """Load a module from either custom component or built-in.""" + try: + return hass.data[DATA_KEY][comp_or_platform] + except KeyError: + pass - Looks in config dir first, then built-in components. - Only returns it if also found to be valid. - - Async friendly. - """ - if comp_name in _COMPONENT_CACHE: - return _COMPONENT_CACHE[comp_name] - - _check_prepared() - - # If we ie. try to load custom_components.switch.wemo but the parent - # custom_components.switch does not exist, importing it will trigger - # an exception because it will try to import the parent. - # Because of this behavior, we will approach loading sub components - # with caution: only load it if we can verify that the parent exists. - # We do not want to silent the ImportErrors as they provide valuable - # information to track down when debugging Home Assistant. - - # First check custom, then built-in - potential_paths = ['custom_components.{}'.format(comp_name), - 'homeassistant.components.{}'.format(comp_name)] - - for path in potential_paths: - # Validate here that root component exists - # If path contains a '.' we are specifying a sub-component - # Using rsplit we get the parent component from sub-component - root_comp = path.rsplit(".", 1)[0] if '.' in comp_name else path - - if root_comp not in AVAILABLE_COMPONENTS: - continue + # Try custom component + module = _load_module(hass.config.path(PATH_CUSTOM_COMPONENTS), + comp_or_platform) + if module is None: try: - module = importlib.import_module(path) + module = importlib.import_module( + '{}.{}'.format(PACKAGE_COMPONENTS, comp_or_platform)) + except ImportError: + module = None - # In Python 3 you can import files from directories that do not - # contain the file __init__.py. A directory is a valid module if - # it contains a file with the .py extension. In this case Python - # will succeed in importing the directory as a module and call it - # a namespace. We do not care about namespaces. - # This prevents that when only - # custom_components/switch/some_platform.py exists, - # the import custom_components.switch would succeed. - if module.__spec__.origin == 'namespace': - continue + cache = hass.data.get(DATA_KEY) + if cache is None: + cache = hass.data[DATA_KEY] = {} + cache[comp_or_platform] = module - _LOGGER.info("Loaded %s from %s", comp_name, path) + return module - _COMPONENT_CACHE[comp_name] = module - - return module - - except ImportError as err: - # This error happens if for example custom_components/switch - # exists and we try to load switch.demo. - if str(err) != "No module named '{}'".format(path): - _LOGGER.exception( - ("Error loading %s. Make sure all " - "dependencies are installed"), path) - - _LOGGER.error("Unable to find component %s", comp_name) +def _find_spec(path, name): + for finder in sys.meta_path: + try: + spec = finder.find_spec(name, path=path) + if spec is not None: + return spec + except AttributeError: + # Not all finders have the find_spec method + pass return None +def _load_module(path, name): + """Load a module based on a folder and a name.""" + spec = _find_spec([path], name) + + # Special handling if loading platforms and the folder is a namespace + # (namespace is a folder without __init__.py) + if spec is None and '.' in name: + parent_spec = _find_spec([path], name.split('.')[0]) + if (parent_spec is None or + parent_spec.submodule_search_locations is None): + return None + spec = _find_spec(parent_spec.submodule_search_locations, name) + + # Not found + if spec is None: + return None + + # This is a namespace + if spec.loader is None: + return None + + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + return module + + class Components: """Helper to load components.""" @@ -180,7 +137,7 @@ class Components: def __getattr__(self, comp_name): """Fetch a component.""" - component = get_component(comp_name) + component = get_component(self._hass, comp_name) if component is None: raise ImportError('Unable to load {}'.format(comp_name)) wrapped = ModuleWrapper(self._hass, component) @@ -230,7 +187,7 @@ def bind_hass(func): return func -def load_order_component(comp_name: str) -> OrderedSet: +def load_order_component(hass, comp_name: str) -> OrderedSet: """Return an OrderedSet of components in the correct order of loading. Raises HomeAssistantError if a circular dependency is detected. @@ -238,16 +195,16 @@ def load_order_component(comp_name: str) -> OrderedSet: Async friendly. """ - return _load_order_component(comp_name, OrderedSet(), set()) + return _load_order_component(hass, comp_name, OrderedSet(), set()) -def _load_order_component(comp_name: str, load_order: OrderedSet, +def _load_order_component(hass, comp_name: str, load_order: OrderedSet, loading: Set) -> OrderedSet: """Recursive function to get load order of components. Async friendly. """ - component = get_component(comp_name) + component = get_component(hass, comp_name) # If None it does not exist, error already thrown by get_component. if component is None: @@ -266,7 +223,8 @@ def _load_order_component(comp_name: str, load_order: OrderedSet, comp_name, dependency) return OrderedSet() - dep_load_order = _load_order_component(dependency, load_order, loading) + dep_load_order = _load_order_component( + hass, dependency, load_order, loading) # length == 0 means error loading dependency or children if not dep_load_order: @@ -280,14 +238,3 @@ def _load_order_component(comp_name: str, load_order: OrderedSet, loading.remove(comp_name) return load_order - - -def _check_prepared() -> None: - """Issue a warning if loader.prepare() has never been called. - - Async friendly. - """ - if not PREPARED: - _LOGGER.warning(( - "You did not call loader.prepare() yet. " - "Certain functionality might not be working")) diff --git a/homeassistant/scripts/check_config.py b/homeassistant/scripts/check_config.py index 4375d973a0b..3a1ffa82d47 100644 --- a/homeassistant/scripts/check_config.py +++ b/homeassistant/scripts/check_config.py @@ -16,8 +16,8 @@ from homeassistant import bootstrap, core, loader from homeassistant.config import ( get_default_config_dir, CONF_CORE, CORE_CONFIG_SCHEMA, CONF_PACKAGES, merge_packages_config, _format_config_error, - find_config_file, load_yaml_config_file, get_component, - extract_domain_configs, config_per_platform, get_platform) + find_config_file, load_yaml_config_file, + extract_domain_configs, config_per_platform) import homeassistant.util.yaml as yaml from homeassistant.exceptions import HomeAssistantError @@ -201,18 +201,10 @@ def check(config_dir, secrets=False): yaml.yaml.SafeLoader.add_constructor('!secret', yaml._secret_yaml) try: - class HassConfig(): - """Hass object with config.""" - - def __init__(self, conf_dir): - """Init the config_dir.""" - self.config = core.Config() - self.config.config_dir = conf_dir - - loader.prepare(HassConfig(config_dir)) - - res['components'] = check_ha_config_file(config_dir) + hass = core.HomeAssistant() + hass.config.config_dir = config_dir + res['components'] = check_ha_config_file(hass) res['secret_cache'] = OrderedDict(yaml.__SECRET_CACHE) for err in res['components'].errors: @@ -222,6 +214,7 @@ def check(config_dir, secrets=False): res['except'].setdefault(domain, []).append(err.config) except Exception as err: # pylint: disable=broad-except + _LOGGER.exception("BURB") print(color('red', 'Fatal error while loading config:'), str(err)) res['except'].setdefault(ERROR_STR, []).append(str(err)) finally: @@ -290,8 +283,9 @@ class HomeAssistantConfig(OrderedDict): return self -def check_ha_config_file(config_dir): +def check_ha_config_file(hass): """Check if Home Assistant configuration file is valid.""" + config_dir = hass.config.config_dir result = HomeAssistantConfig() def _pack_error(package, component, config, message): @@ -330,7 +324,7 @@ def check_ha_config_file(config_dir): # Merge packages merge_packages_config( - config, core_config.get(CONF_PACKAGES, {}), _pack_error) + hass, config, core_config.get(CONF_PACKAGES, {}), _pack_error) del core_config[CONF_PACKAGES] # Ensure we have no None values after merge @@ -343,7 +337,7 @@ def check_ha_config_file(config_dir): # Process and validate config for domain in components: - component = get_component(domain) + component = loader.get_component(hass, domain) if not component: result.add_error("Component not found: {}".format(domain)) continue @@ -375,7 +369,7 @@ def check_ha_config_file(config_dir): platforms.append(p_validated) continue - platform = get_platform(domain, p_name) + platform = loader.get_platform(hass, domain, p_name) if platform is None: result.add_error( diff --git a/homeassistant/setup.py b/homeassistant/setup.py index 169a160af65..f26aa9b61f1 100644 --- a/homeassistant/setup.py +++ b/homeassistant/setup.py @@ -98,14 +98,14 @@ async def _async_setup_component(hass: core.HomeAssistant, _LOGGER.error("Setup failed for %s: %s", domain, msg) async_notify_setup_error(hass, domain, link) - component = loader.get_component(domain) + component = loader.get_component(hass, domain) if not component: log_error("Component not found.", False) return False # Validate no circular dependencies - components = loader.load_order_component(domain) + components = loader.load_order_component(hass, domain) # OrderedSet is empty if component or dependencies could not be resolved if not components: @@ -159,7 +159,7 @@ async def _async_setup_component(hass: core.HomeAssistant, elif result is not True: log_error("Component did not return boolean if setup was successful. " "Disabling component.") - loader.set_component(domain, None) + loader.set_component(hass, domain, None) return False for entry in hass.config_entries.async_entries(domain): @@ -193,7 +193,7 @@ async def async_prepare_setup_platform(hass: core.HomeAssistant, config, platform_path, msg) async_notify_setup_error(hass, platform_path) - platform = loader.get_platform(domain, platform_name) + platform = loader.get_platform(hass, domain, platform_name) # Not found if platform is None: diff --git a/tests/common.py b/tests/common.py index b04abda7c28..f53d1c2be2b 100644 --- a/tests/common.py +++ b/tests/common.py @@ -10,8 +10,7 @@ import logging import threading from contextlib import contextmanager -from homeassistant import ( - auth, core as ha, loader, data_entry_flow, config_entries) +from homeassistant import auth, core as ha, data_entry_flow, config_entries from homeassistant.setup import setup_component, async_setup_component from homeassistant.config import async_process_component_config from homeassistant.helpers import ( @@ -138,9 +137,6 @@ def async_test_home_assistant(loop): hass.config.units = METRIC_SYSTEM hass.config.skip_pip = True - if 'custom_components.test' not in loader.AVAILABLE_COMPONENTS: - yield from loop.run_in_executor(None, loader.prepare, hass) - hass.state = ha.CoreState.running # Mock async_start diff --git a/tests/components/climate/test_generic_thermostat.py b/tests/components/climate/test_generic_thermostat.py index bd0b764c6fe..7bc0b0a18e7 100644 --- a/tests/components/climate/test_generic_thermostat.py +++ b/tests/components/climate/test_generic_thermostat.py @@ -116,7 +116,7 @@ class TestGenericThermostatHeaterSwitching(unittest.TestCase): def test_heater_switch(self): """Test heater switching test switch.""" - platform = loader.get_component('switch.test') + platform = loader.get_component(self.hass, 'switch.test') platform.init() self.switch_1 = platform.DEVICES[1] assert setup_component(self.hass, switch.DOMAIN, {'switch': { diff --git a/tests/components/config/test_config_entries.py b/tests/components/config/test_config_entries.py index f53be8818a3..84d15578e13 100644 --- a/tests/components/config/test_config_entries.py +++ b/tests/components/config/test_config_entries.py @@ -17,10 +17,10 @@ from homeassistant.loader import set_component from tests.common import MockConfigEntry, MockModule, mock_coro_func -@pytest.fixture(scope='session', autouse=True) -def mock_test_component(): +@pytest.fixture(autouse=True) +def mock_test_component(hass): """Ensure a component called 'test' exists.""" - set_component('test', MockModule('test')) + set_component(hass, 'test', MockModule('test')) @pytest.fixture @@ -172,7 +172,8 @@ def test_abort(hass, client): def test_create_account(hass, client): """Test a flow that creates an account.""" set_component( - 'test', MockModule('test', async_setup_entry=mock_coro_func(True))) + hass, 'test', + MockModule('test', async_setup_entry=mock_coro_func(True))) class TestFlow(FlowHandler): VERSION = 1 @@ -204,7 +205,8 @@ def test_create_account(hass, client): def test_two_step_flow(hass, client): """Test we can finish a two step flow.""" set_component( - 'test', MockModule('test', async_setup_entry=mock_coro_func(True))) + hass, 'test', + MockModule('test', async_setup_entry=mock_coro_func(True))) class TestFlow(FlowHandler): VERSION = 1 diff --git a/tests/components/device_tracker/test_asuswrt.py b/tests/components/device_tracker/test_asuswrt.py index d2ae8965668..0cbece6d1b0 100644 --- a/tests/components/device_tracker/test_asuswrt.py +++ b/tests/components/device_tracker/test_asuswrt.py @@ -3,9 +3,9 @@ import os from datetime import timedelta import unittest from unittest import mock +import socket import voluptuous as vol -from future.backports import socket from homeassistant.setup import setup_component from homeassistant.components import device_tracker diff --git a/tests/components/device_tracker/test_init.py b/tests/components/device_tracker/test_init.py index 912bd315ecd..0b17b4e0ac8 100644 --- a/tests/components/device_tracker/test_init.py +++ b/tests/components/device_tracker/test_init.py @@ -189,7 +189,7 @@ class TestComponentsDeviceTracker(unittest.TestCase): def test_update_stale(self): """Test stalled update.""" - scanner = get_component('device_tracker.test').SCANNER + scanner = get_component(self.hass, 'device_tracker.test').SCANNER scanner.reset() scanner.come_home('DEV1') @@ -251,7 +251,7 @@ class TestComponentsDeviceTracker(unittest.TestCase): hide_if_away=True) device_tracker.update_config(self.yaml_devices, dev_id, device) - scanner = get_component('device_tracker.test').SCANNER + scanner = get_component(self.hass, 'device_tracker.test').SCANNER scanner.reset() with assert_setup_component(1, device_tracker.DOMAIN): @@ -270,7 +270,7 @@ class TestComponentsDeviceTracker(unittest.TestCase): hide_if_away=True) device_tracker.update_config(self.yaml_devices, dev_id, device) - scanner = get_component('device_tracker.test').SCANNER + scanner = get_component(self.hass, 'device_tracker.test').SCANNER scanner.reset() with assert_setup_component(1, device_tracker.DOMAIN): @@ -431,7 +431,7 @@ class TestComponentsDeviceTracker(unittest.TestCase): 'zone': zone_info }) - scanner = get_component('device_tracker.test').SCANNER + scanner = get_component(self.hass, 'device_tracker.test').SCANNER scanner.reset() scanner.come_home('dev1') @@ -547,7 +547,7 @@ def test_bad_platform(hass): async def test_adding_unknown_device_to_config(mock_device_tracker_conf, hass): """Test the adding of unknown devices to configuration file.""" - scanner = get_component('device_tracker.test').SCANNER + scanner = get_component(hass, 'device_tracker.test').SCANNER scanner.reset() scanner.come_home('DEV1') diff --git a/tests/components/light/test_init.py b/tests/components/light/test_init.py index 4e8fad261bd..634e3774b8a 100644 --- a/tests/components/light/test_init.py +++ b/tests/components/light/test_init.py @@ -118,7 +118,7 @@ class TestLight(unittest.TestCase): def test_services(self): """Test the provided services.""" - platform = loader.get_component('light.test') + platform = loader.get_component(self.hass, 'light.test') platform.init() self.assertTrue( @@ -267,7 +267,7 @@ class TestLight(unittest.TestCase): def test_broken_light_profiles(self): """Test light profiles.""" - platform = loader.get_component('light.test') + platform = loader.get_component(self.hass, 'light.test') platform.init() user_light_file = self.hass.config.path(light.LIGHT_PROFILES_FILE) @@ -282,7 +282,7 @@ class TestLight(unittest.TestCase): def test_light_profiles(self): """Test light profiles.""" - platform = loader.get_component('light.test') + platform = loader.get_component(self.hass, 'light.test') platform.init() user_light_file = self.hass.config.path(light.LIGHT_PROFILES_FILE) diff --git a/tests/components/scene/test_init.py b/tests/components/scene/test_init.py index 25ea818c774..a832e249832 100644 --- a/tests/components/scene/test_init.py +++ b/tests/components/scene/test_init.py @@ -16,7 +16,7 @@ class TestScene(unittest.TestCase): def setUp(self): # pylint: disable=invalid-name """Setup things to be run when tests are started.""" self.hass = get_test_home_assistant() - test_light = loader.get_component('light.test') + test_light = loader.get_component(self.hass, 'light.test') test_light.init() self.assertTrue(setup_component(self.hass, light.DOMAIN, { diff --git a/tests/components/switch/test_flux.py b/tests/components/switch/test_flux.py index c42061db958..61e665f265c 100644 --- a/tests/components/switch/test_flux.py +++ b/tests/components/switch/test_flux.py @@ -71,7 +71,7 @@ class TestSwitchFlux(unittest.TestCase): def test_flux_when_switch_is_off(self): """Test the flux switch when it is off.""" - platform = loader.get_component('light.test') + platform = loader.get_component(self.hass, 'light.test') platform.init() self.assertTrue( setup_component(self.hass, light.DOMAIN, @@ -113,7 +113,7 @@ class TestSwitchFlux(unittest.TestCase): def test_flux_before_sunrise(self): """Test the flux switch before sunrise.""" - platform = loader.get_component('light.test') + platform = loader.get_component(self.hass, 'light.test') platform.init() self.assertTrue( setup_component(self.hass, light.DOMAIN, @@ -160,7 +160,7 @@ class TestSwitchFlux(unittest.TestCase): # pylint: disable=invalid-name def test_flux_after_sunrise_before_sunset(self): """Test the flux switch after sunrise and before sunset.""" - platform = loader.get_component('light.test') + platform = loader.get_component(self.hass, 'light.test') platform.init() self.assertTrue( setup_component(self.hass, light.DOMAIN, @@ -207,7 +207,7 @@ class TestSwitchFlux(unittest.TestCase): # pylint: disable=invalid-name def test_flux_after_sunset_before_stop(self): """Test the flux switch after sunset and before stop.""" - platform = loader.get_component('light.test') + platform = loader.get_component(self.hass, 'light.test') platform.init() self.assertTrue( setup_component(self.hass, light.DOMAIN, @@ -255,7 +255,7 @@ class TestSwitchFlux(unittest.TestCase): # pylint: disable=invalid-name def test_flux_after_stop_before_sunrise(self): """Test the flux switch after stop and before sunrise.""" - platform = loader.get_component('light.test') + platform = loader.get_component(self.hass, 'light.test') platform.init() self.assertTrue( setup_component(self.hass, light.DOMAIN, @@ -302,7 +302,7 @@ class TestSwitchFlux(unittest.TestCase): # pylint: disable=invalid-name def test_flux_with_custom_start_stop_times(self): """Test the flux with custom start and stop times.""" - platform = loader.get_component('light.test') + platform = loader.get_component(self.hass, 'light.test') platform.init() self.assertTrue( setup_component(self.hass, light.DOMAIN, @@ -353,7 +353,7 @@ class TestSwitchFlux(unittest.TestCase): This test has the stop_time on the next day (after midnight). """ - platform = loader.get_component('light.test') + platform = loader.get_component(self.hass, 'light.test') platform.init() self.assertTrue( setup_component(self.hass, light.DOMAIN, @@ -405,7 +405,7 @@ class TestSwitchFlux(unittest.TestCase): This test has the stop_time on the next day (after midnight). """ - platform = loader.get_component('light.test') + platform = loader.get_component(self.hass, 'light.test') platform.init() self.assertTrue( setup_component(self.hass, light.DOMAIN, @@ -456,7 +456,7 @@ class TestSwitchFlux(unittest.TestCase): This test has the stop_time on the next day (after midnight). """ - platform = loader.get_component('light.test') + platform = loader.get_component(self.hass, 'light.test') platform.init() self.assertTrue( setup_component(self.hass, light.DOMAIN, @@ -507,7 +507,7 @@ class TestSwitchFlux(unittest.TestCase): This test has the stop_time on the next day (after midnight). """ - platform = loader.get_component('light.test') + platform = loader.get_component(self.hass, 'light.test') platform.init() self.assertTrue( setup_component(self.hass, light.DOMAIN, @@ -558,7 +558,7 @@ class TestSwitchFlux(unittest.TestCase): This test has the stop_time on the next day (after midnight). """ - platform = loader.get_component('light.test') + platform = loader.get_component(self.hass, 'light.test') platform.init() self.assertTrue( setup_component(self.hass, light.DOMAIN, @@ -606,7 +606,7 @@ class TestSwitchFlux(unittest.TestCase): # pylint: disable=invalid-name def test_flux_with_custom_colortemps(self): """Test the flux with custom start and stop colortemps.""" - platform = loader.get_component('light.test') + platform = loader.get_component(self.hass, 'light.test') platform.init() self.assertTrue( setup_component(self.hass, light.DOMAIN, @@ -656,7 +656,7 @@ class TestSwitchFlux(unittest.TestCase): # pylint: disable=invalid-name def test_flux_with_custom_brightness(self): """Test the flux with custom start and stop colortemps.""" - platform = loader.get_component('light.test') + platform = loader.get_component(self.hass, 'light.test') platform.init() self.assertTrue( setup_component(self.hass, light.DOMAIN, @@ -704,7 +704,7 @@ class TestSwitchFlux(unittest.TestCase): def test_flux_with_multiple_lights(self): """Test the flux switch with multiple light entities.""" - platform = loader.get_component('light.test') + platform = loader.get_component(self.hass, 'light.test') platform.init() self.assertTrue( setup_component(self.hass, light.DOMAIN, @@ -773,7 +773,7 @@ class TestSwitchFlux(unittest.TestCase): def test_flux_with_mired(self): """Test the flux switch´s mode mired.""" - platform = loader.get_component('light.test') + platform = loader.get_component(self.hass, 'light.test') platform.init() self.assertTrue( setup_component(self.hass, light.DOMAIN, @@ -818,7 +818,7 @@ class TestSwitchFlux(unittest.TestCase): def test_flux_with_rgb(self): """Test the flux switch´s mode rgb.""" - platform = loader.get_component('light.test') + platform = loader.get_component(self.hass, 'light.test') platform.init() self.assertTrue( setup_component(self.hass, light.DOMAIN, diff --git a/tests/components/switch/test_init.py b/tests/components/switch/test_init.py index 090e3c74bf1..d679aa2c827 100644 --- a/tests/components/switch/test_init.py +++ b/tests/components/switch/test_init.py @@ -17,7 +17,7 @@ class TestSwitch(unittest.TestCase): def setUp(self): """Setup things to be run when tests are started.""" self.hass = get_test_home_assistant() - platform = loader.get_component('switch.test') + platform = loader.get_component(self.hass, 'switch.test') platform.init() # Switch 1 is ON, switch 2 is OFF self.switch_1, self.switch_2, self.switch_3 = \ @@ -79,10 +79,10 @@ class TestSwitch(unittest.TestCase): def test_setup_two_platforms(self): """Test with bad configuration.""" # Test if switch component returns 0 switches - test_platform = loader.get_component('switch.test') + test_platform = loader.get_component(self.hass, 'switch.test') test_platform.init(True) - loader.set_component('switch.test2', test_platform) + loader.set_component(self.hass, 'switch.test2', test_platform) test_platform.init(False) self.assertTrue(setup_component( diff --git a/tests/components/test_device_sun_light_trigger.py b/tests/components/test_device_sun_light_trigger.py index 3c73e85c4e5..a8b8a201217 100644 --- a/tests/components/test_device_sun_light_trigger.py +++ b/tests/components/test_device_sun_light_trigger.py @@ -22,12 +22,12 @@ class TestDeviceSunLightTrigger(unittest.TestCase): self.hass = get_test_home_assistant() self.scanner = loader.get_component( - 'device_tracker.test').get_scanner(None, None) + self.hass, 'device_tracker.test').get_scanner(None, None) self.scanner.reset() self.scanner.come_home('DEV1') - loader.get_component('light.test').init() + loader.get_component(self.hass, 'light.test').init() with patch( 'homeassistant.components.device_tracker.load_yaml_config_file', diff --git a/tests/helpers/test_config_validation.py b/tests/helpers/test_config_validation.py index 90be56bbc7c..aff0acf9e3a 100644 --- a/tests/helpers/test_config_validation.py +++ b/tests/helpers/test_config_validation.py @@ -10,8 +10,6 @@ import voluptuous as vol import homeassistant.helpers.config_validation as cv -from tests.common import get_test_home_assistant - def test_boolean(): """Test boolean validation.""" @@ -256,24 +254,6 @@ def test_event_schema(): cv.EVENT_SCHEMA(value) -def test_platform_validator(): - """Test platform validation.""" - hass = None - - try: - hass = get_test_home_assistant() - - schema = vol.Schema(cv.platform_validator('light')) - - with pytest.raises(vol.MultipleInvalid): - schema('platform_that_does_not_exist') - - schema('hue') - finally: - if hass is not None: - hass.stop() - - def test_icon(): """Test icon validation.""" schema = vol.Schema(cv.icon) diff --git a/tests/helpers/test_discovery.py b/tests/helpers/test_discovery.py index b345400ba17..c7b39954d85 100644 --- a/tests/helpers/test_discovery.py +++ b/tests/helpers/test_discovery.py @@ -129,11 +129,11 @@ class TestHelpersDiscovery: platform_calls.append('disc' if discovery_info else 'component') loader.set_component( - 'test_component', + self.hass, 'test_component', MockModule('test_component', setup=component_setup)) loader.set_component( - 'switch.test_circular', + self.hass, 'switch.test_circular', MockPlatform(setup_platform, dependencies=['test_component'])) @@ -177,11 +177,11 @@ class TestHelpersDiscovery: return True loader.set_component( - 'test_component1', + self.hass, 'test_component1', MockModule('test_component1', setup=component1_setup)) loader.set_component( - 'test_component2', + self.hass, 'test_component2', MockModule('test_component2', setup=component2_setup)) @callback diff --git a/tests/helpers/test_entity_component.py b/tests/helpers/test_entity_component.py index 0bc6a7601dc..504f31cc987 100644 --- a/tests/helpers/test_entity_component.py +++ b/tests/helpers/test_entity_component.py @@ -75,9 +75,9 @@ class TestHelpersEntityComponent(unittest.TestCase): component_setup = Mock(return_value=True) platform_setup = Mock(return_value=None) loader.set_component( - 'test_component', + self.hass, 'test_component', MockModule('test_component', setup=component_setup)) - loader.set_component('test_domain.mod2', + loader.set_component(self.hass, 'test_domain.mod2', MockPlatform(platform_setup, ['test_component'])) component = EntityComponent(_LOGGER, DOMAIN, self.hass) @@ -100,8 +100,10 @@ class TestHelpersEntityComponent(unittest.TestCase): platform1_setup = Mock(side_effect=Exception('Broken')) platform2_setup = Mock(return_value=None) - loader.set_component('test_domain.mod1', MockPlatform(platform1_setup)) - loader.set_component('test_domain.mod2', MockPlatform(platform2_setup)) + loader.set_component(self.hass, 'test_domain.mod1', + MockPlatform(platform1_setup)) + loader.set_component(self.hass, 'test_domain.mod2', + MockPlatform(platform2_setup)) component = EntityComponent(_LOGGER, DOMAIN, self.hass) @@ -145,7 +147,7 @@ class TestHelpersEntityComponent(unittest.TestCase): """Test the platform setup.""" add_devices([MockEntity(should_poll=True)]) - loader.set_component('test_domain.platform', + loader.set_component(self.hass, 'test_domain.platform', MockPlatform(platform_setup)) component = EntityComponent(_LOGGER, DOMAIN, self.hass) @@ -172,7 +174,7 @@ class TestHelpersEntityComponent(unittest.TestCase): platform = MockPlatform(platform_setup) - loader.set_component('test_domain.platform', platform) + loader.set_component(self.hass, 'test_domain.platform', platform) component = EntityComponent(_LOGGER, DOMAIN, self.hass) @@ -220,7 +222,8 @@ def test_platform_not_ready(hass): """Test that we retry when platform not ready.""" platform1_setup = Mock(side_effect=[PlatformNotReady, PlatformNotReady, None]) - loader.set_component('test_domain.mod1', MockPlatform(platform1_setup)) + loader.set_component(hass, 'test_domain.mod1', + MockPlatform(platform1_setup)) component = EntityComponent(_LOGGER, DOMAIN, hass) @@ -316,10 +319,11 @@ def test_setup_dependencies_platform(hass): We're explictely testing that we process dependencies even if a component with the same name has already been loaded. """ - loader.set_component('test_component', MockModule('test_component')) - loader.set_component('test_component2', MockModule('test_component2')) + loader.set_component(hass, 'test_component', MockModule('test_component')) + loader.set_component(hass, 'test_component2', + MockModule('test_component2')) loader.set_component( - 'test_domain.test_component', + hass, 'test_domain.test_component', MockPlatform(dependencies=['test_component', 'test_component2'])) component = EntityComponent(_LOGGER, DOMAIN, hass) @@ -341,7 +345,7 @@ async def test_setup_entry(hass): """Test setup entry calls async_setup_entry on platform.""" mock_setup_entry = Mock(return_value=mock_coro(True)) loader.set_component( - 'test_domain.entry_domain', + hass, 'test_domain.entry_domain', MockPlatform(async_setup_entry=mock_setup_entry)) component = EntityComponent(_LOGGER, DOMAIN, hass) @@ -366,7 +370,7 @@ async def test_setup_entry_fails_duplicate(hass): """Test we don't allow setting up a config entry twice.""" mock_setup_entry = Mock(return_value=mock_coro(True)) loader.set_component( - 'test_domain.entry_domain', + hass, 'test_domain.entry_domain', MockPlatform(async_setup_entry=mock_setup_entry)) component = EntityComponent(_LOGGER, DOMAIN, hass) @@ -382,7 +386,7 @@ async def test_unload_entry_resets_platform(hass): """Test unloading an entry removes all entities.""" mock_setup_entry = Mock(return_value=mock_coro(True)) loader.set_component( - 'test_domain.entry_domain', + hass, 'test_domain.entry_domain', MockPlatform(async_setup_entry=mock_setup_entry)) component = EntityComponent(_LOGGER, DOMAIN, hass) diff --git a/tests/helpers/test_entity_platform.py b/tests/helpers/test_entity_platform.py index 2018cb27541..4e09f9576f2 100644 --- a/tests/helpers/test_entity_platform.py +++ b/tests/helpers/test_entity_platform.py @@ -147,7 +147,7 @@ class TestHelpersEntityPlatform(unittest.TestCase): platform = MockPlatform(platform_setup) platform.SCAN_INTERVAL = timedelta(seconds=30) - loader.set_component('test_domain.platform', platform) + loader.set_component(self.hass, 'test_domain.platform', platform) component = EntityComponent(_LOGGER, DOMAIN, self.hass) @@ -184,7 +184,7 @@ def test_platform_warn_slow_setup(hass): """Warn we log when platform setup takes a long time.""" platform = MockPlatform() - loader.set_component('test_domain.platform', platform) + loader.set_component(hass, 'test_domain.platform', platform) component = EntityComponent(_LOGGER, DOMAIN, hass) @@ -218,7 +218,7 @@ def test_platform_error_slow_setup(hass, caplog): platform = MockPlatform(async_setup_platform=setup_platform) component = EntityComponent(_LOGGER, DOMAIN, hass) - loader.set_component('test_domain.test_platform', platform) + loader.set_component(hass, 'test_domain.test_platform', platform) yield from component.async_setup({ DOMAIN: { 'platform': 'test_platform', @@ -260,7 +260,7 @@ def test_parallel_updates_async_platform(hass): platform.async_setup_platform = mock_update - loader.set_component('test_domain.platform', platform) + loader.set_component(hass, 'test_domain.platform', platform) component = EntityComponent(_LOGGER, DOMAIN, hass) component._platforms = {} @@ -288,7 +288,7 @@ def test_parallel_updates_async_platform_with_constant(hass): platform.async_setup_platform = mock_update platform.PARALLEL_UPDATES = 1 - loader.set_component('test_domain.platform', platform) + loader.set_component(hass, 'test_domain.platform', platform) component = EntityComponent(_LOGGER, DOMAIN, hass) component._platforms = {} @@ -309,7 +309,7 @@ def test_parallel_updates_sync_platform(hass): """Warn we log when platform setup takes a long time.""" platform = MockPlatform(setup_platform=lambda *args: None) - loader.set_component('test_domain.platform', platform) + loader.set_component(hass, 'test_domain.platform', platform) component = EntityComponent(_LOGGER, DOMAIN, hass) component._platforms = {} diff --git a/tests/helpers/test_service.py b/tests/helpers/test_service.py index a987f5130f1..79054726c03 100644 --- a/tests/helpers/test_service.py +++ b/tests/helpers/test_service.py @@ -138,7 +138,7 @@ class TestServiceHelpers(unittest.TestCase): self.hass.states.set('light.Ceiling', STATE_OFF) self.hass.states.set('light.Kitchen', STATE_OFF) - loader.get_component('group').Group.create_group( + loader.get_component(self.hass, 'group').Group.create_group( self.hass, 'test', ['light.Ceiling', 'light.Kitchen']) call = ha.ServiceCall('light', 'turn_on', @@ -160,7 +160,7 @@ class TestServiceHelpers(unittest.TestCase): @asyncio.coroutine def test_async_get_all_descriptions(hass): """Test async_get_all_descriptions.""" - group = loader.get_component('group') + group = loader.get_component(hass, 'group') group_config = {group.DOMAIN: {}} yield from async_setup_component(hass, group.DOMAIN, group_config) descriptions = yield from service.async_get_all_descriptions(hass) @@ -170,7 +170,7 @@ def test_async_get_all_descriptions(hass): assert 'description' in descriptions['group']['reload'] assert 'fields' in descriptions['group']['reload'] - logger = loader.get_component('logger') + logger = loader.get_component(hass, 'logger') logger_config = {logger.DOMAIN: {}} yield from async_setup_component(hass, logger.DOMAIN, logger_config) descriptions = yield from service.async_get_all_descriptions(hass) diff --git a/tests/helpers/test_translation.py b/tests/helpers/test_translation.py index c72efca8c29..99c6f7dddf1 100644 --- a/tests/helpers/test_translation.py +++ b/tests/helpers/test_translation.py @@ -50,15 +50,15 @@ async def test_component_translation_file(hass): }) assert path.normpath(translation.component_translation_file( - 'switch.test', 'en')) == path.normpath(hass.config.path( + hass, 'switch.test', 'en')) == path.normpath(hass.config.path( 'custom_components', 'switch', '.translations', 'test.en.json')) assert path.normpath(translation.component_translation_file( - 'test_standalone', 'en')) == path.normpath(hass.config.path( + hass, 'test_standalone', 'en')) == path.normpath(hass.config.path( 'custom_components', '.translations', 'test_standalone.en.json')) assert path.normpath(translation.component_translation_file( - 'test_package', 'en')) == path.normpath(hass.config.path( + hass, 'test_package', 'en')) == path.normpath(hass.config.path( 'custom_components', 'test_package', '.translations', 'en.json')) diff --git a/tests/scripts/test_check_config.py b/tests/scripts/test_check_config.py index 28a3f2ebdc8..8dfc5db90e0 100644 --- a/tests/scripts/test_check_config.py +++ b/tests/scripts/test_check_config.py @@ -7,7 +7,6 @@ from unittest.mock import patch import homeassistant.scripts.check_config as check_config from homeassistant.config import YAML_CONFIG_FILE -from homeassistant.loader import set_component from tests.common import patch_yaml_files, get_test_config_dir _LOGGER = logging.getLogger(__name__) @@ -106,7 +105,6 @@ class TestCheckConfig(unittest.TestCase): def test_component_platform_not_found(self, isfile_patch): """Test errors if component or platform not found.""" # Make sure they don't exist - set_component('beer', None) files = { YAML_CONFIG_FILE: BASE_CONFIG + 'beer:', } @@ -119,7 +117,6 @@ class TestCheckConfig(unittest.TestCase): assert res['secrets'] == {} assert len(res['yaml_files']) == 1 - set_component('light.beer', None) files = { YAML_CONFIG_FILE: BASE_CONFIG + 'light:\n platform: beer', } diff --git a/tests/test_config.py b/tests/test_config.py index 652b931366a..4b1115c3814 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -568,7 +568,7 @@ def merge_log_err(hass): yield logerr -def test_merge(merge_log_err): +def test_merge(merge_log_err, hass): """Test if we can merge packages.""" packages = { 'pack_dict': {'input_boolean': {'ib1': None}}, @@ -582,7 +582,7 @@ def test_merge(merge_log_err): 'input_boolean': {'ib2': None}, 'light': {'platform': 'test'} } - config_util.merge_packages_config(config, packages) + config_util.merge_packages_config(hass, config, packages) assert merge_log_err.call_count == 0 assert len(config) == 5 @@ -592,7 +592,7 @@ def test_merge(merge_log_err): assert config['wake_on_lan'] is None -def test_merge_try_falsy(merge_log_err): +def test_merge_try_falsy(merge_log_err, hass): """Ensure we dont add falsy items like empty OrderedDict() to list.""" packages = { 'pack_falsy_to_lst': {'automation': OrderedDict()}, @@ -603,7 +603,7 @@ def test_merge_try_falsy(merge_log_err): 'automation': {'do': 'something'}, 'light': {'some': 'light'}, } - config_util.merge_packages_config(config, packages) + config_util.merge_packages_config(hass, config, packages) assert merge_log_err.call_count == 0 assert len(config) == 3 @@ -611,7 +611,7 @@ def test_merge_try_falsy(merge_log_err): assert len(config['light']) == 1 -def test_merge_new(merge_log_err): +def test_merge_new(merge_log_err, hass): """Test adding new components to outer scope.""" packages = { 'pack_1': {'light': [{'platform': 'one'}]}, @@ -624,7 +624,7 @@ def test_merge_new(merge_log_err): config = { config_util.CONF_CORE: {config_util.CONF_PACKAGES: packages}, } - config_util.merge_packages_config(config, packages) + config_util.merge_packages_config(hass, config, packages) assert merge_log_err.call_count == 0 assert 'api' in config @@ -633,7 +633,7 @@ def test_merge_new(merge_log_err): assert len(config['panel_custom']) == 1 -def test_merge_type_mismatch(merge_log_err): +def test_merge_type_mismatch(merge_log_err, hass): """Test if we have a type mismatch for packages.""" packages = { 'pack_1': {'input_boolean': [{'ib1': None}]}, @@ -646,7 +646,7 @@ def test_merge_type_mismatch(merge_log_err): 'input_select': [{'ib2': None}], 'light': [{'platform': 'two'}] } - config_util.merge_packages_config(config, packages) + config_util.merge_packages_config(hass, config, packages) assert merge_log_err.call_count == 2 assert len(config) == 4 @@ -654,7 +654,7 @@ def test_merge_type_mismatch(merge_log_err): assert len(config['light']) == 2 -def test_merge_once_only(merge_log_err): +def test_merge_once_only(merge_log_err, hass): """Test if we have a merge for a comp that may occur only once.""" packages = { 'pack_2': { @@ -666,7 +666,7 @@ def test_merge_once_only(merge_log_err): config_util.CONF_CORE: {config_util.CONF_PACKAGES: packages}, 'mqtt': {}, 'api': {} } - config_util.merge_packages_config(config, packages) + config_util.merge_packages_config(hass, config, packages) assert merge_log_err.call_count == 1 assert len(config) == 3 @@ -682,13 +682,13 @@ def test_merge_id_schema(hass): 'qwikswitch': 'dict', } for name, expected_type in types.items(): - module = config_util.get_component(name) + module = config_util.get_component(hass, name) typ, _ = config_util._identify_config_schema(module) assert typ == expected_type, "{} expected {}, got {}".format( name, expected_type, typ) -def test_merge_duplicate_keys(merge_log_err): +def test_merge_duplicate_keys(merge_log_err, hass): """Test if keys in dicts are duplicates.""" packages = { 'pack_1': {'input_select': {'ib1': None}}, @@ -697,7 +697,7 @@ def test_merge_duplicate_keys(merge_log_err): config_util.CONF_CORE: {config_util.CONF_PACKAGES: packages}, 'input_select': {'ib1': None}, } - config_util.merge_packages_config(config, packages) + config_util.merge_packages_config(hass, config, packages) assert merge_log_err.call_count == 1 assert len(config) == 2 diff --git a/tests/test_config_entries.py b/tests/test_config_entries.py index b46909d7732..1518706db55 100644 --- a/tests/test_config_entries.py +++ b/tests/test_config_entries.py @@ -27,7 +27,7 @@ def test_call_setup_entry(hass): mock_setup_entry = MagicMock(return_value=mock_coro(True)) loader.set_component( - 'comp', + hass, 'comp', MockModule('comp', async_setup_entry=mock_setup_entry)) result = yield from async_setup_component(hass, 'comp', {}) @@ -36,12 +36,12 @@ def test_call_setup_entry(hass): @asyncio.coroutine -def test_remove_entry(manager): +def test_remove_entry(hass, manager): """Test that we can remove an entry.""" mock_unload_entry = MagicMock(return_value=mock_coro(True)) loader.set_component( - 'test', + hass, 'test', MockModule('comp', async_unload_entry=mock_unload_entry)) MockConfigEntry(domain='test', entry_id='test1').add_to_manager(manager) @@ -63,7 +63,7 @@ def test_remove_entry(manager): @asyncio.coroutine -def test_remove_entry_raises(manager): +def test_remove_entry_raises(hass, manager): """Test if a component raises while removing entry.""" @asyncio.coroutine def mock_unload_entry(hass, entry): @@ -71,7 +71,7 @@ def test_remove_entry_raises(manager): raise Exception("BROKEN") loader.set_component( - 'test', + hass, 'test', MockModule('comp', async_unload_entry=mock_unload_entry)) MockConfigEntry(domain='test', entry_id='test1').add_to_manager(manager) @@ -96,7 +96,7 @@ def test_add_entry_calls_setup_entry(hass, manager): mock_setup_entry = MagicMock(return_value=mock_coro(True)) loader.set_component( - 'comp', + hass, 'comp', MockModule('comp', async_setup_entry=mock_setup_entry)) class TestFlow(data_entry_flow.FlowHandler): @@ -151,6 +151,8 @@ def test_domains_gets_uniques(manager): @asyncio.coroutine def test_saving_and_loading(hass): """Test that we're saving and loading correctly.""" + loader.set_component(hass, 'test', MockModule('test')) + class TestFlow(data_entry_flow.FlowHandler): VERSION = 5 @@ -217,12 +219,12 @@ async def test_forward_entry_sets_up_component(hass): mock_original_setup_entry = MagicMock(return_value=mock_coro(True)) loader.set_component( - 'original', + hass, 'original', MockModule('original', async_setup_entry=mock_original_setup_entry)) mock_forwarded_setup_entry = MagicMock(return_value=mock_coro(True)) loader.set_component( - 'forwarded', + hass, 'forwarded', MockModule('forwarded', async_setup_entry=mock_forwarded_setup_entry)) await hass.config_entries.async_forward_entry_setup(entry, 'forwarded') @@ -236,7 +238,7 @@ async def test_forward_entry_does_not_setup_entry_if_setup_fails(hass): mock_setup = MagicMock(return_value=mock_coro(False)) mock_setup_entry = MagicMock() - loader.set_component('forwarded', MockModule( + hass, loader.set_component(hass, 'forwarded', MockModule( 'forwarded', async_setup=mock_setup, async_setup_entry=mock_setup_entry, @@ -249,6 +251,7 @@ async def test_forward_entry_does_not_setup_entry_if_setup_fails(hass): async def test_discovery_notification(hass): """Test that we create/dismiss a notification when source is discovery.""" + loader.set_component(hass, 'test', MockModule('test')) await async_setup_component(hass, 'persistent_notification', {}) class TestFlow(data_entry_flow.FlowHandler): diff --git a/tests/test_loader.py b/tests/test_loader.py index 7fc33df57bb..646526e94ea 100644 --- a/tests/test_loader.py +++ b/tests/test_loader.py @@ -27,37 +27,40 @@ class TestLoader(unittest.TestCase): def test_set_component(self): """Test if set_component works.""" - loader.set_component('switch.test_set', http) + comp = object() + loader.set_component(self.hass, 'switch.test_set', comp) - self.assertEqual(http, loader.get_component('switch.test_set')) + self.assertEqual(comp, + loader.get_component(self.hass, 'switch.test_set')) def test_get_component(self): """Test if get_component works.""" - self.assertEqual(http, loader.get_component('http')) - - self.assertIsNotNone(loader.get_component('switch.test')) + self.assertEqual(http, loader.get_component(self.hass, 'http')) + self.assertIsNotNone(loader.get_component(self.hass, 'light.hue')) def test_load_order_component(self): """Test if we can get the proper load order of components.""" - loader.set_component('mod1', MockModule('mod1')) - loader.set_component('mod2', MockModule('mod2', ['mod1'])) - loader.set_component('mod3', MockModule('mod3', ['mod2'])) + loader.set_component(self.hass, 'mod1', MockModule('mod1')) + loader.set_component(self.hass, 'mod2', MockModule('mod2', ['mod1'])) + loader.set_component(self.hass, 'mod3', MockModule('mod3', ['mod2'])) self.assertEqual( - ['mod1', 'mod2', 'mod3'], loader.load_order_component('mod3')) + ['mod1', 'mod2', 'mod3'], + loader.load_order_component(self.hass, 'mod3')) # Create circular dependency - loader.set_component('mod1', MockModule('mod1', ['mod3'])) + loader.set_component(self.hass, 'mod1', MockModule('mod1', ['mod3'])) - self.assertEqual([], loader.load_order_component('mod3')) + self.assertEqual([], loader.load_order_component(self.hass, 'mod3')) # Depend on non-existing component - loader.set_component('mod1', MockModule('mod1', ['nonexisting'])) + loader.set_component(self.hass, 'mod1', + MockModule('mod1', ['nonexisting'])) - self.assertEqual([], loader.load_order_component('mod1')) + self.assertEqual([], loader.load_order_component(self.hass, 'mod1')) # Try to get load order for non-existing component - self.assertEqual([], loader.load_order_component('mod1')) + self.assertEqual([], loader.load_order_component(self.hass, 'mod1')) def test_component_loader(hass): diff --git a/tests/test_requirements.py b/tests/test_requirements.py index 5f09e0bd83e..8ae0f6c11de 100644 --- a/tests/test_requirements.py +++ b/tests/test_requirements.py @@ -35,7 +35,8 @@ class TestRequirements: mock_dirname.return_value = 'ha_package_path' self.hass.config.skip_pip = False loader.set_component( - 'comp', MockModule('comp', requirements=['package==0.0.1'])) + self.hass, 'comp', + MockModule('comp', requirements=['package==0.0.1'])) assert setup.setup_component(self.hass, 'comp') assert 'comp' in self.hass.config.components assert mock_install.call_args == mock.call( @@ -53,7 +54,8 @@ class TestRequirements: mock_dirname.return_value = 'ha_package_path' self.hass.config.skip_pip = False loader.set_component( - 'comp', MockModule('comp', requirements=['package==0.0.1'])) + self.hass, 'comp', + MockModule('comp', requirements=['package==0.0.1'])) assert setup.setup_component(self.hass, 'comp') assert 'comp' in self.hass.config.components assert mock_install.call_args == mock.call( diff --git a/tests/test_setup.py b/tests/test_setup.py index 6a94310793c..6f0c282e016 100644 --- a/tests/test_setup.py +++ b/tests/test_setup.py @@ -49,6 +49,7 @@ class TestSetup: } }, required=True) loader.set_component( + self.hass, 'comp_conf', MockModule('comp_conf', config_schema=config_schema)) with assert_setup_component(0): @@ -93,10 +94,12 @@ class TestSetup: 'hello': str, }) loader.set_component( + self.hass, 'platform_conf', MockModule('platform_conf', platform_schema=platform_schema)) loader.set_component( + self.hass, 'platform_conf.whatever', MockPlatform('whatever')) with assert_setup_component(0): @@ -179,7 +182,8 @@ class TestSetup: """Test we do not setup a component twice.""" mock_setup = mock.MagicMock(return_value=True) - loader.set_component('comp', MockModule('comp', setup=mock_setup)) + loader.set_component( + self.hass, 'comp', MockModule('comp', setup=mock_setup)) assert setup.setup_component(self.hass, 'comp') assert mock_setup.called @@ -195,6 +199,7 @@ class TestSetup: """Component setup should fail if requirement can't install.""" self.hass.config.skip_pip = False loader.set_component( + self.hass, 'comp', MockModule('comp', requirements=['package==0.0.1'])) assert not setup.setup_component(self.hass, 'comp') @@ -210,6 +215,7 @@ class TestSetup: result.append(1) loader.set_component( + self.hass, 'comp', MockModule('comp', async_setup=async_setup)) def setup_component(): @@ -227,20 +233,23 @@ class TestSetup: def test_component_not_setup_missing_dependencies(self): """Test we do not setup a component if not all dependencies loaded.""" deps = ['non_existing'] - loader.set_component('comp', MockModule('comp', dependencies=deps)) + loader.set_component( + self.hass, 'comp', MockModule('comp', dependencies=deps)) assert not setup.setup_component(self.hass, 'comp', {}) assert 'comp' not in self.hass.config.components self.hass.data.pop(setup.DATA_SETUP) - loader.set_component('non_existing', MockModule('non_existing')) + loader.set_component( + self.hass, 'non_existing', MockModule('non_existing')) assert setup.setup_component(self.hass, 'comp', {}) def test_component_failing_setup(self): """Test component that fails setup.""" loader.set_component( - 'comp', MockModule('comp', setup=lambda hass, config: False)) + self.hass, 'comp', + MockModule('comp', setup=lambda hass, config: False)) assert not setup.setup_component(self.hass, 'comp', {}) assert 'comp' not in self.hass.config.components @@ -251,7 +260,8 @@ class TestSetup: """Setup that raises exception.""" raise Exception('fail!') - loader.set_component('comp', MockModule('comp', setup=exception_setup)) + loader.set_component( + self.hass, 'comp', MockModule('comp', setup=exception_setup)) assert not setup.setup_component(self.hass, 'comp', {}) assert 'comp' not in self.hass.config.components @@ -264,11 +274,12 @@ class TestSetup: return True raise Exception('Config not passed in: {}'.format(config)) - loader.set_component('comp_a', - MockModule('comp_a', setup=config_check_setup)) + loader.set_component( + self.hass, 'comp_a', + MockModule('comp_a', setup=config_check_setup)) - loader.set_component('switch.platform_a', MockPlatform('comp_b', - ['comp_a'])) + loader.set_component( + self.hass, 'switch.platform_a', MockPlatform('comp_b', ['comp_a'])) setup.setup_component(self.hass, 'switch', { 'comp_a': { @@ -289,6 +300,7 @@ class TestSetup: mock_setup = mock.MagicMock(spec_set=True) loader.set_component( + self.hass, 'switch.platform_a', MockPlatform(platform_schema=platform_schema, setup_platform=mock_setup)) @@ -330,29 +342,34 @@ class TestSetup: def test_disable_component_if_invalid_return(self): """Test disabling component if invalid return.""" loader.set_component( + self.hass, 'disabled_component', MockModule('disabled_component', setup=lambda hass, config: None)) assert not setup.setup_component(self.hass, 'disabled_component') - assert loader.get_component('disabled_component') is None + assert loader.get_component(self.hass, 'disabled_component') is None assert 'disabled_component' not in self.hass.config.components self.hass.data.pop(setup.DATA_SETUP) loader.set_component( + self.hass, 'disabled_component', MockModule('disabled_component', setup=lambda hass, config: False)) assert not setup.setup_component(self.hass, 'disabled_component') - assert loader.get_component('disabled_component') is not None + assert loader.get_component( + self.hass, 'disabled_component') is not None assert 'disabled_component' not in self.hass.config.components self.hass.data.pop(setup.DATA_SETUP) loader.set_component( + self.hass, 'disabled_component', MockModule('disabled_component', setup=lambda hass, config: True)) assert setup.setup_component(self.hass, 'disabled_component') - assert loader.get_component('disabled_component') is not None + assert loader.get_component( + self.hass, 'disabled_component') is not None assert 'disabled_component' in self.hass.config.components def test_all_work_done_before_start(self): @@ -373,14 +390,17 @@ class TestSetup: return True loader.set_component( + self.hass, 'test_component1', MockModule('test_component1', setup=component1_setup)) loader.set_component( + self.hass, 'test_component2', MockModule('test_component2', setup=component_track_setup)) loader.set_component( + self.hass, 'test_component3', MockModule('test_component3', setup=component_track_setup)) @@ -409,7 +429,8 @@ def test_component_cannot_depend_config(hass): @asyncio.coroutine def test_component_warn_slow_setup(hass): """Warn we log when a component setup takes a long time.""" - loader.set_component('test_component1', MockModule('test_component1')) + loader.set_component( + hass, 'test_component1', MockModule('test_component1')) with mock.patch.object(hass.loop, 'call_later', mock.MagicMock()) \ as mock_call: result = yield from setup.async_setup_component( @@ -430,7 +451,7 @@ def test_component_warn_slow_setup(hass): def test_platform_no_warn_slow(hass): """Do not warn for long entity setup time.""" loader.set_component( - 'test_component1', + hass, 'test_component1', MockModule('test_component1', platform_schema=PLATFORM_SCHEMA)) with mock.patch.object(hass.loop, 'call_later', mock.MagicMock()) \ as mock_call: diff --git a/tests/testing_config/custom_components/test_standalone.py b/tests/testing_config/custom_components/test_standalone.py index f0d4ba7982b..de3a360a4da 100644 --- a/tests/testing_config/custom_components/test_standalone.py +++ b/tests/testing_config/custom_components/test_standalone.py @@ -2,6 +2,6 @@ DOMAIN = 'test_standalone' -def setup(hass, config): +async def async_setup(hass, config): """Mock a successful setup.""" return True From 2f0fc0934f0eae56b9a833e8e449043bb79d5d40 Mon Sep 17 00:00:00 2001 From: corneyl Date: Tue, 1 May 2018 21:06:41 +0200 Subject: [PATCH 090/155] Buienradar improvements: continuous sensors and unique ID's (#13249) * Force update continuous sensors when new measurement available. * Added unique ID's based on coordinates, sensor type and client name. * Fixed over-indentation (hound review) * Revert "Added unique ID's based on coordinates, sensor type and client name." This reverts commit 3345e67a155c7953afc42c1b1b676616a7a77e56. * Fix lint errors. * Re-added unique ID's based on location. * Removed wrong error logging. * Removed creating UUID from unique id * Lint --- homeassistant/components/sensor/buienradar.py | 58 ++++++++++++------- 1 file changed, 37 insertions(+), 21 deletions(-) diff --git a/homeassistant/components/sensor/buienradar.py b/homeassistant/components/sensor/buienradar.py index 5d74f038eaa..6eb67f7cbd8 100644 --- a/homeassistant/components/sensor/buienradar.py +++ b/homeassistant/components/sensor/buienradar.py @@ -161,7 +161,8 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None): dev = [] for sensor_type in config[CONF_MONITORED_CONDITIONS]: - dev.append(BrSensor(sensor_type, config.get(CONF_NAME, 'br'))) + dev.append(BrSensor(sensor_type, config.get(CONF_NAME, 'br'), + coordinates)) async_add_devices(dev) data = BrData(hass, coordinates, timeframe, dev) @@ -172,9 +173,9 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None): class BrSensor(Entity): """Representation of an Buienradar sensor.""" - def __init__(self, sensor_type, client_name): + def __init__(self, sensor_type, client_name, coordinates): """Initialize the sensor.""" - from buienradar.buienradar import (PRECIPITATION_FORECAST) + from buienradar.buienradar import (PRECIPITATION_FORECAST, CONDITION) self.client_name = client_name self._name = SENSOR_TYPES[sensor_type][0] @@ -185,10 +186,22 @@ class BrSensor(Entity): self._attribution = None self._measured = None self._stationname = None + self._unique_id = self.uid(coordinates) + + # All continuous sensors should be forced to be updated + self._force_update = self.type != SYMBOL and \ + not self.type.startswith(CONDITION) if self.type.startswith(PRECIPITATION_FORECAST): self._timeframe = None + def uid(self, coordinates): + """Generate a unique id using coordinates and sensor type.""" + # The combination of the location, name an sensor type is unique + return "%2.6f%2.6f%s" % (coordinates[CONF_LATITUDE], + coordinates[CONF_LONGITUDE], + self.type) + def load_data(self, data): """Load the sensor with relevant data.""" # Find sensor @@ -198,6 +211,11 @@ class BrSensor(Entity): PRECIPITATION_FORECAST, STATIONNAME, TIMEFRAME) + # Check if we have a new measurement, + # otherwise we do not have to update the sensor + if self._measured == data.get(MEASURED): + return False + self._attribution = data.get(ATTRIBUTION) self._stationname = data.get(STATIONNAME) self._measured = data.get(MEASURED) @@ -246,18 +264,12 @@ class BrSensor(Entity): return False else: try: - new_state = data.get(FORECAST)[fcday].get(self.type[:-3]) + self._state = data.get(FORECAST)[fcday].get(self.type[:-3]) + return True except IndexError: _LOGGER.warning("No forecast for fcday=%s...", fcday) return False - if new_state != self._state: - self._state = new_state - return True - return False - - return False - if self.type == SYMBOL or self.type.startswith(CONDITION): # update weather symbol & status text condition = data.get(CONDITION, None) @@ -286,27 +298,26 @@ class BrSensor(Entity): if self.type.startswith(PRECIPITATION_FORECAST): # update nested precipitation forecast sensors nested = data.get(PRECIPITATION_FORECAST) - new_state = nested.get(self.type[len(PRECIPITATION_FORECAST)+1:]) self._timeframe = nested.get(TIMEFRAME) # pylint: disable=protected-access - if new_state != self._state: - self._state = new_state - return True - return False + self._state = nested.get(self.type[len(PRECIPITATION_FORECAST)+1:]) + return True # update all other sensors - new_state = data.get(self.type) # pylint: disable=protected-access - if new_state != self._state: - self._state = new_state - return True - return False + self._state = data.get(self.type) + return True @property def attribution(self): """Return the attribution.""" return self._attribution + @property + def unique_id(self): + """Return the unique id.""" + return self._unique_id + @property def name(self): """Return the name of the sensor.""" @@ -360,6 +371,11 @@ class BrSensor(Entity): """Return possible sensor specific icon.""" return SENSOR_TYPES[self.type][2] + @property + def force_update(self): + """Return true for continuous sensors, false for discrete sensors.""" + return self._force_update + class BrData(object): """Get the latest data and updates the states.""" From 8d5c3a2b91c8e074638168511c4f0371d452292e Mon Sep 17 00:00:00 2001 From: escoand Date: Tue, 1 May 2018 21:20:38 +0200 Subject: [PATCH 091/155] add volumio discovery (#14220) * add volumio discovery * add missing library * Update volumio.py --- homeassistant/components/discovery.py | 1 + .../components/media_player/volumio.py | 29 ++++++++++++++++--- 2 files changed, 26 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/discovery.py b/homeassistant/components/discovery.py index 69d0f4796ff..07eb5aaab82 100644 --- a/homeassistant/components/discovery.py +++ b/homeassistant/components/discovery.py @@ -79,6 +79,7 @@ SERVICE_HANDLERS = { 'bluesound': ('media_player', 'bluesound'), 'songpal': ('media_player', 'songpal'), 'kodi': ('media_player', 'kodi'), + 'volumio': ('media_player', 'volumio'), } OPTIONAL_SERVICE_HANDLERS = { diff --git a/homeassistant/components/media_player/volumio.py b/homeassistant/components/media_player/volumio.py index 0a940c0aa9d..11ab1615617 100644 --- a/homeassistant/components/media_player/volumio.py +++ b/homeassistant/components/media_player/volumio.py @@ -8,6 +8,7 @@ Volumio rest API: https://volumio.github.io/docs/API/REST_API.html """ from datetime import timedelta import logging +import socket import asyncio import aiohttp @@ -31,6 +32,8 @@ DEFAULT_HOST = 'localhost' DEFAULT_NAME = 'Volumio' DEFAULT_PORT = 3000 +DATA_VOLUMIO = 'volumio' + TIMEOUT = 10 SUPPORT_VOLUMIO = SUPPORT_PAUSE | SUPPORT_VOLUME_SET | SUPPORT_VOLUME_MUTE | \ @@ -50,11 +53,29 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ @asyncio.coroutine def async_setup_platform(hass, config, async_add_devices, discovery_info=None): """Set up the Volumio platform.""" - host = config.get(CONF_HOST) - port = config.get(CONF_PORT) - name = config.get(CONF_NAME) + if DATA_VOLUMIO not in hass.data: + hass.data[DATA_VOLUMIO] = dict() - async_add_devices([Volumio(name, host, port, hass)]) + # This is a manual configuration? + if discovery_info is None: + name = config.get(CONF_NAME) + host = config.get(CONF_HOST) + port = config.get(CONF_PORT) + else: + name = "{} ({})".format(DEFAULT_NAME, discovery_info.get('hostname')) + host = discovery_info.get('host') + port = discovery_info.get('port') + + # Only add a device once, so discovered devices do not override manual + # config. + ip_addr = socket.gethostbyname(host) + if ip_addr in hass.data[DATA_VOLUMIO]: + return + + entity = Volumio(name, host, port, hass) + + hass.data[DATA_VOLUMIO][ip_addr] = entity + async_add_devices([entity]) class Volumio(MediaPlayerDevice): From 7a054719129bcc8f0fa3dc9861f2b6f56a3ee981 Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Tue, 1 May 2018 13:36:43 -0600 Subject: [PATCH 092/155] Converts RainMachine to hub model (part 2) (#14225) * Converts RainMachine to hub model (part 2) * Small style adjustments for consistency * Moving MAC calculation to one-time call in component * Removing unneeded attribute * Bumping Travis * Lint --- homeassistant/components/rainmachine.py | 26 ++++++++-- .../components/switch/rainmachine.py | 47 ++++++++----------- 2 files changed, 41 insertions(+), 32 deletions(-) diff --git a/homeassistant/components/rainmachine.py b/homeassistant/components/rainmachine.py index 4c8b8a1114f..99cec53c2ed 100644 --- a/homeassistant/components/rainmachine.py +++ b/homeassistant/components/rainmachine.py @@ -8,11 +8,10 @@ import logging from datetime import timedelta import voluptuous as vol -from requests.exceptions import ConnectTimeout -from homeassistant.helpers import config_validation as cv +from homeassistant.helpers import config_validation as cv, discovery from homeassistant.const import ( - CONF_IP_ADDRESS, CONF_PASSWORD, CONF_PORT, CONF_SSL) + CONF_IP_ADDRESS, CONF_PASSWORD, CONF_PORT, CONF_SSL, CONF_SWITCHES) REQUIREMENTS = ['regenmaschine==0.4.1'] @@ -24,6 +23,8 @@ DOMAIN = 'rainmachine' NOTIFICATION_ID = 'rainmachine_notification' NOTIFICATION_TITLE = 'RainMachine Component Setup' +CONF_ZONE_RUN_TIME = 'zone_run_time' + DEFAULT_ATTRIBUTION = 'Data provided by Green Electronics LLC' DEFAULT_PORT = 8080 DEFAULT_SSL = True @@ -31,6 +32,11 @@ DEFAULT_SSL = True MIN_SCAN_TIME = timedelta(seconds=1) MIN_SCAN_TIME_FORCED = timedelta(milliseconds=100) +SWITCH_SCHEMA = vol.Schema({ + vol.Optional(CONF_ZONE_RUN_TIME): + cv.positive_int +}) + CONFIG_SCHEMA = vol.Schema( { DOMAIN: vol.Schema({ @@ -38,6 +44,7 @@ CONFIG_SCHEMA = vol.Schema( vol.Required(CONF_PASSWORD): cv.string, vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, vol.Optional(CONF_SSL, default=DEFAULT_SSL): cv.boolean, + vol.Optional(CONF_SWITCHES): SWITCH_SCHEMA, }) }, extra=vol.ALLOW_EXTRA) @@ -47,6 +54,7 @@ def setup(hass, config): """Set up the RainMachine component.""" from regenmaschine import Authenticator, Client from regenmaschine.exceptions import HTTPError + from requests.exceptions import ConnectTimeout conf = config[DOMAIN] ip_address = conf[CONF_IP_ADDRESS] @@ -54,11 +62,14 @@ def setup(hass, config): port = conf[CONF_PORT] ssl = conf[CONF_SSL] + _LOGGER.debug('Setting up RainMachine client') + try: auth = Authenticator.create_local( ip_address, password, port=port, https=ssl) client = Client(auth) - hass.data[DATA_RAINMACHINE] = client + mac = client.provision.wifi()['macAddress'] + hass.data[DATA_RAINMACHINE] = (client, mac) except (HTTPError, ConnectTimeout, UnboundLocalError) as exc_info: _LOGGER.error('An error occurred: %s', str(exc_info)) hass.components.persistent_notification.create( @@ -68,4 +79,11 @@ def setup(hass, config): title=NOTIFICATION_TITLE, notification_id=NOTIFICATION_ID) return False + + _LOGGER.debug('Setting up switch platform') + switch_config = conf.get(CONF_SWITCHES, {}) + discovery.load_platform(hass, 'switch', DOMAIN, switch_config, config) + + _LOGGER.debug('Setup complete') + return True diff --git a/homeassistant/components/switch/rainmachine.py b/homeassistant/components/switch/rainmachine.py index cdada7ce274..8306b323330 100644 --- a/homeassistant/components/switch/rainmachine.py +++ b/homeassistant/components/switch/rainmachine.py @@ -2,40 +2,33 @@ from logging import getLogger -import voluptuous as vol - -import homeassistant.helpers.config_validation as cv from homeassistant.components.rainmachine import ( - DATA_RAINMACHINE, DEFAULT_ATTRIBUTION, MIN_SCAN_TIME, MIN_SCAN_TIME_FORCED) -from homeassistant.components.switch import PLATFORM_SCHEMA, SwitchDevice -from homeassistant.const import ATTR_ATTRIBUTION, ATTR_DEVICE_CLASS + CONF_ZONE_RUN_TIME, DATA_RAINMACHINE, DEFAULT_ATTRIBUTION, MIN_SCAN_TIME, + MIN_SCAN_TIME_FORCED) +from homeassistant.components.switch import SwitchDevice +from homeassistant.const import ATTR_ATTRIBUTION from homeassistant.util import Throttle -_LOGGER = getLogger(__name__) DEPENDENCIES = ['rainmachine'] +_LOGGER = getLogger(__name__) + ATTR_CYCLES = 'cycles' ATTR_TOTAL_DURATION = 'total_duration' -CONF_ZONE_RUN_TIME = 'zone_run_time' - -DEFAULT_ZONE_RUN_SECONDS = 60 * 10 - -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Optional(CONF_ZONE_RUN_TIME, default=DEFAULT_ZONE_RUN_SECONDS): - cv.positive_int -}) +DEFAULT_ZONE_RUN = 60 * 10 def setup_platform(hass, config, add_devices, discovery_info=None): """Set this component up under its platform.""" - client = hass.data.get(DATA_RAINMACHINE) - device_name = client.provision.device_name()['name'] - device_mac = client.provision.wifi()['macAddress'] + if discovery_info is None: + return - _LOGGER.debug('Config received: %s', config) + _LOGGER.debug('Config received: %s', discovery_info) - zone_run_time = config[CONF_ZONE_RUN_TIME] + zone_run_time = discovery_info.get(CONF_ZONE_RUN_TIME, DEFAULT_ZONE_RUN) + + client, device_mac = hass.data.get(DATA_RAINMACHINE) entities = [] for program in client.programs.all().get('programs', {}): @@ -44,7 +37,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): _LOGGER.debug('Adding program: %s', program) entities.append( - RainMachineProgram(client, device_name, device_mac, program)) + RainMachineProgram(client, device_mac, program)) for zone in client.zones.all().get('zones', {}): if not zone.get('active'): @@ -52,7 +45,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): _LOGGER.debug('Adding zone: %s', zone) entities.append( - RainMachineZone(client, device_name, device_mac, zone, + RainMachineZone(client, device_mac, zone, zone_run_time)) add_devices(entities, True) @@ -61,18 +54,16 @@ def setup_platform(hass, config, add_devices, discovery_info=None): class RainMachineEntity(SwitchDevice): """A class to represent a generic RainMachine entity.""" - def __init__(self, client, device_name, device_mac, entity_json): + def __init__(self, client, device_mac, entity_json): """Initialize a generic RainMachine entity.""" self._api_type = 'remote' if client.auth.using_remote_api else 'local' self._client = client self._entity_json = entity_json self.device_mac = device_mac - self.device_name = device_name self._attrs = { - ATTR_ATTRIBUTION: DEFAULT_ATTRIBUTION, - ATTR_DEVICE_CLASS: self.device_name + ATTR_ATTRIBUTION: DEFAULT_ATTRIBUTION } @property @@ -156,10 +147,10 @@ class RainMachineProgram(RainMachineEntity): class RainMachineZone(RainMachineEntity): """A RainMachine zone.""" - def __init__(self, client, device_name, device_mac, zone_json, + def __init__(self, client, device_mac, zone_json, zone_run_time): """Initialize a RainMachine zone.""" - super().__init__(client, device_name, device_mac, zone_json) + super().__init__(client, device_mac, zone_json) self._run_time = zone_run_time self._attrs.update({ ATTR_CYCLES: self._entity_json.get('noOfCycles'), From e4655a7e63ec95502b2ac78cce4bc931a75328ba Mon Sep 17 00:00:00 2001 From: Otto Winter Date: Tue, 1 May 2018 21:38:08 +0200 Subject: [PATCH 093/155] Add MQTT Sensor device_class (#14033) * Add MQTT Sensor device_class * Add test --- homeassistant/components/sensor/mqtt.py | 15 ++++++++--- tests/components/sensor/test_mqtt.py | 36 ++++++++++++++++++++++++- 2 files changed, 47 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/sensor/mqtt.py b/homeassistant/components/sensor/mqtt.py index d7d66a3a145..997fd312a6a 100644 --- a/homeassistant/components/sensor/mqtt.py +++ b/homeassistant/components/sensor/mqtt.py @@ -15,9 +15,10 @@ from homeassistant.core import callback from homeassistant.components.mqtt import ( CONF_AVAILABILITY_TOPIC, CONF_STATE_TOPIC, CONF_PAYLOAD_AVAILABLE, CONF_PAYLOAD_NOT_AVAILABLE, CONF_QOS, MqttAvailability) +from homeassistant.components.sensor import DEVICE_CLASSES_SCHEMA from homeassistant.const import ( CONF_FORCE_UPDATE, CONF_NAME, CONF_VALUE_TEMPLATE, STATE_UNKNOWN, - CONF_UNIT_OF_MEASUREMENT, CONF_ICON) + CONF_UNIT_OF_MEASUREMENT, CONF_ICON, CONF_DEVICE_CLASS) from homeassistant.helpers.entity import Entity import homeassistant.components.mqtt as mqtt import homeassistant.helpers.config_validation as cv @@ -39,6 +40,7 @@ PLATFORM_SCHEMA = mqtt.MQTT_RO_PLATFORM_SCHEMA.extend({ vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, vol.Optional(CONF_UNIT_OF_MEASUREMENT): cv.string, vol.Optional(CONF_ICON): cv.icon, + vol.Optional(CONF_DEVICE_CLASS): DEVICE_CLASSES_SCHEMA, vol.Optional(CONF_JSON_ATTRS, default=[]): cv.ensure_list_csv, vol.Optional(CONF_EXPIRE_AFTER): cv.positive_int, vol.Optional(CONF_FORCE_UPDATE, default=DEFAULT_FORCE_UPDATE): cv.boolean, @@ -66,6 +68,7 @@ async def async_setup_platform(hass: HomeAssistantType, config: ConfigType, config.get(CONF_FORCE_UPDATE), config.get(CONF_EXPIRE_AFTER), config.get(CONF_ICON), + config.get(CONF_DEVICE_CLASS), value_template, config.get(CONF_JSON_ATTRS), config.get(CONF_UNIQUE_ID), @@ -79,8 +82,8 @@ class MqttSensor(MqttAvailability, Entity): """Representation of a sensor that can be updated using MQTT.""" def __init__(self, name, state_topic, qos, unit_of_measurement, - force_update, expire_after, icon, value_template, - json_attributes, unique_id: Optional[str], + force_update, expire_after, icon, device_class: Optional[str], + value_template, json_attributes, unique_id: Optional[str], availability_topic, payload_available, payload_not_available): """Initialize the sensor.""" @@ -95,6 +98,7 @@ class MqttSensor(MqttAvailability, Entity): self._template = value_template self._expire_after = expire_after self._icon = icon + self._device_class = device_class self._expiration_trigger = None self._json_attributes = set(json_attributes) self._unique_id = unique_id @@ -190,3 +194,8 @@ class MqttSensor(MqttAvailability, Entity): def icon(self): """Return the icon.""" return self._icon + + @property + def device_class(self) -> Optional[str]: + """Return the device class of the sensor.""" + return self._device_class diff --git a/tests/components/sensor/test_mqtt.py b/tests/components/sensor/test_mqtt.py index 88e74e11008..2583f52b3d2 100644 --- a/tests/components/sensor/test_mqtt.py +++ b/tests/components/sensor/test_mqtt.py @@ -10,7 +10,8 @@ import homeassistant.components.sensor as sensor from homeassistant.const import EVENT_STATE_CHANGED, STATE_UNAVAILABLE import homeassistant.util.dt as dt_util -from tests.common import mock_mqtt_component, fire_mqtt_message +from tests.common import mock_mqtt_component, fire_mqtt_message, \ + assert_setup_component from tests.common import get_test_home_assistant, mock_component @@ -350,3 +351,36 @@ class TestSensorMQTT(unittest.TestCase): self.hass.block_till_done() assert len(self.hass.states.all()) == 1 + + def test_invalid_device_class(self): + """Test device_class option with invalid value.""" + with assert_setup_component(0): + assert setup_component(self.hass, 'sensor', { + 'sensor': { + 'platform': 'mqtt', + 'name': 'Test 1', + 'state_topic': 'test-topic', + 'device_class': 'foobarnotreal' + } + }) + + def test_valid_device_class(self): + """Test device_class option with valid values.""" + assert setup_component(self.hass, 'sensor', { + 'sensor': [{ + 'platform': 'mqtt', + 'name': 'Test 1', + 'state_topic': 'test-topic', + 'device_class': 'temperature' + }, { + 'platform': 'mqtt', + 'name': 'Test 2', + 'state_topic': 'test-topic', + }] + }) + self.hass.block_till_done() + + state = self.hass.states.get('sensor.test_1') + assert state.attributes['device_class'] == 'temperature' + state = self.hass.states.get('sensor.test_2') + assert 'device_class' not in state.attributes From c2d00be91ea4acb90ba944723fe42fe6a6b8a218 Mon Sep 17 00:00:00 2001 From: Matt Snyder Date: Tue, 1 May 2018 14:38:45 -0500 Subject: [PATCH 094/155] Allow independent control of white level on flux_led component (#13985) * Allow independent control of white level on flux_led component. Also preserve brightness on color change. * Limit white value support to RGBW mode. * Requested changes. * Correct liniting issues * Formatting --- homeassistant/components/light/flux_led.py | 39 +++++++++++++++------- 1 file changed, 27 insertions(+), 12 deletions(-) diff --git a/homeassistant/components/light/flux_led.py b/homeassistant/components/light/flux_led.py index 6ffdcc0bb4a..6c7f2e98e37 100644 --- a/homeassistant/components/light/flux_led.py +++ b/homeassistant/components/light/flux_led.py @@ -12,9 +12,9 @@ import voluptuous as vol from homeassistant.const import CONF_DEVICES, CONF_NAME, CONF_PROTOCOL from homeassistant.components.light import ( - ATTR_BRIGHTNESS, ATTR_HS_COLOR, ATTR_EFFECT, EFFECT_COLORLOOP, - EFFECT_RANDOM, SUPPORT_BRIGHTNESS, SUPPORT_EFFECT, - SUPPORT_COLOR, Light, PLATFORM_SCHEMA) + ATTR_BRIGHTNESS, ATTR_HS_COLOR, ATTR_EFFECT, ATTR_WHITE_VALUE, + EFFECT_COLORLOOP, EFFECT_RANDOM, SUPPORT_BRIGHTNESS, SUPPORT_EFFECT, + SUPPORT_COLOR, SUPPORT_WHITE_VALUE, Light, PLATFORM_SCHEMA) import homeassistant.helpers.config_validation as cv import homeassistant.util.color as color_util @@ -191,8 +191,16 @@ class FluxLight(Light): @property def supported_features(self): """Flag supported features.""" + if self._mode is MODE_RGBW: + return SUPPORT_FLUX_LED | SUPPORT_WHITE_VALUE + return SUPPORT_FLUX_LED + @property + def white_value(self): + """Return the white value of this light between 0..255.""" + return self._bulb.getRgbw()[3] + @property def effect_list(self): """Return the list of supported effects.""" @@ -212,24 +220,31 @@ class FluxLight(Light): brightness = kwargs.get(ATTR_BRIGHTNESS) effect = kwargs.get(ATTR_EFFECT) + white = kwargs.get(ATTR_WHITE_VALUE) - if rgb is not None and brightness is not None: - self._bulb.setRgb(*tuple(rgb), brightness=brightness) - elif rgb is not None: - self._bulb.setRgb(*tuple(rgb)) + # color change only + if rgb is not None: + self._bulb.setRgb(*tuple(rgb), brightness=self.brightness) + + # brightness change only elif brightness is not None: - if self._mode == MODE_RGBW: - self._bulb.setWarmWhite255(brightness) - elif self._mode == MODE_RGB: - (red, green, blue) = self._bulb.getRgb() - self._bulb.setRgb(red, green, blue, brightness=brightness) + (red, green, blue) = self._bulb.getRgb() + self._bulb.setRgb(red, green, blue, brightness=brightness) + + # random color effect elif effect == EFFECT_RANDOM: self._bulb.setRgb(random.randint(0, 255), random.randint(0, 255), random.randint(0, 255)) + + # effect selection elif effect in EFFECT_MAP: self._bulb.setPresetPattern(EFFECT_MAP[effect], 50) + # white change only + elif white is not None: + self._bulb.setWarmWhite255(white) + def turn_off(self, **kwargs): """Turn the specified or all lights off.""" self._bulb.turnOff() From 6453ea4e6181924cddf72ea58eb0c72e942b0772 Mon Sep 17 00:00:00 2001 From: Mohamad Tarbin Date: Tue, 1 May 2018 16:27:20 -0400 Subject: [PATCH 095/155] Add Social Blade Sensor (#14060) * Adding Dominion Energy Sensor * Update : remove white spacves and set the update time to be daily * Update : update spacing as per hound suggestions, Move imports * Update : Fix Travis CI build errors * Update Documentations on method levels * Update Documentations on method levels * Update Documentations on method levels * Add Exception Handeling if login failed, add PLATFORM_SCHEMA * Add Exception Handeling if login failed, add PLATFORM_SCHEMA * Add Exception Handeling if login failed, add PLATFORM_SCHEMA * Update dominionenergy.py * Adding Selenium to requirements_all.txt * Checking the username/password while setup * Checking the username/password while setup * removing extra white space * Update : Adding the Platform only if credentials works * Update : Add PlatformNotReady exception * Update : Add PlatformNotReady exception * Update .coveragerc * Remove change * Adding USCIS component * Adding Line after the class DOC * Update : Extract USCIS logic code to Component * Update : Extract USCIS logic code to Component * Adding CURRENT_STATUS * Change Error handling, remove date from attributes * Update the Version for USCIS * Add Social Blade Sensor * Update class documentation * Update coverage and requirements_all * Update : houndci error with intent * Update : Add coverage * Update uscis.py * Add comments * Add comments * Delete dominionenergy.py * Update requirements_all.txt * Update .coveragerc * Update .coveragerc * Update .coveragerc * Update : update after code review * Fix remaining issues --- .coveragerc | 1 + .../components/sensor/socialblade.py | 90 +++++++++++++++++++ requirements_all.txt | 3 + 3 files changed, 94 insertions(+) create mode 100644 homeassistant/components/sensor/socialblade.py diff --git a/.coveragerc b/.coveragerc index 1852d7d7365..94722666c05 100644 --- a/.coveragerc +++ b/.coveragerc @@ -661,6 +661,7 @@ omit = homeassistant/components/sensor/sma.py homeassistant/components/sensor/snmp.py homeassistant/components/sensor/sochain.py + homeassistant/components/sensor/socialblade.py homeassistant/components/sensor/sonarr.py homeassistant/components/sensor/speedtest.py homeassistant/components/sensor/spotcrime.py diff --git a/homeassistant/components/sensor/socialblade.py b/homeassistant/components/sensor/socialblade.py new file mode 100644 index 00000000000..1e0084e1404 --- /dev/null +++ b/homeassistant/components/sensor/socialblade.py @@ -0,0 +1,90 @@ +""" +Support for Social Blade. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/sensor.socialblade/ +""" +from datetime import timedelta +import logging + +import voluptuous as vol + +from homeassistant.components.sensor import PLATFORM_SCHEMA +from homeassistant.const import CONF_NAME +from homeassistant.helpers import config_validation as cv +from homeassistant.helpers.entity import Entity +from homeassistant.util import Throttle + +_LOGGER = logging.getLogger(__name__) + +REQUIREMENTS = ['socialbladeclient==0.2'] + +CHANNEL_ID = 'channel_id' + +DEFAULT_NAME = "Social Blade" + +MIN_TIME_BETWEEN_UPDATES = timedelta(hours=2) + +SUBSCRIBERS = 'subscribers' + +TOTAL_VIEWS = 'total_views' + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Required(CHANNEL_ID): cv.string, + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, +}) + + +def setup_platform(hass, config, add_devices, discovery_info=None): + """Set up the Social Blade sensor.""" + social_blade = SocialBladeSensor( + config[CHANNEL_ID], config[CONF_NAME]) + + social_blade.update() + if social_blade.valid_channel_id is False: + return + + add_devices([social_blade]) + + +class SocialBladeSensor(Entity): + """Representation of a Social Blade Sensor.""" + + def __init__(self, case, name): + """Initialize the Social Blade sensor.""" + self._state = None + self.channel_id = case + self._attributes = None + self.valid_channel_id = None + self._name = name + + @property + def name(self): + """Return the name.""" + return self._name + + @property + def state(self): + """Return the state.""" + return self._state + + @property + def device_state_attributes(self): + """Return the state attributes.""" + if self._attributes: + return self._attributes + + @Throttle(MIN_TIME_BETWEEN_UPDATES) + def update(self): + """Get the latest data from Social Blade.""" + import socialbladeclient + try: + data = socialbladeclient.get_data(self.channel_id) + self._attributes = {TOTAL_VIEWS: data[TOTAL_VIEWS]} + self._state = data[SUBSCRIBERS] + self.valid_channel_id = True + + except (ValueError, IndexError): + _LOGGER.error("Unable to find valid channel ID") + self.valid_channel_id = False + self._attributes = None diff --git a/requirements_all.txt b/requirements_all.txt index 93bf26f5239..a609ee5f7f2 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1203,6 +1203,9 @@ smappy==0.2.15 # homeassistant.components.media_player.snapcast snapcast==2.0.8 +# homeassistant.components.sensor.socialblade +socialbladeclient==0.2 + # homeassistant.components.climate.honeywell somecomfort==0.5.2 From e968b1a0f4e63ea862068f960d3777088fee7639 Mon Sep 17 00:00:00 2001 From: Diogo Gomes Date: Wed, 2 May 2018 14:15:30 +0100 Subject: [PATCH 096/155] UPnP code cleanup (#14235) * missing async calls * lint * cleanup --- homeassistant/components/sensor/upnp.py | 10 ++++++++-- homeassistant/components/upnp.py | 12 ++++-------- 2 files changed, 12 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/sensor/upnp.py b/homeassistant/components/sensor/upnp.py index e0c57ca9ac6..07b63553fcb 100644 --- a/homeassistant/components/sensor/upnp.py +++ b/homeassistant/components/sensor/upnp.py @@ -11,6 +11,8 @@ from homeassistant.helpers.entity import Entity _LOGGER = logging.getLogger(__name__) +DEPENDENCIES = ['upnp'] + BYTES_RECEIVED = 1 BYTES_SENT = 2 PACKETS_RECEIVED = 3 @@ -25,12 +27,16 @@ SENSOR_TYPES = { } -async def async_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 IGD sensors.""" + if discovery_info is None: + return + device = hass.data[DATA_UPNP] service = device.find_first_service(CIC_SERVICE) unit = discovery_info['unit'] - add_devices([ + async_add_devices([ IGDSensor(service, t, unit if SENSOR_TYPES[t][1] else '#') for t in SENSOR_TYPES], True) diff --git a/homeassistant/components/upnp.py b/homeassistant/components/upnp.py index 26a59746aea..8aeb93fed25 100644 --- a/homeassistant/components/upnp.py +++ b/homeassistant/components/upnp.py @@ -50,7 +50,7 @@ CONFIG_SCHEMA = vol.Schema({ DOMAIN: vol.Schema({ vol.Optional(CONF_ENABLE_PORT_MAPPING, default=True): cv.boolean, vol.Optional(CONF_UNITS, default="MBytes"): vol.In(UNITS), - vol.Optional(CONF_LOCAL_IP): ip_address, + vol.Optional(CONF_LOCAL_IP): vol.All(ip_address, cv.string), vol.Optional(CONF_PORTS): vol.Schema({vol.Any(CONF_HASS, cv.positive_int): cv.positive_int}) }), @@ -62,9 +62,7 @@ async def async_setup(hass, config): config = config[DOMAIN] host = config.get(CONF_LOCAL_IP) - if host is not None: - host = str(host) - else: + if host is None: host = get_local_ip() if host == '127.0.0.1': @@ -90,10 +88,8 @@ async def async_setup(hass, config): service = device.find_first_service(IP_SERVICE) if _service['serviceType'] == CIC_SERVICE: unit = config.get(CONF_UNITS) - discovery.load_platform(hass, 'sensor', - DOMAIN, - {'unit': unit}, - config) + hass.async_add_job(discovery.async_load_platform( + hass, 'sensor', DOMAIN, {'unit': unit}, config)) except UpnpSoapError as error: _LOGGER.error(error) return False From 8b13658d3b39c1deadd5d6a88c102b8a856d80f3 Mon Sep 17 00:00:00 2001 From: Sebastian Muszynski Date: Wed, 2 May 2018 15:21:50 +0200 Subject: [PATCH 097/155] Improve config schema of the blackbird component (#14007) * Import moved, return values removed and redundant log message removed * Improve config schema of the blackbird component * Tests updated * Handle updated * Schema fixed --- .../components/media_player/blackbird.py | 56 +++++++++---------- .../components/media_player/test_blackbird.py | 43 ++++++-------- 2 files changed, 44 insertions(+), 55 deletions(-) diff --git a/homeassistant/components/media_player/blackbird.py b/homeassistant/components/media_player/blackbird.py index 37b3c0ff819..1c976f5eecd 100644 --- a/homeassistant/components/media_player/blackbird.py +++ b/homeassistant/components/media_player/blackbird.py @@ -5,6 +5,7 @@ For more details about this platform, please refer to the documentation at https://home-assistant.io/components/media_player.blackbird """ import logging +import socket import voluptuous as vol @@ -50,71 +51,68 @@ ZONE_IDS = vol.All(vol.Coerce(int), vol.Range(min=1, max=8)) # Valid source ids: 1-8 SOURCE_IDS = vol.All(vol.Coerce(int), vol.Range(min=1, max=8)) -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Required(CONF_TYPE): vol.In(['serial', 'socket']), - vol.Optional(CONF_PORT): cv.string, - vol.Optional(CONF_HOST): cv.string, - vol.Required(CONF_ZONES): vol.Schema({ZONE_IDS: ZONE_SCHEMA}), - vol.Required(CONF_SOURCES): vol.Schema({SOURCE_IDS: SOURCE_SCHEMA}), -}) +PLATFORM_SCHEMA = vol.All( + cv.has_at_least_one_key(CONF_PORT, CONF_HOST), + PLATFORM_SCHEMA.extend({ + vol.Exclusive(CONF_PORT, CONF_TYPE): cv.string, + vol.Exclusive(CONF_HOST, CONF_TYPE): cv.string, + vol.Required(CONF_ZONES): vol.Schema({ZONE_IDS: ZONE_SCHEMA}), + vol.Required(CONF_SOURCES): vol.Schema({SOURCE_IDS: SOURCE_SCHEMA}), + })) # 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: + hass.data[DATA_BLACKBIRD] = {} + port = config.get(CONF_PORT) host = config.get(CONF_HOST) - device_type = config.get(CONF_TYPE) - import socket from pyblackbird import get_blackbird from serial import SerialException - if device_type == 'serial': - if port is None: - _LOGGER.error("No port configured") - return + connection = None + if port is not None: try: blackbird = get_blackbird(port) + connection = port except SerialException: _LOGGER.error("Error connecting to the Blackbird controller") return - elif device_type == 'socket': + if host is not None: try: - if host is None: - _LOGGER.error("No host configured") - return blackbird = get_blackbird(host, False) + connection = host except socket.timeout: _LOGGER.error("Error connecting to the Blackbird controller") return - else: - _LOGGER.error("Incorrect device type specified") - return - sources = {source_id: extra[CONF_NAME] for source_id, extra in config[CONF_SOURCES].items()} - hass.data[DATA_BLACKBIRD] = [] + devices = [] for zone_id, extra in config[CONF_ZONES].items(): _LOGGER.info("Adding zone %d - %s", zone_id, extra[CONF_NAME]) - hass.data[DATA_BLACKBIRD].append(BlackbirdZone( - blackbird, sources, zone_id, extra[CONF_NAME])) + unique_id = "{}-{}".format(connection, zone_id) + device = BlackbirdZone(blackbird, sources, zone_id, extra[CONF_NAME]) + hass.data[DATA_BLACKBIRD][unique_id] = device + devices.append(device) - add_devices(hass.data[DATA_BLACKBIRD], True) + add_devices(devices, True) def service_handle(service): """Handle for services.""" entity_ids = service.data.get(ATTR_ENTITY_ID) source = service.data.get(ATTR_SOURCE) if entity_ids: - devices = [device for device in hass.data[DATA_BLACKBIRD] + devices = [device for device in hass.data[DATA_BLACKBIRD].values() if device.entity_id in entity_ids] else: - devices = hass.data[DATA_BLACKBIRD] + devices = hass.data[DATA_BLACKBIRD].values() for device in devices: if service.service == SERVICE_SETALLZONES: @@ -146,14 +144,13 @@ class BlackbirdZone(MediaPlayerDevice): """Retrieve latest state.""" state = self._blackbird.zone_status(self._zone_id) if not state: - return False + return self._state = STATE_ON if state.power else STATE_OFF idx = state.av if idx in self._source_id_name: self._source = self._source_id_name[idx] else: self._source = None - return True @property def name(self): @@ -187,7 +184,6 @@ class BlackbirdZone(MediaPlayerDevice): def set_all_zones(self, source): """Set all zones to one source.""" - _LOGGER.debug("Setting all zones") if source not in self._source_name_id: return idx = self._source_name_id[source] diff --git a/tests/components/media_player/test_blackbird.py b/tests/components/media_player/test_blackbird.py index 86bfdfb52c4..eea6295b79e 100644 --- a/tests/components/media_player/test_blackbird.py +++ b/tests/components/media_player/test_blackbird.py @@ -59,7 +59,6 @@ class TestBlackbirdSchema(unittest.TestCase): """Test valid schema.""" valid_schema = { 'platform': 'blackbird', - 'type': 'serial', 'port': '/dev/ttyUSB0', 'zones': {1: {'name': 'a'}, 2: {'name': 'a'}, @@ -87,8 +86,7 @@ class TestBlackbirdSchema(unittest.TestCase): """Test valid schema.""" valid_schema = { 'platform': 'blackbird', - 'type': 'socket', - 'port': '192.168.1.50', + 'host': '192.168.1.50', 'zones': {1: {'name': 'a'}, 2: {'name': 'a'}, 3: {'name': 'a'}, @@ -109,10 +107,18 @@ class TestBlackbirdSchema(unittest.TestCase): schemas = ( {}, # Empty None, # None - # Missing type + # Port and host used concurrently + { + 'platform': 'blackbird', + 'port': '/dev/ttyUSB0', + 'host': '192.168.1.50', + 'name': 'Name', + 'zones': {1: {'name': 'a'}}, + 'sources': {1: {'name': 'b'}}, + }, + # Port or host missing { 'platform': 'blackbird', - 'port': 'aaa', 'name': 'Name', 'zones': {1: {'name': 'a'}}, 'sources': {1: {'name': 'b'}}, @@ -120,8 +126,7 @@ class TestBlackbirdSchema(unittest.TestCase): # Invalid zone number { 'platform': 'blackbird', - 'type': 'serial', - 'port': 'aaa', + 'port': '/dev/ttyUSB0', 'name': 'Name', 'zones': {11: {'name': 'a'}}, 'sources': {1: {'name': 'b'}}, @@ -129,8 +134,7 @@ class TestBlackbirdSchema(unittest.TestCase): # Invalid source number { 'platform': 'blackbird', - 'type': 'serial', - 'port': 'aaa', + 'port': '/dev/ttyUSB0', 'name': 'Name', 'zones': {1: {'name': 'a'}}, 'sources': {9: {'name': 'b'}}, @@ -138,8 +142,7 @@ class TestBlackbirdSchema(unittest.TestCase): # Zone missing name { 'platform': 'blackbird', - 'type': 'serial', - 'port': 'aaa', + 'port': '/dev/ttyUSB0', 'name': 'Name', 'zones': {1: {}}, 'sources': {1: {'name': 'b'}}, @@ -147,21 +150,11 @@ class TestBlackbirdSchema(unittest.TestCase): # Source missing name { 'platform': 'blackbird', - 'type': 'serial', - 'port': 'aaa', + 'port': '/dev/ttyUSB0', 'name': 'Name', 'zones': {1: {'name': 'a'}}, 'sources': {1: {}}, }, - # Invalid type - { - 'platform': 'blackbird', - 'type': 'aaa', - 'port': 'aaa', - 'name': 'Name', - 'zones': {1: {'name': 'a'}}, - 'sources': {1: {'name': 'b'}}, - }, ) for value in schemas: with self.assertRaises(vol.MultipleInvalid): @@ -181,7 +174,6 @@ class TestBlackbirdMediaPlayer(unittest.TestCase): new=lambda *a: self.blackbird): setup_platform(self.hass, { 'platform': 'blackbird', - 'type': 'serial', 'port': '/dev/ttyUSB0', 'zones': {3: {'name': 'Zone name'}}, 'sources': {1: {'name': 'one'}, @@ -189,7 +181,7 @@ class TestBlackbirdMediaPlayer(unittest.TestCase): 2: {'name': 'two'}}, }, lambda *args, **kwargs: None, {}) self.hass.block_till_done() - self.media_player = self.hass.data[DATA_BLACKBIRD][0] + self.media_player = self.hass.data[DATA_BLACKBIRD]['/dev/ttyUSB0-3'] self.media_player.hass = self.hass self.media_player.entity_id = 'media_player.zone_3' @@ -203,7 +195,8 @@ class TestBlackbirdMediaPlayer(unittest.TestCase): self.assertTrue(self.hass.services.has_service(DOMAIN, SERVICE_SETALLZONES)) self.assertEqual(len(self.hass.data[DATA_BLACKBIRD]), 1) - self.assertEqual(self.hass.data[DATA_BLACKBIRD][0].name, 'Zone name') + self.assertEqual(self.hass.data[DATA_BLACKBIRD]['/dev/ttyUSB0-3'].name, + 'Zone name') def test_setallzones_service_call_with_entity_id(self): """Test set all zone source service call with entity id.""" From bf056b6f019199a15ad0b78bf1e5439c60861d61 Mon Sep 17 00:00:00 2001 From: Anders Melchiorsen Date: Wed, 2 May 2018 15:25:08 +0200 Subject: [PATCH 098/155] Fix Hue color state for missing xy (#14230) --- homeassistant/components/light/hue.py | 2 +- tests/components/light/test_hue.py | 13 +++++++++++++ 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/light/hue.py b/homeassistant/components/light/hue.py index 9f662718514..837a6f82510 100644 --- a/homeassistant/components/light/hue.py +++ b/homeassistant/components/light/hue.py @@ -245,7 +245,7 @@ class HueLight(Light): mode = self._color_mode source = self.light.action if self.is_group else self.light.state - if mode in ('xy', 'hs'): + if mode in ('xy', 'hs') and 'xy' in source: return color.color_xy_to_hs(*source['xy']) return None diff --git a/tests/components/light/test_hue.py b/tests/components/light/test_hue.py index 8f5b52ea6de..a1e3867f9c3 100644 --- a/tests/components/light/test_hue.py +++ b/tests/components/light/test_hue.py @@ -650,6 +650,19 @@ def test_hs_color(): assert light.hs_color is None + light = hue_light.HueLight( + light=Mock(state={ + 'colormode': 'hs', + 'hue': 1234, + 'sat': 123, + }), + request_bridge_update=None, + bridge=Mock(), + is_group=False, + ) + + assert light.hs_color is None + light = hue_light.HueLight( light=Mock(state={ 'colormode': 'xy', From ce98dfe3959bda7c80542d14e6713b613bd15661 Mon Sep 17 00:00:00 2001 From: Mathieu Velten Date: Wed, 2 May 2018 15:38:24 +0200 Subject: [PATCH 099/155] Add support for tracking devices on Netgear access points (#13331) * Netgear: add support for tracking devices on access points * Netgear: add SSL support and autodetection --- .../components/device_tracker/netgear.py | 103 ++++++++++++++---- requirements_all.txt | 2 +- 2 files changed, 84 insertions(+), 21 deletions(-) diff --git a/homeassistant/components/device_tracker/netgear.py b/homeassistant/components/device_tracker/netgear.py index 25d5d38b2a7..0e48e3072b2 100644 --- a/homeassistant/components/device_tracker/netgear.py +++ b/homeassistant/components/device_tracker/netgear.py @@ -12,21 +12,27 @@ import homeassistant.helpers.config_validation as cv from homeassistant.components.device_tracker import ( DOMAIN, PLATFORM_SCHEMA, DeviceScanner) from homeassistant.const import ( - CONF_HOST, CONF_PASSWORD, CONF_USERNAME, CONF_PORT) + CONF_HOST, CONF_PASSWORD, CONF_USERNAME, CONF_PORT, CONF_SSL, + CONF_DEVICES, CONF_EXCLUDE) -REQUIREMENTS = ['pynetgear==0.3.3'] +REQUIREMENTS = ['pynetgear==0.4.0'] _LOGGER = logging.getLogger(__name__) -DEFAULT_HOST = 'routerlogin.net' -DEFAULT_USER = 'admin' -DEFAULT_PORT = 5000 +CONF_APS = 'accesspoints' PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Optional(CONF_HOST, default=DEFAULT_HOST): cv.string, - vol.Optional(CONF_USERNAME, default=DEFAULT_USER): cv.string, + vol.Optional(CONF_HOST, default=''): cv.string, + vol.Optional(CONF_SSL, default=False): cv.boolean, + vol.Optional(CONF_USERNAME, default=''): cv.string, vol.Required(CONF_PASSWORD): cv.string, - vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port + vol.Optional(CONF_PORT, default=None): vol.Any(None, cv.port), + vol.Optional(CONF_DEVICES, default=[]): + vol.All(cv.ensure_list, [cv.string]), + vol.Optional(CONF_EXCLUDE, default=[]): + vol.All(cv.ensure_list, [cv.string]), + vol.Optional(CONF_APS, default=[]): + vol.All(cv.ensure_list, [cv.string]), }) @@ -34,11 +40,16 @@ def get_scanner(hass, config): """Validate the configuration and returns a Netgear scanner.""" info = config[DOMAIN] host = info.get(CONF_HOST) + ssl = info.get(CONF_SSL) username = info.get(CONF_USERNAME) password = info.get(CONF_PASSWORD) port = info.get(CONF_PORT) + devices = info.get(CONF_DEVICES) + excluded_devices = info.get(CONF_EXCLUDE) + accesspoints = info.get(CONF_APS) - scanner = NetgearDeviceScanner(host, username, password, port) + scanner = NetgearDeviceScanner(host, ssl, username, password, port, + devices, excluded_devices, accesspoints) return scanner if scanner.success_init else None @@ -46,16 +57,21 @@ def get_scanner(hass, config): class NetgearDeviceScanner(DeviceScanner): """Queries a Netgear wireless router using the SOAP-API.""" - def __init__(self, host, username, password, port): + def __init__(self, host, ssl, username, password, port, devices, + excluded_devices, accesspoints): """Initialize the scanner.""" import pynetgear + self.tracked_devices = devices + self.excluded_devices = excluded_devices + self.tracked_accesspoints = accesspoints + self.last_results = [] - self._api = pynetgear.Netgear(password, host, username, port) + self._api = pynetgear.Netgear(password, host, username, port, ssl) _LOGGER.info("Logging in") - results = self._api.get_attached_devices() + results = self.get_attached_devices() self.success_init = results is not None @@ -68,15 +84,50 @@ class NetgearDeviceScanner(DeviceScanner): """Scan for new devices and return a list with found device IDs.""" self._update_info() - return (device.mac for device in self.last_results) + devices = [] + + for dev in self.last_results: + tracked = (not self.tracked_devices or + dev.mac in self.tracked_devices or + dev.name in self.tracked_devices) + tracked = tracked and (not self.excluded_devices or not( + dev.mac in self.excluded_devices or + dev.name in self.excluded_devices)) + if tracked: + devices.append(dev.mac) + if (self.tracked_accesspoints and + dev.conn_ap_mac in self.tracked_accesspoints): + devices.append(dev.mac + "_" + dev.conn_ap_mac) + + return devices def get_device_name(self, device): - """Return the name of the given device or None if we don't know.""" - try: - return next(result.name for result in self.last_results - if result.mac == device) - except StopIteration: - return None + """Return the name of the given device or the MAC if we don't know.""" + parts = device.split("_") + mac = parts[0] + ap_mac = None + if len(parts) > 1: + ap_mac = parts[1] + + name = None + for dev in self.last_results: + if dev.mac == mac: + name = dev.name + break + + if not name or name == "--": + name = mac + + if ap_mac: + ap_name = "Router" + for dev in self.last_results: + if dev.mac == ap_mac: + ap_name = dev.name + break + + return name + " on " + ap_name + + return name def _update_info(self): """Retrieve latest information from the Netgear router. @@ -88,9 +139,21 @@ class NetgearDeviceScanner(DeviceScanner): _LOGGER.info("Scanning") - results = self._api.get_attached_devices() + results = self.get_attached_devices() if results is None: _LOGGER.warning("Error scanning devices") self.last_results = results or [] + + def get_attached_devices(self): + """ + List attached devices with pynetgear. + + The v2 method takes more time and is more heavy on the router + so we only use it if we need connected AP info. + """ + if self.tracked_accesspoints: + return self._api.get_attached_devices_2() + + return self._api.get_attached_devices() diff --git a/requirements_all.txt b/requirements_all.txt index a609ee5f7f2..16f6001d576 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -869,7 +869,7 @@ pymysensors==0.11.1 pynello==1.5.1 # homeassistant.components.device_tracker.netgear -pynetgear==0.3.3 +pynetgear==0.4.0 # homeassistant.components.switch.netio pynetio==0.1.6 From 14c7fa888225e48280ffa7b1e77253467ef8f8e6 Mon Sep 17 00:00:00 2001 From: Otto Winter Date: Wed, 2 May 2018 20:23:07 +0200 Subject: [PATCH 100/155] WUnderground unique ids (#13311) * WUnderground unique_id * Remove async_generate_entity_id * Lint * Address comment --- .../components/sensor/wunderground.py | 48 +++++++++---------- tests/components/sensor/test_wunderground.py | 24 +++++++++- 2 files changed, 46 insertions(+), 26 deletions(-) diff --git a/homeassistant/components/sensor/wunderground.py b/homeassistant/components/sensor/wunderground.py index bbee167d4b0..7f2df4bcda9 100644 --- a/homeassistant/components/sensor/wunderground.py +++ b/homeassistant/components/sensor/wunderground.py @@ -15,13 +15,13 @@ import voluptuous as vol from homeassistant.helpers.typing import HomeAssistantType, ConfigType from homeassistant.components import sensor -from homeassistant.components.sensor import PLATFORM_SCHEMA, ENTITY_ID_FORMAT +from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.const import ( CONF_MONITORED_CONDITIONS, CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE, TEMP_FAHRENHEIT, TEMP_CELSIUS, LENGTH_INCHES, LENGTH_KILOMETERS, - LENGTH_MILES, LENGTH_FEET, ATTR_ATTRIBUTION, CONF_ENTITY_NAMESPACE) + LENGTH_MILES, LENGTH_FEET, ATTR_ATTRIBUTION) from homeassistant.exceptions import PlatformNotReady -from homeassistant.helpers.entity import Entity, async_generate_entity_id +from homeassistant.helpers.entity import Entity from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.util import Throttle import homeassistant.helpers.config_validation as cv @@ -618,8 +618,6 @@ LANG_CODES = [ 'CY', 'SN', 'JI', 'YI', ] -DEFAULT_ENTITY_NAMESPACE = 'pws' - PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Required(CONF_API_KEY): cv.string, vol.Optional(CONF_PWS_ID): cv.string, @@ -629,31 +627,30 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Inclusive(CONF_LONGITUDE, 'coordinates', 'Latitude and longitude must exist together'): cv.longitude, vol.Required(CONF_MONITORED_CONDITIONS): - vol.All(cv.ensure_list, vol.Length(min=1), [vol.In(SENSOR_TYPES)]), - vol.Optional(CONF_ENTITY_NAMESPACE, - default=DEFAULT_ENTITY_NAMESPACE): cv.string, + vol.All(cv.ensure_list, vol.Length(min=1), [vol.In(SENSOR_TYPES)]) }) -# Stores a list of entity ids we added in order to support multiple stations -# at once. -ADDED_ENTITY_IDS_KEY = 'wunderground_added_entity_ids' - async def async_setup_platform(hass: HomeAssistantType, config: ConfigType, async_add_devices, discovery_info=None): """Set up the WUnderground sensor.""" - hass.data.setdefault(ADDED_ENTITY_IDS_KEY, set()) - latitude = config.get(CONF_LATITUDE, hass.config.latitude) longitude = config.get(CONF_LONGITUDE, hass.config.longitude) - namespace = config.get(CONF_ENTITY_NAMESPACE) + pws_id = config.get(CONF_PWS_ID) rest = WUndergroundData( - hass, config.get(CONF_API_KEY), config.get(CONF_PWS_ID), + hass, config.get(CONF_API_KEY), pws_id, config.get(CONF_LANG), latitude, longitude) + + if pws_id is None: + unique_id_base = "@{:06f},{:06f}".format(longitude, latitude) + else: + # Manually specified weather station, use that for unique_id + unique_id_base = pws_id sensors = [] for variable in config[CONF_MONITORED_CONDITIONS]: - sensors.append(WUndergroundSensor(hass, rest, variable, namespace)) + sensors.append(WUndergroundSensor(hass, rest, variable, + unique_id_base)) await rest.async_update() if not rest.data: @@ -666,7 +663,7 @@ class WUndergroundSensor(Entity): """Implementing the WUnderground sensor.""" def __init__(self, hass: HomeAssistantType, rest, condition, - namespace: str): + unique_id_base: str): """Initialize the sensor.""" self.rest = rest self._condition = condition @@ -678,12 +675,10 @@ class WUndergroundSensor(Entity): self._entity_picture = None self._unit_of_measurement = self._cfg_expand("unit_of_measurement") self.rest.request_feature(SENSOR_TYPES[condition].feature) - current_ids = set(hass.states.async_entity_ids(sensor.DOMAIN)) - current_ids |= hass.data[ADDED_ENTITY_IDS_KEY] - self.entity_id = async_generate_entity_id( - ENTITY_ID_FORMAT, "{} {}".format(namespace, condition), - current_ids=current_ids) - hass.data[ADDED_ENTITY_IDS_KEY].add(self.entity_id) + # This is only the suggested entity id, it might get changed by + # the entity registry later. + self.entity_id = sensor.ENTITY_ID_FORMAT.format('pws_' + condition) + self._unique_id = "{},{}".format(unique_id_base, condition) def _cfg_expand(self, what, default=None): """Parse and return sensor data.""" @@ -763,6 +758,11 @@ class WUndergroundSensor(Entity): self._entity_picture = re.sub(r'^http://', 'https://', url, flags=re.IGNORECASE) + @property + def unique_id(self) -> str: + """Return a unique ID.""" + return self._unique_id + class WUndergroundData(object): """Get data from WUnderground.""" diff --git a/tests/components/sensor/test_wunderground.py b/tests/components/sensor/test_wunderground.py index 65526e2d938..3f490b4ab12 100644 --- a/tests/components/sensor/test_wunderground.py +++ b/tests/components/sensor/test_wunderground.py @@ -148,10 +148,11 @@ def test_invalid_data(hass, aioclient_mock): async def test_entity_id_with_multiple_stations(hass, aioclient_mock): """Test not generating duplicate entity ids with multiple stations.""" aioclient_mock.get(URL, text=load_fixture('wunderground-valid.json')) + aioclient_mock.get(PWS_URL, text=load_fixture('wunderground-valid.json')) config = [ VALID_CONFIG, - {**VALID_CONFIG, 'entity_namespace': 'hi'} + {**VALID_CONFIG_PWS, 'entity_namespace': 'hi'} ] await async_setup_component(hass, 'sensor', {'sensor': config}) await hass.async_block_till_done() @@ -160,6 +161,25 @@ async def test_entity_id_with_multiple_stations(hass, aioclient_mock): assert state is not None assert state.state == 'Clear' - state = hass.states.get('sensor.hi_weather') + state = hass.states.get('sensor.hi_pws_weather') assert state is not None assert state.state == 'Clear' + + +async def test_fails_because_of_unique_id(hass, aioclient_mock): + """Test same config twice fails because of unique_id.""" + aioclient_mock.get(URL, text=load_fixture('wunderground-valid.json')) + aioclient_mock.get(PWS_URL, text=load_fixture('wunderground-valid.json')) + + config = [ + VALID_CONFIG, + {**VALID_CONFIG, 'entity_namespace': 'hi'}, + VALID_CONFIG_PWS + ] + await async_setup_component(hass, 'sensor', {'sensor': config}) + await hass.async_block_till_done() + + states = hass.states.async_all() + expected = len(VALID_CONFIG['monitored_conditions']) + \ + len(VALID_CONFIG_PWS['monitored_conditions']) + assert len(states) == expected From b66be59598c3d47413e2eb4d516216f8b52a3e09 Mon Sep 17 00:00:00 2001 From: Mick Vleeshouwer Date: Wed, 2 May 2018 20:37:41 +0200 Subject: [PATCH 101/155] Add PostNL sensor (Dutch Postal Services) (#12366) * Add basic PostNL sensor (WIP) * Update PostNL sensor * Bump version * Small updates to PostNL package based on feedback * Remove unused import * Pass api to sensor * Refactor based on feedback * Update based on feedback * Fix feedback * Clean up * Bugfiix * Bugfix * SCAN_INTERVAL fix * Remove unused import * Refactor for new wrapper implementation * Update postnl package requirement * Change throttle logic * Update package version * Add new line * Minor changes * Change refresh time to 30 minutes * Update requirements_all.txt --- .coveragerc | 1 + homeassistant/components/sensor/postnl.py | 110 ++++++++++++++++++++++ requirements_all.txt | 3 + 3 files changed, 114 insertions(+) create mode 100644 homeassistant/components/sensor/postnl.py diff --git a/.coveragerc b/.coveragerc index 94722666c05..cf7a5a2cd9c 100644 --- a/.coveragerc +++ b/.coveragerc @@ -640,6 +640,7 @@ omit = homeassistant/components/sensor/plex.py homeassistant/components/sensor/pocketcasts.py homeassistant/components/sensor/pollen.py + homeassistant/components/sensor/postnl.py homeassistant/components/sensor/pushbullet.py homeassistant/components/sensor/pvoutput.py homeassistant/components/sensor/pyload.py diff --git a/homeassistant/components/sensor/postnl.py b/homeassistant/components/sensor/postnl.py new file mode 100644 index 00000000000..c38f58b7916 --- /dev/null +++ b/homeassistant/components/sensor/postnl.py @@ -0,0 +1,110 @@ +""" +Sensor for PostNL packages. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/sensor.postnl/ +""" +from datetime import timedelta +import logging + +import voluptuous as vol + +from homeassistant.components.sensor import PLATFORM_SCHEMA +from homeassistant.const import ( + ATTR_ATTRIBUTION, CONF_NAME, CONF_PASSWORD, CONF_USERNAME) +import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.entity import Entity +from homeassistant.util import Throttle + +REQUIREMENTS = ['postnl_api==1.0.1'] + +_LOGGER = logging.getLogger(__name__) + +ATTRIBUTION = 'Information provided by PostNL' + +DEFAULT_NAME = 'postnl' + +ICON = 'mdi:package-variant-closed' + +MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=30) + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Required(CONF_USERNAME): cv.string, + vol.Required(CONF_PASSWORD): cv.string, + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, +}) + + +# 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 + + username = config.get(CONF_USERNAME) + password = config.get(CONF_PASSWORD) + name = config.get(CONF_NAME) + + try: + api = PostNL_API(username, password) + + except UnauthorizedException: + _LOGGER.exception("Can't connect to the PostNL webservice") + return + + add_devices([PostNLSensor(api, name)], True) + + +class PostNLSensor(Entity): + """Representation of a PostNL sensor.""" + + def __init__(self, api, name): + """Initialize the PostNL sensor.""" + self._name = name + self._attributes = None + self._state = None + self._api = api + + @property + def name(self): + """Return the name of the sensor.""" + return self._name + + @property + def state(self): + """Return the state of the sensor.""" + return self._state + + @property + def unit_of_measurement(self): + """Return the unit of measurement of this entity, if any.""" + return 'package(s)' + + @property + def device_state_attributes(self): + """Return the state attributes.""" + return self._attributes + + @property + def icon(self): + """Icon to use in the frontend.""" + return ICON + + @Throttle(MIN_TIME_BETWEEN_UPDATES) + def update(self): + """Update device state.""" + shipments = self._api.get_relevant_shipments() + status_counts = {} + + for shipment in shipments: + status = shipment['status']['formatted']['short'] + status = self._api.parse_datetime(status, '%d-%m-%Y', '%H:%M') + + name = shipment['settings']['title'] + status_counts[name] = status + + self._attributes = { + ATTR_ATTRIBUTION: ATTRIBUTION, + **status_counts + } + + self._state = len(status_counts) diff --git a/requirements_all.txt b/requirements_all.txt index 16f6001d576..1283011d7ac 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -632,6 +632,9 @@ pmsensor==0.4 # homeassistant.components.sensor.pocketcasts pocketcasts==0.1 +# homeassistant.components.sensor.postnl +postnl_api==1.0.1 + # homeassistant.components.climate.proliphix proliphix==0.4.1 From 351e8921fa2e39d0711bc28a65b33ddd887de6db Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Per=20Osb=C3=A4ck?= Date: Wed, 2 May 2018 21:06:09 +0200 Subject: [PATCH 102/155] python_openzwave update config service (#12060) * update python-openzwave to 4.1.0 * add service which updates the configuration files from github * 0.4.3 --- homeassistant/components/zwave/__init__.py | 7 +++++++ homeassistant/components/zwave/const.py | 1 + homeassistant/components/zwave/services.yaml | 3 +++ 3 files changed, 11 insertions(+) diff --git a/homeassistant/components/zwave/__init__.py b/homeassistant/components/zwave/__init__.py index d56b4bc91b4..c2d4a13a934 100644 --- a/homeassistant/components/zwave/__init__.py +++ b/homeassistant/components/zwave/__init__.py @@ -359,6 +359,11 @@ def setup(hass, config): _LOGGER.info("Z-Wave soft_reset have been initialized") network.controller.soft_reset() + def update_config(service): + """Update the config from git.""" + _LOGGER.info("Configuration update has been initialized") + network.controller.update_ozw_config() + def test_network(service): """Test the network by sending commands to all the nodes.""" _LOGGER.info("Z-Wave test_network have been initialized") @@ -616,6 +621,8 @@ def setup(hass, config): hass.services.register(DOMAIN, const.SERVICE_HEAL_NETWORK, heal_network) hass.services.register(DOMAIN, const.SERVICE_SOFT_RESET, soft_reset) + hass.services.register(DOMAIN, const.SERVICE_UPDATE_CONFIG, + update_config) hass.services.register(DOMAIN, const.SERVICE_TEST_NETWORK, test_network) hass.services.register(DOMAIN, const.SERVICE_STOP_NETWORK, diff --git a/homeassistant/components/zwave/const.py b/homeassistant/components/zwave/const.py index 8e1a22047c1..b42b6d0fce7 100644 --- a/homeassistant/components/zwave/const.py +++ b/homeassistant/components/zwave/const.py @@ -51,6 +51,7 @@ SERVICE_RENAME_VALUE = "rename_value" SERVICE_REFRESH_ENTITY = "refresh_entity" SERVICE_REFRESH_NODE = "refresh_node" SERVICE_RESET_NODE_METERS = "reset_node_meters" +SERVICE_UPDATE_CONFIG = "update_config" EVENT_SCENE_ACTIVATED = "zwave.scene_activated" EVENT_NODE_EVENT = "zwave.node_event" diff --git a/homeassistant/components/zwave/services.yaml b/homeassistant/components/zwave/services.yaml index 61855143d59..1762c33237d 100644 --- a/homeassistant/components/zwave/services.yaml +++ b/homeassistant/components/zwave/services.yaml @@ -119,6 +119,9 @@ set_wakeup: value: description: Value of the interval to set. (integer) +update_config: + description: Attempt to update ozw configuration files from git to support newer devices. + start_network: description: Start the Z-Wave network. This might take a while, depending on how big your Z-Wave network is. From f72d5683749340ed5c65bb177d0de61f1e914fd8 Mon Sep 17 00:00:00 2001 From: Andrey Date: Wed, 2 May 2018 23:10:26 +0300 Subject: [PATCH 103/155] Add unique_id to zwave node entity (#14201) * Add unique_id to zwave node entity * Wait 30s before adding zwave node if its unique_id is not ready * Use only node_id in unique_id. Update name, manufacturer, and product attributes on node update. --- homeassistant/components/zwave/__init__.py | 47 +++++++++++++++---- homeassistant/components/zwave/const.py | 1 + homeassistant/components/zwave/node_entity.py | 17 +++++++ tests/components/zwave/test_init.py | 41 ++++++++++++++++ tests/components/zwave/test_node_entity.py | 13 ++++- tests/mock/zwave.py | 4 ++ 6 files changed, 113 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/zwave/__init__.py b/homeassistant/components/zwave/__init__.py index c2d4a13a934..01b17023c12 100644 --- a/homeassistant/components/zwave/__init__.py +++ b/homeassistant/components/zwave/__init__.py @@ -297,15 +297,46 @@ def setup(hass, config): def node_added(node): """Handle a new node on the network.""" entity = ZWaveNodeEntity(node, network) - name = node_name(node) - generated_id = generate_entity_id(DOMAIN + '.{}', name, []) - node_config = device_config.get(generated_id) - if node_config.get(CONF_IGNORED): - _LOGGER.info( - "Ignoring node entity %s due to device settings", - generated_id) + + def _add_node_to_component(): + name = node_name(node) + generated_id = generate_entity_id(DOMAIN + '.{}', name, []) + node_config = device_config.get(generated_id) + if node_config.get(CONF_IGNORED): + _LOGGER.info( + "Ignoring node entity %s due to device settings", + generated_id) + return + component.add_entities([entity]) + + if entity.unique_id: + _add_node_to_component() return - component.add_entities([entity]) + + async def _check_node_ready(): + """Wait for node to be parsed.""" + start_time = dt_util.utcnow() + while True: + waited = int((dt_util.utcnow()-start_time).total_seconds()) + + if entity.unique_id: + _LOGGER.info("Z-Wave node %d ready after %d seconds", + entity.node_id, waited) + break + elif waited >= const.NODE_READY_WAIT_SECS: + # Wait up to NODE_READY_WAIT_SECS seconds for the Z-Wave + # node to be ready. + _LOGGER.warning( + "Z-Wave node %d not ready after %d seconds, " + "continuing anyway", + entity.node_id, waited) + break + else: + await asyncio.sleep(1, loop=hass.loop) + + hass.async_add_job(_add_node_to_component) + + hass.add_job(_check_node_ready) def network_ready(): """Handle the query of all awake nodes.""" diff --git a/homeassistant/components/zwave/const.py b/homeassistant/components/zwave/const.py index b42b6d0fce7..3e503e4d9a4 100644 --- a/homeassistant/components/zwave/const.py +++ b/homeassistant/components/zwave/const.py @@ -20,6 +20,7 @@ ATTR_POLL_INTENSITY = "poll_intensity" ATTR_VALUE_INDEX = "value_index" ATTR_VALUE_INSTANCE = "value_instance" NETWORK_READY_WAIT_SECS = 300 +NODE_READY_WAIT_SECS = 30 DISCOVERY_DEVICE = 'device' diff --git a/homeassistant/components/zwave/node_entity.py b/homeassistant/components/zwave/node_entity.py index 5a4b1b02504..bcddcb0b800 100644 --- a/homeassistant/components/zwave/node_entity.py +++ b/homeassistant/components/zwave/node_entity.py @@ -81,6 +81,7 @@ class ZWaveNodeEntity(ZWaveBaseEntity): self._name = node_name(self.node) self._product_name = node.product_name self._manufacturer_name = node.manufacturer_name + self._unique_id = self._compute_unique_id() self._attributes = {} self.wakeup_interval = None self.location = None @@ -95,6 +96,11 @@ class ZWaveNodeEntity(ZWaveBaseEntity): dispatcher.connect( self.network_scene_activated, ZWaveNetwork.SIGNAL_SCENE_EVENT) + @property + def unique_id(self): + """Unique ID of Z-wave node.""" + return self._unique_id + def network_node_changed(self, node=None, value=None, args=None): """Handle a changed node on the network.""" if node and node.node_id != self.node_id: @@ -138,8 +144,14 @@ class ZWaveNodeEntity(ZWaveBaseEntity): self.wakeup_interval = None self.battery_level = self.node.get_battery_level() + self._product_name = self.node.product_name + self._manufacturer_name = self.node.manufacturer_name + self._name = node_name(self.node) self._attributes = attributes + if not self._unique_id: + self._unique_id = self._compute_unique_id() + self.maybe_schedule_update() def network_node_event(self, node, value): @@ -229,3 +241,8 @@ class ZWaveNodeEntity(ZWaveBaseEntity): attrs[ATTR_WAKEUP] = self.wakeup_interval return attrs + + def _compute_unique_id(self): + if self._manufacturer_name and self._product_name: + return 'node-{}'.format(self.node_id) + return None diff --git a/tests/components/zwave/test_init.py b/tests/components/zwave/test_init.py index 004e5e95ca0..faa7357bd8a 100644 --- a/tests/components/zwave/test_init.py +++ b/tests/components/zwave/test_init.py @@ -224,6 +224,47 @@ def test_node_discovery(hass, mock_openzwave): assert hass.states.get('zwave.mock_node').state is 'unknown' +async def test_unparsed_node_discovery(hass, mock_openzwave): + """Test discovery of a node.""" + mock_receivers = [] + + def mock_connect(receiver, signal, *args, **kwargs): + if signal == MockNetwork.SIGNAL_NODE_ADDED: + mock_receivers.append(receiver) + + with patch('pydispatch.dispatcher.connect', new=mock_connect): + await async_setup_component(hass, 'zwave', {'zwave': {}}) + + assert len(mock_receivers) == 1 + + node = MockNode(node_id=14, manufacturer_name=None) + + sleeps = [] + + def utcnow(): + return datetime.fromtimestamp(len(sleeps)) + + asyncio_sleep = asyncio.sleep + + async def sleep(duration, loop): + if duration > 0: + sleeps.append(duration) + await asyncio_sleep(0, loop=loop) + + with patch('homeassistant.components.zwave.dt_util.utcnow', new=utcnow): + with patch('asyncio.sleep', new=sleep): + with patch.object(zwave, '_LOGGER') as mock_logger: + hass.async_add_job(mock_receivers[0], node) + await hass.async_block_till_done() + + assert len(sleeps) == const.NODE_READY_WAIT_SECS + assert mock_logger.warning.called + assert len(mock_logger.warning.mock_calls) == 1 + assert mock_logger.warning.mock_calls[0][1][1:] == \ + (14, const.NODE_READY_WAIT_SECS) + assert hass.states.get('zwave.mock_node').state is 'unknown' + + @asyncio.coroutine def test_node_ignored(hass, mock_openzwave): """Test discovery of a node.""" diff --git a/tests/components/zwave/test_node_entity.py b/tests/components/zwave/test_node_entity.py index 299821d3685..f4d9b3ef0e8 100644 --- a/tests/components/zwave/test_node_entity.py +++ b/tests/components/zwave/test_node_entity.py @@ -182,8 +182,6 @@ class TestZWaveNodeEntity(unittest.TestCase): query_stage='Dynamic', is_awake=True, is_ready=False, is_failed=False, is_info_received=True, max_baud_rate=40000, is_zwave_plus=False, capabilities=[], neighbors=[], location=None) - self.node.manufacturer_name = 'Test Manufacturer' - self.node.product_name = 'Test Product' self.entity = node_entity.ZWaveNodeEntity(self.node, self.zwave_network) @@ -357,3 +355,14 @@ class TestZWaveNodeEntity(unittest.TestCase): def test_not_polled(self): """Test should_poll property.""" self.assertFalse(self.entity.should_poll) + + def test_unique_id(self): + """Test unique_id.""" + self.assertEqual('node-567', self.entity.unique_id) + + def test_unique_id_missing_data(self): + """Test unique_id.""" + self.node.manufacturer_name = None + entity = node_entity.ZWaveNodeEntity(self.node, self.zwave_network) + + self.assertIsNone(entity.unique_id) diff --git a/tests/mock/zwave.py b/tests/mock/zwave.py index 672cc884904..67bfb590c3f 100644 --- a/tests/mock/zwave.py +++ b/tests/mock/zwave.py @@ -119,6 +119,8 @@ class MockNode(MagicMock): product_type='678', command_classes=None, can_wake_up_value=True, + manufacturer_name='Test Manufacturer', + product_name='Test Product', network=None, **kwargs): """Initialize a Z-Wave mock node.""" @@ -128,6 +130,8 @@ class MockNode(MagicMock): self.manufacturer_id = manufacturer_id self.product_id = product_id self.product_type = product_type + self.manufacturer_name = manufacturer_name + self.product_name = product_name self.can_wake_up_value = can_wake_up_value self._command_classes = command_classes or [] if network is not None: From 64b9fbd8d9b654ca6a94d85a53e741f8ad22217b Mon Sep 17 00:00:00 2001 From: Mark Coombes Date: Wed, 2 May 2018 16:28:43 -0400 Subject: [PATCH 104/155] Add prereqs for HomeKit Controller (#14172) --- virtualization/Docker/setup_docker_prereqs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/virtualization/Docker/setup_docker_prereqs b/virtualization/Docker/setup_docker_prereqs index bd70af28dce..302dfba2e1d 100755 --- a/virtualization/Docker/setup_docker_prereqs +++ b/virtualization/Docker/setup_docker_prereqs @@ -25,6 +25,8 @@ PACKAGES=( libsodium13 # homeassistant.components.zwave libudev-dev + # homeassistant.components.homekit_controller + libmpc-dev libmpfr-dev libgmp-dev ) # Required debian packages for building dependencies From c851dfa2c7bd77601976102d566d695006499fab Mon Sep 17 00:00:00 2001 From: Diogo Gomes Date: Wed, 2 May 2018 22:29:07 +0100 Subject: [PATCH 105/155] Restores switch state, case the switch is optimistic (#14151) * Add restore_state to optimistic switch * no need to schedule update * test added * lint * new async syntax * lint --- homeassistant/components/switch/mqtt.py | 9 +++++++- tests/components/switch/test_mqtt.py | 30 +++++++++++++++---------- 2 files changed, 26 insertions(+), 13 deletions(-) diff --git a/homeassistant/components/switch/mqtt.py b/homeassistant/components/switch/mqtt.py index 15dc6f1d0f4..69f12536c5f 100644 --- a/homeassistant/components/switch/mqtt.py +++ b/homeassistant/components/switch/mqtt.py @@ -16,9 +16,10 @@ from homeassistant.components.mqtt import ( from homeassistant.components.switch import SwitchDevice from homeassistant.const import ( CONF_NAME, CONF_OPTIMISTIC, CONF_VALUE_TEMPLATE, CONF_PAYLOAD_OFF, - CONF_PAYLOAD_ON, CONF_ICON) + CONF_PAYLOAD_ON, CONF_ICON, STATE_ON) import homeassistant.components.mqtt as mqtt import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.restore_state import async_get_last_state _LOGGER = logging.getLogger(__name__) @@ -112,6 +113,12 @@ class MqttSwitch(MqttAvailability, SwitchDevice): self.hass, self._state_topic, state_message_received, self._qos) + if self._optimistic: + last_state = await async_get_last_state(self.hass, + self.entity_id) + if last_state: + self._state = last_state.state == STATE_ON + @property def should_poll(self): """Return the polling state.""" diff --git a/tests/components/switch/test_mqtt.py b/tests/components/switch/test_mqtt.py index f79d0706321..b5e2a0b0395 100644 --- a/tests/components/switch/test_mqtt.py +++ b/tests/components/switch/test_mqtt.py @@ -1,12 +1,14 @@ """The tests for the MQTT switch platform.""" import unittest +from unittest.mock import patch from homeassistant.setup import setup_component from homeassistant.const import STATE_ON, STATE_OFF, STATE_UNAVAILABLE,\ ATTR_ASSUMED_STATE +import homeassistant.core as ha import homeassistant.components.switch as switch from tests.common import ( - mock_mqtt_component, fire_mqtt_message, get_test_home_assistant) + mock_mqtt_component, fire_mqtt_message, get_test_home_assistant, mock_coro) class TestSwitchMQTT(unittest.TestCase): @@ -52,19 +54,23 @@ class TestSwitchMQTT(unittest.TestCase): def test_sending_mqtt_commands_and_optimistic(self): """Test the sending MQTT commands in optimistic mode.""" - assert setup_component(self.hass, switch.DOMAIN, { - switch.DOMAIN: { - 'platform': 'mqtt', - 'name': 'test', - 'command_topic': 'command-topic', - 'payload_on': 'beer on', - 'payload_off': 'beer off', - 'qos': '2' - } - }) + fake_state = ha.State('switch.test', 'on') + + with patch('homeassistant.components.switch.mqtt.async_get_last_state', + return_value=mock_coro(fake_state)): + assert setup_component(self.hass, switch.DOMAIN, { + switch.DOMAIN: { + 'platform': 'mqtt', + 'name': 'test', + 'command_topic': 'command-topic', + 'payload_on': 'beer on', + 'payload_off': 'beer off', + 'qos': '2' + } + }) state = self.hass.states.get('switch.test') - self.assertEqual(STATE_OFF, state.state) + self.assertEqual(STATE_ON, state.state) self.assertTrue(state.attributes.get(ATTR_ASSUMED_STATE)) switch.turn_on(self.hass, 'switch.test') From ef4498ec27583b9d34ddd30ee60f8d2106a90070 Mon Sep 17 00:00:00 2001 From: giangvo Date: Thu, 3 May 2018 07:45:31 +1000 Subject: [PATCH 106/155] Issue/add template fans (#12027) * add template fan * add-template: address PR comments * add-template: remove unused import * add-template: revert async_track_state_change change * add-template: use yield from * Revert "add-template: use yield from" This reverts commit 1e053714a7c75c29367e3d04cf52161ebfaabba1. * add-template: use yield * add-template: remove unused import * add-template: remove async_add_job usages * use components * add-template: use async/await * add-template: fix style * add-template: remove str() * address pr comments * fix style --- homeassistant/components/fan/template.py | 324 +++++++++++++ tests/components/fan/test_template.py | 549 +++++++++++++++++++++++ 2 files changed, 873 insertions(+) create mode 100644 homeassistant/components/fan/template.py create mode 100644 tests/components/fan/test_template.py diff --git a/homeassistant/components/fan/template.py b/homeassistant/components/fan/template.py new file mode 100644 index 00000000000..31b335eb2bc --- /dev/null +++ b/homeassistant/components/fan/template.py @@ -0,0 +1,324 @@ +""" +Support for Template fans. + +For more details about this platform, please refer to the documentation +https://home-assistant.io/components/fan.template/ +""" +import logging + +import voluptuous as vol + +from homeassistant.core import callback +from homeassistant.const import ( + CONF_FRIENDLY_NAME, CONF_VALUE_TEMPLATE, CONF_ENTITY_ID, + STATE_ON, STATE_OFF, MATCH_ALL, EVENT_HOMEASSISTANT_START, + STATE_UNKNOWN) + +from homeassistant.exceptions import TemplateError +import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.config_validation import PLATFORM_SCHEMA +from homeassistant.helpers.entity import ToggleEntity +from homeassistant.components.fan import (SPEED_LOW, SPEED_MEDIUM, + SPEED_HIGH, SUPPORT_SET_SPEED, + SUPPORT_OSCILLATE, FanEntity, + ATTR_SPEED, ATTR_OSCILLATING, + ENTITY_ID_FORMAT) + +from homeassistant.helpers.entity import async_generate_entity_id +from homeassistant.helpers.script import Script + +_LOGGER = logging.getLogger(__name__) + +CONF_FANS = 'fans' +CONF_SPEED_LIST = 'speeds' +CONF_SPEED_TEMPLATE = 'speed_template' +CONF_OSCILLATING_TEMPLATE = 'oscillating_template' +CONF_ON_ACTION = 'turn_on' +CONF_OFF_ACTION = 'turn_off' +CONF_SET_SPEED_ACTION = 'set_speed' +CONF_SET_OSCILLATING_ACTION = 'set_oscillating' + +_VALID_STATES = [STATE_ON, STATE_OFF] +_VALID_OSC = [True, False] + +FAN_SCHEMA = vol.Schema({ + vol.Optional(CONF_FRIENDLY_NAME): cv.string, + vol.Required(CONF_VALUE_TEMPLATE): cv.template, + vol.Optional(CONF_SPEED_TEMPLATE): cv.template, + vol.Optional(CONF_OSCILLATING_TEMPLATE): cv.template, + + vol.Required(CONF_ON_ACTION): cv.SCRIPT_SCHEMA, + vol.Required(CONF_OFF_ACTION): cv.SCRIPT_SCHEMA, + + vol.Optional(CONF_SET_SPEED_ACTION): cv.SCRIPT_SCHEMA, + vol.Optional(CONF_SET_OSCILLATING_ACTION): cv.SCRIPT_SCHEMA, + + vol.Optional( + CONF_SPEED_LIST, + default=[SPEED_LOW, SPEED_MEDIUM, SPEED_HIGH] + ): cv.ensure_list, + + vol.Optional(CONF_ENTITY_ID): cv.entity_ids +}) + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Required(CONF_FANS): vol.Schema({cv.slug: FAN_SCHEMA}), +}) + + +async def async_setup_platform( + hass, config, async_add_devices, discovery_info=None +): + """Set up the Template Fans.""" + fans = [] + + for device, device_config in config[CONF_FANS].items(): + friendly_name = device_config.get(CONF_FRIENDLY_NAME, device) + + state_template = device_config[CONF_VALUE_TEMPLATE] + speed_template = device_config.get(CONF_SPEED_TEMPLATE) + oscillating_template = device_config.get( + CONF_OSCILLATING_TEMPLATE + ) + + on_action = device_config[CONF_ON_ACTION] + off_action = device_config[CONF_OFF_ACTION] + set_speed_action = device_config.get(CONF_SET_SPEED_ACTION) + set_oscillating_action = device_config.get(CONF_SET_OSCILLATING_ACTION) + + speed_list = device_config[CONF_SPEED_LIST] + + entity_ids = set() + manual_entity_ids = device_config.get(CONF_ENTITY_ID) + + for template in (state_template, speed_template, oscillating_template): + if template is None: + continue + template.hass = hass + + if entity_ids == MATCH_ALL or manual_entity_ids is not None: + continue + + template_entity_ids = template.extract_entities() + if template_entity_ids == MATCH_ALL: + entity_ids = MATCH_ALL + else: + entity_ids |= set(template_entity_ids) + + if manual_entity_ids is not None: + entity_ids = manual_entity_ids + elif entity_ids != MATCH_ALL: + entity_ids = list(entity_ids) + + fans.append( + TemplateFan( + hass, device, friendly_name, + state_template, speed_template, oscillating_template, + on_action, off_action, set_speed_action, + set_oscillating_action, speed_list, entity_ids + ) + ) + + async_add_devices(fans) + + +class TemplateFan(FanEntity): + """A template fan component.""" + + def __init__(self, hass, device_id, friendly_name, + state_template, speed_template, oscillating_template, + on_action, off_action, set_speed_action, + set_oscillating_action, speed_list, entity_ids): + """Initialize the fan.""" + self.hass = hass + self.entity_id = async_generate_entity_id( + ENTITY_ID_FORMAT, device_id, hass=hass) + self._name = friendly_name + + self._template = state_template + self._speed_template = speed_template + self._oscillating_template = oscillating_template + self._supported_features = 0 + + self._on_script = Script(hass, on_action) + self._off_script = Script(hass, off_action) + + self._set_speed_script = None + if set_speed_action: + self._set_speed_script = Script(hass, set_speed_action) + + self._set_oscillating_script = None + if set_oscillating_action: + self._set_oscillating_script = Script(hass, set_oscillating_action) + + self._state = STATE_OFF + self._speed = None + self._oscillating = None + + self._template.hass = self.hass + if self._speed_template: + self._speed_template.hass = self.hass + self._supported_features |= SUPPORT_SET_SPEED + if self._oscillating_template: + self._oscillating_template.hass = self.hass + self._supported_features |= SUPPORT_OSCILLATE + + self._entities = entity_ids + # List of valid speeds + self._speed_list = speed_list + + @property + def name(self): + """Return the display name of this fan.""" + return self._name + + @property + def supported_features(self) -> int: + """Flag supported features.""" + return self._supported_features + + @property + def speed_list(self: ToggleEntity) -> list: + """Get the list of available speeds.""" + return self._speed_list + + @property + def is_on(self): + """Return true if device is on.""" + return self._state == STATE_ON + + @property + def speed(self): + """Return the current speed.""" + return self._speed + + @property + def oscillating(self): + """Return the oscillation state.""" + return self._oscillating + + @property + def should_poll(self): + """Return the polling state.""" + return False + + # pylint: disable=arguments-differ + async def async_turn_on(self, speed: str = None) -> None: + """Turn on the fan.""" + await self._on_script.async_run() + self._state = STATE_ON + + if speed is not None: + await self.async_set_speed(speed) + + # pylint: disable=arguments-differ + async def async_turn_off(self) -> None: + """Turn off the fan.""" + await self._off_script.async_run() + self._state = STATE_OFF + + async def async_set_speed(self, speed: str) -> None: + """Set the speed of the fan.""" + if self._set_speed_script is None: + return + + if speed in self._speed_list: + self._speed = speed + await self._set_speed_script.async_run({ATTR_SPEED: speed}) + else: + _LOGGER.error( + 'Received invalid speed: %s. ' + + 'Expected: %s.', + speed, self._speed_list) + + async def async_oscillate(self, oscillating: bool) -> None: + """Set oscillation of the fan.""" + if self._set_oscillating_script is None: + return + + await self._set_oscillating_script.async_run( + {ATTR_OSCILLATING: oscillating} + ) + self._oscillating = oscillating + + async def async_added_to_hass(self): + """Register callbacks.""" + @callback + def template_fan_state_listener(entity, old_state, new_state): + """Handle target device state changes.""" + self.async_schedule_update_ha_state(True) + + @callback + def template_fan_startup(event): + """Update template on startup.""" + self.hass.helpers.event.async_track_state_change( + self._entities, template_fan_state_listener) + + self.async_schedule_update_ha_state(True) + + self.hass.bus.async_listen_once( + EVENT_HOMEASSISTANT_START, template_fan_startup) + + async def async_update(self): + """Update the state from the template.""" + # Update state + try: + state = self._template.async_render() + except TemplateError as ex: + _LOGGER.error(ex) + state = None + self._state = None + + # Validate state + if state in _VALID_STATES: + self._state = state + elif state == STATE_UNKNOWN: + self._state = None + else: + _LOGGER.error( + 'Received invalid fan is_on state: %s. ' + + 'Expected: %s.', + state, ', '.join(_VALID_STATES)) + self._state = None + + # Update speed if 'speed_template' is configured + if self._speed_template is not None: + try: + speed = self._speed_template.async_render() + except TemplateError as ex: + _LOGGER.error(ex) + speed = None + self._state = None + + # Validate speed + if speed in self._speed_list: + self._speed = speed + elif speed == STATE_UNKNOWN: + self._speed = None + else: + _LOGGER.error( + 'Received invalid speed: %s. ' + + 'Expected: %s.', + speed, self._speed_list) + self._speed = None + + # Update oscillating if 'oscillating_template' is configured + if self._oscillating_template is not None: + try: + oscillating = self._oscillating_template.async_render() + except TemplateError as ex: + _LOGGER.error(ex) + self._state = None + + # Validate osc + if oscillating == 'True' or oscillating is True: + self._oscillating = True + elif oscillating == 'False' or oscillating is False: + self._oscillating = False + elif oscillating == STATE_UNKNOWN: + self._oscillating = None + else: + _LOGGER.error( + 'Received invalid oscillating: %s. ' + + 'Expected: True/False.', oscillating) + self._oscillating = None diff --git a/tests/components/fan/test_template.py b/tests/components/fan/test_template.py new file mode 100644 index 00000000000..719a3f96aed --- /dev/null +++ b/tests/components/fan/test_template.py @@ -0,0 +1,549 @@ +"""The tests for the Template fan platform.""" +import logging + +from homeassistant.core import callback +from homeassistant import setup +import homeassistant.components as components +from homeassistant.const import STATE_ON, STATE_OFF +from homeassistant.components.fan import ( + ATTR_SPEED, ATTR_OSCILLATING, SPEED_LOW, SPEED_MEDIUM, SPEED_HIGH) + +from tests.common import ( + get_test_home_assistant, assert_setup_component) +_LOGGER = logging.getLogger(__name__) + + +_TEST_FAN = 'fan.test_fan' +# Represent for fan's state +_STATE_INPUT_BOOLEAN = 'input_boolean.state' +# Represent for fan's speed +_SPEED_INPUT_SELECT = 'input_select.speed' +# Represent for fan's oscillating +_OSC_INPUT = 'input_select.osc' + + +class TestTemplateFan: + """Test the Template light.""" + + hass = None + calls = None + # pylint: disable=invalid-name + + def setup_method(self, method): + """Setup.""" + self.hass = get_test_home_assistant() + + self.calls = [] + + @callback + def record_call(service): + """Track function calls..""" + self.calls.append(service) + + self.hass.services.register('test', 'automation', record_call) + + def teardown_method(self, method): + """Stop everything that was started.""" + self.hass.stop() + + # Configuration tests # + def test_missing_optional_config(self): + """Test: missing optional template is ok.""" + with assert_setup_component(1, 'fan'): + assert setup.setup_component(self.hass, 'fan', { + 'fan': { + 'platform': 'template', + 'fans': { + 'test_fan': { + 'value_template': "{{ 'on' }}", + + 'turn_on': { + 'service': 'script.fan_on' + }, + 'turn_off': { + 'service': 'script.fan_off' + } + } + } + } + }) + + self.hass.start() + self.hass.block_till_done() + + self._verify(STATE_ON, None, None) + + def test_missing_value_template_config(self): + """Test: missing 'value_template' will fail.""" + with assert_setup_component(0, 'fan'): + assert setup.setup_component(self.hass, 'fan', { + 'fan': { + 'platform': 'template', + 'fans': { + 'test_fan': { + 'turn_on': { + 'service': 'script.fan_on' + }, + 'turn_off': { + 'service': 'script.fan_off' + } + } + } + } + }) + + self.hass.start() + self.hass.block_till_done() + + assert self.hass.states.all() == [] + + def test_missing_turn_on_config(self): + """Test: missing 'turn_on' will fail.""" + with assert_setup_component(0, 'fan'): + assert setup.setup_component(self.hass, 'fan', { + 'fan': { + 'platform': 'template', + 'fans': { + 'test_fan': { + 'value_template': "{{ 'on' }}", + 'turn_off': { + 'service': 'script.fan_off' + } + } + } + } + }) + + self.hass.start() + self.hass.block_till_done() + + assert self.hass.states.all() == [] + + def test_missing_turn_off_config(self): + """Test: missing 'turn_off' will fail.""" + with assert_setup_component(0, 'fan'): + assert setup.setup_component(self.hass, 'fan', { + 'fan': { + 'platform': 'template', + 'fans': { + 'test_fan': { + 'value_template': "{{ 'on' }}", + 'turn_on': { + 'service': 'script.fan_on' + } + } + } + } + }) + + self.hass.start() + self.hass.block_till_done() + + assert self.hass.states.all() == [] + + def test_invalid_config(self): + """Test: missing 'turn_off' will fail.""" + with assert_setup_component(0, 'fan'): + assert setup.setup_component(self.hass, 'fan', { + 'platform': 'template', + 'fans': { + 'test_fan': { + 'value_template': "{{ 'on' }}", + 'turn_on': { + 'service': 'script.fan_on' + } + } + } + }) + + self.hass.start() + self.hass.block_till_done() + + assert self.hass.states.all() == [] + + # End of configuration tests # + + # Template tests # + def test_templates_with_entities(self): + """Test tempalates with values from other entities.""" + value_template = """ + {% if is_state('input_boolean.state', 'True') %} + {{ 'on' }} + {% else %} + {{ 'off' }} + {% endif %} + """ + + with assert_setup_component(1, 'fan'): + assert setup.setup_component(self.hass, 'fan', { + 'fan': { + 'platform': 'template', + 'fans': { + 'test_fan': { + 'value_template': value_template, + 'speed_template': + "{{ states('input_select.speed') }}", + 'oscillating_template': + "{{ states('input_select.osc') }}", + 'turn_on': { + 'service': 'script.fan_on' + }, + 'turn_off': { + 'service': 'script.fan_off' + } + } + } + } + }) + + self.hass.start() + self.hass.block_till_done() + + self._verify(STATE_OFF, None, None) + + self.hass.states.set(_STATE_INPUT_BOOLEAN, True) + self.hass.states.set(_SPEED_INPUT_SELECT, SPEED_MEDIUM) + self.hass.states.set(_OSC_INPUT, 'True') + self.hass.block_till_done() + + self._verify(STATE_ON, SPEED_MEDIUM, True) + + def test_templates_with_valid_values(self): + """Test templates with valid values.""" + with assert_setup_component(1, 'fan'): + assert setup.setup_component(self.hass, 'fan', { + 'fan': { + 'platform': 'template', + 'fans': { + 'test_fan': { + 'value_template': + "{{ 'on' }}", + 'speed_template': + "{{ 'medium' }}", + 'oscillating_template': + "{{ 1 == 1 }}", + + 'turn_on': { + 'service': 'script.fan_on' + }, + 'turn_off': { + 'service': 'script.fan_off' + } + } + } + } + }) + + self.hass.start() + self.hass.block_till_done() + + self._verify(STATE_ON, SPEED_MEDIUM, True) + + def test_templates_invalid_values(self): + """Test templates with invalid values.""" + with assert_setup_component(1, 'fan'): + assert setup.setup_component(self.hass, 'fan', { + 'fan': { + 'platform': 'template', + 'fans': { + 'test_fan': { + 'value_template': + "{{ 'abc' }}", + 'speed_template': + "{{ '0' }}", + 'oscillating_template': + "{{ 'xyz' }}", + + 'turn_on': { + 'service': 'script.fan_on' + }, + 'turn_off': { + 'service': 'script.fan_off' + } + } + } + } + }) + + self.hass.start() + self.hass.block_till_done() + + self._verify(STATE_OFF, None, None) + + # End of template tests # + + # Function tests # + def test_on_off(self): + """Test turn on and turn off.""" + self._register_components() + + # Turn on fan + components.fan.turn_on(self.hass, _TEST_FAN) + self.hass.block_till_done() + + # verify + assert self.hass.states.get(_STATE_INPUT_BOOLEAN).state == STATE_ON + self._verify(STATE_ON, None, None) + + # Turn off fan + components.fan.turn_off(self.hass, _TEST_FAN) + self.hass.block_till_done() + + # verify + assert self.hass.states.get(_STATE_INPUT_BOOLEAN).state == STATE_OFF + self._verify(STATE_OFF, None, None) + + def test_on_with_speed(self): + """Test turn on with speed.""" + self._register_components() + + # Turn on fan with high speed + components.fan.turn_on(self.hass, _TEST_FAN, SPEED_HIGH) + self.hass.block_till_done() + + # verify + assert self.hass.states.get(_STATE_INPUT_BOOLEAN).state == STATE_ON + assert self.hass.states.get(_SPEED_INPUT_SELECT).state == SPEED_HIGH + self._verify(STATE_ON, SPEED_HIGH, None) + + def test_set_speed(self): + """Test set valid speed.""" + self._register_components() + + # Turn on fan + components.fan.turn_on(self.hass, _TEST_FAN) + self.hass.block_till_done() + + # Set fan's speed to high + components.fan.set_speed(self.hass, _TEST_FAN, SPEED_HIGH) + self.hass.block_till_done() + + # verify + assert self.hass.states.get(_SPEED_INPUT_SELECT).state == SPEED_HIGH + self._verify(STATE_ON, SPEED_HIGH, None) + + # Set fan's speed to medium + components.fan.set_speed(self.hass, _TEST_FAN, SPEED_MEDIUM) + self.hass.block_till_done() + + # verify + assert self.hass.states.get(_SPEED_INPUT_SELECT).state == SPEED_MEDIUM + self._verify(STATE_ON, SPEED_MEDIUM, None) + + def test_set_invalid_speed_from_initial_stage(self): + """Test set invalid speed when fan is in initial state.""" + self._register_components() + + # Turn on fan + components.fan.turn_on(self.hass, _TEST_FAN) + self.hass.block_till_done() + + # Set fan's speed to 'invalid' + components.fan.set_speed(self.hass, _TEST_FAN, 'invalid') + self.hass.block_till_done() + + # verify speed is unchanged + assert self.hass.states.get(_SPEED_INPUT_SELECT).state == '' + self._verify(STATE_ON, None, None) + + def test_set_invalid_speed(self): + """Test set invalid speed when fan has valid speed.""" + self._register_components() + + # Turn on fan + components.fan.turn_on(self.hass, _TEST_FAN) + self.hass.block_till_done() + + # Set fan's speed to high + components.fan.set_speed(self.hass, _TEST_FAN, SPEED_HIGH) + self.hass.block_till_done() + + # verify + assert self.hass.states.get(_SPEED_INPUT_SELECT).state == SPEED_HIGH + self._verify(STATE_ON, SPEED_HIGH, None) + + # Set fan's speed to 'invalid' + components.fan.set_speed(self.hass, _TEST_FAN, 'invalid') + self.hass.block_till_done() + + # verify speed is unchanged + assert self.hass.states.get(_SPEED_INPUT_SELECT).state == SPEED_HIGH + self._verify(STATE_ON, SPEED_HIGH, None) + + def test_custom_speed_list(self): + """Test set custom speed list.""" + self._register_components(['1', '2', '3']) + + # Turn on fan + components.fan.turn_on(self.hass, _TEST_FAN) + self.hass.block_till_done() + + # Set fan's speed to '1' + components.fan.set_speed(self.hass, _TEST_FAN, '1') + self.hass.block_till_done() + + # verify + assert self.hass.states.get(_SPEED_INPUT_SELECT).state == '1' + self._verify(STATE_ON, '1', None) + + # Set fan's speed to 'medium' which is invalid + components.fan.set_speed(self.hass, _TEST_FAN, SPEED_MEDIUM) + self.hass.block_till_done() + + # verify that speed is unchanged + assert self.hass.states.get(_SPEED_INPUT_SELECT).state == '1' + self._verify(STATE_ON, '1', None) + + def test_set_osc(self): + """Test set oscillating.""" + self._register_components() + + # Turn on fan + components.fan.turn_on(self.hass, _TEST_FAN) + self.hass.block_till_done() + + # Set fan's osc to True + components.fan.oscillate(self.hass, _TEST_FAN, True) + self.hass.block_till_done() + + # verify + assert self.hass.states.get(_OSC_INPUT).state == 'True' + self._verify(STATE_ON, None, True) + + # Set fan's osc to False + components.fan.oscillate(self.hass, _TEST_FAN, False) + self.hass.block_till_done() + + # verify + assert self.hass.states.get(_OSC_INPUT).state == 'False' + self._verify(STATE_ON, None, False) + + def test_set_invalid_osc_from_initial_state(self): + """Test set invalid oscillating when fan is in initial state.""" + self._register_components() + + # Turn on fan + components.fan.turn_on(self.hass, _TEST_FAN) + self.hass.block_till_done() + + # Set fan's osc to 'invalid' + components.fan.oscillate(self.hass, _TEST_FAN, 'invalid') + self.hass.block_till_done() + + # verify + assert self.hass.states.get(_OSC_INPUT).state == '' + self._verify(STATE_ON, None, None) + + def test_set_invalid_osc(self): + """Test set invalid oscillating when fan has valid osc.""" + self._register_components() + + # Turn on fan + components.fan.turn_on(self.hass, _TEST_FAN) + self.hass.block_till_done() + + # Set fan's osc to True + components.fan.oscillate(self.hass, _TEST_FAN, True) + self.hass.block_till_done() + + # verify + assert self.hass.states.get(_OSC_INPUT).state == 'True' + self._verify(STATE_ON, None, True) + + # Set fan's osc to False + components.fan.oscillate(self.hass, _TEST_FAN, None) + self.hass.block_till_done() + + # verify osc is unchanged + assert self.hass.states.get(_OSC_INPUT).state == 'True' + self._verify(STATE_ON, None, True) + + def _verify(self, expected_state, expected_speed, expected_oscillating): + """Verify fan's state, speed and osc.""" + state = self.hass.states.get(_TEST_FAN) + attributes = state.attributes + assert state.state == expected_state + assert attributes.get(ATTR_SPEED, None) == expected_speed + assert attributes.get(ATTR_OSCILLATING, None) == expected_oscillating + + def _register_components(self, speed_list=None): + """Register basic components for testing.""" + with assert_setup_component(1, 'input_boolean'): + assert setup.setup_component( + self.hass, + 'input_boolean', + {'input_boolean': {'state': None}} + ) + + with assert_setup_component(2, 'input_select'): + assert setup.setup_component(self.hass, 'input_select', { + 'input_select': { + 'speed': { + 'name': 'Speed', + 'options': ['', SPEED_LOW, SPEED_MEDIUM, SPEED_HIGH, + '1', '2', '3'] + }, + + 'osc': { + 'name': 'oscillating', + 'options': ['', 'True', 'False'] + }, + } + }) + + with assert_setup_component(1, 'fan'): + value_template = """ + {% if is_state('input_boolean.state', 'on') %} + {{ 'on' }} + {% else %} + {{ 'off' }} + {% endif %} + """ + + test_fan_config = { + 'value_template': value_template, + 'speed_template': + "{{ states('input_select.speed') }}", + 'oscillating_template': + "{{ states('input_select.osc') }}", + + 'turn_on': { + 'service': 'input_boolean.turn_on', + 'entity_id': _STATE_INPUT_BOOLEAN + }, + 'turn_off': { + 'service': 'input_boolean.turn_off', + 'entity_id': _STATE_INPUT_BOOLEAN + }, + 'set_speed': { + 'service': 'input_select.select_option', + + 'data_template': { + 'entity_id': _SPEED_INPUT_SELECT, + 'option': '{{ speed }}' + } + }, + 'set_oscillating': { + 'service': 'input_select.select_option', + + 'data_template': { + 'entity_id': _OSC_INPUT, + 'option': '{{ oscillating }}' + } + } + } + + if speed_list: + test_fan_config['speeds'] = speed_list + + assert setup.setup_component(self.hass, 'fan', { + 'fan': { + 'platform': 'template', + 'fans': { + 'test_fan': test_fan_config + } + } + }) + + self.hass.start() + self.hass.block_till_done() From c9de2f015b1e438be7a943b29d567987eac7149f Mon Sep 17 00:00:00 2001 From: roiff Date: Fri, 4 May 2018 00:22:43 +0800 Subject: [PATCH 107/155] HomeKit - Climate: power state on/off support (#14082) * add power state support on off * Added check for current operation mode * Extended 'set_heat_cool' * Added tests --- .../components/homekit/type_thermostats.py | 24 ++++++-- .../homekit/test_type_thermostats.py | 61 ++++++++++++++++++- 2 files changed, 79 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/homekit/type_thermostats.py b/homeassistant/components/homekit/type_thermostats.py index ce10b96c51c..4faceefe850 100644 --- a/homeassistant/components/homekit/type_thermostats.py +++ b/homeassistant/components/homekit/type_thermostats.py @@ -5,10 +5,10 @@ from homeassistant.components.climate import ( ATTR_CURRENT_TEMPERATURE, ATTR_TEMPERATURE, ATTR_TARGET_TEMP_HIGH, ATTR_TARGET_TEMP_LOW, ATTR_OPERATION_MODE, ATTR_OPERATION_LIST, - STATE_HEAT, STATE_COOL, STATE_AUTO, + STATE_HEAT, STATE_COOL, STATE_AUTO, SUPPORT_ON_OFF, SUPPORT_TARGET_TEMPERATURE_HIGH, SUPPORT_TARGET_TEMPERATURE_LOW) from homeassistant.const import ( - ATTR_SUPPORTED_FEATURES, ATTR_UNIT_OF_MEASUREMENT, + ATTR_ENTITY_ID, ATTR_SUPPORTED_FEATURES, ATTR_UNIT_OF_MEASUREMENT, STATE_OFF, TEMP_CELSIUS, TEMP_FAHRENHEIT) from . import TYPES @@ -41,6 +41,7 @@ class Thermostat(HomeAccessory): """Initialize a Thermostat accessory object.""" super().__init__(*args, category=CATEGORY_THERMOSTAT) self._unit = TEMP_CELSIUS + self.support_power_state = False self.heat_cool_flag_target_state = False self.temperature_flag_target_state = False self.coolingthresh_flag_target_state = False @@ -50,6 +51,8 @@ class Thermostat(HomeAccessory): self.chars = [] features = self.hass.states.get(self.entity_id) \ .attributes.get(ATTR_SUPPORTED_FEATURES) + if features & SUPPORT_ON_OFF: + self.support_power_state = True if features & SUPPORT_TEMP_RANGE: self.chars.extend((CHAR_COOLING_THRESHOLD_TEMPERATURE, CHAR_HEATING_THRESHOLD_TEMPERATURE)) @@ -93,6 +96,13 @@ class Thermostat(HomeAccessory): _LOGGER.debug('%s: Set heat-cool to %d', self.entity_id, value) self.heat_cool_flag_target_state = True hass_value = HC_HOMEKIT_TO_HASS[value] + if self.support_power_state is True: + params = {ATTR_ENTITY_ID: self.entity_id} + if hass_value == STATE_OFF: + self.hass.services.call('climate', 'turn_off', params) + return + else: + self.hass.services.call('climate', 'turn_on', params) self.hass.components.climate.set_operation_mode( operation_mode=hass_value, entity_id=self.entity_id) @@ -178,15 +188,19 @@ class Thermostat(HomeAccessory): # Update target operation mode operation_mode = new_state.attributes.get(ATTR_OPERATION_MODE) - if operation_mode \ - and operation_mode in HC_HASS_TO_HOMEKIT: + if self.support_power_state is True and new_state.state == STATE_OFF: + self.char_target_heat_cool.set_value( + HC_HASS_TO_HOMEKIT[STATE_OFF]) + elif operation_mode and operation_mode in HC_HASS_TO_HOMEKIT: if not self.heat_cool_flag_target_state: self.char_target_heat_cool.set_value( HC_HASS_TO_HOMEKIT[operation_mode]) self.heat_cool_flag_target_state = False # Set current operation mode based on temperatures and target mode - if operation_mode == STATE_HEAT: + if self.support_power_state is True and new_state.state == STATE_OFF: + current_operation_mode = STATE_OFF + elif operation_mode == STATE_HEAT: if isinstance(target_temp, float) and current_temp < target_temp: current_operation_mode = STATE_HEAT else: diff --git a/tests/components/homekit/test_type_thermostats.py b/tests/components/homekit/test_type_thermostats.py index adc3fb018f8..fe2a7f6cd02 100644 --- a/tests/components/homekit/test_type_thermostats.py +++ b/tests/components/homekit/test_type_thermostats.py @@ -7,7 +7,7 @@ from homeassistant.components.climate import ( ATTR_TARGET_TEMP_LOW, ATTR_TARGET_TEMP_HIGH, ATTR_OPERATION_MODE, ATTR_OPERATION_LIST, STATE_COOL, STATE_HEAT, STATE_AUTO) from homeassistant.const import ( - ATTR_SERVICE, ATTR_SERVICE_DATA, ATTR_SUPPORTED_FEATURES, + ATTR_ENTITY_ID, ATTR_SERVICE, ATTR_SERVICE_DATA, ATTR_SUPPORTED_FEATURES, ATTR_UNIT_OF_MEASUREMENT, EVENT_CALL_SERVICE, STATE_OFF, TEMP_CELSIUS, TEMP_FAHRENHEIT) @@ -261,6 +261,65 @@ class TestHomekitThermostats(unittest.TestCase): 25.0) self.assertEqual(acc.char_cooling_thresh_temp.value, 25.0) + def test_power_state(self): + """Test if accessory and HA are updated accordingly.""" + climate = 'climate.test' + + # SUPPORT_ON_OFF = True + self.hass.states.set(climate, STATE_HEAT, + {ATTR_SUPPORTED_FEATURES: 4096, + ATTR_OPERATION_MODE: STATE_HEAT, + ATTR_TEMPERATURE: 23.0, + ATTR_CURRENT_TEMPERATURE: 18.0}) + self.hass.block_till_done() + acc = self.thermostat_cls(self.hass, 'Climate', climate, + 2, config=None) + acc.run() + self.assertTrue(acc.support_power_state) + + self.assertEqual(acc.char_current_heat_cool.value, 1) + self.assertEqual(acc.char_target_heat_cool.value, 1) + + self.hass.states.set(climate, STATE_OFF, + {ATTR_OPERATION_MODE: STATE_HEAT, + ATTR_TEMPERATURE: 23.0, + ATTR_CURRENT_TEMPERATURE: 18.0}) + self.hass.block_till_done() + self.assertEqual(acc.char_current_heat_cool.value, 0) + self.assertEqual(acc.char_target_heat_cool.value, 0) + + self.hass.states.set(climate, STATE_OFF, + {ATTR_OPERATION_MODE: STATE_OFF, + ATTR_TEMPERATURE: 23.0, + ATTR_CURRENT_TEMPERATURE: 18.0}) + self.hass.block_till_done() + self.assertEqual(acc.char_current_heat_cool.value, 0) + self.assertEqual(acc.char_target_heat_cool.value, 0) + + # Set from HomeKit + acc.char_target_heat_cool.client_update_value(1) + self.hass.block_till_done() + self.assertEqual( + self.events[0].data[ATTR_SERVICE], 'turn_on') + self.assertEqual( + self.events[0].data[ATTR_SERVICE_DATA][ATTR_ENTITY_ID], + climate) + self.assertEqual( + self.events[1].data[ATTR_SERVICE], 'set_operation_mode') + self.assertEqual( + self.events[1].data[ATTR_SERVICE_DATA][ATTR_OPERATION_MODE], + STATE_HEAT) + self.assertEqual(acc.char_target_heat_cool.value, 1) + + acc.char_target_heat_cool.client_update_value(0) + self.hass.block_till_done() + self.assertEqual( + self.events[2].data[ATTR_SERVICE], 'turn_off') + self.assertEqual( + self.events[2].data[ATTR_SERVICE_DATA][ATTR_ENTITY_ID], + climate) + self.assertEqual(acc.char_target_heat_cool.value, 0) + def test_thermostat_fahrenheit(self): """Test if accessory and HA are updated accordingly.""" climate = 'climate.test' From e68b52d50d6bb3a1696d168f7df45b8ab639c22f Mon Sep 17 00:00:00 2001 From: cdce8p <30130371+cdce8p@users.noreply.github.com> Date: Thu, 3 May 2018 19:51:36 +0200 Subject: [PATCH 108/155] Demo Sensor - Added device_class support (#14269) --- homeassistant/components/sensor/demo.py | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/sensor/demo.py b/homeassistant/components/sensor/demo.py index ba7c93203df..5cae1a47c23 100644 --- a/homeassistant/components/sensor/demo.py +++ b/homeassistant/components/sensor/demo.py @@ -12,18 +12,21 @@ from homeassistant.helpers.entity import Entity def setup_platform(hass, config, add_devices, discovery_info=None): """Set up the Demo sensors.""" add_devices([ - DemoSensor('Outside Temperature', 15.6, TEMP_CELSIUS, 12), - DemoSensor('Outside Humidity', 54, '%', None), + DemoSensor('Outside Temperature', 15.6, 'temperature', + TEMP_CELSIUS, 12), + DemoSensor('Outside Humidity', 54, 'humidity', '%', None), ]) class DemoSensor(Entity): """Representation of a Demo sensor.""" - def __init__(self, name, state, unit_of_measurement, battery): + def __init__(self, name, state, device_class, + unit_of_measurement, battery): """Initialize the sensor.""" self._name = name self._state = state + self._device_class = device_class self._unit_of_measurement = unit_of_measurement self._battery = battery @@ -32,6 +35,11 @@ class DemoSensor(Entity): """No polling needed for a demo sensor.""" return False + @property + def device_class(self): + """Return the device class of the sensor.""" + return self._device_class + @property def name(self): """Return the name of the sensor.""" From 4ecce2598ac81ce9496be1cc42d836575a22737c Mon Sep 17 00:00:00 2001 From: Erik Eriksson <8228319+molobrakos@users.noreply.github.com> Date: Thu, 3 May 2018 19:54:37 +0200 Subject: [PATCH 109/155] Re-enable eliqonline requirement (#14265) --- homeassistant/components/sensor/eliqonline.py | 3 +-- requirements_all.txt | 3 +++ 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/sensor/eliqonline.py b/homeassistant/components/sensor/eliqonline.py index 23c397053c5..6405c707536 100644 --- a/homeassistant/components/sensor/eliqonline.py +++ b/homeassistant/components/sensor/eliqonline.py @@ -14,8 +14,7 @@ from homeassistant.const import (CONF_ACCESS_TOKEN, CONF_NAME, STATE_UNKNOWN) from homeassistant.helpers.entity import Entity import homeassistant.helpers.config_validation as cv -# pylint: disable=import-error, no-member -REQUIREMENTS = [] # ['eliqonline==1.0.13'] - package disappeared +REQUIREMENTS = ['eliqonline==1.0.14'] _LOGGER = logging.getLogger(__name__) diff --git a/requirements_all.txt b/requirements_all.txt index 1283011d7ac..99917ef9e35 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -276,6 +276,9 @@ dsmr_parser==0.11 # homeassistant.components.sensor.dweet dweepy==0.3.0 +# homeassistant.components.sensor.eliqonline +eliqonline==1.0.14 + # homeassistant.components.enocean enocean==0.40 From 58257af28953eff376467f02789ef76884f88d0c Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 3 May 2018 16:02:59 -0400 Subject: [PATCH 110/155] Add fetching camera thumbnails over websocket (#14231) * Add fetching camera thumbnails over websocket * Lint --- homeassistant/components/camera/__init__.py | 95 +++++++++++++------ homeassistant/components/frontend/__init__.py | 1 + .../components/image_processing/__init__.py | 2 +- homeassistant/components/microsoft_face.py | 2 +- homeassistant/components/websocket_api.py | 7 ++ tests/components/camera/test_init.py | 58 +++++++---- .../image_processing/test_openalpr_cloud.py | 40 ++++---- tests/components/test_microsoft_face.py | 4 +- 8 files changed, 135 insertions(+), 74 deletions(-) diff --git a/homeassistant/components/camera/__init__.py b/homeassistant/components/camera/__init__.py index 1fa89bc2241..c1f92965198 100644 --- a/homeassistant/components/camera/__init__.py +++ b/homeassistant/components/camera/__init__.py @@ -6,6 +6,7 @@ For more details about this component, please refer to the documentation at https://home-assistant.io/components/camera/ """ import asyncio +import base64 import collections from contextlib import suppress from datetime import timedelta @@ -13,20 +14,20 @@ import logging import hashlib from random import SystemRandom -import aiohttp +import attr from aiohttp import web import async_timeout import voluptuous as vol from homeassistant.core import callback -from homeassistant.const import (ATTR_ENTITY_ID, ATTR_ENTITY_PICTURE) +from homeassistant.const import ATTR_ENTITY_ID from homeassistant.exceptions import HomeAssistantError from homeassistant.loader import bind_hass -from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.config_validation import PLATFORM_SCHEMA # noqa from homeassistant.components.http import HomeAssistantView, KEY_AUTHENTICATED +from homeassistant.components import websocket_api import homeassistant.helpers.config_validation as cv DOMAIN = 'camera' @@ -64,6 +65,20 @@ CAMERA_SERVICE_SNAPSHOT = CAMERA_SERVICE_SCHEMA.extend({ vol.Required(ATTR_FILENAME): cv.template }) +WS_TYPE_CAMERA_THUMBNAIL = 'camera_thumbnail' +SCHEMA_WS_CAMERA_THUMBNAIL = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend({ + 'type': WS_TYPE_CAMERA_THUMBNAIL, + 'entity_id': cv.entity_id +}) + + +@attr.s +class Image: + """Represent an image.""" + + content_type = attr.ib(type=str) + content = attr.ib(type=bytes) + @bind_hass def enable_motion_detection(hass, entity_id=None): @@ -92,43 +107,40 @@ def async_snapshot(hass, filename, entity_id=None): @bind_hass -@asyncio.coroutine -def async_get_image(hass, entity_id, timeout=10): +async def async_get_image(hass, entity_id, timeout=10): """Fetch an image from a camera entity.""" - websession = async_get_clientsession(hass) - state = hass.states.get(entity_id) + component = hass.data.get(DOMAIN) - if state is None: - raise HomeAssistantError( - "No entity '{0}' for grab an image".format(entity_id)) + if component is None: + raise HomeAssistantError('Camera component not setup') - url = "{0}{1}".format( - hass.config.api.base_url, - state.attributes.get(ATTR_ENTITY_PICTURE) - ) + camera = component.get_entity(entity_id) - try: + if camera is None: + raise HomeAssistantError('Camera not found') + + with suppress(asyncio.CancelledError, asyncio.TimeoutError): with async_timeout.timeout(timeout, loop=hass.loop): - response = yield from websession.get(url) + image = await camera.async_camera_image() - if response.status != 200: - raise HomeAssistantError("Error {0} on {1}".format( - response.status, url)) + if image: + return Image(camera.content_type, image) - image = yield from response.read() - return image - - except (asyncio.TimeoutError, aiohttp.ClientError): - raise HomeAssistantError("Can't connect to {0}".format(url)) + raise HomeAssistantError('Unable to get image') @asyncio.coroutine def async_setup(hass, config): """Set up the camera component.""" - component = EntityComponent(_LOGGER, DOMAIN, hass, SCAN_INTERVAL) + component = hass.data[DOMAIN] = \ + EntityComponent(_LOGGER, DOMAIN, hass, SCAN_INTERVAL) hass.http.register_view(CameraImageView(component)) hass.http.register_view(CameraMjpegStream(component)) + hass.components.websocket_api.async_register_command( + WS_TYPE_CAMERA_THUMBNAIL, websocket_camera_thumbnail, + SCHEMA_WS_CAMERA_THUMBNAIL + ) yield from component.async_setup(config) @@ -344,20 +356,20 @@ class Camera(Entity): @property def state_attributes(self): """Return the camera state attributes.""" - attr = { + attrs = { 'access_token': self.access_tokens[-1], } if self.model: - attr['model_name'] = self.model + attrs['model_name'] = self.model if self.brand: - attr['brand'] = self.brand + attrs['brand'] = self.brand if self.motion_detection_enabled: - attr['motion_detection'] = self.motion_detection_enabled + attrs['motion_detection'] = self.motion_detection_enabled - return attr + return attrs @callback def async_update_token(self): @@ -440,3 +452,26 @@ class CameraMjpegStream(CameraView): return except ValueError: return web.Response(status=400) + + +@callback +def websocket_camera_thumbnail(hass, connection, msg): + """Handle get camera thumbnail websocket command. + + Async friendly. + """ + async def send_camera_still(): + """Send a camera still.""" + try: + image = await async_get_image(hass, msg['entity_id']) + connection.send_message_outside(websocket_api.result_message( + msg['id'], { + 'content_type': image.content_type, + 'content': base64.b64encode(image.content).decode('utf-8') + } + )) + except HomeAssistantError: + connection.send_message_outside(websocket_api.error_message( + msg['id'], 'image_fetch_failed', 'Unable to fetch image')) + + hass.async_add_job(send_camera_still()) diff --git a/homeassistant/components/frontend/__init__.py b/homeassistant/components/frontend/__init__.py index 564ba286b96..58cea0e0c66 100644 --- a/homeassistant/components/frontend/__init__.py +++ b/homeassistant/components/frontend/__init__.py @@ -606,6 +606,7 @@ def _is_latest(js_option, request): return useragent and hass_frontend.version(useragent) +@callback def websocket_handle_get_panels(hass, connection, msg): """Handle get panels command. diff --git a/homeassistant/components/image_processing/__init__.py b/homeassistant/components/image_processing/__init__.py index de195ce0165..f0cb3a66d52 100644 --- a/homeassistant/components/image_processing/__init__.py +++ b/homeassistant/components/image_processing/__init__.py @@ -132,4 +132,4 @@ class ImageProcessingEntity(Entity): return # process image data - yield from self.async_process_image(image) + yield from self.async_process_image(image.content) diff --git a/homeassistant/components/microsoft_face.py b/homeassistant/components/microsoft_face.py index e99d8d4a5f6..7c167f93142 100644 --- a/homeassistant/components/microsoft_face.py +++ b/homeassistant/components/microsoft_face.py @@ -239,7 +239,7 @@ def async_setup(hass, config): 'post', "persongroups/{0}/persons/{1}/persistedFaces".format( g_id, p_id), - image, + image.content, binary=True ) except HomeAssistantError as err: diff --git a/homeassistant/components/websocket_api.py b/homeassistant/components/websocket_api.py index 84c92631572..4989f4f0db2 100644 --- a/homeassistant/components/websocket_api.py +++ b/homeassistant/components/websocket_api.py @@ -429,6 +429,7 @@ class ActiveConnection: return wsock +@callback def handle_subscribe_events(hass, connection, msg): """Handle subscribe events command. @@ -447,6 +448,7 @@ def handle_subscribe_events(hass, connection, msg): connection.to_write.put_nowait(result_message(msg['id'])) +@callback def handle_unsubscribe_events(hass, connection, msg): """Handle unsubscribe events command. @@ -462,6 +464,7 @@ def handle_unsubscribe_events(hass, connection, msg): msg['id'], ERR_NOT_FOUND, 'Subscription not found.')) +@callback def handle_call_service(hass, connection, msg): """Handle call service command. @@ -476,6 +479,7 @@ def handle_call_service(hass, connection, msg): hass.async_add_job(call_service_helper(msg)) +@callback def handle_get_states(hass, connection, msg): """Handle get states command. @@ -485,6 +489,7 @@ def handle_get_states(hass, connection, msg): msg['id'], hass.states.async_all())) +@callback def handle_get_services(hass, connection, msg): """Handle get services command. @@ -499,6 +504,7 @@ def handle_get_services(hass, connection, msg): hass.async_add_job(get_services_helper(msg)) +@callback def handle_get_config(hass, connection, msg): """Handle get config command. @@ -508,6 +514,7 @@ def handle_get_config(hass, connection, msg): msg['id'], hass.config.as_dict())) +@callback def handle_ping(hass, connection, msg): """Handle ping command. diff --git a/tests/components/camera/test_init.py b/tests/components/camera/test_init.py index 465d6276ad5..d0f1425a595 100644 --- a/tests/components/camera/test_init.py +++ b/tests/components/camera/test_init.py @@ -1,18 +1,19 @@ """The tests for the camera component.""" import asyncio +import base64 from unittest.mock import patch, mock_open import pytest from homeassistant.setup import setup_component, async_setup_component from homeassistant.const import ATTR_ENTITY_PICTURE -import homeassistant.components.camera as camera -import homeassistant.components.http as http +from homeassistant.components import camera, http, websocket_api from homeassistant.exceptions import HomeAssistantError from homeassistant.util.async_ import run_coroutine_threadsafe from tests.common import ( - get_test_home_assistant, get_test_instance_port, assert_setup_component) + get_test_home_assistant, get_test_instance_port, assert_setup_component, + mock_coro) @pytest.fixture @@ -90,36 +91,32 @@ class TestGetImage(object): self.hass, 'camera.demo_camera'), self.hass.loop).result() assert mock_camera.called - assert image == b'Test' + assert image.content == b'Test' def test_get_image_without_exists_camera(self): """Try to get image without exists camera.""" - self.hass.states.remove('camera.demo_camera') - - with pytest.raises(HomeAssistantError): + with patch('homeassistant.helpers.entity_component.EntityComponent.' + 'get_entity', return_value=None), \ + pytest.raises(HomeAssistantError): run_coroutine_threadsafe(camera.async_get_image( self.hass, 'camera.demo_camera'), self.hass.loop).result() - def test_get_image_with_timeout(self, aioclient_mock): + def test_get_image_with_timeout(self): """Try to get image with timeout.""" - aioclient_mock.get(self.url, exc=asyncio.TimeoutError()) - - with pytest.raises(HomeAssistantError): + with patch('homeassistant.components.camera.Camera.async_camera_image', + side_effect=asyncio.TimeoutError), \ + pytest.raises(HomeAssistantError): run_coroutine_threadsafe(camera.async_get_image( self.hass, 'camera.demo_camera'), self.hass.loop).result() - assert len(aioclient_mock.mock_calls) == 1 - - def test_get_image_with_bad_http_state(self, aioclient_mock): - """Try to get image with bad http status.""" - aioclient_mock.get(self.url, status=400) - - with pytest.raises(HomeAssistantError): + def test_get_image_fails(self): + """Try to get image with timeout.""" + with patch('homeassistant.components.camera.Camera.async_camera_image', + return_value=mock_coro(None)), \ + pytest.raises(HomeAssistantError): run_coroutine_threadsafe(camera.async_get_image( self.hass, 'camera.demo_camera'), self.hass.loop).result() - assert len(aioclient_mock.mock_calls) == 1 - @asyncio.coroutine def test_snapshot_service(hass, mock_camera): @@ -136,3 +133,24 @@ def test_snapshot_service(hass, mock_camera): assert len(mock_write.mock_calls) == 1 assert mock_write.mock_calls[0][1][0] == b'Test' + + +async def test_webocket_camera_thumbnail(hass, hass_ws_client, mock_camera): + """Test camera_thumbnail websocket command.""" + await async_setup_component(hass, 'camera') + + client = await hass_ws_client(hass) + await client.send_json({ + 'id': 5, + 'type': 'camera_thumbnail', + 'entity_id': 'camera.demo_camera', + }) + + msg = await client.receive_json() + + assert msg['id'] == 5 + assert msg['type'] == websocket_api.TYPE_RESULT + assert msg['success'] + assert msg['result']['content_type'] == 'image/jpeg' + assert msg['result']['content'] == \ + base64.b64encode(b'Test').decode('utf-8') diff --git a/tests/components/image_processing/test_openalpr_cloud.py b/tests/components/image_processing/test_openalpr_cloud.py index e840bce54f7..50060e08a4b 100644 --- a/tests/components/image_processing/test_openalpr_cloud.py +++ b/tests/components/image_processing/test_openalpr_cloud.py @@ -3,14 +3,13 @@ import asyncio from unittest.mock import patch, PropertyMock from homeassistant.core import callback -from homeassistant.const import ATTR_ENTITY_PICTURE from homeassistant.setup import setup_component -import homeassistant.components.image_processing as ip +from homeassistant.components import camera, image_processing as ip from homeassistant.components.image_processing.openalpr_cloud import ( OPENALPR_API_URL) from tests.common import ( - get_test_home_assistant, assert_setup_component, load_fixture) + get_test_home_assistant, assert_setup_component, load_fixture, mock_coro) class TestOpenAlprCloudSetup(object): @@ -131,11 +130,6 @@ class TestOpenAlprCloud(object): new_callable=PropertyMock(return_value=False)): setup_component(self.hass, ip.DOMAIN, config) - state = self.hass.states.get('camera.demo_camera') - self.url = "{0}{1}".format( - self.hass.config.api.base_url, - state.attributes.get(ATTR_ENTITY_PICTURE)) - self.alpr_events = [] @callback @@ -158,18 +152,20 @@ class TestOpenAlprCloud(object): def test_openalpr_process_image(self, aioclient_mock): """Setup and scan a picture and test plates from event.""" - aioclient_mock.get(self.url, content=b'image') aioclient_mock.post( OPENALPR_API_URL, params=self.params, text=load_fixture('alpr_cloud.json'), status=200 ) - ip.scan(self.hass, entity_id='image_processing.test_local') - self.hass.block_till_done() + with patch('homeassistant.components.camera.async_get_image', + return_value=mock_coro( + camera.Image('image/jpeg', b'image'))): + ip.scan(self.hass, entity_id='image_processing.test_local') + self.hass.block_till_done() state = self.hass.states.get('image_processing.test_local') - assert len(aioclient_mock.mock_calls) == 2 + assert len(aioclient_mock.mock_calls) == 1 assert len(self.alpr_events) == 5 assert state.attributes.get('vehicles') == 1 assert state.state == 'H786P0J' @@ -184,28 +180,32 @@ class TestOpenAlprCloud(object): def test_openalpr_process_image_api_error(self, aioclient_mock): """Setup and scan a picture and test api error.""" - aioclient_mock.get(self.url, content=b'image') aioclient_mock.post( OPENALPR_API_URL, params=self.params, text="{'error': 'error message'}", status=400 ) - ip.scan(self.hass, entity_id='image_processing.test_local') - self.hass.block_till_done() + with patch('homeassistant.components.camera.async_get_image', + return_value=mock_coro( + camera.Image('image/jpeg', b'image'))): + ip.scan(self.hass, entity_id='image_processing.test_local') + self.hass.block_till_done() - assert len(aioclient_mock.mock_calls) == 2 + assert len(aioclient_mock.mock_calls) == 1 assert len(self.alpr_events) == 0 def test_openalpr_process_image_api_timeout(self, aioclient_mock): """Setup and scan a picture and test api error.""" - aioclient_mock.get(self.url, content=b'image') aioclient_mock.post( OPENALPR_API_URL, params=self.params, exc=asyncio.TimeoutError() ) - ip.scan(self.hass, entity_id='image_processing.test_local') - self.hass.block_till_done() + with patch('homeassistant.components.camera.async_get_image', + return_value=mock_coro( + camera.Image('image/jpeg', b'image'))): + ip.scan(self.hass, entity_id='image_processing.test_local') + self.hass.block_till_done() - assert len(aioclient_mock.mock_calls) == 2 + assert len(aioclient_mock.mock_calls) == 1 assert len(self.alpr_events) == 0 diff --git a/tests/components/test_microsoft_face.py b/tests/components/test_microsoft_face.py index 7a047a73f47..370059a0a09 100644 --- a/tests/components/test_microsoft_face.py +++ b/tests/components/test_microsoft_face.py @@ -2,7 +2,7 @@ import asyncio from unittest.mock import patch -import homeassistant.components.microsoft_face as mf +from homeassistant.components import camera, microsoft_face as mf from homeassistant.setup import setup_component from tests.common import ( @@ -190,7 +190,7 @@ class TestMicrosoftFaceSetup(object): assert len(aioclient_mock.mock_calls) == 1 @patch('homeassistant.components.camera.async_get_image', - return_value=mock_coro(b'Test')) + return_value=mock_coro(camera.Image('image/jpeg', b'Test'))) def test_service_face(self, camera_mock, aioclient_mock): """Setup component, test person face services.""" aioclient_mock.get( From 15e75b07d83c5779fbbdfa0ba92ba26da3f9823f Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 3 May 2018 16:03:26 -0400 Subject: [PATCH 111/155] Allow fetching media player covers via websocket connection (#14233) Lint --- .../components/media_player/__init__.py | 49 ++++++++++++++++++- tests/components/media_player/test_init.py | 37 ++++++++++++++ 2 files changed, 85 insertions(+), 1 deletion(-) create mode 100644 tests/components/media_player/test_init.py diff --git a/homeassistant/components/media_player/__init__.py b/homeassistant/components/media_player/__init__.py index 20fd3b875c8..20a1a473ba8 100644 --- a/homeassistant/components/media_player/__init__.py +++ b/homeassistant/components/media_player/__init__.py @@ -5,6 +5,7 @@ For more details about this component, please refer to the documentation at https://home-assistant.io/components/media_player/ """ import asyncio +import base64 from datetime import timedelta import functools as ft import collections @@ -17,6 +18,7 @@ from aiohttp.hdrs import CONTENT_TYPE, CACHE_CONTROL import async_timeout import voluptuous as vol +from homeassistant.core import callback from homeassistant.components.http import KEY_AUTHENTICATED, HomeAssistantView from homeassistant.const import ( STATE_OFF, STATE_IDLE, STATE_PLAYING, STATE_UNKNOWN, ATTR_ENTITY_ID, @@ -31,6 +33,7 @@ from homeassistant.helpers.config_validation import PLATFORM_SCHEMA # noqa from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity_component import EntityComponent from homeassistant.loader import bind_hass +from homeassistant.components import websocket_api _LOGGER = logging.getLogger(__name__) _RND = SystemRandom() @@ -361,11 +364,22 @@ def set_shuffle(hass, shuffle, entity_id=None): hass.services.call(DOMAIN, SERVICE_SHUFFLE_SET, data) +WS_TYPE_MEDIA_PLAYER_THUMBNAIL = 'media_player_thumbnail' +SCHEMA_WEBSOCKET_GET_THUMBNAIL = \ + websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend({ + 'type': WS_TYPE_MEDIA_PLAYER_THUMBNAIL, + 'entity_id': cv.entity_id + }) + + async def async_setup(hass, config): """Track states and offer events for media_players.""" - component = EntityComponent( + component = hass.data[DOMAIN] = EntityComponent( logging.getLogger(__name__), DOMAIN, hass, SCAN_INTERVAL) + hass.components.websocket_api.async_register_command( + WS_TYPE_MEDIA_PLAYER_THUMBNAIL, websocket_handle_thumbnail, + SCHEMA_WEBSOCKET_GET_THUMBNAIL) hass.http.register_view(MediaPlayerImageView(component)) await component.async_setup(config) @@ -942,3 +956,36 @@ class MediaPlayerImageView(HomeAssistantView): headers = {CACHE_CONTROL: 'max-age=3600'} return web.Response( body=data, content_type=content_type, headers=headers) + + +@callback +def websocket_handle_thumbnail(hass, connection, msg): + """Handle get media player cover command. + + Async friendly. + """ + component = hass.data[DOMAIN] + player = component.get_entity(msg['entity_id']) + + if player is None: + connection.send_message_outside(websocket_api.error_message( + msg['id'], 'entity_not_found', 'Entity not found')) + return + + async def send_image(): + """Send image.""" + data, content_type = await player.async_get_media_image() + + if data is None: + connection.send_message_outside(websocket_api.error_message( + msg['id'], 'thumbnail_fetch_failed', + 'Failed to fetch thumbnail')) + return + + connection.send_message_outside(websocket_api.result_message( + msg['id'], { + 'content_type': content_type, + 'content': base64.b64encode(data).decode('utf-8') + })) + + hass.async_add_job(send_image()) diff --git a/tests/components/media_player/test_init.py b/tests/components/media_player/test_init.py new file mode 100644 index 00000000000..5d632d4de0b --- /dev/null +++ b/tests/components/media_player/test_init.py @@ -0,0 +1,37 @@ +"""Test the base functions of the media player.""" +import base64 +from unittest.mock import patch + +from homeassistant.setup import async_setup_component +from homeassistant.components import websocket_api + +from tests.common import mock_coro + + +async def test_get_panels(hass, hass_ws_client): + """Test get_panels command.""" + await async_setup_component(hass, 'media_player', { + 'media_player': { + 'platform': 'demo' + } + }) + + client = await hass_ws_client(hass) + + with patch('homeassistant.components.media_player.MediaPlayerDevice.' + 'async_get_media_image', return_value=mock_coro( + (b'image', 'image/jpeg'))): + await client.send_json({ + 'id': 5, + 'type': 'media_player_thumbnail', + 'entity_id': 'media_player.bedroom', + }) + + msg = await client.receive_json() + + assert msg['id'] == 5 + assert msg['type'] == websocket_api.TYPE_RESULT + assert msg['success'] + assert msg['result']['content_type'] == 'image/jpeg' + assert msg['result']['content'] == \ + base64.b64encode(b'image').decode('utf-8') From 8cabec7ac114ef8a0a6e20862ab41de05d4da1d7 Mon Sep 17 00:00:00 2001 From: cdce8p <30130371+cdce8p@users.noreply.github.com> Date: Thu, 3 May 2018 23:28:03 +0200 Subject: [PATCH 112/155] Fix ZWave light brightness (#14261) * Fix ZWave light brightness * The brightness should always be an integer * Changed to round --- homeassistant/components/light/zwave.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/light/zwave.py b/homeassistant/components/light/zwave.py index 286ce73f1ed..04216780c80 100644 --- a/homeassistant/components/light/zwave.py +++ b/homeassistant/components/light/zwave.py @@ -61,7 +61,7 @@ def get_device(node, values, node_config, **kwargs): def brightness_state(value): """Return the brightness and state.""" if value.data > 0: - return round((value.data / 99) * 255, 0), STATE_ON + return round((value.data / 99) * 255), STATE_ON return 0, STATE_OFF From 9859840b9ce3047752662e3c81098d6b83e3933c Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Fri, 4 May 2018 10:48:13 +0200 Subject: [PATCH 113/155] Update issue templates --- .github/ISSUE_TEMPLATE/Bug_report.md | 50 ++++++++++++++++++++++++++++ 1 file changed, 50 insertions(+) create mode 100644 .github/ISSUE_TEMPLATE/Bug_report.md diff --git a/.github/ISSUE_TEMPLATE/Bug_report.md b/.github/ISSUE_TEMPLATE/Bug_report.md new file mode 100644 index 00000000000..b2721db553f --- /dev/null +++ b/.github/ISSUE_TEMPLATE/Bug_report.md @@ -0,0 +1,50 @@ +--- +name: Bug report +about: Create a report to help us improve + +--- + + + +**Home Assistant release with the issue:** + + + +**Last working Home Assistant release (if known):** + + +**Operating environment (Hass.io/Docker/Windows/etc.):** + + +**Component/platform:** + + + +**Description of problem:** + + + +**Problem-relevant `configuration.yaml` entries and (fill out even if it seems unimportant):** +```yaml + +``` + +**Traceback (if applicable):** +``` + +``` + +**Additional information:** From a7a3cff0f1c5f4110dd4ea619c1a6a5ded4251d9 Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Fri, 4 May 2018 10:52:20 +0200 Subject: [PATCH 114/155] Update issue templates --- .github/ISSUE_TEMPLATE/Feature_request.md | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 .github/ISSUE_TEMPLATE/Feature_request.md diff --git a/.github/ISSUE_TEMPLATE/Feature_request.md b/.github/ISSUE_TEMPLATE/Feature_request.md new file mode 100644 index 00000000000..3489db2b397 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/Feature_request.md @@ -0,0 +1,7 @@ +--- +name: Feature request +about: Suggest an idea for this project + +--- + +Please use the forum for [feature requests](https://community.home-assistant.io/c/feature-requests). Thanks From 54ccbbcb64faecb84466c7533235c20b1e454224 Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Fri, 4 May 2018 10:54:55 +0200 Subject: [PATCH 115/155] Update issue templates --- .github/ISSUE_TEMPLATE/Feature_request.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/Feature_request.md b/.github/ISSUE_TEMPLATE/Feature_request.md index 3489db2b397..69ab7c33a85 100644 --- a/.github/ISSUE_TEMPLATE/Feature_request.md +++ b/.github/ISSUE_TEMPLATE/Feature_request.md @@ -1,7 +1,8 @@ --- name: Feature request -about: Suggest an idea for this project +about: Please use the forum for [feature requests](https://community.home-assistant.io/c/feature-requests). + Thanks --- -Please use the forum for [feature requests](https://community.home-assistant.io/c/feature-requests). Thanks + From 5f8f6666e6e7a72c05f0c450518b8196ed537285 Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Fri, 4 May 2018 10:55:55 +0200 Subject: [PATCH 116/155] Update issue templates --- .github/ISSUE_TEMPLATE/Feature_request.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/Feature_request.md b/.github/ISSUE_TEMPLATE/Feature_request.md index 69ab7c33a85..e8e12dfa167 100644 --- a/.github/ISSUE_TEMPLATE/Feature_request.md +++ b/.github/ISSUE_TEMPLATE/Feature_request.md @@ -1,7 +1,7 @@ --- name: Feature request -about: Please use the forum for [feature requests](https://community.home-assistant.io/c/feature-requests). - Thanks +about: Please use the forum (https://community.home-assistant.io/c/feature-requests) for + feature requests. Thanks --- From b49d98407c4c2bd3df132a03e16afa9faa1e2276 Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Fri, 4 May 2018 10:56:35 +0200 Subject: [PATCH 117/155] Remove feature request --- .github/ISSUE_TEMPLATE/Feature_request.md | 8 -------- 1 file changed, 8 deletions(-) delete mode 100644 .github/ISSUE_TEMPLATE/Feature_request.md diff --git a/.github/ISSUE_TEMPLATE/Feature_request.md b/.github/ISSUE_TEMPLATE/Feature_request.md deleted file mode 100644 index e8e12dfa167..00000000000 --- a/.github/ISSUE_TEMPLATE/Feature_request.md +++ /dev/null @@ -1,8 +0,0 @@ ---- -name: Feature request -about: Please use the forum (https://community.home-assistant.io/c/feature-requests) for - feature requests. Thanks - ---- - - From fa0ad7b3171d6abcf0b765558a1f1c824323fd94 Mon Sep 17 00:00:00 2001 From: Anders Melchiorsen Date: Fri, 4 May 2018 12:28:56 +0200 Subject: [PATCH 118/155] Color fixes for Wink lights (#14263) --- homeassistant/components/light/wink.py | 34 +++++++++----------------- 1 file changed, 12 insertions(+), 22 deletions(-) diff --git a/homeassistant/components/light/wink.py b/homeassistant/components/light/wink.py index fd957f8f11d..04e9c34b0f6 100644 --- a/homeassistant/components/light/wink.py +++ b/homeassistant/components/light/wink.py @@ -5,7 +5,6 @@ For more details about this platform, please refer to the documentation at https://home-assistant.io/components/light.wink/ """ import asyncio -import colorsys from homeassistant.components.light import ( ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, ATTR_HS_COLOR, SUPPORT_BRIGHTNESS, @@ -54,29 +53,19 @@ class WinkLight(WinkDevice, Light): return int(self.wink.brightness() * 255) return None - @property - def rgb_color(self): - """Define current bulb color in RGB.""" - if not self.wink.supports_hue_saturation(): - return None - else: - hue = self.wink.color_hue() - saturation = self.wink.color_saturation() - value = int(self.wink.brightness() * 255) - if hue is None or saturation is None or value is None: - return None - rgb = colorsys.hsv_to_rgb(hue, saturation, value) - r_value = int(round(rgb[0])) - g_value = int(round(rgb[1])) - b_value = int(round(rgb[2])) - return r_value, g_value, b_value - @property def hs_color(self): """Define current bulb color.""" - if not self.wink.supports_xy_color(): - return None - return color_util.color_xy_to_hs(*self.wink.color_xy()) + if self.wink.supports_xy_color(): + return color_util.color_xy_to_hs(*self.wink.color_xy()) + + if self.wink.supports_hue_saturation(): + hue = self.wink.color_hue() + saturation = self.wink.color_saturation() + if hue is not None and saturation is not None: + return hue*360, saturation*100 + + return None @property def color_temp(self): @@ -104,7 +93,8 @@ class WinkLight(WinkDevice, Light): xy_color = color_util.color_hs_to_xy(*hs_color) state_kwargs['color_xy'] = xy_color if self.wink.supports_hue_saturation(): - state_kwargs['color_hue_saturation'] = hs_color + hs_scaled = hs_color[0]/360, hs_color[1]/100 + state_kwargs['color_hue_saturation'] = hs_scaled if color_temp_mired: state_kwargs['color_kelvin'] = mired_to_kelvin(color_temp_mired) From c80b752d0e3db93b35cc8d203b1ed98e2b5a9c95 Mon Sep 17 00:00:00 2001 From: Boyi C Date: Fri, 4 May 2018 18:29:07 +0800 Subject: [PATCH 119/155] fix check config not working after #14211 (#14259) --- homeassistant/config.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/config.py b/homeassistant/config.py index d69704a7032..5c432490f6a 100644 --- a/homeassistant/config.py +++ b/homeassistant/config.py @@ -687,7 +687,7 @@ async def async_check_ha_config_file(hass): from homeassistant.scripts.check_config import check_ha_config_file res = await hass.async_add_job( - check_ha_config_file, hass.config.config_dir) + check_ha_config_file, hass) if not res.errors: return None From 36cf2125ce4767975d97cc30f8fe35b3e4ece9e7 Mon Sep 17 00:00:00 2001 From: Otto Winter Date: Fri, 4 May 2018 13:49:13 +0200 Subject: [PATCH 120/155] Issue Template Fix CRLF (#14283) --- .github/ISSUE_TEMPLATE/Bug_report.md | 100 +++++++++++++-------------- 1 file changed, 50 insertions(+), 50 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/Bug_report.md b/.github/ISSUE_TEMPLATE/Bug_report.md index b2721db553f..2c418c6f63e 100644 --- a/.github/ISSUE_TEMPLATE/Bug_report.md +++ b/.github/ISSUE_TEMPLATE/Bug_report.md @@ -1,50 +1,50 @@ ---- -name: Bug report -about: Create a report to help us improve - ---- - - - -**Home Assistant release with the issue:** - - - -**Last working Home Assistant release (if known):** - - -**Operating environment (Hass.io/Docker/Windows/etc.):** - - -**Component/platform:** - - - -**Description of problem:** - - - -**Problem-relevant `configuration.yaml` entries and (fill out even if it seems unimportant):** -```yaml - -``` - -**Traceback (if applicable):** -``` - -``` - -**Additional information:** +--- +name: Bug report +about: Create a report to help us improve + +--- + + + +**Home Assistant release with the issue:** + + + +**Last working Home Assistant release (if known):** + + +**Operating environment (Hass.io/Docker/Windows/etc.):** + + +**Component/platform:** + + + +**Description of problem:** + + + +**Problem-relevant `configuration.yaml` entries and (fill out even if it seems unimportant):** +```yaml + +``` + +**Traceback (if applicable):** +``` + +``` + +**Additional information:** From f98525acbfc8517d5b6aaac881a4e0573d5757a1 Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Fri, 4 May 2018 14:58:34 +0200 Subject: [PATCH 121/155] Upgrade attrs to 18.1.0 (#14281) --- 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 6de885942fb..0f3c9ac255d 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -9,7 +9,7 @@ aiohttp==3.1.3 async_timeout==2.0.1 astral==1.6 certifi>=2017.4.17 -attrs==17.4.0 +attrs==18.1.0 # Breaks Python 3.6 and is not needed for our supported Python versions enum34==1000000000.0.0 diff --git a/requirements_all.txt b/requirements_all.txt index 99917ef9e35..002c5eb93e2 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -10,7 +10,7 @@ aiohttp==3.1.3 async_timeout==2.0.1 astral==1.6 certifi>=2017.4.17 -attrs==17.4.0 +attrs==18.1.0 # homeassistant.components.nuimo_controller --only-binary=all https://github.com/getSenic/nuimo-linux-python/archive/29fc42987f74d8090d0e2382e8f248ff5990b8c9.zip#nuimo==1.0.0 diff --git a/setup.py b/setup.py index 8815b0227ad..3db7c737a2c 100755 --- a/setup.py +++ b/setup.py @@ -53,7 +53,7 @@ REQUIRES = [ 'async_timeout==2.0.1', 'astral==1.6', 'certifi>=2017.4.17', - 'attrs==17.4.0', + 'attrs==18.1.0', ] MIN_PY_VERSION = '.'.join(map(str, hass_const.REQUIRED_PYTHON_VER)) From e37fd5b132666f9d57e31623adc9ecf1e00ad069 Mon Sep 17 00:00:00 2001 From: cdce8p <30130371+cdce8p@users.noreply.github.com> Date: Fri, 4 May 2018 16:46:00 +0200 Subject: [PATCH 122/155] Update HAP-python to 2.0.0 (#14278) * Fixed async (added 'async_add_job' and 'add_job') * Driver status * Use pyhap category constants * Changed 'set_broker' to 'set_driver' * Changed loader method names * Use 'serv.configure_char' * Use 'self.set_info_service' * Use 'self.add_preload_service' * Fix hound issue * Updated HAP-python to 2.0.0 --- homeassistant/components/homekit/__init__.py | 33 +++++--- .../components/homekit/accessories.py | 65 +++------------ homeassistant/components/homekit/const.py | 12 --- .../components/homekit/type_covers.py | 48 +++++------ .../components/homekit/type_lights.py | 35 ++++---- .../components/homekit/type_locks.py | 20 +++-- .../homekit/type_security_systems.py | 20 +++-- .../components/homekit/type_sensors.py | 55 ++++++------ .../components/homekit/type_switches.py | 12 +-- .../components/homekit/type_thermostats.py | 46 +++++----- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/homekit/test_accessories.py | 83 +++++-------------- tests/components/homekit/test_homekit.py | 42 ++++++---- 14 files changed, 203 insertions(+), 272 deletions(-) diff --git a/homeassistant/components/homekit/__init__.py b/homeassistant/components/homekit/__init__.py index 4984cfee959..080dd2a7cbd 100644 --- a/homeassistant/components/homekit/__init__.py +++ b/homeassistant/components/homekit/__init__.py @@ -30,7 +30,13 @@ from .util import ( TYPES = Registry() _LOGGER = logging.getLogger(__name__) -REQUIREMENTS = ['HAP-python==1.1.9'] +REQUIREMENTS = ['HAP-python==2.0.0'] + +# #### Driver Status #### +STATUS_READY = 0 +STATUS_RUNNING = 1 +STATUS_STOPPED = 2 +STATUS_WAIT = 3 CONFIG_SCHEMA = vol.Schema({ @@ -57,7 +63,7 @@ async def async_setup(hass, config): entity_config = conf[CONF_ENTITY_CONFIG] homekit = HomeKit(hass, port, ip_address, entity_filter, entity_config) - homekit.setup() + await hass.async_add_job(homekit.setup) if auto_start: hass.bus.async_listen_once(EVENT_HOMEASSISTANT_START, homekit.start) @@ -65,8 +71,10 @@ async def async_setup(hass, config): def handle_homekit_service_start(service): """Handle start HomeKit service call.""" - if homekit.started: - _LOGGER.warning('HomeKit is already running') + if homekit.status != STATUS_READY: + _LOGGER.warning( + 'HomeKit is not ready. Either it is already running or has ' + 'been stopped.') return homekit.start() @@ -162,7 +170,7 @@ class HomeKit(): self._ip_address = ip_address self._filter = entity_filter self._config = entity_config - self.started = False + self.status = STATUS_READY self.bridge = None self.driver = None @@ -191,9 +199,9 @@ class HomeKit(): def start(self, *args): """Start the accessory driver.""" - if self.started: + if self.status != STATUS_READY: return - self.started = True + self.status = STATUS_WAIT # pylint: disable=unused-variable from . import ( # noqa F401 @@ -202,19 +210,20 @@ class HomeKit(): for state in self.hass.states.all(): self.add_bridge_accessory(state) - self.bridge.set_broker(self.driver) + self.bridge.set_driver(self.driver) if not self.bridge.paired: show_setup_message(self.hass, self.bridge) _LOGGER.debug('Driver start') - self.driver.start() + self.hass.add_job(self.driver.start) + self.status = STATUS_RUNNING def stop(self, *args): """Stop the accessory driver.""" - if not self.started: + if self.status != STATUS_RUNNING: return + self.status = STATUS_STOPPED _LOGGER.debug('Driver stop') - if self.driver and self.driver.run_sentinel: - self.driver.stop() + self.hass.add_job(self.driver.stop) diff --git a/homeassistant/components/homekit/accessories.py b/homeassistant/components/homekit/accessories.py index c7703b221d8..c47c3f8fbe7 100644 --- a/homeassistant/components/homekit/accessories.py +++ b/homeassistant/components/homekit/accessories.py @@ -4,8 +4,9 @@ from functools import wraps from inspect import getmodule import logging -from pyhap.accessory import Accessory, Bridge, Category +from pyhap.accessory import Accessory, Bridge from pyhap.accessory_driver import AccessoryDriver +from pyhap.const import CATEGORY_OTHER from homeassistant.const import __version__ from homeassistant.core import callback as ha_callback @@ -15,9 +16,8 @@ from homeassistant.helpers.event import ( from homeassistant.util import dt as dt_util from .const import ( - DEBOUNCE_TIMEOUT, BRIDGE_MODEL, BRIDGE_NAME, BRIDGE_SERIAL_NUMBER, - MANUFACTURER, SERV_ACCESSORY_INFO, CHAR_FIRMWARE_REVISION, - CHAR_MANUFACTURER, CHAR_MODEL, CHAR_NAME, CHAR_SERIAL_NUMBER) + DEBOUNCE_TIMEOUT, BRIDGE_MODEL, BRIDGE_NAME, + BRIDGE_SERIAL_NUMBER, MANUFACTURER) from .util import ( show_setup_message, dismiss_setup_message) @@ -61,59 +61,20 @@ def debounce(func): return wrapper -def add_preload_service(acc, service, chars=None): - """Define and return a service to be available for the accessory.""" - from pyhap.loader import get_serv_loader, get_char_loader - service = get_serv_loader().get(service) - if chars: - chars = chars if isinstance(chars, list) else [chars] - for char_name in chars: - char = get_char_loader().get(char_name) - service.add_characteristic(char) - acc.add_service(service) - return service - - -def setup_char(char_name, service, value=None, properties=None, callback=None): - """Helper function to return fully configured characteristic.""" - char = service.get_characteristic(char_name) - if value: - char.value = value - if properties: - char.override_properties(properties) - if callback: - char.setter_callback = callback - return char - - -def set_accessory_info(acc, name, model, serial_number, - manufacturer=MANUFACTURER, - firmware_revision=__version__): - """Set the default accessory information.""" - service = acc.get_service(SERV_ACCESSORY_INFO) - service.get_characteristic(CHAR_NAME).set_value(name) - service.get_characteristic(CHAR_MODEL).set_value(model) - service.get_characteristic(CHAR_MANUFACTURER).set_value(manufacturer) - service.get_characteristic(CHAR_SERIAL_NUMBER).set_value(serial_number) - service.get_characteristic(CHAR_FIRMWARE_REVISION) \ - .set_value(firmware_revision) - - class HomeAccessory(Accessory): """Adapter class for Accessory.""" - def __init__(self, hass, name, entity_id, aid, category): + def __init__(self, hass, name, entity_id, aid, category=CATEGORY_OTHER): """Initialize a Accessory object.""" super().__init__(name, aid=aid) domain = split_entity_id(entity_id)[0].replace("_", " ").title() - set_accessory_info(self, name, model=domain, serial_number=entity_id) - self.category = getattr(Category, category, Category.OTHER) + self.set_info_service( + firmware_revision=__version__, manufacturer=MANUFACTURER, + model=domain, serial_number=entity_id) + self.category = category self.entity_id = entity_id self.hass = hass - def _set_services(self): - add_preload_service(self, SERV_ACCESSORY_INFO) - def run(self): """Method called by accessory after driver is started.""" state = self.hass.states.get(self.entity_id) @@ -143,13 +104,11 @@ class HomeBridge(Bridge): def __init__(self, hass, name=BRIDGE_NAME): """Initialize a Bridge object.""" super().__init__(name) - set_accessory_info(self, name, model=BRIDGE_MODEL, - serial_number=BRIDGE_SERIAL_NUMBER) + self.set_info_service( + firmware_revision=__version__, manufacturer=MANUFACTURER, + model=BRIDGE_MODEL, serial_number=BRIDGE_SERIAL_NUMBER) self.hass = hass - def _set_services(self): - add_preload_service(self, SERV_ACCESSORY_INFO) - def setup_message(self): """Prevent print of pyhap setup message to terminal.""" pass diff --git a/homeassistant/components/homekit/const.py b/homeassistant/components/homekit/const.py index 9c9f60eef94..ce46e84a2ef 100644 --- a/homeassistant/components/homekit/const.py +++ b/homeassistant/components/homekit/const.py @@ -23,17 +23,6 @@ BRIDGE_NAME = 'Home Assistant Bridge' BRIDGE_SERIAL_NUMBER = 'homekit.bridge' MANUFACTURER = 'Home Assistant' -# #### Categories #### -CATEGORY_ALARM_SYSTEM = 'ALARM_SYSTEM' -CATEGORY_GARAGE_DOOR_OPENER = 'GARAGE_DOOR_OPENER' -CATEGORY_LIGHT = 'LIGHTBULB' -CATEGORY_LOCK = 'DOOR_LOCK' -CATEGORY_SENSOR = 'SENSOR' -CATEGORY_SWITCH = 'SWITCH' -CATEGORY_THERMOSTAT = 'THERMOSTAT' -CATEGORY_WINDOW_COVERING = 'WINDOW_COVERING' - - # #### Services #### SERV_ACCESSORY_INFO = 'AccessoryInformation' SERV_AIR_QUALITY_SENSOR = 'AirQualitySensor' @@ -56,7 +45,6 @@ SERV_THERMOSTAT = 'Thermostat' SERV_WINDOW_COVERING = 'WindowCovering' # CurrentPosition, TargetPosition, PositionState - # #### Characteristics #### CHAR_AIR_PARTICULATE_DENSITY = 'AirParticulateDensity' CHAR_AIR_QUALITY = 'AirQuality' diff --git a/homeassistant/components/homekit/type_covers.py b/homeassistant/components/homekit/type_covers.py index 8ec715e0e01..b30109f711d 100644 --- a/homeassistant/components/homekit/type_covers.py +++ b/homeassistant/components/homekit/type_covers.py @@ -1,6 +1,8 @@ """Class to hold all cover accessories.""" import logging +from pyhap.const import CATEGORY_WINDOW_COVERING, CATEGORY_GARAGE_DOOR_OPENER + from homeassistant.components.cover import ( ATTR_CURRENT_POSITION, ATTR_POSITION, DOMAIN, SUPPORT_STOP) from homeassistant.const import ( @@ -9,12 +11,11 @@ from homeassistant.const import ( ATTR_SUPPORTED_FEATURES) from . import TYPES -from .accessories import HomeAccessory, add_preload_service, setup_char +from .accessories import HomeAccessory from .const import ( - CATEGORY_WINDOW_COVERING, SERV_WINDOW_COVERING, - CHAR_CURRENT_POSITION, CHAR_TARGET_POSITION, CHAR_POSITION_STATE, - CATEGORY_GARAGE_DOOR_OPENER, SERV_GARAGE_DOOR_OPENER, - CHAR_CURRENT_DOOR_STATE, CHAR_TARGET_DOOR_STATE) + SERV_WINDOW_COVERING, CHAR_CURRENT_POSITION, + CHAR_TARGET_POSITION, CHAR_POSITION_STATE, + SERV_GARAGE_DOOR_OPENER, CHAR_CURRENT_DOOR_STATE, CHAR_TARGET_DOOR_STATE) _LOGGER = logging.getLogger(__name__) @@ -32,12 +33,11 @@ class GarageDoorOpener(HomeAccessory): super().__init__(*args, category=CATEGORY_GARAGE_DOOR_OPENER) self.flag_target_state = False - serv_garage_door = add_preload_service(self, SERV_GARAGE_DOOR_OPENER) - self.char_current_state = setup_char( - CHAR_CURRENT_DOOR_STATE, serv_garage_door, value=0) - self.char_target_state = setup_char( - CHAR_TARGET_DOOR_STATE, serv_garage_door, value=0, - callback=self.set_state) + serv_garage_door = self.add_preload_service(SERV_GARAGE_DOOR_OPENER) + self.char_current_state = serv_garage_door.configure_char( + CHAR_CURRENT_DOOR_STATE, value=0) + self.char_target_state = serv_garage_door.configure_char( + CHAR_TARGET_DOOR_STATE, value=0, setter_callback=self.set_state) def set_state(self, value): """Change garage state if call came from HomeKit.""" @@ -74,12 +74,11 @@ class WindowCovering(HomeAccessory): super().__init__(*args, category=CATEGORY_WINDOW_COVERING) self.homekit_target = None - serv_cover = add_preload_service(self, SERV_WINDOW_COVERING) - self.char_current_position = setup_char( - CHAR_CURRENT_POSITION, serv_cover, value=0) - self.char_target_position = setup_char( - CHAR_TARGET_POSITION, serv_cover, value=0, - callback=self.move_cover) + serv_cover = self.add_preload_service(SERV_WINDOW_COVERING) + self.char_current_position = serv_cover.configure_char( + CHAR_CURRENT_POSITION, value=0) + self.char_target_position = serv_cover.configure_char( + CHAR_TARGET_POSITION, value=0, setter_callback=self.move_cover) def move_cover(self, value): """Move cover to value if call came from HomeKit.""" @@ -115,14 +114,13 @@ class WindowCoveringBasic(HomeAccessory): .attributes.get(ATTR_SUPPORTED_FEATURES) self.supports_stop = features & SUPPORT_STOP - serv_cover = add_preload_service(self, SERV_WINDOW_COVERING) - self.char_current_position = setup_char( - CHAR_CURRENT_POSITION, serv_cover, value=0) - self.char_target_position = setup_char( - CHAR_TARGET_POSITION, serv_cover, value=0, - callback=self.move_cover) - self.char_position_state = setup_char( - CHAR_POSITION_STATE, serv_cover, value=2) + serv_cover = self.add_preload_service(SERV_WINDOW_COVERING) + self.char_current_position = serv_cover.configure_char( + CHAR_CURRENT_POSITION, value=0) + self.char_target_position = serv_cover.configure_char( + CHAR_TARGET_POSITION, value=0, setter_callback=self.move_cover) + self.char_position_state = serv_cover.configure_char( + CHAR_POSITION_STATE, value=2) def move_cover(self, value): """Move cover to value if call came from HomeKit.""" diff --git a/homeassistant/components/homekit/type_lights.py b/homeassistant/components/homekit/type_lights.py index 9a7bce76fba..3efb0e99df6 100644 --- a/homeassistant/components/homekit/type_lights.py +++ b/homeassistant/components/homekit/type_lights.py @@ -1,16 +1,17 @@ """Class to hold all light accessories.""" import logging +from pyhap.const import CATEGORY_LIGHTBULB + from homeassistant.components.light import ( ATTR_HS_COLOR, ATTR_COLOR_TEMP, ATTR_BRIGHTNESS, ATTR_MIN_MIREDS, ATTR_MAX_MIREDS, SUPPORT_COLOR, SUPPORT_COLOR_TEMP, SUPPORT_BRIGHTNESS) from homeassistant.const import ATTR_SUPPORTED_FEATURES, STATE_ON, STATE_OFF from . import TYPES -from .accessories import ( - HomeAccessory, add_preload_service, debounce, setup_char) +from .accessories import HomeAccessory, debounce from .const import ( - CATEGORY_LIGHT, SERV_LIGHTBULB, CHAR_COLOR_TEMPERATURE, + SERV_LIGHTBULB, CHAR_COLOR_TEMPERATURE, CHAR_BRIGHTNESS, CHAR_HUE, CHAR_ON, CHAR_SATURATION) _LOGGER = logging.getLogger(__name__) @@ -27,7 +28,7 @@ class Light(HomeAccessory): def __init__(self, *args, config): """Initialize a new Light accessory object.""" - super().__init__(*args, category=CATEGORY_LIGHT) + super().__init__(*args, category=CATEGORY_LIGHTBULB) self._flag = {CHAR_ON: False, CHAR_BRIGHTNESS: False, CHAR_HUE: False, CHAR_SATURATION: False, CHAR_COLOR_TEMPERATURE: False, RGB_COLOR: False} @@ -46,30 +47,28 @@ class Light(HomeAccessory): self._hue = None self._saturation = None - serv_light = add_preload_service(self, SERV_LIGHTBULB, self.chars) - self.char_on = setup_char( - CHAR_ON, serv_light, value=self._state, callback=self.set_state) + serv_light = self.add_preload_service(SERV_LIGHTBULB, self.chars) + self.char_on = serv_light.configure_char( + CHAR_ON, value=self._state, setter_callback=self.set_state) if CHAR_BRIGHTNESS in self.chars: - self.char_brightness = setup_char( - CHAR_BRIGHTNESS, serv_light, value=0, - callback=self.set_brightness) + self.char_brightness = serv_light.configure_char( + CHAR_BRIGHTNESS, value=0, setter_callback=self.set_brightness) if CHAR_COLOR_TEMPERATURE in self.chars: min_mireds = self.hass.states.get(self.entity_id) \ .attributes.get(ATTR_MIN_MIREDS, 153) max_mireds = self.hass.states.get(self.entity_id) \ .attributes.get(ATTR_MAX_MIREDS, 500) - self.char_color_temperature = setup_char( - CHAR_COLOR_TEMPERATURE, serv_light, value=min_mireds, + self.char_color_temperature = serv_light.configure_char( + CHAR_COLOR_TEMPERATURE, value=min_mireds, properties={'minValue': min_mireds, 'maxValue': max_mireds}, - callback=self.set_color_temperature) + setter_callback=self.set_color_temperature) if CHAR_HUE in self.chars: - self.char_hue = setup_char( - CHAR_HUE, serv_light, value=0, callback=self.set_hue) + self.char_hue = serv_light.configure_char( + CHAR_HUE, value=0, setter_callback=self.set_hue) if CHAR_SATURATION in self.chars: - self.char_saturation = setup_char( - CHAR_SATURATION, serv_light, value=75, - callback=self.set_saturation) + self.char_saturation = serv_light.configure_char( + CHAR_SATURATION, value=75, setter_callback=self.set_saturation) def set_state(self, value): """Set state if call came from HomeKit.""" diff --git a/homeassistant/components/homekit/type_locks.py b/homeassistant/components/homekit/type_locks.py index f34fc6c6a7f..e7f18d44805 100644 --- a/homeassistant/components/homekit/type_locks.py +++ b/homeassistant/components/homekit/type_locks.py @@ -1,13 +1,15 @@ """Class to hold all lock accessories.""" import logging +from pyhap.const import CATEGORY_DOOR_LOCK + from homeassistant.components.lock import ( ATTR_ENTITY_ID, STATE_LOCKED, STATE_UNLOCKED, STATE_UNKNOWN) from . import TYPES -from .accessories import HomeAccessory, add_preload_service, setup_char +from .accessories import HomeAccessory from .const import ( - CATEGORY_LOCK, SERV_LOCK, CHAR_LOCK_CURRENT_STATE, CHAR_LOCK_TARGET_STATE) + SERV_LOCK, CHAR_LOCK_CURRENT_STATE, CHAR_LOCK_TARGET_STATE) _LOGGER = logging.getLogger(__name__) @@ -29,16 +31,16 @@ class Lock(HomeAccessory): def __init__(self, *args, config): """Initialize a Lock accessory object.""" - super().__init__(*args, category=CATEGORY_LOCK) + super().__init__(*args, category=CATEGORY_DOOR_LOCK) self.flag_target_state = False - serv_lock_mechanism = add_preload_service(self, SERV_LOCK) - self.char_current_state = setup_char( - CHAR_LOCK_CURRENT_STATE, serv_lock_mechanism, + serv_lock_mechanism = self.add_preload_service(SERV_LOCK) + self.char_current_state = serv_lock_mechanism.configure_char( + CHAR_LOCK_CURRENT_STATE, value=HASS_TO_HOMEKIT[STATE_UNKNOWN]) - self.char_target_state = setup_char( - CHAR_LOCK_TARGET_STATE, serv_lock_mechanism, - value=HASS_TO_HOMEKIT[STATE_LOCKED], callback=self.set_state) + self.char_target_state = serv_lock_mechanism.configure_char( + CHAR_LOCK_TARGET_STATE, value=HASS_TO_HOMEKIT[STATE_LOCKED], + setter_callback=self.set_state) def set_state(self, value): """Set lock state to value if call came from HomeKit.""" diff --git a/homeassistant/components/homekit/type_security_systems.py b/homeassistant/components/homekit/type_security_systems.py index 0762e0f25f9..968e60f2842 100644 --- a/homeassistant/components/homekit/type_security_systems.py +++ b/homeassistant/components/homekit/type_security_systems.py @@ -1,16 +1,18 @@ """Class to hold all alarm control panel accessories.""" import logging +from pyhap.const import CATEGORY_ALARM_SYSTEM + from homeassistant.const import ( STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME, STATE_ALARM_ARMED_NIGHT, STATE_ALARM_DISARMED, ATTR_ENTITY_ID, ATTR_CODE) from . import TYPES -from .accessories import HomeAccessory, add_preload_service, setup_char +from .accessories import HomeAccessory from .const import ( - CATEGORY_ALARM_SYSTEM, SERV_SECURITY_SYSTEM, - CHAR_CURRENT_SECURITY_STATE, CHAR_TARGET_SECURITY_STATE) + SERV_SECURITY_SYSTEM, CHAR_CURRENT_SECURITY_STATE, + CHAR_TARGET_SECURITY_STATE) _LOGGER = logging.getLogger(__name__) @@ -33,12 +35,12 @@ class SecuritySystem(HomeAccessory): self._alarm_code = config.get(ATTR_CODE) self.flag_target_state = False - serv_alarm = add_preload_service(self, SERV_SECURITY_SYSTEM) - self.char_current_state = setup_char( - CHAR_CURRENT_SECURITY_STATE, serv_alarm, value=3) - self.char_target_state = setup_char( - CHAR_TARGET_SECURITY_STATE, serv_alarm, value=3, - callback=self.set_security_state) + serv_alarm = self.add_preload_service(SERV_SECURITY_SYSTEM) + self.char_current_state = serv_alarm.configure_char( + CHAR_CURRENT_SECURITY_STATE, value=3) + self.char_target_state = serv_alarm.configure_char( + CHAR_TARGET_SECURITY_STATE, value=3, + setter_callback=self.set_security_state) def set_security_state(self, value): """Move security state to value if call came from HomeKit.""" diff --git a/homeassistant/components/homekit/type_sensors.py b/homeassistant/components/homekit/type_sensors.py index 7d7bbc5edd6..393b6beffd6 100644 --- a/homeassistant/components/homekit/type_sensors.py +++ b/homeassistant/components/homekit/type_sensors.py @@ -1,14 +1,16 @@ """Class to hold all sensor accessories.""" import logging +from pyhap.const import CATEGORY_SENSOR + from homeassistant.const import ( ATTR_UNIT_OF_MEASUREMENT, TEMP_CELSIUS, ATTR_DEVICE_CLASS, STATE_ON, STATE_HOME) from . import TYPES -from .accessories import HomeAccessory, add_preload_service, setup_char +from .accessories import HomeAccessory from .const import ( - CATEGORY_SENSOR, SERV_HUMIDITY_SENSOR, SERV_TEMPERATURE_SENSOR, + SERV_HUMIDITY_SENSOR, SERV_TEMPERATURE_SENSOR, CHAR_CURRENT_HUMIDITY, CHAR_CURRENT_TEMPERATURE, PROP_CELSIUS, SERV_AIR_QUALITY_SENSOR, CHAR_AIR_QUALITY, CHAR_AIR_PARTICULATE_DENSITY, CHAR_CARBON_DIOXIDE_LEVEL, CHAR_CARBON_DIOXIDE_PEAK_LEVEL, @@ -52,10 +54,9 @@ class TemperatureSensor(HomeAccessory): def __init__(self, *args, config): """Initialize a TemperatureSensor accessory object.""" super().__init__(*args, category=CATEGORY_SENSOR) - serv_temp = add_preload_service(self, SERV_TEMPERATURE_SENSOR) - self.char_temp = setup_char( - CHAR_CURRENT_TEMPERATURE, serv_temp, value=0, - properties=PROP_CELSIUS) + serv_temp = self.add_preload_service(SERV_TEMPERATURE_SENSOR) + self.char_temp = serv_temp.configure_char( + CHAR_CURRENT_TEMPERATURE, value=0, properties=PROP_CELSIUS) self.unit = None def update_state(self, new_state): @@ -76,9 +77,9 @@ class HumiditySensor(HomeAccessory): def __init__(self, *args, config): """Initialize a HumiditySensor accessory object.""" super().__init__(*args, category=CATEGORY_SENSOR) - serv_humidity = add_preload_service(self, SERV_HUMIDITY_SENSOR) - self.char_humidity = setup_char( - CHAR_CURRENT_HUMIDITY, serv_humidity, value=0) + serv_humidity = self.add_preload_service(SERV_HUMIDITY_SENSOR) + self.char_humidity = serv_humidity.configure_char( + CHAR_CURRENT_HUMIDITY, value=0) def update_state(self, new_state): """Update accessory after state change.""" @@ -97,12 +98,12 @@ class AirQualitySensor(HomeAccessory): """Initialize a AirQualitySensor accessory object.""" super().__init__(*args, category=CATEGORY_SENSOR) - serv_air_quality = add_preload_service(self, SERV_AIR_QUALITY_SENSOR, - [CHAR_AIR_PARTICULATE_DENSITY]) - self.char_quality = setup_char( - CHAR_AIR_QUALITY, serv_air_quality, value=0) - self.char_density = setup_char( - CHAR_AIR_PARTICULATE_DENSITY, serv_air_quality, value=0) + serv_air_quality = self.add_preload_service( + SERV_AIR_QUALITY_SENSOR, [CHAR_AIR_PARTICULATE_DENSITY]) + self.char_quality = serv_air_quality.configure_char( + CHAR_AIR_QUALITY, value=0) + self.char_density = serv_air_quality.configure_char( + CHAR_AIR_PARTICULATE_DENSITY, value=0) def update_state(self, new_state): """Update accessory after state change.""" @@ -121,14 +122,14 @@ class CarbonDioxideSensor(HomeAccessory): """Initialize a CarbonDioxideSensor accessory object.""" super().__init__(*args, category=CATEGORY_SENSOR) - serv_co2 = add_preload_service(self, SERV_CARBON_DIOXIDE_SENSOR, [ + serv_co2 = self.add_preload_service(SERV_CARBON_DIOXIDE_SENSOR, [ CHAR_CARBON_DIOXIDE_LEVEL, CHAR_CARBON_DIOXIDE_PEAK_LEVEL]) - self.char_co2 = setup_char( - CHAR_CARBON_DIOXIDE_LEVEL, serv_co2, value=0) - self.char_peak = setup_char( - CHAR_CARBON_DIOXIDE_PEAK_LEVEL, serv_co2, value=0) - self.char_detected = setup_char( - CHAR_CARBON_DIOXIDE_DETECTED, serv_co2, value=0) + self.char_co2 = serv_co2.configure_char( + CHAR_CARBON_DIOXIDE_LEVEL, value=0) + self.char_peak = serv_co2.configure_char( + CHAR_CARBON_DIOXIDE_PEAK_LEVEL, value=0) + self.char_detected = serv_co2.configure_char( + CHAR_CARBON_DIOXIDE_DETECTED, value=0) def update_state(self, new_state): """Update accessory after state change.""" @@ -149,9 +150,9 @@ class LightSensor(HomeAccessory): """Initialize a LightSensor accessory object.""" super().__init__(*args, category=CATEGORY_SENSOR) - serv_light = add_preload_service(self, SERV_LIGHT_SENSOR) - self.char_light = setup_char( - CHAR_CURRENT_AMBIENT_LIGHT_LEVEL, serv_light, value=0) + serv_light = self.add_preload_service(SERV_LIGHT_SENSOR) + self.char_light = serv_light.configure_char( + CHAR_CURRENT_AMBIENT_LIGHT_LEVEL, value=0) def update_state(self, new_state): """Update accessory after state change.""" @@ -174,8 +175,8 @@ class BinarySensor(HomeAccessory): if device_class in BINARY_SENSOR_SERVICE_MAP \ else BINARY_SENSOR_SERVICE_MAP[DEVICE_CLASS_OCCUPANCY] - service = add_preload_service(self, service_char[0]) - self.char_detected = setup_char(service_char[1], service, value=0) + service = self.add_preload_service(service_char[0]) + self.char_detected = service.configure_char(service_char[1], value=0) def update_state(self, new_state): """Update accessory after state change.""" diff --git a/homeassistant/components/homekit/type_switches.py b/homeassistant/components/homekit/type_switches.py index aaf13e4ea7e..68a4fcdab0a 100644 --- a/homeassistant/components/homekit/type_switches.py +++ b/homeassistant/components/homekit/type_switches.py @@ -1,13 +1,15 @@ """Class to hold all switch accessories.""" import logging +from pyhap.const import CATEGORY_SWITCH + from homeassistant.const import ( ATTR_ENTITY_ID, SERVICE_TURN_ON, SERVICE_TURN_OFF, STATE_ON) from homeassistant.core import split_entity_id from . import TYPES -from .accessories import HomeAccessory, add_preload_service, setup_char -from .const import CATEGORY_SWITCH, SERV_SWITCH, CHAR_ON +from .accessories import HomeAccessory +from .const import SERV_SWITCH, CHAR_ON _LOGGER = logging.getLogger(__name__) @@ -22,9 +24,9 @@ class Switch(HomeAccessory): self._domain = split_entity_id(self.entity_id)[0] self.flag_target_state = False - serv_switch = add_preload_service(self, SERV_SWITCH) - self.char_on = setup_char( - CHAR_ON, serv_switch, value=False, callback=self.set_state) + serv_switch = self.add_preload_service(SERV_SWITCH) + self.char_on = serv_switch.configure_char( + CHAR_ON, value=False, setter_callback=self.set_state) def set_state(self, value): """Move switch state to value if call came from HomeKit.""" diff --git a/homeassistant/components/homekit/type_thermostats.py b/homeassistant/components/homekit/type_thermostats.py index 4faceefe850..15fd8160a7e 100644 --- a/homeassistant/components/homekit/type_thermostats.py +++ b/homeassistant/components/homekit/type_thermostats.py @@ -1,6 +1,8 @@ """Class to hold all thermostat accessories.""" import logging +from pyhap.const import CATEGORY_THERMOSTAT + from homeassistant.components.climate import ( ATTR_CURRENT_TEMPERATURE, ATTR_TEMPERATURE, ATTR_TARGET_TEMP_HIGH, ATTR_TARGET_TEMP_LOW, @@ -12,10 +14,9 @@ from homeassistant.const import ( STATE_OFF, TEMP_CELSIUS, TEMP_FAHRENHEIT) from . import TYPES -from .accessories import ( - HomeAccessory, add_preload_service, debounce, setup_char) +from .accessories import HomeAccessory, debounce from .const import ( - CATEGORY_THERMOSTAT, SERV_THERMOSTAT, CHAR_CURRENT_HEATING_COOLING, + SERV_THERMOSTAT, CHAR_CURRENT_HEATING_COOLING, CHAR_TARGET_HEATING_COOLING, CHAR_CURRENT_TEMPERATURE, CHAR_TARGET_TEMPERATURE, CHAR_TEMP_DISPLAY_UNITS, CHAR_COOLING_THRESHOLD_TEMPERATURE, CHAR_HEATING_THRESHOLD_TEMPERATURE) @@ -57,38 +58,37 @@ class Thermostat(HomeAccessory): self.chars.extend((CHAR_COOLING_THRESHOLD_TEMPERATURE, CHAR_HEATING_THRESHOLD_TEMPERATURE)) - serv_thermostat = add_preload_service( - self, SERV_THERMOSTAT, self.chars) + serv_thermostat = self.add_preload_service(SERV_THERMOSTAT, self.chars) # Current and target mode characteristics - self.char_current_heat_cool = setup_char( - CHAR_CURRENT_HEATING_COOLING, serv_thermostat, value=0) - self.char_target_heat_cool = setup_char( - CHAR_TARGET_HEATING_COOLING, serv_thermostat, value=0, - callback=self.set_heat_cool) + self.char_current_heat_cool = serv_thermostat.configure_char( + CHAR_CURRENT_HEATING_COOLING, value=0) + self.char_target_heat_cool = serv_thermostat.configure_char( + CHAR_TARGET_HEATING_COOLING, value=0, + setter_callback=self.set_heat_cool) # Current and target temperature characteristics - self.char_current_temp = setup_char( - CHAR_CURRENT_TEMPERATURE, serv_thermostat, value=21.0) - self.char_target_temp = setup_char( - CHAR_TARGET_TEMPERATURE, serv_thermostat, value=21.0, - callback=self.set_target_temperature) + self.char_current_temp = serv_thermostat.configure_char( + CHAR_CURRENT_TEMPERATURE, value=21.0) + self.char_target_temp = serv_thermostat.configure_char( + CHAR_TARGET_TEMPERATURE, value=21.0, + setter_callback=self.set_target_temperature) # Display units characteristic - self.char_display_units = setup_char( - CHAR_TEMP_DISPLAY_UNITS, serv_thermostat, value=0) + self.char_display_units = serv_thermostat.configure_char( + CHAR_TEMP_DISPLAY_UNITS, value=0) # If the device supports it: high and low temperature characteristics self.char_cooling_thresh_temp = None self.char_heating_thresh_temp = None if CHAR_COOLING_THRESHOLD_TEMPERATURE in self.chars: - self.char_cooling_thresh_temp = setup_char( - CHAR_COOLING_THRESHOLD_TEMPERATURE, serv_thermostat, - value=23.0, callback=self.set_cooling_threshold) + self.char_cooling_thresh_temp = serv_thermostat.configure_char( + CHAR_COOLING_THRESHOLD_TEMPERATURE, value=23.0, + setter_callback=self.set_cooling_threshold) if CHAR_HEATING_THRESHOLD_TEMPERATURE in self.chars: - self.char_heating_thresh_temp = setup_char( - CHAR_HEATING_THRESHOLD_TEMPERATURE, serv_thermostat, - value=19.0, callback=self.set_heating_threshold) + self.char_heating_thresh_temp = serv_thermostat.configure_char( + CHAR_HEATING_THRESHOLD_TEMPERATURE, value=19.0, + setter_callback=self.set_heating_threshold) def set_heat_cool(self, value): """Move operation mode to value if call came from HomeKit.""" diff --git a/requirements_all.txt b/requirements_all.txt index 002c5eb93e2..0981ef154c4 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -28,7 +28,7 @@ Adafruit-SHT31==1.0.2 DoorBirdPy==0.1.3 # homeassistant.components.homekit -HAP-python==1.1.9 +HAP-python==2.0.0 # homeassistant.components.notify.mastodon Mastodon.py==1.2.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 0605b3d2e24..0e0b4f4da9f 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -19,7 +19,7 @@ requests_mock==1.4 # homeassistant.components.homekit -HAP-python==1.1.9 +HAP-python==2.0.0 # homeassistant.components.notify.html5 PyJWT==1.6.0 diff --git a/tests/components/homekit/test_accessories.py b/tests/components/homekit/test_accessories.py index 3df76185a51..faa982f62f3 100644 --- a/tests/components/homekit/test_accessories.py +++ b/tests/components/homekit/test_accessories.py @@ -7,12 +7,12 @@ import unittest from unittest.mock import call, patch, Mock from homeassistant.components.homekit.accessories import ( - add_preload_service, set_accessory_info, debounce, HomeAccessory, HomeBridge, HomeDriver) from homeassistant.components.homekit.const import ( - BRIDGE_MODEL, BRIDGE_NAME, SERV_ACCESSORY_INFO, CHAR_FIRMWARE_REVISION, - CHAR_MANUFACTURER, CHAR_MODEL, CHAR_NAME, CHAR_SERIAL_NUMBER, MANUFACTURER) -from homeassistant.const import ATTR_NOW, EVENT_TIME_CHANGED + BRIDGE_MODEL, BRIDGE_NAME, BRIDGE_SERIAL_NUMBER, SERV_ACCESSORY_INFO, + CHAR_FIRMWARE_REVISION, CHAR_MANUFACTURER, CHAR_MODEL, CHAR_NAME, + CHAR_SERIAL_NUMBER, MANUFACTURER) +from homeassistant.const import __version__, ATTR_NOW, EVENT_TIME_CHANGED import homeassistant.util.dt as dt_util from tests.common import get_test_home_assistant @@ -62,73 +62,25 @@ class TestAccessories(unittest.TestCase): hass.stop() - def test_add_preload_service(self): - """Test add_preload_service without additional characteristics.""" - acc = Mock() - serv = add_preload_service(acc, 'AirPurifier') - self.assertEqual(acc.mock_calls, [call.add_service(serv)]) - with self.assertRaises(ValueError): - serv.get_characteristic('Name') - - # Test with typo in service name - with self.assertRaises(KeyError): - add_preload_service(Mock(), 'AirPurifierTypo') - - # Test adding additional characteristic as string - serv = add_preload_service(Mock(), 'AirPurifier', 'Name') - serv.get_characteristic('Name') - - # Test adding additional characteristics as list - serv = add_preload_service(Mock(), 'AirPurifier', - ['Name', 'RotationSpeed']) - serv.get_characteristic('Name') - serv.get_characteristic('RotationSpeed') - - # Test adding additional characteristic with typo - with self.assertRaises(KeyError): - add_preload_service(Mock(), 'AirPurifier', 'NameTypo') - - def test_set_accessory_info(self): - """Test setting the basic accessory information.""" - # Test HomeAccessory - acc = HomeAccessory('HA', 'Home Accessory', 'homekit.accessory', 2, '') - set_accessory_info(acc, 'name', 'model', '0000', MANUFACTURER, '1.2.3') - - serv = acc.get_service(SERV_ACCESSORY_INFO) - self.assertEqual(serv.get_characteristic(CHAR_NAME).value, 'name') - self.assertEqual(serv.get_characteristic(CHAR_MODEL).value, 'model') - self.assertEqual( - serv.get_characteristic(CHAR_SERIAL_NUMBER).value, '0000') - self.assertEqual( - serv.get_characteristic(CHAR_MANUFACTURER).value, MANUFACTURER) - self.assertEqual( - serv.get_characteristic(CHAR_FIRMWARE_REVISION).value, '1.2.3') - - # Test HomeBridge - acc = HomeBridge('hass') - set_accessory_info(acc, 'name', 'model', '0000', MANUFACTURER, '1.2.3') - - serv = acc.get_service(SERV_ACCESSORY_INFO) - self.assertEqual(serv.get_characteristic(CHAR_MODEL).value, 'model') - self.assertEqual( - serv.get_characteristic(CHAR_SERIAL_NUMBER).value, '0000') - self.assertEqual( - serv.get_characteristic(CHAR_MANUFACTURER).value, MANUFACTURER) - self.assertEqual( - serv.get_characteristic(CHAR_FIRMWARE_REVISION).value, '1.2.3') - def test_home_accessory(self): """Test HomeAccessory class.""" hass = get_test_home_assistant() - acc = HomeAccessory(hass, 'Home Accessory', 'homekit.accessory', 2, '') + acc = HomeAccessory(hass, 'Home Accessory', 'homekit.accessory', 2) self.assertEqual(acc.hass, hass) self.assertEqual(acc.display_name, 'Home Accessory') self.assertEqual(acc.category, 1) # Category.OTHER self.assertEqual(len(acc.services), 1) serv = acc.services[0] # SERV_ACCESSORY_INFO + self.assertEqual(serv.display_name, SERV_ACCESSORY_INFO) + self.assertEqual( + serv.get_characteristic(CHAR_NAME).value, 'Home Accessory') + self.assertEqual( + serv.get_characteristic(CHAR_MANUFACTURER).value, MANUFACTURER) self.assertEqual( serv.get_characteristic(CHAR_MODEL).value, 'Homekit') + self.assertEqual(serv.get_characteristic(CHAR_SERIAL_NUMBER).value, + 'homekit.accessory') hass.states.set('homekit.accessory', 'on') hass.block_till_done() @@ -136,7 +88,7 @@ class TestAccessories(unittest.TestCase): hass.states.set('homekit.accessory', 'off') hass.block_till_done() - acc = HomeAccessory('hass', 'test_name', 'test_model.demo', 2, '') + acc = HomeAccessory('hass', 'test_name', 'test_model.demo', 2) self.assertEqual(acc.display_name, 'test_name') self.assertEqual(acc.aid, 2) self.assertEqual(len(acc.services), 1) @@ -155,8 +107,17 @@ class TestAccessories(unittest.TestCase): self.assertEqual(len(bridge.services), 1) serv = bridge.services[0] # SERV_ACCESSORY_INFO self.assertEqual(serv.display_name, SERV_ACCESSORY_INFO) + self.assertEqual( + serv.get_characteristic(CHAR_NAME).value, BRIDGE_NAME) + self.assertEqual( + serv.get_characteristic(CHAR_FIRMWARE_REVISION).value, __version__) + self.assertEqual( + serv.get_characteristic(CHAR_MANUFACTURER).value, MANUFACTURER) self.assertEqual( serv.get_characteristic(CHAR_MODEL).value, BRIDGE_MODEL) + self.assertEqual( + serv.get_characteristic(CHAR_SERIAL_NUMBER).value, + BRIDGE_SERIAL_NUMBER) bridge = HomeBridge('hass', 'test_name') self.assertEqual(bridge.display_name, 'test_name') diff --git a/tests/components/homekit/test_homekit.py b/tests/components/homekit/test_homekit.py index 7ae37becbd5..082953038b5 100644 --- a/tests/components/homekit/test_homekit.py +++ b/tests/components/homekit/test_homekit.py @@ -4,7 +4,9 @@ from unittest.mock import call, patch, ANY, Mock from homeassistant import setup from homeassistant.core import State -from homeassistant.components.homekit import HomeKit, generate_aid +from homeassistant.components.homekit import ( + HomeKit, generate_aid, + STATUS_READY, STATUS_RUNNING, STATUS_STOPPED, STATUS_WAIT) from homeassistant.components.homekit.accessories import HomeBridge from homeassistant.components.homekit.const import ( DOMAIN, HOMEKIT_FILE, CONF_AUTO_START, @@ -79,24 +81,28 @@ class TestHomeKit(unittest.TestCase): CONF_IP_ADDRESS: '172.0.0.0'}} self.assertTrue(setup.setup_component( self.hass, DOMAIN, config)) - - self.hass.bus.fire(EVENT_HOMEASSISTANT_START) self.hass.block_till_done() self.assertEqual(mock_homekit.mock_calls, [ call(self.hass, 11111, '172.0.0.0', ANY, {}), call().setup()]) - # Test start call with driver stopped. + # Test auto_start disabled homekit.reset_mock() - homekit.configure_mock(**{'started': False}) + self.hass.bus.fire(EVENT_HOMEASSISTANT_START) + self.hass.block_till_done() + self.assertEqual(homekit.mock_calls, []) + + # Test start call with driver is ready + homekit.reset_mock() + homekit.status = STATUS_READY self.hass.services.call('homekit', 'start') self.assertEqual(homekit.mock_calls, [call.start()]) - # Test start call with driver started. + # Test start call with driver started homekit.reset_mock() - homekit.configure_mock(**{'started': True}) + homekit.status = STATUS_STOPPED self.hass.services.call(DOMAIN, SERVICE_HOMEKIT_START) self.assertEqual(homekit.mock_calls, []) @@ -180,34 +186,38 @@ class TestHomeKit(unittest.TestCase): state = self.hass.states.all()[0] homekit.start() + self.hass.block_till_done() self.assertEqual(mock_add_bridge_acc.mock_calls, [call(state)]) self.assertEqual(mock_show_setup_msg.mock_calls, [ call(self.hass, homekit.bridge)]) self.assertEqual(homekit.driver.mock_calls, [call.start()]) - self.assertTrue(homekit.started) + self.assertEqual(homekit.status, STATUS_RUNNING) # Test start() if already started homekit.driver.reset_mock() homekit.start() + self.hass.block_till_done() self.assertEqual(homekit.driver.mock_calls, []) def test_homekit_stop(self): """Test HomeKit stop method.""" - homekit = HomeKit(None, None, None, None, None) + homekit = HomeKit(self.hass, None, None, None, None) homekit.driver = Mock() - # Test if started = False + self.assertEqual(homekit.status, STATUS_READY) homekit.stop() - self.assertFalse(homekit.driver.stop.called) - - # Test if driver not started - homekit.started = True - homekit.driver.configure_mock(**{'run_sentinel': None}) + self.hass.block_till_done() + homekit.status = STATUS_WAIT homekit.stop() + self.hass.block_till_done() + homekit.status = STATUS_STOPPED + homekit.stop() + self.hass.block_till_done() self.assertFalse(homekit.driver.stop.called) # Test if driver is started - homekit.driver.configure_mock(**{'run_sentinel': 'sentinel'}) + homekit.status = STATUS_RUNNING homekit.stop() + self.hass.block_till_done() self.assertTrue(homekit.driver.stop.called) From 7900ba30bff3eb41976380515c412b8257716c3a Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Fri, 4 May 2018 17:09:05 +0200 Subject: [PATCH 123/155] Upgrade holidays to 0.9.5 (#14274) --- homeassistant/components/binary_sensor/workday.py | 15 ++++++++------- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 10 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/binary_sensor/workday.py b/homeassistant/components/binary_sensor/workday.py index 8935ad5115d..b37be3f6cb6 100644 --- a/homeassistant/components/binary_sensor/workday.py +++ b/homeassistant/components/binary_sensor/workday.py @@ -17,16 +17,17 @@ import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) -REQUIREMENTS = ['holidays==0.9.4'] +REQUIREMENTS = ['holidays==0.9.5'] # List of all countries currently supported by holidays # There seems to be no way to get the list out at runtime -ALL_COUNTRIES = ['Australia', 'AU', 'Austria', 'AT', 'Belgium', 'BE', 'Canada', - 'CA', 'Colombia', 'CO', 'Czech', 'CZ', 'Denmark', 'DK', - 'England', 'EuropeanCentralBank', 'ECB', 'TAR', 'Finland', - 'FI', 'France', 'FRA', 'Germany', 'DE', 'Ireland', - 'Isle of Man', 'Italy', 'IT', 'Japan', 'JP', 'Mexico', 'MX', - 'Netherlands', 'NL', 'NewZealand', 'NZ', 'Northern Ireland', +ALL_COUNTRIES = ['Argentina', 'AR', 'Australia', 'AU', 'Austria', 'AT', + 'Belgium', 'BE', 'Canada', 'CA', 'Colombia', 'CO', 'Czech', + 'CZ', 'Denmark', 'DK', 'England', 'EuropeanCentralBank', + 'ECB', 'TAR', 'Finland', 'FI', 'France', 'FRA', 'Germany', + 'DE', 'Hungary', 'HU', 'Ireland', 'Isle of Man', 'Italy', + 'IT', 'Japan', 'JP', 'Mexico', 'MX', 'Netherlands', 'NL', + 'NewZealand', 'NZ', 'Northern Ireland', 'Norway', 'NO', 'Polish', 'PL', 'Portugal', 'PT', 'PortugalExt', 'PTE', 'Scotland', 'Slovenia', 'SI', 'Slovakia', 'SK', 'South Africa', 'ZA', 'Spain', 'ES', diff --git a/requirements_all.txt b/requirements_all.txt index 0981ef154c4..395253c0f21 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -383,7 +383,7 @@ hikvision==0.4 hipnotify==1.0.8 # homeassistant.components.binary_sensor.workday -holidays==0.9.4 +holidays==0.9.5 # homeassistant.components.frontend home-assistant-frontend==20180426.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 0e0b4f4da9f..7816d9c6f24 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -78,7 +78,7 @@ haversine==0.4.5 hbmqtt==0.9.1 # homeassistant.components.binary_sensor.workday -holidays==0.9.4 +holidays==0.9.5 # homeassistant.components.frontend home-assistant-frontend==20180426.0 From bb76ba67f319775cc8abf1047098b0b723eab7b1 Mon Sep 17 00:00:00 2001 From: cdce8p <30130371+cdce8p@users.noreply.github.com> Date: Fri, 4 May 2018 22:48:38 +0200 Subject: [PATCH 124/155] Homekit: Changed device_class requirement Humidity Sensor (#14277) --- homeassistant/components/homekit/__init__.py | 9 ++++----- tests/components/homekit/test_get_accessories.py | 10 ++-------- 2 files changed, 6 insertions(+), 13 deletions(-) diff --git a/homeassistant/components/homekit/__init__.py b/homeassistant/components/homekit/__init__.py index 080dd2a7cbd..3abfffd67e0 100644 --- a/homeassistant/components/homekit/__init__.py +++ b/homeassistant/components/homekit/__init__.py @@ -126,10 +126,10 @@ def get_accessory(hass, state, aid, config): unit = state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) device_class = state.attributes.get(ATTR_DEVICE_CLASS) - if device_class == DEVICE_CLASS_TEMPERATURE or unit == TEMP_CELSIUS \ - or unit == TEMP_FAHRENHEIT: + if device_class == DEVICE_CLASS_TEMPERATURE or \ + unit in (TEMP_CELSIUS, TEMP_FAHRENHEIT): a_type = 'TemperatureSensor' - elif device_class == DEVICE_CLASS_HUMIDITY or unit == '%': + elif device_class == DEVICE_CLASS_HUMIDITY and unit == '%': a_type = 'HumiditySensor' elif device_class == DEVICE_CLASS_PM25 \ or DEVICE_CLASS_PM25 in state.entity_id: @@ -141,8 +141,7 @@ def get_accessory(hass, state, aid, config): unit == 'lux' or unit == 'lx': a_type = 'LightSensor' - elif state.domain == 'switch' or state.domain == 'remote' \ - or state.domain == 'input_boolean' or state.domain == 'script': + elif state.domain in ('switch', 'remote', 'input_boolean', 'script'): a_type = 'Switch' if a_type is None: diff --git a/tests/components/homekit/test_get_accessories.py b/tests/components/homekit/test_get_accessories.py index 76736ce45ad..71f9c8e6656 100644 --- a/tests/components/homekit/test_get_accessories.py +++ b/tests/components/homekit/test_get_accessories.py @@ -68,14 +68,8 @@ class TestGetAccessories(unittest.TestCase): """Test humidity sensor with device class humidity.""" with patch.dict(TYPES, {'HumiditySensor': self.mock_type}): state = State('sensor.humidity', '20', - {ATTR_DEVICE_CLASS: 'humidity'}) - get_accessory(None, state, 2, {}) - - def test_sensor_humidity_unit(self): - """Test humidity sensor with % as unit.""" - with patch.dict(TYPES, {'HumiditySensor': self.mock_type}): - state = State('sensor.humidity', '20', - {ATTR_UNIT_OF_MEASUREMENT: '%'}) + {ATTR_DEVICE_CLASS: 'humidity', + ATTR_UNIT_OF_MEASUREMENT: '%'}) get_accessory(None, state, 2, {}) def test_air_quality_sensor(self): From 255a85ad022e1c3708adb25799fd01a0d9c10e3f Mon Sep 17 00:00:00 2001 From: Matt Schmitt Date: Fri, 4 May 2018 18:09:16 -0400 Subject: [PATCH 125/155] HomeKit: Support triggered state for alarm_control_panel (#14285) --- .../homekit/type_security_systems.py | 22 +++++++++++-------- .../homekit/test_type_security_systems.py | 10 +++++++-- 2 files changed, 21 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/homekit/type_security_systems.py b/homeassistant/components/homekit/type_security_systems.py index 968e60f2842..e32860d1fef 100644 --- a/homeassistant/components/homekit/type_security_systems.py +++ b/homeassistant/components/homekit/type_security_systems.py @@ -6,7 +6,7 @@ from pyhap.const import CATEGORY_ALARM_SYSTEM from homeassistant.const import ( STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME, STATE_ALARM_ARMED_NIGHT, STATE_ALARM_DISARMED, - ATTR_ENTITY_ID, ATTR_CODE) + STATE_ALARM_TRIGGERED, ATTR_ENTITY_ID, ATTR_CODE) from . import TYPES from .accessories import HomeAccessory @@ -16,13 +16,16 @@ from .const import ( _LOGGER = logging.getLogger(__name__) -HASS_TO_HOMEKIT = {STATE_ALARM_DISARMED: 3, STATE_ALARM_ARMED_HOME: 0, - STATE_ALARM_ARMED_AWAY: 1, STATE_ALARM_ARMED_NIGHT: 2} +HASS_TO_HOMEKIT = {STATE_ALARM_ARMED_HOME: 0, + STATE_ALARM_ARMED_AWAY: 1, + STATE_ALARM_ARMED_NIGHT: 2, + STATE_ALARM_DISARMED: 3, + STATE_ALARM_TRIGGERED: 4} HOMEKIT_TO_HASS = {c: s for s, c in HASS_TO_HOMEKIT.items()} -STATE_TO_SERVICE = {STATE_ALARM_DISARMED: 'alarm_disarm', - STATE_ALARM_ARMED_HOME: 'alarm_arm_home', +STATE_TO_SERVICE = {STATE_ALARM_ARMED_HOME: 'alarm_arm_home', STATE_ALARM_ARMED_AWAY: 'alarm_arm_away', - STATE_ALARM_ARMED_NIGHT: 'alarm_arm_night'} + STATE_ALARM_ARMED_NIGHT: 'alarm_arm_night', + STATE_ALARM_DISARMED: 'alarm_disarm'} @TYPES.register('SecuritySystem') @@ -64,7 +67,8 @@ class SecuritySystem(HomeAccessory): _LOGGER.debug('%s: Updated current state to %s (%d)', self.entity_id, hass_state, current_security_state) - if not self.flag_target_state: + # SecuritySystemTargetSTate does not support triggered + if not self.flag_target_state and \ + hass_state != STATE_ALARM_TRIGGERED: self.char_target_state.set_value(current_security_state) - if self.char_target_state.value == self.char_current_state.value: - self.flag_target_state = False + self.flag_target_state = False diff --git a/tests/components/homekit/test_type_security_systems.py b/tests/components/homekit/test_type_security_systems.py index 9c1ff0faf1a..baa461af772 100644 --- a/tests/components/homekit/test_type_security_systems.py +++ b/tests/components/homekit/test_type_security_systems.py @@ -7,7 +7,8 @@ from homeassistant.components.homekit.type_security_systems import ( from homeassistant.const import ( ATTR_CODE, ATTR_SERVICE, ATTR_SERVICE_DATA, EVENT_CALL_SERVICE, STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME, - STATE_ALARM_ARMED_NIGHT, STATE_ALARM_DISARMED, STATE_UNKNOWN) + STATE_ALARM_ARMED_NIGHT, STATE_ALARM_DISARMED, STATE_ALARM_TRIGGERED, + STATE_UNKNOWN) from tests.common import get_test_home_assistant @@ -65,10 +66,15 @@ class TestHomekitSecuritySystems(unittest.TestCase): self.assertEqual(acc.char_target_state.value, 3) self.assertEqual(acc.char_current_state.value, 3) + self.hass.states.set(acp, STATE_ALARM_TRIGGERED) + self.hass.block_till_done() + self.assertEqual(acc.char_target_state.value, 3) + self.assertEqual(acc.char_current_state.value, 4) + self.hass.states.set(acp, STATE_UNKNOWN) self.hass.block_till_done() self.assertEqual(acc.char_target_state.value, 3) - self.assertEqual(acc.char_current_state.value, 3) + self.assertEqual(acc.char_current_state.value, 4) # Set from HomeKit acc.char_target_state.client_update_value(0) From 354470469f6b5b464218f6934eb5e0e144acfe6b Mon Sep 17 00:00:00 2001 From: Diogo Gomes Date: Sat, 5 May 2018 02:10:08 +0100 Subject: [PATCH 126/155] Fix filter sensor missing window_size argument (#14252) * missing window_size argument * test throttle filter configuration --- homeassistant/components/sensor/filter.py | 2 ++ tests/components/sensor/test_filter.py | 3 +++ 2 files changed, 5 insertions(+) diff --git a/homeassistant/components/sensor/filter.py b/homeassistant/components/sensor/filter.py index 5b28faf78ca..9c05028b394 100644 --- a/homeassistant/components/sensor/filter.py +++ b/homeassistant/components/sensor/filter.py @@ -89,6 +89,8 @@ FILTER_TIME_SMA_SCHEMA = FILTER_SCHEMA.extend({ FILTER_THROTTLE_SCHEMA = FILTER_SCHEMA.extend({ vol.Required(CONF_FILTER_NAME): FILTER_NAME_THROTTLE, + vol.Optional(CONF_FILTER_WINDOW_SIZE, + default=DEFAULT_WINDOW_SIZE): vol.Coerce(int), }) PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ diff --git a/tests/components/sensor/test_filter.py b/tests/components/sensor/test_filter.py index 43432f3304c..8e79306fe13 100644 --- a/tests/components/sensor/test_filter.py +++ b/tests/components/sensor/test_filter.py @@ -67,6 +67,9 @@ class TestFilterSensor(unittest.TestCase): 'filter': 'lowpass', 'time_constant': 10, 'precision': 2 + }, { + 'filter': 'throttle', + 'window_size': 1 }] } } From 75bf4830713c7a4dca018bc568ac37693204b831 Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Sat, 5 May 2018 10:45:09 +0200 Subject: [PATCH 127/155] Upgrade astral to 1.6.1 (#14297) --- 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 0f3c9ac255d..f6666c829e0 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -7,7 +7,7 @@ voluptuous==0.11.1 typing>=3,<4 aiohttp==3.1.3 async_timeout==2.0.1 -astral==1.6 +astral==1.6.1 certifi>=2017.4.17 attrs==18.1.0 diff --git a/requirements_all.txt b/requirements_all.txt index 395253c0f21..88dd3d60904 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -8,7 +8,7 @@ voluptuous==0.11.1 typing>=3,<4 aiohttp==3.1.3 async_timeout==2.0.1 -astral==1.6 +astral==1.6.1 certifi>=2017.4.17 attrs==18.1.0 diff --git a/setup.py b/setup.py index 3db7c737a2c..8a68617afd9 100755 --- a/setup.py +++ b/setup.py @@ -51,7 +51,7 @@ REQUIRES = [ 'typing>=3,<4', 'aiohttp==3.1.3', 'async_timeout==2.0.1', - 'astral==1.6', + 'astral==1.6.1', 'certifi>=2017.4.17', 'attrs==18.1.0', ] From 5ade84d75f70165795079c585e438c3ab46b4662 Mon Sep 17 00:00:00 2001 From: Nick Whyte Date: Sat, 5 May 2018 19:17:28 +1000 Subject: [PATCH 128/155] BOM Weather throttle fix (#14234) --- homeassistant/components/sensor/bom.py | 39 +++++++++----------------- 1 file changed, 13 insertions(+), 26 deletions(-) diff --git a/homeassistant/components/sensor/bom.py b/homeassistant/components/sensor/bom.py index 272d5d1e0b8..128f532e459 100644 --- a/homeassistant/components/sensor/bom.py +++ b/homeassistant/components/sensor/bom.py @@ -33,8 +33,7 @@ CONF_STATION = 'station' CONF_ZONE_ID = 'zone_id' CONF_WMO_ID = 'wmo_id' -MIN_TIME_BETWEEN_UPDATES = datetime.timedelta(seconds=60) -LAST_UPDATE = 0 +MIN_TIME_BETWEEN_UPDATES = datetime.timedelta(minutes=35) # Sensor types are defined like: Name, units SENSOR_TYPES = { @@ -114,13 +113,13 @@ def setup_platform(hass, config, add_devices, discovery_info=None): _LOGGER.error("Could not get BOM weather station from lat/lon") return False - rest = BOMCurrentData(hass, station) + bom_data = BOMCurrentData(hass, station) try: - rest.update() + bom_data.update() except ValueError as err: _LOGGER.error("Received error from BOM_Current: %s", err) return False - add_devices([BOMCurrentSensor(rest, variable, config.get(CONF_NAME)) + add_devices([BOMCurrentSensor(bom_data, variable, config.get(CONF_NAME)) for variable in config[CONF_MONITORED_CONDITIONS]]) return True @@ -128,9 +127,9 @@ def setup_platform(hass, config, add_devices, discovery_info=None): class BOMCurrentSensor(Entity): """Implementation of a BOM current sensor.""" - def __init__(self, rest, condition, stationname): + def __init__(self, bom_data, condition, stationname): """Initialize the sensor.""" - self.rest = rest + self.bom_data = bom_data self._condition = condition self.stationname = stationname @@ -146,8 +145,8 @@ class BOMCurrentSensor(Entity): @property def state(self): """Return the state of the sensor.""" - if self.rest.data and self._condition in self.rest.data: - return self.rest.data[self._condition] + if self.bom_data.data and self._condition in self.bom_data.data: + return self.bom_data.data[self._condition] return STATE_UNKNOWN @@ -156,11 +155,11 @@ class BOMCurrentSensor(Entity): """Return the state attributes of the device.""" attr = {} attr['Sensor Id'] = self._condition - attr['Zone Id'] = self.rest.data['history_product'] - attr['Station Id'] = self.rest.data['wmo'] - attr['Station Name'] = self.rest.data['name'] + attr['Zone Id'] = self.bom_data.data['history_product'] + attr['Station Id'] = self.bom_data.data['wmo'] + attr['Station Name'] = self.bom_data.data['name'] attr['Last Update'] = datetime.datetime.strptime(str( - self.rest.data['local_date_time_full']), '%Y%m%d%H%M%S') + self.bom_data.data['local_date_time_full']), '%Y%m%d%H%M%S') attr[ATTR_ATTRIBUTION] = CONF_ATTRIBUTION return attr @@ -171,7 +170,7 @@ class BOMCurrentSensor(Entity): def update(self): """Update current conditions.""" - self.rest.update() + self.bom_data.update() class BOMCurrentData(object): @@ -182,7 +181,6 @@ class BOMCurrentData(object): self._hass = hass self._zone_id, self._wmo_id = station_id.split('.') self.data = None - self._lastupdate = LAST_UPDATE def _build_url(self): url = _RESOURCE.format(self._zone_id, self._zone_id, self._wmo_id) @@ -192,20 +190,9 @@ class BOMCurrentData(object): @Throttle(MIN_TIME_BETWEEN_UPDATES) def update(self): """Get the latest data from BOM.""" - if self._lastupdate != 0 and \ - ((datetime.datetime.now() - self._lastupdate) < - datetime.timedelta(minutes=35)): - _LOGGER.info( - "BOM was updated %s minutes ago, skipping update as" - " < 35 minutes", (datetime.datetime.now() - self._lastupdate)) - return self._lastupdate - try: result = requests.get(self._build_url(), timeout=10).json() self.data = result['observations']['data'][0] - self._lastupdate = datetime.datetime.strptime( - str(self.data['local_date_time_full']), '%Y%m%d%H%M%S') - return self._lastupdate except ValueError as err: _LOGGER.error("Check BOM %s", err.args) self.data = None From ec3ce4c80d67be9d90f5d87433e13b7a9778c941 Mon Sep 17 00:00:00 2001 From: blackwind Date: Sat, 5 May 2018 07:31:39 -0600 Subject: [PATCH 129/155] Publish attributes unconditionally (#14179) * Publish attributes unconditionally Because the attribute publish command was previously hidden behind `if val:`, falsy values like False and 0.0 weren't being published, thereby making Statestream -- particularly in the case of booleans, where the first True would be retained indefinitely -- a completely worthless indicator of state. * Change bool test to False to confirm falsy values pass --- homeassistant/components/mqtt_statestream.py | 7 +++---- tests/components/test_mqtt_statestream.py | 4 ++-- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/mqtt_statestream.py b/homeassistant/components/mqtt_statestream.py index 4427870c294..205a638c574 100644 --- a/homeassistant/components/mqtt_statestream.py +++ b/homeassistant/components/mqtt_statestream.py @@ -88,10 +88,9 @@ def async_setup(hass, config): if publish_attributes: for key, val in new_state.attributes.items(): - if val: - encoded_val = json.dumps(val, cls=JSONEncoder) - hass.components.mqtt.async_publish(mybase + key, - encoded_val, 1, True) + encoded_val = json.dumps(val, cls=JSONEncoder) + hass.components.mqtt.async_publish(mybase + key, + encoded_val, 1, True) async_track_state_change(hass, MATCH_ALL, _state_publisher) return True diff --git a/tests/components/test_mqtt_statestream.py b/tests/components/test_mqtt_statestream.py index 76d8e48d03a..e120c3a7dd2 100644 --- a/tests/components/test_mqtt_statestream.py +++ b/tests/components/test_mqtt_statestream.py @@ -134,7 +134,7 @@ class TestMqttStateStream(object): test_attributes = { "testing": "YES", "list": ["a", "b", "c"], - "bool": True + "bool": False } # Set a state of an entity @@ -150,7 +150,7 @@ class TestMqttStateStream(object): 1, True), call.async_publish(self.hass, 'pub/fake/entity/list', '["a", "b", "c"]', 1, True), - call.async_publish(self.hass, 'pub/fake/entity/bool', "true", + call.async_publish(self.hass, 'pub/fake/entity/bool', "false", 1, True) ] From 95d27bd1fa5be4e839bd26e105b7d5737142e332 Mon Sep 17 00:00:00 2001 From: cdce8p <30130371+cdce8p@users.noreply.github.com> Date: Sat, 5 May 2018 15:37:40 +0200 Subject: [PATCH 130/155] Sensor device classes (#14282) * Added light device class, moved device classes to const * Removed unnecessary icons * Replace 'lux' with 'lx' * Fix comment * Changed device_class name --- homeassistant/components/homekit/__init__.py | 9 +++-- homeassistant/components/sensor/__init__.py | 10 ++++-- homeassistant/components/sensor/abode.py | 20 ++++++----- homeassistant/components/sensor/bh1750.py | 4 +-- homeassistant/components/sensor/deconz.py | 12 ++----- homeassistant/components/sensor/demo.py | 8 +++-- homeassistant/components/sensor/ecobee.py | 5 +-- homeassistant/components/sensor/homematic.py | 2 +- homeassistant/components/sensor/isy994.py | 2 +- .../components/sensor/linux_battery.py | 11 ++---- homeassistant/components/sensor/miflora.py | 2 +- homeassistant/components/sensor/mysensors.py | 2 +- homeassistant/components/sensor/nest.py | 7 ++-- homeassistant/components/sensor/tahoma.py | 2 +- .../components/sensor/tellduslive.py | 36 ++++++++++++------- homeassistant/components/sensor/vera.py | 2 +- .../components/sensor/xiaomi_aqara.py | 20 +++++++---- homeassistant/const.py | 6 ++++ .../homekit/test_get_accessories.py | 11 ++---- 19 files changed, 92 insertions(+), 79 deletions(-) diff --git a/homeassistant/components/homekit/__init__.py b/homeassistant/components/homekit/__init__.py index 3abfffd67e0..c31093a5eb8 100644 --- a/homeassistant/components/homekit/__init__.py +++ b/homeassistant/components/homekit/__init__.py @@ -14,7 +14,8 @@ from homeassistant.components.cover import ( from homeassistant.const import ( ATTR_SUPPORTED_FEATURES, ATTR_UNIT_OF_MEASUREMENT, ATTR_DEVICE_CLASS, CONF_IP_ADDRESS, CONF_PORT, TEMP_CELSIUS, - TEMP_FAHRENHEIT, EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP) + TEMP_FAHRENHEIT, EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP, + DEVICE_CLASS_HUMIDITY, DEVICE_CLASS_ILLUMINANCE, DEVICE_CLASS_TEMPERATURE) import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entityfilter import FILTER_SCHEMA from homeassistant.util import get_local_ip @@ -22,8 +23,7 @@ from homeassistant.util.decorator import Registry from .const import ( DOMAIN, HOMEKIT_FILE, CONF_AUTO_START, CONF_ENTITY_CONFIG, CONF_FILTER, DEFAULT_PORT, DEFAULT_AUTO_START, SERVICE_HOMEKIT_START, - DEVICE_CLASS_CO2, DEVICE_CLASS_LIGHT, DEVICE_CLASS_HUMIDITY, - DEVICE_CLASS_PM25, DEVICE_CLASS_TEMPERATURE) + DEVICE_CLASS_CO2, DEVICE_CLASS_PM25) from .util import ( validate_entity_config, show_setup_message) @@ -137,8 +137,7 @@ def get_accessory(hass, state, aid, config): elif device_class == DEVICE_CLASS_CO2 \ or DEVICE_CLASS_CO2 in state.entity_id: a_type = 'CarbonDioxideSensor' - elif device_class == DEVICE_CLASS_LIGHT or unit == 'lm' or \ - unit == 'lux' or unit == 'lx': + elif device_class == DEVICE_CLASS_ILLUMINANCE or unit in ('lm', 'lx'): a_type = 'LightSensor' elif state.domain in ('switch', 'remote', 'input_boolean', 'script'): diff --git a/homeassistant/components/sensor/__init__.py b/homeassistant/components/sensor/__init__.py index bed1850b34d..8550d175b63 100644 --- a/homeassistant/components/sensor/__init__.py +++ b/homeassistant/components/sensor/__init__.py @@ -12,6 +12,9 @@ import voluptuous as vol from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.config_validation import PLATFORM_SCHEMA # noqa +from homeassistant.const import ( + DEVICE_CLASS_BATTERY, DEVICE_CLASS_HUMIDITY, DEVICE_CLASS_ILLUMINANCE, + DEVICE_CLASS_TEMPERATURE) _LOGGER = logging.getLogger(__name__) @@ -21,9 +24,10 @@ ENTITY_ID_FORMAT = DOMAIN + '.{}' SCAN_INTERVAL = timedelta(seconds=30) DEVICE_CLASSES = [ - 'battery', # % of battery that is left - 'humidity', # % of humidity in the air - 'temperature', # temperature (C/F) + DEVICE_CLASS_BATTERY, # % of battery that is left + DEVICE_CLASS_HUMIDITY, # % of humidity in the air + DEVICE_CLASS_ILLUMINANCE, # current light level (lx/lm) + DEVICE_CLASS_TEMPERATURE, # temperature (C/F) ] DEVICE_CLASSES_SCHEMA = vol.All(vol.Lower, vol.In(DEVICE_CLASSES)) diff --git a/homeassistant/components/sensor/abode.py b/homeassistant/components/sensor/abode.py index 1a700e24de6..b51ab288c1a 100644 --- a/homeassistant/components/sensor/abode.py +++ b/homeassistant/components/sensor/abode.py @@ -7,6 +7,8 @@ https://home-assistant.io/components/sensor.abode/ import logging from homeassistant.components.abode import AbodeDevice, DOMAIN as ABODE_DOMAIN +from homeassistant.const import ( + DEVICE_CLASS_HUMIDITY, DEVICE_CLASS_ILLUMINANCE, DEVICE_CLASS_TEMPERATURE) _LOGGER = logging.getLogger(__name__) @@ -14,9 +16,9 @@ DEPENDENCIES = ['abode'] # Sensor types: Name, icon SENSOR_TYPES = { - 'temp': ['Temperature', 'thermometer'], - 'humidity': ['Humidity', 'water-percent'], - 'lux': ['Lux', 'lightbulb'], + 'temp': ['Temperature', DEVICE_CLASS_TEMPERATURE], + 'humidity': ['Humidity', DEVICE_CLASS_HUMIDITY], + 'lux': ['Lux', DEVICE_CLASS_ILLUMINANCE], } @@ -46,20 +48,20 @@ class AbodeSensor(AbodeDevice): """Initialize a sensor for an Abode device.""" super().__init__(data, device) self._sensor_type = sensor_type - self._icon = 'mdi:{}'.format(SENSOR_TYPES[self._sensor_type][1]) self._name = '{0} {1}'.format( self._device.name, SENSOR_TYPES[self._sensor_type][0]) - - @property - def icon(self): - """Icon to use in the frontend, if any.""" - return self._icon + self._device_class = SENSOR_TYPES[self._sensor_type][1] @property def name(self): """Return the name of the sensor.""" return self._name + @property + def device_class(self): + """Return the device class.""" + return self._device_class + @property def state(self): """Return the state of the sensor.""" diff --git a/homeassistant/components/sensor/bh1750.py b/homeassistant/components/sensor/bh1750.py index 0c538a6cfcc..6d34d4ea9f8 100644 --- a/homeassistant/components/sensor/bh1750.py +++ b/homeassistant/components/sensor/bh1750.py @@ -12,7 +12,7 @@ import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA import homeassistant.helpers.config_validation as cv -from homeassistant.const import CONF_NAME +from homeassistant.const import CONF_NAME, DEVICE_CLASS_ILLUMINANCE from homeassistant.helpers.entity import Entity REQUIREMENTS = ['i2csense==0.0.4', @@ -130,7 +130,7 @@ class BH1750Sensor(Entity): @property def device_class(self) -> str: """Return the class of this device, from component DEVICE_CLASSES.""" - return 'light' + return DEVICE_CLASS_ILLUMINANCE @asyncio.coroutine def async_update(self): diff --git a/homeassistant/components/sensor/deconz.py b/homeassistant/components/sensor/deconz.py index 69be7f52d6c..b4a3cb8c6c5 100644 --- a/homeassistant/components/sensor/deconz.py +++ b/homeassistant/components/sensor/deconz.py @@ -6,10 +6,10 @@ https://home-assistant.io/components/sensor.deconz/ """ from homeassistant.components.deconz import ( DOMAIN as DATA_DECONZ, DATA_DECONZ_ID) -from homeassistant.const import ATTR_BATTERY_LEVEL, ATTR_VOLTAGE +from homeassistant.const import ( + ATTR_BATTERY_LEVEL, ATTR_VOLTAGE, DEVICE_CLASS_BATTERY) from homeassistant.core import callback from homeassistant.helpers.entity import Entity -from homeassistant.helpers.icon import icon_for_battery_level from homeassistant.util import slugify DEPENDENCIES = ['deconz'] @@ -126,7 +126,6 @@ class DeconzBattery(Entity): """Register dispatcher callback for update of battery state.""" self._device = device self._name = '{} {}'.format(self._device.name, 'Battery Level') - self._device_class = 'battery' self._unit_of_measurement = "%" async def async_added_to_hass(self): @@ -158,12 +157,7 @@ class DeconzBattery(Entity): @property def device_class(self): """Return the class of the sensor.""" - return self._device_class - - @property - def icon(self): - """Return the icon to use in the frontend.""" - return icon_for_battery_level(int(self.state)) + return DEVICE_CLASS_BATTERY @property def unit_of_measurement(self): diff --git a/homeassistant/components/sensor/demo.py b/homeassistant/components/sensor/demo.py index 5cae1a47c23..325d3e0ae58 100644 --- a/homeassistant/components/sensor/demo.py +++ b/homeassistant/components/sensor/demo.py @@ -4,7 +4,9 @@ Demo platform that has a couple of fake sensors. For more details about this platform, please refer to the documentation https://home-assistant.io/components/demo/ """ -from homeassistant.const import ATTR_BATTERY_LEVEL, TEMP_CELSIUS +from homeassistant.const import ( + ATTR_BATTERY_LEVEL, TEMP_CELSIUS, DEVICE_CLASS_HUMIDITY, + DEVICE_CLASS_TEMPERATURE) from homeassistant.helpers.entity import Entity @@ -12,9 +14,9 @@ from homeassistant.helpers.entity import Entity def setup_platform(hass, config, add_devices, discovery_info=None): """Set up the Demo sensors.""" add_devices([ - DemoSensor('Outside Temperature', 15.6, 'temperature', + DemoSensor('Outside Temperature', 15.6, DEVICE_CLASS_TEMPERATURE, TEMP_CELSIUS, 12), - DemoSensor('Outside Humidity', 54, 'humidity', '%', None), + DemoSensor('Outside Humidity', 54, DEVICE_CLASS_HUMIDITY, '%', None), ]) diff --git a/homeassistant/components/sensor/ecobee.py b/homeassistant/components/sensor/ecobee.py index 7274f421f15..a478f964f5a 100644 --- a/homeassistant/components/sensor/ecobee.py +++ b/homeassistant/components/sensor/ecobee.py @@ -5,7 +5,8 @@ For more details about this platform, please refer to the documentation at https://home-assistant.io/components/sensor.ecobee/ """ from homeassistant.components import ecobee -from homeassistant.const import TEMP_FAHRENHEIT +from homeassistant.const import ( + DEVICE_CLASS_HUMIDITY, DEVICE_CLASS_TEMPERATURE, TEMP_FAHRENHEIT) from homeassistant.helpers.entity import Entity DEPENDENCIES = ['ecobee'] @@ -55,7 +56,7 @@ class EcobeeSensor(Entity): @property def device_class(self): """Return the device class of the sensor.""" - if self.type in ('temperature', 'humidity'): + if self.type in (DEVICE_CLASS_HUMIDITY, DEVICE_CLASS_TEMPERATURE): return self.type return None diff --git a/homeassistant/components/sensor/homematic.py b/homeassistant/components/sensor/homematic.py index 350f1e2eb59..bdbc207a79c 100644 --- a/homeassistant/components/sensor/homematic.py +++ b/homeassistant/components/sensor/homematic.py @@ -43,7 +43,7 @@ HM_UNIT_HA_CAST = { 'ENERGY_COUNTER': 'Wh', 'GAS_POWER': 'm3', 'GAS_ENERGY_COUNTER': 'm3', - 'LUX': 'lux', + 'LUX': 'lx', 'RAIN_COUNTER': 'mm', 'WIND_SPEED': 'km/h', 'WIND_DIRECTION': '°', diff --git a/homeassistant/components/sensor/isy994.py b/homeassistant/components/sensor/isy994.py index c34a4a8fca7..ecf7bc0b8c2 100644 --- a/homeassistant/components/sensor/isy994.py +++ b/homeassistant/components/sensor/isy994.py @@ -49,7 +49,7 @@ UOM_FRIENDLY_NAME = { '33': 'kWH', '34': 'liedu', '35': 'l', - '36': 'lux', + '36': 'lx', '37': 'mercalli', '38': 'm', '39': 'm³/hr', diff --git a/homeassistant/components/sensor/linux_battery.py b/homeassistant/components/sensor/linux_battery.py index 1f0e3e89e5c..aad8c2f7a92 100644 --- a/homeassistant/components/sensor/linux_battery.py +++ b/homeassistant/components/sensor/linux_battery.py @@ -10,7 +10,7 @@ import os import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA -from homeassistant.const import CONF_NAME +from homeassistant.const import CONF_NAME, DEVICE_CLASS_BATTERY from homeassistant.helpers.entity import Entity import homeassistant.helpers.config_validation as cv @@ -48,8 +48,6 @@ DEFAULT_SYSTEM = 'linux' SYSTEMS = ['android', 'linux'] -ICON = 'mdi:battery' - PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Optional(CONF_BATTERY, default=DEFAULT_BATTERY): cv.positive_int, vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, @@ -97,7 +95,7 @@ class LinuxBatterySensor(Entity): @property def device_class(self): """Return the device class of the sensor.""" - return 'battery' + return DEVICE_CLASS_BATTERY @property def state(self): @@ -109,11 +107,6 @@ class LinuxBatterySensor(Entity): """Return the unit the value is expressed in.""" return self._unit_of_measurement - @property - def icon(self): - """Return the icon to use in the frontend, if any.""" - return ICON - @property def device_state_attributes(self): """Return the state attributes of the sensor.""" diff --git a/homeassistant/components/sensor/miflora.py b/homeassistant/components/sensor/miflora.py index 98cc7731d4d..f1f8adab062 100644 --- a/homeassistant/components/sensor/miflora.py +++ b/homeassistant/components/sensor/miflora.py @@ -38,7 +38,7 @@ DEFAULT_TIMEOUT = 10 # Sensor types are defined like: Name, units SENSOR_TYPES = { 'temperature': ['Temperature', '°C'], - 'light': ['Light intensity', 'lux'], + 'light': ['Light intensity', 'lx'], 'moisture': ['Moisture', '%'], 'conductivity': ['Conductivity', 'µS/cm'], 'battery': ['Battery', '%'], diff --git a/homeassistant/components/sensor/mysensors.py b/homeassistant/components/sensor/mysensors.py index 669ef3998de..1add4157f0e 100644 --- a/homeassistant/components/sensor/mysensors.py +++ b/homeassistant/components/sensor/mysensors.py @@ -26,7 +26,7 @@ SENSORS = { 'V_PERCENTAGE': ['%', 'mdi:percent'], 'V_LEVEL': { 'S_SOUND': ['dB', 'mdi:volume-high'], 'S_VIBRATION': ['Hz', None], - 'S_LIGHT_LEVEL': ['lux', 'white-balance-sunny']}, + 'S_LIGHT_LEVEL': ['lx', 'white-balance-sunny']}, 'V_ORP': ['mV', None], 'V_EC': ['μS/cm', None], 'V_VAR': ['var', None], diff --git a/homeassistant/components/sensor/nest.py b/homeassistant/components/sensor/nest.py index 5ee4f738051..9ce50dc61e5 100644 --- a/homeassistant/components/sensor/nest.py +++ b/homeassistant/components/sensor/nest.py @@ -9,8 +9,9 @@ import logging from homeassistant.components.nest import DATA_NEST from homeassistant.helpers.entity import Entity -from homeassistant.const import (TEMP_CELSIUS, TEMP_FAHRENHEIT, - CONF_MONITORED_CONDITIONS) +from homeassistant.const import ( + TEMP_CELSIUS, TEMP_FAHRENHEIT, CONF_MONITORED_CONDITIONS, + DEVICE_CLASS_TEMPERATURE) DEPENDENCIES = ['nest'] SENSOR_TYPES = ['humidity', @@ -143,7 +144,7 @@ class NestTempSensor(NestSensor): @property def device_class(self): """Return the device class of the sensor.""" - return 'temperature' + return DEVICE_CLASS_TEMPERATURE def update(self): """Retrieve latest state.""" diff --git a/homeassistant/components/sensor/tahoma.py b/homeassistant/components/sensor/tahoma.py index cafa942f65b..aedecfe61e5 100644 --- a/homeassistant/components/sensor/tahoma.py +++ b/homeassistant/components/sensor/tahoma.py @@ -49,7 +49,7 @@ class TahomaSensor(TahomaDevice, Entity): elif self.tahoma_device.type == 'io:SomfyContactIOSystemSensor': return None elif self.tahoma_device.type == 'io:LightIOSystemSensor': - return 'lux' + return 'lx' elif self.tahoma_device.type == 'Humidity Sensor': return '%' diff --git a/homeassistant/components/sensor/tellduslive.py b/homeassistant/components/sensor/tellduslive.py index 61a084c6266..048ca988e3d 100644 --- a/homeassistant/components/sensor/tellduslive.py +++ b/homeassistant/components/sensor/tellduslive.py @@ -7,7 +7,9 @@ https://home-assistant.io/components/sensor.tellduslive/ import logging from homeassistant.components.tellduslive import TelldusLiveEntity -from homeassistant.const import TEMP_CELSIUS +from homeassistant.const import ( + DEVICE_CLASS_HUMIDITY, DEVICE_CLASS_ILLUMINANCE, DEVICE_CLASS_TEMPERATURE, + TEMP_CELSIUS) _LOGGER = logging.getLogger(__name__) @@ -25,18 +27,20 @@ SENSOR_TYPE_DEW_POINT = 'dewp' SENSOR_TYPE_BAROMETRIC_PRESSURE = 'barpress' SENSOR_TYPES = { - SENSOR_TYPE_TEMPERATURE: ['Temperature', TEMP_CELSIUS, 'mdi:thermometer'], - SENSOR_TYPE_HUMIDITY: ['Humidity', '%', 'mdi:water'], - SENSOR_TYPE_RAINRATE: ['Rain rate', 'mm/h', 'mdi:water'], - SENSOR_TYPE_RAINTOTAL: ['Rain total', 'mm', 'mdi:water'], - SENSOR_TYPE_WINDDIRECTION: ['Wind direction', '', ''], - SENSOR_TYPE_WINDAVERAGE: ['Wind average', 'm/s', ''], - SENSOR_TYPE_WINDGUST: ['Wind gust', 'm/s', ''], - SENSOR_TYPE_UV: ['UV', 'UV', ''], - SENSOR_TYPE_WATT: ['Power', 'W', ''], - SENSOR_TYPE_LUMINANCE: ['Luminance', 'lx', ''], - SENSOR_TYPE_DEW_POINT: ['Dew Point', TEMP_CELSIUS, 'mdi:thermometer'], - SENSOR_TYPE_BAROMETRIC_PRESSURE: ['Barometric Pressure', 'kPa', ''], + SENSOR_TYPE_TEMPERATURE: ['Temperature', TEMP_CELSIUS, None, + DEVICE_CLASS_TEMPERATURE], + SENSOR_TYPE_HUMIDITY: ['Humidity', '%', None, DEVICE_CLASS_HUMIDITY], + SENSOR_TYPE_RAINRATE: ['Rain rate', 'mm/h', 'mdi:water', None], + SENSOR_TYPE_RAINTOTAL: ['Rain total', 'mm', 'mdi:water', None], + SENSOR_TYPE_WINDDIRECTION: ['Wind direction', '', '', None], + SENSOR_TYPE_WINDAVERAGE: ['Wind average', 'm/s', '', None], + SENSOR_TYPE_WINDGUST: ['Wind gust', 'm/s', '', None], + SENSOR_TYPE_UV: ['UV', 'UV', '', None], + SENSOR_TYPE_WATT: ['Power', 'W', '', None], + SENSOR_TYPE_LUMINANCE: ['Luminance', 'lx', None, DEVICE_CLASS_ILLUMINANCE], + SENSOR_TYPE_DEW_POINT: + ['Dew Point', TEMP_CELSIUS, None, DEVICE_CLASS_TEMPERATURE], + SENSOR_TYPE_BAROMETRIC_PRESSURE: ['Barometric Pressure', 'kPa', '', None], } @@ -117,3 +121,9 @@ class TelldusLiveSensor(TelldusLiveEntity): """Return the icon.""" return SENSOR_TYPES[self._type][2] \ if self._type in SENSOR_TYPES else None + + @property + def device_class(self): + """Return the device class.""" + return SENSOR_TYPES[self._type][3] \ + if self._type in SENSOR_TYPES else None diff --git a/homeassistant/components/sensor/vera.py b/homeassistant/components/sensor/vera.py index c81c208e33e..eb8ccae768e 100644 --- a/homeassistant/components/sensor/vera.py +++ b/homeassistant/components/sensor/vera.py @@ -52,7 +52,7 @@ class VeraSensor(VeraDevice, Entity): if self.vera_device.category == veraApi.CATEGORY_TEMPERATURE_SENSOR: return self._temperature_units elif self.vera_device.category == veraApi.CATEGORY_LIGHT_SENSOR: - return 'lux' + return 'lx' elif self.vera_device.category == veraApi.CATEGORY_UV_SENSOR: return 'level' elif self.vera_device.category == veraApi.CATEGORY_HUMIDITY_SENSOR: diff --git a/homeassistant/components/sensor/xiaomi_aqara.py b/homeassistant/components/sensor/xiaomi_aqara.py index 497a3915154..3192d0d2f60 100644 --- a/homeassistant/components/sensor/xiaomi_aqara.py +++ b/homeassistant/components/sensor/xiaomi_aqara.py @@ -3,16 +3,18 @@ import logging from homeassistant.components.xiaomi_aqara import (PY_XIAOMI_GATEWAY, XiaomiDevice) -from homeassistant.const import TEMP_CELSIUS +from homeassistant.const import ( + DEVICE_CLASS_HUMIDITY, DEVICE_CLASS_ILLUMINANCE, DEVICE_CLASS_TEMPERATURE, + TEMP_CELSIUS) _LOGGER = logging.getLogger(__name__) SENSOR_TYPES = { - 'temperature': [TEMP_CELSIUS, 'mdi:thermometer'], - 'humidity': ['%', 'mdi:water-percent'], - 'illumination': ['lm', 'mdi:weather-sunset'], - 'lux': ['lx', 'mdi:weather-sunset'], - 'pressure': ['hPa', 'mdi:gauge'] + 'temperature': [TEMP_CELSIUS, None, DEVICE_CLASS_TEMPERATURE], + 'humidity': ['%', None, DEVICE_CLASS_HUMIDITY], + 'illumination': ['lm', None, DEVICE_CLASS_ILLUMINANCE], + 'lux': ['lx', None, DEVICE_CLASS_ILLUMINANCE], + 'pressure': ['hPa', 'mdi:gauge', None] } @@ -66,6 +68,12 @@ class XiaomiSensor(XiaomiDevice): except TypeError: return None + @property + def device_class(self): + """Return the device class of this entity.""" + return SENSOR_TYPES.get(self._data_key)[2] \ + if self._data_key in SENSOR_TYPES else None + @property def state(self): """Return the state of the sensor.""" diff --git a/homeassistant/const.py b/homeassistant/const.py index 2e96e2f29c0..52a50ba9607 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -166,6 +166,12 @@ EVENT_SERVICE_REMOVED = 'service_removed' EVENT_LOGBOOK_ENTRY = 'logbook_entry' EVENT_THEMES_UPDATED = 'themes_updated' +# #### DEVICE CLASSES #### +DEVICE_CLASS_BATTERY = 'battery' +DEVICE_CLASS_HUMIDITY = 'humidity' +DEVICE_CLASS_ILLUMINANCE = 'illuminance' +DEVICE_CLASS_TEMPERATURE = 'temperature' + # #### STATES #### STATE_ON = 'on' STATE_OFF = 'off' diff --git a/tests/components/homekit/test_get_accessories.py b/tests/components/homekit/test_get_accessories.py index 71f9c8e6656..cff52b2ff20 100644 --- a/tests/components/homekit/test_get_accessories.py +++ b/tests/components/homekit/test_get_accessories.py @@ -99,10 +99,10 @@ class TestGetAccessories(unittest.TestCase): get_accessory(None, state, 2, {}) def test_light_sensor(self): - """Test light sensor with device class lux.""" + """Test light sensor with device class illuminance.""" with patch.dict(TYPES, {'LightSensor': self.mock_type}): state = State('sensor.light', '900', - {ATTR_DEVICE_CLASS: 'light'}) + {ATTR_DEVICE_CLASS: 'illuminance'}) get_accessory(None, state, 2, {}) def test_light_sensor_unit_lm(self): @@ -112,13 +112,6 @@ class TestGetAccessories(unittest.TestCase): {ATTR_UNIT_OF_MEASUREMENT: 'lm'}) get_accessory(None, state, 2, {}) - def test_light_sensor_unit_lux(self): - """Test light sensor with lux as unit.""" - with patch.dict(TYPES, {'LightSensor': self.mock_type}): - state = State('sensor.light', '900', - {ATTR_UNIT_OF_MEASUREMENT: 'lux'}) - get_accessory(None, state, 2, {}) - def test_light_sensor_unit_lx(self): """Test light sensor with lx as unit.""" with patch.dict(TYPES, {'LightSensor': self.mock_type}): From af8cd63838d035ad80420f4296cdbc312fe407ca Mon Sep 17 00:00:00 2001 From: Lukas Barth Date: Sat, 5 May 2018 16:00:36 +0200 Subject: [PATCH 131/155] Matrix Chatbot (#13355) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Add first version of the Matrix bot * It's a stupid but necessary change… * Dont list it twice * All hail the linter! * More linter-pleasing * Use the correct user ID * Add expression commands * Add tests for new validators * Fix room alias handling * Wording * Defer setup * Simplify commands * Handle exceptions * Update requirements * Review * Move login back to constructor * Fix review comments --- .coveragerc | 4 +- CODEOWNERS | 2 + homeassistant/components/matrix.py | 351 +++++++++++++++++++++ homeassistant/components/notify/matrix.py | 169 +--------- homeassistant/helpers/config_validation.py | 30 ++ requirements_all.txt | 4 +- tests/helpers/test_config_validation.py | 28 ++ 7 files changed, 433 insertions(+), 155 deletions(-) create mode 100644 homeassistant/components/matrix.py diff --git a/.coveragerc b/.coveragerc index cf7a5a2cd9c..d2192ca2e46 100644 --- a/.coveragerc +++ b/.coveragerc @@ -166,6 +166,9 @@ omit = homeassistant/components/mailgun.py homeassistant/components/*/mailgun.py + homeassistant/components/matrix.py + homeassistant/components/*/matrix.py + homeassistant/components/maxcube.py homeassistant/components/*/maxcube.py @@ -519,7 +522,6 @@ omit = homeassistant/components/notify/lannouncer.py homeassistant/components/notify/llamalab_automate.py homeassistant/components/notify/mastodon.py - homeassistant/components/notify/matrix.py homeassistant/components/notify/message_bird.py homeassistant/components/notify/mycroft.py homeassistant/components/notify/nfandroidtv.py diff --git a/CODEOWNERS b/CODEOWNERS index a62ed67db66..33966d1badb 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -94,6 +94,8 @@ homeassistant/components/*/hive.py @Rendili @KJonline homeassistant/components/homekit/* @cdce8p homeassistant/components/knx.py @Julius2342 homeassistant/components/*/knx.py @Julius2342 +homeassistant/components/matrix.py @tinloaf +homeassistant/components/*/matrix.py @tinloaf homeassistant/components/qwikswitch.py @kellerza homeassistant/components/*/qwikswitch.py @kellerza homeassistant/components/*/rfxtrx.py @danielhiversen diff --git a/homeassistant/components/matrix.py b/homeassistant/components/matrix.py new file mode 100644 index 00000000000..569b012b484 --- /dev/null +++ b/homeassistant/components/matrix.py @@ -0,0 +1,351 @@ +""" +The matrix bot component. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/matrix/ +""" +import logging +import os +from functools import partial + +import voluptuous as vol + +import homeassistant.helpers.config_validation as cv +from homeassistant.components.notify import (ATTR_TARGET, ATTR_MESSAGE) +from homeassistant.const import (CONF_USERNAME, CONF_PASSWORD, + CONF_VERIFY_SSL, CONF_NAME, + EVENT_HOMEASSISTANT_STOP, + EVENT_HOMEASSISTANT_START) +from homeassistant.util.json import load_json, save_json +from homeassistant.exceptions import HomeAssistantError + +REQUIREMENTS = ['matrix-client==0.2.0'] + +_LOGGER = logging.getLogger(__name__) + +SESSION_FILE = '.matrix.conf' + +CONF_HOMESERVER = 'homeserver' +CONF_ROOMS = 'rooms' +CONF_COMMANDS = 'commands' +CONF_WORD = 'word' +CONF_EXPRESSION = 'expression' + +EVENT_MATRIX_COMMAND = 'matrix_command' + +DOMAIN = 'matrix' + +COMMAND_SCHEMA = vol.All( + # Basic Schema + vol.Schema({ + vol.Exclusive(CONF_WORD, 'trigger'): cv.string, + vol.Exclusive(CONF_EXPRESSION, 'trigger'): cv.is_regex, + vol.Required(CONF_NAME): cv.string, + vol.Optional(CONF_ROOMS, default=[]): vol.All(cv.ensure_list, + [cv.string]), + }), + # Make sure it's either a word or an expression command + cv.has_at_least_one_key(CONF_WORD, CONF_EXPRESSION) +) + +CONFIG_SCHEMA = vol.Schema({ + DOMAIN: vol.Schema({ + vol.Required(CONF_HOMESERVER): cv.url, + vol.Optional(CONF_VERIFY_SSL, default=True): cv.boolean, + vol.Required(CONF_USERNAME): cv.matches_regex("@[^:]*:.*"), + vol.Required(CONF_PASSWORD): cv.string, + vol.Optional(CONF_ROOMS, default=[]): vol.All(cv.ensure_list, + [cv.string]), + vol.Optional(CONF_COMMANDS, default=[]): [COMMAND_SCHEMA] + }) +}, extra=vol.ALLOW_EXTRA) + +SERVICE_SEND_MESSAGE = 'send_message' + +SERVICE_SCHEMA_SEND_MESSAGE = vol.Schema({ + vol.Required(ATTR_MESSAGE): cv.string, + vol.Required(ATTR_TARGET): vol.All(cv.ensure_list, [cv.string]), +}) + + +def setup(hass, config): + """Set up the Matrix bot component.""" + from matrix_client.client import MatrixRequestError + + config = config[DOMAIN] + + try: + bot = MatrixBot( + hass, + os.path.join(hass.config.path(), SESSION_FILE), + config[CONF_HOMESERVER], + config[CONF_VERIFY_SSL], + config[CONF_USERNAME], + config[CONF_PASSWORD], + config[CONF_ROOMS], + config[CONF_COMMANDS]) + hass.data[DOMAIN] = bot + except MatrixRequestError as exception: + _LOGGER.error("Matrix failed to log in: %s", str(exception)) + return False + + hass.services.register( + DOMAIN, SERVICE_SEND_MESSAGE, bot.handle_send_message, + schema=SERVICE_SCHEMA_SEND_MESSAGE) + + return True + + +class MatrixBot(object): + """The Matrix Bot.""" + + def __init__(self, hass, config_file, homeserver, verify_ssl, + username, password, listening_rooms, commands): + """Set up the client.""" + self.hass = hass + + self._session_filepath = config_file + self._auth_tokens = self._get_auth_tokens() + + self._homeserver = homeserver + self._verify_tls = verify_ssl + self._mx_id = username + self._password = password + + self._listening_rooms = listening_rooms + + # Logging in is deferred b/c it does I/O + self._setup_done = False + + # We have to fetch the aliases for every room to make sure we don't + # join it twice by accident. However, fetching aliases is costly, + # so we only do it once per room. + self._aliases_fetched_for = set() + + # word commands are stored dict-of-dict: First dict indexes by room ID + # / alias, second dict indexes by the word + self._word_commands = {} + + # regular expression commands are stored as a list of commands per + # room, i.e., a dict-of-list + self._expression_commands = {} + + for command in commands: + if not command.get(CONF_ROOMS): + command[CONF_ROOMS] = listening_rooms + + if command.get(CONF_WORD): + for room_id in command[CONF_ROOMS]: + if room_id not in self._word_commands: + self._word_commands[room_id] = {} + self._word_commands[room_id][command[CONF_WORD]] = command + else: + for room_id in command[CONF_ROOMS]: + if room_id not in self._expression_commands: + self._expression_commands[room_id] = [] + self._expression_commands[room_id].append(command) + + # Log in. This raises a MatrixRequestError if login is unsuccessful + self._client = self._login() + + def handle_matrix_exception(exception): + """Handle exceptions raised inside the Matrix SDK.""" + _LOGGER.error("Matrix exception:\n %s", str(exception)) + + self._client.start_listener_thread( + exception_handler=handle_matrix_exception) + + def stop_client(_): + """Run once when Home Assistant stops.""" + self._client.stop_listener_thread() + + self.hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, stop_client) + + # Joining rooms potentially does a lot of I/O, so we defer it + def handle_startup(_): + """Run once when Home Assistant finished startup.""" + self._join_rooms() + + self.hass.bus.listen_once(EVENT_HOMEASSISTANT_START, handle_startup) + + def _handle_room_message(self, room_id, room, event): + """Handle a message sent to a room.""" + if event['content']['msgtype'] != 'm.text': + return + + if event['sender'] == self._mx_id: + return + + _LOGGER.debug("Handling message: %s", event['content']['body']) + + if event['content']['body'][0] == "!": + # Could trigger a single-word command. + pieces = event['content']['body'].split(' ') + cmd = pieces[0][1:] + + command = self._word_commands.get(room_id, {}).get(cmd) + if command: + event_data = { + 'command': command[CONF_NAME], + 'sender': event['sender'], + 'room': room_id, + 'args': pieces[1:] + } + self.hass.bus.fire(EVENT_MATRIX_COMMAND, event_data) + + # After single-word commands, check all regex commands in the room + for command in self._expression_commands.get(room_id, []): + match = command[CONF_EXPRESSION].match(event['content']['body']) + if not match: + continue + event_data = { + 'command': command[CONF_NAME], + 'sender': event['sender'], + 'room': room_id, + 'args': match.groupdict() + } + self.hass.bus.fire(EVENT_MATRIX_COMMAND, event_data) + + def _join_or_get_room(self, room_id_or_alias): + """Join a room or get it, if we are already in the room. + + We can't just always call join_room(), since that seems to crash + the client if we're already in the room. + """ + rooms = self._client.get_rooms() + if room_id_or_alias in rooms: + _LOGGER.debug("Already in room %s", room_id_or_alias) + return rooms[room_id_or_alias] + + for room in rooms.values(): + if room.room_id not in self._aliases_fetched_for: + room.update_aliases() + self._aliases_fetched_for.add(room.room_id) + + if room_id_or_alias in room.aliases: + _LOGGER.debug("Already in room %s (known as %s)", + room.room_id, room_id_or_alias) + return room + + room = self._client.join_room(room_id_or_alias) + _LOGGER.info("Joined room %s (known as %s)", room.room_id, + room_id_or_alias) + return room + + def _join_rooms(self): + """Join the rooms that we listen for commands in.""" + from matrix_client.client import MatrixRequestError + + for room_id in self._listening_rooms: + try: + room = self._join_or_get_room(room_id) + room.add_listener(partial(self._handle_room_message, room_id), + "m.room.message") + + except MatrixRequestError as ex: + _LOGGER.error("Could not join room %s: %s", room_id, ex) + + def _get_auth_tokens(self): + """ + Read sorted authentication tokens from disk. + + Returns the auth_tokens dictionary. + """ + try: + auth_tokens = load_json(self._session_filepath) + + return auth_tokens + except HomeAssistantError as ex: + _LOGGER.warning( + "Loading authentication tokens from file '%s' failed: %s", + self._session_filepath, str(ex)) + return {} + + def _store_auth_token(self, token): + """Store authentication token to session and persistent storage.""" + self._auth_tokens[self._mx_id] = token + + save_json(self._session_filepath, self._auth_tokens) + + def _login(self): + """Login to the matrix homeserver and return the client instance.""" + from matrix_client.client import MatrixRequestError + + # Attempt to generate a valid client using either of the two possible + # login methods: + client = None + + # If we have an authentication token + if self._mx_id in self._auth_tokens: + try: + client = self._login_by_token() + _LOGGER.debug("Logged in using stored token.") + + except MatrixRequestError as ex: + _LOGGER.warning( + "Login by token failed, falling back to password. " + "login_by_token raised: (%d) %s", + ex.code, ex.content) + + # If we still don't have a client try password. + if not client: + try: + client = self._login_by_password() + _LOGGER.debug("Logged in using password.") + + except MatrixRequestError as ex: + _LOGGER.error( + "Login failed, both token and username/password invalid " + "login_by_password raised: (%d) %s", + ex.code, ex.content) + + # re-raise the error so _setup can catch it. + raise + + return client + + def _login_by_token(self): + """Login using authentication token and return the client.""" + from matrix_client.client import MatrixClient + + return MatrixClient( + base_url=self._homeserver, + token=self._auth_tokens[self._mx_id], + user_id=self._mx_id, + valid_cert_check=self._verify_tls) + + def _login_by_password(self): + """Login using password authentication and return the client.""" + from matrix_client.client import MatrixClient + + _client = MatrixClient( + base_url=self._homeserver, + valid_cert_check=self._verify_tls) + + _client.login_with_password(self._mx_id, self._password) + + self._store_auth_token(_client.token) + + return _client + + def _send_message(self, message, target_rooms): + """Send the message to the matrix server.""" + from matrix_client.client import MatrixRequestError + + for target_room in target_rooms: + try: + room = self._join_or_get_room(target_room) + _LOGGER.debug(room.send_text(message)) + except MatrixRequestError as ex: + _LOGGER.error( + "Unable to deliver message to room '%s': (%d): %s", + target_room, ex.code, ex.content) + + def handle_send_message(self, service): + """Handle the send_message service.""" + if not self._setup_done: + _LOGGER.warning("Could not send message: setup is not done!") + return + + self._send_message(service.data[ATTR_MESSAGE], + service.data[ATTR_TARGET]) diff --git a/homeassistant/components/notify/matrix.py b/homeassistant/components/notify/matrix.py index 03bc53e204c..fc29ad91dc9 100644 --- a/homeassistant/components/notify/matrix.py +++ b/homeassistant/components/notify/matrix.py @@ -5,181 +5,46 @@ For more details about this platform, please refer to the documentation at https://home-assistant.io/components/notify.matrix/ """ import logging -import os -from urllib.parse import urlparse import voluptuous as vol import homeassistant.helpers.config_validation as cv from homeassistant.components.notify import (ATTR_TARGET, PLATFORM_SCHEMA, - BaseNotificationService) -from homeassistant.const import CONF_USERNAME, CONF_PASSWORD, CONF_VERIFY_SSL -from homeassistant.util.json import load_json, save_json - -REQUIREMENTS = ['matrix-client==0.0.6'] + BaseNotificationService, + ATTR_MESSAGE) _LOGGER = logging.getLogger(__name__) -SESSION_FILE = 'matrix.conf' - -CONF_HOMESERVER = 'homeserver' CONF_DEFAULT_ROOM = 'default_room' +DOMAIN = 'matrix' +DEPENDENCIES = [DOMAIN] + PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Required(CONF_HOMESERVER): cv.url, - vol.Optional(CONF_VERIFY_SSL, default=True): cv.boolean, - vol.Required(CONF_USERNAME): cv.string, - vol.Required(CONF_PASSWORD): cv.string, vol.Required(CONF_DEFAULT_ROOM): cv.string, }) def get_service(hass, config, discovery_info=None): """Get the Matrix notification service.""" - from matrix_client.client import MatrixRequestError - - try: - return MatrixNotificationService( - os.path.join(hass.config.path(), SESSION_FILE), - config.get(CONF_HOMESERVER), - config.get(CONF_DEFAULT_ROOM), - config.get(CONF_VERIFY_SSL), - config.get(CONF_USERNAME), - config.get(CONF_PASSWORD)) - - except MatrixRequestError: - return None + return MatrixNotificationService(config.get(CONF_DEFAULT_ROOM)) class MatrixNotificationService(BaseNotificationService): """Send Notifications to a Matrix Room.""" - def __init__(self, config_file, homeserver, default_room, verify_ssl, - username, password): - """Set up the client.""" - self.session_filepath = config_file - self.auth_tokens = self.get_auth_tokens() + def __init__(self, default_room): + """Set up the notification service.""" + self._default_room = default_room - self.homeserver = homeserver - self.default_room = default_room - self.verify_tls = verify_ssl - self.username = username - self.password = password - - self.mx_id = "{user}@{homeserver}".format( - user=username, homeserver=urlparse(homeserver).netloc) - - # Login, this will raise a MatrixRequestError if login is unsuccessful - self.client = self.login() - - def get_auth_tokens(self): - """ - Read sorted authentication tokens from disk. - - Returns the auth_tokens dictionary. - """ - if not os.path.exists(self.session_filepath): - return {} - - try: - data = load_json(self.session_filepath) - - auth_tokens = {} - for mx_id, token in data.items(): - auth_tokens[mx_id] = token - - return auth_tokens - - except (OSError, IOError, PermissionError) as ex: - _LOGGER.warning( - "Loading authentication tokens from file '%s' failed: %s", - self.session_filepath, str(ex)) - return {} - - def store_auth_token(self, token): - """Store authentication token to session and persistent storage.""" - self.auth_tokens[self.mx_id] = token - - save_json(self.session_filepath, self.auth_tokens) - - def login(self): - """Login to the matrix homeserver and return the client instance.""" - from matrix_client.client import MatrixRequestError - - # Attempt to generate a valid client using either of the two possible - # login methods: - client = None - - # If we have an authentication token - if self.mx_id in self.auth_tokens: - try: - client = self.login_by_token() - _LOGGER.debug("Logged in using stored token.") - - except MatrixRequestError as ex: - _LOGGER.warning( - "Login by token failed, falling back to password. " - "login_by_token raised: (%d) %s", - ex.code, ex.content) - - # If we still don't have a client try password. - if not client: - try: - client = self.login_by_password() - _LOGGER.debug("Logged in using password.") - - except MatrixRequestError as ex: - _LOGGER.error( - "Login failed, both token and username/password invalid " - "login_by_password raised: (%d) %s", - ex.code, ex.content) - - # re-raise the error so the constructor can catch it. - raise - - return client - - def login_by_token(self): - """Login using authentication token and return the client.""" - from matrix_client.client import MatrixClient - - return MatrixClient( - base_url=self.homeserver, - token=self.auth_tokens[self.mx_id], - user_id=self.username, - valid_cert_check=self.verify_tls) - - def login_by_password(self): - """Login using password authentication and return the client.""" - from matrix_client.client import MatrixClient - - _client = MatrixClient( - base_url=self.homeserver, - valid_cert_check=self.verify_tls) - - _client.login_with_password(self.username, self.password) - - self.store_auth_token(_client.token) - - return _client - - def send_message(self, message, **kwargs): + def send_message(self, message="", **kwargs): """Send the message to the matrix server.""" - from matrix_client.client import MatrixRequestError + target_rooms = kwargs.get(ATTR_TARGET) or [self._default_room] - target_rooms = kwargs.get(ATTR_TARGET) or [self.default_room] + service_data = { + ATTR_TARGET: target_rooms, + ATTR_MESSAGE: message + } - rooms = self.client.get_rooms() - for target_room in target_rooms: - try: - if target_room in rooms: - room = rooms[target_room] - else: - room = self.client.join_room(target_room) - - _LOGGER.debug(room.send_text(message)) - - except MatrixRequestError as ex: - _LOGGER.error( - "Unable to deliver message to room '%s': (%d): %s", - target_room, ex.code, ex.content) + return self.hass.services.call( + DOMAIN, 'send_message', service_data=service_data) diff --git a/homeassistant/helpers/config_validation.py b/homeassistant/helpers/config_validation.py index 8177999cc94..0bd490940a9 100644 --- a/homeassistant/helpers/config_validation.py +++ b/homeassistant/helpers/config_validation.py @@ -96,6 +96,36 @@ def isdevice(value): raise vol.Invalid('No device at {} found'.format(value)) +def matches_regex(regex): + """Validate that the value is a string that matches a regex.""" + regex = re.compile(regex) + + def validator(value: Any) -> str: + """Validate that value matches the given regex.""" + if not isinstance(value, str): + raise vol.Invalid('not a string value: {}'.format(value)) + + if not regex.match(value): + raise vol.Invalid('value {} does not match regular expression {}' + .format(regex.pattern, value)) + + return value + return validator + + +def is_regex(value): + """Validate that a string is a valid regular expression.""" + try: + r = re.compile(value) + return r + except TypeError: + raise vol.Invalid("value {} is of the wrong type for a regular " + "expression".format(value)) + except re.error: + raise vol.Invalid("value {} is not a valid regular expression".format( + value)) + + def isfile(value: Any) -> str: """Validate that the value is an existing file.""" if value is None: diff --git a/requirements_all.txt b/requirements_all.txt index 88dd3d60904..74f9ff8e195 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -511,8 +511,8 @@ luftdaten==0.1.3 # homeassistant.components.sensor.lyft lyft_rides==0.2 -# homeassistant.components.notify.matrix -matrix-client==0.0.6 +# homeassistant.components.matrix +matrix-client==0.2.0 # homeassistant.components.maxcube maxcube-api==0.1.0 diff --git a/tests/helpers/test_config_validation.py b/tests/helpers/test_config_validation.py index aff0acf9e3a..28efcb3e868 100644 --- a/tests/helpers/test_config_validation.py +++ b/tests/helpers/test_config_validation.py @@ -565,3 +565,31 @@ def test_socket_timeout(): # pylint: disable=invalid-name assert _GLOBAL_DEFAULT_TIMEOUT == schema(None) assert schema(1) == 1.0 + + +def test_matches_regex(): + """Test matches_regex validator.""" + schema = vol.Schema(cv.matches_regex('.*uiae.*')) + + with pytest.raises(vol.Invalid): + schema(1.0) + + with pytest.raises(vol.Invalid): + schema(" nrtd ") + + test_str = "This is a test including uiae." + assert(schema(test_str) == test_str) + + +def test_is_regex(): + """Test the is_regex validator.""" + schema = vol.Schema(cv.is_regex) + + with pytest.raises(vol.Invalid): + schema("(") + + with pytest.raises(vol.Invalid): + schema({"a dict": "is not a regex"}) + + valid_re = ".*" + schema(valid_re) From 8410b63d9c3191bb48df7b6fc9116453637c4138 Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Sat, 5 May 2018 16:11:00 +0200 Subject: [PATCH 132/155] deCONZ add new device without restart (#14221) * Add new device without restarting hass * Remove debug prints * Fix copy paste error * Fix comments from balloob Add tests to verify signalling with new added devices * Fix hound comments Add test to verify when new sensor is added * Fix tests * Unload entry should unsubscribe all deconz dispatchers * Make sure mock setup also creates unsub in hass data * Fix copy paste issue * Lint --- .../components/binary_sensor/deconz.py | 22 ++++++--- homeassistant/components/deconz/__init__.py | 37 +++++++++++--- homeassistant/components/deconz/const.py | 1 + homeassistant/components/light/deconz.py | 37 +++++++++----- homeassistant/components/sensor/deconz.py | 30 +++++++----- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/binary_sensor/test_deconz.py | 28 ++++++++++- tests/components/deconz/test_init.py | 49 +++++++++++++++++++ tests/components/light/test_deconz.py | 30 +++++++++++- tests/components/sensor/test_deconz.py | 22 +++++++-- 11 files changed, 212 insertions(+), 48 deletions(-) diff --git a/homeassistant/components/binary_sensor/deconz.py b/homeassistant/components/binary_sensor/deconz.py index a9a3e28f4be..9faa703d13c 100644 --- a/homeassistant/components/binary_sensor/deconz.py +++ b/homeassistant/components/binary_sensor/deconz.py @@ -6,9 +6,10 @@ https://home-assistant.io/components/binary_sensor.deconz/ """ from homeassistant.components.binary_sensor import BinarySensorDevice from homeassistant.components.deconz import ( - DOMAIN as DATA_DECONZ, DATA_DECONZ_ID) + DOMAIN as DATA_DECONZ, DATA_DECONZ_ID, DATA_DECONZ_UNSUB) from homeassistant.const import ATTR_BATTERY_LEVEL from homeassistant.core import callback +from homeassistant.helpers.dispatcher import async_dispatcher_connect DEPENDENCIES = ['deconz'] @@ -21,14 +22,19 @@ async def async_setup_platform(hass, config, async_add_devices, async def async_setup_entry(hass, config_entry, async_add_devices): """Set up the deCONZ binary sensor.""" - from pydeconz.sensor import DECONZ_BINARY_SENSOR - sensors = hass.data[DATA_DECONZ].sensors - entities = [] + @callback + def async_add_sensor(sensors): + """Add binary sensor from deCONZ.""" + from pydeconz.sensor import DECONZ_BINARY_SENSOR + entities = [] + for sensor in sensors: + if sensor.type in DECONZ_BINARY_SENSOR: + entities.append(DeconzBinarySensor(sensor)) + async_add_devices(entities, True) + hass.data[DATA_DECONZ_UNSUB].append( + async_dispatcher_connect(hass, 'deconz_new_sensor', async_add_sensor)) - for sensor in sensors.values(): - if sensor and sensor.type in DECONZ_BINARY_SENSOR: - entities.append(DeconzBinarySensor(sensor)) - async_add_devices(entities, True) + async_add_sensor(hass.data[DATA_DECONZ].sensors.values()) class DeconzBinarySensor(BinarySensorDevice): diff --git a/homeassistant/components/deconz/__init__.py b/homeassistant/components/deconz/__init__.py index 75414598693..47573be6add 100644 --- a/homeassistant/components/deconz/__init__.py +++ b/homeassistant/components/deconz/__init__.py @@ -11,15 +11,18 @@ from homeassistant.const import ( CONF_ID, CONF_PORT, EVENT_HOMEASSISTANT_STOP) from homeassistant.core import EventOrigin, callback from homeassistant.helpers import aiohttp_client, config_validation as cv +from homeassistant.helpers.dispatcher import ( + async_dispatcher_connect, async_dispatcher_send) from homeassistant.util import slugify from homeassistant.util.json import load_json # Loading the config flow file will register the flow from .config_flow import configured_hosts from .const import ( - CONFIG_FILE, DATA_DECONZ_EVENT, DATA_DECONZ_ID, DOMAIN, _LOGGER) + CONFIG_FILE, DATA_DECONZ_EVENT, DATA_DECONZ_ID, + DATA_DECONZ_UNSUB, DOMAIN, _LOGGER) -REQUIREMENTS = ['pydeconz==36'] +REQUIREMENTS = ['pydeconz==37'] CONFIG_SCHEMA = vol.Schema({ DOMAIN: vol.Schema({ @@ -69,14 +72,20 @@ async def async_setup_entry(hass, config_entry): Start websocket for push notification of state changes from deCONZ. """ from pydeconz import DeconzSession - from pydeconz.sensor import SWITCH as DECONZ_REMOTE if DOMAIN in hass.data: _LOGGER.error( "Config entry failed since one deCONZ instance already exists") return False + @callback + def async_add_device_callback(device_type, device): + """Called when a new device has been created in deCONZ.""" + async_dispatcher_send( + hass, 'deconz_new_{}'.format(device_type), [device]) + session = aiohttp_client.async_get_clientsession(hass) - deconz = DeconzSession(hass.loop, session, **config_entry.data) + deconz = DeconzSession(hass.loop, session, **config_entry.data, + async_add_device=async_add_device_callback) result = await deconz.async_load_parameters() if result is False: _LOGGER.error("Failed to communicate with deCONZ") @@ -84,14 +93,24 @@ async def async_setup_entry(hass, config_entry): hass.data[DOMAIN] = deconz hass.data[DATA_DECONZ_ID] = {} + hass.data[DATA_DECONZ_EVENT] = [] + hass.data[DATA_DECONZ_UNSUB] = [] for component in ['binary_sensor', 'light', 'scene', 'sensor']: hass.async_add_job(hass.config_entries.async_forward_entry_setup( config_entry, component)) - hass.data[DATA_DECONZ_EVENT] = [DeconzEvent( - hass, sensor) for sensor in deconz.sensors.values() - if sensor.type in DECONZ_REMOTE] + @callback + def async_add_remote(sensors): + """Setup remote from deCONZ.""" + from pydeconz.sensor import SWITCH as DECONZ_REMOTE + for sensor in sensors: + if sensor.type in DECONZ_REMOTE: + hass.data[DATA_DECONZ_EVENT].append(DeconzEvent(hass, sensor)) + hass.data[DATA_DECONZ_UNSUB].append( + async_dispatcher_connect(hass, 'deconz_new_sensor', async_add_remote)) + + async_add_remote(deconz.sensors.values()) deconz.start() @@ -148,6 +167,10 @@ async def async_unload_entry(hass, config_entry): for component in ['binary_sensor', 'light', 'scene', 'sensor']: await hass.config_entries.async_forward_entry_unload( config_entry, component) + dispatchers = hass.data[DATA_DECONZ_UNSUB] + for unsub_dispatcher in dispatchers: + unsub_dispatcher() + hass.data[DATA_DECONZ_UNSUB] = [] hass.data[DATA_DECONZ_EVENT] = [] hass.data[DATA_DECONZ_ID] = [] return True diff --git a/homeassistant/components/deconz/const.py b/homeassistant/components/deconz/const.py index e6d393c8ee7..48e5ea75d68 100644 --- a/homeassistant/components/deconz/const.py +++ b/homeassistant/components/deconz/const.py @@ -7,3 +7,4 @@ DOMAIN = 'deconz' CONFIG_FILE = 'deconz.conf' DATA_DECONZ_EVENT = 'deconz_events' DATA_DECONZ_ID = 'deconz_entities' +DATA_DECONZ_UNSUB = 'deconz_dispatchers' diff --git a/homeassistant/components/light/deconz.py b/homeassistant/components/light/deconz.py index 36ad572a263..916e60c00b1 100644 --- a/homeassistant/components/light/deconz.py +++ b/homeassistant/components/light/deconz.py @@ -5,13 +5,14 @@ For more details about this component, please refer to the documentation at https://home-assistant.io/components/light.deconz/ """ from homeassistant.components.deconz import ( - DOMAIN as DATA_DECONZ, DATA_DECONZ_ID) + DOMAIN as DATA_DECONZ, DATA_DECONZ_ID, DATA_DECONZ_UNSUB) 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, SUPPORT_BRIGHTNESS, SUPPORT_COLOR, SUPPORT_COLOR_TEMP, SUPPORT_EFFECT, SUPPORT_FLASH, SUPPORT_TRANSITION, Light) from homeassistant.core import callback +from homeassistant.helpers.dispatcher import async_dispatcher_connect import homeassistant.util.color as color_util DEPENDENCIES = ['deconz'] @@ -19,23 +20,35 @@ DEPENDENCIES = ['deconz'] async def async_setup_platform(hass, config, async_add_devices, discovery_info=None): - """Old way of setting up deCONZ lights.""" + """Old way of setting up deCONZ lights and group.""" pass async def async_setup_entry(hass, config_entry, async_add_devices): - """Set up the deCONZ lights from a config entry.""" - lights = hass.data[DATA_DECONZ].lights - groups = hass.data[DATA_DECONZ].groups - entities = [] + """Set up the deCONZ lights and groups from a config entry.""" + @callback + def async_add_light(lights): + """Add light from deCONZ.""" + entities = [] + 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)) - for light in lights.values(): - entities.append(DeconzLight(light)) + @callback + def async_add_group(groups): + """Add group from deCONZ.""" + entities = [] + for group in groups: + if group.lights: + 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)) - for group in groups.values(): - if group.lights: # Don't create entity for group not containing light - entities.append(DeconzLight(group)) - async_add_devices(entities, True) + async_add_light(hass.data[DATA_DECONZ].lights.values()) + async_add_group(hass.data[DATA_DECONZ].groups.values()) class DeconzLight(Light): diff --git a/homeassistant/components/sensor/deconz.py b/homeassistant/components/sensor/deconz.py index b4a3cb8c6c5..221cdf2129e 100644 --- a/homeassistant/components/sensor/deconz.py +++ b/homeassistant/components/sensor/deconz.py @@ -5,10 +5,11 @@ For more details about this component, please refer to the documentation at https://home-assistant.io/components/sensor.deconz/ """ from homeassistant.components.deconz import ( - DOMAIN as DATA_DECONZ, DATA_DECONZ_ID) + DOMAIN as DATA_DECONZ, DATA_DECONZ_ID, DATA_DECONZ_UNSUB) from homeassistant.const import ( ATTR_BATTERY_LEVEL, ATTR_VOLTAGE, DEVICE_CLASS_BATTERY) from homeassistant.core import callback +from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity import Entity from homeassistant.util import slugify @@ -27,18 +28,23 @@ async def async_setup_platform(hass, config, async_add_devices, async def async_setup_entry(hass, config_entry, async_add_devices): """Set up the deCONZ sensors.""" - from pydeconz.sensor import DECONZ_SENSOR, SWITCH as DECONZ_REMOTE - sensors = hass.data[DATA_DECONZ].sensors - entities = [] + @callback + def async_add_sensor(sensors): + """Add sensors from deCONZ.""" + from pydeconz.sensor import DECONZ_SENSOR, SWITCH as DECONZ_REMOTE + entities = [] + for sensor in sensors: + if sensor.type in DECONZ_SENSOR: + if sensor.type in DECONZ_REMOTE: + if sensor.battery: + entities.append(DeconzBattery(sensor)) + else: + entities.append(DeconzSensor(sensor)) + async_add_devices(entities, True) + hass.data[DATA_DECONZ_UNSUB].append( + async_dispatcher_connect(hass, 'deconz_new_sensor', async_add_sensor)) - for sensor in sensors.values(): - if sensor and sensor.type in DECONZ_SENSOR: - if sensor.type in DECONZ_REMOTE: - if sensor.battery: - entities.append(DeconzBattery(sensor)) - else: - entities.append(DeconzSensor(sensor)) - async_add_devices(entities, True) + async_add_sensor(hass.data[DATA_DECONZ].sensors.values()) class DeconzSensor(Entity): diff --git a/requirements_all.txt b/requirements_all.txt index 74f9ff8e195..406b460d0a4 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -745,7 +745,7 @@ pycsspeechtts==1.0.2 pydaikin==0.4 # homeassistant.components.deconz -pydeconz==36 +pydeconz==37 # homeassistant.components.zwave pydispatcher==2.0.5 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 7816d9c6f24..df0f5722b86 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -133,7 +133,7 @@ py-canary==0.5.0 pyblackbird==0.5 # homeassistant.components.deconz -pydeconz==36 +pydeconz==37 # homeassistant.components.zwave pydispatcher==2.0.5 diff --git a/tests/components/binary_sensor/test_deconz.py b/tests/components/binary_sensor/test_deconz.py index 84ed059e97e..88dd0dae737 100644 --- a/tests/components/binary_sensor/test_deconz.py +++ b/tests/components/binary_sensor/test_deconz.py @@ -3,6 +3,7 @@ from unittest.mock import Mock, patch from homeassistant import config_entries from homeassistant.components import deconz +from homeassistant.helpers.dispatcher import async_dispatcher_send from tests.common import mock_coro @@ -14,6 +15,13 @@ SENSOR = { "type": "ZHAPresence", "state": {"presence": False}, "config": {} + }, + "2": { + "id": "Sensor 2 id", + "name": "Sensor 2 name", + "type": "ZHATemperature", + "state": {"temperature": False}, + "config": {} } } @@ -30,6 +38,7 @@ async def setup_bridge(hass, data): return_value=mock_coro(data)): await bridge.async_load_parameters() hass.data[deconz.DOMAIN] = bridge + 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') @@ -40,7 +49,7 @@ async def setup_bridge(hass, data): async def test_no_binary_sensors(hass): - """Test the update_lights function with some lights.""" + """Test that no sensors in deconz results in no sensor entities.""" data = {} await setup_bridge(hass, data) assert len(hass.data[deconz.DATA_DECONZ_ID]) == 0 @@ -48,8 +57,23 @@ async def test_no_binary_sensors(hass): async def test_binary_sensors(hass): - """Test the update_lights function with some lights.""" + """Test successful creation of binary sensor entities.""" data = {"sensors": SENSOR} await setup_bridge(hass, data) assert "binary_sensor.sensor_1_name" in hass.data[deconz.DATA_DECONZ_ID] + assert "binary_sensor.sensor_2_name" not in \ + hass.data[deconz.DATA_DECONZ_ID] assert len(hass.states.async_all()) == 1 + + +async def test_add_new_sensor(hass): + """Test successful creation of sensor entities.""" + data = {} + await setup_bridge(hass, data) + sensor = Mock() + sensor.name = 'name' + sensor.type = 'ZHAPresence' + sensor.register_async_callback = Mock() + async_dispatcher_send(hass, 'deconz_new_sensor', [sensor]) + await hass.async_block_till_done() + assert "binary_sensor.name" in hass.data[deconz.DATA_DECONZ_ID] diff --git a/tests/components/deconz/test_init.py b/tests/components/deconz/test_init.py index b09edf42a87..888094deea6 100644 --- a/tests/components/deconz/test_init.py +++ b/tests/components/deconz/test_init.py @@ -1,6 +1,7 @@ """Test deCONZ component setup process.""" from unittest.mock import Mock, patch +from homeassistant.helpers.dispatcher import async_dispatcher_send from homeassistant.setup import async_setup_component from homeassistant.components import deconz @@ -97,6 +98,7 @@ async def test_setup_entry_successful(hass): assert await deconz.async_setup_entry(hass, entry) is True assert hass.data[deconz.DOMAIN] assert hass.data[deconz.DATA_DECONZ_ID] == {} + assert len(hass.data[deconz.DATA_DECONZ_UNSUB]) == 1 assert len(mock_add_job.mock_calls) == 4 assert len(mock_config_entries.async_forward_entry_setup.mock_calls) == 4 assert mock_config_entries.async_forward_entry_setup.mock_calls[0][1] == \ @@ -121,5 +123,52 @@ async def test_unload_entry(hass): hass.data[deconz.DATA_DECONZ_ID] = {'id': 'deconzid'} assert await deconz.async_unload_entry(hass, entry) assert deconz.DOMAIN not in hass.data + assert len(hass.data[deconz.DATA_DECONZ_UNSUB]) == 0 assert len(hass.data[deconz.DATA_DECONZ_EVENT]) == 0 assert len(hass.data[deconz.DATA_DECONZ_ID]) == 0 + + +async def test_add_new_device(hass): + """Test adding a new device generates a signal for platforms.""" + new_event = { + "t": "event", + "e": "added", + "r": "sensors", + "id": "1", + "sensor": { + "config": { + "on": "True", + "reachable": "True" + }, + "name": "event", + "state": {}, + "type": "ZHASwitch" + } + } + entry = Mock() + entry.data = {'host': '1.2.3.4', 'port': 80, 'api_key': '1234567890ABCDEF'} + with patch.object(deconz, 'async_dispatcher_send') as mock_dispatch_send, \ + patch('pydeconz.DeconzSession.async_load_parameters', + return_value=mock_coro(True)): + assert await deconz.async_setup_entry(hass, entry) is True + hass.data[deconz.DOMAIN].async_event_handler(new_event) + await hass.async_block_till_done() + assert len(mock_dispatch_send.mock_calls) == 1 + assert len(mock_dispatch_send.mock_calls[0]) == 3 + + +async def test_add_new_remote(hass): + """Test new added device creates a new remote.""" + entry = Mock() + entry.data = {'host': '1.2.3.4', 'port': 80, 'api_key': '1234567890ABCDEF'} + remote = Mock() + remote.name = 'name' + remote.type = 'ZHASwitch' + remote.register_async_callback = Mock() + with patch('pydeconz.DeconzSession.async_load_parameters', + return_value=mock_coro(True)): + assert await deconz.async_setup_entry(hass, entry) is True + + async_dispatcher_send(hass, 'deconz_new_sensor', [remote]) + await hass.async_block_till_done() + assert len(hass.data[deconz.DATA_DECONZ_EVENT]) == 1 diff --git a/tests/components/light/test_deconz.py b/tests/components/light/test_deconz.py index d907697354e..2608d77ce2a 100644 --- a/tests/components/light/test_deconz.py +++ b/tests/components/light/test_deconz.py @@ -3,6 +3,7 @@ from unittest.mock import Mock, patch from homeassistant import config_entries from homeassistant.components import deconz +from homeassistant.helpers.dispatcher import async_dispatcher_send from tests.common import mock_coro @@ -49,6 +50,7 @@ async def setup_bridge(hass, data): return_value=mock_coro(data)): await bridge.async_load_parameters() hass.data[deconz.DOMAIN] = bridge + 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') @@ -58,7 +60,7 @@ async def setup_bridge(hass, data): async def test_no_lights_or_groups(hass): - """Test the update_lights function with some lights.""" + """Test that no lights or groups entities are created.""" data = {} await setup_bridge(hass, data) assert len(hass.data[deconz.DATA_DECONZ_ID]) == 0 @@ -66,9 +68,33 @@ async def test_no_lights_or_groups(hass): async def test_lights_and_groups(hass): - """Test the update_lights function with some lights.""" + """Test that lights or groups entities are created.""" await setup_bridge(hass, {"lights": LIGHT, "groups": GROUP}) assert "light.light_1_name" in hass.data[deconz.DATA_DECONZ_ID] assert "light.group_1_name" in hass.data[deconz.DATA_DECONZ_ID] assert "light.group_2_name" not in hass.data[deconz.DATA_DECONZ_ID] assert len(hass.states.async_all()) == 3 + + +async def test_add_new_light(hass): + """Test successful creation of light entities.""" + data = {} + await setup_bridge(hass, data) + light = Mock() + light.name = 'name' + light.register_async_callback = Mock() + async_dispatcher_send(hass, 'deconz_new_light', [light]) + await hass.async_block_till_done() + assert "light.name" in hass.data[deconz.DATA_DECONZ_ID] + + +async def test_add_new_group(hass): + """Test successful creation of group entities.""" + data = {} + await setup_bridge(hass, data) + 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 "light.name" in hass.data[deconz.DATA_DECONZ_ID] diff --git a/tests/components/sensor/test_deconz.py b/tests/components/sensor/test_deconz.py index d6c026e88bd..8f6a53e6e65 100644 --- a/tests/components/sensor/test_deconz.py +++ b/tests/components/sensor/test_deconz.py @@ -1,8 +1,10 @@ """deCONZ sensor platform tests.""" from unittest.mock import Mock, patch + from homeassistant import config_entries from homeassistant.components import deconz +from homeassistant.helpers.dispatcher import async_dispatcher_send from tests.common import mock_coro @@ -51,6 +53,7 @@ async def setup_bridge(hass, data): return_value=mock_coro(data)): await bridge.async_load_parameters() hass.data[deconz.DOMAIN] = bridge + hass.data[deconz.DATA_DECONZ_UNSUB] = [] hass.data[deconz.DATA_DECONZ_EVENT] = [] hass.data[deconz.DATA_DECONZ_ID] = {} config_entry = config_entries.ConfigEntry( @@ -61,15 +64,15 @@ async def setup_bridge(hass, data): async def test_no_sensors(hass): - """Test the update_lights function with some lights.""" + """Test that no sensors in deconz results in no sensor entities.""" data = {} await setup_bridge(hass, data) assert len(hass.data[deconz.DATA_DECONZ_ID]) == 0 assert len(hass.states.async_all()) == 0 -async def test_binary_sensors(hass): - """Test the update_lights function with some lights.""" +async def test_sensors(hass): + """Test successful creation of sensor entities.""" data = {"sensors": SENSOR} await setup_bridge(hass, data) assert "sensor.sensor_1_name" in hass.data[deconz.DATA_DECONZ_ID] @@ -81,3 +84,16 @@ async def test_binary_sensors(hass): assert "sensor.sensor_4_name_battery_level" in \ hass.data[deconz.DATA_DECONZ_ID] assert len(hass.states.async_all()) == 2 + + +async def test_add_new_sensor(hass): + """Test successful creation of sensor entities.""" + data = {} + await setup_bridge(hass, data) + sensor = Mock() + sensor.name = 'name' + sensor.type = 'ZHATemperature' + sensor.register_async_callback = Mock() + async_dispatcher_send(hass, 'deconz_new_sensor', [sensor]) + await hass.async_block_till_done() + assert "sensor.name" in hass.data[deconz.DATA_DECONZ_ID] From 1a936220e9a8741b707b10cd197f16d915b748b9 Mon Sep 17 00:00:00 2001 From: Jesse Newland Date: Sat, 5 May 2018 09:21:58 -0500 Subject: [PATCH 133/155] Add alarmdotcom sensor status (#14254) * bump to match Xorso/pyalarmdotcom#9 * Load additional status attributes * missed a spot --- .../components/alarm_control_panel/alarmdotcom.py | 9 ++++++++- requirements_all.txt | 2 +- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/alarm_control_panel/alarmdotcom.py b/homeassistant/components/alarm_control_panel/alarmdotcom.py index 0e96e6448ff..31d93373286 100644 --- a/homeassistant/components/alarm_control_panel/alarmdotcom.py +++ b/homeassistant/components/alarm_control_panel/alarmdotcom.py @@ -17,7 +17,7 @@ from homeassistant.const import ( from homeassistant.helpers.aiohttp_client import async_get_clientsession import homeassistant.helpers.config_validation as cv -REQUIREMENTS = ['pyalarmdotcom==0.3.1'] +REQUIREMENTS = ['pyalarmdotcom==0.3.2'] _LOGGER = logging.getLogger(__name__) @@ -93,6 +93,13 @@ class AlarmDotCom(alarm.AlarmControlPanel): return STATE_ALARM_ARMED_AWAY return STATE_UNKNOWN + @property + def device_state_attributes(self): + """Return the state attributes.""" + return { + 'sensor_status': self._alarm.sensor_status + } + @asyncio.coroutine def async_alarm_disarm(self, code=None): """Send disarm command.""" diff --git a/requirements_all.txt b/requirements_all.txt index 406b460d0a4..d24c2e07043 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -698,7 +698,7 @@ pyads==2.2.6 pyairvisual==1.0.0 # homeassistant.components.alarm_control_panel.alarmdotcom -pyalarmdotcom==0.3.1 +pyalarmdotcom==0.3.2 # homeassistant.components.arlo pyarlo==0.1.2 From f6e29a66471f009e9c3f142970c1b113ed4f4e9e Mon Sep 17 00:00:00 2001 From: Jesse Newland Date: Sat, 5 May 2018 09:23:01 -0500 Subject: [PATCH 134/155] Add domain to labels and count state changes to Prometheus (#14253) * Add domain to labels * Count state changes --- homeassistant/components/prometheus.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/prometheus.py b/homeassistant/components/prometheus.py index dc1cbd945a7..96ed098567d 100644 --- a/homeassistant/components/prometheus.py +++ b/homeassistant/components/prometheus.py @@ -86,9 +86,16 @@ class Metrics(object): if hasattr(self, handler): getattr(self, handler)(state) + metric = self._metric( + 'state_change', + self.prometheus_client.Counter, + 'The number of state changes', + ) + metric.labels(**self._labels(state)).inc() + def _metric(self, metric, factory, documentation, labels=None): if labels is None: - labels = ['entity', 'friendly_name'] + labels = ['entity', 'friendly_name', 'domain'] try: return self._metrics[metric] @@ -100,6 +107,7 @@ class Metrics(object): def _labels(state): return { 'entity': state.entity_id, + 'domain': state.domain, 'friendly_name': state.attributes.get('friendly_name'), } From 4d085882d5d5752548c1e977da43c3e00848089d Mon Sep 17 00:00:00 2001 From: Jason Kingsbury Date: Sat, 5 May 2018 15:30:54 +0100 Subject: [PATCH 135/155] Add support for max_volume (#13822) * onkyo: add support for max volume range * onkyo: make flake8 happy * onkyo: fix PEP8 D205 on line 181 * onkyo: use range for max_volume configuration * onkyo: fix line too long --- .../components/media_player/onkyo.py | 23 +++++++++++++++---- 1 file changed, 18 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/media_player/onkyo.py b/homeassistant/components/media_player/onkyo.py index 58703165385..245ab8bb54c 100644 --- a/homeassistant/components/media_player/onkyo.py +++ b/homeassistant/components/media_player/onkyo.py @@ -22,9 +22,11 @@ REQUIREMENTS = ['onkyo-eiscp==1.2.4'] _LOGGER = logging.getLogger(__name__) CONF_SOURCES = 'sources' +CONF_MAX_VOLUME = 'max_volume' CONF_ZONE2 = 'zone2' DEFAULT_NAME = 'Onkyo Receiver' +SUPPORTED_MAX_VOLUME = 80 SUPPORT_ONKYO = SUPPORT_VOLUME_SET | SUPPORT_VOLUME_MUTE | \ SUPPORT_TURN_ON | SUPPORT_TURN_OFF | SUPPORT_SELECT_SOURCE | SUPPORT_PLAY @@ -39,6 +41,8 @@ DEFAULT_SOURCES = {'tv': 'TV', 'bd': 'Bluray', 'game': 'Game', 'aux1': 'Aux1', PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Optional(CONF_HOST): cv.string, vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + vol.Optional(CONF_MAX_VOLUME, default=SUPPORTED_MAX_VOLUME): + vol.All(vol.Coerce(int), vol.Range(min=1, max=SUPPORTED_MAX_VOLUME)), vol.Optional(CONF_SOURCES, default=DEFAULT_SOURCES): {cv.string: cv.string}, vol.Optional(CONF_ZONE2, default=False): cv.boolean, @@ -57,7 +61,9 @@ def setup_platform(hass, config, add_devices, discovery_info=None): try: hosts.append(OnkyoDevice( eiscp.eISCP(host), config.get(CONF_SOURCES), - name=config.get(CONF_NAME))) + name=config.get(CONF_NAME), + max_volume=config.get(CONF_MAX_VOLUME), + )) KNOWN_HOSTS.append(host) # Add Zone2 if configured @@ -80,7 +86,8 @@ def setup_platform(hass, config, add_devices, discovery_info=None): class OnkyoDevice(MediaPlayerDevice): """Representation of an Onkyo device.""" - def __init__(self, receiver, sources, name=None): + def __init__(self, receiver, sources, name=None, + max_volume=SUPPORTED_MAX_VOLUME): """Initialize the Onkyo Receiver.""" self._receiver = receiver self._muted = False @@ -88,6 +95,7 @@ class OnkyoDevice(MediaPlayerDevice): self._pwstate = STATE_OFF self._name = name or '{}_{}'.format( receiver.info['model_name'], receiver.info['identifier']) + self._max_volume = max_volume self._current_source = None self._source_list = list(sources.values()) self._source_mapping = sources @@ -141,7 +149,7 @@ class OnkyoDevice(MediaPlayerDevice): 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 + self._volume = volume_raw[1] / self._max_volume @property def name(self): @@ -183,8 +191,13 @@ class OnkyoDevice(MediaPlayerDevice): self.command('system-power standby') def set_volume_level(self, volume): - """Set volume level, input is range 0..1. Onkyo ranges from 1-80.""" - self.command('volume {}'.format(int(volume*80))) + """ + Set volume level, input is range 0..1. + + Onkyo ranges from 1-80 however 80 is usually far too loud + so allow the user to specify the upper range with CONF_MAX_VOLUME + """ + self.command('volume {}'.format(int(volume * self._max_volume))) def mute_volume(self, mute): """Mute (true) or unmute (false) media player.""" From b9e893184ad2db3b36872de40f28d13843fb0086 Mon Sep 17 00:00:00 2001 From: Robin Date: Sat, 5 May 2018 15:57:53 +0100 Subject: [PATCH 136/155] Refactor ImageProcessingFaceEntity (#14296) * Refactor ImageProcessingFaceEntity * Replace STATE_UNKNOWN with None --- .../components/image_processing/__init__.py | 98 ++++++++++++++++ .../components/image_processing/demo.py | 7 +- .../image_processing/dlib_face_detect.py | 4 +- .../image_processing/dlib_face_identify.py | 5 +- .../image_processing/microsoft_face_detect.py | 5 +- .../microsoft_face_identify.py | 105 +----------------- 6 files changed, 110 insertions(+), 114 deletions(-) diff --git a/homeassistant/components/image_processing/__init__.py b/homeassistant/components/image_processing/__init__.py index f0cb3a66d52..c6100ff701d 100644 --- a/homeassistant/components/image_processing/__init__.py +++ b/homeassistant/components/image_processing/__init__.py @@ -10,6 +10,7 @@ import logging import voluptuous as vol +from homeassistant.core import callback import homeassistant.helpers.config_validation as cv from homeassistant.const import ( ATTR_ENTITY_ID, CONF_NAME, CONF_ENTITY_ID) @@ -17,6 +18,7 @@ from homeassistant.exceptions import HomeAssistantError from homeassistant.loader import bind_hass from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity_component import EntityComponent +from homeassistant.util.async_ import run_callback_threadsafe _LOGGER = logging.getLogger(__name__) @@ -33,7 +35,16 @@ DEVICE_CLASSES = [ SERVICE_SCAN = 'scan' +EVENT_DETECT_FACE = 'image_processing.detect_face' + +ATTR_AGE = 'age' ATTR_CONFIDENCE = 'confidence' +ATTR_FACES = 'faces' +ATTR_GENDER = 'gender' +ATTR_GLASSES = 'glasses' +ATTR_NAME = 'name' +ATTR_MOTION = 'motion' +ATTR_TOTAL_FACES = 'total_faces' CONF_SOURCE = 'source' CONF_CONFIDENCE = 'confidence' @@ -133,3 +144,90 @@ class ImageProcessingEntity(Entity): # process image data yield from self.async_process_image(image.content) + + +class ImageProcessingFaceEntity(ImageProcessingEntity): + """Base entity class for face image processing.""" + + def __init__(self): + """Initialize base face identify/verify entity.""" + self.faces = [] + self.total_faces = 0 + + @property + def state(self): + """Return the state of the entity.""" + confidence = 0 + state = None + + # No confidence support + if not self.confidence: + return self.total_faces + + # Search high confidence + for face in self.faces: + if ATTR_CONFIDENCE not in face: + continue + + f_co = face[ATTR_CONFIDENCE] + if f_co > confidence: + confidence = f_co + for attr in [ATTR_NAME, ATTR_MOTION]: + if attr in face: + state = face[attr] + break + + return state + + @property + def device_class(self): + """Return the class of this device, from component DEVICE_CLASSES.""" + return 'face' + + @property + def state_attributes(self): + """Return device specific state attributes.""" + attr = { + ATTR_FACES: self.faces, + ATTR_TOTAL_FACES: self.total_faces, + } + + return attr + + def process_faces(self, faces, total): + """Send event with detected faces and store data.""" + run_callback_threadsafe( + self.hass.loop, self.async_process_faces, faces, total).result() + + @callback + def async_process_faces(self, faces, total): + """Send event with detected faces and store data. + + known are a dict in follow format: + [ + { + ATTR_CONFIDENCE: 80, + ATTR_NAME: 'Name', + ATTR_AGE: 12.0, + ATTR_GENDER: 'man', + ATTR_MOTION: 'smile', + ATTR_GLASSES: 'sunglasses' + }, + ] + + This method must be run in the event loop. + """ + # Send events + for face in faces: + if ATTR_CONFIDENCE in face and self.confidence: + if face[ATTR_CONFIDENCE] < self.confidence: + continue + + face.update({ATTR_ENTITY_ID: self.entity_id}) + self.hass.async_add_job( + self.hass.bus.async_fire, EVENT_DETECT_FACE, face + ) + + # Update entity store + self.faces = faces + self.total_faces = total diff --git a/homeassistant/components/image_processing/demo.py b/homeassistant/components/image_processing/demo.py index 788d12520f5..e225113b5b1 100644 --- a/homeassistant/components/image_processing/demo.py +++ b/homeassistant/components/image_processing/demo.py @@ -4,11 +4,12 @@ Support for the demo image processing. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/demo/ """ -from homeassistant.components.image_processing import ATTR_CONFIDENCE +from homeassistant.components.image_processing import ( + ImageProcessingFaceEntity, ATTR_CONFIDENCE, ATTR_NAME, ATTR_AGE, + ATTR_GENDER + ) from homeassistant.components.image_processing.openalpr_local import ( ImageProcessingAlprEntity) -from homeassistant.components.image_processing.microsoft_face_identify import ( - ImageProcessingFaceEntity, ATTR_NAME, ATTR_AGE, ATTR_GENDER) def setup_platform(hass, config, add_devices, discovery_info=None): diff --git a/homeassistant/components/image_processing/dlib_face_detect.py b/homeassistant/components/image_processing/dlib_face_detect.py index 65705feb7f7..d4a20da253c 100644 --- a/homeassistant/components/image_processing/dlib_face_detect.py +++ b/homeassistant/components/image_processing/dlib_face_detect.py @@ -11,9 +11,7 @@ from homeassistant.core import split_entity_id # pylint: disable=unused-import from homeassistant.components.image_processing import PLATFORM_SCHEMA # noqa from homeassistant.components.image_processing import ( - CONF_SOURCE, CONF_ENTITY_ID, CONF_NAME) -from homeassistant.components.image_processing.microsoft_face_identify import ( - ImageProcessingFaceEntity) + ImageProcessingFaceEntity, CONF_SOURCE, CONF_ENTITY_ID, CONF_NAME) REQUIREMENTS = ['face_recognition==1.0.0'] diff --git a/homeassistant/components/image_processing/dlib_face_identify.py b/homeassistant/components/image_processing/dlib_face_identify.py index 22594aa2547..bf34eb4c2da 100644 --- a/homeassistant/components/image_processing/dlib_face_identify.py +++ b/homeassistant/components/image_processing/dlib_face_identify.py @@ -11,9 +11,8 @@ import voluptuous as vol from homeassistant.core import split_entity_id from homeassistant.components.image_processing import ( - PLATFORM_SCHEMA, CONF_SOURCE, CONF_ENTITY_ID, CONF_NAME) -from homeassistant.components.image_processing.microsoft_face_identify import ( - ImageProcessingFaceEntity) + ImageProcessingFaceEntity, PLATFORM_SCHEMA, CONF_SOURCE, CONF_ENTITY_ID, + CONF_NAME) import homeassistant.helpers.config_validation as cv REQUIREMENTS = ['face_recognition==1.0.0'] diff --git a/homeassistant/components/image_processing/microsoft_face_detect.py b/homeassistant/components/image_processing/microsoft_face_detect.py index 6770ff1bdf6..cd1e341a218 100644 --- a/homeassistant/components/image_processing/microsoft_face_detect.py +++ b/homeassistant/components/image_processing/microsoft_face_detect.py @@ -13,9 +13,8 @@ from homeassistant.core import split_entity_id from homeassistant.exceptions import HomeAssistantError from homeassistant.components.microsoft_face import DATA_MICROSOFT_FACE from homeassistant.components.image_processing import ( - PLATFORM_SCHEMA, CONF_SOURCE, CONF_ENTITY_ID, CONF_NAME) -from homeassistant.components.image_processing.microsoft_face_identify import ( - ImageProcessingFaceEntity, ATTR_GENDER, ATTR_AGE, ATTR_GLASSES) + PLATFORM_SCHEMA, ImageProcessingFaceEntity, ATTR_AGE, ATTR_GENDER, + ATTR_GLASSES, CONF_SOURCE, CONF_ENTITY_ID, CONF_NAME) import homeassistant.helpers.config_validation as cv DEPENDENCIES = ['microsoft_face'] diff --git a/homeassistant/components/image_processing/microsoft_face_identify.py b/homeassistant/components/image_processing/microsoft_face_identify.py index 51f1cd42f47..32f02e1820e 100644 --- a/homeassistant/components/image_processing/microsoft_face_identify.py +++ b/homeassistant/components/image_processing/microsoft_face_identify.py @@ -9,30 +9,18 @@ import logging import voluptuous as vol -from homeassistant.core import split_entity_id, callback -from homeassistant.const import STATE_UNKNOWN +from homeassistant.core import split_entity_id from homeassistant.exceptions import HomeAssistantError from homeassistant.components.microsoft_face import DATA_MICROSOFT_FACE from homeassistant.components.image_processing import ( - PLATFORM_SCHEMA, ImageProcessingEntity, CONF_CONFIDENCE, CONF_SOURCE, - CONF_ENTITY_ID, CONF_NAME, ATTR_ENTITY_ID, ATTR_CONFIDENCE) + PLATFORM_SCHEMA, ImageProcessingFaceEntity, ATTR_NAME, + CONF_CONFIDENCE, CONF_SOURCE, CONF_ENTITY_ID, CONF_NAME, ATTR_CONFIDENCE) import homeassistant.helpers.config_validation as cv -from homeassistant.util.async_ import run_callback_threadsafe DEPENDENCIES = ['microsoft_face'] _LOGGER = logging.getLogger(__name__) -EVENT_DETECT_FACE = 'image_processing.detect_face' - -ATTR_NAME = 'name' -ATTR_TOTAL_FACES = 'total_faces' -ATTR_AGE = 'age' -ATTR_GENDER = 'gender' -ATTR_MOTION = 'motion' -ATTR_GLASSES = 'glasses' -ATTR_FACES = 'faces' - CONF_GROUP = 'group' PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ @@ -57,93 +45,6 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None): async_add_devices(entities) -class ImageProcessingFaceEntity(ImageProcessingEntity): - """Base entity class for face image processing.""" - - def __init__(self): - """Initialize base face identify/verify entity.""" - self.faces = [] - self.total_faces = 0 - - @property - def state(self): - """Return the state of the entity.""" - confidence = 0 - state = STATE_UNKNOWN - - # No confidence support - if not self.confidence: - return self.total_faces - - # Search high confidence - for face in self.faces: - if ATTR_CONFIDENCE not in face: - continue - - f_co = face[ATTR_CONFIDENCE] - if f_co > confidence: - confidence = f_co - for attr in [ATTR_NAME, ATTR_MOTION]: - if attr in face: - state = face[attr] - break - - return state - - @property - def device_class(self): - """Return the class of this device, from component DEVICE_CLASSES.""" - return 'face' - - @property - def state_attributes(self): - """Return device specific state attributes.""" - attr = { - ATTR_FACES: self.faces, - ATTR_TOTAL_FACES: self.total_faces, - } - - return attr - - def process_faces(self, faces, total): - """Send event with detected faces and store data.""" - run_callback_threadsafe( - self.hass.loop, self.async_process_faces, faces, total).result() - - @callback - def async_process_faces(self, faces, total): - """Send event with detected faces and store data. - - known are a dict in follow format: - [ - { - ATTR_CONFIDENCE: 80, - ATTR_NAME: 'Name', - ATTR_AGE: 12.0, - ATTR_GENDER: 'man', - ATTR_MOTION: 'smile', - ATTR_GLASSES: 'sunglasses' - }, - ] - - This method must be run in the event loop. - """ - # Send events - for face in faces: - if ATTR_CONFIDENCE in face and self.confidence: - if face[ATTR_CONFIDENCE] < self.confidence: - continue - - face.update({ATTR_ENTITY_ID: self.entity_id}) - self.hass.async_add_job( - self.hass.bus.async_fire, EVENT_DETECT_FACE, face - ) - - # Update entity store - self.faces = faces - self.total_faces = total - - class MicrosoftFaceIdentifyEntity(ImageProcessingFaceEntity): """Representation of the Microsoft Face API entity for identify.""" From 2e8eaf40f728f9cbf75d3da5b2337da20a9099cc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ron=20=C5=A0meral?= Date: Sat, 5 May 2018 17:06:32 +0200 Subject: [PATCH 137/155] Onkyo: SUPPORT_VOLUME_STEP (#14299) --- .../components/media_player/onkyo.py | 22 +++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/media_player/onkyo.py b/homeassistant/components/media_player/onkyo.py index 245ab8bb54c..39c278ff95d 100644 --- a/homeassistant/components/media_player/onkyo.py +++ b/homeassistant/components/media_player/onkyo.py @@ -13,7 +13,8 @@ import voluptuous as vol from homeassistant.components.media_player import ( SUPPORT_TURN_OFF, SUPPORT_TURN_ON, SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET, - SUPPORT_SELECT_SOURCE, SUPPORT_PLAY, MediaPlayerDevice, PLATFORM_SCHEMA) + SUPPORT_VOLUME_STEP, SUPPORT_SELECT_SOURCE, SUPPORT_PLAY, + MediaPlayerDevice, PLATFORM_SCHEMA) from homeassistant.const import (STATE_OFF, STATE_ON, CONF_HOST, CONF_NAME) import homeassistant.helpers.config_validation as cv @@ -29,7 +30,8 @@ DEFAULT_NAME = 'Onkyo Receiver' SUPPORTED_MAX_VOLUME = 80 SUPPORT_ONKYO = SUPPORT_VOLUME_SET | SUPPORT_VOLUME_MUTE | \ - SUPPORT_TURN_ON | SUPPORT_TURN_OFF | SUPPORT_SELECT_SOURCE | SUPPORT_PLAY + SUPPORT_VOLUME_STEP | 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', @@ -199,6 +201,14 @@ class OnkyoDevice(MediaPlayerDevice): """ self.command('volume {}'.format(int(volume * self._max_volume))) + def volume_up(self): + """Increase volume by 1 step.""" + self.command('volume level-up') + + def volume_down(self): + """Decrease volume by 1 step.""" + self.command('volume level-down') + def mute_volume(self, mute): """Mute (true) or unmute (false) media player.""" if mute: @@ -264,6 +274,14 @@ class OnkyoDeviceZone2(OnkyoDevice): """Set volume level, input is range 0..1. Onkyo ranges from 1-80.""" self.command('zone2.volume={}'.format(int(volume*80))) + def volume_up(self): + """Increase volume by 1 step.""" + self.command('zone2.volume=level-up') + + def volume_down(self): + """Decrease volume by 1 step.""" + self.command('zone2.volume=level-down') + def mute_volume(self, mute): """Mute (true) or unmute (false) media player.""" if mute: From 64ba2c63c7bdf498875ce2117fc5a255d157be17 Mon Sep 17 00:00:00 2001 From: Tom Harris Date: Sat, 5 May 2018 11:15:20 -0400 Subject: [PATCH 138/155] Add All-Linking capabilities (#14065) * Setup all-linking service * Remove extra line * Remove linefeed and tab escape chars * Add services delete_all_link, load_all_link_database and print_all_link_database * Check if reload is set * Confirm entity is InsteonPLMEntity before attempting to load or print ALDB * Debug load and print ALDB * Debug print aldb * Debug print_aldb * Get entity via platform * Track Insteon entities in component * Store entity list in hass.data * Add entity to hass.data * Add ref to hass in InsteonPLMEntity * Pass hass correctly to InsteonPLMBinarySensor * Fix reference to ALDBStatus.PARTIAL * Print ALDB record as string * Get ALDB record from memory address * Reformat ALDB log output * Add print_im_aldb service * Remove reference to self in print_aldb_to_log * Remove reference to self in print_aldb_to_log * Fix spelling issue with load_all_link_database service * Bump insteonplm to 0.9.1 * Changes from code review * Code review changes * Fix syntax error * Correct reference to cv.boolean and update requirements * Update requirements * Fix flake8 errors * Reload as boolean test * Remove hass from entity init --- .../components/binary_sensor/insteon_plm.py | 2 +- homeassistant/components/fan/insteon_plm.py | 2 +- .../__init__.py} | 136 +++++++++++++++++- .../components/insteon_plm/services.yaml | 32 +++++ homeassistant/components/light/insteon_plm.py | 2 +- .../components/sensor/insteon_plm.py | 2 +- .../components/switch/insteon_plm.py | 2 +- requirements_all.txt | 2 +- 8 files changed, 171 insertions(+), 9 deletions(-) rename homeassistant/components/{insteon_plm.py => insteon_plm/__init__.py} (57%) create mode 100644 homeassistant/components/insteon_plm/services.yaml diff --git a/homeassistant/components/binary_sensor/insteon_plm.py b/homeassistant/components/binary_sensor/insteon_plm.py index 06079d6aa3b..9cb87b31749 100644 --- a/homeassistant/components/binary_sensor/insteon_plm.py +++ b/homeassistant/components/binary_sensor/insteon_plm.py @@ -23,7 +23,7 @@ SENSOR_TYPES = {'openClosedSensor': 'opening', @asyncio.coroutine def async_setup_platform(hass, config, async_add_devices, discovery_info=None): """Set up the INSTEON PLM device class for the hass platform.""" - plm = hass.data['insteon_plm'] + plm = hass.data['insteon_plm'].get('plm') address = discovery_info['address'] device = plm.devices[address] diff --git a/homeassistant/components/fan/insteon_plm.py b/homeassistant/components/fan/insteon_plm.py index f30abdbaa30..0911295d090 100644 --- a/homeassistant/components/fan/insteon_plm.py +++ b/homeassistant/components/fan/insteon_plm.py @@ -31,7 +31,7 @@ _LOGGER = logging.getLogger(__name__) @asyncio.coroutine def async_setup_platform(hass, config, async_add_devices, discovery_info=None): """Set up the INSTEON PLM device class for the hass platform.""" - plm = hass.data['insteon_plm'] + plm = hass.data['insteon_plm'].get('plm') address = discovery_info['address'] device = plm.devices[address] diff --git a/homeassistant/components/insteon_plm.py b/homeassistant/components/insteon_plm/__init__.py similarity index 57% rename from homeassistant/components/insteon_plm.py rename to homeassistant/components/insteon_plm/__init__.py index d867f0c3d28..246e84ec71f 100644 --- a/homeassistant/components/insteon_plm.py +++ b/homeassistant/components/insteon_plm/__init__.py @@ -11,12 +11,13 @@ import voluptuous as vol from homeassistant.core import callback from homeassistant.const import (CONF_PORT, EVENT_HOMEASSISTANT_STOP, - CONF_PLATFORM) + CONF_PLATFORM, + CONF_ENTITY_ID) import homeassistant.helpers.config_validation as cv from homeassistant.helpers import discovery from homeassistant.helpers.entity import Entity -REQUIREMENTS = ['insteonplm==0.8.6'] +REQUIREMENTS = ['insteonplm==0.9.1'] _LOGGER = logging.getLogger(__name__) @@ -29,6 +30,17 @@ CONF_SUBCAT = 'subcat' CONF_FIRMWARE = 'firmware' CONF_PRODUCT_KEY = 'product_key' +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_ALL_LINK_GROUP = 'group' +SRV_ALL_LINK_MODE = 'mode' +SRV_LOAD_DB_RELOAD = 'reload' +SRV_CONTROLLER = 'controller' +SRV_RESPONDER = 'responder' + CONF_DEVICE_OVERRIDE_SCHEMA = vol.All( cv.deprecated(CONF_PLATFORM), vol.Schema({ vol.Required(CONF_ADDRESS): cv.string, @@ -47,6 +59,24 @@ CONFIG_SCHEMA = vol.Schema({ }) }, extra=vol.ALLOW_EXTRA) +ADD_ALL_LINK_SCHEMA = vol.Schema({ + vol.Required(SRV_ALL_LINK_GROUP): vol.Range(min=0, max=255), + vol.Required(SRV_ALL_LINK_MODE): vol.In([SRV_CONTROLLER, SRV_RESPONDER]), + }) + +DEL_ALL_LINK_SCHEMA = vol.Schema({ + vol.Required(SRV_ALL_LINK_GROUP): vol.Range(min=0, max=255), + }) + +LOAD_ALDB_SCHEMA = vol.Schema({ + vol.Required(CONF_ENTITY_ID): cv.entity_id, + vol.Optional(SRV_LOAD_DB_RELOAD, default='false'): cv.boolean, + }) + +PRINT_ALDB_SCHEMA = vol.Schema({ + vol.Required(CONF_ENTITY_ID): cv.entity_id, + }) + @asyncio.coroutine def async_setup(hass, config): @@ -54,6 +84,7 @@ def async_setup(hass, config): import insteonplm ipdb = IPDB() + plm = None conf = config[DOMAIN] port = conf.get(CONF_PORT) @@ -79,6 +110,60 @@ def async_setup(hass, config): 'state_key': state_key}, hass_config=config)) + def add_all_link(service): + """Add an INSTEON All-Link between two devices.""" + group = service.data.get(SRV_ALL_LINK_GROUP) + mode = service.data.get(SRV_ALL_LINK_MODE) + link_mode = 1 if mode.lower() == SRV_CONTROLLER else 0 + plm.start_all_linking(link_mode, group) + + def del_all_link(service): + """Delete an INSTEON All-Link between two devices.""" + group = service.data.get(SRV_ALL_LINK_GROUP) + plm.start_all_linking(255, group) + + def load_aldb(service): + """Load the device All-Link database.""" + entity_id = service.data.get(CONF_ENTITY_ID) + reload = service.data.get(SRV_LOAD_DB_RELOAD) + entities = hass.data[DOMAIN].get('entities') + entity = entities.get(entity_id) + if entity: + entity.load_aldb(reload) + else: + _LOGGER.error('Entity %s is not an INSTEON device', entity_id) + + def print_aldb(service): + """Print the All-Link Database for a device.""" + # For now this sends logs to the log file. + # Furture direction is to create an INSTEON control panel. + entity_id = service.data.get(CONF_ENTITY_ID) + entities = hass.data[DOMAIN].get('entities') + entity = entities.get(entity_id) + if entity: + entity.print_aldb() + else: + _LOGGER.error('Entity %s is not an INSTEON device', entity_id) + + def print_im_aldb(service): + """Print the All-Link Database for a device.""" + # For now this sends logs to the log file. + # Furture direction is to create an INSTEON control panel. + print_aldb_to_log(plm.aldb) + + def _register_services(): + hass.services.register(DOMAIN, SRV_ADD_ALL_LINK, add_all_link, + schema=ADD_ALL_LINK_SCHEMA) + hass.services.register(DOMAIN, SRV_DEL_ALL_LINK, del_all_link, + schema=DEL_ALL_LINK_SCHEMA) + hass.services.register(DOMAIN, SRV_LOAD_ALDB, load_aldb, + schema=LOAD_ALDB_SCHEMA) + hass.services.register(DOMAIN, SRV_PRINT_ALDB, print_aldb, + schema=PRINT_ALDB_SCHEMA) + hass.services.register(DOMAIN, SRV_PRINT_IM_ALDB, print_im_aldb, + schema=None) + _LOGGER.debug("Insteon_plm Services registered") + _LOGGER.info("Looking for PLM on %s", port) conn = yield from insteonplm.Connection.create( device=port, @@ -100,11 +185,14 @@ def async_setup(hass, config): plm.devices.add_override(address, CONF_PRODUCT_KEY, device_override[prop]) - hass.data['insteon_plm'] = plm + hass.data[DOMAIN] = {} + hass.data[DOMAIN]['plm'] = plm + hass.data[DOMAIN]['entities'] = {} hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, conn.close) plm.devices.add_device_callback(async_plm_new_device) + hass.async_add_job(_register_services) return True @@ -169,6 +257,7 @@ class InsteonPLMEntity(Entity): """Initialize the INSTEON PLM binary sensor.""" self._insteon_device_state = device.states[state_key] self._insteon_device = device + self._insteon_device.aldb.add_loaded_callback(self._aldb_loaded) @property def should_poll(self): @@ -215,3 +304,44 @@ class InsteonPLMEntity(Entity): """Register INSTEON update events.""" self._insteon_device_state.register_updates( self.async_entity_update) + self.hass.data[DOMAIN]['entities'][self.entity_id] = self + + def load_aldb(self, reload=False): + """Load the device All-Link Database.""" + if reload: + self._insteon_device.aldb.clear() + self._insteon_device.read_aldb() + + def print_aldb(self): + """Print the device ALDB to the log file.""" + print_aldb_to_log(self._insteon_device.aldb) + + @callback + def _aldb_loaded(self): + """All-Link Database loaded for the device.""" + self.print_aldb() + + +def print_aldb_to_log(aldb): + """Print the All-Link Database to the log file.""" + from insteonplm.devices import ALDBStatus + _LOGGER.info('ALDB load status is %s', aldb.status.name) + if aldb.status not in [ALDBStatus.LOADED, ALDBStatus.PARTIAL]: + _LOGGER.warning('Device All-Link database not loaded') + _LOGGER.warning('Use service insteon_plm.load_aldb first') + return + + _LOGGER.info('RecID In Use Mode HWM Group Address Data 1 Data 2 Data 3') + _LOGGER.info('----- ------ ---- --- ----- -------- ------ ------ ------') + for mem_addr in aldb: + rec = aldb[mem_addr] + # For now we write this to the log + # Roadmap is to create a configuration panel + in_use = 'Y' if rec.control_flags.is_in_use else 'N' + mode = 'C' if rec.control_flags.is_controller else 'R' + hwm = 'Y' if rec.control_flags.is_high_water_mark else 'N' + _LOGGER.info(' {:04x} {:s} {:s} {:s} {:3d} {:s}' + ' {:3d} {:3d} {:3d}'.format( + rec.mem_addr, in_use, mode, hwm, + rec.group, rec.address.human, + rec.data1, rec.data2, rec.data3)) diff --git a/homeassistant/components/insteon_plm/services.yaml b/homeassistant/components/insteon_plm/services.yaml new file mode 100644 index 00000000000..a0e250fef1f --- /dev/null +++ b/homeassistant/components/insteon_plm/services.yaml @@ -0,0 +1,32 @@ +add_all_link: + description: Tells the Insteom Modem (IM) start All-Linking mode. Once the the IM is in All-Linking mode, press the link button on the device to complete All-Linking. + fields: + group: + description: All-Link group number. + example: 1 + mode: + description: Linking mode controller - IM is controller responder - IM is responder + example: 'controller' +delete_all_link: + description: Tells the Insteon Modem (IM) to remove an All-Link record from the All-Link Database of the IM and a device. Once the IM is set to delete the link, press the link button on the corresponding device to complete the process. + fields: + group: + description: All-Link group number. + example: 1 +load_all_link_database: + description: Load the All-Link Database for a device. WARNING - Loading a device All-LInk database is very time consuming and inconsistant. This may take a LONG time and may need to be repeated to obtain all records. + fields: + entity_id: + description: Name of the device to print + example: 'light.1a2b3c' + reload: + description: Reload all records. If true the current records are cleared from memory (does not effect the device) and the records are reloaded. If false the existing records are left in place and only missing records are added. Default is false. + example: 'true' +print_all_link_database: + description: Print the All-Link Database for a device. Requires that the All-Link Database is loaded into memory. + fields: + entity_id: + description: Name of the device to print + example: 'light.1a2b3c' +print_im_all_link_database: + description: Print the All-Link Database for the INSTEON Modem (IM). diff --git a/homeassistant/components/light/insteon_plm.py b/homeassistant/components/light/insteon_plm.py index 40453da38e5..8a3b463c2bd 100644 --- a/homeassistant/components/light/insteon_plm.py +++ b/homeassistant/components/light/insteon_plm.py @@ -21,7 +21,7 @@ MAX_BRIGHTNESS = 255 @asyncio.coroutine def async_setup_platform(hass, config, async_add_devices, discovery_info=None): """Set up the Insteon PLM device.""" - plm = hass.data['insteon_plm'] + plm = hass.data['insteon_plm'].get('plm') address = discovery_info['address'] device = plm.devices[address] diff --git a/homeassistant/components/sensor/insteon_plm.py b/homeassistant/components/sensor/insteon_plm.py index a72b8efbc05..61f5877ed78 100644 --- a/homeassistant/components/sensor/insteon_plm.py +++ b/homeassistant/components/sensor/insteon_plm.py @@ -18,7 +18,7 @@ _LOGGER = logging.getLogger(__name__) @asyncio.coroutine def async_setup_platform(hass, config, async_add_devices, discovery_info=None): """Set up the INSTEON PLM device class for the hass platform.""" - plm = hass.data['insteon_plm'] + plm = hass.data['insteon_plm'].get('plm') address = discovery_info['address'] device = plm.devices[address] diff --git a/homeassistant/components/switch/insteon_plm.py b/homeassistant/components/switch/insteon_plm.py index 5f9482ce955..be562e9d909 100644 --- a/homeassistant/components/switch/insteon_plm.py +++ b/homeassistant/components/switch/insteon_plm.py @@ -18,7 +18,7 @@ _LOGGER = logging.getLogger(__name__) @asyncio.coroutine def async_setup_platform(hass, config, async_add_devices, discovery_info=None): """Set up the INSTEON PLM device class for the hass platform.""" - plm = hass.data['insteon_plm'] + plm = hass.data['insteon_plm'].get('plm') address = discovery_info['address'] device = plm.devices[address] diff --git a/requirements_all.txt b/requirements_all.txt index d24c2e07043..e9033091f3e 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -446,7 +446,7 @@ influxdb==5.0.0 insteonlocal==0.53 # homeassistant.components.insteon_plm -insteonplm==0.8.6 +insteonplm==0.9.1 # homeassistant.components.verisure jsonpath==0.75 From a4b69833d4e9ac99cc3f196e5cbb2fa5c5f644c4 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sat, 5 May 2018 11:32:36 -0400 Subject: [PATCH 139/155] Update translations --- .../components/deconz/.translations/bg.json | 25 +++++++++++++ .../components/deconz/.translations/cy.json | 26 ++++++++++++++ .../components/deconz/.translations/da.json | 11 ++++++ .../components/deconz/.translations/de.json | 26 ++++++++++++++ .../components/deconz/.translations/en.json | 36 +++++++++---------- .../components/deconz/.translations/hu.json | 22 ++++++++++++ .../components/deconz/.translations/ko.json | 26 ++++++++++++++ .../components/deconz/.translations/lb.json | 26 ++++++++++++++ .../components/deconz/.translations/nl.json | 26 ++++++++++++++ .../components/deconz/.translations/no.json | 26 ++++++++++++++ .../components/deconz/.translations/pl.json | 26 ++++++++++++++ .../components/deconz/.translations/pt.json | 7 ++++ .../components/deconz/.translations/ru.json | 26 ++++++++++++++ .../components/deconz/.translations/sl.json | 26 ++++++++++++++ .../deconz/.translations/zh-Hans.json | 26 ++++++++++++++ .../deconz/.translations/zh-Hant.json | 25 +++++++++++++ .../components/hue/.translations/bg.json | 29 +++++++++++++++ .../components/hue/.translations/cy.json | 29 +++++++++++++++ .../components/hue/.translations/da.json | 19 ++++++++++ .../components/hue/.translations/de.json | 5 ++- .../components/hue/.translations/en.json | 5 ++- .../components/hue/.translations/es.json | 11 ++++++ .../components/hue/.translations/hu.json | 28 +++++++++++++++ .../components/hue/.translations/it.json | 10 ++++++ .../components/hue/.translations/ko.json | 5 ++- .../components/hue/.translations/lb.json | 29 +++++++++++++++ .../components/hue/.translations/nl.json | 7 ++-- .../components/hue/.translations/no.json | 5 ++- .../components/hue/.translations/pl.json | 5 ++- .../components/hue/.translations/pt.json | 5 +++ .../components/hue/.translations/ru.json | 29 +++++++++++++++ .../components/hue/.translations/sl.json | 5 ++- .../components/hue/.translations/zh-Hans.json | 7 ++-- .../components/hue/.translations/zh-Hant.json | 29 +++++++++++++++ .../sensor/.translations/season.bg.json | 8 +++++ .../sensor/.translations/season.da.json | 8 +++++ .../sensor/.translations/season.hu.json | 8 +++++ .../sensor/.translations/season.it.json | 8 +++++ .../sensor/.translations/season.lb.json | 8 +++++ .../sensor/.translations/season.ru.json | 8 +++++ .../components/zone/.translations/cy.json | 21 +++++++++++ .../components/zone/.translations/de.json | 21 +++++++++++ .../components/zone/.translations/en.json | 30 ++++++++-------- .../components/zone/.translations/ko.json | 21 +++++++++++ .../components/zone/.translations/lb.json | 21 +++++++++++ .../components/zone/.translations/nl.json | 21 +++++++++++ .../components/zone/.translations/no.json | 21 +++++++++++ .../components/zone/.translations/pl.json | 21 +++++++++++ .../components/zone/.translations/pt.json | 20 +++++++++++ .../components/zone/.translations/ru.json | 21 +++++++++++ .../zone/.translations/zh-Hans.json | 21 +++++++++++ 51 files changed, 892 insertions(+), 43 deletions(-) create mode 100644 homeassistant/components/deconz/.translations/bg.json create mode 100644 homeassistant/components/deconz/.translations/cy.json create mode 100644 homeassistant/components/deconz/.translations/da.json create mode 100644 homeassistant/components/deconz/.translations/de.json create mode 100644 homeassistant/components/deconz/.translations/hu.json create mode 100644 homeassistant/components/deconz/.translations/ko.json create mode 100644 homeassistant/components/deconz/.translations/lb.json create mode 100644 homeassistant/components/deconz/.translations/nl.json create mode 100644 homeassistant/components/deconz/.translations/no.json create mode 100644 homeassistant/components/deconz/.translations/pl.json create mode 100644 homeassistant/components/deconz/.translations/pt.json create mode 100644 homeassistant/components/deconz/.translations/ru.json create mode 100644 homeassistant/components/deconz/.translations/sl.json create mode 100644 homeassistant/components/deconz/.translations/zh-Hans.json create mode 100644 homeassistant/components/deconz/.translations/zh-Hant.json create mode 100644 homeassistant/components/hue/.translations/bg.json create mode 100644 homeassistant/components/hue/.translations/cy.json create mode 100644 homeassistant/components/hue/.translations/da.json create mode 100644 homeassistant/components/hue/.translations/es.json create mode 100644 homeassistant/components/hue/.translations/hu.json create mode 100644 homeassistant/components/hue/.translations/it.json create mode 100644 homeassistant/components/hue/.translations/lb.json create mode 100644 homeassistant/components/hue/.translations/pt.json create mode 100644 homeassistant/components/hue/.translations/ru.json create mode 100644 homeassistant/components/hue/.translations/zh-Hant.json create mode 100644 homeassistant/components/sensor/.translations/season.bg.json create mode 100644 homeassistant/components/sensor/.translations/season.da.json create mode 100644 homeassistant/components/sensor/.translations/season.hu.json create mode 100644 homeassistant/components/sensor/.translations/season.it.json create mode 100644 homeassistant/components/sensor/.translations/season.lb.json create mode 100644 homeassistant/components/sensor/.translations/season.ru.json create mode 100644 homeassistant/components/zone/.translations/cy.json create mode 100644 homeassistant/components/zone/.translations/de.json create mode 100644 homeassistant/components/zone/.translations/ko.json create mode 100644 homeassistant/components/zone/.translations/lb.json create mode 100644 homeassistant/components/zone/.translations/nl.json create mode 100644 homeassistant/components/zone/.translations/no.json create mode 100644 homeassistant/components/zone/.translations/pl.json create mode 100644 homeassistant/components/zone/.translations/pt.json create mode 100644 homeassistant/components/zone/.translations/ru.json create mode 100644 homeassistant/components/zone/.translations/zh-Hans.json diff --git a/homeassistant/components/deconz/.translations/bg.json b/homeassistant/components/deconz/.translations/bg.json new file mode 100644 index 00000000000..91727cae257 --- /dev/null +++ b/homeassistant/components/deconz/.translations/bg.json @@ -0,0 +1,25 @@ +{ + "config": { + "abort": { + "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" + }, + "error": { + "no_key": "\u041d\u0435 \u043c\u043e\u0436\u0430 \u0434\u0430 \u0441\u0435 \u043f\u043e\u043b\u0443\u0447\u0438 API \u043a\u043b\u044e\u0447" + }, + "step": { + "init": { + "data": { + "host": "\u0425\u043e\u0441\u0442", + "port": "\u041f\u043e\u0440\u0442 (\u0441\u0442\u043e\u0439\u043d\u043e\u0441\u0442 \u043f\u043e \u043f\u043e\u0434\u0440\u0430\u0437\u0431\u0438\u0440\u0430\u043d\u0435: '80')" + }, + "title": "\u0414\u0435\u0444\u0438\u043d\u0438\u0440\u0430\u043d\u0435 \u043d\u0430 deCONZ \u0448\u043b\u044e\u0437" + }, + "link": { + "description": "\u041e\u0442\u043a\u043b\u044e\u0447\u0438 deCONZ \u0448\u043b\u044e\u0437\u0430 \u0437\u0430 \u0434\u0430 \u0441\u0435 \u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0438\u0440\u0430 \u0441 Home Assistant.\n\n1. \u041e\u0442\u0432\u043e\u0440\u0435\u0442\u0435 \u0441\u0438\u0441\u0442\u0435\u043c\u043d\u0438\u0442\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u043d\u0430 deCONZ\n2. \u041d\u0430\u0442\u0438\u0441\u043d\u0435\u0442\u0435 \u0431\u0443\u0442\u043e\u043d\u0430 \"Unlock Gateway\"", + "title": "\u0412\u0440\u044a\u0437\u043a\u0430 \u0441 deCONZ" + } + }, + "title": "deCONZ" + } +} \ No newline at end of file diff --git a/homeassistant/components/deconz/.translations/cy.json b/homeassistant/components/deconz/.translations/cy.json new file mode 100644 index 00000000000..fff54bb3f6c --- /dev/null +++ b/homeassistant/components/deconz/.translations/cy.json @@ -0,0 +1,26 @@ +{ + "config": { + "abort": { + "already_configured": "Pont eisoes wedi'i ffurfweddu", + "no_bridges": "Dim pontydd deCONZ wedi eu darganfod", + "one_instance_only": "Elfen dim ond yn cefnogi enghraifft deCONZ" + }, + "error": { + "no_key": "Methu cael allwedd API" + }, + "step": { + "init": { + "data": { + "host": "Gwesteiwr", + "port": "Port (gwerth diofyn: '80')" + }, + "title": "Diffiniwch porth dad-adeiladu" + }, + "link": { + "description": "Datgloi eich porth deCONZ i gofrestru gyda Cynorthwydd Cartref.\n\n1. Ewch i osodiadau system deCONZ \n2. Bwyso botwm \"Datgloi porth\"", + "title": "Cysylltu \u00e2 deCONZ" + } + }, + "title": "deCONZ" + } +} \ No newline at end of file diff --git a/homeassistant/components/deconz/.translations/da.json b/homeassistant/components/deconz/.translations/da.json new file mode 100644 index 00000000000..698f55c59ec --- /dev/null +++ b/homeassistant/components/deconz/.translations/da.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "init": { + "data": { + "host": "V\u00e6rt" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/deconz/.translations/de.json b/homeassistant/components/deconz/.translations/de.json new file mode 100644 index 00000000000..9d3dc9e6e62 --- /dev/null +++ b/homeassistant/components/deconz/.translations/de.json @@ -0,0 +1,26 @@ +{ + "config": { + "abort": { + "already_configured": "Bridge ist bereits konfiguriert", + "no_bridges": "Keine deCON-Bridges entdeckt", + "one_instance_only": "Komponente unterst\u00fctzt nur eine deCONZ-Instanz" + }, + "error": { + "no_key": "Es konnte kein API-Schl\u00fcssel abgerufen werden" + }, + "step": { + "init": { + "data": { + "host": "Host", + "port": "Port (Standartwert : '80')" + }, + "title": "Definieren Sie den deCONZ-Gateway" + }, + "link": { + "description": "Entsperren Sie Ihr deCONZ-Gateway, um sich bei Home Assistant zu registrieren. \n\n 1. Gehen Sie zu den deCONZ-Systemeinstellungen \n 2. Dr\u00fccken Sie die Taste \"Gateway entsperren\"", + "title": "Mit deCONZ verbinden" + } + }, + "title": "deCONZ" + } +} \ No newline at end of file diff --git a/homeassistant/components/deconz/.translations/en.json b/homeassistant/components/deconz/.translations/en.json index 7ea68af01c1..0009986d45f 100644 --- a/homeassistant/components/deconz/.translations/en.json +++ b/homeassistant/components/deconz/.translations/en.json @@ -1,26 +1,26 @@ { "config": { - "title": "deCONZ", - "step": { - "init": { - "title": "Define deCONZ gateway", - "data": { - "host": "Host", - "port": "Port (default value: '80')" - } - }, - "link": { - "title": "Link with deCONZ", - "description": "Unlock your deCONZ gateway to register with Home Assistant.\n\n1. Go to deCONZ system settings\n2. Press \"Unlock Gateway\" button" - } - }, - "error": { - "no_key": "Couldn't get an API key" - }, "abort": { "already_configured": "Bridge is already configured", "no_bridges": "No deCONZ bridges discovered", "one_instance_only": "Component only supports one deCONZ instance" - } + }, + "error": { + "no_key": "Couldn't get an API key" + }, + "step": { + "init": { + "data": { + "host": "Host", + "port": "Port (default value: '80')" + }, + "title": "Define deCONZ gateway" + }, + "link": { + "description": "Unlock your deCONZ gateway to register with Home Assistant.\n\n1. Go to deCONZ system settings\n2. Press \"Unlock Gateway\" button", + "title": "Link with deCONZ" + } + }, + "title": "deCONZ" } } \ No newline at end of file diff --git a/homeassistant/components/deconz/.translations/hu.json b/homeassistant/components/deconz/.translations/hu.json new file mode 100644 index 00000000000..42aab9c6d7e --- /dev/null +++ b/homeassistant/components/deconz/.translations/hu.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "one_instance_only": "Ez a komponens csak egy deCONZ egys\u00e9get t\u00e1mogat" + }, + "error": { + "no_key": "API kulcs lek\u00e9r\u00e9se nem siker\u00fclt" + }, + "step": { + "init": { + "data": { + "host": "H\u00e1zigazda (Host)", + "port": "Port (alap\u00e9rtelmezett \u00e9rt\u00e9k: '80')" + } + }, + "link": { + "title": "Kapcsol\u00f3d\u00e1s a deCONZ-hoz" + } + }, + "title": "deCONZ" + } +} \ No newline at end of file diff --git a/homeassistant/components/deconz/.translations/ko.json b/homeassistant/components/deconz/.translations/ko.json new file mode 100644 index 00000000000..d6de1028218 --- /dev/null +++ b/homeassistant/components/deconz/.translations/ko.json @@ -0,0 +1,26 @@ +{ + "config": { + "abort": { + "already_configured": "\ube0c\ub9bf\uc9c0\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4", + "no_bridges": "\ubc1c\uacac\ub41c deCONZ \ube0c\ub9bf\uc9c0\uac00 \uc5c6\uc2b5\ub2c8\ub2e4", + "one_instance_only": "\uad6c\uc131\uc694\uc18c\ub294 \ud558\ub098\uc758 deCONZ \uc778\uc2a4\ud134\uc2a4 \ub9cc \uc9c0\uc6d0\ud569\ub2c8\ub2e4" + }, + "error": { + "no_key": "API \ud0a4\ub97c \uac00\uc838\uc62c \uc218 \uc5c6\uc2b5\ub2c8\ub2e4" + }, + "step": { + "init": { + "data": { + "host": "\ud638\uc2a4\ud2b8", + "port": "\ud3ec\ud2b8 (\uae30\ubcf8\uac12: '80')" + }, + "title": "deCONZ \uac8c\uc774\ud2b8\uc6e8\uc774 \uc815\uc758" + }, + "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" + } +} \ No newline at end of file diff --git a/homeassistant/components/deconz/.translations/lb.json b/homeassistant/components/deconz/.translations/lb.json new file mode 100644 index 00000000000..2a9dfc5e543 --- /dev/null +++ b/homeassistant/components/deconz/.translations/lb.json @@ -0,0 +1,26 @@ +{ + "config": { + "abort": { + "already_configured": "Bridge ass schon konfigur\u00e9iert", + "no_bridges": "Keng dECONZ bridges fonnt", + "one_instance_only": "Komponent \u00ebnnerst\u00ebtzt n\u00ebmmen eng deCONZ Instanz" + }, + "error": { + "no_key": "Konnt keen API Schl\u00ebssel kr\u00e9ien" + }, + "step": { + "init": { + "data": { + "host": "Host", + "port": "Port (Standard Wert: '80')" + }, + "title": "deCONZ gateway d\u00e9fin\u00e9ieren" + }, + "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" + } + }, + "title": "deCONZ" + } +} \ No newline at end of file diff --git a/homeassistant/components/deconz/.translations/nl.json b/homeassistant/components/deconz/.translations/nl.json new file mode 100644 index 00000000000..90d13bb39b4 --- /dev/null +++ b/homeassistant/components/deconz/.translations/nl.json @@ -0,0 +1,26 @@ +{ + "config": { + "abort": { + "already_configured": "Bridge is al geconfigureerd", + "no_bridges": "Geen deCONZ bruggen ontdekt", + "one_instance_only": "Component ondersteunt slechts \u00e9\u00e9n deCONZ instance" + }, + "error": { + "no_key": "Kon geen API-sleutel ophalen" + }, + "step": { + "init": { + "data": { + "host": "Host", + "port": "Poort (standaard: '80')" + }, + "title": "Definieer deCONZ gateway" + }, + "link": { + "description": "Ontgrendel je deCONZ gateway om te registreren met Home Assistant.\n\n1. Ga naar deCONZ systeeminstellingen\n2. Druk op de knop \"Gateway ontgrendelen\"", + "title": "Koppel met deCONZ" + } + }, + "title": "deCONZ" + } +} \ No newline at end of file diff --git a/homeassistant/components/deconz/.translations/no.json b/homeassistant/components/deconz/.translations/no.json new file mode 100644 index 00000000000..25e3b0b7d68 --- /dev/null +++ b/homeassistant/components/deconz/.translations/no.json @@ -0,0 +1,26 @@ +{ + "config": { + "abort": { + "already_configured": "Broen er allerede konfigurert", + "no_bridges": "Ingen deCONZ broer oppdaget", + "one_instance_only": "Komponenten st\u00f8tter bare \u00e9n deCONZ forekomst" + }, + "error": { + "no_key": "Kunne ikke f\u00e5 en API-n\u00f8kkel" + }, + "step": { + "init": { + "data": { + "host": "Vert", + "port": "Port (standardverdi: '80')" + }, + "title": "Definer deCONZ-gatewayen" + }, + "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" + } + }, + "title": "deCONZ" + } +} \ No newline at end of file diff --git a/homeassistant/components/deconz/.translations/pl.json b/homeassistant/components/deconz/.translations/pl.json new file mode 100644 index 00000000000..bb7488fcbec --- /dev/null +++ b/homeassistant/components/deconz/.translations/pl.json @@ -0,0 +1,26 @@ +{ + "config": { + "abort": { + "already_configured": "Mostek jest ju\u017c skonfigurowany", + "no_bridges": "Nie odkryto mostk\u00f3w deCONZ", + "one_instance_only": "Komponent obs\u0142uguje tylko jedn\u0105 instancj\u0119 deCONZ" + }, + "error": { + "no_key": "Nie mo\u017cna uzyska\u0107 klucza API" + }, + "step": { + "init": { + "data": { + "host": "Host", + "port": "Port (warto\u015b\u0107 domy\u015blna: \"80\")" + }, + "title": "Zdefiniuj bramk\u0119 deCONZ" + }, + "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" + } + }, + "title": "deCONZ" + } +} \ No newline at end of file diff --git a/homeassistant/components/deconz/.translations/pt.json b/homeassistant/components/deconz/.translations/pt.json new file mode 100644 index 00000000000..2a00c698691 --- /dev/null +++ b/homeassistant/components/deconz/.translations/pt.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "Bridge j\u00e1 est\u00e1 configurada" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/deconz/.translations/ru.json b/homeassistant/components/deconz/.translations/ru.json new file mode 100644 index 00000000000..b0dc6a8a4a8 --- /dev/null +++ b/homeassistant/components/deconz/.translations/ru.json @@ -0,0 +1,26 @@ +{ + "config": { + "abort": { + "already_configured": "\u0428\u043b\u044e\u0437 \u0443\u0436\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0435\u043d", + "no_bridges": "\u0428\u043b\u044e\u0437\u044b deCONZ \u043d\u0435 \u043d\u0430\u0439\u0434\u0435\u043d\u044b", + "one_instance_only": "\u041a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442 \u043f\u043e\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u0442 \u0442\u043e\u043b\u044c\u043a\u043e \u043e\u0434\u0438\u043d \u044d\u043a\u0437\u0435\u043c\u043f\u043b\u044f\u0440 deCONZ" + }, + "error": { + "no_key": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u043b\u0443\u0447\u0438\u0442\u044c \u043a\u043b\u044e\u0447 API" + }, + "step": { + "init": { + "data": { + "host": "\u0425\u043e\u0441\u0442", + "port": "\u041f\u043e\u0440\u0442 (\u043f\u043e \u0443\u043c\u043e\u043b\u0447\u0430\u043d\u0438\u044e: '80')" + }, + "title": "\u041e\u043f\u0440\u0435\u0434\u0435\u043b\u0438\u0442\u044c \u0448\u043b\u044e\u0437 deCONZ" + }, + "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" + } + }, + "title": "deCONZ" + } +} \ No newline at end of file diff --git a/homeassistant/components/deconz/.translations/sl.json b/homeassistant/components/deconz/.translations/sl.json new file mode 100644 index 00000000000..b738002b273 --- /dev/null +++ b/homeassistant/components/deconz/.translations/sl.json @@ -0,0 +1,26 @@ +{ + "config": { + "abort": { + "already_configured": "Most je \u017ee nastavljen", + "no_bridges": "Ni odkritih mostov deCONZ", + "one_instance_only": "Komponenta podpira le en primerek deCONZ" + }, + "error": { + "no_key": "Klju\u010da API ni mogo\u010de dobiti" + }, + "step": { + "init": { + "data": { + "host": "Gostitelj", + "port": "Vrata (privzeta vrednost: '80')" + }, + "title": "Dolo\u010dite deCONZ prehod" + }, + "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" + } + }, + "title": "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 new file mode 100644 index 00000000000..f41b5b5111c --- /dev/null +++ b/homeassistant/components/deconz/.translations/zh-Hans.json @@ -0,0 +1,26 @@ +{ + "config": { + "abort": { + "already_configured": "\u6865\u63a5\u5668\u5df2\u914d\u7f6e\u5b8c\u6210", + "no_bridges": "\u6ca1\u6709\u53d1\u73b0 deCONZ \u7684\u6865\u63a5\u8bbe\u5907", + "one_instance_only": "\u7ec4\u4ef6\u53ea\u652f\u6301\u4e00\u4e2a deCONZ \u5b9e\u4f8b" + }, + "error": { + "no_key": "\u65e0\u6cd5\u83b7\u53d6 API \u5bc6\u94a5" + }, + "step": { + "init": { + "data": { + "host": "\u4e3b\u673a", + "port": "\u7aef\u53e3\uff08\u9ed8\u8ba4\u503c\uff1a'80'\uff09" + }, + "title": "\u5b9a\u4e49 deCONZ \u7f51\u5173" + }, + "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" + } + }, + "title": "deCONZ" + } +} \ No newline at end of file diff --git a/homeassistant/components/deconz/.translations/zh-Hant.json b/homeassistant/components/deconz/.translations/zh-Hant.json new file mode 100644 index 00000000000..33be3846eb8 --- /dev/null +++ b/homeassistant/components/deconz/.translations/zh-Hant.json @@ -0,0 +1,25 @@ +{ + "config": { + "abort": { + "no_bridges": "\u672a\u641c\u5c0b\u5230 deCONZ Bridfe", + "one_instance_only": "\u7d44\u4ef6\u50c5\u652f\u63f4\u4e00\u7d44 deCONZ \u5be6\u4f8b" + }, + "error": { + "no_key": "\u7121\u6cd5\u53d6\u5f97 API key" + }, + "step": { + "init": { + "data": { + "host": "\u4e3b\u6a5f\u7aef", + "port": "\u901a\u8a0a\u57e0\uff08\u9810\u8a2d\u503c\uff1a'80'\uff09" + }, + "title": "\u5b9a\u7fa9 deCONZ \u7db2\u95dc" + }, + "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" + } + }, + "title": "deCONZ" + } +} \ No newline at end of file diff --git a/homeassistant/components/hue/.translations/bg.json b/homeassistant/components/hue/.translations/bg.json new file mode 100644 index 00000000000..276f5053bf7 --- /dev/null +++ b/homeassistant/components/hue/.translations/bg.json @@ -0,0 +1,29 @@ +{ + "config": { + "abort": { + "all_configured": "\u0412\u0441\u0438\u0447\u043a\u0438 \u0431\u0430\u0437\u043e\u0432\u0438 \u0441\u0442\u0430\u043d\u0446\u0438\u0438 Philips Hue \u0441\u0430 \u0432\u0435\u0447\u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u0438", + "already_configured": "\u0411\u0430\u0437\u043e\u0432\u0430\u0442\u0430 \u0441\u0442\u0430\u043d\u0446\u0438\u044f \u0435 \u0432\u0435\u0447\u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u0430", + "cannot_connect": "\u041d\u0435 \u043c\u043e\u0436\u0430 \u0434\u0430 \u0441\u0435 \u0441\u0432\u044a\u0440\u0436\u0435 \u0441 \u0431\u0430\u0437\u043e\u0432\u0430\u0442\u0430 \u0441\u0442\u0430\u043d\u0446\u0438\u044f", + "discover_timeout": "\u041d\u0435\u0432\u044a\u0437\u043c\u043e\u0436\u043d\u043e \u0435 \u0434\u0430 \u0431\u044a\u0434\u0430\u0442 \u043e\u0442\u043a\u0440\u0438\u0442\u0438 \u0431\u0430\u0437\u043e\u0432\u0438 \u0441\u0442\u0430\u043d\u0446\u0438\u0438 \u043d\u0430 Philips Hue", + "no_bridges": "\u041d\u0435 \u0441\u0430 \u043e\u0442\u043a\u0440\u0438\u0442\u0438 \u0431\u0430\u0437\u043e\u0432\u0438 \u0441\u0442\u0430\u043d\u0446\u0438\u0438 \u043d\u0430 Philips Hue", + "unknown": "\u041d\u0435\u0438\u0437\u0432\u0435\u0441\u0442\u043d\u0430 \u0433\u0440\u0435\u0448\u043a\u0430" + }, + "error": { + "linking": "\u041f\u043e\u044f\u0432\u0438 \u0441\u0435 \u043d\u0435\u0438\u0437\u0432\u0435\u0441\u0442\u043d\u0430 \u0433\u0440\u0435\u0448\u043a\u0430 \u043f\u0440\u0438 \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435\u0442\u043e.", + "register_failed": "\u0420\u0435\u0433\u0438\u0441\u0442\u0440\u0430\u0446\u0438\u044f\u0442\u0430 \u043d\u0435 \u0431\u0435 \u0443\u0441\u043f\u0435\u0448\u043d\u0430, \u043c\u043e\u043b\u044f \u043e\u043f\u0438\u0442\u0430\u0439\u0442\u0435 \u043e\u0442\u043d\u043e\u0432\u043e" + }, + "step": { + "init": { + "data": { + "host": "\u0425\u043e\u0441\u0442" + }, + "title": "\u0418\u0437\u0431\u0435\u0440\u0435\u0442\u0435 \u0431\u0430\u0437\u043e\u0432\u0430 \u0441\u0442\u0430\u043d\u0446\u0438\u044f Philips Hue" + }, + "link": { + "description": "\u041d\u0430\u0442\u0438\u0441\u043d\u0435\u0442\u0435 \u0431\u0443\u0442\u043e\u043d\u0430 \u043d\u0430 \u0431\u0430\u0437\u043e\u0432\u0430\u0442\u0430 \u0441\u0442\u0430\u043d\u0446\u0438\u044f, \u0437\u0430 \u0434\u0430 \u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0438\u0440\u0430\u0442\u0435 Philips Hue \u0441 Home Assistant. \n\n![\u041c\u0435\u0441\u0442\u043e\u043f\u043e\u043b\u043e\u0436\u0435\u043d\u0438\u0435 \u043d\u0430 \u0431\u0443\u0442\u043e\u043d\u0430 \u043d\u0430 \u0431\u0430\u0437\u043e\u0432\u0430\u0442\u0430 \u0441\u0442\u0430\u043d\u0446\u0438\u044f](/static/images/config_philips_hue.jpg)", + "title": "\u0421\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435 \u043d\u0430 \u0445\u044a\u0431" + } + }, + "title": "\u0411\u0430\u0437\u043e\u0432\u0430 \u0441\u0442\u0430\u043d\u0446\u0438\u044f Philips Hue" + } +} \ No newline at end of file diff --git a/homeassistant/components/hue/.translations/cy.json b/homeassistant/components/hue/.translations/cy.json new file mode 100644 index 00000000000..f5476f73edb --- /dev/null +++ b/homeassistant/components/hue/.translations/cy.json @@ -0,0 +1,29 @@ +{ + "config": { + "abort": { + "all_configured": "Mae holl bontydd Philips Hue eisoes wedi eu ffurfweddu", + "already_configured": "Pont eisoes wedi'i ffurfweddu", + "cannot_connect": "Methu cysylltu i'r bont", + "discover_timeout": "Methu darganfod pontydd Hue", + "no_bridges": "Dim pontydd Philips Hue wedi'i ddarganfod", + "unknown": "Digwyddodd gwall anhysbys" + }, + "error": { + "linking": "Digwyddodd gwall cysylltu anhysbys.", + "register_failed": "Wedi methu \u00e2 chofrestru, pl\u00eds ceisiwch eto" + }, + "step": { + "init": { + "data": { + "host": "Gwesteiwr" + }, + "title": "Dewiswch bont Hue" + }, + "link": { + "description": "Pwyswch y botwm ar y bont i gofrestru Philips Hue gyda Cynorthwydd Cartref.\n\n![Lleoliad botwm ar bont](/static/images/config_philips_hue.jpg)", + "title": "Hwb cyswllt" + } + }, + "title": "Pont Phillips Hue" + } +} \ No newline at end of file diff --git a/homeassistant/components/hue/.translations/da.json b/homeassistant/components/hue/.translations/da.json new file mode 100644 index 00000000000..3e5e2b1d3d7 --- /dev/null +++ b/homeassistant/components/hue/.translations/da.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "no_bridges": "Ingen Philips Hue bridge fundet" + }, + "step": { + "init": { + "data": { + "host": "V\u00e6rt" + }, + "title": "V\u00e6lg Hue bridge" + }, + "link": { + "title": "Link Hub" + } + }, + "title": "Philips Hue Bridge" + } +} \ No newline at end of file diff --git a/homeassistant/components/hue/.translations/de.json b/homeassistant/components/hue/.translations/de.json index f11af7756c7..d466488e9fc 100644 --- a/homeassistant/components/hue/.translations/de.json +++ b/homeassistant/components/hue/.translations/de.json @@ -2,8 +2,11 @@ "config": { "abort": { "all_configured": "Alle Philips Hue Bridges sind bereits konfiguriert", + "already_configured": "Bridge ist bereits konfiguriert", + "cannot_connect": "Verbindung zur Bridge nicht m\u00f6glich", "discover_timeout": "Nicht in der Lage Hue Bridges zu entdecken", - "no_bridges": "Philips Hue Bridges entdeckt" + "no_bridges": "Keine Philips Hue Bridges entdeckt", + "unknown": "Unbekannter Fehler ist aufgetreten" }, "error": { "linking": "Unbekannter Link-Fehler aufgetreten.", diff --git a/homeassistant/components/hue/.translations/en.json b/homeassistant/components/hue/.translations/en.json index cbf63301da2..b0459ec3916 100644 --- a/homeassistant/components/hue/.translations/en.json +++ b/homeassistant/components/hue/.translations/en.json @@ -2,8 +2,11 @@ "config": { "abort": { "all_configured": "All Philips Hue bridges are already configured", + "already_configured": "Bridge is already configured", + "cannot_connect": "Unable to connect to the bridge", "discover_timeout": "Unable to discover Hue bridges", - "no_bridges": "No Philips Hue bridges discovered" + "no_bridges": "No Philips Hue bridges discovered", + "unknown": "Unknown error occurred" }, "error": { "linking": "Unknown linking error occurred.", diff --git a/homeassistant/components/hue/.translations/es.json b/homeassistant/components/hue/.translations/es.json new file mode 100644 index 00000000000..d58469af044 --- /dev/null +++ b/homeassistant/components/hue/.translations/es.json @@ -0,0 +1,11 @@ +{ + "config": { + "abort": { + "unknown": "Se produjo un error desconocido" + }, + "error": { + "linking": "Se produjo un error de enlace desconocido.", + "register_failed": "No se pudo registrar, intente de nuevo" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/hue/.translations/hu.json b/homeassistant/components/hue/.translations/hu.json new file mode 100644 index 00000000000..a4032dcbcfc --- /dev/null +++ b/homeassistant/components/hue/.translations/hu.json @@ -0,0 +1,28 @@ +{ + "config": { + "abort": { + "all_configured": "M\u00e1r minden Philips Hue bridge konfigur\u00e1lt", + "already_configured": "A bridge m\u00e1r konfigur\u00e1lt", + "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", + "unknown": "Ismeretlen hiba t\u00f6rt\u00e9nt" + }, + "error": { + "linking": "Ismeretlen \u00f6sszekapcsol\u00e1si hiba t\u00f6rt\u00e9nt.", + "register_failed": "Regisztr\u00e1ci\u00f3 nem siker\u00fclt, k\u00e9rem pr\u00f3b\u00e1lja \u00fajra" + }, + "step": { + "init": { + "data": { + "host": "H\u00e1zigazda (Host)" + }, + "title": "V\u00e1lassz Hue bridge-t" + }, + "link": { + "title": "Kapcsol\u00f3d\u00e1s a hubhoz" + } + }, + "title": "Philips Hue Bridge" + } +} \ No newline at end of file diff --git a/homeassistant/components/hue/.translations/it.json b/homeassistant/components/hue/.translations/it.json new file mode 100644 index 00000000000..2c7a8c1924d --- /dev/null +++ b/homeassistant/components/hue/.translations/it.json @@ -0,0 +1,10 @@ +{ + "config": { + "abort": { + "all_configured": "Tutti i bridge Philips Hue sono gi\u00e0 configurati", + "discover_timeout": "Impossibile trovare i bridge Hue", + "no_bridges": "Nessun bridge Hue di Philips trovato" + }, + "title": "Philips Hue Bridge" + } +} \ No newline at end of file diff --git a/homeassistant/components/hue/.translations/ko.json b/homeassistant/components/hue/.translations/ko.json index 226ae8ba1f6..47306a35414 100644 --- a/homeassistant/components/hue/.translations/ko.json +++ b/homeassistant/components/hue/.translations/ko.json @@ -2,8 +2,11 @@ "config": { "abort": { "all_configured": "\ubaa8\ub4e0 \ud544\ub9bd\uc2a4 Hue \ube0c\ub9bf\uc9c0\uac00 \uc774\ubbf8 \uc124\uc815\ub41c \uc0c1\ud0dc\uc785\ub2c8\ub2e4", + "already_configured": "\ube0c\ub9bf\uc9c0\uac00 \uc774\ubbf8 \uc124\uc815\ub41c \uc0c1\ud0dc\uc785\ub2c8\ub2e4", + "cannot_connect": "\ube0c\ub9ac\uc9c0\uc5d0 \uc5f0\uacb0\ud560 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4", "discover_timeout": "Hue \ube0c\ub9bf\uc9c0\ub97c \ucc3e\uc744 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4", - "no_bridges": "\ubc1c\uacac\ub41c \ud544\ub9bd\uc2a4 Hue \ube0c\ub9bf\uc9c0\uac00 \uc5c6\uc2b5\ub2c8\ub2e4" + "no_bridges": "\ubc1c\uacac\ub41c \ud544\ub9bd\uc2a4 Hue \ube0c\ub9bf\uc9c0\uac00 \uc5c6\uc2b5\ub2c8\ub2e4", + "unknown": "\uc54c \uc218\uc5c6\ub294 \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4" }, "error": { "linking": "\uc54c \uc218\uc5c6\ub294 \uc5f0\uacb0 \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4.", diff --git a/homeassistant/components/hue/.translations/lb.json b/homeassistant/components/hue/.translations/lb.json new file mode 100644 index 00000000000..c4ad10da278 --- /dev/null +++ b/homeassistant/components/hue/.translations/lb.json @@ -0,0 +1,29 @@ +{ + "config": { + "abort": { + "all_configured": "All Philips Hue Bridge si scho\u00a0konfigur\u00e9iert", + "already_configured": "Bridge ass scho konfigur\u00e9iert", + "cannot_connect": "Keng Verbindung mat der bridge m\u00e9iglech", + "discover_timeout": "Keng Hue bridge fonnt", + "no_bridges": "Keng Philips Hue Bridge fonnt", + "unknown": "Onbekannten Feeler opgetrueden" + }, + "error": { + "linking": "Onbekannte Liaisoun's Feeler opgetrueden", + "register_failed": "Feeler beim registr\u00e9ieren, prob\u00e9iert w.e.g. nach emol" + }, + "step": { + "init": { + "data": { + "host": "Host" + }, + "title": "Hue Bridge auswielen" + }, + "link": { + "description": "Dr\u00e9ckt de Kn\u00e4ppchen un der Bridge fir den Philips Hue mam Home Assistant ze registr\u00e9ieren.\n\n![Kn\u00e4ppchen un der Bridge](/static/images/config_philips_hue.jpg)", + "title": "Link Hub" + } + }, + "title": "Philips Hue Bridge" + } +} \ No newline at end of file diff --git a/homeassistant/components/hue/.translations/nl.json b/homeassistant/components/hue/.translations/nl.json index 750ae39db12..88c611b1633 100644 --- a/homeassistant/components/hue/.translations/nl.json +++ b/homeassistant/components/hue/.translations/nl.json @@ -2,8 +2,11 @@ "config": { "abort": { "all_configured": "Alle Philips Hue bridges zijn al geconfigureerd", + "already_configured": "Bridge is al geconfigureerd", + "cannot_connect": "Kan geen verbinding maken met bridge", "discover_timeout": "Hue bridges kunnen niet worden gevonden", - "no_bridges": "Geen Philips Hue bridges ontdekt" + "no_bridges": "Geen Philips Hue bridges ontdekt", + "unknown": "Onbekende fout opgetreden" }, "error": { "linking": "Er is een onbekende verbindingsfout opgetreden.", @@ -17,7 +20,7 @@ "title": "Kies Hue bridge" }, "link": { - "description": "Druk op de knop van de bridge om Philips Hue te registreren met de Home Assistant. ![Locatie van de knop op bridge] (/static/images/config_philips_hue.jpg)", + "description": "Druk op de knop van de bridge om Philips Hue te registreren met Home Assistant. ![Locatie van de knop op bridge] (/static/images/config_philips_hue.jpg)", "title": "Link Hub" } }, diff --git a/homeassistant/components/hue/.translations/no.json b/homeassistant/components/hue/.translations/no.json index 604475d2ff2..309e9f6a299 100644 --- a/homeassistant/components/hue/.translations/no.json +++ b/homeassistant/components/hue/.translations/no.json @@ -2,8 +2,11 @@ "config": { "abort": { "all_configured": "Alle Philips Hue Bridger er allerede konfigurert", + "already_configured": "Bridge er allerede konfigurert", + "cannot_connect": "Kan ikke koble til Bridge", "discover_timeout": "Kunne ikke oppdage Hue Bridger", - "no_bridges": "Ingen Philips Hue Bridger oppdaget" + "no_bridges": "Ingen Philips Hue Bridger oppdaget", + "unknown": "Ukjent feil oppstod" }, "error": { "linking": "Ukjent koblingsfeil oppstod.", diff --git a/homeassistant/components/hue/.translations/pl.json b/homeassistant/components/hue/.translations/pl.json index e364b7033a1..784fa0d99a6 100644 --- a/homeassistant/components/hue/.translations/pl.json +++ b/homeassistant/components/hue/.translations/pl.json @@ -2,8 +2,11 @@ "config": { "abort": { "all_configured": "Wszystkie mostki Hue s\u0105 ju\u017c skonfigurowane", + "already_configured": "Mostek jest ju\u017c skonfigurowany", + "cannot_connect": "Nie mo\u017cna po\u0142\u0105czy\u0107 si\u0119 z mostkiem", "discover_timeout": "Nie mo\u017cna wykry\u0107 \u017cadnych mostk\u00f3w Hue", - "no_bridges": "Nie wykryto \u017cadnych mostk\u00f3w Hue" + "no_bridges": "Nie wykryto \u017cadnych mostk\u00f3w Hue", + "unknown": "Wyst\u0105pi\u0142 nieznany b\u0142\u0105d" }, "error": { "linking": "Wyst\u0105pi\u0142 nieznany b\u0142\u0105d w trakcie \u0142\u0105czenia.", diff --git a/homeassistant/components/hue/.translations/pt.json b/homeassistant/components/hue/.translations/pt.json new file mode 100644 index 00000000000..8c4c45f9c89 --- /dev/null +++ b/homeassistant/components/hue/.translations/pt.json @@ -0,0 +1,5 @@ +{ + "config": { + "title": "" + } +} \ No newline at end of file diff --git a/homeassistant/components/hue/.translations/ru.json b/homeassistant/components/hue/.translations/ru.json new file mode 100644 index 00000000000..ea1e4fff1bf --- /dev/null +++ b/homeassistant/components/hue/.translations/ru.json @@ -0,0 +1,29 @@ +{ + "config": { + "abort": { + "all_configured": "\u0412\u0441\u0435 Philips Hue \u0448\u043b\u044e\u0437\u044b \u0443\u0436\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0435\u043d\u044b", + "already_configured": "\u0428\u043b\u044e\u0437 \u0443\u0436\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0435\u043d", + "cannot_connect": "\u041d\u0435\u0432\u043e\u0437\u043c\u043e\u0436\u043d\u043e \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f \u043a \u0448\u043b\u044e\u0437\u0443", + "discover_timeout": "\u041d\u0435\u0432\u043e\u0437\u043c\u043e\u0436\u043d\u043e \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0438\u0442\u044c \u0448\u043b\u044e\u0437\u044b Philips Hue", + "no_bridges": "\u0428\u043b\u044e\u0437\u044b Philips Hue \u043d\u0435 \u043d\u0430\u0439\u0434\u0435\u043d\u044b", + "unknown": "\u041d\u0435\u0438\u0437\u0432\u0435\u0441\u0442\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430" + }, + "error": { + "linking": "\u041d\u0435\u0438\u0437\u0432\u0435\u0441\u0442\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430 \u0441\u043e\u043f\u0440\u044f\u0436\u0435\u043d\u0438\u044f", + "register_failed": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u0437\u0430\u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0438\u0440\u043e\u0432\u0430\u0442\u044c\u0441\u044f, \u043f\u043e\u043f\u0440\u043e\u0431\u0443\u0439\u0442\u0435 \u0441\u043d\u043e\u0432\u0430" + }, + "step": { + "init": { + "data": { + "host": "\u0425\u043e\u0441\u0442" + }, + "title": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u0448\u043b\u044e\u0437 Hue" + }, + "link": { + "description": "\u041d\u0430\u0436\u043c\u0438\u0442\u0435 \u043a\u043d\u043e\u043f\u043a\u0443 \u043d\u0430 \u0448\u043b\u044e\u0437\u0435 \u0434\u043b\u044f \u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0430\u0446\u0438\u0438 Philips Hue \u0432 Home Assistant.\n\n![\u0420\u0430\u0441\u043f\u043e\u043b\u043e\u0436\u0435\u043d\u0438\u0435 \u043a\u043d\u043e\u043f\u043a\u0438 \u043d\u0430 \u0448\u043b\u044e\u0437\u0435](/static/images/config_philips_hue.jpg)", + "title": "\u0421\u0432\u044f\u0437\u044c \u0441 \u0445\u0430\u0431\u043e\u043c" + } + }, + "title": "\u0428\u043b\u044e\u0437 Philips Hue" + } +} \ No newline at end of file diff --git a/homeassistant/components/hue/.translations/sl.json b/homeassistant/components/hue/.translations/sl.json index a6c858e0e40..4245ce02c66 100644 --- a/homeassistant/components/hue/.translations/sl.json +++ b/homeassistant/components/hue/.translations/sl.json @@ -2,8 +2,11 @@ "config": { "abort": { "all_configured": "Vsi mostovi Philips Hue so \u017ee konfigurirani", + "already_configured": "Most je \u017ee konfiguriran", + "cannot_connect": "Ni mogo\u010de vzpostaviti povezave z mostom", "discover_timeout": "Ni bilo mogo\u010de odkriti Hue mostov", - "no_bridges": "Ni odkritih mostov Philips Hue" + "no_bridges": "Ni odkritih mostov Philips Hue", + "unknown": "Pri\u0161lo je do neznane napake" }, "error": { "linking": "Pri\u0161lo je do neznane napake pri povezavi.", diff --git a/homeassistant/components/hue/.translations/zh-Hans.json b/homeassistant/components/hue/.translations/zh-Hans.json index 5a94e084dd2..1d904070b81 100644 --- a/homeassistant/components/hue/.translations/zh-Hans.json +++ b/homeassistant/components/hue/.translations/zh-Hans.json @@ -2,8 +2,11 @@ "config": { "abort": { "all_configured": "\u5168\u90e8\u98de\u5229\u6d66 Hue \u6865\u63a5\u5668\u5df2\u914d\u7f6e", + "already_configured": "\u98de\u5229\u6d66 Hue Bridge \u5df2\u914d\u7f6e\u5b8c\u6210", + "cannot_connect": "\u65e0\u6cd5\u8fde\u63a5\u5230 \u98de\u5229\u6d66 Hue Bridge", "discover_timeout": "\u65e0\u6cd5\u55c5\u63a2 Hue \u6865\u63a5\u5668", - "no_bridges": "\u672a\u53d1\u73b0\u98de\u5229\u6d66 Hue Bridge" + "no_bridges": "\u672a\u53d1\u73b0\u98de\u5229\u6d66 Hue Bridge", + "unknown": "\u51fa\u73b0\u672a\u77e5\u7684\u9519\u8bef" }, "error": { "linking": "\u53d1\u751f\u672a\u77e5\u7684\u8fde\u63a5\u9519\u8bef\u3002", @@ -17,7 +20,7 @@ "title": "\u9009\u62e9 Hue Bridge" }, "link": { - "description": "\u8bf7\u6309\u4e0b\u6865\u63a5\u5668\u4e0a\u7684\u6309\u94ae\uff0c\u5728 Home Assistant \u4e0a\u6ce8\u518c\u98de\u5229\u6d66 Hue ![\u6865\u63a5\u5668\u6309\u94ae\u4f4d\u7f6e](/static/images/config_philips_hue.jpg)", + "description": "\u8bf7\u6309\u4e0b\u6865\u63a5\u5668\u4e0a\u7684\u6309\u94ae\uff0c\u4ee5\u5728 Home Assistant \u4e0a\u6ce8\u518c\u98de\u5229\u6d66 Hue\u3002\n\n![\u6865\u63a5\u5668\u6309\u94ae\u4f4d\u7f6e](/static/images/config_philips_hue.jpg)", "title": "\u8fde\u63a5\u4e2d\u67a2" } }, diff --git a/homeassistant/components/hue/.translations/zh-Hant.json b/homeassistant/components/hue/.translations/zh-Hant.json new file mode 100644 index 00000000000..eae4c09da49 --- /dev/null +++ b/homeassistant/components/hue/.translations/zh-Hant.json @@ -0,0 +1,29 @@ +{ + "config": { + "abort": { + "all_configured": "\u6240\u6709 Philips Hue Bridge \u7686\u5df2\u8a2d\u5b9a\u5b8c\u6210", + "already_configured": "Bridge \u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "cannot_connect": "\u7121\u6cd5\u9023\u7dda\u81f3 Bridge", + "discover_timeout": "\u7121\u6cd5\u641c\u5c0b\u5230 Hue Bridge", + "no_bridges": "\u672a\u641c\u5c0b\u5230 Philips Hue Bridge", + "unknown": "\u767c\u751f\u672a\u77e5\u932f\u8aa4" + }, + "error": { + "linking": "\u767c\u751f\u672a\u77e5\u9023\u7d50\u932f\u8aa4\u3002", + "register_failed": "\u8a3b\u518a\u5931\u6557\uff0c\u8acb\u7a0d\u5f8c\u518d\u8a66" + }, + "step": { + "init": { + "data": { + "host": "\u4e3b\u6a5f\u7aef" + }, + "title": "\u9078\u64c7 Hue Bridge" + }, + "link": { + "description": "\u6309\u4e0b Bridge \u4e0a\u7684\u6309\u9215\uff0c\u4ee5\u5c07 Philips Hue \u8a3b\u518a\u81f3 Home Assistant\u3002\n\n![Location of button on bridge](/static/images/config_philips_hue.jpg)", + "title": "\u9023\u7d50 Hub" + } + }, + "title": "Philips Hue Bridge" + } +} \ No newline at end of file diff --git a/homeassistant/components/sensor/.translations/season.bg.json b/homeassistant/components/sensor/.translations/season.bg.json new file mode 100644 index 00000000000..e3865ca42e5 --- /dev/null +++ b/homeassistant/components/sensor/.translations/season.bg.json @@ -0,0 +1,8 @@ +{ + "state": { + "autumn": "\u0415\u0441\u0435\u043d", + "spring": "\u041f\u0440\u043e\u043b\u0435\u0442", + "summer": "\u041b\u044f\u0442\u043e", + "winter": "\u0417\u0438\u043c\u0430" + } +} \ No newline at end of file diff --git a/homeassistant/components/sensor/.translations/season.da.json b/homeassistant/components/sensor/.translations/season.da.json new file mode 100644 index 00000000000..9cded2f9c0f --- /dev/null +++ b/homeassistant/components/sensor/.translations/season.da.json @@ -0,0 +1,8 @@ +{ + "state": { + "autumn": "Efter\u00e5r", + "spring": "For\u00e5r", + "summer": "Sommer", + "winter": "Vinter" + } +} \ No newline at end of file diff --git a/homeassistant/components/sensor/.translations/season.hu.json b/homeassistant/components/sensor/.translations/season.hu.json new file mode 100644 index 00000000000..63596b09784 --- /dev/null +++ b/homeassistant/components/sensor/.translations/season.hu.json @@ -0,0 +1,8 @@ +{ + "state": { + "autumn": "\u0150sz", + "spring": "Tavasz", + "summer": "Ny\u00e1r", + "winter": "T\u00e9l" + } +} \ No newline at end of file diff --git a/homeassistant/components/sensor/.translations/season.it.json b/homeassistant/components/sensor/.translations/season.it.json new file mode 100644 index 00000000000..d9138f6b16e --- /dev/null +++ b/homeassistant/components/sensor/.translations/season.it.json @@ -0,0 +1,8 @@ +{ + "state": { + "autumn": "Autunno", + "spring": "Primavera", + "summer": "Estate", + "winter": "Inverno" + } +} \ No newline at end of file diff --git a/homeassistant/components/sensor/.translations/season.lb.json b/homeassistant/components/sensor/.translations/season.lb.json new file mode 100644 index 00000000000..f33afde7a07 --- /dev/null +++ b/homeassistant/components/sensor/.translations/season.lb.json @@ -0,0 +1,8 @@ +{ + "state": { + "autumn": "Hierscht", + "spring": "Fr\u00e9ijoer", + "summer": "Summer", + "winter": "Wanter" + } +} \ No newline at end of file diff --git a/homeassistant/components/sensor/.translations/season.ru.json b/homeassistant/components/sensor/.translations/season.ru.json new file mode 100644 index 00000000000..2b04886b72d --- /dev/null +++ b/homeassistant/components/sensor/.translations/season.ru.json @@ -0,0 +1,8 @@ +{ + "state": { + "autumn": "\u041e\u0441\u0435\u043d\u044c", + "spring": "\u0412\u0435\u0441\u043d\u0430", + "summer": "\u041b\u0435\u0442\u043e", + "winter": "\u0417\u0438\u043c\u0430" + } +} \ No newline at end of file diff --git a/homeassistant/components/zone/.translations/cy.json b/homeassistant/components/zone/.translations/cy.json new file mode 100644 index 00000000000..e34fae81b61 --- /dev/null +++ b/homeassistant/components/zone/.translations/cy.json @@ -0,0 +1,21 @@ +{ + "config": { + "error": { + "name_exists": "Enw eisoes yn bodoli" + }, + "step": { + "init": { + "data": { + "icon": "Eicon", + "latitude": "Lledred", + "longitude": "Hydred", + "name": "Enw", + "passive": "Goddefol", + "radius": "Radiws" + }, + "title": "Ddiffinio paramedrau parth" + } + }, + "title": "Parth" + } +} \ No newline at end of file diff --git a/homeassistant/components/zone/.translations/de.json b/homeassistant/components/zone/.translations/de.json new file mode 100644 index 00000000000..fc1e3537f33 --- /dev/null +++ b/homeassistant/components/zone/.translations/de.json @@ -0,0 +1,21 @@ +{ + "config": { + "error": { + "name_exists": "Name existiert bereits" + }, + "step": { + "init": { + "data": { + "icon": "Symbol", + "latitude": "Breitengrad", + "longitude": "L\u00e4ngengrad", + "name": "Name", + "passive": "Passiv", + "radius": "Radius" + }, + "title": "Definieren Sie die Zonenparameter" + } + }, + "title": "Zone" + } +} \ No newline at end of file diff --git a/homeassistant/components/zone/.translations/en.json b/homeassistant/components/zone/.translations/en.json index ff2c7c07c14..1faf0110a53 100644 --- a/homeassistant/components/zone/.translations/en.json +++ b/homeassistant/components/zone/.translations/en.json @@ -1,21 +1,21 @@ { "config": { - "title": "Zone", - "step": { - "init": { - "title": "Define zone parameters", - "data": { - "name": "Name", - "latitude": "Latitude", - "longitude": "Longitude", - "radius": "Radius", - "passive": "Passive", - "icon": "Icon" - } - } - }, "error": { "name_exists": "Name already exists" - } + }, + "step": { + "init": { + "data": { + "icon": "Icon", + "latitude": "Latitude", + "longitude": "Longitude", + "name": "Name", + "passive": "Passive", + "radius": "Radius" + }, + "title": "Define zone parameters" + } + }, + "title": "Zone" } } \ No newline at end of file diff --git a/homeassistant/components/zone/.translations/ko.json b/homeassistant/components/zone/.translations/ko.json new file mode 100644 index 00000000000..364f8f3cc77 --- /dev/null +++ b/homeassistant/components/zone/.translations/ko.json @@ -0,0 +1,21 @@ +{ + "config": { + "error": { + "name_exists": "\uc774\ub984\uc774 \uc774\ubbf8 \uc874\uc7ac\ud569\ub2c8\ub2e4" + }, + "step": { + "init": { + "data": { + "icon": "\uc544\uc774\ucf58", + "latitude": "\uc704\ub3c4", + "longitude": "\uacbd\ub3c4", + "name": "\uc774\ub984", + "passive": "\uc790\ub3d9\ud654 \uc804\uc6a9", + "radius": "\ubc18\uacbd" + }, + "title": "\uad6c\uc5ed \ub9e4\uac1c \ubcc0\uc218 \uc815\uc758" + } + }, + "title": "\uad6c\uc5ed" + } +} \ No newline at end of file diff --git a/homeassistant/components/zone/.translations/lb.json b/homeassistant/components/zone/.translations/lb.json new file mode 100644 index 00000000000..10b65bcca30 --- /dev/null +++ b/homeassistant/components/zone/.translations/lb.json @@ -0,0 +1,21 @@ +{ + "config": { + "error": { + "name_exists": "Numm g\u00ebtt et schonn" + }, + "step": { + "init": { + "data": { + "icon": "Ikone", + "latitude": "Breedegrad", + "longitude": "L\u00e4ngegrad", + "name": "Numm", + "passive": "Passif", + "radius": "Radius" + }, + "title": "D\u00e9fin\u00e9iert Zone Parameter" + } + }, + "title": "Zone" + } +} \ No newline at end of file diff --git a/homeassistant/components/zone/.translations/nl.json b/homeassistant/components/zone/.translations/nl.json new file mode 100644 index 00000000000..6dcf565ada6 --- /dev/null +++ b/homeassistant/components/zone/.translations/nl.json @@ -0,0 +1,21 @@ +{ + "config": { + "error": { + "name_exists": "Naam bestaat al" + }, + "step": { + "init": { + "data": { + "icon": "Pictogram", + "latitude": "Breedtegraad", + "longitude": "Lengtegraad", + "name": "Naam", + "passive": "Passief", + "radius": "Straal" + }, + "title": "Definieer zone parameters" + } + }, + "title": "Zone" + } +} \ No newline at end of file diff --git a/homeassistant/components/zone/.translations/no.json b/homeassistant/components/zone/.translations/no.json new file mode 100644 index 00000000000..3c1a91976f0 --- /dev/null +++ b/homeassistant/components/zone/.translations/no.json @@ -0,0 +1,21 @@ +{ + "config": { + "error": { + "name_exists": "Navnet eksisterer allerede" + }, + "step": { + "init": { + "data": { + "icon": "Ikon", + "latitude": "Breddegrad", + "longitude": "Lengdegrad", + "name": "Navn", + "passive": "Passiv", + "radius": "Radius" + }, + "title": "Definer sone parametere" + } + }, + "title": "Sone" + } +} \ No newline at end of file diff --git a/homeassistant/components/zone/.translations/pl.json b/homeassistant/components/zone/.translations/pl.json new file mode 100644 index 00000000000..e649de4c75e --- /dev/null +++ b/homeassistant/components/zone/.translations/pl.json @@ -0,0 +1,21 @@ +{ + "config": { + "error": { + "name_exists": "Nazwa ju\u017c istnieje" + }, + "step": { + "init": { + "data": { + "icon": "Ikona", + "latitude": "Szeroko\u015b\u0107 geograficzna", + "longitude": "D\u0142ugo\u015b\u0107 geograficzna", + "name": "Nazwa", + "passive": "Pasywnie", + "radius": "Promie\u0144" + }, + "title": "Zdefiniuj parametry strefy" + } + }, + "title": "Strefa" + } +} \ No newline at end of file diff --git a/homeassistant/components/zone/.translations/pt.json b/homeassistant/components/zone/.translations/pt.json new file mode 100644 index 00000000000..a4ced557805 --- /dev/null +++ b/homeassistant/components/zone/.translations/pt.json @@ -0,0 +1,20 @@ +{ + "config": { + "error": { + "name_exists": "Nome j\u00e1 existente" + }, + "step": { + "init": { + "data": { + "icon": "\u00cdcone", + "latitude": "Latitude", + "longitude": "Longitude", + "name": "Nome", + "passive": "Passivo", + "radius": "Raio" + } + } + }, + "title": "Zona" + } +} \ No newline at end of file diff --git a/homeassistant/components/zone/.translations/ru.json b/homeassistant/components/zone/.translations/ru.json new file mode 100644 index 00000000000..f0619f2163c --- /dev/null +++ b/homeassistant/components/zone/.translations/ru.json @@ -0,0 +1,21 @@ +{ + "config": { + "error": { + "name_exists": "\u0418\u043c\u044f \u0443\u0436\u0435 \u0441\u0443\u0449\u0435\u0441\u0442\u0432\u0443\u0435\u0442" + }, + "step": { + "init": { + "data": { + "icon": "\u0417\u043d\u0430\u0447\u043e\u043a", + "latitude": "\u0428\u0438\u0440\u043e\u0442\u0430", + "longitude": "\u0414\u043e\u043b\u0433\u043e\u0442\u0430", + "name": "\u041d\u0430\u0437\u0432\u0430\u043d\u0438\u0435", + "passive": "\u041f\u0430\u0441\u0441\u0438\u0432\u043d\u0430\u044f", + "radius": "\u0420\u0430\u0434\u0438\u0443\u0441" + }, + "title": "\u041e\u043f\u0440\u0435\u0434\u0435\u043b\u0438\u0442\u044c \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u044b \u0437\u043e\u043d\u044b" + } + }, + "title": "\u0417\u043e\u043d\u0430" + } +} \ No newline at end of file diff --git a/homeassistant/components/zone/.translations/zh-Hans.json b/homeassistant/components/zone/.translations/zh-Hans.json new file mode 100644 index 00000000000..6d06b68dad8 --- /dev/null +++ b/homeassistant/components/zone/.translations/zh-Hans.json @@ -0,0 +1,21 @@ +{ + "config": { + "error": { + "name_exists": "\u540d\u79f0\u5df2\u5b58\u5728" + }, + "step": { + "init": { + "data": { + "icon": "\u56fe\u6807", + "latitude": "\u7eac\u5ea6", + "longitude": "\u7ecf\u5ea6", + "name": "\u540d\u79f0", + "passive": "\u88ab\u52a8", + "radius": "\u534a\u5f84" + }, + "title": "\u5b9a\u4e49\u533a\u57df\u76f8\u5173\u53d8\u91cf" + } + }, + "title": "\u533a\u57df" + } +} \ No newline at end of file From 83e342daf2b2d71170b227a30883b85e2be94746 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sat, 5 May 2018 11:35:42 -0400 Subject: [PATCH 140/155] Update frontend to 20180505.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 58cea0e0c66..b4eb6df07e1 100644 --- a/homeassistant/components/frontend/__init__.py +++ b/homeassistant/components/frontend/__init__.py @@ -25,7 +25,7 @@ from homeassistant.core import callback from homeassistant.helpers.translation import async_get_translations from homeassistant.loader import bind_hass -REQUIREMENTS = ['home-assistant-frontend==20180426.0'] +REQUIREMENTS = ['home-assistant-frontend==20180505.0'] DOMAIN = 'frontend' DEPENDENCIES = ['api', 'websocket_api', 'http', 'system_log'] diff --git a/requirements_all.txt b/requirements_all.txt index e9033091f3e..e74070cb98c 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -386,7 +386,7 @@ hipnotify==1.0.8 holidays==0.9.5 # homeassistant.components.frontend -home-assistant-frontend==20180426.0 +home-assistant-frontend==20180505.0 # homeassistant.components.homekit_controller # homekit==0.6 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index df0f5722b86..939e4314718 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -81,7 +81,7 @@ hbmqtt==0.9.1 holidays==0.9.5 # homeassistant.components.frontend -home-assistant-frontend==20180426.0 +home-assistant-frontend==20180505.0 # homeassistant.components.influxdb # homeassistant.components.sensor.influxdb From 1e31af77de90c9b3e89cb5e6f7c15334b05b3c46 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sat, 5 May 2018 11:41:55 -0400 Subject: [PATCH 141/155] Version bump to 0.69.0b0 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 30c73546cf7..7462710ea23 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -2,7 +2,7 @@ """Constants used by Home Assistant components.""" MAJOR_VERSION = 0 MINOR_VERSION = 69 -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 5c88e897af38c69fc188ef2dfbfd62372f522088 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 7 May 2018 10:00:54 -0400 Subject: [PATCH 142/155] Update netdisco to 1.4.1 --- homeassistant/components/discovery.py | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/discovery.py b/homeassistant/components/discovery.py index 07eb5aaab82..65d0a1c76f3 100644 --- a/homeassistant/components/discovery.py +++ b/homeassistant/components/discovery.py @@ -21,7 +21,7 @@ from homeassistant.helpers.event import async_track_point_in_utc_time from homeassistant.helpers.discovery import async_load_platform, async_discover import homeassistant.util.dt as dt_util -REQUIREMENTS = ['netdisco==1.4.0'] +REQUIREMENTS = ['netdisco==1.4.1'] DOMAIN = 'discovery' diff --git a/requirements_all.txt b/requirements_all.txt index e74070cb98c..735f39a8013 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -553,7 +553,7 @@ nad_receiver==0.0.9 nanoleaf==0.4.1 # homeassistant.components.discovery -netdisco==1.4.0 +netdisco==1.4.1 # homeassistant.components.sensor.neurio_energy neurio==0.3.1 From ab621808bd3ddeae5055545c906ebf593dbf6789 Mon Sep 17 00:00:00 2001 From: Justin Loutsenhizer Date: Sun, 6 May 2018 13:18:26 -0400 Subject: [PATCH 143/155] Add missing 'sensor' to ABODE_PLATFORMS (#14313) This fixes missing light, humidity, temperature sensors from abode component. --- homeassistant/components/abode.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/abode.py b/homeassistant/components/abode.py index 2f56bb7c2b5..6d5feb87dc2 100644 --- a/homeassistant/components/abode.py +++ b/homeassistant/components/abode.py @@ -81,7 +81,7 @@ TRIGGER_SCHEMA = vol.Schema({ ABODE_PLATFORMS = [ 'alarm_control_panel', 'binary_sensor', 'lock', 'switch', 'cover', - 'camera', 'light' + 'camera', 'light', 'sensor' ] From c48986a4674466d437cc7a1f672ac465c0189ce6 Mon Sep 17 00:00:00 2001 From: cdce8p <30130371+cdce8p@users.noreply.github.com> Date: Mon, 7 May 2018 02:55:38 +0200 Subject: [PATCH 144/155] Add debounce to move_cover (#14314) * Add debounce to move_cover * Fix spelling mistake --- .../components/homekit/type_covers.py | 4 ++- .../homekit/type_security_systems.py | 2 +- tests/components/homekit/test_type_covers.py | 34 ++++++++++++++----- 3 files changed, 29 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/homekit/type_covers.py b/homeassistant/components/homekit/type_covers.py index b30109f711d..3de87cf63e8 100644 --- a/homeassistant/components/homekit/type_covers.py +++ b/homeassistant/components/homekit/type_covers.py @@ -11,7 +11,7 @@ from homeassistant.const import ( ATTR_SUPPORTED_FEATURES) from . import TYPES -from .accessories import HomeAccessory +from .accessories import HomeAccessory, debounce from .const import ( SERV_WINDOW_COVERING, CHAR_CURRENT_POSITION, CHAR_TARGET_POSITION, CHAR_POSITION_STATE, @@ -80,6 +80,7 @@ class WindowCovering(HomeAccessory): self.char_target_position = serv_cover.configure_char( CHAR_TARGET_POSITION, value=0, setter_callback=self.move_cover) + @debounce def move_cover(self, value): """Move cover to value if call came from HomeKit.""" _LOGGER.debug('%s: Set position to %d', self.entity_id, value) @@ -122,6 +123,7 @@ class WindowCoveringBasic(HomeAccessory): self.char_position_state = serv_cover.configure_char( CHAR_POSITION_STATE, value=2) + @debounce def move_cover(self, value): """Move cover to value if call came from HomeKit.""" _LOGGER.debug('%s: Set position to %d', self.entity_id, value) diff --git a/homeassistant/components/homekit/type_security_systems.py b/homeassistant/components/homekit/type_security_systems.py index e32860d1fef..ab16f921e99 100644 --- a/homeassistant/components/homekit/type_security_systems.py +++ b/homeassistant/components/homekit/type_security_systems.py @@ -67,7 +67,7 @@ class SecuritySystem(HomeAccessory): _LOGGER.debug('%s: Updated current state to %s (%d)', self.entity_id, hass_state, current_security_state) - # SecuritySystemTargetSTate does not support triggered + # SecuritySystemTargetState does not support triggered if not self.flag_target_state and \ hass_state != STATE_ALARM_TRIGGERED: self.char_target_state.set_value(current_security_state) diff --git a/tests/components/homekit/test_type_covers.py b/tests/components/homekit/test_type_covers.py index 2dcb48a4d4c..313d58e78fd 100644 --- a/tests/components/homekit/test_type_covers.py +++ b/tests/components/homekit/test_type_covers.py @@ -4,19 +4,35 @@ import unittest from homeassistant.core import callback from homeassistant.components.cover import ( ATTR_POSITION, ATTR_CURRENT_POSITION, SUPPORT_STOP) -from homeassistant.components.homekit.type_covers import ( - GarageDoorOpener, WindowCovering, WindowCoveringBasic) from homeassistant.const import ( STATE_CLOSED, STATE_UNAVAILABLE, STATE_UNKNOWN, STATE_OPEN, ATTR_SERVICE, ATTR_SERVICE_DATA, EVENT_CALL_SERVICE, ATTR_SUPPORTED_FEATURES) from tests.common import get_test_home_assistant +from tests.components.homekit.test_accessories import patch_debounce -class TestHomekitSensors(unittest.TestCase): +class TestHomekitCovers(unittest.TestCase): """Test class for all accessory types regarding covers.""" + @classmethod + def setUpClass(cls): + """Setup Light class import and debounce patcher.""" + cls.patcher = patch_debounce() + cls.patcher.start() + _import = __import__('homeassistant.components.homekit.type_covers', + fromlist=['GarageDoorOpener', 'WindowCovering,', + 'WindowCoveringBasic']) + cls.garage_cls = _import.GarageDoorOpener + cls.window_cls = _import.WindowCovering + cls.window_basic_cls = _import.WindowCoveringBasic + + @classmethod + def tearDownClass(cls): + """Stop debounce patcher.""" + cls.patcher.stop() + def setUp(self): """Setup things to be run when tests are started.""" self.hass = get_test_home_assistant() @@ -37,7 +53,7 @@ class TestHomekitSensors(unittest.TestCase): """Test if accessory and HA are updated accordingly.""" garage_door = 'cover.garage_door' - acc = GarageDoorOpener(self.hass, 'Cover', garage_door, 2, config=None) + acc = self.garage_cls(self.hass, 'Cover', garage_door, 2, config=None) acc.run() self.assertEqual(acc.aid, 2) @@ -95,7 +111,7 @@ class TestHomekitSensors(unittest.TestCase): """Test if accessory and HA are updated accordingly.""" window_cover = 'cover.window' - acc = WindowCovering(self.hass, 'Cover', window_cover, 2, config=None) + acc = self.window_cls(self.hass, 'Cover', window_cover, 2, config=None) acc.run() self.assertEqual(acc.aid, 2) @@ -146,8 +162,8 @@ class TestHomekitSensors(unittest.TestCase): self.hass.states.set(window_cover, STATE_UNKNOWN, {ATTR_SUPPORTED_FEATURES: 0}) - acc = WindowCoveringBasic(self.hass, 'Cover', window_cover, 2, - config=None) + acc = self.window_basic_cls(self.hass, 'Cover', window_cover, 2, + config=None) acc.run() self.assertEqual(acc.aid, 2) @@ -214,8 +230,8 @@ class TestHomekitSensors(unittest.TestCase): self.hass.states.set(window_cover, STATE_UNKNOWN, {ATTR_SUPPORTED_FEATURES: SUPPORT_STOP}) - acc = WindowCoveringBasic(self.hass, 'Cover', window_cover, 2, - config=None) + acc = self.window_basic_cls(self.hass, 'Cover', window_cover, 2, + config=None) acc.run() # Set from HomeKit From c4ec2e3434e3981476de6581bb578cf88dd01693 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sun, 6 May 2018 20:54:56 -0400 Subject: [PATCH 145/155] Fix module names for custom components (#14317) * Fix module names for custom components * Also set __package__ correctly * bla * Remove print --- homeassistant/loader.py | 23 +++++++++++++++---- tests/test_loader.py | 18 +++++++++++++-- .../test_package/__init__.py | 2 +- 3 files changed, 36 insertions(+), 7 deletions(-) diff --git a/homeassistant/loader.py b/homeassistant/loader.py index 322870952f2..b6dabb1d883 100644 --- a/homeassistant/loader.py +++ b/homeassistant/loader.py @@ -73,13 +73,15 @@ def get_component(hass, comp_or_platform): # Try custom component module = _load_module(hass.config.path(PATH_CUSTOM_COMPONENTS), - comp_or_platform) + PATH_CUSTOM_COMPONENTS, comp_or_platform) if module is None: try: module = importlib.import_module( '{}.{}'.format(PACKAGE_COMPONENTS, comp_or_platform)) + _LOGGER.debug('Loaded %s (built-in)', comp_or_platform) except ImportError: + _LOGGER.warning('Unable to find %s', comp_or_platform) module = None cache = hass.data.get(DATA_KEY) @@ -102,18 +104,20 @@ def _find_spec(path, name): return None -def _load_module(path, name): +def _load_module(path, base_module, name): """Load a module based on a folder and a name.""" + mod_name = "{}.{}".format(base_module, name) spec = _find_spec([path], name) # Special handling if loading platforms and the folder is a namespace # (namespace is a folder without __init__.py) if spec is None and '.' in name: - parent_spec = _find_spec([path], name.split('.')[0]) + mod_parent_name = name.split('.')[0] + parent_spec = _find_spec([path], mod_parent_name) if (parent_spec is None or parent_spec.submodule_search_locations is None): return None - spec = _find_spec(parent_spec.submodule_search_locations, name) + spec = _find_spec(parent_spec.submodule_search_locations, mod_name) # Not found if spec is None: @@ -123,8 +127,19 @@ def _load_module(path, name): if spec.loader is None: return None + _LOGGER.debug('Loaded %s (%s)', name, base_module) + module = importlib.util.module_from_spec(spec) spec.loader.exec_module(module) + # A hack, I know. Don't currently know how to work around it. + if not module.__name__.startswith(base_module): + module.__name__ = "{}.{}".format(base_module, name) + + if not module.__package__: + module.__package__ = base_module + elif not module.__package__.startswith(base_module): + module.__package__ = "{}.{}".format(base_module, name) + return module diff --git a/tests/test_loader.py b/tests/test_loader.py index 646526e94ea..e8a79c6501f 100644 --- a/tests/test_loader.py +++ b/tests/test_loader.py @@ -30,8 +30,7 @@ class TestLoader(unittest.TestCase): comp = object() loader.set_component(self.hass, 'switch.test_set', comp) - self.assertEqual(comp, - loader.get_component(self.hass, 'switch.test_set')) + assert loader.get_component(self.hass, 'switch.test_set') is comp def test_get_component(self): """Test if get_component works.""" @@ -106,3 +105,18 @@ def test_helpers_wrapper(hass): yield from hass.async_block_till_done() assert result == ['hello'] + + +async def test_custom_component_name(hass): + """Test the name attribte of custom components.""" + comp = loader.get_component(hass, 'test_standalone') + assert comp.__name__ == 'custom_components.test_standalone' + assert comp.__package__ == 'custom_components' + + comp = loader.get_component(hass, 'test_package') + assert comp.__name__ == 'custom_components.test_package' + assert comp.__package__ == 'custom_components.test_package' + + comp = loader.get_component(hass, 'light.test') + assert comp.__name__ == 'custom_components.light.test' + assert comp.__package__ == 'custom_components.light' diff --git a/tests/testing_config/custom_components/test_package/__init__.py b/tests/testing_config/custom_components/test_package/__init__.py index 528f056948b..ee669c6c9b5 100644 --- a/tests/testing_config/custom_components/test_package/__init__.py +++ b/tests/testing_config/custom_components/test_package/__init__.py @@ -2,6 +2,6 @@ DOMAIN = 'test_package' -def setup(hass, config): +async def async_setup(hass, config): """Mock a successful setup.""" return True From 6a74fa344d693b12d9839b229f178dd87cf8e2af Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 7 May 2018 05:25:48 -0400 Subject: [PATCH 146/155] Revert custom component loading logic (#14327) * Revert custom component loading logic * Lint * Fix tests * Guard for infinite inserts into sys.path --- homeassistant/loader.py | 114 +++++++----------- tests/components/notify/test_file.py | 46 ++++--- tests/test_loader.py | 4 + .../image_processing/test.py | 6 +- .../custom_components/light/test.py | 5 +- .../custom_components/switch/test.py | 5 +- .../test_package/__init__.py | 3 + .../custom_components/test_package/const.py | 2 + 8 files changed, 84 insertions(+), 101 deletions(-) create mode 100644 tests/testing_config/custom_components/test_package/const.py diff --git a/homeassistant/loader.py b/homeassistant/loader.py index b6dabb1d883..e94fb2d6833 100644 --- a/homeassistant/loader.py +++ b/homeassistant/loader.py @@ -31,12 +31,6 @@ PREPARED = False DEPENDENCY_BLACKLIST = set(('config',)) -# List of available components -AVAILABLE_COMPONENTS = [] # type: List[str] - -# Dict of loaded components mapped name => module -_COMPONENT_CACHE = {} # type: Dict[str, ModuleType] - _LOGGER = logging.getLogger(__name__) @@ -64,85 +58,63 @@ def get_platform(hass, domain: str, platform: str) -> Optional[ModuleType]: return get_component(hass, PLATFORM_FORMAT.format(domain, platform)) -def get_component(hass, comp_or_platform): - """Load a module from either custom component or built-in.""" +def get_component(hass, comp_or_platform) -> Optional[ModuleType]: + """Try to load specified component. + + Looks in config dir first, then built-in components. + Only returns it if also found to be valid. + Async friendly. + """ try: return hass.data[DATA_KEY][comp_or_platform] except KeyError: pass - # Try custom component - module = _load_module(hass.config.path(PATH_CUSTOM_COMPONENTS), - PATH_CUSTOM_COMPONENTS, comp_or_platform) - - if module is None: - try: - module = importlib.import_module( - '{}.{}'.format(PACKAGE_COMPONENTS, comp_or_platform)) - _LOGGER.debug('Loaded %s (built-in)', comp_or_platform) - except ImportError: - _LOGGER.warning('Unable to find %s', comp_or_platform) - module = None - cache = hass.data.get(DATA_KEY) if cache is None: + # Only insert if it's not there (happens during tests) + if sys.path[0] != hass.config.config_dir: + sys.path.insert(0, hass.config.config_dir) cache = hass.data[DATA_KEY] = {} - cache[comp_or_platform] = module - return module + # First check custom, then built-in + potential_paths = ['custom_components.{}'.format(comp_or_platform), + 'homeassistant.components.{}'.format(comp_or_platform)] - -def _find_spec(path, name): - for finder in sys.meta_path: + for path in potential_paths: try: - spec = finder.find_spec(name, path=path) - if spec is not None: - return spec - except AttributeError: - # Not all finders have the find_spec method - pass + module = importlib.import_module(path) + + # In Python 3 you can import files from directories that do not + # contain the file __init__.py. A directory is a valid module if + # it contains a file with the .py extension. In this case Python + # will succeed in importing the directory as a module and call it + # a namespace. We do not care about namespaces. + # This prevents that when only + # custom_components/switch/some_platform.py exists, + # the import custom_components.switch would succeed. + if module.__spec__.origin == 'namespace': + continue + + _LOGGER.info("Loaded %s from %s", comp_or_platform, path) + + cache[comp_or_platform] = module + + return module + + except ImportError as err: + # This error happens if for example custom_components/switch + # exists and we try to load switch.demo. + if str(err) != "No module named '{}'".format(path): + _LOGGER.exception( + ("Error loading %s. Make sure all " + "dependencies are installed"), path) + + _LOGGER.error("Unable to find component %s", comp_or_platform) + return None -def _load_module(path, base_module, name): - """Load a module based on a folder and a name.""" - mod_name = "{}.{}".format(base_module, name) - spec = _find_spec([path], name) - - # Special handling if loading platforms and the folder is a namespace - # (namespace is a folder without __init__.py) - if spec is None and '.' in name: - mod_parent_name = name.split('.')[0] - parent_spec = _find_spec([path], mod_parent_name) - if (parent_spec is None or - parent_spec.submodule_search_locations is None): - return None - spec = _find_spec(parent_spec.submodule_search_locations, mod_name) - - # Not found - if spec is None: - return None - - # This is a namespace - if spec.loader is None: - return None - - _LOGGER.debug('Loaded %s (%s)', name, base_module) - - module = importlib.util.module_from_spec(spec) - spec.loader.exec_module(module) - # A hack, I know. Don't currently know how to work around it. - if not module.__name__.startswith(base_module): - module.__name__ = "{}.{}".format(base_module, name) - - if not module.__package__: - module.__package__ = base_module - elif not module.__package__.startswith(base_module): - module.__package__ = "{}.{}".format(base_module, name) - - return module - - class Components: """Helper to load components.""" diff --git a/tests/components/notify/test_file.py b/tests/components/notify/test_file.py index 42b9eb9d82d..c5064fca851 100644 --- a/tests/components/notify/test_file.py +++ b/tests/components/notify/test_file.py @@ -35,28 +35,30 @@ class TestNotifyFile(unittest.TestCase): assert setup_component(self.hass, notify.DOMAIN, config) assert not handle_config[notify.DOMAIN] - def _test_notify_file(self, timestamp, mock_utcnow, mock_stat): + def _test_notify_file(self, timestamp): """Test the notify file output.""" - mock_utcnow.return_value = dt_util.as_utc(dt_util.now()) - mock_stat.return_value.st_size = 0 + filename = 'mock_file' + message = 'one, two, testing, testing' + with assert_setup_component(1) as handle_config: + self.assertTrue(setup_component(self.hass, notify.DOMAIN, { + 'notify': { + 'name': 'test', + 'platform': 'file', + 'filename': filename, + 'timestamp': timestamp, + } + })) + assert handle_config[notify.DOMAIN] m_open = mock_open() with patch( 'homeassistant.components.notify.file.open', m_open, create=True - ): - filename = 'mock_file' - message = 'one, two, testing, testing' - with assert_setup_component(1) as handle_config: - self.assertTrue(setup_component(self.hass, notify.DOMAIN, { - 'notify': { - 'name': 'test', - 'platform': 'file', - 'filename': filename, - 'timestamp': timestamp, - } - })) - assert handle_config[notify.DOMAIN] + ), patch('homeassistant.components.notify.file.os.stat') as mock_st, \ + patch('homeassistant.util.dt.utcnow', + return_value=dt_util.utcnow()): + + mock_st.return_value.st_size = 0 title = '{} notifications (Log started: {})\n{}\n'.format( ATTR_TITLE_DEFAULT, dt_util.utcnow().isoformat(), @@ -82,14 +84,10 @@ class TestNotifyFile(unittest.TestCase): dt_util.utcnow().isoformat(), message))] ) - @patch('homeassistant.components.notify.file.os.stat') - @patch('homeassistant.util.dt.utcnow') - def test_notify_file(self, mock_utcnow, mock_stat): + def test_notify_file(self): """Test the notify file output without timestamp.""" - self._test_notify_file(False, mock_utcnow, mock_stat) + self._test_notify_file(False) - @patch('homeassistant.components.notify.file.os.stat') - @patch('homeassistant.util.dt.utcnow') - def test_notify_file_timestamp(self, mock_utcnow, mock_stat): + def test_notify_file_timestamp(self): """Test the notify file output with timestamp.""" - self._test_notify_file(True, mock_utcnow, mock_stat) + self._test_notify_file(True) diff --git a/tests/test_loader.py b/tests/test_loader.py index e8a79c6501f..c97e94a7ce1 100644 --- a/tests/test_loader.py +++ b/tests/test_loader.py @@ -120,3 +120,7 @@ async def test_custom_component_name(hass): comp = loader.get_component(hass, 'light.test') assert comp.__name__ == 'custom_components.light.test' assert comp.__package__ == 'custom_components.light' + + # Test custom components is mounted + from custom_components.test_package import TEST + assert TEST == 5 diff --git a/tests/testing_config/custom_components/image_processing/test.py b/tests/testing_config/custom_components/image_processing/test.py index 29d362699f5..b50050ed68e 100644 --- a/tests/testing_config/custom_components/image_processing/test.py +++ b/tests/testing_config/custom_components/image_processing/test.py @@ -3,9 +3,11 @@ from homeassistant.components.image_processing import ImageProcessingEntity -def setup_platform(hass, config, add_devices, discovery_info=None): +async def async_setup_platform(hass, config, async_add_devices_callback, + discovery_info=None): """Set up the test image_processing platform.""" - add_devices([TestImageProcessing('camera.demo_camera', "Test")]) + async_add_devices_callback([ + TestImageProcessing('camera.demo_camera', "Test")]) class TestImageProcessing(ImageProcessingEntity): diff --git a/tests/testing_config/custom_components/light/test.py b/tests/testing_config/custom_components/light/test.py index 71625dfdf93..fbf79f9e770 100644 --- a/tests/testing_config/custom_components/light/test.py +++ b/tests/testing_config/custom_components/light/test.py @@ -21,6 +21,7 @@ def init(empty=False): ] -def setup_platform(hass, config, add_devices_callback, discovery_info=None): +async def async_setup_platform(hass, config, async_add_devices_callback, + discovery_info=None): """Return mock devices.""" - add_devices_callback(DEVICES) + async_add_devices_callback(DEVICES) diff --git a/tests/testing_config/custom_components/switch/test.py b/tests/testing_config/custom_components/switch/test.py index 2819f2f2951..79126b7b52a 100644 --- a/tests/testing_config/custom_components/switch/test.py +++ b/tests/testing_config/custom_components/switch/test.py @@ -21,6 +21,7 @@ def init(empty=False): ] -def setup_platform(hass, config, add_devices_callback, discovery_info=None): +async def async_setup_platform(hass, config, async_add_devices_callback, + discovery_info=None): """Find and return test switches.""" - add_devices_callback(DEVICES) + async_add_devices_callback(DEVICES) diff --git a/tests/testing_config/custom_components/test_package/__init__.py b/tests/testing_config/custom_components/test_package/__init__.py index ee669c6c9b5..85e78a7f9d6 100644 --- a/tests/testing_config/custom_components/test_package/__init__.py +++ b/tests/testing_config/custom_components/test_package/__init__.py @@ -1,4 +1,7 @@ """Provide a mock package component.""" +from .const import TEST # noqa + + DOMAIN = 'test_package' diff --git a/tests/testing_config/custom_components/test_package/const.py b/tests/testing_config/custom_components/test_package/const.py new file mode 100644 index 00000000000..7e13e04cb47 --- /dev/null +++ b/tests/testing_config/custom_components/test_package/const.py @@ -0,0 +1,2 @@ +"""Constants for test_package custom component.""" +TEST = 5 From 3e5d76efb277501a9fc8b14647fe7fa351f4c073 Mon Sep 17 00:00:00 2001 From: Javier Gonel Date: Mon, 7 May 2018 16:52:33 +0300 Subject: [PATCH 147/155] fix(hbmqtt): partial packets breaking hbmqtt (#14329) This issue was fixed in hbmqtt/issues#95 that was released in hbmqtt 0.9.2 --- homeassistant/components/mqtt/server.py | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/mqtt/server.py b/homeassistant/components/mqtt/server.py index db251ab4180..8a012928792 100644 --- a/homeassistant/components/mqtt/server.py +++ b/homeassistant/components/mqtt/server.py @@ -13,7 +13,7 @@ import voluptuous as vol from homeassistant.const import EVENT_HOMEASSISTANT_STOP import homeassistant.helpers.config_validation as cv -REQUIREMENTS = ['hbmqtt==0.9.1'] +REQUIREMENTS = ['hbmqtt==0.9.2'] DEPENDENCIES = ['http'] # None allows custom config to be created through generate_config diff --git a/requirements_all.txt b/requirements_all.txt index 735f39a8013..6ddf0f81f61 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -371,7 +371,7 @@ ha-philipsjs==0.0.3 haversine==0.4.5 # homeassistant.components.mqtt.server -hbmqtt==0.9.1 +hbmqtt==0.9.2 # homeassistant.components.climate.heatmiser heatmiserV3==0.9.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 939e4314718..976f4d87280 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -75,7 +75,7 @@ ha-ffmpeg==1.9 haversine==0.4.5 # homeassistant.components.mqtt.server -hbmqtt==0.9.1 +hbmqtt==0.9.2 # homeassistant.components.binary_sensor.workday holidays==0.9.5 From a4e1615127e5e4fde91a07ca855475aab556552a Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 7 May 2018 10:05:34 -0400 Subject: [PATCH 148/155] Version bump to 0.69.0b1 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 7462710ea23..a1c19d41efe 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -2,7 +2,7 @@ """Constants used by Home Assistant components.""" MAJOR_VERSION = 0 MINOR_VERSION = 69 -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 b1eb35ee119c07db2e85ebf44ad5599413d104e1 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 7 May 2018 13:12:12 -0400 Subject: [PATCH 149/155] Ignore more loading errors (#14331) --- homeassistant/loader.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/homeassistant/loader.py b/homeassistant/loader.py index e94fb2d6833..67647a323c9 100644 --- a/homeassistant/loader.py +++ b/homeassistant/loader.py @@ -105,7 +105,16 @@ def get_component(hass, comp_or_platform) -> Optional[ModuleType]: except ImportError as err: # This error happens if for example custom_components/switch # exists and we try to load switch.demo. - if str(err) != "No module named '{}'".format(path): + # Ignore errors for custom_components, custom_components.switch + # and custom_components.switch.demo. + white_listed_errors = [] + parts = [] + for part in path.split('.'): + parts.append(part) + white_listed_errors.append( + "No module named '{}'".format('.'.join(parts))) + + if str(err) not in white_listed_errors: _LOGGER.exception( ("Error loading %s. Make sure all " "dependencies are installed"), path) From 8d24541ffe3d20bf4b714656a3505f0958aeeff5 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 7 May 2018 13:12:54 -0400 Subject: [PATCH 150/155] Version bump to 0.69.0b2 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index a1c19d41efe..7d54bf6356d 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -2,7 +2,7 @@ """Constants used by Home Assistant components.""" MAJOR_VERSION = 0 MINOR_VERSION = 69 -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 01ec4a7afdf616d2ff37a266c48e33804b16bab6 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 8 May 2018 20:48:46 -0400 Subject: [PATCH 151/155] Bump frontend to 20180509.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 b4eb6df07e1..0d267077991 100644 --- a/homeassistant/components/frontend/__init__.py +++ b/homeassistant/components/frontend/__init__.py @@ -25,7 +25,7 @@ from homeassistant.core import callback from homeassistant.helpers.translation import async_get_translations from homeassistant.loader import bind_hass -REQUIREMENTS = ['home-assistant-frontend==20180505.0'] +REQUIREMENTS = ['home-assistant-frontend==20180509.0'] DOMAIN = 'frontend' DEPENDENCIES = ['api', 'websocket_api', 'http', 'system_log'] diff --git a/requirements_all.txt b/requirements_all.txt index 6ddf0f81f61..6bcd267e456 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -386,7 +386,7 @@ hipnotify==1.0.8 holidays==0.9.5 # homeassistant.components.frontend -home-assistant-frontend==20180505.0 +home-assistant-frontend==20180509.0 # homeassistant.components.homekit_controller # homekit==0.6 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 976f4d87280..a25f36a8195 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==20180505.0 +home-assistant-frontend==20180509.0 # homeassistant.components.influxdb # homeassistant.components.sensor.influxdb From 2d0e3c14021d51ce7c780720d61ec303718dce90 Mon Sep 17 00:00:00 2001 From: Anders Melchiorsen Date: Wed, 9 May 2018 02:54:38 +0200 Subject: [PATCH 152/155] Ignore NaN values for influxdb (#14347) * Ignore NaN values for influxdb * Catch TypeError --- homeassistant/components/influxdb.py | 10 +++++++--- tests/components/test_influxdb.py | 2 +- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/influxdb.py b/homeassistant/components/influxdb.py index 1f7f9f6262f..6d54324542a 100644 --- a/homeassistant/components/influxdb.py +++ b/homeassistant/components/influxdb.py @@ -9,6 +9,7 @@ import re import queue import threading import time +import math import requests.exceptions import voluptuous as vol @@ -220,9 +221,12 @@ def setup(hass, config): json['fields'][key] = float( RE_DECIMAL.sub('', new_value)) - # Infinity is not a valid float in InfluxDB - if (key, float("inf")) in json['fields'].items(): - del json['fields'][key] + # Infinity and NaN are not valid floats in InfluxDB + try: + if not math.isfinite(json['fields'][key]): + del json['fields'][key] + except (KeyError, TypeError): + pass json['tags'].update(tags) diff --git a/tests/components/test_influxdb.py b/tests/components/test_influxdb.py index c909a8488be..e2323aca855 100644 --- a/tests/components/test_influxdb.py +++ b/tests/components/test_influxdb.py @@ -217,7 +217,7 @@ class TestInfluxDB(unittest.TestCase): """Test the event listener for missing units.""" self._setup() - attrs = {'bignumstring': "9" * 999} + attrs = {'bignumstring': '9' * 999, 'nonumstring': 'nan'} state = mock.MagicMock( state=8, domain='fake', entity_id='fake.entity-id', object_id='entity', attributes=attrs) From f406fd57ac52eea2f2fc0ab12e8b9c914b64f741 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 8 May 2018 20:55:35 -0400 Subject: [PATCH 153/155] Version bump to 0.69.0b3 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 7d54bf6356d..60bc6a78213 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -2,7 +2,7 @@ """Constants used by Home Assistant components.""" MAJOR_VERSION = 0 MINOR_VERSION = 69 -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 3b39ab5b94e0692df742c12478fb4f5e27ec5a56 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 10 May 2018 17:13:00 -0400 Subject: [PATCH 154/155] Remove domain expiry sensor (#14381) --- .../components/sensor/domain_expiry.py | 76 ------------------- requirements_all.txt | 3 - 2 files changed, 79 deletions(-) delete mode 100644 homeassistant/components/sensor/domain_expiry.py diff --git a/homeassistant/components/sensor/domain_expiry.py b/homeassistant/components/sensor/domain_expiry.py deleted file mode 100644 index 9364ce041f2..00000000000 --- a/homeassistant/components/sensor/domain_expiry.py +++ /dev/null @@ -1,76 +0,0 @@ -""" -Counter for the days till domain will expire. - -For more details about this sensor please refer to the documentation at -https://home-assistant.io/components/sensor.domain_expiry/ -""" -import logging -from datetime import datetime, timedelta - -import voluptuous as vol - -import homeassistant.helpers.config_validation as cv -from homeassistant.components.sensor import PLATFORM_SCHEMA -from homeassistant.const import (CONF_NAME, CONF_DOMAIN) -from homeassistant.helpers.entity import Entity - -REQUIREMENTS = ['python-whois==0.6.9'] - -_LOGGER = logging.getLogger(__name__) - -DEFAULT_NAME = 'Domain Expiry' - -SCAN_INTERVAL = timedelta(hours=24) - -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Required(CONF_DOMAIN): cv.string, - vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string -}) - - -def setup_platform(hass, config, add_devices, discovery_info=None): - """Set up domain expiry sensor.""" - server_name = config.get(CONF_DOMAIN) - sensor_name = config.get(CONF_NAME) - - add_devices([DomainExpiry(sensor_name, server_name)], True) - - -class DomainExpiry(Entity): - """Implementation of the domain expiry sensor.""" - - def __init__(self, sensor_name, server_name): - """Initialize the sensor.""" - self.server_name = server_name - self._name = sensor_name - self._state = None - - @property - def name(self): - """Return the name of the sensor.""" - return self._name - - @property - def unit_of_measurement(self): - """Return the unit this state is expressed in.""" - return 'days' - - @property - def state(self): - """Return the state of the sensor.""" - return self._state - - @property - def icon(self): - """Icon to use in the frontend, if any.""" - return 'mdi:earth' - - def update(self): - """Fetch the domain information.""" - import whois - domain = whois.whois(self.server_name) - if isinstance(domain.expiration_date, datetime): - expiry = domain.expiration_date - datetime.today() - self._state = expiry.days - else: - _LOGGER.error("Cannot get expiry date for %s", self.server_name) diff --git a/requirements_all.txt b/requirements_all.txt index 6bcd267e456..0887ff98996 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1041,9 +1041,6 @@ python-velbus==2.0.11 # homeassistant.components.media_player.vlc python-vlc==1.1.2 -# homeassistant.components.sensor.domain_expiry -python-whois==0.6.9 - # homeassistant.components.wink python-wink==1.7.3 From 8c0b45af1e98e84d97d02ff6fe0657b0bb977c87 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 10 May 2018 22:40:04 -0400 Subject: [PATCH 155/155] Version bump to 0.69.0 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 60bc6a78213..0f319891649 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -2,7 +2,7 @@ """Constants used by Home Assistant components.""" MAJOR_VERSION = 0 MINOR_VERSION = 69 -PATCH_VERSION = '0b3' +PATCH_VERSION = '0' __short_version__ = '{}.{}'.format(MAJOR_VERSION, MINOR_VERSION) __version__ = '{}.{}'.format(__short_version__, PATCH_VERSION) REQUIRED_PYTHON_VER = (3, 5, 3)