From 27a8171a8b3056c1347ca8b86903845bfd7d2dc8 Mon Sep 17 00:00:00 2001 From: Aaron Godfrey Date: Wed, 16 Jan 2019 21:19:52 -0800 Subject: [PATCH 001/222] Remove color call to set lights to black. (#20176) Calling clear all is enough to turn off the light. Calling the color command makes the light no longer function until clear all is called again. The component calls clear all beforing turning it on which is why it works through home assistant. However if you try to control the light via the hyperion app or through kodi after it has been turned off via home assistant it will not function until you call clear all again. --- homeassistant/components/light/hyperion.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/homeassistant/components/light/hyperion.py b/homeassistant/components/light/hyperion.py index 16be7d45825..ebe209c745e 100644 --- a/homeassistant/components/light/hyperion.py +++ b/homeassistant/components/light/hyperion.py @@ -177,11 +177,6 @@ class Hyperion(Light): def turn_off(self, **kwargs): """Disconnect all remotes.""" self.json_request({'command': 'clearall'}) - self.json_request({ - 'command': 'color', - 'priority': self._priority, - 'color': [0, 0, 0] - }) def update(self): """Get the lights status.""" From e1b63d9706c606c6e89808d07212dfad099dff1a Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 16 Jan 2019 23:12:18 -0800 Subject: [PATCH 002/222] Sensibo to use HA operation modes (#20180) --- homeassistant/components/climate/sensibo.py | 25 ++++++++++++++++----- 1 file changed, 19 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/climate/sensibo.py b/homeassistant/components/climate/sensibo.py index 8532c611d25..bf1cf5bf345 100644 --- a/homeassistant/components/climate/sensibo.py +++ b/homeassistant/components/climate/sensibo.py @@ -19,7 +19,8 @@ from homeassistant.components.climate import ( ATTR_CURRENT_HUMIDITY, ClimateDevice, DOMAIN, PLATFORM_SCHEMA, SUPPORT_TARGET_TEMPERATURE, SUPPORT_OPERATION_MODE, SUPPORT_FAN_MODE, SUPPORT_SWING_MODE, - SUPPORT_ON_OFF) + SUPPORT_ON_OFF, STATE_HEAT, STATE_COOL, STATE_FAN_ONLY, STATE_DRY, + STATE_AUTO) from homeassistant.exceptions import PlatformNotReady from homeassistant.helpers import config_validation as cv from homeassistant.helpers.aiohttp_client import async_get_clientsession @@ -57,6 +58,16 @@ FIELD_TO_FLAG = { 'on': SUPPORT_ON_OFF, } +SENSIBO_TO_HA = { + "cool": STATE_COOL, + "heat": STATE_HEAT, + "fan": STATE_FAN_ONLY, + "auto": STATE_AUTO, + "dry": STATE_DRY +} + +HA_TO_SENSIBO = {value: key for key, value in SENSIBO_TO_HA.items()} + async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): @@ -129,9 +140,10 @@ class SensiboClimate(ClimateDevice): self._ac_states = data['acState'] self._status = data['connectionStatus']['isAlive'] capabilities = data['remoteCapabilities'] - self._operations = sorted(capabilities['modes'].keys()) - self._current_capabilities = capabilities[ - 'modes'][self.current_operation] + self._operations = [SENSIBO_TO_HA[mode] for mode + in capabilities['modes']] + self._current_capabilities = \ + capabilities['modes'][self._ac_states['mode']] temperature_unit_key = data.get('temperatureUnit') or \ self._ac_states.get('temperatureUnit') if temperature_unit_key: @@ -186,7 +198,7 @@ class SensiboClimate(ClimateDevice): @property def current_operation(self): """Return current operation ie. heat, cool, idle.""" - return self._ac_states['mode'] + return SENSIBO_TO_HA.get(self._ac_states['mode']) @property def current_humidity(self): @@ -293,7 +305,8 @@ class SensiboClimate(ClimateDevice): """Set new target operation mode.""" with async_timeout.timeout(TIMEOUT): await self._client.async_set_ac_state_property( - self._id, 'mode', operation_mode, self._ac_states) + self._id, 'mode', HA_TO_SENSIBO[operation_mode], + self._ac_states) async def async_set_swing_mode(self, swing_mode): """Set new target swing operation.""" From 2de6a94506d01ea1e7dbd8aa4b56ab1129ac6459 Mon Sep 17 00:00:00 2001 From: Rohan Kapoor Date: Thu, 17 Jan 2019 02:13:15 -0800 Subject: [PATCH 003/222] Embed zoneminder platforms into component (#20182) --- .coveragerc | 1 - CODEOWNERS | 3 +-- .../components/{camera/zoneminder.py => zoneminder/camera.py} | 0 .../components/{sensor/zoneminder.py => zoneminder/sensor.py} | 0 .../components/{switch/zoneminder.py => zoneminder/switch.py} | 0 5 files changed, 1 insertion(+), 3 deletions(-) rename homeassistant/components/{camera/zoneminder.py => zoneminder/camera.py} (100%) rename homeassistant/components/{sensor/zoneminder.py => zoneminder/sensor.py} (100%) rename homeassistant/components/{switch/zoneminder.py => zoneminder/switch.py} (100%) diff --git a/.coveragerc b/.coveragerc index 857b6b90a22..cd8b6da4d71 100644 --- a/.coveragerc +++ b/.coveragerc @@ -451,7 +451,6 @@ omit = homeassistant/components/*/zigbee.py homeassistant/components/zoneminder/* - homeassistant/components/*/zoneminder.py homeassistant/components/tuya.py homeassistant/components/*/tuya.py diff --git a/CODEOWNERS b/CODEOWNERS index cfb83919b9c..ff5211bd776 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -268,8 +268,7 @@ homeassistant/components/*/xiaomi_aqara.py @danielhiversen @syssi homeassistant/components/*/xiaomi_miio.py @rytilahti @syssi # Z -homeassistant/components/zoneminder/ @rohankapoorcom -homeassistant/components/*/zoneminder.py @rohankapoorcom +homeassistant/components/zoneminder/* @rohankapoorcom # Other code homeassistant/scripts/check_config.py @kellerza diff --git a/homeassistant/components/camera/zoneminder.py b/homeassistant/components/zoneminder/camera.py similarity index 100% rename from homeassistant/components/camera/zoneminder.py rename to homeassistant/components/zoneminder/camera.py diff --git a/homeassistant/components/sensor/zoneminder.py b/homeassistant/components/zoneminder/sensor.py similarity index 100% rename from homeassistant/components/sensor/zoneminder.py rename to homeassistant/components/zoneminder/sensor.py diff --git a/homeassistant/components/switch/zoneminder.py b/homeassistant/components/zoneminder/switch.py similarity index 100% rename from homeassistant/components/switch/zoneminder.py rename to homeassistant/components/zoneminder/switch.py From 136364f5db449dc4638ff695754377450b788da3 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 17 Jan 2019 10:30:47 -0800 Subject: [PATCH 004/222] Distribute reconnect (#20181) --- homeassistant/components/cloud/iot.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/cloud/iot.py b/homeassistant/components/cloud/iot.py index 7d633a4b2ac..8638a4233ce 100644 --- a/homeassistant/components/cloud/iot.py +++ b/homeassistant/components/cloud/iot.py @@ -2,6 +2,7 @@ import asyncio import logging import pprint +import random import uuid from aiohttp import hdrs, client_exceptions, WSMsgType @@ -107,9 +108,11 @@ class CloudIoT: self.tries += 1 try: - # Sleep 2^tries seconds between retries - self.retry_task = hass.async_create_task(asyncio.sleep( - 2**min(9, self.tries), loop=hass.loop)) + # Sleep 2^tries + 0…tries*3 seconds between retries + self.retry_task = hass.async_create_task( + asyncio.sleep(2**min(9, self.tries) + + random.randint(0, self.tries * 3), + loop=hass.loop)) yield from self.retry_task self.retry_task = None except asyncio.CancelledError: From 0fe5d567a25b245c2f151f5847349b92566ece1d Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 17 Jan 2019 10:33:01 -0800 Subject: [PATCH 005/222] Add command to refresh auth (#20183) --- homeassistant/components/cloud/iot.py | 11 ++++++++--- tests/components/cloud/test_iot.py | 28 +++++++++++++++++++++++---- 2 files changed, 32 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/cloud/iot.py b/homeassistant/components/cloud/iot.py index 8638a4233ce..ed24fe48d40 100644 --- a/homeassistant/components/cloud/iot.py +++ b/homeassistant/components/cloud/iot.py @@ -316,15 +316,20 @@ def async_handle_google_actions(hass, cloud, payload): @HANDLERS.register('cloud') -@asyncio.coroutine -def async_handle_cloud(hass, cloud, payload): +async def async_handle_cloud(hass, cloud, payload): """Handle an incoming IoT message for cloud component.""" action = payload['action'] if action == 'logout': - yield from cloud.logout() + # Log out of Home Assistant Cloud + await cloud.logout() _LOGGER.error("You have been logged out from Home Assistant cloud: %s", payload['reason']) + elif action == 'refresh_auth': + # Refresh the auth token between now and payload['seconds'] + hass.helpers.event.async_call_later( + random.randint(0, payload['seconds']), + lambda now: auth_api.check_token(cloud)) else: _LOGGER.warning("Received unknown cloud action: %s", action) diff --git a/tests/components/cloud/test_iot.py b/tests/components/cloud/test_iot.py index 2133a803aef..1a528f8cedf 100644 --- a/tests/components/cloud/test_iot.py +++ b/tests/components/cloud/test_iot.py @@ -10,8 +10,9 @@ from homeassistant.components.cloud import ( Cloud, iot, auth_api, MODE_DEV) from homeassistant.components.cloud.const import ( PREF_ENABLE_ALEXA, PREF_ENABLE_GOOGLE) +from homeassistant.util import dt as dt_util from tests.components.alexa import test_smart_home as test_alexa -from tests.common import mock_coro +from tests.common import mock_coro, async_fire_time_changed from . import mock_cloud_prefs @@ -147,17 +148,36 @@ def test_handler_forwarding(): assert payload == 'payload' -@asyncio.coroutine -def test_handling_core_messages(hass, mock_cloud): +async def test_handling_core_messages_logout(hass, mock_cloud): """Test handling core messages.""" mock_cloud.logout.return_value = mock_coro() - yield from iot.async_handle_cloud(hass, mock_cloud, { + await iot.async_handle_cloud(hass, mock_cloud, { 'action': 'logout', 'reason': 'Logged in at two places.' }) assert len(mock_cloud.logout.mock_calls) == 1 +async def test_handling_core_messages_refresh_auth(hass, mock_cloud): + """Test handling core messages.""" + mock_cloud.hass = hass + with patch('random.randint', return_value=0) as mock_rand, patch( + 'homeassistant.components.cloud.auth_api.check_token' + ) as mock_check: + await iot.async_handle_cloud(hass, mock_cloud, { + 'action': 'refresh_auth', + 'seconds': 230, + }) + async_fire_time_changed(hass, dt_util.utcnow()) + await hass.async_block_till_done() + + assert len(mock_rand.mock_calls) == 1 + assert mock_rand.mock_calls[0][1] == (0, 230) + + assert len(mock_check.mock_calls) == 1 + assert mock_check.mock_calls[0][1][0] is mock_cloud + + @asyncio.coroutine def test_cloud_getting_disconnected_by_server(mock_client, caplog, mock_cloud): """Test server disconnecting instance.""" From 5232df34cb0185b42bf5f4d0e1f4df48d6a6d94f Mon Sep 17 00:00:00 2001 From: Rohan Kapoor Date: Thu, 17 Jan 2019 10:52:53 -0800 Subject: [PATCH 006/222] Add a Zoneminder availability sensor (#20184) * Embed zoneminder platforms into component * Add a binary sensor for ZoneMinder availability * Lint * Add missing docstrings --- .../components/zoneminder/__init__.py | 5 ++ .../components/zoneminder/binary_sensor.py | 50 +++++++++++++++++++ 2 files changed, 55 insertions(+) create mode 100644 homeassistant/components/zoneminder/binary_sensor.py diff --git a/homeassistant/components/zoneminder/__init__.py b/homeassistant/components/zoneminder/__init__.py index 258841e20d0..2a7859ebba4 100644 --- a/homeassistant/components/zoneminder/__init__.py +++ b/homeassistant/components/zoneminder/__init__.py @@ -12,6 +12,7 @@ import homeassistant.helpers.config_validation as cv from homeassistant.const import ( CONF_HOST, CONF_PASSWORD, CONF_PATH, CONF_SSL, CONF_USERNAME, CONF_VERIFY_SSL, ATTR_NAME, ATTR_ID) +from homeassistant.helpers.discovery import async_load_platform _LOGGER = logging.getLogger(__name__) @@ -93,4 +94,8 @@ def setup(hass, config): schema=SET_RUN_STATE_SCHEMA ) + hass.async_create_task( + async_load_platform(hass, 'binary_sensor', DOMAIN, {}, config) + ) + return success diff --git a/homeassistant/components/zoneminder/binary_sensor.py b/homeassistant/components/zoneminder/binary_sensor.py new file mode 100644 index 00000000000..e206ffa80f1 --- /dev/null +++ b/homeassistant/components/zoneminder/binary_sensor.py @@ -0,0 +1,50 @@ +""" +Support for ZoneMinder Binary Sensor. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/binary_sensor.zoneminder/ +""" +from homeassistant.components.binary_sensor import BinarySensorDevice + +from homeassistant.components.zoneminder import DOMAIN as ZONEMINDER_DOMAIN + +DEPENDENCIES = ['zoneminder'] + + +async def async_setup_platform(hass, config, add_entities, + discovery_info=None): + """Set up the ZoneMinder binary sensor platform.""" + sensors = [] + for host_name, zm_client in hass.data[ZONEMINDER_DOMAIN].items(): + sensors.append(ZMAvailabilitySensor(host_name, zm_client)) + add_entities(sensors) + return True + + +class ZMAvailabilitySensor(BinarySensorDevice): + """Representation of the availability of ZoneMinder as a binary sensor.""" + + def __init__(self, host_name, client): + """Initialize availability sensor.""" + self._state = None + self._name = host_name + self._client = client + + @property + def name(self): + """Return the name of this binary sensor.""" + return self._name + + @property + def is_on(self): + """Return true if the binary sensor is on.""" + return self._state + + @property + def device_class(self): + """Return the class of this device, from component DEVICE_CLASSES.""" + return 'connectivity' + + def update(self): + """Update the state of this sensor (availability of ZoneMinder).""" + self._state = self._client.is_available From d1c6eb4f3e2762b21be90e4a1b0bcc8ada22767c Mon Sep 17 00:00:00 2001 From: emontnemery Date: Thu, 17 Jan 2019 19:53:52 +0100 Subject: [PATCH 007/222] Add JSON attribute topic to MQTT cover (#20190) * Add JSON attribute topic to MQTT cover * Lint --- homeassistant/components/mqtt/cover.py | 12 +- tests/components/mqtt/test_binary_sensor.py | 145 +++++++++++------- tests/components/mqtt/test_cover.py | 104 ++++++++++++- tests/components/mqtt/test_sensor.py | 161 ++++++++++++-------- 4 files changed, 302 insertions(+), 120 deletions(-) diff --git a/homeassistant/components/mqtt/cover.py b/homeassistant/components/mqtt/cover.py index 5ebe51a3bce..7922254b327 100644 --- a/homeassistant/components/mqtt/cover.py +++ b/homeassistant/components/mqtt/cover.py @@ -21,7 +21,7 @@ from homeassistant.const import ( STATE_CLOSED, STATE_UNKNOWN, CONF_DEVICE) from homeassistant.components.mqtt import ( ATTR_DISCOVERY_HASH, CONF_COMMAND_TOPIC, CONF_QOS, CONF_RETAIN, - CONF_STATE_TOPIC, MqttAvailability, MqttDiscoveryUpdate, + CONF_STATE_TOPIC, MqttAttributes, MqttAvailability, MqttDiscoveryUpdate, MqttEntityDeviceInfo, subscription) from homeassistant.components.mqtt.discovery import ( MQTT_DISCOVERY_NEW, clear_discovery_hash) @@ -123,7 +123,8 @@ PLATFORM_SCHEMA = vol.All(mqtt.MQTT_BASE_PLATFORM_SCHEMA.extend({ default=DEFAULT_TILT_INVERT_STATE): cv.boolean, vol.Optional(CONF_UNIQUE_ID): cv.string, vol.Optional(CONF_DEVICE): mqtt.MQTT_ENTITY_DEVICE_INFO_SCHEMA, -}).extend(mqtt.MQTT_AVAILABILITY_SCHEMA.schema), validate_options) +}).extend(mqtt.MQTT_AVAILABILITY_SCHEMA.schema).extend( + mqtt.MQTT_JSON_ATTRS_SCHEMA.schema), validate_options) async def async_setup_platform(hass: HomeAssistantType, config: ConfigType, @@ -156,8 +157,8 @@ async def _async_setup_entity(config, async_add_entities, discovery_hash=None): async_add_entities([MqttCover(config, discovery_hash)]) -class MqttCover(MqttAvailability, MqttDiscoveryUpdate, MqttEntityDeviceInfo, - CoverDevice): +class MqttCover(MqttAttributes, MqttAvailability, MqttDiscoveryUpdate, + MqttEntityDeviceInfo, CoverDevice): """Representation of a cover that can be controlled using MQTT.""" def __init__(self, config, discovery_hash): @@ -176,6 +177,7 @@ class MqttCover(MqttAvailability, MqttDiscoveryUpdate, MqttEntityDeviceInfo, device_config = config.get(CONF_DEVICE) + MqttAttributes.__init__(self, config) MqttAvailability.__init__(self, config) MqttDiscoveryUpdate.__init__(self, discovery_hash, self.discovery_update) @@ -190,6 +192,7 @@ class MqttCover(MqttAvailability, MqttDiscoveryUpdate, MqttEntityDeviceInfo, """Handle updated discovery message.""" config = PLATFORM_SCHEMA(discovery_payload) self._setup_from_config(config) + await self.attributes_discovery_update(config) await self.availability_discovery_update(config) await self._subscribe_topics() self.async_schedule_update_ha_state() @@ -290,6 +293,7 @@ class MqttCover(MqttAvailability, MqttDiscoveryUpdate, MqttEntityDeviceInfo, """Unsubscribe when removed.""" self._sub_state = await subscription.async_unsubscribe_topics( self.hass, self._sub_state) + await MqttAttributes.async_will_remove_from_hass(self) await MqttAvailability.async_will_remove_from_hass(self) @property diff --git a/tests/components/mqtt/test_binary_sensor.py b/tests/components/mqtt/test_binary_sensor.py index 5a1c80beae2..f17d4e65c30 100644 --- a/tests/components/mqtt/test_binary_sensor.py +++ b/tests/components/mqtt/test_binary_sensor.py @@ -1,7 +1,7 @@ """The tests for the MQTT binary sensor platform.""" import json import unittest -from unittest.mock import ANY, Mock, patch +from unittest.mock import ANY, Mock from datetime import timedelta import homeassistant.core as ha @@ -283,66 +283,105 @@ class TestSensorMQTT(unittest.TestCase): assert STATE_OFF == state.state assert 3 == len(events) - def test_setting_sensor_attribute_via_mqtt_json_message(self): - """Test the setting of attribute via MQTT with JSON payload.""" - mock_component(self.hass, 'mqtt') - assert setup_component(self.hass, binary_sensor.DOMAIN, { - binary_sensor.DOMAIN: { - 'platform': 'mqtt', - 'name': 'test', - 'state_topic': 'test-topic', - 'json_attributes_topic': 'attr-topic' - } - }) - fire_mqtt_message(self.hass, 'attr-topic', '{ "val": "100" }') - self.hass.block_till_done() - state = self.hass.states.get('binary_sensor.test') +async def test_setting_attribute_via_mqtt_json_message(hass, mqtt_mock): + """Test the setting of attribute via MQTT with JSON payload.""" + assert await async_setup_component(hass, binary_sensor.DOMAIN, { + binary_sensor.DOMAIN: { + 'platform': 'mqtt', + 'name': 'test', + 'state_topic': 'test-topic', + 'json_attributes_topic': 'attr-topic' + } + }) - assert '100' == \ - state.attributes.get('val') + async_fire_mqtt_message(hass, 'attr-topic', '{ "val": "100" }') + await hass.async_block_till_done() + state = hass.states.get('binary_sensor.test') - @patch('homeassistant.components.mqtt._LOGGER') - def test_update_with_json_attrs_not_dict(self, mock_logger): - """Test attributes get extracted from a JSON result.""" - mock_component(self.hass, 'mqtt') - assert setup_component(self.hass, binary_sensor.DOMAIN, { - binary_sensor.DOMAIN: { - 'platform': 'mqtt', - 'name': 'test', - 'state_topic': 'test-topic', - 'json_attributes_topic': 'attr-topic' - } - }) + assert '100' == state.attributes.get('val') - fire_mqtt_message(self.hass, 'attr-topic', '[ "list", "of", "things"]') - self.hass.block_till_done() - state = self.hass.states.get('binary_sensor.test') - assert state.attributes.get('val') is None - mock_logger.warning.assert_called_with( - 'JSON result was not a dictionary') +async def test_update_with_json_attrs_not_dict(hass, mqtt_mock, caplog): + """Test attributes get extracted from a JSON result.""" + assert await async_setup_component(hass, binary_sensor.DOMAIN, { + binary_sensor.DOMAIN: { + 'platform': 'mqtt', + 'name': 'test', + 'state_topic': 'test-topic', + 'json_attributes_topic': 'attr-topic' + } + }) - @patch('homeassistant.components.mqtt._LOGGER') - def test_update_with_json_attrs_bad_JSON(self, mock_logger): - """Test attributes get extracted from a JSON result.""" - mock_component(self.hass, 'mqtt') - assert setup_component(self.hass, binary_sensor.DOMAIN, { - binary_sensor.DOMAIN: { - 'platform': 'mqtt', - 'name': 'test', - 'state_topic': 'test-topic', - 'json_attributes_topic': 'attr-topic' - } - }) + async_fire_mqtt_message(hass, 'attr-topic', '[ "list", "of", "things"]') + await hass.async_block_till_done() + state = hass.states.get('binary_sensor.test') - fire_mqtt_message(self.hass, 'attr-topic', 'This is not JSON') - self.hass.block_till_done() + assert state.attributes.get('val') is None + assert 'JSON result was not a dictionary' in caplog.text - state = self.hass.states.get('binary_sensor.test') - assert state.attributes.get('val') is None - mock_logger.warning.assert_called_with( - 'Erroneous JSON: %s', 'This is not JSON') + +async def test_update_with_json_attrs_bad_JSON(hass, mqtt_mock, caplog): + """Test attributes get extracted from a JSON result.""" + assert await async_setup_component(hass, binary_sensor.DOMAIN, { + binary_sensor.DOMAIN: { + 'platform': 'mqtt', + 'name': 'test', + 'state_topic': 'test-topic', + 'json_attributes_topic': 'attr-topic' + } + }) + + async_fire_mqtt_message(hass, 'attr-topic', 'This is not JSON') + await hass.async_block_till_done() + + state = hass.states.get('binary_sensor.test') + assert state.attributes.get('val') is None + assert 'Erroneous JSON: This is not JSON' in caplog.text + + +async def test_discovery_update_attr(hass, mqtt_mock, caplog): + """Test update of discovered MQTTAttributes.""" + entry = MockConfigEntry(domain=mqtt.DOMAIN) + await async_start(hass, 'homeassistant', {}, entry) + data1 = ( + '{ "name": "Beer",' + ' "command_topic": "test_topic",' + ' "json_attributes_topic": "attr-topic1" }' + ) + data2 = ( + '{ "name": "Beer",' + ' "command_topic": "test_topic",' + ' "json_attributes_topic": "attr-topic2" }' + ) + async_fire_mqtt_message(hass, 'homeassistant/binary_sensor/bla/config', + data1) + await hass.async_block_till_done() + async_fire_mqtt_message(hass, 'attr-topic1', '{ "val": "100" }') + await hass.async_block_till_done() + await hass.async_block_till_done() + state = hass.states.get('binary_sensor.beer') + assert '100' == state.attributes.get('val') + + # Change json_attributes_topic + async_fire_mqtt_message(hass, 'homeassistant/binary_sensor/bla/config', + data2) + await hass.async_block_till_done() + await hass.async_block_till_done() + + # Verify we are no longer subscribing to the old topic + async_fire_mqtt_message(hass, 'attr-topic1', '{ "val": "50" }') + await hass.async_block_till_done() + await hass.async_block_till_done() + state = hass.states.get('binary_sensor.beer') + assert '100' == state.attributes.get('val') + + # Verify we are subscribing to the new topic + async_fire_mqtt_message(hass, 'attr-topic2', '{ "val": "75" }') + await hass.async_block_till_done() + await hass.async_block_till_done() + state = hass.states.get('binary_sensor.beer') + assert '75' == state.attributes.get('val') async def test_unique_id(hass): diff --git a/tests/components/mqtt/test_cover.py b/tests/components/mqtt/test_cover.py index 36f566d0c19..500f261ba4e 100644 --- a/tests/components/mqtt/test_cover.py +++ b/tests/components/mqtt/test_cover.py @@ -16,8 +16,8 @@ from homeassistant.const import ( from homeassistant.setup import setup_component, async_setup_component from tests.common import ( - get_test_home_assistant, mock_mqtt_component, async_fire_mqtt_message, - fire_mqtt_message, MockConfigEntry, async_mock_mqtt_component, + MockConfigEntry, async_fire_mqtt_message, async_mock_mqtt_component, + fire_mqtt_message, get_test_home_assistant, mock_mqtt_component, mock_registry) @@ -1044,6 +1044,106 @@ class TestCoverMQTT(unittest.TestCase): assert STATE_UNAVAILABLE == state.state +async def test_setting_attribute_via_mqtt_json_message(hass, mqtt_mock): + """Test the setting of attribute via MQTT with JSON payload.""" + assert await async_setup_component(hass, cover.DOMAIN, { + cover.DOMAIN: { + 'platform': 'mqtt', + 'name': 'test', + 'state_topic': 'test-topic', + 'json_attributes_topic': 'attr-topic' + } + }) + + async_fire_mqtt_message(hass, 'attr-topic', '{ "val": "100" }') + await hass.async_block_till_done() + state = hass.states.get('cover.test') + + assert '100' == state.attributes.get('val') + + +async def test_update_with_json_attrs_not_dict(hass, mqtt_mock, caplog): + """Test attributes get extracted from a JSON result.""" + assert await async_setup_component(hass, cover.DOMAIN, { + cover.DOMAIN: { + 'platform': 'mqtt', + 'name': 'test', + 'state_topic': 'test-topic', + 'json_attributes_topic': 'attr-topic' + } + }) + + async_fire_mqtt_message(hass, 'attr-topic', '[ "list", "of", "things"]') + await hass.async_block_till_done() + state = hass.states.get('cover.test') + + assert state.attributes.get('val') is None + assert 'JSON result was not a dictionary' in caplog.text + + +async def test_update_with_json_attrs_bad_JSON(hass, mqtt_mock, caplog): + """Test attributes get extracted from a JSON result.""" + assert await async_setup_component(hass, cover.DOMAIN, { + cover.DOMAIN: { + 'platform': 'mqtt', + 'name': 'test', + 'state_topic': 'test-topic', + 'json_attributes_topic': 'attr-topic' + } + }) + + async_fire_mqtt_message(hass, 'attr-topic', 'This is not JSON') + await hass.async_block_till_done() + + state = hass.states.get('cover.test') + assert state.attributes.get('val') is None + assert 'Erroneous JSON: This is not JSON' in caplog.text + + +async def test_discovery_update_attr(hass, mqtt_mock, caplog): + """Test update of discovered MQTTAttributes.""" + entry = MockConfigEntry(domain=mqtt.DOMAIN) + await async_start(hass, 'homeassistant', {}, entry) + data1 = ( + '{ "name": "Beer",' + ' "command_topic": "test_topic",' + ' "json_attributes_topic": "attr-topic1" }' + ) + data2 = ( + '{ "name": "Beer",' + ' "command_topic": "test_topic",' + ' "json_attributes_topic": "attr-topic2" }' + ) + async_fire_mqtt_message(hass, 'homeassistant/cover/bla/config', + data1) + await hass.async_block_till_done() + async_fire_mqtt_message(hass, 'attr-topic1', '{ "val": "100" }') + await hass.async_block_till_done() + await hass.async_block_till_done() + state = hass.states.get('cover.beer') + assert '100' == state.attributes.get('val') + + # Change json_attributes_topic + async_fire_mqtt_message(hass, 'homeassistant/cover/bla/config', + data2) + await hass.async_block_till_done() + await hass.async_block_till_done() + + # Verify we are no longer subscribing to the old topic + async_fire_mqtt_message(hass, 'attr-topic1', '{ "val": "50" }') + await hass.async_block_till_done() + await hass.async_block_till_done() + state = hass.states.get('cover.beer') + assert '100' == state.attributes.get('val') + + # Verify we are subscribing to the new topic + async_fire_mqtt_message(hass, 'attr-topic2', '{ "val": "75" }') + await hass.async_block_till_done() + await hass.async_block_till_done() + state = hass.states.get('cover.beer') + assert '75' == state.attributes.get('val') + + async def test_discovery_removal_cover(hass, mqtt_mock, caplog): """Test removal of discovered cover.""" entry = MockConfigEntry(domain=mqtt.DOMAIN) diff --git a/tests/components/mqtt/test_sensor.py b/tests/components/mqtt/test_sensor.py index 9de76ff64f4..101eea44e28 100644 --- a/tests/components/mqtt/test_sensor.py +++ b/tests/components/mqtt/test_sensor.py @@ -333,67 +333,6 @@ class TestSensorMQTT(unittest.TestCase): state.attributes.get('val') assert '100' == state.state - def test_setting_sensor_attribute_via_mqtt_json_topic(self): - """Test the setting of attribute via MQTT with JSON payload.""" - mock_component(self.hass, 'mqtt') - assert setup_component(self.hass, sensor.DOMAIN, { - sensor.DOMAIN: { - 'platform': 'mqtt', - 'name': 'test', - 'state_topic': 'test-topic', - 'json_attributes_topic': 'attr-topic' - } - }) - - fire_mqtt_message(self.hass, 'attr-topic', '{ "val": "100" }') - self.hass.block_till_done() - state = self.hass.states.get('sensor.test') - - assert '100' == \ - state.attributes.get('val') - - @patch('homeassistant.components.mqtt._LOGGER') - def test_update_with_json_attrs_topic_not_dict(self, mock_logger): - """Test attributes get extracted from a JSON result.""" - mock_component(self.hass, 'mqtt') - assert setup_component(self.hass, sensor.DOMAIN, { - sensor.DOMAIN: { - 'platform': 'mqtt', - 'name': 'test', - 'state_topic': 'test-topic', - 'json_attributes_topic': 'attr-topic' - } - }) - - fire_mqtt_message(self.hass, 'attr-topic', '[ "list", "of", "things"]') - self.hass.block_till_done() - state = self.hass.states.get('sensor.test') - - assert state.attributes.get('val') is None - mock_logger.warning.assert_called_with( - 'JSON result was not a dictionary') - - @patch('homeassistant.components.mqtt._LOGGER') - def test_update_with_json_attrs_topic_bad_JSON(self, mock_logger): - """Test attributes get extracted from a JSON result.""" - mock_component(self.hass, 'mqtt') - assert setup_component(self.hass, sensor.DOMAIN, { - sensor.DOMAIN: { - 'platform': 'mqtt', - 'name': 'test', - 'state_topic': 'test-topic', - 'json_attributes_topic': 'attr-topic' - } - }) - - fire_mqtt_message(self.hass, 'attr-topic', 'This is not JSON') - self.hass.block_till_done() - - state = self.hass.states.get('sensor.test') - assert state.attributes.get('val') is None - mock_logger.warning.assert_called_with( - 'Erroneous JSON: %s', 'This is not JSON') - def test_invalid_device_class(self): """Test device_class option with invalid value.""" with assert_setup_component(0): @@ -428,6 +367,106 @@ class TestSensorMQTT(unittest.TestCase): assert 'device_class' not in state.attributes +async def test_setting_attribute_via_mqtt_json_message(hass, mqtt_mock): + """Test the setting of attribute via MQTT with JSON payload.""" + assert await async_setup_component(hass, sensor.DOMAIN, { + sensor.DOMAIN: { + 'platform': 'mqtt', + 'name': 'test', + 'state_topic': 'test-topic', + 'json_attributes_topic': 'attr-topic' + } + }) + + async_fire_mqtt_message(hass, 'attr-topic', '{ "val": "100" }') + await hass.async_block_till_done() + state = hass.states.get('sensor.test') + + assert '100' == state.attributes.get('val') + + +async def test_update_with_json_attrs_not_dict(hass, mqtt_mock, caplog): + """Test attributes get extracted from a JSON result.""" + assert await async_setup_component(hass, sensor.DOMAIN, { + sensor.DOMAIN: { + 'platform': 'mqtt', + 'name': 'test', + 'state_topic': 'test-topic', + 'json_attributes_topic': 'attr-topic' + } + }) + + async_fire_mqtt_message(hass, 'attr-topic', '[ "list", "of", "things"]') + await hass.async_block_till_done() + state = hass.states.get('sensor.test') + + assert state.attributes.get('val') is None + assert 'JSON result was not a dictionary' in caplog.text + + +async def test_update_with_json_attrs_bad_JSON(hass, mqtt_mock, caplog): + """Test attributes get extracted from a JSON result.""" + assert await async_setup_component(hass, sensor.DOMAIN, { + sensor.DOMAIN: { + 'platform': 'mqtt', + 'name': 'test', + 'state_topic': 'test-topic', + 'json_attributes_topic': 'attr-topic' + } + }) + + async_fire_mqtt_message(hass, 'attr-topic', 'This is not JSON') + await hass.async_block_till_done() + + state = hass.states.get('sensor.test') + assert state.attributes.get('val') is None + assert 'Erroneous JSON: This is not JSON' in caplog.text + + +async def test_discovery_update_attr(hass, mqtt_mock, caplog): + """Test update of discovered MQTTAttributes.""" + entry = MockConfigEntry(domain=mqtt.DOMAIN) + await async_start(hass, 'homeassistant', {}, entry) + data1 = ( + '{ "name": "Beer",' + ' "command_topic": "test_topic",' + ' "json_attributes_topic": "attr-topic1" }' + ) + data2 = ( + '{ "name": "Beer",' + ' "command_topic": "test_topic",' + ' "json_attributes_topic": "attr-topic2" }' + ) + async_fire_mqtt_message(hass, 'homeassistant/sensor/bla/config', + data1) + await hass.async_block_till_done() + async_fire_mqtt_message(hass, 'attr-topic1', '{ "val": "100" }') + await hass.async_block_till_done() + await hass.async_block_till_done() + state = hass.states.get('sensor.beer') + assert '100' == state.attributes.get('val') + + # Change json_attributes_topic + async_fire_mqtt_message(hass, 'homeassistant/sensor/bla/config', + data2) + await hass.async_block_till_done() + await hass.async_block_till_done() + + # Verify we are no longer subscribing to the old topic + async_fire_mqtt_message(hass, 'attr-topic1', '{ "val": "50" }') + await hass.async_block_till_done() + await hass.async_block_till_done() + state = hass.states.get('sensor.beer') + assert '100' == state.attributes.get('val') + + # Verify we are subscribing to the new topic + async_fire_mqtt_message(hass, 'attr-topic2', '{ "val": "75" }') + await hass.async_block_till_done() + await hass.async_block_till_done() + state = hass.states.get('sensor.beer') + assert '75' == state.attributes.get('val') + + async def test_unique_id(hass): """Test unique id option only creates one sensor per unique_id.""" await async_mock_mqtt_component(hass) From 234f348ba128dcb51e09276188be1913b5983bfa Mon Sep 17 00:00:00 2001 From: emontnemery Date: Thu, 17 Jan 2019 19:54:22 +0100 Subject: [PATCH 008/222] Add JSON attribute topic to MQTT light (#20191) --- .../components/mqtt/light/schema_basic.py | 13 +- .../components/mqtt/light/schema_json.py | 11 +- .../components/mqtt/light/schema_template.py | 13 +- tests/components/mqtt/test_light.py | 100 +++++++++++++++ tests/components/mqtt/test_light_json.py | 105 ++++++++++++++++ tests/components/mqtt/test_light_template.py | 115 ++++++++++++++++++ 6 files changed, 346 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/mqtt/light/schema_basic.py b/homeassistant/components/mqtt/light/schema_basic.py index fdfc1961db3..3be8de5c722 100644 --- a/homeassistant/components/mqtt/light/schema_basic.py +++ b/homeassistant/components/mqtt/light/schema_basic.py @@ -20,7 +20,8 @@ from homeassistant.const import ( CONF_RGB, CONF_STATE, CONF_VALUE_TEMPLATE, CONF_WHITE_VALUE, CONF_XY) from homeassistant.components.mqtt import ( CONF_COMMAND_TOPIC, CONF_QOS, CONF_RETAIN, CONF_STATE_TOPIC, - MqttAvailability, MqttDiscoveryUpdate, MqttEntityDeviceInfo, subscription) + MqttAttributes, MqttAvailability, MqttDiscoveryUpdate, + MqttEntityDeviceInfo, subscription) from homeassistant.helpers.restore_state import RestoreEntity import homeassistant.helpers.config_validation as cv import homeassistant.util.color as color_util @@ -107,7 +108,8 @@ PLATFORM_SCHEMA_BASIC = mqtt.MQTT_RW_PLATFORM_SCHEMA.extend({ vol.Optional(CONF_ON_COMMAND_TYPE, default=DEFAULT_ON_COMMAND_TYPE): vol.In(VALUES_ON_COMMAND_TYPE), vol.Optional(CONF_DEVICE): mqtt.MQTT_ENTITY_DEVICE_INFO_SCHEMA, -}).extend(mqtt.MQTT_AVAILABILITY_SCHEMA.schema) +}).extend(mqtt.MQTT_AVAILABILITY_SCHEMA.schema).extend( + mqtt.MQTT_JSON_ATTRS_SCHEMA.schema) async def async_setup_entity_basic(hass, config, async_add_entities, @@ -120,8 +122,8 @@ async def async_setup_entity_basic(hass, config, async_add_entities, # pylint: disable=too-many-ancestors -class MqttLight(MqttAvailability, MqttDiscoveryUpdate, MqttEntityDeviceInfo, - Light, RestoreEntity): +class MqttLight(MqttAttributes, MqttAvailability, MqttDiscoveryUpdate, + MqttEntityDeviceInfo, Light, RestoreEntity): """Representation of a MQTT light.""" def __init__(self, config, discovery_hash): @@ -152,6 +154,7 @@ class MqttLight(MqttAvailability, MqttDiscoveryUpdate, MqttEntityDeviceInfo, device_config = config.get(CONF_DEVICE) + MqttAttributes.__init__(self, config) MqttAvailability.__init__(self, config) MqttDiscoveryUpdate.__init__(self, discovery_hash, self.discovery_update) @@ -166,6 +169,7 @@ class MqttLight(MqttAvailability, MqttDiscoveryUpdate, MqttEntityDeviceInfo, """Handle updated discovery message.""" config = PLATFORM_SCHEMA_BASIC(discovery_payload) self._setup_from_config(config) + await self.attributes_discovery_update(config) await self.availability_discovery_update(config) await self._subscribe_topics() self.async_schedule_update_ha_state() @@ -467,6 +471,7 @@ class MqttLight(MqttAvailability, MqttDiscoveryUpdate, MqttEntityDeviceInfo, """Unsubscribe when removed.""" self._sub_state = await subscription.async_unsubscribe_topics( self.hass, self._sub_state) + await MqttAttributes.async_will_remove_from_hass(self) await MqttAvailability.async_will_remove_from_hass(self) @property diff --git a/homeassistant/components/mqtt/light/schema_json.py b/homeassistant/components/mqtt/light/schema_json.py index 6c986cbf49f..1c32b0c5783 100644 --- a/homeassistant/components/mqtt/light/schema_json.py +++ b/homeassistant/components/mqtt/light/schema_json.py @@ -17,7 +17,8 @@ from homeassistant.components.light import ( SUPPORT_FLASH, SUPPORT_TRANSITION, SUPPORT_WHITE_VALUE, Light) from homeassistant.components.mqtt import ( CONF_COMMAND_TOPIC, CONF_QOS, CONF_RETAIN, CONF_STATE_TOPIC, - MqttAvailability, MqttDiscoveryUpdate, MqttEntityDeviceInfo, subscription) + MqttAttributes, MqttAvailability, MqttDiscoveryUpdate, + MqttEntityDeviceInfo, subscription) from homeassistant.const import ( CONF_BRIGHTNESS, CONF_COLOR_TEMP, CONF_DEVICE, CONF_EFFECT, CONF_NAME, CONF_OPTIMISTIC, CONF_RGB, CONF_WHITE_VALUE, CONF_XY, STATE_ON) @@ -80,7 +81,8 @@ PLATFORM_SCHEMA_JSON = mqtt.MQTT_RW_PLATFORM_SCHEMA.extend({ vol.Optional(CONF_HS, default=DEFAULT_HS): cv.boolean, vol.Required(CONF_COMMAND_TOPIC): mqtt.valid_publish_topic, vol.Optional(CONF_DEVICE): mqtt.MQTT_ENTITY_DEVICE_INFO_SCHEMA, -}).extend(mqtt.MQTT_AVAILABILITY_SCHEMA.schema) +}).extend(mqtt.MQTT_AVAILABILITY_SCHEMA.schema).extend( + mqtt.MQTT_JSON_ATTRS_SCHEMA.schema) async def async_setup_entity_json(hass: HomeAssistantType, config: ConfigType, @@ -90,7 +92,7 @@ async def async_setup_entity_json(hass: HomeAssistantType, config: ConfigType, # pylint: disable=too-many-ancestors -class MqttLightJson(MqttAvailability, MqttDiscoveryUpdate, +class MqttLightJson(MqttAttributes, MqttAvailability, MqttDiscoveryUpdate, MqttEntityDeviceInfo, Light, RestoreEntity): """Representation of a MQTT JSON light.""" @@ -115,6 +117,7 @@ class MqttLightJson(MqttAvailability, MqttDiscoveryUpdate, device_config = config.get(CONF_DEVICE) + MqttAttributes.__init__(self, config) MqttAvailability.__init__(self, config) MqttDiscoveryUpdate.__init__(self, discovery_hash, self.discovery_update) @@ -129,6 +132,7 @@ class MqttLightJson(MqttAvailability, MqttDiscoveryUpdate, """Handle updated discovery message.""" config = PLATFORM_SCHEMA_JSON(discovery_payload) self._setup_from_config(config) + await self.attributes_discovery_update(config) await self.availability_discovery_update(config) await self._subscribe_topics() self.async_schedule_update_ha_state() @@ -297,6 +301,7 @@ class MqttLightJson(MqttAvailability, MqttDiscoveryUpdate, """Unsubscribe when removed.""" self._sub_state = await subscription.async_unsubscribe_topics( self.hass, self._sub_state) + await MqttAttributes.async_will_remove_from_hass(self) await MqttAvailability.async_will_remove_from_hass(self) @property diff --git a/homeassistant/components/mqtt/light/schema_template.py b/homeassistant/components/mqtt/light/schema_template.py index 53423679050..7020550710b 100644 --- a/homeassistant/components/mqtt/light/schema_template.py +++ b/homeassistant/components/mqtt/light/schema_template.py @@ -18,7 +18,8 @@ from homeassistant.const import ( CONF_DEVICE, CONF_NAME, CONF_OPTIMISTIC, STATE_ON, STATE_OFF) from homeassistant.components.mqtt import ( CONF_STATE_TOPIC, CONF_COMMAND_TOPIC, CONF_QOS, CONF_RETAIN, - MqttAvailability, MqttDiscoveryUpdate, MqttEntityDeviceInfo, subscription) + MqttAttributes, MqttAvailability, MqttDiscoveryUpdate, + MqttEntityDeviceInfo, subscription) import homeassistant.helpers.config_validation as cv import homeassistant.util.color as color_util from homeassistant.helpers.restore_state import RestoreEntity @@ -66,7 +67,8 @@ PLATFORM_SCHEMA_TEMPLATE = mqtt.MQTT_RW_PLATFORM_SCHEMA.extend({ vol.All(vol.Coerce(int), vol.In([0, 1, 2])), vol.Optional(CONF_UNIQUE_ID): cv.string, vol.Optional(CONF_DEVICE): mqtt.MQTT_ENTITY_DEVICE_INFO_SCHEMA, -}).extend(mqtt.MQTT_AVAILABILITY_SCHEMA.schema) +}).extend(mqtt.MQTT_AVAILABILITY_SCHEMA.schema).extend( + mqtt.MQTT_JSON_ATTRS_SCHEMA.schema) async def async_setup_entity_template(hass, config, async_add_entities, @@ -76,8 +78,8 @@ async def async_setup_entity_template(hass, config, async_add_entities, # pylint: disable=too-many-ancestors -class MqttTemplate(MqttAvailability, MqttDiscoveryUpdate, MqttEntityDeviceInfo, - Light, RestoreEntity): +class MqttTemplate(MqttAttributes, MqttAvailability, MqttDiscoveryUpdate, + MqttEntityDeviceInfo, Light, RestoreEntity): """Representation of a MQTT Template light.""" def __init__(self, config, discovery_hash): @@ -102,6 +104,7 @@ class MqttTemplate(MqttAvailability, MqttDiscoveryUpdate, MqttEntityDeviceInfo, device_config = config.get(CONF_DEVICE) + MqttAttributes.__init__(self, config) MqttAvailability.__init__(self, config) MqttDiscoveryUpdate.__init__(self, discovery_hash, self.discovery_update) @@ -116,6 +119,7 @@ class MqttTemplate(MqttAvailability, MqttDiscoveryUpdate, MqttEntityDeviceInfo, """Handle updated discovery message.""" config = PLATFORM_SCHEMA_TEMPLATE(discovery_payload) self._setup_from_config(config) + await self.attributes_discovery_update(config) await self.availability_discovery_update(config) await self._subscribe_topics() self.async_schedule_update_ha_state() @@ -270,6 +274,7 @@ class MqttTemplate(MqttAvailability, MqttDiscoveryUpdate, MqttEntityDeviceInfo, """Unsubscribe when removed.""" self._sub_state = await subscription.async_unsubscribe_topics( self.hass, self._sub_state) + await MqttAttributes.async_will_remove_from_hass(self) await MqttAvailability.async_will_remove_from_hass(self) @property diff --git a/tests/components/mqtt/test_light.py b/tests/components/mqtt/test_light.py index 951a9f04be9..a424263af8c 100644 --- a/tests/components/mqtt/test_light.py +++ b/tests/components/mqtt/test_light.py @@ -1070,6 +1070,106 @@ async def test_custom_availability_payload(hass, mqtt_mock): assert STATE_UNAVAILABLE == state.state +async def test_setting_attribute_via_mqtt_json_message(hass, mqtt_mock): + """Test the setting of attribute via MQTT with JSON payload.""" + assert await async_setup_component(hass, light.DOMAIN, { + light.DOMAIN: { + 'platform': 'mqtt', + 'name': 'test', + 'command_topic': 'test-topic', + 'json_attributes_topic': 'attr-topic' + } + }) + + async_fire_mqtt_message(hass, 'attr-topic', '{ "val": "100" }') + await hass.async_block_till_done() + state = hass.states.get('light.test') + + assert '100' == state.attributes.get('val') + + +async def test_update_with_json_attrs_not_dict(hass, mqtt_mock, caplog): + """Test attributes get extracted from a JSON result.""" + assert await async_setup_component(hass, light.DOMAIN, { + light.DOMAIN: { + 'platform': 'mqtt', + 'name': 'test', + 'command_topic': 'test-topic', + 'json_attributes_topic': 'attr-topic' + } + }) + + async_fire_mqtt_message(hass, 'attr-topic', '[ "list", "of", "things"]') + await hass.async_block_till_done() + state = hass.states.get('light.test') + + assert state.attributes.get('val') is None + assert 'JSON result was not a dictionary' in caplog.text + + +async def test_update_with_json_attrs_bad_JSON(hass, mqtt_mock, caplog): + """Test attributes get extracted from a JSON result.""" + assert await async_setup_component(hass, light.DOMAIN, { + light.DOMAIN: { + 'platform': 'mqtt', + 'name': 'test', + 'command_topic': 'test-topic', + 'json_attributes_topic': 'attr-topic' + } + }) + + async_fire_mqtt_message(hass, 'attr-topic', 'This is not JSON') + await hass.async_block_till_done() + + state = hass.states.get('light.test') + assert state.attributes.get('val') is None + assert 'Erroneous JSON: This is not JSON' in caplog.text + + +async def test_discovery_update_attr(hass, mqtt_mock, caplog): + """Test update of discovered MQTTAttributes.""" + entry = MockConfigEntry(domain=mqtt.DOMAIN) + await async_start(hass, 'homeassistant', {}, entry) + data1 = ( + '{ "name": "Beer",' + ' "command_topic": "test_topic",' + ' "json_attributes_topic": "attr-topic1" }' + ) + data2 = ( + '{ "name": "Beer",' + ' "command_topic": "test_topic",' + ' "json_attributes_topic": "attr-topic2" }' + ) + async_fire_mqtt_message(hass, 'homeassistant/light/bla/config', + data1) + await hass.async_block_till_done() + async_fire_mqtt_message(hass, 'attr-topic1', '{ "val": "100" }') + await hass.async_block_till_done() + await hass.async_block_till_done() + state = hass.states.get('light.beer') + assert '100' == state.attributes.get('val') + + # Change json_attributes_topic + async_fire_mqtt_message(hass, 'homeassistant/light/bla/config', + data2) + await hass.async_block_till_done() + await hass.async_block_till_done() + + # Verify we are no longer subscribing to the old topic + async_fire_mqtt_message(hass, 'attr-topic1', '{ "val": "50" }') + await hass.async_block_till_done() + await hass.async_block_till_done() + state = hass.states.get('light.beer') + assert '100' == state.attributes.get('val') + + # Verify we are subscribing to the new topic + async_fire_mqtt_message(hass, 'attr-topic2', '{ "val": "75" }') + await hass.async_block_till_done() + await hass.async_block_till_done() + state = hass.states.get('light.beer') + assert '75' == state.attributes.get('val') + + async def test_unique_id(hass): """Test unique id option only creates one light per unique_id.""" await async_mock_mqtt_component(hass) diff --git a/tests/components/mqtt/test_light_json.py b/tests/components/mqtt/test_light_json.py index 691e34104e1..7621da724c9 100644 --- a/tests/components/mqtt/test_light_json.py +++ b/tests/components/mqtt/test_light_json.py @@ -536,6 +536,111 @@ async def test_custom_availability_payload(hass, mqtt_mock): assert STATE_UNAVAILABLE == state.state +async def test_setting_attribute_via_mqtt_json_message(hass, mqtt_mock): + """Test the setting of attribute via MQTT with JSON payload.""" + assert await async_setup_component(hass, light.DOMAIN, { + light.DOMAIN: { + 'platform': 'mqtt', + 'schema': 'json', + 'name': 'test', + 'command_topic': 'test-topic', + 'json_attributes_topic': 'attr-topic' + } + }) + + async_fire_mqtt_message(hass, 'attr-topic', '{ "val": "100" }') + await hass.async_block_till_done() + state = hass.states.get('light.test') + + assert '100' == state.attributes.get('val') + + +async def test_update_with_json_attrs_not_dict(hass, mqtt_mock, caplog): + """Test attributes get extracted from a JSON result.""" + assert await async_setup_component(hass, light.DOMAIN, { + light.DOMAIN: { + 'platform': 'mqtt', + 'schema': 'json', + 'name': 'test', + 'command_topic': 'test-topic', + 'json_attributes_topic': 'attr-topic' + } + }) + + async_fire_mqtt_message(hass, 'attr-topic', '[ "list", "of", "things"]') + await hass.async_block_till_done() + state = hass.states.get('light.test') + + assert state.attributes.get('val') is None + assert 'JSON result was not a dictionary' in caplog.text + + +async def test_update_with_json_attrs_bad_JSON(hass, mqtt_mock, caplog): + """Test attributes get extracted from a JSON result.""" + assert await async_setup_component(hass, light.DOMAIN, { + light.DOMAIN: { + 'platform': 'mqtt', + 'schema': 'json', + 'name': 'test', + 'command_topic': 'test-topic', + 'json_attributes_topic': 'attr-topic' + } + }) + + async_fire_mqtt_message(hass, 'attr-topic', 'This is not JSON') + await hass.async_block_till_done() + + state = hass.states.get('light.test') + assert state.attributes.get('val') is None + assert 'Erroneous JSON: This is not JSON' in caplog.text + + +async def test_discovery_update_attr(hass, mqtt_mock, caplog): + """Test update of discovered MQTTAttributes.""" + entry = MockConfigEntry(domain=mqtt.DOMAIN) + await async_start(hass, 'homeassistant', {}, entry) + data1 = ( + '{ "name": "Beer",' + ' "schema": "json",' + ' "command_topic": "test_topic",' + ' "json_attributes_topic": "attr-topic1" }' + ) + data2 = ( + '{ "name": "Beer",' + ' "schema": "json",' + ' "command_topic": "test_topic",' + ' "json_attributes_topic": "attr-topic2" }' + ) + async_fire_mqtt_message(hass, 'homeassistant/light/bla/config', + data1) + await hass.async_block_till_done() + async_fire_mqtt_message(hass, 'attr-topic1', '{ "val": "100" }') + await hass.async_block_till_done() + await hass.async_block_till_done() + state = hass.states.get('light.beer') + assert '100' == state.attributes.get('val') + + # Change json_attributes_topic + async_fire_mqtt_message(hass, 'homeassistant/light/bla/config', + data2) + await hass.async_block_till_done() + await hass.async_block_till_done() + + # Verify we are no longer subscribing to the old topic + async_fire_mqtt_message(hass, 'attr-topic1', '{ "val": "50" }') + await hass.async_block_till_done() + await hass.async_block_till_done() + state = hass.states.get('light.beer') + assert '100' == state.attributes.get('val') + + # Verify we are subscribing to the new topic + async_fire_mqtt_message(hass, 'attr-topic2', '{ "val": "75" }') + await hass.async_block_till_done() + await hass.async_block_till_done() + state = hass.states.get('light.beer') + assert '75' == state.attributes.get('val') + + async def test_unique_id(hass): """Test unique id option only creates one light per unique_id.""" await async_mock_mqtt_component(hass) diff --git a/tests/components/mqtt/test_light_template.py b/tests/components/mqtt/test_light_template.py index f9946fc5b88..509f2ee5d36 100644 --- a/tests/components/mqtt/test_light_template.py +++ b/tests/components/mqtt/test_light_template.py @@ -485,6 +485,121 @@ async def test_custom_availability_payload(hass, mqtt_mock): assert STATE_UNAVAILABLE == state.state +async def test_setting_attribute_via_mqtt_json_message(hass, mqtt_mock): + """Test the setting of attribute via MQTT with JSON payload.""" + assert await async_setup_component(hass, light.DOMAIN, { + light.DOMAIN: { + 'platform': 'mqtt', + 'schema': 'template', + 'name': 'test', + 'command_topic': 'test-topic', + 'command_on_template': 'on,{{ transition }}', + 'command_off_template': 'off,{{ transition|d }}', + 'json_attributes_topic': 'attr-topic' + } + }) + + async_fire_mqtt_message(hass, 'attr-topic', '{ "val": "100" }') + await hass.async_block_till_done() + state = hass.states.get('light.test') + + assert '100' == state.attributes.get('val') + + +async def test_update_with_json_attrs_not_dict(hass, mqtt_mock, caplog): + """Test attributes get extracted from a JSON result.""" + assert await async_setup_component(hass, light.DOMAIN, { + light.DOMAIN: { + 'platform': 'mqtt', + 'schema': 'template', + 'name': 'test', + 'command_topic': 'test-topic', + 'command_on_template': 'on,{{ transition }}', + 'command_off_template': 'off,{{ transition|d }}', + 'json_attributes_topic': 'attr-topic' + } + }) + + async_fire_mqtt_message(hass, 'attr-topic', '[ "list", "of", "things"]') + await hass.async_block_till_done() + state = hass.states.get('light.test') + + assert state.attributes.get('val') is None + assert 'JSON result was not a dictionary' in caplog.text + + +async def test_update_with_json_attrs_bad_JSON(hass, mqtt_mock, caplog): + """Test attributes get extracted from a JSON result.""" + assert await async_setup_component(hass, light.DOMAIN, { + light.DOMAIN: { + 'platform': 'mqtt', + 'schema': 'template', + 'name': 'test', + 'command_topic': 'test-topic', + 'command_on_template': 'on,{{ transition }}', + 'command_off_template': 'off,{{ transition|d }}', + 'json_attributes_topic': 'attr-topic' + } + }) + + async_fire_mqtt_message(hass, 'attr-topic', 'This is not JSON') + await hass.async_block_till_done() + + state = hass.states.get('light.test') + assert state.attributes.get('val') is None + assert 'Erroneous JSON: This is not JSON' in caplog.text + + +async def test_discovery_update_attr(hass, mqtt_mock, caplog): + """Test update of discovered MQTTAttributes.""" + entry = MockConfigEntry(domain=mqtt.DOMAIN) + await async_start(hass, 'homeassistant', {}, entry) + data1 = ( + '{ "name": "Beer",' + ' "schema": "template",' + ' "command_topic": "test_topic",' + ' "command_on_template": "on",' + ' "command_off_template": "off",' + ' "json_attributes_topic": "attr-topic1" }' + ) + data2 = ( + '{ "name": "Beer",' + ' "schema": "template",' + ' "command_topic": "test_topic",' + ' "command_on_template": "on",' + ' "command_off_template": "off",' + ' "json_attributes_topic": "attr-topic2" }' + ) + async_fire_mqtt_message(hass, 'homeassistant/light/bla/config', + data1) + await hass.async_block_till_done() + async_fire_mqtt_message(hass, 'attr-topic1', '{ "val": "100" }') + await hass.async_block_till_done() + await hass.async_block_till_done() + state = hass.states.get('light.beer') + assert '100' == state.attributes.get('val') + + # Change json_attributes_topic + async_fire_mqtt_message(hass, 'homeassistant/light/bla/config', + data2) + await hass.async_block_till_done() + await hass.async_block_till_done() + + # Verify we are no longer subscribing to the old topic + async_fire_mqtt_message(hass, 'attr-topic1', '{ "val": "50" }') + await hass.async_block_till_done() + await hass.async_block_till_done() + state = hass.states.get('light.beer') + assert '100' == state.attributes.get('val') + + # Verify we are subscribing to the new topic + async_fire_mqtt_message(hass, 'attr-topic2', '{ "val": "75" }') + await hass.async_block_till_done() + await hass.async_block_till_done() + state = hass.states.get('light.beer') + assert '75' == state.attributes.get('val') + + async def test_unique_id(hass): """Test unique id option only creates one light per unique_id.""" await async_mock_mqtt_component(hass) From f094a7369dd946386f5d30fc228f61041bde9bd6 Mon Sep 17 00:00:00 2001 From: emontnemery Date: Thu, 17 Jan 2019 19:55:22 +0100 Subject: [PATCH 009/222] Add JSON attribute topic to MQTT switch (#20192) --- homeassistant/components/mqtt/switch.py | 12 ++- tests/components/mqtt/test_switch.py | 100 ++++++++++++++++++++++++ 2 files changed, 108 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/mqtt/switch.py b/homeassistant/components/mqtt/switch.py index bc8eac86a6d..8124dcf811b 100644 --- a/homeassistant/components/mqtt/switch.py +++ b/homeassistant/components/mqtt/switch.py @@ -11,7 +11,7 @@ import voluptuous as vol from homeassistant.core import callback from homeassistant.components.mqtt import ( ATTR_DISCOVERY_HASH, CONF_COMMAND_TOPIC, CONF_QOS, CONF_RETAIN, - CONF_STATE_TOPIC, MqttAvailability, MqttDiscoveryUpdate, + CONF_STATE_TOPIC, MqttAttributes, MqttAvailability, MqttDiscoveryUpdate, MqttEntityDeviceInfo, subscription) from homeassistant.components.mqtt.discovery import ( MQTT_DISCOVERY_NEW, clear_discovery_hash) @@ -47,7 +47,8 @@ PLATFORM_SCHEMA = mqtt.MQTT_RW_PLATFORM_SCHEMA.extend({ vol.Optional(CONF_UNIQUE_ID): cv.string, vol.Optional(CONF_OPTIMISTIC, default=DEFAULT_OPTIMISTIC): cv.boolean, vol.Optional(CONF_DEVICE): mqtt.MQTT_ENTITY_DEVICE_INFO_SCHEMA, -}).extend(mqtt.MQTT_AVAILABILITY_SCHEMA.schema) +}).extend(mqtt.MQTT_AVAILABILITY_SCHEMA.schema).extend( + mqtt.MQTT_JSON_ATTRS_SCHEMA.schema) async def async_setup_platform(hass: HomeAssistantType, config: ConfigType, @@ -83,8 +84,8 @@ async def _async_setup_entity(config, async_add_entities, # pylint: disable=too-many-ancestors -class MqttSwitch(MqttAvailability, MqttDiscoveryUpdate, MqttEntityDeviceInfo, - SwitchDevice, RestoreEntity): +class MqttSwitch(MqttAttributes, MqttAvailability, MqttDiscoveryUpdate, + MqttEntityDeviceInfo, SwitchDevice, RestoreEntity): """Representation of a switch that can be toggled using MQTT.""" def __init__(self, config, discovery_hash): @@ -102,6 +103,7 @@ class MqttSwitch(MqttAvailability, MqttDiscoveryUpdate, MqttEntityDeviceInfo, device_config = config.get(CONF_DEVICE) + MqttAttributes.__init__(self, config) MqttAvailability.__init__(self, config) MqttDiscoveryUpdate.__init__(self, discovery_hash, self.discovery_update) @@ -116,6 +118,7 @@ class MqttSwitch(MqttAvailability, MqttDiscoveryUpdate, MqttEntityDeviceInfo, """Handle updated discovery message.""" config = PLATFORM_SCHEMA(discovery_payload) self._setup_from_config(config) + await self.attributes_discovery_update(config) await self.availability_discovery_update(config) await self._subscribe_topics() self.async_schedule_update_ha_state() @@ -172,6 +175,7 @@ class MqttSwitch(MqttAvailability, MqttDiscoveryUpdate, MqttEntityDeviceInfo, """Unsubscribe when removed.""" self._sub_state = await subscription.async_unsubscribe_topics( self.hass, self._sub_state) + await MqttAttributes.async_will_remove_from_hass(self) await MqttAvailability.async_will_remove_from_hass(self) @property diff --git a/tests/components/mqtt/test_switch.py b/tests/components/mqtt/test_switch.py index f5adb4062c6..b282b3149c4 100644 --- a/tests/components/mqtt/test_switch.py +++ b/tests/components/mqtt/test_switch.py @@ -257,6 +257,106 @@ async def test_custom_state_payload(hass, mock_publish): assert STATE_OFF == state.state +async def test_setting_attribute_via_mqtt_json_message(hass, mqtt_mock): + """Test the setting of attribute via MQTT with JSON payload.""" + assert await async_setup_component(hass, switch.DOMAIN, { + switch.DOMAIN: { + 'platform': 'mqtt', + 'name': 'test', + 'command_topic': 'test-topic', + 'json_attributes_topic': 'attr-topic' + } + }) + + async_fire_mqtt_message(hass, 'attr-topic', '{ "val": "100" }') + await hass.async_block_till_done() + state = hass.states.get('switch.test') + + assert '100' == state.attributes.get('val') + + +async def test_update_with_json_attrs_not_dict(hass, mqtt_mock, caplog): + """Test attributes get extracted from a JSON result.""" + assert await async_setup_component(hass, switch.DOMAIN, { + switch.DOMAIN: { + 'platform': 'mqtt', + 'name': 'test', + 'command_topic': 'test-topic', + 'json_attributes_topic': 'attr-topic' + } + }) + + async_fire_mqtt_message(hass, 'attr-topic', '[ "list", "of", "things"]') + await hass.async_block_till_done() + state = hass.states.get('switch.test') + + assert state.attributes.get('val') is None + assert 'JSON result was not a dictionary' in caplog.text + + +async def test_update_with_json_attrs_bad_JSON(hass, mqtt_mock, caplog): + """Test attributes get extracted from a JSON result.""" + assert await async_setup_component(hass, switch.DOMAIN, { + switch.DOMAIN: { + 'platform': 'mqtt', + 'name': 'test', + 'command_topic': 'test-topic', + 'json_attributes_topic': 'attr-topic' + } + }) + + async_fire_mqtt_message(hass, 'attr-topic', 'This is not JSON') + await hass.async_block_till_done() + + state = hass.states.get('switch.test') + assert state.attributes.get('val') is None + assert 'Erroneous JSON: This is not JSON' in caplog.text + + +async def test_discovery_update_attr(hass, mqtt_mock, caplog): + """Test update of discovered MQTTAttributes.""" + entry = MockConfigEntry(domain=mqtt.DOMAIN) + await async_start(hass, 'homeassistant', {}, entry) + data1 = ( + '{ "name": "Beer",' + ' "command_topic": "test_topic",' + ' "json_attributes_topic": "attr-topic1" }' + ) + data2 = ( + '{ "name": "Beer",' + ' "command_topic": "test_topic",' + ' "json_attributes_topic": "attr-topic2" }' + ) + async_fire_mqtt_message(hass, 'homeassistant/switch/bla/config', + data1) + await hass.async_block_till_done() + async_fire_mqtt_message(hass, 'attr-topic1', '{ "val": "100" }') + await hass.async_block_till_done() + await hass.async_block_till_done() + state = hass.states.get('switch.beer') + assert '100' == state.attributes.get('val') + + # Change json_attributes_topic + async_fire_mqtt_message(hass, 'homeassistant/switch/bla/config', + data2) + await hass.async_block_till_done() + await hass.async_block_till_done() + + # Verify we are no longer subscribing to the old topic + async_fire_mqtt_message(hass, 'attr-topic1', '{ "val": "50" }') + await hass.async_block_till_done() + await hass.async_block_till_done() + state = hass.states.get('switch.beer') + assert '100' == state.attributes.get('val') + + # Verify we are subscribing to the new topic + async_fire_mqtt_message(hass, 'attr-topic2', '{ "val": "75" }') + await hass.async_block_till_done() + await hass.async_block_till_done() + state = hass.states.get('switch.beer') + assert '75' == state.attributes.get('val') + + async def test_unique_id(hass): """Test unique id option only creates one switch per unique_id.""" await async_mock_mqtt_component(hass) From 6800871c1334493447ab8f2b391055c14cf8cf25 Mon Sep 17 00:00:00 2001 From: emontnemery Date: Thu, 17 Jan 2019 23:44:57 +0100 Subject: [PATCH 010/222] Log exceptions thrown by signal callbacks (#20015) * Log exceptions thrown by signal callbacks * Fix unsub * Simplify traceback print * Typing * Add test * lint * Review comments * Rework MQTT test case * Fix bad merge * Fix bad merge --- homeassistant/helpers/dispatcher.py | 10 +++++-- tests/components/mqtt/test_init.py | 43 +++++++++++++++-------------- tests/helpers/test_dispatcher.py | 19 ++++++++++++- 3 files changed, 48 insertions(+), 24 deletions(-) diff --git a/homeassistant/helpers/dispatcher.py b/homeassistant/helpers/dispatcher.py index a28cd3d6392..ec07984f901 100644 --- a/homeassistant/helpers/dispatcher.py +++ b/homeassistant/helpers/dispatcher.py @@ -5,6 +5,7 @@ from typing import Any, Callable from homeassistant.core import callback from homeassistant.loader import bind_hass from homeassistant.util.async_ import run_callback_threadsafe +from homeassistant.util.logging import catch_log_exception from .typing import HomeAssistantType @@ -40,13 +41,18 @@ def async_dispatcher_connect(hass: HomeAssistantType, signal: str, if signal not in hass.data[DATA_DISPATCHER]: hass.data[DATA_DISPATCHER][signal] = [] - hass.data[DATA_DISPATCHER][signal].append(target) + wrapped_target = catch_log_exception( + target, lambda *args: + "Exception in {} when dispatching '{}': {}".format( + target.__name__, signal, args)) + + hass.data[DATA_DISPATCHER][signal].append(wrapped_target) @callback def async_remove_dispatcher() -> None: """Remove signal listener.""" try: - hass.data[DATA_DISPATCHER][signal].remove(target) + hass.data[DATA_DISPATCHER][signal].remove(wrapped_target) except (KeyError, ValueError): # KeyError is key target listener did not exist # ValueError if listener did not exist within signal diff --git a/tests/components/mqtt/test_init.py b/tests/components/mqtt/test_init.py index 540cfe0369d..707d9ff6021 100644 --- a/tests/components/mqtt/test_init.py +++ b/tests/components/mqtt/test_init.py @@ -13,10 +13,10 @@ from homeassistant.components import mqtt from homeassistant.const import (EVENT_CALL_SERVICE, ATTR_DOMAIN, ATTR_SERVICE, EVENT_HOMEASSISTANT_STOP) -from tests.common import (get_test_home_assistant, mock_coro, - mock_mqtt_component, - threadsafe_coroutine_factory, fire_mqtt_message, - async_fire_mqtt_message, MockConfigEntry) +from tests.common import ( + MockConfigEntry, async_fire_mqtt_message, async_mock_mqtt_component, + fire_mqtt_message, get_test_home_assistant, mock_coro, mock_mqtt_component, + threadsafe_coroutine_factory) @pytest.fixture @@ -297,23 +297,6 @@ class TestMQTTCallbacks(unittest.TestCase): "b'\\x9a' on test-topic with encoding utf-8" in \ test_handle.output[0] - def test_message_callback_exception_gets_logged(self): - """Test exception raised by message handler.""" - @callback - def bad_handler(*args): - """Record calls.""" - raise Exception('This is a bad message callback') - mqtt.subscribe(self.hass, 'test-topic', bad_handler) - - with self.assertLogs(level='WARNING') as test_handle: - fire_mqtt_message(self.hass, 'test-topic', 'test') - - self.hass.block_till_done() - assert \ - "Exception in bad_handler when handling msg on 'test-topic':" \ - " 'test'" in \ - test_handle.output[0] - def test_all_subscriptions_run_when_decode_fails(self): """Test all other subscriptions still run when decode fails for one.""" mqtt.subscribe(self.hass, 'test-topic', self.record_calls, @@ -766,3 +749,21 @@ def test_mqtt_subscribes_topics_on_connect(hass): async def test_setup_fails_without_config(hass): """Test if the MQTT component fails to load with no config.""" assert not await async_setup_component(hass, mqtt.DOMAIN, {}) + + +async def test_message_callback_exception_gets_logged(hass, caplog): + """Test exception raised by message handler.""" + await async_mock_mqtt_component(hass) + + @callback + def bad_handler(*args): + """Record calls.""" + raise Exception('This is a bad message callback') + + await mqtt.async_subscribe(hass, 'test-topic', bad_handler) + async_fire_mqtt_message(hass, 'test-topic', 'test') + await hass.async_block_till_done() + + assert \ + "Exception in bad_handler when handling msg on 'test-topic':" \ + " 'test'" in caplog.text diff --git a/tests/helpers/test_dispatcher.py b/tests/helpers/test_dispatcher.py index ef1ad2336eb..2812bc6353b 100644 --- a/tests/helpers/test_dispatcher.py +++ b/tests/helpers/test_dispatcher.py @@ -3,7 +3,7 @@ import asyncio from homeassistant.core import callback from homeassistant.helpers.dispatcher import ( - dispatcher_send, dispatcher_connect) + async_dispatcher_connect, dispatcher_send, dispatcher_connect) from tests.common import get_test_home_assistant @@ -134,3 +134,20 @@ class TestHelpersDispatcher: self.hass.block_till_done() assert calls == [3, 2, 'bla'] + + +async def test_callback_exception_gets_logged(hass, caplog): + """Test exception raised by signal handler.""" + @callback + def bad_handler(*args): + """Record calls.""" + raise Exception('This is a bad message callback') + + async_dispatcher_connect(hass, 'test', bad_handler) + dispatcher_send(hass, 'test', 'bad') + await hass.async_block_till_done() + await hass.async_block_till_done() + + assert \ + "Exception in bad_handler when dispatching 'test': ('bad',)" \ + in caplog.text From afa019ae47f8bd61c96ad702203a566d2969cd5d Mon Sep 17 00:00:00 2001 From: ehendrix23 Date: Thu, 17 Jan 2019 19:35:45 -0700 Subject: [PATCH 011/222] Set ehendrix23 as owner for harmony platform (#20203) Put myself (ehendrix23) as code owner for remote.harmony platform --- CODEOWNERS | 1 + 1 file changed, 1 insertion(+) diff --git a/CODEOWNERS b/CODEOWNERS index ff5211bd776..4b4931ecc3a 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -95,6 +95,7 @@ homeassistant/components/notify/syslog.py @fabaff homeassistant/components/notify/xmpp.py @fabaff homeassistant/components/notify/yessssms.py @flowolf homeassistant/components/plant.py @ChristianKuehnel +homeassistant/components/remote/harmony.py @ehendrix23 homeassistant/components/scene/lifx_cloud.py @amelchio homeassistant/components/sensor/airvisual.py @bachya homeassistant/components/sensor/alpha_vantage.py @fabaff From 81a5208762069fa1e0ef15919decf57ed0df41b0 Mon Sep 17 00:00:00 2001 From: zhujisheng <30714273+zhujisheng@users.noreply.github.com> Date: Fri, 18 Jan 2019 20:40:49 +0800 Subject: [PATCH 012/222] Add platform image_processing.qrcode (#20215) * Add platform image_processing.qrcode * Update qrcode.py --- .coveragerc | 1 + .../components/image_processing/qrcode.py | 69 +++++++++++++++++++ requirements_all.txt | 4 ++ 3 files changed, 74 insertions(+) create mode 100644 homeassistant/components/image_processing/qrcode.py diff --git a/.coveragerc b/.coveragerc index cd8b6da4d71..29a9caa8066 100644 --- a/.coveragerc +++ b/.coveragerc @@ -573,6 +573,7 @@ omit = homeassistant/components/image_processing/dlib_face_identify.py homeassistant/components/image_processing/seven_segments.py homeassistant/components/image_processing/tensorflow.py + homeassistant/components/image_processing/qrcode.py homeassistant/components/keyboard_remote.py homeassistant/components/keyboard.py homeassistant/components/light/avion.py diff --git a/homeassistant/components/image_processing/qrcode.py b/homeassistant/components/image_processing/qrcode.py new file mode 100644 index 00000000000..00f4ad025b2 --- /dev/null +++ b/homeassistant/components/image_processing/qrcode.py @@ -0,0 +1,69 @@ +""" +Support for the QR image processing. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/image_processing.qr/ +""" +from homeassistant.core import split_entity_id +from homeassistant.components.image_processing import ( + ImageProcessingEntity, CONF_SOURCE, CONF_ENTITY_ID, CONF_NAME) + +REQUIREMENTS = ['pyzbar==0.1.7', 'pillow==5.4.1'] + + +def setup_platform(hass, config, add_entities, discovery_info=None): + """Set up the demo image processing platform.""" + # pylint: disable=unused-argument + entities = [] + for camera in config[CONF_SOURCE]: + entities.append(QrEntity( + camera[CONF_ENTITY_ID], camera.get(CONF_NAME) + )) + + add_entities(entities) + + +class QrEntity(ImageProcessingEntity): + """QR image processing entity.""" + + def __init__(self, camera_entity, name): + """Initialize QR image processing entity.""" + super().__init__() + + self._camera = camera_entity + if name: + self._name = name + else: + self._name = "QR {0}".format( + split_entity_id(camera_entity)[1]) + self._state = None + + @property + def camera_entity(self): + """Return camera entity id from process pictures.""" + return self._camera + + @property + def state(self): + """Return the state of the entity.""" + return self._state + + @property + def name(self): + """Return the name of the entity.""" + return self._name + + def process_image(self, image): + """Process image.""" + import io + from pyzbar import pyzbar + from PIL import Image + + stream = io.BytesIO(image) + img = Image.open(stream) + + barcodes = pyzbar.decode(img) + if barcodes: + self._state = barcodes[0].data.decode("utf-8") + else: + self._state = None diff --git a/requirements_all.txt b/requirements_all.txt index cd6c901c4b7..9138a1e0c03 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -789,6 +789,7 @@ piglow==1.2.4 pilight==0.1.1 # homeassistant.components.camera.proxy +# homeassistant.components.image_processing.qrcode # homeassistant.components.image_processing.tensorflow pillow==5.4.1 @@ -1397,6 +1398,9 @@ pyxeoma==1.4.0 # homeassistant.components.zabbix pyzabbix==0.7.4 +# homeassistant.components.image_processing.qrcode +pyzbar==0.1.7 + # homeassistant.components.sensor.qnap qnapstats==0.2.7 From 84b1fcbc361a5c12f0eaa93e502dffe11b84e628 Mon Sep 17 00:00:00 2001 From: Rohan Kapoor Date: Fri, 18 Jan 2019 04:42:52 -0800 Subject: [PATCH 013/222] Add verify_ssl to restful_command and switch.rest (#20199) (#20207) --- homeassistant/components/rest_command.py | 12 ++++++++---- homeassistant/components/switch/rest.py | 14 +++++++++----- tests/components/switch/test_rest.py | 2 +- 3 files changed, 18 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/rest_command.py b/homeassistant/components/rest_command.py index 3f9b258634d..ce5873f41d4 100644 --- a/homeassistant/components/rest_command.py +++ b/homeassistant/components/rest_command.py @@ -14,7 +14,7 @@ import voluptuous as vol from homeassistant.const import ( CONF_TIMEOUT, CONF_USERNAME, CONF_PASSWORD, CONF_URL, CONF_PAYLOAD, - CONF_METHOD, CONF_HEADERS) + CONF_METHOD, CONF_HEADERS, CONF_VERIFY_SSL) from homeassistant.helpers.aiohttp_client import async_get_clientsession import homeassistant.helpers.config_validation as cv @@ -24,6 +24,7 @@ _LOGGER = logging.getLogger(__name__) DEFAULT_TIMEOUT = 10 DEFAULT_METHOD = 'get' +DEFAULT_VERIFY_SSL = True SUPPORT_REST_METHODS = [ 'get', @@ -43,7 +44,8 @@ COMMAND_SCHEMA = vol.Schema({ vol.Inclusive(CONF_PASSWORD, 'authentication'): cv.string, vol.Optional(CONF_PAYLOAD): cv.template, vol.Optional(CONF_TIMEOUT, default=DEFAULT_TIMEOUT): vol.Coerce(int), - vol.Optional(CONF_CONTENT_TYPE): cv.string + vol.Optional(CONF_CONTENT_TYPE): cv.string, + vol.Optional(CONF_VERIFY_SSL, default=DEFAULT_VERIFY_SSL): cv.boolean, }) CONFIG_SCHEMA = vol.Schema({ @@ -55,10 +57,12 @@ CONFIG_SCHEMA = vol.Schema({ async def async_setup(hass, config): """Set up the REST command component.""" - websession = async_get_clientsession(hass) - def async_register_rest_command(name, command_config): """Create service for rest command.""" + websession = async_get_clientsession( + hass, + command_config.get(CONF_VERIFY_SSL) + ) timeout = command_config[CONF_TIMEOUT] method = command_config[CONF_METHOD] diff --git a/homeassistant/components/switch/rest.py b/homeassistant/components/switch/rest.py index 9b8f889a8ae..5f1920ae1af 100644 --- a/homeassistant/components/switch/rest.py +++ b/homeassistant/components/switch/rest.py @@ -14,7 +14,7 @@ import voluptuous as vol from homeassistant.components.switch import (SwitchDevice, PLATFORM_SCHEMA) from homeassistant.const import ( CONF_HEADERS, CONF_NAME, CONF_RESOURCE, CONF_TIMEOUT, CONF_METHOD, - CONF_USERNAME, CONF_PASSWORD) + CONF_USERNAME, CONF_PASSWORD, CONF_VERIFY_SSL) from homeassistant.helpers.aiohttp_client import async_get_clientsession import homeassistant.helpers.config_validation as cv @@ -29,6 +29,7 @@ DEFAULT_BODY_OFF = 'OFF' DEFAULT_BODY_ON = 'ON' DEFAULT_NAME = 'REST Switch' DEFAULT_TIMEOUT = 10 +DEFAULT_VERIFY_SSL = True SUPPORT_REST_METHODS = ['post', 'put'] @@ -44,6 +45,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Optional(CONF_TIMEOUT, default=DEFAULT_TIMEOUT): cv.positive_int, vol.Inclusive(CONF_USERNAME, 'authentication'): cv.string, vol.Inclusive(CONF_PASSWORD, 'authentication'): cv.string, + vol.Optional(CONF_VERIFY_SSL, default=DEFAULT_VERIFY_SSL): cv.boolean, }) @@ -59,6 +61,7 @@ async def async_setup_platform(hass, config, async_add_entities, username = config.get(CONF_USERNAME) password = config.get(CONF_PASSWORD) resource = config.get(CONF_RESOURCE) + verify_ssl = config.get(CONF_VERIFY_SSL) auth = None if username: @@ -74,7 +77,7 @@ async def async_setup_platform(hass, config, async_add_entities, try: switch = RestSwitch(name, resource, method, headers, auth, body_on, - body_off, is_on_template, timeout) + body_off, is_on_template, timeout, verify_ssl) req = await switch.get_device_state(hass) if req.status >= 400: @@ -92,7 +95,7 @@ class RestSwitch(SwitchDevice): """Representation of a switch that can be toggled using REST.""" def __init__(self, name, resource, method, headers, auth, body_on, - body_off, is_on_template, timeout): + body_off, is_on_template, timeout, verify_ssl): """Initialize the REST switch.""" self._state = None self._name = name @@ -104,6 +107,7 @@ class RestSwitch(SwitchDevice): self._body_off = body_off self._is_on_template = is_on_template self._timeout = timeout + self._verify_ssl = verify_ssl @property def name(self): @@ -148,7 +152,7 @@ class RestSwitch(SwitchDevice): async def set_device_state(self, body): """Send a state update to the device.""" - websession = async_get_clientsession(self.hass) + websession = async_get_clientsession(self.hass, self._verify_ssl) with async_timeout.timeout(self._timeout, loop=self.hass.loop): req = await getattr(websession, self._method)( @@ -167,7 +171,7 @@ class RestSwitch(SwitchDevice): async def get_device_state(self, hass): """Get the latest data from REST API and update the state.""" - websession = async_get_clientsession(hass) + websession = async_get_clientsession(hass, self._verify_ssl) with async_timeout.timeout(self._timeout, loop=hass.loop): req = await websession.get(self._resource, auth=self._auth, diff --git a/tests/components/switch/test_rest.py b/tests/components/switch/test_rest.py index cb27ab40855..56f3f0eebc5 100644 --- a/tests/components/switch/test_rest.py +++ b/tests/components/switch/test_rest.py @@ -106,7 +106,7 @@ class TestRestSwitch: self.body_off = Template('off', self.hass) self.switch = rest.RestSwitch( self.name, self.resource, self.method, self.headers, self.auth, - self.body_on, self.body_off, None, 10) + self.body_on, self.body_off, None, 10, True) self.switch.hass = self.hass def teardown_method(self): From 63b19094c173139b50dacb48ec5a8d2db34b4689 Mon Sep 17 00:00:00 2001 From: Anders Melchiorsen Date: Fri, 18 Jan 2019 13:43:48 +0100 Subject: [PATCH 014/222] Improve Sonos discovery (#20196) --- homeassistant/components/sonos/__init__.py | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/sonos/__init__.py b/homeassistant/components/sonos/__init__.py index 529df41de58..b4f507a60dd 100644 --- a/homeassistant/components/sonos/__init__.py +++ b/homeassistant/components/sonos/__init__.py @@ -4,7 +4,7 @@ from homeassistant.helpers import config_entry_flow DOMAIN = 'sonos' -REQUIREMENTS = ['pysonos==0.0.5'] +REQUIREMENTS = ['pysonos==0.0.6'] async def async_setup(hass, config): diff --git a/requirements_all.txt b/requirements_all.txt index 9138a1e0c03..20bb1aaf8a3 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1209,7 +1209,7 @@ pysma==0.3.1 pysnmp==4.4.8 # homeassistant.components.sonos -pysonos==0.0.5 +pysonos==0.0.6 # homeassistant.components.spc pyspcwebgw==0.4.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index fb49f040b7e..039772db66c 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -202,7 +202,7 @@ pyotp==2.2.6 pyqwikswitch==0.8 # homeassistant.components.sonos -pysonos==0.0.5 +pysonos==0.0.6 # homeassistant.components.spc pyspcwebgw==0.4.0 From e80702a45ce575b89c39a0ca4e8b373bfc33e3e3 Mon Sep 17 00:00:00 2001 From: Louis Matthijssen Date: Fri, 18 Jan 2019 15:02:27 +0100 Subject: [PATCH 015/222] Fix unused friendly name for SolarEdge sensor (#20109) --- homeassistant/components/sensor/solaredge.py | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/sensor/solaredge.py b/homeassistant/components/sensor/solaredge.py index 1cabe7c0445..fa49cdb3bfe 100644 --- a/homeassistant/components/sensor/solaredge.py +++ b/homeassistant/components/sensor/solaredge.py @@ -28,14 +28,14 @@ SCAN_INTERVAL = timedelta(minutes=10) # Supported sensor types: # Key: ['json_key', 'name', unit, icon] SENSOR_TYPES = { - 'life_time_data': ['lifeTimeData', "Lifetime energy", 'Wh', - 'mdi:solar-power'], - 'last_year_data': ['lastYearData', "Energy this year", 'Wh', - 'mdi:solar-power'], - 'last_month_data': ['lastMonthData', "Energy this month", 'Wh', + 'lifetime_energy': ['lifeTimeData', "Lifetime energy", 'Wh', 'mdi:solar-power'], - 'last_day_data': ['lastDayData', "Energy today", 'Wh', - 'mdi:solar-power'], + 'energy_this_year': ['lastYearData', "Energy this year", 'Wh', + 'mdi:solar-power'], + 'energy_this_month': ['lastMonthData', "Energy this month", 'Wh', + 'mdi:solar-power'], + 'energy_today': ['lastDayData', "Energy today", 'Wh', + 'mdi:solar-power'], 'current_power': ['currentPower', "Current Power", 'W', 'mdi:solar-power'] } @@ -106,7 +106,8 @@ class SolarEdgeSensor(Entity): @property def name(self): """Return the name.""" - return "{}_{}".format(self.platform_name, self.sensor_key) + return "{} ({})".format(self.platform_name, + SENSOR_TYPES[self.sensor_key][1]) @property def unit_of_measurement(self): From 440d479be89bb3aa059f3d3f820c5aa476971fb3 Mon Sep 17 00:00:00 2001 From: Joakim Lindbom Date: Sat, 19 Jan 2019 07:12:56 +0100 Subject: [PATCH 016/222] Fix for issue #19086 (#20225) --- homeassistant/components/media_player/volumio.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/media_player/volumio.py b/homeassistant/components/media_player/volumio.py index 373d3c380fc..bd43e6c3710 100644 --- a/homeassistant/components/media_player/volumio.py +++ b/homeassistant/components/media_player/volumio.py @@ -189,7 +189,7 @@ class Volumio(MediaPlayerDevice): """Volume level of the media player (0..1).""" volume = self._state.get('volume', None) if volume is not None and volume != "": - volume = volume / 100 + volume = int(volume) / 100 return volume @property From 5b8f64093bd4bec353012e3d7eac3c8b3cf52fa9 Mon Sep 17 00:00:00 2001 From: Erik Date: Sat, 19 Jan 2019 11:58:21 +0100 Subject: [PATCH 017/222] Add JSON attribute topic to MQTT vacuum --- homeassistant/components/mqtt/vacuum.py | 12 ++- tests/components/mqtt/test_vacuum.py | 100 ++++++++++++++++++++++++ 2 files changed, 108 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/mqtt/vacuum.py b/homeassistant/components/mqtt/vacuum.py index 612737c990d..baa43411411 100644 --- a/homeassistant/components/mqtt/vacuum.py +++ b/homeassistant/components/mqtt/vacuum.py @@ -10,7 +10,7 @@ import voluptuous as vol from homeassistant.components import mqtt from homeassistant.components.mqtt import ( - ATTR_DISCOVERY_HASH, MqttAvailability, MqttDiscoveryUpdate, + ATTR_DISCOVERY_HASH, MqttAttributes, MqttAvailability, MqttDiscoveryUpdate, MqttEntityDeviceInfo, subscription) from homeassistant.components.mqtt.discovery import MQTT_DISCOVERY_NEW from homeassistant.components.vacuum import ( @@ -147,7 +147,8 @@ PLATFORM_SCHEMA = mqtt.MQTT_BASE_PLATFORM_SCHEMA.extend({ vol.Optional(CONF_SEND_COMMAND_TOPIC): mqtt.valid_publish_topic, vol.Optional(CONF_UNIQUE_ID): cv.string, vol.Optional(CONF_DEVICE): mqtt.MQTT_ENTITY_DEVICE_INFO_SCHEMA, -}).extend(mqtt.MQTT_AVAILABILITY_SCHEMA.schema) +}).extend(mqtt.MQTT_AVAILABILITY_SCHEMA.schema).extend( + mqtt.MQTT_JSON_ATTRS_SCHEMA.schema) async def async_setup_platform(hass, config, async_add_entities, @@ -176,8 +177,8 @@ async def _async_setup_entity(config, async_add_entities, # pylint: disable=too-many-ancestors -class MqttVacuum(MqttAvailability, MqttDiscoveryUpdate, MqttEntityDeviceInfo, - VacuumDevice): +class MqttVacuum(MqttAttributes, MqttAvailability, MqttDiscoveryUpdate, + MqttEntityDeviceInfo, VacuumDevice): """Representation of a MQTT-controlled vacuum.""" def __init__(self, config, discovery_info): @@ -198,6 +199,7 @@ class MqttVacuum(MqttAvailability, MqttDiscoveryUpdate, MqttEntityDeviceInfo, device_config = config.get(CONF_DEVICE) + MqttAttributes.__init__(self, config) MqttAvailability.__init__(self, config) MqttDiscoveryUpdate.__init__(self, discovery_info, self.discovery_update) @@ -253,6 +255,7 @@ class MqttVacuum(MqttAvailability, MqttDiscoveryUpdate, MqttEntityDeviceInfo, """Handle updated discovery message.""" config = PLATFORM_SCHEMA(discovery_payload) self._setup_from_config(config) + await self.attributes_discovery_update(config) await self.availability_discovery_update(config) await self._subscribe_topics() self.async_schedule_update_ha_state() @@ -265,6 +268,7 @@ class MqttVacuum(MqttAvailability, MqttDiscoveryUpdate, MqttEntityDeviceInfo, async def async_will_remove_from_hass(self): """Unsubscribe when removed.""" await subscription.async_unsubscribe_topics(self.hass, self._sub_state) + await MqttAttributes.async_will_remove_from_hass(self) await MqttAvailability.async_will_remove_from_hass(self) async def _subscribe_topics(self): diff --git a/tests/components/mqtt/test_vacuum.py b/tests/components/mqtt/test_vacuum.py index 356ce44c6cb..99a7b3579f3 100644 --- a/tests/components/mqtt/test_vacuum.py +++ b/tests/components/mqtt/test_vacuum.py @@ -333,6 +333,106 @@ async def test_discovery_update_vacuum(hass, mock_publish): assert state is None +async def test_setting_attribute_via_mqtt_json_message(hass, mqtt_mock): + """Test the setting of attribute via MQTT with JSON payload.""" + assert await async_setup_component(hass, vacuum.DOMAIN, { + vacuum.DOMAIN: { + 'platform': 'mqtt', + 'name': 'test', + 'state_topic': 'test-topic', + 'json_attributes_topic': 'attr-topic' + } + }) + + async_fire_mqtt_message(hass, 'attr-topic', '{ "val": "100" }') + await hass.async_block_till_done() + state = hass.states.get('vacuum.test') + + assert '100' == state.attributes.get('val') + + +async def test_update_with_json_attrs_not_dict(hass, mqtt_mock, caplog): + """Test attributes get extracted from a JSON result.""" + assert await async_setup_component(hass, vacuum.DOMAIN, { + vacuum.DOMAIN: { + 'platform': 'mqtt', + 'name': 'test', + 'state_topic': 'test-topic', + 'json_attributes_topic': 'attr-topic' + } + }) + + async_fire_mqtt_message(hass, 'attr-topic', '[ "list", "of", "things"]') + await hass.async_block_till_done() + state = hass.states.get('vacuum.test') + + assert state.attributes.get('val') is None + assert 'JSON result was not a dictionary' in caplog.text + + +async def test_update_with_json_attrs_bad_JSON(hass, mqtt_mock, caplog): + """Test attributes get extracted from a JSON result.""" + assert await async_setup_component(hass, vacuum.DOMAIN, { + vacuum.DOMAIN: { + 'platform': 'mqtt', + 'name': 'test', + 'state_topic': 'test-topic', + 'json_attributes_topic': 'attr-topic' + } + }) + + async_fire_mqtt_message(hass, 'attr-topic', 'This is not JSON') + await hass.async_block_till_done() + + state = hass.states.get('vacuum.test') + assert state.attributes.get('val') is None + assert 'Erroneous JSON: This is not JSON' in caplog.text + + +async def test_discovery_update_attr(hass, mqtt_mock, caplog): + """Test update of discovered MQTTAttributes.""" + entry = MockConfigEntry(domain=mqtt.DOMAIN) + await async_start(hass, 'homeassistant', {}, entry) + data1 = ( + '{ "name": "Beer",' + ' "command_topic": "test_topic",' + ' "json_attributes_topic": "attr-topic1" }' + ) + data2 = ( + '{ "name": "Beer",' + ' "command_topic": "test_topic",' + ' "json_attributes_topic": "attr-topic2" }' + ) + async_fire_mqtt_message(hass, 'homeassistant/vacuum/bla/config', + data1) + await hass.async_block_till_done() + async_fire_mqtt_message(hass, 'attr-topic1', '{ "val": "100" }') + await hass.async_block_till_done() + await hass.async_block_till_done() + state = hass.states.get('vacuum.beer') + assert '100' == state.attributes.get('val') + + # Change json_attributes_topic + async_fire_mqtt_message(hass, 'homeassistant/vacuum/bla/config', + data2) + await hass.async_block_till_done() + await hass.async_block_till_done() + + # Verify we are no longer subscribing to the old topic + async_fire_mqtt_message(hass, 'attr-topic1', '{ "val": "50" }') + await hass.async_block_till_done() + await hass.async_block_till_done() + state = hass.states.get('vacuum.beer') + assert '100' == state.attributes.get('val') + + # Verify we are subscribing to the new topic + async_fire_mqtt_message(hass, 'attr-topic2', '{ "val": "75" }') + await hass.async_block_till_done() + await hass.async_block_till_done() + state = hass.states.get('vacuum.beer') + assert '75' == state.attributes.get('val') + + async def test_unique_id(hass, mock_publish): """Test unique id option only creates one vacuum per unique_id.""" await async_mock_mqtt_component(hass) From 8000b971808194307bafb1496d405fd0f0e7c436 Mon Sep 17 00:00:00 2001 From: Otto Winter Date: Sat, 19 Jan 2019 17:13:32 +0100 Subject: [PATCH 018/222] Bump aioesphomeapi to 1.4.2 (#20247) * Bump aioesphomeapi to 1.4.2 * Update requirements_all.txt --- homeassistant/components/esphome/__init__.py | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/esphome/__init__.py b/homeassistant/components/esphome/__init__.py index 7578f2e244f..1ff2c10c828 100644 --- a/homeassistant/components/esphome/__init__.py +++ b/homeassistant/components/esphome/__init__.py @@ -31,7 +31,7 @@ if TYPE_CHECKING: ServiceCall DOMAIN = 'esphome' -REQUIREMENTS = ['aioesphomeapi==1.4.1'] +REQUIREMENTS = ['aioesphomeapi==1.4.2'] DISPATCHER_UPDATE_ENTITY = 'esphome_{entry_id}_update_{component_key}_{key}' diff --git a/requirements_all.txt b/requirements_all.txt index 20bb1aaf8a3..a10ba96a50a 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -96,7 +96,7 @@ aioautomatic==0.6.5 aiodns==1.1.1 # homeassistant.components.esphome -aioesphomeapi==1.4.1 +aioesphomeapi==1.4.2 # homeassistant.components.freebox aiofreepybox==0.0.6 From fb52f66da046f467c85a0b5afa225f3d20ec54dd Mon Sep 17 00:00:00 2001 From: Victor Vostrikov <1998617+gorynychzmey@users.noreply.github.com> Date: Sat, 19 Jan 2019 18:08:53 +0100 Subject: [PATCH 019/222] Use local IP to discover IGD device (#20035) * Use local_ip from config to discover IGD device In case of multi-homed server UPNP discovery finds IGD device on some "default" interface. WIth this modification discovery will be performed from 'local_ip'. * Update device.py * Changed version of async_upnp_client in requirements * Used aysnc_upnp_client==0.14.0 * Changed requirement to async_upnp_client==0.14.0.dev0 * Changed requirement to async_upnp_client==0.14.0.dev0 * Changed requirement to async_upnp_client==0.14.0.dev0 * Fixed code style * Fixed code style * Changed version of async_upnp_client in requerements * Changed version of async_upnp_client in requirements * Regenerated requirements (new async_upnp_client) * Regenerated requirements (new async_upnp_client) * Changed requirement to async_upnp_client=0.14.1 * Changed requirement to async_upnp_client=0.14.1 * Updated requirements * Updated requirements.txt * Corrected requirements * Corrected import of DeviceState * Constants changed according new async_upnp_client * Upgraded for async_upnp_client==0.14.2 --- homeassistant/components/media_player/dlna_dmr.py | 14 +++++++------- homeassistant/components/upnp/__init__.py | 2 +- homeassistant/components/upnp/device.py | 12 ++++++++---- requirements_all.txt | 2 +- 4 files changed, 17 insertions(+), 13 deletions(-) diff --git a/homeassistant/components/media_player/dlna_dmr.py b/homeassistant/components/media_player/dlna_dmr.py index 479445e9a89..9d271f0db6c 100644 --- a/homeassistant/components/media_player/dlna_dmr.py +++ b/homeassistant/components/media_player/dlna_dmr.py @@ -26,7 +26,7 @@ from homeassistant.helpers.aiohttp_client import async_get_clientsession import homeassistant.helpers.config_validation as cv from homeassistant.util import get_local_ip -REQUIREMENTS = ['async-upnp-client==0.13.8'] +REQUIREMENTS = ['async-upnp-client==0.14.2'] _LOGGER = logging.getLogger(__name__) @@ -145,7 +145,7 @@ async def async_setup_platform( raise PlatformNotReady() # wrap with DmrDevice - from async_upnp_client.dlna import DmrDevice + from async_upnp_client.profiles.dlna import DmrDevice dlna_device = DmrDevice(upnp_device, event_handler) # create our own device @@ -314,8 +314,8 @@ class DlnaDmrDevice(MediaPlayerDevice): await self._device.async_wait_for_can_play() # If already playing, no need to call Play - from async_upnp_client import dlna - if self._device.state == dlna.STATE_PLAYING: + from async_upnp_client.profiles.dlna import DeviceState + if self._device.state == DeviceState.PLAYING: return # Play it @@ -355,12 +355,12 @@ class DlnaDmrDevice(MediaPlayerDevice): if not self._available: return STATE_OFF - from async_upnp_client import dlna + from async_upnp_client.profiles.dlna import DeviceState if self._device.state is None: return STATE_ON - if self._device.state == dlna.STATE_PLAYING: + if self._device.state == DeviceState.PLAYING: return STATE_PLAYING - if self._device.state == dlna.STATE_PAUSED: + if self._device.state == DeviceState.PAUSED: return STATE_PAUSED return STATE_IDLE diff --git a/homeassistant/components/upnp/__init__.py b/homeassistant/components/upnp/__init__.py index d44cf2a8683..820bce2cc50 100644 --- a/homeassistant/components/upnp/__init__.py +++ b/homeassistant/components/upnp/__init__.py @@ -29,7 +29,7 @@ from .const import LOGGER as _LOGGER from .device import Device -REQUIREMENTS = ['async-upnp-client==0.13.8'] +REQUIREMENTS = ['async-upnp-client==0.14.2'] NOTIFICATION_ID = 'upnp_notification' NOTIFICATION_TITLE = 'UPnP/IGD Setup' diff --git a/homeassistant/components/upnp/device.py b/homeassistant/components/upnp/device.py index a99123129aa..6bbf0a3dd53 100644 --- a/homeassistant/components/upnp/device.py +++ b/homeassistant/components/upnp/device.py @@ -8,6 +8,7 @@ from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.typing import HomeAssistantType from .const import LOGGER as _LOGGER +from .const import (DOMAIN, CONF_LOCAL_IP) class Device: @@ -22,15 +23,18 @@ class Device: async def async_discover(cls, hass: HomeAssistantType): """Discovery UPNP/IGD devices.""" _LOGGER.debug('Discovering UPnP/IGD devices') + local_ip = hass.data[DOMAIN]['config'].get(CONF_LOCAL_IP) + if local_ip: + local_ip = IPv4Address(local_ip) # discover devices - from async_upnp_client.igd import IgdDevice - discovery_infos = await IgdDevice.async_discover() + from async_upnp_client.profiles.igd import IgdDevice + discovery_infos = await IgdDevice.async_search(source_ip=local_ip) # add extra info and store devices devices = [] for discovery_info in discovery_infos: - discovery_info['udn'] = discovery_info['usn'].split('::')[0] + discovery_info['udn'] = discovery_info['_udn'] discovery_info['ssdp_description'] = discovery_info['location'] discovery_info['source'] = 'async_upnp_client' _LOGGER.debug('Discovered device: %s', discovery_info) @@ -56,7 +60,7 @@ class Device: upnp_device = await factory.async_create_device(ssdp_description) # wrap with async_upnp_client.IgdDevice - from async_upnp_client.igd import IgdDevice + from async_upnp_client.profiles.igd import IgdDevice igd_device = IgdDevice(upnp_device, None) return cls(igd_device) diff --git a/requirements_all.txt b/requirements_all.txt index a10ba96a50a..a1e5c2512bf 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -164,7 +164,7 @@ asterisk_mbox==0.5.0 # homeassistant.components.upnp # homeassistant.components.media_player.dlna_dmr -async-upnp-client==0.13.8 +async-upnp-client==0.14.2 # homeassistant.components.light.avion # avion==0.10 From 0987219b2828417c78e12763656626401eb934bb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20H=C3=B8yer=20Iversen?= Date: Sat, 19 Jan 2019 19:23:22 +0100 Subject: [PATCH 020/222] Tibber Pulse for homes without subscriptions (#20246) --- homeassistant/components/sensor/tibber.py | 5 +++-- homeassistant/components/tibber/__init__.py | 2 +- requirements_all.txt | 2 +- 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/sensor/tibber.py b/homeassistant/components/sensor/tibber.py index 0ba470ca778..bc3951f0dea 100644 --- a/homeassistant/components/sensor/tibber.py +++ b/homeassistant/components/sensor/tibber.py @@ -45,11 +45,12 @@ async def async_setup_platform(hass, config, async_add_entities, except aiohttp.ClientError as err: _LOGGER.error("Error connecting to Tibber home: %s ", err) raise PlatformNotReady() - dev.append(TibberSensorElPrice(home)) + if home.has_active_subscription: + dev.append(TibberSensorElPrice(home)) if home.has_real_time_consumption: dev.append(TibberSensorRT(home)) - async_add_entities(dev, False) + async_add_entities(dev, True) class TibberSensorElPrice(Entity): diff --git a/homeassistant/components/tibber/__init__.py b/homeassistant/components/tibber/__init__.py index 8967021a0cf..8492596332a 100644 --- a/homeassistant/components/tibber/__init__.py +++ b/homeassistant/components/tibber/__init__.py @@ -16,7 +16,7 @@ from homeassistant.const import (EVENT_HOMEASSISTANT_STOP, CONF_ACCESS_TOKEN, from homeassistant.helpers import discovery from homeassistant.helpers.aiohttp_client import async_get_clientsession -REQUIREMENTS = ['pyTibber==0.9.0'] +REQUIREMENTS = ['pyTibber==0.9.1'] DOMAIN = 'tibber' diff --git a/requirements_all.txt b/requirements_all.txt index a1e5c2512bf..ae384c756cf 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -879,7 +879,7 @@ pyRFXtrx==0.23 pySwitchmate==0.4.4 # homeassistant.components.tibber -pyTibber==0.9.0 +pyTibber==0.9.1 # homeassistant.components.switch.dlink pyW215==0.6.0 From 6bf42ad43d3894d12461d45f17e16cacf83523a7 Mon Sep 17 00:00:00 2001 From: Matthew Wegner Date: Sat, 19 Jan 2019 13:37:02 -0700 Subject: [PATCH 021/222] Added Search Configuration to IMAP Sensor (#19749) * Added Search Configuration to IMAP Sensor The IMAP sensor currently only counts unread emails in a folder. By exposing the IMAP search parameter, the sensor can be used to count other results: - All emails in an inbox - Emails sent from an address - Emails matching a subject - Other advanced searches, especially with vendor-specific extensions. Gmail in particular supports X-GM-RAW, which lets you use any Gmail search directly ("emails with X label older than 14 days with", etc) For my use case, I just wanted total emails in a folder, to show an "X/Y" counter for total/unread. I started work on a one-off script to throw the data in, but figured I'd try to extend Home Assistant more directly, especially since this IMAP sensor correctly handles servers that push data. This is my first Home Assistant contribution, so apologies in advance if something is out of place! It's a pretty minimal modification. * Added Server Response Checking Looks like no library exception is thrown, so check for response text before parsing out results (previous code just counts spaces, so an error actually returns a state value of 4). * IMAP Warning -> Error, Count Initializes to None IMAP search response parsing throws an error instead of a warning. Email count initializes as None instead 0. Email count is untouched in case of failure to parse response (i.e. if server is temporarily down or throwing errors, or maybe due to user updating their authentication/login/etc). Fixed line length on error so it fits under 80 characters. * Fixed Indent on Logger Error Sorry about the churn! Python is pretty far from my daily-use language. (I did run this one through pep8, at least) --- homeassistant/components/sensor/imap.py | 32 ++++++++++++++++--------- 1 file changed, 21 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/sensor/imap.py b/homeassistant/components/sensor/imap.py index 2ea1fd576e6..b8d363417c2 100644 --- a/homeassistant/components/sensor/imap.py +++ b/homeassistant/components/sensor/imap.py @@ -24,6 +24,7 @@ REQUIREMENTS = ['aioimaplib==0.7.13'] CONF_SERVER = 'server' CONF_FOLDER = 'folder' +CONF_SEARCH = 'search' DEFAULT_PORT = 993 @@ -36,6 +37,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Required(CONF_SERVER): cv.string, vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, vol.Optional(CONF_FOLDER, default='INBOX'): cv.string, + vol.Optional(CONF_SEARCH, default='UnSeen UnDeleted'): cv.string, }) @@ -49,7 +51,8 @@ async def async_setup_platform(hass, config.get(CONF_PASSWORD), config.get(CONF_SERVER), config.get(CONF_PORT), - config.get(CONF_FOLDER)) + config.get(CONF_FOLDER), + config.get(CONF_SEARCH)) if not await sensor.connection(): raise PlatformNotReady @@ -60,7 +63,7 @@ async def async_setup_platform(hass, class ImapSensor(Entity): """Representation of an IMAP sensor.""" - def __init__(self, name, user, password, server, port, folder): + def __init__(self, name, user, password, server, port, folder, search): """Initialize the sensor.""" self._name = name or user self._user = user @@ -68,7 +71,8 @@ class ImapSensor(Entity): self._server = server self._port = port self._folder = folder - self._unread_count = 0 + self._email_count = None + self._search = search self._connection = None self._does_push = None self._idle_loop_task = None @@ -90,8 +94,8 @@ class ImapSensor(Entity): @property def state(self): - """Return the number of unread emails.""" - return self._unread_count + """Return the number of emails found.""" + return self._email_count @property def available(self): @@ -127,7 +131,7 @@ class ImapSensor(Entity): while True: try: if await self.connection(): - await self.refresh_unread_count() + await self.refresh_email_count() await self.async_update_ha_state() idle = await self._connection.idle_start() @@ -146,16 +150,22 @@ class ImapSensor(Entity): try: if await self.connection(): - await self.refresh_unread_count() + await self.refresh_email_count() except (aioimaplib.AioImapException, asyncio.TimeoutError): self.disconnected() - async def refresh_unread_count(self): - """Check the number of unread emails.""" + async def refresh_email_count(self): + """Check the number of found emails.""" if self._connection: await self._connection.noop() - _, lines = await self._connection.search('UnSeen UnDeleted') - self._unread_count = len(lines[0].split()) + result, lines = await self._connection.search(self._search) + + if result == 'OK': + self._email_count = len(lines[0].split()) + else: + _LOGGER.error("Can't parse IMAP server response to search " + "'%s': %s / %s", + self._search, result, lines[0]) def disconnected(self): """Forget the connection after it was lost.""" From a8d3a904e76e74aad98ae7aea0a7aac2e699f863 Mon Sep 17 00:00:00 2001 From: Andre Lengwenus Date: Sun, 20 Jan 2019 14:50:29 +0100 Subject: [PATCH 022/222] Support for relay ports for LCN light platform (#19632) * Added relay ports to LCN lights platform * Exchanged validation for ports with uppercase validator. Makes interfacing with pypck enums much more simple. * Removed supported_features property as it is correctly inherited from parent * Removed type annotations. --- homeassistant/components/lcn.py | 17 ++++--- homeassistant/components/light/lcn.py | 64 +++++++++++++++++++++++++-- 2 files changed, 71 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/lcn.py b/homeassistant/components/lcn.py index 597acb3bb02..f9ff9e64c3d 100644 --- a/homeassistant/components/lcn.py +++ b/homeassistant/components/lcn.py @@ -32,8 +32,10 @@ CONF_TRANSITION = 'transition' CONF_DIMMABLE = 'dimmable' CONF_CONNECTIONS = 'connections' -DIM_MODES = ['steps50', 'steps200'] -OUTPUT_PORTS = ['output1', 'output2', 'output3', 'output4'] +DIM_MODES = ['STEPS50', 'STEPS200'] +OUTPUT_PORTS = ['OUTPUT1', 'OUTPUT2', 'OUTPUT3', 'OUTPUT4'] +RELAY_PORTS = ['RELAY1', 'RELAY2', 'RELAY3', 'RELAY4', + 'RELAY5', 'RELAY6', 'RELAY7', 'RELAY8'] # Regex for address validation PATTERN_ADDRESS = re.compile('^((?P\\w+)\\.)?s?(?P\\d+)' @@ -85,7 +87,8 @@ def is_address(value): LIGHTS_SCHEMA = vol.Schema({ vol.Required(CONF_NAME): cv.string, vol.Required(CONF_ADDRESS): is_address, - vol.Required(CONF_OUTPUT): vol.All(vol.In(OUTPUT_PORTS), vol.Upper), + vol.Required(CONF_OUTPUT): vol.All(vol.Upper, + vol.In(OUTPUT_PORTS + RELAY_PORTS)), vol.Optional(CONF_DIMMABLE, default=False): vol.Coerce(bool), vol.Optional(CONF_TRANSITION, default=0): vol.All(vol.Coerce(float), vol.Range(min=0., max=486.), @@ -98,8 +101,8 @@ CONNECTION_SCHEMA = vol.Schema({ vol.Required(CONF_USERNAME): cv.string, vol.Required(CONF_PASSWORD): cv.string, vol.Optional(CONF_SK_NUM_TRIES, default=3): cv.positive_int, - vol.Optional(CONF_DIM_MODE, default='steps50'): vol.All(vol.In(DIM_MODES), - vol.Upper), + vol.Optional(CONF_DIM_MODE, default='steps50'): vol.All(vol.Upper, + vol.In(DIM_MODES)), vol.Optional(CONF_NAME): cv.string }) @@ -180,11 +183,11 @@ class LcnDevice(Entity): self._name = config[CONF_NAME] @property - def should_poll(self) -> bool: + def should_poll(self): """Lcn device entity pushes its state to HA.""" return False - async def async_added_to_hass(self) -> None: + async def async_added_to_hass(self): """Run when entity about to be added to hass.""" self.address_connection.register_for_inputs( self.input_received) diff --git a/homeassistant/components/light/lcn.py b/homeassistant/components/light/lcn.py index 3f00d305a14..f64eadaca5d 100644 --- a/homeassistant/components/light/lcn.py +++ b/homeassistant/components/light/lcn.py @@ -7,7 +7,7 @@ https://home-assistant.io/components/light.lcn/ from homeassistant.components.lcn import ( CONF_CONNECTIONS, CONF_DIMMABLE, CONF_OUTPUT, CONF_TRANSITION, DATA_LCN, - LcnDevice, get_connection) + OUTPUT_PORTS, LcnDevice, get_connection) from homeassistant.components.light import ( ATTR_BRIGHTNESS, ATTR_TRANSITION, SUPPORT_BRIGHTNESS, SUPPORT_TRANSITION, Light) @@ -29,7 +29,13 @@ async def async_setup_platform(hass, hass_config, async_add_entities, connection = get_connection(connections, connection_id) address_connection = connection.get_address_conn(addr) - devices.append(LcnOutputLight(config, address_connection)) + if config[CONF_OUTPUT] in OUTPUT_PORTS: + device = LcnOutputLight(config, address_connection) + else: # in RELAY_PORTS + device = LcnRelayLight(config, address_connection) + + devices.append(device) + async_add_entities(devices) @@ -50,7 +56,7 @@ class LcnOutputLight(LcnDevice, Light): self._is_on = None self._is_dimming_to_zero = False - async def async_added_to_hass(self) -> None: + async def async_added_to_hass(self): """Run when entity about to be added to hass.""" await super().async_added_to_hass() self.hass.async_create_task( @@ -119,3 +125,55 @@ class LcnOutputLight(LcnDevice, Light): if not self._is_dimming_to_zero: self._is_on = self.brightness > 0 self.async_schedule_update_ha_state() + + +class LcnRelayLight(LcnDevice, Light): + """Representation of a LCN light for relay ports.""" + + def __init__(self, config, address_connection): + """Initialize the LCN light.""" + super().__init__(config, address_connection) + + self.output = self.pypck.lcn_defs.RelayPort[config[CONF_OUTPUT]] + + self._is_on = None + + async def async_added_to_hass(self): + """Run when entity about to be added to hass.""" + await super().async_added_to_hass() + self.hass.async_create_task( + self.address_connection.activate_status_request_handler( + self.output)) + + @property + def is_on(self): + """Return True if entity is on.""" + return self._is_on + + async def async_turn_on(self, **kwargs): + """Turn the entity on.""" + self._is_on = True + + states = [self.pypck.lcn_defs.RelayStateModifier.NOCHANGE] * 8 + states[self.output.value] = self.pypck.lcn_defs.RelayStateModifier.ON + self.address_connection.control_relays(states) + + await self.async_update_ha_state() + + async def async_turn_off(self, **kwargs): + """Turn the entity off.""" + self._is_on = False + + states = [self.pypck.lcn_defs.RelayStateModifier.NOCHANGE] * 8 + states[self.output.value] = self.pypck.lcn_defs.RelayStateModifier.OFF + self.address_connection.control_relays(states) + + await self.async_update_ha_state() + + def input_received(self, input_obj): + """Set light state when LCN input object (command) is received.""" + if not isinstance(input_obj, self.pypck.inputs.ModStatusRelays): + return + + self._is_on = input_obj.get_state(self.output.value) + self.async_schedule_update_ha_state() From 7c25389f0d66c2338783467bebb3f0f84df8b91e Mon Sep 17 00:00:00 2001 From: Andre Lengwenus Date: Sun, 20 Jan 2019 20:49:30 +0100 Subject: [PATCH 023/222] Add LCN switch platform (#20267) * Add LCN switch platform * Added guard clause for discovery_info check and removed unnecessary parathesis --- homeassistant/components/lcn.py | 16 ++- homeassistant/components/switch/lcn.py | 135 +++++++++++++++++++++++++ 2 files changed, 149 insertions(+), 2 deletions(-) create mode 100755 homeassistant/components/switch/lcn.py diff --git a/homeassistant/components/lcn.py b/homeassistant/components/lcn.py index f9ff9e64c3d..8efdcc99794 100644 --- a/homeassistant/components/lcn.py +++ b/homeassistant/components/lcn.py @@ -12,7 +12,7 @@ import voluptuous as vol from homeassistant.const import ( CONF_ADDRESS, CONF_HOST, CONF_LIGHTS, CONF_NAME, CONF_PASSWORD, CONF_PORT, - CONF_USERNAME) + CONF_SWITCHES, CONF_USERNAME) import homeassistant.helpers.config_validation as cv from homeassistant.helpers.discovery import async_load_platform from homeassistant.helpers.entity import Entity @@ -95,6 +95,13 @@ LIGHTS_SCHEMA = vol.Schema({ lambda value: value * 1000), }) +SWITCHES_SCHEMA = vol.Schema({ + vol.Required(CONF_NAME): cv.string, + vol.Required(CONF_ADDRESS): is_address, + vol.Required(CONF_OUTPUT): vol.All(vol.Upper, + vol.In(OUTPUT_PORTS + RELAY_PORTS)) +}) + CONNECTION_SCHEMA = vol.Schema({ vol.Required(CONF_HOST): cv.string, vol.Required(CONF_PORT): cv.port, @@ -110,7 +117,8 @@ CONFIG_SCHEMA = vol.Schema({ DOMAIN: vol.Schema({ vol.Required(CONF_CONNECTIONS): vol.All( cv.ensure_list, has_unique_connection_names, [CONNECTION_SCHEMA]), - vol.Required(CONF_LIGHTS): vol.All(cv.ensure_list, [LIGHTS_SCHEMA]) + vol.Optional(CONF_LIGHTS): vol.All(cv.ensure_list, [LIGHTS_SCHEMA]), + vol.Optional(CONF_SWITCHES): vol.All(cv.ensure_list, [SWITCHES_SCHEMA]) }) }, extra=vol.ALLOW_EXTRA) @@ -168,6 +176,10 @@ async def async_setup(hass, config): async_load_platform(hass, 'light', DOMAIN, config[DOMAIN][CONF_LIGHTS], config)) + hass.async_create_task( + async_load_platform(hass, 'switch', DOMAIN, + config[DOMAIN][CONF_SWITCHES], config)) + return True diff --git a/homeassistant/components/switch/lcn.py b/homeassistant/components/switch/lcn.py new file mode 100755 index 00000000000..468afe178b5 --- /dev/null +++ b/homeassistant/components/switch/lcn.py @@ -0,0 +1,135 @@ +""" +Support for LCN switches. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/switch.lcn/ +""" + +from homeassistant.components.lcn import ( + CONF_CONNECTIONS, CONF_OUTPUT, DATA_LCN, OUTPUT_PORTS, LcnDevice, + get_connection) +from homeassistant.components.switch import SwitchDevice +from homeassistant.const import CONF_ADDRESS + +DEPENDENCIES = ['lcn'] + + +async def async_setup_platform(hass, hass_config, async_add_entities, + discovery_info=None): + """Set up the LCN switch platform.""" + if discovery_info is None: + return + + import pypck + + devices = [] + for config in discovery_info: + address, connection_id = config[CONF_ADDRESS] + addr = pypck.lcn_addr.LcnAddr(*address) + connections = hass.data[DATA_LCN][CONF_CONNECTIONS] + connection = get_connection(connections, connection_id) + address_connection = connection.get_address_conn(addr) + + if config[CONF_OUTPUT] in OUTPUT_PORTS: + device = LcnOutputSwitch(config, address_connection) + else: # in RELAY_PORTS + device = LcnRelaySwitch(config, address_connection) + + devices.append(device) + + async_add_entities(devices) + + +class LcnOutputSwitch(LcnDevice, SwitchDevice): + """Representation of a LCN switch for output ports.""" + + def __init__(self, config, address_connection): + """Initialize the LCN switch.""" + super().__init__(config, address_connection) + + self.output = self.pypck.lcn_defs.OutputPort[config[CONF_OUTPUT]] + + self._is_on = None + + async def async_added_to_hass(self): + """Run when entity about to be added to hass.""" + await super().async_added_to_hass() + self.hass.async_create_task( + self.address_connection.activate_status_request_handler( + self.output)) + + @property + def is_on(self): + """Return True if entity is on.""" + return self._is_on + + async def async_turn_on(self, **kwargs): + """Turn the entity on.""" + self._is_on = True + self.address_connection.dim_output(self.output.value, 100, 0) + await self.async_update_ha_state() + + async def async_turn_off(self, **kwargs): + """Turn the entity off.""" + self._is_on = False + self.address_connection.dim_output(self.output.value, 0, 0) + await self.async_update_ha_state() + + def input_received(self, input_obj): + """Set switch state when LCN input object (command) is received.""" + if not isinstance(input_obj, self.pypck.inputs.ModStatusOutput) or \ + input_obj.get_output_id() != self.output.value: + return + + self._is_on = input_obj.get_percent() > 0 + self.async_schedule_update_ha_state() + + +class LcnRelaySwitch(LcnDevice, SwitchDevice): + """Representation of a LCN switch for relay ports.""" + + def __init__(self, config, address_connection): + """Initialize the LCN switch.""" + super().__init__(config, address_connection) + + self.output = self.pypck.lcn_defs.RelayPort[config[CONF_OUTPUT]] + + self._is_on = None + + async def async_added_to_hass(self): + """Run when entity about to be added to hass.""" + await super().async_added_to_hass() + self.hass.async_create_task( + self.address_connection.activate_status_request_handler( + self.output)) + + @property + def is_on(self): + """Return True if entity is on.""" + return self._is_on + + async def async_turn_on(self, **kwargs): + """Turn the entity on.""" + self._is_on = True + + states = [self.pypck.lcn_defs.RelayStateModifier.NOCHANGE] * 8 + states[self.output.value] = self.pypck.lcn_defs.RelayStateModifier.ON + self.address_connection.control_relays(states) + await self.async_update_ha_state() + + async def async_turn_off(self, **kwargs): + """Turn the entity off.""" + self._is_on = False + + states = [self.pypck.lcn_defs.RelayStateModifier.NOCHANGE] * 8 + states[self.output.value] = self.pypck.lcn_defs.RelayStateModifier.OFF + self.address_connection.control_relays(states) + await self.async_update_ha_state() + + def input_received(self, input_obj): + """Set switch state when LCN input object (command) is received.""" + if not isinstance(input_obj, self.pypck.inputs.ModStatusRelays): + return + + self._is_on = input_obj.get_state(self.output.value) + self.async_schedule_update_ha_state() From 0e5fa010a747eaa8d4204459adaf6cae05df58a7 Mon Sep 17 00:00:00 2001 From: kbickar Date: Sun, 20 Jan 2019 15:02:36 -0500 Subject: [PATCH 024/222] Updated sense library to 0.6.0 (#20271) --- homeassistant/components/sense.py | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/sense.py b/homeassistant/components/sense.py index 8ddeb3d2ecc..2fac2820230 100644 --- a/homeassistant/components/sense.py +++ b/homeassistant/components/sense.py @@ -12,7 +12,7 @@ from homeassistant.const import CONF_EMAIL, CONF_PASSWORD, CONF_TIMEOUT import homeassistant.helpers.config_validation as cv from homeassistant.helpers.discovery import load_platform -REQUIREMENTS = ['sense_energy==0.5.1'] +REQUIREMENTS = ['sense_energy==0.6.0'] _LOGGER = logging.getLogger(__name__) diff --git a/requirements_all.txt b/requirements_all.txt index ae384c756cf..7a128cf14fe 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1478,7 +1478,7 @@ sendgrid==5.6.0 sense-hat==2.2.0 # homeassistant.components.sense -sense_energy==0.5.1 +sense_energy==0.6.0 # homeassistant.components.media_player.aquostv sharp_aquos_rc==0.3.2 From fd2cff6b1c384f9f16ba5473bd6a707cf97153f1 Mon Sep 17 00:00:00 2001 From: koreth Date: Sun, 20 Jan 2019 12:16:48 -0800 Subject: [PATCH 025/222] Fire events for Lutron RadioRA2 keypad buttons (#20090) * Add binary sensor for Lutron RadioRA2 keypad buttons Allow automations to be triggered from RadioRA2 keypads by exposing each button as a binary sensor. * Remove binary sensor component; fire events directly instead. * Address comments from code review --- homeassistant/components/lutron.py | 52 +++++++++++++++++++++++++++++- 1 file changed, 51 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/lutron.py b/homeassistant/components/lutron.py index 7f1e1d25ae1..435039ce4bd 100644 --- a/homeassistant/components/lutron.py +++ b/homeassistant/components/lutron.py @@ -9,9 +9,11 @@ import logging import voluptuous as vol import homeassistant.helpers.config_validation as cv -from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME +from homeassistant.const import ( + ATTR_ID, CONF_HOST, CONF_PASSWORD, CONF_USERNAME) from homeassistant.helpers import discovery from homeassistant.helpers.entity import Entity +from homeassistant.util import slugify REQUIREMENTS = ['pylutron==0.2.0'] @@ -19,9 +21,13 @@ DOMAIN = 'lutron' _LOGGER = logging.getLogger(__name__) +LUTRON_BUTTONS = 'lutron_buttons' LUTRON_CONTROLLER = 'lutron_controller' LUTRON_DEVICES = 'lutron_devices' +# Attribute on events that indicates what action was taken with the button. +ATTR_ACTION = 'action' + CONFIG_SCHEMA = vol.Schema({ DOMAIN: vol.Schema({ vol.Required(CONF_HOST): cv.string, @@ -35,6 +41,7 @@ def setup(hass, base_config): """Set up the Lutron component.""" from pylutron import Lutron + hass.data[LUTRON_BUTTONS] = [] hass.data[LUTRON_CONTROLLER] = None hass.data[LUTRON_DEVICES] = {'light': [], 'cover': [], @@ -70,6 +77,9 @@ def setup(hass, base_config): hass.data[LUTRON_DEVICES]['scene'].append( (area.name, keypad.name, button, led)) + hass.data[LUTRON_BUTTONS].append( + LutronButton(hass, keypad, button)) + for component in ('light', 'cover', 'switch', 'scene'): discovery.load_platform(hass, component, DOMAIN, None, base_config) return True @@ -105,3 +115,43 @@ class LutronDevice(Entity): def should_poll(self): """No polling needed.""" return False + + +class LutronButton: + """Representation of a button on a Lutron keypad. + + This is responsible for firing events as keypad buttons are pressed + (and possibly released, depending on the button type). It is not + represented as an entity; it simply fires events. + """ + + def __init__(self, hass, keypad, button): + """Register callback for activity on the button.""" + name = '{}: {}'.format(keypad.name, button.name) + self._hass = hass + self._has_release_event = 'RaiseLower' in button.button_type + self._id = slugify(name) + self._event = 'lutron_event' + + button.subscribe(self.button_callback, None) + + def button_callback(self, button, context, event, params): + """Fire an event about a button being pressed or released.""" + from pylutron import Button + + if self._has_release_event: + # A raise/lower button; we will get callbacks when the button is + # pressed and when it's released, so fire events for each. + if event == Button.Event.PRESSED: + action = 'pressed' + else: + action = 'released' + else: + # A single-action button; the Lutron controller won't tell us + # when the button is released, so use a different action name + # than for buttons where we expect a release event. + action = 'single' + + data = {ATTR_ID: self._id, ATTR_ACTION: action} + + self._hass.bus.fire(self._event, data) From 2eb5ce9dfe8fe7e6317efc2dfef917b12115bbb5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20H=C3=B8yer=20Iversen?= Date: Sun, 20 Jan 2019 21:37:02 +0100 Subject: [PATCH 026/222] Update Tibber library (#20273) --- homeassistant/components/tibber/__init__.py | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/tibber/__init__.py b/homeassistant/components/tibber/__init__.py index 8492596332a..850e40d5eeb 100644 --- a/homeassistant/components/tibber/__init__.py +++ b/homeassistant/components/tibber/__init__.py @@ -16,7 +16,7 @@ from homeassistant.const import (EVENT_HOMEASSISTANT_STOP, CONF_ACCESS_TOKEN, from homeassistant.helpers import discovery from homeassistant.helpers.aiohttp_client import async_get_clientsession -REQUIREMENTS = ['pyTibber==0.9.1'] +REQUIREMENTS = ['pyTibber==0.9.2'] DOMAIN = 'tibber' diff --git a/requirements_all.txt b/requirements_all.txt index 7a128cf14fe..d1009a2efd6 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -879,7 +879,7 @@ pyRFXtrx==0.23 pySwitchmate==0.4.4 # homeassistant.components.tibber -pyTibber==0.9.1 +pyTibber==0.9.2 # homeassistant.components.switch.dlink pyW215==0.6.0 From 5b8cb10ad71de7de8001642e8495d1c75b2f4546 Mon Sep 17 00:00:00 2001 From: NotoriousBDG Date: Sun, 20 Jan 2019 16:30:17 -0500 Subject: [PATCH 027/222] Make Netatmo battery_percent icon dynamic (#20275) --- homeassistant/components/sensor/netatmo.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/sensor/netatmo.py b/homeassistant/components/sensor/netatmo.py index cd4552d91a4..e871bfa5e1b 100644 --- a/homeassistant/components/sensor/netatmo.py +++ b/homeassistant/components/sensor/netatmo.py @@ -13,7 +13,7 @@ import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.const import ( TEMP_CELSIUS, DEVICE_CLASS_HUMIDITY, DEVICE_CLASS_TEMPERATURE, - STATE_UNKNOWN) + DEVICE_CLASS_BATTERY, STATE_UNKNOWN) from homeassistant.helpers.entity import Entity import homeassistant.helpers.config_validation as cv @@ -39,7 +39,7 @@ SENSOR_TYPES = { 'sum_rain_24': ['sum_rain_24', 'mm', 'mdi:weather-rainy', None], 'battery_vp': ['Battery', '', 'mdi:battery', None], 'battery_lvl': ['Battery_lvl', '', 'mdi:battery', None], - 'battery_percent': ['battery_percent', '%', 'mdi:battery', None], + 'battery_percent': ['battery_percent', '%', None, DEVICE_CLASS_BATTERY], 'min_temp': ['Min Temp.', TEMP_CELSIUS, 'mdi:thermometer', None], 'max_temp': ['Max Temp.', TEMP_CELSIUS, 'mdi:thermometer', None], 'windangle': ['Angle', '', 'mdi:compass', None], From 58bb6f2e99b7a2dc4e3f5b5c8775b2e759a49fe3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ville=20Skytt=C3=A4?= Date: Mon, 21 Jan 2019 01:03:12 +0200 Subject: [PATCH 028/222] Add type hints to helpers.condition (#20266) --- homeassistant/components/zone/zone.py | 2 +- homeassistant/core.py | 2 +- homeassistant/helpers/condition.py | 141 +++++++++++++++++--------- homeassistant/helpers/template.py | 6 +- homeassistant/helpers/typing.py | 3 +- tox.ini | 2 +- 6 files changed, 101 insertions(+), 55 deletions(-) diff --git a/homeassistant/components/zone/zone.py b/homeassistant/components/zone/zone.py index b7c2e9ee858..ee8b53d6ee4 100644 --- a/homeassistant/components/zone/zone.py +++ b/homeassistant/components/zone/zone.py @@ -56,7 +56,7 @@ def async_active_zone(hass, latitude, longitude, radius=0): return closest -def in_zone(zone, latitude, longitude, radius=0): +def in_zone(zone, latitude, longitude, radius=0) -> bool: """Test if given latitude, longitude is in given zone. Async friendly. diff --git a/homeassistant/core.py b/homeassistant/core.py index 2834730408e..75f3fe7dae6 100644 --- a/homeassistant/core.py +++ b/homeassistant/core.py @@ -678,7 +678,7 @@ class State: "State max length is 255 characters.").format(entity_id)) self.entity_id = entity_id.lower() - self.state = state + self.state = state # type: str self.attributes = MappingProxyType(attributes or {}) self.last_updated = last_updated or dt_util.utcnow() self.last_changed = last_changed or self.last_updated diff --git a/homeassistant/helpers/condition.py b/homeassistant/helpers/condition.py index 86112e2aea2..4b71b770973 100644 --- a/homeassistant/helpers/condition.py +++ b/homeassistant/helpers/condition.py @@ -1,12 +1,14 @@ """Offer reusable conditions.""" -from datetime import timedelta +from datetime import datetime, timedelta import functools as ft import logging import sys +from typing import Callable, Container, Optional, Union, cast -from homeassistant.helpers.typing import ConfigType +from homeassistant.helpers.template import Template +from homeassistant.helpers.typing import ConfigType, TemplateVarsType -from homeassistant.core import HomeAssistant +from homeassistant.core import HomeAssistant, State from homeassistant.components import zone as zone_cmp from homeassistant.const import ( ATTR_GPS_ACCURACY, ATTR_LATITUDE, ATTR_LONGITUDE, @@ -29,25 +31,30 @@ _LOGGER = logging.getLogger(__name__) # pylint: disable=invalid-name -def _threaded_factory(async_factory): +def _threaded_factory(async_factory: + Callable[[ConfigType, bool], Callable[..., bool]]) \ + -> Callable[[ConfigType, bool], Callable[..., bool]]: """Create threaded versions of async factories.""" @ft.wraps(async_factory) - def factory(config, config_validation=True): + def factory(config: ConfigType, + config_validation: bool = True) -> Callable[..., bool]: """Threaded factory.""" async_check = async_factory(config, config_validation) - def condition_if(hass, variables=None): + def condition_if(hass: HomeAssistant, + variables: TemplateVarsType = None) -> bool: """Validate condition.""" - return run_callback_threadsafe( + return cast(bool, run_callback_threadsafe( hass.loop, async_check, hass, variables, - ).result() + ).result()) return condition_if return factory -def async_from_config(config: ConfigType, config_validation: bool = True): +def async_from_config(config: ConfigType, + config_validation: bool = True) -> Callable[..., bool]: """Turn a condition configuration into a method. Should be run on the event loop. @@ -64,20 +71,22 @@ def async_from_config(config: ConfigType, config_validation: bool = True): raise HomeAssistantError('Invalid condition "{}" specified {}'.format( config.get(CONF_CONDITION), config)) - return factory(config, config_validation) + return cast(Callable[..., bool], factory(config, config_validation)) from_config = _threaded_factory(async_from_config) -def async_and_from_config(config: ConfigType, config_validation: bool = True): +def async_and_from_config(config: ConfigType, + config_validation: bool = True) \ + -> Callable[..., bool]: """Create multi condition matcher using 'AND'.""" if config_validation: config = cv.AND_CONDITION_SCHEMA(config) checks = None def if_and_condition(hass: HomeAssistant, - variables=None) -> bool: + variables: TemplateVarsType = None) -> bool: """Test and condition.""" nonlocal checks @@ -101,14 +110,16 @@ def async_and_from_config(config: ConfigType, config_validation: bool = True): and_from_config = _threaded_factory(async_and_from_config) -def async_or_from_config(config: ConfigType, config_validation: bool = True): +def async_or_from_config(config: ConfigType, + config_validation: bool = True) \ + -> Callable[..., bool]: """Create multi condition matcher using 'OR'.""" if config_validation: config = cv.OR_CONDITION_SCHEMA(config) checks = None def if_or_condition(hass: HomeAssistant, - variables=None) -> bool: + variables: TemplateVarsType = None) -> bool: """Test and condition.""" nonlocal checks @@ -131,17 +142,22 @@ def async_or_from_config(config: ConfigType, config_validation: bool = True): or_from_config = _threaded_factory(async_or_from_config) -def numeric_state(hass: HomeAssistant, entity, below=None, above=None, - value_template=None, variables=None): +def numeric_state(hass: HomeAssistant, entity: Union[None, str, State], + below: Optional[float] = None, above: Optional[float] = None, + value_template: Optional[Template] = None, + variables: TemplateVarsType = None) -> bool: """Test a numeric state condition.""" - return run_callback_threadsafe( + return cast(bool, run_callback_threadsafe( hass.loop, async_numeric_state, hass, entity, below, above, value_template, variables, - ).result() + ).result()) -def async_numeric_state(hass: HomeAssistant, entity, below=None, above=None, - value_template=None, variables=None): +def async_numeric_state(hass: HomeAssistant, entity: Union[None, str, State], + below: Optional[float] = None, + above: Optional[float] = None, + value_template: Optional[Template] = None, + variables: TemplateVarsType = None) -> bool: """Test a numeric state condition.""" if isinstance(entity, str): entity = hass.states.get(entity) @@ -164,22 +180,24 @@ def async_numeric_state(hass: HomeAssistant, entity, below=None, above=None, return False try: - value = float(value) + fvalue = float(value) except ValueError: _LOGGER.warning("Value cannot be processed as a number: %s " "(Offending entity: %s)", entity, value) return False - if below is not None and value >= below: + if below is not None and fvalue >= below: return False - if above is not None and value <= above: + if above is not None and fvalue <= above: return False return True -def async_numeric_state_from_config(config, config_validation=True): +def async_numeric_state_from_config(config: ConfigType, + config_validation: bool = True) \ + -> Callable[..., bool]: """Wrap action method with state based condition.""" if config_validation: config = cv.NUMERIC_STATE_CONDITION_SCHEMA(config) @@ -188,7 +206,8 @@ def async_numeric_state_from_config(config, config_validation=True): above = config.get(CONF_ABOVE) value_template = config.get(CONF_VALUE_TEMPLATE) - def if_numeric_state(hass, variables=None): + def if_numeric_state(hass: HomeAssistant, + variables: TemplateVarsType = None) -> bool: """Test numeric state condition.""" if value_template is not None: value_template.hass = hass @@ -202,7 +221,8 @@ def async_numeric_state_from_config(config, config_validation=True): numeric_state_from_config = _threaded_factory(async_numeric_state_from_config) -def state(hass, entity, req_state, for_period=None): +def state(hass: HomeAssistant, entity: Union[None, str, State], req_state: str, + for_period: Optional[timedelta] = None) -> bool: """Test if state matches requirements. Async friendly. @@ -212,6 +232,7 @@ def state(hass, entity, req_state, for_period=None): if entity is None: return False + assert isinstance(entity, State) is_state = entity.state == req_state @@ -221,22 +242,26 @@ def state(hass, entity, req_state, for_period=None): return dt_util.utcnow() - for_period > entity.last_changed -def state_from_config(config, config_validation=True): +def state_from_config(config: ConfigType, + config_validation: bool = True) -> Callable[..., bool]: """Wrap action method with state based condition.""" if config_validation: config = cv.STATE_CONDITION_SCHEMA(config) entity_id = config.get(CONF_ENTITY_ID) - req_state = config.get(CONF_STATE) + req_state = cast(str, config.get(CONF_STATE)) for_period = config.get('for') - def if_state(hass, variables=None): + def if_state(hass: HomeAssistant, + variables: TemplateVarsType = None) -> bool: """Test if condition.""" return state(hass, entity_id, req_state, for_period) return if_state -def sun(hass, before=None, after=None, before_offset=None, after_offset=None): +def sun(hass: HomeAssistant, before: Optional[str] = None, + after: Optional[str] = None, before_offset: Optional[timedelta] = None, + after_offset: Optional[timedelta] = None) -> bool: """Test if current time matches sun requirements.""" utcnow = dt_util.utcnow() today = dt_util.as_local(utcnow).date() @@ -254,22 +279,27 @@ def sun(hass, before=None, after=None, before_offset=None, after_offset=None): # There is no sunset today return False - if before == SUN_EVENT_SUNRISE and utcnow > sunrise + before_offset: + if before == SUN_EVENT_SUNRISE and \ + utcnow > cast(datetime, sunrise) + before_offset: return False - if before == SUN_EVENT_SUNSET and utcnow > sunset + before_offset: + if before == SUN_EVENT_SUNSET and \ + utcnow > cast(datetime, sunset) + before_offset: return False - if after == SUN_EVENT_SUNRISE and utcnow < sunrise + after_offset: + if after == SUN_EVENT_SUNRISE and \ + utcnow < cast(datetime, sunrise) + after_offset: return False - if after == SUN_EVENT_SUNSET and utcnow < sunset + after_offset: + if after == SUN_EVENT_SUNSET and \ + utcnow < cast(datetime, sunset) + after_offset: return False return True -def sun_from_config(config, config_validation=True): +def sun_from_config(config: ConfigType, + config_validation: bool = True) -> Callable[..., bool]: """Wrap action method with sun based condition.""" if config_validation: config = cv.SUN_CONDITION_SCHEMA(config) @@ -278,21 +308,24 @@ def sun_from_config(config, config_validation=True): before_offset = config.get('before_offset') after_offset = config.get('after_offset') - def time_if(hass, variables=None): + def time_if(hass: HomeAssistant, + variables: TemplateVarsType = None) -> bool: """Validate time based if-condition.""" return sun(hass, before, after, before_offset, after_offset) return time_if -def template(hass, value_template, variables=None): +def template(hass: HomeAssistant, value_template: Template, + variables: TemplateVarsType = None) -> bool: """Test if template condition matches.""" - return run_callback_threadsafe( + return cast(bool, run_callback_threadsafe( hass.loop, async_template, hass, value_template, variables, - ).result() + ).result()) -def async_template(hass, value_template, variables=None): +def async_template(hass: HomeAssistant, value_template: Template, + variables: TemplateVarsType = None) -> bool: """Test if template condition matches.""" try: value = value_template.async_render(variables) @@ -303,13 +336,16 @@ def async_template(hass, value_template, variables=None): return value.lower() == 'true' -def async_template_from_config(config, config_validation=True): +def async_template_from_config(config: ConfigType, + config_validation: bool = True) \ + -> Callable[..., bool]: """Wrap action method with state based condition.""" if config_validation: config = cv.TEMPLATE_CONDITION_SCHEMA(config) - value_template = config.get(CONF_VALUE_TEMPLATE) + value_template = cast(Template, config.get(CONF_VALUE_TEMPLATE)) - def template_if(hass, variables=None): + def template_if(hass: HomeAssistant, + variables: TemplateVarsType = None) -> bool: """Validate template based if-condition.""" value_template.hass = hass @@ -321,7 +357,9 @@ def async_template_from_config(config, config_validation=True): template_from_config = _threaded_factory(async_template_from_config) -def time(before=None, after=None, weekday=None): +def time(before: Optional[dt_util.dt.time] = None, + after: Optional[dt_util.dt.time] = None, + weekday: Union[None, str, Container[str]] = None) -> bool: """Test if local time condition matches. Handle the fact that time is continuous and we may be testing for @@ -354,7 +392,8 @@ def time(before=None, after=None, weekday=None): return True -def time_from_config(config, config_validation=True): +def time_from_config(config: ConfigType, + config_validation: bool = True) -> Callable[..., bool]: """Wrap action method with time based condition.""" if config_validation: config = cv.TIME_CONDITION_SCHEMA(config) @@ -362,14 +401,16 @@ def time_from_config(config, config_validation=True): after = config.get(CONF_AFTER) weekday = config.get(CONF_WEEKDAY) - def time_if(hass, variables=None): + def time_if(hass: HomeAssistant, + variables: TemplateVarsType = None) -> bool: """Validate time based if-condition.""" return time(before, after, weekday) return time_if -def zone(hass, zone_ent, entity): +def zone(hass: HomeAssistant, zone_ent: Union[None, str, State], + entity: Union[None, str, State]) -> bool: """Test if zone-condition matches. Async friendly. @@ -396,14 +437,16 @@ def zone(hass, zone_ent, entity): entity.attributes.get(ATTR_GPS_ACCURACY, 0)) -def zone_from_config(config, config_validation=True): +def zone_from_config(config: ConfigType, + config_validation: bool = True) -> Callable[..., bool]: """Wrap action method with zone based condition.""" if config_validation: config = cv.ZONE_CONDITION_SCHEMA(config) entity_id = config.get(CONF_ENTITY_ID) zone_entity_id = config.get(CONF_ZONE) - def if_in_zone(hass, variables=None): + def if_in_zone(hass: HomeAssistant, + variables: TemplateVarsType = None) -> bool: """Test if condition.""" return zone(hass, zone_entity_id, entity_id) diff --git a/homeassistant/helpers/template.py b/homeassistant/helpers/template.py index e82302dfd3b..267bf8853d9 100644 --- a/homeassistant/helpers/template.py +++ b/homeassistant/helpers/template.py @@ -18,6 +18,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.helpers.typing import TemplateVarsType from homeassistant.loader import bind_hass from homeassistant.util import convert from homeassistant.util import dt as dt_util @@ -115,7 +116,7 @@ class Template: """Extract all entities for state_changed listener.""" return extract_entities(self.template, variables) - def render(self, variables=None, **kwargs): + def render(self, variables: TemplateVarsType = None, **kwargs): """Render given template.""" if variables is not None: kwargs.update(variables) @@ -123,7 +124,8 @@ class Template: return run_callback_threadsafe( self.hass.loop, self.async_render, kwargs).result() - def async_render(self, variables=None, **kwargs): + def async_render(self, variables: TemplateVarsType = None, + **kwargs) -> str: """Render given template. This method must be run in the event loop. diff --git a/homeassistant/helpers/typing.py b/homeassistant/helpers/typing.py index 3919d896fd1..91b49283be8 100644 --- a/homeassistant/helpers/typing.py +++ b/homeassistant/helpers/typing.py @@ -1,5 +1,5 @@ """Typing Helpers for Home Assistant.""" -from typing import Dict, Any, Tuple +from typing import Dict, Any, Tuple, Optional import homeassistant.core @@ -9,6 +9,7 @@ GPSType = Tuple[float, float] ConfigType = Dict[str, Any] HomeAssistantType = homeassistant.core.HomeAssistant ServiceDataType = Dict[str, Any] +TemplateVarsType = Optional[Dict[str, Any]] # Custom type for recorder Queries QueryType = Any diff --git a/tox.ini b/tox.ini index 1ab771ff24b..d240149cff8 100644 --- a/tox.ini +++ b/tox.ini @@ -60,4 +60,4 @@ whitelist_externals=/bin/bash deps = -r{toxinidir}/requirements_test.txt commands = - /bin/bash -c 'mypy homeassistant/*.py homeassistant/{auth,util}/ homeassistant/helpers/{__init__,deprecation,dispatcher,entity_values,entityfilter,icon,intent,json,location,signal,state,sun,temperature,translation,typing}.py' + /bin/bash -c 'mypy homeassistant/*.py homeassistant/{auth,util}/ homeassistant/helpers/{__init__,condition,deprecation,dispatcher,entity_values,entityfilter,icon,intent,json,location,signal,state,sun,temperature,translation,typing}.py' From 362ac725bf404d3b67aadc5c3e45addd088d2ce2 Mon Sep 17 00:00:00 2001 From: Anders Melchiorsen Date: Mon, 21 Jan 2019 00:10:12 +0100 Subject: [PATCH 029/222] Remove double logging of automation action (#20264) --- homeassistant/components/automation/__init__.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/homeassistant/components/automation/__init__.py b/homeassistant/components/automation/__init__.py index 6c9b04f9fa2..836901cde30 100644 --- a/homeassistant/components/automation/__init__.py +++ b/homeassistant/components/automation/__init__.py @@ -375,8 +375,6 @@ def _async_get_action(hass, config, name): async def action(entity_id, variables, context): """Execute an action.""" _LOGGER.info('Executing %s', name) - hass.components.logbook.async_log_entry( - name, 'has been triggered', DOMAIN, entity_id) try: await script_obj.async_run(variables, context) From d5dcb8f140c081dacaff7cf72fd3d246d7dd07dc Mon Sep 17 00:00:00 2001 From: Andre Lengwenus Date: Mon, 21 Jan 2019 00:49:28 +0100 Subject: [PATCH 030/222] Add discovery_info check to LCN light platform (#20280) * Added discovery_info check to LCN light platform * Removed whitespaces --- homeassistant/components/light/lcn.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/homeassistant/components/light/lcn.py b/homeassistant/components/light/lcn.py index f64eadaca5d..b9457b7b7d9 100644 --- a/homeassistant/components/light/lcn.py +++ b/homeassistant/components/light/lcn.py @@ -19,6 +19,9 @@ DEPENDENCIES = ['lcn'] async def async_setup_platform(hass, hass_config, async_add_entities, discovery_info=None): """Set up the LCN light platform.""" + if discovery_info is None: + return + import pypck devices = [] From f771667c14a113bd6774a61a51915354ecde2431 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sun, 20 Jan 2019 16:22:42 -0800 Subject: [PATCH 031/222] Updated frontend to 20190120.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 a60168ac114..4d18b2fa841 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==20190116.0'] +REQUIREMENTS = ['home-assistant-frontend==20190120.0'] DOMAIN = 'frontend' DEPENDENCIES = ['api', 'websocket_api', 'http', 'system_log', diff --git a/requirements_all.txt b/requirements_all.txt index d1009a2efd6..0e5ccd36625 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -514,7 +514,7 @@ hole==0.3.0 holidays==0.9.9 # homeassistant.components.frontend -home-assistant-frontend==20190116.0 +home-assistant-frontend==20190120.0 # homeassistant.components.zwave homeassistant-pyozw==0.1.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 039772db66c..5733d7e9630 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -107,7 +107,7 @@ hdate==0.8.7 holidays==0.9.9 # homeassistant.components.frontend -home-assistant-frontend==20190116.0 +home-assistant-frontend==20190120.0 # homeassistant.components.homematicip_cloud homematicip==0.10.3 From 08a57959b9d10ed3ac365d98c79d330dfa3e15d1 Mon Sep 17 00:00:00 2001 From: koreth Date: Sun, 20 Jan 2019 16:32:01 -0800 Subject: [PATCH 032/222] Reduce log noise from Envisalink component (#20282) --- homeassistant/components/envisalink/__init__.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/envisalink/__init__.py b/homeassistant/components/envisalink/__init__.py index c6b694c7f5f..8b89b307db9 100644 --- a/homeassistant/components/envisalink/__init__.py +++ b/homeassistant/components/envisalink/__init__.py @@ -146,19 +146,19 @@ async def async_setup(hass, config): @callback def zones_updated_callback(data): """Handle zone timer updates.""" - _LOGGER.info("Envisalink sent a zone update event. Updating zones...") + _LOGGER.debug("Envisalink sent a zone update event. Updating zones...") async_dispatcher_send(hass, SIGNAL_ZONE_UPDATE, data) @callback def alarm_data_updated_callback(data): """Handle non-alarm based info updates.""" - _LOGGER.info("Envisalink sent new alarm info. Updating alarms...") + _LOGGER.debug("Envisalink sent new alarm info. Updating alarms...") async_dispatcher_send(hass, SIGNAL_KEYPAD_UPDATE, data) @callback def partition_updated_callback(data): """Handle partition changes thrown by evl (including alarms).""" - _LOGGER.info("The envisalink sent a partition update event") + _LOGGER.debug("The envisalink sent a partition update event") async_dispatcher_send(hass, SIGNAL_PARTITION_UPDATE, data) @callback From 1218127d8372d3792abbd6546f375bed12221463 Mon Sep 17 00:00:00 2001 From: Anders Melchiorsen Date: Mon, 21 Jan 2019 01:33:11 +0100 Subject: [PATCH 033/222] Fix 'all' entity_id in service call extraction (#20281) --- homeassistant/helpers/entity_component.py | 5 +++-- tests/helpers/test_entity_component.py | 2 +- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/homeassistant/helpers/entity_component.py b/homeassistant/helpers/entity_component.py index ce876991097..21634121cd2 100644 --- a/homeassistant/helpers/entity_component.py +++ b/homeassistant/helpers/entity_component.py @@ -7,7 +7,8 @@ import logging from homeassistant import config as conf_util from homeassistant.setup import async_prepare_setup_platform from homeassistant.const import ( - ATTR_ENTITY_ID, CONF_SCAN_INTERVAL, CONF_ENTITY_NAMESPACE, MATCH_ALL) + ATTR_ENTITY_ID, CONF_SCAN_INTERVAL, CONF_ENTITY_NAMESPACE, + ENTITY_MATCH_ALL) from homeassistant.core import callback from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import config_per_platform, discovery @@ -163,7 +164,7 @@ class EntityComponent: """ data_ent_id = service.data.get(ATTR_ENTITY_ID) - if data_ent_id in (None, MATCH_ALL): + if data_ent_id in (None, ENTITY_MATCH_ALL): if data_ent_id is None: self.logger.warning( 'Not passing an entity ID to a service to target all ' diff --git a/tests/helpers/test_entity_component.py b/tests/helpers/test_entity_component.py index 8f54f0ee5bc..27e33a4fe7d 100644 --- a/tests/helpers/test_entity_component.py +++ b/tests/helpers/test_entity_component.py @@ -479,7 +479,7 @@ async def test_extract_all_use_match_all(hass, caplog): MockEntity(name='test_2'), ]) - call = ha.ServiceCall('test', 'service', {'entity_id': '*'}) + call = ha.ServiceCall('test', 'service', {'entity_id': 'all'}) assert ['test_domain.test_1', 'test_domain.test_2'] == \ sorted(ent.entity_id for ent in From 62844e237c0f131f5b225b59de0d80b0514b270e Mon Sep 17 00:00:00 2001 From: Anders Melchiorsen Date: Mon, 21 Jan 2019 01:33:39 +0100 Subject: [PATCH 034/222] Allow 'all' entity_id in service schema (#20278) --- homeassistant/helpers/config_validation.py | 2 +- tests/helpers/test_config_validation.py | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/homeassistant/helpers/config_validation.py b/homeassistant/helpers/config_validation.py index 245cc5d46bd..92fe935085a 100644 --- a/homeassistant/helpers/config_validation.py +++ b/homeassistant/helpers/config_validation.py @@ -517,7 +517,7 @@ SERVICE_SCHEMA = vol.All(vol.Schema({ vol.Exclusive('service_template', 'service name'): template, vol.Optional('data'): dict, vol.Optional('data_template'): {match_all: template_complex}, - vol.Optional(CONF_ENTITY_ID): entity_ids, + vol.Optional(CONF_ENTITY_ID): comp_entity_ids, }), has_at_least_one_key('service', 'service_template')) NUMERIC_STATE_CONDITION_SCHEMA = vol.All(vol.Schema({ diff --git a/tests/helpers/test_config_validation.py b/tests/helpers/test_config_validation.py index 791570981e2..03dd3cfe55a 100644 --- a/tests/helpers/test_config_validation.py +++ b/tests/helpers/test_config_validation.py @@ -332,6 +332,10 @@ def test_service_schema(): 'service': 'homeassistant.turn_on', 'entity_id': 'light.kitchen', }, + { + 'service': 'light.turn_on', + 'entity_id': 'all', + }, { 'service': 'homeassistant.turn_on', 'entity_id': ['light.kitchen', 'light.ceiling'], From e69ca810e4e8e212e0544e2fc9a86563daac7d6b Mon Sep 17 00:00:00 2001 From: Teemu R Date: Mon, 21 Jan 2019 01:36:01 +0100 Subject: [PATCH 035/222] Print a message when reconnected after a connection failure, requirement for IQS silver (#20261) --- homeassistant/components/media_player/songpal.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/homeassistant/components/media_player/songpal.py b/homeassistant/components/media_player/songpal.py index 165ef668a95..59293fd3e8c 100644 --- a/homeassistant/components/media_player/songpal.py +++ b/homeassistant/components/media_player/songpal.py @@ -180,6 +180,8 @@ class SongpalDevice(MediaPlayerDevice): await self.async_update_ha_state(force_refresh=True) delay = min(2*delay, 300) + _LOGGER.info("Reconnected to %s", self.name) + self.dev.on_notification(VolumeChange, _volume_changed) self.dev.on_notification(ContentChange, _source_changed) self.dev.on_notification(PowerChange, _power_changed) From ab4e4787e30cfd11daadcdb3f01eb99d3dbef870 Mon Sep 17 00:00:00 2001 From: emontnemery Date: Mon, 21 Jan 2019 01:41:50 +0100 Subject: [PATCH 036/222] Add JSON attribute topic to MQTT lock (#20241) --- homeassistant/components/mqtt/lock.py | 12 ++-- tests/components/mqtt/test_lock.py | 100 ++++++++++++++++++++++++++ 2 files changed, 108 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/mqtt/lock.py b/homeassistant/components/mqtt/lock.py index e82498a9b12..5f1c169c349 100644 --- a/homeassistant/components/mqtt/lock.py +++ b/homeassistant/components/mqtt/lock.py @@ -12,7 +12,7 @@ from homeassistant.core import callback from homeassistant.components.lock import LockDevice from homeassistant.components.mqtt import ( ATTR_DISCOVERY_HASH, CONF_COMMAND_TOPIC, CONF_QOS, CONF_RETAIN, - CONF_STATE_TOPIC, MqttAvailability, MqttDiscoveryUpdate, + CONF_STATE_TOPIC, MqttAttributes, MqttAvailability, MqttDiscoveryUpdate, MqttEntityDeviceInfo, subscription) from homeassistant.const import ( CONF_DEVICE, CONF_NAME, CONF_OPTIMISTIC, CONF_VALUE_TEMPLATE) @@ -44,7 +44,8 @@ PLATFORM_SCHEMA = mqtt.MQTT_RW_PLATFORM_SCHEMA.extend({ vol.Optional(CONF_OPTIMISTIC, default=DEFAULT_OPTIMISTIC): cv.boolean, vol.Optional(CONF_UNIQUE_ID): cv.string, vol.Optional(CONF_DEVICE): mqtt.MQTT_ENTITY_DEVICE_INFO_SCHEMA, -}).extend(mqtt.MQTT_AVAILABILITY_SCHEMA.schema) +}).extend(mqtt.MQTT_AVAILABILITY_SCHEMA.schema).extend( + mqtt.MQTT_JSON_ATTRS_SCHEMA.schema) async def async_setup_platform(hass: HomeAssistantType, config: ConfigType, @@ -78,8 +79,8 @@ async def _async_setup_entity(config, async_add_entities, async_add_entities([MqttLock(config, discovery_hash)]) -class MqttLock(MqttAvailability, MqttDiscoveryUpdate, MqttEntityDeviceInfo, - LockDevice): +class MqttLock(MqttAttributes, MqttAvailability, MqttDiscoveryUpdate, + MqttEntityDeviceInfo, LockDevice): """Representation of a lock that can be toggled using MQTT.""" def __init__(self, config, discovery_hash): @@ -92,6 +93,7 @@ class MqttLock(MqttAvailability, MqttDiscoveryUpdate, MqttEntityDeviceInfo, device_config = config.get(CONF_DEVICE) + MqttAttributes.__init__(self, config) MqttAvailability.__init__(self, config) MqttDiscoveryUpdate.__init__(self, discovery_hash, self.discovery_update) @@ -106,6 +108,7 @@ class MqttLock(MqttAvailability, MqttDiscoveryUpdate, MqttEntityDeviceInfo, """Handle updated discovery message.""" config = PLATFORM_SCHEMA(discovery_payload) self._config = config + await self.attributes_discovery_update(config) await self.availability_discovery_update(config) await self._subscribe_topics() self.async_schedule_update_ha_state() @@ -143,6 +146,7 @@ class MqttLock(MqttAvailability, MqttDiscoveryUpdate, MqttEntityDeviceInfo, """Unsubscribe when removed.""" self._sub_state = await subscription.async_unsubscribe_topics( self.hass, self._sub_state) + await MqttAttributes.async_will_remove_from_hass(self) await MqttAvailability.async_will_remove_from_hass(self) @property diff --git a/tests/components/mqtt/test_lock.py b/tests/components/mqtt/test_lock.py index 83ae806d295..33802bc0795 100644 --- a/tests/components/mqtt/test_lock.py +++ b/tests/components/mqtt/test_lock.py @@ -139,6 +139,106 @@ async def test_custom_availability_payload(hass, mqtt_mock): assert state.state is STATE_UNAVAILABLE +async def test_setting_attribute_via_mqtt_json_message(hass, mqtt_mock): + """Test the setting of attribute via MQTT with JSON payload.""" + assert await async_setup_component(hass, lock.DOMAIN, { + lock.DOMAIN: { + 'platform': 'mqtt', + 'name': 'test', + 'command_topic': 'test-topic', + 'json_attributes_topic': 'attr-topic' + } + }) + + async_fire_mqtt_message(hass, 'attr-topic', '{ "val": "100" }') + await hass.async_block_till_done() + state = hass.states.get('lock.test') + + assert '100' == state.attributes.get('val') + + +async def test_update_with_json_attrs_not_dict(hass, mqtt_mock, caplog): + """Test attributes get extracted from a JSON result.""" + assert await async_setup_component(hass, lock.DOMAIN, { + lock.DOMAIN: { + 'platform': 'mqtt', + 'name': 'test', + 'command_topic': 'test-topic', + 'json_attributes_topic': 'attr-topic' + } + }) + + async_fire_mqtt_message(hass, 'attr-topic', '[ "list", "of", "things"]') + await hass.async_block_till_done() + state = hass.states.get('lock.test') + + assert state.attributes.get('val') is None + assert 'JSON result was not a dictionary' in caplog.text + + +async def test_update_with_json_attrs_bad_JSON(hass, mqtt_mock, caplog): + """Test attributes get extracted from a JSON result.""" + assert await async_setup_component(hass, lock.DOMAIN, { + lock.DOMAIN: { + 'platform': 'mqtt', + 'name': 'test', + 'command_topic': 'test-topic', + 'json_attributes_topic': 'attr-topic' + } + }) + + async_fire_mqtt_message(hass, 'attr-topic', 'This is not JSON') + await hass.async_block_till_done() + + state = hass.states.get('lock.test') + assert state.attributes.get('val') is None + assert 'Erroneous JSON: This is not JSON' in caplog.text + + +async def test_discovery_update_attr(hass, mqtt_mock, caplog): + """Test update of discovered MQTTAttributes.""" + entry = MockConfigEntry(domain=mqtt.DOMAIN) + await async_start(hass, 'homeassistant', {}, entry) + data1 = ( + '{ "name": "Beer",' + ' "command_topic": "test_topic",' + ' "json_attributes_topic": "attr-topic1" }' + ) + data2 = ( + '{ "name": "Beer",' + ' "command_topic": "test_topic",' + ' "json_attributes_topic": "attr-topic2" }' + ) + async_fire_mqtt_message(hass, 'homeassistant/lock/bla/config', + data1) + await hass.async_block_till_done() + async_fire_mqtt_message(hass, 'attr-topic1', '{ "val": "100" }') + await hass.async_block_till_done() + await hass.async_block_till_done() + state = hass.states.get('lock.beer') + assert '100' == state.attributes.get('val') + + # Change json_attributes_topic + async_fire_mqtt_message(hass, 'homeassistant/lock/bla/config', + data2) + await hass.async_block_till_done() + await hass.async_block_till_done() + + # Verify we are no longer subscribing to the old topic + async_fire_mqtt_message(hass, 'attr-topic1', '{ "val": "50" }') + await hass.async_block_till_done() + await hass.async_block_till_done() + state = hass.states.get('lock.beer') + assert '100' == state.attributes.get('val') + + # Verify we are subscribing to the new topic + async_fire_mqtt_message(hass, 'attr-topic2', '{ "val": "75" }') + await hass.async_block_till_done() + await hass.async_block_till_done() + state = hass.states.get('lock.beer') + assert '75' == state.attributes.get('val') + + async def test_unique_id(hass): """Test unique id option only creates one light per unique_id.""" await async_mock_mqtt_component(hass) From 89e9d827a23fbbe4b1adaebb3ce1c351292076bb Mon Sep 17 00:00:00 2001 From: emontnemery Date: Mon, 21 Jan 2019 01:42:17 +0100 Subject: [PATCH 037/222] Add JSON attribute topic to MQTT fan (#20240) --- homeassistant/components/mqtt/fan.py | 13 ++-- tests/components/mqtt/test_fan.py | 100 +++++++++++++++++++++++++++ 2 files changed, 109 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/mqtt/fan.py b/homeassistant/components/mqtt/fan.py index 22f89a40e04..7e359c2bdc5 100644 --- a/homeassistant/components/mqtt/fan.py +++ b/homeassistant/components/mqtt/fan.py @@ -15,7 +15,7 @@ from homeassistant.const import ( CONF_PAYLOAD_OFF, CONF_PAYLOAD_ON, CONF_DEVICE) from homeassistant.components.mqtt import ( ATTR_DISCOVERY_HASH, CONF_COMMAND_TOPIC, CONF_QOS, CONF_RETAIN, - CONF_STATE_TOPIC, MqttAvailability, MqttDiscoveryUpdate, + CONF_STATE_TOPIC, MqttAttributes, MqttAvailability, MqttDiscoveryUpdate, MqttEntityDeviceInfo, subscription) import homeassistant.helpers.config_validation as cv from homeassistant.helpers.dispatcher import async_dispatcher_connect @@ -80,7 +80,8 @@ PLATFORM_SCHEMA = mqtt.MQTT_RW_PLATFORM_SCHEMA.extend({ vol.Optional(CONF_OPTIMISTIC, default=DEFAULT_OPTIMISTIC): cv.boolean, vol.Optional(CONF_UNIQUE_ID): cv.string, vol.Optional(CONF_DEVICE): mqtt.MQTT_ENTITY_DEVICE_INFO_SCHEMA, -}).extend(mqtt.MQTT_AVAILABILITY_SCHEMA.schema) +}).extend(mqtt.MQTT_AVAILABILITY_SCHEMA.schema).extend( + mqtt.MQTT_JSON_ATTRS_SCHEMA.schema) async def async_setup_platform(hass: HomeAssistantType, config: ConfigType, @@ -117,8 +118,9 @@ async def _async_setup_entity(config, async_add_entities, )]) -class MqttFan(MqttAvailability, MqttDiscoveryUpdate, MqttEntityDeviceInfo, - FanEntity): +# pylint: disable=too-many-ancestors +class MqttFan(MqttAttributes, MqttAvailability, MqttDiscoveryUpdate, + MqttEntityDeviceInfo, FanEntity): """A MQTT fan component.""" def __init__(self, config, discovery_hash): @@ -142,6 +144,7 @@ class MqttFan(MqttAvailability, MqttDiscoveryUpdate, MqttEntityDeviceInfo, device_config = config.get(CONF_DEVICE) + MqttAttributes.__init__(self, config) MqttAvailability.__init__(self, config) MqttDiscoveryUpdate.__init__(self, discovery_hash, self.discovery_update) @@ -156,6 +159,7 @@ class MqttFan(MqttAvailability, MqttDiscoveryUpdate, MqttEntityDeviceInfo, """Handle updated discovery message.""" config = PLATFORM_SCHEMA(discovery_payload) self._setup_from_config(config) + await self.attributes_discovery_update(config) await self.availability_discovery_update(config) await self._subscribe_topics() self.async_schedule_update_ha_state() @@ -273,6 +277,7 @@ class MqttFan(MqttAvailability, MqttDiscoveryUpdate, MqttEntityDeviceInfo, """Unsubscribe when removed.""" self._sub_state = await subscription.async_unsubscribe_topics( self.hass, self._sub_state) + await MqttAttributes.async_will_remove_from_hass(self) await MqttAvailability.async_will_remove_from_hass(self) @property diff --git a/tests/components/mqtt/test_fan.py b/tests/components/mqtt/test_fan.py index fea6f6dda74..ebea6afd4ae 100644 --- a/tests/components/mqtt/test_fan.py +++ b/tests/components/mqtt/test_fan.py @@ -195,6 +195,106 @@ async def test_discovery_broken(hass, mqtt_mock, caplog): assert state is None +async def test_setting_attribute_via_mqtt_json_message(hass, mqtt_mock): + """Test the setting of attribute via MQTT with JSON payload.""" + assert await async_setup_component(hass, fan.DOMAIN, { + fan.DOMAIN: { + 'platform': 'mqtt', + 'name': 'test', + 'command_topic': 'test-topic', + 'json_attributes_topic': 'attr-topic' + } + }) + + async_fire_mqtt_message(hass, 'attr-topic', '{ "val": "100" }') + await hass.async_block_till_done() + state = hass.states.get('fan.test') + + assert '100' == state.attributes.get('val') + + +async def test_update_with_json_attrs_not_dict(hass, mqtt_mock, caplog): + """Test attributes get extracted from a JSON result.""" + assert await async_setup_component(hass, fan.DOMAIN, { + fan.DOMAIN: { + 'platform': 'mqtt', + 'name': 'test', + 'command_topic': 'test-topic', + 'json_attributes_topic': 'attr-topic' + } + }) + + async_fire_mqtt_message(hass, 'attr-topic', '[ "list", "of", "things"]') + await hass.async_block_till_done() + state = hass.states.get('fan.test') + + assert state.attributes.get('val') is None + assert 'JSON result was not a dictionary' in caplog.text + + +async def test_update_with_json_attrs_bad_JSON(hass, mqtt_mock, caplog): + """Test attributes get extracted from a JSON result.""" + assert await async_setup_component(hass, fan.DOMAIN, { + fan.DOMAIN: { + 'platform': 'mqtt', + 'name': 'test', + 'command_topic': 'test-topic', + 'json_attributes_topic': 'attr-topic' + } + }) + + async_fire_mqtt_message(hass, 'attr-topic', 'This is not JSON') + await hass.async_block_till_done() + + state = hass.states.get('fan.test') + assert state.attributes.get('val') is None + assert 'Erroneous JSON: This is not JSON' in caplog.text + + +async def test_discovery_update_attr(hass, mqtt_mock, caplog): + """Test update of discovered MQTTAttributes.""" + entry = MockConfigEntry(domain=mqtt.DOMAIN) + await async_start(hass, 'homeassistant', {}, entry) + data1 = ( + '{ "name": "Beer",' + ' "command_topic": "test_topic",' + ' "json_attributes_topic": "attr-topic1" }' + ) + data2 = ( + '{ "name": "Beer",' + ' "command_topic": "test_topic",' + ' "json_attributes_topic": "attr-topic2" }' + ) + async_fire_mqtt_message(hass, 'homeassistant/fan/bla/config', + data1) + await hass.async_block_till_done() + async_fire_mqtt_message(hass, 'attr-topic1', '{ "val": "100" }') + await hass.async_block_till_done() + await hass.async_block_till_done() + state = hass.states.get('fan.beer') + assert '100' == state.attributes.get('val') + + # Change json_attributes_topic + async_fire_mqtt_message(hass, 'homeassistant/fan/bla/config', + data2) + await hass.async_block_till_done() + await hass.async_block_till_done() + + # Verify we are no longer subscribing to the old topic + async_fire_mqtt_message(hass, 'attr-topic1', '{ "val": "50" }') + await hass.async_block_till_done() + await hass.async_block_till_done() + state = hass.states.get('fan.beer') + assert '100' == state.attributes.get('val') + + # Verify we are subscribing to the new topic + async_fire_mqtt_message(hass, 'attr-topic2', '{ "val": "75" }') + await hass.async_block_till_done() + await hass.async_block_till_done() + state = hass.states.get('fan.beer') + assert '75' == state.attributes.get('val') + + async def test_unique_id(hass): """Test unique_id option only creates one fan per id.""" await async_mock_mqtt_component(hass) From dbba3eb0d4930bef40e756f8be48cc43aaea299b Mon Sep 17 00:00:00 2001 From: emontnemery Date: Mon, 21 Jan 2019 01:42:35 +0100 Subject: [PATCH 038/222] Add JSON attribute topic to MQTT climate (#20239) --- homeassistant/components/mqtt/climate.py | 13 ++- tests/components/mqtt/test_climate.py | 100 +++++++++++++++++++++++ 2 files changed, 109 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/mqtt/climate.py b/homeassistant/components/mqtt/climate.py index 71950f9b1b7..b317d11d10b 100644 --- a/homeassistant/components/mqtt/climate.py +++ b/homeassistant/components/mqtt/climate.py @@ -22,7 +22,8 @@ from homeassistant.const import ( STATE_OFF) from homeassistant.components.mqtt import ( ATTR_DISCOVERY_HASH, CONF_QOS, CONF_RETAIN, MQTT_BASE_PLATFORM_SCHEMA, - MqttAvailability, MqttDiscoveryUpdate, MqttEntityDeviceInfo, subscription) + MqttAttributes, MqttAvailability, MqttDiscoveryUpdate, + MqttEntityDeviceInfo, subscription) from homeassistant.components.mqtt.discovery import ( MQTT_DISCOVERY_NEW, clear_discovery_hash) import homeassistant.helpers.config_validation as cv @@ -144,7 +145,8 @@ PLATFORM_SCHEMA = SCHEMA_BASE.extend({ vol.Optional(CONF_TEMP_STEP, default=1.0): vol.Coerce(float), vol.Optional(CONF_UNIQUE_ID): cv.string, vol.Optional(CONF_DEVICE): mqtt.MQTT_ENTITY_DEVICE_INFO_SCHEMA, -}).extend(mqtt.MQTT_AVAILABILITY_SCHEMA.schema) +}).extend(mqtt.MQTT_AVAILABILITY_SCHEMA.schema).extend( + mqtt.MQTT_JSON_ATTRS_SCHEMA.schema) async def async_setup_platform(hass: HomeAssistantType, config: ConfigType, @@ -183,8 +185,8 @@ async def _async_setup_entity(hass, config, async_add_entities, )]) -class MqttClimate(MqttAvailability, MqttDiscoveryUpdate, MqttEntityDeviceInfo, - ClimateDevice): +class MqttClimate(MqttAttributes, MqttAvailability, MqttDiscoveryUpdate, + MqttEntityDeviceInfo, ClimateDevice): """Representation of an MQTT climate device.""" def __init__(self, hass, config, discovery_hash): @@ -210,6 +212,7 @@ class MqttClimate(MqttAvailability, MqttDiscoveryUpdate, MqttEntityDeviceInfo, device_config = config.get(CONF_DEVICE) + MqttAttributes.__init__(self, config) MqttAvailability.__init__(self, config) MqttDiscoveryUpdate.__init__(self, discovery_hash, self.discovery_update) @@ -225,6 +228,7 @@ class MqttClimate(MqttAvailability, MqttDiscoveryUpdate, MqttEntityDeviceInfo, config = PLATFORM_SCHEMA(discovery_payload) self._config = config self._setup_from_config(config) + await self.attributes_discovery_update(config) await self.availability_discovery_update(config) await self._subscribe_topics() self.async_schedule_update_ha_state() @@ -463,6 +467,7 @@ class MqttClimate(MqttAvailability, MqttDiscoveryUpdate, MqttEntityDeviceInfo, """Unsubscribe when removed.""" self._sub_state = await subscription.async_unsubscribe_topics( self.hass, self._sub_state) + await MqttAttributes.async_will_remove_from_hass(self) await MqttAvailability.async_will_remove_from_hass(self) @property diff --git a/tests/components/mqtt/test_climate.py b/tests/components/mqtt/test_climate.py index a2aa424eeee..ac5e1b07f27 100644 --- a/tests/components/mqtt/test_climate.py +++ b/tests/components/mqtt/test_climate.py @@ -674,6 +674,106 @@ class TestMQTTClimate(unittest.TestCase): assert 0.01 == temp_step +async def test_setting_attribute_via_mqtt_json_message(hass, mqtt_mock): + """Test the setting of attribute via MQTT with JSON payload.""" + assert await async_setup_component(hass, climate.DOMAIN, { + climate.DOMAIN: { + 'platform': 'mqtt', + 'name': 'test', + 'state_topic': 'test-topic', + 'json_attributes_topic': 'attr-topic' + } + }) + + async_fire_mqtt_message(hass, 'attr-topic', '{ "val": "100" }') + await hass.async_block_till_done() + state = hass.states.get('climate.test') + + assert '100' == state.attributes.get('val') + + +async def test_update_with_json_attrs_not_dict(hass, mqtt_mock, caplog): + """Test attributes get extracted from a JSON result.""" + assert await async_setup_component(hass, climate.DOMAIN, { + climate.DOMAIN: { + 'platform': 'mqtt', + 'name': 'test', + 'state_topic': 'test-topic', + 'json_attributes_topic': 'attr-topic' + } + }) + + async_fire_mqtt_message(hass, 'attr-topic', '[ "list", "of", "things"]') + await hass.async_block_till_done() + state = hass.states.get('climate.test') + + assert state.attributes.get('val') is None + assert 'JSON result was not a dictionary' in caplog.text + + +async def test_update_with_json_attrs_bad_JSON(hass, mqtt_mock, caplog): + """Test attributes get extracted from a JSON result.""" + assert await async_setup_component(hass, climate.DOMAIN, { + climate.DOMAIN: { + 'platform': 'mqtt', + 'name': 'test', + 'state_topic': 'test-topic', + 'json_attributes_topic': 'attr-topic' + } + }) + + async_fire_mqtt_message(hass, 'attr-topic', 'This is not JSON') + await hass.async_block_till_done() + + state = hass.states.get('climate.test') + assert state.attributes.get('val') is None + assert 'Erroneous JSON: This is not JSON' in caplog.text + + +async def test_discovery_update_attr(hass, mqtt_mock, caplog): + """Test update of discovered MQTTAttributes.""" + entry = MockConfigEntry(domain=mqtt.DOMAIN) + await async_start(hass, 'homeassistant', {}, entry) + data1 = ( + '{ "name": "Beer",' + ' "command_topic": "test_topic",' + ' "json_attributes_topic": "attr-topic1" }' + ) + data2 = ( + '{ "name": "Beer",' + ' "command_topic": "test_topic",' + ' "json_attributes_topic": "attr-topic2" }' + ) + async_fire_mqtt_message(hass, 'homeassistant/climate/bla/config', + data1) + await hass.async_block_till_done() + async_fire_mqtt_message(hass, 'attr-topic1', '{ "val": "100" }') + await hass.async_block_till_done() + await hass.async_block_till_done() + state = hass.states.get('climate.beer') + assert '100' == state.attributes.get('val') + + # Change json_attributes_topic + async_fire_mqtt_message(hass, 'homeassistant/climate/bla/config', + data2) + await hass.async_block_till_done() + await hass.async_block_till_done() + + # Verify we are no longer subscribing to the old topic + async_fire_mqtt_message(hass, 'attr-topic1', '{ "val": "50" }') + await hass.async_block_till_done() + await hass.async_block_till_done() + state = hass.states.get('climate.beer') + assert '100' == state.attributes.get('val') + + # Verify we are subscribing to the new topic + async_fire_mqtt_message(hass, 'attr-topic2', '{ "val": "75" }') + await hass.async_block_till_done() + await hass.async_block_till_done() + state = hass.states.get('climate.beer') + assert '75' == state.attributes.get('val') + + async def test_unique_id(hass): """Test unique id option only creates one climate per unique_id.""" await async_mock_mqtt_component(hass) From 3fcbcd5a38684ebed692e8cc2887b7b2a8b49875 Mon Sep 17 00:00:00 2001 From: emontnemery Date: Mon, 21 Jan 2019 01:42:56 +0100 Subject: [PATCH 039/222] Add JSON attribute topic to MQTT alarm (#20238) --- .../components/mqtt/alarm_control_panel.py | 12 +- .../mqtt/test_alarm_control_panel.py | 103 ++++++++++++++++++ 2 files changed, 111 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/mqtt/alarm_control_panel.py b/homeassistant/components/mqtt/alarm_control_panel.py index 5bd4117ecee..7c412a3febe 100644 --- a/homeassistant/components/mqtt/alarm_control_panel.py +++ b/homeassistant/components/mqtt/alarm_control_panel.py @@ -18,7 +18,7 @@ from homeassistant.const import ( STATE_ALARM_TRIGGERED, STATE_UNKNOWN) from homeassistant.components.mqtt import ( ATTR_DISCOVERY_HASH, CONF_COMMAND_TOPIC, CONF_QOS, CONF_RETAIN, - CONF_STATE_TOPIC, MqttAvailability, MqttDiscoveryUpdate, + CONF_STATE_TOPIC, MqttAttributes, MqttAvailability, MqttDiscoveryUpdate, MqttEntityDeviceInfo, subscription) from homeassistant.components.mqtt.discovery import ( MQTT_DISCOVERY_NEW, clear_discovery_hash) @@ -49,7 +49,8 @@ PLATFORM_SCHEMA = mqtt.MQTT_BASE_PLATFORM_SCHEMA.extend({ vol.Optional(CONF_PAYLOAD_DISARM, default=DEFAULT_DISARM): cv.string, vol.Optional(CONF_UNIQUE_ID): cv.string, vol.Optional(CONF_DEVICE): mqtt.MQTT_ENTITY_DEVICE_INFO_SCHEMA, -}).extend(mqtt.MQTT_AVAILABILITY_SCHEMA.schema) +}).extend(mqtt.MQTT_AVAILABILITY_SCHEMA.schema).extend( + mqtt.MQTT_JSON_ATTRS_SCHEMA.schema) async def async_setup_platform(hass: HomeAssistantType, config: ConfigType, @@ -83,8 +84,8 @@ async def _async_setup_entity(config, async_add_entities, async_add_entities([MqttAlarm(config, discovery_hash)]) -class MqttAlarm(MqttAvailability, MqttDiscoveryUpdate, MqttEntityDeviceInfo, - alarm.AlarmControlPanel): +class MqttAlarm(MqttAttributes, MqttAvailability, MqttDiscoveryUpdate, + MqttEntityDeviceInfo, alarm.AlarmControlPanel): """Representation of a MQTT alarm status.""" def __init__(self, config, discovery_hash): @@ -96,6 +97,7 @@ class MqttAlarm(MqttAvailability, MqttDiscoveryUpdate, MqttEntityDeviceInfo, device_config = config.get(CONF_DEVICE) + MqttAttributes.__init__(self, config) MqttAvailability.__init__(self, config) MqttDiscoveryUpdate.__init__(self, discovery_hash, self.discovery_update) @@ -110,6 +112,7 @@ class MqttAlarm(MqttAvailability, MqttDiscoveryUpdate, MqttEntityDeviceInfo, """Handle updated discovery message.""" config = PLATFORM_SCHEMA(discovery_payload) self._config = config + await self.attributes_discovery_update(config) await self.availability_discovery_update(config) await self._subscribe_topics() self.async_schedule_update_ha_state() @@ -137,6 +140,7 @@ class MqttAlarm(MqttAvailability, MqttDiscoveryUpdate, MqttEntityDeviceInfo, """Unsubscribe when removed.""" self._sub_state = await subscription.async_unsubscribe_topics( self.hass, self._sub_state) + await MqttAttributes.async_will_remove_from_hass(self) await MqttAvailability.async_will_remove_from_hass(self) @property diff --git a/tests/components/mqtt/test_alarm_control_panel.py b/tests/components/mqtt/test_alarm_control_panel.py index 1a89e2382e3..600d348dd98 100644 --- a/tests/components/mqtt/test_alarm_control_panel.py +++ b/tests/components/mqtt/test_alarm_control_panel.py @@ -246,6 +246,109 @@ class TestAlarmControlPanelMQTT(unittest.TestCase): fire_mqtt_message(self.hass, 'availability-topic', 'good') +async def test_setting_attribute_via_mqtt_json_message(hass, mqtt_mock): + """Test the setting of attribute via MQTT with JSON payload.""" + assert await async_setup_component(hass, alarm_control_panel.DOMAIN, { + alarm_control_panel.DOMAIN: { + 'platform': 'mqtt', + 'name': 'test', + 'command_topic': 'test-topic', + 'state_topic': 'test-topic', + 'json_attributes_topic': 'attr-topic' + } + }) + + async_fire_mqtt_message(hass, 'attr-topic', '{ "val": "100" }') + await hass.async_block_till_done() + state = hass.states.get('alarm_control_panel.test') + + assert '100' == state.attributes.get('val') + + +async def test_update_with_json_attrs_not_dict(hass, mqtt_mock, caplog): + """Test attributes get extracted from a JSON result.""" + assert await async_setup_component(hass, alarm_control_panel.DOMAIN, { + alarm_control_panel.DOMAIN: { + 'platform': 'mqtt', + 'name': 'test', + 'command_topic': 'test-topic', + 'state_topic': 'test-topic', + 'json_attributes_topic': 'attr-topic' + } + }) + + async_fire_mqtt_message(hass, 'attr-topic', '[ "list", "of", "things"]') + await hass.async_block_till_done() + state = hass.states.get('alarm_control_panel.test') + + assert state.attributes.get('val') is None + assert 'JSON result was not a dictionary' in caplog.text + + +async def test_update_with_json_attrs_bad_JSON(hass, mqtt_mock, caplog): + """Test attributes get extracted from a JSON result.""" + assert await async_setup_component(hass, alarm_control_panel.DOMAIN, { + alarm_control_panel.DOMAIN: { + 'platform': 'mqtt', + 'name': 'test', + 'command_topic': 'test-topic', + 'state_topic': 'test-topic', + 'json_attributes_topic': 'attr-topic' + } + }) + + async_fire_mqtt_message(hass, 'attr-topic', 'This is not JSON') + await hass.async_block_till_done() + + state = hass.states.get('alarm_control_panel.test') + assert state.attributes.get('val') is None + assert 'Erroneous JSON: This is not JSON' in caplog.text + + +async def test_discovery_update_attr(hass, mqtt_mock, caplog): + """Test update of discovered MQTTAttributes.""" + entry = MockConfigEntry(domain=mqtt.DOMAIN) + await async_start(hass, 'homeassistant', {}, entry) + data1 = ( + '{ "name": "Beer",' + ' "command_topic": "test_topic",' + ' "json_attributes_topic": "attr-topic1" }' + ) + data2 = ( + '{ "name": "Beer",' + ' "command_topic": "test_topic",' + ' "json_attributes_topic": "attr-topic2" }' + ) + async_fire_mqtt_message( + hass, 'homeassistant/alarm_control_panel/bla/config', data1) + await hass.async_block_till_done() + async_fire_mqtt_message(hass, 'attr-topic1', '{ "val": "100" }') + await hass.async_block_till_done() + await hass.async_block_till_done() + state = hass.states.get('alarm_control_panel.beer') + assert '100' == state.attributes.get('val') + + # Change json_attributes_topic + async_fire_mqtt_message( + hass, 'homeassistant/alarm_control_panel/bla/config', data2) + await hass.async_block_till_done() + await hass.async_block_till_done() + + # Verify we are no longer subscribing to the old topic + async_fire_mqtt_message(hass, 'attr-topic1', '{ "val": "50" }') + await hass.async_block_till_done() + await hass.async_block_till_done() + state = hass.states.get('alarm_control_panel.beer') + assert '100' == state.attributes.get('val') + + # Verify we are subscribing to the new topic + async_fire_mqtt_message(hass, 'attr-topic2', '{ "val": "75" }') + await hass.async_block_till_done() + await hass.async_block_till_done() + state = hass.states.get('alarm_control_panel.beer') + assert '75' == state.attributes.get('val') + + async def test_unique_id(hass): """Test unique id option only creates one alarm per unique_id.""" await async_mock_mqtt_component(hass) From 935e5c67a3e9892f9bc3611c8ee4efa629bedea4 Mon Sep 17 00:00:00 2001 From: Phil Bruckner Date: Sun, 20 Jan 2019 18:46:14 -0600 Subject: [PATCH 040/222] Handle non-string values in JSON renderer (#20233) Handle the case of async_render_with_possible_json_value's value argument being something other than a string. This can happen, e.g., when using the SQL sensor to extract a datetime column such as last_changed and also using its value_template to convert that datetime to another format. This was causing a TypeError from json.loads, but async_render_with_possible_json_value was only catching ValueError's. --- homeassistant/helpers/template.py | 2 +- tests/helpers/test_template.py | 11 +++++++++++ 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/homeassistant/helpers/template.py b/homeassistant/helpers/template.py index 267bf8853d9..03ae37843d8 100644 --- a/homeassistant/helpers/template.py +++ b/homeassistant/helpers/template.py @@ -167,7 +167,7 @@ class Template: try: variables['value_json'] = json.loads(value) - except ValueError: + except (ValueError, TypeError): pass try: diff --git a/tests/helpers/test_template.py b/tests/helpers/test_template.py index 02331c400d3..3febd4037ad 100644 --- a/tests/helpers/test_template.py +++ b/tests/helpers/test_template.py @@ -4,6 +4,7 @@ from datetime import datetime import unittest import random import math +import pytz from unittest.mock import patch from homeassistant.components import group @@ -422,6 +423,16 @@ class TestHelpersTemplate(unittest.TestCase): assert '' == \ tpl.render_with_possible_json_value('{"hello": "world"}', '') + def test_render_with_possible_json_value_non_string_value(self): + """Render with possible JSON value with non-string value.""" + tpl = template.Template(""" +{{ strptime(value~'+0000', '%Y-%m-%d %H:%M:%S%z') }} + """, self.hass) + value = datetime(2019, 1, 18, 12, 13, 14) + expected = str(pytz.utc.localize(value)) + assert expected == \ + tpl.render_with_possible_json_value(value) + def test_raise_exception_on_error(self): """Test raising an exception on error.""" with pytest.raises(TemplateError): From 5999df1953984c30dbcef7651e93a9d939540106 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sun, 20 Jan 2019 17:31:09 -0800 Subject: [PATCH 041/222] Clean up build artifacts correctly --- script/release | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/script/release b/script/release index cf4f808377e..4dc94eb7f15 100755 --- a/script/release +++ b/script/release @@ -27,6 +27,6 @@ then exit 1 fi -rm -rf dist +rm -rf dist build python3 setup.py sdist bdist_wheel python3 -m twine upload dist/* --skip-existing From 9482a6303da196c55739af2772896d2e8819913c Mon Sep 17 00:00:00 2001 From: Jon Caruana Date: Sun, 20 Jan 2019 19:23:36 -0800 Subject: [PATCH 042/222] Add EverLights light component (#19817) * EverLights light integration. Supports single color (with color and brightness parameters) or saved pattern (with effect parameter). * Fix pylint parameter name warning. * Code review feedback. * Add tests for the two helper functions of EverLights component. * Fixes for review feedback. * Change test style. * Style fixes for hound. --- .coveragerc | 1 + homeassistant/components/light/everlights.py | 177 +++++++++++++++++++ requirements_all.txt | 3 + tests/components/light/test_everlights.py | 16 ++ 4 files changed, 197 insertions(+) create mode 100644 homeassistant/components/light/everlights.py create mode 100644 tests/components/light/test_everlights.py diff --git a/.coveragerc b/.coveragerc index 29a9caa8066..93c9b28e103 100644 --- a/.coveragerc +++ b/.coveragerc @@ -581,6 +581,7 @@ omit = homeassistant/components/light/blinkt.py homeassistant/components/light/decora_wifi.py homeassistant/components/light/decora.py + homeassistant/components/light/everlights.py homeassistant/components/light/flux_led.py homeassistant/components/light/futurenow.py homeassistant/components/light/greenwave.py diff --git a/homeassistant/components/light/everlights.py b/homeassistant/components/light/everlights.py new file mode 100644 index 00000000000..31e72c78fd6 --- /dev/null +++ b/homeassistant/components/light/everlights.py @@ -0,0 +1,177 @@ +""" +Support for EverLights lights. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/light.everlights/ +""" +import logging +from datetime import timedelta +from typing import Tuple + +import voluptuous as vol + +from homeassistant.const import CONF_HOSTS +from homeassistant.components.light import ( + ATTR_BRIGHTNESS, ATTR_HS_COLOR, ATTR_EFFECT, + SUPPORT_BRIGHTNESS, SUPPORT_EFFECT, SUPPORT_COLOR, + Light, PLATFORM_SCHEMA) +import homeassistant.helpers.config_validation as cv +import homeassistant.util.color as color_util +from homeassistant.helpers.aiohttp_client import async_get_clientsession +from homeassistant.exceptions import PlatformNotReady + +REQUIREMENTS = ['pyeverlights==0.1.0'] + +_LOGGER = logging.getLogger(__name__) + +SUPPORT_EVERLIGHTS = (SUPPORT_EFFECT | SUPPORT_BRIGHTNESS | SUPPORT_COLOR) + +SCAN_INTERVAL = timedelta(minutes=1) + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Required(CONF_HOSTS): vol.All(cv.ensure_list, [cv.string]), +}) + +NAME_FORMAT = "EverLights {} Zone {}" + + +def color_rgb_to_int(red: int, green: int, blue: int) -> int: + """Return a RGB color as an integer.""" + return red*256*256+green*256+blue + + +def color_int_to_rgb(value: int) -> Tuple[int, int, int]: + """Return an RGB tuple from an integer.""" + return (value >> 16, (value >> 8) & 0xff, value & 0xff) + + +async def async_setup_platform(hass, config, async_add_entities, + discovery_info=None): + """Set up the EverLights lights from configuration.yaml.""" + import pyeverlights + lights = [] + + for ipaddr in config[CONF_HOSTS]: + api = pyeverlights.EverLights(ipaddr, + async_get_clientsession(hass)) + + try: + status = await api.get_status() + + effects = await api.get_all_patterns() + + except pyeverlights.ConnectionError: + raise PlatformNotReady + + else: + lights.append(EverLightsLight(api, pyeverlights.ZONE_1, + status, effects)) + lights.append(EverLightsLight(api, pyeverlights.ZONE_2, + status, effects)) + + async_add_entities(lights) + + +class EverLightsLight(Light): + """Representation of a Flux light.""" + + def __init__(self, api, channel, status, effects): + """Initialize the light.""" + self._api = api + self._channel = channel + self._status = status + self._effects = effects + self._mac = status['mac'] + self._error_reported = False + self._hs_color = [255, 255] + self._brightness = 255 + self._effect = None + self._available = True + + @property + def unique_id(self) -> str: + """Return a unique ID.""" + return '{}-{}'.format(self._mac, self._channel) + + @property + def available(self) -> bool: + """Return True if entity is available.""" + return self._available + + @property + def name(self): + """Return the name of the device.""" + return NAME_FORMAT.format(self._mac, self._channel) + + @property + def is_on(self): + """Return true if device is on.""" + return self._status['ch{}Active'.format(self._channel)] == 1 + + @property + def brightness(self): + """Return the brightness of this light between 0..255.""" + return self._brightness + + @property + def hs_color(self): + """Return the color property.""" + return self._hs_color + + @property + def effect(self): + """Return the effect property.""" + return self._effect + + @property + def supported_features(self): + """Flag supported features.""" + return SUPPORT_EVERLIGHTS + + @property + def effect_list(self): + """Return the list of supported effects.""" + return self._effects + + async def async_turn_on(self, **kwargs): + """Turn the light on.""" + hs_color = kwargs.get(ATTR_HS_COLOR, self._hs_color) + brightness = kwargs.get(ATTR_BRIGHTNESS, self._brightness) + effect = kwargs.get(ATTR_EFFECT) + + if effect is not None: + colors = await self._api.set_pattern_by_id(self._channel, effect) + + rgb = color_int_to_rgb(colors[0]) + hsv = color_util.color_RGB_to_hsv(*rgb) + hs_color = hsv[:2] + brightness = hsv[2] / 100 * 255 + + else: + rgb = color_util.color_hsv_to_RGB(*hs_color, brightness/255*100) + colors = [color_rgb_to_int(*rgb)] + + await self._api.set_pattern(self._channel, colors) + + self._hs_color = hs_color + self._brightness = brightness + self._effect = effect + + async def async_turn_off(self, **kwargs): + """Turn the light off.""" + await self._api.clear_pattern(self._channel) + + async def async_update(self): + """Synchronize state with control box.""" + import pyeverlights + + try: + self._status = await self._api.get_status() + except pyeverlights.ConnectionError: + if self._available: + _LOGGER.warning("EverLights control box connection lost.") + self._available = False + else: + if not self._available: + _LOGGER.warning("EverLights control box connection restored.") + self._available = True diff --git a/requirements_all.txt b/requirements_all.txt index 0e5ccd36625..a7ccfc7fe71 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -981,6 +981,9 @@ pyenvisalink==3.8 # homeassistant.components.climate.ephember pyephember==0.2.0 +# homeassistant.components.light.everlights +pyeverlights==0.1.0 + # homeassistant.components.sensor.fido pyfido==2.1.1 diff --git a/tests/components/light/test_everlights.py b/tests/components/light/test_everlights.py new file mode 100644 index 00000000000..026e7927c8d --- /dev/null +++ b/tests/components/light/test_everlights.py @@ -0,0 +1,16 @@ +"""The tests for the everlights component.""" +from homeassistant.components.light import everlights + + +def test_color_rgb_to_int(): + """Test RGB to integer conversion.""" + assert everlights.color_rgb_to_int(0x00, 0x00, 0x00) == 0x000000 + assert everlights.color_rgb_to_int(0xff, 0xff, 0xff) == 0xffffff + assert everlights.color_rgb_to_int(0x12, 0x34, 0x56) == 0x123456 + + +def test_int_to_rgb(): + """Test integer to RGB conversion.""" + assert everlights.color_int_to_rgb(0x000000) == (0x00, 0x00, 0x00) + assert everlights.color_int_to_rgb(0xffffff) == (0xff, 0xff, 0xff) + assert everlights.color_int_to_rgb(0x123456) == (0x12, 0x34, 0x56) From 5c208da82eab974ec72087d21588f7c3b1a3bc65 Mon Sep 17 00:00:00 2001 From: Andrew Sayre <6730289+andrewsayre@users.noreply.github.com> Date: Mon, 21 Jan 2019 00:27:32 -0600 Subject: [PATCH 043/222] Added recursive detection of functools.partial. (#20284) --- homeassistant/core.py | 5 +++-- homeassistant/util/logging.py | 9 +++++++-- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/homeassistant/core.py b/homeassistant/core.py index 75f3fe7dae6..f140cd1a0dd 100644 --- a/homeassistant/core.py +++ b/homeassistant/core.py @@ -259,9 +259,10 @@ class HomeAssistant: """ task = None + # Check for partials to properly determine if coroutine function check_target = target - if isinstance(target, functools.partial): - check_target = target.func + while isinstance(check_target, functools.partial): + check_target = check_target.func if asyncio.iscoroutine(check_target): task = self.loop.create_task(target) # type: ignore diff --git a/homeassistant/util/logging.py b/homeassistant/util/logging.py index ae32566c73c..214d9417e2a 100644 --- a/homeassistant/util/logging.py +++ b/homeassistant/util/logging.py @@ -1,7 +1,7 @@ """Logging utilities.""" import asyncio from asyncio.events import AbstractEventLoop -from functools import wraps +from functools import partial, wraps import inspect import logging import threading @@ -139,8 +139,13 @@ def catch_log_exception( friendly_msg = format_err(*args) logging.getLogger(module_name).error('%s\n%s', friendly_msg, exc_msg) + # Check for partials to properly determine if coroutine function + check_func = func + while isinstance(check_func, partial): + check_func = check_func.func + wrapper_func = None - if asyncio.iscoroutinefunction(func): + if asyncio.iscoroutinefunction(check_func): @wraps(func) async def async_wrapper(*args: Any) -> None: """Catch and log exception.""" From ec9575a86f96c1978daeea3286b884b7c9e49e1a Mon Sep 17 00:00:00 2001 From: Andrey Kupreychik Date: Mon, 21 Jan 2019 15:22:44 +0700 Subject: [PATCH 044/222] Added Xiaomi AirPurifier 2S profile (#20285) --- homeassistant/components/fan/xiaomi_miio.py | 23 +++++++++++++++++++-- 1 file changed, 21 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/fan/xiaomi_miio.py b/homeassistant/components/fan/xiaomi_miio.py index d3b52622a95..96cacac3782 100644 --- a/homeassistant/components/fan/xiaomi_miio.py +++ b/homeassistant/components/fan/xiaomi_miio.py @@ -38,7 +38,7 @@ MODEL_AIRPURIFIER_MA1 = 'zhimi.airpurifier.ma1' MODEL_AIRPURIFIER_MA2 = 'zhimi.airpurifier.ma2' MODEL_AIRPURIFIER_SA1 = 'zhimi.airpurifier.sa1' MODEL_AIRPURIFIER_SA2 = 'zhimi.airpurifier.sa2' -MODEL_AIRPURIFIER_MC1 = 'zhimi.airpurifier.mc1' +MODEL_AIRPURIFIER_2S = 'zhimi.airpurifier.mc1' MODEL_AIRHUMIDIFIER_V1 = 'zhimi.humidifier.v1' MODEL_AIRHUMIDIFIER_CA = 'zhimi.humidifier.ca1' @@ -62,7 +62,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ MODEL_AIRPURIFIER_MA2, MODEL_AIRPURIFIER_SA1, MODEL_AIRPURIFIER_SA2, - MODEL_AIRPURIFIER_MC1, + MODEL_AIRPURIFIER_2S, MODEL_AIRHUMIDIFIER_V1, MODEL_AIRHUMIDIFIER_CA, MODEL_AIRFRESH_VA2, @@ -175,6 +175,15 @@ AVAILABLE_ATTRIBUTES_AIRPURIFIER_PRO_V7 = { ATTR_VOLUME: 'volume', } +AVAILABLE_ATTRIBUTES_AIRPURIFIER_2S = { + **AVAILABLE_ATTRIBUTES_AIRPURIFIER_COMMON, + ATTR_BUZZER: 'buzzer', + ATTR_FILTER_RFID_PRODUCT_ID: 'filter_rfid_product_id', + ATTR_FILTER_RFID_TAG: 'filter_rfid_tag', + ATTR_FILTER_TYPE: 'filter_type', + ATTR_ILLUMINANCE: 'illuminance', +} + AVAILABLE_ATTRIBUTES_AIRPURIFIER_V3 = { # Common set isn't used here. It's a very basic version of the device. ATTR_AIR_QUALITY_INDEX: 'aqi', @@ -249,6 +258,7 @@ AVAILABLE_ATTRIBUTES_AIRFRESH = { OPERATION_MODES_AIRPURIFIER = ['Auto', 'Silent', 'Favorite', 'Idle'] OPERATION_MODES_AIRPURIFIER_PRO = ['Auto', 'Silent', 'Favorite'] OPERATION_MODES_AIRPURIFIER_PRO_V7 = OPERATION_MODES_AIRPURIFIER_PRO +OPERATION_MODES_AIRPURIFIER_2S = ['Auto', 'Silent', 'Favorite'] OPERATION_MODES_AIRPURIFIER_V3 = ['Auto', 'Silent', 'Favorite', 'Idle', 'Medium', 'High', 'Strong'] OPERATION_MODES_AIRFRESH = ['Auto', 'Silent', 'Interval', 'Low', @@ -289,6 +299,11 @@ FEATURE_FLAGS_AIRPURIFIER_PRO_V7 = (FEATURE_SET_CHILD_LOCK | FEATURE_SET_FAVORITE_LEVEL | FEATURE_SET_VOLUME) +FEATURE_FLAGS_AIRPURIFIER_2S = (FEATURE_SET_BUZZER | + FEATURE_SET_CHILD_LOCK | + FEATURE_SET_LED | + FEATURE_SET_FAVORITE_LEVEL) + FEATURE_FLAGS_AIRPURIFIER_V3 = (FEATURE_SET_BUZZER | FEATURE_SET_CHILD_LOCK | FEATURE_SET_LED) @@ -619,6 +634,10 @@ class XiaomiAirPurifier(XiaomiGenericDevice): self._available_attributes = \ AVAILABLE_ATTRIBUTES_AIRPURIFIER_PRO_V7 self._speed_list = OPERATION_MODES_AIRPURIFIER_PRO_V7 + elif self._model == MODEL_AIRPURIFIER_2S: + self._device_features = FEATURE_FLAGS_AIRPURIFIER_2S + self._available_attributes = AVAILABLE_ATTRIBUTES_AIRPURIFIER_2S + self._speed_list = OPERATION_MODES_AIRPURIFIER_2S elif self._model == MODEL_AIRPURIFIER_V3: self._device_features = FEATURE_FLAGS_AIRPURIFIER_V3 self._available_attributes = AVAILABLE_ATTRIBUTES_AIRPURIFIER_V3 From e4f42d12828c4805e8093e8e354957fd2ec6765c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20H=C3=B8yer=20Iversen?= Date: Mon, 21 Jan 2019 16:12:03 +0100 Subject: [PATCH 045/222] Update Tibber lib (#20289) --- homeassistant/components/sensor/tibber.py | 2 +- homeassistant/components/tibber/__init__.py | 2 +- requirements_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/sensor/tibber.py b/homeassistant/components/sensor/tibber.py index bc3951f0dea..215cd8a81f5 100644 --- a/homeassistant/components/sensor/tibber.py +++ b/homeassistant/components/sensor/tibber.py @@ -36,7 +36,7 @@ async def async_setup_platform(hass, config, async_add_entities, tibber_connection = hass.data.get(TIBBER_DOMAIN) dev = [] - for home in tibber_connection.get_homes(): + for home in tibber_connection.get_homes(only_active=False): try: await home.update_info() except asyncio.TimeoutError as err: diff --git a/homeassistant/components/tibber/__init__.py b/homeassistant/components/tibber/__init__.py index 850e40d5eeb..91e6ae4bfa8 100644 --- a/homeassistant/components/tibber/__init__.py +++ b/homeassistant/components/tibber/__init__.py @@ -16,7 +16,7 @@ from homeassistant.const import (EVENT_HOMEASSISTANT_STOP, CONF_ACCESS_TOKEN, from homeassistant.helpers import discovery from homeassistant.helpers.aiohttp_client import async_get_clientsession -REQUIREMENTS = ['pyTibber==0.9.2'] +REQUIREMENTS = ['pyTibber==0.9.3'] DOMAIN = 'tibber' diff --git a/requirements_all.txt b/requirements_all.txt index a7ccfc7fe71..eee0529e082 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -879,7 +879,7 @@ pyRFXtrx==0.23 pySwitchmate==0.4.4 # homeassistant.components.tibber -pyTibber==0.9.2 +pyTibber==0.9.3 # homeassistant.components.switch.dlink pyW215==0.6.0 From 6ca0da5c5229f03ce2bc7256eff84e8ca15a9038 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 21 Jan 2019 09:21:11 -0800 Subject: [PATCH 046/222] Updated frontend to 20190121.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 4d18b2fa841..d7b77fa903f 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==20190120.0'] +REQUIREMENTS = ['home-assistant-frontend==20190121.0'] DOMAIN = 'frontend' DEPENDENCIES = ['api', 'websocket_api', 'http', 'system_log', diff --git a/requirements_all.txt b/requirements_all.txt index eee0529e082..e2a0a70a88c 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -514,7 +514,7 @@ hole==0.3.0 holidays==0.9.9 # homeassistant.components.frontend -home-assistant-frontend==20190120.0 +home-assistant-frontend==20190121.0 # homeassistant.components.zwave homeassistant-pyozw==0.1.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 5733d7e9630..8b24a7b2466 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -107,7 +107,7 @@ hdate==0.8.7 holidays==0.9.9 # homeassistant.components.frontend -home-assistant-frontend==20190120.0 +home-assistant-frontend==20190121.0 # homeassistant.components.homematicip_cloud homematicip==0.10.3 From c36c7080688669a0ee3d49b882d0b2559ad16c19 Mon Sep 17 00:00:00 2001 From: Johann Kellerman Date: Mon, 21 Jan 2019 19:45:11 +0200 Subject: [PATCH 047/222] Align valid_entity_id with new slugify (#20231) * slug * ensure a dot * fix * schema_with_slug_keys * lint * test --- homeassistant/components/script.py | 2 +- homeassistant/config.py | 9 ++++----- homeassistant/core.py | 14 +++++++------- homeassistant/helpers/config_validation.py | 20 ++++++++++++++++++-- tests/components/switch/test_wake_on_lan.py | 4 ++-- 5 files changed, 32 insertions(+), 17 deletions(-) diff --git a/homeassistant/components/script.py b/homeassistant/components/script.py index 54490af3cfa..15df6907468 100644 --- a/homeassistant/components/script.py +++ b/homeassistant/components/script.py @@ -45,7 +45,7 @@ SCRIPT_ENTRY_SCHEMA = vol.Schema({ }) CONFIG_SCHEMA = vol.Schema({ - DOMAIN: vol.Schema({cv.slug: SCRIPT_ENTRY_SCHEMA}) + DOMAIN: cv.schema_with_slug_keys(SCRIPT_ENTRY_SCHEMA) }, extra=vol.ALLOW_EXTRA) SCRIPT_SERVICE_SCHEMA = vol.Schema(dict) diff --git a/homeassistant/config.py b/homeassistant/config.py index 10d3ce21a00..0edadf6a78d 100644 --- a/homeassistant/config.py +++ b/homeassistant/config.py @@ -170,10 +170,9 @@ def _no_duplicate_auth_mfa_module(configs: Sequence[Dict[str, Any]]) \ return configs -PACKAGES_CONFIG_SCHEMA = vol.Schema({ - cv.slug: vol.Schema( # Package names are slugs - {cv.string: vol.Any(dict, list, None)}) # Component configuration -}) +PACKAGES_CONFIG_SCHEMA = cv.schema_with_slug_keys( # Package names are slugs + vol.Schema({cv.string: vol.Any(dict, list, None)}) # Component config +) CUSTOMIZE_DICT_SCHEMA = vol.Schema({ vol.Optional(ATTR_FRIENDLY_NAME): cv.string, @@ -627,7 +626,7 @@ def _identify_config_schema(module: ModuleType) -> \ except (AttributeError, KeyError): return None, None t_schema = str(schema) - if t_schema.startswith('{'): + if t_schema.startswith('{') or 'schema_with_slug_keys' in t_schema: return ('dict', schema) if t_schema.startswith(('[', 'All(.) -ENTITY_ID_PATTERN = re.compile(r"^(\w+)\.(\w+)$") - # How long to wait till things that run on startup have to finish. TIMEOUT_EVENT_START = 15 @@ -77,8 +73,12 @@ def split_entity_id(entity_id: str) -> List[str]: def valid_entity_id(entity_id: str) -> bool: - """Test if an entity ID is a valid format.""" - return ENTITY_ID_PATTERN.match(entity_id) is not None + """Test if an entity ID is a valid format. + + Format: . where both are slugs. + """ + return ('.' in entity_id and + slugify(entity_id) == entity_id.replace('.', '_', 1)) def valid_state(state: str) -> bool: diff --git a/homeassistant/helpers/config_validation.py b/homeassistant/helpers/config_validation.py index 92fe935085a..ef0166bc16d 100644 --- a/homeassistant/helpers/config_validation.py +++ b/homeassistant/helpers/config_validation.py @@ -319,7 +319,23 @@ def service(value): .format(value)) -def slug(value): +def schema_with_slug_keys(value_schema: Union[T, Callable]) -> Callable: + """Ensure dicts have slugs as keys. + + Replacement of vol.Schema({cv.slug: value_schema}) to prevent misleading + "Extra keys" errors from voluptuous. + """ + schema = vol.Schema({str: value_schema}) + + def verify(value: Dict) -> Dict: + """Validate all keys are slugs and then the value_schema.""" + for key in value.keys(): + slug(key) + return schema(value) + return verify + + +def slug(value: Any) -> str: """Validate value is a valid slug.""" if value is None: raise vol.Invalid('Slug should not be None') @@ -330,7 +346,7 @@ def slug(value): raise vol.Invalid('invalid slug {} (try {})'.format(value, slg)) -def slugify(value): +def slugify(value: Any) -> str: """Coerce a value to a slug.""" if value is None: raise vol.Invalid('Slug should not be None') diff --git a/tests/components/switch/test_wake_on_lan.py b/tests/components/switch/test_wake_on_lan.py index c3f4e04057d..312a49b5183 100644 --- a/tests/components/switch/test_wake_on_lan.py +++ b/tests/components/switch/test_wake_on_lan.py @@ -139,11 +139,11 @@ class TestWOLSwitch(unittest.TestCase): 'mac_address': '00-01-02-03-04-05', 'host': 'validhostname', 'turn_off': { - 'service': 'shell_command.turn_off_TARGET', + 'service': 'shell_command.turn_off_target', }, } }) - calls = mock_service(self.hass, 'shell_command', 'turn_off_TARGET') + calls = mock_service(self.hass, 'shell_command', 'turn_off_target') state = self.hass.states.get('switch.wake_on_lan') assert STATE_OFF == state.state From bb21cb6c89740c236a6fedcadb5ab219ae75be15 Mon Sep 17 00:00:00 2001 From: Andrew Sayre <6730289+andrewsayre@users.noreply.github.com> Date: Mon, 21 Jan 2019 13:50:41 -0600 Subject: [PATCH 048/222] Remove trailing slash from base_url and added ability to generate webhook path. (#20295) --- homeassistant/components/http/__init__.py | 1 + homeassistant/components/webhook.py | 14 +++++++++++--- tests/components/http/test_init.py | 16 ++++++++++++++++ tests/components/test_webhook.py | 6 ++++++ 4 files changed, 34 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/http/__init__.py b/homeassistant/components/http/__init__.py index a6b9588fce3..d43ba989f28 100644 --- a/homeassistant/components/http/__init__.py +++ b/homeassistant/components/http/__init__.py @@ -99,6 +99,7 @@ class ApiConfig: self.port = port self.api_password = api_password + host = host.rstrip('/') if host.startswith(("http://", "https://")): self.base_url = host elif use_ssl: diff --git a/homeassistant/components/webhook.py b/homeassistant/components/webhook.py index 6742f33c72d..9ec6d0298ea 100644 --- a/homeassistant/components/webhook.py +++ b/homeassistant/components/webhook.py @@ -19,6 +19,7 @@ DEPENDENCIES = ['http'] _LOGGER = logging.getLogger(__name__) +URL_WEBHOOK_PATH = "/api/webhook/{webhook_id}" WS_TYPE_LIST = 'webhook/list' SCHEMA_WS_LIST = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend({ vol.Required('type'): WS_TYPE_LIST, @@ -58,8 +59,15 @@ def async_generate_id(): @callback @bind_hass def async_generate_url(hass, webhook_id): - """Generate a webhook_id.""" - return "{}/api/webhook/{}".format(hass.config.api.base_url, webhook_id) + """Generate the full URL for a webhook_id.""" + return "{}{}".format(hass.config.api.base_url, + async_generate_path(webhook_id)) + + +@callback +def async_generate_path(webhook_id): + """Generate the path component for a webhook_id.""" + return URL_WEBHOOK_PATH.format(webhook_id=webhook_id) @bind_hass @@ -97,7 +105,7 @@ async def async_setup(hass, config): class WebhookView(HomeAssistantView): """Handle incoming webhook requests.""" - url = "/api/webhook/{webhook_id}" + url = URL_WEBHOOK_PATH name = "api:webhook" requires_auth = False diff --git a/tests/components/http/test_init.py b/tests/components/http/test_init.py index 1c1afe711c6..fadb91a3e03 100644 --- a/tests/components/http/test_init.py +++ b/tests/components/http/test_init.py @@ -80,6 +80,11 @@ class TestApiConfig(unittest.TestCase): api_config = http.ApiConfig('http://example.com', use_ssl=True) assert api_config.base_url == 'http://example.com:8123' + def test_api_base_url_removes_trailing_slash(hass): + """Test a trialing slash is removed when setting the API URL.""" + api_config = http.ApiConfig('http://example.com/') + assert api_config.base_url == 'http://example.com:8123' + async def test_api_base_url_with_domain(hass): """Test setting API URL.""" @@ -124,6 +129,17 @@ async def test_api_no_base_url(hass): assert hass.config.api.base_url == 'http://127.0.0.1:8123' +async def test_api_base_url_removes_trailing_slash(hass): + """Test setting api url.""" + result = await async_setup_component(hass, 'http', { + 'http': { + 'base_url': 'https://example.com/' + } + }) + assert result + assert hass.config.api.base_url == 'https://example.com' + + async def test_not_log_password(hass, aiohttp_client, caplog, legacy_auth): """Test access with password doesn't get logged.""" assert await async_setup_component(hass, 'api', { diff --git a/tests/components/test_webhook.py b/tests/components/test_webhook.py index e67cf7481cc..9c6c9e6a799 100644 --- a/tests/components/test_webhook.py +++ b/tests/components/test_webhook.py @@ -44,6 +44,12 @@ async def test_generate_webhook_url(hass): assert url == 'https://example.com/api/webhook/some_id' +async def test_async_generate_path(hass): + """Test generating just the path component of the url correctly.""" + path = hass.components.webhook.async_generate_path('some_id') + assert path == '/api/webhook/some_id' + + async def test_posting_webhook_nonexisting(hass, mock_client): """Test posting to a nonexisting webhook.""" resp = await mock_client.post('/api/webhook/non-existing') From 4b3cdb9f4e191e4892a613048db48a0c05369e7b Mon Sep 17 00:00:00 2001 From: Ted Drain Date: Mon, 21 Jan 2019 12:05:42 -0800 Subject: [PATCH 049/222] Add radiotherm is_on method to return on/off (#20283) * Added state method to return current operating state to fix #18244 for radiotherm component. * Changed to set the is_on property when actively heating or cooling. --- homeassistant/components/climate/radiotherm.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/homeassistant/components/climate/radiotherm.py b/homeassistant/components/climate/radiotherm.py index f0423d32c96..a72bf711242 100644 --- a/homeassistant/components/climate/radiotherm.py +++ b/homeassistant/components/climate/radiotherm.py @@ -219,6 +219,11 @@ class RadioThermostat(ClimateDevice): """Return true if away mode is on.""" return self._away + @property + def is_on(self): + """Return true if on.""" + return self._tstate != STATE_IDLE + def update(self): """Update and validate the data from the thermostat.""" # Radio thermostats are very slow, and sometimes don't respond From 6511e11ec96bba1b203635851325b2797d44fd5b Mon Sep 17 00:00:00 2001 From: Johann Kellerman Date: Tue, 22 Jan 2019 02:36:04 +0200 Subject: [PATCH 050/222] Config Validator: schema_with_slug_keys (#20298) * schema_with_slug_keys * Update config_validation.py * Update config_validation.py --- homeassistant/components/alert.py | 4 +--- homeassistant/components/axis/__init__.py | 4 +--- homeassistant/components/binary_sensor/template.py | 2 +- homeassistant/components/binary_sensor/trend.py | 2 +- homeassistant/components/counter/__init__.py | 6 +++--- homeassistant/components/cover/command_line.py | 2 +- homeassistant/components/cover/garadget.py | 2 +- homeassistant/components/cover/opengarage.py | 2 +- homeassistant/components/cover/scsgate.py | 3 ++- homeassistant/components/cover/template.py | 2 +- homeassistant/components/cover/velbus.py | 2 +- homeassistant/components/fan/template.py | 2 +- homeassistant/components/history_graph.py | 2 +- homeassistant/components/input_boolean.py | 6 +++--- homeassistant/components/input_datetime.py | 7 ++++--- homeassistant/components/input_number.py | 6 +++--- homeassistant/components/input_select.py | 7 ++++--- homeassistant/components/input_text.py | 6 +++--- homeassistant/components/light/scsgate.py | 3 ++- homeassistant/components/light/template.py | 2 +- homeassistant/components/media_player/universal.py | 4 ++-- homeassistant/components/panel_iframe.py | 8 +++++--- homeassistant/components/proximity.py | 4 +--- homeassistant/components/remote/xiaomi_miio.py | 2 +- homeassistant/components/rest_command.py | 4 +--- homeassistant/components/sensor/lacrosse.py | 2 +- homeassistant/components/sensor/sma.py | 4 ++-- homeassistant/components/sensor/template.py | 2 +- homeassistant/components/shell_command.py | 4 +--- homeassistant/components/switch/broadlink.py | 2 +- homeassistant/components/switch/command_line.py | 2 +- homeassistant/components/switch/kankun.py | 2 +- homeassistant/components/switch/scsgate.py | 3 ++- homeassistant/components/switch/telnet.py | 2 +- homeassistant/components/switch/template.py | 2 +- homeassistant/components/timer/__init__.py | 6 +++--- homeassistant/components/volvooncall.py | 4 ++-- homeassistant/helpers/config_validation.py | 3 +++ 38 files changed, 66 insertions(+), 66 deletions(-) diff --git a/homeassistant/components/alert.py b/homeassistant/components/alert.py index 759a2185047..3a18281e49b 100644 --- a/homeassistant/components/alert.py +++ b/homeassistant/components/alert.py @@ -46,9 +46,7 @@ ALERT_SCHEMA = vol.Schema({ vol.Required(CONF_NOTIFIERS): cv.ensure_list}) CONFIG_SCHEMA = vol.Schema({ - DOMAIN: vol.Schema({ - cv.slug: ALERT_SCHEMA, - }), + DOMAIN: cv.schema_with_slug_keys(ALERT_SCHEMA), }, extra=vol.ALLOW_EXTRA) diff --git a/homeassistant/components/axis/__init__.py b/homeassistant/components/axis/__init__.py index 26fe41724f9..fd2e603445c 100644 --- a/homeassistant/components/axis/__init__.py +++ b/homeassistant/components/axis/__init__.py @@ -50,9 +50,7 @@ DEVICE_SCHEMA = vol.Schema({ }) CONFIG_SCHEMA = vol.Schema({ - DOMAIN: vol.Schema({ - cv.slug: DEVICE_SCHEMA, - }), + DOMAIN: cv.schema_with_slug_keys(DEVICE_SCHEMA), }, extra=vol.ALLOW_EXTRA) SERVICE_VAPIX_CALL = 'vapix_call' diff --git a/homeassistant/components/binary_sensor/template.py b/homeassistant/components/binary_sensor/template.py index d5f8b16e0c1..605ab24a264 100644 --- a/homeassistant/components/binary_sensor/template.py +++ b/homeassistant/components/binary_sensor/template.py @@ -41,7 +41,7 @@ SENSOR_SCHEMA = vol.Schema({ }) PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Required(CONF_SENSORS): vol.Schema({cv.slug: SENSOR_SCHEMA}), + vol.Required(CONF_SENSORS): cv.schema_with_slug_keys(SENSOR_SCHEMA), }) diff --git a/homeassistant/components/binary_sensor/trend.py b/homeassistant/components/binary_sensor/trend.py index 4773e88f5df..8f3ff5d798e 100644 --- a/homeassistant/components/binary_sensor/trend.py +++ b/homeassistant/components/binary_sensor/trend.py @@ -51,7 +51,7 @@ SENSOR_SCHEMA = vol.Schema({ }) PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Required(CONF_SENSORS): vol.Schema({cv.slug: SENSOR_SCHEMA}), + vol.Required(CONF_SENSORS): cv.schema_with_slug_keys(SENSOR_SCHEMA), }) diff --git a/homeassistant/components/counter/__init__.py b/homeassistant/components/counter/__init__.py index cd3a29df2b6..aeef2818f63 100644 --- a/homeassistant/components/counter/__init__.py +++ b/homeassistant/components/counter/__init__.py @@ -37,8 +37,8 @@ SERVICE_SCHEMA = vol.Schema({ }) CONFIG_SCHEMA = vol.Schema({ - DOMAIN: vol.Schema({ - cv.slug: vol.Any({ + DOMAIN: cv.schema_with_slug_keys( + vol.Any({ vol.Optional(CONF_ICON): cv.icon, vol.Optional(CONF_INITIAL, default=DEFAULT_INITIAL): cv.positive_int, @@ -46,7 +46,7 @@ CONFIG_SCHEMA = vol.Schema({ vol.Optional(CONF_RESTORE, default=True): cv.boolean, vol.Optional(CONF_STEP, default=DEFAULT_STEP): cv.positive_int, }, None) - }) + ) }, extra=vol.ALLOW_EXTRA) diff --git a/homeassistant/components/cover/command_line.py b/homeassistant/components/cover/command_line.py index bebf78b1db6..4f4fca1b27a 100644 --- a/homeassistant/components/cover/command_line.py +++ b/homeassistant/components/cover/command_line.py @@ -27,7 +27,7 @@ COVER_SCHEMA = vol.Schema({ }) PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Required(CONF_COVERS): vol.Schema({cv.slug: COVER_SCHEMA}), + vol.Required(CONF_COVERS): cv.schema_with_slug_keys(COVER_SCHEMA), }) diff --git a/homeassistant/components/cover/garadget.py b/homeassistant/components/cover/garadget.py index 03756a971bc..28be3dc6b82 100644 --- a/homeassistant/components/cover/garadget.py +++ b/homeassistant/components/cover/garadget.py @@ -47,7 +47,7 @@ COVER_SCHEMA = vol.Schema({ }) PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Required(CONF_COVERS): vol.Schema({cv.slug: COVER_SCHEMA}), + vol.Required(CONF_COVERS): cv.schema_with_slug_keys(COVER_SCHEMA), }) diff --git a/homeassistant/components/cover/opengarage.py b/homeassistant/components/cover/opengarage.py index 19a87c5bf7c..664d2e291ac 100644 --- a/homeassistant/components/cover/opengarage.py +++ b/homeassistant/components/cover/opengarage.py @@ -46,7 +46,7 @@ COVER_SCHEMA = vol.Schema({ }) PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Required(CONF_COVERS): vol.Schema({cv.slug: COVER_SCHEMA}), + vol.Required(CONF_COVERS): cv.schema_with_slug_keys(COVER_SCHEMA), }) diff --git a/homeassistant/components/cover/scsgate.py b/homeassistant/components/cover/scsgate.py index a6f09c7237d..2d85c1fe3c3 100644 --- a/homeassistant/components/cover/scsgate.py +++ b/homeassistant/components/cover/scsgate.py @@ -18,7 +18,8 @@ _LOGGER = logging.getLogger(__name__) DEPENDENCIES = ['scsgate'] PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Required(CONF_DEVICES): vol.Schema({cv.slug: scsgate.SCSGATE_SCHEMA}), + vol.Required(CONF_DEVICES): + cv.schema_with_slug_keys(scsgate.SCSGATE_SCHEMA), }) diff --git a/homeassistant/components/cover/template.py b/homeassistant/components/cover/template.py index f64e4ae7a3f..1d3642a6036 100644 --- a/homeassistant/components/cover/template.py +++ b/homeassistant/components/cover/template.py @@ -67,7 +67,7 @@ COVER_SCHEMA = vol.Schema({ }) PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Required(CONF_COVERS): vol.Schema({cv.slug: COVER_SCHEMA}), + vol.Required(CONF_COVERS): cv.schema_with_slug_keys(COVER_SCHEMA), }) diff --git a/homeassistant/components/cover/velbus.py b/homeassistant/components/cover/velbus.py index a8501778884..7e5099cecf8 100644 --- a/homeassistant/components/cover/velbus.py +++ b/homeassistant/components/cover/velbus.py @@ -26,7 +26,7 @@ COVER_SCHEMA = vol.Schema({ }) PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Required(CONF_COVERS): vol.Schema({cv.slug: COVER_SCHEMA}), + vol.Required(CONF_COVERS): cv.schema_with_slug_keys(COVER_SCHEMA), }) DEPENDENCIES = ['velbus'] diff --git a/homeassistant/components/fan/template.py b/homeassistant/components/fan/template.py index a2f33d40e48..d9182b79a40 100644 --- a/homeassistant/components/fan/template.py +++ b/homeassistant/components/fan/template.py @@ -62,7 +62,7 @@ FAN_SCHEMA = vol.Schema({ }) PLATFORM_SCHEMA = cv.PLATFORM_SCHEMA.extend({ - vol.Required(CONF_FANS): vol.Schema({cv.slug: FAN_SCHEMA}), + vol.Required(CONF_FANS): cv.schema_with_slug_keys(FAN_SCHEMA), }) diff --git a/homeassistant/components/history_graph.py b/homeassistant/components/history_graph.py index fa7d615dce2..7d9db379705 100644 --- a/homeassistant/components/history_graph.py +++ b/homeassistant/components/history_graph.py @@ -34,7 +34,7 @@ GRAPH_SCHEMA = vol.Schema({ CONFIG_SCHEMA = vol.Schema({ - DOMAIN: vol.Schema({cv.slug: GRAPH_SCHEMA}) + DOMAIN: cv.schema_with_slug_keys(GRAPH_SCHEMA) }, extra=vol.ALLOW_EXTRA) diff --git a/homeassistant/components/input_boolean.py b/homeassistant/components/input_boolean.py index 541e38202fc..896de61130c 100644 --- a/homeassistant/components/input_boolean.py +++ b/homeassistant/components/input_boolean.py @@ -30,13 +30,13 @@ SERVICE_SCHEMA = vol.Schema({ }) CONFIG_SCHEMA = vol.Schema({ - DOMAIN: vol.Schema({ - cv.slug: vol.Any({ + DOMAIN: cv.schema_with_slug_keys( + vol.Any({ vol.Optional(CONF_NAME): cv.string, vol.Optional(CONF_INITIAL): cv.boolean, vol.Optional(CONF_ICON): cv.icon, }, None) - }) + ) }, extra=vol.ALLOW_EXTRA) diff --git a/homeassistant/components/input_datetime.py b/homeassistant/components/input_datetime.py index 6ac9a24d044..63dcc364c9c 100644 --- a/homeassistant/components/input_datetime.py +++ b/homeassistant/components/input_datetime.py @@ -46,14 +46,15 @@ def has_date_or_time(conf): CONFIG_SCHEMA = vol.Schema({ - DOMAIN: vol.Schema({ - cv.slug: vol.All({ + DOMAIN: cv.schema_with_slug_keys( + vol.All({ vol.Optional(CONF_NAME): cv.string, vol.Optional(CONF_HAS_DATE, default=False): cv.boolean, vol.Optional(CONF_HAS_TIME, default=False): cv.boolean, vol.Optional(CONF_ICON): cv.icon, vol.Optional(CONF_INITIAL): cv.string, - }, has_date_or_time)}) + }, has_date_or_time) + ) }, extra=vol.ALLOW_EXTRA) diff --git a/homeassistant/components/input_number.py b/homeassistant/components/input_number.py index b6c6eab3cf5..8cfa7abaf20 100644 --- a/homeassistant/components/input_number.py +++ b/homeassistant/components/input_number.py @@ -63,8 +63,8 @@ def _cv_input_number(cfg): CONFIG_SCHEMA = vol.Schema({ - DOMAIN: vol.Schema({ - cv.slug: vol.All({ + DOMAIN: cv.schema_with_slug_keys( + vol.All({ vol.Optional(CONF_NAME): cv.string, vol.Required(CONF_MIN): vol.Coerce(float), vol.Required(CONF_MAX): vol.Coerce(float), @@ -76,7 +76,7 @@ CONFIG_SCHEMA = vol.Schema({ vol.Optional(CONF_MODE, default=MODE_SLIDER): vol.In([MODE_BOX, MODE_SLIDER]), }, _cv_input_number) - }) + ) }, required=True, extra=vol.ALLOW_EXTRA) diff --git a/homeassistant/components/input_select.py b/homeassistant/components/input_select.py index cc9a73bf915..fc858e75397 100644 --- a/homeassistant/components/input_select.py +++ b/homeassistant/components/input_select.py @@ -64,14 +64,15 @@ def _cv_input_select(cfg): CONFIG_SCHEMA = vol.Schema({ - DOMAIN: vol.Schema({ - cv.slug: vol.All({ + DOMAIN: cv.schema_with_slug_keys( + vol.All({ vol.Optional(CONF_NAME): cv.string, vol.Required(CONF_OPTIONS): vol.All(cv.ensure_list, vol.Length(min=1), [cv.string]), vol.Optional(CONF_INITIAL): cv.string, vol.Optional(CONF_ICON): cv.icon, - }, _cv_input_select)}) + }, _cv_input_select) + ) }, required=True, extra=vol.ALLOW_EXTRA) diff --git a/homeassistant/components/input_text.py b/homeassistant/components/input_text.py index 8ac64b398f4..580337a3af3 100644 --- a/homeassistant/components/input_text.py +++ b/homeassistant/components/input_text.py @@ -55,8 +55,8 @@ def _cv_input_text(cfg): CONFIG_SCHEMA = vol.Schema({ - DOMAIN: vol.Schema({ - cv.slug: vol.All({ + DOMAIN: cv.schema_with_slug_keys( + vol.All({ vol.Optional(CONF_NAME): cv.string, vol.Optional(CONF_MIN, default=0): vol.Coerce(int), vol.Optional(CONF_MAX, default=100): vol.Coerce(int), @@ -67,7 +67,7 @@ CONFIG_SCHEMA = vol.Schema({ vol.Optional(CONF_MODE, default=MODE_TEXT): vol.In([MODE_TEXT, MODE_PASSWORD]), }, _cv_input_text) - }) + ) }, required=True, extra=vol.ALLOW_EXTRA) diff --git a/homeassistant/components/light/scsgate.py b/homeassistant/components/light/scsgate.py index 4a18bc99672..c218e194791 100644 --- a/homeassistant/components/light/scsgate.py +++ b/homeassistant/components/light/scsgate.py @@ -19,7 +19,8 @@ _LOGGER = logging.getLogger(__name__) DEPENDENCIES = ['scsgate'] PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Required(CONF_DEVICES): vol.Schema({cv.slug: scsgate.SCSGATE_SCHEMA}), + vol.Required(CONF_DEVICES): + cv.schema_with_slug_keys(scsgate.SCSGATE_SCHEMA), }) diff --git a/homeassistant/components/light/template.py b/homeassistant/components/light/template.py index 2447dabe3c7..bf930dd1b38 100644 --- a/homeassistant/components/light/template.py +++ b/homeassistant/components/light/template.py @@ -44,7 +44,7 @@ LIGHT_SCHEMA = vol.Schema({ }) PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Required(CONF_LIGHTS): vol.Schema({cv.slug: LIGHT_SCHEMA}), + vol.Required(CONF_LIGHTS): cv.schema_with_slug_keys(LIGHT_SCHEMA), }) diff --git a/homeassistant/components/media_player/universal.py b/homeassistant/components/media_player/universal.py index 47eaf599929..18b953a0372 100644 --- a/homeassistant/components/media_player/universal.py +++ b/homeassistant/components/media_player/universal.py @@ -47,8 +47,8 @@ CONF_SERVICE_DATA = 'service_data' OFF_STATES = [STATE_IDLE, STATE_OFF, STATE_UNAVAILABLE] -ATTRS_SCHEMA = vol.Schema({cv.slug: cv.string}) -CMD_SCHEMA = vol.Schema({cv.slug: cv.SERVICE_SCHEMA}) +ATTRS_SCHEMA = cv.schema_with_slug_keys(cv.string) +CMD_SCHEMA = cv.schema_with_slug_keys(cv.SERVICE_SCHEMA) PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Required(CONF_NAME): cv.string, diff --git a/homeassistant/components/panel_iframe.py b/homeassistant/components/panel_iframe.py index 86594b74995..030fbbf9324 100644 --- a/homeassistant/components/panel_iframe.py +++ b/homeassistant/components/panel_iframe.py @@ -19,8 +19,8 @@ CONF_RELATIVE_URL_ERROR_MSG = "Invalid relative URL. Absolute path required." CONF_RELATIVE_URL_REGEX = r'\A/' CONFIG_SCHEMA = vol.Schema({ - DOMAIN: vol.Schema({ - cv.slug: { + DOMAIN: cv.schema_with_slug_keys( + vol.Schema({ # pylint: disable=no-value-for-parameter vol.Optional(CONF_TITLE): cv.string, vol.Optional(CONF_ICON): cv.icon, @@ -29,7 +29,9 @@ CONFIG_SCHEMA = vol.Schema({ CONF_RELATIVE_URL_REGEX, msg=CONF_RELATIVE_URL_ERROR_MSG), vol.Url()), - }})}, extra=vol.ALLOW_EXTRA) + }) + ) +}, extra=vol.ALLOW_EXTRA) async def async_setup(hass, config): diff --git a/homeassistant/components/proximity.py b/homeassistant/components/proximity.py index 38b37cad51e..e8d86d480e5 100644 --- a/homeassistant/components/proximity.py +++ b/homeassistant/components/proximity.py @@ -49,9 +49,7 @@ ZONE_SCHEMA = vol.Schema({ }) CONFIG_SCHEMA = vol.Schema({ - DOMAIN: vol.Schema({ - cv.slug: ZONE_SCHEMA, - }), + DOMAIN: cv.schema_with_slug_keys(ZONE_SCHEMA), }, extra=vol.ALLOW_EXTRA) diff --git a/homeassistant/components/remote/xiaomi_miio.py b/homeassistant/components/remote/xiaomi_miio.py index a247cb3e914..c8ffd043321 100644 --- a/homeassistant/components/remote/xiaomi_miio.py +++ b/homeassistant/components/remote/xiaomi_miio.py @@ -57,7 +57,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Optional(ATTR_HIDDEN, default=True): cv.boolean, vol.Required(CONF_TOKEN): vol.All(str, vol.Length(min=32, max=32)), vol.Optional(CONF_COMMANDS, default={}): - vol.Schema({cv.slug: COMMAND_SCHEMA}), + cv.schema_with_slug_keys(COMMAND_SCHEMA), }, extra=vol.ALLOW_EXTRA) diff --git a/homeassistant/components/rest_command.py b/homeassistant/components/rest_command.py index ce5873f41d4..c1ccc73b81c 100644 --- a/homeassistant/components/rest_command.py +++ b/homeassistant/components/rest_command.py @@ -49,9 +49,7 @@ COMMAND_SCHEMA = vol.Schema({ }) CONFIG_SCHEMA = vol.Schema({ - DOMAIN: vol.Schema({ - cv.slug: COMMAND_SCHEMA, - }), + DOMAIN: cv.schema_with_slug_keys(COMMAND_SCHEMA), }, extra=vol.ALLOW_EXTRA) diff --git a/homeassistant/components/sensor/lacrosse.py b/homeassistant/components/sensor/lacrosse.py index a2dbaa8f324..32b1dac9250 100644 --- a/homeassistant/components/sensor/lacrosse.py +++ b/homeassistant/components/sensor/lacrosse.py @@ -45,7 +45,7 @@ SENSOR_SCHEMA = vol.Schema({ }) PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Required(CONF_SENSORS): vol.Schema({cv.slug: SENSOR_SCHEMA}), + vol.Required(CONF_SENSORS): cv.schema_with_slug_keys(SENSOR_SCHEMA), vol.Optional(CONF_BAUD, default=DEFAULT_BAUD): cv.string, vol.Optional(CONF_DATARATE): cv.positive_int, vol.Optional(CONF_DEVICE, default=DEFAULT_DEVICE): cv.string, diff --git a/homeassistant/components/sensor/sma.py b/homeassistant/components/sensor/sma.py index 4bfa62bf6dd..61009a472fb 100644 --- a/homeassistant/components/sensor/sma.py +++ b/homeassistant/components/sensor/sma.py @@ -68,9 +68,9 @@ PLATFORM_SCHEMA = vol.All(PLATFORM_SCHEMA.extend({ vol.Required(CONF_PASSWORD): cv.string, vol.Optional(CONF_GROUP, default=GROUPS[0]): vol.In(GROUPS), vol.Optional(CONF_SENSORS, default={}): - vol.Schema({cv.slug: cv.ensure_list}), + cv.schema_with_slug_keys(cv.ensure_list), vol.Optional(CONF_CUSTOM, default={}): - vol.Schema({cv.slug: CUSTOM_SCHEMA}), + cv.schema_with_slug_keys(CUSTOM_SCHEMA), }, extra=vol.PREVENT_EXTRA), _check_sensor_schema) diff --git a/homeassistant/components/sensor/template.py b/homeassistant/components/sensor/template.py index 3fa45935617..5f3af4a06a4 100644 --- a/homeassistant/components/sensor/template.py +++ b/homeassistant/components/sensor/template.py @@ -36,7 +36,7 @@ SENSOR_SCHEMA = vol.Schema({ }) PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Required(CONF_SENSORS): vol.Schema({cv.slug: SENSOR_SCHEMA}), + vol.Required(CONF_SENSORS): cv.schema_with_slug_keys(SENSOR_SCHEMA), }) diff --git a/homeassistant/components/shell_command.py b/homeassistant/components/shell_command.py index 2a95dd5c144..f9ec8da54e3 100644 --- a/homeassistant/components/shell_command.py +++ b/homeassistant/components/shell_command.py @@ -21,9 +21,7 @@ DOMAIN = 'shell_command' _LOGGER = logging.getLogger(__name__) CONFIG_SCHEMA = vol.Schema({ - DOMAIN: vol.Schema({ - cv.slug: cv.string, - }), + DOMAIN: cv.schema_with_slug_keys(cv.string), }, extra=vol.ALLOW_EXTRA) diff --git a/homeassistant/components/switch/broadlink.py b/homeassistant/components/switch/broadlink.py index 685402611a0..9c17767f033 100644 --- a/homeassistant/components/switch/broadlink.py +++ b/homeassistant/components/switch/broadlink.py @@ -59,7 +59,7 @@ MP1_SWITCH_SLOT_SCHEMA = vol.Schema({ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Optional(CONF_SWITCHES, default={}): - vol.Schema({cv.slug: SWITCH_SCHEMA}), + cv.schema_with_slug_keys(SWITCH_SCHEMA), vol.Optional(CONF_SLOTS, default={}): MP1_SWITCH_SLOT_SCHEMA, vol.Required(CONF_HOST): cv.string, vol.Required(CONF_MAC): cv.string, diff --git a/homeassistant/components/switch/command_line.py b/homeassistant/components/switch/command_line.py index d25c5708316..4edbd79ee0c 100644 --- a/homeassistant/components/switch/command_line.py +++ b/homeassistant/components/switch/command_line.py @@ -28,7 +28,7 @@ SWITCH_SCHEMA = vol.Schema({ }) PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Required(CONF_SWITCHES): vol.Schema({cv.slug: SWITCH_SCHEMA}), + vol.Required(CONF_SWITCHES): cv.schema_with_slug_keys(SWITCH_SCHEMA), }) diff --git a/homeassistant/components/switch/kankun.py b/homeassistant/components/switch/kankun.py index 59966739b91..86e7fcdab3e 100644 --- a/homeassistant/components/switch/kankun.py +++ b/homeassistant/components/switch/kankun.py @@ -30,7 +30,7 @@ SWITCH_SCHEMA = vol.Schema({ }) PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Required(CONF_SWITCHES): vol.Schema({cv.slug: SWITCH_SCHEMA}), + vol.Required(CONF_SWITCHES): cv.schema_with_slug_keys(SWITCH_SCHEMA), }) diff --git a/homeassistant/components/switch/scsgate.py b/homeassistant/components/switch/scsgate.py index bb8c067ebd9..9344aeab7ed 100644 --- a/homeassistant/components/switch/scsgate.py +++ b/homeassistant/components/switch/scsgate.py @@ -24,7 +24,8 @@ CONF_SCENARIO = 'scenario' CONF_SCS_ID = 'scs_id' PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Required(CONF_DEVICES): vol.Schema({cv.slug: scsgate.SCSGATE_SCHEMA}), + vol.Required(CONF_DEVICES): + cv.schema_with_slug_keys(scsgate.SCSGATE_SCHEMA), }) diff --git a/homeassistant/components/switch/telnet.py b/homeassistant/components/switch/telnet.py index 440279a70a8..7c3baf2981a 100644 --- a/homeassistant/components/switch/telnet.py +++ b/homeassistant/components/switch/telnet.py @@ -32,7 +32,7 @@ SWITCH_SCHEMA = vol.Schema({ }) PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Required(CONF_SWITCHES): vol.Schema({cv.slug: SWITCH_SCHEMA}), + vol.Required(CONF_SWITCHES): cv.schema_with_slug_keys(SWITCH_SCHEMA), }) SCAN_INTERVAL = timedelta(seconds=10) diff --git a/homeassistant/components/switch/template.py b/homeassistant/components/switch/template.py index 51cea68f6b3..a2098c2f5fd 100644 --- a/homeassistant/components/switch/template.py +++ b/homeassistant/components/switch/template.py @@ -38,7 +38,7 @@ SWITCH_SCHEMA = vol.Schema({ }) PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Required(CONF_SWITCHES): vol.Schema({cv.slug: SWITCH_SCHEMA}), + vol.Required(CONF_SWITCHES): cv.schema_with_slug_keys(SWITCH_SCHEMA), }) diff --git a/homeassistant/components/timer/__init__.py b/homeassistant/components/timer/__init__.py index 364975671c4..b898c577bb2 100644 --- a/homeassistant/components/timer/__init__.py +++ b/homeassistant/components/timer/__init__.py @@ -53,14 +53,14 @@ SERVICE_SCHEMA_DURATION = vol.Schema({ }) CONFIG_SCHEMA = vol.Schema({ - DOMAIN: vol.Schema({ - cv.slug: vol.Any({ + DOMAIN: cv.schema_with_slug_keys( + vol.Any({ vol.Optional(CONF_NAME): cv.string, vol.Optional(CONF_ICON): cv.icon, vol.Optional(CONF_DURATION, timedelta(DEFAULT_DURATION)): cv.time_period, }, None) - }) + ) }, extra=vol.ALLOW_EXTRA) diff --git a/homeassistant/components/volvooncall.py b/homeassistant/components/volvooncall.py index 9f9b58ec8b6..ce4dccbaf75 100644 --- a/homeassistant/components/volvooncall.py +++ b/homeassistant/components/volvooncall.py @@ -93,8 +93,8 @@ CONFIG_SCHEMA = vol.Schema({ vol.Required(CONF_PASSWORD): cv.string, vol.Optional(CONF_UPDATE_INTERVAL, default=DEFAULT_UPDATE_INTERVAL): ( vol.All(cv.time_period, vol.Clamp(min=MIN_UPDATE_INTERVAL))), - vol.Optional(CONF_NAME, default={}): vol.Schema( - {cv.slug: cv.string}), + vol.Optional(CONF_NAME, default={}): + cv.schema_with_slug_keys(cv.string), vol.Optional(CONF_RESOURCES): vol.All( cv.ensure_list, [vol.In(RESOURCES)]), vol.Optional(CONF_REGION): cv.string, diff --git a/homeassistant/helpers/config_validation.py b/homeassistant/helpers/config_validation.py index ef0166bc16d..475135b4cce 100644 --- a/homeassistant/helpers/config_validation.py +++ b/homeassistant/helpers/config_validation.py @@ -329,6 +329,9 @@ def schema_with_slug_keys(value_schema: Union[T, Callable]) -> Callable: def verify(value: Dict) -> Dict: """Validate all keys are slugs and then the value_schema.""" + if not isinstance(value, dict): + raise vol.Invalid('expected dictionary') + for key in value.keys(): slug(key) return schema(value) From 3e056a24dd600cc6fb2997b34da3ba48c8b9fdd8 Mon Sep 17 00:00:00 2001 From: Fabien Piuzzi Date: Tue, 22 Jan 2019 06:21:59 +0100 Subject: [PATCH 051/222] Bugfix: prevent error notification when octoprint server auto detected but no configuration present. (#20303) --- homeassistant/components/octoprint.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/homeassistant/components/octoprint.py b/homeassistant/components/octoprint.py index b626e9a93b5..853ee67db9d 100644 --- a/homeassistant/components/octoprint.py +++ b/homeassistant/components/octoprint.py @@ -92,6 +92,10 @@ def setup(hass, config): discovery.listen(hass, SERVICE_OCTOPRINT, device_discovered) + if DOMAIN not in config: + # Skip the setup if there is no configuration present + return True + for printer in config[DOMAIN]: name = printer[CONF_NAME] ssl = 's' if printer[CONF_SSL] else '' From d4194713728469babb479853a6eba5979d85f144 Mon Sep 17 00:00:00 2001 From: Fabien Piuzzi Date: Tue, 22 Jan 2019 06:22:38 +0100 Subject: [PATCH 052/222] Fix typo C02 to CO2 (#20306) * Fix type C02 to CO2 and added VOC to air quality platform * singularized volatile_organic_compound * Remove VOC prop * Update __init__.py * Update __init__.py --- homeassistant/components/air_quality/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/air_quality/__init__.py b/homeassistant/components/air_quality/__init__.py index 7aed61ee11c..5f770e84b37 100644 --- a/homeassistant/components/air_quality/__init__.py +++ b/homeassistant/components/air_quality/__init__.py @@ -15,7 +15,7 @@ _LOGGER = logging.getLogger(__name__) ATTR_AQI = 'air_quality_index' ATTR_ATTRIBUTION = 'attribution' -ATTR_C02 = 'carbon_dioxide' +ATTR_CO2 = 'carbon_dioxide' ATTR_CO = 'carbon_monoxide' ATTR_N2O = 'nitrogen_oxide' ATTR_NO = 'nitrogen_monoxide' @@ -35,7 +35,7 @@ SCAN_INTERVAL = timedelta(seconds=30) PROP_TO_ATTR = { 'air_quality_index': ATTR_AQI, 'attribution': ATTR_ATTRIBUTION, - 'carbon_dioxide': ATTR_C02, + 'carbon_dioxide': ATTR_CO2, 'carbon_monoxide': ATTR_CO, 'nitrogen_oxide': ATTR_N2O, 'nitrogen_monoxide': ATTR_NO, From 5a30b0507d9a0c3d709f04d4d3cca9c8d508614f Mon Sep 17 00:00:00 2001 From: Johann Kellerman Date: Tue, 22 Jan 2019 09:21:43 +0200 Subject: [PATCH 053/222] Add git to the development Dockerfile (#20276) * git_on_dev * feedback --- virtualization/Docker/Dockerfile.dev | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/virtualization/Docker/Dockerfile.dev b/virtualization/Docker/Dockerfile.dev index 41f447dff12..c01706782a0 100644 --- a/virtualization/Docker/Dockerfile.dev +++ b/virtualization/Docker/Dockerfile.dev @@ -33,6 +33,11 @@ RUN pip3 install --no-cache-dir -r requirements_all.txt && \ # BEGIN: Development additions +# Install git +RUN apt-get update \ + && apt-get install -y --no-install-recommends git \ + && rm -rf /var/lib/apt/lists/* + # Install nodejs RUN curl -sL https://deb.nodesource.com/setup_7.x | bash - && \ apt-get install -y nodejs From a8ef7a2774cc697f740185dfc439e2cf599d1004 Mon Sep 17 00:00:00 2001 From: krygal Date: Tue, 22 Jan 2019 08:16:35 +0000 Subject: [PATCH 054/222] Add device tracker support for EE Brightbox 2 router (#19611) * Added device tracker support for EE Brightbox 2 * removed timeago dependency * get scanner checks and improved tests * fixed lint issues * removed redundant timeago from test requirements * fixed variable naming in test * removed unecessary blank line --- .../components/device_tracker/ee_brightbox.py | 107 +++++++++++++++ requirements_all.txt | 3 + requirements_test_all.txt | 3 + script/gen_requirements_all.py | 1 + .../device_tracker/test_ee_brightbox.py | 122 ++++++++++++++++++ 5 files changed, 236 insertions(+) create mode 100644 homeassistant/components/device_tracker/ee_brightbox.py create mode 100644 tests/components/device_tracker/test_ee_brightbox.py diff --git a/homeassistant/components/device_tracker/ee_brightbox.py b/homeassistant/components/device_tracker/ee_brightbox.py new file mode 100644 index 00000000000..fc23abda1db --- /dev/null +++ b/homeassistant/components/device_tracker/ee_brightbox.py @@ -0,0 +1,107 @@ +""" +Support for EE Brightbox router. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/device_tracker.ee_brightbox/ +""" +import logging + +import voluptuous as vol + +from homeassistant.components.device_tracker import ( + DOMAIN, PLATFORM_SCHEMA, DeviceScanner) +from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME +import homeassistant.helpers.config_validation as cv + +REQUIREMENTS = ['eebrightbox==0.0.4'] + +_LOGGER = logging.getLogger(__name__) + +CONF_VERSION = 'version' + +CONF_DEFAULT_IP = '192.168.1.1' +CONF_DEFAULT_USERNAME = 'admin' +CONF_DEFAULT_VERSION = 2 + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Required(CONF_VERSION, default=CONF_DEFAULT_VERSION): cv.positive_int, + vol.Required(CONF_HOST, default=CONF_DEFAULT_IP): cv.string, + vol.Required(CONF_USERNAME, default=CONF_DEFAULT_USERNAME): cv.string, + vol.Required(CONF_PASSWORD): cv.string, +}) + + +def get_scanner(hass, config): + """Return a router scanner instance.""" + scanner = EEBrightBoxScanner(config[DOMAIN]) + + return scanner if scanner.check_config() else None + + +class EEBrightBoxScanner(DeviceScanner): + """Scan EE Brightbox router.""" + + def __init__(self, config): + """Initialise the scanner.""" + self.config = config + self.devices = {} + + def check_config(self): + """Check if provided configuration and credentials are correct.""" + from eebrightbox import EEBrightBox, EEBrightBoxException + + try: + with EEBrightBox(self.config) as ee_brightbox: + return bool(ee_brightbox.get_devices()) + except EEBrightBoxException: + _LOGGER.exception("Failed to connect to the router") + return False + + def scan_devices(self): + """Scan for devices.""" + from eebrightbox import EEBrightBox + + with EEBrightBox(self.config) as ee_brightbox: + self.devices = {d['mac']: d for d in ee_brightbox.get_devices()} + + macs = [d['mac'] for d in self.devices.values() if d['activity_ip']] + + _LOGGER.debug('Scan devices %s', macs) + + return macs + + def get_device_name(self, device): + """Get the name of a device from hostname.""" + if device in self.devices: + return self.devices[device]['hostname'] or None + + return None + + def get_extra_attributes(self, device): + """ + Get the extra attributes of a device. + + Extra attributes include: + - ip + - mac + - port - ethX or wifiX + - last_active + """ + port_map = { + 'wl1': 'wifi5Ghz', + 'wl0': 'wifi2.4Ghz', + 'eth0': 'eth0', + 'eth1': 'eth1', + 'eth2': 'eth2', + 'eth3': 'eth3', + } + + if device in self.devices: + return { + 'ip': self.devices[device]['ip'], + 'mac': self.devices[device]['mac'], + 'port': port_map[self.devices[device]['port']], + 'last_active': self.devices[device]['time_last_active'], + } + + return {} diff --git a/requirements_all.txt b/requirements_all.txt index e2a0a70a88c..a693defc6ee 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -342,6 +342,9 @@ dweepy==0.3.0 # homeassistant.components.edp_redy edp_redy==0.0.3 +# homeassistant.components.device_tracker.ee_brightbox +eebrightbox==0.0.4 + # homeassistant.components.media_player.horizon einder==0.3.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 8b24a7b2466..082cd3a195f 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -61,6 +61,9 @@ defusedxml==0.5.0 # homeassistant.components.sensor.dsmr dsmr_parser==0.12 +# homeassistant.components.device_tracker.ee_brightbox +eebrightbox==0.0.4 + # homeassistant.components.emulated_roku emulated_roku==0.1.7 diff --git a/script/gen_requirements_all.py b/script/gen_requirements_all.py index e351c7b022b..67702635d47 100755 --- a/script/gen_requirements_all.py +++ b/script/gen_requirements_all.py @@ -46,6 +46,7 @@ TEST_REQUIREMENTS = ( 'coinmarketcap', 'defusedxml', 'dsmr_parser', + 'eebrightbox', 'emulated_roku', 'enturclient', 'ephem', diff --git a/tests/components/device_tracker/test_ee_brightbox.py b/tests/components/device_tracker/test_ee_brightbox.py new file mode 100644 index 00000000000..75609571e6c --- /dev/null +++ b/tests/components/device_tracker/test_ee_brightbox.py @@ -0,0 +1,122 @@ +"""Tests for the EE BrightBox device scanner.""" +from datetime import datetime + +from asynctest import patch +import pytest + +from homeassistant.components.device_tracker import DOMAIN +from homeassistant.const import ( + CONF_PASSWORD, CONF_PLATFORM) +from homeassistant.setup import async_setup_component + + +def _configure_mock_get_devices(eebrightbox_mock): + eebrightbox_instance = eebrightbox_mock.return_value + eebrightbox_instance.__enter__.return_value = eebrightbox_instance + eebrightbox_instance.get_devices.return_value = [ + { + 'mac': 'AA:BB:CC:DD:EE:FF', + 'ip': '192.168.1.10', + 'hostname': 'hostnameAA', + 'activity_ip': True, + 'port': 'eth0', + 'time_last_active': datetime(2019, 1, 20, 16, 4, 0), + }, + { + 'mac': '11:22:33:44:55:66', + 'hostname': 'hostname11', + 'ip': '192.168.1.11', + 'activity_ip': True, + 'port': 'wl0', + 'time_last_active': datetime(2019, 1, 20, 11, 9, 0), + }, + { + 'mac': 'FF:FF:FF:FF:FF:FF', + 'hostname': 'hostnameFF', + 'ip': '192.168.1.12', + 'activity_ip': False, + 'port': 'wl1', + 'time_last_active': datetime(2019, 1, 15, 16, 9, 0), + } + ] + + +def _configure_mock_failed_config_check(eebrightbox_mock): + from eebrightbox import EEBrightBoxException + eebrightbox_instance = eebrightbox_mock.return_value + eebrightbox_instance.__enter__.side_effect = EEBrightBoxException( + "Failed to connect to the router") + + +@pytest.fixture(autouse=True) +def mock_dev_track(mock_device_tracker_conf): + """Mock device tracker config loading.""" + pass + + +@patch('eebrightbox.EEBrightBox') +async def test_missing_credentials(eebrightbox_mock, hass): + """Test missing credentials.""" + _configure_mock_get_devices(eebrightbox_mock) + + result = await async_setup_component(hass, DOMAIN, { + DOMAIN: { + CONF_PLATFORM: 'ee_brightbox', + } + }) + + assert result + + await hass.async_block_till_done() + + assert hass.states.get('device_tracker.hostnameaa') is None + assert hass.states.get('device_tracker.hostname11') is None + assert hass.states.get('device_tracker.hostnameff') is None + + +@patch('eebrightbox.EEBrightBox') +async def test_invalid_credentials(eebrightbox_mock, hass): + """Test invalid credentials.""" + _configure_mock_failed_config_check(eebrightbox_mock) + + result = await async_setup_component(hass, DOMAIN, { + DOMAIN: { + CONF_PLATFORM: 'ee_brightbox', + CONF_PASSWORD: 'test_password', + } + }) + + assert result + + await hass.async_block_till_done() + + assert hass.states.get('device_tracker.hostnameaa') is None + assert hass.states.get('device_tracker.hostname11') is None + assert hass.states.get('device_tracker.hostnameff') is None + + +@patch('eebrightbox.EEBrightBox') +async def test_get_devices(eebrightbox_mock, hass): + """Test valid configuration.""" + _configure_mock_get_devices(eebrightbox_mock) + + result = await async_setup_component(hass, DOMAIN, { + DOMAIN: { + CONF_PLATFORM: 'ee_brightbox', + CONF_PASSWORD: 'test_password', + } + }) + + assert result + + await hass.async_block_till_done() + + assert hass.states.get('device_tracker.hostnameaa') is not None + assert hass.states.get('device_tracker.hostname11') is not None + assert hass.states.get('device_tracker.hostnameff') is None + + state = hass.states.get('device_tracker.hostnameaa') + assert state.attributes['mac'] == 'AA:BB:CC:DD:EE:FF' + assert state.attributes['ip'] == '192.168.1.10' + assert state.attributes['port'] == 'eth0' + assert state.attributes['last_active'] == datetime(2019, 1, 20, 16, 4, 0) From 89ba374d51514bf5a705200104cefa7218957282 Mon Sep 17 00:00:00 2001 From: Mattias Welponer Date: Tue, 22 Jan 2019 09:22:46 +0100 Subject: [PATCH 055/222] HomematicIP add cover FROLL and BROLL devices (#19794) * Add cover FROLL and BROLL devices * Fix import * Fix async calls * Update cover functions and async fixes * Update test * Cleanup code * Update header * Merge imports * Update * Remove init * Update coveragerc file * Update coveragerc --- .coveragerc | 3 +- .../components/cover/homematicip_cloud.py | 70 +++++++++++++++++++ .../components/homematicip_cloud/const.py | 1 + .../components/homematicip_cloud/test_hap.py | 6 +- 4 files changed, 76 insertions(+), 4 deletions(-) create mode 100644 homeassistant/components/cover/homematicip_cloud.py diff --git a/.coveragerc b/.coveragerc index 93c9b28e103..237cbf8f8b7 100644 --- a/.coveragerc +++ b/.coveragerc @@ -169,7 +169,8 @@ omit = homeassistant/components/homematic/__init__.py homeassistant/components/*/homematic.py - homeassistant/components/homematicip_cloud.py + homeassistant/components/homematicip_cloud/hap.py + homeassistant/components/homematicip_cloud/device.py homeassistant/components/*/homematicip_cloud.py homeassistant/components/homeworks.py diff --git a/homeassistant/components/cover/homematicip_cloud.py b/homeassistant/components/cover/homematicip_cloud.py new file mode 100644 index 00000000000..27f26805e81 --- /dev/null +++ b/homeassistant/components/cover/homematicip_cloud.py @@ -0,0 +1,70 @@ +""" +Support for HomematicIP Cloud cover devices. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/cover.homematicip_cloud/ +""" +import logging + +from homeassistant.components.cover import ( + ATTR_POSITION, CoverDevice) +from homeassistant.components.homematicip_cloud import ( + HMIPC_HAPID, HomematicipGenericDevice, DOMAIN as HMIPC_DOMAIN) + +DEPENDENCIES = ['homematicip_cloud'] + +_LOGGER = logging.getLogger(__name__) + + +async def async_setup_platform( + hass, config, async_add_entities, discovery_info=None): + """Set up the HomematicIP Cloud cover devices.""" + pass + + +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up the HomematicIP cover from a config entry.""" + from homematicip.aio.device import AsyncFullFlushShutter + + home = hass.data[HMIPC_DOMAIN][config_entry.data[HMIPC_HAPID]].home + devices = [] + for device in home.devices: + if isinstance(device, AsyncFullFlushShutter): + devices.append(HomematicipCoverShutter(home, device)) + + if devices: + async_add_entities(devices) + + +class HomematicipCoverShutter(HomematicipGenericDevice, CoverDevice): + """Representation of a HomematicIP Cloud cover device.""" + + @property + def current_cover_position(self): + """Return current position of cover.""" + return int(self._device.shutterLevel * 100) + + async def async_set_cover_position(self, **kwargs): + """Move the cover to a specific position.""" + position = kwargs[ATTR_POSITION] + level = position / 100.0 + await self._device.set_shutter_level(level) + + @property + def is_closed(self): + """Return if the cover is closed.""" + if self._device.shutterLevel is not None: + return self._device.shutterLevel == 0 + return None + + async def async_open_cover(self, **kwargs): + """Open the cover.""" + await self._device.set_shutter_level(1) + + async def async_close_cover(self, **kwargs): + """Close the cover.""" + await self._device.set_shutter_level(0) + + async def async_stop_cover(self, **kwargs): + """Stop the device if in motion.""" + await self._device.set_shutter_stop() diff --git a/homeassistant/components/homematicip_cloud/const.py b/homeassistant/components/homematicip_cloud/const.py index ba9c37b83d7..06864d50ad1 100644 --- a/homeassistant/components/homematicip_cloud/const.py +++ b/homeassistant/components/homematicip_cloud/const.py @@ -9,6 +9,7 @@ COMPONENTS = [ 'alarm_control_panel', 'binary_sensor', 'climate', + 'cover', 'light', 'sensor', 'switch', diff --git a/tests/components/homematicip_cloud/test_hap.py b/tests/components/homematicip_cloud/test_hap.py index 92f58b37662..521920b9281 100644 --- a/tests/components/homematicip_cloud/test_hap.py +++ b/tests/components/homematicip_cloud/test_hap.py @@ -65,7 +65,7 @@ async def test_hap_setup_works(aioclient_mock): assert await hap.async_setup() is True assert hap.home is home - assert len(hass.config_entries.async_forward_entry_setup.mock_calls) == 6 + assert len(hass.config_entries.async_forward_entry_setup.mock_calls) == 7 assert hass.config_entries.async_forward_entry_setup.mock_calls[0][1] == \ (entry, 'alarm_control_panel') assert hass.config_entries.async_forward_entry_setup.mock_calls[1][1] == \ @@ -107,10 +107,10 @@ async def test_hap_reset_unloads_entry_if_setup(): assert hap.home is home assert len(hass.services.async_register.mock_calls) == 0 - assert len(hass.config_entries.async_forward_entry_setup.mock_calls) == 6 + assert len(hass.config_entries.async_forward_entry_setup.mock_calls) == 7 hass.config_entries.async_forward_entry_unload.return_value = \ mock_coro(True) await hap.async_reset() - assert len(hass.config_entries.async_forward_entry_unload.mock_calls) == 6 + assert len(hass.config_entries.async_forward_entry_unload.mock_calls) == 7 From f84c0ee473025cf5000d8a0063a6296b860e179b Mon Sep 17 00:00:00 2001 From: Jason Hu Date: Tue, 22 Jan 2019 04:23:33 -0800 Subject: [PATCH 056/222] Upgrade python-nest to 4.1.0 (#20313) --- homeassistant/components/nest/__init__.py | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/nest/__init__.py b/homeassistant/components/nest/__init__.py index 5bbd36f4b9d..7f0fe27df73 100644 --- a/homeassistant/components/nest/__init__.py +++ b/homeassistant/components/nest/__init__.py @@ -27,7 +27,7 @@ from homeassistant.helpers.entity import Entity from .const import DOMAIN from . import local_auth -REQUIREMENTS = ['python-nest==4.0.5'] +REQUIREMENTS = ['python-nest==4.1.0'] _CONFIGURING = {} _LOGGER = logging.getLogger(__name__) diff --git a/requirements_all.txt b/requirements_all.txt index a693defc6ee..22ff8887370 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1297,7 +1297,7 @@ python-mpd2==1.0.0 python-mystrom==0.4.4 # homeassistant.components.nest -python-nest==4.0.5 +python-nest==4.1.0 # homeassistant.components.device_tracker.nmap_tracker python-nmap==0.6.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 082cd3a195f..1f07c530659 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -215,7 +215,7 @@ pyspcwebgw==0.4.0 python-forecastio==1.4.0 # homeassistant.components.nest -python-nest==4.0.5 +python-nest==4.1.0 # homeassistant.components.sensor.awair python_awair==0.0.3 From 9b7780edf0be9ff5a86c1aad71ac22a90b48c523 Mon Sep 17 00:00:00 2001 From: Mateusz Korniak Date: Tue, 22 Jan 2019 14:14:27 +0100 Subject: [PATCH 057/222] Ecoal (esterownik.pl) static fuel boiler and pump controller (#18480) - matkor * Starting work on ecoal boiler controller iface. * Sending some values/states to controller. * Basic status parsing, and simple settings. * Platform configuration. * Temp sensors seems be working. * Switch from separate h/m/s to datetime. * Vocabulary updates. * secondary_central_heating_pump -> central_heating_pump2 * Pumps as switches. * Optional enabling pumps via config. * requests==2.20.1 added to REQUIREMENTS. * Optional enabling temp sensors from configuration yaml. * autopep8, black, pylint. * flake8. * pydocstyle * All style checkers again. * requests==2.20.1 required by homeassistant.components.sensor.ecoal_boiler. * Verify / set switches in update(). Code cleanup. * script/lint + travis issues. * Cleanup, imperative mood. * pylint, travis. * Updated .coveragerc. * Using configuration consts from homeassistant.const * typo. * Replace global ECOAL_CONTR with hass.data[DATA_ECOAL_BOILER]. Remove requests from REQUIREMENTS. * Killed .update()/reread_update() in Entities __init__()s. Removed debug/comments. * Removed debug/comments. * script/lint fixes. * script/gen_requirements_all.py run. * Travis fixes. * Configuration now validated. * Split controller code to separate package. * Replace in module docs with link to https://home-assistant.io . * Correct component module path in .coveragerc. More vals from const.py. Use dict[key] for required config keys. Check if credentials are correct during component setup. Renamed add_devices to add_entities. * Sensor/switch depends on ecoal_boiler component. EcoalSwitch inherits from SwitchDevice. Killed same as default should_poll(). Remove not neede schedule_update_ha_state() calls from turn_on/off. * lint fixes. * Move sensors/switches configuration to component setup. * Lint fixes. * Invalidating ecoal iface cache instead of force read in turn_on/off(). * Fail component setup before adding any platform entities. Kill NOTE. * Disallow setting entity names from config file, use code defined default names. * Rework configuration file to use monitored_conditions like in rainmachine component. * Killed pylint exception. Log error when connection to controller fails. * A few fixes. * Linted. --- .coveragerc | 3 + homeassistant/components/ecoal_boiler.py | 98 +++++++++++++++++++ .../components/sensor/ecoal_boiler.py | 63 ++++++++++++ .../components/switch/ecoal_boiler.py | 85 ++++++++++++++++ requirements_all.txt | 3 + 5 files changed, 252 insertions(+) create mode 100644 homeassistant/components/ecoal_boiler.py create mode 100644 homeassistant/components/sensor/ecoal_boiler.py create mode 100644 homeassistant/components/switch/ecoal_boiler.py diff --git a/.coveragerc b/.coveragerc index 237cbf8f8b7..e9627277905 100644 --- a/.coveragerc +++ b/.coveragerc @@ -91,6 +91,9 @@ omit = homeassistant/components/eight_sleep.py homeassistant/components/*/eight_sleep.py + homeassistant/components/ecoal_boiler.py + homeassistant/components/*/ecoal_boiler.py + homeassistant/components/ecobee.py homeassistant/components/*/ecobee.py diff --git a/homeassistant/components/ecoal_boiler.py b/homeassistant/components/ecoal_boiler.py new file mode 100644 index 00000000000..bd08024e64a --- /dev/null +++ b/homeassistant/components/ecoal_boiler.py @@ -0,0 +1,98 @@ +""" +Component to control ecoal/esterownik.pl coal/wood boiler controller. + +For more details about this component, please refer to the documentation at +https://home-assistant.io/components/ecoal_boiler/ +""" +import logging + +import voluptuous as vol + +from homeassistant.const import (CONF_HOST, CONF_PASSWORD, CONF_USERNAME, + CONF_MONITORED_CONDITIONS, CONF_SENSORS, + CONF_SWITCHES) +import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.discovery import load_platform + +REQUIREMENTS = ['ecoaliface==0.4.0'] + +_LOGGER = logging.getLogger(__name__) + +DOMAIN = "ecoal_boiler" +DATA_ECOAL_BOILER = 'data_' + DOMAIN + +DEFAULT_USERNAME = "admin" +DEFAULT_PASSWORD = "admin" + + +# Available pump ids with assigned HA names +# Available as switches +AVAILABLE_PUMPS = { + "central_heating_pump": "Central heating pump", + "central_heating_pump2": "Central heating pump2", + "domestic_hot_water_pump": "Domestic hot water pump", +} + +# Available temp sensor ids with assigned HA names +# Available as sensors +AVAILABLE_SENSORS = { + "outdoor_temp": 'Outdoor temperature', + "indoor_temp": 'Indoor temperature', + "indoor2_temp": 'Indoor temperature 2', + "domestic_hot_water_temp": 'Domestic hot water temperature', + "target_domestic_hot_water_temp": 'Target hot water temperature', + "feedwater_in_temp": 'Feedwater input temperature', + "feedwater_out_temp": 'Feedwater output temperature', + "target_feedwater_temp": 'Target feedwater temperature', + "fuel_feeder_temp": 'Fuel feeder temperature', + "exhaust_temp": 'Exhaust temperature', +} + +SWITCH_SCHEMA = vol.Schema({ + vol.Optional(CONF_MONITORED_CONDITIONS, default=list(AVAILABLE_PUMPS)): + vol.All(cv.ensure_list, [vol.In(AVAILABLE_PUMPS)]) +}) + +SENSOR_SCHEMA = vol.Schema({ + vol.Optional(CONF_MONITORED_CONDITIONS, default=list(AVAILABLE_SENSORS)): + vol.All(cv.ensure_list, [vol.In(AVAILABLE_SENSORS)]) +}) + +CONFIG_SCHEMA = vol.Schema({ + DOMAIN: vol.Schema({ + vol.Required(CONF_HOST): cv.string, + vol.Optional(CONF_USERNAME, + default=DEFAULT_USERNAME): cv.string, + vol.Optional(CONF_PASSWORD, + default=DEFAULT_PASSWORD): cv.string, + vol.Optional(CONF_SWITCHES, default={}): SWITCH_SCHEMA, + vol.Optional(CONF_SENSORS, default={}): SENSOR_SCHEMA, + }) +}, extra=vol.ALLOW_EXTRA) + + +def setup(hass, hass_config): + """Set up global ECoalController instance same for sensors and switches.""" + from ecoaliface.simple import ECoalController + + conf = hass_config[DOMAIN] + host = conf[CONF_HOST] + username = conf[CONF_USERNAME] + passwd = conf[CONF_PASSWORD] + # Creating ECoalController instance makes HTTP request to controller. + ecoal_contr = ECoalController(host, username, passwd) + if ecoal_contr.version is None: + # Wrong credentials nor network config + _LOGGER.error("Unable to read controller status from %s@%s" + " (wrong host/credentials)", username, host, ) + return False + _LOGGER.debug("Detected controller version: %r @%s", + ecoal_contr.version, host, ) + hass.data[DATA_ECOAL_BOILER] = ecoal_contr + # Setup switches + switches = conf[CONF_SWITCHES][CONF_MONITORED_CONDITIONS] + load_platform(hass, 'switch', DOMAIN, switches, hass_config) + # Setup temp sensors + sensors = conf[CONF_SENSORS][CONF_MONITORED_CONDITIONS] + load_platform(hass, 'sensor', DOMAIN, sensors, hass_config) + return True diff --git a/homeassistant/components/sensor/ecoal_boiler.py b/homeassistant/components/sensor/ecoal_boiler.py new file mode 100644 index 00000000000..de81d16470c --- /dev/null +++ b/homeassistant/components/sensor/ecoal_boiler.py @@ -0,0 +1,63 @@ +""" +Allows reading temperatures from ecoal/esterownik.pl controller. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/sensor.ecoal_boiler/ +""" +import logging + +from homeassistant.components.ecoal_boiler import ( + DATA_ECOAL_BOILER, AVAILABLE_SENSORS, ) +from homeassistant.const import TEMP_CELSIUS +from homeassistant.helpers.entity import Entity + +_LOGGER = logging.getLogger(__name__) + +DEPENDENCIES = ['ecoal_boiler'] + + +def setup_platform(hass, config, add_entities, discovery_info=None): + """Set up the ecoal sensors.""" + if discovery_info is None: + return + devices = [] + ecoal_contr = hass.data[DATA_ECOAL_BOILER] + for sensor_id in discovery_info: + name = AVAILABLE_SENSORS[sensor_id] + devices.append(EcoalTempSensor(ecoal_contr, name, sensor_id)) + add_entities(devices, True) + + +class EcoalTempSensor(Entity): + """Representation of a temperature sensor using ecoal status data.""" + + def __init__(self, ecoal_contr, name, status_attr): + """Initialize the sensor.""" + self._ecoal_contr = ecoal_contr + self._name = name + self._status_attr = status_attr + self._state = None + + @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.""" + return TEMP_CELSIUS + + def update(self): + """Fetch new state data for the sensor. + + This is the only method that should fetch new data for Home Assistant. + """ + # Old values read 0.5 back can still be used + status = self._ecoal_contr.get_cached_status() + self._state = getattr(status, self._status_attr) diff --git a/homeassistant/components/switch/ecoal_boiler.py b/homeassistant/components/switch/ecoal_boiler.py new file mode 100644 index 00000000000..d8d6c98bb8b --- /dev/null +++ b/homeassistant/components/switch/ecoal_boiler.py @@ -0,0 +1,85 @@ +""" +Allows to configuration ecoal (esterownik.pl) pumps as switches. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/switch.ecoal_boiler/ +""" +import logging +from typing import Optional + +from homeassistant.components.switch import SwitchDevice +from homeassistant.components.ecoal_boiler import ( + DATA_ECOAL_BOILER, AVAILABLE_PUMPS, ) + +_LOGGER = logging.getLogger(__name__) + +DEPENDENCIES = ['ecoal_boiler'] + + +def setup_platform(hass, config, add_entities, discovery_info=None): + """Set up switches based on ecoal interface.""" + if discovery_info is None: + return + ecoal_contr = hass.data[DATA_ECOAL_BOILER] + switches = [] + for pump_id in discovery_info: + name = AVAILABLE_PUMPS[pump_id] + switches.append(EcoalSwitch(ecoal_contr, name, pump_id)) + add_entities(switches, True) + + +class EcoalSwitch(SwitchDevice): + """Representation of Ecoal switch.""" + + def __init__(self, ecoal_contr, name, state_attr): + """ + Initialize switch. + + Sets HA switch to state as read from controller. + """ + self._ecoal_contr = ecoal_contr + self._name = name + self._state_attr = state_attr + # Ecoalcotroller holds convention that same postfix is used + # to set attribute + # set_() + # as attribute name in status instance: + # status. + self._contr_set_fun = getattr(self._ecoal_contr, "set_" + state_attr) + # No value set, will be read from controller instead + self._state = None + + @property + def name(self) -> Optional[str]: + """Return the name of the switch.""" + return self._name + + def update(self): + """Fetch new state data for the sensor. + + This is the only method that should fetch new data for Home Assistant. + """ + status = self._ecoal_contr.get_cached_status() + self._state = getattr(status, self._state_attr) + + def invalidate_ecoal_cache(self): + """Invalidate ecoal interface cache. + + Forces that next read from ecaol interface to not use cache. + """ + self._ecoal_contr.status = None + + @property + def is_on(self) -> bool: + """Return true if device is on.""" + return self._state + + def turn_on(self, **kwargs) -> None: + """Turn the device on.""" + self._contr_set_fun(1) + self.invalidate_ecoal_cache() + + def turn_off(self, **kwargs) -> None: + """Turn the device off.""" + self._contr_set_fun(0) + self.invalidate_ecoal_cache() diff --git a/requirements_all.txt b/requirements_all.txt index 22ff8887370..c44c442334f 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -339,6 +339,9 @@ dsmr_parser==0.12 # homeassistant.components.sensor.dweet dweepy==0.3.0 +# homeassistant.components.ecoal_boiler +ecoaliface==0.4.0 + # homeassistant.components.edp_redy edp_redy==0.0.3 From e2a4fdeadff7a403434f284e2abd3e5c66deaba6 Mon Sep 17 00:00:00 2001 From: rolfberkenbosch <30292281+rolfberkenbosch@users.noreply.github.com> Date: Tue, 22 Jan 2019 17:24:40 +0100 Subject: [PATCH 058/222] Update locationsharinglib to version 3.0.11 (#20322) * Update google_maps.py There are known bug in locationsharinglib version 3.0.9, the developer has fixed this in 3.0.11. (https://github.com/costastf/locationsharinglib/issues/42) * Update requirements_all.txt --- 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 1f95414541c..c324f3c2757 100644 --- a/homeassistant/components/device_tracker/google_maps.py +++ b/homeassistant/components/device_tracker/google_maps.py @@ -19,7 +19,7 @@ from homeassistant.helpers.event import track_time_interval from homeassistant.helpers.typing import ConfigType from homeassistant.util import slugify, dt as dt_util -REQUIREMENTS = ['locationsharinglib==3.0.9'] +REQUIREMENTS = ['locationsharinglib==3.0.11'] _LOGGER = logging.getLogger(__name__) diff --git a/requirements_all.txt b/requirements_all.txt index c44c442334f..ceb0a6b522b 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -631,7 +631,7 @@ liveboxplaytv==2.0.2 lmnotify==0.0.4 # homeassistant.components.device_tracker.google_maps -locationsharinglib==3.0.9 +locationsharinglib==3.0.11 # homeassistant.components.logi_circle logi_circle==0.1.7 From 91ef78adc5ec16a581b3219c53cce673a360cdec Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Tue, 22 Jan 2019 18:45:16 +0100 Subject: [PATCH 059/222] Upgrade youtube_dl to 2019.01.17 (#20318) --- 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 940f2dd79ca..973fae9a0de 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==2019.01.10'] +REQUIREMENTS = ['youtube_dl==2019.01.17'] _LOGGER = logging.getLogger(__name__) diff --git a/requirements_all.txt b/requirements_all.txt index ceb0a6b522b..96c13d41e59 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1739,7 +1739,7 @@ yeelight==0.4.3 yeelightsunflower==0.0.10 # homeassistant.components.media_extractor -youtube_dl==2019.01.10 +youtube_dl==2019.01.17 # homeassistant.components.light.zengge zengge==0.2 From 5b2518847443cddcda48d6ca8fdb5376b39f2f92 Mon Sep 17 00:00:00 2001 From: Richard Mitchell Date: Tue, 22 Jan 2019 17:50:21 +0000 Subject: [PATCH 060/222] Should require the 'GATTOOL' setup extras which includes pexpect. (#20263) * Should require the 'GATTOOL' setup extras which includes pexpect. * Also fix skybeacon's requirement and requirements_all. --- homeassistant/components/device_tracker/bluetooth_le_tracker.py | 2 +- homeassistant/components/sensor/skybeacon.py | 2 +- requirements_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/device_tracker/bluetooth_le_tracker.py b/homeassistant/components/device_tracker/bluetooth_le_tracker.py index a07fdfdcf81..825ef04ccc5 100644 --- a/homeassistant/components/device_tracker/bluetooth_le_tracker.py +++ b/homeassistant/components/device_tracker/bluetooth_le_tracker.py @@ -15,7 +15,7 @@ import homeassistant.util.dt as dt_util _LOGGER = logging.getLogger(__name__) -REQUIREMENTS = ['pygatt==3.2.0'] +REQUIREMENTS = ['pygatt[GATTTOOL]==3.2.0'] BLE_PREFIX = 'BLE_' MIN_SEEN_NEW = 5 diff --git a/homeassistant/components/sensor/skybeacon.py b/homeassistant/components/sensor/skybeacon.py index 441053a7e7e..6960999306d 100644 --- a/homeassistant/components/sensor/skybeacon.py +++ b/homeassistant/components/sensor/skybeacon.py @@ -16,7 +16,7 @@ from homeassistant.const import ( import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity -REQUIREMENTS = ['pygatt==3.2.0'] +REQUIREMENTS = ['pygatt[GATTTOOL]==3.2.0'] _LOGGER = logging.getLogger(__name__) diff --git a/requirements_all.txt b/requirements_all.txt index 96c13d41e59..8780677e040 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1013,7 +1013,7 @@ pyfttt==0.3 # homeassistant.components.device_tracker.bluetooth_le_tracker # homeassistant.components.sensor.skybeacon -pygatt==3.2.0 +pygatt[GATTTOOL]==3.2.0 # homeassistant.components.cover.gogogate2 pygogogate2==0.1.1 From c87c5797db1e4551bc7e3b1d80b5b95bf8dd2a16 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 21 Jan 2019 21:20:08 -0800 Subject: [PATCH 061/222] Updated frontend to 20190121.1 --- homeassistant/components/frontend/__init__.py | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/frontend/__init__.py b/homeassistant/components/frontend/__init__.py index d7b77fa903f..f5cc33b63a0 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==20190121.0'] +REQUIREMENTS = ['home-assistant-frontend==20190121.1'] DOMAIN = 'frontend' DEPENDENCIES = ['api', 'websocket_api', 'http', 'system_log', diff --git a/requirements_all.txt b/requirements_all.txt index 8780677e040..3ce631ef8c7 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -520,7 +520,7 @@ hole==0.3.0 holidays==0.9.9 # homeassistant.components.frontend -home-assistant-frontend==20190121.0 +home-assistant-frontend==20190121.1 # homeassistant.components.zwave homeassistant-pyozw==0.1.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 1f07c530659..9ade351f8a6 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -110,7 +110,7 @@ hdate==0.8.7 holidays==0.9.9 # homeassistant.components.frontend -home-assistant-frontend==20190121.0 +home-assistant-frontend==20190121.1 # homeassistant.components.homematicip_cloud homematicip==0.10.3 From e964750ac19415af58f20f77264cdfa7826fed0f Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 22 Jan 2019 14:07:17 -0800 Subject: [PATCH 062/222] Fix invalid entity ID in entity registry (#20328) --- homeassistant/helpers/entity_registry.py | 11 ++++++-- tests/helpers/test_entity_registry.py | 34 ++++++++++++++++++++++++ 2 files changed, 43 insertions(+), 2 deletions(-) diff --git a/homeassistant/helpers/entity_registry.py b/homeassistant/helpers/entity_registry.py index 8216681496b..82530708838 100644 --- a/homeassistant/helpers/entity_registry.py +++ b/homeassistant/helpers/entity_registry.py @@ -122,8 +122,15 @@ class EntityRegistry: entity_id = self.async_get_entity_id(domain, platform, unique_id) if entity_id: return self._async_update_entity( - entity_id, config_entry_id=config_entry_id, - device_id=device_id) + entity_id, + config_entry_id=config_entry_id, + device_id=device_id, + # When we changed our slugify algorithm, we invalidated some + # stored entity IDs with either a __ or ending in _. + # Fix introduced in 0.86 (Jan 23, 2018). Next line can be + # removed when we release 1.0 or in 2019. + new_entity_id='.'.join(slugify(part) for part + in entity_id.split('.', 1))) entity_id = self.async_generate_entity_id( domain, suggested_object_id or '{}_{}'.format(platform, unique_id), diff --git a/tests/helpers/test_entity_registry.py b/tests/helpers/test_entity_registry.py index a8c9086b2d2..ef7b4a60ee2 100644 --- a/tests/helpers/test_entity_registry.py +++ b/tests/helpers/test_entity_registry.py @@ -4,6 +4,7 @@ from unittest.mock import patch import pytest +from homeassistant.core import valid_entity_id from homeassistant.helpers import entity_registry from tests.common import mock_registry, flush_store @@ -222,3 +223,36 @@ async def test_migration(hass): assert entry.name == 'Test Name' assert entry.disabled_by == 'hass' assert entry.config_entry_id == 'test-config-id' + + +async def test_loading_invalid_entity_id(hass, hass_storage): + """Test we autofix invalid entity IDs.""" + hass_storage[entity_registry.STORAGE_KEY] = { + 'version': entity_registry.STORAGE_VERSION, + 'data': { + 'entities': [ + { + 'entity_id': 'test.invalid__middle', + 'platform': 'super_platform', + 'unique_id': 'id-invalid-middle', + 'name': 'registry override', + }, { + 'entity_id': 'test.invalid_end_', + 'platform': 'super_platform', + 'unique_id': 'id-invalid-end', + } + ] + } + } + + registry = await entity_registry.async_get_registry(hass) + + entity_invalid_middle = registry.async_get_or_create( + 'test', 'super_platform', 'id-invalid-middle') + + assert valid_entity_id(entity_invalid_middle.entity_id) + + entity_invalid_end = registry.async_get_or_create( + 'test', 'super_platform', 'id-invalid-end') + + assert valid_entity_id(entity_invalid_end.entity_id) From 3484e506e8b24c776980b29f8e0fbe5ee4a9b1f2 Mon Sep 17 00:00:00 2001 From: Sebastian Muszynski Date: Wed, 23 Jan 2019 05:04:13 +0100 Subject: [PATCH 063/222] Fix xiaomi speed attribute name clash (#20312) --- homeassistant/components/fan/xiaomi_miio.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/fan/xiaomi_miio.py b/homeassistant/components/fan/xiaomi_miio.py index 96cacac3782..2e0b1657d23 100644 --- a/homeassistant/components/fan/xiaomi_miio.py +++ b/homeassistant/components/fan/xiaomi_miio.py @@ -111,7 +111,7 @@ ATTR_TRANS_LEVEL = 'trans_level' ATTR_HARDWARE_VERSION = 'hardware_version' # Air Humidifier CA -ATTR_SPEED = 'speed' +ATTR_MOTOR_SPEED = 'motor_speed' ATTR_DEPTH = 'depth' ATTR_DRY = 'dry' @@ -232,7 +232,7 @@ AVAILABLE_ATTRIBUTES_AIRHUMIDIFIER = { AVAILABLE_ATTRIBUTES_AIRHUMIDIFIER_CA = { **AVAILABLE_ATTRIBUTES_AIRHUMIDIFIER_COMMON, - ATTR_SPEED: 'speed', + ATTR_MOTOR_SPEED: 'speed', ATTR_DEPTH: 'depth', ATTR_DRY: 'dry', } From db277ad02398077bdb9e9678156bd50f8948e941 Mon Sep 17 00:00:00 2001 From: Frank Date: Wed, 23 Jan 2019 08:47:37 +0100 Subject: [PATCH 064/222] Add data/data_template/title to alert component (#17616) * Add data/data_template/title to alert component * Fix line length * Fix tests * Fix lint * fix line length * Fix tests, make title templatable * Fix test * Fix test * Optimize data, make title templated * Fix line length * Add title template * typo * Fix tests --- homeassistant/components/alert.py | 38 ++++++++++++++++----- tests/components/test_alert.py | 55 ++++++++++++++++++++++++++----- 2 files changed, 76 insertions(+), 17 deletions(-) diff --git a/homeassistant/components/alert.py b/homeassistant/components/alert.py index 3a18281e49b..579a19c1b52 100644 --- a/homeassistant/components/alert.py +++ b/homeassistant/components/alert.py @@ -5,19 +5,19 @@ For more details about this component, please refer to the documentation at https://home-assistant.io/components/alert/ """ import asyncio -from datetime import datetime, timedelta import logging +from datetime import datetime, timedelta import voluptuous as vol +import homeassistant.helpers.config_validation as cv from homeassistant.components.notify import ( - ATTR_MESSAGE, DOMAIN as DOMAIN_NOTIFY) + ATTR_MESSAGE, ATTR_TITLE, ATTR_DATA, DOMAIN as DOMAIN_NOTIFY) from homeassistant.const import ( CONF_ENTITY_ID, STATE_IDLE, CONF_NAME, CONF_STATE, STATE_ON, STATE_OFF, SERVICE_TURN_ON, SERVICE_TURN_OFF, SERVICE_TOGGLE, ATTR_ENTITY_ID) -from homeassistant.helpers.entity import ToggleEntity from homeassistant.helpers import service, event -import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.entity import ToggleEntity _LOGGER = logging.getLogger(__name__) @@ -30,6 +30,8 @@ CONF_REPEAT = 'repeat' CONF_SKIP_FIRST = 'skip_first' CONF_ALERT_MESSAGE = 'message' CONF_DONE_MESSAGE = 'done_message' +CONF_TITLE = 'title' +CONF_DATA = 'data' DEFAULT_CAN_ACK = True DEFAULT_SKIP_FIRST = False @@ -43,13 +45,14 @@ ALERT_SCHEMA = vol.Schema({ vol.Required(CONF_SKIP_FIRST, default=DEFAULT_SKIP_FIRST): cv.boolean, vol.Optional(CONF_ALERT_MESSAGE): cv.template, vol.Optional(CONF_DONE_MESSAGE): cv.template, + vol.Optional(CONF_TITLE): cv.template, + vol.Optional(CONF_DATA): dict, vol.Required(CONF_NOTIFIERS): cv.ensure_list}) CONFIG_SCHEMA = vol.Schema({ DOMAIN: cv.schema_with_slug_keys(ALERT_SCHEMA), }, extra=vol.ALLOW_EXTRA) - ALERT_SERVICE_SCHEMA = vol.Schema({ vol.Required(ATTR_ENTITY_ID): cv.entity_ids, }) @@ -77,12 +80,14 @@ async def async_setup(hass, config): done_message_template = cfg.get(CONF_DONE_MESSAGE) notifiers = cfg.get(CONF_NOTIFIERS) can_ack = cfg.get(CONF_CAN_ACK) + title_template = cfg.get(CONF_TITLE) + data = cfg.get(CONF_DATA) entities.append(Alert(hass, object_id, name, watched_entity_id, alert_state, repeat, skip_first, message_template, done_message_template, notifiers, - can_ack)) + can_ack, title_template, data)) if not entities: return False @@ -127,12 +132,14 @@ class Alert(ToggleEntity): def __init__(self, hass, entity_id, name, watched_entity_id, state, repeat, skip_first, message_template, - done_message_template, notifiers, can_ack): + done_message_template, notifiers, can_ack, title_template, + data): """Initialize the alert.""" self.hass = hass self._name = name self._alert_state = state self._skip_first = skip_first + self._data = data self._message_template = message_template if self._message_template is not None: @@ -142,6 +149,10 @@ class Alert(ToggleEntity): if self._done_message_template is not None: self._done_message_template.hass = hass + self._title_template = title_template + if self._title_template is not None: + self._title_template.hass = hass + self._notifiers = notifiers self._can_ack = can_ack @@ -251,9 +262,20 @@ class Alert(ToggleEntity): await self._send_notification_message(message) async def _send_notification_message(self, message): + + msg_payload = {ATTR_MESSAGE: message} + + if self._title_template is not None: + title = self._title_template.async_render() + msg_payload.update({ATTR_TITLE: title}) + if self._data: + msg_payload.update({ATTR_DATA: self._data}) + + _LOGGER.debug(msg_payload) + for target in self._notifiers: await self.hass.services.async_call( - DOMAIN_NOTIFY, target, {ATTR_MESSAGE: message}) + DOMAIN_NOTIFY, target, msg_payload) async def async_turn_on(self, **kwargs): """Async Unacknowledge alert.""" diff --git a/tests/components/test_alert.py b/tests/components/test_alert.py index 9fda58c37a3..57da830203e 100644 --- a/tests/components/test_alert.py +++ b/tests/components/test_alert.py @@ -1,17 +1,16 @@ """The tests for the Alert component.""" +import unittest # pylint: disable=protected-access from copy import deepcopy -import unittest -from homeassistant.setup import setup_component -from homeassistant.core import callback -from homeassistant.components.alert import DOMAIN import homeassistant.components.alert as alert import homeassistant.components.notify as notify +from homeassistant.components.alert import DOMAIN from homeassistant.const import ( ATTR_ENTITY_ID, CONF_ENTITY_ID, STATE_IDLE, CONF_NAME, CONF_STATE, SERVICE_TOGGLE, SERVICE_TURN_OFF, SERVICE_TURN_ON, STATE_ON, STATE_OFF) - +from homeassistant.core import callback +from homeassistant.setup import setup_component from tests.common import get_test_home_assistant NAME = "alert_test" @@ -19,6 +18,13 @@ DONE_MESSAGE = "alert_gone" NOTIFIER = 'test' TEMPLATE = "{{ states.sensor.test.entity_id }}" TEST_ENTITY = "sensor.test" +TITLE = "{{ states.sensor.test.entity_id }}" +TEST_TITLE = "sensor.test" +TEST_DATA = { + 'data': { + 'inline_keyboard': ['Close garage:/close_garage'] + } +} TEST_CONFIG = \ {alert.DOMAIN: { NAME: { @@ -28,10 +34,13 @@ TEST_CONFIG = \ CONF_STATE: STATE_ON, alert.CONF_REPEAT: 30, alert.CONF_SKIP_FIRST: False, - alert.CONF_NOTIFIERS: [NOTIFIER]} - }} + alert.CONF_NOTIFIERS: [NOTIFIER], + alert.CONF_TITLE: TITLE, + alert.CONF_DATA: {} + } + }} TEST_NOACK = [NAME, NAME, "sensor.test", - STATE_ON, [30], False, None, None, NOTIFIER, False] + STATE_ON, [30], False, None, None, NOTIFIER, False, None, None] ENTITY_ID = alert.ENTITY_ID_FORMAT.format(NAME) @@ -200,7 +209,7 @@ class TestAlert(unittest.TestCase): """Test notifications.""" events = [] config = deepcopy(TEST_CONFIG) - del(config[alert.DOMAIN][NAME][alert.CONF_DONE_MESSAGE]) + del (config[alert.DOMAIN][NAME][alert.CONF_DONE_MESSAGE]) @callback def record_event(event): @@ -286,6 +295,34 @@ class TestAlert(unittest.TestCase): last_event = events[-1] self.assertEqual(last_event.data[notify.ATTR_MESSAGE], TEST_ENTITY) + def test_sending_titled_notification(self): + """Test notifications.""" + events = self._setup_notify() + + config = deepcopy(TEST_CONFIG) + config[alert.DOMAIN][NAME][alert.CONF_TITLE] = TITLE + assert setup_component(self.hass, alert.DOMAIN, config) + + self.hass.states.set(TEST_ENTITY, STATE_ON) + self.hass.block_till_done() + self.assertEqual(1, len(events)) + last_event = events[-1] + self.assertEqual(last_event.data[notify.ATTR_TITLE], TEST_TITLE) + + def test_sending_data_notification(self): + """Test notifications.""" + events = self._setup_notify() + + config = deepcopy(TEST_CONFIG) + config[alert.DOMAIN][NAME][alert.CONF_DATA] = TEST_DATA + assert setup_component(self.hass, alert.DOMAIN, config) + + self.hass.states.set(TEST_ENTITY, STATE_ON) + self.hass.block_till_done() + self.assertEqual(1, len(events)) + last_event = events[-1] + self.assertEqual(last_event.data[notify.ATTR_DATA], TEST_DATA) + def test_skipfirst(self): """Test skipping first notification.""" config = deepcopy(TEST_CONFIG) From eaa9c4d437d966e4ff415db69c32cf9d44fec037 Mon Sep 17 00:00:00 2001 From: Malte Franken Date: Wed, 23 Jan 2019 20:04:41 +1100 Subject: [PATCH 065/222] Remove creation of geolocation default group (#20338) --- homeassistant/components/geo_location/__init__.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/homeassistant/components/geo_location/__init__.py b/homeassistant/components/geo_location/__init__.py index 4e05c5b41fe..4597a56c61a 100644 --- a/homeassistant/components/geo_location/__init__.py +++ b/homeassistant/components/geo_location/__init__.py @@ -22,15 +22,12 @@ DOMAIN = 'geo_location' ENTITY_ID_FORMAT = DOMAIN + '.{}' -GROUP_NAME_ALL_EVENTS = 'All Geolocation Events' - SCAN_INTERVAL = timedelta(seconds=60) async def async_setup(hass, config): """Set up the Geolocation component.""" - component = EntityComponent( - _LOGGER, DOMAIN, hass, SCAN_INTERVAL, GROUP_NAME_ALL_EVENTS) + component = EntityComponent(_LOGGER, DOMAIN, hass, SCAN_INTERVAL) await component.async_setup(config) return True From 07f1e2ce75a131f4fbb186a2463a3716d9c1d1a6 Mon Sep 17 00:00:00 2001 From: Fabien Piuzzi Date: Wed, 23 Jan 2019 12:50:17 +0100 Subject: [PATCH 066/222] Add Octoprint custom path (#20302) * Added custom path config option to octoprint component * Added some debug logging * removed debug logging for base url * Fixed single/double quotes style --- homeassistant/components/octoprint.py | 50 +++++++++++++++++---------- 1 file changed, 31 insertions(+), 19 deletions(-) diff --git a/homeassistant/components/octoprint.py b/homeassistant/components/octoprint.py index 853ee67db9d..869f3bd7d6e 100644 --- a/homeassistant/components/octoprint.py +++ b/homeassistant/components/octoprint.py @@ -13,8 +13,8 @@ from aiohttp.hdrs import CONTENT_TYPE from homeassistant.components.discovery import SERVICE_OCTOPRINT from homeassistant.const import ( - CONF_API_KEY, CONF_HOST, CONTENT_TYPE_JSON, CONF_NAME, CONF_PORT, - CONF_SSL, TEMP_CELSIUS, CONF_MONITORED_CONDITIONS, CONF_SENSORS, + CONF_API_KEY, CONF_HOST, CONTENT_TYPE_JSON, CONF_NAME, CONF_PATH, + CONF_PORT, CONF_SSL, TEMP_CELSIUS, CONF_MONITORED_CONDITIONS, CONF_SENSORS, CONF_BINARY_SENSORS) from homeassistant.helpers import discovery import homeassistant.helpers.config_validation as cv @@ -36,10 +36,20 @@ def has_all_unique_names(value): return value +def ensure_valid_path(value): + """Validate the path, ensuring it starts and ends with a /.""" + vol.Schema(cv.string)(value) + if value[0] != '/': + value = '/' + value + if value[-1] != '/': + value += '/' + return value + + BINARY_SENSOR_TYPES = { # API Endpoint, Group, Key, unit 'Printing': ['printer', 'state', 'printing', None], - 'Printing Error': ['printer', 'state', 'error', None] + "Printing Error": ['printer', 'state', 'error', None] } BINARY_SENSOR_SCHEMA = vol.Schema({ @@ -51,12 +61,12 @@ BINARY_SENSOR_SCHEMA = vol.Schema({ SENSOR_TYPES = { # API Endpoint, Group, Key, unit, icon 'Temperatures': ['printer', 'temperature', '*', TEMP_CELSIUS], - 'Current State': ['printer', 'state', 'text', None, 'mdi:printer-3d'], - 'Job Percentage': ['job', 'progress', 'completion', '%', + "Current State": ['printer', 'state', 'text', None, 'mdi:printer-3d'], + "Job Percentage": ['job', 'progress', 'completion', '%', 'mdi:file-percent'], - 'Time Remaining': ['job', 'progress', 'printTimeLeft', 'seconds', + "Time Remaining": ['job', 'progress', 'printTimeLeft', 'seconds', 'mdi:clock-end'], - 'Time Elapsed': ['job', 'progress', 'printTime', 'seconds', + "Time Elapsed": ['job', 'progress', 'printTime', 'seconds', 'mdi:clock-start'], } @@ -72,6 +82,7 @@ CONFIG_SCHEMA = vol.Schema({ vol.Required(CONF_HOST): cv.string, vol.Optional(CONF_SSL, default=False): cv.boolean, vol.Optional(CONF_PORT, default=80): cv.port, + vol.Optional(CONF_PATH, default='/'): ensure_valid_path, vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, vol.Optional(CONF_NUMBER_OF_TOOLS, default=0): cv.positive_int, vol.Optional(CONF_BED, default=False): cv.boolean, @@ -88,7 +99,7 @@ def setup(hass, config): def device_discovered(service, info): """Get called when an Octoprint server has been discovered.""" - _LOGGER.debug('Found an Octoprint server: %s', info) + _LOGGER.debug("Found an Octoprint server: %s", info) discovery.listen(hass, SERVICE_OCTOPRINT, device_discovered) @@ -99,9 +110,10 @@ def setup(hass, config): for printer in config[DOMAIN]: name = printer[CONF_NAME] ssl = 's' if printer[CONF_SSL] else '' - base_url = 'http{}://{}:{}/api/'.format(ssl, - printer[CONF_HOST], - printer[CONF_PORT]) + base_url = 'http{}://{}:{}{}api/'.format(ssl, + printer[CONF_HOST], + printer[CONF_PORT], + printer[CONF_PATH]) api_key = printer[CONF_API_KEY] number_of_tools = printer[CONF_NUMBER_OF_TOOLS] bed = printer[CONF_BED] @@ -154,7 +166,7 @@ class OctoPrintAPI: tools = [] if self.number_of_tools > 0: for tool_number in range(0, self.number_of_tools): - tools.append("tool" + str(tool_number)) + tools.append('tool' + str(tool_number)) if self.bed: tools.append('bed') if not self.bed and self.number_of_tools == 0: @@ -167,12 +179,12 @@ class OctoPrintAPI: """Send a get request, and return the response as a dict.""" # Only query the API at most every 30 seconds now = time.time() - if endpoint == "job": + if endpoint == 'job': last_time = self.job_last_reading[1] if last_time is not None: if now - last_time < 30.0: return self.job_last_reading[0] - elif endpoint == "printer": + elif endpoint == 'printer': last_time = self.printer_last_reading[1] if last_time is not None: if now - last_time < 30.0: @@ -183,11 +195,11 @@ class OctoPrintAPI: response = requests.get( url, headers=self.headers, timeout=9) response.raise_for_status() - if endpoint == "job": + if endpoint == 'job': self.job_last_reading[0] = response.json() self.job_last_reading[1] = time.time() self.job_available = True - elif endpoint == "printer": + elif endpoint == 'printer': self.printer_last_reading[0] = response.json() self.printer_last_reading[1] = time.time() self.printer_available = True @@ -200,13 +212,13 @@ class OctoPrintAPI: log_string = "Failed to update OctoPrint status. " + \ " Error: %s" % (conn_exc) # Only log the first failure - if endpoint == "job": + if endpoint == 'job': log_string = "Endpoint: job " + log_string if not self.job_error_logged: _LOGGER.error(log_string) self.job_error_logged = True self.job_available = False - elif endpoint == "printer": + elif endpoint == 'printer': log_string = "Endpoint: printer " + log_string if not self.printer_error_logged: _LOGGER.error(log_string) @@ -229,7 +241,7 @@ def get_value_from_json(json_dict, sensor_type, group, tool): return None if sensor_type in json_dict[group]: - if sensor_type == "target" and json_dict[sensor_type] is None: + if sensor_type == 'target' and json_dict[sensor_type] is None: return 0 return json_dict[group][sensor_type] From e8f0e534f98c39d56a131c9596aa5f09482383f2 Mon Sep 17 00:00:00 2001 From: shbatm Date: Wed, 23 Jan 2019 10:48:35 -0600 Subject: [PATCH 067/222] Update Requirement for PyISY Package in isy994 Component to v1.1.1 (#20349) --- homeassistant/components/isy994.py | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/isy994.py b/homeassistant/components/isy994.py index 9b539b0690a..a9916ed54fe 100644 --- a/homeassistant/components/isy994.py +++ b/homeassistant/components/isy994.py @@ -17,7 +17,7 @@ from homeassistant.helpers import discovery, config_validation as cv from homeassistant.helpers.entity import Entity from homeassistant.helpers.typing import ConfigType, Dict -REQUIREMENTS = ['PyISY==1.1.0'] +REQUIREMENTS = ['PyISY==1.1.1'] _LOGGER = logging.getLogger(__name__) diff --git a/requirements_all.txt b/requirements_all.txt index 3ce631ef8c7..1cbea00ad96 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -39,7 +39,7 @@ HAP-python==2.4.2 Mastodon.py==1.3.1 # homeassistant.components.isy994 -PyISY==1.1.0 +PyISY==1.1.1 # homeassistant.components.sensor.mvglive PyMVGLive==1.1.4 From 16a4180fab3ac65f02c6c03642f21011d194f688 Mon Sep 17 00:00:00 2001 From: Corey Edwards Date: Wed, 23 Jan 2019 09:50:04 -0700 Subject: [PATCH 068/222] Fix mpd logging format string field (#20333) * Fix format string field * Remove str.format and let _LOGGER handle formatting * Remove trailing period from log message --- homeassistant/components/media_player/mpd.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/media_player/mpd.py b/homeassistant/components/media_player/mpd.py index d006b5692f1..09d0a976b82 100644 --- a/homeassistant/components/media_player/mpd.py +++ b/homeassistant/components/media_player/mpd.py @@ -300,14 +300,13 @@ class MpdDevice(MediaPlayerDevice): def play_media(self, media_type, media_id, **kwargs): """Send the media player the command for playing a playlist.""" - _LOGGER.debug(str.format("Playing playlist: {0}", media_id)) + _LOGGER.debug("Playing playlist: %s", media_id) if media_type == MEDIA_TYPE_PLAYLIST: if media_id in self._playlists: self._currentplaylist = media_id else: self._currentplaylist = None - _LOGGER.warning(str.format("Unknown playlist name %s.", - media_id)) + _LOGGER.warning("Unknown playlist name %s", media_id) self._client.clear() self._client.load(media_id) self._client.play() From c6cee1ccd3b8557b1ad3dcc5956393029dac35cb Mon Sep 17 00:00:00 2001 From: Jonas Pedersen Date: Wed, 23 Jan 2019 17:58:45 +0100 Subject: [PATCH 069/222] Add Danfoss Air HRV support (#20138) * Add support for Danfoss Air HRV systems. * Correct lint errors after initial commit of Danfoss Air HRV support. * A couple of lint fixes for danfoss_air. * Refactor to comply with HA standards. * Style fix. * Use wildcard for danfoss_air in .coveragerc. * Remove config example from header documentation. Correct import name for platforms. --- .coveragerc | 2 + .../components/danfoss_air/__init__.py | 83 +++++++++++++++++++ .../components/danfoss_air/binary_sensor.py | 56 +++++++++++++ .../components/danfoss_air/sensor.py | 76 +++++++++++++++++ requirements_all.txt | 3 + 5 files changed, 220 insertions(+) create mode 100644 homeassistant/components/danfoss_air/__init__.py create mode 100644 homeassistant/components/danfoss_air/binary_sensor.py create mode 100644 homeassistant/components/danfoss_air/sensor.py diff --git a/.coveragerc b/.coveragerc index e9627277905..04d8f0203be 100644 --- a/.coveragerc +++ b/.coveragerc @@ -80,6 +80,8 @@ omit = homeassistant/components/digital_ocean.py homeassistant/components/*/digital_ocean.py + homeassistant/components/danfoss_air/* + homeassistant/components/dominos.py homeassistant/components/doorbird.py diff --git a/homeassistant/components/danfoss_air/__init__.py b/homeassistant/components/danfoss_air/__init__.py new file mode 100644 index 00000000000..17a3952adec --- /dev/null +++ b/homeassistant/components/danfoss_air/__init__.py @@ -0,0 +1,83 @@ +""" +Support for Danfoss Air HRV. + +For more details about this component, please refer to the documentation at +https://home-assistant.io/components/danfoss_air/ +""" +from datetime import timedelta +import logging + +import voluptuous as vol + +from homeassistant.const import CONF_HOST +from homeassistant.helpers import discovery +import homeassistant.helpers.config_validation as cv +from homeassistant.util import Throttle + +REQUIREMENTS = ['pydanfossair==0.0.6'] + +_LOGGER = logging.getLogger(__name__) + +DANFOSS_AIR_PLATFORMS = ['sensor', 'binary_sensor'] +DOMAIN = 'danfoss_air' + +MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=60) + +CONFIG_SCHEMA = vol.Schema({ + DOMAIN: vol.Schema({ + vol.Required(CONF_HOST): cv.string, + }), +}, extra=vol.ALLOW_EXTRA) + + +def setup(hass, config): + """Set up the Danfoss Air component.""" + conf = config[DOMAIN] + + hass.data[DOMAIN] = DanfossAir(conf[CONF_HOST]) + + for platform in DANFOSS_AIR_PLATFORMS: + discovery.load_platform(hass, platform, DOMAIN, {}, config) + + return True + + +class DanfossAir: + """Handle all communication with Danfoss Air CCM unit.""" + + def __init__(self, host): + """Initialize the Danfoss Air CCM connection.""" + self._data = {} + + from pydanfossair.danfossclient import DanfossClient + + self._client = DanfossClient(host) + + def get_value(self, item): + """Get value for sensor.""" + if item in self._data: + return self._data[item] + + return None + + @Throttle(MIN_TIME_BETWEEN_UPDATES) + def update(self): + """Use the data from Danfoss Air API.""" + _LOGGER.debug("Fetching data from Danfoss Air CCM module") + from pydanfossair.commands import ReadCommand + self._data[ReadCommand.exhaustTemperature] \ + = self._client.command(ReadCommand.exhaustTemperature) + self._data[ReadCommand.outdoorTemperature] \ + = self._client.command(ReadCommand.outdoorTemperature) + self._data[ReadCommand.supplyTemperature] \ + = self._client.command(ReadCommand.supplyTemperature) + self._data[ReadCommand.extractTemperature] \ + = self._client.command(ReadCommand.extractTemperature) + self._data[ReadCommand.humidity] \ + = round(self._client.command(ReadCommand.humidity), 2) + self._data[ReadCommand.filterPercent] \ + = round(self._client.command(ReadCommand.filterPercent), 2) + self._data[ReadCommand.bypass] \ + = self._client.command(ReadCommand.bypass) + + _LOGGER.debug("Done fetching data from Danfoss Air CCM module") diff --git a/homeassistant/components/danfoss_air/binary_sensor.py b/homeassistant/components/danfoss_air/binary_sensor.py new file mode 100644 index 00000000000..905ead24a0f --- /dev/null +++ b/homeassistant/components/danfoss_air/binary_sensor.py @@ -0,0 +1,56 @@ +""" +Support for the for Danfoss Air HRV binary sensor platform. + +For more details about this component, please refer to the documentation at +https://home-assistant.io/components/binary_sensor.danfoss_air/ +""" +from homeassistant.components.binary_sensor import BinarySensorDevice +from homeassistant.components.danfoss_air import DOMAIN \ + as DANFOSS_AIR_DOMAIN + + +def setup_platform(hass, config, add_devices, discovery_info=None): + """Set up the available Danfoss Air sensors etc.""" + from pydanfossair.commands import ReadCommand + data = hass.data[DANFOSS_AIR_DOMAIN] + + sensors = [["Danfoss Air Bypass Active", ReadCommand.bypass]] + + dev = [] + + for sensor in sensors: + dev.append(DanfossAirBinarySensor(data, sensor[0], sensor[1])) + + add_devices(dev, True) + + +class DanfossAirBinarySensor(BinarySensorDevice): + """Representation of a Danfoss Air binary sensor.""" + + def __init__(self, data, name, sensor_type): + """Initialize the Danfoss Air binary sensor.""" + self._data = data + self._name = name + self._state = None + self._type = sensor_type + + @property + def name(self): + """Return the name of the sensor.""" + return self._name + + @property + def is_on(self): + """Return the state of the sensor.""" + return self._state + + @property + def device_class(self): + """Type of device class.""" + return "opening" + + def update(self): + """Fetch new state data for the sensor.""" + self._data.update() + + self._state = self._data.get_value(self._type) diff --git a/homeassistant/components/danfoss_air/sensor.py b/homeassistant/components/danfoss_air/sensor.py new file mode 100644 index 00000000000..dfb9686edea --- /dev/null +++ b/homeassistant/components/danfoss_air/sensor.py @@ -0,0 +1,76 @@ +""" +Support for the for Danfoss Air HRV sensor platform. + +For more details about this component, please refer to the documentation at +https://home-assistant.io/components/sensor.danfoss_air/ +""" +from homeassistant.components.danfoss_air import DOMAIN \ + as DANFOSS_AIR_DOMAIN +from homeassistant.const import TEMP_CELSIUS +from homeassistant.helpers.entity import Entity + + +def setup_platform(hass, config, add_devices, discovery_info=None): + """Set up the available Danfoss Air sensors etc.""" + from pydanfossair.commands import ReadCommand + + data = hass.data[DANFOSS_AIR_DOMAIN] + + sensors = [ + ["Danfoss Air Exhaust Temperature", TEMP_CELSIUS, + ReadCommand.exhaustTemperature], + ["Danfoss Air Outdoor Temperature", TEMP_CELSIUS, + ReadCommand.outdoorTemperature], + ["Danfoss Air Supply Temperature", TEMP_CELSIUS, + ReadCommand.supplyTemperature], + ["Danfoss Air Extract Temperature", TEMP_CELSIUS, + ReadCommand.extractTemperature], + ["Danfoss Air Remaining Filter", '%', + ReadCommand.filterPercent], + ["Danfoss Air Humidity", '%', + ReadCommand.humidity] + ] + + dev = [] + + for sensor in sensors: + dev.append(DanfossAir(data, sensor[0], sensor[1], sensor[2])) + + add_devices(dev, True) + + +class DanfossAir(Entity): + """Representation of a Sensor.""" + + def __init__(self, data, name, sensorUnit, sensorType): + """Initialize the sensor.""" + self._data = data + self._name = name + self._state = None + self._type = sensorType + self._unit = sensorUnit + + @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.""" + return self._unit + + def update(self): + """Update the new state of the sensor. + + This is done through the DanfossAir object tthat does the actually + communication with the Air CCM. + """ + self._data.update() + + self._state = self._data.get_value(self._type) diff --git a/requirements_all.txt b/requirements_all.txt index 1cbea00ad96..84c0cac4f7d 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -954,6 +954,9 @@ pycsspeechtts==1.0.2 # homeassistant.components.daikin pydaikin==0.9 +# homeassistant.components.danfoss_air +pydanfossair==0.0.6 + # homeassistant.components.deconz pydeconz==47 From 5bf3b2dd9f5eeb24247231ecf4213eb6b0f28389 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20H=C3=B8yer=20Iversen?= Date: Wed, 23 Jan 2019 20:17:45 +0100 Subject: [PATCH 070/222] clean up of islamic_prayer_times (#20352) update_sensors was not awaited in async_track_point_in_time() --- .../components/sensor/islamic_prayer_times.py | 41 ++++++++++--------- 1 file changed, 21 insertions(+), 20 deletions(-) diff --git a/homeassistant/components/sensor/islamic_prayer_times.py b/homeassistant/components/sensor/islamic_prayer_times.py index a1ea5212461..50331435491 100644 --- a/homeassistant/components/sensor/islamic_prayer_times.py +++ b/homeassistant/components/sensor/islamic_prayer_times.py @@ -6,13 +6,15 @@ https://home-assistant.io/components/sensor.islamic_prayer_times/ """ import logging from datetime import datetime, timedelta + import voluptuous as vol -from homeassistant.components.sensor import PLATFORM_SCHEMA + import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.entity import Entity import homeassistant.util.dt as dt_util -from homeassistant.helpers.event import async_track_point_in_time +from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.const import DEVICE_CLASS_TIMESTAMP +from homeassistant.helpers.entity import Entity +from homeassistant.helpers.event import async_track_point_in_time REQUIREMENTS = ['prayer_times_calculator==0.0.3'] @@ -118,27 +120,26 @@ async def schedule_future_update(hass, sensors, midnight_time, _LOGGER.debug("Next update scheduled for: %s", str(next_update_at)) + async def update_sensors(_): + """Update sensors with new prayer times.""" + # Update prayer times + prayer_times = prayer_times_data.get_new_prayer_times() + + _LOGGER.debug("New prayer times retrieved. Updating sensors.") + + # Update all prayer times sensors + for sensor in sensors: + sensor.async_schedule_update_ha_state(True) + + # Schedule next update + await schedule_future_update(hass, sensors, prayer_times['Midnight'], + prayer_times_data) + async_track_point_in_time(hass, - update_sensors(hass, sensors, prayer_times_data), + update_sensors, next_update_at) -async def update_sensors(hass, sensors, prayer_times_data): - """Update sensors with new prayer times.""" - # Update prayer times - prayer_times = prayer_times_data.get_new_prayer_times() - - _LOGGER.debug("New prayer times retrieved. Updating sensors.") - - # Update all prayer times sensors - for sensor in sensors: - sensor.async_schedule_update_ha_state(True) - - # Schedule next update - await schedule_future_update(hass, sensors, prayer_times['Midnight'], - prayer_times_data) - - class IslamicPrayerTimesData: """Data object for Islamic prayer times.""" From 7ca795152694f7921cb87225ff62962a74bd3de1 Mon Sep 17 00:00:00 2001 From: Kevin Fronczak Date: Wed, 23 Jan 2019 11:37:21 -0800 Subject: [PATCH 071/222] Hotfix for blink initialization failure. Fixes #20335 (#20351) --- homeassistant/components/blink/__init__.py | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/blink/__init__.py b/homeassistant/components/blink/__init__.py index ac2a4574b9c..57500fcc8a6 100644 --- a/homeassistant/components/blink/__init__.py +++ b/homeassistant/components/blink/__init__.py @@ -15,7 +15,7 @@ from homeassistant.const import ( CONF_BINARY_SENSORS, CONF_SENSORS, CONF_FILENAME, CONF_MONITORED_CONDITIONS, TEMP_FAHRENHEIT) -REQUIREMENTS = ['blinkpy==0.11.1'] +REQUIREMENTS = ['blinkpy==0.11.2'] _LOGGER = logging.getLogger(__name__) diff --git a/requirements_all.txt b/requirements_all.txt index 84c0cac4f7d..6ea29d243c9 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -196,7 +196,7 @@ bellows==0.7.0 bimmer_connected==0.5.3 # homeassistant.components.blink -blinkpy==0.11.1 +blinkpy==0.11.2 # homeassistant.components.light.blinksticklight blinkstick==1.1.8 From a396ee2cb575f1c33990eeee67b67f5b35add8f5 Mon Sep 17 00:00:00 2001 From: Jc2k Date: Wed, 23 Jan 2019 19:44:21 +0000 Subject: [PATCH 072/222] Bump homekit==0.12.2 + improve controller reliability (#20325) Sessions were timing out and requiring a HA restart in order to restore functionality. Network disconnects are now handled and sessions will be automatically recovered after they fail. --- .../components/homekit_controller/__init__.py | 14 +++++++------- requirements_all.txt | 2 +- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/homekit_controller/__init__.py b/homeassistant/components/homekit_controller/__init__.py index 6fdde7ddd50..17cff702b70 100644 --- a/homeassistant/components/homekit_controller/__init__.py +++ b/homeassistant/components/homekit_controller/__init__.py @@ -13,7 +13,7 @@ from homeassistant.helpers import discovery from homeassistant.helpers.entity import Entity from homeassistant.helpers.event import call_later -REQUIREMENTS = ['homekit==0.12.0'] +REQUIREMENTS = ['homekit==0.12.2'] DOMAIN = 'homekit_controller' HOMEKIT_DIR = '.homekit' @@ -49,10 +49,6 @@ RETRY_INTERVAL = 60 # seconds PAIRING_FILE = "pairing.json" -class HomeKitConnectionError(ConnectionError): - """Raised when unable to connect to target device.""" - - def get_serial(accessory): """Obtain the serial number of a HomeKit device.""" # pylint: disable=import-error @@ -101,13 +97,14 @@ class HKDevice(): """Handle setup of a HomeKit accessory.""" # pylint: disable=import-error from homekit.model.services import ServicesTypes + from homekit.exceptions import AccessoryDisconnectedError self.pairing.pairing_data['AccessoryIP'] = self.host self.pairing.pairing_data['AccessoryPort'] = self.port try: data = self.pairing.list_accessories_and_characteristics() - except HomeKitConnectionError: + except AccessoryDisconnectedError: call_later( self.hass, RETRY_INTERVAL, lambda _: self.accessory_setup()) return @@ -199,10 +196,13 @@ class HomeKitEntity(Entity): def update(self): """Obtain a HomeKit device's state.""" + # pylint: disable=import-error + from homekit.exceptions import AccessoryDisconnectedError + try: pairing = self._accessory.pairing data = pairing.list_accessories_and_characteristics() - except HomeKitConnectionError: + except AccessoryDisconnectedError: return for accessory in data: if accessory['aid'] != self._aid: diff --git a/requirements_all.txt b/requirements_all.txt index 6ea29d243c9..41c1d081b7e 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -526,7 +526,7 @@ home-assistant-frontend==20190121.1 homeassistant-pyozw==0.1.2 # homeassistant.components.homekit_controller -# homekit==0.12.0 +# homekit==0.12.2 # homeassistant.components.homematicip_cloud homematicip==0.10.3 From 0300ef2040c680efcb47afb8e1b9a23602712d7c Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 23 Jan 2019 16:33:21 -0800 Subject: [PATCH 073/222] Fix entity registry comments (#20357) --- homeassistant/helpers/entity_registry.py | 4 ++-- tests/helpers/test_entity_registry.py | 9 +++++++++ 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/homeassistant/helpers/entity_registry.py b/homeassistant/helpers/entity_registry.py index 82530708838..e4f266854ef 100644 --- a/homeassistant/helpers/entity_registry.py +++ b/homeassistant/helpers/entity_registry.py @@ -127,8 +127,8 @@ class EntityRegistry: device_id=device_id, # When we changed our slugify algorithm, we invalidated some # stored entity IDs with either a __ or ending in _. - # Fix introduced in 0.86 (Jan 23, 2018). Next line can be - # removed when we release 1.0 or in 2019. + # Fix introduced in 0.86 (Jan 23, 2019). Next line can be + # removed when we release 1.0 or in 2020. new_entity_id='.'.join(slugify(part) for part in entity_id.split('.', 1))) diff --git a/tests/helpers/test_entity_registry.py b/tests/helpers/test_entity_registry.py index ef7b4a60ee2..b1c13a36c6d 100644 --- a/tests/helpers/test_entity_registry.py +++ b/tests/helpers/test_entity_registry.py @@ -240,6 +240,10 @@ async def test_loading_invalid_entity_id(hass, hass_storage): 'entity_id': 'test.invalid_end_', 'platform': 'super_platform', 'unique_id': 'id-invalid-end', + }, { + 'entity_id': 'test._invalid_start', + 'platform': 'super_platform', + 'unique_id': 'id-invalid-start', } ] } @@ -256,3 +260,8 @@ async def test_loading_invalid_entity_id(hass, hass_storage): 'test', 'super_platform', 'id-invalid-end') assert valid_entity_id(entity_invalid_end.entity_id) + + entity_invalid_start = registry.async_get_or_create( + 'test', 'super_platform', 'id-invalid-start') + + assert valid_entity_id(entity_invalid_start.entity_id) From 971d933140b5112e1c9813fcc280d61497ef6f46 Mon Sep 17 00:00:00 2001 From: Eliseo Martelli Date: Thu, 24 Jan 2019 02:05:16 +0100 Subject: [PATCH 074/222] [FIX] Time reporting incorrect in sensor.gtt (#20362) * quick fix * remove print statement * fixes * remove lambda * added pylint disable * should be fine now --- homeassistant/components/sensor/gtt.py | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/sensor/gtt.py b/homeassistant/components/sensor/gtt.py index f0e141f3549..a64c743381d 100644 --- a/homeassistant/components/sensor/gtt.py +++ b/homeassistant/components/sensor/gtt.py @@ -79,8 +79,7 @@ class GttSensor(Entity): def update(self): """Update device state.""" self.data.get_data() - next_time = datetime.strptime( - self.data.state_bus['time'][0]['run'], "%H:%M") + next_time = get_datetime(self.data.state_bus) self._state = next_time.isoformat() @@ -99,8 +98,7 @@ class GttData: def get_data(self): """Get the data from the api.""" self.bus_list = self._pygtt.get_by_stop(self._stop) - self.bus_list.sort(key=lambda b: - datetime.strptime(b['time'][0]['run'], "%H:%M")) + self.bus_list.sort(key=get_datetime) if self._bus_name is not None: self.state_bus = self.get_bus_by_name() @@ -113,3 +111,13 @@ class GttData: for bus in self.bus_list: if bus['bus_name'] == self._bus_name: return bus + + +def get_datetime(bus): + """Get the datetime from a bus.""" + bustime = datetime.strptime(bus['time'][0]['run'], "%H:%M") + now = datetime.now() + bustime = bustime.replace(year=now.year, month=now.month, day=now.day) + if bustime < now: + bustime = bustime + timedelta(days=1) + return bustime From 697c331903f8a440a4ce324a4fb0788351dc86c3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20H=C3=B8yer=20Iversen?= Date: Thu, 24 Jan 2019 02:05:56 +0100 Subject: [PATCH 075/222] Clean up concord232 (#20353) * Clean up concord232 * concord cleanup * clean up * fix import --- .../alarm_control_panel/concord232.py | 40 ++++++------------- 1 file changed, 13 insertions(+), 27 deletions(-) diff --git a/homeassistant/components/alarm_control_panel/concord232.py b/homeassistant/components/alarm_control_panel/concord232.py index 015b3cfce33..564fb0f1630 100644 --- a/homeassistant/components/alarm_control_panel/concord232.py +++ b/homeassistant/components/alarm_control_panel/concord232.py @@ -5,18 +5,18 @@ For more details about this platform, please refer to the documentation at https://home-assistant.io/components/alarm_control_panel.concord232/ """ import datetime -from datetime import timedelta import logging import requests import voluptuous as vol import homeassistant.components.alarm_control_panel as alarm +import homeassistant.helpers.config_validation as cv from homeassistant.components.alarm_control_panel import PLATFORM_SCHEMA from homeassistant.const import ( CONF_HOST, CONF_NAME, CONF_PORT, STATE_ALARM_ARMED_AWAY, - STATE_ALARM_ARMED_HOME, STATE_ALARM_DISARMED, STATE_UNKNOWN) -import homeassistant.helpers.config_validation as cv + STATE_ALARM_ARMED_HOME, STATE_ALARM_DISARMED) + REQUIREMENTS = ['concord232==0.15'] @@ -26,7 +26,7 @@ DEFAULT_HOST = 'localhost' DEFAULT_NAME = 'CONCORD232' DEFAULT_PORT = 5007 -SCAN_INTERVAL = timedelta(seconds=10) +SCAN_INTERVAL = datetime.timedelta(seconds=10) PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Optional(CONF_HOST, default=DEFAULT_HOST): cv.string, @@ -44,33 +44,24 @@ def setup_platform(hass, config, add_entities, discovery_info=None): url = 'http://{}:{}'.format(host, port) try: - add_entities([Concord232Alarm(hass, url, name)]) + add_entities([Concord232Alarm(url, name)], True) except requests.exceptions.ConnectionError as ex: _LOGGER.error("Unable to connect to Concord232: %s", str(ex)) - return class Concord232Alarm(alarm.AlarmControlPanel): """Representation of the Concord232-based alarm panel.""" - def __init__(self, hass, url, name): + def __init__(self, url, name): """Initialize the Concord232 alarm panel.""" from concord232 import client as concord232_client - self._state = STATE_UNKNOWN - self._hass = hass + self._state = None self._name = name self._url = url - - try: - client = concord232_client.Client(self._url) - except requests.exceptions.ConnectionError as ex: - _LOGGER.error("Unable to connect to Concord232: %s", str(ex)) - - self._alarm = client + self._alarm = concord232_client.Client(self._url) self._alarm.partitions = self._alarm.list_partitions() self._alarm.last_partition_update = datetime.datetime.now() - self.update() @property def name(self): @@ -94,22 +85,17 @@ class Concord232Alarm(alarm.AlarmControlPanel): except requests.exceptions.ConnectionError as ex: _LOGGER.error("Unable to connect to %(host)s: %(reason)s", dict(host=self._url, reason=ex)) - newstate = STATE_UNKNOWN + return except IndexError: _LOGGER.error("Concord232 reports no partitions") - newstate = STATE_UNKNOWN + return if part['arming_level'] == 'Off': - newstate = STATE_ALARM_DISARMED + self._state = STATE_ALARM_DISARMED elif 'Home' in part['arming_level']: - newstate = STATE_ALARM_ARMED_HOME + self._state = STATE_ALARM_ARMED_HOME else: - newstate = STATE_ALARM_ARMED_AWAY - - if not newstate == self._state: - _LOGGER.info("State change from %s to %s", self._state, newstate) - self._state = newstate - return self._state + self._state = STATE_ALARM_ARMED_AWAY def alarm_disarm(self, code=None): """Send disarm command.""" From af3afb673a6a9ea3d53d7e55f65f5ba2107ea1a0 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 23 Jan 2019 21:12:38 -0800 Subject: [PATCH 076/222] Fix restore state crashing invalid entity ID (#20367) --- homeassistant/helpers/restore_state.py | 6 +++-- tests/helpers/test_restore_state.py | 34 +++++++++++++++++++++++++- 2 files changed, 37 insertions(+), 3 deletions(-) diff --git a/homeassistant/helpers/restore_state.py b/homeassistant/helpers/restore_state.py index 33b612b555a..355555ec9dc 100644 --- a/homeassistant/helpers/restore_state.py +++ b/homeassistant/helpers/restore_state.py @@ -4,7 +4,8 @@ import logging from datetime import timedelta, datetime from typing import Any, Dict, List, Set, Optional # noqa pylint_disable=unused-import -from homeassistant.core import HomeAssistant, callback, State, CoreState +from homeassistant.core import ( + HomeAssistant, callback, State, CoreState, valid_entity_id) from homeassistant.const import ( EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP) import homeassistant.util.dt as dt_util @@ -80,7 +81,8 @@ class RestoreStateData(): else: data.last_states = { item['state']['entity_id']: StoredState.from_dict(item) - for item in stored_states} + for item in stored_states + if valid_entity_id(item['state']['entity_id'])} _LOGGER.debug( 'Created cache with %s', list(data.last_states)) diff --git a/tests/helpers/test_restore_state.py b/tests/helpers/test_restore_state.py index b13bc87421b..bc2ab6937c3 100644 --- a/tests/helpers/test_restore_state.py +++ b/tests/helpers/test_restore_state.py @@ -6,7 +6,8 @@ from homeassistant.core import CoreState, State from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.entity import Entity from homeassistant.helpers.restore_state import ( - RestoreStateData, RestoreEntity, StoredState, DATA_RESTORE_STATE_TASK) + RestoreStateData, RestoreEntity, StoredState, DATA_RESTORE_STATE_TASK, + STORAGE_KEY) from homeassistant.util import dt as dt_util from asynctest import patch @@ -218,3 +219,34 @@ async def test_state_saved_on_remove(hass): # We should store the input boolean state when it is removed assert data.last_states['input_boolean.b0'].state.state == 'on' + + +async def test_restoring_invalid_entity_id(hass, hass_storage): + """Test restoring invalid entity IDs.""" + entity = RestoreEntity() + entity.hass = hass + entity.entity_id = 'test.invalid__entity_id' + now = dt_util.utcnow().isoformat() + hass_storage[STORAGE_KEY] = { + 'version': 1, + 'key': STORAGE_KEY, + 'data': [ + { + 'state': { + 'entity_id': 'test.invalid__entity_id', + 'state': 'off', + 'attributes': {}, + 'last_changed': now, + 'last_updated': now, + 'context': { + 'id': '3c2243ff5f30447eb12e7348cfd5b8ff', + 'user_id': None + } + }, + 'last_seen': dt_util.utcnow().isoformat() + } + ] + } + + state = await entity.async_get_last_state() + assert state is None From 5580bec1d3df5e40a366a38ee33203232714bfbc Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 23 Jan 2019 21:13:55 -0800 Subject: [PATCH 077/222] Calling save before load would crash Lovelace storage (#20368) --- homeassistant/components/lovelace/__init__.py | 2 ++ tests/components/lovelace/test_init.py | 21 +++++++++++++++++++ 2 files changed, 23 insertions(+) diff --git a/homeassistant/components/lovelace/__init__.py b/homeassistant/components/lovelace/__init__.py index e6f122bce19..c3254d84a73 100644 --- a/homeassistant/components/lovelace/__init__.py +++ b/homeassistant/components/lovelace/__init__.py @@ -101,6 +101,8 @@ class LovelaceStorage: async def async_save(self, config): """Save config.""" + if self._data is None: + self._data = {'config': None} self._data['config'] = config await self._store.async_save(self._data) diff --git a/tests/components/lovelace/test_init.py b/tests/components/lovelace/test_init.py index 15548b28cfb..20490f8c0cd 100644 --- a/tests/components/lovelace/test_init.py +++ b/tests/components/lovelace/test_init.py @@ -50,6 +50,27 @@ async def test_lovelace_from_storage(hass, hass_ws_client, hass_storage): } +async def test_lovelace_from_storage_save_before_load(hass, hass_ws_client, + hass_storage): + """Test we can load lovelace config from storage.""" + assert await async_setup_component(hass, 'lovelace', {}) + client = await hass_ws_client(hass) + + # Store new config + await client.send_json({ + 'id': 6, + 'type': 'lovelace/config/save', + 'config': { + 'yo': 'hello' + } + }) + response = await client.receive_json() + assert response['success'] + assert hass_storage[lovelace.STORAGE_KEY]['data'] == { + 'config': {'yo': 'hello'} + } + + async def test_lovelace_from_yaml(hass, hass_ws_client): """Test we load lovelace config from yaml.""" assert await async_setup_component(hass, 'lovelace', { From 074fcd96ed5ee801f3ca087424d8c91a6b75813a Mon Sep 17 00:00:00 2001 From: Diogo Gomes Date: Thu, 24 Jan 2019 05:14:21 +0000 Subject: [PATCH 078/222] Fix error when API doesn't return a forecast. (#20365) * add guard * wrong logic --- homeassistant/components/weather/ipma.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/homeassistant/components/weather/ipma.py b/homeassistant/components/weather/ipma.py index a2f5058ac1e..fda0fef4f25 100644 --- a/homeassistant/components/weather/ipma.py +++ b/homeassistant/components/weather/ipma.py @@ -116,6 +116,9 @@ class IPMAWeather(WeatherEntity): @property def condition(self): """Return the current condition.""" + if not self._forecast: + return + return next((k for k, v in CONDITION_CLASSES.items() if self._forecast[0].idWeatherType in v), None) From 1bd31e34598a1f57c81e65d4ef9d5d4c6b38ce14 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20H=C3=B8yer=20Iversen?= Date: Thu, 24 Jan 2019 08:20:20 +0100 Subject: [PATCH 079/222] Change STATE_UNKOWN to None (#20337) * Change STATE_UNKOWN to None * Change STATE_UNKOWN to None * tests * tests * tests * tests * tests * style * fix comments * fix comments * update fan test --- .../components/alarm_control_panel/alarmdotcom.py | 6 +++--- .../components/alarm_control_panel/concord232.py | 1 - .../components/alarm_control_panel/totalconnect.py | 6 +++--- .../components/alarm_control_panel/verisure.py | 5 ++--- .../components/alarm_control_panel/wink.py | 5 ++--- homeassistant/components/binary_sensor/maxcube.py | 3 +-- homeassistant/components/climate/__init__.py | 4 ++-- homeassistant/components/climate/nest.py | 4 ++-- homeassistant/components/cover/__init__.py | 4 ++-- homeassistant/components/cover/garadget.py | 8 ++++---- homeassistant/components/cover/wink.py | 5 ++--- homeassistant/components/fan/__init__.py | 7 +++---- homeassistant/components/fan/comfoconnect.py | 3 +-- homeassistant/components/fan/wink.py | 4 ++-- homeassistant/components/hdmi_cec.py | 4 ++-- homeassistant/components/homekit/type_locks.py | 4 ++-- .../components/image_processing/openalpr_local.py | 4 ++-- homeassistant/components/lock/__init__.py | 4 ++-- homeassistant/components/lock/verisure.py | 8 ++++---- homeassistant/components/media_player/__init__.py | 4 ++-- homeassistant/components/media_player/anthemav.py | 4 ++-- homeassistant/components/media_player/aquostv.py | 4 ++-- homeassistant/components/media_player/denon.py | 5 ++--- .../components/media_player/lg_netcast.py | 4 ++-- .../components/media_player/panasonic_bluray.py | 4 ++-- .../components/media_player/panasonic_viera.py | 5 ++--- .../components/media_player/philips_js.py | 4 ++-- homeassistant/components/media_player/pioneer.py | 5 ++--- homeassistant/components/media_player/roku.py | 8 ++++---- homeassistant/components/media_player/spotify.py | 4 ++-- .../components/media_player/squeezebox.py | 4 ++-- homeassistant/components/media_player/vizio.py | 7 +++---- homeassistant/components/media_player/webostv.py | 6 +++--- .../components/mqtt/alarm_control_panel.py | 4 ++-- homeassistant/components/mqtt/sensor.py | 6 +++--- homeassistant/components/plant.py | 2 +- homeassistant/components/sensor/amcrest.py | 4 ++-- homeassistant/components/sensor/arest.py | 6 +++--- .../components/sensor/comed_hourly_pricing.py | 4 ++-- homeassistant/components/sensor/comfoconnect.py | 5 ++--- homeassistant/components/sensor/dnsip.py | 5 ++--- homeassistant/components/sensor/dsmr.py | 8 ++++---- homeassistant/components/sensor/dweet.py | 8 ++++---- homeassistant/components/sensor/geo_rss_events.py | 4 ++-- homeassistant/components/sensor/google_wifi.py | 4 ++-- homeassistant/components/sensor/gpsd.py | 4 ++-- homeassistant/components/sensor/kwb.py | 5 ++--- homeassistant/components/sensor/london_air.py | 3 +-- homeassistant/components/sensor/min_max.py | 14 +++++++------- homeassistant/components/sensor/mvglive.py | 5 ++--- homeassistant/components/sensor/netatmo.py | 4 ++-- homeassistant/components/sensor/pilight.py | 4 ++-- homeassistant/components/sensor/rest.py | 10 ++++------ homeassistant/components/sensor/ring.py | 6 +++--- homeassistant/components/sensor/scrape.py | 6 +++--- homeassistant/components/sensor/tank_utility.py | 5 ++--- homeassistant/components/sensor/travisci.py | 6 +++--- homeassistant/components/sensor/xbox_live.py | 4 ++-- homeassistant/components/sensor/yweather.py | 4 ++-- tests/components/fan/test_init.py | 2 +- tests/components/media_player/test_universal.py | 4 ++-- tests/components/sensor/test_dsmr.py | 5 ++--- tests/components/sensor/test_google_wifi.py | 4 ++-- tests/components/sensor/test_rest.py | 3 +-- 64 files changed, 147 insertions(+), 168 deletions(-) diff --git a/homeassistant/components/alarm_control_panel/alarmdotcom.py b/homeassistant/components/alarm_control_panel/alarmdotcom.py index 03cf9c1ddf8..4f2913771b1 100644 --- a/homeassistant/components/alarm_control_panel/alarmdotcom.py +++ b/homeassistant/components/alarm_control_panel/alarmdotcom.py @@ -13,7 +13,7 @@ import homeassistant.components.alarm_control_panel as alarm from homeassistant.components.alarm_control_panel import PLATFORM_SCHEMA from homeassistant.const import ( CONF_CODE, CONF_NAME, CONF_PASSWORD, CONF_USERNAME, STATE_ALARM_ARMED_AWAY, - STATE_ALARM_ARMED_HOME, STATE_ALARM_DISARMED, STATE_UNKNOWN) + STATE_ALARM_ARMED_HOME, STATE_ALARM_DISARMED) from homeassistant.helpers.aiohttp_client import async_get_clientsession import homeassistant.helpers.config_validation as cv @@ -57,7 +57,7 @@ class AlarmDotCom(alarm.AlarmControlPanel): self._username = username self._password = password self._websession = async_get_clientsession(self._hass) - self._state = STATE_UNKNOWN + self._state = None self._alarm = Alarmdotcom( username, password, self._websession, hass.loop) @@ -93,7 +93,7 @@ class AlarmDotCom(alarm.AlarmControlPanel): return STATE_ALARM_ARMED_HOME if self._alarm.state.lower() == 'armed away': return STATE_ALARM_ARMED_AWAY - return STATE_UNKNOWN + return None @property def device_state_attributes(self): diff --git a/homeassistant/components/alarm_control_panel/concord232.py b/homeassistant/components/alarm_control_panel/concord232.py index 564fb0f1630..155d6b6ae49 100644 --- a/homeassistant/components/alarm_control_panel/concord232.py +++ b/homeassistant/components/alarm_control_panel/concord232.py @@ -17,7 +17,6 @@ from homeassistant.const import ( CONF_HOST, CONF_NAME, CONF_PORT, STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME, STATE_ALARM_DISARMED) - REQUIREMENTS = ['concord232==0.15'] _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/alarm_control_panel/totalconnect.py b/homeassistant/components/alarm_control_panel/totalconnect.py index 97f46cb0dfd..3b0725658d4 100644 --- a/homeassistant/components/alarm_control_panel/totalconnect.py +++ b/homeassistant/components/alarm_control_panel/totalconnect.py @@ -14,7 +14,7 @@ from homeassistant.components.alarm_control_panel import PLATFORM_SCHEMA from homeassistant.const import ( CONF_PASSWORD, CONF_USERNAME, STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME, STATE_ALARM_ARMED_NIGHT, STATE_ALARM_DISARMED, - STATE_ALARM_ARMING, STATE_ALARM_DISARMING, STATE_UNKNOWN, CONF_NAME, + STATE_ALARM_ARMING, STATE_ALARM_DISARMING, CONF_NAME, STATE_ALARM_ARMED_CUSTOM_BYPASS) @@ -52,7 +52,7 @@ class TotalConnect(alarm.AlarmControlPanel): self._name = name self._username = username self._password = password - self._state = STATE_UNKNOWN + self._state = None self._client = TotalConnectClient.TotalConnectClient( username, password) @@ -85,7 +85,7 @@ class TotalConnect(alarm.AlarmControlPanel): elif status == self._client.DISARMING: state = STATE_ALARM_DISARMING else: - state = STATE_UNKNOWN + state = None self._state = state diff --git a/homeassistant/components/alarm_control_panel/verisure.py b/homeassistant/components/alarm_control_panel/verisure.py index 6b381ef5a47..160f152ef8a 100644 --- a/homeassistant/components/alarm_control_panel/verisure.py +++ b/homeassistant/components/alarm_control_panel/verisure.py @@ -11,8 +11,7 @@ import homeassistant.components.alarm_control_panel as alarm from homeassistant.components.verisure import CONF_ALARM, CONF_CODE_DIGITS from homeassistant.components.verisure import HUB as hub from homeassistant.const import ( - STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME, STATE_ALARM_DISARMED, - STATE_UNKNOWN) + STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME, STATE_ALARM_DISARMED) _LOGGER = logging.getLogger(__name__) @@ -44,7 +43,7 @@ class VerisureAlarm(alarm.AlarmControlPanel): def __init__(self): """Initialize the Verisure alarm panel.""" - self._state = STATE_UNKNOWN + self._state = None self._digits = hub.config.get(CONF_CODE_DIGITS) self._changed_by = None diff --git a/homeassistant/components/alarm_control_panel/wink.py b/homeassistant/components/alarm_control_panel/wink.py index 001c6fad85c..b2ae3578133 100644 --- a/homeassistant/components/alarm_control_panel/wink.py +++ b/homeassistant/components/alarm_control_panel/wink.py @@ -9,8 +9,7 @@ import logging import homeassistant.components.alarm_control_panel as alarm from homeassistant.components.wink import DOMAIN, WinkDevice from homeassistant.const import ( - STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME, STATE_ALARM_DISARMED, - STATE_UNKNOWN) + STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME, STATE_ALARM_DISARMED) _LOGGER = logging.getLogger(__name__) @@ -52,7 +51,7 @@ class WinkCameraDevice(WinkDevice, alarm.AlarmControlPanel): elif wink_state == "night": state = STATE_ALARM_ARMED_HOME else: - state = STATE_UNKNOWN + state = None return state def alarm_disarm(self, code=None): diff --git a/homeassistant/components/binary_sensor/maxcube.py b/homeassistant/components/binary_sensor/maxcube.py index 6bb9278d8d5..850a416acc5 100644 --- a/homeassistant/components/binary_sensor/maxcube.py +++ b/homeassistant/components/binary_sensor/maxcube.py @@ -8,7 +8,6 @@ import logging from homeassistant.components.binary_sensor import BinarySensorDevice from homeassistant.components.maxcube import DATA_KEY -from homeassistant.const import STATE_UNKNOWN _LOGGER = logging.getLogger(__name__) @@ -40,7 +39,7 @@ class MaxCubeShutter(BinarySensorDevice): self._sensor_type = 'window' self._rf_address = rf_address self._cubehandle = handler - self._state = STATE_UNKNOWN + self._state = None @property def should_poll(self): diff --git a/homeassistant/components/climate/__init__.py b/homeassistant/components/climate/__init__.py index d116a885319..6d7f9432e39 100644 --- a/homeassistant/components/climate/__init__.py +++ b/homeassistant/components/climate/__init__.py @@ -18,7 +18,7 @@ from homeassistant.helpers.config_validation import PLATFORM_SCHEMA # noqa import homeassistant.helpers.config_validation as cv from homeassistant.const import ( ATTR_ENTITY_ID, ATTR_TEMPERATURE, SERVICE_TURN_ON, SERVICE_TURN_OFF, - STATE_ON, STATE_OFF, STATE_UNKNOWN, TEMP_CELSIUS, PRECISION_WHOLE, + STATE_ON, STATE_OFF, TEMP_CELSIUS, PRECISION_WHOLE, PRECISION_TENTHS) DEFAULT_MIN_TEMP = 7 @@ -208,7 +208,7 @@ class ClimateDevice(Entity): return self.current_operation if self.is_on: return STATE_ON - return STATE_UNKNOWN + return None @property def precision(self): diff --git a/homeassistant/components/climate/nest.py b/homeassistant/components/climate/nest.py index e580476e56a..bd6bb2991cc 100644 --- a/homeassistant/components/climate/nest.py +++ b/homeassistant/components/climate/nest.py @@ -18,7 +18,7 @@ from homeassistant.components.climate import ( SUPPORT_OPERATION_MODE, SUPPORT_AWAY_MODE, SUPPORT_FAN_MODE) from homeassistant.const import ( TEMP_CELSIUS, TEMP_FAHRENHEIT, - CONF_SCAN_INTERVAL, STATE_ON, STATE_OFF, STATE_UNKNOWN) + CONF_SCAN_INTERVAL, STATE_ON, STATE_OFF) from homeassistant.helpers.dispatcher import async_dispatcher_connect DEPENDENCIES = ['nest'] @@ -163,7 +163,7 @@ class NestThermostat(ClimateDevice): return self._mode if self._mode == NEST_MODE_HEAT_COOL: return STATE_AUTO - return STATE_UNKNOWN + return None @property def target_temperature(self): diff --git a/homeassistant/components/cover/__init__.py b/homeassistant/components/cover/__init__.py index ef8fcc42302..b5b2a91b097 100644 --- a/homeassistant/components/cover/__init__.py +++ b/homeassistant/components/cover/__init__.py @@ -21,7 +21,7 @@ from homeassistant.const import ( SERVICE_OPEN_COVER, SERVICE_CLOSE_COVER, SERVICE_SET_COVER_POSITION, SERVICE_STOP_COVER, SERVICE_OPEN_COVER_TILT, SERVICE_CLOSE_COVER_TILT, SERVICE_STOP_COVER_TILT, SERVICE_SET_COVER_TILT_POSITION, STATE_OPEN, - STATE_CLOSED, STATE_UNKNOWN, STATE_OPENING, STATE_CLOSING, ATTR_ENTITY_ID) + STATE_CLOSED, STATE_OPENING, STATE_CLOSING, ATTR_ENTITY_ID) _LOGGER = logging.getLogger(__name__) @@ -178,7 +178,7 @@ class CoverDevice(Entity): closed = self.is_closed if closed is None: - return STATE_UNKNOWN + return None return STATE_CLOSED if closed else STATE_OPEN diff --git a/homeassistant/components/cover/garadget.py b/homeassistant/components/cover/garadget.py index 28be3dc6b82..426afc6d314 100644 --- a/homeassistant/components/cover/garadget.py +++ b/homeassistant/components/cover/garadget.py @@ -14,7 +14,7 @@ from homeassistant.components.cover import CoverDevice, PLATFORM_SCHEMA from homeassistant.helpers.event import track_utc_time_change from homeassistant.const import ( CONF_DEVICE, CONF_USERNAME, CONF_PASSWORD, CONF_ACCESS_TOKEN, CONF_NAME, - STATE_UNKNOWN, STATE_CLOSED, STATE_OPEN, CONF_COVERS) + STATE_CLOSED, STATE_OPEN, CONF_COVERS) _LOGGER = logging.getLogger(__name__) @@ -83,7 +83,7 @@ class GaradgetCover(CoverDevice): self.obtained_token = False self._username = args['username'] self._password = args['password'] - self._state = STATE_UNKNOWN + self._state = None self.time_in_state = None self.signal = None self.sensor = None @@ -156,7 +156,7 @@ class GaradgetCover(CoverDevice): @property def is_closed(self): """Return if the cover is closed.""" - if self._state == STATE_UNKNOWN: + if self._state is None: return None return self._state == STATE_CLOSED @@ -226,7 +226,7 @@ class GaradgetCover(CoverDevice): try: status = self._get_variable('doorStatus') _LOGGER.debug("Current Status: %s", status['status']) - self._state = STATES_MAP.get(status['status'], STATE_UNKNOWN) + self._state = STATES_MAP.get(status['status'], None) self.time_in_state = status['time'] self.signal = status['signal'] self.sensor = status['sensor'] diff --git a/homeassistant/components/cover/wink.py b/homeassistant/components/cover/wink.py index 857283b9b6c..3cf9c753e3a 100644 --- a/homeassistant/components/cover/wink.py +++ b/homeassistant/components/cover/wink.py @@ -4,8 +4,7 @@ Support for Wink Covers. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/cover.wink/ """ -from homeassistant.components.cover import CoverDevice, STATE_UNKNOWN, \ - ATTR_POSITION +from homeassistant.components.cover import CoverDevice, ATTR_POSITION from homeassistant.components.wink import WinkDevice, DOMAIN DEPENDENCIES = ['wink'] @@ -54,7 +53,7 @@ class WinkCoverDevice(WinkDevice, CoverDevice): """Return the current position of cover shutter.""" if self.wink.state() is not None: return int(self.wink.state()*100) - return STATE_UNKNOWN + return None @property def is_closed(self): diff --git a/homeassistant/components/fan/__init__.py b/homeassistant/components/fan/__init__.py index a54d52f4b12..3525b95c007 100644 --- a/homeassistant/components/fan/__init__.py +++ b/homeassistant/components/fan/__init__.py @@ -12,8 +12,7 @@ import voluptuous as vol from homeassistant.components import group from homeassistant.const import (SERVICE_TURN_ON, SERVICE_TOGGLE, - SERVICE_TURN_OFF, ATTR_ENTITY_ID, - STATE_UNKNOWN) + SERVICE_TURN_OFF, ATTR_ENTITY_ID) from homeassistant.loader import bind_hass from homeassistant.helpers.entity import ToggleEntity from homeassistant.helpers.entity_component import EntityComponent @@ -94,7 +93,7 @@ def is_on(hass, entity_id: str = None) -> bool: """Return if the fans are on based on the statemachine.""" entity_id = entity_id or ENTITY_ID_ALL_FANS state = hass.states.get(entity_id) - return state.attributes[ATTR_SPEED] not in [SPEED_OFF, STATE_UNKNOWN] + return state.attributes[ATTR_SPEED] not in [SPEED_OFF, None] async def async_setup(hass, config: dict): @@ -199,7 +198,7 @@ class FanEntity(ToggleEntity): @property def is_on(self): """Return true if the entity is on.""" - return self.speed not in [SPEED_OFF, STATE_UNKNOWN] + return self.speed not in [SPEED_OFF, None] @property def speed(self) -> str: diff --git a/homeassistant/components/fan/comfoconnect.py b/homeassistant/components/fan/comfoconnect.py index a1f13da6c09..0e0ac8c80b6 100644 --- a/homeassistant/components/fan/comfoconnect.py +++ b/homeassistant/components/fan/comfoconnect.py @@ -11,7 +11,6 @@ from homeassistant.components.comfoconnect import ( from homeassistant.components.fan import ( FanEntity, SPEED_OFF, SPEED_LOW, SPEED_MEDIUM, SPEED_HIGH, SUPPORT_SET_SPEED) -from homeassistant.const import STATE_UNKNOWN from homeassistant.helpers.dispatcher import (dispatcher_connect) _LOGGER = logging.getLogger(__name__) @@ -79,7 +78,7 @@ class ComfoConnectFan(FanEntity): speed = self._ccb.data[SENSOR_FAN_SPEED_MODE] return SPEED_MAPPING[speed] except KeyError: - return STATE_UNKNOWN + return None @property def speed_list(self): diff --git a/homeassistant/components/fan/wink.py b/homeassistant/components/fan/wink.py index d0dc386d74d..eca985a8d1e 100644 --- a/homeassistant/components/fan/wink.py +++ b/homeassistant/components/fan/wink.py @@ -7,7 +7,7 @@ https://home-assistant.io/components/fan.wink/ import logging from homeassistant.components.fan import ( - SPEED_HIGH, SPEED_LOW, SPEED_MEDIUM, STATE_UNKNOWN, SUPPORT_DIRECTION, + SPEED_HIGH, SPEED_LOW, SPEED_MEDIUM, SUPPORT_DIRECTION, SUPPORT_SET_SPEED, FanEntity) from homeassistant.components.wink import DOMAIN, WinkDevice @@ -71,7 +71,7 @@ class WinkFanDevice(WinkDevice, FanEntity): return SPEED_MEDIUM if SPEED_HIGH == current_wink_speed: return SPEED_HIGH - return STATE_UNKNOWN + return None @property def current_direction(self): diff --git a/homeassistant/components/hdmi_cec.py b/homeassistant/components/hdmi_cec.py index a630a9ef1ad..5fb2e19edcf 100644 --- a/homeassistant/components/hdmi_cec.py +++ b/homeassistant/components/hdmi_cec.py @@ -15,7 +15,7 @@ import homeassistant.helpers.config_validation as cv from homeassistant.helpers import discovery from homeassistant.components.media_player import DOMAIN as MEDIA_PLAYER from homeassistant.components.switch import DOMAIN as SWITCH -from homeassistant.const import (EVENT_HOMEASSISTANT_START, STATE_UNKNOWN, +from homeassistant.const import (EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP, STATE_ON, STATE_OFF, CONF_DEVICES, CONF_PLATFORM, STATE_PLAYING, STATE_IDLE, @@ -324,7 +324,7 @@ class CecDevice(Entity): """Initialize the device.""" self._device = device self._icon = None - self._state = STATE_UNKNOWN + self._state = None self._logical_address = logical self.entity_id = "%s.%d" % (DOMAIN, self._logical_address) diff --git a/homeassistant/components/homekit/type_locks.py b/homeassistant/components/homekit/type_locks.py index fb211617ecf..22c47d59c62 100644 --- a/homeassistant/components/homekit/type_locks.py +++ b/homeassistant/components/homekit/type_locks.py @@ -4,8 +4,8 @@ import logging from pyhap.const import CATEGORY_DOOR_LOCK from homeassistant.components.lock import ( - ATTR_ENTITY_ID, DOMAIN, STATE_LOCKED, STATE_UNLOCKED, STATE_UNKNOWN) -from homeassistant.const import ATTR_CODE + ATTR_ENTITY_ID, DOMAIN, STATE_LOCKED, STATE_UNLOCKED) +from homeassistant.const import ATTR_CODE, STATE_UNKNOWN from . import TYPES from .accessories import HomeAccessory diff --git a/homeassistant/components/image_processing/openalpr_local.py b/homeassistant/components/image_processing/openalpr_local.py index 9d5ebf2e2b9..4a98594d50c 100644 --- a/homeassistant/components/image_processing/openalpr_local.py +++ b/homeassistant/components/image_processing/openalpr_local.py @@ -13,7 +13,7 @@ import voluptuous as vol import homeassistant.helpers.config_validation as cv from homeassistant.core import split_entity_id, callback -from homeassistant.const import STATE_UNKNOWN, CONF_REGION +from homeassistant.const import CONF_REGION from homeassistant.components.image_processing import ( PLATFORM_SCHEMA, ImageProcessingEntity, CONF_CONFIDENCE, CONF_SOURCE, CONF_ENTITY_ID, CONF_NAME, ATTR_ENTITY_ID, ATTR_CONFIDENCE) @@ -82,7 +82,7 @@ class ImageProcessingAlprEntity(ImageProcessingEntity): def state(self): """Return the state of the entity.""" confidence = 0 - plate = STATE_UNKNOWN + plate = None # search high plate for i_pl, i_co in self.plates.items(): diff --git a/homeassistant/components/lock/__init__.py b/homeassistant/components/lock/__init__.py index 72e87f763d2..750977fac87 100644 --- a/homeassistant/components/lock/__init__.py +++ b/homeassistant/components/lock/__init__.py @@ -17,7 +17,7 @@ from homeassistant.helpers.config_validation import PLATFORM_SCHEMA # noqa import homeassistant.helpers.config_validation as cv from homeassistant.const import ( ATTR_CODE, ATTR_CODE_FORMAT, ATTR_ENTITY_ID, STATE_LOCKED, STATE_UNLOCKED, - STATE_UNKNOWN, SERVICE_LOCK, SERVICE_UNLOCK, SERVICE_OPEN) + SERVICE_LOCK, SERVICE_UNLOCK, SERVICE_OPEN) from homeassistant.components import group ATTR_CHANGED_BY = 'changed_by' @@ -150,5 +150,5 @@ class LockDevice(Entity): """Return the state.""" locked = self.is_locked if locked is None: - return STATE_UNKNOWN + return None return STATE_LOCKED if locked else STATE_UNLOCKED diff --git a/homeassistant/components/lock/verisure.py b/homeassistant/components/lock/verisure.py index 25c7e1aa8ea..cf7d58b17a8 100644 --- a/homeassistant/components/lock/verisure.py +++ b/homeassistant/components/lock/verisure.py @@ -12,7 +12,7 @@ from homeassistant.components.verisure import ( CONF_LOCKS, CONF_DEFAULT_LOCK_CODE, CONF_CODE_DIGITS) from homeassistant.components.lock import LockDevice from homeassistant.const import ( - ATTR_CODE, STATE_LOCKED, STATE_UNKNOWN, STATE_UNLOCKED) + ATTR_CODE, STATE_LOCKED, STATE_UNLOCKED) _LOGGER = logging.getLogger(__name__) @@ -36,7 +36,7 @@ class VerisureDoorlock(LockDevice): def __init__(self, device_label): """Initialize the Verisure lock.""" self._device_label = device_label - self._state = STATE_UNKNOWN + self._state = None self._digits = hub.config.get(CONF_CODE_DIGITS) self._changed_by = None self._change_timestamp = 0 @@ -80,7 +80,7 @@ class VerisureDoorlock(LockDevice): "$.doorLockStatusList[?(@.deviceLabel=='%s')].lockedState", self._device_label) if status == 'UNLOCKED': - self._state = STATE_UNLOCKED + self._state = None elif status == 'LOCKED': self._state = STATE_LOCKED elif status != 'PENDING': @@ -96,7 +96,7 @@ class VerisureDoorlock(LockDevice): def unlock(self, **kwargs): """Send unlock command.""" - if self._state == STATE_UNLOCKED: + if self._state is None: return code = kwargs.get(ATTR_CODE, self._default_lock_code) diff --git a/homeassistant/components/media_player/__init__.py b/homeassistant/components/media_player/__init__.py index cd109cce7d3..b526d1659ba 100644 --- a/homeassistant/components/media_player/__init__.py +++ b/homeassistant/components/media_player/__init__.py @@ -27,7 +27,7 @@ from homeassistant.const import ( SERVICE_MEDIA_SEEK, SERVICE_MEDIA_STOP, SERVICE_SHUFFLE_SET, SERVICE_TOGGLE, SERVICE_TURN_OFF, SERVICE_TURN_ON, SERVICE_VOLUME_DOWN, SERVICE_VOLUME_MUTE, SERVICE_VOLUME_SET, SERVICE_VOLUME_UP, STATE_IDLE, - STATE_OFF, STATE_PLAYING, STATE_UNKNOWN) + STATE_OFF, STATE_PLAYING) from homeassistant.helpers.aiohttp_client import async_get_clientsession import homeassistant.helpers.config_validation as cv from homeassistant.helpers.config_validation import PLATFORM_SCHEMA # noqa @@ -317,7 +317,7 @@ class MediaPlayerDevice(Entity): @property def state(self): """State of the player.""" - return STATE_UNKNOWN + return None @property def access_token(self): diff --git a/homeassistant/components/media_player/anthemav.py b/homeassistant/components/media_player/anthemav.py index 0a9f208dae4..a0bc3d05dcb 100644 --- a/homeassistant/components/media_player/anthemav.py +++ b/homeassistant/components/media_player/anthemav.py @@ -13,7 +13,7 @@ from homeassistant.components.media_player import ( SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET, MediaPlayerDevice) from homeassistant.const import ( CONF_HOST, CONF_NAME, CONF_PORT, EVENT_HOMEASSISTANT_STOP, STATE_OFF, - STATE_ON, STATE_UNKNOWN) + STATE_ON) import homeassistant.helpers.config_validation as cv REQUIREMENTS = ['anthemav==1.1.8'] @@ -100,7 +100,7 @@ class AnthemAVR(MediaPlayerDevice): return STATE_ON if pwrstate is False: return STATE_OFF - return STATE_UNKNOWN + return None @property def is_volume_muted(self): diff --git a/homeassistant/components/media_player/aquostv.py b/homeassistant/components/media_player/aquostv.py index ac399307126..5c1994e65fc 100644 --- a/homeassistant/components/media_player/aquostv.py +++ b/homeassistant/components/media_player/aquostv.py @@ -15,7 +15,7 @@ from homeassistant.components.media_player import ( SUPPORT_VOLUME_STEP, MediaPlayerDevice) from homeassistant.const import ( CONF_HOST, CONF_NAME, CONF_PASSWORD, CONF_PORT, CONF_TIMEOUT, - CONF_USERNAME, STATE_OFF, STATE_ON, STATE_UNKNOWN) + CONF_USERNAME, STATE_OFF, STATE_ON) import homeassistant.helpers.config_validation as cv REQUIREMENTS = ['sharp_aquos_rc==0.3.2'] @@ -113,7 +113,7 @@ class SharpAquosTVDevice(MediaPlayerDevice): self._name = name # Assume that the TV is not muted self._muted = False - self._state = STATE_UNKNOWN + self._state = None self._remote = remote self._volume = 0 self._source = None diff --git a/homeassistant/components/media_player/denon.py b/homeassistant/components/media_player/denon.py index c0f296c2fb8..79b69b551ce 100644 --- a/homeassistant/components/media_player/denon.py +++ b/homeassistant/components/media_player/denon.py @@ -14,8 +14,7 @@ from homeassistant.components.media_player import ( SUPPORT_PREVIOUS_TRACK, SUPPORT_SELECT_SOURCE, SUPPORT_STOP, SUPPORT_TURN_OFF, SUPPORT_TURN_ON, SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET, MediaPlayerDevice) -from homeassistant.const import ( - CONF_HOST, CONF_NAME, STATE_OFF, STATE_ON, STATE_UNKNOWN) +from homeassistant.const import CONF_HOST, CONF_NAME, STATE_OFF, STATE_ON import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) @@ -169,7 +168,7 @@ class DenonDevice(MediaPlayerDevice): if self._pwstate == 'PWON': return STATE_ON - return STATE_UNKNOWN + return None @property def volume_level(self): diff --git a/homeassistant/components/media_player/lg_netcast.py b/homeassistant/components/media_player/lg_netcast.py index 92f48411401..c2f63c71f89 100644 --- a/homeassistant/components/media_player/lg_netcast.py +++ b/homeassistant/components/media_player/lg_netcast.py @@ -18,7 +18,7 @@ from homeassistant.components.media_player import ( MediaPlayerDevice) from homeassistant.const import ( CONF_ACCESS_TOKEN, CONF_HOST, CONF_NAME, STATE_OFF, STATE_PAUSED, - STATE_PLAYING, STATE_UNKNOWN) + STATE_PLAYING) import homeassistant.helpers.config_validation as cv REQUIREMENTS = ['pylgnetcast-homeassistant==0.2.0.dev0'] @@ -68,7 +68,7 @@ class LgTVDevice(MediaPlayerDevice): self._volume = 0 self._channel_name = '' self._program_name = '' - self._state = STATE_UNKNOWN + self._state = None self._sources = {} self._source_names = [] diff --git a/homeassistant/components/media_player/panasonic_bluray.py b/homeassistant/components/media_player/panasonic_bluray.py index bcd34f162c7..041efed74bf 100644 --- a/homeassistant/components/media_player/panasonic_bluray.py +++ b/homeassistant/components/media_player/panasonic_bluray.py @@ -13,7 +13,7 @@ from homeassistant.components.media_player import ( PLATFORM_SCHEMA, SUPPORT_PAUSE, SUPPORT_PLAY, SUPPORT_STOP, SUPPORT_TURN_OFF, SUPPORT_TURN_ON, MediaPlayerDevice) from homeassistant.const import ( - CONF_HOST, CONF_NAME, STATE_IDLE, STATE_OFF, STATE_PLAYING, STATE_UNKNOWN) + CONF_HOST, CONF_NAME, STATE_IDLE, STATE_OFF, STATE_PLAYING) import homeassistant.helpers.config_validation as cv from homeassistant.util.dt import utcnow @@ -101,7 +101,7 @@ class PanasonicBluRay(MediaPlayerDevice): state = self._device.get_play_status() if state[0] == 'error': - self._state = STATE_UNKNOWN + self._state = None elif state[0] in ['off', 'standby']: # We map both of these to off. If it's really off we can't # turn it on, but from standby we can go to idle by pressing diff --git a/homeassistant/components/media_player/panasonic_viera.py b/homeassistant/components/media_player/panasonic_viera.py index d3e56c4dfb1..bff108d70d7 100644 --- a/homeassistant/components/media_player/panasonic_viera.py +++ b/homeassistant/components/media_player/panasonic_viera.py @@ -14,8 +14,7 @@ from homeassistant.components.media_player import ( SUPPORT_TURN_OFF, SUPPORT_TURN_ON, SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET, SUPPORT_VOLUME_STEP, MediaPlayerDevice) from homeassistant.const import ( - CONF_HOST, CONF_MAC, CONF_NAME, CONF_PORT, STATE_OFF, STATE_ON, - STATE_UNKNOWN) + CONF_HOST, CONF_MAC, CONF_NAME, CONF_PORT, STATE_OFF, STATE_ON) import homeassistant.helpers.config_validation as cv REQUIREMENTS = ['panasonic_viera==0.3.1', 'wakeonlan==1.1.6'] @@ -81,7 +80,7 @@ class PanasonicVieraTVDevice(MediaPlayerDevice): self._uuid = uuid self._muted = False self._playing = True - self._state = STATE_UNKNOWN + self._state = None self._remote = remote self._volume = 0 diff --git a/homeassistant/components/media_player/philips_js.py b/homeassistant/components/media_player/philips_js.py index 7d434ab480e..506e5a9e479 100644 --- a/homeassistant/components/media_player/philips_js.py +++ b/homeassistant/components/media_player/philips_js.py @@ -15,7 +15,7 @@ from homeassistant.components.media_player import ( SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET, SUPPORT_VOLUME_STEP, MediaPlayerDevice) from homeassistant.const import ( - CONF_API_VERSION, CONF_HOST, CONF_NAME, STATE_OFF, STATE_ON, STATE_UNKNOWN) + CONF_API_VERSION, CONF_HOST, CONF_NAME, STATE_OFF, STATE_ON) import homeassistant.helpers.config_validation as cv from homeassistant.helpers.script import Script from homeassistant.util import Throttle @@ -70,7 +70,7 @@ class PhilipsTV(MediaPlayerDevice): """Initialize the Philips TV.""" self._tv = tv self._name = name - self._state = STATE_UNKNOWN + self._state = None self._min_volume = None self._max_volume = None self._volume = None diff --git a/homeassistant/components/media_player/pioneer.py b/homeassistant/components/media_player/pioneer.py index 29e4068f1d4..171343d4adb 100644 --- a/homeassistant/components/media_player/pioneer.py +++ b/homeassistant/components/media_player/pioneer.py @@ -14,8 +14,7 @@ from homeassistant.components.media_player import ( SUPPORT_TURN_OFF, SUPPORT_TURN_ON, SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET, MediaPlayerDevice) from homeassistant.const import ( - CONF_HOST, CONF_NAME, CONF_PORT, CONF_TIMEOUT, STATE_OFF, STATE_ON, - STATE_UNKNOWN) + CONF_HOST, CONF_NAME, CONF_PORT, CONF_TIMEOUT, STATE_OFF, STATE_ON) import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) @@ -157,7 +156,7 @@ class PioneerDevice(MediaPlayerDevice): if self._pwstate == "PWR0": return STATE_ON - return STATE_UNKNOWN + return None @property def volume_level(self): diff --git a/homeassistant/components/media_player/roku.py b/homeassistant/components/media_player/roku.py index 20a6f42d729..9dc1151064d 100644 --- a/homeassistant/components/media_player/roku.py +++ b/homeassistant/components/media_player/roku.py @@ -11,8 +11,8 @@ from homeassistant.components.media_player import ( MEDIA_TYPE_MOVIE, SUPPORT_NEXT_TRACK, SUPPORT_PLAY, SUPPORT_PLAY_MEDIA, SUPPORT_PREVIOUS_TRACK, SUPPORT_SELECT_SOURCE, SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET, MediaPlayerDevice) -from homeassistant.const import ( - CONF_HOST, STATE_HOME, STATE_IDLE, STATE_PLAYING, STATE_UNKNOWN) +from homeassistant.const import (CONF_HOST, STATE_HOME, STATE_IDLE, + STATE_PLAYING) DEPENDENCIES = ['roku'] @@ -83,7 +83,7 @@ class RokuDevice(MediaPlayerDevice): def state(self): """Return the state of the device.""" if self.current_app is None: - return STATE_UNKNOWN + return None if (self.current_app.name == "Power Saver" or self.current_app.is_screensaver): @@ -93,7 +93,7 @@ class RokuDevice(MediaPlayerDevice): if self.current_app.name is not None: return STATE_PLAYING - return STATE_UNKNOWN + return None @property def supported_features(self): diff --git a/homeassistant/components/media_player/spotify.py b/homeassistant/components/media_player/spotify.py index 8a4ffeeb157..4fbd43f3f16 100644 --- a/homeassistant/components/media_player/spotify.py +++ b/homeassistant/components/media_player/spotify.py @@ -16,7 +16,7 @@ from homeassistant.components.media_player import ( SUPPORT_SELECT_SOURCE, SUPPORT_SHUFFLE_SET, SUPPORT_VOLUME_SET, MediaPlayerDevice) from homeassistant.const import ( - CONF_NAME, STATE_IDLE, STATE_PAUSED, STATE_PLAYING, STATE_UNKNOWN) + CONF_NAME, STATE_IDLE, STATE_PAUSED, STATE_PLAYING) from homeassistant.core import callback import homeassistant.helpers.config_validation as cv @@ -132,7 +132,7 @@ class SpotifyMediaPlayer(MediaPlayerDevice): self._artist = None self._uri = None self._image_url = None - self._state = STATE_UNKNOWN + self._state = None self._current_device = None self._devices = {} self._volume = None diff --git a/homeassistant/components/media_player/squeezebox.py b/homeassistant/components/media_player/squeezebox.py index f8347830141..73b6a070419 100644 --- a/homeassistant/components/media_player/squeezebox.py +++ b/homeassistant/components/media_player/squeezebox.py @@ -21,7 +21,7 @@ from homeassistant.components.media_player import ( SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET, MediaPlayerDevice) from homeassistant.const import ( ATTR_COMMAND, CONF_HOST, CONF_PASSWORD, CONF_PORT, CONF_USERNAME, - STATE_IDLE, STATE_OFF, STATE_PAUSED, STATE_PLAYING, STATE_UNKNOWN) + STATE_IDLE, STATE_OFF, STATE_PAUSED, STATE_PLAYING) from homeassistant.helpers.aiohttp_client import async_get_clientsession import homeassistant.helpers.config_validation as cv from homeassistant.util.dt import utcnow @@ -242,7 +242,7 @@ class SqueezeBoxDevice(MediaPlayerDevice): return STATE_PLAYING if self._status['mode'] == 'stop': return STATE_IDLE - return STATE_UNKNOWN + return None def async_query(self, *parameters): """Send a command to the LMS. diff --git a/homeassistant/components/media_player/vizio.py b/homeassistant/components/media_player/vizio.py index e3f426cc5c6..5aae8661bd8 100644 --- a/homeassistant/components/media_player/vizio.py +++ b/homeassistant/components/media_player/vizio.py @@ -16,8 +16,7 @@ from homeassistant.components.media_player import ( SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET, SUPPORT_VOLUME_STEP, MediaPlayerDevice) from homeassistant.const import ( - CONF_ACCESS_TOKEN, CONF_HOST, CONF_NAME, STATE_OFF, STATE_ON, - STATE_UNKNOWN) + CONF_ACCESS_TOKEN, CONF_HOST, CONF_NAME, STATE_OFF, STATE_ON) from homeassistant.helpers import config_validation as cv REQUIREMENTS = ['pyvizio==0.0.4'] @@ -82,7 +81,7 @@ class VizioDevice(MediaPlayerDevice): import pyvizio self._device = pyvizio.Vizio(DEVICE_ID, host, DEFAULT_NAME, token) self._name = name - self._state = STATE_UNKNOWN + self._state = None self._volume_level = None self._volume_step = volume_step self._current_input = None @@ -93,7 +92,7 @@ class VizioDevice(MediaPlayerDevice): """Retrieve latest state of the TV.""" is_on = self._device.get_power_state() if is_on is None: - self._state = STATE_UNKNOWN + self._state = None return if is_on is False: self._state = STATE_OFF diff --git a/homeassistant/components/media_player/webostv.py b/homeassistant/components/media_player/webostv.py index 946e0517435..f80e29d35a0 100644 --- a/homeassistant/components/media_player/webostv.py +++ b/homeassistant/components/media_player/webostv.py @@ -20,7 +20,7 @@ from homeassistant.components.media_player import ( SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_STEP, MediaPlayerDevice) from homeassistant.const import ( CONF_CUSTOMIZE, CONF_FILENAME, CONF_HOST, CONF_NAME, CONF_TIMEOUT, - STATE_OFF, STATE_PAUSED, STATE_PLAYING, STATE_UNKNOWN) + STATE_OFF, STATE_PAUSED, STATE_PLAYING) import homeassistant.helpers.config_validation as cv from homeassistant.helpers.script import Script @@ -168,7 +168,7 @@ class LgWebOSDevice(MediaPlayerDevice): self._volume = 0 self._current_source = None self._current_source_id = None - self._state = STATE_UNKNOWN + self._state = None self._source_list = {} self._app_list = {} self._channel = None @@ -181,7 +181,7 @@ class LgWebOSDevice(MediaPlayerDevice): current_input = self._client.get_input() if current_input is not None: self._current_source_id = current_input - if self._state in (STATE_UNKNOWN, STATE_OFF): + if self._state in (None, STATE_OFF): self._state = STATE_PLAYING else: self._state = STATE_OFF diff --git a/homeassistant/components/mqtt/alarm_control_panel.py b/homeassistant/components/mqtt/alarm_control_panel.py index 7c412a3febe..f1e0d8589a3 100644 --- a/homeassistant/components/mqtt/alarm_control_panel.py +++ b/homeassistant/components/mqtt/alarm_control_panel.py @@ -15,7 +15,7 @@ from homeassistant.components import mqtt from homeassistant.const import ( CONF_CODE, CONF_DEVICE, CONF_NAME, STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME, STATE_ALARM_DISARMED, STATE_ALARM_PENDING, - STATE_ALARM_TRIGGERED, STATE_UNKNOWN) + STATE_ALARM_TRIGGERED) from homeassistant.components.mqtt import ( ATTR_DISCOVERY_HASH, CONF_COMMAND_TOPIC, CONF_QOS, CONF_RETAIN, CONF_STATE_TOPIC, MqttAttributes, MqttAvailability, MqttDiscoveryUpdate, @@ -90,7 +90,7 @@ class MqttAlarm(MqttAttributes, MqttAvailability, MqttDiscoveryUpdate, def __init__(self, config, discovery_hash): """Init the MQTT Alarm Control Panel.""" - self._state = STATE_UNKNOWN + self._state = None self._config = config self._unique_id = config.get(CONF_UNIQUE_ID) self._sub_state = None diff --git a/homeassistant/components/mqtt/sensor.py b/homeassistant/components/mqtt/sensor.py index 688352b1ef6..05b54c9eab6 100644 --- a/homeassistant/components/mqtt/sensor.py +++ b/homeassistant/components/mqtt/sensor.py @@ -20,7 +20,7 @@ from homeassistant.components.mqtt.discovery import ( MQTT_DISCOVERY_NEW, clear_discovery_hash) from homeassistant.components.sensor import DEVICE_CLASSES_SCHEMA from homeassistant.const import ( - CONF_FORCE_UPDATE, CONF_NAME, CONF_VALUE_TEMPLATE, STATE_UNKNOWN, + CONF_FORCE_UPDATE, CONF_NAME, CONF_VALUE_TEMPLATE, CONF_UNIT_OF_MEASUREMENT, CONF_ICON, CONF_DEVICE_CLASS, CONF_DEVICE) from homeassistant.helpers.entity import Entity from homeassistant.components import mqtt @@ -95,7 +95,7 @@ class MqttSensor(MqttAttributes, MqttAvailability, MqttDiscoveryUpdate, """Initialize the sensor.""" self._config = config self._unique_id = config.get(CONF_UNIQUE_ID) - self._state = STATE_UNKNOWN + self._state = None self._sub_state = None self._expiration_trigger = None self._attributes = None @@ -188,7 +188,7 @@ class MqttSensor(MqttAttributes, MqttAvailability, MqttDiscoveryUpdate, def value_is_expired(self, *_): """Triggered when value is expired.""" self._expiration_trigger = None - self._state = STATE_UNKNOWN + self._state = None self.async_schedule_update_ha_state() @property diff --git a/homeassistant/components/plant.py b/homeassistant/components/plant.py index 55793558fd9..ed43562d221 100644 --- a/homeassistant/components/plant.py +++ b/homeassistant/components/plant.py @@ -167,7 +167,7 @@ class Plant(Entity): for reading, entity_id in config['sensors'].items(): self._sensormap[entity_id] = reading self._readingmap[reading] = entity_id - self._state = STATE_UNKNOWN + self._state = None self._name = name self._battery = None self._moisture = None diff --git a/homeassistant/components/sensor/amcrest.py b/homeassistant/components/sensor/amcrest.py index 50d6e9b7fa9..22e13d05e20 100644 --- a/homeassistant/components/sensor/amcrest.py +++ b/homeassistant/components/sensor/amcrest.py @@ -9,7 +9,7 @@ import logging from homeassistant.components.amcrest import DATA_AMCREST, SENSORS from homeassistant.helpers.entity import Entity -from homeassistant.const import CONF_NAME, CONF_SENSORS, STATE_UNKNOWN +from homeassistant.const import CONF_NAME, CONF_SENSORS DEPENDENCIES = ['amcrest'] @@ -48,7 +48,7 @@ class AmcrestSensor(Entity): self._name = '{0}_{1}'.format(name, SENSORS.get(self._sensor_type)[0]) self._icon = 'mdi:{}'.format(SENSORS.get(self._sensor_type)[2]) - self._state = STATE_UNKNOWN + self._state = None @property def name(self): diff --git a/homeassistant/components/sensor/arest.py b/homeassistant/components/sensor/arest.py index 3d85a331f6f..e0c5ef129ce 100644 --- a/homeassistant/components/sensor/arest.py +++ b/homeassistant/components/sensor/arest.py @@ -13,7 +13,7 @@ import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.const import ( CONF_UNIT_OF_MEASUREMENT, CONF_VALUE_TEMPLATE, CONF_RESOURCE, - CONF_MONITORED_VARIABLES, CONF_NAME, STATE_UNKNOWN) + CONF_MONITORED_VARIABLES, CONF_NAME) from homeassistant.exceptions import TemplateError from homeassistant.helpers.entity import Entity from homeassistant.util import Throttle @@ -116,7 +116,7 @@ class ArestSensor(Entity): self._name = '{} {}'.format(location.title(), name.title()) self._variable = variable self._pin = pin - self._state = STATE_UNKNOWN + self._state = None self._unit_of_measurement = unit_of_measurement self._renderer = renderer @@ -145,7 +145,7 @@ class ArestSensor(Entity): return values['error'] value = self._renderer( - values.get('value', values.get(self._variable, STATE_UNKNOWN))) + values.get('value', values.get(self._variable, None))) return value def update(self): diff --git a/homeassistant/components/sensor/comed_hourly_pricing.py b/homeassistant/components/sensor/comed_hourly_pricing.py index b5d230d8517..12b8e917f9d 100644 --- a/homeassistant/components/sensor/comed_hourly_pricing.py +++ b/homeassistant/components/sensor/comed_hourly_pricing.py @@ -15,7 +15,7 @@ import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.const import ( - ATTR_ATTRIBUTION, CONF_NAME, CONF_OFFSET, STATE_UNKNOWN) + ATTR_ATTRIBUTION, CONF_NAME, CONF_OFFSET) from homeassistant.helpers.aiohttp_client import async_get_clientsession import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity @@ -120,7 +120,7 @@ class ComedHourlyPricingSensor(Entity): float(data[0]['price']) + self.offset, 2) else: - self._state = STATE_UNKNOWN + self._state = None except (asyncio.TimeoutError, aiohttp.ClientError) as err: _LOGGER.error("Could not get data from ComEd API: %s", err) diff --git a/homeassistant/components/sensor/comfoconnect.py b/homeassistant/components/sensor/comfoconnect.py index b13c8d8d263..9ae6a4de091 100644 --- a/homeassistant/components/sensor/comfoconnect.py +++ b/homeassistant/components/sensor/comfoconnect.py @@ -11,8 +11,7 @@ from homeassistant.components.comfoconnect import ( ATTR_CURRENT_HUMIDITY, ATTR_OUTSIDE_TEMPERATURE, ATTR_OUTSIDE_HUMIDITY, ATTR_AIR_FLOW_SUPPLY, ATTR_AIR_FLOW_EXHAUST, SIGNAL_COMFOCONNECT_UPDATE_RECEIVED) -from homeassistant.const import ( - CONF_RESOURCES, TEMP_CELSIUS, STATE_UNKNOWN) +from homeassistant.const import CONF_RESOURCES, TEMP_CELSIUS from homeassistant.helpers.dispatcher import dispatcher_connect from homeassistant.helpers.entity import Entity @@ -122,7 +121,7 @@ class ComfoConnectSensor(Entity): try: return self._ccb.data[self._sensor_id] except KeyError: - return STATE_UNKNOWN + return None @property def name(self): diff --git a/homeassistant/components/sensor/dnsip.py b/homeassistant/components/sensor/dnsip.py index c3ec5fd4ce2..48cf8debea6 100644 --- a/homeassistant/components/sensor/dnsip.py +++ b/homeassistant/components/sensor/dnsip.py @@ -9,7 +9,6 @@ from datetime import timedelta import voluptuous as vol -from homeassistant.const import STATE_UNKNOWN from homeassistant.components.sensor import PLATFORM_SCHEMA import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity @@ -73,7 +72,7 @@ class WanIpSensor(Entity): self.resolver = aiodns.DNSResolver(loop=self.hass.loop) self.resolver.nameservers = [resolver] self.querytype = 'AAAA' if ipv6 else 'A' - self._state = STATE_UNKNOWN + self._state = None @property def name(self): @@ -97,4 +96,4 @@ class WanIpSensor(Entity): if response: self._state = response[0].host else: - self._state = STATE_UNKNOWN + self._state = None diff --git a/homeassistant/components/sensor/dsmr.py b/homeassistant/components/sensor/dsmr.py index e3cf704d432..ed3b409c49d 100644 --- a/homeassistant/components/sensor/dsmr.py +++ b/homeassistant/components/sensor/dsmr.py @@ -13,7 +13,7 @@ import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.const import ( - CONF_HOST, CONF_PORT, EVENT_HOMEASSISTANT_STOP, STATE_UNKNOWN) + CONF_HOST, CONF_PORT, EVENT_HOMEASSISTANT_STOP) from homeassistant.core import CoreState import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity @@ -270,7 +270,7 @@ class DSMREntity(Entity): if value is not None: return value - return STATE_UNKNOWN + return None @property def unit_of_measurement(self): @@ -287,7 +287,7 @@ class DSMREntity(Entity): if value == '0001': return 'low' - return STATE_UNKNOWN + return None class DerivativeDSMREntity(DSMREntity): @@ -300,7 +300,7 @@ class DerivativeDSMREntity(DSMREntity): _previous_reading = None _previous_timestamp = None - _state = STATE_UNKNOWN + _state = None @property def state(self): diff --git a/homeassistant/components/sensor/dweet.py b/homeassistant/components/sensor/dweet.py index 25bcaa18bab..5f85164f35d 100644 --- a/homeassistant/components/sensor/dweet.py +++ b/homeassistant/components/sensor/dweet.py @@ -13,7 +13,7 @@ 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_VALUE_TEMPLATE, STATE_UNKNOWN, CONF_UNIT_OF_MEASUREMENT) + CONF_NAME, CONF_VALUE_TEMPLATE, CONF_UNIT_OF_MEASUREMENT) from homeassistant.helpers.entity import Entity REQUIREMENTS = ['dweepy==0.3.0'] @@ -69,7 +69,7 @@ class DweetSensor(Entity): self.dweet = dweet self._name = name self._value_template = value_template - self._state = STATE_UNKNOWN + self._state = None self._unit_of_measurement = unit_of_measurement @property @@ -92,11 +92,11 @@ class DweetSensor(Entity): self.dweet.update() if self.dweet.data is None: - self._state = STATE_UNKNOWN + self._state = None else: values = json.dumps(self.dweet.data[0]['content']) self._state = self._value_template.render_with_possible_json_value( - values, STATE_UNKNOWN) + values, None) class DweetData: diff --git a/homeassistant/components/sensor/geo_rss_events.py b/homeassistant/components/sensor/geo_rss_events.py index e5e2f3d46f1..ab406f9241e 100644 --- a/homeassistant/components/sensor/geo_rss_events.py +++ b/homeassistant/components/sensor/geo_rss_events.py @@ -16,7 +16,7 @@ import voluptuous as vol import homeassistant.helpers.config_validation as cv from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.const import ( - STATE_UNKNOWN, CONF_UNIT_OF_MEASUREMENT, CONF_NAME, + CONF_UNIT_OF_MEASUREMENT, CONF_NAME, CONF_LATITUDE, CONF_LONGITUDE, CONF_RADIUS, CONF_URL) from homeassistant.helpers.entity import Entity @@ -89,7 +89,7 @@ class GeoRssServiceSensor(Entity): """Initialize the sensor.""" self._category = category self._service_name = service_name - self._state = STATE_UNKNOWN + self._state = None self._state_attributes = None self._unit_of_measurement = unit_of_measurement from georss_client.generic_feed import GenericFeed diff --git a/homeassistant/components/sensor/google_wifi.py b/homeassistant/components/sensor/google_wifi.py index 35db8f7c9e8..b78e9afb8b9 100644 --- a/homeassistant/components/sensor/google_wifi.py +++ b/homeassistant/components/sensor/google_wifi.py @@ -97,7 +97,7 @@ class GoogleWifiSensor(Entity): """Initialize a Google Wifi sensor.""" self._api = api self._name = name - self._state = STATE_UNKNOWN + self._state = None variable_info = MONITORED_CONDITIONS[variable] self._var_name = variable @@ -135,7 +135,7 @@ class GoogleWifiSensor(Entity): if self.available: self._state = self._api.data[self._var_name] else: - self._state = STATE_UNKNOWN + self._state = None class GoogleWifiAPI: diff --git a/homeassistant/components/sensor/gpsd.py b/homeassistant/components/sensor/gpsd.py index 0504cf7a511..b1ce428e1f2 100644 --- a/homeassistant/components/sensor/gpsd.py +++ b/homeassistant/components/sensor/gpsd.py @@ -10,7 +10,7 @@ import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.const import ( - ATTR_LATITUDE, ATTR_LONGITUDE, STATE_UNKNOWN, CONF_HOST, CONF_PORT, + ATTR_LATITUDE, ATTR_LONGITUDE, CONF_HOST, CONF_PORT, CONF_NAME) from homeassistant.helpers.entity import Entity import homeassistant.helpers.config_validation as cv @@ -93,7 +93,7 @@ class GpsdSensor(Entity): return "3D Fix" if self.agps_thread.data_stream.mode == 2: return "2D Fix" - return STATE_UNKNOWN + return None @property def device_state_attributes(self): diff --git a/homeassistant/components/sensor/kwb.py b/homeassistant/components/sensor/kwb.py index 20e5bc7f4ac..f490fbd5b14 100644 --- a/homeassistant/components/sensor/kwb.py +++ b/homeassistant/components/sensor/kwb.py @@ -9,8 +9,7 @@ import logging import voluptuous as vol from homeassistant.const import (CONF_HOST, CONF_PORT, CONF_DEVICE, - CONF_NAME, EVENT_HOMEASSISTANT_STOP, - STATE_UNKNOWN) + CONF_NAME, EVENT_HOMEASSISTANT_STOP) from homeassistant.helpers.entity import Entity from homeassistant.components.sensor import PLATFORM_SCHEMA import homeassistant.helpers.config_validation as cv @@ -105,7 +104,7 @@ class KWBSensor(Entity): """Return the state of value.""" if self._sensor.value is not None and self._sensor.available: return self._sensor.value - return STATE_UNKNOWN + return None @property def unit_of_measurement(self): diff --git a/homeassistant/components/sensor/london_air.py b/homeassistant/components/sensor/london_air.py index 6c96bb48e97..afb50d766f4 100644 --- a/homeassistant/components/sensor/london_air.py +++ b/homeassistant/components/sensor/london_air.py @@ -11,7 +11,6 @@ import requests import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA -from homeassistant.const import STATE_UNKNOWN import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity from homeassistant.util import Throttle @@ -143,7 +142,7 @@ class AirSensor(Entity): if sites_status: self._state = max(set(sites_status), key=sites_status.count) else: - self._state = STATE_UNKNOWN + self._state = None def parse_species(species_data): diff --git a/homeassistant/components/sensor/min_max.py b/homeassistant/components/sensor/min_max.py index f0334ef3255..e18c67471d9 100644 --- a/homeassistant/components/sensor/min_max.py +++ b/homeassistant/components/sensor/min_max.py @@ -70,20 +70,20 @@ async def async_setup_platform(hass, config, async_add_entities, def calc_min(sensor_values): """Calculate min value, honoring unknown states.""" - val = STATE_UNKNOWN + val = None for sval in sensor_values: if sval != STATE_UNKNOWN: - if val == STATE_UNKNOWN or val > sval: + if val is None or val > sval: val = sval return val def calc_max(sensor_values): """Calculate max value, honoring unknown states.""" - val = STATE_UNKNOWN + val = None for sval in sensor_values: if sval != STATE_UNKNOWN: - if val == STATE_UNKNOWN or val < sval: + if val is None or val < sval: val = sval return val @@ -97,7 +97,7 @@ def calc_mean(sensor_values, round_digits): val += sval count += 1 if count == 0: - return STATE_UNKNOWN + return None return round(val/count, round_digits) @@ -119,7 +119,7 @@ class MinMaxSensor(Entity): if self._sensor_type == v)).capitalize() self._unit_of_measurement = None self._unit_of_measurement_mismatch = False - self.min_value = self.max_value = self.mean = self.last = STATE_UNKNOWN + self.min_value = self.max_value = self.mean = self.last = None self.count_sensors = len(self._entity_ids) self.states = {} @@ -164,7 +164,7 @@ class MinMaxSensor(Entity): def state(self): """Return the state of the sensor.""" if self._unit_of_measurement_mismatch: - return STATE_UNKNOWN + return None return getattr(self, next( k for k, v in SENSOR_TYPES.items() if self._sensor_type == v)) diff --git a/homeassistant/components/sensor/mvglive.py b/homeassistant/components/sensor/mvglive.py index 5ec66aafe2f..71690f643f4 100644 --- a/homeassistant/components/sensor/mvglive.py +++ b/homeassistant/components/sensor/mvglive.py @@ -14,8 +14,7 @@ import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.const import ( - CONF_NAME, ATTR_ATTRIBUTION, STATE_UNKNOWN - ) + CONF_NAME, ATTR_ATTRIBUTION) REQUIREMENTS = ['PyMVGLive==1.1.4'] @@ -87,7 +86,7 @@ class MVGLiveSensor(Entity): self._name = name self.data = MVGLiveData(station, destinations, directions, lines, products, timeoffset, number) - self._state = STATE_UNKNOWN + self._state = None self._icon = ICONS['-'] @property diff --git a/homeassistant/components/sensor/netatmo.py b/homeassistant/components/sensor/netatmo.py index e871bfa5e1b..d593d93729b 100644 --- a/homeassistant/components/sensor/netatmo.py +++ b/homeassistant/components/sensor/netatmo.py @@ -13,7 +13,7 @@ import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.const import ( TEMP_CELSIUS, DEVICE_CLASS_HUMIDITY, DEVICE_CLASS_TEMPERATURE, - DEVICE_CLASS_BATTERY, STATE_UNKNOWN) + DEVICE_CLASS_BATTERY) from homeassistant.helpers.entity import Entity import homeassistant.helpers.config_validation as cv @@ -161,7 +161,7 @@ class NetAtmoSensor(Entity): if data is None: _LOGGER.warning("No data found for %s", self.module_name) - self._state = STATE_UNKNOWN + self._state = None return if self.type == 'temperature': diff --git a/homeassistant/components/sensor/pilight.py b/homeassistant/components/sensor/pilight.py index 85843018c01..ddcbe018f8e 100644 --- a/homeassistant/components/sensor/pilight.py +++ b/homeassistant/components/sensor/pilight.py @@ -9,7 +9,7 @@ import logging import voluptuous as vol from homeassistant.const import ( - CONF_NAME, STATE_UNKNOWN, CONF_UNIT_OF_MEASUREMENT, CONF_PAYLOAD) + CONF_NAME, CONF_UNIT_OF_MEASUREMENT, CONF_PAYLOAD) from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.helpers.entity import Entity from homeassistant.components import pilight @@ -46,7 +46,7 @@ class PilightSensor(Entity): def __init__(self, hass, name, variable, payload, unit_of_measurement): """Initialize the sensor.""" - self._state = STATE_UNKNOWN + self._state = None self._hass = hass self._name = name self._variable = variable diff --git a/homeassistant/components/sensor/rest.py b/homeassistant/components/sensor/rest.py index 5b92753eb90..4eb4b940095 100644 --- a/homeassistant/components/sensor/rest.py +++ b/homeassistant/components/sensor/rest.py @@ -18,7 +18,7 @@ from homeassistant.const import ( CONF_METHOD, CONF_PASSWORD, CONF_PAYLOAD, CONF_RESOURCE, CONF_UNIT_OF_MEASUREMENT, CONF_USERNAME, CONF_VALUE_TEMPLATE, CONF_VERIFY_SSL, CONF_DEVICE_CLASS, - HTTP_BASIC_AUTHENTICATION, HTTP_DIGEST_AUTHENTICATION, STATE_UNKNOWN) + HTTP_BASIC_AUTHENTICATION, HTTP_DIGEST_AUTHENTICATION) from homeassistant.exceptions import PlatformNotReady from homeassistant.helpers.entity import Entity import homeassistant.helpers.config_validation as cv @@ -100,7 +100,7 @@ class RestSensor(Entity): self._hass = hass self.rest = rest self._name = name - self._state = STATE_UNKNOWN + self._state = None self._unit_of_measurement = unit_of_measurement self._device_class = device_class self._value_template = value_template @@ -159,11 +159,9 @@ class RestSensor(Entity): _LOGGER.debug("Erroneous JSON: %s", value) else: _LOGGER.warning("Empty reply found when expecting JSON data") - if value is None: - value = STATE_UNKNOWN - elif self._value_template is not None: + if value is not None and self._value_template is not None: value = self._value_template.render_with_possible_json_value( - value, STATE_UNKNOWN) + value, None) self._state = value diff --git a/homeassistant/components/sensor/ring.py b/homeassistant/components/sensor/ring.py index 92c033241e0..9478768f889 100644 --- a/homeassistant/components/sensor/ring.py +++ b/homeassistant/components/sensor/ring.py @@ -15,7 +15,7 @@ from homeassistant.components.ring import ( from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.const import ( CONF_ENTITY_NAMESPACE, CONF_MONITORED_CONDITIONS, - STATE_UNKNOWN, ATTR_ATTRIBUTION) + ATTR_ATTRIBUTION) from homeassistant.helpers.entity import Entity from homeassistant.helpers.icon import icon_for_battery_level @@ -98,7 +98,7 @@ class RingSensor(Entity): self._kind = SENSOR_TYPES.get(self._sensor_type)[4] self._name = "{0} {1}".format( self._data.name, SENSOR_TYPES.get(self._sensor_type)[0]) - self._state = STATE_UNKNOWN + self._state = None self._tz = str(hass.config.time_zone) self._unique_id = '{}-{}'.format(self._data.id, self._sensor_type) @@ -141,7 +141,7 @@ class RingSensor(Entity): @property def icon(self): """Icon to use in the frontend, if any.""" - if self._sensor_type == 'battery' and self._state is not STATE_UNKNOWN: + if self._sensor_type == 'battery' and self._state is not None: return icon_for_battery_level(battery_level=int(self._state), charging=False) return self._icon diff --git a/homeassistant/components/sensor/scrape.py b/homeassistant/components/sensor/scrape.py index 9a67d381d42..6dd52789f71 100644 --- a/homeassistant/components/sensor/scrape.py +++ b/homeassistant/components/sensor/scrape.py @@ -12,7 +12,7 @@ from requests.auth import HTTPBasicAuth, HTTPDigestAuth from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.components.sensor.rest import RestData from homeassistant.const import ( - CONF_NAME, CONF_RESOURCE, CONF_UNIT_OF_MEASUREMENT, STATE_UNKNOWN, + CONF_NAME, CONF_RESOURCE, CONF_UNIT_OF_MEASUREMENT, CONF_VALUE_TEMPLATE, CONF_VERIFY_SSL, CONF_USERNAME, CONF_HEADERS, CONF_PASSWORD, CONF_AUTHENTICATION, HTTP_BASIC_AUTHENTICATION, HTTP_DIGEST_AUTHENTICATION) @@ -87,7 +87,7 @@ class ScrapeSensor(Entity): """Initialize a web scrape sensor.""" self.rest = rest self._name = name - self._state = STATE_UNKNOWN + self._state = None self._select = select self._attr = attr self._value_template = value_template @@ -129,6 +129,6 @@ class ScrapeSensor(Entity): if self._value_template is not None: self._state = self._value_template.render_with_possible_json_value( - value, STATE_UNKNOWN) + value, None) else: self._state = value diff --git a/homeassistant/components/sensor/tank_utility.py b/homeassistant/components/sensor/tank_utility.py index 55928a80f13..c807f1aa4c7 100644 --- a/homeassistant/components/sensor/tank_utility.py +++ b/homeassistant/components/sensor/tank_utility.py @@ -13,8 +13,7 @@ import voluptuous as vol import homeassistant.helpers.config_validation as cv from homeassistant.components.sensor import PLATFORM_SCHEMA -from homeassistant.const import (CONF_DEVICES, CONF_EMAIL, CONF_PASSWORD, - STATE_UNKNOWN) +from homeassistant.const import CONF_DEVICES, CONF_EMAIL, CONF_PASSWORD from homeassistant.helpers.entity import Entity @@ -78,7 +77,7 @@ class TankUtilitySensor(Entity): self._password = password self._token = token self._device = device - self._state = STATE_UNKNOWN + self._state = None self._name = "Tank Utility " + self.device self._unit_of_measurement = SENSOR_UNIT_OF_MEASUREMENT self._attributes = {} diff --git a/homeassistant/components/sensor/travisci.py b/homeassistant/components/sensor/travisci.py index 40ae130d150..e1bd74b993c 100644 --- a/homeassistant/components/sensor/travisci.py +++ b/homeassistant/components/sensor/travisci.py @@ -13,7 +13,7 @@ import homeassistant.helpers.config_validation as cv from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.const import ( ATTR_ATTRIBUTION, CONF_API_KEY, CONF_SCAN_INTERVAL, - CONF_MONITORED_CONDITIONS, STATE_UNKNOWN) + CONF_MONITORED_CONDITIONS) from homeassistant.helpers.entity import Entity REQUIREMENTS = ['TravisPy==0.3.5'] @@ -107,7 +107,7 @@ class TravisCISensor(Entity): self._repo_name = repo_name self._user = user self._branch = branch - self._state = STATE_UNKNOWN + self._state = None self._name = "{0} {1}".format(self._repo_name, SENSOR_TYPES[self._sensor_type][0]) @@ -132,7 +132,7 @@ class TravisCISensor(Entity): attrs = {} attrs[ATTR_ATTRIBUTION] = CONF_ATTRIBUTION - if self._build and self._state is not STATE_UNKNOWN: + if self._build and self._state is not None: if self._user and self._sensor_type == 'state': attrs['Owner Name'] = self._user.name attrs['Owner Email'] = self._user.email diff --git a/homeassistant/components/sensor/xbox_live.py b/homeassistant/components/sensor/xbox_live.py index 3432927dda0..9670b4b2f9c 100644 --- a/homeassistant/components/sensor/xbox_live.py +++ b/homeassistant/components/sensor/xbox_live.py @@ -57,7 +57,7 @@ class XboxSensor(Entity): def __init__(self, hass, api, xuid): """Initialize the sensor.""" self._hass = hass - self._state = STATE_UNKNOWN + self._state = None self._presence = {} self._xuid = xuid self._api = api @@ -117,5 +117,5 @@ class XboxSensor(Entity): def update(self): """Update state data from Xbox API.""" presence = self._api.get_user_presence(self._xuid) - self._state = presence.get('state', STATE_UNKNOWN) + self._state = presence.get('state') self._presence = presence.get('devices', {}) diff --git a/homeassistant/components/sensor/yweather.py b/homeassistant/components/sensor/yweather.py index e84a77b7bb6..243329680e1 100644 --- a/homeassistant/components/sensor/yweather.py +++ b/homeassistant/components/sensor/yweather.py @@ -12,7 +12,7 @@ import voluptuous as vol import homeassistant.helpers.config_validation as cv from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.const import ( - TEMP_CELSIUS, CONF_MONITORED_CONDITIONS, CONF_NAME, STATE_UNKNOWN, + TEMP_CELSIUS, CONF_MONITORED_CONDITIONS, CONF_NAME, ATTR_ATTRIBUTION) from homeassistant.helpers.entity import Entity from homeassistant.util import Throttle @@ -99,7 +99,7 @@ class YahooWeatherSensor(Entity): self._client = name self._name = SENSOR_TYPES[sensor_type][0] self._type = sensor_type - self._state = STATE_UNKNOWN + self._state = None self._unit = SENSOR_TYPES[sensor_type][1] self._data = weather_data self._forecast = forecast diff --git a/tests/components/fan/test_init.py b/tests/components/fan/test_init.py index 920a2b81016..1361abf10de 100644 --- a/tests/components/fan/test_init.py +++ b/tests/components/fan/test_init.py @@ -27,7 +27,7 @@ class TestFanEntity(unittest.TestCase): def test_fanentity(self): """Test fan entity methods.""" - assert 'on' == self.fan.state + assert 'off' == self.fan.state assert 0 == len(self.fan.speed_list) assert 0 == self.fan.supported_features assert {'speed_list': []} == self.fan.state_attributes diff --git a/tests/components/media_player/test_universal.py b/tests/components/media_player/test_universal.py index 9f0f6d0728b..58911776836 100644 --- a/tests/components/media_player/test_universal.py +++ b/tests/components/media_player/test_universal.py @@ -5,7 +5,7 @@ import unittest from voluptuous.error import MultipleInvalid from homeassistant.const import ( - STATE_OFF, STATE_ON, STATE_UNKNOWN, STATE_PLAYING, STATE_PAUSED) + STATE_OFF, STATE_ON, STATE_PLAYING, STATE_PAUSED) import homeassistant.components.switch as switch import homeassistant.components.input_number as input_number import homeassistant.components.input_select as input_select @@ -119,7 +119,7 @@ class MockMediaPlayer(media_player.MediaPlayerDevice): def turn_on(self): """Mock turn_on function.""" - self._state = STATE_UNKNOWN + self._state = None def turn_off(self): """Mock turn_off function.""" diff --git a/tests/components/sensor/test_dsmr.py b/tests/components/sensor/test_dsmr.py index 9ab8d61f739..673cadd6208 100644 --- a/tests/components/sensor/test_dsmr.py +++ b/tests/components/sensor/test_dsmr.py @@ -12,7 +12,6 @@ from unittest.mock import Mock import asynctest from homeassistant.bootstrap import async_setup_component from homeassistant.components.sensor.dsmr import DerivativeDSMREntity -from homeassistant.const import STATE_UNKNOWN import pytest from tests.common import assert_setup_component @@ -99,7 +98,7 @@ def test_derivative(): entity = DerivativeDSMREntity('test', '1.0.0') yield from entity.async_update() - assert entity.state == STATE_UNKNOWN, 'initial state not unknown' + assert entity.state is None, 'initial state not unknown' entity.telegram = { '1.0.0': MBusObject([ @@ -109,7 +108,7 @@ def test_derivative(): } yield from entity.async_update() - assert entity.state == STATE_UNKNOWN, \ + assert entity.state is None, \ 'state after first update should still be unknown' entity.telegram = { diff --git a/tests/components/sensor/test_google_wifi.py b/tests/components/sensor/test_google_wifi.py index a4b18d0ed4a..989cd13c5d6 100644 --- a/tests/components/sensor/test_google_wifi.py +++ b/tests/components/sensor/test_google_wifi.py @@ -169,7 +169,7 @@ class TestGoogleWifiSensor(unittest.TestCase): sensor = self.sensor_dict[name]['sensor'] self.fake_delay(2) sensor.update() - assert STATE_UNKNOWN == sensor.state + assert sensor.state is None @requests_mock.Mocker() def test_update_when_value_changed(self, mock_req): @@ -213,7 +213,7 @@ class TestGoogleWifiSensor(unittest.TestCase): for name in self.sensor_dict: sensor = self.sensor_dict[name]['sensor'] sensor.update() - assert STATE_UNKNOWN == sensor.state + assert sensor.state is None def update_side_effect(self): """Mock representation of update function.""" diff --git a/tests/components/sensor/test_rest.py b/tests/components/sensor/test_rest.py index 05ab628b1a8..3e71be8a6f6 100644 --- a/tests/components/sensor/test_rest.py +++ b/tests/components/sensor/test_rest.py @@ -11,7 +11,6 @@ from homeassistant.exceptions import PlatformNotReady from homeassistant.setup import setup_component import homeassistant.components.sensor as sensor import homeassistant.components.sensor.rest as rest -from homeassistant.const import STATE_UNKNOWN from homeassistant.helpers.config_validation import template from tests.common import get_test_home_assistant, assert_setup_component @@ -175,7 +174,7 @@ class TestRestSensor(unittest.TestCase): self.rest.update = Mock( 'rest.RestData.update', side_effect=self.update_side_effect(None)) self.sensor.update() - assert STATE_UNKNOWN == self.sensor.state + assert self.sensor.state is None assert not self.sensor.available def test_update_when_value_changed(self): From 7038dd484a3a1795194fabb5b401360317ec98fa Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Thu, 24 Jan 2019 09:37:26 +0100 Subject: [PATCH 080/222] Upgrade TwitterAPI to 2.5.9 (#20372) --- homeassistant/components/notify/twitter.py | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/notify/twitter.py b/homeassistant/components/notify/twitter.py index 8f80156c436..d494952716e 100644 --- a/homeassistant/components/notify/twitter.py +++ b/homeassistant/components/notify/twitter.py @@ -19,7 +19,7 @@ from homeassistant.components.notify import ( from homeassistant.const import CONF_ACCESS_TOKEN, CONF_USERNAME from homeassistant.helpers.event import async_track_point_in_time -REQUIREMENTS = ['TwitterAPI==2.5.8'] +REQUIREMENTS = ['TwitterAPI==2.5.9'] _LOGGER = logging.getLogger(__name__) diff --git a/requirements_all.txt b/requirements_all.txt index 41c1d081b7e..8f2f5f91aca 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -72,7 +72,7 @@ RtmAPI==0.7.0 TravisPy==0.3.5 # homeassistant.components.notify.twitter -TwitterAPI==2.5.8 +TwitterAPI==2.5.9 # homeassistant.components.sensor.waze_travel_time WazeRouteCalculator==0.6 From 0be922dc9cce320b7de17bdbe02b4cf151f14c3d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fr=C3=A9deric=20Kinnaer?= Date: Thu, 24 Jan 2019 12:55:39 +0100 Subject: [PATCH 081/222] SongPal: do not crash if active_source is not (yet) available - fixes #20343 (#20344) * SongPal: error handling if active_source can't be detected * sonpal: Add comment to the use of getattr() for property source * songpal: make comment single-line --- homeassistant/components/media_player/songpal.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/media_player/songpal.py b/homeassistant/components/media_player/songpal.py index 59293fd3e8c..e67578539ad 100644 --- a/homeassistant/components/media_player/songpal.py +++ b/homeassistant/components/media_player/songpal.py @@ -288,7 +288,8 @@ class SongpalDevice(MediaPlayerDevice): @property def source(self): """Return currently active source.""" - return self._active_source.title + # Avoid a KeyError when _active_source is not (yet) populated + return getattr(self._active_source, 'title', None) @property def volume_level(self): From 2559bc4226c9398592f235d2c3d4e41b2b54a11e Mon Sep 17 00:00:00 2001 From: zewelor Date: Thu, 24 Jan 2019 17:41:07 +0100 Subject: [PATCH 082/222] Add yeelight start_flow service and ability to declare custom effects (#20107) --- homeassistant/components/light/services.yaml | 13 ++ homeassistant/components/light/yeelight.py | 128 ++++++++++++++++--- 2 files changed, 123 insertions(+), 18 deletions(-) diff --git a/homeassistant/components/light/services.yaml b/homeassistant/components/light/services.yaml index 3507c6d2cda..9836bf97f90 100644 --- a/homeassistant/components/light/services.yaml +++ b/homeassistant/components/light/services.yaml @@ -192,3 +192,16 @@ yeelight_set_mode: mode: description: Operation mode. Valid values are 'last', 'normal', 'rgb', 'hsv', 'color_flow', 'moonlight'. example: 'moonlight' + +yeelight_start_flow: + description: Start a custom flow, using transitions from https://yeelight.readthedocs.io/en/stable/yeelight.html#flow-objects + fields: + entity_id: + description: Name of the light entity. + example: 'light.yeelight' + count: + description: The number of times to run this flow (0 to run forever). + example: 0 + transitions: + description: Array of transitions, for desired effect. Examples https://yeelight.readthedocs.io/en/stable/flow.html + example: '[{ "TemperatureTransition": [1900, 1000, 80] }, { "TemperatureTransition": [1900, 1000, 10] }]' diff --git a/homeassistant/components/light/yeelight.py b/homeassistant/components/light/yeelight.py index 25704eea0cc..249f542325f 100644 --- a/homeassistant/components/light/yeelight.py +++ b/homeassistant/components/light/yeelight.py @@ -39,15 +39,48 @@ CONF_MODEL = 'model' CONF_TRANSITION = 'transition' CONF_SAVE_ON_CHANGE = 'save_on_change' CONF_MODE_MUSIC = 'use_music_mode' +CONF_CUSTOM_EFFECTS = 'custom_effects' +CONF_FLOW_PARAMS = 'flow_params' DATA_KEY = 'light.yeelight' +ATTR_MODE = 'mode' +ATTR_COUNT = 'count' +ATTR_TRANSITIONS = 'transitions' + +YEELIGHT_RGB_TRANSITION = 'RGBTransition' +YEELIGHT_HSV_TRANSACTION = 'HSVTransition' +YEELIGHT_TEMPERATURE_TRANSACTION = 'TemperatureTransition' +YEELIGHT_SLEEP_TRANSACTION = 'SleepTransition' + +YEELIGHT_SERVICE_SCHEMA = vol.Schema({ + vol.Required(ATTR_ENTITY_ID): cv.entity_ids, +}) + +YEELIGHT_FLOW_TRANSITION_SCHEMA = { + vol.Optional(ATTR_COUNT, default=0): cv.positive_int, + vol.Required(ATTR_TRANSITIONS): [{ + vol.Exclusive(YEELIGHT_RGB_TRANSITION, CONF_TRANSITION): + vol.All(cv.ensure_list, [cv.positive_int]), + vol.Exclusive(YEELIGHT_HSV_TRANSACTION, CONF_TRANSITION): + vol.All(cv.ensure_list, [cv.positive_int]), + vol.Exclusive(YEELIGHT_TEMPERATURE_TRANSACTION, CONF_TRANSITION): + vol.All(cv.ensure_list, [cv.positive_int]), + vol.Exclusive(YEELIGHT_SLEEP_TRANSACTION, CONF_TRANSITION): + vol.All(cv.ensure_list, [cv.positive_int]), + }] +} + DEVICE_SCHEMA = vol.Schema({ 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=False): cv.boolean, vol.Optional(CONF_MODEL): cv.string, + vol.Optional(CONF_CUSTOM_EFFECTS): [{ + vol.Required(CONF_NAME): cv.string, + vol.Required(CONF_FLOW_PARAMS): YEELIGHT_FLOW_TRANSITION_SCHEMA + }] }) PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( @@ -103,11 +136,7 @@ YEELIGHT_EFFECT_LIST = [ EFFECT_STOP] SERVICE_SET_MODE = 'yeelight_set_mode' -ATTR_MODE = 'mode' - -YEELIGHT_SERVICE_SCHEMA = vol.Schema({ - vol.Optional(ATTR_ENTITY_ID): cv.entity_ids, -}) +SERVICE_START_FLOW = 'yeelight_start_flow' def _cmd(func): @@ -123,6 +152,19 @@ def _cmd(func): return _wrap +def _parse_custom_effects(effects_config): + effects = {} + for config in effects_config: + params = config[CONF_FLOW_PARAMS] + transitions = YeelightLight.transitions_config_parser( + params[ATTR_TRANSITIONS]) + + effects[config[CONF_NAME]] = \ + {ATTR_COUNT: params[ATTR_COUNT], ATTR_TRANSITIONS: transitions} + + return effects + + def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the Yeelight bulbs.""" from yeelight.enums import PowerMode @@ -151,8 +193,10 @@ def setup_platform(hass, config, add_entities, discovery_info=None): name = device_config[CONF_NAME] _LOGGER.debug("Adding configured %s", name) + custom_effects = _parse_custom_effects(config[CONF_CUSTOM_EFFECTS]) device = {'name': name, 'ipaddr': ipaddr} - light = YeelightLight(device, device_config) + light = YeelightLight(device, device_config, + custom_effects=custom_effects) lights.append(light) hass.data[DATA_KEY][name] = light @@ -163,15 +207,14 @@ def setup_platform(hass, config, add_entities, discovery_info=None): params = {key: value for key, value in service.data.items() if key != ATTR_ENTITY_ID} entity_ids = service.data.get(ATTR_ENTITY_ID) - if entity_ids: - target_devices = [dev for dev in hass.data[DATA_KEY].values() - if dev.entity_id in entity_ids] - else: - target_devices = hass.data[DATA_KEY].values() + target_devices = [dev for dev in hass.data[DATA_KEY].values() + if dev.entity_id in entity_ids] for target_device in target_devices: if service.service == SERVICE_SET_MODE: target_device.set_mode(**params) + elif service.service == SERVICE_START_FLOW: + target_device.start_flow(**params) service_schema_set_mode = YEELIGHT_SERVICE_SCHEMA.extend({ vol.Required(ATTR_MODE): @@ -181,11 +224,18 @@ def setup_platform(hass, config, add_entities, discovery_info=None): DOMAIN, SERVICE_SET_MODE, service_handler, schema=service_schema_set_mode) + service_schema_start_flow = YEELIGHT_SERVICE_SCHEMA.extend( + YEELIGHT_FLOW_TRANSITION_SCHEMA + ) + hass.services.register( + DOMAIN, SERVICE_START_FLOW, service_handler, + schema=service_schema_start_flow) + class YeelightLight(Light): """Representation of a Yeelight light.""" - def __init__(self, device, config): + def __init__(self, device, config, custom_effects=None): """Initialize the Yeelight light.""" self.config = config self._name = device['name'] @@ -204,6 +254,11 @@ class YeelightLight(Light): self._min_mireds = None self._max_mireds = None + if custom_effects: + self._custom_effects = custom_effects + else: + self._custom_effects = {} + @property def available(self) -> bool: """Return if bulb is available.""" @@ -217,7 +272,7 @@ class YeelightLight(Light): @property def effect_list(self): """Return the list of supported effects.""" - return YEELIGHT_EFFECT_LIST + return YEELIGHT_EFFECT_LIST + self.custom_effects_names @property def color_temp(self) -> int: @@ -249,6 +304,16 @@ class YeelightLight(Light): """Return maximum supported color temperature.""" return self._max_mireds + @property + def custom_effects(self): + """Return dict with custom effects.""" + return self._custom_effects + + @property + def custom_effects_names(self): + """Return list with custom effects names.""" + return list(self.custom_effects.keys()) + def _get_hs_from_properties(self): rgb = self._properties.get('rgb', None) color_mode = self._properties.get('color_mode', None) @@ -435,15 +500,17 @@ class YeelightLight(Light): EFFECT_SLOWDOWN: slowdown, } - if effect in effects_map: + if effect in self.custom_effects_names: + flow = Flow(**self.custom_effects[effect]) + elif effect in effects_map: flow = Flow(count=0, transitions=effects_map[effect]()) - if effect == EFFECT_FAST_RANDOM_LOOP: + elif effect == EFFECT_FAST_RANDOM_LOOP: flow = Flow(count=0, transitions=randomloop(duration=250)) - if effect == EFFECT_WHATSAPP: + elif effect == EFFECT_WHATSAPP: flow = Flow(count=2, transitions=pulse(37, 211, 102)) - if effect == EFFECT_FACEBOOK: + elif effect == EFFECT_FACEBOOK: flow = Flow(count=2, transitions=pulse(59, 89, 152)) - if effect == EFFECT_TWITTER: + elif effect == EFFECT_TWITTER: flow = Flow(count=2, transitions=pulse(0, 172, 237)) try: @@ -518,3 +585,28 @@ class YeelightLight(Light): self.async_schedule_update_ha_state(True) except yeelight.BulbException as ex: _LOGGER.error("Unable to set the power mode: %s", ex) + + @staticmethod + def transitions_config_parser(transitions): + """Parse transitions config into initialized objects.""" + import yeelight + + transition_objects = [] + for transition_config in transitions: + transition, params = list(transition_config.items())[0] + transition_objects.append(getattr(yeelight, transition)(*params)) + + return transition_objects + + def start_flow(self, transitions, count=0): + """Start flow.""" + import yeelight + + try: + flow = yeelight.Flow( + count=count, + transitions=self.transitions_config_parser(transitions)) + + self._bulb.start_flow(flow) + except yeelight.BulbException as ex: + _LOGGER.error("Unable to set effect: %s", ex) From 3b1534c126b7465d18955a109ed2809bf79d8c9e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20H=C3=B8yer=20Iversen?= Date: Thu, 24 Jan 2019 21:56:44 +0100 Subject: [PATCH 083/222] Remove logging from tibber (#20382) * Remove logging from tibber * keep guard --- homeassistant/components/sensor/tibber.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/homeassistant/components/sensor/tibber.py b/homeassistant/components/sensor/tibber.py index 215cd8a81f5..1c3ef601633 100644 --- a/homeassistant/components/sensor/tibber.py +++ b/homeassistant/components/sensor/tibber.py @@ -29,8 +29,6 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up the Tibber sensor.""" if discovery_info is None: - _LOGGER.error("Tibber sensor configuration has changed." - " Check https://home-assistant.io/components/tibber/") return tibber_connection = hass.data.get(TIBBER_DOMAIN) From 70c5807976db5a43a9af381d282d862190d0c14e Mon Sep 17 00:00:00 2001 From: Anders Melchiorsen Date: Thu, 24 Jan 2019 22:37:30 +0100 Subject: [PATCH 084/222] Improve deprecation warnings (#20391) --- homeassistant/helpers/entity_component.py | 6 ++++-- homeassistant/helpers/service.py | 8 +++++--- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/homeassistant/helpers/entity_component.py b/homeassistant/helpers/entity_component.py index 21634121cd2..44213e6d7c8 100644 --- a/homeassistant/helpers/entity_component.py +++ b/homeassistant/helpers/entity_component.py @@ -169,7 +169,8 @@ class EntityComponent: self.logger.warning( 'Not passing an entity ID to a service to target all ' 'entities is deprecated. Update your call to %s.%s to be ' - 'instead: entity_id: "*"', service.domain, service.service) + 'instead: entity_id: %s', service.domain, service.service, + ENTITY_MATCH_ALL) return [entity for entity in self.entities if entity.available] @@ -182,8 +183,9 @@ class EntityComponent: """Register an entity service.""" async def handle_service(call): """Handle the service.""" + service_name = "{}.{}".format(self.domain, name) await self.hass.helpers.service.entity_service_call( - self._platforms.values(), func, call + self._platforms.values(), func, call, service_name ) self.hass.services.async_register( diff --git a/homeassistant/helpers/service.py b/homeassistant/helpers/service.py index 2b7638b55ee..d2211d031f5 100644 --- a/homeassistant/helpers/service.py +++ b/homeassistant/helpers/service.py @@ -187,7 +187,7 @@ async def async_get_all_descriptions(hass): @bind_hass -async def entity_service_call(hass, platforms, func, call): +async def entity_service_call(hass, platforms, func, call, service_name=''): """Handle an entity service call. Calls all platforms simultaneously. @@ -204,9 +204,11 @@ async def entity_service_call(hass, platforms, func, call): if ATTR_ENTITY_ID in call.data: target_all_entities = call.data[ATTR_ENTITY_ID] == ENTITY_MATCH_ALL else: + # Remove the service_name parameter along with this warning _LOGGER.warning( - 'Not passing an entity ID to a service to target all entities is ' - 'deprecated. Use instead: entity_id: "%s"', ENTITY_MATCH_ALL) + 'Not passing an entity ID to a service to target all ' + 'entities is deprecated. Update your call to %s to be ' + 'instead: entity_id: %s', service_name, ENTITY_MATCH_ALL) target_all_entities = True if not target_all_entities: From 68bd5f5df88e3dc898488547c5259e6aaaae424d Mon Sep 17 00:00:00 2001 From: mindigmarton Date: Thu, 24 Jan 2019 22:51:15 +0100 Subject: [PATCH 085/222] Upgrade emulated_roku to 0.1.8 to fix invalid encodings, fixes #20388 (#20390) --- homeassistant/components/emulated_roku/__init__.py | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/emulated_roku/__init__.py b/homeassistant/components/emulated_roku/__init__.py index 8ebaa5e4b26..4dec1d5602a 100644 --- a/homeassistant/components/emulated_roku/__init__.py +++ b/homeassistant/components/emulated_roku/__init__.py @@ -16,7 +16,7 @@ from .const import ( CONF_ADVERTISE_IP, CONF_ADVERTISE_PORT, CONF_HOST_IP, CONF_LISTEN_PORT, CONF_SERVERS, CONF_UPNP_BIND_MULTICAST, DOMAIN) -REQUIREMENTS = ['emulated_roku==0.1.7'] +REQUIREMENTS = ['emulated_roku==0.1.8'] SERVER_CONFIG_SCHEMA = vol.Schema({ vol.Required(CONF_NAME): cv.string, diff --git a/requirements_all.txt b/requirements_all.txt index 8f2f5f91aca..8918aefbac5 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -358,7 +358,7 @@ eliqonline==1.2.2 elkm1-lib==0.7.13 # homeassistant.components.emulated_roku -emulated_roku==0.1.7 +emulated_roku==0.1.8 # homeassistant.components.enocean enocean==0.40 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 9ade351f8a6..6138556a022 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -65,7 +65,7 @@ dsmr_parser==0.12 eebrightbox==0.0.4 # homeassistant.components.emulated_roku -emulated_roku==0.1.7 +emulated_roku==0.1.8 # homeassistant.components.sensor.entur_public_transport enturclient==0.1.3 From c508ba166c8fa356d91a0f8323058d21f52b0b36 Mon Sep 17 00:00:00 2001 From: starkillerOG Date: Thu, 24 Jan 2019 23:46:55 +0100 Subject: [PATCH 086/222] Philips Hue - Remove unnessesary warning (#20394) for white hue bulbs and bulbs from other brands like Ikea and Innr, this warning will be issued while this is not really a problem. So just remove the warning. --- homeassistant/components/hue/light.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/homeassistant/components/hue/light.py b/homeassistant/components/hue/light.py index 7a1449e00c6..89b731cb0e5 100644 --- a/homeassistant/components/hue/light.py +++ b/homeassistant/components/hue/light.py @@ -228,9 +228,6 @@ class HueLight(Light): self.is_philips = light.manufacturername == 'Philips' self.gamut_typ = self.light.colorgamuttype self.gamut = self.light.colorgamut - if not self.gamut: - err_msg = 'Can not get color gamut of light "%s"' - _LOGGER.warning(err_msg, self.name) @property def unique_id(self): From c7f5beb794031368d5d6af1b14a623aa84419b53 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 24 Jan 2019 17:53:01 -0800 Subject: [PATCH 087/222] history allowed to load states with invalid entity IDs (#20399) --- homeassistant/components/recorder/models.py | 3 +++ homeassistant/core.py | 7 +++++-- tests/components/recorder/test_models.py | 9 +++++++++ 3 files changed, 17 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/recorder/models.py b/homeassistant/components/recorder/models.py index 7a655c29434..d1be17b83d5 100644 --- a/homeassistant/components/recorder/models.py +++ b/homeassistant/components/recorder/models.py @@ -131,6 +131,9 @@ class States(Base): # type: ignore _process_timestamp(self.last_changed), _process_timestamp(self.last_updated), context=context, + # Temp, because database can still store invalid entity IDs + # Remove with 1.0 or in 2020. + temp_invalid_id_bypass=True ) except ValueError: # When json.loads fails diff --git a/homeassistant/core.py b/homeassistant/core.py index 2fe999f6d13..6ddefd2022d 100644 --- a/homeassistant/core.py +++ b/homeassistant/core.py @@ -664,11 +664,14 @@ class State: attributes: Optional[Dict] = None, last_changed: Optional[datetime.datetime] = None, last_updated: Optional[datetime.datetime] = None, - context: Optional[Context] = None) -> None: + context: Optional[Context] = None, + # Temp, because database can still store invalid entity IDs + # Remove with 1.0 or in 2020. + temp_invalid_id_bypass: Optional[bool] = False) -> None: """Initialize a new state.""" state = str(state) - if not valid_entity_id(entity_id): + if not valid_entity_id(entity_id) and not temp_invalid_id_bypass: raise InvalidEntityFormatError(( "Invalid entity id encountered: {}. " "Format should be .").format(entity_id)) diff --git a/tests/components/recorder/test_models.py b/tests/components/recorder/test_models.py index 3d1beb3a642..b56a7632df3 100644 --- a/tests/components/recorder/test_models.py +++ b/tests/components/recorder/test_models.py @@ -142,3 +142,12 @@ class TestRecorderRuns(unittest.TestCase): assert sorted(run.entity_ids()) == ['sensor.humidity', 'sensor.lux'] assert run.entity_ids(in_run2) == ['sensor.humidity'] + + +def test_states_from_native_invalid_entity_id(): + """Test loading a state from an invalid entity ID.""" + event = States() + event.entity_id = "test.invalid__id" + event.attributes = "{}" + state = event.to_native() + assert state.entity_id == 'test.invalid__id' From 046683ee3fec60f7220fe5375a6a2add9118a8ce Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Fri, 25 Jan 2019 02:53:17 +0100 Subject: [PATCH 088/222] Upgrade numpy to 1.16.0 (#20396) --- homeassistant/components/binary_sensor/trend.py | 2 +- homeassistant/components/image_processing/opencv.py | 2 +- homeassistant/components/image_processing/tensorflow.py | 2 +- homeassistant/components/sensor/pollen.py | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/binary_sensor/trend.py b/homeassistant/components/binary_sensor/trend.py index 8f3ff5d798e..494c3154b84 100644 --- a/homeassistant/components/binary_sensor/trend.py +++ b/homeassistant/components/binary_sensor/trend.py @@ -22,7 +22,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.15.4'] +REQUIREMENTS = ['numpy==1.16.0'] _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/image_processing/opencv.py b/homeassistant/components/image_processing/opencv.py index 7694dbd6735..8ca6e4d8a53 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.15.4'] +REQUIREMENTS = ['numpy==1.16.0'] _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/image_processing/tensorflow.py b/homeassistant/components/image_processing/tensorflow.py index 939b5a821cb..cc25756f2d0 100644 --- a/homeassistant/components/image_processing/tensorflow.py +++ b/homeassistant/components/image_processing/tensorflow.py @@ -20,7 +20,7 @@ from homeassistant.core import split_entity_id from homeassistant.helpers import template import homeassistant.helpers.config_validation as cv -REQUIREMENTS = ['numpy==1.15.4', 'pillow==5.4.1', 'protobuf==3.6.1'] +REQUIREMENTS = ['numpy==1.16.0', 'pillow==5.4.1', 'protobuf==3.6.1'] _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/sensor/pollen.py b/homeassistant/components/sensor/pollen.py index 8f187b82fd2..eab5a14b8ca 100644 --- a/homeassistant/components/sensor/pollen.py +++ b/homeassistant/components/sensor/pollen.py @@ -18,7 +18,7 @@ import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity from homeassistant.util import Throttle -REQUIREMENTS = ['numpy==1.15.4', 'pypollencom==2.2.2'] +REQUIREMENTS = ['numpy==1.16.0', 'pypollencom==2.2.2'] _LOGGER = logging.getLogger(__name__) diff --git a/requirements_all.txt b/requirements_all.txt index 8918aefbac5..787f9abc93c 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -734,7 +734,7 @@ nuheat==0.3.0 # homeassistant.components.image_processing.opencv # homeassistant.components.image_processing.tensorflow # homeassistant.components.sensor.pollen -numpy==1.15.4 +numpy==1.16.0 # homeassistant.components.google oauth2client==4.0.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 6138556a022..2de73e85f79 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -142,7 +142,7 @@ mficlient==0.3.0 # homeassistant.components.image_processing.opencv # homeassistant.components.image_processing.tensorflow # homeassistant.components.sensor.pollen -numpy==1.15.4 +numpy==1.16.0 # homeassistant.components.mqtt # homeassistant.components.shiftr From 400aaf8a3a3d2340dd4275bfc8b69b9926585204 Mon Sep 17 00:00:00 2001 From: Daniel Perna Date: Fri, 25 Jan 2019 02:53:31 +0100 Subject: [PATCH 089/222] Update pyhomematic to 0.1.55 (#20397) --- homeassistant/components/homematic/__init__.py | 9 +++++---- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 7 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/homematic/__init__.py b/homeassistant/components/homematic/__init__.py index e2709bde92a..9a496d914fc 100644 --- a/homeassistant/components/homematic/__init__.py +++ b/homeassistant/components/homematic/__init__.py @@ -19,7 +19,7 @@ from homeassistant.helpers import discovery import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity -REQUIREMENTS = ['pyhomematic==0.1.54'] +REQUIREMENTS = ['pyhomematic==0.1.55'] _LOGGER = logging.getLogger(__name__) @@ -65,7 +65,7 @@ HM_DEVICE_TYPES = { DISCOVER_SWITCHES: [ 'Switch', 'SwitchPowermeter', 'IOSwitch', 'IPSwitch', 'RFSiren', 'IPSwitchPowermeter', 'HMWIOSwitch', 'Rain', 'EcoLogic', - 'IPKeySwitchPowermeter', 'IPGarage'], + 'IPKeySwitchPowermeter', 'IPGarage', 'IPKeySwitch', 'IPMultiIO'], DISCOVER_LIGHTS: ['Dimmer', 'KeyDimmer', 'IPKeyDimmer', 'IPDimmer', 'ColorEffectLight'], DISCOVER_SENSORS: [ @@ -79,7 +79,7 @@ HM_DEVICE_TYPES = { 'IPWeatherSensor', 'RotaryHandleSensorIP', 'IPPassageSensor', 'IPKeySwitchPowermeter', 'IPThermostatWall230V', 'IPWeatherSensorPlus', 'IPWeatherSensorBasic', 'IPBrightnessSensor', 'IPGarage', - 'UniversalSensor', 'MotionIPV2'], + 'UniversalSensor', 'MotionIPV2', 'IPMultiIO'], DISCOVER_CLIMATE: [ 'Thermostat', 'ThermostatWall', 'MAXThermostat', 'ThermostatWall2', 'MAXWallThermostat', 'IPThermostat', 'IPThermostatWall', @@ -89,7 +89,8 @@ HM_DEVICE_TYPES = { 'MotionIP', 'RemoteMotion', 'WeatherSensor', 'TiltSensor', 'IPShutterContact', 'HMWIOSwitch', 'MaxShutterContact', 'Rain', 'WiredSensor', 'PresenceIP', 'IPWeatherSensor', 'IPPassageSensor', - 'SmartwareMotion', 'IPWeatherSensorPlus', 'MotionIPV2'], + 'SmartwareMotion', 'IPWeatherSensorPlus', 'MotionIPV2', 'WaterIP', + 'IPMultiIO'], DISCOVER_COVER: ['Blind', 'KeyBlind', 'IPKeyBlind', 'IPKeyBlindTilt'], DISCOVER_LOCKS: ['KeyMatic'] } diff --git a/requirements_all.txt b/requirements_all.txt index 787f9abc93c..f8368aabf2a 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1037,7 +1037,7 @@ pyhik==0.1.9 pyhiveapi==0.2.17 # homeassistant.components.homematic -pyhomematic==0.1.54 +pyhomematic==0.1.55 # homeassistant.components.homeworks pyhomeworks==0.0.6 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 2de73e85f79..e1c0b3c00d9 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -181,7 +181,7 @@ pydeconz==47 pydispatcher==2.0.5 # homeassistant.components.homematic -pyhomematic==0.1.54 +pyhomematic==0.1.55 # homeassistant.components.litejet pylitejet==0.1 From 55943cfac0c767d10a26877547dbe0b8565603e9 Mon Sep 17 00:00:00 2001 From: Jasper van der Neut - Stulen Date: Fri, 25 Jan 2019 07:36:25 +0100 Subject: [PATCH 090/222] Return windspeed and windgust in km/h instead of m/s. (#20340) Darksky dev docs state (https://darksky/dev/docs): `ca: same as si, except that windSpeed and windGust are in kilometers per hour` --- homeassistant/components/weather/darksky.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/weather/darksky.py b/homeassistant/components/weather/darksky.py index c753c0249ca..4ac3d2a1d22 100644 --- a/homeassistant/components/weather/darksky.py +++ b/homeassistant/components/weather/darksky.py @@ -71,7 +71,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): units = config.get(CONF_UNITS) if not units: - units = 'si' if hass.config.units.is_metric else 'us' + units = 'ca' if hass.config.units.is_metric else 'us' dark_sky = DarkSkyData( config.get(CONF_API_KEY), latitude, longitude, units) From a1da6a677ad6ca544b33fa8855a27b9a1fb27788 Mon Sep 17 00:00:00 2001 From: emontnemery Date: Fri, 25 Jan 2019 14:39:16 +0800 Subject: [PATCH 091/222] Update device registry of MQTT Switch (#19540) * MQTT Switch: Update device registry * Move config_entry to constructor * Remove duplicated code * Fix merge error --- homeassistant/components/mqtt/__init__.py | 20 ++++++++-- homeassistant/components/mqtt/switch.py | 11 +++--- tests/components/mqtt/test_switch.py | 47 +++++++++++++++++++++++ 3 files changed, 70 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/mqtt/__init__.py b/homeassistant/components/mqtt/__init__.py index fcaa05f7921..8868656eb79 100644 --- a/homeassistant/components/mqtt/__init__.py +++ b/homeassistant/components/mqtt/__init__.py @@ -21,8 +21,8 @@ import voluptuous as vol from homeassistant import config_entries from homeassistant.const import ( - CONF_PASSWORD, CONF_PAYLOAD, CONF_PORT, CONF_PROTOCOL, CONF_USERNAME, - CONF_VALUE_TEMPLATE, EVENT_HOMEASSISTANT_STOP, CONF_NAME) + CONF_DEVICE, CONF_PASSWORD, CONF_PAYLOAD, CONF_PORT, CONF_PROTOCOL, + CONF_USERNAME, CONF_VALUE_TEMPLATE, EVENT_HOMEASSISTANT_STOP, CONF_NAME) from homeassistant.core import Event, ServiceCall, callback from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import config_validation as cv @@ -996,9 +996,23 @@ class MqttDiscoveryUpdate(Entity): class MqttEntityDeviceInfo(Entity): """Mixin used for mqtt platforms that support the device registry.""" - def __init__(self, device_config: Optional[ConfigType]) -> None: + def __init__(self, device_config: Optional[ConfigType], + config_entry=None) -> None: """Initialize the device mixin.""" self._device_config = device_config + self._config_entry = config_entry + + async def device_info_discovery_update(self, config: dict): + """Handle updated discovery message.""" + self._device_config = config.get(CONF_DEVICE) + device_registry = await \ + self.hass.helpers.device_registry.async_get_registry() + config_entry_id = self._config_entry.entry_id + device_info = self.device_info + + if config_entry_id is not None and device_info is not None: + device_info['config_entry_id'] = config_entry_id + device_registry.async_get_or_create(**device_info) @property def device_info(self): diff --git a/homeassistant/components/mqtt/switch.py b/homeassistant/components/mqtt/switch.py index 8124dcf811b..ad4356b425b 100644 --- a/homeassistant/components/mqtt/switch.py +++ b/homeassistant/components/mqtt/switch.py @@ -65,7 +65,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): try: discovery_hash = discovery_payload[ATTR_DISCOVERY_HASH] config = PLATFORM_SCHEMA(discovery_payload) - await _async_setup_entity(config, async_add_entities, + await _async_setup_entity(config, async_add_entities, config_entry, discovery_hash) except Exception: if discovery_hash: @@ -77,10 +77,10 @@ async def async_setup_entry(hass, config_entry, async_add_entities): async_discover) -async def _async_setup_entity(config, async_add_entities, +async def _async_setup_entity(config, async_add_entities, config_entry=None, discovery_hash=None): """Set up the MQTT switch.""" - async_add_entities([MqttSwitch(config, discovery_hash)]) + async_add_entities([MqttSwitch(config, config_entry, discovery_hash)]) # pylint: disable=too-many-ancestors @@ -88,7 +88,7 @@ class MqttSwitch(MqttAttributes, MqttAvailability, MqttDiscoveryUpdate, MqttEntityDeviceInfo, SwitchDevice, RestoreEntity): """Representation of a switch that can be toggled using MQTT.""" - def __init__(self, config, discovery_hash): + def __init__(self, config, config_entry, discovery_hash): """Initialize the MQTT switch.""" self._state = False self._sub_state = None @@ -107,7 +107,7 @@ class MqttSwitch(MqttAttributes, MqttAvailability, MqttDiscoveryUpdate, MqttAvailability.__init__(self, config) MqttDiscoveryUpdate.__init__(self, discovery_hash, self.discovery_update) - MqttEntityDeviceInfo.__init__(self, device_config) + MqttEntityDeviceInfo.__init__(self, device_config, config_entry) async def async_added_to_hass(self): """Subscribe to MQTT events.""" @@ -120,6 +120,7 @@ class MqttSwitch(MqttAttributes, MqttAvailability, MqttDiscoveryUpdate, self._setup_from_config(config) await self.attributes_discovery_update(config) await self.availability_discovery_update(config) + await self.device_info_discovery_update(config) await self._subscribe_topics() self.async_schedule_update_ha_state() diff --git a/tests/components/mqtt/test_switch.py b/tests/components/mqtt/test_switch.py index b282b3149c4..5bbb04e1017 100644 --- a/tests/components/mqtt/test_switch.py +++ b/tests/components/mqtt/test_switch.py @@ -521,6 +521,53 @@ async def test_entity_device_info_with_identifier(hass, mqtt_mock): assert device.sw_version == '0.1-beta' +async def test_entity_device_info_update(hass, mqtt_mock): + """Test device registry update.""" + entry = MockConfigEntry(domain=mqtt.DOMAIN) + entry.add_to_hass(hass) + await async_start(hass, 'homeassistant', {}, entry) + registry = await hass.helpers.device_registry.async_get_registry() + + config = { + 'platform': 'mqtt', + 'name': 'Test 1', + 'state_topic': 'test-topic', + 'command_topic': 'test-command-topic', + 'device': { + 'identifiers': ['helloworld'], + 'connections': [ + ["mac", "02:5b:26:a8:dc:12"], + ], + 'manufacturer': 'Whatever', + 'name': 'Beer', + 'model': 'Glass', + 'sw_version': '0.1-beta', + }, + 'unique_id': 'veryunique' + } + + data = json.dumps(config) + async_fire_mqtt_message(hass, 'homeassistant/switch/bla/config', + data) + await hass.async_block_till_done() + await hass.async_block_till_done() + + device = registry.async_get_device({('mqtt', 'helloworld')}, set()) + assert device is not None + assert device.name == 'Beer' + + config['device']['name'] = 'Milk' + data = json.dumps(config) + async_fire_mqtt_message(hass, 'homeassistant/switch/bla/config', + data) + await hass.async_block_till_done() + await hass.async_block_till_done() + + device = registry.async_get_device({('mqtt', 'helloworld')}, set()) + assert device is not None + assert device.name == 'Milk' + + async def test_entity_id_update(hass, mqtt_mock): """Test MQTT subscriptions are managed when entity_id is updated.""" registry = mock_registry(hass, {}) From d84cd01cbfe4543490eca9c37db02aa61f017143 Mon Sep 17 00:00:00 2001 From: emontnemery Date: Fri, 25 Jan 2019 14:40:52 +0800 Subject: [PATCH 092/222] Cleanup if discovered mqtt light can't be added (#19740) * Cleanup if discovered mqtt light can't be added * No bare except * Clear ALREADY_DISCOVERED list with helper * Use constant instead of string literal --- .../components/mqtt/light/__init__.py | 23 ++++++---- .../components/mqtt/light/schema_basic.py | 2 +- .../components/mqtt/light/schema_json.py | 4 +- .../components/mqtt/light/schema_template.py | 2 +- tests/components/mqtt/test_light.py | 35 +++++++++++++- tests/components/mqtt/test_light_json.py | 36 ++++++++++++++- tests/components/mqtt/test_light_template.py | 46 ++++++++++++++++++- 7 files changed, 132 insertions(+), 16 deletions(-) diff --git a/homeassistant/components/mqtt/light/__init__.py b/homeassistant/components/mqtt/light/__init__.py index 93f32cd2791..77a1b1d3c10 100644 --- a/homeassistant/components/mqtt/light/__init__.py +++ b/homeassistant/components/mqtt/light/__init__.py @@ -10,7 +10,8 @@ import voluptuous as vol from homeassistant.components import light from homeassistant.components.mqtt import ATTR_DISCOVERY_HASH -from homeassistant.components.mqtt.discovery import MQTT_DISCOVERY_NEW +from homeassistant.components.mqtt.discovery import ( + MQTT_DISCOVERY_NEW, clear_discovery_hash) from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.typing import HomeAssistantType, ConfigType @@ -44,23 +45,29 @@ PLATFORM_SCHEMA = vol.All(vol.Schema({ async def async_setup_platform(hass: HomeAssistantType, config: ConfigType, async_add_entities, discovery_info=None): """Set up MQTT light through configuration.yaml.""" - await _async_setup_entity(hass, config, async_add_entities) + await _async_setup_entity(config, async_add_entities) async def async_setup_entry(hass, config_entry, async_add_entities): """Set up MQTT light dynamically through MQTT discovery.""" async def async_discover(discovery_payload): """Discover and add a MQTT light.""" - config = PLATFORM_SCHEMA(discovery_payload) - await _async_setup_entity(hass, config, async_add_entities, - discovery_payload[ATTR_DISCOVERY_HASH]) + try: + discovery_hash = discovery_payload[ATTR_DISCOVERY_HASH] + config = PLATFORM_SCHEMA(discovery_payload) + await _async_setup_entity(config, async_add_entities, + discovery_hash) + except Exception: + if discovery_hash: + clear_discovery_hash(hass, discovery_hash) + raise async_dispatcher_connect( hass, MQTT_DISCOVERY_NEW.format(light.DOMAIN, 'mqtt'), async_discover) -async def _async_setup_entity(hass, config, async_add_entities, +async def _async_setup_entity(config, async_add_entities, discovery_hash=None): """Set up a MQTT Light.""" setup_entity = { @@ -68,5 +75,5 @@ async def _async_setup_entity(hass, config, async_add_entities, 'json': schema_json.async_setup_entity_json, 'template': schema_template.async_setup_entity_template, } - await setup_entity[config['schema']]( - hass, config, async_add_entities, discovery_hash) + await setup_entity[config[CONF_SCHEMA]]( + config, async_add_entities, discovery_hash) diff --git a/homeassistant/components/mqtt/light/schema_basic.py b/homeassistant/components/mqtt/light/schema_basic.py index 3be8de5c722..d9f676c8b38 100644 --- a/homeassistant/components/mqtt/light/schema_basic.py +++ b/homeassistant/components/mqtt/light/schema_basic.py @@ -112,7 +112,7 @@ PLATFORM_SCHEMA_BASIC = mqtt.MQTT_RW_PLATFORM_SCHEMA.extend({ mqtt.MQTT_JSON_ATTRS_SCHEMA.schema) -async def async_setup_entity_basic(hass, config, async_add_entities, +async def async_setup_entity_basic(config, async_add_entities, discovery_hash=None): """Set up a MQTT Light.""" config.setdefault( diff --git a/homeassistant/components/mqtt/light/schema_json.py b/homeassistant/components/mqtt/light/schema_json.py index 1c32b0c5783..fcf31f097cc 100644 --- a/homeassistant/components/mqtt/light/schema_json.py +++ b/homeassistant/components/mqtt/light/schema_json.py @@ -25,7 +25,7 @@ from homeassistant.const import ( from homeassistant.core import callback import homeassistant.helpers.config_validation as cv from homeassistant.helpers.restore_state import RestoreEntity -from homeassistant.helpers.typing import ConfigType, HomeAssistantType +from homeassistant.helpers.typing import ConfigType import homeassistant.util.color as color_util from .schema_basic import CONF_BRIGHTNESS_SCALE @@ -85,7 +85,7 @@ PLATFORM_SCHEMA_JSON = mqtt.MQTT_RW_PLATFORM_SCHEMA.extend({ mqtt.MQTT_JSON_ATTRS_SCHEMA.schema) -async def async_setup_entity_json(hass: HomeAssistantType, config: ConfigType, +async def async_setup_entity_json(config: ConfigType, async_add_entities, discovery_hash): """Set up a MQTT JSON Light.""" async_add_entities([MqttLightJson(config, discovery_hash)]) diff --git a/homeassistant/components/mqtt/light/schema_template.py b/homeassistant/components/mqtt/light/schema_template.py index 7020550710b..09aaa359058 100644 --- a/homeassistant/components/mqtt/light/schema_template.py +++ b/homeassistant/components/mqtt/light/schema_template.py @@ -71,7 +71,7 @@ PLATFORM_SCHEMA_TEMPLATE = mqtt.MQTT_RW_PLATFORM_SCHEMA.extend({ mqtt.MQTT_JSON_ATTRS_SCHEMA.schema) -async def async_setup_entity_template(hass, config, async_add_entities, +async def async_setup_entity_template(config, async_add_entities, discovery_hash): """Set up a MQTT Template light.""" async_add_entities([MqttTemplate(config, discovery_hash)]) diff --git a/tests/components/mqtt/test_light.py b/tests/components/mqtt/test_light.py index a424263af8c..1b1ba3862e9 100644 --- a/tests/components/mqtt/test_light.py +++ b/tests/components/mqtt/test_light.py @@ -1239,7 +1239,7 @@ async def test_discovery_deprecated(hass, mqtt_mock, caplog): async def test_discovery_update_light(hass, mqtt_mock, caplog): - """Test removal of discovered light.""" + """Test update of discovered light.""" entry = MockConfigEntry(domain=mqtt.DOMAIN) await async_start(hass, 'homeassistant', {}, entry) @@ -1274,6 +1274,39 @@ async def test_discovery_update_light(hass, mqtt_mock, caplog): assert state is None +async def test_discovery_broken(hass, mqtt_mock, caplog): + """Test handling of bad discovery message.""" + entry = MockConfigEntry(domain=mqtt.DOMAIN) + await async_start(hass, 'homeassistant', {}, entry) + + data1 = ( + '{ "name": "Beer" }' + ) + data2 = ( + '{ "name": "Milk",' + ' "status_topic": "test_topic",' + ' "command_topic": "test_topic" }' + ) + + async_fire_mqtt_message(hass, 'homeassistant/light/bla/config', + data1) + await hass.async_block_till_done() + + state = hass.states.get('light.beer') + assert state is None + + async_fire_mqtt_message(hass, 'homeassistant/light/bla/config', + data2) + await hass.async_block_till_done() + await hass.async_block_till_done() + + state = hass.states.get('light.milk') + assert state is not None + assert state.name == 'Milk' + state = hass.states.get('light.beer') + assert state is None + + async def test_entity_device_info_with_identifier(hass, mqtt_mock): """Test MQTT light device registry integration.""" entry = MockConfigEntry(domain=mqtt.DOMAIN) diff --git a/tests/components/mqtt/test_light_json.py b/tests/components/mqtt/test_light_json.py index 7621da724c9..c8d7c1d3e54 100644 --- a/tests/components/mqtt/test_light_json.py +++ b/tests/components/mqtt/test_light_json.py @@ -707,7 +707,7 @@ async def test_discovery_deprecated(hass, mqtt_mock, caplog): async def test_discovery_update_light(hass, mqtt_mock, caplog): - """Test removal of discovered light.""" + """Test update of discovered light.""" entry = MockConfigEntry(domain=mqtt.DOMAIN) await async_start(hass, 'homeassistant', {}, entry) @@ -744,6 +744,40 @@ async def test_discovery_update_light(hass, mqtt_mock, caplog): assert state is None +async def test_discovery_broken(hass, mqtt_mock, caplog): + """Test handling of bad discovery message.""" + entry = MockConfigEntry(domain=mqtt.DOMAIN) + await async_start(hass, 'homeassistant', {}, entry) + + data1 = ( + '{ "name": "Beer" }' + ) + data2 = ( + '{ "name": "Milk",' + ' "schema": "json",' + ' "status_topic": "test_topic",' + ' "command_topic": "test_topic" }' + ) + + async_fire_mqtt_message(hass, 'homeassistant/light/bla/config', + data1) + await hass.async_block_till_done() + + state = hass.states.get('light.beer') + assert state is None + + async_fire_mqtt_message(hass, 'homeassistant/light/bla/config', + data2) + await hass.async_block_till_done() + await hass.async_block_till_done() + + state = hass.states.get('light.milk') + assert state is not None + assert state.name == 'Milk' + state = hass.states.get('light.beer') + assert state is None + + async def test_entity_device_info_with_identifier(hass, mqtt_mock): """Test MQTT light device registry integration.""" entry = MockConfigEntry(domain=mqtt.DOMAIN) diff --git a/tests/components/mqtt/test_light_template.py b/tests/components/mqtt/test_light_template.py index 509f2ee5d36..13fe086684c 100644 --- a/tests/components/mqtt/test_light_template.py +++ b/tests/components/mqtt/test_light_template.py @@ -627,7 +627,7 @@ async def test_unique_id(hass): assert len(hass.states.async_entity_ids(light.DOMAIN)) == 1 -async def test_discovery(hass, mqtt_mock, caplog): +async def test_discovery_removal(hass, mqtt_mock, caplog): """Test removal of discovered mqtt_json lights.""" entry = MockConfigEntry(domain=mqtt.DOMAIN) await async_start(hass, 'homeassistant', {'mqtt': {}}, entry) @@ -644,6 +644,12 @@ async def test_discovery(hass, mqtt_mock, caplog): state = hass.states.get('light.beer') assert state is not None assert state.name == 'Beer' + async_fire_mqtt_message(hass, 'homeassistant/light/bla/config', + '') + await hass.async_block_till_done() + await hass.async_block_till_done() + state = hass.states.get('light.beer') + assert state is None async def test_discovery_deprecated(hass, mqtt_mock, caplog): @@ -666,7 +672,7 @@ async def test_discovery_deprecated(hass, mqtt_mock, caplog): async def test_discovery_update_light(hass, mqtt_mock, caplog): - """Test removal of discovered light.""" + """Test update of discovered light.""" entry = MockConfigEntry(domain=mqtt.DOMAIN) await async_start(hass, 'homeassistant', {}, entry) @@ -707,6 +713,42 @@ async def test_discovery_update_light(hass, mqtt_mock, caplog): assert state is None +async def test_discovery_broken(hass, mqtt_mock, caplog): + """Test handling of bad discovery message.""" + entry = MockConfigEntry(domain=mqtt.DOMAIN) + await async_start(hass, 'homeassistant', {}, entry) + + data1 = ( + '{ "name": "Beer" }' + ) + data2 = ( + '{ "name": "Milk",' + ' "schema": "template",' + ' "status_topic": "test_topic",' + ' "command_topic": "test_topic",' + ' "command_on_template": "on",' + ' "command_off_template": "off"}' + ) + + async_fire_mqtt_message(hass, 'homeassistant/light/bla/config', + data1) + await hass.async_block_till_done() + + state = hass.states.get('light.beer') + assert state is None + + async_fire_mqtt_message(hass, 'homeassistant/light/bla/config', + data2) + await hass.async_block_till_done() + await hass.async_block_till_done() + + state = hass.states.get('light.milk') + assert state is not None + assert state.name == 'Milk' + state = hass.states.get('light.beer') + assert state is None + + async def test_entity_device_info_with_identifier(hass, mqtt_mock): """Test MQTT light device registry integration.""" entry = MockConfigEntry(domain=mqtt.DOMAIN) From ec5da05804dd66c2199bebb5f3a023b9656a0c63 Mon Sep 17 00:00:00 2001 From: emontnemery Date: Fri, 25 Jan 2019 14:43:56 +0800 Subject: [PATCH 093/222] Add character encoding to MQTT automation. (#20292) --- homeassistant/components/automation/mqtt.py | 8 +++- tests/components/automation/test_mqtt.py | 42 +++++++++++++++++++++ 2 files changed, 48 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/automation/mqtt.py b/homeassistant/components/automation/mqtt.py index 67c538154e5..1fa0d540610 100644 --- a/homeassistant/components/automation/mqtt.py +++ b/homeassistant/components/automation/mqtt.py @@ -15,19 +15,23 @@ import homeassistant.helpers.config_validation as cv DEPENDENCIES = ['mqtt'] +CONF_ENCODING = 'encoding' CONF_TOPIC = 'topic' +DEFAULT_ENCODING = 'utf-8' TRIGGER_SCHEMA = vol.Schema({ vol.Required(CONF_PLATFORM): mqtt.DOMAIN, vol.Required(CONF_TOPIC): mqtt.valid_subscribe_topic, vol.Optional(CONF_PAYLOAD): cv.string, + vol.Optional(CONF_ENCODING, default=DEFAULT_ENCODING): cv.string, }) async def async_trigger(hass, config, action, automation_info): """Listen for state changes based on configuration.""" - topic = config.get(CONF_TOPIC) + topic = config[CONF_TOPIC] payload = config.get(CONF_PAYLOAD) + encoding = config[CONF_ENCODING] or None @callback def mqtt_automation_listener(msg_topic, msg_payload, qos): @@ -50,5 +54,5 @@ async def async_trigger(hass, config, action, automation_info): }) remove = await mqtt.async_subscribe( - hass, topic, mqtt_automation_listener) + hass, topic, mqtt_automation_listener, encoding=encoding) return remove diff --git a/tests/components/automation/test_mqtt.py b/tests/components/automation/test_mqtt.py index 196fdaa9a6f..7d2fe5fa439 100644 --- a/tests/components/automation/test_mqtt.py +++ b/tests/components/automation/test_mqtt.py @@ -1,5 +1,6 @@ """The tests for the MQTT automation.""" import pytest +from unittest import mock from homeassistant.setup import async_setup_component import homeassistant.components.automation as automation @@ -92,3 +93,44 @@ async def test_if_not_fires_on_topic_but_no_payload_match(hass, calls): async_fire_mqtt_message(hass, 'test-topic', 'no-hello') await hass.async_block_till_done() assert 0 == len(calls) + + +async def test_encoding_default(hass, calls): + """Test default encoding.""" + mock_mqtt = await async_mock_mqtt_component(hass) + + assert await async_setup_component(hass, automation.DOMAIN, { + automation.DOMAIN: { + 'trigger': { + 'platform': 'mqtt', + 'topic': 'test-topic' + }, + 'action': { + 'service': 'test.automation' + } + } + }) + + mock_mqtt.async_subscribe.assert_called_once_with( + 'test-topic', mock.ANY, 0, 'utf-8') + + +async def test_encoding_custom(hass, calls): + """Test default encoding.""" + mock_mqtt = await async_mock_mqtt_component(hass) + + assert await async_setup_component(hass, automation.DOMAIN, { + automation.DOMAIN: { + 'trigger': { + 'platform': 'mqtt', + 'topic': 'test-topic', + 'encoding': '' + }, + 'action': { + 'service': 'test.automation' + } + } + }) + + mock_mqtt.async_subscribe.assert_called_once_with( + 'test-topic', mock.ANY, 0, None) From c94834d8f6b9284d80101625ae773430d4f9d49f Mon Sep 17 00:00:00 2001 From: Anders Melchiorsen Date: Fri, 25 Jan 2019 07:50:26 +0100 Subject: [PATCH 094/222] Add LIFX listen port advanced configuration (#20299) --- homeassistant/components/lifx/__init__.py | 2 ++ homeassistant/components/light/lifx.py | 6 +++++- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/lifx/__init__.py b/homeassistant/components/lifx/__init__.py index f2713197ed1..a2ae6266a8d 100644 --- a/homeassistant/components/lifx/__init__.py +++ b/homeassistant/components/lifx/__init__.py @@ -3,6 +3,7 @@ import voluptuous as vol import homeassistant.helpers.config_validation as cv from homeassistant import config_entries +from homeassistant.const import CONF_PORT from homeassistant.helpers import config_entry_flow from homeassistant.components.light import DOMAIN as LIGHT_DOMAIN @@ -15,6 +16,7 @@ CONF_BROADCAST = 'broadcast' INTERFACE_SCHEMA = vol.Schema({ vol.Optional(CONF_SERVER): cv.string, + vol.Optional(CONF_PORT): cv.port, vol.Optional(CONF_BROADCAST): cv.string, }) diff --git a/homeassistant/components/light/lifx.py b/homeassistant/components/light/lifx.py index 8951b2876a2..f448d61ee04 100644 --- a/homeassistant/components/light/lifx.py +++ b/homeassistant/components/light/lifx.py @@ -22,7 +22,8 @@ from homeassistant.components.light import ( SUPPORT_TRANSITION, VALID_BRIGHTNESS, VALID_BRIGHTNESS_PCT, Light, preprocess_turn_on_alternatives) from homeassistant.components.lifx import ( - DOMAIN as LIFX_DOMAIN, DATA_LIFX_MANAGER, CONF_SERVER, CONF_BROADCAST) + DOMAIN as LIFX_DOMAIN, DATA_LIFX_MANAGER, CONF_SERVER, CONF_PORT, + CONF_BROADCAST) from homeassistant.const import ATTR_ENTITY_ID, EVENT_HOMEASSISTANT_STOP from homeassistant.core import callback import homeassistant.helpers.config_validation as cv @@ -230,6 +231,9 @@ class LIFXManager: listen_ip = interface.get(CONF_SERVER) if listen_ip: kwargs['listen_ip'] = listen_ip + listen_port = interface.get(CONF_PORT) + if listen_port: + kwargs['listen_port'] = listen_port lifx_discovery.start(**kwargs) self.discoveries.append(lifx_discovery) From d4c7515681c322e34c36686f77421b9135775a49 Mon Sep 17 00:00:00 2001 From: Andrey Kupreychik Date: Fri, 25 Jan 2019 17:07:45 +0700 Subject: [PATCH 095/222] Add time_throttle filter to sensor.filter (#20334) * Added time_throttle filter * Added time_throttle filter test * Updated comments for time_throttle filter --- homeassistant/components/sensor/filter.py | 36 ++++++++++++++++++++++- tests/components/sensor/test_filter.py | 14 ++++++++- 2 files changed, 48 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/sensor/filter.py b/homeassistant/components/sensor/filter.py index 42589d6bed3..3d05dd28e79 100644 --- a/homeassistant/components/sensor/filter.py +++ b/homeassistant/components/sensor/filter.py @@ -32,6 +32,7 @@ FILTER_NAME_RANGE = 'range' FILTER_NAME_LOWPASS = 'lowpass' FILTER_NAME_OUTLIER = 'outlier' FILTER_NAME_THROTTLE = 'throttle' +FILTER_NAME_TIME_THROTTLE = 'time_throttle' FILTER_NAME_TIME_SMA = 'time_simple_moving_average' FILTERS = Registry() @@ -101,6 +102,12 @@ FILTER_THROTTLE_SCHEMA = FILTER_SCHEMA.extend({ default=DEFAULT_WINDOW_SIZE): vol.Coerce(int), }) +FILTER_TIME_THROTTLE_SCHEMA = FILTER_SCHEMA.extend({ + vol.Required(CONF_FILTER_NAME): FILTER_NAME_TIME_THROTTLE, + vol.Required(CONF_FILTER_WINDOW_SIZE): vol.All(cv.time_period, + cv.positive_timedelta) +}) + PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Required(CONF_ENTITY_ID): cv.entity_id, vol.Optional(CONF_NAME): cv.string, @@ -109,6 +116,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ FILTER_LOWPASS_SCHEMA, FILTER_TIME_SMA_SCHEMA, FILTER_THROTTLE_SCHEMA, + FILTER_TIME_THROTTLE_SCHEMA, FILTER_RANGE_SCHEMA)]) }) @@ -444,7 +452,7 @@ class TimeSMAFilter(Filter): The window_size is determined by time, and SMA is time weighted. Args: - variant (enum): type of argorithm used to connect discrete values + type (enum): type of algorithm used to connect discrete values """ def __init__(self, window_size, precision, entity, @@ -502,3 +510,29 @@ class ThrottleFilter(Filter): self._skip_processing = True return new_state + + +@FILTERS.register(FILTER_NAME_TIME_THROTTLE) +class TimeThrottleFilter(Filter): + """Time Throttle Filter. + + One sample per time period. + """ + + def __init__(self, window_size, precision, entity): + """Initialize Filter.""" + super().__init__(FILTER_NAME_TIME_THROTTLE, + window_size, precision, entity) + self._time_window = window_size + self._last_emitted_at = None + + def _filter_state(self, new_state): + """Implement the filter.""" + window_start = new_state.timestamp - self._time_window + if not self._last_emitted_at or self._last_emitted_at <= window_start: + self._last_emitted_at = new_state.timestamp + self._skip_processing = False + else: + self._skip_processing = True + + return new_state diff --git a/tests/components/sensor/test_filter.py b/tests/components/sensor/test_filter.py index 3d44b7d131d..29718314ef4 100644 --- a/tests/components/sensor/test_filter.py +++ b/tests/components/sensor/test_filter.py @@ -5,7 +5,7 @@ from unittest.mock import patch from homeassistant.components.sensor.filter import ( LowPassFilter, OutlierFilter, ThrottleFilter, TimeSMAFilter, - RangeFilter) + RangeFilter, TimeThrottleFilter) import homeassistant.util.dt as dt_util from homeassistant.setup import setup_component import homeassistant.core as ha @@ -178,6 +178,18 @@ class TestFilterSensor(unittest.TestCase): filtered.append(new_state) assert [20, 21] == [f.state for f in filtered] + def test_time_throttle(self): + """Test if lowpass filter works.""" + filt = TimeThrottleFilter(window_size=timedelta(minutes=2), + precision=2, + entity=None) + filtered = [] + for state in self.values: + new_state = filt.filter_state(state) + if not filt.skip_processing: + filtered.append(new_state) + assert [20, 18, 22] == [f.state for f in filtered] + def test_time_sma(self): """Test if time_sma filter works.""" filt = TimeSMAFilter(window_size=timedelta(minutes=2), From 1fcaaf93ad2ad78b478888a053216b86b5f95346 Mon Sep 17 00:00:00 2001 From: jonudewux Date: Fri, 25 Jan 2019 12:57:13 +0200 Subject: [PATCH 096/222] Upgrade youtube_dl to 2019.01.24 (#20408) --- 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 973fae9a0de..333f62a9aa7 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==2019.01.17'] +REQUIREMENTS = ['youtube_dl==2019.01.24'] _LOGGER = logging.getLogger(__name__) diff --git a/requirements_all.txt b/requirements_all.txt index f8368aabf2a..8d93a499c9d 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1742,7 +1742,7 @@ yeelight==0.4.3 yeelightsunflower==0.0.10 # homeassistant.components.media_extractor -youtube_dl==2019.01.17 +youtube_dl==2019.01.24 # homeassistant.components.light.zengge zengge==0.2 From cea2bf94bdbfea3274eb7b12fd764fb1e6533519 Mon Sep 17 00:00:00 2001 From: Jc2k Date: Fri, 25 Jan 2019 15:43:01 +0000 Subject: [PATCH 097/222] Move homekit_controller entity types under homekit_controller platform (#20376) * Move homekit_controller entity types under homekit_controller platform * Update coveragerc as homekit_controller moved --- .coveragerc | 3 +-- .../alarm_control_panel.py} | 0 .../homekit_controller.py => homekit_controller/climate.py} | 0 .../homekit_controller.py => homekit_controller/cover.py} | 0 .../homekit_controller.py => homekit_controller/light.py} | 0 .../{lock/homekit_controller.py => homekit_controller/lock.py} | 0 .../homekit_controller.py => homekit_controller/switch.py} | 0 7 files changed, 1 insertion(+), 2 deletions(-) rename homeassistant/components/{alarm_control_panel/homekit_controller.py => homekit_controller/alarm_control_panel.py} (100%) rename homeassistant/components/{climate/homekit_controller.py => homekit_controller/climate.py} (100%) rename homeassistant/components/{cover/homekit_controller.py => homekit_controller/cover.py} (100%) rename homeassistant/components/{light/homekit_controller.py => homekit_controller/light.py} (100%) rename homeassistant/components/{lock/homekit_controller.py => homekit_controller/lock.py} (100%) rename homeassistant/components/{switch/homekit_controller.py => homekit_controller/switch.py} (100%) diff --git a/.coveragerc b/.coveragerc index 04d8f0203be..e6a1c752f87 100644 --- a/.coveragerc +++ b/.coveragerc @@ -168,8 +168,7 @@ omit = homeassistant/components/hlk_sw16.py homeassistant/components/*/hlk_sw16.py - homeassistant/components/homekit_controller/__init__.py - homeassistant/components/*/homekit_controller.py + homeassistant/components/homekit_controller/* homeassistant/components/homematic/__init__.py homeassistant/components/*/homematic.py diff --git a/homeassistant/components/alarm_control_panel/homekit_controller.py b/homeassistant/components/homekit_controller/alarm_control_panel.py similarity index 100% rename from homeassistant/components/alarm_control_panel/homekit_controller.py rename to homeassistant/components/homekit_controller/alarm_control_panel.py diff --git a/homeassistant/components/climate/homekit_controller.py b/homeassistant/components/homekit_controller/climate.py similarity index 100% rename from homeassistant/components/climate/homekit_controller.py rename to homeassistant/components/homekit_controller/climate.py diff --git a/homeassistant/components/cover/homekit_controller.py b/homeassistant/components/homekit_controller/cover.py similarity index 100% rename from homeassistant/components/cover/homekit_controller.py rename to homeassistant/components/homekit_controller/cover.py diff --git a/homeassistant/components/light/homekit_controller.py b/homeassistant/components/homekit_controller/light.py similarity index 100% rename from homeassistant/components/light/homekit_controller.py rename to homeassistant/components/homekit_controller/light.py diff --git a/homeassistant/components/lock/homekit_controller.py b/homeassistant/components/homekit_controller/lock.py similarity index 100% rename from homeassistant/components/lock/homekit_controller.py rename to homeassistant/components/homekit_controller/lock.py diff --git a/homeassistant/components/switch/homekit_controller.py b/homeassistant/components/homekit_controller/switch.py similarity index 100% rename from homeassistant/components/switch/homekit_controller.py rename to homeassistant/components/homekit_controller/switch.py From d6c12e47f41c10324ae14aa7862134b03e254aa4 Mon Sep 17 00:00:00 2001 From: zewelor Date: Fri, 25 Jan 2019 18:49:50 +0100 Subject: [PATCH 098/222] Fix cast platform album name property (#20411) --- homeassistant/components/media_player/cast.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/media_player/cast.py b/homeassistant/components/media_player/cast.py index d6515b9476d..20a44c0e910 100644 --- a/homeassistant/components/media_player/cast.py +++ b/homeassistant/components/media_player/cast.py @@ -613,7 +613,7 @@ class CastDevice(MediaPlayerDevice): return self.media_status.artist if self.media_status else None @property - def media_album(self): + def media_album_name(self): """Album of current playing media (Music track only).""" return self.media_status.album_name if self.media_status else None From 4518e6bdf7d06ac9a15a1131c82fccae4029937f Mon Sep 17 00:00:00 2001 From: coreGreenberet Date: Fri, 25 Jan 2019 19:00:37 +0100 Subject: [PATCH 099/222] Fix minor homematicip cloud binary sensor issues (#20398) * fix for smoke detection * a tilted window is now considered as "open"/on * changed comparison to enum * line length * insert brackets for line length and comparison * indentation should now be ok for hound changed api version to 0.10.4 * indentation should now be ok for hound changed api version to 0.10.4 * updating requirement files * satisfy lint --- .../components/binary_sensor/homematicip_cloud.py | 8 ++++---- homeassistant/components/homematicip_cloud/__init__.py | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/binary_sensor/homematicip_cloud.py b/homeassistant/components/binary_sensor/homematicip_cloud.py index aba979b94f2..910666f93cb 100644 --- a/homeassistant/components/binary_sensor/homematicip_cloud.py +++ b/homeassistant/components/binary_sensor/homematicip_cloud.py @@ -15,8 +15,6 @@ DEPENDENCIES = ['homematicip_cloud'] _LOGGER = logging.getLogger(__name__) -STATE_SMOKE_OFF = 'IDLE_OFF' - async def async_setup_platform( hass, config, async_add_entities, discovery_info=None): @@ -65,7 +63,7 @@ class HomematicipShutterContact(HomematicipGenericDevice, BinarySensorDevice): return True if self._device.windowState is None: return None - return self._device.windowState == WindowState.OPEN + return self._device.windowState != WindowState.CLOSED class HomematicipMotionDetector(HomematicipGenericDevice, BinarySensorDevice): @@ -95,7 +93,9 @@ class HomematicipSmokeDetector(HomematicipGenericDevice, BinarySensorDevice): @property def is_on(self): """Return true if smoke is detected.""" - return self._device.smokeDetectorAlarmType != STATE_SMOKE_OFF + from homematicip.base.enums import SmokeDetectorAlarmType + return (self._device.smokeDetectorAlarmType + != SmokeDetectorAlarmType.IDLE_OFF) class HomematicipWaterDetector(HomematicipGenericDevice, BinarySensorDevice): diff --git a/homeassistant/components/homematicip_cloud/__init__.py b/homeassistant/components/homematicip_cloud/__init__.py index 700e6274c35..b0ea1a3b348 100644 --- a/homeassistant/components/homematicip_cloud/__init__.py +++ b/homeassistant/components/homematicip_cloud/__init__.py @@ -19,7 +19,7 @@ from .const import ( from .device import HomematicipGenericDevice # noqa: F401 from .hap import HomematicipAuth, HomematicipHAP # noqa: F401 -REQUIREMENTS = ['homematicip==0.10.3'] +REQUIREMENTS = ['homematicip==0.10.4'] _LOGGER = logging.getLogger(__name__) diff --git a/requirements_all.txt b/requirements_all.txt index 8d93a499c9d..72ae6e59b10 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -529,7 +529,7 @@ homeassistant-pyozw==0.1.2 # homekit==0.12.2 # homeassistant.components.homematicip_cloud -homematicip==0.10.3 +homematicip==0.10.4 # homeassistant.components.google # homeassistant.components.remember_the_milk diff --git a/requirements_test_all.txt b/requirements_test_all.txt index e1c0b3c00d9..659f02459d3 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -113,7 +113,7 @@ holidays==0.9.9 home-assistant-frontend==20190121.1 # homeassistant.components.homematicip_cloud -homematicip==0.10.3 +homematicip==0.10.4 # homeassistant.components.influxdb # homeassistant.components.sensor.influxdb From 2bc7444427623686209e376a97efc2043b96c3fb Mon Sep 17 00:00:00 2001 From: coreGreenberet Date: Fri, 25 Jan 2019 19:45:42 +0100 Subject: [PATCH 100/222] Fix homematicip cloud alarm_arm_home (#20321) --- .../components/alarm_control_panel/homematicip_cloud.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/alarm_control_panel/homematicip_cloud.py b/homeassistant/components/alarm_control_panel/homematicip_cloud.py index 8c483121650..3fdfc768c52 100644 --- a/homeassistant/components/alarm_control_panel/homematicip_cloud.py +++ b/homeassistant/components/alarm_control_panel/homematicip_cloud.py @@ -76,7 +76,7 @@ class HomematicipSecurityZone(HomematicipGenericDevice, AlarmControlPanel): async def async_alarm_arm_home(self, code=None): """Send arm home command.""" - await self._home.set_security_zones_activation(True, False) + await self._home.set_security_zones_activation(False, True) async def async_alarm_arm_away(self, code=None): """Send arm away command.""" From 76c02954037f32db49ca95ee7d3a6bdf8b20aeed Mon Sep 17 00:00:00 2001 From: kennedyshead Date: Fri, 25 Jan 2019 23:27:44 +0100 Subject: [PATCH 101/222] Bumps aioasuswrt (#20432) --- homeassistant/components/asuswrt.py | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/asuswrt.py b/homeassistant/components/asuswrt.py index 8dedb1f640a..898485b5cb3 100644 --- a/homeassistant/components/asuswrt.py +++ b/homeassistant/components/asuswrt.py @@ -14,7 +14,7 @@ from homeassistant.const import ( from homeassistant.helpers import config_validation as cv from homeassistant.helpers.discovery import async_load_platform -REQUIREMENTS = ['aioasuswrt==1.1.17'] +REQUIREMENTS = ['aioasuswrt==1.1.18'] _LOGGER = logging.getLogger(__name__) diff --git a/requirements_all.txt b/requirements_all.txt index 72ae6e59b10..52ff7a3563a 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -87,7 +87,7 @@ abodepy==0.15.0 afsapi==0.0.4 # homeassistant.components.asuswrt -aioasuswrt==1.1.17 +aioasuswrt==1.1.18 # homeassistant.components.device_tracker.automatic aioautomatic==0.6.5 From d8e43978b7d63ccb1fb1c74fa0f04566cd87574e Mon Sep 17 00:00:00 2001 From: emontnemery Date: Tue, 1 Jan 2019 11:11:55 +0100 Subject: [PATCH 102/222] Update device registry of MQTT binary_sensor --- .../components/mqtt/binary_sensor.py | 13 +++-- tests/components/mqtt/test_binary_sensor.py | 47 +++++++++++++++++++ 2 files changed, 55 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/mqtt/binary_sensor.py b/homeassistant/components/mqtt/binary_sensor.py index 95886a46299..f066116892e 100644 --- a/homeassistant/components/mqtt/binary_sensor.py +++ b/homeassistant/components/mqtt/binary_sensor.py @@ -65,7 +65,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): try: discovery_hash = discovery_payload[ATTR_DISCOVERY_HASH] config = PLATFORM_SCHEMA(discovery_payload) - await _async_setup_entity(config, async_add_entities, + await _async_setup_entity(config, async_add_entities, config_entry, discovery_hash) except Exception: if discovery_hash: @@ -77,16 +77,18 @@ async def async_setup_entry(hass, config_entry, async_add_entities): async_discover) -async def _async_setup_entity(config, async_add_entities, discovery_hash=None): +async def _async_setup_entity(config, async_add_entities, config_entry=None, + discovery_hash=None): """Set up the MQTT binary sensor.""" - async_add_entities([MqttBinarySensor(config, discovery_hash)]) + async_add_entities([MqttBinarySensor(config, config_entry, + discovery_hash)]) class MqttBinarySensor(MqttAttributes, MqttAvailability, MqttDiscoveryUpdate, MqttEntityDeviceInfo, BinarySensorDevice): """Representation a binary sensor that is updated by MQTT.""" - def __init__(self, config, discovery_hash): + def __init__(self, config, config_entry, discovery_hash): """Initialize the MQTT binary sensor.""" self._config = config self._unique_id = config.get(CONF_UNIQUE_ID) @@ -100,7 +102,7 @@ class MqttBinarySensor(MqttAttributes, MqttAvailability, MqttDiscoveryUpdate, MqttAvailability.__init__(self, config) MqttDiscoveryUpdate.__init__(self, discovery_hash, self.discovery_update) - MqttEntityDeviceInfo.__init__(self, device_config) + MqttEntityDeviceInfo.__init__(self, device_config, config_entry) async def async_added_to_hass(self): """Subscribe mqtt events.""" @@ -113,6 +115,7 @@ class MqttBinarySensor(MqttAttributes, MqttAvailability, MqttDiscoveryUpdate, self._config = config await self.attributes_discovery_update(config) await self.availability_discovery_update(config) + await self.device_info_discovery_update(config) await self._subscribe_topics() self.async_schedule_update_ha_state() diff --git a/tests/components/mqtt/test_binary_sensor.py b/tests/components/mqtt/test_binary_sensor.py index f17d4e65c30..0f40312cb2d 100644 --- a/tests/components/mqtt/test_binary_sensor.py +++ b/tests/components/mqtt/test_binary_sensor.py @@ -531,6 +531,53 @@ async def test_entity_device_info_with_identifier(hass, mqtt_mock): assert device.sw_version == '0.1-beta' +async def test_entity_device_info_update(hass, mqtt_mock): + """Test device registry update.""" + entry = MockConfigEntry(domain=mqtt.DOMAIN) + entry.add_to_hass(hass) + await async_start(hass, 'homeassistant', {}, entry) + registry = await hass.helpers.device_registry.async_get_registry() + + config = { + 'platform': 'mqtt', + 'name': 'Test 1', + 'state_topic': 'test-topic', + 'command_topic': 'test-command-topic', + 'device': { + 'identifiers': ['helloworld'], + 'connections': [ + ["mac", "02:5b:26:a8:dc:12"], + ], + 'manufacturer': 'Whatever', + 'name': 'Beer', + 'model': 'Glass', + 'sw_version': '0.1-beta', + }, + 'unique_id': 'veryunique' + } + + data = json.dumps(config) + async_fire_mqtt_message(hass, 'homeassistant/binary_sensor/bla/config', + data) + await hass.async_block_till_done() + await hass.async_block_till_done() + + device = registry.async_get_device({('mqtt', 'helloworld')}, set()) + assert device is not None + assert device.name == 'Beer' + + config['device']['name'] = 'Milk' + data = json.dumps(config) + async_fire_mqtt_message(hass, 'homeassistant/binary_sensor/bla/config', + data) + await hass.async_block_till_done() + await hass.async_block_till_done() + + device = registry.async_get_device({('mqtt', 'helloworld')}, set()) + assert device is not None + assert device.name == 'Milk' + + async def test_entity_id_update(hass, mqtt_mock): """Test MQTT subscriptions are managed when entity_id is updated.""" registry = mock_registry(hass, {}) From 6d2e7db12315df9090b20469f049fabae9059332 Mon Sep 17 00:00:00 2001 From: emontnemery Date: Tue, 1 Jan 2019 11:12:40 +0100 Subject: [PATCH 103/222] Update device registry of MQTT climate --- homeassistant/components/mqtt/climate.py | 17 ++++----- tests/components/mqtt/test_climate.py | 47 ++++++++++++++++++++++++ 2 files changed, 54 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/mqtt/climate.py b/homeassistant/components/mqtt/climate.py index b317d11d10b..9db96fdff94 100644 --- a/homeassistant/components/mqtt/climate.py +++ b/homeassistant/components/mqtt/climate.py @@ -163,7 +163,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): discovery_hash = discovery_payload[ATTR_DISCOVERY_HASH] config = PLATFORM_SCHEMA(discovery_payload) await _async_setup_entity(hass, config, async_add_entities, - discovery_hash) + config_entry, discovery_hash) except Exception: if discovery_hash: clear_discovery_hash(hass, discovery_hash) @@ -175,21 +175,17 @@ async def async_setup_entry(hass, config_entry, async_add_entities): async def _async_setup_entity(hass, config, async_add_entities, - discovery_hash=None): + config_entry=None, discovery_hash=None): """Set up the MQTT climate devices.""" - async_add_entities([ - MqttClimate( - hass, - config, - discovery_hash, - )]) + async_add_entities([MqttClimate(hass, config, config_entry, + discovery_hash,)]) class MqttClimate(MqttAttributes, MqttAvailability, MqttDiscoveryUpdate, MqttEntityDeviceInfo, ClimateDevice): """Representation of an MQTT climate device.""" - def __init__(self, hass, config, discovery_hash): + def __init__(self, hass, config, config_entry, discovery_hash): """Initialize the climate device.""" self._config = config self._unique_id = config.get(CONF_UNIQUE_ID) @@ -216,7 +212,7 @@ class MqttClimate(MqttAttributes, MqttAvailability, MqttDiscoveryUpdate, MqttAvailability.__init__(self, config) MqttDiscoveryUpdate.__init__(self, discovery_hash, self.discovery_update) - MqttEntityDeviceInfo.__init__(self, device_config) + MqttEntityDeviceInfo.__init__(self, device_config, config_entry) async def async_added_to_hass(self): """Handle being added to home assistant.""" @@ -230,6 +226,7 @@ class MqttClimate(MqttAttributes, MqttAvailability, MqttDiscoveryUpdate, self._setup_from_config(config) await self.attributes_discovery_update(config) await self.availability_discovery_update(config) + await self.device_info_discovery_update(config) await self._subscribe_topics() self.async_schedule_update_ha_state() diff --git a/tests/components/mqtt/test_climate.py b/tests/components/mqtt/test_climate.py index ac5e1b07f27..4803f915636 100644 --- a/tests/components/mqtt/test_climate.py +++ b/tests/components/mqtt/test_climate.py @@ -921,6 +921,53 @@ async def test_entity_device_info_with_identifier(hass, mqtt_mock): assert device.sw_version == '0.1-beta' +async def test_entity_device_info_update(hass, mqtt_mock): + """Test device registry update.""" + entry = MockConfigEntry(domain=mqtt.DOMAIN) + entry.add_to_hass(hass) + await async_start(hass, 'homeassistant', {}, entry) + registry = await hass.helpers.device_registry.async_get_registry() + + config = { + 'platform': 'mqtt', + 'name': 'Test 1', + 'state_topic': 'test-topic', + 'command_topic': 'test-command-topic', + 'device': { + 'identifiers': ['helloworld'], + 'connections': [ + ["mac", "02:5b:26:a8:dc:12"], + ], + 'manufacturer': 'Whatever', + 'name': 'Beer', + 'model': 'Glass', + 'sw_version': '0.1-beta', + }, + 'unique_id': 'veryunique' + } + + data = json.dumps(config) + async_fire_mqtt_message(hass, 'homeassistant/climate/bla/config', + data) + await hass.async_block_till_done() + await hass.async_block_till_done() + + device = registry.async_get_device({('mqtt', 'helloworld')}, set()) + assert device is not None + assert device.name == 'Beer' + + config['device']['name'] = 'Milk' + data = json.dumps(config) + async_fire_mqtt_message(hass, 'homeassistant/climate/bla/config', + data) + await hass.async_block_till_done() + await hass.async_block_till_done() + + device = registry.async_get_device({('mqtt', 'helloworld')}, set()) + assert device is not None + assert device.name == 'Milk' + + async def test_entity_id_update(hass, mqtt_mock): """Test MQTT subscriptions are managed when entity_id is updated.""" registry = mock_registry(hass, {}) From d39784906b26d4322c9ed72f073eb0967f8f6470 Mon Sep 17 00:00:00 2001 From: Louis Matthijssen Date: Sat, 26 Jan 2019 03:13:44 +0100 Subject: [PATCH 104/222] Fix HTTP login attempts check triggering too late (#20431) --- homeassistant/components/http/ban.py | 2 +- tests/components/http/test_ban.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/http/ban.py b/homeassistant/components/http/ban.py index d6d7168ce6d..0d748c91c66 100644 --- a/homeassistant/components/http/ban.py +++ b/homeassistant/components/http/ban.py @@ -104,7 +104,7 @@ async def process_wrong_login(request): request.app[KEY_FAILED_LOGIN_ATTEMPTS][remote_addr] += 1 - if (request.app[KEY_FAILED_LOGIN_ATTEMPTS][remote_addr] > + if (request.app[KEY_FAILED_LOGIN_ATTEMPTS][remote_addr] >= request.app[KEY_LOGIN_THRESHOLD]): new_ban = IpBan(remote_addr) request.app[KEY_BANNED_IPS].append(new_ban) diff --git a/tests/components/http/test_ban.py b/tests/components/http/test_ban.py index 6624937da8d..954337bb413 100644 --- a/tests/components/http/test_ban.py +++ b/tests/components/http/test_ban.py @@ -71,7 +71,7 @@ async def test_ip_bans_file_creation(hass, aiohttp_client): raise HTTPUnauthorized app.router.add_get('/', unauth_handler) - setup_bans(hass, app, 1) + setup_bans(hass, app, 2) mock_real_ip(app)("200.201.202.204") with patch('homeassistant.components.http.ban.async_load_ip_bans_config', From 310c073c64a9c15888a637f649c2dc23827d1d99 Mon Sep 17 00:00:00 2001 From: Adam Belebczuk Date: Sat, 26 Jan 2019 05:00:06 -0500 Subject: [PATCH 105/222] WeMo - Fix device discovery issues (#20446) --- homeassistant/components/wemo.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/wemo.py b/homeassistant/components/wemo.py index b80ddb017ce..3ec9b8920c3 100644 --- a/homeassistant/components/wemo.py +++ b/homeassistant/components/wemo.py @@ -20,8 +20,8 @@ REQUIREMENTS = ['pywemo==0.4.34'] DOMAIN = 'wemo' -# Mapping from Wemo device type to Home Assistant component type. -WEMO_DEVICE_TYPE_DISPATCH = { +# Mapping from Wemo model_name to component. +WEMO_MODEL_DISPATCH = { 'Bridge': 'light', 'CoffeeMaker': 'switch', 'Dimmer': 'light', @@ -30,7 +30,8 @@ WEMO_DEVICE_TYPE_DISPATCH = { 'LightSwitch': 'switch', 'Maker': 'switch', 'Motion': 'binary_sensor', - 'Switch': 'switch' + 'Sensor': 'binary_sensor', + 'Socket': 'switch' } SUBSCRIPTION_REGISTRY = None @@ -109,7 +110,7 @@ def setup(hass, config): def discovery_dispatch(service, discovery_info): """Dispatcher for incoming WeMo discovery events.""" # name, model, location, mac - device_type = discovery_info.get('device_type') + model_name = discovery_info.get('model_name') serial = discovery_info.get('serial') # Only register a device once @@ -121,7 +122,7 @@ def setup(hass, config): _LOGGER.debug('Discovered unique WeMo device: %s', serial) KNOWN_DEVICES.append(serial) - component = WEMO_DEVICE_TYPE_DISPATCH.get(device_type, 'switch') + component = WEMO_MODEL_DISPATCH.get(model_name, 'switch') discovery.load_platform(hass, component, DOMAIN, discovery_info, config) @@ -165,7 +166,7 @@ def setup(hass, config): device.host, device.port) discovery_info = { - 'device_type': device.__class__.__name__, + 'model_name': device.model_name, 'serial': device.serialnumber, 'mac_address': device.mac, 'ssdp_description': url, From b3c3721a79f3e3bdf12b031f74298a00685fd227 Mon Sep 17 00:00:00 2001 From: Austin Drummond Date: Sat, 26 Jan 2019 05:01:04 -0500 Subject: [PATCH 106/222] Add alarm type workaround zwave lock Yale YRD240 (#20438) --- homeassistant/components/lock/zwave.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/lock/zwave.py b/homeassistant/components/lock/zwave.py index 77afe688c2e..c907d5101a9 100644 --- a/homeassistant/components/lock/zwave.py +++ b/homeassistant/components/lock/zwave.py @@ -39,8 +39,8 @@ DEVICE_MAPPINGS = { # Kwikset 914TRL ZW500 (0x0090, 0x440): WORKAROUND_DEVICE_STATE, (0x0090, 0x446): WORKAROUND_DEVICE_STATE, - # Yale YRD210 - (0x0129, 0x0209): WORKAROUND_DEVICE_STATE, + # Yale YRD210, Yale YRD240 + (0x0129, 0x0209): WORKAROUND_DEVICE_STATE | WORKAROUND_ALARM_TYPE, (0x0129, 0xAA00): WORKAROUND_DEVICE_STATE, (0x0129, 0x0000): WORKAROUND_DEVICE_STATE, # Yale YRD220 (as reported by adrum in PR #17386) From e593383b4d7fb1c8f83f2f1d522abde0f605f450 Mon Sep 17 00:00:00 2001 From: Anders Melchiorsen Date: Sat, 26 Jan 2019 11:02:16 +0100 Subject: [PATCH 107/222] Error handling for recorder purge (#20424) --- homeassistant/components/recorder/purge.py | 35 +++++++++++----------- tests/components/recorder/test_purge.py | 2 +- 2 files changed, 18 insertions(+), 19 deletions(-) diff --git a/homeassistant/components/recorder/purge.py b/homeassistant/components/recorder/purge.py index 17909194793..3ee733c26ea 100644 --- a/homeassistant/components/recorder/purge.py +++ b/homeassistant/components/recorder/purge.py @@ -12,28 +12,27 @@ _LOGGER = logging.getLogger(__name__) def purge_old_data(instance, purge_days, repack): """Purge events and states older than purge_days ago.""" from .models import States, Events + from sqlalchemy.exc import SQLAlchemyError purge_before = dt_util.utcnow() - timedelta(days=purge_days) _LOGGER.debug("Purging events before %s", purge_before) - with session_scope(session=instance.get_session()) as session: - deleted_rows = session.query(States) \ - .filter((States.last_updated < purge_before)) \ - .delete(synchronize_session=False) - _LOGGER.debug("Deleted %s states", deleted_rows) + try: + with session_scope(session=instance.get_session()) as session: + deleted_rows = session.query(States) \ + .filter((States.last_updated < purge_before)) \ + .delete(synchronize_session=False) + _LOGGER.debug("Deleted %s states", deleted_rows) - deleted_rows = session.query(Events) \ - .filter((Events.time_fired < purge_before)) \ - .delete(synchronize_session=False) - _LOGGER.debug("Deleted %s events", deleted_rows) + deleted_rows = session.query(Events) \ + .filter((Events.time_fired < purge_before)) \ + .delete(synchronize_session=False) + _LOGGER.debug("Deleted %s events", deleted_rows) - # Execute sqlite vacuum command to free up space on disk - _LOGGER.debug("DB engine driver: %s", instance.engine.driver) - if repack and instance.engine.driver == 'pysqlite': - from sqlalchemy import exc - - _LOGGER.debug("Vacuuming SQLite to free space") - try: + # Execute sqlite vacuum command to free up space on disk + if repack and instance.engine.driver == 'pysqlite': + _LOGGER.debug("Vacuuming SQLite to free space") instance.engine.execute("VACUUM") - except exc.OperationalError as err: - _LOGGER.error("Error vacuuming SQLite: %s.", err) + + except SQLAlchemyError as err: + _LOGGER.warning("Error purging history: %s.", err) diff --git a/tests/components/recorder/test_purge.py b/tests/components/recorder/test_purge.py index 7b03c7c3d8a..d5cd692b68b 100644 --- a/tests/components/recorder/test_purge.py +++ b/tests/components/recorder/test_purge.py @@ -170,5 +170,5 @@ class TestRecorderPurge(unittest.TestCase): service_data=service_data) self.hass.block_till_done() self.hass.data[DATA_INSTANCE].block_till_done() - assert mock_logger.debug.mock_calls[4][1][0] == \ + assert mock_logger.debug.mock_calls[3][1][0] == \ "Vacuuming SQLite to free space" From a0b93c2add081e32db9c4cd0ede88f55ca19da8c Mon Sep 17 00:00:00 2001 From: "David F. Mulcahey" Date: Sat, 26 Jan 2019 08:54:49 -0500 Subject: [PATCH 108/222] ZHA component rewrite part 1 (#20456) * rearrange files * add init to module * update imports * update coveragerc * put blank line back... git raw view be damned --- .coveragerc | 7 +- homeassistant/components/zha/__init__.py | 4 +- homeassistant/components/zha/api.py | 2 +- homeassistant/components/zha/binary_sensor.py | 8 +- homeassistant/components/zha/config_flow.py | 4 +- homeassistant/components/zha/const.py | 101 ++---------------- .../zha/{entities => core}/__init__.py | 6 +- homeassistant/components/zha/core/const.py | 95 ++++++++++++++++ .../components/zha/{ => core}/helpers.py | 0 .../zha/{entities => core}/listeners.py | 0 .../zha/{entities => }/device_entity.py | 0 .../components/zha/{entities => }/entity.py | 4 +- homeassistant/components/zha/fan.py | 6 +- homeassistant/components/zha/light.py | 8 +- homeassistant/components/zha/sensor.py | 6 +- homeassistant/components/zha/switch.py | 6 +- 16 files changed, 132 insertions(+), 125 deletions(-) rename homeassistant/components/zha/{entities => core}/__init__.py (50%) create mode 100644 homeassistant/components/zha/core/const.py rename homeassistant/components/zha/{ => core}/helpers.py (100%) rename homeassistant/components/zha/{entities => core}/listeners.py (100%) rename homeassistant/components/zha/{entities => }/device_entity.py (100%) rename homeassistant/components/zha/{entities => }/entity.py (99%) diff --git a/.coveragerc b/.coveragerc index e6a1c752f87..161b62c1896 100644 --- a/.coveragerc +++ b/.coveragerc @@ -448,8 +448,11 @@ omit = homeassistant/components/zha/sensor.py homeassistant/components/zha/switch.py homeassistant/components/zha/api.py - homeassistant/components/zha/entities/* - homeassistant/components/zha/helpers.py + homeassistant/components/zha/entity.py + homeassistant/components/zha/device_entity.py + homeassistant/components/zha/core/helpers.py + homeassistant/components/zha/core/const.py + homeassistant/components/zha/core/listeners.py homeassistant/components/*/zha.py homeassistant/components/zigbee.py diff --git a/homeassistant/components/zha/__init__.py b/homeassistant/components/zha/__init__.py index 335295b2c2c..486274a4163 100644 --- a/homeassistant/components/zha/__init__.py +++ b/homeassistant/components/zha/__init__.py @@ -22,8 +22,8 @@ from . import config_flow # noqa # pylint: disable=unused-import from . import const as zha_const from .event import ZhaEvent, ZhaRelayEvent from . import api -from .helpers import convert_ieee -from .entities import ZhaDeviceEntity +from .core.helpers import convert_ieee +from .device_entity import ZhaDeviceEntity from .const import ( COMPONENTS, CONF_BAUDRATE, CONF_DATABASE, CONF_DEVICE_CONFIG, CONF_RADIO_TYPE, CONF_USB_PATH, DATA_ZHA, DATA_ZHA_BRIDGE_ID, diff --git a/homeassistant/components/zha/api.py b/homeassistant/components/zha/api.py index 308c221bf2f..0312a40967f 100644 --- a/homeassistant/components/zha/api.py +++ b/homeassistant/components/zha/api.py @@ -11,7 +11,7 @@ import voluptuous as vol from homeassistant.components import websocket_api from homeassistant.const import ATTR_ENTITY_ID import homeassistant.helpers.config_validation as cv -from .entities import ZhaDeviceEntity +from .device_entity import ZhaDeviceEntity from .const import ( DOMAIN, ATTR_CLUSTER_ID, ATTR_CLUSTER_TYPE, ATTR_ATTRIBUTE, ATTR_VALUE, ATTR_MANUFACTURER, ATTR_COMMAND, ATTR_COMMAND_TYPE, ATTR_ARGS, IN, OUT, diff --git a/homeassistant/components/zha/binary_sensor.py b/homeassistant/components/zha/binary_sensor.py index fce9376700e..d0f23ff3dd2 100644 --- a/homeassistant/components/zha/binary_sensor.py +++ b/homeassistant/components/zha/binary_sensor.py @@ -10,11 +10,11 @@ from homeassistant.components.binary_sensor import DOMAIN, BinarySensorDevice from homeassistant.const import STATE_ON from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.restore_state import RestoreEntity -from . import helpers -from .const import ( +from .core import helpers +from .core.const import ( DATA_ZHA, DATA_ZHA_DISPATCHERS, REPORT_CONFIG_IMMEDIATE, ZHA_DISCOVERY_NEW) -from .entities import ZhaEntity -from .entities.listeners import ( +from .entity import ZhaEntity +from .core.listeners import ( OnOffListener, LevelListener ) diff --git a/homeassistant/components/zha/config_flow.py b/homeassistant/components/zha/config_flow.py index 1c903ec3056..d995a2179fe 100644 --- a/homeassistant/components/zha/config_flow.py +++ b/homeassistant/components/zha/config_flow.py @@ -6,9 +6,9 @@ import voluptuous as vol from homeassistant import config_entries -from .const import ( +from .core.const import ( CONF_RADIO_TYPE, CONF_USB_PATH, DEFAULT_DATABASE_NAME, DOMAIN, RadioType) -from .helpers import check_zigpy_connection +from .core.helpers import check_zigpy_connection @config_entries.HANDLERS.register(DOMAIN) diff --git a/homeassistant/components/zha/const.py b/homeassistant/components/zha/const.py index 47c3982c5d6..abcd17a0461 100644 --- a/homeassistant/components/zha/const.py +++ b/homeassistant/components/zha/const.py @@ -1,95 +1,8 @@ -"""All constants related to the ZHA component.""" -import enum +""" +Backwards compatible constants bridge. -DOMAIN = 'zha' - -BAUD_RATES = [ - 2400, 4800, 9600, 14400, 19200, 38400, 57600, 115200, 128000, 256000 -] - -DATA_ZHA = 'zha' -DATA_ZHA_CONFIG = 'config' -DATA_ZHA_BRIDGE_ID = 'zha_bridge_id' -DATA_ZHA_RADIO = 'zha_radio' -DATA_ZHA_DISPATCHERS = 'zha_dispatchers' -DATA_ZHA_CORE_COMPONENT = 'zha_core_component' -DATA_ZHA_CORE_EVENTS = 'zha_core_events' -ZHA_DISCOVERY_NEW = 'zha_discovery_new_{}' - -COMPONENTS = [ - 'binary_sensor', - 'fan', - 'light', - 'sensor', - 'switch', -] - -CONF_BAUDRATE = 'baudrate' -CONF_DATABASE = 'database_path' -CONF_DEVICE_CONFIG = 'device_config' -CONF_RADIO_TYPE = 'radio_type' -CONF_USB_PATH = 'usb_path' -DATA_DEVICE_CONFIG = 'zha_device_config' -ENABLE_QUIRKS = 'enable_quirks' - -DEFAULT_RADIO_TYPE = 'ezsp' -DEFAULT_BAUDRATE = 57600 -DEFAULT_DATABASE_NAME = 'zigbee.db' - -ATTR_CLUSTER_ID = 'cluster_id' -ATTR_CLUSTER_TYPE = 'cluster_type' -ATTR_ATTRIBUTE = 'attribute' -ATTR_VALUE = 'value' -ATTR_MANUFACTURER = 'manufacturer' -ATTR_COMMAND = 'command' -ATTR_COMMAND_TYPE = 'command_type' -ATTR_ARGS = 'args' - -IN = 'in' -OUT = 'out' -CLIENT_COMMANDS = 'client_commands' -SERVER_COMMANDS = 'server_commands' -SERVER = 'server' - - -class RadioType(enum.Enum): - """Possible options for radio type.""" - - ezsp = 'ezsp' - xbee = 'xbee' - deconz = 'deconz' - - @classmethod - def list(cls): - """Return list of enum's values.""" - return [e.value for e in RadioType] - - -DISCOVERY_KEY = 'zha_discovery_info' -DEVICE_CLASS = {} -SINGLE_INPUT_CLUSTER_DEVICE_CLASS = {} -SINGLE_OUTPUT_CLUSTER_DEVICE_CLASS = {} -CUSTOM_CLUSTER_MAPPINGS = {} -COMPONENT_CLUSTERS = {} -EVENTABLE_CLUSTERS = [] - -REPORT_CONFIG_MAX_INT = 900 -REPORT_CONFIG_MAX_INT_BATTERY_SAVE = 10800 -REPORT_CONFIG_MIN_INT = 30 -REPORT_CONFIG_MIN_INT_ASAP = 1 -REPORT_CONFIG_MIN_INT_IMMEDIATE = 0 -REPORT_CONFIG_MIN_INT_OP = 5 -REPORT_CONFIG_MIN_INT_BATTERY_SAVE = 3600 -REPORT_CONFIG_RPT_CHANGE = 1 -REPORT_CONFIG_DEFAULT = (REPORT_CONFIG_MIN_INT, REPORT_CONFIG_MAX_INT, - REPORT_CONFIG_RPT_CHANGE) -REPORT_CONFIG_ASAP = (REPORT_CONFIG_MIN_INT_ASAP, REPORT_CONFIG_MAX_INT, - REPORT_CONFIG_RPT_CHANGE) -REPORT_CONFIG_BATTERY_SAVE = (REPORT_CONFIG_MIN_INT_BATTERY_SAVE, - REPORT_CONFIG_MAX_INT, - REPORT_CONFIG_RPT_CHANGE) -REPORT_CONFIG_IMMEDIATE = (REPORT_CONFIG_MIN_INT_IMMEDIATE, - REPORT_CONFIG_MAX_INT, - REPORT_CONFIG_RPT_CHANGE) -REPORT_CONFIG_OP = (REPORT_CONFIG_MIN_INT_OP, REPORT_CONFIG_MAX_INT, - REPORT_CONFIG_RPT_CHANGE) +For more details on this platform, please refer to the documentation +at https://home-assistant.io/components/fan.zha/ +""" +# pylint: disable=W0614,W0401 +from .core.const import * # noqa: F401,F403 diff --git a/homeassistant/components/zha/entities/__init__.py b/homeassistant/components/zha/core/__init__.py similarity index 50% rename from homeassistant/components/zha/entities/__init__.py rename to homeassistant/components/zha/core/__init__.py index c3c3ea163ed..47e6ed2b0ee 100644 --- a/homeassistant/components/zha/entities/__init__.py +++ b/homeassistant/components/zha/core/__init__.py @@ -1,10 +1,6 @@ """ -Entities for Zigbee Home Automation. +Core module for Zigbee Home Automation. For more details about this component, please refer to the documentation at https://home-assistant.io/components/zha/ """ - -# flake8: noqa -from .device_entity import ZhaDeviceEntity -from .entity import ZhaEntity diff --git a/homeassistant/components/zha/core/const.py b/homeassistant/components/zha/core/const.py new file mode 100644 index 00000000000..47c3982c5d6 --- /dev/null +++ b/homeassistant/components/zha/core/const.py @@ -0,0 +1,95 @@ +"""All constants related to the ZHA component.""" +import enum + +DOMAIN = 'zha' + +BAUD_RATES = [ + 2400, 4800, 9600, 14400, 19200, 38400, 57600, 115200, 128000, 256000 +] + +DATA_ZHA = 'zha' +DATA_ZHA_CONFIG = 'config' +DATA_ZHA_BRIDGE_ID = 'zha_bridge_id' +DATA_ZHA_RADIO = 'zha_radio' +DATA_ZHA_DISPATCHERS = 'zha_dispatchers' +DATA_ZHA_CORE_COMPONENT = 'zha_core_component' +DATA_ZHA_CORE_EVENTS = 'zha_core_events' +ZHA_DISCOVERY_NEW = 'zha_discovery_new_{}' + +COMPONENTS = [ + 'binary_sensor', + 'fan', + 'light', + 'sensor', + 'switch', +] + +CONF_BAUDRATE = 'baudrate' +CONF_DATABASE = 'database_path' +CONF_DEVICE_CONFIG = 'device_config' +CONF_RADIO_TYPE = 'radio_type' +CONF_USB_PATH = 'usb_path' +DATA_DEVICE_CONFIG = 'zha_device_config' +ENABLE_QUIRKS = 'enable_quirks' + +DEFAULT_RADIO_TYPE = 'ezsp' +DEFAULT_BAUDRATE = 57600 +DEFAULT_DATABASE_NAME = 'zigbee.db' + +ATTR_CLUSTER_ID = 'cluster_id' +ATTR_CLUSTER_TYPE = 'cluster_type' +ATTR_ATTRIBUTE = 'attribute' +ATTR_VALUE = 'value' +ATTR_MANUFACTURER = 'manufacturer' +ATTR_COMMAND = 'command' +ATTR_COMMAND_TYPE = 'command_type' +ATTR_ARGS = 'args' + +IN = 'in' +OUT = 'out' +CLIENT_COMMANDS = 'client_commands' +SERVER_COMMANDS = 'server_commands' +SERVER = 'server' + + +class RadioType(enum.Enum): + """Possible options for radio type.""" + + ezsp = 'ezsp' + xbee = 'xbee' + deconz = 'deconz' + + @classmethod + def list(cls): + """Return list of enum's values.""" + return [e.value for e in RadioType] + + +DISCOVERY_KEY = 'zha_discovery_info' +DEVICE_CLASS = {} +SINGLE_INPUT_CLUSTER_DEVICE_CLASS = {} +SINGLE_OUTPUT_CLUSTER_DEVICE_CLASS = {} +CUSTOM_CLUSTER_MAPPINGS = {} +COMPONENT_CLUSTERS = {} +EVENTABLE_CLUSTERS = [] + +REPORT_CONFIG_MAX_INT = 900 +REPORT_CONFIG_MAX_INT_BATTERY_SAVE = 10800 +REPORT_CONFIG_MIN_INT = 30 +REPORT_CONFIG_MIN_INT_ASAP = 1 +REPORT_CONFIG_MIN_INT_IMMEDIATE = 0 +REPORT_CONFIG_MIN_INT_OP = 5 +REPORT_CONFIG_MIN_INT_BATTERY_SAVE = 3600 +REPORT_CONFIG_RPT_CHANGE = 1 +REPORT_CONFIG_DEFAULT = (REPORT_CONFIG_MIN_INT, REPORT_CONFIG_MAX_INT, + REPORT_CONFIG_RPT_CHANGE) +REPORT_CONFIG_ASAP = (REPORT_CONFIG_MIN_INT_ASAP, REPORT_CONFIG_MAX_INT, + REPORT_CONFIG_RPT_CHANGE) +REPORT_CONFIG_BATTERY_SAVE = (REPORT_CONFIG_MIN_INT_BATTERY_SAVE, + REPORT_CONFIG_MAX_INT, + REPORT_CONFIG_RPT_CHANGE) +REPORT_CONFIG_IMMEDIATE = (REPORT_CONFIG_MIN_INT_IMMEDIATE, + REPORT_CONFIG_MAX_INT, + REPORT_CONFIG_RPT_CHANGE) +REPORT_CONFIG_OP = (REPORT_CONFIG_MIN_INT_OP, REPORT_CONFIG_MAX_INT, + REPORT_CONFIG_RPT_CHANGE) diff --git a/homeassistant/components/zha/helpers.py b/homeassistant/components/zha/core/helpers.py similarity index 100% rename from homeassistant/components/zha/helpers.py rename to homeassistant/components/zha/core/helpers.py diff --git a/homeassistant/components/zha/entities/listeners.py b/homeassistant/components/zha/core/listeners.py similarity index 100% rename from homeassistant/components/zha/entities/listeners.py rename to homeassistant/components/zha/core/listeners.py diff --git a/homeassistant/components/zha/entities/device_entity.py b/homeassistant/components/zha/device_entity.py similarity index 100% rename from homeassistant/components/zha/entities/device_entity.py rename to homeassistant/components/zha/device_entity.py diff --git a/homeassistant/components/zha/entities/entity.py b/homeassistant/components/zha/entity.py similarity index 99% rename from homeassistant/components/zha/entities/entity.py rename to homeassistant/components/zha/entity.py index 8f8c8e58e05..e112e32d592 100644 --- a/homeassistant/components/zha/entities/entity.py +++ b/homeassistant/components/zha/entity.py @@ -13,11 +13,11 @@ from homeassistant.core import callback from homeassistant.helpers import entity from homeassistant.helpers.device_registry import CONNECTION_ZIGBEE from homeassistant.util import slugify -from ..const import ( +from .core.const import ( DATA_ZHA, DATA_ZHA_BRIDGE_ID, DOMAIN, ATTR_CLUSTER_ID, ATTR_ATTRIBUTE, ATTR_VALUE, ATTR_MANUFACTURER, ATTR_COMMAND, SERVER, ATTR_COMMAND_TYPE, ATTR_ARGS, IN, OUT, CLIENT_COMMANDS, SERVER_COMMANDS) -from ..helpers import bind_configure_reporting +from .core.helpers import bind_configure_reporting _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/zha/fan.py b/homeassistant/components/zha/fan.py index 630ab3f7bb9..f6dbef50923 100644 --- a/homeassistant/components/zha/fan.py +++ b/homeassistant/components/zha/fan.py @@ -10,10 +10,10 @@ from homeassistant.components.fan import ( DOMAIN, SPEED_HIGH, SPEED_LOW, SPEED_MEDIUM, SPEED_OFF, SUPPORT_SET_SPEED, FanEntity) from homeassistant.helpers.dispatcher import async_dispatcher_connect -from . import helpers -from .const import ( +from .core import helpers +from .core.const import ( DATA_ZHA, DATA_ZHA_DISPATCHERS, REPORT_CONFIG_OP, ZHA_DISCOVERY_NEW) -from .entities import ZhaEntity +from .entity import ZhaEntity DEPENDENCIES = ['zha'] diff --git a/homeassistant/components/zha/light.py b/homeassistant/components/zha/light.py index 766608b35b1..49a09112b31 100644 --- a/homeassistant/components/zha/light.py +++ b/homeassistant/components/zha/light.py @@ -9,12 +9,12 @@ import logging from homeassistant.components import light from homeassistant.helpers.dispatcher import async_dispatcher_connect import homeassistant.util.color as color_util -from . import helpers -from .const import ( +from .core import helpers +from .core.const import ( DATA_ZHA, DATA_ZHA_DISPATCHERS, REPORT_CONFIG_ASAP, REPORT_CONFIG_DEFAULT, REPORT_CONFIG_IMMEDIATE, ZHA_DISCOVERY_NEW) -from .entities import ZhaEntity -from .entities.listeners import ( +from .entity import ZhaEntity +from .core.listeners import ( OnOffListener, LevelListener ) diff --git a/homeassistant/components/zha/sensor.py b/homeassistant/components/zha/sensor.py index dabbcb79815..ae45fad0826 100644 --- a/homeassistant/components/zha/sensor.py +++ b/homeassistant/components/zha/sensor.py @@ -10,11 +10,11 @@ from homeassistant.components.sensor import DOMAIN from homeassistant.const import TEMP_CELSIUS from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.util.temperature import convert as convert_temperature -from . import helpers -from .const import ( +from .core import helpers +from .core.const import ( DATA_ZHA, DATA_ZHA_DISPATCHERS, REPORT_CONFIG_MAX_INT, REPORT_CONFIG_MIN_INT, REPORT_CONFIG_RPT_CHANGE, ZHA_DISCOVERY_NEW) -from .entities import ZhaEntity +from .entity import ZhaEntity _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/zha/switch.py b/homeassistant/components/zha/switch.py index 793da4e1e3a..09c20acd088 100644 --- a/homeassistant/components/zha/switch.py +++ b/homeassistant/components/zha/switch.py @@ -8,10 +8,10 @@ import logging from homeassistant.components.switch import DOMAIN, SwitchDevice from homeassistant.helpers.dispatcher import async_dispatcher_connect -from . import helpers -from .const import ( +from .core import helpers +from .core.const import ( DATA_ZHA, DATA_ZHA_DISPATCHERS, REPORT_CONFIG_IMMEDIATE, ZHA_DISCOVERY_NEW) -from .entities import ZhaEntity +from .entity import ZhaEntity _LOGGER = logging.getLogger(__name__) From a85e018bc44874f4b22f5ca7dc151b32a4175428 Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Sat, 26 Jan 2019 15:54:35 +0100 Subject: [PATCH 109/222] Upgrade astral to 1.8 (#20459) --- 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 06577be4763..b9443c287a1 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -1,5 +1,5 @@ aiohttp==3.5.4 -astral==1.7.1 +astral==1.8 async_timeout==3.0.1 attrs==18.2.0 bcrypt==3.1.5 diff --git a/requirements_all.txt b/requirements_all.txt index 52ff7a3563a..83a72aebb33 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1,6 +1,6 @@ # Home Assistant core aiohttp==3.5.4 -astral==1.7.1 +astral==1.8 async_timeout==3.0.1 attrs==18.2.0 bcrypt==3.1.5 diff --git a/setup.py b/setup.py index d8c2c57b3d3..0ceaa7d55b3 100755 --- a/setup.py +++ b/setup.py @@ -33,7 +33,7 @@ PACKAGES = find_packages(exclude=['tests', 'tests.*']) REQUIRES = [ 'aiohttp==3.5.4', - 'astral==1.7.1', + 'astral==1.8', 'async_timeout==3.0.1', 'attrs==18.2.0', 'bcrypt==3.1.5', From ed6e3495152ae20a41b967c637f93e23d4b0a34e Mon Sep 17 00:00:00 2001 From: Jonas Pedersen Date: Sat, 26 Jan 2019 15:55:25 +0100 Subject: [PATCH 110/222] Correct minor comments from PR#20138. (#20454) --- homeassistant/components/danfoss_air/__init__.py | 5 +---- .../components/danfoss_air/binary_sensor.py | 4 ++-- homeassistant/components/danfoss_air/sensor.py | 12 ++++++------ 3 files changed, 9 insertions(+), 12 deletions(-) diff --git a/homeassistant/components/danfoss_air/__init__.py b/homeassistant/components/danfoss_air/__init__.py index 17a3952adec..80c36b6f0c6 100644 --- a/homeassistant/components/danfoss_air/__init__.py +++ b/homeassistant/components/danfoss_air/__init__.py @@ -55,10 +55,7 @@ class DanfossAir: def get_value(self, item): """Get value for sensor.""" - if item in self._data: - return self._data[item] - - return None + return self._data.get(item) @Throttle(MIN_TIME_BETWEEN_UPDATES) def update(self): diff --git a/homeassistant/components/danfoss_air/binary_sensor.py b/homeassistant/components/danfoss_air/binary_sensor.py index 905ead24a0f..bf8fe952993 100644 --- a/homeassistant/components/danfoss_air/binary_sensor.py +++ b/homeassistant/components/danfoss_air/binary_sensor.py @@ -9,7 +9,7 @@ from homeassistant.components.danfoss_air import DOMAIN \ as DANFOSS_AIR_DOMAIN -def setup_platform(hass, config, add_devices, discovery_info=None): +def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the available Danfoss Air sensors etc.""" from pydanfossair.commands import ReadCommand data = hass.data[DANFOSS_AIR_DOMAIN] @@ -21,7 +21,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): for sensor in sensors: dev.append(DanfossAirBinarySensor(data, sensor[0], sensor[1])) - add_devices(dev, True) + add_entities(dev, True) class DanfossAirBinarySensor(BinarySensorDevice): diff --git a/homeassistant/components/danfoss_air/sensor.py b/homeassistant/components/danfoss_air/sensor.py index dfb9686edea..2f3807c4999 100644 --- a/homeassistant/components/danfoss_air/sensor.py +++ b/homeassistant/components/danfoss_air/sensor.py @@ -10,7 +10,7 @@ from homeassistant.const import TEMP_CELSIUS from homeassistant.helpers.entity import Entity -def setup_platform(hass, config, add_devices, discovery_info=None): +def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the available Danfoss Air sensors etc.""" from pydanfossair.commands import ReadCommand @@ -36,19 +36,19 @@ def setup_platform(hass, config, add_devices, discovery_info=None): for sensor in sensors: dev.append(DanfossAir(data, sensor[0], sensor[1], sensor[2])) - add_devices(dev, True) + add_entities(dev, True) class DanfossAir(Entity): """Representation of a Sensor.""" - def __init__(self, data, name, sensorUnit, sensorType): + def __init__(self, data, name, sensor_unit, sensor_type): """Initialize the sensor.""" self._data = data self._name = name self._state = None - self._type = sensorType - self._unit = sensorUnit + self._type = sensor_type + self._unit = sensor_unit @property def name(self): @@ -68,7 +68,7 @@ class DanfossAir(Entity): def update(self): """Update the new state of the sensor. - This is done through the DanfossAir object tthat does the actually + This is done through the DanfossAir object that does the actual communication with the Air CCM. """ self._data.update() From 1d5ffe9ad5d4a0a918cce33d338419962ac21545 Mon Sep 17 00:00:00 2001 From: Diogo Gomes Date: Sat, 26 Jan 2019 15:33:11 +0000 Subject: [PATCH 111/222] Utility meter (#19718) * initial commit * test service calls * lint * float -> Decimal * extra tests * lint * lint * lint * lint * fix self reset * clean * add services * improve service example description * add optional paused initialization * fix * travis fix * fix YEARLY * add tests for previous bug * address comments and suggestions from @ottowinter * lint * remove debug * add discoverability capabilities * no need for _hass * Update homeassistant/components/sensor/utility_meter.py Co-Authored-By: dgomes * Update homeassistant/components/sensor/utility_meter.py Co-Authored-By: dgomes * correct comment * improve error handling * address @MartinHjelmare comments * address @MartinHjelmare comments * one patch is enought * follow @ballob suggestion in https://github.com/home-assistant/architecture/issues/131 * fix tests * review fixes * major refactor * lint * lint * address comments by @MartinHjelmare * rename variable --- .../components/utility_meter/__init__.py | 176 +++++++++++++ .../components/utility_meter/const.py | 30 +++ .../components/utility_meter/sensor.py | 243 ++++++++++++++++++ .../components/utility_meter/services.yaml | 25 ++ tests/components/utility_meter/__init__.py | 1 + tests/components/utility_meter/test_init.py | 102 ++++++++ tests/components/utility_meter/test_sensor.py | 136 ++++++++++ 7 files changed, 713 insertions(+) create mode 100644 homeassistant/components/utility_meter/__init__.py create mode 100644 homeassistant/components/utility_meter/const.py create mode 100644 homeassistant/components/utility_meter/sensor.py create mode 100644 homeassistant/components/utility_meter/services.yaml create mode 100644 tests/components/utility_meter/__init__.py create mode 100644 tests/components/utility_meter/test_init.py create mode 100644 tests/components/utility_meter/test_sensor.py diff --git a/homeassistant/components/utility_meter/__init__.py b/homeassistant/components/utility_meter/__init__.py new file mode 100644 index 00000000000..8a8e669ba88 --- /dev/null +++ b/homeassistant/components/utility_meter/__init__.py @@ -0,0 +1,176 @@ +""" +Component to track utility consumption over given periods of time. + +For more details about this component, please refer to the documentation +at https://www.home-assistant.io/components/utility_meter/ +""" + +import logging + +import voluptuous as vol + +from homeassistant.const import (ATTR_ENTITY_ID, CONF_NAME) +import homeassistant.helpers.config_validation as cv +from homeassistant.helpers import discovery +from homeassistant.helpers.dispatcher import async_dispatcher_send +from homeassistant.helpers.entity_component import EntityComponent +from homeassistant.helpers.restore_state import RestoreEntity +from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN +from .const import ( + DOMAIN, SIGNAL_RESET_METER, METER_TYPES, CONF_SOURCE_SENSOR, + CONF_METER_TYPE, CONF_METER_OFFSET, CONF_TARIFF_ENTITY, CONF_TARIFF, + CONF_TARIFFS, CONF_METER, DATA_UTILITY, SERVICE_RESET, + SERVICE_SELECT_TARIFF, SERVICE_SELECT_NEXT_TARIFF, + ATTR_TARIFF) + +_LOGGER = logging.getLogger(__name__) + +TARIFF_ICON = "mdi:clock-outline" + +ATTR_TARIFFS = 'tariffs' + +SERVICE_METER_SCHEMA = vol.Schema({ + vol.Required(ATTR_ENTITY_ID): cv.entity_ids, +}) + +SERVICE_SELECT_TARIFF_SCHEMA = SERVICE_METER_SCHEMA.extend({ + vol.Required(ATTR_TARIFF): cv.string +}) + +METER_CONFIG_SCHEMA = vol.Schema({ + vol.Required(CONF_SOURCE_SENSOR): cv.entity_id, + vol.Optional(CONF_NAME): cv.string, + vol.Optional(CONF_METER_TYPE): vol.In(METER_TYPES), + vol.Optional(CONF_METER_OFFSET, default=0): cv.positive_int, + vol.Optional(CONF_TARIFFS, default=[]): vol.All( + cv.ensure_list, [cv.string]), +}) + +CONFIG_SCHEMA = vol.Schema({ + DOMAIN: vol.Schema({ + cv.slug: METER_CONFIG_SCHEMA, + }), +}, extra=vol.ALLOW_EXTRA) + + +async def async_setup(hass, config): + """Set up an Utility Meter.""" + component = EntityComponent(_LOGGER, DOMAIN, hass) + hass.data[DATA_UTILITY] = {} + + for meter, conf in config.get(DOMAIN).items(): + _LOGGER.debug("Setup %s.%s", DOMAIN, meter) + + hass.data[DATA_UTILITY][meter] = conf + + if not conf[CONF_TARIFFS]: + # only one entity is required + hass.async_create_task(discovery.async_load_platform( + hass, SENSOR_DOMAIN, DOMAIN, + [{CONF_METER: meter, CONF_NAME: meter}], config)) + else: + # create tariff selection + await component.async_add_entities([ + TariffSelect(meter, list(conf[CONF_TARIFFS])) + ]) + hass.data[DATA_UTILITY][meter][CONF_TARIFF_ENTITY] =\ + "{}.{}".format(DOMAIN, meter) + + # add one meter for each tariff + tariff_confs = [] + for tariff in conf[CONF_TARIFFS]: + tariff_confs.append({ + CONF_METER: meter, + CONF_NAME: "{} {}".format(meter, tariff), + CONF_TARIFF: tariff, + }) + hass.async_create_task(discovery.async_load_platform( + hass, SENSOR_DOMAIN, DOMAIN, tariff_confs, config)) + + component.async_register_entity_service( + SERVICE_RESET, SERVICE_METER_SCHEMA, + 'async_reset_meters' + ) + + component.async_register_entity_service( + SERVICE_SELECT_TARIFF, SERVICE_SELECT_TARIFF_SCHEMA, + 'async_select_tariff' + ) + + component.async_register_entity_service( + SERVICE_SELECT_NEXT_TARIFF, SERVICE_METER_SCHEMA, + 'async_next_tariff' + ) + + return True + + +class TariffSelect(RestoreEntity): + """Representation of a Tariff selector.""" + + def __init__(self, name, tariffs): + """Initialize a tariff selector.""" + self._name = name + self._current_tariff = None + self._tariffs = tariffs + self._icon = TARIFF_ICON + + async def async_added_to_hass(self): + """Run when entity about to be added.""" + await super().async_added_to_hass() + if self._current_tariff is not None: + return + + state = await self.async_get_last_state() + if not state or state.state not in self._tariffs: + self._current_tariff = self._tariffs[0] + else: + self._current_tariff = state.state + + @property + def should_poll(self): + """If entity should be polled.""" + return False + + @property + def name(self): + """Return the name of the select input.""" + return self._name + + @property + def icon(self): + """Return the icon to be used for this entity.""" + return self._icon + + @property + def state(self): + """Return the state of the component.""" + return self._current_tariff + + @property + def state_attributes(self): + """Return the state attributes.""" + return { + ATTR_TARIFFS: self._tariffs, + } + + async def async_reset_meters(self): + """Reset all sensors of this meter.""" + async_dispatcher_send(self.hass, SIGNAL_RESET_METER, + self.entity_id) + + async def async_select_tariff(self, tariff): + """Select new option.""" + if tariff not in self._tariffs: + _LOGGER.warning('Invalid tariff: %s (possible tariffs: %s)', + tariff, ', '.join(self._tariffs)) + return + self._current_tariff = tariff + await self.async_update_ha_state() + + async def async_next_tariff(self): + """Offset current index.""" + current_index = self._tariffs.index(self._current_tariff) + new_index = (current_index + 1) % len(self._tariffs) + self._current_tariff = self._tariffs[new_index] + await self.async_update_ha_state() diff --git a/homeassistant/components/utility_meter/const.py b/homeassistant/components/utility_meter/const.py new file mode 100644 index 00000000000..4d2df0372b5 --- /dev/null +++ b/homeassistant/components/utility_meter/const.py @@ -0,0 +1,30 @@ +"""Constants for the utility meter component.""" +DOMAIN = 'utility_meter' + +HOURLY = 'hourly' +DAILY = 'daily' +WEEKLY = 'weekly' +MONTHLY = 'monthly' +YEARLY = 'yearly' + +METER_TYPES = [HOURLY, DAILY, WEEKLY, MONTHLY, YEARLY] + +DATA_UTILITY = 'utility_meter_data' + +CONF_METER = 'meter' +CONF_SOURCE_SENSOR = 'source' +CONF_METER_TYPE = 'cycle' +CONF_METER_OFFSET = 'offset' +CONF_PAUSED = 'paused' +CONF_TARIFFS = 'tariffs' +CONF_TARIFF = 'tariff' +CONF_TARIFF_ENTITY = 'tariff_entity' + +ATTR_TARIFF = 'tariff' + +SIGNAL_START_PAUSE_METER = 'utility_meter_start_pause' +SIGNAL_RESET_METER = 'utility_meter_reset' + +SERVICE_RESET = 'reset' +SERVICE_SELECT_TARIFF = 'select_tariff' +SERVICE_SELECT_NEXT_TARIFF = 'next_tariff' diff --git a/homeassistant/components/utility_meter/sensor.py b/homeassistant/components/utility_meter/sensor.py new file mode 100644 index 00000000000..cd86f9c0bd0 --- /dev/null +++ b/homeassistant/components/utility_meter/sensor.py @@ -0,0 +1,243 @@ +""" +Utility meter from sensors providing raw data. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/sensor.utility_meter/ +""" +import logging + +from decimal import Decimal, DecimalException + +import homeassistant.util.dt as dt_util +from homeassistant.const import ( + CONF_NAME, ATTR_UNIT_OF_MEASUREMENT, + EVENT_HOMEASSISTANT_START, STATE_UNKNOWN, STATE_UNAVAILABLE) +from homeassistant.core import callback +from homeassistant.helpers.event import ( + async_track_state_change, async_track_time_change) +from homeassistant.helpers.dispatcher import ( + async_dispatcher_connect) +from homeassistant.helpers.restore_state import RestoreEntity +from .const import ( + DATA_UTILITY, SIGNAL_RESET_METER, + HOURLY, DAILY, WEEKLY, MONTHLY, YEARLY, + CONF_SOURCE_SENSOR, CONF_METER_TYPE, CONF_METER_OFFSET, + CONF_TARIFF, CONF_TARIFF_ENTITY, CONF_METER) + +_LOGGER = logging.getLogger(__name__) + +ATTR_SOURCE_ID = 'source' +ATTR_STATUS = 'status' +ATTR_PERIOD = 'meter_period' +ATTR_LAST_PERIOD = 'last_period' +ATTR_LAST_RESET = 'last_reset' +ATTR_TARIFF = 'tariff' + +ICON = 'mdi:counter' + +PRECISION = 3 +PAUSED = 'paused' +COLLECTING = 'collecting' + + +async def async_setup_platform(hass, config, async_add_entities, + discovery_info=None): + """Set up the utility meter sensor.""" + if discovery_info is None: + _LOGGER.error("This platform is only available through discovery") + return + + meters = [] + for conf in discovery_info: + meter = conf[CONF_METER] + conf_meter_source = hass.data[DATA_UTILITY][meter][CONF_SOURCE_SENSOR] + conf_meter_type = hass.data[DATA_UTILITY][meter].get(CONF_METER_TYPE) + conf_meter_offset = hass.data[DATA_UTILITY][meter][CONF_METER_OFFSET] + conf_meter_tariff_entity = hass.data[DATA_UTILITY][meter].get( + CONF_TARIFF_ENTITY) + + meters.append(UtilityMeterSensor(conf_meter_source, + conf.get(CONF_NAME), + conf_meter_type, + conf_meter_offset, + conf.get(CONF_TARIFF), + conf_meter_tariff_entity)) + + async_add_entities(meters) + + +class UtilityMeterSensor(RestoreEntity): + """Representation of an utility meter sensor.""" + + def __init__(self, source_entity, name, meter_type, meter_offset=0, + tariff=None, tariff_entity=None): + """Initialize the Utility Meter sensor.""" + self._sensor_source_id = source_entity + self._state = 0 + self._last_period = 0 + self._last_reset = dt_util.now() + self._collecting = None + if name: + self._name = name + else: + self._name = '{} meter'.format(source_entity) + self._unit_of_measurement = None + self._period = meter_type + self._period_offset = meter_offset + self._tariff = tariff + self._tariff_entity = tariff_entity + + @callback + def async_reading(self, entity, old_state, new_state): + """Handle the sensor state changes.""" + if any([old_state is None, + old_state.state in [STATE_UNKNOWN, STATE_UNAVAILABLE], + new_state.state in [STATE_UNKNOWN, STATE_UNAVAILABLE]]): + return + + if self._unit_of_measurement is None and\ + new_state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) is not None: + self._unit_of_measurement = new_state.attributes.get( + ATTR_UNIT_OF_MEASUREMENT) + + try: + diff = Decimal(new_state.state) - Decimal(old_state.state) + + if diff < 0: + # Source sensor just rolled over for unknow reasons, + return + self._state += diff + + except ValueError as err: + _LOGGER.warning("While processing state changes: %s", err) + except DecimalException as err: + _LOGGER.warning("Invalid state (%s > %s): %s", + old_state.state, new_state.state, err) + self.async_schedule_update_ha_state() + + @callback + def async_tariff_change(self, entity, old_state, new_state): + """Handle tariff changes.""" + if self._tariff == new_state.state: + self._collecting = async_track_state_change( + self.hass, self._sensor_source_id, self.async_reading) + else: + self._collecting() + self._collecting = None + + _LOGGER.debug("%s - %s - source <%s>", self._name, + COLLECTING if self._collecting is not None + else PAUSED, self._sensor_source_id) + + self.async_schedule_update_ha_state() + + async def _async_reset_meter(self, event): + """Determine cycle - Helper function for larger then daily cycles.""" + now = dt_util.now() + if self._period == WEEKLY and now.weekday() != self._period_offset: + return + if self._period == MONTHLY and\ + now.day != (1 + self._period_offset): + return + if self._period == YEARLY and\ + (now.month != (1 + self._period_offset) or now.day != 1): + return + await self.async_reset_meter(self._tariff_entity) + + async def async_reset_meter(self, entity_id): + """Reset meter.""" + if self._tariff_entity != entity_id: + return + _LOGGER.debug("Reset utility meter <%s>", self.entity_id) + self._last_reset = dt_util.now() + self._last_period = str(self._state) + self._state = 0 + await self.async_update_ha_state() + + async def async_added_to_hass(self): + """Handle entity which will be added.""" + await super().async_added_to_hass() + + if self._period == HOURLY: + async_track_time_change(self.hass, self._async_reset_meter, + minute=self._period_offset, second=0) + elif self._period == DAILY: + async_track_time_change(self.hass, self._async_reset_meter, + hour=self._period_offset, minute=0, + second=0) + elif self._period in [WEEKLY, MONTHLY, YEARLY]: + async_track_time_change(self.hass, self._async_reset_meter, + hour=0, minute=0, second=0) + + async_dispatcher_connect( + self.hass, SIGNAL_RESET_METER, self.async_reset_meter) + + state = await self.async_get_last_state() + if state: + self._state = Decimal(state.state) + self._unit_of_measurement = state.attributes.get( + ATTR_UNIT_OF_MEASUREMENT) + self._last_period = state.attributes.get(ATTR_LAST_PERIOD) + self._last_reset = state.attributes.get(ATTR_LAST_RESET) + await self.async_update_ha_state() + if state.attributes.get(ATTR_STATUS) == PAUSED: + # Fake cancelation function to init the meter paused + self._collecting = lambda: None + + @callback + def async_source_tracking(event): + """Wait for source to be ready, then start meter.""" + if self._tariff_entity is not None: + _LOGGER.debug("track %s", self._tariff_entity) + async_track_state_change(self.hass, self._tariff_entity, + self.async_tariff_change) + + tariff_entity_state = self.hass.states.get(self._tariff_entity) + if self._tariff != tariff_entity_state.state: + return + + self._collecting = async_track_state_change( + self.hass, self._sensor_source_id, self.async_reading) + + self.hass.bus.async_listen_once( + EVENT_HOMEASSISTANT_START, async_source_tracking) + + @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 the value is expressed in.""" + return self._unit_of_measurement + + @property + def should_poll(self): + """No polling needed.""" + return False + + @property + def device_state_attributes(self): + """Return the state attributes of the sensor.""" + state_attr = { + ATTR_SOURCE_ID: self._sensor_source_id, + ATTR_STATUS: PAUSED if self._collecting is None else COLLECTING, + ATTR_LAST_PERIOD: self._last_period, + ATTR_LAST_RESET: self._last_reset, + } + if self._period is not None: + state_attr[ATTR_PERIOD] = self._period + if self._tariff is not None: + state_attr[ATTR_TARIFF] = self._tariff + return state_attr + + @property + def icon(self): + """Return the icon to use in the frontend, if any.""" + return ICON diff --git a/homeassistant/components/utility_meter/services.yaml b/homeassistant/components/utility_meter/services.yaml new file mode 100644 index 00000000000..7c09117d48f --- /dev/null +++ b/homeassistant/components/utility_meter/services.yaml @@ -0,0 +1,25 @@ +# Describes the format for available switch services + +reset: + description: Resets the counter of an utility meter. + fields: + entity_id: + description: Name(s) of the utility meter to reset + example: 'utility_meter.energy' + +next_tariff: + description: Changes the tariff to the next one. + fields: + entity_id: + description: Name(s) of entities to reset + example: 'utility_meter.energy' + +select_tariff: + description: selects the current tariff of an utility meter. + fields: + entity_id: + description: Name of the entity to set the tariff for + example: 'utility_meter.energy' + tariff: + description: Name of the tariff to switch to + example: 'offpeak' diff --git a/tests/components/utility_meter/__init__.py b/tests/components/utility_meter/__init__.py new file mode 100644 index 00000000000..bcb65403918 --- /dev/null +++ b/tests/components/utility_meter/__init__.py @@ -0,0 +1 @@ +"""Tests for Utility Meter component.""" diff --git a/tests/components/utility_meter/test_init.py b/tests/components/utility_meter/test_init.py new file mode 100644 index 00000000000..51a458506fb --- /dev/null +++ b/tests/components/utility_meter/test_init.py @@ -0,0 +1,102 @@ +"""The tests for the utility_meter component.""" +import logging + +from datetime import timedelta +from unittest.mock import patch + +from homeassistant.const import ( + EVENT_HOMEASSISTANT_START, ATTR_ENTITY_ID) +from homeassistant.components.utility_meter.const import ( + SERVICE_RESET, SERVICE_SELECT_TARIFF, SERVICE_SELECT_NEXT_TARIFF, + ATTR_TARIFF) +from homeassistant.setup import async_setup_component +import homeassistant.util.dt as dt_util +from homeassistant.components.utility_meter.const import DOMAIN +from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN + +_LOGGER = logging.getLogger(__name__) + + +async def test_services(hass): + """Test energy sensor reset service.""" + config = { + 'utility_meter': { + 'energy_bill': { + 'source': 'sensor.energy', + 'cycle': 'hourly', + 'tariffs': ['peak', 'offpeak'], + } + } + } + + assert await async_setup_component(hass, DOMAIN, config) + assert await async_setup_component(hass, SENSOR_DOMAIN, config) + await hass.async_block_till_done() + + hass.bus.async_fire(EVENT_HOMEASSISTANT_START) + entity_id = config[DOMAIN]['energy_bill']['source'] + hass.states.async_set(entity_id, 1, {"unit_of_measurement": "kWh"}) + await hass.async_block_till_done() + + now = dt_util.utcnow() + timedelta(seconds=10) + with patch('homeassistant.util.dt.utcnow', + return_value=now): + hass.states.async_set(entity_id, 3, {"unit_of_measurement": "kWh"}, + force_update=True) + await hass.async_block_till_done() + + state = hass.states.get('sensor.energy_bill_peak') + assert state.state == '2' + + state = hass.states.get('sensor.energy_bill_offpeak') + assert state.state == '0' + + # Next tariff + data = {ATTR_ENTITY_ID: 'utility_meter.energy_bill'} + await hass.services.async_call(DOMAIN, + SERVICE_SELECT_NEXT_TARIFF, data) + await hass.async_block_till_done() + + now += timedelta(seconds=10) + with patch('homeassistant.util.dt.utcnow', + return_value=now): + hass.states.async_set(entity_id, 4, {"unit_of_measurement": "kWh"}, + force_update=True) + await hass.async_block_till_done() + + state = hass.states.get('sensor.energy_bill_peak') + assert state.state == '2' + + state = hass.states.get('sensor.energy_bill_offpeak') + assert state.state == '1' + + # Change tariff + data = {ATTR_ENTITY_ID: 'utility_meter.energy_bill', + ATTR_TARIFF: 'peak'} + await hass.services.async_call(DOMAIN, + SERVICE_SELECT_TARIFF, data) + await hass.async_block_till_done() + + now += timedelta(seconds=10) + with patch('homeassistant.util.dt.utcnow', + return_value=now): + hass.states.async_set(entity_id, 5, {"unit_of_measurement": "kWh"}, + force_update=True) + await hass.async_block_till_done() + + state = hass.states.get('sensor.energy_bill_peak') + assert state.state == '3' + + state = hass.states.get('sensor.energy_bill_offpeak') + assert state.state == '1' + + # Reset meters + data = {ATTR_ENTITY_ID: 'utility_meter.energy_bill'} + await hass.services.async_call(DOMAIN, SERVICE_RESET, data) + await hass.async_block_till_done() + + state = hass.states.get('sensor.energy_bill_peak') + assert state.state == '0' + + state = hass.states.get('sensor.energy_bill_offpeak') + assert state.state == '0' diff --git a/tests/components/utility_meter/test_sensor.py b/tests/components/utility_meter/test_sensor.py new file mode 100644 index 00000000000..23fc8872570 --- /dev/null +++ b/tests/components/utility_meter/test_sensor.py @@ -0,0 +1,136 @@ +"""The tests for the utility_meter sensor platform.""" +import logging + +from datetime import timedelta +from unittest.mock import patch +from contextlib import contextmanager + +from tests.common import async_fire_time_changed +from homeassistant.const import EVENT_HOMEASSISTANT_START +from homeassistant.setup import async_setup_component +import homeassistant.util.dt as dt_util +from homeassistant.components.utility_meter.const import DOMAIN +from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN + +_LOGGER = logging.getLogger(__name__) + + +@contextmanager +def alter_time(retval): + """Manage multiple time mocks.""" + patch1 = patch("homeassistant.util.dt.utcnow", return_value=retval) + patch2 = patch("homeassistant.util.dt.now", return_value=retval) + + with patch1, patch2: + yield + + +async def test_state(hass): + """Test utility sensor state.""" + config = { + 'utility_meter': { + 'energy_bill': { + 'source': 'sensor.energy', + } + } + } + + assert await async_setup_component(hass, DOMAIN, config) + assert await async_setup_component(hass, SENSOR_DOMAIN, config) + await hass.async_block_till_done() + + hass.bus.async_fire(EVENT_HOMEASSISTANT_START) + entity_id = config[DOMAIN]['energy_bill']['source'] + hass.states.async_set(entity_id, 2, {"unit_of_measurement": "kWh"}) + await hass.async_block_till_done() + + now = dt_util.utcnow() + timedelta(seconds=10) + with patch('homeassistant.util.dt.utcnow', + return_value=now): + hass.states.async_set(entity_id, 3, {"unit_of_measurement": "kWh"}, + force_update=True) + await hass.async_block_till_done() + + state = hass.states.get('sensor.energy_bill') + assert state is not None + + assert state.state == '1' + + +async def _test_self_reset(hass, cycle, start_time, expect_reset=True): + """Test energy sensor self reset.""" + config = { + 'utility_meter': { + 'energy_bill': { + 'source': 'sensor.energy', + 'cycle': cycle + } + } + } + + assert await async_setup_component(hass, DOMAIN, config) + assert await async_setup_component(hass, SENSOR_DOMAIN, config) + await hass.async_block_till_done() + + hass.bus.async_fire(EVENT_HOMEASSISTANT_START) + entity_id = config[DOMAIN]['energy_bill']['source'] + + now = dt_util.parse_datetime(start_time) + with alter_time(now): + async_fire_time_changed(hass, now) + hass.states.async_set(entity_id, 1, {"unit_of_measurement": "kWh"}) + await hass.async_block_till_done() + + now += timedelta(seconds=30) + with alter_time(now): + async_fire_time_changed(hass, now) + hass.states.async_set(entity_id, 3, {"unit_of_measurement": "kWh"}, + force_update=True) + await hass.async_block_till_done() + + now += timedelta(seconds=30) + with alter_time(now): + async_fire_time_changed(hass, now) + await hass.async_block_till_done() + hass.states.async_set(entity_id, 6, {"unit_of_measurement": "kWh"}, + force_update=True) + await hass.async_block_till_done() + + state = hass.states.get('sensor.energy_bill') + if expect_reset: + assert state.attributes.get('last_period') == '2' + assert state.state == '3' + else: + assert state.attributes.get('last_period') == 0 + assert state.state == '5' + + +async def test_self_reset_hourly(hass): + """Test hourly reset of meter.""" + await _test_self_reset(hass, 'hourly', "2017-12-31T23:59:00.000000+00:00") + + +async def test_self_reset_daily(hass): + """Test daily reset of meter.""" + await _test_self_reset(hass, 'daily', "2017-12-31T23:59:00.000000+00:00") + + +async def test_self_reset_weekly(hass): + """Test weekly reset of meter.""" + await _test_self_reset(hass, 'weekly', "2017-12-31T23:59:00.000000+00:00") + + +async def test_self_reset_monthly(hass): + """Test monthly reset of meter.""" + await _test_self_reset(hass, 'monthly', "2017-12-31T23:59:00.000000+00:00") + + +async def test_self_reset_yearly(hass): + """Test yearly reset of meter.""" + await _test_self_reset(hass, 'yearly', "2017-12-31T23:59:00.000000+00:00") + + +async def test_self_no_reset_yearly(hass): + """Test yearly reset of meter does not occur after 1st January.""" + await _test_self_reset(hass, 'yearly', "2018-01-01T23:59:00.000000+00:00", + expect_reset=False) From f3285f96bb910b93bd2a4e14c94c9701f8494d59 Mon Sep 17 00:00:00 2001 From: Jef D Date: Sat, 26 Jan 2019 19:02:46 +0100 Subject: [PATCH 112/222] Add Co2signal sensor (#19204) * Initial commit for the co2signal sensor * Clean code * Run script gen_requirements_all.py * remove unintended character * Remove redundancy * Remove unused imports * Code style * Code style fixes * Code style * Fix comments PR Comments by @fabaff * Remove redundant comments and variables * Follow the latest home-assistant guidelines * Bump CO2Signal version * Round API result * Improve default latitude/longitude handling * Improve friendly name * Improve config handling * Make lines shorter * Style * Convert default to variable None does not pass cv.string * Message if not inclusive * Shorten line * Update requirements * Update co2signal.py Group imports; remove empty lines; refactor use of location_type; remove logging messages; remove unused functions; add global variables * Update co2signal.py Import platform schema from sensor * Small fix * Update co2signal.py Remove last mentions of location_type * Review changes Add attribution Formatting * Missing whitespace * Update co2signal.py Fix pylint * Update co2signal.py Change blank lines * Update co2signal.py Initialise _data --- homeassistant/components/sensor/co2signal.py | 113 +++++++++++++++++++ requirements_all.txt | 3 + 2 files changed, 116 insertions(+) create mode 100644 homeassistant/components/sensor/co2signal.py diff --git a/homeassistant/components/sensor/co2signal.py b/homeassistant/components/sensor/co2signal.py new file mode 100644 index 00000000000..ad46f3b494f --- /dev/null +++ b/homeassistant/components/sensor/co2signal.py @@ -0,0 +1,113 @@ +""" +Support for the CO2signal platform. + +For more details about this platform, please refer to the documentation +""" +import logging + +import voluptuous as vol + +import homeassistant.helpers.config_validation as cv +from homeassistant.const import ( + ATTR_ATTRIBUTION, CONF_TOKEN, CONF_LATITUDE, CONF_LONGITUDE) +from homeassistant.components.sensor import PLATFORM_SCHEMA +from homeassistant.helpers.entity import Entity + +CONF_COUNTRY_CODE = "country_code" + +REQUIREMENTS = ['co2signal==0.4.1'] + +_LOGGER = logging.getLogger(__name__) + +ATTRIBUTION = 'Data provided by CO2signal' + +MSG_LOCATION = "Please use either coordinates or the country code. " \ + "For the coordinates, " \ + "you need to use both latitude and longitude." +CO2_INTENSITY_UNIT = "CO2eq/kWh" +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Required(CONF_TOKEN): cv.string, + vol.Inclusive(CONF_LATITUDE, 'coords', msg=MSG_LOCATION): cv.latitude, + vol.Inclusive(CONF_LONGITUDE, 'coords', msg=MSG_LOCATION): cv.longitude, + vol.Optional(CONF_COUNTRY_CODE): cv.string, +}) + + +def setup_platform(hass, config, add_entities, discovery_info=None): + """Set up the CO2signal sensor.""" + token = config[CONF_TOKEN] + lat = config.get(CONF_LATITUDE, hass.config.latitude) + lon = config.get(CONF_LONGITUDE, hass.config.longitude) + country_code = config.get(CONF_COUNTRY_CODE) + + _LOGGER.debug("Setting up the sensor using the %s", country_code) + + devs = [] + + devs.append(CO2Sensor(token, + country_code, + lat, + lon)) + add_entities(devs, True) + + +class CO2Sensor(Entity): + """Implementation of the CO2Signal sensor.""" + + def __init__(self, token, country_code, lat, lon): + """Initialize the sensor.""" + self._token = token + self._country_code = country_code + self._latitude = lat + self._longitude = lon + self._data = None + + if country_code is not None: + device_name = country_code + else: + device_name = '{lat}/{lon}'\ + .format(lat=round(self._latitude, 2), + lon=round(self._longitude, 2)) + + self._friendly_name = 'CO2 intensity - {}'.format(device_name) + + @property + def name(self): + """Return the name of the sensor.""" + return self._friendly_name + + @property + def icon(self): + """Icon to use in the frontend, if any.""" + return 'mdi:periodic-table-co2' + + @property + def state(self): + """Return the state of the device.""" + return self._data + + @property + def unit_of_measurement(self): + """Return the unit of measurement of this entity, if any.""" + return CO2_INTENSITY_UNIT + + @property + def device_state_attributes(self): + """Return the state attributes of the last update.""" + return {ATTR_ATTRIBUTION: ATTRIBUTION} + + def update(self): + """Get the latest data and updates the states.""" + import CO2Signal + + _LOGGER.debug("Update data for %s", self._friendly_name) + + if self._country_code is not None: + self._data = CO2Signal.get_latest_carbon_intensity( + self._token, country_code=self._country_code) + else: + self._data = CO2Signal.get_latest_carbon_intensity( + self._token, + latitude=self._latitude, longitude=self._longitude) + + self._data = round(self._data, 2) diff --git a/requirements_all.txt b/requirements_all.txt index 83a72aebb33..7db8d380850 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -255,6 +255,9 @@ caldav==0.5.0 # homeassistant.components.notify.ciscospark ciscosparkapi==0.4.2 +# homeassistant.components.sensor.co2signal +co2signal==0.4.1 + # homeassistant.components.coinbase coinbase==2.1.0 From 05d41bc0eea1ae13dccd876bfafb1361196f5af8 Mon Sep 17 00:00:00 2001 From: "David F. Mulcahey" Date: Sat, 26 Jan 2019 13:28:13 -0500 Subject: [PATCH 113/222] introduce gateway (#20460) --- .coveragerc | 1 + homeassistant/components/zha/__init__.py | 342 +------------------ homeassistant/components/zha/core/gateway.py | 342 +++++++++++++++++++ 3 files changed, 350 insertions(+), 335 deletions(-) create mode 100644 homeassistant/components/zha/core/gateway.py diff --git a/.coveragerc b/.coveragerc index 161b62c1896..45dbeb9cb60 100644 --- a/.coveragerc +++ b/.coveragerc @@ -453,6 +453,7 @@ omit = homeassistant/components/zha/core/helpers.py homeassistant/components/zha/core/const.py homeassistant/components/zha/core/listeners.py + homeassistant/components/zha/core/gateway.py homeassistant/components/*/zha.py homeassistant/components/zigbee.py diff --git a/homeassistant/components/zha/__init__.py b/homeassistant/components/zha/__init__.py index 486274a4163..4f9b5b04362 100644 --- a/homeassistant/components/zha/__init__.py +++ b/homeassistant/components/zha/__init__.py @@ -4,7 +4,6 @@ Support for Zigbee Home Automation devices. For more details about this component, please refer to the documentation at https://home-assistant.io/components/zha/ """ -import collections import logging import os import types @@ -14,26 +13,18 @@ import voluptuous as vol from homeassistant import config_entries, const as ha_const import homeassistant.helpers.config_validation as cv from homeassistant.helpers.device_registry import CONNECTION_ZIGBEE -from homeassistant.helpers.dispatcher import async_dispatcher_send -from homeassistant.helpers.entity_component import EntityComponent # Loading the config flow file will register the flow from . import config_flow # noqa # pylint: disable=unused-import -from . import const as zha_const -from .event import ZhaEvent, ZhaRelayEvent from . import api -from .core.helpers import convert_ieee -from .device_entity import ZhaDeviceEntity +from .core.gateway import ZHAGateway from .const import ( COMPONENTS, CONF_BAUDRATE, CONF_DATABASE, CONF_DEVICE_CONFIG, CONF_RADIO_TYPE, CONF_USB_PATH, DATA_ZHA, DATA_ZHA_BRIDGE_ID, DATA_ZHA_CONFIG, DATA_ZHA_CORE_COMPONENT, DATA_ZHA_DISPATCHERS, DATA_ZHA_RADIO, DEFAULT_BAUDRATE, DEFAULT_DATABASE_NAME, - DEFAULT_RADIO_TYPE, DOMAIN, ZHA_DISCOVERY_NEW, RadioType, - EVENTABLE_CLUSTERS, DATA_ZHA_CORE_EVENTS, ENABLE_QUIRKS, - DEVICE_CLASS, SINGLE_INPUT_CLUSTER_DEVICE_CLASS, - SINGLE_OUTPUT_CLUSTER_DEVICE_CLASS, CUSTOM_CLUSTER_MAPPINGS, - COMPONENT_CLUSTERS) + DEFAULT_RADIO_TYPE, DOMAIN, RadioType, DATA_ZHA_CORE_EVENTS, + ENABLE_QUIRKS) REQUIREMENTS = [ 'bellows==0.7.0', @@ -96,7 +87,6 @@ async def async_setup_entry(hass, config_entry): Will automatically load components to support devices found on the network. """ - establish_device_mappings() hass.data[DATA_ZHA] = hass.data.get(DATA_ZHA, {}) hass.data[DATA_ZHA][DATA_ZHA_DISPATCHERS] = [] @@ -148,13 +138,13 @@ async def async_setup_entry(hass, config_entry): ) application_controller = ControllerApplication(radio, database) - listener = ApplicationListener(hass, config) - application_controller.add_listener(listener) + zha_gateway = ZHAGateway(hass, config) + application_controller.add_listener(zha_gateway) await application_controller.startup(auto_form=True) for device in application_controller.devices.values(): hass.async_create_task( - listener.async_device_initialized(device, False)) + zha_gateway.async_device_initialized(device, False)) device_registry = await \ hass.helpers.device_registry.async_get_registry() @@ -175,7 +165,7 @@ async def async_setup_entry(hass, config_entry): config_entry, component) ) - api.async_load_api(hass, application_controller, listener) + api.async_load_api(hass, application_controller, zha_gateway) def zha_shutdown(event): """Close radio.""" @@ -211,321 +201,3 @@ async def async_unload_entry(hass, config_entry): del hass.data[DATA_ZHA] return True - - -def establish_device_mappings(): - """Establish mappings between ZCL objects and HA ZHA objects. - - These cannot be module level, as importing bellows must be done in a - in a function. - """ - from zigpy import zcl, quirks - from zigpy.profiles import PROFILES, zha, zll - from .sensor import RelativeHumiditySensor - - if zha.PROFILE_ID not in DEVICE_CLASS: - DEVICE_CLASS[zha.PROFILE_ID] = {} - if zll.PROFILE_ID not in DEVICE_CLASS: - DEVICE_CLASS[zll.PROFILE_ID] = {} - - EVENTABLE_CLUSTERS.append(zcl.clusters.general.AnalogInput.cluster_id) - EVENTABLE_CLUSTERS.append(zcl.clusters.general.LevelControl.cluster_id) - EVENTABLE_CLUSTERS.append(zcl.clusters.general.MultistateInput.cluster_id) - EVENTABLE_CLUSTERS.append(zcl.clusters.general.OnOff.cluster_id) - - DEVICE_CLASS[zha.PROFILE_ID].update({ - 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.LEVEL_CONTROLLABLE_OUTPUT: 'light', - 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].update({ - zll.DeviceType.ON_OFF_LIGHT: 'light', - zll.DeviceType.ON_OFF_PLUGIN_UNIT: 'switch', - zll.DeviceType.DIMMABLE_LIGHT: 'light', - zll.DeviceType.DIMMABLE_PLUGIN_UNIT: 'light', - 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_INPUT_CLUSTER_DEVICE_CLASS.update({ - zcl.clusters.general.OnOff: 'switch', - zcl.clusters.measurement.RelativeHumidity: 'sensor', - zcl.clusters.measurement.TemperatureMeasurement: 'sensor', - zcl.clusters.measurement.PressureMeasurement: 'sensor', - zcl.clusters.measurement.IlluminanceMeasurement: 'sensor', - zcl.clusters.smartenergy.Metering: 'sensor', - zcl.clusters.homeautomation.ElectricalMeasurement: 'sensor', - zcl.clusters.general.PowerConfiguration: 'sensor', - zcl.clusters.security.IasZone: 'binary_sensor', - zcl.clusters.measurement.OccupancySensing: 'binary_sensor', - zcl.clusters.hvac.Fan: 'fan', - }) - SINGLE_OUTPUT_CLUSTER_DEVICE_CLASS.update({ - zcl.clusters.general.OnOff: 'binary_sensor', - }) - - # A map of device/cluster to component/sub-component - CUSTOM_CLUSTER_MAPPINGS.update({ - (quirks.smartthings.SmartthingsTemperatureHumiditySensor, 64581): - ('sensor', RelativeHumiditySensor) - }) - - # A map of hass components to all Zigbee clusters it could use - for profile_id, classes in DEVICE_CLASS.items(): - profile = PROFILES[profile_id] - for device_type, component in classes.items(): - if component not in COMPONENT_CLUSTERS: - COMPONENT_CLUSTERS[component] = (set(), set()) - clusters = profile.CLUSTERS[device_type] - COMPONENT_CLUSTERS[component][0].update(clusters[0]) - COMPONENT_CLUSTERS[component][1].update(clusters[1]) - - -class ApplicationListener: - """All handlers for events that happen on the ZigBee application.""" - - def __init__(self, hass, config): - """Initialize the listener.""" - self._hass = hass - self._config = config - self._component = EntityComponent(_LOGGER, DOMAIN, hass) - self._device_registry = collections.defaultdict(list) - self._events = {} - - for component in COMPONENTS: - hass.data[DATA_ZHA][component] = ( - hass.data[DATA_ZHA].get(component, {}) - ) - hass.data[DATA_ZHA][DATA_ZHA_CORE_COMPONENT] = self._component - hass.data[DATA_ZHA][DATA_ZHA_CORE_EVENTS] = self._events - - def device_joined(self, device): - """Handle device joined. - - At this point, no information about the device is known other than its - address - """ - # Wait for device_initialized, instead - pass - - def raw_device_initialized(self, device): - """Handle a device initialization without quirks loaded.""" - # Wait for device_initialized, instead - pass - - def device_initialized(self, device): - """Handle device joined and basic information discovered.""" - self._hass.async_create_task( - self.async_device_initialized(device, True)) - - def device_left(self, device): - """Handle device leaving the network.""" - pass - - def device_removed(self, device): - """Handle device being removed from the network.""" - for device_entity in self._device_registry[device.ieee]: - self._hass.async_create_task(device_entity.async_remove()) - if device.ieee in self._events: - self._events.pop(device.ieee) - - def get_device_entity(self, ieee_str): - """Return ZHADeviceEntity for given ieee.""" - ieee = convert_ieee(ieee_str) - if ieee in self._device_registry: - entities = self._device_registry[ieee] - entity = next( - ent for ent in entities if isinstance(ent, ZhaDeviceEntity)) - return entity - return None - - def get_entities_for_ieee(self, ieee_str): - """Return list of entities for given ieee.""" - ieee = convert_ieee(ieee_str) - if ieee in self._device_registry: - return self._device_registry[ieee] - return [] - - @property - def device_registry(self) -> str: - """Return devices.""" - return self._device_registry - - async def async_device_initialized(self, device, join): - """Handle device joined and basic information discovered (async).""" - import zigpy.profiles - - device_manufacturer = device_model = None - - for endpoint_id, endpoint in device.endpoints.items(): - if endpoint_id == 0: # ZDO - continue - - if endpoint.manufacturer is not None: - device_manufacturer = endpoint.manufacturer - if endpoint.model is not None: - device_model = endpoint.model - - component = None - profile_clusters = ([], []) - device_key = "{}-{}".format(device.ieee, endpoint_id) - node_config = {} - if CONF_DEVICE_CONFIG in self._config: - node_config = self._config[CONF_DEVICE_CONFIG].get( - device_key, {} - ) - - if endpoint.profile_id in zigpy.profiles.PROFILES: - profile = zigpy.profiles.PROFILES[endpoint.profile_id] - if zha_const.DEVICE_CLASS.get(endpoint.profile_id, - {}).get(endpoint.device_type, - None): - profile_clusters = profile.CLUSTERS[endpoint.device_type] - profile_info = zha_const.DEVICE_CLASS[endpoint.profile_id] - component = profile_info[endpoint.device_type] - - if ha_const.CONF_TYPE in node_config: - component = node_config[ha_const.CONF_TYPE] - profile_clusters = zha_const.COMPONENT_CLUSTERS[component] - - if component: - in_clusters = [endpoint.in_clusters[c] - for c in profile_clusters[0] - if c in endpoint.in_clusters] - out_clusters = [endpoint.out_clusters[c] - for c in profile_clusters[1] - if c in endpoint.out_clusters] - discovery_info = { - 'application_listener': self, - 'endpoint': endpoint, - 'in_clusters': {c.cluster_id: c for c in in_clusters}, - 'out_clusters': {c.cluster_id: c for c in out_clusters}, - 'manufacturer': endpoint.manufacturer, - 'model': endpoint.model, - 'new_join': join, - 'unique_id': device_key, - } - - if join: - async_dispatcher_send( - self._hass, - ZHA_DISCOVERY_NEW.format(component), - discovery_info - ) - else: - self._hass.data[DATA_ZHA][component][device_key] = ( - discovery_info - ) - - 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', - join, - ) - - 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', - join, - ) - - endpoint_entity = ZhaDeviceEntity( - device, - device_manufacturer, - device_model, - self, - ) - await self._component.async_add_entities([endpoint_entity]) - - 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, - is_new_join): - """Try to set up an entity from a "bare" cluster.""" - if cluster.cluster_id in EVENTABLE_CLUSTERS: - if cluster.endpoint.device.ieee not in self._events: - self._events.update({cluster.endpoint.device.ieee: []}) - from zigpy.zcl.clusters.general import OnOff, LevelControl - if discovery_attr == 'out_clusters' and \ - (cluster.cluster_id == OnOff.cluster_id or - cluster.cluster_id == LevelControl.cluster_id): - self._events[cluster.endpoint.device.ieee].append( - ZhaRelayEvent(self._hass, cluster) - ) - else: - self._events[cluster.endpoint.device.ieee].append(ZhaEvent( - self._hass, - cluster - )) - - if cluster.cluster_id in profile_clusters: - return - - component = sub_component = None - for cluster_type, candidate_component in device_classes.items(): - if isinstance(cluster, cluster_type): - component = candidate_component - break - - for signature, comp in zha_const.CUSTOM_CLUSTER_MAPPINGS.items(): - if (isinstance(endpoint.device, signature[0]) and - cluster.cluster_id == signature[1]): - component = comp[0] - sub_component = comp[1] - break - - if component is None: - return - - cluster_key = "{}-{}".format(device_key, cluster.cluster_id) - discovery_info = { - 'application_listener': self, - 'endpoint': endpoint, - 'in_clusters': {}, - 'out_clusters': {}, - 'manufacturer': endpoint.manufacturer, - 'model': endpoint.model, - 'new_join': is_new_join, - 'unique_id': cluster_key, - 'entity_suffix': '_{}'.format(cluster.cluster_id), - } - discovery_info[discovery_attr] = {cluster.cluster_id: cluster} - if sub_component: - discovery_info.update({'sub_component': sub_component}) - - if is_new_join: - async_dispatcher_send( - self._hass, - ZHA_DISCOVERY_NEW.format(component), - discovery_info - ) - else: - self._hass.data[DATA_ZHA][component][cluster_key] = discovery_info diff --git a/homeassistant/components/zha/core/gateway.py b/homeassistant/components/zha/core/gateway.py new file mode 100644 index 00000000000..479b2f79b26 --- /dev/null +++ b/homeassistant/components/zha/core/gateway.py @@ -0,0 +1,342 @@ +""" +Virtual gateway for Zigbee Home Automation. + +For more details about this component, please refer to the documentation at +https://home-assistant.io/components/zha/ +""" + +import collections +import logging +from homeassistant import const as ha_const +from homeassistant.helpers.dispatcher import async_dispatcher_send +from homeassistant.helpers.entity_component import EntityComponent +from . import const as zha_const +from .const import ( + COMPONENTS, CONF_DEVICE_CONFIG, DATA_ZHA, DATA_ZHA_CORE_COMPONENT, DOMAIN, + ZHA_DISCOVERY_NEW, EVENTABLE_CLUSTERS, DATA_ZHA_CORE_EVENTS, DEVICE_CLASS, + SINGLE_INPUT_CLUSTER_DEVICE_CLASS, SINGLE_OUTPUT_CLUSTER_DEVICE_CLASS, + CUSTOM_CLUSTER_MAPPINGS, COMPONENT_CLUSTERS) +from ..device_entity import ZhaDeviceEntity +from ..event import ZhaEvent, ZhaRelayEvent +from .helpers import convert_ieee + +_LOGGER = logging.getLogger(__name__) + + +class ZHAGateway: + """Gateway that handles events that happen on the ZHA Zigbee network.""" + + def __init__(self, hass, config): + """Initialize the gateway.""" + self._hass = hass + self._config = config + self._component = EntityComponent(_LOGGER, DOMAIN, hass) + self._device_registry = collections.defaultdict(list) + self._events = {} + establish_device_mappings() + + for component in COMPONENTS: + hass.data[DATA_ZHA][component] = ( + hass.data[DATA_ZHA].get(component, {}) + ) + hass.data[DATA_ZHA][DATA_ZHA_CORE_COMPONENT] = self._component + hass.data[DATA_ZHA][DATA_ZHA_CORE_EVENTS] = self._events + + def device_joined(self, device): + """Handle device joined. + + At this point, no information about the device is known other than its + address + """ + # Wait for device_initialized, instead + pass + + def raw_device_initialized(self, device): + """Handle a device initialization without quirks loaded.""" + # Wait for device_initialized, instead + pass + + def device_initialized(self, device): + """Handle device joined and basic information discovered.""" + self._hass.async_create_task( + self.async_device_initialized(device, True)) + + def device_left(self, device): + """Handle device leaving the network.""" + pass + + def device_removed(self, device): + """Handle device being removed from the network.""" + for device_entity in self._device_registry[device.ieee]: + self._hass.async_create_task(device_entity.async_remove()) + if device.ieee in self._events: + self._events.pop(device.ieee) + + def get_device_entity(self, ieee_str): + """Return ZHADeviceEntity for given ieee.""" + ieee = convert_ieee(ieee_str) + if ieee in self._device_registry: + entities = self._device_registry[ieee] + entity = next( + ent for ent in entities if isinstance(ent, ZhaDeviceEntity)) + return entity + return None + + def get_entities_for_ieee(self, ieee_str): + """Return list of entities for given ieee.""" + ieee = convert_ieee(ieee_str) + if ieee in self._device_registry: + return self._device_registry[ieee] + return [] + + @property + def device_registry(self) -> str: + """Return devices.""" + return self._device_registry + + async def async_device_initialized(self, device, join): + """Handle device joined and basic information discovered (async).""" + import zigpy.profiles + + device_manufacturer = device_model = None + + for endpoint_id, endpoint in device.endpoints.items(): + if endpoint_id == 0: # ZDO + continue + + if endpoint.manufacturer is not None: + device_manufacturer = endpoint.manufacturer + if endpoint.model is not None: + device_model = endpoint.model + + component = None + profile_clusters = ([], []) + device_key = "{}-{}".format(device.ieee, endpoint_id) + node_config = {} + if CONF_DEVICE_CONFIG in self._config: + node_config = self._config[CONF_DEVICE_CONFIG].get( + device_key, {} + ) + + if endpoint.profile_id in zigpy.profiles.PROFILES: + profile = zigpy.profiles.PROFILES[endpoint.profile_id] + if zha_const.DEVICE_CLASS.get(endpoint.profile_id, + {}).get(endpoint.device_type, + None): + profile_clusters = profile.CLUSTERS[endpoint.device_type] + profile_info = zha_const.DEVICE_CLASS[endpoint.profile_id] + component = profile_info[endpoint.device_type] + + if ha_const.CONF_TYPE in node_config: + component = node_config[ha_const.CONF_TYPE] + profile_clusters = zha_const.COMPONENT_CLUSTERS[component] + + if component: + in_clusters = [endpoint.in_clusters[c] + for c in profile_clusters[0] + if c in endpoint.in_clusters] + out_clusters = [endpoint.out_clusters[c] + for c in profile_clusters[1] + if c in endpoint.out_clusters] + discovery_info = { + 'application_listener': self, + 'endpoint': endpoint, + 'in_clusters': {c.cluster_id: c for c in in_clusters}, + 'out_clusters': {c.cluster_id: c for c in out_clusters}, + 'manufacturer': endpoint.manufacturer, + 'model': endpoint.model, + 'new_join': join, + 'unique_id': device_key, + } + + if join: + async_dispatcher_send( + self._hass, + ZHA_DISCOVERY_NEW.format(component), + discovery_info + ) + else: + self._hass.data[DATA_ZHA][component][device_key] = ( + discovery_info + ) + + 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', + join, + ) + + 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', + join, + ) + + endpoint_entity = ZhaDeviceEntity( + device, + device_manufacturer, + device_model, + self, + ) + await self._component.async_add_entities([endpoint_entity]) + + 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, + is_new_join): + """Try to set up an entity from a "bare" cluster.""" + if cluster.cluster_id in EVENTABLE_CLUSTERS: + if cluster.endpoint.device.ieee not in self._events: + self._events.update({cluster.endpoint.device.ieee: []}) + from zigpy.zcl.clusters.general import OnOff, LevelControl + if discovery_attr == 'out_clusters' and \ + (cluster.cluster_id == OnOff.cluster_id or + cluster.cluster_id == LevelControl.cluster_id): + self._events[cluster.endpoint.device.ieee].append( + ZhaRelayEvent(self._hass, cluster) + ) + else: + self._events[cluster.endpoint.device.ieee].append(ZhaEvent( + self._hass, + cluster + )) + + if cluster.cluster_id in profile_clusters: + return + + component = sub_component = None + for cluster_type, candidate_component in device_classes.items(): + if isinstance(cluster, cluster_type): + component = candidate_component + break + + for signature, comp in zha_const.CUSTOM_CLUSTER_MAPPINGS.items(): + if (isinstance(endpoint.device, signature[0]) and + cluster.cluster_id == signature[1]): + component = comp[0] + sub_component = comp[1] + break + + if component is None: + return + + cluster_key = "{}-{}".format(device_key, cluster.cluster_id) + discovery_info = { + 'application_listener': self, + 'endpoint': endpoint, + 'in_clusters': {}, + 'out_clusters': {}, + 'manufacturer': endpoint.manufacturer, + 'model': endpoint.model, + 'new_join': is_new_join, + 'unique_id': cluster_key, + 'entity_suffix': '_{}'.format(cluster.cluster_id), + } + discovery_info[discovery_attr] = {cluster.cluster_id: cluster} + if sub_component: + discovery_info.update({'sub_component': sub_component}) + + if is_new_join: + async_dispatcher_send( + self._hass, + ZHA_DISCOVERY_NEW.format(component), + discovery_info + ) + else: + self._hass.data[DATA_ZHA][component][cluster_key] = discovery_info + + +def establish_device_mappings(): + """Establish mappings between ZCL objects and HA ZHA objects. + + These cannot be module level, as importing bellows must be done in a + in a function. + """ + from zigpy import zcl, quirks + from zigpy.profiles import PROFILES, zha, zll + from ..sensor import RelativeHumiditySensor + + if zha.PROFILE_ID not in DEVICE_CLASS: + DEVICE_CLASS[zha.PROFILE_ID] = {} + if zll.PROFILE_ID not in DEVICE_CLASS: + DEVICE_CLASS[zll.PROFILE_ID] = {} + + EVENTABLE_CLUSTERS.append(zcl.clusters.general.AnalogInput.cluster_id) + EVENTABLE_CLUSTERS.append(zcl.clusters.general.LevelControl.cluster_id) + EVENTABLE_CLUSTERS.append(zcl.clusters.general.MultistateInput.cluster_id) + EVENTABLE_CLUSTERS.append(zcl.clusters.general.OnOff.cluster_id) + + DEVICE_CLASS[zha.PROFILE_ID].update({ + 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.LEVEL_CONTROLLABLE_OUTPUT: 'light', + 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].update({ + zll.DeviceType.ON_OFF_LIGHT: 'light', + zll.DeviceType.ON_OFF_PLUGIN_UNIT: 'switch', + zll.DeviceType.DIMMABLE_LIGHT: 'light', + zll.DeviceType.DIMMABLE_PLUGIN_UNIT: 'light', + 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_INPUT_CLUSTER_DEVICE_CLASS.update({ + zcl.clusters.general.OnOff: 'switch', + zcl.clusters.measurement.RelativeHumidity: 'sensor', + zcl.clusters.measurement.TemperatureMeasurement: 'sensor', + zcl.clusters.measurement.PressureMeasurement: 'sensor', + zcl.clusters.measurement.IlluminanceMeasurement: 'sensor', + zcl.clusters.smartenergy.Metering: 'sensor', + zcl.clusters.homeautomation.ElectricalMeasurement: 'sensor', + zcl.clusters.general.PowerConfiguration: 'sensor', + zcl.clusters.security.IasZone: 'binary_sensor', + zcl.clusters.measurement.OccupancySensing: 'binary_sensor', + zcl.clusters.hvac.Fan: 'fan', + }) + SINGLE_OUTPUT_CLUSTER_DEVICE_CLASS.update({ + zcl.clusters.general.OnOff: 'binary_sensor', + }) + + # A map of device/cluster to component/sub-component + CUSTOM_CLUSTER_MAPPINGS.update({ + (quirks.smartthings.SmartthingsTemperatureHumiditySensor, 64581): + ('sensor', RelativeHumiditySensor) + }) + + # A map of hass components to all Zigbee clusters it could use + for profile_id, classes in DEVICE_CLASS.items(): + profile = PROFILES[profile_id] + for device_type, component in classes.items(): + if component not in COMPONENT_CLUSTERS: + COMPONENT_CLUSTERS[component] = (set(), set()) + clusters = profile.CLUSTERS[device_type] + COMPONENT_CLUSTERS[component][0].update(clusters[0]) + COMPONENT_CLUSTERS[component][1].update(clusters[1]) From 1d16bb2cd4f0a5b181334ab90b11a0da143e045e Mon Sep 17 00:00:00 2001 From: emontnemery Date: Sat, 26 Jan 2019 19:46:41 +0100 Subject: [PATCH 114/222] Update device registry of MQTT fan (#20445) --- homeassistant/components/mqtt/fan.py | 14 ++++---- tests/components/mqtt/test_fan.py | 49 +++++++++++++++++++++++++++- 2 files changed, 54 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/mqtt/fan.py b/homeassistant/components/mqtt/fan.py index 7e359c2bdc5..47bfa3620e9 100644 --- a/homeassistant/components/mqtt/fan.py +++ b/homeassistant/components/mqtt/fan.py @@ -97,7 +97,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): try: discovery_hash = discovery_payload[ATTR_DISCOVERY_HASH] config = PLATFORM_SCHEMA(discovery_payload) - await _async_setup_entity(config, async_add_entities, + await _async_setup_entity(config, async_add_entities, config_entry, discovery_hash) except Exception: if discovery_hash: @@ -109,13 +109,10 @@ async def async_setup_entry(hass, config_entry, async_add_entities): async_discover) -async def _async_setup_entity(config, async_add_entities, +async def _async_setup_entity(config, async_add_entities, config_entry=None, discovery_hash=None): """Set up the MQTT fan.""" - async_add_entities([MqttFan( - config, - discovery_hash, - )]) + async_add_entities([MqttFan(config, config_entry, discovery_hash)]) # pylint: disable=too-many-ancestors @@ -123,7 +120,7 @@ class MqttFan(MqttAttributes, MqttAvailability, MqttDiscoveryUpdate, MqttEntityDeviceInfo, FanEntity): """A MQTT fan component.""" - def __init__(self, config, discovery_hash): + def __init__(self, config, config_entry, discovery_hash): """Initialize the MQTT fan.""" self._unique_id = config.get(CONF_UNIQUE_ID) self._state = False @@ -148,7 +145,7 @@ class MqttFan(MqttAttributes, MqttAvailability, MqttDiscoveryUpdate, MqttAvailability.__init__(self, config) MqttDiscoveryUpdate.__init__(self, discovery_hash, self.discovery_update) - MqttEntityDeviceInfo.__init__(self, device_config) + MqttEntityDeviceInfo.__init__(self, device_config, config_entry) async def async_added_to_hass(self): """Subscribe to MQTT events.""" @@ -161,6 +158,7 @@ class MqttFan(MqttAttributes, MqttAvailability, MqttDiscoveryUpdate, self._setup_from_config(config) await self.attributes_discovery_update(config) await self.availability_discovery_update(config) + await self.device_info_discovery_update(config) await self._subscribe_topics() self.async_schedule_update_ha_state() diff --git a/tests/components/mqtt/test_fan.py b/tests/components/mqtt/test_fan.py index ebea6afd4ae..8c250443f6b 100644 --- a/tests/components/mqtt/test_fan.py +++ b/tests/components/mqtt/test_fan.py @@ -322,7 +322,7 @@ async def test_unique_id(hass): async def test_entity_device_info_with_identifier(hass, mqtt_mock): """Test MQTT fan device registry integration.""" - entry = MockConfigEntry(domain='mqtt') + entry = MockConfigEntry(domain=mqtt.DOMAIN) entry.add_to_hass(hass) await async_start(hass, 'homeassistant', {}, entry) registry = await hass.helpers.device_registry.async_get_registry() @@ -359,6 +359,53 @@ async def test_entity_device_info_with_identifier(hass, mqtt_mock): assert device.sw_version == '0.1-beta' +async def test_entity_device_info_update(hass, mqtt_mock): + """Test device registry update.""" + entry = MockConfigEntry(domain=mqtt.DOMAIN) + entry.add_to_hass(hass) + await async_start(hass, 'homeassistant', {}, entry) + registry = await hass.helpers.device_registry.async_get_registry() + + config = { + 'platform': 'mqtt', + 'name': 'Test 1', + 'state_topic': 'test-topic', + 'command_topic': 'test-command-topic', + 'device': { + 'identifiers': ['helloworld'], + 'connections': [ + ["mac", "02:5b:26:a8:dc:12"], + ], + 'manufacturer': 'Whatever', + 'name': 'Beer', + 'model': 'Glass', + 'sw_version': '0.1-beta', + }, + 'unique_id': 'veryunique' + } + + data = json.dumps(config) + async_fire_mqtt_message(hass, 'homeassistant/fan/bla/config', + data) + await hass.async_block_till_done() + await hass.async_block_till_done() + + device = registry.async_get_device({('mqtt', 'helloworld')}, set()) + assert device is not None + assert device.name == 'Beer' + + config['device']['name'] = 'Milk' + data = json.dumps(config) + async_fire_mqtt_message(hass, 'homeassistant/fan/bla/config', + data) + await hass.async_block_till_done() + await hass.async_block_till_done() + + device = registry.async_get_device({('mqtt', 'helloworld')}, set()) + assert device is not None + assert device.name == 'Milk' + + async def test_entity_id_update(hass, mqtt_mock): """Test MQTT subscriptions are managed when entity_id is updated.""" registry = mock_registry(hass, {}) From 85ccd71d39950ec5e28c7ffef7d6b92b2a062efe Mon Sep 17 00:00:00 2001 From: emontnemery Date: Sat, 26 Jan 2019 19:48:18 +0100 Subject: [PATCH 115/222] Update device registry of MQTT sensor (#20440) --- homeassistant/components/mqtt/sensor.py | 11 +++--- tests/components/mqtt/test_sensor.py | 47 +++++++++++++++++++++++++ 2 files changed, 53 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/mqtt/sensor.py b/homeassistant/components/mqtt/sensor.py index 05b54c9eab6..475aef84c3d 100644 --- a/homeassistant/components/mqtt/sensor.py +++ b/homeassistant/components/mqtt/sensor.py @@ -69,7 +69,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): try: discovery_hash = discovery_payload[ATTR_DISCOVERY_HASH] config = PLATFORM_SCHEMA(discovery_payload) - await _async_setup_entity(config, async_add_entities, + await _async_setup_entity(config, async_add_entities, config_entry, discovery_hash) except Exception: if discovery_hash: @@ -82,16 +82,16 @@ async def async_setup_entry(hass, config_entry, async_add_entities): async def _async_setup_entity(config: ConfigType, async_add_entities, - discovery_hash=None): + config_entry=None, discovery_hash=None): """Set up MQTT sensor.""" - async_add_entities([MqttSensor(config, discovery_hash)]) + async_add_entities([MqttSensor(config, config_entry, discovery_hash)]) class MqttSensor(MqttAttributes, MqttAvailability, MqttDiscoveryUpdate, MqttEntityDeviceInfo, Entity): """Representation of a sensor that can be updated using MQTT.""" - def __init__(self, config, discovery_hash): + def __init__(self, config, config_entry, discovery_hash): """Initialize the sensor.""" self._config = config self._unique_id = config.get(CONF_UNIQUE_ID) @@ -110,7 +110,7 @@ class MqttSensor(MqttAttributes, MqttAvailability, MqttDiscoveryUpdate, MqttAvailability.__init__(self, config) MqttDiscoveryUpdate.__init__(self, discovery_hash, self.discovery_update) - MqttEntityDeviceInfo.__init__(self, device_config) + MqttEntityDeviceInfo.__init__(self, device_config, config_entry) async def async_added_to_hass(self): """Subscribe to MQTT events.""" @@ -123,6 +123,7 @@ class MqttSensor(MqttAttributes, MqttAvailability, MqttDiscoveryUpdate, self._config = config await self.attributes_discovery_update(config) await self.availability_discovery_update(config) + await self.device_info_discovery_update(config) await self._subscribe_topics() self.async_schedule_update_ha_state() diff --git a/tests/components/mqtt/test_sensor.py b/tests/components/mqtt/test_sensor.py index 101eea44e28..a89b0b444ff 100644 --- a/tests/components/mqtt/test_sensor.py +++ b/tests/components/mqtt/test_sensor.py @@ -616,6 +616,53 @@ async def test_entity_device_info_with_identifier(hass, mqtt_mock): assert device.sw_version == '0.1-beta' +async def test_entity_device_info_update(hass, mqtt_mock): + """Test device registry update.""" + entry = MockConfigEntry(domain=mqtt.DOMAIN) + entry.add_to_hass(hass) + await async_start(hass, 'homeassistant', {}, entry) + registry = await hass.helpers.device_registry.async_get_registry() + + config = { + 'platform': 'mqtt', + 'name': 'Test 1', + 'state_topic': 'test-topic', + 'command_topic': 'test-command-topic', + 'device': { + 'identifiers': ['helloworld'], + 'connections': [ + ["mac", "02:5b:26:a8:dc:12"], + ], + 'manufacturer': 'Whatever', + 'name': 'Beer', + 'model': 'Glass', + 'sw_version': '0.1-beta', + }, + 'unique_id': 'veryunique' + } + + data = json.dumps(config) + async_fire_mqtt_message(hass, 'homeassistant/sensor/bla/config', + data) + await hass.async_block_till_done() + await hass.async_block_till_done() + + device = registry.async_get_device({('mqtt', 'helloworld')}, set()) + assert device is not None + assert device.name == 'Beer' + + config['device']['name'] = 'Milk' + data = json.dumps(config) + async_fire_mqtt_message(hass, 'homeassistant/sensor/bla/config', + data) + await hass.async_block_till_done() + await hass.async_block_till_done() + + device = registry.async_get_device({('mqtt', 'helloworld')}, set()) + assert device is not None + assert device.name == 'Milk' + + async def test_entity_id_update(hass, mqtt_mock): """Test MQTT subscriptions are managed when entity_id is updated.""" registry = mock_registry(hass, {}) From 85c72fbca64819c35be0f1ade8fbd5c5ba000729 Mon Sep 17 00:00:00 2001 From: emontnemery Date: Sat, 26 Jan 2019 19:48:35 +0100 Subject: [PATCH 116/222] Update device registry of MQTT alarm (#20439) --- .../components/mqtt/alarm_control_panel.py | 11 +++-- .../mqtt/test_alarm_control_panel.py | 47 +++++++++++++++++++ 2 files changed, 53 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/mqtt/alarm_control_panel.py b/homeassistant/components/mqtt/alarm_control_panel.py index f1e0d8589a3..024e926055f 100644 --- a/homeassistant/components/mqtt/alarm_control_panel.py +++ b/homeassistant/components/mqtt/alarm_control_panel.py @@ -66,7 +66,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): try: discovery_hash = discovery_payload[ATTR_DISCOVERY_HASH] config = PLATFORM_SCHEMA(discovery_payload) - await _async_setup_entity(config, async_add_entities, + await _async_setup_entity(config, async_add_entities, config_entry, discovery_hash) except Exception: if discovery_hash: @@ -78,17 +78,17 @@ async def async_setup_entry(hass, config_entry, async_add_entities): async_discover) -async def _async_setup_entity(config, async_add_entities, +async def _async_setup_entity(config, async_add_entities, config_entry=None, discovery_hash=None): """Set up the MQTT Alarm Control Panel platform.""" - async_add_entities([MqttAlarm(config, discovery_hash)]) + async_add_entities([MqttAlarm(config, config_entry, discovery_hash)]) class MqttAlarm(MqttAttributes, MqttAvailability, MqttDiscoveryUpdate, MqttEntityDeviceInfo, alarm.AlarmControlPanel): """Representation of a MQTT alarm status.""" - def __init__(self, config, discovery_hash): + def __init__(self, config, config_entry, discovery_hash): """Init the MQTT Alarm Control Panel.""" self._state = None self._config = config @@ -101,7 +101,7 @@ class MqttAlarm(MqttAttributes, MqttAvailability, MqttDiscoveryUpdate, MqttAvailability.__init__(self, config) MqttDiscoveryUpdate.__init__(self, discovery_hash, self.discovery_update) - MqttEntityDeviceInfo.__init__(self, device_config) + MqttEntityDeviceInfo.__init__(self, device_config, config_entry) async def async_added_to_hass(self): """Subscribe mqtt events.""" @@ -114,6 +114,7 @@ class MqttAlarm(MqttAttributes, MqttAvailability, MqttDiscoveryUpdate, self._config = config await self.attributes_discovery_update(config) await self.availability_discovery_update(config) + await self.device_info_discovery_update(config) await self._subscribe_topics() self.async_schedule_update_ha_state() diff --git a/tests/components/mqtt/test_alarm_control_panel.py b/tests/components/mqtt/test_alarm_control_panel.py index 600d348dd98..7bbaa977131 100644 --- a/tests/components/mqtt/test_alarm_control_panel.py +++ b/tests/components/mqtt/test_alarm_control_panel.py @@ -515,6 +515,53 @@ async def test_entity_device_info_with_identifier(hass, mqtt_mock): assert device.sw_version == '0.1-beta' +async def test_entity_device_info_update(hass, mqtt_mock): + """Test device registry update.""" + entry = MockConfigEntry(domain=mqtt.DOMAIN) + entry.add_to_hass(hass) + await async_start(hass, 'homeassistant', {}, entry) + registry = await hass.helpers.device_registry.async_get_registry() + + config = { + 'platform': 'mqtt', + 'name': 'Test 1', + 'state_topic': 'test-topic', + 'command_topic': 'test-command-topic', + 'device': { + 'identifiers': ['helloworld'], + 'connections': [ + ["mac", "02:5b:26:a8:dc:12"], + ], + 'manufacturer': 'Whatever', + 'name': 'Beer', + 'model': 'Glass', + 'sw_version': '0.1-beta', + }, + 'unique_id': 'veryunique' + } + + data = json.dumps(config) + async_fire_mqtt_message( + hass, 'homeassistant/alarm_control_panel/bla/config', data) + await hass.async_block_till_done() + await hass.async_block_till_done() + + device = registry.async_get_device({('mqtt', 'helloworld')}, set()) + assert device is not None + assert device.name == 'Beer' + + config['device']['name'] = 'Milk' + data = json.dumps(config) + async_fire_mqtt_message( + hass, 'homeassistant/alarm_control_panel/bla/config', data) + await hass.async_block_till_done() + await hass.async_block_till_done() + + device = registry.async_get_device({('mqtt', 'helloworld')}, set()) + assert device is not None + assert device.name == 'Milk' + + async def test_entity_id_update(hass, mqtt_mock): """Test MQTT subscriptions are managed when entity_id is updated.""" registry = mock_registry(hass, {}) From 60dc337f3f4bda3aba588519a055e36f213bdb74 Mon Sep 17 00:00:00 2001 From: emontnemery Date: Sat, 26 Jan 2019 19:52:41 +0100 Subject: [PATCH 117/222] Update device registry of MQTT cover (#20443) * Update device registry of MQTT cover * Move config_entry to constructor --- homeassistant/components/mqtt/cover.py | 12 +++--- tests/components/mqtt/test_cover.py | 55 ++++++++++++++++++++++++++ 2 files changed, 62 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/mqtt/cover.py b/homeassistant/components/mqtt/cover.py index 7922254b327..477169a37de 100644 --- a/homeassistant/components/mqtt/cover.py +++ b/homeassistant/components/mqtt/cover.py @@ -140,7 +140,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): try: discovery_hash = discovery_payload[ATTR_DISCOVERY_HASH] config = PLATFORM_SCHEMA(discovery_payload) - await _async_setup_entity(config, async_add_entities, + await _async_setup_entity(config, async_add_entities, config_entry, discovery_hash) except Exception: if discovery_hash: @@ -152,16 +152,17 @@ async def async_setup_entry(hass, config_entry, async_add_entities): async_discover) -async def _async_setup_entity(config, async_add_entities, discovery_hash=None): +async def _async_setup_entity(config, async_add_entities, config_entry=None, + discovery_hash=None): """Set up the MQTT Cover.""" - async_add_entities([MqttCover(config, discovery_hash)]) + async_add_entities([MqttCover(config, config_entry, discovery_hash)]) class MqttCover(MqttAttributes, MqttAvailability, MqttDiscoveryUpdate, MqttEntityDeviceInfo, CoverDevice): """Representation of a cover that can be controlled using MQTT.""" - def __init__(self, config, discovery_hash): + def __init__(self, config, config_entry, discovery_hash): """Initialize the cover.""" self._unique_id = config.get(CONF_UNIQUE_ID) self._position = None @@ -181,7 +182,7 @@ class MqttCover(MqttAttributes, MqttAvailability, MqttDiscoveryUpdate, MqttAvailability.__init__(self, config) MqttDiscoveryUpdate.__init__(self, discovery_hash, self.discovery_update) - MqttEntityDeviceInfo.__init__(self, device_config) + MqttEntityDeviceInfo.__init__(self, device_config, config_entry) async def async_added_to_hass(self): """Subscribe MQTT events.""" @@ -194,6 +195,7 @@ class MqttCover(MqttAttributes, MqttAvailability, MqttDiscoveryUpdate, self._setup_from_config(config) await self.attributes_discovery_update(config) await self.availability_discovery_update(config) + await self.device_info_discovery_update(config) await self._subscribe_topics() self.async_schedule_update_ha_state() diff --git a/tests/components/mqtt/test_cover.py b/tests/components/mqtt/test_cover.py index 500f261ba4e..0f6d184b775 100644 --- a/tests/components/mqtt/test_cover.py +++ b/tests/components/mqtt/test_cover.py @@ -758,6 +758,7 @@ class TestCoverMQTT(unittest.TestCase): 'set_position_topic': None, 'set_position_template': None, 'unique_id': None, 'device_config': None, }, + None, None) assert 44 == mqtt_cover.find_percentage_in_range(44) @@ -788,6 +789,7 @@ class TestCoverMQTT(unittest.TestCase): 'set_position_topic': None, 'set_position_template': None, 'unique_id': None, 'device_config': None, }, + None, None) assert 40 == mqtt_cover.find_percentage_in_range(120) @@ -818,6 +820,7 @@ class TestCoverMQTT(unittest.TestCase): 'set_position_topic': None, 'set_position_template': None, 'unique_id': None, 'device_config': None, }, + None, None) assert 56 == mqtt_cover.find_percentage_in_range(44) @@ -848,6 +851,7 @@ class TestCoverMQTT(unittest.TestCase): 'set_position_topic': None, 'set_position_template': None, 'unique_id': None, 'device_config': None, }, + None, None) assert 60 == mqtt_cover.find_percentage_in_range(120) @@ -878,6 +882,7 @@ class TestCoverMQTT(unittest.TestCase): 'set_position_topic': None, 'set_position_template': None, 'unique_id': None, 'device_config': None, }, + None, None) assert 44 == mqtt_cover.find_in_range_from_percent(44) @@ -908,6 +913,7 @@ class TestCoverMQTT(unittest.TestCase): 'set_position_topic': None, 'set_position_template': None, 'unique_id': None, 'device_config': None, }, + None, None) assert 120 == mqtt_cover.find_in_range_from_percent(40) @@ -938,6 +944,7 @@ class TestCoverMQTT(unittest.TestCase): 'set_position_topic': None, 'set_position_template': None, 'unique_id': None, 'device_config': None, }, + None, None) assert 44 == mqtt_cover.find_in_range_from_percent(56) @@ -968,6 +975,7 @@ class TestCoverMQTT(unittest.TestCase): 'set_position_topic': None, 'set_position_template': None, 'unique_id': None, 'device_config': None, }, + None, None) assert 120 == mqtt_cover.find_in_range_from_percent(60) @@ -1293,6 +1301,53 @@ async def test_entity_device_info_with_identifier(hass, mqtt_mock): assert device.sw_version == '0.1-beta' +async def test_entity_device_info_update(hass, mqtt_mock): + """Test device registry update.""" + entry = MockConfigEntry(domain=mqtt.DOMAIN) + entry.add_to_hass(hass) + await async_start(hass, 'homeassistant', {}, entry) + registry = await hass.helpers.device_registry.async_get_registry() + + config = { + 'platform': 'mqtt', + 'name': 'Test 1', + 'state_topic': 'test-topic', + 'command_topic': 'test-command-topic', + 'device': { + 'identifiers': ['helloworld'], + 'connections': [ + ["mac", "02:5b:26:a8:dc:12"], + ], + 'manufacturer': 'Whatever', + 'name': 'Beer', + 'model': 'Glass', + 'sw_version': '0.1-beta', + }, + 'unique_id': 'veryunique' + } + + data = json.dumps(config) + async_fire_mqtt_message(hass, 'homeassistant/cover/bla/config', + data) + await hass.async_block_till_done() + await hass.async_block_till_done() + + device = registry.async_get_device({('mqtt', 'helloworld')}, set()) + assert device is not None + assert device.name == 'Beer' + + config['device']['name'] = 'Milk' + data = json.dumps(config) + async_fire_mqtt_message(hass, 'homeassistant/cover/bla/config', + data) + await hass.async_block_till_done() + await hass.async_block_till_done() + + device = registry.async_get_device({('mqtt', 'helloworld')}, set()) + assert device is not None + assert device.name == 'Milk' + + async def test_entity_id_update(hass, mqtt_mock): """Test MQTT subscriptions are managed when entity_id is updated.""" registry = mock_registry(hass, {}) From b9bf6963fd17d0ff6c91bf3a336710e6c3f99f88 Mon Sep 17 00:00:00 2001 From: starkillerOG Date: Sat, 26 Jan 2019 21:50:34 +0100 Subject: [PATCH 118/222] Philips Hue, include debug message for color gamut (#20455) --- homeassistant/components/hue/light.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/hue/light.py b/homeassistant/components/hue/light.py index 89b731cb0e5..a10b42fbeee 100644 --- a/homeassistant/components/hue/light.py +++ b/homeassistant/components/hue/light.py @@ -228,6 +228,7 @@ class HueLight(Light): self.is_philips = light.manufacturername == 'Philips' self.gamut_typ = self.light.colorgamuttype self.gamut = self.light.colorgamut + _LOGGER.debug("Color gamut of %s: %s", self.name, str(self.gamut)) @property def unique_id(self): From 9920699bb884ba78899b6cf576b3a4a7d0b8cd03 Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Sat, 26 Jan 2019 22:11:00 +0100 Subject: [PATCH 119/222] Upgrade sqlalchemy to 1.2.16 (#20474) --- homeassistant/components/recorder/__init__.py | 2 +- homeassistant/components/sensor/sql.py | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/recorder/__init__.py b/homeassistant/components/recorder/__init__.py index 4142e72337a..eb97d197e3e 100644 --- a/homeassistant/components/recorder/__init__.py +++ b/homeassistant/components/recorder/__init__.py @@ -33,7 +33,7 @@ from . import migration, purge from .const import DATA_INSTANCE from .util import session_scope -REQUIREMENTS = ['sqlalchemy==1.2.15'] +REQUIREMENTS = ['sqlalchemy==1.2.16'] _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/sensor/sql.py b/homeassistant/components/sensor/sql.py index 1533aa94822..136e3cac23b 100644 --- a/homeassistant/components/sensor/sql.py +++ b/homeassistant/components/sensor/sql.py @@ -20,7 +20,7 @@ from homeassistant.helpers.entity import Entity _LOGGER = logging.getLogger(__name__) -REQUIREMENTS = ['sqlalchemy==1.2.15'] +REQUIREMENTS = ['sqlalchemy==1.2.16'] CONF_COLUMN_NAME = 'column' CONF_QUERIES = 'queries' diff --git a/requirements_all.txt b/requirements_all.txt index 7db8d380850..961effbf16e 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1562,7 +1562,7 @@ spotipy-homeassistant==2.4.4.dev1 # homeassistant.components.recorder # homeassistant.components.sensor.sql -sqlalchemy==1.2.15 +sqlalchemy==1.2.16 # homeassistant.components.sensor.srp_energy srpenergy==1.0.5 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 659f02459d3..b2571c1a460 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -261,7 +261,7 @@ somecomfort==0.5.2 # homeassistant.components.recorder # homeassistant.components.sensor.sql -sqlalchemy==1.2.15 +sqlalchemy==1.2.16 # homeassistant.components.sensor.srp_energy srpenergy==1.0.5 From 38b1ce3fe05d6c7301f368f0ca9cd8234d5bac32 Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Sat, 26 Jan 2019 22:11:36 +0100 Subject: [PATCH 120/222] Upgrade psutil to 5.5.0 (#20462) --- 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 dbb2f4ed032..d0b3df5dd0e 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.8'] +REQUIREMENTS = ['psutil==5.5.0'] _LOGGER = logging.getLogger(__name__) diff --git a/requirements_all.txt b/requirements_all.txt index 961effbf16e..3d8e39dce7c 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -838,7 +838,7 @@ prometheus_client==0.2.0 protobuf==3.6.1 # homeassistant.components.sensor.systemmonitor -psutil==5.4.8 +psutil==5.5.0 # homeassistant.components.wink pubnubsub-handler==1.0.3 From 87316c4e836414ea64adb0658f18e08abafa978c Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sat, 26 Jan 2019 14:09:41 -0800 Subject: [PATCH 121/222] Warn for old slugs/entity ids (#20478) * Warn for old slugs/entity ids * add comments * Lint * LInt * Lint * Lint --- homeassistant/bootstrap.py | 29 +++++++++++++++++ homeassistant/helpers/config_validation.py | 36 +++++++++++++++++++++- tests/helpers/test_config_validation.py | 28 +++++++++++++++++ 3 files changed, 92 insertions(+), 1 deletion(-) diff --git a/homeassistant/bootstrap.py b/homeassistant/bootstrap.py index c764bfe8c21..5dd62005609 100644 --- a/homeassistant/bootstrap.py +++ b/homeassistant/bootstrap.py @@ -18,6 +18,7 @@ from homeassistant.util.logging import AsyncHandler from homeassistant.util.package import async_get_user_site, is_virtual_env from homeassistant.util.yaml import clear_secret_cache from homeassistant.exceptions import HomeAssistantError +from homeassistant.helpers import config_validation as cv _LOGGER = logging.getLogger(__name__) @@ -153,6 +154,34 @@ async def async_from_config_dict(config: Dict[str, Any], stop = time() _LOGGER.info("Home Assistant initialized in %.2fs", stop-start) + # TEMP: warn users for invalid slugs + # Remove after 0.94 or 1.0 + if cv.INVALID_SLUGS_FOUND or cv.INVALID_ENTITY_IDS_FOUND: + msg = [] + + if cv.INVALID_ENTITY_IDS_FOUND: + msg.append( + "Your configuration contains invalid entity ID references. " + "Please find and update the following. " + "This will become a breaking change." + ) + msg.append('\n'.join('- {} -> {}'.format(*item) + for item + in cv.INVALID_ENTITY_IDS_FOUND.items())) + + if cv.INVALID_SLUGS_FOUND: + msg.append( + "Your configuration contains invalid slugs. " + "Please find and update the following. " + "This will become a breaking change." + ) + msg.append('\n'.join('- {} -> {}'.format(*item) + for item in cv.INVALID_SLUGS_FOUND.items())) + + hass.components.persistent_notification.async_create( + '\n\n'.join(msg), "Config Warning", "config_warning" + ) + return hass diff --git a/homeassistant/helpers/config_validation.py b/homeassistant/helpers/config_validation.py index 475135b4cce..1edf52cbb7f 100644 --- a/homeassistant/helpers/config_validation.py +++ b/homeassistant/helpers/config_validation.py @@ -26,6 +26,13 @@ from homeassistant.helpers import template as template_helper # pylint: disable=invalid-name TIME_PERIOD_ERROR = "offset {} should be format 'HH:MM' or 'HH:MM:SS'" +OLD_SLUG_VALIDATION = r'^[a-z0-9_]+$' +OLD_ENTITY_ID_VALIDATION = r"^(\w+)\.(\w+)$" +# Keep track of invalid slugs and entity ids found so we can create a +# persistent notification. Rare temporary exception to use a global. +INVALID_SLUGS_FOUND = {} +INVALID_ENTITY_IDS_FOUND = {} + # Home Assistant types byte = vol.All(vol.Coerce(int), vol.Range(min=0, max=255)) @@ -149,6 +156,18 @@ def entity_id(value: Any) -> str: value = string(value).lower() if valid_entity_id(value): return value + elif re.match(OLD_ENTITY_ID_VALIDATION, value): + # To ease the breaking change, we allow old slugs for now + # Remove after 0.94 or 1.0 + fixed = '.'.join(util_slugify(part) for part in value.split('.', 1)) + INVALID_ENTITY_IDS_FOUND[value] = fixed + logging.getLogger(__name__).warning( + "Found invalid entity_id %s, please update with %s. This " + "will become a breaking change.", + value, fixed + ) + return value + raise vol.Invalid('Entity ID {} is an invalid entity id'.format(value)) @@ -333,7 +352,22 @@ def schema_with_slug_keys(value_schema: Union[T, Callable]) -> Callable: raise vol.Invalid('expected dictionary') for key in value.keys(): - slug(key) + try: + slug(key) + except vol.Invalid: + # To ease the breaking change, we allow old slugs for now + # Remove after 0.94 or 1.0 + if re.match(OLD_SLUG_VALIDATION, key): + fixed = util_slugify(key) + INVALID_SLUGS_FOUND[key] = fixed + logging.getLogger(__name__).warning( + "Found invalid slug %s, please update with %s. This " + "will be come a breaking change.", + key, fixed + ) + else: + raise + return schema(value) return verify diff --git a/tests/helpers/test_config_validation.py b/tests/helpers/test_config_validation.py index 03dd3cfe55a..1bae84b5320 100644 --- a/tests/helpers/test_config_validation.py +++ b/tests/helpers/test_config_validation.py @@ -602,3 +602,31 @@ def test_comp_entity_ids(): for invalid in (['light.kitchen', 'not-entity-id'], '*', ''): with pytest.raises(vol.Invalid): schema(invalid) + + +def test_schema_with_slug_keys_allows_old_slugs(caplog): + """Test schema with slug keys allowing old slugs.""" + schema = cv.schema_with_slug_keys(str) + + with patch.dict(cv.INVALID_SLUGS_FOUND, clear=True): + for value in ('_world', 'wow__yeah'): + caplog.clear() + # Will raise if not allowing old slugs + schema({value: 'yo'}) + assert "Found invalid slug {}".format(value) in caplog.text + + assert len(cv.INVALID_SLUGS_FOUND) == 2 + + +def test_entity_id_allow_old_validation(caplog): + """Test schema allowing old entity_ids.""" + schema = vol.Schema(cv.entity_id) + + with patch.dict(cv.INVALID_ENTITY_IDS_FOUND, clear=True): + for value in ('hello.__world', 'great.wow__yeah'): + caplog.clear() + # Will raise if not allowing old entity ID + schema(value) + assert "Found invalid entity_id {}".format(value) in caplog.text + + assert len(cv.INVALID_ENTITY_IDS_FOUND) == 2 From a368db9ad40e14392552153d9df5f32d45c030f6 Mon Sep 17 00:00:00 2001 From: Shantanu Tushar Date: Sun, 27 Jan 2019 03:41:16 +0530 Subject: [PATCH 122/222] Include exception details in the error log (#20461) * Include exception details in the error log I see this error quite often in my HA logs and this will be helpful for anyone who is attempting to debug this. * Minor change * Remove line break --- .../components/media_player/firetv.py | 23 +++++++++---------- 1 file changed, 11 insertions(+), 12 deletions(-) diff --git a/homeassistant/components/media_player/firetv.py b/homeassistant/components/media_player/firetv.py index 80be58c04e1..c04ed96d6e0 100644 --- a/homeassistant/components/media_player/firetv.py +++ b/homeassistant/components/media_player/firetv.py @@ -81,7 +81,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): device = FireTVDevice(ftv, name, get_source, get_sources) add_entities([device]) - _LOGGER.info("Setup Fire TV at %s%s", host, adb_log) + _LOGGER.debug("Setup Fire TV at %s%s", host, adb_log) def adb_decorator(override_available=False): @@ -96,17 +96,17 @@ def adb_decorator(override_available=False): # If an ADB command is already running, skip this command if not self.adb_lock.acquire(blocking=False): - _LOGGER.info('Skipping an ADB command because a previous ' - 'command is still running') + _LOGGER.info("Skipping an ADB command because a previous " + "command is still running") return None # Additional ADB commands will be prevented while trying this one try: returns = func(self, *args, **kwargs) - except self.exceptions: - _LOGGER.error('Failed to execute an ADB command; will attempt ' - 'to re-establish the ADB connection in the next ' - 'update') + except self.exceptions as err: + _LOGGER.error( + "Failed to execute an ADB command. ADB connection re-" + "establishing attempt in the next update. Error: %s", err) returns = None self._available = False # pylint: disable=protected-access finally: @@ -137,9 +137,9 @@ class FireTVDevice(MediaPlayerDevice): self.adb_lock = threading.Lock() # ADB exceptions to catch - self.exceptions = (AttributeError, BrokenPipeError, TypeError, - ValueError, InvalidChecksumError, - InvalidCommandError, InvalidResponseError) + self.exceptions = ( + AttributeError, BrokenPipeError, TypeError, ValueError, + InvalidChecksumError, InvalidCommandError, InvalidResponseError) self._state = None self._available = self.firetv.available @@ -231,8 +231,7 @@ class FireTVDevice(MediaPlayerDevice): self._running_apps = None # Check if the launcher is active. - if self._current_app in [PACKAGE_LAUNCHER, - PACKAGE_SETTINGS]: + if self._current_app in [PACKAGE_LAUNCHER, PACKAGE_SETTINGS]: self._state = STATE_STANDBY # Check for a wake lock (device is playing). From 38c33bd01e2358bd0f5839afd9512fb4c9b8cf86 Mon Sep 17 00:00:00 2001 From: Rohan Kapoor Date: Sat, 26 Jan 2019 17:11:09 -0800 Subject: [PATCH 123/222] Fix linting error (#20488) --- homeassistant/helpers/config_validation.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/helpers/config_validation.py b/homeassistant/helpers/config_validation.py index 1edf52cbb7f..56d64cd8fd9 100644 --- a/homeassistant/helpers/config_validation.py +++ b/homeassistant/helpers/config_validation.py @@ -156,7 +156,7 @@ def entity_id(value: Any) -> str: value = string(value).lower() if valid_entity_id(value): return value - elif re.match(OLD_ENTITY_ID_VALIDATION, value): + if re.match(OLD_ENTITY_ID_VALIDATION, value): # To ease the breaking change, we allow old slugs for now # Remove after 0.94 or 1.0 fixed = '.'.join(util_slugify(part) for part in value.split('.', 1)) From 239c60c09f07ef15236fa9f123d2f16efbdd97a2 Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Sat, 26 Jan 2019 23:21:51 -0700 Subject: [PATCH 124/222] Use HASS latitude/longitude as defaults for Lyft (#20491) --- homeassistant/components/sensor/lyft.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/sensor/lyft.py b/homeassistant/components/sensor/lyft.py index 671308871e5..6fb4a6bf8be 100644 --- a/homeassistant/components/sensor/lyft.py +++ b/homeassistant/components/sensor/lyft.py @@ -33,8 +33,8 @@ MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=60) PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Required(CONF_CLIENT_ID): cv.string, vol.Required(CONF_CLIENT_SECRET): cv.string, - vol.Required(CONF_START_LATITUDE): cv.latitude, - vol.Required(CONF_START_LONGITUDE): cv.longitude, + vol.Optional(CONF_START_LATITUDE): cv.latitude, + vol.Optional(CONF_START_LONGITUDE): cv.longitude, vol.Optional(CONF_END_LATITUDE): cv.latitude, vol.Optional(CONF_END_LONGITUDE): cv.longitude, vol.Optional(CONF_PRODUCT_IDS): @@ -56,7 +56,8 @@ def setup_platform(hass, config, add_entities, discovery_info=None): session = auth_flow.get_session() timeandpriceest = LyftEstimate( - session, config[CONF_START_LATITUDE], config[CONF_START_LONGITUDE], + session, config.get(CONF_START_LATITUDE, hass.config.latitude), + config.get(CONF_START_LONGITUDE, hass.config.longitude), config.get(CONF_END_LATITUDE), config.get(CONF_END_LONGITUDE)) timeandpriceest.fetch_data() except APIError as exc: From d82e5ecbb0f093feca7d973a8cef836cc06ee701 Mon Sep 17 00:00:00 2001 From: Rohan Kapoor Date: Sun, 27 Jan 2019 01:28:20 -0800 Subject: [PATCH 125/222] Upgrade zm-py to 0.3.1 (#20489) --- homeassistant/components/zoneminder/__init__.py | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/zoneminder/__init__.py b/homeassistant/components/zoneminder/__init__.py index 2a7859ebba4..4591e14a006 100644 --- a/homeassistant/components/zoneminder/__init__.py +++ b/homeassistant/components/zoneminder/__init__.py @@ -16,7 +16,7 @@ from homeassistant.helpers.discovery import async_load_platform _LOGGER = logging.getLogger(__name__) -REQUIREMENTS = ['zm-py==0.3.0'] +REQUIREMENTS = ['zm-py==0.3.1'] CONF_PATH_ZMS = 'path_zms' diff --git a/requirements_all.txt b/requirements_all.txt index 3d8e39dce7c..f1d964d3e39 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1772,4 +1772,4 @@ zigpy-xbee==0.1.1 zigpy==0.2.0 # homeassistant.components.zoneminder -zm-py==0.3.0 +zm-py==0.3.1 From 7368c623d42d83fa9e431d53b72652deec80b543 Mon Sep 17 00:00:00 2001 From: Rohan Kapoor Date: Sun, 27 Jan 2019 01:36:10 -0800 Subject: [PATCH 126/222] Split out dovado to a component and sensor platform (#20339) * Split out dovado to a component and sensor platform * Lint * Address code review comments (#20339) * Switch to using a notify platform for dovado SMS (#20339) * Optimizing imports * Remove return on `setup_platform`. * Clean up unneeded constants --- .coveragerc | 3 +- homeassistant/components/dovado/__init__.py | 79 +++++++++ homeassistant/components/dovado/notify.py | 38 +++++ homeassistant/components/dovado/sensor.py | 116 +++++++++++++ homeassistant/components/sensor/dovado.py | 172 -------------------- requirements_all.txt | 2 +- 6 files changed, 236 insertions(+), 174 deletions(-) create mode 100644 homeassistant/components/dovado/__init__.py create mode 100644 homeassistant/components/dovado/notify.py create mode 100644 homeassistant/components/dovado/sensor.py delete mode 100644 homeassistant/components/sensor/dovado.py diff --git a/.coveragerc b/.coveragerc index 45dbeb9cb60..b89b3a6be60 100644 --- a/.coveragerc +++ b/.coveragerc @@ -87,6 +87,8 @@ omit = homeassistant/components/doorbird.py homeassistant/components/*/doorbird.py + homeassistant/components/dovado/* + homeassistant/components/dweet.py homeassistant/components/*/dweet.py @@ -754,7 +756,6 @@ omit = homeassistant/components/sensor/dht.py 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 diff --git a/homeassistant/components/dovado/__init__.py b/homeassistant/components/dovado/__init__.py new file mode 100644 index 00000000000..7a50ac815b1 --- /dev/null +++ b/homeassistant/components/dovado/__init__.py @@ -0,0 +1,79 @@ +""" +Support for Dovado router. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/dovado/ +""" +import logging +from datetime import timedelta + +import voluptuous as vol + +import homeassistant.helpers.config_validation as cv +from homeassistant.const import ( + CONF_USERNAME, CONF_PASSWORD, CONF_HOST, CONF_PORT, + DEVICE_DEFAULT_NAME) +from homeassistant.util import Throttle + +_LOGGER = logging.getLogger(__name__) + +REQUIREMENTS = ['dovado==0.4.1'] + +DOMAIN = 'dovado' + +CONFIG_SCHEMA = vol.Schema({ + vol.Required(CONF_USERNAME): cv.string, + vol.Required(CONF_PASSWORD): cv.string, + vol.Optional(CONF_HOST): cv.string, + vol.Optional(CONF_PORT): cv.port, +}) + +MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=30) + + +def setup(hass, config): + """Set up the Dovado component.""" + import dovado + + hass.data[DOMAIN] = DovadoData( + dovado.Dovado( + config[CONF_USERNAME], + config[CONF_PASSWORD], + config.get(CONF_HOST), + config.get(CONF_PORT) + ) + ) + return True + + +class DovadoData: + """Maintains a connection to the router.""" + + def __init__(self, client): + """Set up a new Dovado connection.""" + self._client = client + self.state = {} + + @property + def name(self): + """Name of the router.""" + return self.state.get("product name", DEVICE_DEFAULT_NAME) + + @Throttle(MIN_TIME_BETWEEN_UPDATES) + def update(self): + """Update device state.""" + try: + self.state = self._client.state or {} + if not self.state: + return False + self.state.update( + connected=self.state.get("modem status") == "CONNECTED") + _LOGGER.debug("Received: %s", self.state) + return True + except OSError as error: + _LOGGER.warning("Could not contact the router: %s", error) + + @property + def client(self): + """Dovado client instance.""" + return self._client diff --git a/homeassistant/components/dovado/notify.py b/homeassistant/components/dovado/notify.py new file mode 100644 index 00000000000..00036378a78 --- /dev/null +++ b/homeassistant/components/dovado/notify.py @@ -0,0 +1,38 @@ +""" +Support for SMS notifications from the Dovado router. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/notify.dovado/ +""" +import logging + +from homeassistant.components.dovado import DOMAIN as DOVADO_DOMAIN +from homeassistant.components.notify import BaseNotificationService, \ + ATTR_TARGET + +_LOGGER = logging.getLogger(__name__) + +DEPENDENCIES = ['dovado'] + + +def get_service(hass, config, discovery_info=None): + """Get the Dovado Router SMS notification service.""" + return DovadoSMSNotificationService(hass.data[DOVADO_DOMAIN].client) + + +class DovadoSMSNotificationService(BaseNotificationService): + """Implement the notification service for the Dovado SMS component.""" + + def __init__(self, client): + """Initialize the service.""" + self._client = client + + def send_message(self, message, **kwargs): + """Send SMS to the specified target phone number.""" + target = kwargs.get(ATTR_TARGET) + + if not target: + _LOGGER.error("One target is required") + return + + self._client.send_sms(target, message) diff --git a/homeassistant/components/dovado/sensor.py b/homeassistant/components/dovado/sensor.py new file mode 100644 index 00000000000..b89275b1795 --- /dev/null +++ b/homeassistant/components/dovado/sensor.py @@ -0,0 +1,116 @@ +""" +Support for sensors from the Dovado router. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/sensor.dovado/ +""" +import logging +import re +from datetime import timedelta + +import voluptuous as vol + +import homeassistant.helpers.config_validation as cv +from homeassistant.components.dovado import DOMAIN as DOVADO_DOMAIN +from homeassistant.components.sensor import PLATFORM_SCHEMA +from homeassistant.const import CONF_SENSORS +from homeassistant.helpers.entity import Entity + +_LOGGER = logging.getLogger(__name__) + +DEPENDENCIES = ['dovado'] + +MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=30) + +SENSOR_UPLOAD = 'upload' +SENSOR_DOWNLOAD = 'download' +SENSOR_SIGNAL = 'signal' +SENSOR_NETWORK = 'network' +SENSOR_SMS_UNREAD = 'sms' + +SENSORS = { + SENSOR_NETWORK: ('signal strength', 'Network', None, + 'mdi:access-point-network'), + SENSOR_SIGNAL: ('signal strength', 'Signal Strength', '%', + 'mdi:signal'), + SENSOR_SMS_UNREAD: ('sms unread', 'SMS unread', '', + 'mdi:message-text-outline'), + SENSOR_UPLOAD: ('traffic modem tx', 'Sent', 'GB', + 'mdi:cloud-upload'), + SENSOR_DOWNLOAD: ('traffic modem rx', 'Received', 'GB', + 'mdi:cloud-download'), +} + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Required(CONF_SENSORS): vol.All( + cv.ensure_list, [vol.In(SENSORS)] + ), +}) + + +def setup_platform(hass, config, add_entities, discovery_info=None): + """Set up the Dovado sensor platform.""" + dovado = hass.data[DOVADO_DOMAIN] + + entities = [] + for sensor in config[CONF_SENSORS]: + entities.append(DovadoSensor(dovado, sensor)) + + add_entities(entities) + + +class DovadoSensor(Entity): + """Representation of a Dovado sensor.""" + + def __init__(self, data, sensor): + """Initialize the sensor.""" + self._data = data + self._sensor = sensor + self._state = self._compute_state() + + def _compute_state(self): + state = self._data.state.get(SENSORS[self._sensor][0]) + if self._sensor == SENSOR_NETWORK: + match = re.search(r"\((.+)\)", state) + return match.group(1) if match else None + if self._sensor == SENSOR_SIGNAL: + try: + return int(state.split()[0]) + except ValueError: + return None + if self._sensor == SENSOR_SMS_UNREAD: + return int(state) + if self._sensor in [SENSOR_UPLOAD, SENSOR_DOWNLOAD]: + return round(float(state) / 1e6, 1) + return state + + def update(self): + """Update sensor values.""" + self._data.update() + self._state = self._compute_state() + + @property + def name(self): + """Return the name of the sensor.""" + return "{} {}".format(self._data.name, SENSORS[self._sensor][1]) + + @property + def state(self): + """Return the sensor state.""" + return self._state + + @property + def icon(self): + """Return the icon for the sensor.""" + return SENSORS[self._sensor][3] + + @property + def unit_of_measurement(self): + """Return the unit of measurement.""" + return SENSORS[self._sensor][2] + + @property + def device_state_attributes(self): + """Return the state attributes.""" + return {k: v for k, v in self._data.state.items() + if k not in ['date', 'time']} diff --git a/homeassistant/components/sensor/dovado.py b/homeassistant/components/sensor/dovado.py deleted file mode 100644 index 03c2ad601df..00000000000 --- a/homeassistant/components/sensor/dovado.py +++ /dev/null @@ -1,172 +0,0 @@ -""" -Support for Dovado router. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/sensor.dovado/ -""" -import logging -import re -from datetime import timedelta - -import voluptuous as vol - -from homeassistant.helpers.entity import Entity -from homeassistant.util import Throttle -from homeassistant.util import slugify -import homeassistant.helpers.config_validation as cv -from homeassistant.const import ( - CONF_USERNAME, CONF_PASSWORD, CONF_HOST, CONF_PORT, CONF_SENSORS, - DEVICE_DEFAULT_NAME) -from homeassistant.components.sensor import (DOMAIN, PLATFORM_SCHEMA) - -_LOGGER = logging.getLogger(__name__) - -REQUIREMENTS = ['dovado==0.4.1'] - -MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=30) - -SENSOR_UPLOAD = 'upload' -SENSOR_DOWNLOAD = 'download' -SENSOR_SIGNAL = 'signal' -SENSOR_NETWORK = 'network' -SENSOR_SMS_UNREAD = 'sms' - -SENSORS = { - SENSOR_NETWORK: ('signal strength', 'Network', None, - 'mdi:access-point-network'), - SENSOR_SIGNAL: ('signal strength', 'Signal Strength', '%', - 'mdi:signal'), - SENSOR_SMS_UNREAD: ('sms unread', 'SMS unread', '', - 'mdi:message-text-outline'), - SENSOR_UPLOAD: ('traffic modem tx', 'Sent', 'GB', - 'mdi:cloud-upload'), - SENSOR_DOWNLOAD: ('traffic modem rx', 'Received', 'GB', - 'mdi:cloud-download'), -} - -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Required(CONF_USERNAME): cv.string, - vol.Required(CONF_PASSWORD): cv.string, - vol.Optional(CONF_HOST): cv.string, - vol.Optional(CONF_PORT): cv.port, - vol.Optional(CONF_SENSORS): - vol.All(cv.ensure_list, [vol.In(SENSORS)]), -}) - - -def setup_platform(hass, config, add_entities, discovery_info=None): - """Set up the Dovado platform for sensors.""" - return Dovado().setup(hass, config, add_entities) - - -class Dovado: - """A connection to the router.""" - - def __init__(self): - """Initialize.""" - self.state = {} - self._dovado = None - - def setup(self, hass, config, add_entities): - """Set up the connection.""" - import dovado - self._dovado = dovado.Dovado( - config.get(CONF_USERNAME), config.get(CONF_PASSWORD), - config.get(CONF_HOST), config.get(CONF_PORT)) - - if not self.update(): - return False - - def send_sms(service): - """Send SMS through the router.""" - number = service.data.get('number') - message = service.data.get('message') - _LOGGER.debug("message for %s: %s", number, message) - self._dovado.send_sms(number, message) - - if self.state.get('sms') == 'enabled': - service_name = slugify("{} {}".format(self.name, 'send_sms')) - hass.services.register(DOMAIN, service_name, send_sms) - - for sensor in SENSORS: - if sensor in config.get(CONF_SENSORS, [sensor]): - add_entities([DovadoSensor(self, sensor)]) - - return True - - @property - def name(self): - """Name of the router.""" - return self.state.get("product name", DEVICE_DEFAULT_NAME) - - @Throttle(MIN_TIME_BETWEEN_UPDATES) - def update(self): - """Update device state.""" - _LOGGER.info("Updating") - try: - self.state = self._dovado.state or {} - if not self.state: - return False - self.state.update( - connected=self.state.get("modem status") == "CONNECTED") - _LOGGER.debug("Received: %s", self.state) - return True - except OSError as error: - _LOGGER.warning("Could not contact the router: %s", error) - - -class DovadoSensor(Entity): - """Representation of a Dovado sensor.""" - - def __init__(self, dovado, sensor): - """Initialize the sensor.""" - self._dovado = dovado - self._sensor = sensor - self._state = self._compute_state() - - def _compute_state(self): - state = self._dovado.state.get(SENSORS[self._sensor][0]) - if self._sensor == SENSOR_NETWORK: - match = re.search(r"\((.+)\)", state) - return match.group(1) if match else None - if self._sensor == SENSOR_SIGNAL: - try: - return int(state.split()[0]) - except ValueError: - return 0 - if self._sensor == SENSOR_SMS_UNREAD: - return int(state) - if self._sensor in [SENSOR_UPLOAD, SENSOR_DOWNLOAD]: - return round(float(state) / 1e6, 1) - return state - - def update(self): - """Update sensor values.""" - self._dovado.update() - self._state = self._compute_state() - - @property - def name(self): - """Return the name of the sensor.""" - return "{} {}".format(self._dovado.name, SENSORS[self._sensor][1]) - - @property - def state(self): - """Return the sensor state.""" - return self._state - - @property - def icon(self): - """Return the icon for the sensor.""" - return SENSORS[self._sensor][3] - - @property - def unit_of_measurement(self): - """Return the unit of measurement.""" - return SENSORS[self._sensor][2] - - @property - def device_state_attributes(self): - """Return the state attributes.""" - return {k: v for k, v in self._dovado.state.items() - if k not in ['date', 'time']} diff --git a/requirements_all.txt b/requirements_all.txt index f1d964d3e39..637bb16d800 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -332,7 +332,7 @@ dlipower==0.7.165 # homeassistant.components.doorbird doorbirdpy==2.0.6 -# homeassistant.components.sensor.dovado +# homeassistant.components.dovado dovado==0.4.1 # homeassistant.components.sensor.dsmr From 10e3698fd7c4f1e529b5d4b28ae89b762132b473 Mon Sep 17 00:00:00 2001 From: Jc2k Date: Sun, 27 Jan 2019 11:34:49 +0000 Subject: [PATCH 127/222] Add homekit_controller tests (#20457) * Add a test for a homekit_controller switch * Add a test for a homekit_controller lightbulb * Add a test for homekit_controller thermostat * Changes from review * Patch utcnow to known time in HK tests * Neater fixture use per review --- .../components/homekit_controller/climate.py | 11 +- requirements_all.txt | 2 +- requirements_test_all.txt | 3 + script/gen_requirements_all.py | 2 +- tests/components/homekit_controller/common.py | 144 ++++++++++++++++++ .../components/homekit_controller/conftest.py | 14 ++ .../homekit_controller/test_climate.py | 77 ++++++++++ .../homekit_controller/test_light.py | 46 ++++++ .../homekit_controller/test_switch.py | 49 ++++++ 9 files changed, 343 insertions(+), 5 deletions(-) create mode 100644 tests/components/homekit_controller/common.py create mode 100644 tests/components/homekit_controller/conftest.py create mode 100644 tests/components/homekit_controller/test_climate.py create mode 100644 tests/components/homekit_controller/test_light.py create mode 100644 tests/components/homekit_controller/test_switch.py diff --git a/homeassistant/components/homekit_controller/climate.py b/homeassistant/components/homekit_controller/climate.py index e703bfe182d..484c064d53d 100644 --- a/homeassistant/components/homekit_controller/climate.py +++ b/homeassistant/components/homekit_controller/climate.py @@ -27,6 +27,8 @@ MODE_HOMEKIT_TO_HASS = { # Map of hass operation modes to homekit modes MODE_HASS_TO_HOMEKIT = {v: k for k, v in MODE_HOMEKIT_TO_HASS.items()} +DEFAULT_VALID_MODES = list(MODE_HOMEKIT_TO_HASS) + def setup_platform(hass, config, add_entities, discovery_info=None): """Set up Homekit climate.""" @@ -50,10 +52,10 @@ class HomeKitClimateDevice(HomeKitEntity, ClimateDevice): def update_characteristics(self, characteristics): """Synchronise device state with Home Assistant.""" # pylint: disable=import-error - from homekit.models.characteristics import CharacteristicsTypes + from homekit.model.characteristics import CharacteristicsTypes for characteristic in characteristics: - ctype = characteristic['type'] + ctype = CharacteristicsTypes.get_short_uuid(characteristic['type']) if ctype == CharacteristicsTypes.HEATING_COOLING_CURRENT: self._state = MODE_HOMEKIT_TO_HASS.get( characteristic['value']) @@ -62,8 +64,11 @@ class HomeKitClimateDevice(HomeKitEntity, ClimateDevice): self._features |= SUPPORT_OPERATION_MODE self._current_mode = MODE_HOMEKIT_TO_HASS.get( characteristic['value']) + + valid_values = characteristic.get( + 'valid-values', DEFAULT_VALID_MODES) self._valid_modes = [MODE_HOMEKIT_TO_HASS.get( - mode) for mode in characteristic['valid-values']] + mode) for mode in valid_values] elif ctype == CharacteristicsTypes.TEMPERATURE_CURRENT: self._current_temp = characteristic['value'] elif ctype == CharacteristicsTypes.TEMPERATURE_TARGET: diff --git a/requirements_all.txt b/requirements_all.txt index 637bb16d800..85e9aeab078 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -529,7 +529,7 @@ home-assistant-frontend==20190121.1 homeassistant-pyozw==0.1.2 # homeassistant.components.homekit_controller -# homekit==0.12.2 +homekit==0.12.2 # homeassistant.components.homematicip_cloud homematicip==0.10.4 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index b2571c1a460..6f16780d4c2 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -112,6 +112,9 @@ holidays==0.9.9 # homeassistant.components.frontend home-assistant-frontend==20190121.1 +# homeassistant.components.homekit_controller +homekit==0.12.2 + # homeassistant.components.homematicip_cloud homematicip==0.10.4 diff --git a/script/gen_requirements_all.py b/script/gen_requirements_all.py index 67702635d47..8817ee61e8f 100755 --- a/script/gen_requirements_all.py +++ b/script/gen_requirements_all.py @@ -32,7 +32,6 @@ COMMENT_REQUIREMENTS = ( 'i2csense', 'credstash', 'bme680', - 'homekit', 'py_noaa', ) @@ -64,6 +63,7 @@ TEST_REQUIREMENTS = ( 'hdate', 'holidays', 'home-assistant-frontend', + 'homekit', 'homematicip', 'influxdb', 'jsonpath', diff --git a/tests/components/homekit_controller/common.py b/tests/components/homekit_controller/common.py new file mode 100644 index 00000000000..f96b046b4cf --- /dev/null +++ b/tests/components/homekit_controller/common.py @@ -0,0 +1,144 @@ +"""Code to support homekit_controller tests.""" +from datetime import timedelta +from unittest import mock + +from homeassistant.components.homekit_controller import ( + DOMAIN, HOMEKIT_ACCESSORY_DISPATCH, SERVICE_HOMEKIT) +from homeassistant.setup import async_setup_component +import homeassistant.util.dt as dt_util +from tests.common import async_fire_time_changed, fire_service_discovered + + +class FakePairing: + """ + A test fake that pretends to be a paired HomeKit accessory. + + This only contains methods and values that exist on the upstream Pairing + class. + """ + + def __init__(self, accessory): + """Create a fake pairing from an accessory model.""" + self.accessory = accessory + self.pairing_data = { + 'accessories': self.list_accessories_and_characteristics() + } + + def list_accessories_and_characteristics(self): + """Fake implementation of list_accessories_and_characteristics.""" + return [self.accessory.to_accessory_and_service_list()] + + def get_characteristics(self, characteristics): + """Fake implementation of get_characteristics.""" + results = {} + for aid, cid in characteristics: + for service in self.accessory.services: + for char in service.characteristics: + if char.iid != cid: + continue + results[(aid, cid)] = { + 'value': char.get_value() + } + return results + + def put_characteristics(self, characteristics): + """Fake implementation of put_characteristics.""" + for _, cid, new_val in characteristics: + for service in self.accessory.services: + for char in service.characteristics: + if char.iid != cid: + continue + char.set_value(new_val) + + +class FakeController: + """ + A test fake that pretends to be a paired HomeKit accessory. + + This only contains methods and values that exist on the upstream Controller + class. + """ + + def __init__(self): + """Create a Fake controller with no pairings.""" + self.pairings = {} + + def add(self, accessory): + """Create and register a fake pairing for a simulated accessory.""" + pairing = FakePairing(accessory) + self.pairings['00:00:00:00:00:00'] = pairing + return pairing + + +class Helper: + """Helper methods for interacting with HomeKit fakes.""" + + def __init__(self, hass, entity_id, pairing, accessory): + """Create a helper for a given accessory/entity.""" + from homekit.model.services import ServicesTypes + from homekit.model.characteristics import CharacteristicsTypes + + self.hass = hass + self.entity_id = entity_id + self.pairing = pairing + self.accessory = accessory + + self.characteristics = {} + for service in self.accessory.services: + service_name = ServicesTypes.get_short(service.type) + for char in service.characteristics: + char_name = CharacteristicsTypes.get_short(char.type) + self.characteristics[(service_name, char_name)] = char + + async def poll_and_get_state(self): + """Trigger a time based poll and return the current entity state.""" + next_update = dt_util.utcnow() + timedelta(seconds=60) + async_fire_time_changed(self.hass, next_update) + await self.hass.async_block_till_done() + + state = self.hass.states.get(self.entity_id) + assert state is not None + return state + + +async def setup_test_component(hass, services): + """Load a fake homekit accessory based on a homekit accessory model.""" + from homekit.model import Accessory + from homekit.model.services import ServicesTypes + + domain = None + for service in services: + service_name = ServicesTypes.get_short(service.type) + if service_name in HOMEKIT_ACCESSORY_DISPATCH: + domain = HOMEKIT_ACCESSORY_DISPATCH[service_name] + break + + assert domain, 'Cannot map test homekit services to homeassistant domain' + + config = { + 'discovery': { + } + } + + with mock.patch('homekit.Controller') as controller: + fake_controller = controller.return_value = FakeController() + await async_setup_component(hass, DOMAIN, config) + + accessory = Accessory('TestDevice', 'example.com', 'Test', '0001', '0.1') + accessory.services.extend(services) + pairing = fake_controller.add(accessory) + + discovery_info = { + 'host': '127.0.0.1', + 'port': 8080, + 'properties': { + 'md': 'TestDevice', + 'id': '00:00:00:00:00:00', + 'c#': 1, + } + } + + fire_service_discovered(hass, SERVICE_HOMEKIT, discovery_info) + await hass.async_block_till_done() + + return Helper(hass, '.'.join((domain, 'testdevice')), pairing, accessory) diff --git a/tests/components/homekit_controller/conftest.py b/tests/components/homekit_controller/conftest.py new file mode 100644 index 00000000000..fe6cffdc09f --- /dev/null +++ b/tests/components/homekit_controller/conftest.py @@ -0,0 +1,14 @@ +"""HomeKit controller session fixtures.""" +import datetime +from unittest import mock + +import pytest + + +@pytest.fixture +def utcnow(request): + """Freeze time at a known point.""" + start_dt = datetime.datetime(2019, 1, 1, 0, 0, 0) + with mock.patch('homeassistant.util.dt.utcnow') as dt_utcnow: + dt_utcnow.return_value = start_dt + yield dt_utcnow diff --git a/tests/components/homekit_controller/test_climate.py b/tests/components/homekit_controller/test_climate.py new file mode 100644 index 00000000000..9f5cc9d8764 --- /dev/null +++ b/tests/components/homekit_controller/test_climate.py @@ -0,0 +1,77 @@ +"""Basic checks for HomeKitclimate.""" +from homeassistant.components.climate import ( + DOMAIN, SERVICE_SET_OPERATION_MODE, SERVICE_SET_TEMPERATURE) +from tests.components.homekit_controller.common import ( + setup_test_component) + + +HEATING_COOLING_TARGET = ('thermostat', 'heating-cooling.target') +HEATING_COOLING_CURRENT = ('thermostat', 'heating-cooling.current') +TEMPERATURE_TARGET = ('thermostat', 'temperature.target') +TEMPERATURE_CURRENT = ('thermostat', 'temperature.current') + + +async def test_climate_change_thermostat_state(hass, utcnow): + """Test that we can turn a HomeKit thermostat on and off again.""" + from homekit.model.services import ThermostatService + + helper = await setup_test_component(hass, [ThermostatService()]) + + await hass.services.async_call(DOMAIN, SERVICE_SET_OPERATION_MODE, { + 'entity_id': 'climate.testdevice', + 'operation_mode': 'heat', + }, blocking=True) + + assert helper.characteristics[HEATING_COOLING_TARGET].value == 1 + + await hass.services.async_call(DOMAIN, SERVICE_SET_OPERATION_MODE, { + 'entity_id': 'climate.testdevice', + 'operation_mode': 'cool', + }, blocking=True) + assert helper.characteristics[HEATING_COOLING_TARGET].value == 2 + + +async def test_climate_change_thermostat_temperature(hass, utcnow): + """Test that we can turn a HomeKit thermostat on and off again.""" + from homekit.model.services import ThermostatService + + helper = await setup_test_component(hass, [ThermostatService()]) + + await hass.services.async_call(DOMAIN, SERVICE_SET_TEMPERATURE, { + 'entity_id': 'climate.testdevice', + 'temperature': 21, + }, blocking=True) + assert helper.characteristics[TEMPERATURE_TARGET].value == 21 + + await hass.services.async_call(DOMAIN, SERVICE_SET_TEMPERATURE, { + 'entity_id': 'climate.testdevice', + 'temperature': 25, + }, blocking=True) + assert helper.characteristics[TEMPERATURE_TARGET].value == 25 + + +async def test_climate_read_thermostat_state(hass, utcnow): + """Test that we can read the state of a HomeKit thermostat accessory.""" + from homekit.model.services import ThermostatService + + helper = await setup_test_component(hass, [ThermostatService()]) + + # Simulate that heating is on + helper.characteristics[TEMPERATURE_CURRENT].value = 19 + helper.characteristics[TEMPERATURE_TARGET].value = 21 + helper.characteristics[HEATING_COOLING_CURRENT].value = 1 + helper.characteristics[HEATING_COOLING_TARGET].value = 1 + + state = await helper.poll_and_get_state() + assert state.state == 'heat' + assert state.attributes['current_temperature'] == 19 + + # Simulate that cooling is on + helper.characteristics[TEMPERATURE_CURRENT].value = 21 + helper.characteristics[TEMPERATURE_TARGET].value = 19 + helper.characteristics[HEATING_COOLING_CURRENT].value = 2 + helper.characteristics[HEATING_COOLING_TARGET].value = 2 + + state = await helper.poll_and_get_state() + assert state.state == 'cool' + assert state.attributes['current_temperature'] == 21 diff --git a/tests/components/homekit_controller/test_light.py b/tests/components/homekit_controller/test_light.py new file mode 100644 index 00000000000..152940818c1 --- /dev/null +++ b/tests/components/homekit_controller/test_light.py @@ -0,0 +1,46 @@ +"""Basic checks for HomeKitSwitch.""" +from tests.components.homekit_controller.common import ( + setup_test_component) + + +async def test_switch_change_light_state(hass, utcnow): + """Test that we can turn a HomeKit light on and off again.""" + from homekit.model.services import BHSLightBulbService + + helper = await setup_test_component(hass, [BHSLightBulbService()]) + + await hass.services.async_call('light', 'turn_on', { + 'entity_id': 'light.testdevice', + 'brightness': 255, + 'hs_color': [4, 5], + }, blocking=True) + assert helper.characteristics[('lightbulb', 'on')].value == 1 + assert helper.characteristics[('lightbulb', 'brightness')].value == 100 + assert helper.characteristics[('lightbulb', 'hue')].value == 4 + assert helper.characteristics[('lightbulb', 'saturation')].value == 5 + + await hass.services.async_call('light', 'turn_off', { + 'entity_id': 'light.testdevice', + }, blocking=True) + assert helper.characteristics[('lightbulb', 'on')].value == 0 + + +async def test_switch_read_light_state(hass, utcnow): + """Test that we can read the state of a HomeKit light accessory.""" + from homekit.model.services import BHSLightBulbService + + helper = await setup_test_component(hass, [BHSLightBulbService()]) + + # Initial state is that the light is off + state = await helper.poll_and_get_state() + assert state.state == 'off' + + # Simulate that someone switched on the device in the real world not via HA + helper.characteristics[('lightbulb', 'on')].set_value(True) + state = await helper.poll_and_get_state() + assert state.state == 'on' + + # Simulate that device switched off in the real world not via HA + helper.characteristics[('lightbulb', 'on')].set_value(False) + state = await helper.poll_and_get_state() + assert state.state == 'off' diff --git a/tests/components/homekit_controller/test_switch.py b/tests/components/homekit_controller/test_switch.py new file mode 100644 index 00000000000..8915f5858cf --- /dev/null +++ b/tests/components/homekit_controller/test_switch.py @@ -0,0 +1,49 @@ +"""Basic checks for HomeKitSwitch.""" +from tests.components.homekit_controller.common import ( + setup_test_component) + + +async def test_switch_change_outlet_state(hass, utcnow): + """Test that we can turn a HomeKit outlet on and off again.""" + from homekit.model.services import OutletService + + helper = await setup_test_component(hass, [OutletService()]) + + await hass.services.async_call('switch', 'turn_on', { + 'entity_id': 'switch.testdevice', + }, blocking=True) + assert helper.characteristics[('outlet', 'on')].value == 1 + + await hass.services.async_call('switch', 'turn_off', { + 'entity_id': 'switch.testdevice', + }, blocking=True) + assert helper.characteristics[('outlet', 'on')].value == 0 + + +async def test_switch_read_outlet_state(hass, utcnow): + """Test that we can read the state of a HomeKit outlet accessory.""" + from homekit.model.services import OutletService + + helper = await setup_test_component(hass, [OutletService()]) + + # Initial state is that the switch is off and the outlet isn't in use + switch_1 = await helper.poll_and_get_state() + assert switch_1.state == 'off' + assert switch_1.attributes['outlet_in_use'] is False + + # Simulate that someone switched on the device in the real world not via HA + helper.characteristics[('outlet', 'on')].set_value(True) + switch_1 = await helper.poll_and_get_state() + assert switch_1.state == 'on' + assert switch_1.attributes['outlet_in_use'] is False + + # Simulate that device switched off in the real world not via HA + helper.characteristics[('outlet', 'on')].set_value(False) + switch_1 = await helper.poll_and_get_state() + assert switch_1.state == 'off' + + # Simulate that someone plugged something into the device + helper.characteristics[('outlet', 'outlet-in-use')].value = True + switch_1 = await helper.poll_and_get_state() + assert switch_1.state == 'off' + assert switch_1.attributes['outlet_in_use'] is True From 2aab646be2bcb354f0d8af68f2110461eb9532e9 Mon Sep 17 00:00:00 2001 From: Steven Looman Date: Sun, 27 Jan 2019 13:52:51 +0100 Subject: [PATCH 128/222] Upgrade to async-upnp-client==0.14.3 (#20502) --- homeassistant/components/media_player/dlna_dmr.py | 2 +- homeassistant/components/upnp/__init__.py | 2 +- requirements_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/media_player/dlna_dmr.py b/homeassistant/components/media_player/dlna_dmr.py index 9d271f0db6c..0121f6e98d3 100644 --- a/homeassistant/components/media_player/dlna_dmr.py +++ b/homeassistant/components/media_player/dlna_dmr.py @@ -26,7 +26,7 @@ from homeassistant.helpers.aiohttp_client import async_get_clientsession import homeassistant.helpers.config_validation as cv from homeassistant.util import get_local_ip -REQUIREMENTS = ['async-upnp-client==0.14.2'] +REQUIREMENTS = ['async-upnp-client==0.14.3'] _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/upnp/__init__.py b/homeassistant/components/upnp/__init__.py index 820bce2cc50..494c39c06ba 100644 --- a/homeassistant/components/upnp/__init__.py +++ b/homeassistant/components/upnp/__init__.py @@ -29,7 +29,7 @@ from .const import LOGGER as _LOGGER from .device import Device -REQUIREMENTS = ['async-upnp-client==0.14.2'] +REQUIREMENTS = ['async-upnp-client==0.14.3'] NOTIFICATION_ID = 'upnp_notification' NOTIFICATION_TITLE = 'UPnP/IGD Setup' diff --git a/requirements_all.txt b/requirements_all.txt index 85e9aeab078..1d57bf3b295 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -164,7 +164,7 @@ asterisk_mbox==0.5.0 # homeassistant.components.upnp # homeassistant.components.media_player.dlna_dmr -async-upnp-client==0.14.2 +async-upnp-client==0.14.3 # homeassistant.components.light.avion # avion==0.10 From ae84a91ea83beabfa032d1441295e8d019ebc39c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20H=C3=B8yer=20Iversen?= Date: Sun, 27 Jan 2019 17:39:56 +0100 Subject: [PATCH 129/222] Upgrade tibber library (#20504) * Upgrade tibber library * Upgrade tibber library --- homeassistant/components/tibber/__init__.py | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/tibber/__init__.py b/homeassistant/components/tibber/__init__.py index 91e6ae4bfa8..c2d1daa584c 100644 --- a/homeassistant/components/tibber/__init__.py +++ b/homeassistant/components/tibber/__init__.py @@ -16,7 +16,7 @@ from homeassistant.const import (EVENT_HOMEASSISTANT_STOP, CONF_ACCESS_TOKEN, from homeassistant.helpers import discovery from homeassistant.helpers.aiohttp_client import async_get_clientsession -REQUIREMENTS = ['pyTibber==0.9.3'] +REQUIREMENTS = ['pyTibber==0.9.4'] DOMAIN = 'tibber' diff --git a/requirements_all.txt b/requirements_all.txt index 1d57bf3b295..b31ec2199d3 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -888,7 +888,7 @@ pyRFXtrx==0.23 pySwitchmate==0.4.4 # homeassistant.components.tibber -pyTibber==0.9.3 +pyTibber==0.9.4 # homeassistant.components.switch.dlink pyW215==0.6.0 From 1f93984fd5861493cff8fd5ee9a5c244a7713728 Mon Sep 17 00:00:00 2001 From: emontnemery Date: Sun, 27 Jan 2019 17:42:45 +0100 Subject: [PATCH 130/222] Update device registry of MQTT Lock (#20501) --- homeassistant/components/mqtt/lock.py | 11 ++++--- tests/components/mqtt/test_lock.py | 47 +++++++++++++++++++++++++++ 2 files changed, 53 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/mqtt/lock.py b/homeassistant/components/mqtt/lock.py index 5f1c169c349..e52d2c60bc7 100644 --- a/homeassistant/components/mqtt/lock.py +++ b/homeassistant/components/mqtt/lock.py @@ -61,7 +61,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): try: discovery_hash = discovery_payload[ATTR_DISCOVERY_HASH] config = PLATFORM_SCHEMA(discovery_payload) - await _async_setup_entity(config, async_add_entities, + await _async_setup_entity(config, async_add_entities, config_entry, discovery_hash) except Exception: if discovery_hash: @@ -73,17 +73,17 @@ async def async_setup_entry(hass, config_entry, async_add_entities): async_discover) -async def _async_setup_entity(config, async_add_entities, +async def _async_setup_entity(config, async_add_entities, config_entry=None, discovery_hash=None): """Set up the MQTT Lock platform.""" - async_add_entities([MqttLock(config, discovery_hash)]) + async_add_entities([MqttLock(config, config_entry, discovery_hash)]) class MqttLock(MqttAttributes, MqttAvailability, MqttDiscoveryUpdate, MqttEntityDeviceInfo, LockDevice): """Representation of a lock that can be toggled using MQTT.""" - def __init__(self, config, discovery_hash): + def __init__(self, config, config_entry, discovery_hash): """Initialize the lock.""" self._config = config self._unique_id = config.get(CONF_UNIQUE_ID) @@ -97,7 +97,7 @@ class MqttLock(MqttAttributes, MqttAvailability, MqttDiscoveryUpdate, MqttAvailability.__init__(self, config) MqttDiscoveryUpdate.__init__(self, discovery_hash, self.discovery_update) - MqttEntityDeviceInfo.__init__(self, device_config) + MqttEntityDeviceInfo.__init__(self, device_config, config_entry) async def async_added_to_hass(self): """Subscribe to MQTT events.""" @@ -110,6 +110,7 @@ class MqttLock(MqttAttributes, MqttAvailability, MqttDiscoveryUpdate, self._config = config await self.attributes_discovery_update(config) await self.availability_discovery_update(config) + await self.device_info_discovery_update(config) await self._subscribe_topics() self.async_schedule_update_ha_state() diff --git a/tests/components/mqtt/test_lock.py b/tests/components/mqtt/test_lock.py index 33802bc0795..3dd21535694 100644 --- a/tests/components/mqtt/test_lock.py +++ b/tests/components/mqtt/test_lock.py @@ -389,6 +389,53 @@ async def test_entity_device_info_with_identifier(hass, mqtt_mock): assert device.sw_version == '0.1-beta' +async def test_entity_device_info_update(hass, mqtt_mock): + """Test device registry update.""" + entry = MockConfigEntry(domain=mqtt.DOMAIN) + entry.add_to_hass(hass) + await async_start(hass, 'homeassistant', {}, entry) + registry = await hass.helpers.device_registry.async_get_registry() + + config = { + 'platform': 'mqtt', + 'name': 'Test 1', + 'state_topic': 'test-topic', + 'command_topic': 'test-command-topic', + 'device': { + 'identifiers': ['helloworld'], + 'connections': [ + ["mac", "02:5b:26:a8:dc:12"], + ], + 'manufacturer': 'Whatever', + 'name': 'Beer', + 'model': 'Glass', + 'sw_version': '0.1-beta', + }, + 'unique_id': 'veryunique' + } + + data = json.dumps(config) + async_fire_mqtt_message(hass, 'homeassistant/lock/bla/config', + data) + await hass.async_block_till_done() + await hass.async_block_till_done() + + device = registry.async_get_device({('mqtt', 'helloworld')}, set()) + assert device is not None + assert device.name == 'Beer' + + config['device']['name'] = 'Milk' + data = json.dumps(config) + async_fire_mqtt_message(hass, 'homeassistant/lock/bla/config', + data) + await hass.async_block_till_done() + await hass.async_block_till_done() + + device = registry.async_get_device({('mqtt', 'helloworld')}, set()) + assert device is not None + assert device.name == 'Milk' + + async def test_entity_id_update(hass, mqtt_mock): """Test MQTT subscriptions are managed when entity_id is updated.""" registry = mock_registry(hass, {}) From 8804f55fcc10d7cc878376ef1b4281e284e23daa Mon Sep 17 00:00:00 2001 From: emontnemery Date: Sun, 27 Jan 2019 17:43:16 +0100 Subject: [PATCH 131/222] Update device registry of MQTT Vacuum (#20500) --- homeassistant/components/mqtt/vacuum.py | 11 +++--- tests/components/mqtt/test_vacuum.py | 47 +++++++++++++++++++++++++ 2 files changed, 53 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/mqtt/vacuum.py b/homeassistant/components/mqtt/vacuum.py index baa43411411..fedf33c5a2a 100644 --- a/homeassistant/components/mqtt/vacuum.py +++ b/homeassistant/components/mqtt/vacuum.py @@ -163,17 +163,17 @@ async def async_setup_entry(hass, config_entry, async_add_entities): async def async_discover(discovery_payload): """Discover and add a MQTT vacuum.""" config = PLATFORM_SCHEMA(discovery_payload) - await _async_setup_entity(config, async_add_entities, + await _async_setup_entity(config, async_add_entities, config_entry, discovery_payload[ATTR_DISCOVERY_HASH]) async_dispatcher_connect( hass, MQTT_DISCOVERY_NEW.format(DOMAIN, 'mqtt'), async_discover) -async def _async_setup_entity(config, async_add_entities, +async def _async_setup_entity(config, async_add_entities, config_entry, discovery_hash=None): """Set up the MQTT vacuum.""" - async_add_entities([MqttVacuum(config, discovery_hash)]) + async_add_entities([MqttVacuum(config, config_entry, discovery_hash)]) # pylint: disable=too-many-ancestors @@ -181,7 +181,7 @@ class MqttVacuum(MqttAttributes, MqttAvailability, MqttDiscoveryUpdate, MqttEntityDeviceInfo, VacuumDevice): """Representation of a MQTT-controlled vacuum.""" - def __init__(self, config, discovery_info): + def __init__(self, config, config_entry, discovery_info): """Initialize the vacuum.""" self._cleaning = False self._charging = False @@ -203,7 +203,7 @@ class MqttVacuum(MqttAttributes, MqttAvailability, MqttDiscoveryUpdate, MqttAvailability.__init__(self, config) MqttDiscoveryUpdate.__init__(self, discovery_info, self.discovery_update) - MqttEntityDeviceInfo.__init__(self, device_config) + MqttEntityDeviceInfo.__init__(self, device_config, config_entry) def _setup_from_config(self, config): self._name = config.get(CONF_NAME) @@ -257,6 +257,7 @@ class MqttVacuum(MqttAttributes, MqttAvailability, MqttDiscoveryUpdate, self._setup_from_config(config) await self.attributes_discovery_update(config) await self.availability_discovery_update(config) + await self.device_info_discovery_update(config) await self._subscribe_topics() self.async_schedule_update_ha_state() diff --git a/tests/components/mqtt/test_vacuum.py b/tests/components/mqtt/test_vacuum.py index 99a7b3579f3..04a82bb20e1 100644 --- a/tests/components/mqtt/test_vacuum.py +++ b/tests/components/mqtt/test_vacuum.py @@ -494,3 +494,50 @@ async def test_entity_device_info_with_identifier(hass, mock_publish): assert device.name == 'Beer' assert device.model == 'Glass' assert device.sw_version == '0.1-beta' + + +async def test_entity_device_info_update(hass, mqtt_mock): + """Test device registry update.""" + entry = MockConfigEntry(domain=mqtt.DOMAIN) + entry.add_to_hass(hass) + await async_start(hass, 'homeassistant', {}, entry) + registry = await hass.helpers.device_registry.async_get_registry() + + config = { + 'platform': 'mqtt', + 'name': 'Test 1', + 'state_topic': 'test-topic', + 'command_topic': 'test-command-topic', + 'device': { + 'identifiers': ['helloworld'], + 'connections': [ + ["mac", "02:5b:26:a8:dc:12"], + ], + 'manufacturer': 'Whatever', + 'name': 'Beer', + 'model': 'Glass', + 'sw_version': '0.1-beta', + }, + 'unique_id': 'veryunique' + } + + data = json.dumps(config) + async_fire_mqtt_message(hass, 'homeassistant/vacuum/bla/config', + data) + await hass.async_block_till_done() + await hass.async_block_till_done() + + device = registry.async_get_device({('mqtt', 'helloworld')}, set()) + assert device is not None + assert device.name == 'Beer' + + config['device']['name'] = 'Milk' + data = json.dumps(config) + async_fire_mqtt_message(hass, 'homeassistant/vacuum/bla/config', + data) + await hass.async_block_till_done() + await hass.async_block_till_done() + + device = registry.async_get_device({('mqtt', 'helloworld')}, set()) + assert device is not None + assert device.name == 'Milk' From 648adcc708523aa7264587a51c149840f9c2ff5b Mon Sep 17 00:00:00 2001 From: emontnemery Date: Sun, 27 Jan 2019 18:54:52 +0100 Subject: [PATCH 132/222] Small cleanup of MQTT platforms (#20503) * Move CONF_UNIQUE_ID to init * Sort imports * Update ordering --- homeassistant/components/mqtt/__init__.py | 9 ++- .../components/mqtt/alarm_control_panel.py | 19 +++-- .../components/mqtt/binary_sensor.py | 20 ++--- homeassistant/components/mqtt/camera.py | 14 ++-- homeassistant/components/mqtt/climate.py | 34 ++++----- homeassistant/components/mqtt/config_flow.py | 2 +- homeassistant/components/mqtt/cover.py | 73 +++++++++---------- homeassistant/components/mqtt/discovery.py | 2 +- homeassistant/components/mqtt/fan.py | 28 +++---- .../components/mqtt/light/schema_basic.py | 3 +- .../components/mqtt/light/schema_json.py | 3 +- .../components/mqtt/light/schema_template.py | 5 +- homeassistant/components/mqtt/lock.py | 15 ++-- homeassistant/components/mqtt/sensor.py | 23 +++--- homeassistant/components/mqtt/switch.py | 15 ++-- homeassistant/components/mqtt/vacuum.py | 12 ++- .../mqtt/test_alarm_control_panel.py | 16 ++-- tests/components/mqtt/test_binary_sensor.py | 18 ++--- tests/components/mqtt/test_camera.py | 3 +- tests/components/mqtt/test_climate.py | 21 +++--- tests/components/mqtt/test_config_flow.py | 2 +- tests/components/mqtt/test_cover.py | 14 ++-- tests/components/mqtt/test_discovery.py | 8 +- tests/components/mqtt/test_fan.py | 7 +- tests/components/mqtt/test_init.py | 8 +- tests/components/mqtt/test_light.py | 10 +-- tests/components/mqtt/test_light_json.py | 12 +-- tests/components/mqtt/test_light_template.py | 10 +-- tests/components/mqtt/test_lock.py | 8 +- tests/components/mqtt/test_sensor.py | 15 ++-- tests/components/mqtt/test_server.py | 4 +- tests/components/mqtt/test_subscription.py | 2 +- tests/components/mqtt/test_switch.py | 19 ++--- tests/components/mqtt/test_vacuum.py | 18 ++--- 34 files changed, 225 insertions(+), 247 deletions(-) diff --git a/homeassistant/components/mqtt/__init__.py b/homeassistant/components/mqtt/__init__.py index 8868656eb79..8852ae76e08 100644 --- a/homeassistant/components/mqtt/__init__.py +++ b/homeassistant/components/mqtt/__init__.py @@ -21,12 +21,12 @@ import voluptuous as vol from homeassistant import config_entries from homeassistant.const import ( - CONF_DEVICE, CONF_PASSWORD, CONF_PAYLOAD, CONF_PORT, CONF_PROTOCOL, - CONF_USERNAME, CONF_VALUE_TEMPLATE, EVENT_HOMEASSISTANT_STOP, CONF_NAME) + CONF_DEVICE, CONF_NAME, CONF_PASSWORD, CONF_PAYLOAD, CONF_PORT, + CONF_PROTOCOL, CONF_USERNAME, CONF_VALUE_TEMPLATE, + EVENT_HOMEASSISTANT_STOP) from homeassistant.core import Event, ServiceCall, callback from homeassistant.exceptions import HomeAssistantError -from homeassistant.helpers import config_validation as cv -from homeassistant.helpers import template +from homeassistant.helpers import config_validation as cv, template from homeassistant.helpers.entity import Entity from homeassistant.helpers.typing import ( ConfigType, HomeAssistantType, ServiceDataType) @@ -76,6 +76,7 @@ CONF_JSON_ATTRS_TOPIC = 'json_attributes_topic' CONF_QOS = 'qos' CONF_RETAIN = 'retain' +CONF_UNIQUE_ID = 'unique_id' CONF_IDENTIFIERS = 'identifiers' CONF_CONNECTIONS = 'connections' CONF_MANUFACTURER = 'manufacturer' diff --git a/homeassistant/components/mqtt/alarm_control_panel.py b/homeassistant/components/mqtt/alarm_control_panel.py index 024e926055f..6bb864f2cdb 100644 --- a/homeassistant/components/mqtt/alarm_control_panel.py +++ b/homeassistant/components/mqtt/alarm_control_panel.py @@ -9,29 +9,28 @@ import re import voluptuous as vol -from homeassistant.core import callback -import homeassistant.components.alarm_control_panel as alarm from homeassistant.components import mqtt +import homeassistant.components.alarm_control_panel as alarm +from homeassistant.components.mqtt import ( + ATTR_DISCOVERY_HASH, CONF_COMMAND_TOPIC, CONF_QOS, CONF_RETAIN, + CONF_STATE_TOPIC, CONF_UNIQUE_ID, MqttAttributes, MqttAvailability, + MqttDiscoveryUpdate, MqttEntityDeviceInfo, subscription) +from homeassistant.components.mqtt.discovery import ( + MQTT_DISCOVERY_NEW, clear_discovery_hash) from homeassistant.const import ( CONF_CODE, CONF_DEVICE, CONF_NAME, STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME, STATE_ALARM_DISARMED, STATE_ALARM_PENDING, STATE_ALARM_TRIGGERED) -from homeassistant.components.mqtt import ( - ATTR_DISCOVERY_HASH, CONF_COMMAND_TOPIC, CONF_QOS, CONF_RETAIN, - CONF_STATE_TOPIC, MqttAttributes, MqttAvailability, MqttDiscoveryUpdate, - MqttEntityDeviceInfo, subscription) -from homeassistant.components.mqtt.discovery import ( - MQTT_DISCOVERY_NEW, clear_discovery_hash) +from homeassistant.core import callback import homeassistant.helpers.config_validation as cv from homeassistant.helpers.dispatcher import async_dispatcher_connect -from homeassistant.helpers.typing import HomeAssistantType, ConfigType +from homeassistant.helpers.typing import ConfigType, HomeAssistantType _LOGGER = logging.getLogger(__name__) CONF_PAYLOAD_DISARM = 'payload_disarm' CONF_PAYLOAD_ARM_HOME = 'payload_arm_home' CONF_PAYLOAD_ARM_AWAY = 'payload_arm_away' -CONF_UNIQUE_ID = 'unique_id' DEFAULT_ARM_AWAY = 'ARM_AWAY' DEFAULT_ARM_HOME = 'ARM_HOME' diff --git a/homeassistant/components/mqtt/binary_sensor.py b/homeassistant/components/mqtt/binary_sensor.py index f066116892e..297d4436c7c 100644 --- a/homeassistant/components/mqtt/binary_sensor.py +++ b/homeassistant/components/mqtt/binary_sensor.py @@ -8,28 +8,28 @@ import logging import voluptuous as vol -from homeassistant.core import callback -from homeassistant.components import mqtt, binary_sensor +from homeassistant.components import binary_sensor, mqtt from homeassistant.components.binary_sensor import ( - BinarySensorDevice, DEVICE_CLASSES_SCHEMA) -from homeassistant.const import ( - CONF_FORCE_UPDATE, CONF_NAME, CONF_VALUE_TEMPLATE, CONF_PAYLOAD_ON, - CONF_PAYLOAD_OFF, CONF_DEVICE_CLASS, CONF_DEVICE) + DEVICE_CLASSES_SCHEMA, BinarySensorDevice) from homeassistant.components.mqtt import ( - ATTR_DISCOVERY_HASH, CONF_QOS, CONF_STATE_TOPIC, MqttAttributes, - MqttAvailability, MqttDiscoveryUpdate, MqttEntityDeviceInfo, subscription) + ATTR_DISCOVERY_HASH, CONF_QOS, CONF_STATE_TOPIC, CONF_UNIQUE_ID, + MqttAttributes, MqttAvailability, MqttDiscoveryUpdate, + MqttEntityDeviceInfo, subscription) from homeassistant.components.mqtt.discovery import ( MQTT_DISCOVERY_NEW, clear_discovery_hash) +from homeassistant.const import ( + CONF_DEVICE, CONF_DEVICE_CLASS, CONF_FORCE_UPDATE, CONF_NAME, + CONF_PAYLOAD_OFF, CONF_PAYLOAD_ON, CONF_VALUE_TEMPLATE) +from homeassistant.core import callback import homeassistant.helpers.config_validation as cv from homeassistant.helpers.dispatcher import async_dispatcher_connect import homeassistant.helpers.event as evt -from homeassistant.helpers.typing import HomeAssistantType, ConfigType +from homeassistant.helpers.typing import ConfigType, HomeAssistantType _LOGGER = logging.getLogger(__name__) DEFAULT_NAME = 'MQTT Binary sensor' CONF_OFF_DELAY = 'off_delay' -CONF_UNIQUE_ID = 'unique_id' DEFAULT_PAYLOAD_OFF = 'OFF' DEFAULT_PAYLOAD_ON = 'ON' DEFAULT_FORCE_UPDATE = False diff --git a/homeassistant/components/mqtt/camera.py b/homeassistant/components/mqtt/camera.py index 7bda891e921..50b998f6e9c 100644 --- a/homeassistant/components/mqtt/camera.py +++ b/homeassistant/components/mqtt/camera.py @@ -10,19 +10,19 @@ import logging import voluptuous as vol -from homeassistant.helpers.dispatcher import async_dispatcher_connect -from homeassistant.helpers.typing import HomeAssistantType, ConfigType -from homeassistant.core import callback -from homeassistant.const import CONF_NAME -from homeassistant.components import mqtt, camera -from homeassistant.components.camera import Camera, PLATFORM_SCHEMA +from homeassistant.components import camera, mqtt +from homeassistant.components.camera import PLATFORM_SCHEMA, Camera +from homeassistant.components.mqtt import CONF_UNIQUE_ID from homeassistant.components.mqtt.discovery import MQTT_DISCOVERY_NEW +from homeassistant.const import CONF_NAME +from homeassistant.core import callback from homeassistant.helpers import config_validation as cv +from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.helpers.typing import ConfigType, HomeAssistantType _LOGGER = logging.getLogger(__name__) CONF_TOPIC = 'topic' -CONF_UNIQUE_ID = 'unique_id' DEFAULT_NAME = 'MQTT Camera' DEPENDENCIES = ['mqtt'] diff --git a/homeassistant/components/mqtt/climate.py b/homeassistant/components/mqtt/climate.py index 9db96fdff94..db46f11b88e 100644 --- a/homeassistant/components/mqtt/climate.py +++ b/homeassistant/components/mqtt/climate.py @@ -8,29 +8,27 @@ import logging import voluptuous as vol -from homeassistant.core import callback -from homeassistant.components import mqtt, climate - +from homeassistant.components import climate, mqtt from homeassistant.components.climate import ( - STATE_HEAT, STATE_COOL, STATE_DRY, STATE_FAN_ONLY, ClimateDevice, - PLATFORM_SCHEMA as CLIMATE_PLATFORM_SCHEMA, STATE_AUTO, - ATTR_OPERATION_MODE, SUPPORT_TARGET_TEMPERATURE, SUPPORT_OPERATION_MODE, - SUPPORT_SWING_MODE, SUPPORT_FAN_MODE, SUPPORT_AWAY_MODE, SUPPORT_HOLD_MODE, - SUPPORT_AUX_HEAT, DEFAULT_MIN_TEMP, DEFAULT_MAX_TEMP) -from homeassistant.const import ( - ATTR_TEMPERATURE, CONF_DEVICE, CONF_NAME, CONF_VALUE_TEMPLATE, STATE_ON, - STATE_OFF) + ATTR_OPERATION_MODE, DEFAULT_MAX_TEMP, DEFAULT_MIN_TEMP, + PLATFORM_SCHEMA as CLIMATE_PLATFORM_SCHEMA, STATE_AUTO, STATE_COOL, + STATE_DRY, STATE_FAN_ONLY, STATE_HEAT, SUPPORT_AUX_HEAT, SUPPORT_AWAY_MODE, + SUPPORT_FAN_MODE, SUPPORT_HOLD_MODE, SUPPORT_OPERATION_MODE, + SUPPORT_SWING_MODE, SUPPORT_TARGET_TEMPERATURE, ClimateDevice) +from homeassistant.components.fan import SPEED_HIGH, SPEED_LOW, SPEED_MEDIUM from homeassistant.components.mqtt import ( - ATTR_DISCOVERY_HASH, CONF_QOS, CONF_RETAIN, MQTT_BASE_PLATFORM_SCHEMA, - MqttAttributes, MqttAvailability, MqttDiscoveryUpdate, - MqttEntityDeviceInfo, subscription) + ATTR_DISCOVERY_HASH, CONF_QOS, CONF_RETAIN, CONF_UNIQUE_ID, + MQTT_BASE_PLATFORM_SCHEMA, MqttAttributes, MqttAvailability, + MqttDiscoveryUpdate, MqttEntityDeviceInfo, subscription) from homeassistant.components.mqtt.discovery import ( MQTT_DISCOVERY_NEW, clear_discovery_hash) +from homeassistant.const import ( + ATTR_TEMPERATURE, CONF_DEVICE, CONF_NAME, CONF_VALUE_TEMPLATE, STATE_OFF, + STATE_ON) +from homeassistant.core import callback import homeassistant.helpers.config_validation as cv from homeassistant.helpers.dispatcher import async_dispatcher_connect -from homeassistant.helpers.typing import HomeAssistantType, ConfigType -from homeassistant.components.fan import (SPEED_LOW, SPEED_MEDIUM, - SPEED_HIGH) +from homeassistant.helpers.typing import ConfigType, HomeAssistantType _LOGGER = logging.getLogger(__name__) @@ -79,8 +77,6 @@ CONF_MIN_TEMP = 'min_temp' CONF_MAX_TEMP = 'max_temp' CONF_TEMP_STEP = 'temp_step' -CONF_UNIQUE_ID = 'unique_id' - TEMPLATE_KEYS = ( CONF_POWER_STATE_TEMPLATE, CONF_MODE_STATE_TEMPLATE, diff --git a/homeassistant/components/mqtt/config_flow.py b/homeassistant/components/mqtt/config_flow.py index aee825d06de..54f00d70658 100644 --- a/homeassistant/components/mqtt/config_flow.py +++ b/homeassistant/components/mqtt/config_flow.py @@ -6,7 +6,7 @@ import voluptuous as vol from homeassistant import config_entries from homeassistant.const import ( - CONF_PASSWORD, CONF_PORT, CONF_USERNAME, CONF_PROTOCOL, CONF_HOST) + CONF_HOST, CONF_PASSWORD, CONF_PORT, CONF_PROTOCOL, CONF_USERNAME) from .const import CONF_BROKER, CONF_DISCOVERY, DEFAULT_DISCOVERY diff --git a/homeassistant/components/mqtt/cover.py b/homeassistant/components/mqtt/cover.py index 477169a37de..26d10e85a34 100644 --- a/homeassistant/components/mqtt/cover.py +++ b/homeassistant/components/mqtt/cover.py @@ -8,70 +8,67 @@ import logging import voluptuous as vol -from homeassistant.core import callback -from homeassistant.components import mqtt, cover +from homeassistant.components import cover, mqtt from homeassistant.components.cover import ( - CoverDevice, ATTR_TILT_POSITION, SUPPORT_OPEN_TILT, - SUPPORT_CLOSE_TILT, SUPPORT_STOP_TILT, SUPPORT_SET_TILT_POSITION, - SUPPORT_OPEN, SUPPORT_CLOSE, SUPPORT_STOP, SUPPORT_SET_POSITION, - ATTR_POSITION) -from homeassistant.exceptions import TemplateError -from homeassistant.const import ( - CONF_NAME, CONF_VALUE_TEMPLATE, CONF_OPTIMISTIC, STATE_OPEN, - STATE_CLOSED, STATE_UNKNOWN, CONF_DEVICE) + ATTR_POSITION, ATTR_TILT_POSITION, SUPPORT_CLOSE, SUPPORT_CLOSE_TILT, + SUPPORT_OPEN, SUPPORT_OPEN_TILT, SUPPORT_SET_POSITION, + SUPPORT_SET_TILT_POSITION, SUPPORT_STOP, SUPPORT_STOP_TILT, CoverDevice) from homeassistant.components.mqtt import ( ATTR_DISCOVERY_HASH, CONF_COMMAND_TOPIC, CONF_QOS, CONF_RETAIN, - CONF_STATE_TOPIC, MqttAttributes, MqttAvailability, MqttDiscoveryUpdate, - MqttEntityDeviceInfo, subscription) + CONF_STATE_TOPIC, CONF_UNIQUE_ID, MqttAttributes, MqttAvailability, + MqttDiscoveryUpdate, MqttEntityDeviceInfo, subscription) from homeassistant.components.mqtt.discovery import ( MQTT_DISCOVERY_NEW, clear_discovery_hash) +from homeassistant.const import ( + CONF_DEVICE, CONF_NAME, CONF_OPTIMISTIC, CONF_VALUE_TEMPLATE, STATE_CLOSED, + STATE_OPEN, STATE_UNKNOWN) +from homeassistant.core import callback +from homeassistant.exceptions import TemplateError import homeassistant.helpers.config_validation as cv from homeassistant.helpers.dispatcher import async_dispatcher_connect -from homeassistant.helpers.typing import HomeAssistantType, ConfigType +from homeassistant.helpers.typing import ConfigType, HomeAssistantType _LOGGER = logging.getLogger(__name__) DEPENDENCIES = ['mqtt'] CONF_GET_POSITION_TOPIC = 'position_topic' - +CONF_SET_POSITION_TEMPLATE = 'set_position_template' +CONF_SET_POSITION_TOPIC = 'set_position_topic' CONF_TILT_COMMAND_TOPIC = 'tilt_command_topic' CONF_TILT_STATUS_TOPIC = 'tilt_status_topic' -CONF_SET_POSITION_TOPIC = 'set_position_topic' -CONF_SET_POSITION_TEMPLATE = 'set_position_template' -CONF_PAYLOAD_OPEN = 'payload_open' CONF_PAYLOAD_CLOSE = 'payload_close' +CONF_PAYLOAD_OPEN = 'payload_open' CONF_PAYLOAD_STOP = 'payload_stop' -CONF_STATE_OPEN = 'state_open' -CONF_STATE_CLOSED = 'state_closed' -CONF_POSITION_OPEN = 'position_open' CONF_POSITION_CLOSED = 'position_closed' +CONF_POSITION_OPEN = 'position_open' +CONF_STATE_CLOSED = 'state_closed' +CONF_STATE_OPEN = 'state_open' CONF_TILT_CLOSED_POSITION = 'tilt_closed_value' -CONF_TILT_OPEN_POSITION = 'tilt_opened_value' -CONF_TILT_MIN = 'tilt_min' -CONF_TILT_MAX = 'tilt_max' -CONF_TILT_STATE_OPTIMISTIC = 'tilt_optimistic' CONF_TILT_INVERT_STATE = 'tilt_invert_state' -CONF_UNIQUE_ID = 'unique_id' +CONF_TILT_MAX = 'tilt_max' +CONF_TILT_MIN = 'tilt_min' +CONF_TILT_OPEN_POSITION = 'tilt_opened_value' +CONF_TILT_STATE_OPTIMISTIC = 'tilt_optimistic' -TILT_PAYLOAD = "tilt" -COVER_PAYLOAD = "cover" +TILT_PAYLOAD = 'tilt' +COVER_PAYLOAD = 'cover' DEFAULT_NAME = 'MQTT Cover' -DEFAULT_PAYLOAD_OPEN = 'OPEN' -DEFAULT_PAYLOAD_CLOSE = 'CLOSE' -DEFAULT_PAYLOAD_STOP = 'STOP' -DEFAULT_POSITION_OPEN = 100 -DEFAULT_POSITION_CLOSED = 0 DEFAULT_OPTIMISTIC = False +DEFAULT_PAYLOAD_CLOSE = 'CLOSE' +DEFAULT_PAYLOAD_OPEN = 'OPEN' +DEFAULT_PAYLOAD_STOP = 'STOP' +DEFAULT_POSITION_CLOSED = 0 +DEFAULT_POSITION_OPEN = 100 DEFAULT_RETAIN = False DEFAULT_TILT_CLOSED_POSITION = 0 -DEFAULT_TILT_OPEN_POSITION = 100 -DEFAULT_TILT_MIN = 0 -DEFAULT_TILT_MAX = 100 -DEFAULT_TILT_OPTIMISTIC = False DEFAULT_TILT_INVERT_STATE = False +DEFAULT_TILT_MAX = 100 +DEFAULT_TILT_MIN = 0 +DEFAULT_TILT_OPEN_POSITION = 100 +DEFAULT_TILT_OPTIMISTIC = False OPEN_CLOSE_FEATURES = (SUPPORT_OPEN | SUPPORT_CLOSE | SUPPORT_STOP) TILT_FEATURES = (SUPPORT_OPEN_TILT | SUPPORT_CLOSE_TILT | SUPPORT_STOP_TILT | @@ -180,8 +177,8 @@ class MqttCover(MqttAttributes, MqttAvailability, MqttDiscoveryUpdate, MqttAttributes.__init__(self, config) MqttAvailability.__init__(self, config) - MqttDiscoveryUpdate.__init__(self, discovery_hash, - self.discovery_update) + MqttDiscoveryUpdate.__init__( + self, discovery_hash, self.discovery_update) MqttEntityDeviceInfo.__init__(self, device_config, config_entry) async def async_added_to_hass(self): diff --git a/homeassistant/components/mqtt/discovery.py b/homeassistant/components/mqtt/discovery.py index fc8b9091763..693a1a4d41d 100644 --- a/homeassistant/components/mqtt/discovery.py +++ b/homeassistant/components/mqtt/discovery.py @@ -10,7 +10,7 @@ import logging import re from homeassistant.components import mqtt -from homeassistant.components.mqtt import CONF_STATE_TOPIC, ATTR_DISCOVERY_HASH +from homeassistant.components.mqtt import ATTR_DISCOVERY_HASH, CONF_STATE_TOPIC from homeassistant.const import CONF_PLATFORM from homeassistant.helpers.discovery import async_load_platform from homeassistant.helpers.dispatcher import async_dispatcher_send diff --git a/homeassistant/components/mqtt/fan.py b/homeassistant/components/mqtt/fan.py index 47bfa3620e9..1d1258993de 100644 --- a/homeassistant/components/mqtt/fan.py +++ b/homeassistant/components/mqtt/fan.py @@ -8,24 +8,23 @@ import logging import voluptuous as vol -from homeassistant.core import callback from homeassistant.components import fan, mqtt -from homeassistant.const import ( - CONF_NAME, CONF_OPTIMISTIC, CONF_STATE, STATE_ON, STATE_OFF, - CONF_PAYLOAD_OFF, CONF_PAYLOAD_ON, CONF_DEVICE) +from homeassistant.components.fan import ( + ATTR_SPEED, SPEED_HIGH, SPEED_LOW, SPEED_MEDIUM, SPEED_OFF, + SUPPORT_OSCILLATE, SUPPORT_SET_SPEED, FanEntity) from homeassistant.components.mqtt import ( ATTR_DISCOVERY_HASH, CONF_COMMAND_TOPIC, CONF_QOS, CONF_RETAIN, - CONF_STATE_TOPIC, MqttAttributes, MqttAvailability, MqttDiscoveryUpdate, - MqttEntityDeviceInfo, subscription) -import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.dispatcher import async_dispatcher_connect -from homeassistant.helpers.typing import HomeAssistantType, ConfigType -from homeassistant.components.fan import (SPEED_LOW, SPEED_MEDIUM, - SPEED_HIGH, FanEntity, - SUPPORT_SET_SPEED, SUPPORT_OSCILLATE, - SPEED_OFF, ATTR_SPEED) + CONF_STATE_TOPIC, CONF_UNIQUE_ID, MqttAttributes, MqttAvailability, + MqttDiscoveryUpdate, MqttEntityDeviceInfo, subscription) from homeassistant.components.mqtt.discovery import ( MQTT_DISCOVERY_NEW, clear_discovery_hash) +from homeassistant.const import ( + CONF_DEVICE, CONF_NAME, CONF_OPTIMISTIC, CONF_PAYLOAD_OFF, CONF_PAYLOAD_ON, + CONF_STATE, STATE_OFF, STATE_ON) +from homeassistant.core import callback +import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.helpers.typing import ConfigType, HomeAssistantType _LOGGER = logging.getLogger(__name__) @@ -44,7 +43,6 @@ CONF_PAYLOAD_LOW_SPEED = 'payload_low_speed' CONF_PAYLOAD_MEDIUM_SPEED = 'payload_medium_speed' CONF_PAYLOAD_HIGH_SPEED = 'payload_high_speed' CONF_SPEED_LIST = 'speeds' -CONF_UNIQUE_ID = 'unique_id' DEFAULT_NAME = 'MQTT Fan' DEFAULT_PAYLOAD_ON = 'ON' @@ -202,8 +200,6 @@ class MqttFan(MqttAttributes, MqttAvailability, MqttDiscoveryUpdate, self._supported_features |= (self._topic[CONF_SPEED_STATE_TOPIC] is not None and SUPPORT_SET_SPEED) - self._unique_id = config.get(CONF_UNIQUE_ID) - async def _subscribe_topics(self): """(Re)Subscribe to topics.""" topics = {} diff --git a/homeassistant/components/mqtt/light/schema_basic.py b/homeassistant/components/mqtt/light/schema_basic.py index d9f676c8b38..c4bf41fa673 100644 --- a/homeassistant/components/mqtt/light/schema_basic.py +++ b/homeassistant/components/mqtt/light/schema_basic.py @@ -20,7 +20,7 @@ from homeassistant.const import ( CONF_RGB, CONF_STATE, CONF_VALUE_TEMPLATE, CONF_WHITE_VALUE, CONF_XY) from homeassistant.components.mqtt import ( CONF_COMMAND_TOPIC, CONF_QOS, CONF_RETAIN, CONF_STATE_TOPIC, - MqttAttributes, MqttAvailability, MqttDiscoveryUpdate, + CONF_UNIQUE_ID, MqttAttributes, MqttAvailability, MqttDiscoveryUpdate, MqttEntityDeviceInfo, subscription) from homeassistant.helpers.restore_state import RestoreEntity import homeassistant.helpers.config_validation as cv @@ -58,7 +58,6 @@ CONF_WHITE_VALUE_SCALE = 'white_value_scale' CONF_WHITE_VALUE_STATE_TOPIC = 'white_value_state_topic' CONF_WHITE_VALUE_TEMPLATE = 'white_value_template' CONF_ON_COMMAND_TYPE = 'on_command_type' -CONF_UNIQUE_ID = 'unique_id' DEFAULT_BRIGHTNESS_SCALE = 255 DEFAULT_NAME = 'MQTT Light' diff --git a/homeassistant/components/mqtt/light/schema_json.py b/homeassistant/components/mqtt/light/schema_json.py index fcf31f097cc..e1372b9a80f 100644 --- a/homeassistant/components/mqtt/light/schema_json.py +++ b/homeassistant/components/mqtt/light/schema_json.py @@ -17,7 +17,7 @@ from homeassistant.components.light import ( SUPPORT_FLASH, SUPPORT_TRANSITION, SUPPORT_WHITE_VALUE, Light) from homeassistant.components.mqtt import ( CONF_COMMAND_TOPIC, CONF_QOS, CONF_RETAIN, CONF_STATE_TOPIC, - MqttAttributes, MqttAvailability, MqttDiscoveryUpdate, + CONF_UNIQUE_ID, MqttAttributes, MqttAvailability, MqttDiscoveryUpdate, MqttEntityDeviceInfo, subscription) from homeassistant.const import ( CONF_BRIGHTNESS, CONF_COLOR_TEMP, CONF_DEVICE, CONF_EFFECT, CONF_NAME, @@ -54,7 +54,6 @@ CONF_EFFECT_LIST = 'effect_list' CONF_FLASH_TIME_LONG = 'flash_time_long' CONF_FLASH_TIME_SHORT = 'flash_time_short' CONF_HS = 'hs' -CONF_UNIQUE_ID = 'unique_id' # Stealing some of these from the base MQTT configs. PLATFORM_SCHEMA_JSON = mqtt.MQTT_RW_PLATFORM_SCHEMA.extend({ diff --git a/homeassistant/components/mqtt/light/schema_template.py b/homeassistant/components/mqtt/light/schema_template.py index 09aaa359058..80d773060d7 100644 --- a/homeassistant/components/mqtt/light/schema_template.py +++ b/homeassistant/components/mqtt/light/schema_template.py @@ -17,8 +17,8 @@ from homeassistant.components.light import ( from homeassistant.const import ( CONF_DEVICE, CONF_NAME, CONF_OPTIMISTIC, STATE_ON, STATE_OFF) from homeassistant.components.mqtt import ( - CONF_STATE_TOPIC, CONF_COMMAND_TOPIC, CONF_QOS, CONF_RETAIN, - MqttAttributes, MqttAvailability, MqttDiscoveryUpdate, + CONF_COMMAND_TOPIC, CONF_QOS, CONF_RETAIN, CONF_STATE_TOPIC, + CONF_UNIQUE_ID, MqttAttributes, MqttAvailability, MqttDiscoveryUpdate, MqttEntityDeviceInfo, subscription) import homeassistant.helpers.config_validation as cv import homeassistant.util.color as color_util @@ -44,7 +44,6 @@ CONF_GREEN_TEMPLATE = 'green_template' CONF_RED_TEMPLATE = 'red_template' CONF_STATE_TEMPLATE = 'state_template' CONF_WHITE_VALUE_TEMPLATE = 'white_value_template' -CONF_UNIQUE_ID = 'unique_id' PLATFORM_SCHEMA_TEMPLATE = mqtt.MQTT_RW_PLATFORM_SCHEMA.extend({ vol.Optional(CONF_BLUE_TEMPLATE): cv.template, diff --git a/homeassistant/components/mqtt/lock.py b/homeassistant/components/mqtt/lock.py index e52d2c60bc7..da872fda612 100644 --- a/homeassistant/components/mqtt/lock.py +++ b/homeassistant/components/mqtt/lock.py @@ -8,26 +8,25 @@ import logging import voluptuous as vol -from homeassistant.core import callback +from homeassistant.components import lock, mqtt from homeassistant.components.lock import LockDevice from homeassistant.components.mqtt import ( ATTR_DISCOVERY_HASH, CONF_COMMAND_TOPIC, CONF_QOS, CONF_RETAIN, - CONF_STATE_TOPIC, MqttAttributes, MqttAvailability, MqttDiscoveryUpdate, - MqttEntityDeviceInfo, subscription) -from homeassistant.const import ( - CONF_DEVICE, CONF_NAME, CONF_OPTIMISTIC, CONF_VALUE_TEMPLATE) -from homeassistant.components import mqtt, lock + CONF_STATE_TOPIC, CONF_UNIQUE_ID, MqttAttributes, MqttAvailability, + MqttDiscoveryUpdate, MqttEntityDeviceInfo, subscription) from homeassistant.components.mqtt.discovery import ( MQTT_DISCOVERY_NEW, clear_discovery_hash) +from homeassistant.const import ( + CONF_DEVICE, CONF_NAME, CONF_OPTIMISTIC, CONF_VALUE_TEMPLATE) +from homeassistant.core import callback import homeassistant.helpers.config_validation as cv from homeassistant.helpers.dispatcher import async_dispatcher_connect -from homeassistant.helpers.typing import HomeAssistantType, ConfigType +from homeassistant.helpers.typing import ConfigType, HomeAssistantType _LOGGER = logging.getLogger(__name__) CONF_PAYLOAD_LOCK = 'payload_lock' CONF_PAYLOAD_UNLOCK = 'payload_unlock' -CONF_UNIQUE_ID = 'unique_id' DEFAULT_NAME = 'MQTT Lock' DEFAULT_OPTIMISTIC = False diff --git a/homeassistant/components/mqtt/sensor.py b/homeassistant/components/mqtt/sensor.py index 475aef84c3d..9637caa9053 100644 --- a/homeassistant/components/mqtt/sensor.py +++ b/homeassistant/components/mqtt/sensor.py @@ -4,37 +4,36 @@ Support for MQTT sensors. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/sensor.mqtt/ """ -import logging -import json from datetime import timedelta +import json +import logging from typing import Optional import voluptuous as vol -from homeassistant.core import callback -from homeassistant.components import sensor +from homeassistant.components import mqtt, sensor from homeassistant.components.mqtt import ( - ATTR_DISCOVERY_HASH, CONF_QOS, CONF_STATE_TOPIC, MqttAttributes, - MqttAvailability, MqttDiscoveryUpdate, MqttEntityDeviceInfo, subscription) + ATTR_DISCOVERY_HASH, CONF_QOS, CONF_STATE_TOPIC, CONF_UNIQUE_ID, + MqttAttributes, MqttAvailability, MqttDiscoveryUpdate, + MqttEntityDeviceInfo, subscription) from homeassistant.components.mqtt.discovery import ( MQTT_DISCOVERY_NEW, clear_discovery_hash) from homeassistant.components.sensor import DEVICE_CLASSES_SCHEMA from homeassistant.const import ( - CONF_FORCE_UPDATE, CONF_NAME, CONF_VALUE_TEMPLATE, - CONF_UNIT_OF_MEASUREMENT, CONF_ICON, CONF_DEVICE_CLASS, CONF_DEVICE) -from homeassistant.helpers.entity import Entity -from homeassistant.components import mqtt + CONF_DEVICE, CONF_DEVICE_CLASS, CONF_FORCE_UPDATE, CONF_ICON, CONF_NAME, + CONF_UNIT_OF_MEASUREMENT, CONF_VALUE_TEMPLATE) +from homeassistant.core import callback import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.typing import HomeAssistantType, ConfigType from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.helpers.entity import Entity from homeassistant.helpers.event import async_track_point_in_utc_time +from homeassistant.helpers.typing import ConfigType, HomeAssistantType from homeassistant.util import dt as dt_util _LOGGER = logging.getLogger(__name__) CONF_EXPIRE_AFTER = 'expire_after' CONF_JSON_ATTRS = 'json_attributes' -CONF_UNIQUE_ID = 'unique_id' DEFAULT_NAME = 'MQTT Sensor' DEFAULT_FORCE_UPDATE = False diff --git a/homeassistant/components/mqtt/switch.py b/homeassistant/components/mqtt/switch.py index ad4356b425b..56fc1a51206 100644 --- a/homeassistant/components/mqtt/switch.py +++ b/homeassistant/components/mqtt/switch.py @@ -8,22 +8,22 @@ import logging import voluptuous as vol -from homeassistant.core import callback +from homeassistant.components import mqtt, switch from homeassistant.components.mqtt import ( ATTR_DISCOVERY_HASH, CONF_COMMAND_TOPIC, CONF_QOS, CONF_RETAIN, - CONF_STATE_TOPIC, MqttAttributes, MqttAvailability, MqttDiscoveryUpdate, - MqttEntityDeviceInfo, subscription) + CONF_STATE_TOPIC, CONF_UNIQUE_ID, MqttAttributes, MqttAvailability, + MqttDiscoveryUpdate, MqttEntityDeviceInfo, subscription) from homeassistant.components.mqtt.discovery import ( MQTT_DISCOVERY_NEW, clear_discovery_hash) 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, STATE_ON, CONF_DEVICE) -from homeassistant.components import mqtt, switch + CONF_DEVICE, CONF_ICON, CONF_NAME, CONF_OPTIMISTIC, CONF_PAYLOAD_OFF, + CONF_PAYLOAD_ON, CONF_VALUE_TEMPLATE, STATE_ON) +from homeassistant.core import callback import homeassistant.helpers.config_validation as cv from homeassistant.helpers.dispatcher import async_dispatcher_connect -from homeassistant.helpers.typing import HomeAssistantType, ConfigType from homeassistant.helpers.restore_state import RestoreEntity +from homeassistant.helpers.typing import ConfigType, HomeAssistantType _LOGGER = logging.getLogger(__name__) @@ -33,7 +33,6 @@ DEFAULT_NAME = 'MQTT Switch' DEFAULT_PAYLOAD_ON = 'ON' DEFAULT_PAYLOAD_OFF = 'OFF' DEFAULT_OPTIMISTIC = False -CONF_UNIQUE_ID = 'unique_id' CONF_STATE_ON = "state_on" CONF_STATE_OFF = "state_off" diff --git a/homeassistant/components/mqtt/vacuum.py b/homeassistant/components/mqtt/vacuum.py index fedf33c5a2a..ca3f800385f 100644 --- a/homeassistant/components/mqtt/vacuum.py +++ b/homeassistant/components/mqtt/vacuum.py @@ -10,16 +10,15 @@ import voluptuous as vol from homeassistant.components import mqtt from homeassistant.components.mqtt import ( - ATTR_DISCOVERY_HASH, MqttAttributes, MqttAvailability, MqttDiscoveryUpdate, - MqttEntityDeviceInfo, subscription) + ATTR_DISCOVERY_HASH, CONF_UNIQUE_ID, MqttAttributes, MqttAvailability, + MqttDiscoveryUpdate, MqttEntityDeviceInfo, subscription) from homeassistant.components.mqtt.discovery import MQTT_DISCOVERY_NEW from homeassistant.components.vacuum import ( - SUPPORT_BATTERY, SUPPORT_CLEAN_SPOT, SUPPORT_FAN_SPEED, + DOMAIN, SUPPORT_BATTERY, SUPPORT_CLEAN_SPOT, SUPPORT_FAN_SPEED, SUPPORT_LOCATE, SUPPORT_PAUSE, SUPPORT_RETURN_HOME, SUPPORT_SEND_COMMAND, SUPPORT_STATUS, SUPPORT_STOP, SUPPORT_TURN_OFF, SUPPORT_TURN_ON, - VacuumDevice, DOMAIN) -from homeassistant.const import ( - ATTR_SUPPORTED_FEATURES, CONF_NAME, CONF_DEVICE) + VacuumDevice) +from homeassistant.const import ATTR_SUPPORTED_FEATURES, CONF_DEVICE, CONF_NAME from homeassistant.core import callback import homeassistant.helpers.config_validation as cv from homeassistant.helpers.dispatcher import async_dispatcher_connect @@ -94,7 +93,6 @@ CONF_FAN_SPEED_TEMPLATE = 'fan_speed_template' CONF_SET_FAN_SPEED_TOPIC = 'set_fan_speed_topic' CONF_FAN_SPEED_LIST = 'fan_speed_list' CONF_SEND_COMMAND_TOPIC = 'send_command_topic' -CONF_UNIQUE_ID = 'unique_id' DEFAULT_NAME = 'MQTT Vacuum' DEFAULT_RETAIN = False diff --git a/tests/components/mqtt/test_alarm_control_panel.py b/tests/components/mqtt/test_alarm_control_panel.py index 7bbaa977131..0bcb29ec9c6 100644 --- a/tests/components/mqtt/test_alarm_control_panel.py +++ b/tests/components/mqtt/test_alarm_control_panel.py @@ -3,18 +3,18 @@ import json import unittest from unittest.mock import ANY -from homeassistant.setup import setup_component -from homeassistant.const import ( - STATE_ALARM_DISARMED, STATE_ALARM_ARMED_HOME, STATE_ALARM_ARMED_AWAY, - STATE_ALARM_PENDING, STATE_ALARM_TRIGGERED, STATE_UNAVAILABLE, - STATE_UNKNOWN) from homeassistant.components import alarm_control_panel, mqtt from homeassistant.components.mqtt.discovery import async_start +from homeassistant.const import ( + STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME, STATE_ALARM_DISARMED, + STATE_ALARM_PENDING, STATE_ALARM_TRIGGERED, STATE_UNAVAILABLE, + STATE_UNKNOWN) +from homeassistant.setup import setup_component from tests.common import ( - assert_setup_component, async_fire_mqtt_message, async_mock_mqtt_component, - async_setup_component, fire_mqtt_message, get_test_home_assistant, - mock_mqtt_component, MockConfigEntry, mock_registry) + MockConfigEntry, assert_setup_component, async_fire_mqtt_message, + async_mock_mqtt_component, async_setup_component, fire_mqtt_message, + get_test_home_assistant, mock_mqtt_component, mock_registry) from tests.components.alarm_control_panel import common CODE = 'HELLO_CODE' diff --git a/tests/components/mqtt/test_binary_sensor.py b/tests/components/mqtt/test_binary_sensor.py index 0f40312cb2d..2f8a87b8a9b 100644 --- a/tests/components/mqtt/test_binary_sensor.py +++ b/tests/components/mqtt/test_binary_sensor.py @@ -1,23 +1,21 @@ """The tests for the MQTT binary sensor platform.""" +from datetime import timedelta import json import unittest from unittest.mock import ANY, Mock -from datetime import timedelta -import homeassistant.core as ha -from homeassistant.setup import setup_component, async_setup_component from homeassistant.components import binary_sensor, mqtt from homeassistant.components.mqtt.discovery import async_start - -from homeassistant.const import STATE_OFF, STATE_ON -from homeassistant.const import EVENT_STATE_CHANGED, STATE_UNAVAILABLE - +from homeassistant.const import ( + EVENT_STATE_CHANGED, STATE_OFF, STATE_ON, STATE_UNAVAILABLE) +import homeassistant.core as ha +from homeassistant.setup import async_setup_component, setup_component import homeassistant.util.dt as dt_util from tests.common import ( - get_test_home_assistant, fire_mqtt_message, async_fire_mqtt_message, - fire_time_changed, mock_component, mock_mqtt_component, mock_registry, - async_mock_mqtt_component, MockConfigEntry) + MockConfigEntry, async_fire_mqtt_message, async_mock_mqtt_component, + fire_mqtt_message, fire_time_changed, get_test_home_assistant, + mock_component, mock_mqtt_component, mock_registry) class TestSensorMQTT(unittest.TestCase): diff --git a/tests/components/mqtt/test_camera.py b/tests/components/mqtt/test_camera.py index 8665f26aba9..a127ce0e68e 100644 --- a/tests/components/mqtt/test_camera.py +++ b/tests/components/mqtt/test_camera.py @@ -3,8 +3,7 @@ import asyncio from homeassistant.setup import async_setup_component -from tests.common import ( - async_mock_mqtt_component, async_fire_mqtt_message) +from tests.common import async_fire_mqtt_message, async_mock_mqtt_component @asyncio.coroutine diff --git a/tests/components/mqtt/test_climate.py b/tests/components/mqtt/test_climate.py index 4803f915636..c9b7c748ea5 100644 --- a/tests/components/mqtt/test_climate.py +++ b/tests/components/mqtt/test_climate.py @@ -7,21 +7,20 @@ from unittest.mock import ANY import pytest import voluptuous as vol -from homeassistant.util.unit_system import ( - METRIC_SYSTEM -) -from homeassistant.setup import setup_component from homeassistant.components import climate, mqtt -from homeassistant.const import STATE_OFF, STATE_UNAVAILABLE from homeassistant.components.climate import ( - SUPPORT_OPERATION_MODE, SUPPORT_TARGET_TEMPERATURE, - SUPPORT_FAN_MODE, SUPPORT_SWING_MODE, SUPPORT_HOLD_MODE, - SUPPORT_AWAY_MODE, SUPPORT_AUX_HEAT, DEFAULT_MIN_TEMP, DEFAULT_MAX_TEMP) + DEFAULT_MAX_TEMP, DEFAULT_MIN_TEMP, SUPPORT_AUX_HEAT, SUPPORT_AWAY_MODE, + SUPPORT_FAN_MODE, SUPPORT_HOLD_MODE, SUPPORT_OPERATION_MODE, + SUPPORT_SWING_MODE, SUPPORT_TARGET_TEMPERATURE) from homeassistant.components.mqtt.discovery import async_start +from homeassistant.const import STATE_OFF, STATE_UNAVAILABLE +from homeassistant.setup import setup_component +from homeassistant.util.unit_system import METRIC_SYSTEM + from tests.common import ( - async_fire_mqtt_message, async_mock_mqtt_component, async_setup_component, - fire_mqtt_message, get_test_home_assistant, mock_component, - mock_mqtt_component, MockConfigEntry, mock_registry) + MockConfigEntry, async_fire_mqtt_message, async_mock_mqtt_component, + async_setup_component, fire_mqtt_message, get_test_home_assistant, + mock_component, mock_mqtt_component, mock_registry) from tests.components.climate import common ENTITY_CLIMATE = 'climate.test' diff --git a/tests/components/mqtt/test_config_flow.py b/tests/components/mqtt/test_config_flow.py index 66bf9b97807..9d822ba854b 100644 --- a/tests/components/mqtt/test_config_flow.py +++ b/tests/components/mqtt/test_config_flow.py @@ -5,7 +5,7 @@ import pytest from homeassistant.setup import async_setup_component -from tests.common import mock_coro, MockConfigEntry +from tests.common import MockConfigEntry, mock_coro @pytest.fixture(autouse=True) diff --git a/tests/components/mqtt/test_cover.py b/tests/components/mqtt/test_cover.py index 0f6d184b775..343bb3643c6 100644 --- a/tests/components/mqtt/test_cover.py +++ b/tests/components/mqtt/test_cover.py @@ -4,16 +4,16 @@ import unittest from unittest.mock import ANY from homeassistant.components import cover, mqtt -from homeassistant.components.cover import (ATTR_POSITION, ATTR_TILT_POSITION) +from homeassistant.components.cover import ATTR_POSITION, ATTR_TILT_POSITION from homeassistant.components.mqtt.cover import MqttCover from homeassistant.components.mqtt.discovery import async_start from homeassistant.const import ( - ATTR_ASSUMED_STATE, ATTR_ENTITY_ID, - SERVICE_CLOSE_COVER, SERVICE_CLOSE_COVER_TILT, SERVICE_OPEN_COVER, - SERVICE_OPEN_COVER_TILT, SERVICE_SET_COVER_POSITION, - SERVICE_SET_COVER_TILT_POSITION, SERVICE_STOP_COVER, - STATE_CLOSED, STATE_OPEN, STATE_UNAVAILABLE, STATE_UNKNOWN) -from homeassistant.setup import setup_component, async_setup_component + ATTR_ASSUMED_STATE, ATTR_ENTITY_ID, SERVICE_CLOSE_COVER, + SERVICE_CLOSE_COVER_TILT, SERVICE_OPEN_COVER, SERVICE_OPEN_COVER_TILT, + SERVICE_SET_COVER_POSITION, SERVICE_SET_COVER_TILT_POSITION, + SERVICE_STOP_COVER, STATE_CLOSED, STATE_OPEN, STATE_UNAVAILABLE, + STATE_UNKNOWN) +from homeassistant.setup import async_setup_component, setup_component from tests.common import ( MockConfigEntry, async_fire_mqtt_message, async_mock_mqtt_component, diff --git a/tests/components/mqtt/test_discovery.py b/tests/components/mqtt/test_discovery.py index 083227e27c0..47bd912fbc8 100644 --- a/tests/components/mqtt/test_discovery.py +++ b/tests/components/mqtt/test_discovery.py @@ -3,11 +3,11 @@ import asyncio from unittest.mock import patch from homeassistant.components import mqtt -from homeassistant.components.mqtt.discovery import async_start, \ - ALREADY_DISCOVERED -from homeassistant.const import STATE_ON, STATE_OFF +from homeassistant.components.mqtt.discovery import ( + ALREADY_DISCOVERED, async_start) +from homeassistant.const import STATE_OFF, STATE_ON -from tests.common import async_fire_mqtt_message, mock_coro, MockConfigEntry +from tests.common import MockConfigEntry, async_fire_mqtt_message, mock_coro @asyncio.coroutine diff --git a/tests/components/mqtt/test_fan.py b/tests/components/mqtt/test_fan.py index 8c250443f6b..38b38ff7648 100644 --- a/tests/components/mqtt/test_fan.py +++ b/tests/components/mqtt/test_fan.py @@ -2,13 +2,14 @@ import json from unittest.mock import ANY -from homeassistant.setup import async_setup_component from homeassistant.components import fan, mqtt from homeassistant.components.mqtt.discovery import async_start from homeassistant.const import ATTR_ASSUMED_STATE, STATE_UNAVAILABLE +from homeassistant.setup import async_setup_component -from tests.common import async_fire_mqtt_message, MockConfigEntry, \ - async_mock_mqtt_component, mock_registry +from tests.common import ( + MockConfigEntry, async_fire_mqtt_message, async_mock_mqtt_component, + mock_registry) async def test_fail_setup_if_no_command_topic(hass, mqtt_mock): diff --git a/tests/components/mqtt/test_init.py b/tests/components/mqtt/test_init.py index 707d9ff6021..94506efa909 100644 --- a/tests/components/mqtt/test_init.py +++ b/tests/components/mqtt/test_init.py @@ -1,17 +1,17 @@ """The tests for the MQTT component.""" import asyncio +import ssl import unittest from unittest import mock -import ssl import pytest import voluptuous as vol +from homeassistant.components import mqtt +from homeassistant.const import ( + ATTR_DOMAIN, ATTR_SERVICE, EVENT_CALL_SERVICE, EVENT_HOMEASSISTANT_STOP) from homeassistant.core import callback from homeassistant.setup import async_setup_component -from homeassistant.components import mqtt -from homeassistant.const import (EVENT_CALL_SERVICE, ATTR_DOMAIN, ATTR_SERVICE, - EVENT_HOMEASSISTANT_STOP) from tests.common import ( MockConfigEntry, async_fire_mqtt_message, async_mock_mqtt_component, diff --git a/tests/components/mqtt/test_light.py b/tests/components/mqtt/test_light.py index 1b1ba3862e9..9ad3a8d8323 100644 --- a/tests/components/mqtt/test_light.py +++ b/tests/components/mqtt/test_light.py @@ -157,16 +157,16 @@ import json from unittest import mock from unittest.mock import ANY, patch -from homeassistant.setup import async_setup_component -from homeassistant.const import ( - STATE_ON, STATE_OFF, STATE_UNAVAILABLE, ATTR_ASSUMED_STATE) from homeassistant.components import light, mqtt from homeassistant.components.mqtt.discovery import async_start +from homeassistant.const import ( + ATTR_ASSUMED_STATE, STATE_OFF, STATE_ON, STATE_UNAVAILABLE) import homeassistant.core as ha +from homeassistant.setup import async_setup_component from tests.common import ( - assert_setup_component, async_fire_mqtt_message, async_mock_mqtt_component, - mock_coro, MockConfigEntry, mock_registry) + MockConfigEntry, assert_setup_component, async_fire_mqtt_message, + async_mock_mqtt_component, mock_coro, mock_registry) from tests.components.light import common diff --git a/tests/components/mqtt/test_light_json.py b/tests/components/mqtt/test_light_json.py index c8d7c1d3e54..86523d955a0 100644 --- a/tests/components/mqtt/test_light_json.py +++ b/tests/components/mqtt/test_light_json.py @@ -90,17 +90,17 @@ light: import json from unittest.mock import ANY, patch -from homeassistant.setup import async_setup_component -from homeassistant.const import ( - STATE_ON, STATE_OFF, STATE_UNAVAILABLE, ATTR_ASSUMED_STATE, - ATTR_SUPPORTED_FEATURES) from homeassistant.components import light, mqtt from homeassistant.components.mqtt.discovery import async_start +from homeassistant.const import ( + ATTR_ASSUMED_STATE, ATTR_SUPPORTED_FEATURES, STATE_OFF, STATE_ON, + STATE_UNAVAILABLE) import homeassistant.core as ha +from homeassistant.setup import async_setup_component from tests.common import ( - mock_coro, async_fire_mqtt_message, async_mock_mqtt_component, - MockConfigEntry, mock_registry) + MockConfigEntry, async_fire_mqtt_message, async_mock_mqtt_component, + mock_coro, mock_registry) async def test_fail_setup_if_no_command_topic(hass, mqtt_mock): diff --git a/tests/components/mqtt/test_light_template.py b/tests/components/mqtt/test_light_template.py index 13fe086684c..66048b3e3bf 100644 --- a/tests/components/mqtt/test_light_template.py +++ b/tests/components/mqtt/test_light_template.py @@ -29,16 +29,16 @@ If your light doesn't support RGB feature, omit `(red|green|blue)_template`. import json from unittest.mock import ANY, patch -from homeassistant.setup import async_setup_component -from homeassistant.const import ( - STATE_ON, STATE_OFF, STATE_UNAVAILABLE, ATTR_ASSUMED_STATE) from homeassistant.components import light, mqtt from homeassistant.components.mqtt.discovery import async_start +from homeassistant.const import ( + ATTR_ASSUMED_STATE, STATE_OFF, STATE_ON, STATE_UNAVAILABLE) import homeassistant.core as ha +from homeassistant.setup import async_setup_component from tests.common import ( - async_fire_mqtt_message, assert_setup_component, mock_coro, - async_mock_mqtt_component, MockConfigEntry, mock_registry) + MockConfigEntry, assert_setup_component, async_fire_mqtt_message, + async_mock_mqtt_component, mock_coro, mock_registry) async def test_setup_fails(hass, mqtt_mock): diff --git a/tests/components/mqtt/test_lock.py b/tests/components/mqtt/test_lock.py index 3dd21535694..c4741445cca 100644 --- a/tests/components/mqtt/test_lock.py +++ b/tests/components/mqtt/test_lock.py @@ -2,14 +2,14 @@ import json from unittest.mock import ANY -from homeassistant.setup import async_setup_component -from homeassistant.const import ( - STATE_LOCKED, STATE_UNLOCKED, STATE_UNAVAILABLE, ATTR_ASSUMED_STATE) from homeassistant.components import lock, mqtt from homeassistant.components.mqtt.discovery import async_start +from homeassistant.const import ( + ATTR_ASSUMED_STATE, STATE_LOCKED, STATE_UNAVAILABLE, STATE_UNLOCKED) +from homeassistant.setup import async_setup_component from tests.common import ( - async_fire_mqtt_message, async_mock_mqtt_component, MockConfigEntry, + MockConfigEntry, async_fire_mqtt_message, async_mock_mqtt_component, mock_registry) diff --git a/tests/components/mqtt/test_sensor.py b/tests/components/mqtt/test_sensor.py index a89b0b444ff..cd637d2aa01 100644 --- a/tests/components/mqtt/test_sensor.py +++ b/tests/components/mqtt/test_sensor.py @@ -1,22 +1,21 @@ """The tests for the MQTT sensor platform.""" +from datetime import datetime, timedelta import json import unittest - -from datetime import timedelta, datetime from unittest.mock import ANY, patch -import homeassistant.core as ha -from homeassistant.setup import setup_component, async_setup_component from homeassistant.components import mqtt from homeassistant.components.mqtt.discovery import async_start import homeassistant.components.sensor as sensor from homeassistant.const import EVENT_STATE_CHANGED, STATE_UNAVAILABLE +import homeassistant.core as ha +from homeassistant.setup import async_setup_component, setup_component import homeassistant.util.dt as dt_util -from tests.common import mock_mqtt_component, fire_mqtt_message, \ - assert_setup_component, async_fire_mqtt_message, \ - async_mock_mqtt_component, MockConfigEntry, mock_registry -from tests.common import get_test_home_assistant, mock_component +from tests.common import ( + MockConfigEntry, assert_setup_component, async_fire_mqtt_message, + async_mock_mqtt_component, fire_mqtt_message, get_test_home_assistant, + mock_component, mock_mqtt_component, mock_registry) class TestSensorMQTT(unittest.TestCase): diff --git a/tests/components/mqtt/test_server.py b/tests/components/mqtt/test_server.py index 9f80f753690..2589adf2f9c 100644 --- a/tests/components/mqtt/test_server.py +++ b/tests/components/mqtt/test_server.py @@ -1,9 +1,9 @@ """The tests for the MQTT component embedded server.""" -from unittest.mock import Mock, MagicMock, patch +from unittest.mock import MagicMock, Mock, patch +import homeassistant.components.mqtt as mqtt from homeassistant.const import CONF_PASSWORD from homeassistant.setup import setup_component -import homeassistant.components.mqtt as mqtt from tests.common import get_test_home_assistant, mock_coro diff --git a/tests/components/mqtt/test_subscription.py b/tests/components/mqtt/test_subscription.py index 69386e2bad4..b4b005d0d1e 100644 --- a/tests/components/mqtt/test_subscription.py +++ b/tests/components/mqtt/test_subscription.py @@ -1,9 +1,9 @@ """The tests for the MQTT subscription component.""" from unittest import mock -from homeassistant.core import callback from homeassistant.components.mqtt.subscription import ( async_subscribe_topics, async_unsubscribe_topics) +from homeassistant.core import callback from tests.common import async_fire_mqtt_message, async_mock_mqtt_component diff --git a/tests/components/mqtt/test_switch.py b/tests/components/mqtt/test_switch.py index 5bbb04e1017..f1956207a55 100644 --- a/tests/components/mqtt/test_switch.py +++ b/tests/components/mqtt/test_switch.py @@ -1,19 +1,20 @@ """The tests for the MQTT switch platform.""" import json -from asynctest import patch -import pytest from unittest.mock import ANY -from homeassistant.setup import async_setup_component -from homeassistant.const import STATE_ON, STATE_OFF, STATE_UNAVAILABLE,\ - ATTR_ASSUMED_STATE -import homeassistant.core as ha -from homeassistant.components import switch, mqtt +from asynctest import patch +import pytest + +from homeassistant.components import mqtt, switch from homeassistant.components.mqtt.discovery import async_start +from homeassistant.const import ( + ATTR_ASSUMED_STATE, STATE_OFF, STATE_ON, STATE_UNAVAILABLE) +import homeassistant.core as ha +from homeassistant.setup import async_setup_component from tests.common import ( - mock_coro, async_mock_mqtt_component, async_fire_mqtt_message, - MockConfigEntry, mock_registry) + MockConfigEntry, async_fire_mqtt_message, async_mock_mqtt_component, + mock_coro, mock_registry) from tests.components.switch import common diff --git a/tests/components/mqtt/test_vacuum.py b/tests/components/mqtt/test_vacuum.py index 04a82bb20e1..f7cc6ce1f44 100644 --- a/tests/components/mqtt/test_vacuum.py +++ b/tests/components/mqtt/test_vacuum.py @@ -1,20 +1,20 @@ """The tests for the Mqtt vacuum platform.""" import json + import pytest -from homeassistant.setup import async_setup_component -from homeassistant.const import ( - CONF_PLATFORM, STATE_OFF, STATE_ON, STATE_UNAVAILABLE, CONF_NAME) -from homeassistant.components import vacuum, mqtt -from homeassistant.components.vacuum import ( - ATTR_BATTERY_LEVEL, ATTR_BATTERY_ICON, ATTR_STATUS, - ATTR_FAN_SPEED) +from homeassistant.components import mqtt, vacuum from homeassistant.components.mqtt import ( CONF_COMMAND_TOPIC, vacuum as mqttvacuum) from homeassistant.components.mqtt.discovery import async_start +from homeassistant.components.vacuum import ( + ATTR_BATTERY_ICON, ATTR_BATTERY_LEVEL, ATTR_FAN_SPEED, ATTR_STATUS) +from homeassistant.const import ( + CONF_NAME, CONF_PLATFORM, STATE_OFF, STATE_ON, STATE_UNAVAILABLE) +from homeassistant.setup import async_setup_component + from tests.common import ( - async_mock_mqtt_component, - async_fire_mqtt_message, MockConfigEntry) + MockConfigEntry, async_fire_mqtt_message, async_mock_mqtt_component) from tests.components.vacuum import common default_config = { From f91ff76b95a2e4180d7b773b95b478b308ce9e04 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20H=C3=B8yer=20Iversen?= Date: Sun, 27 Jan 2019 19:20:43 +0100 Subject: [PATCH 133/222] Upgrade mill library (#20514) --- homeassistant/components/climate/mill.py | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/climate/mill.py b/homeassistant/components/climate/mill.py index afd655bed22..b735927cb80 100644 --- a/homeassistant/components/climate/mill.py +++ b/homeassistant/components/climate/mill.py @@ -19,7 +19,7 @@ from homeassistant.const import ( from homeassistant.helpers import config_validation as cv from homeassistant.helpers.aiohttp_client import async_get_clientsession -REQUIREMENTS = ['millheater==0.3.3'] +REQUIREMENTS = ['millheater==0.3.4'] _LOGGER = logging.getLogger(__name__) diff --git a/requirements_all.txt b/requirements_all.txt index b31ec2199d3..c1da51b4306 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -680,7 +680,7 @@ mficlient==0.3.0 miflora==0.4.0 # homeassistant.components.climate.mill -millheater==0.3.3 +millheater==0.3.4 # homeassistant.components.sensor.mitemp_bt mitemp_bt==0.0.1 From 7412d0f97c9b6dee65ca733b40c3c46273a4ff10 Mon Sep 17 00:00:00 2001 From: Heine Furubotten Date: Sun, 27 Jan 2019 21:32:23 +0100 Subject: [PATCH 134/222] Add nilu air_quality platform (#19674) * Add nilu air_pollutants platform * Code Review - validation, DRYs, rm state override, new attr - Repeated code moved to own method. - Removed override of state property. - New attr for showing nilu pollution recommendations. - More validation of stations input. - Minor fixes and typos. * Removed unused prop * Check for none result from client before entity add * Moved platform to air_quality component * Updated outdated docstrings * Minor changes --- .coveragerc | 1 + homeassistant/components/air_quality/nilu.py | 252 +++++++++++++++++++ requirements_all.txt | 3 + 3 files changed, 256 insertions(+) create mode 100644 homeassistant/components/air_quality/nilu.py diff --git a/.coveragerc b/.coveragerc index b89b3a6be60..a4b27469039 100644 --- a/.coveragerc +++ b/.coveragerc @@ -470,6 +470,7 @@ omit = homeassistant/components/*/spider.py homeassistant/components/air_quality/opensensemap.py + homeassistant/components/air_quality/nilu.py homeassistant/components/alarm_control_panel/alarmdotcom.py homeassistant/components/alarm_control_panel/canary.py homeassistant/components/alarm_control_panel/concord232.py diff --git a/homeassistant/components/air_quality/nilu.py b/homeassistant/components/air_quality/nilu.py new file mode 100644 index 00000000000..2ab38c1ad95 --- /dev/null +++ b/homeassistant/components/air_quality/nilu.py @@ -0,0 +1,252 @@ +""" +Sensor for checking the air quality around Norway. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/air_quality.nilu/ +""" +from datetime import timedelta +import logging + +import voluptuous as vol + +from homeassistant.components.air_quality import ( + PLATFORM_SCHEMA, AirQualityEntity) +from homeassistant.const import ( + CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME, CONF_SHOW_ON_MAP) +import homeassistant.helpers.config_validation as cv +from homeassistant.util import Throttle + +REQUIREMENTS = ['niluclient==0.1.2'] + +_LOGGER = logging.getLogger(__name__) + +ATTR_AREA = 'area' +ATTR_POLLUTION_INDEX = 'nilu_pollution_index' +ATTRIBUTION = "Data provided by luftkvalitet.info and nilu.no" + +CONF_AREA = 'area' +CONF_STATION = 'stations' + +DEFAULT_NAME = 'NILU' + +SCAN_INTERVAL = timedelta(minutes=30) + +CONF_ALLOWED_AREAS = [ + 'Bergen', + 'Birkenes', + 'Bodø', + 'Brumunddal', + 'Bærum', + 'Drammen', + 'Elverum', + 'Fredrikstad', + 'Gjøvik', + 'Grenland', + 'Halden', + 'Hamar', + 'Harstad', + 'Hurdal', + 'Karasjok', + 'Kristiansand', + 'Kårvatn', + 'Lillehammer', + 'Lillesand', + 'Lillestrøm', + 'Lørenskog', + 'Mo i Rana', + 'Moss', + 'Narvik', + 'Oslo', + 'Prestebakke', + 'Sandve', + 'Sarpsborg', + 'Stavanger', + 'Sør-Varanger', + 'Tromsø', + 'Trondheim', + 'Tustervatn', + 'Zeppelinfjellet', + 'Ålesund', +] + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Inclusive(CONF_LATITUDE, 'coordinates', + 'Latitude and longitude must exist together'): cv.latitude, + vol.Inclusive(CONF_LONGITUDE, 'coordinates', + 'Latitude and longitude must exist together'): cv.longitude, + vol.Exclusive(CONF_AREA, 'station_collection', + 'Can only configure one specific station or ' + 'stations in a specific area pr sensor. ' + 'Please only configure station or area.' + ): vol.All(cv.string, vol.In(CONF_ALLOWED_AREAS)), + vol.Exclusive(CONF_STATION, 'station_collection', + 'Can only configure one specific station or ' + 'stations in a specific area pr sensor. ' + 'Please only configure station or area.' + ): vol.All(cv.ensure_list, [cv.string]), + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + vol.Optional(CONF_SHOW_ON_MAP, default=False): cv.boolean, +}) + + +def setup_platform(hass, config, add_entities, discovery_info=None): + """Set up the NILU air quality sensor.""" + import niluclient as nilu + name = config.get(CONF_NAME) + area = config.get(CONF_AREA) + stations = config.get(CONF_STATION) + show_on_map = config.get(CONF_SHOW_ON_MAP) + + sensors = [] + + if area: + stations = nilu.lookup_stations_in_area(area) + elif not area and not stations: + latitude = config.get(CONF_LATITUDE, hass.config.latitude) + longitude = config.get(CONF_LONGITUDE, hass.config.longitude) + location_client = nilu.create_location_client(latitude, longitude) + stations = location_client.station_names + + for station in stations: + client = NiluData(nilu.create_station_client(station)) + client.update() + if client.data.sensors: + sensors.append(NiluSensor(client, name, show_on_map)) + else: + _LOGGER.warning("%s didn't give any sensors results", station) + + add_entities(sensors, True) + + +class NiluData: + """Class for handling the data retrieval.""" + + def __init__(self, api): + """Initialize the data object.""" + self.api = api + + @property + def data(self): + """Get data cached in client.""" + return self.api.data + + @Throttle(SCAN_INTERVAL) + def update(self): + """Get the latest data from nilu API.""" + self.api.update() + + +class NiluSensor(AirQualityEntity): + """Single nilu station air sensor.""" + + def __init__(self, api_data: NiluData, name: str, show_on_map: bool): + """Initialize the sensor.""" + self._api = api_data + self._name = "{} {}".format(name, api_data.data.name) + self._max_aqi = None + self._attrs = {} + + if show_on_map: + self._attrs[CONF_LATITUDE] = api_data.data.latitude + self._attrs[CONF_LONGITUDE] = api_data.data.longitude + + @property + def attribution(self) -> str: + """Return the attribution.""" + return ATTRIBUTION + + @property + def device_state_attributes(self) -> dict: + """Return other details about the sensor state.""" + return self._attrs + + @property + def name(self) -> str: + """Return the name of the sensor.""" + return self._name + + @property + def air_quality_index(self) -> str: + """Return the Air Quality Index (AQI).""" + return self._max_aqi + + @property + def carbon_monoxide(self) -> str: + """Return the CO (carbon monoxide) level.""" + from niluclient import CO + return self.get_component_state(CO) + + @property + def carbon_dioxide(self) -> str: + """Return the CO2 (carbon dioxide) level.""" + from niluclient import CO2 + return self.get_component_state(CO2) + + @property + def nitrogen_oxide(self) -> str: + """Return the N2O (nitrogen oxide) level.""" + from niluclient import NOX + return self.get_component_state(NOX) + + @property + def nitrogen_monoxide(self) -> str: + """Return the NO (nitrogen monoxide) level.""" + from niluclient import NO + return self.get_component_state(NO) + + @property + def nitrogen_dioxide(self) -> str: + """Return the NO2 (nitrogen dioxide) level.""" + from niluclient import NO2 + return self.get_component_state(NO2) + + @property + def ozone(self) -> str: + """Return the O3 (ozone) level.""" + from niluclient import OZONE + return self.get_component_state(OZONE) + + @property + def particulate_matter_2_5(self) -> str: + """Return the particulate matter 2.5 level.""" + from niluclient import PM25 + return self.get_component_state(PM25) + + @property + def particulate_matter_10(self) -> str: + """Return the particulate matter 10 level.""" + from niluclient import PM10 + return self.get_component_state(PM10) + + @property + def particulate_matter_0_1(self) -> str: + """Return the particulate matter 0.1 level.""" + from niluclient import PM1 + return self.get_component_state(PM1) + + @property + def sulphur_dioxide(self) -> str: + """Return the SO2 (sulphur dioxide) level.""" + from niluclient import SO2 + return self.get_component_state(SO2) + + def get_component_state(self, component_name: str) -> str: + """Return formatted value of specified component.""" + if component_name in self._api.data.sensors: + sensor = self._api.data.sensors[component_name] + return sensor.value + return None + + def update(self) -> None: + """Update the sensor.""" + import niluclient as nilu + self._api.update() + + sensors = self._api.data.sensors.values() + if sensors: + max_index = max([s.pollution_index for s in sensors]) + self._max_aqi = max_index + self._attrs[ATTR_POLLUTION_INDEX] = \ + nilu.POLLUTION_INDEX[self._max_aqi] + + self._attrs[ATTR_AREA] = self._api.data.area diff --git a/requirements_all.txt b/requirements_all.txt index c1da51b4306..9732bc8f0b9 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -724,6 +724,9 @@ neurio==0.3.1 # homeassistant.components.light.niko_home_control niko-home-control==0.1.8 +# homeassistant.components.air_quality.nilu +niluclient==0.1.2 + # homeassistant.components.sensor.nederlandse_spoorwegen nsapi==2.7.4 From 3d4f2926e9de9d85deab3a6f084bc552e4a9a5d9 Mon Sep 17 00:00:00 2001 From: Eliseo Martelli Date: Sun, 27 Jan 2019 21:44:19 +0100 Subject: [PATCH 135/222] Add Iliad Italy (Mobile Telephony Provider) Sensor (#19645) * working state * Attrs * > requirements generated * linting * fixes * coveragerc * ordered imports and fixed auth * Added Throttle * moved throttle to decorator * remove scan interval * lower throttle * moved to scan interval * Add attribution --- .coveragerc | 1 + .../components/sensor/iliad_italy.py | 119 ++++++++++++++++++ requirements_all.txt | 3 + 3 files changed, 123 insertions(+) create mode 100644 homeassistant/components/sensor/iliad_italy.py diff --git a/.coveragerc b/.coveragerc index a4b27469039..2d4fb3f81a7 100644 --- a/.coveragerc +++ b/.coveragerc @@ -793,6 +793,7 @@ omit = homeassistant/components/sensor/hp_ilo.py homeassistant/components/sensor/htu21d.py homeassistant/components/sensor/upnp.py + homeassistant/components/sensor/iliad_italy.py homeassistant/components/sensor/imap_email_content.py homeassistant/components/sensor/imap.py homeassistant/components/sensor/influxdb.py diff --git a/homeassistant/components/sensor/iliad_italy.py b/homeassistant/components/sensor/iliad_italy.py new file mode 100644 index 00000000000..1e1e5077e80 --- /dev/null +++ b/homeassistant/components/sensor/iliad_italy.py @@ -0,0 +1,119 @@ +""" +Sensor to get Iliad Italy personal data. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/sensor.iliad_italy/ +""" +from datetime import timedelta +import logging + +import voluptuous as vol + +from homeassistant.components.sensor import PLATFORM_SCHEMA +from homeassistant.const import ATTR_ATTRIBUTION, CONF_PASSWORD, CONF_USERNAME +from homeassistant.helpers.aiohttp_client import async_get_clientsession +import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.entity import Entity +import homeassistant.util.dt as dt_util + +REQUIREMENTS = ['aioiliad==0.1.1'] + +_LOGGER = logging.getLogger(__name__) + +ATTRIBUTION = "Data provided by Iliad Italy" + +ICON = 'mdi:phone' + +SCAN_INTERVAL = timedelta(minutes=10) + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Required(CONF_USERNAME): cv.string, + vol.Required(CONF_PASSWORD): cv.string, +}) + + +async def async_setup_platform( + hass, conf, async_add_entities, discovery_info=None): + """Set up the Iliad Italy sensor platform.""" + from aioiliad import Iliad + iliad = Iliad(conf[CONF_USERNAME], conf[CONF_PASSWORD], + async_get_clientsession(hass), hass.loop) + await iliad.login() + + if not iliad.is_logged_in(): + _LOGGER.error("Check username and password") + return + + async_add_entities([IliadSensor(iliad)], True) + + +class IliadSensor(Entity): + """Representation of a Iliad Italy Sensor.""" + + def __init__(self, iliad): + """Initialize the Iliad Italy sensor.""" + from aioiliad.IliadData import IliadData + self._iliad = iliad + self._iliaddata = IliadData(self._iliad) + self._data = None + self._state = None + + @property + def name(self): + """Return the name of the sensor.""" + return "Iliad {}".format(self._data['info']['utente']) + + @property + def icon(self): + """Return the icon of the sensor.""" + return ICON + + @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 the sensor.""" + return '€' + + @property + def device_state_attributes(self): + """Return the state attributes of the sensor.""" + attr = { + ATTR_ATTRIBUTION: ATTRIBUTION, + 'next_renewal': + dt_util.utc_from_timestamp( + self._data['info']['rinnovo']).isoformat(), + 'italy_sent_sms': self._data['italy']['sms'], + 'italy_over_plan_sms': self._data['italy']['sms_extra'], + 'italy_sent_mms': self._data['italy']['mms'], + 'italy_over_plan_mms': self._data['italy']['mms_extra'], + 'italy_calls_seconds': self._data['italy']['chiamate'], + 'italy_over_plan_calls': self._data['italy']['chiamate_extra'], + 'italy_data': self._data['italy']['internet'], + 'italy_data_max': self._data['italy']['internet_max'], + 'italy_data_over_plan': self._data['italy']['internet_over'], + + 'abroad_sent_sms': self._data['estero']['sms'], + 'abroad_over_plan_sms': self._data['estero']['sms_extra'], + 'abroad_sent_mms': self._data['estero']['mms'], + 'abroad_over_plan_mms': self._data['estero']['mms_extra'], + 'abroad_calls_seconds': self._data['estero']['chiamate'], + 'abroad_over_plan_calls': self._data['estero']['chiamate_extra'], + 'abroad_data': self._data['estero']['internet'], + 'abroad_data_max': self._data['estero']['internet_max'], + 'abroad_data_over_plan': self._data['estero']['internet_over'], + } + return attr + + async def async_update(self): + """Update device state.""" + await self._iliaddata.update() + self._data = { + 'italy': self._iliaddata.get_italy(), + 'estero': self._iliaddata.get_estero(), + 'info': self._iliaddata.get_general_info() + } + self._state = self._data['info']['credito'].replace('€', '') diff --git a/requirements_all.txt b/requirements_all.txt index 9732bc8f0b9..32ce1905320 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -114,6 +114,9 @@ aiohttp_cors==0.7.0 # homeassistant.components.hue aiohue==1.8.0 +# homeassistant.components.sensor.iliad_italy +aioiliad==0.1.1 + # homeassistant.components.sensor.imap aioimaplib==0.7.13 From f575d1d3a6fb0fdfb488a25978269375c96a882b Mon Sep 17 00:00:00 2001 From: Rohan Kapoor Date: Sun, 27 Jan 2019 13:18:20 -0800 Subject: [PATCH 136/222] Load/unload geofency entities correctly between component and platform (#20497) * Load/unload geofency entities correctly between component and platform * Lint * Await the coroutine directly --- homeassistant/components/geofency/__init__.py | 12 +++++---- .../components/geofency/device_tracker.py | 21 ++++++++++++--- tests/components/geofency/test_init.py | 26 ++++++++++++++++--- 3 files changed, 47 insertions(+), 12 deletions(-) diff --git a/homeassistant/components/geofency/__init__.py b/homeassistant/components/geofency/__init__.py index 093ebaa2fd3..239af14add8 100644 --- a/homeassistant/components/geofency/__init__.py +++ b/homeassistant/components/geofency/__init__.py @@ -10,10 +10,10 @@ import voluptuous as vol from aiohttp import web import homeassistant.helpers.config_validation as cv +from homeassistant.components.device_tracker import DOMAIN as DEVICE_TRACKER from homeassistant.const import HTTP_UNPROCESSABLE_ENTITY, STATE_NOT_HOME, \ ATTR_LATITUDE, ATTR_LONGITUDE, CONF_WEBHOOK_ID, HTTP_OK, ATTR_NAME from homeassistant.helpers import config_entry_flow -from homeassistant.helpers.discovery import async_load_platform from homeassistant.helpers.dispatcher import async_dispatcher_send from homeassistant.util import slugify @@ -71,10 +71,6 @@ async def async_setup(hass, hass_config): config = hass_config[DOMAIN] mobile_beacons = config[CONF_MOBILE_BEACONS] hass.data[DOMAIN] = [slugify(beacon) for beacon in mobile_beacons] - - hass.async_create_task( - async_load_platform(hass, 'device_tracker', DOMAIN, {}, hass_config) - ) return True @@ -136,12 +132,18 @@ async def async_setup_entry(hass, entry): """Configure based on config entry.""" hass.components.webhook.async_register( DOMAIN, 'Geofency', entry.data[CONF_WEBHOOK_ID], handle_webhook) + + hass.async_create_task( + hass.config_entries.async_forward_entry_setup(entry, DEVICE_TRACKER) + ) return True async def async_unload_entry(hass, entry): """Unload a config entry.""" hass.components.webhook.async_unregister(entry.data[CONF_WEBHOOK_ID]) + + await hass.config_entries.async_forward_entry_unload(entry, DEVICE_TRACKER) return True config_entry_flow.register_webhook_flow( diff --git a/homeassistant/components/geofency/device_tracker.py b/homeassistant/components/geofency/device_tracker.py index af11194c1d6..eea0960ec11 100644 --- a/homeassistant/components/geofency/device_tracker.py +++ b/homeassistant/components/geofency/device_tracker.py @@ -6,16 +6,21 @@ https://home-assistant.io/components/device_tracker.geofency/ """ import logging -from homeassistant.components.geofency import TRACKER_UPDATE +from homeassistant.components.device_tracker import DOMAIN as \ + DEVICE_TRACKER_DOMAIN +from homeassistant.components.geofency import TRACKER_UPDATE, \ + DOMAIN as GEOFENCY_DOMAIN from homeassistant.helpers.dispatcher import async_dispatcher_connect _LOGGER = logging.getLogger(__name__) DEPENDENCIES = ['geofency'] +DATA_KEY = '{}.{}'.format(GEOFENCY_DOMAIN, DEVICE_TRACKER_DOMAIN) -async def async_setup_scanner(hass, config, async_see, discovery_info=None): - """Set up the Geofency device tracker.""" + +async def async_setup_entry(hass, entry, async_see): + """Configure a dispatcher connection based on a config entry.""" async def _set_location(device, gps, location_name, attributes): """Fire HA event to set location.""" await async_see( @@ -25,5 +30,13 @@ async def async_setup_scanner(hass, config, async_see, discovery_info=None): attributes=attributes ) - async_dispatcher_connect(hass, TRACKER_UPDATE, _set_location) + hass.data[DATA_KEY] = async_dispatcher_connect( + hass, TRACKER_UPDATE, _set_location + ) + return True + + +async def async_unload_entry(hass, entry): + """Unload the config entry and remove the dispatcher connection.""" + hass.data[DATA_KEY]() return True diff --git a/tests/components/geofency/test_init.py b/tests/components/geofency/test_init.py index dbad7ba668b..41c232a51c3 100644 --- a/tests/components/geofency/test_init.py +++ b/tests/components/geofency/test_init.py @@ -5,14 +5,16 @@ from unittest.mock import patch, Mock import pytest from homeassistant import data_entry_flow -from homeassistant.components import zone +from homeassistant.components import zone, geofency from homeassistant.components.geofency import ( - CONF_MOBILE_BEACONS, DOMAIN) + CONF_MOBILE_BEACONS, DOMAIN, TRACKER_UPDATE) from homeassistant.const import ( HTTP_OK, HTTP_UNPROCESSABLE_ENTITY, STATE_HOME, - STATE_NOT_HOME) + STATE_NOT_HOME, CONF_WEBHOOK_ID) +from homeassistant.helpers.dispatcher import DATA_DISPATCHER from homeassistant.setup import async_setup_component from homeassistant.util import slugify +from tests.common import MockConfigEntry HOME_LATITUDE = 37.239622 HOME_LONGITUDE = -115.815811 @@ -281,3 +283,21 @@ async def test_beacon_enter_and_exit_car(hass, geofency_client, webhook_id): state_name = hass.states.get('{}.{}'.format( 'device_tracker', device_name)).state assert STATE_HOME == state_name + + +@pytest.mark.xfail( + reason='The device_tracker component does not support unloading yet.' +) +async def test_load_unload_entry(hass, geofency_client): + """Test that the appropriate dispatch signals are added and removed.""" + entry = MockConfigEntry(domain=DOMAIN, data={ + CONF_WEBHOOK_ID: 'geofency_test' + }) + + await geofency.async_setup_entry(hass, entry) + await hass.async_block_till_done() + assert 1 == len(hass.data[DATA_DISPATCHER][TRACKER_UPDATE]) + + await geofency.async_unload_entry(hass, entry) + await hass.async_block_till_done() + assert 0 == len(hass.data[DATA_DISPATCHER][TRACKER_UPDATE]) From 0c87fb421e8ea435a8192f0ea418d4ce39143feb Mon Sep 17 00:00:00 2001 From: Rohan Kapoor Date: Sun, 27 Jan 2019 14:43:16 -0800 Subject: [PATCH 137/222] Load/unload locative entities correctly between component and platform (#20498) * Embed device_tracker in locative * Load/unload locative entities correctly between component and platform * Await the coroutine directly * Await the correct coroutine --- homeassistant/components/locative/__init__.py | 14 +++++----- .../device_tracker.py} | 19 +++++++++++--- tests/components/locative/test_init.py | 26 +++++++++++++++++-- 3 files changed, 48 insertions(+), 11 deletions(-) rename homeassistant/components/{device_tracker/locative.py => locative/device_tracker.py} (51%) diff --git a/homeassistant/components/locative/__init__.py b/homeassistant/components/locative/__init__.py index 1cc47270ba3..195eacf17c2 100644 --- a/homeassistant/components/locative/__init__.py +++ b/homeassistant/components/locative/__init__.py @@ -12,11 +12,10 @@ from aiohttp import web import homeassistant.helpers.config_validation as cv from homeassistant.components.device_tracker import \ - DOMAIN as DEVICE_TRACKER_DOMAIN + DOMAIN as DEVICE_TRACKER from homeassistant.const import HTTP_UNPROCESSABLE_ENTITY, ATTR_LATITUDE, \ ATTR_LONGITUDE, STATE_NOT_HOME, CONF_WEBHOOK_ID, ATTR_ID, HTTP_OK from homeassistant.helpers import config_entry_flow -from homeassistant.helpers.discovery import async_load_platform from homeassistant.helpers.dispatcher import async_dispatcher_send _LOGGER = logging.getLogger(__name__) @@ -57,9 +56,6 @@ WEBHOOK_SCHEMA = vol.All( async def async_setup(hass, hass_config): """Set up the Locative component.""" - hass.async_create_task( - async_load_platform(hass, 'device_tracker', DOMAIN, {}, hass_config) - ) return True @@ -93,7 +89,7 @@ async def handle_webhook(hass, webhook_id, request): if direction == 'exit': current_state = hass.states.get( - '{}.{}'.format(DEVICE_TRACKER_DOMAIN, device)) + '{}.{}'.format(DEVICE_TRACKER, device)) if current_state is None or current_state.state == location_name: location_name = STATE_NOT_HOME @@ -140,12 +136,18 @@ async def async_setup_entry(hass, entry): """Configure based on config entry.""" hass.components.webhook.async_register( DOMAIN, 'Locative', entry.data[CONF_WEBHOOK_ID], handle_webhook) + + hass.async_create_task( + hass.config_entries.async_forward_entry_setup(entry, DEVICE_TRACKER) + ) return True async def async_unload_entry(hass, entry): """Unload a config entry.""" hass.components.webhook.async_unregister(entry.data[CONF_WEBHOOK_ID]) + + await hass.config_entries.async_forward_entry_unload(entry, DEVICE_TRACKER) return True config_entry_flow.register_webhook_flow( diff --git a/homeassistant/components/device_tracker/locative.py b/homeassistant/components/locative/device_tracker.py similarity index 51% rename from homeassistant/components/device_tracker/locative.py rename to homeassistant/components/locative/device_tracker.py index e7a63077a3a..20808c773f0 100644 --- a/homeassistant/components/device_tracker/locative.py +++ b/homeassistant/components/locative/device_tracker.py @@ -6,6 +6,9 @@ https://home-assistant.io/components/device_tracker.locative/ """ import logging +from homeassistant.components.device_tracker import \ + DOMAIN as DEVICE_TRACKER_DOMAIN +from homeassistant.components.locative import DOMAIN as LOCATIVE_DOMAIN from homeassistant.components.locative import TRACKER_UPDATE from homeassistant.helpers.dispatcher import async_dispatcher_connect @@ -13,9 +16,11 @@ _LOGGER = logging.getLogger(__name__) DEPENDENCIES = ['locative'] +DATA_KEY = '{}.{}'.format(LOCATIVE_DOMAIN, DEVICE_TRACKER_DOMAIN) -async def async_setup_scanner(hass, config, async_see, discovery_info=None): - """Set up an endpoint for the Locative device tracker.""" + +async def async_setup_entry(hass, entry, async_see): + """Configure a dispatcher connection based on a config entry.""" async def _set_location(device, gps_location, location_name): """Fire HA event to set location.""" await async_see( @@ -24,5 +29,13 @@ async def async_setup_scanner(hass, config, async_see, discovery_info=None): location_name=location_name ) - async_dispatcher_connect(hass, TRACKER_UPDATE, _set_location) + hass.data[DATA_KEY] = async_dispatcher_connect( + hass, TRACKER_UPDATE, _set_location + ) + return True + + +async def async_unload_entry(hass, entry): + """Unload the config entry and remove the dispatcher connection.""" + hass.data[DATA_KEY]() return True diff --git a/tests/components/locative/test_init.py b/tests/components/locative/test_init.py index 5f18d47eb22..877d25d04bd 100644 --- a/tests/components/locative/test_init.py +++ b/tests/components/locative/test_init.py @@ -4,11 +4,15 @@ from unittest.mock import patch, Mock import pytest from homeassistant import data_entry_flow +from homeassistant.components import locative from homeassistant.components.device_tracker import \ DOMAIN as DEVICE_TRACKER_DOMAIN -from homeassistant.components.locative import DOMAIN -from homeassistant.const import HTTP_OK, HTTP_UNPROCESSABLE_ENTITY +from homeassistant.components.locative import DOMAIN, TRACKER_UPDATE +from homeassistant.const import HTTP_OK, HTTP_UNPROCESSABLE_ENTITY, \ + CONF_WEBHOOK_ID +from homeassistant.helpers.dispatcher import DATA_DISPATCHER from homeassistant.setup import async_setup_component +from tests.common import MockConfigEntry @pytest.fixture(autouse=True) @@ -234,3 +238,21 @@ async def test_exit_first(hass, locative_client, webhook_id): state = hass.states.get('{}.{}'.format(DEVICE_TRACKER_DOMAIN, data['device'])) assert state.state == 'not_home' + + +@pytest.mark.xfail( + reason='The device_tracker component does not support unloading yet.' +) +async def test_load_unload_entry(hass): + """Test that the appropriate dispatch signals are added and removed.""" + entry = MockConfigEntry(domain=DOMAIN, data={ + CONF_WEBHOOK_ID: 'locative_test' + }) + + await locative.async_setup_entry(hass, entry) + await hass.async_block_till_done() + assert 1 == len(hass.data[DATA_DISPATCHER][TRACKER_UPDATE]) + + await locative.async_unload_entry(hass, entry) + await hass.async_block_till_done() + assert 0 == len(hass.data[DATA_DISPATCHER][TRACKER_UPDATE]) From d179686edf1be7692ffce2a77a1a843e8a283f55 Mon Sep 17 00:00:00 2001 From: Rohan Kapoor Date: Sun, 27 Jan 2019 15:37:19 -0800 Subject: [PATCH 138/222] Load/unload gpslogger entities correctly between component and platform (#20448) * Embed device_tracker in gpslogger * Load/unload gpslogger entities correctly between component and platform * Await the coroutine directly --- .../components/device_tracker/gpslogger.py | 32 -------------- .../components/gpslogger/__init__.py | 11 +++-- .../components/gpslogger/device_tracker.py | 44 +++++++++++++++++++ tests/components/gpslogger/test_init.py | 26 +++++++++-- 4 files changed, 74 insertions(+), 39 deletions(-) delete mode 100644 homeassistant/components/device_tracker/gpslogger.py create mode 100644 homeassistant/components/gpslogger/device_tracker.py diff --git a/homeassistant/components/device_tracker/gpslogger.py b/homeassistant/components/device_tracker/gpslogger.py deleted file mode 100644 index e0d9b37bf84..00000000000 --- a/homeassistant/components/device_tracker/gpslogger.py +++ /dev/null @@ -1,32 +0,0 @@ -""" -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 logging - -from homeassistant.components.gpslogger import TRACKER_UPDATE -from homeassistant.helpers.dispatcher import async_dispatcher_connect -from homeassistant.helpers.typing import HomeAssistantType, ConfigType - -_LOGGER = logging.getLogger(__name__) - -DEPENDENCIES = ['gpslogger'] - - -async def async_setup_scanner(hass: HomeAssistantType, config: ConfigType, - async_see, discovery_info=None): - """Set up an endpoint for the GPSLogger device tracker.""" - async def _set_location(device, gps_location, battery, accuracy, attrs): - """Fire HA event to set location.""" - await async_see( - dev_id=device, - gps=gps_location, - battery=battery, - gps_accuracy=accuracy, - attributes=attrs - ) - - async_dispatcher_connect(hass, TRACKER_UPDATE, _set_location) - return True diff --git a/homeassistant/components/gpslogger/__init__.py b/homeassistant/components/gpslogger/__init__.py index 4d1a5708331..d4150900223 100644 --- a/homeassistant/components/gpslogger/__init__.py +++ b/homeassistant/components/gpslogger/__init__.py @@ -15,8 +15,8 @@ from homeassistant.components.device_tracker.tile import ATTR_ALTITUDE from homeassistant.const import HTTP_UNPROCESSABLE_ENTITY, \ HTTP_OK, ATTR_LATITUDE, ATTR_LONGITUDE, CONF_WEBHOOK_ID from homeassistant.helpers import config_entry_flow -from homeassistant.helpers.discovery import async_load_platform from homeassistant.helpers.dispatcher import async_dispatcher_send +from homeassistant.components.device_tracker import DOMAIN as DEVICE_TRACKER _LOGGER = logging.getLogger(__name__) @@ -57,9 +57,6 @@ WEBHOOK_SCHEMA = vol.Schema({ async def async_setup(hass, hass_config): """Set up the GPSLogger component.""" - hass.async_create_task( - async_load_platform(hass, 'device_tracker', DOMAIN, {}, hass_config) - ) return True @@ -103,12 +100,18 @@ async def async_setup_entry(hass, entry): """Configure based on config entry.""" hass.components.webhook.async_register( DOMAIN, 'GPSLogger', entry.data[CONF_WEBHOOK_ID], handle_webhook) + + hass.async_create_task( + hass.config_entries.async_forward_entry_setup(entry, DEVICE_TRACKER) + ) return True async def async_unload_entry(hass, entry): """Unload a config entry.""" hass.components.webhook.async_unregister(entry.data[CONF_WEBHOOK_ID]) + + await hass.config_entries.async_forward_entry_unload(entry, DEVICE_TRACKER) return True config_entry_flow.register_webhook_flow( diff --git a/homeassistant/components/gpslogger/device_tracker.py b/homeassistant/components/gpslogger/device_tracker.py new file mode 100644 index 00000000000..8a312afa024 --- /dev/null +++ b/homeassistant/components/gpslogger/device_tracker.py @@ -0,0 +1,44 @@ +""" +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 logging + +from homeassistant.components.device_tracker import DOMAIN as \ + DEVICE_TRACKER_DOMAIN +from homeassistant.components.gpslogger import DOMAIN as GPSLOGGER_DOMAIN, \ + TRACKER_UPDATE +from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.helpers.typing import HomeAssistantType + +_LOGGER = logging.getLogger(__name__) + +DEPENDENCIES = ['gpslogger'] + +DATA_KEY = '{}.{}'.format(GPSLOGGER_DOMAIN, DEVICE_TRACKER_DOMAIN) + + +async def async_setup_entry(hass: HomeAssistantType, entry, async_see): + """Configure a dispatcher connection based on a config entry.""" + async def _set_location(device, gps_location, battery, accuracy, attrs): + """Fire HA event to set location.""" + await async_see( + dev_id=device, + gps=gps_location, + battery=battery, + gps_accuracy=accuracy, + attributes=attrs + ) + + hass.data[DATA_KEY] = async_dispatcher_connect( + hass, TRACKER_UPDATE, _set_location + ) + return True + + +async def async_unload_entry(hass: HomeAssistantType, entry): + """Unload the config entry and remove the dispatcher connection.""" + hass.data[DATA_KEY]() + return True diff --git a/tests/components/gpslogger/test_init.py b/tests/components/gpslogger/test_init.py index cf818e54911..db1ae655c25 100644 --- a/tests/components/gpslogger/test_init.py +++ b/tests/components/gpslogger/test_init.py @@ -2,15 +2,17 @@ from unittest.mock import patch, Mock import pytest +from homeassistant.helpers.dispatcher import DATA_DISPATCHER from homeassistant import data_entry_flow -from homeassistant.components import zone +from homeassistant.components import zone, gpslogger from homeassistant.components.device_tracker import \ DOMAIN as DEVICE_TRACKER_DOMAIN -from homeassistant.components.gpslogger import DOMAIN +from homeassistant.components.gpslogger import DOMAIN, TRACKER_UPDATE from homeassistant.const import HTTP_OK, HTTP_UNPROCESSABLE_ENTITY, \ - STATE_HOME, STATE_NOT_HOME + STATE_HOME, STATE_NOT_HOME, CONF_WEBHOOK_ID from homeassistant.setup import async_setup_component +from tests.common import MockConfigEntry HOME_LATITUDE = 37.239622 HOME_LONGITUDE = -115.815811 @@ -164,3 +166,21 @@ async def test_enter_with_attrs(hass, gpslogger_client, webhook_id): assert 102.0 == state.attributes['altitude'] assert 'gps' == state.attributes['provider'] assert 'running' == state.attributes['activity'] + + +@pytest.mark.xfail( + reason='The device_tracker component does not support unloading yet.' +) +async def test_load_unload_entry(hass): + """Test that the appropriate dispatch signals are added and removed.""" + entry = MockConfigEntry(domain=DOMAIN, data={ + CONF_WEBHOOK_ID: 'gpslogger_test' + }) + + await gpslogger.async_setup_entry(hass, entry) + await hass.async_block_till_done() + assert 1 == len(hass.data[DATA_DISPATCHER][TRACKER_UPDATE]) + + await gpslogger.async_unload_entry(hass, entry) + await hass.async_block_till_done() + assert 0 == len(hass.data[DATA_DISPATCHER][TRACKER_UPDATE]) From 29984efd8c5dc8c8ed41ae5ec045841c786eed98 Mon Sep 17 00:00:00 2001 From: David Lie Date: Mon, 28 Jan 2019 02:07:39 -0500 Subject: [PATCH 139/222] Use more up-to-date version of pyfoscam library (#20419) * Change foscam python library to pyfoscam, which is more up to date and has several critical bug fixes. * Update requirements_all.txt to match. * Inserting automatically generated requirements.txt --- homeassistant/components/camera/foscam.py | 4 ++-- requirements_all.txt | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/camera/foscam.py b/homeassistant/components/camera/foscam.py index ceec57f7755..173e115cbaf 100644 --- a/homeassistant/components/camera/foscam.py +++ b/homeassistant/components/camera/foscam.py @@ -15,7 +15,7 @@ from homeassistant.helpers import config_validation as cv _LOGGER = logging.getLogger(__name__) -REQUIREMENTS = ['libpyfoscam==1.0'] +REQUIREMENTS = ['pyfoscam==1.2'] CONF_IP = 'ip' @@ -43,7 +43,7 @@ class FoscamCam(Camera): def __init__(self, device_info): """Initialize a Foscam camera.""" - from libpyfoscam import FoscamCamera + from foscam import FoscamCamera super(FoscamCam, self).__init__() diff --git a/requirements_all.txt b/requirements_all.txt index 32ce1905320..58c9e81d272 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -605,9 +605,6 @@ libnacl==1.6.1 # homeassistant.components.dyson libpurecoollink==0.4.2 -# homeassistant.components.camera.foscam -libpyfoscam==1.0 - # homeassistant.components.device_tracker.mikrotik librouteros==2.2.0 @@ -1017,6 +1014,9 @@ pyflunearyou==1.0.1 # homeassistant.components.light.futurenow pyfnip==0.2 +# homeassistant.components.camera.foscam +pyfoscam==1.2 + # homeassistant.components.fritzbox pyfritzhome==0.4.0 From f33e432cab6584b33f39ee6c2d0ec9b5891bbf7c Mon Sep 17 00:00:00 2001 From: Christian Biamont Date: Mon, 28 Jan 2019 12:30:15 +0100 Subject: [PATCH 140/222] Reset Brottsplatskartan incident types every day (#20117) * Reset the incident types count every day * Remove functionality that was never implemented We don't need to keep track of previous incidents because it's not used anywhere. * Create empty dictionary with a pair of braces: {} --- homeassistant/components/sensor/brottsplatskartan.py | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/sensor/brottsplatskartan.py b/homeassistant/components/sensor/brottsplatskartan.py index dc338e6b84a..c308f2eac53 100644 --- a/homeassistant/components/sensor/brottsplatskartan.py +++ b/homeassistant/components/sensor/brottsplatskartan.py @@ -69,11 +69,9 @@ class BrottsplatskartanSensor(Entity): def __init__(self, bpk, name): """Initialize the Brottsplatskartan sensor.""" - import brottsplatskartan - self._attributes = {ATTR_ATTRIBUTION: brottsplatskartan.ATTRIBUTION} + self._attributes = {} self._brottsplatskartan = bpk self._name = name - self._previous_incidents = set() self._state = None @property @@ -93,6 +91,7 @@ class BrottsplatskartanSensor(Entity): def update(self): """Update device state.""" + import brottsplatskartan incident_counts = defaultdict(int) incidents = self._brottsplatskartan.get_incidents() @@ -100,13 +99,10 @@ class BrottsplatskartanSensor(Entity): _LOGGER.debug("Problems fetching incidents") return - if len(incidents) < len(self._previous_incidents): - self._previous_incidents = set() - for incident in incidents: incident_type = incident.get('title_type') incident_counts[incident_type] += 1 - self._previous_incidents.add(incident.get('id')) + self._attributes = {ATTR_ATTRIBUTION: brottsplatskartan.ATTRIBUTION} self._attributes.update(incident_counts) self._state = len(incidents) From 995758b8ac3c7794b90f3ab077a432a029d2df34 Mon Sep 17 00:00:00 2001 From: Jc2k Date: Mon, 28 Jan 2019 12:20:32 +0000 Subject: [PATCH 141/222] Add more HomeKit controller tests (#20515) * homekit_controller tests: automatically find entity ids in tests Some entities use dynamic ids because of the nature of the test fakes it is hard to predict the name of the entity that will be created. This inspects the EntityComponent of the domain to find the freshly created entity. * homekit_controller: Tests can now define their own Service models. All existing tests use models as defined upstream. But upstream only defines a few service models. This adds a generic model helper for creating test service/characteristic models. * homekit_controller: Add cover tests * homekit_controller: Add lock tests * homekit_controller: Add alarm_control_panel tests * homekit_controller: Update light tests for color_temp. * Revert "homekit_controller tests: automatically find entity ids in tests" This reverts commit 506caa4c3e0814eec637145d7d6eaf2bd642e99e. * homekit_controller: Mock entity name so entity_id is consistent. Also remove spurious subclass overrides that are identical to parent class. * homekit_controler: Make tests less awkward as allowed top level imports --- .../components/homekit_controller/cover.py | 12 +- .../components/homekit_controller/light.py | 2 +- tests/components/homekit_controller/common.py | 49 +++- .../test_alarm_control_panel.py | 79 +++++++ .../homekit_controller/test_cover.py | 213 ++++++++++++++++++ .../homekit_controller/test_light.py | 110 +++++++-- .../homekit_controller/test_lock.py | 59 +++++ 7 files changed, 489 insertions(+), 35 deletions(-) create mode 100644 tests/components/homekit_controller/test_alarm_control_panel.py create mode 100644 tests/components/homekit_controller/test_cover.py create mode 100644 tests/components/homekit_controller/test_lock.py diff --git a/homeassistant/components/homekit_controller/cover.py b/homeassistant/components/homekit_controller/cover.py index cd3bc511291..cf9857edc0a 100644 --- a/homeassistant/components/homekit_controller/cover.py +++ b/homeassistant/components/homekit_controller/cover.py @@ -94,11 +94,6 @@ class HomeKitGarageDoorCover(HomeKitEntity, CoverDevice): self._chars['name'] = characteristic['iid'] self._name = characteristic['value'] - @property - def name(self): - """Return the name of the cover.""" - return self._name - @property def available(self): """Return True if entity is available.""" @@ -206,7 +201,7 @@ class HomeKitWindowCover(HomeKitEntity, CoverDevice): self._chars['vertical-tilt.target'] = \ characteristic['iid'] elif ctype == "horizontal-tilt.target": - self._chars['vertical-tilt.target'] = \ + self._chars['horizontal-tilt.target'] = \ characteristic['iid'] elif ctype == "obstruction-detected": self._chars['obstruction-detected'] = characteristic['iid'] @@ -216,11 +211,6 @@ class HomeKitWindowCover(HomeKitEntity, CoverDevice): if 'value' in characteristic: self._name = characteristic['value'] - @property - def name(self): - """Return the name of the cover.""" - return self._name - @property def supported_features(self): """Flag supported features.""" diff --git a/homeassistant/components/homekit_controller/light.py b/homeassistant/components/homekit_controller/light.py index 7c8119f6e89..ef0ffa057fd 100644 --- a/homeassistant/components/homekit_controller/light.py +++ b/homeassistant/components/homekit_controller/light.py @@ -52,7 +52,7 @@ class HomeKitLight(HomeKitEntity, Light): self._features |= SUPPORT_BRIGHTNESS self._brightness = characteristic['value'] elif ctype == 'color-temperature': - self._chars['color_temperature'] = characteristic['iid'] + self._chars['color-temperature'] = characteristic['iid'] self._features |= SUPPORT_COLOR_TEMP self._color_temperature = characteristic['value'] elif ctype == "hue": diff --git a/tests/components/homekit_controller/common.py b/tests/components/homekit_controller/common.py index f96b046b4cf..8bb69e18450 100644 --- a/tests/components/homekit_controller/common.py +++ b/tests/components/homekit_controller/common.py @@ -2,8 +2,13 @@ from datetime import timedelta from unittest import mock +from homekit.model.services import AbstractService, ServicesTypes +from homekit.model.characteristics import ( + AbstractCharacteristic, CharacteristicPermissions, CharacteristicsTypes) +from homekit.model import Accessory, get_id + from homeassistant.components.homekit_controller import ( - DOMAIN, HOMEKIT_ACCESSORY_DISPATCH, SERVICE_HOMEKIT) + DOMAIN, HOMEKIT_ACCESSORY_DISPATCH, SERVICE_HOMEKIT, HomeKitEntity) from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util from tests.common import async_fire_time_changed, fire_service_discovered @@ -75,9 +80,6 @@ class Helper: def __init__(self, hass, entity_id, pairing, accessory): """Create a helper for a given accessory/entity.""" - from homekit.model.services import ServicesTypes - from homekit.model.characteristics import CharacteristicsTypes - self.hass = hass self.entity_id = entity_id self.pairing = pairing @@ -101,11 +103,39 @@ class Helper: return state +class FakeCharacteristic(AbstractCharacteristic): + """ + A model of a generic HomeKit characteristic. + + Base is abstract and can't be instanced directly so this subclass is + needed even though it doesn't add any methods. + """ + + pass + + +class FakeService(AbstractService): + """A model of a generic HomeKit service.""" + + def __init__(self, service_name): + """Create a fake service by its short form HAP spec name.""" + char_type = ServicesTypes.get_uuid(service_name) + super().__init__(char_type, get_id()) + + def add_characteristic(self, name): + """Add a characteristic to this service by name.""" + full_name = 'public.hap.characteristic.' + name + char = FakeCharacteristic(get_id(), full_name, None) + char.perms = [ + CharacteristicPermissions.paired_read, + CharacteristicPermissions.paired_write + ] + self.characteristics.append(char) + return char + + async def setup_test_component(hass, services): """Load a fake homekit accessory based on a homekit accessory model.""" - from homekit.model import Accessory - from homekit.model.services import ServicesTypes - domain = None for service in services: service_name = ServicesTypes.get_short(service.type) @@ -138,7 +168,8 @@ async def setup_test_component(hass, services): } } - fire_service_discovered(hass, SERVICE_HOMEKIT, discovery_info) - await hass.async_block_till_done() + with mock.patch.object(HomeKitEntity, 'name', 'testdevice'): + fire_service_discovered(hass, SERVICE_HOMEKIT, discovery_info) + await hass.async_block_till_done() return Helper(hass, '.'.join((domain, 'testdevice')), pairing, accessory) diff --git a/tests/components/homekit_controller/test_alarm_control_panel.py b/tests/components/homekit_controller/test_alarm_control_panel.py new file mode 100644 index 00000000000..0164da5200f --- /dev/null +++ b/tests/components/homekit_controller/test_alarm_control_panel.py @@ -0,0 +1,79 @@ +"""Basic checks for HomeKitalarm_control_panel.""" +from tests.components.homekit_controller.common import ( + FakeService, setup_test_component) + +CURRENT_STATE = ('security-system', 'security-system-state.current') +TARGET_STATE = ('security-system', 'security-system-state.target') + + +def create_security_system_service(): + """Define a security-system characteristics as per page 219 of HAP spec.""" + service = FakeService('public.hap.service.security-system') + + cur_state = service.add_characteristic('security-system-state.current') + cur_state.value = 0 + + targ_state = service.add_characteristic('security-system-state.target') + targ_state.value = 0 + + # According to the spec, a battery-level characteristic is normally + # part of a seperate service. However as the code was written (which + # predates this test) the battery level would have to be part of the lock + # service as it is here. + targ_state = service.add_characteristic('battery-level') + targ_state.value = 50 + + return service + + +async def test_switch_change_alarm_state(hass, utcnow): + """Test that we can turn a HomeKit alarm on and off again.""" + alarm_control_panel = create_security_system_service() + helper = await setup_test_component(hass, [alarm_control_panel]) + + await hass.services.async_call('alarm_control_panel', 'alarm_arm_home', { + 'entity_id': 'alarm_control_panel.testdevice', + }, blocking=True) + assert helper.characteristics[TARGET_STATE].value == 0 + + await hass.services.async_call('alarm_control_panel', 'alarm_arm_away', { + 'entity_id': 'alarm_control_panel.testdevice', + }, blocking=True) + assert helper.characteristics[TARGET_STATE].value == 1 + + await hass.services.async_call('alarm_control_panel', 'alarm_arm_night', { + 'entity_id': 'alarm_control_panel.testdevice', + }, blocking=True) + assert helper.characteristics[TARGET_STATE].value == 2 + + await hass.services.async_call('alarm_control_panel', 'alarm_disarm', { + 'entity_id': 'alarm_control_panel.testdevice', + }, blocking=True) + assert helper.characteristics[TARGET_STATE].value == 3 + + +async def test_switch_read_alarm_state(hass, utcnow): + """Test that we can read the state of a HomeKit alarm accessory.""" + alarm_control_panel = create_security_system_service() + helper = await setup_test_component(hass, [alarm_control_panel]) + + helper.characteristics[CURRENT_STATE].value = 0 + state = await helper.poll_and_get_state() + assert state.state == 'armed_home' + assert state.attributes['battery_level'] == 50 + + helper.characteristics[CURRENT_STATE].value = 1 + state = await helper.poll_and_get_state() + assert state.state == 'armed_away' + + helper.characteristics[CURRENT_STATE].value = 2 + state = await helper.poll_and_get_state() + assert state.state == 'armed_night' + + helper.characteristics[CURRENT_STATE].value = 3 + state = await helper.poll_and_get_state() + assert state.state == 'disarmed' + + helper.characteristics[CURRENT_STATE].value = 4 + state = await helper.poll_and_get_state() + assert state.state == 'triggered' diff --git a/tests/components/homekit_controller/test_cover.py b/tests/components/homekit_controller/test_cover.py new file mode 100644 index 00000000000..4a0c244492e --- /dev/null +++ b/tests/components/homekit_controller/test_cover.py @@ -0,0 +1,213 @@ +"""Basic checks for HomeKitalarm_control_panel.""" +from tests.components.homekit_controller.common import ( + FakeService, setup_test_component) + +POSITION_STATE = ('window-covering', 'position.state') +POSITION_CURRENT = ('window-covering', 'position.current') +POSITION_TARGET = ('window-covering', 'position.target') + +H_TILT_CURRENT = ('window-covering', 'horizontal-tilt.current') +H_TILT_TARGET = ('window-covering', 'horizontal-tilt.target') + +V_TILT_CURRENT = ('window-covering', 'vertical-tilt.current') +V_TILT_TARGET = ('window-covering', 'vertical-tilt.target') + +WINDOW_OBSTRUCTION = ('window-covering', 'obstruction-detected') + +DOOR_CURRENT = ('garage-door-opener', 'door-state.current') +DOOR_TARGET = ('garage-door-opener', 'door-state.target') +DOOR_OBSTRUCTION = ('garage-door-opener', 'obstruction-detected') + + +def create_window_covering_service(): + """Define a window-covering characteristics as per page 219 of HAP spec.""" + service = FakeService('public.hap.service.window-covering') + + cur_state = service.add_characteristic('position.current') + cur_state.value = 0 + + targ_state = service.add_characteristic('position.target') + targ_state.value = 0 + + position_state = service.add_characteristic('position.state') + position_state.value = 0 + + position_hold = service.add_characteristic('position.hold') + position_hold.value = 0 + + obstruction = service.add_characteristic('obstruction-detected') + obstruction.value = False + + name = service.add_characteristic('name') + name.value = "Window Cover 1" + + return service + + +def create_window_covering_service_with_h_tilt(): + """Define a window-covering characteristics as per page 219 of HAP spec.""" + service = create_window_covering_service() + + tilt_current = service.add_characteristic('horizontal-tilt.current') + tilt_current.value = 0 + + tilt_target = service.add_characteristic('horizontal-tilt.target') + tilt_target.value = 0 + + return service + + +def create_window_covering_service_with_v_tilt(): + """Define a window-covering characteristics as per page 219 of HAP spec.""" + service = create_window_covering_service() + + tilt_current = service.add_characteristic('vertical-tilt.current') + tilt_current.value = 0 + + tilt_target = service.add_characteristic('vertical-tilt.target') + tilt_target.value = 0 + + return service + + +async def test_change_window_cover_state(hass, utcnow): + """Test that we can turn a HomeKit alarm on and off again.""" + window_cover = create_window_covering_service() + helper = await setup_test_component(hass, [window_cover]) + + await hass.services.async_call('cover', 'open_cover', { + 'entity_id': helper.entity_id, + }, blocking=True) + assert helper.characteristics[POSITION_TARGET].value == 100 + + await hass.services.async_call('cover', 'close_cover', { + 'entity_id': helper.entity_id, + }, blocking=True) + assert helper.characteristics[POSITION_TARGET].value == 0 + + +async def test_read_window_cover_state(hass, utcnow): + """Test that we can read the state of a HomeKit alarm accessory.""" + window_cover = create_window_covering_service() + helper = await setup_test_component(hass, [window_cover]) + + helper.characteristics[POSITION_STATE].value = 0 + state = await helper.poll_and_get_state() + assert state.state == 'opening' + + helper.characteristics[POSITION_STATE].value = 1 + state = await helper.poll_and_get_state() + assert state.state == 'closing' + + helper.characteristics[POSITION_STATE].value = 2 + state = await helper.poll_and_get_state() + assert state.state == 'closed' + + helper.characteristics[WINDOW_OBSTRUCTION].value = True + state = await helper.poll_and_get_state() + assert state.attributes['obstruction-detected'] is True + + +async def test_read_window_cover_tilt_horizontal(hass, utcnow): + """Test that horizontal tilt is handled correctly.""" + window_cover = create_window_covering_service_with_h_tilt() + helper = await setup_test_component(hass, [window_cover]) + + helper.characteristics[H_TILT_CURRENT].value = 75 + state = await helper.poll_and_get_state() + assert state.attributes['current_tilt_position'] == 75 + + +async def test_read_window_cover_tilt_vertical(hass, utcnow): + """Test that vertical tilt is handled correctly.""" + window_cover = create_window_covering_service_with_v_tilt() + helper = await setup_test_component(hass, [window_cover]) + + helper.characteristics[V_TILT_CURRENT].value = 75 + state = await helper.poll_and_get_state() + assert state.attributes['current_tilt_position'] == 75 + + +async def test_write_window_cover_tilt_horizontal(hass, utcnow): + """Test that horizontal tilt is written correctly.""" + window_cover = create_window_covering_service_with_h_tilt() + helper = await setup_test_component(hass, [window_cover]) + + await hass.services.async_call('cover', 'set_cover_tilt_position', { + 'entity_id': helper.entity_id, + 'tilt_position': 90 + }, blocking=True) + assert helper.characteristics[H_TILT_TARGET].value == 90 + + +async def test_write_window_cover_tilt_vertical(hass, utcnow): + """Test that vertical tilt is written correctly.""" + window_cover = create_window_covering_service_with_v_tilt() + helper = await setup_test_component(hass, [window_cover]) + + await hass.services.async_call('cover', 'set_cover_tilt_position', { + 'entity_id': helper.entity_id, + 'tilt_position': 90 + }, blocking=True) + assert helper.characteristics[V_TILT_TARGET].value == 90 + + +def create_garage_door_opener_service(): + """Define a garage-door-opener chars as per page 217 of HAP spec.""" + service = FakeService('public.hap.service.garage-door-opener') + + cur_state = service.add_characteristic('door-state.current') + cur_state.value = 0 + + targ_state = service.add_characteristic('door-state.target') + targ_state.value = 0 + + obstruction = service.add_characteristic('obstruction-detected') + obstruction.value = False + + name = service.add_characteristic('name') + name.value = "Garage Door Opener 1" + + return service + + +async def test_change_door_state(hass, utcnow): + """Test that we can turn open and close a HomeKit garage door.""" + door = create_garage_door_opener_service() + helper = await setup_test_component(hass, [door]) + + await hass.services.async_call('cover', 'open_cover', { + 'entity_id': helper.entity_id, + }, blocking=True) + assert helper.characteristics[DOOR_TARGET].value == 0 + + await hass.services.async_call('cover', 'close_cover', { + 'entity_id': helper.entity_id, + }, blocking=True) + assert helper.characteristics[DOOR_TARGET].value == 1 + + +async def test_read_door_state(hass, utcnow): + """Test that we can read the state of a HomeKit garage door.""" + door = create_garage_door_opener_service() + helper = await setup_test_component(hass, [door]) + + helper.characteristics[DOOR_CURRENT].value = 0 + state = await helper.poll_and_get_state() + assert state.state == 'open' + + helper.characteristics[DOOR_CURRENT].value = 1 + state = await helper.poll_and_get_state() + assert state.state == 'closed' + + helper.characteristics[DOOR_CURRENT].value = 2 + state = await helper.poll_and_get_state() + assert state.state == 'opening' + + helper.characteristics[DOOR_CURRENT].value = 3 + state = await helper.poll_and_get_state() + assert state.state == 'closing' + + helper.characteristics[DOOR_OBSTRUCTION].value = True + state = await helper.poll_and_get_state() + assert state.attributes['obstruction-detected'] is True diff --git a/tests/components/homekit_controller/test_light.py b/tests/components/homekit_controller/test_light.py index 152940818c1..0509d70c0b9 100644 --- a/tests/components/homekit_controller/test_light.py +++ b/tests/components/homekit_controller/test_light.py @@ -1,46 +1,128 @@ """Basic checks for HomeKitSwitch.""" from tests.components.homekit_controller.common import ( - setup_test_component) + FakeService, setup_test_component) + + +LIGHT_ON = ('lightbulb', 'on') +LIGHT_BRIGHTNESS = ('lightbulb', 'brightness') +LIGHT_HUE = ('lightbulb', 'hue') +LIGHT_SATURATION = ('lightbulb', 'saturation') +LIGHT_COLOR_TEMP = ('lightbulb', 'color-temperature') + + +def create_lightbulb_service(): + """Define lightbulb characteristics.""" + service = FakeService('public.hap.service.lightbulb') + + on_char = service.add_characteristic('on') + on_char.value = 0 + + brightness = service.add_characteristic('brightness') + brightness.value = 0 + + return service + + +def create_lightbulb_service_with_hs(): + """Define a lightbulb service with hue + saturation.""" + service = create_lightbulb_service() + + hue = service.add_characteristic('hue') + hue.value = 0 + + saturation = service.add_characteristic('saturation') + saturation.value = 0 + + return service + + +def create_lightbulb_service_with_color_temp(): + """Define a lightbulb service with color temp.""" + service = create_lightbulb_service() + + color_temp = service.add_characteristic('color-temperature') + color_temp.value = 0 + + return service async def test_switch_change_light_state(hass, utcnow): """Test that we can turn a HomeKit light on and off again.""" - from homekit.model.services import BHSLightBulbService - - helper = await setup_test_component(hass, [BHSLightBulbService()]) + bulb = create_lightbulb_service_with_hs() + helper = await setup_test_component(hass, [bulb]) await hass.services.async_call('light', 'turn_on', { 'entity_id': 'light.testdevice', 'brightness': 255, 'hs_color': [4, 5], }, blocking=True) - assert helper.characteristics[('lightbulb', 'on')].value == 1 - assert helper.characteristics[('lightbulb', 'brightness')].value == 100 - assert helper.characteristics[('lightbulb', 'hue')].value == 4 - assert helper.characteristics[('lightbulb', 'saturation')].value == 5 + + assert helper.characteristics[LIGHT_ON].value == 1 + assert helper.characteristics[LIGHT_BRIGHTNESS].value == 100 + assert helper.characteristics[LIGHT_HUE].value == 4 + assert helper.characteristics[LIGHT_SATURATION].value == 5 await hass.services.async_call('light', 'turn_off', { 'entity_id': 'light.testdevice', }, blocking=True) - assert helper.characteristics[('lightbulb', 'on')].value == 0 + assert helper.characteristics[LIGHT_ON].value == 0 + + +async def test_switch_change_light_state_color_temp(hass, utcnow): + """Test that we can turn change color_temp.""" + bulb = create_lightbulb_service_with_color_temp() + helper = await setup_test_component(hass, [bulb]) + + await hass.services.async_call('light', 'turn_on', { + 'entity_id': 'light.testdevice', + 'brightness': 255, + 'color_temp': 400, + }, blocking=True) + assert helper.characteristics[LIGHT_ON].value == 1 + assert helper.characteristics[LIGHT_BRIGHTNESS].value == 100 + assert helper.characteristics[LIGHT_COLOR_TEMP].value == 400 async def test_switch_read_light_state(hass, utcnow): """Test that we can read the state of a HomeKit light accessory.""" - from homekit.model.services import BHSLightBulbService - - helper = await setup_test_component(hass, [BHSLightBulbService()]) + bulb = create_lightbulb_service_with_hs() + helper = await setup_test_component(hass, [bulb]) # Initial state is that the light is off state = await helper.poll_and_get_state() assert state.state == 'off' # Simulate that someone switched on the device in the real world not via HA - helper.characteristics[('lightbulb', 'on')].set_value(True) + helper.characteristics[LIGHT_ON].set_value(True) + helper.characteristics[LIGHT_BRIGHTNESS].value = 100 + helper.characteristics[LIGHT_HUE].value = 4 + helper.characteristics[LIGHT_SATURATION].value = 5 state = await helper.poll_and_get_state() assert state.state == 'on' + assert state.attributes['brightness'] == 255 + assert state.attributes['hs_color'] == (4, 5) # Simulate that device switched off in the real world not via HA - helper.characteristics[('lightbulb', 'on')].set_value(False) + helper.characteristics[LIGHT_ON].set_value(False) state = await helper.poll_and_get_state() assert state.state == 'off' + + +async def test_switch_read_light_state_color_temp(hass, utcnow): + """Test that we can read the color_temp of a light accessory.""" + bulb = create_lightbulb_service_with_color_temp() + helper = await setup_test_component(hass, [bulb]) + + # Initial state is that the light is off + state = await helper.poll_and_get_state() + assert state.state == 'off' + + # Simulate that someone switched on the device in the real world not via HA + helper.characteristics[LIGHT_ON].set_value(True) + helper.characteristics[LIGHT_BRIGHTNESS].value = 100 + helper.characteristics[LIGHT_COLOR_TEMP].value = 400 + + state = await helper.poll_and_get_state() + assert state.state == 'on' + assert state.attributes['brightness'] == 255 + assert state.attributes['color_temp'] == 400 diff --git a/tests/components/homekit_controller/test_lock.py b/tests/components/homekit_controller/test_lock.py new file mode 100644 index 00000000000..3347e51c888 --- /dev/null +++ b/tests/components/homekit_controller/test_lock.py @@ -0,0 +1,59 @@ +"""Basic checks for HomeKitLock.""" +from tests.components.homekit_controller.common import ( + FakeService, setup_test_component) + +LOCK_CURRENT_STATE = ('lock-mechanism', 'lock-mechanism.current-state') +LOCK_TARGET_STATE = ('lock-mechanism', 'lock-mechanism.target-state') + + +def create_lock_service(): + """Define a lock characteristics as per page 219 of HAP spec.""" + service = FakeService('public.hap.service.lock-mechanism') + + cur_state = service.add_characteristic('lock-mechanism.current-state') + cur_state.value = 0 + + targ_state = service.add_characteristic('lock-mechanism.target-state') + targ_state.value = 0 + + # According to the spec, a battery-level characteristic is normally + # part of a seperate service. However as the code was written (which + # predates this test) the battery level would have to be part of the lock + # service as it is here. + targ_state = service.add_characteristic('battery-level') + targ_state.value = 50 + + return service + + +async def test_switch_change_lock_state(hass, utcnow): + """Test that we can turn a HomeKit lock on and off again.""" + lock = create_lock_service() + helper = await setup_test_component(hass, [lock]) + + await hass.services.async_call('lock', 'lock', { + 'entity_id': 'lock.testdevice', + }, blocking=True) + assert helper.characteristics[LOCK_TARGET_STATE].value == 1 + + await hass.services.async_call('lock', 'unlock', { + 'entity_id': 'lock.testdevice', + }, blocking=True) + assert helper.characteristics[LOCK_TARGET_STATE].value == 0 + + +async def test_switch_read_lock_state(hass, utcnow): + """Test that we can read the state of a HomeKit lock accessory.""" + lock = create_lock_service() + helper = await setup_test_component(hass, [lock]) + + helper.characteristics[LOCK_CURRENT_STATE].value = 0 + helper.characteristics[LOCK_TARGET_STATE].value = 0 + state = await helper.poll_and_get_state() + assert state.state == 'unlocked' + assert state.attributes['battery_level'] == 50 + + helper.characteristics[LOCK_CURRENT_STATE].value = 1 + helper.characteristics[LOCK_TARGET_STATE].value = 1 + state = await helper.poll_and_get_state() + assert state.state == 'locked' From 41c1997b8808f700165068ba4a11bac7b2331558 Mon Sep 17 00:00:00 2001 From: Jc2k Date: Mon, 28 Jan 2019 16:21:20 +0000 Subject: [PATCH 142/222] Homekit controller BLE groundwork (#20538) * Define the characteristics to poll (or subscribe to) up front * Configure characteristics immediately instead of during first poll * Do as much cover configuration upfront as possible * Remove test workaround as no longer needed * Remove switch code that is already handled by HomeKitEntity * Remove lock code already handled by HomeKitEntity * Remove light code already handled by HomeKitEntity * Remove alarm code already handled by HomeKitEntity * Remove climate code already handled by HomeKitEntity --- .../components/homekit_controller/__init__.py | 60 +++++++++++++++++ .../homekit_controller/alarm_control_panel.py | 16 +++-- .../components/homekit_controller/climate.py | 36 ++++++---- .../components/homekit_controller/cover.py | 66 ++++++++++--------- .../components/homekit_controller/light.py | 33 +++++++--- .../components/homekit_controller/lock.py | 16 +++-- .../components/homekit_controller/switch.py | 11 +++- tests/components/homekit_controller/common.py | 7 +- .../homekit_controller/test_cover.py | 4 +- 9 files changed, 177 insertions(+), 72 deletions(-) diff --git a/homeassistant/components/homekit_controller/__init__.py b/homeassistant/components/homekit_controller/__init__.py index 17cff702b70..2c59f062bcc 100644 --- a/homeassistant/components/homekit_controller/__init__.py +++ b/homeassistant/components/homekit_controller/__init__.py @@ -68,6 +68,11 @@ def get_serial(accessory): return None +def escape_characteristic_name(char_name): + """Escape any dash or dots in a characteristics name.""" + return char_name.replace('-', '_').replace('.', '_') + + class HKDevice(): """HomeKit device.""" @@ -193,6 +198,57 @@ class HomeKitEntity(Entity): self._address = "homekit-{}-{}".format(devinfo['serial'], self._iid) self._features = 0 self._chars = {} + self.setup() + + def setup(self): + """Configure an entity baed on its HomeKit characterstics metadata.""" + # pylint: disable=import-error + from homekit.model.characteristics import CharacteristicsTypes + + pairing_data = self._accessory.pairing.pairing_data + + get_uuid = CharacteristicsTypes.get_uuid + characteristic_types = [ + get_uuid(c) for c in self.get_characteristic_types() + ] + + self._chars_to_poll = [] + self._chars = {} + self._char_names = {} + + for accessory in pairing_data.get('accessories', []): + if accessory['aid'] != self._aid: + continue + for service in accessory['services']: + if service['iid'] != self._iid: + continue + for char in service['characteristics']: + uuid = CharacteristicsTypes.get_uuid(char['type']) + if uuid not in characteristic_types: + continue + self._setup_characteristic(char) + + def _setup_characteristic(self, char): + """Configure an entity based on a HomeKit characteristics metadata.""" + # pylint: disable=import-error + from homekit.model.characteristics import CharacteristicsTypes + + # Build up a list of (aid, iid) tuples to poll on update() + self._chars_to_poll.append((self._aid, char['iid'])) + + # Build a map of ctype -> iid + short_name = CharacteristicsTypes.get_short(char['type']) + self._chars[short_name] = char['iid'] + self._char_names[char['iid']] = short_name + + # Callback to allow entity to configure itself based on this + # characteristics metadata (valid values, value ranges, features, etc) + setup_fn_name = escape_characteristic_name(short_name) + setup_fn = getattr(self, '_setup_{}'.format(setup_fn_name), None) + if not setup_fn: + return + # pylint: disable=E1102 + setup_fn(char) def update(self): """Obtain a HomeKit device's state.""" @@ -228,6 +284,10 @@ class HomeKitEntity(Entity): """Return True if entity is available.""" return self._accessory.pairing is not None + def get_characteristic_types(self): + """Define the homekit characteristics the entity cares about.""" + raise NotImplementedError + def update_characteristics(self, characteristics): """Synchronise a HomeKit device state with Home Assistant.""" raise NotImplementedError diff --git a/homeassistant/components/homekit_controller/alarm_control_panel.py b/homeassistant/components/homekit_controller/alarm_control_panel.py index cc760a851cf..984be8e0c3b 100644 --- a/homeassistant/components/homekit_controller/alarm_control_panel.py +++ b/homeassistant/components/homekit_controller/alarm_control_panel.py @@ -54,6 +54,16 @@ class HomeKitAlarmControlPanel(HomeKitEntity, AlarmControlPanel): self._state = None self._battery_level = None + def get_characteristic_types(self): + """Define the homekit characteristics the entity cares about.""" + # pylint: disable=import-error + from homekit.model.characteristics import CharacteristicsTypes + return [ + CharacteristicsTypes.SECURITY_SYSTEM_STATE_CURRENT, + CharacteristicsTypes.SECURITY_SYSTEM_STATE_TARGET, + CharacteristicsTypes.BATTERY_LEVEL, + ] + def update_characteristics(self, characteristics): """Synchronise the Alarm Control Panel state with Home Assistant.""" # pylint: disable=import-error @@ -63,14 +73,8 @@ class HomeKitAlarmControlPanel(HomeKitEntity, AlarmControlPanel): ctype = characteristic['type'] ctype = CharacteristicsTypes.get_short(ctype) if ctype == "security-system-state.current": - self._chars['security-system-state.current'] = \ - characteristic['iid'] self._state = CURRENT_STATE_MAP[characteristic['value']] - elif ctype == "security-system-state.target": - self._chars['security-system-state.target'] = \ - characteristic['iid'] elif ctype == "battery-level": - self._chars['battery-level'] = characteristic['iid'] self._battery_level = characteristic['value'] @property diff --git a/homeassistant/components/homekit_controller/climate.py b/homeassistant/components/homekit_controller/climate.py index 484c064d53d..042a499c55a 100644 --- a/homeassistant/components/homekit_controller/climate.py +++ b/homeassistant/components/homekit_controller/climate.py @@ -49,6 +49,29 @@ class HomeKitClimateDevice(HomeKitEntity, ClimateDevice): self._current_temp = None self._target_temp = None + def get_characteristic_types(self): + """Define the homekit characteristics the entity cares about.""" + # pylint: disable=import-error + from homekit.model.characteristics import CharacteristicsTypes + return [ + CharacteristicsTypes.HEATING_COOLING_CURRENT, + CharacteristicsTypes.HEATING_COOLING_TARGET, + CharacteristicsTypes.TEMPERATURE_CURRENT, + CharacteristicsTypes.TEMPERATURE_TARGET, + ] + + def _setup_heating_cooling_target(self, characteristic): + self._features |= SUPPORT_OPERATION_MODE + + valid_values = characteristic.get( + 'valid-values', DEFAULT_VALID_MODES) + self._valid_modes = [ + MODE_HOMEKIT_TO_HASS.get(mode) for mode in valid_values + ] + + def _setup_temperature_target(self, characteristic): + self._features |= SUPPORT_TARGET_TEMPERATURE + def update_characteristics(self, characteristics): """Synchronise device state with Home Assistant.""" # pylint: disable=import-error @@ -60,20 +83,11 @@ class HomeKitClimateDevice(HomeKitEntity, ClimateDevice): self._state = MODE_HOMEKIT_TO_HASS.get( characteristic['value']) if ctype == CharacteristicsTypes.HEATING_COOLING_TARGET: - self._chars['target_mode'] = characteristic['iid'] - self._features |= SUPPORT_OPERATION_MODE self._current_mode = MODE_HOMEKIT_TO_HASS.get( characteristic['value']) - - valid_values = characteristic.get( - 'valid-values', DEFAULT_VALID_MODES) - self._valid_modes = [MODE_HOMEKIT_TO_HASS.get( - mode) for mode in valid_values] elif ctype == CharacteristicsTypes.TEMPERATURE_CURRENT: self._current_temp = characteristic['value'] elif ctype == CharacteristicsTypes.TEMPERATURE_TARGET: - self._chars['target_temp'] = characteristic['iid'] - self._features |= SUPPORT_TARGET_TEMPERATURE self._target_temp = characteristic['value'] def set_temperature(self, **kwargs): @@ -81,14 +95,14 @@ class HomeKitClimateDevice(HomeKitEntity, ClimateDevice): temp = kwargs.get(ATTR_TEMPERATURE) characteristics = [{'aid': self._aid, - 'iid': self._chars['target_temp'], + 'iid': self._chars['temperature.target'], 'value': temp}] self.put_characteristics(characteristics) def set_operation_mode(self, operation_mode): """Set new target operation mode.""" characteristics = [{'aid': self._aid, - 'iid': self._chars['target_mode'], + 'iid': self._chars['heating-cooling.target'], 'value': MODE_HASS_TO_HOMEKIT[operation_mode]}] self.put_characteristics(characteristics) diff --git a/homeassistant/components/homekit_controller/cover.py b/homeassistant/components/homekit_controller/cover.py index cf9857edc0a..f9cc2ce435b 100644 --- a/homeassistant/components/homekit_controller/cover.py +++ b/homeassistant/components/homekit_controller/cover.py @@ -62,7 +62,6 @@ class HomeKitGarageDoorCover(HomeKitEntity, CoverDevice): def __init__(self, accessory, discovery_info): """Initialise the Cover.""" super().__init__(accessory, discovery_info) - self._name = None self._state = None self._obstruction_detected = None self.lock_state = None @@ -72,6 +71,20 @@ class HomeKitGarageDoorCover(HomeKitEntity, CoverDevice): """Define this cover as a garage door.""" return 'garage' + def get_characteristic_types(self): + """Define the homekit characteristics the entity cares about.""" + # pylint: disable=import-error + from homekit.model.characteristics import CharacteristicsTypes + return [ + CharacteristicsTypes.DOOR_STATE_CURRENT, + CharacteristicsTypes.DOOR_STATE_TARGET, + CharacteristicsTypes.OBSTRUCTION_DETECTED, + CharacteristicsTypes.NAME, + ] + + def _setup_name(self, char): + self._name = char['value'] + def update_characteristics(self, characteristics): """Synchronise the Cover state with Home Assistant.""" # pylint: disable=import-error @@ -81,18 +94,9 @@ class HomeKitGarageDoorCover(HomeKitEntity, CoverDevice): ctype = characteristic['type'] ctype = CharacteristicsTypes.get_short(ctype) if ctype == "door-state.current": - self._chars['door-state.current'] = \ - characteristic['iid'] self._state = CURRENT_GARAGE_STATE_MAP[characteristic['value']] - elif ctype == "door-state.target": - self._chars['door-state.target'] = \ - characteristic['iid'] elif ctype == "obstruction-detected": - self._chars['obstruction-detected'] = characteristic['iid'] self._obstruction_detected = characteristic['value'] - elif ctype == "name": - self._chars['name'] = characteristic['iid'] - self._name = characteristic['value'] @property def available(self): @@ -151,7 +155,6 @@ class HomeKitWindowCover(HomeKitEntity, CoverDevice): def __init__(self, accessory, discovery_info): """Initialise the Cover.""" super().__init__(accessory, discovery_info) - self._name = None self._state = None self._position = None self._tilt_position = None @@ -164,6 +167,26 @@ class HomeKitWindowCover(HomeKitEntity, CoverDevice): """Return True if entity is available.""" return self._state is not None + def get_characteristic_types(self): + """Define the homekit characteristics the entity cares about.""" + # pylint: disable=import-error + from homekit.model.characteristics import CharacteristicsTypes + return [ + CharacteristicsTypes.POSITION_STATE, + CharacteristicsTypes.POSITION_CURRENT, + CharacteristicsTypes.POSITION_TARGET, + CharacteristicsTypes.POSITION_HOLD, + CharacteristicsTypes.VERTICAL_TILT_CURRENT, + CharacteristicsTypes.VERTICAL_TILT_TARGET, + CharacteristicsTypes.HORIZONTAL_TILT_CURRENT, + CharacteristicsTypes.HORIZONTAL_TILT_TARGET, + CharacteristicsTypes.OBSTRUCTION_DETECTED, + CharacteristicsTypes.NAME, + ] + + def _setup_name(self, char): + self._name = char['value'] + def update_characteristics(self, characteristics): """Synchronise the Cover state with Home Assistant.""" # pylint: disable=import-error @@ -173,43 +196,22 @@ class HomeKitWindowCover(HomeKitEntity, CoverDevice): ctype = characteristic['type'] ctype = CharacteristicsTypes.get_short(ctype) if ctype == "position.state": - self._chars['position.state'] = \ - characteristic['iid'] if 'value' in characteristic: self._state = \ CURRENT_WINDOW_STATE_MAP[characteristic['value']] elif ctype == "position.current": - self._chars['position.current'] = \ - characteristic['iid'] self._position = characteristic['value'] - elif ctype == "position.target": - self._chars['position.target'] = \ - characteristic['iid'] elif ctype == "position.hold": - self._chars['position.hold'] = characteristic['iid'] if 'value' in characteristic: self._hold = characteristic['value'] elif ctype == "vertical-tilt.current": - self._chars['vertical-tilt.current'] = characteristic['iid'] if characteristic['value'] is not None: self._tilt_position = characteristic['value'] elif ctype == "horizontal-tilt.current": - self._chars['horizontal-tilt.current'] = characteristic['iid'] if characteristic['value'] is not None: self._tilt_position = characteristic['value'] - elif ctype == "vertical-tilt.target": - self._chars['vertical-tilt.target'] = \ - characteristic['iid'] - elif ctype == "horizontal-tilt.target": - self._chars['horizontal-tilt.target'] = \ - characteristic['iid'] elif ctype == "obstruction-detected": - self._chars['obstruction-detected'] = characteristic['iid'] self._obstruction_detected = characteristic['value'] - elif ctype == "name": - self._chars['name'] = characteristic['iid'] - if 'value' in characteristic: - self._name = characteristic['value'] @property def supported_features(self): diff --git a/homeassistant/components/homekit_controller/light.py b/homeassistant/components/homekit_controller/light.py index ef0ffa057fd..940e3782379 100644 --- a/homeassistant/components/homekit_controller/light.py +++ b/homeassistant/components/homekit_controller/light.py @@ -36,6 +36,30 @@ class HomeKitLight(HomeKitEntity, Light): self._hue = None self._saturation = None + def get_characteristic_types(self): + """Define the homekit characteristics the entity cares about.""" + # pylint: disable=import-error + from homekit.model.characteristics import CharacteristicsTypes + return [ + CharacteristicsTypes.ON, + CharacteristicsTypes.BRIGHTNESS, + CharacteristicsTypes.COLOR_TEMPERATURE, + CharacteristicsTypes.HUE, + CharacteristicsTypes.SATURATION, + ] + + def _setup_brightness(self, char): + self._features |= SUPPORT_BRIGHTNESS + + def _setup_color_temperature(self, char): + self._features |= SUPPORT_COLOR_TEMP + + def _setup_hue(self, char): + self._features |= SUPPORT_COLOR + + def _setup_saturation(self, char): + self._features |= SUPPORT_COLOR + def update_characteristics(self, characteristics): """Synchronise light state with Home Assistant.""" # pylint: disable=import-error @@ -45,23 +69,14 @@ class HomeKitLight(HomeKitEntity, Light): ctype = characteristic['type'] ctype = CharacteristicsTypes.get_short(ctype) if ctype == "on": - self._chars['on'] = characteristic['iid'] self._on = characteristic['value'] elif ctype == 'brightness': - self._chars['brightness'] = characteristic['iid'] - self._features |= SUPPORT_BRIGHTNESS self._brightness = characteristic['value'] elif ctype == 'color-temperature': - self._chars['color-temperature'] = characteristic['iid'] - self._features |= SUPPORT_COLOR_TEMP self._color_temperature = characteristic['value'] elif ctype == "hue": - self._chars['hue'] = characteristic['iid'] - self._features |= SUPPORT_COLOR self._hue = characteristic['value'] elif ctype == "saturation": - self._chars['saturation'] = characteristic['iid'] - self._features |= SUPPORT_COLOR self._saturation = characteristic['value'] @property diff --git a/homeassistant/components/homekit_controller/lock.py b/homeassistant/components/homekit_controller/lock.py index 910567ed182..53c94317c69 100644 --- a/homeassistant/components/homekit_controller/lock.py +++ b/homeassistant/components/homekit_controller/lock.py @@ -51,6 +51,16 @@ class HomeKitLock(HomeKitEntity, LockDevice): self._name = discovery_info['model'] self._battery_level = None + def get_characteristic_types(self): + """Define the homekit characteristics the entity cares about.""" + # pylint: disable=import-error + from homekit.model.characteristics import CharacteristicsTypes + return [ + CharacteristicsTypes.LOCK_MECHANISM_CURRENT_STATE, + CharacteristicsTypes.LOCK_MECHANISM_TARGET_STATE, + CharacteristicsTypes.BATTERY_LEVEL, + ] + def update_characteristics(self, characteristics): """Synchronise the Lock state with Home Assistant.""" # pylint: disable=import-error @@ -60,14 +70,8 @@ class HomeKitLock(HomeKitEntity, LockDevice): ctype = characteristic['type'] ctype = CharacteristicsTypes.get_short(ctype) if ctype == "lock-mechanism.current-state": - self._chars['lock-mechanism.current-state'] = \ - characteristic['iid'] self._state = CURRENT_STATE_MAP[characteristic['value']] - elif ctype == "lock-mechanism.target-state": - self._chars['lock-mechanism.target-state'] = \ - characteristic['iid'] elif ctype == "battery-level": - self._chars['battery-level'] = characteristic['iid'] self._battery_level = characteristic['value'] @property diff --git a/homeassistant/components/homekit_controller/switch.py b/homeassistant/components/homekit_controller/switch.py index 51a71163bad..4bee51803f3 100644 --- a/homeassistant/components/homekit_controller/switch.py +++ b/homeassistant/components/homekit_controller/switch.py @@ -33,6 +33,15 @@ class HomeKitSwitch(HomeKitEntity, SwitchDevice): self._on = None self._outlet_in_use = None + def get_characteristic_types(self): + """Define the homekit characteristics the entity cares about.""" + # pylint: disable=import-error + from homekit.model.characteristics import CharacteristicsTypes + return [ + CharacteristicsTypes.ON, + CharacteristicsTypes.OUTLET_IN_USE, + ] + def update_characteristics(self, characteristics): """Synchronise the switch state with Home Assistant.""" # pylint: disable=import-error @@ -42,10 +51,8 @@ class HomeKitSwitch(HomeKitEntity, SwitchDevice): ctype = characteristic['type'] ctype = CharacteristicsTypes.get_short(ctype) if ctype == "on": - self._chars['on'] = characteristic['iid'] self._on = characteristic['value'] elif ctype == "outlet-in-use": - self._chars['outlet-in-use'] = characteristic['iid'] self._outlet_in_use = characteristic['value'] @property diff --git a/tests/components/homekit_controller/common.py b/tests/components/homekit_controller/common.py index 8bb69e18450..d3c1f9ab07b 100644 --- a/tests/components/homekit_controller/common.py +++ b/tests/components/homekit_controller/common.py @@ -8,7 +8,7 @@ from homekit.model.characteristics import ( from homekit.model import Accessory, get_id from homeassistant.components.homekit_controller import ( - DOMAIN, HOMEKIT_ACCESSORY_DISPATCH, SERVICE_HOMEKIT, HomeKitEntity) + DOMAIN, HOMEKIT_ACCESSORY_DISPATCH, SERVICE_HOMEKIT) from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util from tests.common import async_fire_time_changed, fire_service_discovered @@ -168,8 +168,7 @@ async def setup_test_component(hass, services): } } - with mock.patch.object(HomeKitEntity, 'name', 'testdevice'): - fire_service_discovered(hass, SERVICE_HOMEKIT, discovery_info) - await hass.async_block_till_done() + fire_service_discovered(hass, SERVICE_HOMEKIT, discovery_info) + await hass.async_block_till_done() return Helper(hass, '.'.join((domain, 'testdevice')), pairing, accessory) diff --git a/tests/components/homekit_controller/test_cover.py b/tests/components/homekit_controller/test_cover.py index 4a0c244492e..062ecc54041 100644 --- a/tests/components/homekit_controller/test_cover.py +++ b/tests/components/homekit_controller/test_cover.py @@ -39,7 +39,7 @@ def create_window_covering_service(): obstruction.value = False name = service.add_characteristic('name') - name.value = "Window Cover 1" + name.value = "testdevice" return service @@ -166,7 +166,7 @@ def create_garage_door_opener_service(): obstruction.value = False name = service.add_characteristic('name') - name.value = "Garage Door Opener 1" + name.value = "testdevice" return service From abeb875c61d935e45350f2a2f265a61826812008 Mon Sep 17 00:00:00 2001 From: Jc2k Date: Mon, 28 Jan 2019 20:27:26 +0000 Subject: [PATCH 143/222] Homekit controller BLE groundwork (part 2) (#20548) * Only fetch values of characteristics we are tracking. * Use callbacks on subclasses to update individual values * Update alarm_control_panel to use update callbacks * Update climate to use update callbacks * Update cover to use update callbacks * Update light to use update callbacks * Update lock to use update callbacks * Update switch to use update callbacks * Remove compatibility code as all entities migrated * pylint by name rather than code --- .../components/homekit_controller/__init__.py | 26 ++++---- .../homekit_controller/alarm_control_panel.py | 15 ++--- .../components/homekit_controller/climate.py | 26 +++----- .../components/homekit_controller/cover.py | 61 ++++++++----------- .../components/homekit_controller/light.py | 30 ++++----- .../components/homekit_controller/lock.py | 15 ++--- .../components/homekit_controller/switch.py | 15 ++--- 7 files changed, 76 insertions(+), 112 deletions(-) diff --git a/homeassistant/components/homekit_controller/__init__.py b/homeassistant/components/homekit_controller/__init__.py index 2c59f062bcc..b5577119178 100644 --- a/homeassistant/components/homekit_controller/__init__.py +++ b/homeassistant/components/homekit_controller/__init__.py @@ -247,7 +247,7 @@ class HomeKitEntity(Entity): setup_fn = getattr(self, '_setup_{}'.format(setup_fn_name), None) if not setup_fn: return - # pylint: disable=E1102 + # pylint: disable=not-callable setup_fn(char) def update(self): @@ -255,19 +255,23 @@ class HomeKitEntity(Entity): # pylint: disable=import-error from homekit.exceptions import AccessoryDisconnectedError + pairing = self._accessory.pairing + try: - pairing = self._accessory.pairing - data = pairing.list_accessories_and_characteristics() + new_values_dict = pairing.get_characteristics(self._chars_to_poll) except AccessoryDisconnectedError: return - for accessory in data: - if accessory['aid'] != self._aid: + + for (_, iid), result in new_values_dict.items(): + if 'value' not in result: continue - for service in accessory['services']: - if service['iid'] != self._iid: - continue - self.update_characteristics(service['characteristics']) - break + # Callback to update the entity with this characteristic value + char_name = escape_characteristic_name(self._char_names[iid]) + update_fn = getattr(self, '_update_{}'.format(char_name), None) + if not update_fn: + continue + # pylint: disable=not-callable + update_fn(result['value']) @property def unique_id(self): @@ -290,7 +294,7 @@ class HomeKitEntity(Entity): def update_characteristics(self, characteristics): """Synchronise a HomeKit device state with Home Assistant.""" - raise NotImplementedError + pass def put_characteristics(self, characteristics): """Control a HomeKit device state from Home Assistant.""" diff --git a/homeassistant/components/homekit_controller/alarm_control_panel.py b/homeassistant/components/homekit_controller/alarm_control_panel.py index 984be8e0c3b..3a2e5170453 100644 --- a/homeassistant/components/homekit_controller/alarm_control_panel.py +++ b/homeassistant/components/homekit_controller/alarm_control_panel.py @@ -64,18 +64,11 @@ class HomeKitAlarmControlPanel(HomeKitEntity, AlarmControlPanel): CharacteristicsTypes.BATTERY_LEVEL, ] - def update_characteristics(self, characteristics): - """Synchronise the Alarm Control Panel state with Home Assistant.""" - # pylint: disable=import-error - from homekit.model.characteristics import CharacteristicsTypes + def _update_security_system_state_current(self, value): + self._state = CURRENT_STATE_MAP[value] - for characteristic in characteristics: - ctype = characteristic['type'] - ctype = CharacteristicsTypes.get_short(ctype) - if ctype == "security-system-state.current": - self._state = CURRENT_STATE_MAP[characteristic['value']] - elif ctype == "battery-level": - self._battery_level = characteristic['value'] + def _update_battery_level(self, value): + self._battery_level = value @property def icon(self): diff --git a/homeassistant/components/homekit_controller/climate.py b/homeassistant/components/homekit_controller/climate.py index 042a499c55a..15378e2b046 100644 --- a/homeassistant/components/homekit_controller/climate.py +++ b/homeassistant/components/homekit_controller/climate.py @@ -72,23 +72,17 @@ class HomeKitClimateDevice(HomeKitEntity, ClimateDevice): def _setup_temperature_target(self, characteristic): self._features |= SUPPORT_TARGET_TEMPERATURE - def update_characteristics(self, characteristics): - """Synchronise device state with Home Assistant.""" - # pylint: disable=import-error - from homekit.model.characteristics import CharacteristicsTypes + def _update_heating_cooling_current(self, value): + self._state = MODE_HOMEKIT_TO_HASS.get(value) - for characteristic in characteristics: - ctype = CharacteristicsTypes.get_short_uuid(characteristic['type']) - if ctype == CharacteristicsTypes.HEATING_COOLING_CURRENT: - self._state = MODE_HOMEKIT_TO_HASS.get( - characteristic['value']) - if ctype == CharacteristicsTypes.HEATING_COOLING_TARGET: - self._current_mode = MODE_HOMEKIT_TO_HASS.get( - characteristic['value']) - elif ctype == CharacteristicsTypes.TEMPERATURE_CURRENT: - self._current_temp = characteristic['value'] - elif ctype == CharacteristicsTypes.TEMPERATURE_TARGET: - self._target_temp = characteristic['value'] + def _update_heating_cooling_target(self, value): + self._current_mode = MODE_HOMEKIT_TO_HASS.get(value) + + def _update_temperature_current(self, value): + self._current_temp = value + + def _update_temperature_target(self, value): + self._target_temp = value def set_temperature(self, **kwargs): """Set new target temperature.""" diff --git a/homeassistant/components/homekit_controller/cover.py b/homeassistant/components/homekit_controller/cover.py index f9cc2ce435b..c8f087254bb 100644 --- a/homeassistant/components/homekit_controller/cover.py +++ b/homeassistant/components/homekit_controller/cover.py @@ -85,18 +85,14 @@ class HomeKitGarageDoorCover(HomeKitEntity, CoverDevice): def _setup_name(self, char): self._name = char['value'] - def update_characteristics(self, characteristics): - """Synchronise the Cover state with Home Assistant.""" - # pylint: disable=import-error - from homekit.model.characteristics import CharacteristicsTypes + def _update_door_state_current(self, value): + self._state = CURRENT_GARAGE_STATE_MAP[value] - for characteristic in characteristics: - ctype = characteristic['type'] - ctype = CharacteristicsTypes.get_short(ctype) - if ctype == "door-state.current": - self._state = CURRENT_GARAGE_STATE_MAP[characteristic['value']] - elif ctype == "obstruction-detected": - self._obstruction_detected = characteristic['value'] + def _update_obstruction_detected(self, value): + self._obstruction_detected = value + + def _update_name(self, value): + self._name = value @property def available(self): @@ -187,31 +183,26 @@ class HomeKitWindowCover(HomeKitEntity, CoverDevice): def _setup_name(self, char): self._name = char['value'] - def update_characteristics(self, characteristics): - """Synchronise the Cover state with Home Assistant.""" - # pylint: disable=import-error - from homekit.model.characteristics import CharacteristicsTypes + def _update_position_state(self, value): + self._state = CURRENT_WINDOW_STATE_MAP[value] - for characteristic in characteristics: - ctype = characteristic['type'] - ctype = CharacteristicsTypes.get_short(ctype) - if ctype == "position.state": - if 'value' in characteristic: - self._state = \ - CURRENT_WINDOW_STATE_MAP[characteristic['value']] - elif ctype == "position.current": - self._position = characteristic['value'] - elif ctype == "position.hold": - if 'value' in characteristic: - self._hold = characteristic['value'] - elif ctype == "vertical-tilt.current": - if characteristic['value'] is not None: - self._tilt_position = characteristic['value'] - elif ctype == "horizontal-tilt.current": - if characteristic['value'] is not None: - self._tilt_position = characteristic['value'] - elif ctype == "obstruction-detected": - self._obstruction_detected = characteristic['value'] + def _update_position_current(self, value): + self._position = value + + def _update_position_hold(self, value): + self._hold = value + + def _update_vertical_tilt_current(self, value): + self._tilt_position = value + + def _update_horizontal_tilt_current(self, value): + self._tilt_position = value + + def _update_obstruction_detected(self, value): + self._obstruction_detected = value + + def _update_name(self, value): + self._hold = value @property def supported_features(self): diff --git a/homeassistant/components/homekit_controller/light.py b/homeassistant/components/homekit_controller/light.py index 940e3782379..74ef8948f45 100644 --- a/homeassistant/components/homekit_controller/light.py +++ b/homeassistant/components/homekit_controller/light.py @@ -60,24 +60,20 @@ class HomeKitLight(HomeKitEntity, Light): def _setup_saturation(self, char): self._features |= SUPPORT_COLOR - def update_characteristics(self, characteristics): - """Synchronise light state with Home Assistant.""" - # pylint: disable=import-error - from homekit.model.characteristics import CharacteristicsTypes + def _update_on(self, value): + self._on = value - for characteristic in characteristics: - ctype = characteristic['type'] - ctype = CharacteristicsTypes.get_short(ctype) - if ctype == "on": - self._on = characteristic['value'] - elif ctype == 'brightness': - self._brightness = characteristic['value'] - elif ctype == 'color-temperature': - self._color_temperature = characteristic['value'] - elif ctype == "hue": - self._hue = characteristic['value'] - elif ctype == "saturation": - self._saturation = characteristic['value'] + def _update_brightness(self, value): + self._brightness = value + + def _update_color_temperature(self, value): + self._color_temperature = value + + def _update_hue(self, value): + self._hue = value + + def _update_saturation(self, value): + self._saturation = value @property def is_on(self): diff --git a/homeassistant/components/homekit_controller/lock.py b/homeassistant/components/homekit_controller/lock.py index 53c94317c69..e27ed444528 100644 --- a/homeassistant/components/homekit_controller/lock.py +++ b/homeassistant/components/homekit_controller/lock.py @@ -61,18 +61,11 @@ class HomeKitLock(HomeKitEntity, LockDevice): CharacteristicsTypes.BATTERY_LEVEL, ] - def update_characteristics(self, characteristics): - """Synchronise the Lock state with Home Assistant.""" - # pylint: disable=import-error - from homekit.model.characteristics import CharacteristicsTypes + def _update_lock_mechanism_current_state(self, value): + self._state = CURRENT_STATE_MAP[value] - for characteristic in characteristics: - ctype = characteristic['type'] - ctype = CharacteristicsTypes.get_short(ctype) - if ctype == "lock-mechanism.current-state": - self._state = CURRENT_STATE_MAP[characteristic['value']] - elif ctype == "battery-level": - self._battery_level = characteristic['value'] + def _update_battery_level(self, value): + self._battery_level = value @property def name(self): diff --git a/homeassistant/components/homekit_controller/switch.py b/homeassistant/components/homekit_controller/switch.py index 4bee51803f3..ba4a04022f0 100644 --- a/homeassistant/components/homekit_controller/switch.py +++ b/homeassistant/components/homekit_controller/switch.py @@ -42,18 +42,11 @@ class HomeKitSwitch(HomeKitEntity, SwitchDevice): CharacteristicsTypes.OUTLET_IN_USE, ] - def update_characteristics(self, characteristics): - """Synchronise the switch state with Home Assistant.""" - # pylint: disable=import-error - from homekit.model.characteristics import CharacteristicsTypes + def _update_on(self, value): + self._on = value - for characteristic in characteristics: - ctype = characteristic['type'] - ctype = CharacteristicsTypes.get_short(ctype) - if ctype == "on": - self._on = characteristic['value'] - elif ctype == "outlet-in-use": - self._outlet_in_use = characteristic['value'] + def _update_outlet_in_use(self, value): + self._outlet_in_use = value @property def is_on(self): From 2c7060896b8bf61ab3a6c3c1c0d7f4022aff92fd Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Mon, 28 Jan 2019 16:35:39 -0700 Subject: [PATCH 144/222] Make Ambient PWS async and cloud-push (#20332) * Moving existing sensor file * Initial functionality in place * Added test for config flow * Updated coverage and CODEOWNERS * Linting * Linting * Member comments * Hound * Moving socket disconnect on HASS stop * Member comments * Removed unnecessary dispatcher call * Config entry fix * Added support in config flow for good accounts with no devices * Hound * Updated comment * Member comments * Stale docstrings * Stale docstring --- .coveragerc | 4 +- CODEOWNERS | 1 + .../ambient_station/.translations/en.json | 19 ++ .../components/ambient_station/__init__.py | 212 ++++++++++++++++++ .../components/ambient_station/config_flow.py | 72 ++++++ .../components/ambient_station/const.py | 13 ++ .../components/ambient_station/sensor.py | 115 ++++++++++ .../components/ambient_station/strings.json | 19 ++ .../components/sensor/ambient_station.py | 212 ------------------ homeassistant/config_entries.py | 1 + requirements_all.txt | 6 +- requirements_test_all.txt | 3 + script/gen_requirements_all.py | 1 + tests/components/ambient_station/__init__.py | 1 + .../ambient_station/test_config_flow.py | 130 +++++++++++ tests/fixtures/ambient_devices.json | 15 ++ 16 files changed, 608 insertions(+), 216 deletions(-) create mode 100644 homeassistant/components/ambient_station/.translations/en.json create mode 100644 homeassistant/components/ambient_station/__init__.py create mode 100644 homeassistant/components/ambient_station/config_flow.py create mode 100644 homeassistant/components/ambient_station/const.py create mode 100644 homeassistant/components/ambient_station/sensor.py create mode 100644 homeassistant/components/ambient_station/strings.json delete mode 100644 homeassistant/components/sensor/ambient_station.py create mode 100644 tests/components/ambient_station/__init__.py create mode 100644 tests/components/ambient_station/test_config_flow.py create mode 100644 tests/fixtures/ambient_devices.json diff --git a/.coveragerc b/.coveragerc index 2d4fb3f81a7..32bcda136ca 100644 --- a/.coveragerc +++ b/.coveragerc @@ -19,6 +19,9 @@ omit = homeassistant/components/alarmdecoder.py homeassistant/components/*/alarmdecoder.py + homeassistant/components/ambient_station/__init__.py + homeassistant/components/ambient_station/sensor.py + homeassistant/components/amcrest.py homeassistant/components/*/amcrest.py @@ -732,7 +735,6 @@ omit = homeassistant/components/sensor/aftership.py homeassistant/components/sensor/airvisual.py homeassistant/components/sensor/alpha_vantage.py - homeassistant/components/sensor/ambient_station.py homeassistant/components/sensor/arest.py homeassistant/components/sensor/arwn.py homeassistant/components/sensor/bbox.py diff --git a/CODEOWNERS b/CODEOWNERS index 4b4931ecc3a..2a2391186f4 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -153,6 +153,7 @@ homeassistant/components/weather/openweathermap.py @fabaff homeassistant/components/xiaomi_aqara.py @danielhiversen @syssi # A +homeassistant/components/ambient_station/* @bachya homeassistant/components/arduino.py @fabaff homeassistant/components/*/arduino.py @fabaff homeassistant/components/*/arest.py @fabaff diff --git a/homeassistant/components/ambient_station/.translations/en.json b/homeassistant/components/ambient_station/.translations/en.json new file mode 100644 index 00000000000..5bd643da55c --- /dev/null +++ b/homeassistant/components/ambient_station/.translations/en.json @@ -0,0 +1,19 @@ +{ + "config": { + "error": { + "identifier_exists": "Application Key and/or API Key already registered", + "invalid_key": "Invalid API Key and/or Application Key", + "no_devices": "No devices found in account" + }, + "step": { + "user": { + "data": { + "api_key": "API Key", + "app_key": "Application Key" + }, + "title": "Fill in your information" + } + }, + "title": "Ambient PWS" + } +} \ No newline at end of file diff --git a/homeassistant/components/ambient_station/__init__.py b/homeassistant/components/ambient_station/__init__.py new file mode 100644 index 00000000000..788927a2700 --- /dev/null +++ b/homeassistant/components/ambient_station/__init__.py @@ -0,0 +1,212 @@ +""" +Support for Ambient Weather Station Service. + +For more details about this component, please refer to the documentation at +https://home-assistant.io/components/ambient_station/ +""" +import logging + +import voluptuous as vol + +from homeassistant.config_entries import SOURCE_IMPORT +from homeassistant.const import ( + ATTR_NAME, ATTR_LOCATION, CONF_API_KEY, CONF_MONITORED_CONDITIONS, + CONF_UNIT_SYSTEM, EVENT_HOMEASSISTANT_STOP) +from homeassistant.exceptions import ConfigEntryNotReady +from homeassistant.helpers import aiohttp_client, config_validation as cv +from homeassistant.helpers.dispatcher import async_dispatcher_send +from homeassistant.helpers.event import async_call_later + +from .config_flow import configured_instances +from .const import ( + ATTR_LAST_DATA, CONF_APP_KEY, DATA_CLIENT, DOMAIN, TOPIC_UPDATE, UNITS_SI, + UNITS_US) + +REQUIREMENTS = ['aioambient==0.1.0'] +_LOGGER = logging.getLogger(__name__) + +DEFAULT_SOCKET_MIN_RETRY = 15 + +SENSOR_TYPES = { + '24hourrainin': ['24 Hr Rain', 'in'], + 'baromabsin': ['Abs Pressure', 'inHg'], + 'baromrelin': ['Rel Pressure', 'inHg'], + 'battout': ['Battery', ''], + 'co2': ['co2', 'ppm'], + 'dailyrainin': ['Daily Rain', 'in'], + 'dewPoint': ['Dew Point', ['°F', '°C']], + 'eventrainin': ['Event Rain', 'in'], + 'feelsLike': ['Feels Like', ['°F', '°C']], + 'hourlyrainin': ['Hourly Rain Rate', 'in/hr'], + 'humidity': ['Humidity', '%'], + 'humidityin': ['Humidity In', '%'], + 'lastRain': ['Last Rain', ''], + 'maxdailygust': ['Max Gust', 'mph'], + 'monthlyrainin': ['Monthly Rain', 'in'], + 'solarradiation': ['Solar Rad', 'W/m^2'], + 'tempf': ['Temp', ['°F', '°C']], + 'tempinf': ['Inside Temp', ['°F', '°C']], + 'totalrainin': ['Lifetime Rain', 'in'], + 'uv': ['uv', 'Index'], + 'weeklyrainin': ['Weekly Rain', 'in'], + 'winddir': ['Wind Dir', '°'], + 'winddir_avg10m': ['Wind Dir Avg 10m', '°'], + 'winddir_avg2m': ['Wind Dir Avg 2m', 'mph'], + 'windgustdir': ['Gust Dir', '°'], + 'windgustmph': ['Wind Gust', 'mph'], + 'windspdmph_avg10m': ['Wind Avg 10m', 'mph'], + 'windspdmph_avg2m': ['Wind Avg 2m', 'mph'], + 'windspeedmph': ['Wind Speed', 'mph'], + 'yearlyrainin': ['Yearly Rain', 'in'], +} + +CONFIG_SCHEMA = vol.Schema({ + DOMAIN: + vol.Schema({ + vol.Required(CONF_APP_KEY): + cv.string, + vol.Required(CONF_API_KEY): + cv.string, + vol.Optional( + CONF_MONITORED_CONDITIONS, default=list(SENSOR_TYPES)): + vol.All(cv.ensure_list, [vol.In(SENSOR_TYPES)]), + vol.Optional(CONF_UNIT_SYSTEM): + vol.In([UNITS_SI, UNITS_US]), + }) +}, extra=vol.ALLOW_EXTRA) + + +async def async_setup(hass, config): + """Set up the Ambient PWS component.""" + hass.data[DOMAIN] = {} + hass.data[DOMAIN][DATA_CLIENT] = {} + + if DOMAIN not in config: + return True + + conf = config[DOMAIN] + + if conf[CONF_APP_KEY] in configured_instances(hass): + return True + + hass.async_create_task( + hass.config_entries.flow.async_init( + DOMAIN, context={'source': SOURCE_IMPORT}, data=conf)) + + return True + + +async def async_setup_entry(hass, config_entry): + """Set up the Ambient PWS as config entry.""" + from aioambient import Client + from aioambient.errors import WebsocketConnectionError + + session = aiohttp_client.async_get_clientsession(hass) + + try: + ambient = AmbientStation( + hass, + config_entry, + Client( + config_entry.data[CONF_API_KEY], + config_entry.data[CONF_APP_KEY], session), + config_entry.data.get( + CONF_MONITORED_CONDITIONS, list(SENSOR_TYPES)), + config_entry.data.get(CONF_UNIT_SYSTEM)) + hass.loop.create_task(ambient.ws_connect()) + hass.data[DOMAIN][DATA_CLIENT][config_entry.entry_id] = ambient + except WebsocketConnectionError as err: + _LOGGER.error('Config entry failed: %s', err) + raise ConfigEntryNotReady + + hass.bus.async_listen_once( + EVENT_HOMEASSISTANT_STOP, ambient.client.websocket.disconnect()) + + return True + + +async def async_unload_entry(hass, config_entry): + """Unload an Ambient PWS config entry.""" + ambient = hass.data[DOMAIN][DATA_CLIENT].pop(config_entry.entry_id) + hass.async_create_task(ambient.ws_disconnect()) + + await hass.config_entries.async_forward_entry_unload( + config_entry, 'sensor') + + return True + + +class AmbientStation: + """Define a class to handle the Ambient websocket.""" + + def __init__( + self, hass, config_entry, client, monitored_conditions, + unit_system): + """Initialize.""" + self._config_entry = config_entry + self._hass = hass + self._ws_reconnect_delay = DEFAULT_SOCKET_MIN_RETRY + self.client = client + self.monitored_conditions = monitored_conditions + self.stations = {} + self.unit_system = unit_system + + async def ws_connect(self): + """Register handlers and connect to the websocket.""" + from aioambient.errors import WebsocketError + + def on_connect(): + """Define a handler to fire when the websocket is connected.""" + _LOGGER.info('Connected to websocket') + + def on_data(data): + """Define a handler to fire when the data is received.""" + mac_address = data['macAddress'] + if data != self.stations[mac_address][ATTR_LAST_DATA]: + _LOGGER.debug('New data received: %s', data) + self.stations[mac_address][ATTR_LAST_DATA] = data + async_dispatcher_send(self._hass, TOPIC_UPDATE) + + def on_disconnect(): + """Define a handler to fire when the websocket is disconnected.""" + _LOGGER.info('Disconnected from websocket') + + def on_subscribed(data): + """Define a handler to fire when the subscription is set.""" + for station in data['devices']: + if station['macAddress'] in self.stations: + continue + + _LOGGER.debug('New station subscription: %s', data) + + self.stations[station['macAddress']] = { + ATTR_LAST_DATA: station['lastData'], + ATTR_LOCATION: station['info']['location'], + ATTR_NAME: station['info']['name'], + } + + self._hass.async_create_task( + self._hass.config_entries.async_forward_entry_setup( + self._config_entry, 'sensor')) + + self._ws_reconnect_delay = DEFAULT_SOCKET_MIN_RETRY + + self.client.websocket.on_connect(on_connect) + self.client.websocket.on_data(on_data) + self.client.websocket.on_disconnect(on_disconnect) + self.client.websocket.on_subscribed(on_subscribed) + + try: + await self.client.websocket.connect() + except WebsocketError as err: + _LOGGER.error("Error with the websocket connection: %s", err) + + self._ws_reconnect_delay = min( + 2 * self._ws_reconnect_delay, 480) + + async_call_later( + self._hass, self._ws_reconnect_delay, self.ws_connect) + + async def ws_disconnect(self): + """Disconnect from the websocket.""" + await self.client.websocket.disconnect() diff --git a/homeassistant/components/ambient_station/config_flow.py b/homeassistant/components/ambient_station/config_flow.py new file mode 100644 index 00000000000..56e747ce5e0 --- /dev/null +++ b/homeassistant/components/ambient_station/config_flow.py @@ -0,0 +1,72 @@ +"""Config flow to configure the Ambient PWS component.""" + +import voluptuous as vol + +from homeassistant import config_entries +from homeassistant.const import CONF_API_KEY +from homeassistant.core import callback +from homeassistant.helpers import aiohttp_client + +from .const import CONF_APP_KEY, DOMAIN + + +@callback +def configured_instances(hass): + """Return a set of configured Ambient PWS instances.""" + return set( + entry.data[CONF_APP_KEY] + for entry in hass.config_entries.async_entries(DOMAIN)) + + +@config_entries.HANDLERS.register(DOMAIN) +class AmbientStationFlowHandler(config_entries.ConfigFlow): + """Handle an Ambient PWS config flow.""" + + VERSION = 1 + CONNECTION_CLASS = config_entries.CONN_CLASS_CLOUD_PUSH + + async def _show_form(self, errors=None): + """Show the form to the user.""" + data_schema = vol.Schema({ + vol.Required(CONF_API_KEY): str, + vol.Required(CONF_APP_KEY): str, + }) + + return self.async_show_form( + step_id='user', + data_schema=data_schema, + errors=errors if errors else {}, + ) + + async def async_step_import(self, import_config): + """Import a config entry from configuration.yaml.""" + return await self.async_step_user(import_config) + + async def async_step_user(self, user_input=None): + """Handle the start of the config flow.""" + from aioambient import Client + from aioambient.errors import AmbientError + + if not user_input: + return await self._show_form() + + if user_input[CONF_APP_KEY] in configured_instances(self.hass): + return await self._show_form({CONF_APP_KEY: 'identifier_exists'}) + + session = aiohttp_client.async_get_clientsession(self.hass) + client = Client( + user_input[CONF_API_KEY], user_input[CONF_APP_KEY], session) + + try: + devices = await client.api.get_devices() + except AmbientError: + return await self._show_form({'base': 'invalid_key'}) + + if not devices: + return await self._show_form({'base': 'no_devices'}) + + # The Application Key (which identifies each config entry) is too long + # to show nicely in the UI, so we take the first 12 characters (similar + # to how GitHub does it): + return self.async_create_entry( + title=user_input[CONF_APP_KEY][:12], data=user_input) diff --git a/homeassistant/components/ambient_station/const.py b/homeassistant/components/ambient_station/const.py new file mode 100644 index 00000000000..df2c5462e66 --- /dev/null +++ b/homeassistant/components/ambient_station/const.py @@ -0,0 +1,13 @@ +"""Define constants for the Ambient PWS component.""" +DOMAIN = 'ambient_station' + +ATTR_LAST_DATA = 'last_data' + +CONF_APP_KEY = 'app_key' + +DATA_CLIENT = 'data_client' + +TOPIC_UPDATE = 'update' + +UNITS_SI = 'si' +UNITS_US = 'us' diff --git a/homeassistant/components/ambient_station/sensor.py b/homeassistant/components/ambient_station/sensor.py new file mode 100644 index 00000000000..d2d89233472 --- /dev/null +++ b/homeassistant/components/ambient_station/sensor.py @@ -0,0 +1,115 @@ +""" +Support for Ambient Weather Station Service. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/sensor.ambient_station/ +""" +import logging + +from homeassistant.components.ambient_station import SENSOR_TYPES +from homeassistant.helpers.entity import Entity +from homeassistant.const import ATTR_NAME +from homeassistant.core import callback +from homeassistant.helpers.dispatcher import async_dispatcher_connect + +from .const import ( + ATTR_LAST_DATA, DATA_CLIENT, DOMAIN, TOPIC_UPDATE, UNITS_SI, UNITS_US) + +DEPENDENCIES = ['ambient_station'] +_LOGGER = logging.getLogger(__name__) + +UNIT_SYSTEM = {UNITS_US: 0, UNITS_SI: 1} + + +async def async_setup_platform( + hass, config, async_add_entities, discovery_info=None): + """Set up an Ambient PWS sensor based on existing config.""" + pass + + +async def async_setup_entry(hass, entry, async_add_entities): + """Set up an Ambient PWS sensor based on a config entry.""" + ambient = hass.data[DOMAIN][DATA_CLIENT][entry.entry_id] + + if ambient.unit_system: + sys_units = ambient.unit_system + elif hass.config.units.is_metric: + sys_units = UNITS_SI + else: + sys_units = UNITS_US + + sensor_list = [] + for mac_address, station in ambient.stations.items(): + for condition in ambient.monitored_conditions: + name, unit = SENSOR_TYPES[condition] + if isinstance(unit, list): + unit = unit[UNIT_SYSTEM[sys_units]] + + sensor_list.append( + AmbientWeatherSensor( + ambient, mac_address, station[ATTR_NAME], condition, name, + unit)) + + async_add_entities(sensor_list, True) + + +class AmbientWeatherSensor(Entity): + """Define an Ambient sensor.""" + + def __init__( + self, ambient, mac_address, station_name, sensor_type, sensor_name, + units): + """Initialize the sensor.""" + self._ambient = ambient + self._async_unsub_dispatcher_connect = None + self._mac_address = mac_address + self._sensor_name = sensor_name + self._sensor_type = sensor_type + self._state = None + self._station_name = station_name + self._units = units + + @property + def name(self): + """Return the name of the sensor.""" + return '{0}_{1}'.format(self._station_name, self._sensor_name) + + @property + def should_poll(self): + """Disable polling.""" + return False + + @property + def state(self): + """Return the state of the sensor.""" + return self._state + + @property + def unit_of_measurement(self): + """Return the unit of measurement.""" + return self._units + + @property + def unique_id(self): + """Return a unique, unchanging string that represents this sensor.""" + return '{0}_{1}'.format(self._mac_address, self._sensor_name) + + async def async_added_to_hass(self): + """Register callbacks.""" + @callback + def update(): + """Update the state.""" + self.async_schedule_update_ha_state(True) + + self._async_unsub_dispatcher_connect = async_dispatcher_connect( + self.hass, TOPIC_UPDATE, update) + + async def async_will_remove_from_hass(self): + """Disconnect dispatcher listener when removed.""" + if self._async_unsub_dispatcher_connect: + self._async_unsub_dispatcher_connect() + + async def async_update(self): + """Fetch new state data for the sensor.""" + self._state = self._ambient.stations[ + self._mac_address][ATTR_LAST_DATA].get(self._sensor_type) diff --git a/homeassistant/components/ambient_station/strings.json b/homeassistant/components/ambient_station/strings.json new file mode 100644 index 00000000000..657b3477bb2 --- /dev/null +++ b/homeassistant/components/ambient_station/strings.json @@ -0,0 +1,19 @@ +{ + "config": { + "title": "Ambient PWS", + "step": { + "user": { + "title": "Fill in your information", + "data": { + "api_key": "API Key", + "app_key": "Application Key" + } + } + }, + "error": { + "identifier_exists": "Application Key and/or API Key already registered", + "invalid_key": "Invalid API Key and/or Application Key", + "no_devices": "No devices found in account" + } + } +} diff --git a/homeassistant/components/sensor/ambient_station.py b/homeassistant/components/sensor/ambient_station.py deleted file mode 100644 index bc44f83d764..00000000000 --- a/homeassistant/components/sensor/ambient_station.py +++ /dev/null @@ -1,212 +0,0 @@ -""" -Support for Ambient Weather Station Service. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/sensor.ambient_station/ -""" - -import asyncio -from datetime import timedelta -import logging - -import voluptuous as vol - -from homeassistant.components.sensor import PLATFORM_SCHEMA -from homeassistant.const import CONF_API_KEY, CONF_MONITORED_CONDITIONS -import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.entity import Entity -from homeassistant.util import Throttle - -REQUIREMENTS = ['ambient_api==1.5.2'] - -CONF_APP_KEY = 'app_key' - -SENSOR_NAME = 0 -SENSOR_UNITS = 1 - -CONF_UNITS = 'units' -UNITS_US = 'us' -UNITS_SI = 'si' -UNIT_SYSTEM = {UNITS_US: 0, UNITS_SI: 1} - -SCAN_INTERVAL = timedelta(seconds=300) - -SENSOR_TYPES = { - 'winddir': ['Wind Dir', '°'], - 'windspeedmph': ['Wind Speed', 'mph'], - 'windgustmph': ['Wind Gust', 'mph'], - 'maxdailygust': ['Max Gust', 'mph'], - 'windgustdir': ['Gust Dir', '°'], - 'windspdmph_avg2m': ['Wind Avg 2m', 'mph'], - 'winddir_avg2m': ['Wind Dir Avg 2m', 'mph'], - 'windspdmph_avg10m': ['Wind Avg 10m', 'mph'], - 'winddir_avg10m': ['Wind Dir Avg 10m', '°'], - 'humidity': ['Humidity', '%'], - 'humidityin': ['Humidity In', '%'], - 'tempf': ['Temp', ['°F', '°C']], - 'tempinf': ['Inside Temp', ['°F', '°C']], - 'battout': ['Battery', ''], - 'hourlyrainin': ['Hourly Rain Rate', 'in/hr'], - 'dailyrainin': ['Daily Rain', 'in'], - '24hourrainin': ['24 Hr Rain', 'in'], - 'weeklyrainin': ['Weekly Rain', 'in'], - 'monthlyrainin': ['Monthly Rain', 'in'], - 'yearlyrainin': ['Yearly Rain', 'in'], - 'eventrainin': ['Event Rain', 'in'], - 'totalrainin': ['Lifetime Rain', 'in'], - 'baromrelin': ['Rel Pressure', 'inHg'], - 'baromabsin': ['Abs Pressure', 'inHg'], - 'uv': ['uv', 'Index'], - 'solarradiation': ['Solar Rad', 'W/m^2'], - 'co2': ['co2', 'ppm'], - 'lastRain': ['Last Rain', ''], - 'dewPoint': ['Dew Point', ['°F', '°C']], - 'feelsLike': ['Feels Like', ['°F', '°C']], -} - -_LOGGER = logging.getLogger(__name__) - -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Required(CONF_API_KEY): cv.string, - vol.Required(CONF_APP_KEY): cv.string, - vol.Required(CONF_MONITORED_CONDITIONS): - vol.All(cv.ensure_list, [vol.In(SENSOR_TYPES)]), - vol.Optional(CONF_UNITS): vol.In([UNITS_SI, UNITS_US]), -}) - - -def setup_platform(hass, config, add_entities, discovery_info=None): - """Initialze each sensor platform for each monitored condition.""" - api_key = config[CONF_API_KEY] - app_key = config[CONF_APP_KEY] - station_data = AmbientStationData(hass, api_key, app_key) - if not station_data.connect_success: - _LOGGER.error("Could not connect to weather station API") - return - - sensor_list = [] - - if CONF_UNITS in config: - sys_units = config[CONF_UNITS] - elif hass.config.units.is_metric: - sys_units = UNITS_SI - else: - sys_units = UNITS_US - - for condition in config[CONF_MONITORED_CONDITIONS]: - # create a sensor object for each monitored condition - sensor_params = SENSOR_TYPES[condition] - name = sensor_params[SENSOR_NAME] - units = sensor_params[SENSOR_UNITS] - if isinstance(units, list): - units = sensor_params[SENSOR_UNITS][UNIT_SYSTEM[sys_units]] - - sensor_list.append(AmbientWeatherSensor(station_data, condition, - name, units)) - - add_entities(sensor_list) - - -class AmbientWeatherSensor(Entity): - """Representation of a Sensor.""" - - def __init__(self, station_data, condition, name, units): - """Initialize the sensor.""" - self._state = None - self.station_data = station_data - self._condition = condition - self._name = name - self._units = units - - @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.""" - return self._units - - async def async_update(self): - """Fetch new state data for the sensor. - - This is the only method that should fetch new data for Home Assistant. - """ - _LOGGER.debug("Getting data for sensor: %s", self._name) - data = await self.station_data.get_data() - if data is None: - # update likely got throttled and returned None, so use the cached - # data from the station_data object - self._state = self.station_data.data[self._condition] - else: - if self._condition in data: - self._state = data[self._condition] - else: - _LOGGER.warning("%s sensor data not available from the " - "station", self._condition) - - _LOGGER.debug("Sensor: %s | Data: %s", self._name, self._state) - - -class AmbientStationData: - """Class to interface with ambient-api library.""" - - def __init__(self, hass, api_key, app_key): - """Initialize station data object.""" - self.hass = hass - self._api_keys = { - 'AMBIENT_ENDPOINT': - 'https://api.ambientweather.net/v1', - 'AMBIENT_API_KEY': api_key, - 'AMBIENT_APPLICATION_KEY': app_key, - 'log_level': 'DEBUG' - } - - self.data = None - self._station = None - self._api = None - self._devices = None - self.connect_success = False - - self.get_data = Throttle(SCAN_INTERVAL)(self.async_update) - self._connect_api() # attempt to connect to API - - async def async_update(self): - """Get new data.""" - # refresh API connection since servers turn over nightly - _LOGGER.debug("Getting new data from server") - new_data = None - await self.hass.async_add_executor_job(self._connect_api) - await asyncio.sleep(2) # need minimum 2 seconds between API calls - if self._station is not None: - data = await self.hass.async_add_executor_job( - self._station.get_data) - if data is not None: - new_data = data[0] - self.data = new_data - else: - _LOGGER.debug("data is None type") - else: - _LOGGER.debug("Station is None type") - - return new_data - - def _connect_api(self): - """Connect to the API and capture new data.""" - from ambient_api.ambientapi import AmbientAPI - - self._api = AmbientAPI(**self._api_keys) - self._devices = self._api.get_devices() - - if self._devices: - self._station = self._devices[0] - if self._station is not None: - self.connect_success = True - else: - _LOGGER.debug("No station devices available") diff --git a/homeassistant/config_entries.py b/homeassistant/config_entries.py index 00b5d797682..159f5651c31 100644 --- a/homeassistant/config_entries.py +++ b/homeassistant/config_entries.py @@ -135,6 +135,7 @@ SOURCE_IMPORT = 'import' HANDLERS = Registry() # Components that have config flows. In future we will auto-generate this list. FLOWS = [ + 'ambient_station', 'cast', 'daikin', 'deconz', diff --git a/requirements_all.txt b/requirements_all.txt index 58c9e81d272..ca4459f66bc 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -86,6 +86,9 @@ abodepy==0.15.0 # homeassistant.components.media_player.frontier_silicon afsapi==0.0.4 +# homeassistant.components.ambient_station +aioambient==0.1.0 + # homeassistant.components.asuswrt aioasuswrt==1.1.18 @@ -141,9 +144,6 @@ alarmdecoder==1.13.2 # homeassistant.components.sensor.alpha_vantage alpha_vantage==2.1.0 -# homeassistant.components.sensor.ambient_station -ambient_api==1.5.2 - # homeassistant.components.amcrest amcrest==1.2.3 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 6f16780d4c2..2dbd2760c7d 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -30,6 +30,9 @@ PyTransportNSW==0.1.1 # homeassistant.components.notify.yessssms YesssSMS==0.2.3 +# homeassistant.components.ambient_station +aioambient==0.1.0 + # homeassistant.components.device_tracker.automatic aioautomatic==0.6.5 diff --git a/script/gen_requirements_all.py b/script/gen_requirements_all.py index 8817ee61e8f..79ba3f8c342 100755 --- a/script/gen_requirements_all.py +++ b/script/gen_requirements_all.py @@ -36,6 +36,7 @@ COMMENT_REQUIREMENTS = ( ) TEST_REQUIREMENTS = ( + 'aioambient', 'aioautomatic', 'aiohttp_cors', 'aiohue', diff --git a/tests/components/ambient_station/__init__.py b/tests/components/ambient_station/__init__.py new file mode 100644 index 00000000000..1de98ab57bb --- /dev/null +++ b/tests/components/ambient_station/__init__.py @@ -0,0 +1 @@ +"""Define tests for the Ambient PWS component.""" diff --git a/tests/components/ambient_station/test_config_flow.py b/tests/components/ambient_station/test_config_flow.py new file mode 100644 index 00000000000..a988208e4a0 --- /dev/null +++ b/tests/components/ambient_station/test_config_flow.py @@ -0,0 +1,130 @@ +"""Define tests for the Ambient PWS config flow.""" +import json + +import aioambient +import pytest + +from homeassistant import data_entry_flow +from homeassistant.components.ambient_station import ( + CONF_APP_KEY, DOMAIN, config_flow) +from homeassistant.const import CONF_API_KEY + +from tests.common import ( + load_fixture, MockConfigEntry, MockDependency, mock_coro) + + +@pytest.fixture +def get_devices_response(): + """Define a fixture for a successful /devices response.""" + return mock_coro() + + +@pytest.fixture +def mock_aioambient(get_devices_response): + """Mock the aioambient library.""" + with MockDependency('aioambient') as mock_aioambient_: + mock_aioambient_.Client( + ).api.get_devices.return_value = get_devices_response + yield mock_aioambient_ + + +async def test_duplicate_error(hass): + """Test that errors are shown when duplicates are added.""" + conf = { + CONF_API_KEY: '12345abcde12345abcde', + CONF_APP_KEY: '67890fghij67890fghij', + } + + MockConfigEntry(domain=DOMAIN, data=conf).add_to_hass(hass) + flow = config_flow.AmbientStationFlowHandler() + flow.hass = hass + + result = await flow.async_step_user(user_input=conf) + assert result['errors'] == {CONF_APP_KEY: 'identifier_exists'} + + +@pytest.mark.parametrize( + 'get_devices_response', + [mock_coro(exception=aioambient.errors.AmbientError)]) +async def test_invalid_api_key(hass, mock_aioambient): + """Test that an invalid API/App Key throws an error.""" + conf = { + CONF_API_KEY: '12345abcde12345abcde', + CONF_APP_KEY: '67890fghij67890fghij', + } + + flow = config_flow.AmbientStationFlowHandler() + flow.hass = hass + + result = await flow.async_step_user(user_input=conf) + assert result['errors'] == {'base': 'invalid_key'} + + +@pytest.mark.parametrize('get_devices_response', [mock_coro(return_value=[])]) +async def test_no_devices(hass, mock_aioambient): + """Test that an account with no associated devices throws an error.""" + conf = { + CONF_API_KEY: '12345abcde12345abcde', + CONF_APP_KEY: '67890fghij67890fghij', + } + + flow = config_flow.AmbientStationFlowHandler() + flow.hass = hass + + result = await flow.async_step_user(user_input=conf) + assert result['errors'] == {'base': 'no_devices'} + + +async def test_show_form(hass): + """Test that the form is served with no input.""" + flow = config_flow.AmbientStationFlowHandler() + flow.hass = hass + + result = await flow.async_step_user(user_input=None) + + assert result['type'] == data_entry_flow.RESULT_TYPE_FORM + assert result['step_id'] == 'user' + + +@pytest.mark.parametrize( + 'get_devices_response', + [mock_coro(return_value=json.loads(load_fixture('ambient_devices.json')))]) +async def test_step_import(hass, mock_aioambient): + """Test that the import step works.""" + conf = { + CONF_API_KEY: '12345abcde12345abcde', + CONF_APP_KEY: '67890fghij67890fghij', + } + + flow = config_flow.AmbientStationFlowHandler() + flow.hass = hass + + result = await flow.async_step_import(import_config=conf) + assert result['type'] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result['title'] == '67890fghij67' + assert result['data'] == { + CONF_API_KEY: '12345abcde12345abcde', + CONF_APP_KEY: '67890fghij67890fghij', + } + + +@pytest.mark.parametrize( + 'get_devices_response', + [mock_coro(return_value=json.loads(load_fixture('ambient_devices.json')))]) +async def test_step_user(hass, mock_aioambient): + """Test that the user step works.""" + conf = { + CONF_API_KEY: '12345abcde12345abcde', + CONF_APP_KEY: '67890fghij67890fghij', + } + + flow = config_flow.AmbientStationFlowHandler() + flow.hass = hass + + result = await flow.async_step_user(user_input=conf) + assert result['type'] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result['title'] == '67890fghij67' + assert result['data'] == { + CONF_API_KEY: '12345abcde12345abcde', + CONF_APP_KEY: '67890fghij67890fghij', + } diff --git a/tests/fixtures/ambient_devices.json b/tests/fixtures/ambient_devices.json new file mode 100644 index 00000000000..cd5edc21cb0 --- /dev/null +++ b/tests/fixtures/ambient_devices.json @@ -0,0 +1,15 @@ +[{ + "macAddress": "12:34:56:78:90:AB", + "lastData": { + "dateutc": 1546889640000, + "baromrelin": 30.09, + "baromabsin": 24.61, + "tempinf": 68.9, + "humidityin": 30, + "date": "2019-01-07T19:34:00.000Z" + }, + "info": { + "name": "Home", + "location": "Home" + } +}] From bd335e1ac158a2726ebf432ce7c20abb06b46b67 Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Tue, 29 Jan 2019 00:52:42 +0100 Subject: [PATCH 145/222] Area registry (#20435) * First draft of area registry * Refactor based on input * Add tests for areas Add tests for updating device * Updating a device shouldn't require area * Fix Martins comment * Require admin * Save after deleting * Rename read to list_areas Fix device entry_dict Remove area id from device when deleting area * Fix tests --- .../components/config/area_registry.py | 126 ++++++++++++++ .../components/config/device_registry.py | 60 +++++-- homeassistant/helpers/area_registry.py | 139 ++++++++++++++++ homeassistant/helpers/device_registry.py | 24 ++- tests/common.py | 53 +++--- tests/components/config/test_area_registry.py | 155 ++++++++++++++++++ .../components/config/test_device_registry.py | 28 +++- tests/helpers/test_area_registry.py | 127 ++++++++++++++ tests/helpers/test_device_registry.py | 37 +++++ 9 files changed, 714 insertions(+), 35 deletions(-) create mode 100644 homeassistant/components/config/area_registry.py create mode 100644 homeassistant/helpers/area_registry.py create mode 100644 tests/components/config/test_area_registry.py create mode 100644 tests/helpers/test_area_registry.py diff --git a/homeassistant/components/config/area_registry.py b/homeassistant/components/config/area_registry.py new file mode 100644 index 00000000000..7f1bb938228 --- /dev/null +++ b/homeassistant/components/config/area_registry.py @@ -0,0 +1,126 @@ +"""HTTP views to interact with the area registry.""" +import voluptuous as vol + +from homeassistant.components import websocket_api +from homeassistant.components.websocket_api.decorators import ( + async_response, require_admin) +from homeassistant.core import callback +from homeassistant.helpers.area_registry import async_get_registry + + +DEPENDENCIES = ['websocket_api'] + +WS_TYPE_LIST = 'config/area_registry/list' +SCHEMA_WS_LIST = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend({ + vol.Required('type'): WS_TYPE_LIST, +}) + +WS_TYPE_CREATE = 'config/area_registry/create' +SCHEMA_WS_CREATE = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend({ + vol.Required('type'): WS_TYPE_CREATE, + vol.Required('name'): str, +}) + +WS_TYPE_DELETE = 'config/area_registry/delete' +SCHEMA_WS_DELETE = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend({ + vol.Required('type'): WS_TYPE_DELETE, + vol.Required('area_id'): str, +}) + +WS_TYPE_UPDATE = 'config/area_registry/update' +SCHEMA_WS_UPDATE = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend({ + vol.Required('type'): WS_TYPE_UPDATE, + vol.Required('area_id'): str, + vol.Required('name'): str, +}) + + +async def async_setup(hass): + """Enable the Area Registry views.""" + hass.components.websocket_api.async_register_command( + WS_TYPE_LIST, websocket_list_areas, SCHEMA_WS_LIST + ) + hass.components.websocket_api.async_register_command( + WS_TYPE_CREATE, websocket_create_area, SCHEMA_WS_CREATE + ) + hass.components.websocket_api.async_register_command( + WS_TYPE_DELETE, websocket_delete_area, SCHEMA_WS_DELETE + ) + hass.components.websocket_api.async_register_command( + WS_TYPE_UPDATE, websocket_update_area, SCHEMA_WS_UPDATE + ) + return True + + +@async_response +async def websocket_list_areas(hass, connection, msg): + """Handle list areas command.""" + registry = await async_get_registry(hass) + connection.send_message(websocket_api.result_message( + msg['id'], [{ + 'name': entry.name, + 'area_id': entry.id, + } for entry in registry.async_list_areas()] + )) + + +@require_admin +@async_response +async def websocket_create_area(hass, connection, msg): + """Create area command.""" + registry = await async_get_registry(hass) + try: + entry = registry.async_create(msg['name']) + except ValueError as err: + connection.send_message(websocket_api.error_message( + msg['id'], 'invalid_info', str(err) + )) + else: + connection.send_message(websocket_api.result_message( + msg['id'], _entry_dict(entry) + )) + + +@require_admin +@async_response +async def websocket_delete_area(hass, connection, msg): + """Delete area command.""" + registry = await async_get_registry(hass) + + try: + await registry.async_delete(msg['area_id']) + except KeyError: + connection.send_message(websocket_api.error_message( + msg['id'], 'invalid_info', "Area ID doesn't exist" + )) + else: + connection.send_message(websocket_api.result_message( + msg['id'], 'success' + )) + + +@require_admin +@async_response +async def websocket_update_area(hass, connection, msg): + """Handle update area websocket command.""" + registry = await async_get_registry(hass) + + try: + entry = registry.async_update(msg['area_id'], msg['name']) + except ValueError as err: + connection.send_message(websocket_api.error_message( + msg['id'], 'invalid_info', str(err) + )) + else: + connection.send_message(websocket_api.result_message( + msg['id'], _entry_dict(entry) + )) + + +@callback +def _entry_dict(entry): + """Convert entry to API format.""" + return { + 'area_id': entry.id, + 'name': entry.name + } diff --git a/homeassistant/components/config/device_registry.py b/homeassistant/components/config/device_registry.py index ecbac703296..d81bdeb1f06 100644 --- a/homeassistant/components/config/device_registry.py +++ b/homeassistant/components/config/device_registry.py @@ -1,8 +1,11 @@ """HTTP views to interact with the device registry.""" import voluptuous as vol -from homeassistant.helpers.device_registry import async_get_registry from homeassistant.components import websocket_api +from homeassistant.components.websocket_api.decorators import ( + async_response, require_admin) +from homeassistant.core import callback +from homeassistant.helpers.device_registry import async_get_registry DEPENDENCIES = ['websocket_api'] @@ -11,29 +14,60 @@ SCHEMA_WS_LIST = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend({ vol.Required('type'): WS_TYPE_LIST, }) +WS_TYPE_UPDATE = 'config/device_registry/update' +SCHEMA_WS_UPDATE = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend({ + vol.Required('type'): WS_TYPE_UPDATE, + vol.Required('device_id'): str, + vol.Optional('area_id'): str, +}) + async def async_setup(hass): - """Enable the Entity Registry views.""" + """Enable the Device Registry views.""" hass.components.websocket_api.async_register_command( WS_TYPE_LIST, websocket_list_devices, SCHEMA_WS_LIST ) + hass.components.websocket_api.async_register_command( + WS_TYPE_UPDATE, websocket_update_device, SCHEMA_WS_UPDATE + ) return True -@websocket_api.async_response +@async_response async def websocket_list_devices(hass, connection, msg): """Handle list devices command.""" registry = await async_get_registry(hass) connection.send_message(websocket_api.result_message( - msg['id'], [{ - 'config_entries': list(entry.config_entries), - 'connections': list(entry.connections), - 'manufacturer': entry.manufacturer, - 'model': entry.model, - 'name': entry.name, - 'sw_version': entry.sw_version, - 'id': entry.id, - 'hub_device_id': entry.hub_device_id, - } for entry in registry.devices.values()] + msg['id'], [_entry_dict(entry) for entry in registry.devices.values()] )) + + +@require_admin +@async_response +async def websocket_update_device(hass, connection, msg): + """Handle update area websocket command.""" + registry = await async_get_registry(hass) + + entry = registry.async_update_device( + msg['device_id'], area_id=msg['area_id']) + + connection.send_message(websocket_api.result_message( + msg['id'], _entry_dict(entry) + )) + + +@callback +def _entry_dict(entry): + """Convert entry to API format.""" + return { + 'config_entries': list(entry.config_entries), + 'connections': list(entry.connections), + 'manufacturer': entry.manufacturer, + 'model': entry.model, + 'name': entry.name, + 'sw_version': entry.sw_version, + 'id': entry.id, + 'hub_device_id': entry.hub_device_id, + 'area_id': entry.area_id, + } diff --git a/homeassistant/helpers/area_registry.py b/homeassistant/helpers/area_registry.py new file mode 100644 index 00000000000..19ad52534cb --- /dev/null +++ b/homeassistant/helpers/area_registry.py @@ -0,0 +1,139 @@ +"""Provide a way to connect devices to one physical location.""" +import logging +import uuid +from collections import OrderedDict +from typing import List, Optional + +import attr + +from homeassistant.core import callback +from homeassistant.loader import bind_hass + +_LOGGER = logging.getLogger(__name__) + +DATA_REGISTRY = 'area_registry' + +STORAGE_KEY = 'core.area_registry' +STORAGE_VERSION = 1 +SAVE_DELAY = 10 + + +@attr.s(slots=True, frozen=True) +class AreaEntry: + """Area Registry Entry.""" + + name = attr.ib(type=str, default=None) + id = attr.ib(type=str, default=attr.Factory(lambda: uuid.uuid4().hex)) + + +class AreaRegistry: + """Class to hold a registry of areas.""" + + def __init__(self, hass) -> None: + """Initialize the area registry.""" + self.hass = hass + self.areas = None + self._store = hass.helpers.storage.Store(STORAGE_VERSION, STORAGE_KEY) + + @callback + def async_list_areas(self) -> List[AreaEntry]: + """Get all areas.""" + return self.areas.values() + + @callback + def async_create(self, name: str) -> AreaEntry: + """Create a new area.""" + if self._async_is_registered(name): + raise ValueError('Name is already in use') + + area = AreaEntry() + self.areas[area.id] = area + + return self.async_update(area.id, name=name) + + async def async_delete(self, area_id: str) -> None: + """Delete area.""" + device_registry = await \ + self.hass.helpers.device_registry.async_get_registry() + device_registry.async_clear_area_id(area_id) + + del self.areas[area_id] + + self.async_schedule_save() + + @callback + def async_update(self, area_id: str, name: str) -> AreaEntry: + """Update name of area.""" + old = self.areas[area_id] + + changes = {} + + if name == old.name: + return old + + if self._async_is_registered(name): + raise ValueError('Name is already in use') + else: + changes['name'] = name + + new = self.areas[area_id] = attr.evolve(old, **changes) + self.async_schedule_save() + return new + + @callback + def _async_is_registered(self, name) -> Optional[AreaEntry]: + """Check if a name is currently registered.""" + for area in self.areas.values(): + if name == area.name: + return area + return False + + async def async_load(self) -> None: + """Load the area registry.""" + data = await self._store.async_load() + + areas = OrderedDict() + + if data is not None: + for area in data['areas']: + areas[area['id']] = AreaEntry( + name=area['name'], + id=area['id'] + ) + + self.areas = areas + + @callback + def async_schedule_save(self) -> None: + """Schedule saving the area registry.""" + self._store.async_delay_save(self._data_to_save, SAVE_DELAY) + + @callback + def _data_to_save(self) -> dict: + """Return data of area registry to store in a file.""" + data = {} + + data['areas'] = [ + { + 'name': entry.name, + 'id': entry.id, + } for entry in self.areas.values() + ] + + return data + + +@bind_hass +async def async_get_registry(hass) -> AreaRegistry: + """Return area registry instance.""" + task = hass.data.get(DATA_REGISTRY) + + if task is None: + async def _load_reg(): + registry = AreaRegistry(hass) + await registry.async_load() + return registry + + task = hass.data[DATA_REGISTRY] = hass.async_create_task(_load_reg()) + + return await task diff --git a/homeassistant/helpers/device_registry.py b/homeassistant/helpers/device_registry.py index ce3700ea174..83827cca235 100644 --- a/homeassistant/helpers/device_registry.py +++ b/homeassistant/helpers/device_registry.py @@ -36,6 +36,7 @@ class DeviceEntry: name = attr.ib(type=str, default=None) sw_version = attr.ib(type=str, default=None) hub_device_id = attr.ib(type=str, default=None) + area_id = attr.ib(type=str, default=None) id = attr.ib(type=str, default=attr.Factory(lambda: uuid.uuid4().hex)) @@ -119,9 +120,14 @@ class DeviceRegistry: manufacturer=manufacturer, model=model, name=name, - sw_version=sw_version, + sw_version=sw_version ) + @callback + def async_update_device(self, device_id, *, area_id=_UNDEF): + """Update properties of a device.""" + return self._async_update_device(device_id, area_id=area_id) + @callback def _async_update_device(self, device_id, *, add_config_entry_id=_UNDEF, remove_config_entry_id=_UNDEF, @@ -131,7 +137,8 @@ class DeviceRegistry: model=_UNDEF, name=_UNDEF, sw_version=_UNDEF, - hub_device_id=_UNDEF): + hub_device_id=_UNDEF, + area_id=_UNDEF): """Update device attributes.""" old = self.devices[device_id] @@ -169,6 +176,9 @@ class DeviceRegistry: if value is not _UNDEF and value != getattr(old, attr_name): changes[attr_name] = value + if (area_id is not _UNDEF and area_id != old.area_id): + changes['area_id'] = area_id + if not changes: return old @@ -197,6 +207,8 @@ class DeviceRegistry: id=device['id'], # Introduced in 0.79 hub_device_id=device.get('hub_device_id'), + # Introduced in 0.87 + area_id=device.get('area_id') ) self.devices = devices @@ -222,6 +234,7 @@ class DeviceRegistry: 'sw_version': entry.sw_version, 'id': entry.id, 'hub_device_id': entry.hub_device_id, + 'area_id': entry.area_id } for entry in self.devices.values() ] @@ -235,6 +248,13 @@ class DeviceRegistry: self._async_update_device( dev_id, remove_config_entry_id=config_entry_id) + @callback + def async_clear_area_id(self, area_id: str) -> None: + """Clear area id from registry entries.""" + for dev_id, device in self.devices.items(): + if area_id == device.area_id: + self._async_update_device(dev_id, area_id=None) + @bind_hass async def async_get_registry(hass) -> DeviceRegistry: diff --git a/tests/common.py b/tests/common.py index d7b28b3039a..3452d945f22 100644 --- a/tests/common.py +++ b/tests/common.py @@ -1,35 +1,37 @@ """Test the helper method for writing tests.""" import asyncio -from collections import OrderedDict -from datetime import timedelta import functools as ft import json +import logging import os import sys -from unittest.mock import patch, MagicMock, Mock -from io import StringIO -import logging import threading -from contextlib import contextmanager -from homeassistant import auth, core as ha, config_entries +from collections import OrderedDict +from contextlib import contextmanager +from datetime import timedelta +from io import StringIO +from unittest.mock import MagicMock, Mock, patch + +import homeassistant.util.dt as date_util +import homeassistant.util.yaml as yaml + +from homeassistant import auth, config_entries, core as ha from homeassistant.auth import ( models as auth_models, auth_store, providers as auth_providers, permissions as auth_permissions) from homeassistant.auth.permissions import system_policies -from homeassistant.setup import setup_component, async_setup_component -from homeassistant.config import async_process_component_config -from homeassistant.helpers import ( - intent, entity, restore_state, entity_registry, - entity_platform, storage, device_registry) -from homeassistant.util.unit_system import METRIC_SYSTEM -import homeassistant.util.dt as date_util -import homeassistant.util.yaml as yaml -from homeassistant.const import ( - STATE_ON, STATE_OFF, DEVICE_DEFAULT_NAME, EVENT_TIME_CHANGED, - EVENT_STATE_CHANGED, EVENT_PLATFORM_DISCOVERED, ATTR_SERVICE, - ATTR_DISCOVERED, SERVER_PORT, EVENT_HOMEASSISTANT_CLOSE) from homeassistant.components import mqtt, recorder +from homeassistant.config import async_process_component_config +from homeassistant.const import ( + ATTR_DISCOVERED, ATTR_SERVICE, DEVICE_DEFAULT_NAME, + EVENT_HOMEASSISTANT_CLOSE, EVENT_PLATFORM_DISCOVERED, EVENT_STATE_CHANGED, + EVENT_TIME_CHANGED, SERVER_PORT, STATE_ON, STATE_OFF) +from homeassistant.helpers import ( + area_registry, device_registry, entity, entity_platform, entity_registry, + intent, restore_state, storage) +from homeassistant.setup import async_setup_component, setup_component +from homeassistant.util.unit_system import METRIC_SYSTEM from homeassistant.util.async_ import ( run_callback_threadsafe, run_coroutine_threadsafe) @@ -333,6 +335,19 @@ def mock_registry(hass, mock_entries=None): return registry +def mock_area_registry(hass, mock_entries=None): + """Mock the Area Registry.""" + registry = area_registry.AreaRegistry(hass) + registry.areas = mock_entries or OrderedDict() + + async def _get_reg(): + return registry + + hass.data[area_registry.DATA_REGISTRY] = \ + hass.loop.create_task(_get_reg()) + return registry + + def mock_device_registry(hass, mock_entries=None): """Mock the Device Registry.""" registry = device_registry.DeviceRegistry(hass) diff --git a/tests/components/config/test_area_registry.py b/tests/components/config/test_area_registry.py new file mode 100644 index 00000000000..875cd1a2e3c --- /dev/null +++ b/tests/components/config/test_area_registry.py @@ -0,0 +1,155 @@ +"""Test area_registry API.""" +import pytest + +from homeassistant.components.config import area_registry +from tests.common import mock_area_registry + + +@pytest.fixture +def client(hass, hass_ws_client): + """Fixture that can interact with the config manager API.""" + hass.loop.run_until_complete(area_registry.async_setup(hass)) + yield hass.loop.run_until_complete(hass_ws_client(hass)) + + +@pytest.fixture +def registry(hass): + """Return an empty, loaded, registry.""" + return mock_area_registry(hass) + + +async def test_list_areas(hass, client, registry): + """Test list entries.""" + registry.async_create('mock 1') + registry.async_create('mock 2') + + await client.send_json({ + 'id': 1, + 'type': 'config/area_registry/list', + }) + + msg = await client.receive_json() + + assert len(msg['result']) == len(registry.areas) + + +async def test_create_area(hass, client, registry): + """Test create entry.""" + await client.send_json({ + 'id': 1, + 'name': "mock", + 'type': 'config/area_registry/create', + }) + + msg = await client.receive_json() + + assert 'mock' in msg['result']['name'] + assert len(registry.areas) == 1 + + +async def test_create_area_with_name_already_in_use(hass, client, registry): + """Test create entry that should fail.""" + registry.async_create('mock') + + await client.send_json({ + 'id': 1, + 'name': "mock", + 'type': 'config/area_registry/create', + }) + + msg = await client.receive_json() + + assert not msg['success'] + assert msg['error']['code'] == 'invalid_info' + assert msg['error']['message'] == "Name is already in use" + assert len(registry.areas) == 1 + + +async def test_delete_area(hass, client, registry): + """Test delete entry.""" + area = registry.async_create('mock') + + await client.send_json({ + 'id': 1, + 'area_id': area.id, + 'type': 'config/area_registry/delete', + }) + + msg = await client.receive_json() + + assert msg['success'] + assert not registry.areas + + +async def test_delete_non_existing_area(hass, client, registry): + """Test delete entry that should fail.""" + registry.async_create('mock') + + await client.send_json({ + 'id': 1, + 'area_id': '', + 'type': 'config/area_registry/delete', + }) + + msg = await client.receive_json() + + assert not msg['success'] + assert msg['error']['code'] == 'invalid_info' + assert msg['error']['message'] == "Area ID doesn't exist" + assert len(registry.areas) == 1 + + +async def test_update_area(hass, client, registry): + """Test update entry.""" + area = registry.async_create('mock 1') + + await client.send_json({ + 'id': 1, + 'area_id': area.id, + 'name': "mock 2", + 'type': 'config/area_registry/update', + }) + + msg = await client.receive_json() + + assert msg['result']['area_id'] == area.id + assert msg['result']['name'] == 'mock 2' + assert len(registry.areas) == 1 + + +async def test_update_area_with_same_name(hass, client, registry): + """Test update entry.""" + area = registry.async_create('mock 1') + + await client.send_json({ + 'id': 1, + 'area_id': area.id, + 'name': "mock 1", + 'type': 'config/area_registry/update', + }) + + msg = await client.receive_json() + + assert msg['result']['area_id'] == area.id + assert msg['result']['name'] == 'mock 1' + assert len(registry.areas) == 1 + + +async def test_update_area_with_name_already_in_use(hass, client, registry): + """Test update entry.""" + area = registry.async_create('mock 1') + registry.async_create('mock 2') + + await client.send_json({ + 'id': 1, + 'area_id': area.id, + 'name': "mock 2", + 'type': 'config/area_registry/update', + }) + + msg = await client.receive_json() + + assert not msg['success'] + assert msg['error']['code'] == 'invalid_info' + assert msg['error']['message'] == "Name is already in use" + assert len(registry.areas) == 2 diff --git a/tests/components/config/test_device_registry.py b/tests/components/config/test_device_registry.py index 87eb0fb2d6f..aa1b9e4e2d4 100644 --- a/tests/components/config/test_device_registry.py +++ b/tests/components/config/test_device_registry.py @@ -1,4 +1,4 @@ -"""Test entity_registry API.""" +"""Test device_registry API.""" import pytest from homeassistant.components.config import device_registry @@ -48,6 +48,7 @@ async def test_list_devices(hass, client, registry): 'name': None, 'sw_version': None, 'hub_device_id': None, + 'area_id': None, }, { 'config_entries': ['1234'], @@ -57,5 +58,30 @@ async def test_list_devices(hass, client, registry): 'name': None, 'sw_version': None, 'hub_device_id': dev1, + 'area_id': None, } ] + + +async def test_update_device(hass, client, registry): + """Test update entry.""" + device = registry.async_get_or_create( + config_entry_id='1234', + connections={('ethernet', '12:34:56:78:90:AB:CD:EF')}, + identifiers={('bridgeid', '0123')}, + manufacturer='manufacturer', model='model') + + assert not device.area_id + + await client.send_json({ + 'id': 1, + 'device_id': device.id, + 'area_id': '12345A', + 'type': 'config/device_registry/update', + }) + + msg = await client.receive_json() + + assert msg['result']['id'] == device.id + assert msg['result']['area_id'] == '12345A' + assert len(registry.devices) == 1 diff --git a/tests/helpers/test_area_registry.py b/tests/helpers/test_area_registry.py new file mode 100644 index 00000000000..9f2801fe334 --- /dev/null +++ b/tests/helpers/test_area_registry.py @@ -0,0 +1,127 @@ +"""Tests for the Area Registry.""" +import pytest + +from homeassistant.helpers import area_registry +from tests.common import mock_area_registry, flush_store + + +@pytest.fixture +def registry(hass): + """Return an empty, loaded, registry.""" + return mock_area_registry(hass) + + +async def test_list_areas(registry): + """Make sure that we can read areas.""" + registry.async_create('mock') + + areas = registry.async_list_areas() + + assert len(areas) == len(registry.areas) + + +async def test_create_area(registry): + """Make sure that we can create an area.""" + area = registry.async_create('mock') + + assert area.name == 'mock' + assert len(registry.areas) == 1 + + +async def test_create_area_with_name_already_in_use(registry): + """Make sure that we can't create an area with a name already in use.""" + area1 = registry.async_create('mock') + + with pytest.raises(ValueError) as e_info: + area2 = registry.async_create('mock') + assert area1 != area2 + assert e_info == "Name is already in use" + + assert len(registry.areas) == 1 + + +async def test_delete_area(registry): + """Make sure that we can delete an area.""" + area = registry.async_create('mock') + + await registry.async_delete(area.id) + + assert not registry.areas + + +async def test_delete_non_existing_area(registry): + """Make sure that we can't delete an area that doesn't exist.""" + registry.async_create('mock') + + with pytest.raises(KeyError): + await registry.async_delete('') + + assert len(registry.areas) == 1 + + +async def test_update_area(registry): + """Make sure that we can read areas.""" + area = registry.async_create('mock') + + updated_area = registry.async_update(area.id, name='mock1') + + assert updated_area != area + assert updated_area.name == 'mock1' + assert len(registry.areas) == 1 + + +async def test_update_area_with_same_name(registry): + """Make sure that we can reapply the same name to the area.""" + area = registry.async_create('mock') + + updated_area = registry.async_update(area.id, name='mock') + + assert updated_area == area + assert len(registry.areas) == 1 + + +async def test_update_area_with_name_already_in_use(registry): + """Make sure that we can't update an area with a name already in use.""" + area1 = registry.async_create('mock1') + area2 = registry.async_create('mock2') + + with pytest.raises(ValueError) as e_info: + registry.async_update(area1.id, name='mock2') + assert e_info == "Name is already in use" + + assert area1.name == 'mock1' + assert area2.name == 'mock2' + assert len(registry.areas) == 2 + + +async def test_load_area(hass, registry): + """Make sure that we can load/save data correctly.""" + registry.async_create('mock1') + registry.async_create('mock2') + + assert len(registry.areas) == 2 + + registry2 = area_registry.AreaRegistry(hass) + await flush_store(registry._store) + await registry2.async_load() + + assert list(registry.areas) == list(registry2.areas) + + +async def test_loading_area_from_storage(hass, hass_storage): + """Test loading stored areas on start.""" + hass_storage[area_registry.STORAGE_KEY] = { + 'version': area_registry.STORAGE_VERSION, + 'data': { + 'areas': [ + { + 'id': '12345A', + 'name': 'mock' + } + ] + } + } + + registry = await area_registry.async_get_registry(hass) + + assert len(registry.areas) == 1 diff --git a/tests/helpers/test_device_registry.py b/tests/helpers/test_device_registry.py index 59bcab92b1e..93fffaa4ecc 100644 --- a/tests/helpers/test_device_registry.py +++ b/tests/helpers/test_device_registry.py @@ -133,6 +133,7 @@ async def test_loading_from_storage(hass, hass_storage): 'model': 'model', 'name': 'name', 'sw_version': 'version', + 'area_id': '12345A' } ] } @@ -146,6 +147,7 @@ async def test_loading_from_storage(hass, hass_storage): identifiers={('serial', '12:34:56:AB:CD:EF')}, manufacturer='manufacturer', model='model') assert entry.id == 'abcdefghijklm' + assert entry.area_id == '12345A' assert isinstance(entry.config_entries, set) @@ -186,6 +188,25 @@ async def test_removing_config_entries(registry): assert entry3.config_entries == set() +async def test_removing_area_id(registry): + """Make sure we can clear area id.""" + entry = registry.async_get_or_create( + config_entry_id='123', + connections={ + (device_registry.CONNECTION_NETWORK_MAC, '12:34:56:AB:CD:EF') + }, + identifiers={('bridgeid', '0123')}, + manufacturer='manufacturer', model='model') + + entry_w_area = registry.async_update_device(entry.id, area_id='12345A') + + registry.async_clear_area_id('12345A') + entry_wo_area = registry.async_get_device({('bridgeid', '0123')}, set()) + + assert not entry_wo_area.area_id + assert entry_w_area != entry_wo_area + + async def test_specifying_hub_device_create(registry): """Test specifying a hub and updating.""" hub = registry.async_get_or_create( @@ -328,3 +349,19 @@ async def test_format_mac(registry): }, ) assert list(invalid_mac_entry.connections)[0][1] == invalid + + +async def test_update(registry): + """Verify that we can update area_id of a device.""" + entry = registry.async_get_or_create( + config_entry_id='1234', + connections={ + (device_registry.CONNECTION_NETWORK_MAC, '12:34:56:AB:CD:EF') + }) + + assert not entry.area_id + + updated_entry = registry.async_update_device(entry.id, area_id='12345A') + + assert updated_entry != entry + assert updated_entry.area_id == '12345A' From bb4ca1f52533b9f42d01bde542984967d29abd1d Mon Sep 17 00:00:00 2001 From: emontnemery Date: Tue, 29 Jan 2019 00:56:47 +0100 Subject: [PATCH 146/222] Cleanup if discovered mqtt vacuum can't be added (#20549) --- homeassistant/components/mqtt/vacuum.py | 15 ++++++++--- tests/components/mqtt/test_vacuum.py | 33 +++++++++++++++++++++++++ 2 files changed, 44 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/mqtt/vacuum.py b/homeassistant/components/mqtt/vacuum.py index ca3f800385f..90cca62da38 100644 --- a/homeassistant/components/mqtt/vacuum.py +++ b/homeassistant/components/mqtt/vacuum.py @@ -12,7 +12,8 @@ from homeassistant.components import mqtt from homeassistant.components.mqtt import ( ATTR_DISCOVERY_HASH, CONF_UNIQUE_ID, MqttAttributes, MqttAvailability, MqttDiscoveryUpdate, MqttEntityDeviceInfo, subscription) -from homeassistant.components.mqtt.discovery import MQTT_DISCOVERY_NEW +from homeassistant.components.mqtt.discovery import ( + MQTT_DISCOVERY_NEW, clear_discovery_hash) from homeassistant.components.vacuum import ( DOMAIN, SUPPORT_BATTERY, SUPPORT_CLEAN_SPOT, SUPPORT_FAN_SPEED, SUPPORT_LOCATE, SUPPORT_PAUSE, SUPPORT_RETURN_HOME, SUPPORT_SEND_COMMAND, @@ -160,9 +161,15 @@ async def async_setup_entry(hass, config_entry, async_add_entities): """Set up MQTT vacuum dynamically through MQTT discovery.""" async def async_discover(discovery_payload): """Discover and add a MQTT vacuum.""" - config = PLATFORM_SCHEMA(discovery_payload) - await _async_setup_entity(config, async_add_entities, config_entry, - discovery_payload[ATTR_DISCOVERY_HASH]) + try: + discovery_hash = discovery_payload[ATTR_DISCOVERY_HASH] + config = PLATFORM_SCHEMA(discovery_payload) + await _async_setup_entity(config, async_add_entities, config_entry, + discovery_hash) + except Exception: + if discovery_hash: + clear_discovery_hash(hass, discovery_hash) + raise async_dispatcher_connect( hass, MQTT_DISCOVERY_NEW.format(DOMAIN, 'mqtt'), async_discover) diff --git a/tests/components/mqtt/test_vacuum.py b/tests/components/mqtt/test_vacuum.py index f7cc6ce1f44..6a61495c143 100644 --- a/tests/components/mqtt/test_vacuum.py +++ b/tests/components/mqtt/test_vacuum.py @@ -299,6 +299,39 @@ async def test_discovery_removal_vacuum(hass, mock_publish): assert state is None +async def test_discovery_broken(hass, mqtt_mock, caplog): + """Test handling of bad discovery message.""" + entry = MockConfigEntry(domain=mqtt.DOMAIN) + await async_start(hass, 'homeassistant', {}, entry) + + data1 = ( + '{ "name": "Beer",' + ' "command_topic": "test_topic#" }' + ) + data2 = ( + '{ "name": "Milk",' + ' "command_topic": "test_topic" }' + ) + + async_fire_mqtt_message(hass, 'homeassistant/vacuum/bla/config', + data1) + await hass.async_block_till_done() + + state = hass.states.get('vacuum.beer') + assert state is None + + async_fire_mqtt_message(hass, 'homeassistant/vacuum/bla/config', + data2) + await hass.async_block_till_done() + await hass.async_block_till_done() + + state = hass.states.get('vacuum.milk') + assert state is not None + assert state.name == 'Milk' + state = hass.states.get('vacuum.beer') + assert state is None + + async def test_discovery_update_vacuum(hass, mock_publish): """Test update of discovered vacuum.""" entry = MockConfigEntry(domain=mqtt.DOMAIN) From d7ba2aad1da30b53ec521914d950901b488424fd Mon Sep 17 00:00:00 2001 From: emontnemery Date: Tue, 29 Jan 2019 01:14:55 +0100 Subject: [PATCH 147/222] Add COMPONENT_SCHEMA and use it in alarm_control_panel (#20224) * Add COMPONENT_SCHEMA and use in alarm and mqtt * Revert MQTT changes * Lint * Small tweak * Add tests * Rename COMPONENT_SCHEMA to PLATFORM_SCHEMA_BASE * Fix tests * Improve tests --- .../alarm_control_panel/__init__.py | 3 +- homeassistant/config.py | 12 +- homeassistant/helpers/config_validation.py | 9 ++ tests/common.py | 7 +- tests/test_setup.py | 115 +++++++++++++++++- 5 files changed, 137 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/alarm_control_panel/__init__.py b/homeassistant/components/alarm_control_panel/__init__.py index 7f3dc2ac8f5..e02e074189c 100644 --- a/homeassistant/components/alarm_control_panel/__init__.py +++ b/homeassistant/components/alarm_control_panel/__init__.py @@ -13,7 +13,8 @@ from homeassistant.const import ( ATTR_CODE, ATTR_CODE_FORMAT, ATTR_ENTITY_ID, SERVICE_ALARM_TRIGGER, SERVICE_ALARM_DISARM, SERVICE_ALARM_ARM_HOME, SERVICE_ALARM_ARM_AWAY, SERVICE_ALARM_ARM_NIGHT, SERVICE_ALARM_ARM_CUSTOM_BYPASS) -from homeassistant.helpers.config_validation import PLATFORM_SCHEMA # noqa +from homeassistant.helpers.config_validation import ( # noqa + PLATFORM_SCHEMA_BASE, PLATFORM_SCHEMA_2 as PLATFORM_SCHEMA) import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity_component import EntityComponent diff --git a/homeassistant/config.py b/homeassistant/config.py index 0edadf6a78d..3fd138f54e4 100644 --- a/homeassistant/config.py +++ b/homeassistant/config.py @@ -742,13 +742,19 @@ def async_process_component_config( async_log_exception(ex, domain, config, hass) return None - elif hasattr(component, 'PLATFORM_SCHEMA'): + elif (hasattr(component, 'PLATFORM_SCHEMA') or + hasattr(component, 'PLATFORM_SCHEMA_BASE')): platforms = [] for p_name, p_config in config_per_platform(config, domain): # Validate component specific platform schema try: - p_validated = component.PLATFORM_SCHEMA( # type: ignore - p_config) + if hasattr(component, 'PLATFORM_SCHEMA_BASE'): + p_validated = \ + component.PLATFORM_SCHEMA_BASE( # type: ignore + p_config) + else: + p_validated = component.PLATFORM_SCHEMA( # type: ignore + p_config) except vol.Invalid as ex: async_log_exception(ex, domain, config, hass) continue diff --git a/homeassistant/helpers/config_validation.py b/homeassistant/helpers/config_validation.py index 56d64cd8fd9..f3371a26725 100644 --- a/homeassistant/helpers/config_validation.py +++ b/homeassistant/helpers/config_validation.py @@ -557,6 +557,15 @@ PLATFORM_SCHEMA = vol.Schema({ vol.Optional(CONF_SCAN_INTERVAL): time_period }, extra=vol.ALLOW_EXTRA) +# This will replace PLATFORM_SCHEMA once all base components are updated +PLATFORM_SCHEMA_2 = vol.Schema({ + vol.Required(CONF_PLATFORM): string, + vol.Optional(CONF_SCAN_INTERVAL): time_period +}) + +PLATFORM_SCHEMA_BASE = PLATFORM_SCHEMA_2.extend({ +}, extra=vol.ALLOW_EXTRA) + EVENT_SCHEMA = vol.Schema({ vol.Optional(CONF_ALIAS): string, vol.Required('event'): string, diff --git a/tests/common.py b/tests/common.py index 3452d945f22..0f9b372c161 100644 --- a/tests/common.py +++ b/tests/common.py @@ -450,8 +450,8 @@ class MockModule: # pylint: disable=invalid-name def __init__(self, domain=None, dependencies=None, setup=None, requirements=None, config_schema=None, platform_schema=None, - async_setup=None, async_setup_entry=None, - async_unload_entry=None): + platform_schema_base=None, async_setup=None, + async_setup_entry=None, async_unload_entry=None): """Initialize the mock module.""" self.DOMAIN = domain self.DEPENDENCIES = dependencies or [] @@ -463,6 +463,9 @@ class MockModule: if platform_schema is not None: self.PLATFORM_SCHEMA = platform_schema + if platform_schema_base is not None: + self.PLATFORM_SCHEMA_BASE = platform_schema_base + if setup is not None: # We run this in executor, wrap it in function self.setup = lambda *args: setup(*args) diff --git a/tests/test_setup.py b/tests/test_setup.py index 2e44ee539d7..6d2cc770013 100644 --- a/tests/test_setup.py +++ b/tests/test_setup.py @@ -14,7 +14,8 @@ from homeassistant.const import ( import homeassistant.config as config_util from homeassistant import setup, loader import homeassistant.util.dt as dt_util -from homeassistant.helpers.config_validation import PLATFORM_SCHEMA +from homeassistant.helpers.config_validation import ( + PLATFORM_SCHEMA_2 as PLATFORM_SCHEMA, PLATFORM_SCHEMA_BASE) from homeassistant.helpers import discovery from tests.common import \ @@ -94,18 +95,24 @@ class TestSetup: platform_schema = PLATFORM_SCHEMA.extend({ 'hello': str, }) + platform_schema_base = PLATFORM_SCHEMA_BASE.extend({ + }) loader.set_component( self.hass, 'platform_conf', - MockModule('platform_conf', platform_schema=platform_schema)) + MockModule('platform_conf', + platform_schema_base=platform_schema_base)) loader.set_component( self.hass, - 'platform_conf.whatever', MockPlatform('whatever')) + 'platform_conf.whatever', + MockPlatform('whatever', + platform_schema=platform_schema)) with assert_setup_component(0): assert setup.setup_component(self.hass, 'platform_conf', { 'platform_conf': { + 'platform': 'whatever', 'hello': 'world', 'invalid': 'extra', } @@ -121,6 +128,7 @@ class TestSetup: 'hello': 'world', }, 'platform_conf 2': { + 'platform': 'whatever', 'invalid': True } }) @@ -175,6 +183,107 @@ class TestSetup: assert 'platform_conf' in self.hass.config.components assert not config['platform_conf'] # empty + def test_validate_platform_config_2(self): + """Test component PLATFORM_SCHEMA_BASE prio over PLATFORM_SCHEMA.""" + platform_schema = PLATFORM_SCHEMA.extend({ + 'hello': str, + }) + platform_schema_base = PLATFORM_SCHEMA_BASE.extend({ + 'hello': 'world', + }) + loader.set_component( + self.hass, + 'platform_conf', + MockModule('platform_conf', + platform_schema=platform_schema, + platform_schema_base=platform_schema_base)) + + loader.set_component( + self.hass, + 'platform_conf.whatever', + MockPlatform('whatever', + platform_schema=platform_schema)) + + with assert_setup_component(0): + assert setup.setup_component(self.hass, 'platform_conf', { + # fail: no extra keys allowed in platform schema + 'platform_conf': { + 'platform': 'whatever', + 'hello': 'world', + 'invalid': 'extra', + } + }) + + self.hass.data.pop(setup.DATA_SETUP) + self.hass.config.components.remove('platform_conf') + + with assert_setup_component(1): + assert setup.setup_component(self.hass, 'platform_conf', { + # pass + 'platform_conf': { + 'platform': 'whatever', + 'hello': 'world', + }, + # fail: key hello violates component platform_schema_base + 'platform_conf 2': { + 'platform': 'whatever', + 'hello': 'there' + } + }) + + self.hass.data.pop(setup.DATA_SETUP) + self.hass.config.components.remove('platform_conf') + + def test_validate_platform_config_3(self): + """Test fallback to component PLATFORM_SCHEMA.""" + component_schema = PLATFORM_SCHEMA_BASE.extend({ + 'hello': str, + }) + platform_schema = PLATFORM_SCHEMA.extend({ + 'cheers': str, + 'hello': 'world', + }) + loader.set_component( + self.hass, + 'platform_conf', + MockModule('platform_conf', + platform_schema=component_schema)) + + loader.set_component( + self.hass, + 'platform_conf.whatever', + MockPlatform('whatever', + platform_schema=platform_schema)) + + with assert_setup_component(0): + assert setup.setup_component(self.hass, 'platform_conf', { + 'platform_conf': { + # fail: no extra keys allowed + 'hello': 'world', + 'invalid': 'extra', + } + }) + + self.hass.data.pop(setup.DATA_SETUP) + self.hass.config.components.remove('platform_conf') + + with assert_setup_component(1): + assert setup.setup_component(self.hass, 'platform_conf', { + # pass + 'platform_conf': { + 'platform': 'whatever', + 'hello': 'world', + }, + # fail: key hello violates component platform_schema + 'platform_conf 2': { + 'platform': 'whatever', + 'hello': 'there' + } + }) + + self.hass.data.pop(setup.DATA_SETUP) + self.hass.config.components.remove('platform_conf') + def test_component_not_found(self): """setup_component should not crash if component doesn't exist.""" assert not setup.setup_component(self.hass, 'non_existing') From bb1583c45315daddb940b2a7694aa3abb5f7ede8 Mon Sep 17 00:00:00 2001 From: emontnemery Date: Tue, 29 Jan 2019 01:21:38 +0100 Subject: [PATCH 148/222] Add discovery update support to MQTT camera (#20529) --- homeassistant/components/mqtt/camera.py | 84 ++++++++++----- tests/components/mqtt/test_camera.py | 133 +++++++++++++++++++++++- 2 files changed, 188 insertions(+), 29 deletions(-) diff --git a/homeassistant/components/mqtt/camera.py b/homeassistant/components/mqtt/camera.py index 50b998f6e9c..be176a39a25 100644 --- a/homeassistant/components/mqtt/camera.py +++ b/homeassistant/components/mqtt/camera.py @@ -12,8 +12,10 @@ import voluptuous as vol from homeassistant.components import camera, mqtt from homeassistant.components.camera import PLATFORM_SCHEMA, Camera -from homeassistant.components.mqtt import CONF_UNIQUE_ID -from homeassistant.components.mqtt.discovery import MQTT_DISCOVERY_NEW +from homeassistant.components.mqtt import ( + ATTR_DISCOVERY_HASH, CONF_UNIQUE_ID, MqttDiscoveryUpdate, subscription) +from homeassistant.components.mqtt.discovery import ( + MQTT_DISCOVERY_NEW, clear_discovery_hash) from homeassistant.const import CONF_NAME from homeassistant.core import callback from homeassistant.helpers import config_validation as cv @@ -37,43 +39,79 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ async def async_setup_platform(hass: HomeAssistantType, config: ConfigType, async_add_entities, discovery_info=None): """Set up MQTT camera through configuration.yaml.""" - await _async_setup_entity(hass, config, async_add_entities) + await _async_setup_entity(config, async_add_entities) async def async_setup_entry(hass, config_entry, async_add_entities): """Set up MQTT camera dynamically through MQTT discovery.""" async def async_discover(discovery_payload): """Discover and add a MQTT camera.""" - config = PLATFORM_SCHEMA(discovery_payload) - await _async_setup_entity(hass, config, async_add_entities) + try: + discovery_hash = discovery_payload[ATTR_DISCOVERY_HASH] + config = PLATFORM_SCHEMA(discovery_payload) + await _async_setup_entity(config, async_add_entities, + discovery_hash) + except Exception: + if discovery_hash: + clear_discovery_hash(hass, discovery_hash) + raise async_dispatcher_connect( hass, MQTT_DISCOVERY_NEW.format(camera.DOMAIN, 'mqtt'), async_discover) -async def _async_setup_entity(hass, config, async_add_entities): +async def _async_setup_entity(config, async_add_entities, discovery_hash=None): """Set up the MQTT Camera.""" - async_add_entities([MqttCamera( - config.get(CONF_NAME), - config.get(CONF_UNIQUE_ID), - config.get(CONF_TOPIC) - )]) + async_add_entities([MqttCamera(config, discovery_hash)]) -class MqttCamera(Camera): +class MqttCamera(MqttDiscoveryUpdate, Camera): """representation of a MQTT camera.""" - def __init__(self, name, unique_id, topic): + def __init__(self, config, discovery_hash): """Initialize the MQTT Camera.""" - super().__init__() + self._config = config + self._unique_id = config.get(CONF_UNIQUE_ID) + self._sub_state = None - self._name = name - self._unique_id = unique_id - self._topic = topic self._qos = 0 self._last_image = None + Camera.__init__(self) + MqttDiscoveryUpdate.__init__(self, discovery_hash, + self.discovery_update) + + async def async_added_to_hass(self): + """Subscribe MQTT events.""" + await super().async_added_to_hass() + await self._subscribe_topics() + + async def discovery_update(self, discovery_payload): + """Handle updated discovery message.""" + config = PLATFORM_SCHEMA(discovery_payload) + self._config = config + await self._subscribe_topics() + self.async_schedule_update_ha_state() + + async def _subscribe_topics(self): + """(Re)Subscribe to topics.""" + @callback + def message_received(topic, payload, qos): + """Handle new MQTT messages.""" + self._last_image = payload + + self._sub_state = await subscription.async_subscribe_topics( + self.hass, self._sub_state, + {'state_topic': {'topic': self._config.get(CONF_TOPIC), + 'msg_callback': message_received, + 'qos': self._qos}}) + + async def async_will_remove_from_hass(self): + """Unsubscribe when removed.""" + self._sub_state = await subscription.async_unsubscribe_topics( + self.hass, self._sub_state) + @asyncio.coroutine def async_camera_image(self): """Return image response.""" @@ -82,19 +120,9 @@ class MqttCamera(Camera): @property def name(self): """Return the name of this camera.""" - return self._name + return self._config.get(CONF_NAME) @property def unique_id(self): """Return a unique ID.""" return self._unique_id - - async def async_added_to_hass(self): - """Subscribe MQTT events.""" - @callback - def message_received(topic, payload, qos): - """Handle new MQTT messages.""" - self._last_image = payload - - await mqtt.async_subscribe( - self.hass, self._topic, message_received, self._qos, None) diff --git a/tests/components/mqtt/test_camera.py b/tests/components/mqtt/test_camera.py index a127ce0e68e..15b4ed22378 100644 --- a/tests/components/mqtt/test_camera.py +++ b/tests/components/mqtt/test_camera.py @@ -1,9 +1,14 @@ """The tests for mqtt camera component.""" import asyncio +from unittest.mock import ANY +from homeassistant.components import camera, mqtt +from homeassistant.components.mqtt.discovery import async_start from homeassistant.setup import async_setup_component -from tests.common import async_fire_mqtt_message, async_mock_mqtt_component +from tests.common import ( + MockConfigEntry, async_fire_mqtt_message, async_mock_mqtt_component, + mock_registry) @asyncio.coroutine @@ -51,3 +56,129 @@ def test_unique_id(hass): async_fire_mqtt_message(hass, 'test-topic', 'payload') yield from hass.async_block_till_done() assert len(hass.states.async_all()) == 1 + + +async def test_discovery_removal_camera(hass, mqtt_mock, caplog): + """Test removal of discovered camera.""" + entry = MockConfigEntry(domain=mqtt.DOMAIN) + await async_start(hass, 'homeassistant', {}, entry) + + data = ( + '{ "name": "Beer",' + ' "topic": "test_topic"}' + ) + + async_fire_mqtt_message(hass, 'homeassistant/camera/bla/config', + data) + await hass.async_block_till_done() + await hass.async_block_till_done() + + state = hass.states.get('camera.beer') + assert state is not None + assert state.name == 'Beer' + + async_fire_mqtt_message(hass, 'homeassistant/camera/bla/config', + '') + await hass.async_block_till_done() + await hass.async_block_till_done() + + state = hass.states.get('camera.beer') + assert state is None + + +async def test_discovery_update_camera(hass, mqtt_mock, caplog): + """Test update of discovered camera.""" + entry = MockConfigEntry(domain=mqtt.DOMAIN) + await async_start(hass, 'homeassistant', {}, entry) + + data1 = ( + '{ "name": "Beer",' + ' "topic": "test_topic"}' + ) + data2 = ( + '{ "name": "Milk",' + ' "topic": "test_topic"}' + ) + + async_fire_mqtt_message(hass, 'homeassistant/camera/bla/config', + data1) + await hass.async_block_till_done() + + state = hass.states.get('camera.beer') + assert state is not None + assert state.name == 'Beer' + + async_fire_mqtt_message(hass, 'homeassistant/camera/bla/config', + data2) + await hass.async_block_till_done() + await hass.async_block_till_done() + + state = hass.states.get('camera.beer') + assert state is not None + assert state.name == 'Milk' + state = hass.states.get('camera.milk') + assert state is None + + +async def test_discovery_broken(hass, mqtt_mock, caplog): + """Test handling of bad discovery message.""" + entry = MockConfigEntry(domain=mqtt.DOMAIN) + await async_start(hass, 'homeassistant', {}, entry) + + data1 = ( + '{ "name": "Beer" }' + ) + data2 = ( + '{ "name": "Milk",' + ' "topic": "test_topic"}' + ) + + async_fire_mqtt_message(hass, 'homeassistant/camera/bla/config', + data1) + await hass.async_block_till_done() + + state = hass.states.get('camera.beer') + assert state is None + + async_fire_mqtt_message(hass, 'homeassistant/camera/bla/config', + data2) + await hass.async_block_till_done() + await hass.async_block_till_done() + + state = hass.states.get('camera.milk') + assert state is not None + assert state.name == 'Milk' + state = hass.states.get('camera.beer') + assert state is None + + +async def test_entity_id_update(hass, mqtt_mock): + """Test MQTT subscriptions are managed when entity_id is updated.""" + registry = mock_registry(hass, {}) + mock_mqtt = await async_mock_mqtt_component(hass) + assert await async_setup_component(hass, camera.DOMAIN, { + camera.DOMAIN: [{ + 'platform': 'mqtt', + 'name': 'beer', + 'topic': 'test-topic', + 'unique_id': 'TOTALLY_UNIQUE' + }] + }) + + state = hass.states.get('camera.beer') + assert state is not None + assert mock_mqtt.async_subscribe.call_count == 1 + mock_mqtt.async_subscribe.assert_any_call('test-topic', ANY, 0, 'utf-8') + mock_mqtt.async_subscribe.reset_mock() + + registry.async_update_entity('camera.beer', new_entity_id='camera.milk') + await hass.async_block_till_done() + await hass.async_block_till_done() + + state = hass.states.get('camera.beer') + assert state is None + + state = hass.states.get('camera.milk') + assert state is not None + assert mock_mqtt.async_subscribe.call_count == 1 + mock_mqtt.async_subscribe.assert_any_call('test-topic', ANY, 0, 'utf-8') From 34090bd021cd66cd5c637c3a386461096e76f910 Mon Sep 17 00:00:00 2001 From: "David F. Mulcahey" Date: Mon, 28 Jan 2019 19:40:00 -0500 Subject: [PATCH 149/222] ZHA component rewrite part 3 - update helpers (#20463) * update helpers * address comments * remove ieee * cluster id as hex too --- homeassistant/components/zha/core/helpers.py | 30 +++++++++++++++----- 1 file changed, 23 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/zha/core/helpers.py b/homeassistant/components/zha/core/helpers.py index a182479d221..6957edc4f3f 100644 --- a/homeassistant/components/zha/core/helpers.py +++ b/homeassistant/components/zha/core/helpers.py @@ -55,7 +55,7 @@ async def bind_cluster(entity_id, cluster): ) -async def configure_reporting(entity_id, cluster, attr, skip_bind=False, +async def configure_reporting(entity_id, cluster, attr, min_report=REPORT_CONFIG_MIN_INT, max_report=REPORT_CONFIG_MAX_INT, reportable_change=REPORT_CONFIG_RPT_CHANGE, @@ -68,12 +68,13 @@ async def configure_reporting(entity_id, cluster, attr, skip_bind=False, from zigpy.exceptions import DeliveryError attr_name = cluster.attributes.get(attr, [attr])[0] + attr_id = get_attr_id_by_name(cluster, attr_name) cluster_name = cluster.ep_attribute kwargs = {} if manufacturer: kwargs['manufacturer'] = manufacturer try: - res = await cluster.configure_reporting(attr, min_report, + res = await cluster.configure_reporting(attr_id, min_report, max_report, reportable_change, **kwargs) _LOGGER.debug( @@ -101,11 +102,11 @@ async def bind_configure_reporting(entity_id, cluster, attr, skip_bind=False, if not skip_bind: await bind_cluster(entity_id, cluster) - await configure_reporting(entity_id, cluster, attr, skip_bind=False, - min_report=REPORT_CONFIG_MIN_INT, - max_report=REPORT_CONFIG_MAX_INT, - reportable_change=REPORT_CONFIG_RPT_CHANGE, - manufacturer=None) + await configure_reporting(entity_id, cluster, attr, + min_report=min_report, + max_report=max_report, + reportable_change=reportable_change, + manufacturer=manufacturer) async def check_zigpy_connection(usb_path, radio_type, database_path): @@ -136,3 +137,18 @@ def convert_ieee(ieee_str): """Convert given ieee string to EUI64.""" from zigpy.types import EUI64, uint8_t return EUI64([uint8_t(p, base=16) for p in ieee_str.split(':')]) + + +def construct_unique_id(cluster): + """Construct a unique id from a cluster.""" + return "0x{:04x}:{}:0x{:04x}".format( + cluster.endpoint.device.nwk, + cluster.endpoint.endpoint_id, + cluster.cluster_id + ) + + +def get_attr_id_by_name(cluster, attr_name): + """Get the attribute id for a cluster attribute by its name.""" + return next((attrid for attrid, (attrname, datatype) in + cluster.attributes.items() if attr_name == attrname), None) From 717a0c2b2d3bbbd0c08fe5642d522280288aaa2d Mon Sep 17 00:00:00 2001 From: Fredrik Erlandsson Date: Tue, 29 Jan 2019 01:46:37 +0100 Subject: [PATCH 150/222] fix #20387 devices without model/protocol (#20530) --- homeassistant/components/tellduslive/entry.py | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/tellduslive/entry.py b/homeassistant/components/tellduslive/entry.py index 929dc700afb..d6e56329699 100644 --- a/homeassistant/components/tellduslive/entry.py +++ b/homeassistant/components/tellduslive/entry.py @@ -116,10 +116,17 @@ class TelldusLiveEntity(Entity): def device_info(self): """Return device info.""" device = self._client.device_info(self.device.device_id) - return { + device_info = { 'identifiers': {('tellduslive', self.device.device_id)}, 'name': self.device.name, - 'model': device['model'].title(), - 'manufacturer': device['protocol'].title(), - 'via_hub': ('tellduslive', device.get('client')), } + model = device.get('model') + if model is not None: + device_info['model'] = model.title() + protocol = device.get('protocol') + if protocol is not None: + device_info['manufacturer'] = protocol.title() + client = device.get('client') + if client is not None: + device_info['via_hub'] = ('tellduslive', client) + return device_info From 3f484228cbe964ce94672aed6b92c82b424fb634 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Per=20Osb=C3=A4ck?= Date: Tue, 29 Jan 2019 01:48:55 +0100 Subject: [PATCH 151/222] Add missing switch for motion_detect (#20540) --- homeassistant/components/android_ip_webcam.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/android_ip_webcam.py b/homeassistant/components/android_ip_webcam.py index b8a2d461489..1cf46174371 100644 --- a/homeassistant/components/android_ip_webcam.py +++ b/homeassistant/components/android_ip_webcam.py @@ -123,8 +123,9 @@ ICON_MAP = { 'whitebalance_lock': 'mdi:white-balance-auto' } -SWITCHES = ['exposure_lock', 'ffc', 'focus', 'gps_active', 'night_vision', - 'overlay', 'torch', 'whitebalance_lock', 'video_recording'] +SWITCHES = ['exposure_lock', 'ffc', 'focus', 'gps_active', + 'motion_detect', 'night_vision', 'overlay', + 'torch', 'whitebalance_lock', 'video_recording'] SENSORS = ['audio_connections', 'battery_level', 'battery_temp', 'battery_voltage', 'light', 'motion', 'pressure', 'proximity', From f353d51ab1a85f2907fd747d6a8b4aeb918f78a4 Mon Sep 17 00:00:00 2001 From: starkillerOG Date: Tue, 29 Jan 2019 02:52:00 +0100 Subject: [PATCH 152/222] Add check to validate gamut (#20518) * color.util - Add check to validate gamut * fix indents * fix typo * Add check to validate gamut * Add tests for gamut checker * fix test * fix pylint issues * fix hue light gamut tests * add check to validate gamut * move None check * Move None check * Include prompt to update bridge/bulb on error * fix wrong commit * fix error message * Update light.py --- homeassistant/components/hue/light.py | 12 +++++++++++- homeassistant/util/color.py | 18 ++++++++++++++++++ tests/components/hue/test_light.py | 12 +++++++++--- tests/util/test_color.py | 20 ++++++++++++++++++++ 4 files changed, 58 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/hue/light.py b/homeassistant/components/hue/light.py index a10b42fbeee..3327d0f9dc2 100644 --- a/homeassistant/components/hue/light.py +++ b/homeassistant/components/hue/light.py @@ -41,6 +41,7 @@ SUPPORT_HUE = { } ATTR_IS_HUE_GROUP = 'is_hue_group' +GAMUT_TYPE_UNAVAILABLE = 'None' # Minimum Hue Bridge API version to support groups # 1.4.0 introduced extended group info # 1.12 introduced the state object for groups @@ -221,7 +222,7 @@ class HueLight(Light): if is_group: self.is_osram = False self.is_philips = False - self.gamut_typ = 'None' + self.gamut_typ = GAMUT_TYPE_UNAVAILABLE self.gamut = None else: self.is_osram = light.manufacturername == 'OSRAM' @@ -229,6 +230,15 @@ class HueLight(Light): self.gamut_typ = self.light.colorgamuttype self.gamut = self.light.colorgamut _LOGGER.debug("Color gamut of %s: %s", self.name, str(self.gamut)) + if self.gamut: + if not color.check_valid_gamut(self.gamut): + err = "Please check for software updates of the bridge " \ + "and/or bulb in the Philips Hue App, " \ + "Color gamut of %s: %s, not valid, " \ + "setting gamut to None." + _LOGGER.warning(err, self.name, str(self.gamut)) + self.gamut_typ = GAMUT_TYPE_UNAVAILABLE + self.gamut = None @property def unique_id(self): diff --git a/homeassistant/util/color.py b/homeassistant/util/color.py index 5a32d89a793..6d7f3336566 100644 --- a/homeassistant/util/color.py +++ b/homeassistant/util/color.py @@ -600,3 +600,21 @@ def check_point_in_lamps_reach(p: Tuple[float, float], t = cross_product(v1, q) / cross_product(v1, v2) return (s >= 0.0) and (t >= 0.0) and (s + t <= 1.0) + + +def check_valid_gamut(Gamut: GamutType) -> bool: + """Check if the supplied gamut is valid.""" + # Check if the three points of the supplied gamut are not on the same line. + v1 = XYPoint(Gamut.green.x - Gamut.red.x, Gamut.green.y - Gamut.red.y) + v2 = XYPoint(Gamut.blue.x - Gamut.red.x, Gamut.blue.y - Gamut.red.y) + not_on_line = cross_product(v1, v2) > 0.0001 + + # Check if all six coordinates of the gamut lie between 0 and 1. + red_valid = Gamut.red.x >= 0 and Gamut.red.x <= 1 and \ + Gamut.red.y >= 0 and Gamut.red.y <= 1 + green_valid = Gamut.green.x >= 0 and Gamut.green.x <= 1 and \ + Gamut.green.y >= 0 and Gamut.green.y <= 1 + blue_valid = Gamut.blue.x >= 0 and Gamut.blue.x <= 1 and \ + Gamut.blue.y >= 0 and Gamut.blue.y <= 1 + + return not_on_line and red_valid and green_valid and blue_valid diff --git a/tests/components/hue/test_light.py b/tests/components/hue/test_light.py index f7865fcf4f8..eabfd6d35aa 100644 --- a/tests/components/hue/test_light.py +++ b/tests/components/hue/test_light.py @@ -701,7 +701,9 @@ def test_available(): """Test available property.""" light = hue_light.HueLight( light=Mock(state={'reachable': False}, - raw=LIGHT_RAW), + raw=LIGHT_RAW, + colorgamuttype=LIGHT_GAMUT_TYPE, + colorgamut=LIGHT_GAMUT), request_bridge_update=None, bridge=Mock(allow_unreachable=False), is_group=False, @@ -711,7 +713,9 @@ def test_available(): light = hue_light.HueLight( light=Mock(state={'reachable': False}, - raw=LIGHT_RAW), + raw=LIGHT_RAW, + colorgamuttype=LIGHT_GAMUT_TYPE, + colorgamut=LIGHT_GAMUT), request_bridge_update=None, bridge=Mock(allow_unreachable=True), is_group=False, @@ -721,7 +725,9 @@ def test_available(): light = hue_light.HueLight( light=Mock(state={'reachable': False}, - raw=LIGHT_RAW), + raw=LIGHT_RAW, + colorgamuttype=LIGHT_GAMUT_TYPE, + colorgamut=LIGHT_GAMUT), request_bridge_update=None, bridge=Mock(allow_unreachable=False), is_group=True, diff --git a/tests/util/test_color.py b/tests/util/test_color.py index b54b2bc5776..14195d43821 100644 --- a/tests/util/test_color.py +++ b/tests/util/test_color.py @@ -8,6 +8,18 @@ import voluptuous as vol GAMUT = color_util.GamutType(color_util.XYPoint(0.704, 0.296), color_util.XYPoint(0.2151, 0.7106), color_util.XYPoint(0.138, 0.08)) +GAMUT_INVALID_1 = color_util.GamutType(color_util.XYPoint(0.704, 0.296), + color_util.XYPoint(-0.201, 0.7106), + color_util.XYPoint(0.138, 0.08)) +GAMUT_INVALID_2 = color_util.GamutType(color_util.XYPoint(0.704, 1.296), + color_util.XYPoint(0.2151, 0.7106), + color_util.XYPoint(0.138, 0.08)) +GAMUT_INVALID_3 = color_util.GamutType(color_util.XYPoint(0.0, 0.0), + color_util.XYPoint(0.0, 0.0), + color_util.XYPoint(0.0, 0.0)) +GAMUT_INVALID_4 = color_util.GamutType(color_util.XYPoint(0.1, 0.1), + color_util.XYPoint(0.3, 0.3), + color_util.XYPoint(0.7, 0.7)) class TestColorUtil(unittest.TestCase): @@ -338,6 +350,14 @@ class TestColorUtil(unittest.TestCase): assert color_util.color_rgb_to_hex(51, 153, 255) == '3399ff' assert color_util.color_rgb_to_hex(255, 67.9204190, 0) == 'ff4400' + def test_gamut(self): + """Test gamut functions.""" + assert color_util.check_valid_gamut(GAMUT) + assert not color_util.check_valid_gamut(GAMUT_INVALID_1) + assert not color_util.check_valid_gamut(GAMUT_INVALID_2) + assert not color_util.check_valid_gamut(GAMUT_INVALID_3) + assert not color_util.check_valid_gamut(GAMUT_INVALID_4) + class ColorTemperatureMiredToKelvinTests(unittest.TestCase): """Test color_temperature_mired_to_kelvin.""" From 3ee3acd5509e52995e3296235d8a92a8fefec7a2 Mon Sep 17 00:00:00 2001 From: emontnemery Date: Tue, 29 Jan 2019 04:45:34 +0100 Subject: [PATCH 153/222] Update device registry of MQTT light (#20441) * Update device registry of MQTT light * Move config_entry to constructor --- .../components/mqtt/light/__init__.py | 6 +-- .../components/mqtt/light/schema_basic.py | 9 ++-- .../components/mqtt/light/schema_json.py | 11 ++-- .../components/mqtt/light/schema_template.py | 9 ++-- tests/components/mqtt/test_light.py | 47 +++++++++++++++++ tests/components/mqtt/test_light_json.py | 48 ++++++++++++++++++ tests/components/mqtt/test_light_template.py | 50 +++++++++++++++++++ 7 files changed, 164 insertions(+), 16 deletions(-) diff --git a/homeassistant/components/mqtt/light/__init__.py b/homeassistant/components/mqtt/light/__init__.py index 77a1b1d3c10..908ad1ac989 100644 --- a/homeassistant/components/mqtt/light/__init__.py +++ b/homeassistant/components/mqtt/light/__init__.py @@ -55,7 +55,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): try: discovery_hash = discovery_payload[ATTR_DISCOVERY_HASH] config = PLATFORM_SCHEMA(discovery_payload) - await _async_setup_entity(config, async_add_entities, + await _async_setup_entity(config, async_add_entities, config_entry, discovery_hash) except Exception: if discovery_hash: @@ -67,7 +67,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): async_discover) -async def _async_setup_entity(config, async_add_entities, +async def _async_setup_entity(config, async_add_entities, config_entry=None, discovery_hash=None): """Set up a MQTT Light.""" setup_entity = { @@ -76,4 +76,4 @@ async def _async_setup_entity(config, async_add_entities, 'template': schema_template.async_setup_entity_template, } await setup_entity[config[CONF_SCHEMA]]( - config, async_add_entities, discovery_hash) + config, async_add_entities, config_entry, discovery_hash) diff --git a/homeassistant/components/mqtt/light/schema_basic.py b/homeassistant/components/mqtt/light/schema_basic.py index c4bf41fa673..0ba1db890d7 100644 --- a/homeassistant/components/mqtt/light/schema_basic.py +++ b/homeassistant/components/mqtt/light/schema_basic.py @@ -111,13 +111,13 @@ PLATFORM_SCHEMA_BASIC = mqtt.MQTT_RW_PLATFORM_SCHEMA.extend({ mqtt.MQTT_JSON_ATTRS_SCHEMA.schema) -async def async_setup_entity_basic(config, async_add_entities, +async def async_setup_entity_basic(config, async_add_entities, config_entry, discovery_hash=None): """Set up a MQTT Light.""" config.setdefault( CONF_STATE_VALUE_TEMPLATE, config.get(CONF_VALUE_TEMPLATE)) - async_add_entities([MqttLight(config, discovery_hash)]) + async_add_entities([MqttLight(config, config_entry, discovery_hash)]) # pylint: disable=too-many-ancestors @@ -125,7 +125,7 @@ class MqttLight(MqttAttributes, MqttAvailability, MqttDiscoveryUpdate, MqttEntityDeviceInfo, Light, RestoreEntity): """Representation of a MQTT light.""" - def __init__(self, config, discovery_hash): + def __init__(self, config, config_entry, discovery_hash): """Initialize MQTT light.""" self._state = False self._sub_state = None @@ -157,7 +157,7 @@ class MqttLight(MqttAttributes, MqttAvailability, MqttDiscoveryUpdate, MqttAvailability.__init__(self, config) MqttDiscoveryUpdate.__init__(self, discovery_hash, self.discovery_update) - MqttEntityDeviceInfo.__init__(self, device_config) + MqttEntityDeviceInfo.__init__(self, device_config, config_entry) async def async_added_to_hass(self): """Subscribe to MQTT events.""" @@ -170,6 +170,7 @@ class MqttLight(MqttAttributes, MqttAvailability, MqttDiscoveryUpdate, self._setup_from_config(config) await self.attributes_discovery_update(config) await self.availability_discovery_update(config) + await self.device_info_discovery_update(config) await self._subscribe_topics() self.async_schedule_update_ha_state() diff --git a/homeassistant/components/mqtt/light/schema_json.py b/homeassistant/components/mqtt/light/schema_json.py index e1372b9a80f..4b1a1cff3ff 100644 --- a/homeassistant/components/mqtt/light/schema_json.py +++ b/homeassistant/components/mqtt/light/schema_json.py @@ -84,10 +84,10 @@ PLATFORM_SCHEMA_JSON = mqtt.MQTT_RW_PLATFORM_SCHEMA.extend({ mqtt.MQTT_JSON_ATTRS_SCHEMA.schema) -async def async_setup_entity_json(config: ConfigType, - async_add_entities, discovery_hash): +async def async_setup_entity_json(config: ConfigType, async_add_entities, + config_entry, discovery_hash): """Set up a MQTT JSON Light.""" - async_add_entities([MqttLightJson(config, discovery_hash)]) + async_add_entities([MqttLightJson(config, config_entry, discovery_hash)]) # pylint: disable=too-many-ancestors @@ -95,7 +95,7 @@ class MqttLightJson(MqttAttributes, MqttAvailability, MqttDiscoveryUpdate, MqttEntityDeviceInfo, Light, RestoreEntity): """Representation of a MQTT JSON light.""" - def __init__(self, config, discovery_hash): + def __init__(self, config, config_entry, discovery_hash): """Initialize MQTT JSON light.""" self._state = False self._sub_state = None @@ -120,7 +120,7 @@ class MqttLightJson(MqttAttributes, MqttAvailability, MqttDiscoveryUpdate, MqttAvailability.__init__(self, config) MqttDiscoveryUpdate.__init__(self, discovery_hash, self.discovery_update) - MqttEntityDeviceInfo.__init__(self, device_config) + MqttEntityDeviceInfo.__init__(self, device_config, config_entry) async def async_added_to_hass(self): """Subscribe to MQTT events.""" @@ -133,6 +133,7 @@ class MqttLightJson(MqttAttributes, MqttAvailability, MqttDiscoveryUpdate, self._setup_from_config(config) await self.attributes_discovery_update(config) await self.availability_discovery_update(config) + await self.device_info_discovery_update(config) await self._subscribe_topics() self.async_schedule_update_ha_state() diff --git a/homeassistant/components/mqtt/light/schema_template.py b/homeassistant/components/mqtt/light/schema_template.py index 80d773060d7..9b8e57c1d2a 100644 --- a/homeassistant/components/mqtt/light/schema_template.py +++ b/homeassistant/components/mqtt/light/schema_template.py @@ -70,10 +70,10 @@ PLATFORM_SCHEMA_TEMPLATE = mqtt.MQTT_RW_PLATFORM_SCHEMA.extend({ mqtt.MQTT_JSON_ATTRS_SCHEMA.schema) -async def async_setup_entity_template(config, async_add_entities, +async def async_setup_entity_template(config, async_add_entities, config_entry, discovery_hash): """Set up a MQTT Template light.""" - async_add_entities([MqttTemplate(config, discovery_hash)]) + async_add_entities([MqttTemplate(config, config_entry, discovery_hash)]) # pylint: disable=too-many-ancestors @@ -81,7 +81,7 @@ class MqttTemplate(MqttAttributes, MqttAvailability, MqttDiscoveryUpdate, MqttEntityDeviceInfo, Light, RestoreEntity): """Representation of a MQTT Template light.""" - def __init__(self, config, discovery_hash): + def __init__(self, config, config_entry, discovery_hash): """Initialize a MQTT Template light.""" self._state = False self._sub_state = None @@ -107,7 +107,7 @@ class MqttTemplate(MqttAttributes, MqttAvailability, MqttDiscoveryUpdate, MqttAvailability.__init__(self, config) MqttDiscoveryUpdate.__init__(self, discovery_hash, self.discovery_update) - MqttEntityDeviceInfo.__init__(self, device_config) + MqttEntityDeviceInfo.__init__(self, device_config, config_entry) async def async_added_to_hass(self): """Subscribe to MQTT events.""" @@ -120,6 +120,7 @@ class MqttTemplate(MqttAttributes, MqttAvailability, MqttDiscoveryUpdate, self._setup_from_config(config) await self.attributes_discovery_update(config) await self.availability_discovery_update(config) + await self.device_info_discovery_update(config) await self._subscribe_topics() self.async_schedule_update_ha_state() diff --git a/tests/components/mqtt/test_light.py b/tests/components/mqtt/test_light.py index 9ad3a8d8323..f501f97331e 100644 --- a/tests/components/mqtt/test_light.py +++ b/tests/components/mqtt/test_light.py @@ -1346,6 +1346,53 @@ async def test_entity_device_info_with_identifier(hass, mqtt_mock): assert device.sw_version == '0.1-beta' +async def test_entity_device_info_update(hass, mqtt_mock): + """Test device registry update.""" + entry = MockConfigEntry(domain=mqtt.DOMAIN) + entry.add_to_hass(hass) + await async_start(hass, 'homeassistant', {}, entry) + registry = await hass.helpers.device_registry.async_get_registry() + + config = { + 'platform': 'mqtt', + 'name': 'Test 1', + 'state_topic': 'test-topic', + 'command_topic': 'test-command-topic', + 'device': { + 'identifiers': ['helloworld'], + 'connections': [ + ["mac", "02:5b:26:a8:dc:12"], + ], + 'manufacturer': 'Whatever', + 'name': 'Beer', + 'model': 'Glass', + 'sw_version': '0.1-beta', + }, + 'unique_id': 'veryunique' + } + + data = json.dumps(config) + async_fire_mqtt_message(hass, 'homeassistant/light/bla/config', + data) + await hass.async_block_till_done() + await hass.async_block_till_done() + + device = registry.async_get_device({('mqtt', 'helloworld')}, set()) + assert device is not None + assert device.name == 'Beer' + + config['device']['name'] = 'Milk' + data = json.dumps(config) + async_fire_mqtt_message(hass, 'homeassistant/light/bla/config', + data) + await hass.async_block_till_done() + await hass.async_block_till_done() + + device = registry.async_get_device({('mqtt', 'helloworld')}, set()) + assert device is not None + assert device.name == 'Milk' + + async def test_entity_id_update(hass, mqtt_mock): """Test MQTT subscriptions are managed when entity_id is updated.""" registry = mock_registry(hass, {}) diff --git a/tests/components/mqtt/test_light_json.py b/tests/components/mqtt/test_light_json.py index 86523d955a0..9533ea5f204 100644 --- a/tests/components/mqtt/test_light_json.py +++ b/tests/components/mqtt/test_light_json.py @@ -818,6 +818,54 @@ async def test_entity_device_info_with_identifier(hass, mqtt_mock): assert device.sw_version == '0.1-beta' +async def test_entity_device_info_update(hass, mqtt_mock): + """Test device registry update.""" + entry = MockConfigEntry(domain=mqtt.DOMAIN) + entry.add_to_hass(hass) + await async_start(hass, 'homeassistant', {}, entry) + registry = await hass.helpers.device_registry.async_get_registry() + + config = { + 'platform': 'mqtt', + 'name': 'Test 1', + 'schema': 'json', + 'state_topic': 'test-topic', + 'command_topic': 'test-command-topic', + 'device': { + 'identifiers': ['helloworld'], + 'connections': [ + ["mac", "02:5b:26:a8:dc:12"], + ], + 'manufacturer': 'Whatever', + 'name': 'Beer', + 'model': 'Glass', + 'sw_version': '0.1-beta', + }, + 'unique_id': 'veryunique' + } + + data = json.dumps(config) + async_fire_mqtt_message(hass, 'homeassistant/light/bla/config', + data) + await hass.async_block_till_done() + await hass.async_block_till_done() + + device = registry.async_get_device({('mqtt', 'helloworld')}, set()) + assert device is not None + assert device.name == 'Beer' + + config['device']['name'] = 'Milk' + data = json.dumps(config) + async_fire_mqtt_message(hass, 'homeassistant/light/bla/config', + data) + await hass.async_block_till_done() + await hass.async_block_till_done() + + device = registry.async_get_device({('mqtt', 'helloworld')}, set()) + assert device is not None + assert device.name == 'Milk' + + async def test_entity_id_update(hass, mqtt_mock): """Test MQTT subscriptions are managed when entity_id is updated.""" registry = mock_registry(hass, {}) diff --git a/tests/components/mqtt/test_light_template.py b/tests/components/mqtt/test_light_template.py index 66048b3e3bf..a7147e83b99 100644 --- a/tests/components/mqtt/test_light_template.py +++ b/tests/components/mqtt/test_light_template.py @@ -791,6 +791,56 @@ async def test_entity_device_info_with_identifier(hass, mqtt_mock): assert device.sw_version == '0.1-beta' +async def test_entity_device_info_update(hass, mqtt_mock): + """Test device registry update.""" + entry = MockConfigEntry(domain=mqtt.DOMAIN) + entry.add_to_hass(hass) + await async_start(hass, 'homeassistant', {}, entry) + registry = await hass.helpers.device_registry.async_get_registry() + + config = { + 'platform': 'mqtt', + 'name': 'Test 1', + 'schema': 'template', + 'state_topic': 'test-topic', + 'command_topic': 'test-command-topic', + 'command_on_template': 'on,{{ transition }}', + 'command_off_template': 'off,{{ transition|d }}', + 'device': { + 'identifiers': ['helloworld'], + 'connections': [ + ["mac", "02:5b:26:a8:dc:12"], + ], + 'manufacturer': 'Whatever', + 'name': 'Beer', + 'model': 'Glass', + 'sw_version': '0.1-beta', + }, + 'unique_id': 'veryunique' + } + + data = json.dumps(config) + async_fire_mqtt_message(hass, 'homeassistant/light/bla/config', + data) + await hass.async_block_till_done() + await hass.async_block_till_done() + + device = registry.async_get_device({('mqtt', 'helloworld')}, set()) + assert device is not None + assert device.name == 'Beer' + + config['device']['name'] = 'Milk' + data = json.dumps(config) + async_fire_mqtt_message(hass, 'homeassistant/light/bla/config', + data) + await hass.async_block_till_done() + await hass.async_block_till_done() + + device = registry.async_get_device({('mqtt', 'helloworld')}, set()) + assert device is not None + assert device.name == 'Milk' + + async def test_entity_id_update(hass, mqtt_mock): """Test MQTT subscriptions are managed when entity_id is updated.""" registry = mock_registry(hass, {}) From cc74035c3bfff501b3af7441c6434e79b9223fa6 Mon Sep 17 00:00:00 2001 From: Rohan Kapoor Date: Mon, 28 Jan 2019 20:26:31 -0800 Subject: [PATCH 154/222] Move CONF_UPDATE_INTERVAL to homeassistant.const (#20526) --- homeassistant/components/freedns.py | 5 ++--- homeassistant/components/mythicbeastsdns.py | 5 ++--- homeassistant/components/sensor/broadlink.py | 3 +-- homeassistant/components/sensor/darksky.py | 3 +-- homeassistant/components/sensor/fedex.py | 3 +-- homeassistant/components/sensor/magicseaweed.py | 1 - homeassistant/components/sensor/ups.py | 3 +-- homeassistant/components/tellduslive/__init__.py | 3 ++- homeassistant/components/tellduslive/const.py | 1 - homeassistant/components/volvooncall.py | 4 ++-- homeassistant/const.py | 1 + 11 files changed, 13 insertions(+), 19 deletions(-) diff --git a/homeassistant/components/freedns.py b/homeassistant/components/freedns.py index 0b5cbeda01a..ec38bb59cc7 100644 --- a/homeassistant/components/freedns.py +++ b/homeassistant/components/freedns.py @@ -12,7 +12,8 @@ import aiohttp import async_timeout import voluptuous as vol -from homeassistant.const import (CONF_URL, CONF_ACCESS_TOKEN) +from homeassistant.const import (CONF_URL, CONF_ACCESS_TOKEN, + CONF_UPDATE_INTERVAL) import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) @@ -24,8 +25,6 @@ DEFAULT_INTERVAL = timedelta(minutes=10) TIMEOUT = 10 UPDATE_URL = 'https://freedns.afraid.org/dynamic/update.php' -CONF_UPDATE_INTERVAL = 'update_interval' - CONFIG_SCHEMA = vol.Schema({ DOMAIN: vol.Schema({ vol.Exclusive(CONF_URL, DOMAIN): cv.string, diff --git a/homeassistant/components/mythicbeastsdns.py b/homeassistant/components/mythicbeastsdns.py index ff45fc8a530..d73e4619c78 100644 --- a/homeassistant/components/mythicbeastsdns.py +++ b/homeassistant/components/mythicbeastsdns.py @@ -9,7 +9,8 @@ import logging import voluptuous as vol import homeassistant.helpers.config_validation as cv -from homeassistant.const import CONF_HOST, CONF_DOMAIN, CONF_PASSWORD +from homeassistant.const import CONF_HOST, CONF_DOMAIN, CONF_PASSWORD, \ + CONF_UPDATE_INTERVAL from homeassistant.helpers.event import async_track_time_interval from homeassistant.helpers.aiohttp_client import async_get_clientsession @@ -21,8 +22,6 @@ DOMAIN = 'mythicbeastsdns' DEFAULT_INTERVAL = timedelta(minutes=10) -CONF_UPDATE_INTERVAL = 'update_interval' - CONFIG_SCHEMA = vol.Schema({ DOMAIN: vol.Schema({ vol.Required(CONF_HOST): cv.string, diff --git a/homeassistant/components/sensor/broadlink.py b/homeassistant/components/sensor/broadlink.py index 21e5b0ee1d9..50f9f955148 100644 --- a/homeassistant/components/sensor/broadlink.py +++ b/homeassistant/components/sensor/broadlink.py @@ -14,7 +14,7 @@ import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.const import ( CONF_HOST, CONF_MAC, CONF_MONITORED_CONDITIONS, CONF_NAME, TEMP_CELSIUS, - CONF_TIMEOUT) + CONF_TIMEOUT, CONF_UPDATE_INTERVAL) from homeassistant.helpers.entity import Entity from homeassistant.util import Throttle import homeassistant.helpers.config_validation as cv @@ -23,7 +23,6 @@ REQUIREMENTS = ['broadlink==0.9.0'] _LOGGER = logging.getLogger(__name__) -CONF_UPDATE_INTERVAL = 'update_interval' DEVICE_DEFAULT_NAME = 'Broadlink sensor' DEFAULT_TIMEOUT = 10 diff --git a/homeassistant/components/sensor/darksky.py b/homeassistant/components/sensor/darksky.py index 06232baca4e..28a51bd8ef2 100644 --- a/homeassistant/components/sensor/darksky.py +++ b/homeassistant/components/sensor/darksky.py @@ -14,7 +14,7 @@ import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.const import ( ATTR_ATTRIBUTION, CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE, - CONF_MONITORED_CONDITIONS, CONF_NAME, UNIT_UV_INDEX) + CONF_MONITORED_CONDITIONS, CONF_NAME, UNIT_UV_INDEX, CONF_UPDATE_INTERVAL) import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity from homeassistant.util import Throttle @@ -28,7 +28,6 @@ ATTRIBUTION = "Powered by Dark Sky" CONF_FORECAST = 'forecast' CONF_LANGUAGE = 'language' CONF_UNITS = 'units' -CONF_UPDATE_INTERVAL = 'update_interval' DEFAULT_LANGUAGE = 'en' diff --git a/homeassistant/components/sensor/fedex.py b/homeassistant/components/sensor/fedex.py index 7d5f47b3631..02938ff837b 100644 --- a/homeassistant/components/sensor/fedex.py +++ b/homeassistant/components/sensor/fedex.py @@ -12,7 +12,7 @@ import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.const import (CONF_NAME, CONF_USERNAME, CONF_PASSWORD, - ATTR_ATTRIBUTION) + ATTR_ATTRIBUTION, CONF_UPDATE_INTERVAL) from homeassistant.helpers.entity import Entity from homeassistant.util import slugify from homeassistant.util import Throttle @@ -23,7 +23,6 @@ REQUIREMENTS = ['fedexdeliverymanager==1.0.6'] _LOGGER = logging.getLogger(__name__) -CONF_UPDATE_INTERVAL = 'update_interval' COOKIE = 'fedexdeliverymanager_cookies.pickle' DOMAIN = 'fedex' diff --git a/homeassistant/components/sensor/magicseaweed.py b/homeassistant/components/sensor/magicseaweed.py index e14af6c3392..0500597b96a 100644 --- a/homeassistant/components/sensor/magicseaweed.py +++ b/homeassistant/components/sensor/magicseaweed.py @@ -23,7 +23,6 @@ _LOGGER = logging.getLogger(__name__) CONF_HOURS = 'hours' CONF_SPOT_ID = 'spot_id' CONF_UNITS = 'units' -CONF_UPDATE_INTERVAL = 'update_interval' DEFAULT_UNIT = 'us' DEFAULT_NAME = 'MSW' diff --git a/homeassistant/components/sensor/ups.py b/homeassistant/components/sensor/ups.py index aa6ce930619..44ecdc433c5 100644 --- a/homeassistant/components/sensor/ups.py +++ b/homeassistant/components/sensor/ups.py @@ -12,7 +12,7 @@ import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.const import (CONF_NAME, CONF_USERNAME, CONF_PASSWORD, - ATTR_ATTRIBUTION) + ATTR_ATTRIBUTION, CONF_UPDATE_INTERVAL) from homeassistant.helpers.entity import Entity from homeassistant.util import slugify from homeassistant.util import Throttle @@ -25,7 +25,6 @@ _LOGGER = logging.getLogger(__name__) DOMAIN = 'ups' COOKIE = 'upsmychoice_cookies.pickle' -CONF_UPDATE_INTERVAL = 'update_interval' ICON = 'mdi:package-variant-closed' STATUS_DELIVERED = 'delivered' diff --git a/homeassistant/components/tellduslive/__init__.py b/homeassistant/components/tellduslive/__init__.py index 472ba3d36f3..d9cd1be59da 100644 --- a/homeassistant/components/tellduslive/__init__.py +++ b/homeassistant/components/tellduslive/__init__.py @@ -13,12 +13,13 @@ import voluptuous as vol from homeassistant import config_entries import homeassistant.helpers.config_validation as cv +from homeassistant.const import CONF_UPDATE_INTERVAL from homeassistant.helpers.dispatcher import async_dispatcher_send from homeassistant.helpers.event import async_track_time_interval from . import config_flow # noqa pylint_disable=unused-import from .const import ( - CONF_HOST, CONF_UPDATE_INTERVAL, DOMAIN, KEY_HOST, KEY_SCAN_INTERVAL, + CONF_HOST, DOMAIN, KEY_HOST, KEY_SCAN_INTERVAL, KEY_SESSION, MIN_UPDATE_INTERVAL, NOT_SO_PRIVATE_KEY, PUBLIC_KEY, SCAN_INTERVAL, SIGNAL_UPDATE_ENTITY, TELLDUS_DISCOVERY_NEW) diff --git a/homeassistant/components/tellduslive/const.py b/homeassistant/components/tellduslive/const.py index 81b3abefdee..80b0513b763 100644 --- a/homeassistant/components/tellduslive/const.py +++ b/homeassistant/components/tellduslive/const.py @@ -18,7 +18,6 @@ KEY_SESSION = 'session' KEY_SCAN_INTERVAL = 'scan_interval' CONF_TOKEN_SECRET = 'token_secret' -CONF_UPDATE_INTERVAL = 'update_interval' PUBLIC_KEY = 'THUPUNECH5YEQA3RE6UYUPRUZ2DUGUGA' NOT_SO_PRIVATE_KEY = 'PHES7U2RADREWAFEBUSTUBAWRASWUTUS' diff --git a/homeassistant/components/volvooncall.py b/homeassistant/components/volvooncall.py index ce4dccbaf75..0d89537b8e8 100644 --- a/homeassistant/components/volvooncall.py +++ b/homeassistant/components/volvooncall.py @@ -10,7 +10,8 @@ import logging import voluptuous as vol from homeassistant.const import (CONF_USERNAME, CONF_PASSWORD, - CONF_NAME, CONF_RESOURCES) + CONF_NAME, CONF_RESOURCES, + CONF_UPDATE_INTERVAL) from homeassistant.helpers import discovery import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity @@ -32,7 +33,6 @@ _LOGGER = logging.getLogger(__name__) MIN_UPDATE_INTERVAL = timedelta(minutes=1) DEFAULT_UPDATE_INTERVAL = timedelta(minutes=1) -CONF_UPDATE_INTERVAL = 'update_interval' CONF_REGION = 'region' CONF_SERVICE_URL = 'service_url' CONF_SCANDINAVIAN_MILES = 'scandinavian_miles' diff --git a/homeassistant/const.py b/homeassistant/const.py index da9c32dee41..8701c682920 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -147,6 +147,7 @@ CONF_TTL = 'ttl' CONF_TYPE = 'type' CONF_UNIT_OF_MEASUREMENT = 'unit_of_measurement' CONF_UNIT_SYSTEM = 'unit_system' +CONF_UPDATE_INTERVAL = 'update_interval' CONF_URL = 'url' CONF_USERNAME = 'username' CONF_VALUE_TEMPLATE = 'value_template' From e22802a4d47b28e0a3c570698ff9fb932a59d50f Mon Sep 17 00:00:00 2001 From: Jc2k Date: Tue, 29 Jan 2019 04:30:56 +0000 Subject: [PATCH 155/222] Add support for HomeKit motion sensor devices (#20555) --- .../components/homekit_controller/__init__.py | 3 +- .../homekit_controller/binary_sensor.py | 55 +++++++++++++++++++ .../homekit_controller/test_binary_sensor.py | 29 ++++++++++ 3 files changed, 86 insertions(+), 1 deletion(-) create mode 100644 homeassistant/components/homekit_controller/binary_sensor.py create mode 100644 tests/components/homekit_controller/test_binary_sensor.py diff --git a/homeassistant/components/homekit_controller/__init__.py b/homeassistant/components/homekit_controller/__init__.py index b5577119178..72b7a502aa2 100644 --- a/homeassistant/components/homekit_controller/__init__.py +++ b/homeassistant/components/homekit_controller/__init__.py @@ -28,7 +28,8 @@ HOMEKIT_ACCESSORY_DISPATCH = { 'garage-door-opener': 'cover', 'window': 'cover', 'window-covering': 'cover', - 'lock-mechanism': 'lock' + 'lock-mechanism': 'lock', + 'motion': 'binary_sensor', } HOMEKIT_IGNORE = [ diff --git a/homeassistant/components/homekit_controller/binary_sensor.py b/homeassistant/components/homekit_controller/binary_sensor.py new file mode 100644 index 00000000000..8fe6248c65e --- /dev/null +++ b/homeassistant/components/homekit_controller/binary_sensor.py @@ -0,0 +1,55 @@ +""" +Support for Homekit motion sensors. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/binary_sensor.homekit_controller/ +""" +import logging + +from homeassistant.components.homekit_controller import (HomeKitEntity, + KNOWN_ACCESSORIES) +from homeassistant.components.binary_sensor import BinarySensorDevice + +DEPENDENCIES = ['homekit_controller'] + +_LOGGER = logging.getLogger(__name__) + + +def setup_platform(hass, config, add_entities, discovery_info=None): + """Set up Homekit motion sensor support.""" + if discovery_info is not None: + accessory = hass.data[KNOWN_ACCESSORIES][discovery_info['serial']] + add_entities([HomeKitMotionSensor(accessory, discovery_info)], True) + + +class HomeKitMotionSensor(HomeKitEntity, BinarySensorDevice): + """Representation of a Homekit sensor.""" + + def __init__(self, *args): + """Initialise the entity.""" + super().__init__(*args) + self._on = False + + def get_characteristic_types(self): + """Define the homekit characteristics the entity is tracking.""" + # pylint: disable=import-error + from homekit.model.characteristics import CharacteristicsTypes + + return [ + CharacteristicsTypes.MOTION_DETECTED, + ] + + def _update_motion_detected(self, value): + self._on = value + + @property + def device_class(self): + """Define this binary_sensor as a motion sensor.""" + return 'motion' + + @property + def is_on(self): + """Has motion been detected.""" + if not self.available: + return False + return self._on diff --git a/tests/components/homekit_controller/test_binary_sensor.py b/tests/components/homekit_controller/test_binary_sensor.py new file mode 100644 index 00000000000..bfcd51b55fb --- /dev/null +++ b/tests/components/homekit_controller/test_binary_sensor.py @@ -0,0 +1,29 @@ +"""Basic checks for HomeKitLock.""" +from tests.components.homekit_controller.common import ( + FakeService, setup_test_component) + +MOTION_DETECTED = ('motion', 'motion-detected') + + +def create_sensor_motion_service(): + """Define motion characteristics as per page 225 of HAP spec.""" + service = FakeService('public.hap.service.sensor.motion') + + cur_state = service.add_characteristic('motion-detected') + cur_state.value = 0 + + return service + + +async def test_sensor_read_state(hass, utcnow): + """Test that we can read the state of a HomeKit motion sensor accessory.""" + sensor = create_sensor_motion_service() + helper = await setup_test_component(hass, [sensor]) + + helper.characteristics[MOTION_DETECTED].value = False + state = await helper.poll_and_get_state() + assert state.state == 'off' + + helper.characteristics[MOTION_DETECTED].value = True + state = await helper.poll_and_get_state() + assert state.state == 'on' From b0ff51b0ef794946ff6ecf7892778b85893c88c2 Mon Sep 17 00:00:00 2001 From: Diogo Gomes Date: Tue, 29 Jan 2019 07:25:36 +0000 Subject: [PATCH 156/222] Add an Integration sensor (#19703) * initial version * Tested * set state added * lint * lint * remove artifacts * Use Decimal instead of float * simplify * travis lint fix * addres comments by @ottowinter * remove job * better sanity check * lower error -> warning * hound * fix state validation * refactor energy -> integration * address @MartinHjelmare comments * new style string format * remove async_set_state * patching the source function --- .../components/sensor/integration.py | 172 ++++++++++++++++++ tests/components/sensor/test_integration.py | 104 +++++++++++ 2 files changed, 276 insertions(+) create mode 100644 homeassistant/components/sensor/integration.py create mode 100644 tests/components/sensor/test_integration.py diff --git a/homeassistant/components/sensor/integration.py b/homeassistant/components/sensor/integration.py new file mode 100644 index 00000000000..9426730be35 --- /dev/null +++ b/homeassistant/components/sensor/integration.py @@ -0,0 +1,172 @@ +""" +Numeric integration of data coming from a source sensor over time. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/sensor.integration/ +""" +import logging + +from decimal import Decimal, DecimalException +import voluptuous as vol + +import homeassistant.helpers.config_validation as cv +from homeassistant.components.sensor import PLATFORM_SCHEMA +from homeassistant.const import ( + CONF_NAME, ATTR_UNIT_OF_MEASUREMENT, STATE_UNKNOWN, STATE_UNAVAILABLE) +from homeassistant.core import callback +from homeassistant.helpers.event import async_track_state_change +from homeassistant.helpers.restore_state import RestoreEntity + +_LOGGER = logging.getLogger(__name__) + +ATTR_SOURCE_ID = 'source' + +CONF_SOURCE_SENSOR = 'source' +CONF_ROUND_DIGITS = 'round' +CONF_UNIT_PREFIX = 'unit_prefix' +CONF_UNIT_TIME = 'unit_time' +CONF_UNIT_OF_MEASUREMENT = 'unit' + +# SI Metric prefixes +UNIT_PREFIXES = {None: 1, + "k": 10**3, + "G": 10**6, + "T": 10**9} + +# SI Time prefixes +UNIT_TIME = {'s': 1, + 'min': 60, + 'h': 60*60, + 'd': 24*60*60} + +ICON = 'mdi:char-histogram' + +DEFAULT_ROUND = 3 + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Optional(CONF_NAME): cv.string, + vol.Required(CONF_SOURCE_SENSOR): cv.entity_id, + vol.Optional(CONF_ROUND_DIGITS, default=DEFAULT_ROUND): vol.Coerce(int), + vol.Optional(CONF_UNIT_PREFIX, default=None): vol.In(UNIT_PREFIXES), + vol.Optional(CONF_UNIT_TIME, default='h'): vol.In(UNIT_TIME), + vol.Optional(CONF_UNIT_OF_MEASUREMENT): cv.string +}) + + +async def async_setup_platform(hass, config, async_add_entities, + discovery_info=None): + """Set up the integration sensor.""" + integral = IntegrationSensor(config[CONF_SOURCE_SENSOR], + config.get(CONF_NAME), + config[CONF_ROUND_DIGITS], + config[CONF_UNIT_PREFIX], + config[CONF_UNIT_TIME], + config.get(CONF_UNIT_OF_MEASUREMENT)) + + async_add_entities([integral]) + + +class IntegrationSensor(RestoreEntity): + """Representation of an integration sensor.""" + + def __init__(self, source_entity, name, round_digits, unit_prefix, + unit_time, unit_of_measurement): + """Initialize the integration sensor.""" + self._sensor_source_id = source_entity + self._round_digits = round_digits + self._state = 0 + + self._name = name if name is not None\ + else '{} integral'.format(source_entity) + + if unit_of_measurement is None: + self._unit_template = "{}{}{}".format( + "" if unit_prefix is None else unit_prefix, + "{}", + unit_time) + # we postpone the definition of unit_of_measurement to later + self._unit_of_measurement = None + else: + self._unit_of_measurement = unit_of_measurement + + self._unit_prefix = UNIT_PREFIXES[unit_prefix] + self._unit_time = UNIT_TIME[unit_time] + + async def async_added_to_hass(self): + """Handle entity which will be added.""" + await super().async_added_to_hass() + state = await self.async_get_last_state() + if state: + try: + self._state = Decimal(state.state) + except ValueError as err: + _LOGGER.warning("Could not restore last state: %s", err) + + @callback + def calc_integration(entity, old_state, new_state): + """Handle the sensor state changes.""" + if old_state is None or\ + old_state.state in [STATE_UNKNOWN, STATE_UNAVAILABLE] or\ + new_state.state in [STATE_UNKNOWN, STATE_UNAVAILABLE]: + return + + if self._unit_of_measurement is None: + unit = new_state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) + self._unit_of_measurement = self._unit_template.format( + "" if unit is None else unit) + + try: + # integration as the Riemann integral of previous measures. + elapsed_time = (new_state.last_updated + - old_state.last_updated).total_seconds() + area = (Decimal(new_state.state) + + Decimal(old_state.state))*Decimal(elapsed_time)/2 + integral = area / (self._unit_prefix * self._unit_time) + + assert isinstance(integral, Decimal) + except ValueError as err: + _LOGGER.warning("While calculating integration: %s", err) + except DecimalException as err: + _LOGGER.warning("Invalid state (%s > %s): %s", + old_state.state, new_state.state, err) + except AssertionError as err: + _LOGGER.error("Could not calculate integral: %s", err) + else: + self._state += integral + self.async_schedule_update_ha_state() + + async_track_state_change( + self.hass, self._sensor_source_id, calc_integration) + + @property + def name(self): + """Return the name of the sensor.""" + return self._name + + @property + def state(self): + """Return the state of the sensor.""" + return round(self._state, self._round_digits) + + @property + def unit_of_measurement(self): + """Return the unit the value is expressed in.""" + return self._unit_of_measurement + + @property + def should_poll(self): + """No polling needed.""" + return False + + @property + def device_state_attributes(self): + """Return the state attributes of the sensor.""" + state_attr = { + ATTR_SOURCE_ID: self._sensor_source_id, + } + return state_attr + + @property + def icon(self): + """Return the icon to use in the frontend.""" + return ICON diff --git a/tests/components/sensor/test_integration.py b/tests/components/sensor/test_integration.py new file mode 100644 index 00000000000..bb4a02c042b --- /dev/null +++ b/tests/components/sensor/test_integration.py @@ -0,0 +1,104 @@ +"""The tests for the integration sensor platform.""" +from datetime import timedelta +from unittest.mock import patch + +from homeassistant.setup import async_setup_component +import homeassistant.util.dt as dt_util + + +async def test_state(hass): + """Test integration sensor state.""" + config = { + 'sensor': { + 'platform': 'integration', + 'name': 'integration', + 'source': 'sensor.power', + 'unit': 'kWh', + 'round': 2, + } + } + + assert await async_setup_component(hass, 'sensor', config) + + entity_id = config['sensor']['source'] + hass.states.async_set(entity_id, 1, {}) + await hass.async_block_till_done() + + now = dt_util.utcnow() + timedelta(seconds=3600) + with patch('homeassistant.util.dt.utcnow', + return_value=now): + hass.states.async_set(entity_id, 1, {}, force_update=True) + await hass.async_block_till_done() + + state = hass.states.get('sensor.integration') + assert state is not None + + # Testing a power sensor at 1 KiloWatts for 1hour = 1kWh + assert round(float(state.state), config['sensor']['round']) == 1.0 + + assert state.attributes.get('unit_of_measurement') == 'kWh' + + +async def test_prefix(hass): + """Test integration sensor state using a power source.""" + config = { + 'sensor': { + 'platform': 'integration', + 'name': 'integration', + 'source': 'sensor.power', + 'round': 2, + 'unit_prefix': 'k' + } + } + + assert await async_setup_component(hass, 'sensor', config) + + entity_id = config['sensor']['source'] + hass.states.async_set(entity_id, 1000, {'unit_of_measurement': 'W'}) + await hass.async_block_till_done() + + now = dt_util.utcnow() + timedelta(seconds=3600) + with patch('homeassistant.util.dt.utcnow', + return_value=now): + hass.states.async_set(entity_id, 1000, {'unit_of_measurement': 'W'}, + force_update=True) + await hass.async_block_till_done() + + state = hass.states.get('sensor.integration') + assert state is not None + + # Testing a power sensor at 1000 Watts for 1hour = 1kWh + assert round(float(state.state), config['sensor']['round']) == 1.0 + assert state.attributes.get('unit_of_measurement') == 'kWh' + + +async def test_suffix(hass): + """Test integration sensor state using a network counter source.""" + config = { + 'sensor': { + 'platform': 'integration', + 'name': 'integration', + 'source': 'sensor.bytes_per_second', + 'round': 2, + 'unit_prefix': 'k', + 'unit_time': 's' + } + } + + assert await async_setup_component(hass, 'sensor', config) + + entity_id = config['sensor']['source'] + hass.states.async_set(entity_id, 1000, {}) + await hass.async_block_till_done() + + now = dt_util.utcnow() + timedelta(seconds=10) + with patch('homeassistant.util.dt.utcnow', + return_value=now): + hass.states.async_set(entity_id, 1000, {}, force_update=True) + await hass.async_block_till_done() + + state = hass.states.get('sensor.integration') + assert state is not None + + # Testing a network speed sensor at 1000 bytes/s over 10s = 10kbytes + assert round(float(state.state), config['sensor']['round']) == 10.0 From 73a0c664b8684b75edfbff673d217d70cb909585 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 28 Jan 2019 23:28:52 -0800 Subject: [PATCH 157/222] Allow usernames to be case-insensitive (#20558) * Allow usernames to be case-insensitive * Fix typing * FLAKE* --- homeassistant/auth/providers/homeassistant.py | 29 ++++++++++++++++--- tests/auth/providers/test_homeassistant.py | 22 +++++++++----- 2 files changed, 40 insertions(+), 11 deletions(-) diff --git a/homeassistant/auth/providers/homeassistant.py b/homeassistant/auth/providers/homeassistant.py index f5605886628..b22f93f11f1 100644 --- a/homeassistant/auth/providers/homeassistant.py +++ b/homeassistant/auth/providers/homeassistant.py @@ -2,7 +2,8 @@ import base64 from collections import OrderedDict import logging -from typing import Any, Dict, List, Optional, cast + +from typing import Any, Dict, List, Optional, Set, cast # noqa: F401 import bcrypt import voluptuous as vol @@ -52,6 +53,9 @@ class Data: self._store = hass.helpers.storage.Store(STORAGE_VERSION, STORAGE_KEY, private=True) self._data = None # type: Optional[Dict[str, Any]] + # Legacy mode will allow usernames to start/end with whitespace + # and will compare usernames case-insensitive. + # Remove in 2020 or when we launch 1.0. self.is_legacy = False @callback @@ -60,7 +64,7 @@ class Data: if self.is_legacy: return username - return username.strip() + return username.strip().casefold() async def async_load(self) -> None: """Load stored data.""" @@ -71,9 +75,26 @@ class Data: 'users': [] } + seen = set() # type: Set[str] + for user in data['users']: username = user['username'] + # check if we have duplicates + folded = username.casefold() + + if folded in seen: + self.is_legacy = True + + logging.getLogger(__name__).warning( + "Home Assistant auth provider is running in legacy mode " + "because we detected usernames that are case-insensitive" + "equivalent. Please change the username: '%s'.", username) + + break + + seen.add(folded) + # check if we have unstripped usernames if username != username.strip(): self.is_legacy = True @@ -81,7 +102,7 @@ class Data: logging.getLogger(__name__).warning( "Home Assistant auth provider is running in legacy mode " "because we detected usernames that start or end in a " - "space. Please change the username.") + "space. Please change the username: '%s'.", username) break @@ -103,7 +124,7 @@ class Data: # Compare all users to avoid timing attacks. for user in self.users: - if username == user['username']: + if self.normalize_username(user['username']) == username: found = user if found is None: diff --git a/tests/auth/providers/test_homeassistant.py b/tests/auth/providers/test_homeassistant.py index b654b42fb35..ffc4d67f21d 100644 --- a/tests/auth/providers/test_homeassistant.py +++ b/tests/auth/providers/test_homeassistant.py @@ -73,7 +73,6 @@ async def test_changing_password_raises_invalid_user(data, hass): async def test_adding_user(data, hass): """Test adding a user.""" data.add_auth('test-user', 'test-pass') - data.validate_login('test-user', 'test-pass') data.validate_login(' test-user ', 'test-pass') @@ -81,7 +80,7 @@ async def test_adding_user_duplicate_username(data, hass): """Test adding a user with duplicate username.""" data.add_auth('test-user', 'test-pass') with pytest.raises(hass_auth.InvalidUser): - data.add_auth('test-user ', 'other-pass') + data.add_auth('TEST-user ', 'other-pass') async def test_validating_password_invalid_password(data, hass): @@ -91,16 +90,22 @@ async def test_validating_password_invalid_password(data, hass): with pytest.raises(hass_auth.InvalidAuth): data.validate_login(' test-user ', 'invalid-pass') + with pytest.raises(hass_auth.InvalidAuth): + data.validate_login('test-user', 'test-pass ') + + with pytest.raises(hass_auth.InvalidAuth): + data.validate_login('test-user', 'Test-pass') + async def test_changing_password(data, hass): """Test adding a user.""" data.add_auth('test-user', 'test-pass') - data.change_password('test-user ', 'new-pass') + data.change_password('TEST-USER ', 'new-pass') with pytest.raises(hass_auth.InvalidAuth): data.validate_login('test-user', 'test-pass') - data.validate_login('test-user', 'new-pass') + data.validate_login('test-UsEr', 'new-pass') async def test_login_flow_validates(data, hass): @@ -122,18 +127,18 @@ async def test_login_flow_validates(data, hass): assert result['errors']['base'] == 'invalid_auth' result = await flow.async_step_init({ - 'username': 'test-user ', + 'username': 'TEST-user ', 'password': 'incorrect-pass', }) assert result['type'] == data_entry_flow.RESULT_TYPE_FORM assert result['errors']['base'] == 'invalid_auth' result = await flow.async_step_init({ - 'username': 'test-user', + 'username': 'test-USER', 'password': 'test-pass', }) assert result['type'] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY - assert result['data']['username'] == 'test-user' + assert result['data']['username'] == 'test-USER' async def test_saving_loading(data, hass): @@ -179,6 +184,9 @@ async def test_legacy_adding_user_duplicate_username(legacy_data, hass): legacy_data.add_auth('test-user', 'test-pass') with pytest.raises(hass_auth.InvalidUser): legacy_data.add_auth('test-user', 'other-pass') + # Not considered duplicate + legacy_data.add_auth('test-user ', 'test-pass') + legacy_data.add_auth('Test-user', 'test-pass') async def test_legacy_validating_password_invalid_password(legacy_data, hass): From 6859d5216eba326f247216dfa2066eedff30feb4 Mon Sep 17 00:00:00 2001 From: Julien Brochet Date: Tue, 29 Jan 2019 09:12:10 +0100 Subject: [PATCH 158/222] Add Synology SRM device tracker (#20320) --- .coveragerc | 1 + .../components/device_tracker/synology_srm.py | 100 ++++++++++++++++++ requirements_all.txt | 3 + 3 files changed, 104 insertions(+) create mode 100644 homeassistant/components/device_tracker/synology_srm.py diff --git a/.coveragerc b/.coveragerc index 32bcda136ca..c60b39ca7ed 100644 --- a/.coveragerc +++ b/.coveragerc @@ -566,6 +566,7 @@ omit = homeassistant/components/device_tracker/sky_hub.py homeassistant/components/device_tracker/snmp.py homeassistant/components/device_tracker/swisscom.py + homeassistant/components/device_tracker/synology_srm.py homeassistant/components/device_tracker/tado.py homeassistant/components/device_tracker/thomson.py homeassistant/components/device_tracker/tile.py diff --git a/homeassistant/components/device_tracker/synology_srm.py b/homeassistant/components/device_tracker/synology_srm.py new file mode 100644 index 00000000000..cc931b797d4 --- /dev/null +++ b/homeassistant/components/device_tracker/synology_srm.py @@ -0,0 +1,100 @@ +"""Device tracker for Synology SRM routers. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/device_tracker.synology_srm/ +""" +import logging +import voluptuous as vol + +import homeassistant.helpers.config_validation as cv +from homeassistant.components.device_tracker import ( + DOMAIN, PLATFORM_SCHEMA, DeviceScanner) +from homeassistant.const import ( + CONF_HOST, CONF_USERNAME, CONF_PASSWORD, + CONF_PORT, CONF_SSL, CONF_VERIFY_SSL) + +REQUIREMENTS = ['synology-srm==0.0.3'] + +_LOGGER = logging.getLogger(__name__) + +DEFAULT_USERNAME = 'admin' +DEFAULT_PORT = 8001 +DEFAULT_SSL = True +DEFAULT_VERIFY_SSL = False + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Required(CONF_HOST): cv.string, + vol.Required(CONF_USERNAME, default=DEFAULT_USERNAME): 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, + vol.Optional(CONF_VERIFY_SSL, default=DEFAULT_VERIFY_SSL): cv.boolean, +}) + + +def get_scanner(hass, config): + """Validate the configuration and return Synology SRM scanner.""" + scanner = SynologySrmDeviceScanner(config[DOMAIN]) + + return scanner if scanner.success_init else None + + +class SynologySrmDeviceScanner(DeviceScanner): + """This class scans for devices connected to a Synology SRM router.""" + + def __init__(self, config): + """Initialize the scanner.""" + import synology_srm + + self.client = synology_srm.Client( + host=config[CONF_HOST], + port=config[CONF_PORT], + username=config[CONF_USERNAME], + password=config[CONF_PASSWORD], + https=config[CONF_SSL] + ) + + if not config[CONF_VERIFY_SSL]: + self.client.http.disable_https_verify() + + self.last_results = [] + self.success_init = self._update_info() + + _LOGGER.info("Synology SRM scanner initialized") + + def scan_devices(self): + """Scan for new devices and return a list with found device IDs.""" + self._update_info() + + return [device['mac'] for device in self.last_results] + + def get_device_name(self, device): + """Return the name of the given device or None if we don't know.""" + filter_named = [result['hostname'] for result in self.last_results if + result['mac'] == device] + + if filter_named: + return filter_named[0] + + return None + + def _update_info(self): + """Check the router for connected devices.""" + _LOGGER.debug("Scanning for connected devices") + + devices = self.client.mesh.network_wifidevice() + last_results = [] + + for device in devices: + last_results.append({ + 'mac': device['mac'], + 'hostname': device['hostname'] + }) + + _LOGGER.debug( + "Found %d device(s) connected to the router", + len(devices) + ) + + self.last_results = last_results + return True diff --git a/requirements_all.txt b/requirements_all.txt index ca4459f66bc..dcb02d63895 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1597,6 +1597,9 @@ suds-py3==1.3.3.0 # homeassistant.components.sensor.swiss_hydrological_data swisshydrodata==0.0.3 +# homeassistant.components.device_tracker.synology_srm +synology-srm==0.0.3 + # homeassistant.components.tahoma tahoma-api==0.0.14 From a7c74151bc5044ee00df72e82a5c0e8e328e20a9 Mon Sep 17 00:00:00 2001 From: Richard Mitchell Date: Tue, 29 Jan 2019 08:15:42 +0000 Subject: [PATCH 159/222] Treat each player as a 'device' for non-client devices. (#20074) --- homeassistant/components/media_player/plex.py | 25 +++++++++++++------ 1 file changed, 17 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/media_player/plex.py b/homeassistant/components/media_player/plex.py index b70c1ffbf28..2110c42d371 100644 --- a/homeassistant/components/media_player/plex.py +++ b/homeassistant/components/media_player/plex.py @@ -174,11 +174,11 @@ def setup_plexserver( # add devices with a session and no client (ex. PlexConnect Apple TV's) if config.get(CONF_INCLUDE_NON_CLIENTS): - for machine_identifier, session in plex_sessions.items(): + for machine_identifier, (session, player) in plex_sessions.items(): if (machine_identifier not in plex_clients and machine_identifier is not None): new_client = PlexClient( - config, None, session, plex_sessions, update_devices, + config, player, session, plex_sessions, update_devices, update_sessions) plex_clients[machine_identifier] = new_client new_plex_clients.append(new_client) @@ -192,7 +192,9 @@ def setup_plexserver( client.force_idle() client.set_availability(client.machine_identifier - in available_client_ids) + in available_client_ids + or client.machine_identifier + in plex_sessions) if not config.get(CONF_REMOVE_UNAVAILABLE_CLIENTS) \ or client.available: @@ -225,7 +227,7 @@ def setup_plexserver( plex_sessions.clear() for session in sessions: for player in session.players: - plex_sessions[player.machineIdentifier] = session + plex_sessions[player.machineIdentifier] = session, player update_sessions() update_devices() @@ -363,6 +365,8 @@ class PlexClient(MediaPlayerDevice): def refresh(self, device, session): """Refresh key device data.""" + import plexapi.exceptions + # new data refresh self._clear_media_details() @@ -370,7 +374,11 @@ class PlexClient(MediaPlayerDevice): self._session = session if device: self._device = device - if "127.0.0.1" in self._device.url("/"): + try: + device_url = self._device.url("/") + except plexapi.exceptions.BadRequest: + device_url = '127.0.0.1' + if "127.0.0.1" in device_url: self._device.proxyThroughServer() self._session = None self._machine_identifier = self._device.machineIdentifier @@ -379,12 +387,13 @@ class PlexClient(MediaPlayerDevice): self._device.protocolCapabilities) # set valid session, preferring device session - if self.plex_sessions.get(self._device.machineIdentifier, None): + if self._device.machineIdentifier in self.plex_sessions: self._session = self.plex_sessions.get( - self._device.machineIdentifier, None) + self._device.machineIdentifier, [None, None])[0] if self._session: - if self._device.machineIdentifier is not None and \ + if self._device is not None and\ + self._device.machineIdentifier is not None and \ self._session.players: self._is_player_available = True self._player = [p for p in self._session.players From 6ff4ea112657ea28902aeeadbe19975af1aa41a1 Mon Sep 17 00:00:00 2001 From: Stealth Hacker <42306475+stealthhacker@users.noreply.github.com> Date: Tue, 29 Jan 2019 00:38:01 -0800 Subject: [PATCH 160/222] Add Recollect Waste (#20121) * Added Recollect Waste Curbside Collection sensor for tracking next collection date and for which types of waste. * Added missing schema attributes. * Adding requirements and coverage entries for Recollect Waste platform. * Added exception handling, some other fixes and suggestions from code review. * Fixed reference to incorrect exception type. * Updated requirements_all.txt with new version of recollect-waste. * Added true to add_entities. Created constant for default time interval. Used different pylint exclusion comment. * Using HA's CONF_SCAN_INTERVAL now. Unique_id is now set in @property. * Changed parameter of timedelta from seconds to days. * Added test run of recollect client during setup_platform. Using built in SCAN_INTERVAL now. * Return nothing in setup_platform if there is an exception. --- .coveragerc | 1 + .../components/sensor/recollect_waste.py | 105 ++++++++++++++++++ requirements_all.txt | 3 + 3 files changed, 109 insertions(+) create mode 100644 homeassistant/components/sensor/recollect_waste.py diff --git a/.coveragerc b/.coveragerc index c60b39ca7ed..27fa0085d0d 100644 --- a/.coveragerc +++ b/.coveragerc @@ -851,6 +851,7 @@ omit = homeassistant/components/sensor/qnap.py homeassistant/components/sensor/radarr.py homeassistant/components/sensor/rainbird.py + homeassistant/components/sensor/recollect_waste.py homeassistant/components/sensor/ripple.py homeassistant/components/sensor/rtorrent.py homeassistant/components/sensor/ruter.py diff --git a/homeassistant/components/sensor/recollect_waste.py b/homeassistant/components/sensor/recollect_waste.py new file mode 100644 index 00000000000..9122973c919 --- /dev/null +++ b/homeassistant/components/sensor/recollect_waste.py @@ -0,0 +1,105 @@ +""" +Support for Recollect Waste curbside collection pickup. + +For more details about this platform, please refer to the documentation at +https://www.home-assistant.io/components/sensor.recollect_waste/ +""" +import logging + +import voluptuous as vol + +import homeassistant.helpers.config_validation as cv +from homeassistant.components.sensor import PLATFORM_SCHEMA +from homeassistant.const import (CONF_NAME) +from homeassistant.helpers.entity import Entity + +REQUIREMENTS = ['recollect-waste==1.0.1'] + +_LOGGER = logging.getLogger(__name__) +ATTR_PICKUP_TYPES = 'pickup_types' +ATTR_AREA_NAME = 'area_name' +CONF_PLACE_ID = 'place_id' +CONF_SERVICE_ID = 'service_id' +DEFAULT_NAME = 'recollect_waste' +ICON = 'mdi:trash-can-outline' +SCAN_INTERVAL = 86400 + + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Required(CONF_PLACE_ID): cv.string, + vol.Required(CONF_SERVICE_ID): cv.string, + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string +}) + + +def setup_platform(hass, config, add_entities, discovery_info=None): + """Set up the Recollect Waste platform.""" + import recollect_waste + + # pylint: disable=no-member + client = recollect_waste.RecollectWasteClient(config[CONF_PLACE_ID], + config[CONF_SERVICE_ID]) + + # Ensure the client can connect to the API successfully + # with given place_id and service_id. + try: + client.get_next_pickup() + # pylint: disable=no-member + except recollect_waste.RecollectWasteException as ex: + _LOGGER.error('Recollect Waste platform error. %s', ex) + return + + add_entities([RecollectWasteSensor( + config.get(CONF_NAME), + client)], True) + + +class RecollectWasteSensor(Entity): + """Recollect Waste Sensor.""" + + def __init__(self, name, client): + """Initialize the sensor.""" + self._attributes = {} + self._name = name + self._state = None + self.client = client + + @property + def name(self): + """Return the name of the sensor.""" + return self._name + + @property + def unique_id(self) -> str: + """Return a unique ID.""" + return "{}{}".format(self.client.place_id, self.client.service_id) + + @property + def state(self): + """Return the state of the sensor.""" + return self._state + + @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 + + def update(self): + """Update device state.""" + import recollect_waste + + try: + pickup_event = self.client.get_next_pickup() + self._state = pickup_event.event_date + self._attributes.update({ + ATTR_PICKUP_TYPES: pickup_event.pickup_types, + ATTR_AREA_NAME: pickup_event.area_name + }) + # pylint: disable=no-member + except recollect_waste.RecollectWasteException as ex: + _LOGGER.error('Recollect Waste platform error. %s', ex) diff --git a/requirements_all.txt b/requirements_all.txt index dcb02d63895..6b9d6de32ab 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1443,6 +1443,9 @@ raincloudy==0.0.5 # homeassistant.components.switch.raspyrfm raspyrfm-client==1.2.8 +# homeassistant.components.sensor.recollect_waste +recollect-waste==1.0.1 + # homeassistant.components.rainmachine regenmaschine==1.1.0 From e95c50c7429d9111188f79e9f20013a93dc1bf31 Mon Sep 17 00:00:00 2001 From: MatteGary Date: Tue, 29 Jan 2019 10:27:26 +0100 Subject: [PATCH 161/222] New Transmission component (#19230) * New Transmission component and interaction First commit for New Transmission component and interaction * Fix commit * Fix commit * Fix + Switch checkin Fix according to failed build and request, first checkin for Turtle Mode Switch in Transmission, still have to figure it out why it's not working. * Bugfixing * Fix commit Multiple fix * Fix * fix for missing config * Update on requirements_all.txt * Fix in requirements_all.txt * Fix * Fix for build * fix * Fix * Fix (again) * Fix * Fix indentation * Fix indentation * Fix Throttle * Update .coveragerc * Fix import and coveragerc --- .coveragerc | 5 +- .../components/sensor/transmission.py | 110 +++------- .../components/switch/transmission.py | 53 ++--- homeassistant/components/transmission.py | 188 ++++++++++++++++++ requirements_all.txt | 3 +- 5 files changed, 238 insertions(+), 121 deletions(-) create mode 100644 homeassistant/components/transmission.py diff --git a/.coveragerc b/.coveragerc index 27fa0085d0d..5c99318b255 100644 --- a/.coveragerc +++ b/.coveragerc @@ -394,6 +394,9 @@ omit = homeassistant/components/tradfri.py homeassistant/components/*/tradfri.py + + homeassistant/components/transmission.py + homeassistant/components/*/transmission.py homeassistant/components/notify/twilio_sms.py homeassistant/components/notify/twilio_call.py @@ -891,7 +894,6 @@ omit = homeassistant/components/sensor/time_date.py homeassistant/components/sensor/torque.py homeassistant/components/sensor/trafikverket_weatherstation.py - homeassistant/components/sensor/transmission.py homeassistant/components/sensor/travisci.py homeassistant/components/sensor/twitch.py homeassistant/components/sensor/uber.py @@ -936,7 +938,6 @@ omit = homeassistant/components/switch/switchmate.py homeassistant/components/switch/telnet.py homeassistant/components/switch/tplink.py - homeassistant/components/switch/transmission.py homeassistant/components/switch/vesync.py homeassistant/components/telegram_bot/* homeassistant/components/thingspeak.py diff --git a/homeassistant/components/sensor/transmission.py b/homeassistant/components/sensor/transmission.py index a669db0e5be..efe32b07fc0 100644 --- a/homeassistant/components/sensor/transmission.py +++ b/homeassistant/components/sensor/transmission.py @@ -4,76 +4,38 @@ Support for monitoring the Transmission BitTorrent client API. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/sensor.transmission/ """ -from datetime import timedelta import logging -import voluptuous as vol - -from homeassistant.components.sensor import PLATFORM_SCHEMA -from homeassistant.const import ( - CONF_HOST, CONF_MONITORED_VARIABLES, CONF_NAME, CONF_PASSWORD, CONF_PORT, - CONF_USERNAME, STATE_IDLE) -import homeassistant.helpers.config_validation as cv +from homeassistant.components.transmission import ( + DATA_TRANSMISSION, SENSOR_TYPES, SCAN_INTERVAL) +from homeassistant.const import STATE_IDLE from homeassistant.helpers.entity import Entity from homeassistant.util import Throttle -from homeassistant.exceptions import PlatformNotReady -REQUIREMENTS = ['transmissionrpc==0.11'] +DEPENDENCIES = ['transmission'] _LOGGER = logging.getLogger(__name__) DEFAULT_NAME = 'Transmission' -DEFAULT_PORT = 9091 - -SENSOR_TYPES = { - 'active_torrents': ['Active Torrents', None], - 'current_status': ['Status', None], - 'download_speed': ['Down Speed', 'MB/s'], - 'paused_torrents': ['Paused Torrents', None], - 'total_torrents': ['Total Torrents', None], - 'upload_speed': ['Up Speed', 'MB/s'], -} - -SCAN_INTERVAL = timedelta(minutes=2) - -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Required(CONF_HOST): cv.string, - vol.Optional(CONF_MONITORED_VARIABLES, default=['torrents']): - vol.All(cv.ensure_list, [vol.In(SENSOR_TYPES)]), - vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, - vol.Optional(CONF_PASSWORD): cv.string, - vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, - vol.Optional(CONF_USERNAME): cv.string, -}) def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the Transmission sensors.""" - import transmissionrpc - from transmissionrpc.error import TransmissionError + if discovery_info is None: + return - name = config.get(CONF_NAME) - host = config.get(CONF_HOST) - username = config.get(CONF_USERNAME) - password = config.get(CONF_PASSWORD) - port = config.get(CONF_PORT) - - try: - transmission = transmissionrpc.Client( - host, port=port, user=username, password=password) - transmission_api = TransmissionData(transmission) - except TransmissionError as error: - if str(error).find("401: Unauthorized"): - _LOGGER.error("Credentials for Transmission client are not valid") - return - - _LOGGER.warning( - "Unable to connect to Transmission client: %s:%s", host, port) - raise PlatformNotReady + transmission_api = hass.data[DATA_TRANSMISSION] + monitored_variables = discovery_info['sensors'] + name = discovery_info['client_name'] dev = [] - for variable in config[CONF_MONITORED_VARIABLES]: - dev.append(TransmissionSensor(variable, transmission_api, name)) + for sensor_type in monitored_variables: + dev.append(TransmissionSensor( + sensor_type, + transmission_api, + name, + SENSOR_TYPES[sensor_type][0], + SENSOR_TYPES[sensor_type][1])) add_entities(dev, True) @@ -81,12 +43,18 @@ def setup_platform(hass, config, add_entities, discovery_info=None): class TransmissionSensor(Entity): """Representation of a Transmission sensor.""" - def __init__(self, sensor_type, transmission_api, client_name): + def __init__( + self, + sensor_type, + transmission_api, + client_name, + sensor_name, + unit_of_measurement): """Initialize the sensor.""" - self._name = SENSOR_TYPES[sensor_type][0] + self._name = sensor_name self._state = None self._transmission_api = transmission_api - self._unit_of_measurement = SENSOR_TYPES[sensor_type][1] + self._unit_of_measurement = unit_of_measurement self._data = None self.client_name = client_name self.type = sensor_type @@ -111,11 +79,17 @@ class TransmissionSensor(Entity): """Could the device be accessed during the last update call.""" return self._transmission_api.available + @Throttle(SCAN_INTERVAL) def update(self): """Get the latest data from Transmission and updates the state.""" self._transmission_api.update() self._data = self._transmission_api.data + if self.type == 'completed_torrents': + self._state = self._transmission_api.get_completed_torrent_count() + elif self.type == 'started_torrents': + self._state = self._transmission_api.get_started_torrent_count() + if self.type == 'current_status': if self._data: upload = self._data.uploadSpeed @@ -146,25 +120,3 @@ class TransmissionSensor(Entity): self._state = self._data.pausedTorrentCount elif self.type == 'total_torrents': self._state = self._data.torrentCount - - -class TransmissionData: - """Get the latest data and update the states.""" - - def __init__(self, api): - """Initialize the Transmission data object.""" - self.data = None - self.available = True - self._api = api - - @Throttle(SCAN_INTERVAL) - def update(self): - """Get the latest data from Transmission instance.""" - from transmissionrpc.error import TransmissionError - - try: - self.data = self._api.session_stats() - self.available = True - except TransmissionError: - self.available = False - _LOGGER.error("Unable to connect to Transmission client") diff --git a/homeassistant/components/switch/transmission.py b/homeassistant/components/switch/transmission.py index 10ab0903dcf..3ce3c7a98f9 100644 --- a/homeassistant/components/switch/transmission.py +++ b/homeassistant/components/switch/transmission.py @@ -6,54 +6,30 @@ https://home-assistant.io/components/switch.transmission/ """ import logging -import voluptuous as vol - -from homeassistant.components.switch import PLATFORM_SCHEMA +from homeassistant.components.transmission import ( + DATA_TRANSMISSION, SCAN_INTERVAL) from homeassistant.const import ( - CONF_HOST, CONF_NAME, CONF_PORT, CONF_PASSWORD, CONF_USERNAME, STATE_OFF, - STATE_ON) + STATE_OFF, STATE_ON) from homeassistant.helpers.entity import ToggleEntity -import homeassistant.helpers.config_validation as cv +from homeassistant.util import Throttle -REQUIREMENTS = ['transmissionrpc==0.11'] +DEPENDENCIES = ['transmission'] _LOGGING = logging.getLogger(__name__) DEFAULT_NAME = 'Transmission Turtle Mode' -DEFAULT_PORT = 9091 - -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Required(CONF_HOST): cv.string, - vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, - vol.Optional(CONF_PASSWORD): cv.string, - vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, - vol.Optional(CONF_USERNAME): cv.string, -}) def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the Transmission switch.""" - import transmissionrpc - from transmissionrpc.error import TransmissionError + if discovery_info is None: + return - name = config.get(CONF_NAME) - host = config.get(CONF_HOST) - username = config.get(CONF_USERNAME) - password = config.get(CONF_PASSWORD) - port = config.get(CONF_PORT) + component_name = DATA_TRANSMISSION + transmission_api = hass.data[component_name] + name = discovery_info['client_name'] - try: - transmission_api = transmissionrpc.Client( - host, port=port, user=username, password=password) - transmission_api.session_stats() - except TransmissionError as error: - _LOGGING.error( - "Connection to Transmission API failed on %s:%s with message %s", - host, port, error.original - ) - return False - - add_entities([TransmissionSwitch(transmission_api, name)]) + add_entities([TransmissionSwitch(transmission_api, name)], True) class TransmissionSwitch(ToggleEntity): @@ -88,14 +64,15 @@ class TransmissionSwitch(ToggleEntity): def turn_on(self, **kwargs): """Turn the device on.""" _LOGGING.debug("Turning Turtle Mode of Transmission on") - self.transmission_client.set_session(alt_speed_enabled=True) + self.transmission_client.set_alt_speed_enabled(True) def turn_off(self, **kwargs): """Turn the device off.""" _LOGGING.debug("Turning Turtle Mode of Transmission off") - self.transmission_client.set_session(alt_speed_enabled=False) + self.transmission_client.set_alt_speed_enabled(False) + @Throttle(SCAN_INTERVAL) def update(self): """Get the latest data from Transmission and updates the state.""" - active = self.transmission_client.get_session().alt_speed_enabled + active = self.transmission_client.get_alt_speed_enabled() self._state = STATE_ON if active else STATE_OFF diff --git a/homeassistant/components/transmission.py b/homeassistant/components/transmission.py new file mode 100644 index 00000000000..cdf55c8e049 --- /dev/null +++ b/homeassistant/components/transmission.py @@ -0,0 +1,188 @@ +""" +Component for monitoring the Transmission BitTorrent client API. + +For more details about this component, please refer to the documentation at +https://home-assistant.io/components/transmission/ +""" +from datetime import timedelta + +import logging +import voluptuous as vol + +from homeassistant.const import ( + CONF_HOST, + CONF_MONITORED_CONDITIONS, + CONF_NAME, + CONF_PASSWORD, + CONF_PORT, + CONF_USERNAME +) +from homeassistant.helpers import discovery, config_validation as cv +from homeassistant.helpers.event import track_time_interval + + +REQUIREMENTS = ['transmissionrpc==0.11'] +_LOGGER = logging.getLogger(__name__) + +DOMAIN = 'transmission' +DATA_TRANSMISSION = 'data_transmission' + +DEFAULT_NAME = 'Transmission' +DEFAULT_PORT = 9091 +TURTLE_MODE = 'turtle_mode' + +SENSOR_TYPES = { + 'active_torrents': ['Active Torrents', None], + 'current_status': ['Status', None], + 'download_speed': ['Down Speed', 'MB/s'], + 'paused_torrents': ['Paused Torrents', None], + 'total_torrents': ['Total Torrents', None], + 'upload_speed': ['Up Speed', 'MB/s'], + 'completed_torrents': ['Completed Torrents', None], + 'started_torrents': ['Started Torrents', None], +} + +CONFIG_SCHEMA = vol.Schema({ + DOMAIN: vol.Schema({ + vol.Required(CONF_HOST): cv.string, + vol.Optional(CONF_PASSWORD): cv.string, + vol.Optional(CONF_USERNAME): cv.string, + vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + vol.Optional(TURTLE_MODE, default=False): cv.boolean, + vol.Optional(CONF_MONITORED_CONDITIONS, default=['current_status']): + vol.All(cv.ensure_list, [vol.In(SENSOR_TYPES)]), + }) +}, extra=vol.ALLOW_EXTRA) + +SCAN_INTERVAL = timedelta(minutes=2) + + +def setup(hass, config): + """Set up the Transmission Component.""" + host = config[DOMAIN][CONF_HOST] + username = config[DOMAIN][CONF_USERNAME] + password = config[DOMAIN][CONF_PASSWORD] + port = config[DOMAIN][CONF_PORT] + + import transmissionrpc + from transmissionrpc.error import TransmissionError + try: + api = transmissionrpc.Client( + host, port=port, user=username, password=password) + api.session_stats() + except TransmissionError as error: + if str(error).find("401: Unauthorized"): + _LOGGER.error("Credentials for" + " Transmission client are not valid") + return False + + tm_data = hass.data[DATA_TRANSMISSION] = TransmissionData( + hass, config, api) + tm_data.init_torrent_list() + + def refresh(event_time): + """Get the latest data from Transmission.""" + tm_data.update() + + track_time_interval(hass, refresh, SCAN_INTERVAL) + + sensorconfig = { + 'sensors': config[DOMAIN][CONF_MONITORED_CONDITIONS], + 'client_name': config[DOMAIN][CONF_NAME]} + discovery.load_platform(hass, 'sensor', DOMAIN, sensorconfig, config) + + if config[DOMAIN][TURTLE_MODE]: + discovery.load_platform(hass, 'switch', DOMAIN, sensorconfig, config) + return True + + +class TransmissionData: + """Get the latest data and update the states.""" + + def __init__(self, hass, config, api): + """Initialize the Transmission RPC API.""" + self.data = None + self.torrents = None + self.session = None + self.available = True + self._api = api + self.completed_torrents = [] + self.started_torrents = [] + self.hass = hass + + def update(self): + """Get the latest data from Transmission instance.""" + from transmissionrpc.error import TransmissionError + + try: + self.data = self._api.session_stats() + self.torrents = self._api.get_torrents() + self.session = self._api.get_session() + + self.check_completed_torrent() + self.check_started_torrent() + + _LOGGER.debug("Torrent Data updated") + self.available = True + except TransmissionError: + self.available = False + _LOGGER.error("Unable to connect to Transmission client") + + def init_torrent_list(self): + """Initialize torrent lists.""" + self.torrents = self._api.get_torrents() + self.completed_torrents = [ + x.name for x in self.torrents if x.status == "seeding"] + self.started_torrents = [ + x.name for x in self.torrents if x.status == "downloading"] + + def check_completed_torrent(self): + """Get completed torrent functionality.""" + actual_torrents = self.torrents + actual_completed_torrents = [ + var.name for var in actual_torrents if var.status == "seeding"] + + tmp_completed_torrents = list( + set(actual_completed_torrents).difference( + self.completed_torrents)) + + for var in tmp_completed_torrents: + self.hass.bus.fire( + 'transmission_downloaded_torrent', { + 'name': var}) + + self.completed_torrents = actual_completed_torrents + + def check_started_torrent(self): + """Get started torrent functionality.""" + actual_torrents = self.torrents + actual_started_torrents = [ + var.name for var + in actual_torrents if var.status == "downloading"] + + tmp_started_torrents = list( + set(actual_started_torrents).difference( + self.started_torrents)) + + for var in tmp_started_torrents: + self.hass.bus.fire( + 'transmission_started_torrent', { + 'name': var}) + self.started_torrents = actual_started_torrents + + def get_started_torrent_count(self): + """Get the number of started torrents.""" + return len(self.started_torrents) + + def get_completed_torrent_count(self): + """Get the number of completed torrents.""" + return len(self.completed_torrents) + + def set_alt_speed_enabled(self, is_enabled): + """Set the alternative speed flag.""" + self._api.set_session(alt_speed_enabled=is_enabled) + + def get_alt_speed_enabled(self): + """Get the alternative speed flag.""" + return self.session.alt_speed_enabled diff --git a/requirements_all.txt b/requirements_all.txt index 6b9d6de32ab..87b7da0bc7c 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1654,8 +1654,7 @@ tp-connected==0.0.4 # homeassistant.components.device_tracker.tplink tplink==0.2.1 -# homeassistant.components.sensor.transmission -# homeassistant.components.switch.transmission +# homeassistant.components.transmission transmissionrpc==0.11 # homeassistant.components.tuya From 988bcf93999dab17f7db20761c662bf1f11bdffb Mon Sep 17 00:00:00 2001 From: kennedyshead Date: Tue, 29 Jan 2019 16:42:38 +0100 Subject: [PATCH 162/222] Fixing the openssl issue (#20570) --- homeassistant/components/asuswrt.py | 2 +- homeassistant/components/sensor/asuswrt.py | 2 +- requirements_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/asuswrt.py b/homeassistant/components/asuswrt.py index 898485b5cb3..0069b3c0d73 100644 --- a/homeassistant/components/asuswrt.py +++ b/homeassistant/components/asuswrt.py @@ -14,7 +14,7 @@ from homeassistant.const import ( from homeassistant.helpers import config_validation as cv from homeassistant.helpers.discovery import async_load_platform -REQUIREMENTS = ['aioasuswrt==1.1.18'] +REQUIREMENTS = ['aioasuswrt==1.1.20'] _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/sensor/asuswrt.py b/homeassistant/components/sensor/asuswrt.py index 6af59ec1809..08e2ec27cb2 100644 --- a/homeassistant/components/sensor/asuswrt.py +++ b/homeassistant/components/sensor/asuswrt.py @@ -60,7 +60,7 @@ class AsuswrtSensor(Entity): async def async_update(self): """Fetch status from asuswrt.""" - self._rates = await self._api.async_get_packets_total() + self._rates = await self._api.async_get_bytes_total() self._speed = await self._api.async_get_current_transfer_rates() diff --git a/requirements_all.txt b/requirements_all.txt index 87b7da0bc7c..d94a746c50e 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -90,7 +90,7 @@ afsapi==0.0.4 aioambient==0.1.0 # homeassistant.components.asuswrt -aioasuswrt==1.1.18 +aioasuswrt==1.1.20 # homeassistant.components.device_tracker.automatic aioautomatic==0.6.5 From e0e5b860e40968159a0ff35504d900690d91fb11 Mon Sep 17 00:00:00 2001 From: Jc2k Date: Tue, 29 Jan 2019 16:25:18 +0000 Subject: [PATCH 163/222] Homekit Motion Sensor Review feedback (#20568) --- homeassistant/components/homekit_controller/binary_sensor.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/homeassistant/components/homekit_controller/binary_sensor.py b/homeassistant/components/homekit_controller/binary_sensor.py index 8fe6248c65e..531297dc911 100644 --- a/homeassistant/components/homekit_controller/binary_sensor.py +++ b/homeassistant/components/homekit_controller/binary_sensor.py @@ -50,6 +50,4 @@ class HomeKitMotionSensor(HomeKitEntity, BinarySensorDevice): @property def is_on(self): """Has motion been detected.""" - if not self.available: - return False return self._on From 48f0e8311bd17799244392a69686bda716dcc59c Mon Sep 17 00:00:00 2001 From: merc1031 Date: Tue, 29 Jan 2019 09:26:07 -0800 Subject: [PATCH 164/222] add empy all groups view. Makes Brilliant Lightpad work (#20564) --- .../components/emulated_hue/__init__.py | 3 ++- .../components/emulated_hue/hue_api.py | 22 +++++++++++++++++++ tests/components/emulated_hue/test_hue_api.py | 17 +++++++++++++- 3 files changed, 40 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/emulated_hue/__init__.py b/homeassistant/components/emulated_hue/__init__.py index 9c0df0f9f03..07ecb9d265a 100644 --- a/homeassistant/components/emulated_hue/__init__.py +++ b/homeassistant/components/emulated_hue/__init__.py @@ -22,7 +22,7 @@ from homeassistant.components.http import real_ip from .hue_api import ( HueUsernameView, HueAllLightsStateView, HueOneLightStateView, - HueOneLightChangeView, HueGroupView) + HueOneLightChangeView, HueGroupView, HueAllGroupsStateView) from .upnp import DescriptionXmlView, UPNPResponderThread DOMAIN = 'emulated_hue' @@ -105,6 +105,7 @@ async def async_setup(hass, yaml_config): HueAllLightsStateView(config).register(app, app.router) HueOneLightStateView(config).register(app, app.router) HueOneLightChangeView(config).register(app, app.router) + HueAllGroupsStateView(config).register(app, app.router) HueGroupView(config).register(app, app.router) upnp_listener = UPNPResponderThread( diff --git a/homeassistant/components/emulated_hue/hue_api.py b/homeassistant/components/emulated_hue/hue_api.py index 3699a45ef30..815e28b4fa4 100644 --- a/homeassistant/components/emulated_hue/hue_api.py +++ b/homeassistant/components/emulated_hue/hue_api.py @@ -56,6 +56,28 @@ class HueUsernameView(HomeAssistantView): return self.json([{'success': {'username': '12345678901234567890'}}]) +class HueAllGroupsStateView(HomeAssistantView): + """Group handler.""" + + url = '/api/{username}/groups' + name = 'emulated_hue:all_groups:state' + requires_auth = False + + def __init__(self, config): + """Initialize the instance of the view.""" + self.config = config + + @core.callback + def get(self, request, username): + """Process a request to make the Brilliant Lightpad work.""" + if not is_local(request[KEY_REAL_IP]): + return self.json_message('only local IPs allowed', + HTTP_BAD_REQUEST) + + return self.json({ + }) + + class HueGroupView(HomeAssistantView): """Group handler to get Logitech Pop working.""" diff --git a/tests/components/emulated_hue/test_hue_api.py b/tests/components/emulated_hue/test_hue_api.py index 8582f5b38cf..70fe894debf 100644 --- a/tests/components/emulated_hue/test_hue_api.py +++ b/tests/components/emulated_hue/test_hue_api.py @@ -15,7 +15,7 @@ from homeassistant.components import ( from homeassistant.components.emulated_hue import Config from homeassistant.components.emulated_hue.hue_api import ( HUE_API_STATE_ON, HUE_API_STATE_BRI, HueUsernameView, HueOneLightStateView, - HueAllLightsStateView, HueOneLightChangeView) + HueAllLightsStateView, HueOneLightChangeView, HueAllGroupsStateView) from homeassistant.const import STATE_ON, STATE_OFF HTTP_SERVER_PORT = get_test_instance_port() @@ -135,6 +135,7 @@ def hue_client(loop, hass_hue, aiohttp_client): HueAllLightsStateView(config).register(web_app, web_app.router) HueOneLightStateView(config).register(web_app, web_app.router) HueOneLightChangeView(config).register(web_app, web_app.router) + HueAllGroupsStateView(config).register(web_app, web_app.router) return loop.run_until_complete(aiohttp_client(web_app)) @@ -420,6 +421,20 @@ def test_proper_put_state_request(hue_client): assert result.status == 400 +@asyncio.coroutine +def test_get_empty_groups_state(hue_client): + """Test the request to get groups endpoint.""" + # Test proper on value parsing + result = yield from hue_client.get( + '/api/username/groups') + + assert result.status == 200 + + result_json = yield from result.json() + + assert result_json == {} + + # pylint: disable=invalid-name async def perform_put_test_on_ceiling_lights(hass_hue, hue_client, content_type='application/json'): From 89fc3b2a1b1afe69db4f0e4ef6cb43fbc4db511c Mon Sep 17 00:00:00 2001 From: emontnemery Date: Tue, 29 Jan 2019 18:29:02 +0100 Subject: [PATCH 165/222] Disable extra=vol.ALLOW_EXTRA for MQTT platforms. (#20562) --- homeassistant/components/mqtt/__init__.py | 3 ++- .../components/mqtt/alarm_control_panel.py | 3 ++- .../components/mqtt/binary_sensor.py | 2 +- homeassistant/components/mqtt/cover.py | 2 +- homeassistant/components/mqtt/discovery.py | 4 ++-- homeassistant/components/mqtt/fan.py | 2 +- .../components/mqtt/light/__init__.py | 19 +++++++++++++------ .../components/mqtt/light/schema_basic.py | 4 +++- .../components/mqtt/light/schema_json.py | 3 ++- .../components/mqtt/light/schema_template.py | 4 +++- homeassistant/components/mqtt/lock.py | 2 +- homeassistant/components/mqtt/sensor.py | 2 +- homeassistant/components/mqtt/switch.py | 2 +- homeassistant/components/mqtt/vacuum.py | 2 +- .../mqtt/test_alarm_control_panel.py | 8 ++++---- tests/components/mqtt/test_binary_sensor.py | 5 ++--- tests/components/mqtt/test_light.py | 12 ++++++------ tests/components/mqtt/test_light_json.py | 10 +++++----- tests/components/mqtt/test_light_template.py | 11 +++++------ tests/components/mqtt/test_lock.py | 4 ++-- tests/components/mqtt/test_sensor.py | 11 +++++------ tests/components/mqtt/test_switch.py | 8 ++++---- 22 files changed, 67 insertions(+), 56 deletions(-) diff --git a/homeassistant/components/mqtt/__init__.py b/homeassistant/components/mqtt/__init__.py index 8852ae76e08..ed2a3cd6c52 100644 --- a/homeassistant/components/mqtt/__init__.py +++ b/homeassistant/components/mqtt/__init__.py @@ -234,7 +234,7 @@ MQTT_JSON_ATTRS_SCHEMA = vol.Schema({ vol.Optional(CONF_JSON_ATTRS_TOPIC): valid_subscribe_topic, }) -MQTT_BASE_PLATFORM_SCHEMA = cv.PLATFORM_SCHEMA.extend(SCHEMA_BASE) +MQTT_BASE_PLATFORM_SCHEMA = cv.PLATFORM_SCHEMA_2.extend(SCHEMA_BASE) # Sensor type platforms subscribe to MQTT events MQTT_RO_PLATFORM_SCHEMA = MQTT_BASE_PLATFORM_SCHEMA.extend({ @@ -985,6 +985,7 @@ class MqttDiscoveryUpdate(Entity): elif self._discovery_update: # Non-empty payload: Notify component _LOGGER.info("Updating component: %s", self.entity_id) + payload.pop(ATTR_DISCOVERY_HASH) self.hass.async_create_task(self._discovery_update(payload)) if self._discovery_hash: diff --git a/homeassistant/components/mqtt/alarm_control_panel.py b/homeassistant/components/mqtt/alarm_control_panel.py index 6bb864f2cdb..b3e4d452b5c 100644 --- a/homeassistant/components/mqtt/alarm_control_panel.py +++ b/homeassistant/components/mqtt/alarm_control_panel.py @@ -40,6 +40,7 @@ DEPENDENCIES = ['mqtt'] PLATFORM_SCHEMA = mqtt.MQTT_BASE_PLATFORM_SCHEMA.extend({ vol.Required(CONF_COMMAND_TOPIC): mqtt.valid_publish_topic, + vol.Optional(CONF_RETAIN, default=mqtt.DEFAULT_RETAIN): cv.boolean, vol.Required(CONF_STATE_TOPIC): mqtt.valid_subscribe_topic, vol.Optional(CONF_CODE): cv.string, vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, @@ -63,7 +64,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): async def async_discover(discovery_payload): """Discover and add an MQTT alarm control panel.""" try: - discovery_hash = discovery_payload[ATTR_DISCOVERY_HASH] + discovery_hash = discovery_payload.pop(ATTR_DISCOVERY_HASH) config = PLATFORM_SCHEMA(discovery_payload) await _async_setup_entity(config, async_add_entities, config_entry, discovery_hash) diff --git a/homeassistant/components/mqtt/binary_sensor.py b/homeassistant/components/mqtt/binary_sensor.py index 297d4436c7c..cb93712776c 100644 --- a/homeassistant/components/mqtt/binary_sensor.py +++ b/homeassistant/components/mqtt/binary_sensor.py @@ -63,7 +63,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): async def async_discover(discovery_payload): """Discover and add a MQTT binary sensor.""" try: - discovery_hash = discovery_payload[ATTR_DISCOVERY_HASH] + discovery_hash = discovery_payload.pop(ATTR_DISCOVERY_HASH) config = PLATFORM_SCHEMA(discovery_payload) await _async_setup_entity(config, async_add_entities, config_entry, discovery_hash) diff --git a/homeassistant/components/mqtt/cover.py b/homeassistant/components/mqtt/cover.py index 26d10e85a34..75fae0e9c15 100644 --- a/homeassistant/components/mqtt/cover.py +++ b/homeassistant/components/mqtt/cover.py @@ -135,7 +135,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): async def async_discover(discovery_payload): """Discover and add an MQTT cover.""" try: - discovery_hash = discovery_payload[ATTR_DISCOVERY_HASH] + discovery_hash = discovery_payload.pop(ATTR_DISCOVERY_HASH) config = PLATFORM_SCHEMA(discovery_payload) await _async_setup_entity(config, async_add_entities, config_entry, discovery_hash) diff --git a/homeassistant/components/mqtt/discovery.py b/homeassistant/components/mqtt/discovery.py index 693a1a4d41d..9a2daf388cb 100644 --- a/homeassistant/components/mqtt/discovery.py +++ b/homeassistant/components/mqtt/discovery.py @@ -216,8 +216,8 @@ async def async_start(hass: HomeAssistantType, discovery_topic, hass_config, key = ABBREVIATIONS.get(key, key) payload[key] = payload.pop(abbreviated_key) - if TOPIC_BASE in payload: - base = payload[TOPIC_BASE] + base = payload.pop(TOPIC_BASE, None) + if base: for key, value in payload.items(): if isinstance(value, str) and value: if value[0] == TOPIC_BASE and key.endswith('_topic'): diff --git a/homeassistant/components/mqtt/fan.py b/homeassistant/components/mqtt/fan.py index 1d1258993de..d15b236038e 100644 --- a/homeassistant/components/mqtt/fan.py +++ b/homeassistant/components/mqtt/fan.py @@ -93,7 +93,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): async def async_discover(discovery_payload): """Discover and add a MQTT fan.""" try: - discovery_hash = discovery_payload[ATTR_DISCOVERY_HASH] + discovery_hash = discovery_payload.pop(ATTR_DISCOVERY_HASH) config = PLATFORM_SCHEMA(discovery_payload) await _async_setup_entity(config, async_add_entities, config_entry, discovery_hash) diff --git a/homeassistant/components/mqtt/light/__init__.py b/homeassistant/components/mqtt/light/__init__.py index 908ad1ac989..4ff6efb8643 100644 --- a/homeassistant/components/mqtt/light/__init__.py +++ b/homeassistant/components/mqtt/light/__init__.py @@ -15,10 +15,6 @@ from homeassistant.components.mqtt.discovery import ( from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.typing import HomeAssistantType, ConfigType -from . import schema_basic -from . import schema_json -from . import schema_template - _LOGGER = logging.getLogger(__name__) DEPENDENCIES = ['mqtt'] @@ -28,6 +24,10 @@ CONF_SCHEMA = 'schema' def validate_mqtt_light(value): """Validate MQTT light schema.""" + from . import schema_basic + from . import schema_json + from . import schema_template + schemas = { 'basic': schema_basic.PLATFORM_SCHEMA_BASIC, 'json': schema_json.PLATFORM_SCHEMA_JSON, @@ -36,9 +36,12 @@ def validate_mqtt_light(value): return schemas[value[CONF_SCHEMA]](value) -PLATFORM_SCHEMA = vol.All(vol.Schema({ +MQTT_LIGHT_SCHEMA_SCHEMA = vol.Schema({ vol.Optional(CONF_SCHEMA, default='basic'): vol.All( vol.Lower, vol.Any('basic', 'json', 'template')) +}) + +PLATFORM_SCHEMA = vol.All(MQTT_LIGHT_SCHEMA_SCHEMA.extend({ }, extra=vol.ALLOW_EXTRA), validate_mqtt_light) @@ -53,7 +56,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): async def async_discover(discovery_payload): """Discover and add a MQTT light.""" try: - discovery_hash = discovery_payload[ATTR_DISCOVERY_HASH] + discovery_hash = discovery_payload.pop(ATTR_DISCOVERY_HASH) config = PLATFORM_SCHEMA(discovery_payload) await _async_setup_entity(config, async_add_entities, config_entry, discovery_hash) @@ -70,6 +73,10 @@ async def async_setup_entry(hass, config_entry, async_add_entities): async def _async_setup_entity(config, async_add_entities, config_entry=None, discovery_hash=None): """Set up a MQTT Light.""" + from . import schema_basic + from . import schema_json + from . import schema_template + setup_entity = { 'basic': schema_basic.async_setup_entity_basic, 'json': schema_json.async_setup_entity_json, diff --git a/homeassistant/components/mqtt/light/schema_basic.py b/homeassistant/components/mqtt/light/schema_basic.py index 0ba1db890d7..4aee026a2f6 100644 --- a/homeassistant/components/mqtt/light/schema_basic.py +++ b/homeassistant/components/mqtt/light/schema_basic.py @@ -26,6 +26,8 @@ from homeassistant.helpers.restore_state import RestoreEntity import homeassistant.helpers.config_validation as cv import homeassistant.util.color as color_util +from . import MQTT_LIGHT_SCHEMA_SCHEMA + _LOGGER = logging.getLogger(__name__) DEPENDENCIES = ['mqtt'] @@ -108,7 +110,7 @@ PLATFORM_SCHEMA_BASIC = mqtt.MQTT_RW_PLATFORM_SCHEMA.extend({ vol.In(VALUES_ON_COMMAND_TYPE), vol.Optional(CONF_DEVICE): mqtt.MQTT_ENTITY_DEVICE_INFO_SCHEMA, }).extend(mqtt.MQTT_AVAILABILITY_SCHEMA.schema).extend( - mqtt.MQTT_JSON_ATTRS_SCHEMA.schema) + mqtt.MQTT_JSON_ATTRS_SCHEMA.schema).extend(MQTT_LIGHT_SCHEMA_SCHEMA.schema) async def async_setup_entity_basic(config, async_add_entities, config_entry, diff --git a/homeassistant/components/mqtt/light/schema_json.py b/homeassistant/components/mqtt/light/schema_json.py index 4b1a1cff3ff..4a97eeea520 100644 --- a/homeassistant/components/mqtt/light/schema_json.py +++ b/homeassistant/components/mqtt/light/schema_json.py @@ -28,6 +28,7 @@ from homeassistant.helpers.restore_state import RestoreEntity from homeassistant.helpers.typing import ConfigType import homeassistant.util.color as color_util +from . import MQTT_LIGHT_SCHEMA_SCHEMA from .schema_basic import CONF_BRIGHTNESS_SCALE _LOGGER = logging.getLogger(__name__) @@ -81,7 +82,7 @@ PLATFORM_SCHEMA_JSON = mqtt.MQTT_RW_PLATFORM_SCHEMA.extend({ vol.Required(CONF_COMMAND_TOPIC): mqtt.valid_publish_topic, vol.Optional(CONF_DEVICE): mqtt.MQTT_ENTITY_DEVICE_INFO_SCHEMA, }).extend(mqtt.MQTT_AVAILABILITY_SCHEMA.schema).extend( - mqtt.MQTT_JSON_ATTRS_SCHEMA.schema) + mqtt.MQTT_JSON_ATTRS_SCHEMA.schema).extend(MQTT_LIGHT_SCHEMA_SCHEMA.schema) async def async_setup_entity_json(config: ConfigType, async_add_entities, diff --git a/homeassistant/components/mqtt/light/schema_template.py b/homeassistant/components/mqtt/light/schema_template.py index 9b8e57c1d2a..4d086fd73e1 100644 --- a/homeassistant/components/mqtt/light/schema_template.py +++ b/homeassistant/components/mqtt/light/schema_template.py @@ -24,6 +24,8 @@ import homeassistant.helpers.config_validation as cv import homeassistant.util.color as color_util from homeassistant.helpers.restore_state import RestoreEntity +from . import MQTT_LIGHT_SCHEMA_SCHEMA + _LOGGER = logging.getLogger(__name__) DOMAIN = 'mqtt_template' @@ -67,7 +69,7 @@ PLATFORM_SCHEMA_TEMPLATE = mqtt.MQTT_RW_PLATFORM_SCHEMA.extend({ vol.Optional(CONF_UNIQUE_ID): cv.string, vol.Optional(CONF_DEVICE): mqtt.MQTT_ENTITY_DEVICE_INFO_SCHEMA, }).extend(mqtt.MQTT_AVAILABILITY_SCHEMA.schema).extend( - mqtt.MQTT_JSON_ATTRS_SCHEMA.schema) + mqtt.MQTT_JSON_ATTRS_SCHEMA.schema).extend(MQTT_LIGHT_SCHEMA_SCHEMA.schema) async def async_setup_entity_template(config, async_add_entities, config_entry, diff --git a/homeassistant/components/mqtt/lock.py b/homeassistant/components/mqtt/lock.py index da872fda612..82462b8171f 100644 --- a/homeassistant/components/mqtt/lock.py +++ b/homeassistant/components/mqtt/lock.py @@ -58,7 +58,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): async def async_discover(discovery_payload): """Discover and add an MQTT lock.""" try: - discovery_hash = discovery_payload[ATTR_DISCOVERY_HASH] + discovery_hash = discovery_payload.pop(ATTR_DISCOVERY_HASH) config = PLATFORM_SCHEMA(discovery_payload) await _async_setup_entity(config, async_add_entities, config_entry, discovery_hash) diff --git a/homeassistant/components/mqtt/sensor.py b/homeassistant/components/mqtt/sensor.py index 9637caa9053..02a4de9cad4 100644 --- a/homeassistant/components/mqtt/sensor.py +++ b/homeassistant/components/mqtt/sensor.py @@ -66,7 +66,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): async def async_discover_sensor(discovery_payload): """Discover and add a discovered MQTT sensor.""" try: - discovery_hash = discovery_payload[ATTR_DISCOVERY_HASH] + discovery_hash = discovery_payload.pop(ATTR_DISCOVERY_HASH) config = PLATFORM_SCHEMA(discovery_payload) await _async_setup_entity(config, async_add_entities, config_entry, discovery_hash) diff --git a/homeassistant/components/mqtt/switch.py b/homeassistant/components/mqtt/switch.py index 56fc1a51206..c9f8c880573 100644 --- a/homeassistant/components/mqtt/switch.py +++ b/homeassistant/components/mqtt/switch.py @@ -62,7 +62,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): async def async_discover(discovery_payload): """Discover and add a MQTT switch.""" try: - discovery_hash = discovery_payload[ATTR_DISCOVERY_HASH] + discovery_hash = discovery_payload.pop(ATTR_DISCOVERY_HASH) config = PLATFORM_SCHEMA(discovery_payload) await _async_setup_entity(config, async_add_entities, config_entry, discovery_hash) diff --git a/homeassistant/components/mqtt/vacuum.py b/homeassistant/components/mqtt/vacuum.py index 90cca62da38..3d53f32c6f6 100644 --- a/homeassistant/components/mqtt/vacuum.py +++ b/homeassistant/components/mqtt/vacuum.py @@ -162,7 +162,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): async def async_discover(discovery_payload): """Discover and add a MQTT vacuum.""" try: - discovery_hash = discovery_payload[ATTR_DISCOVERY_HASH] + discovery_hash = discovery_payload.pop(ATTR_DISCOVERY_HASH) config = PLATFORM_SCHEMA(discovery_payload) await _async_setup_entity(config, async_add_entities, config_entry, discovery_hash) diff --git a/tests/components/mqtt/test_alarm_control_panel.py b/tests/components/mqtt/test_alarm_control_panel.py index 0bcb29ec9c6..572cbdb0e10 100644 --- a/tests/components/mqtt/test_alarm_control_panel.py +++ b/tests/components/mqtt/test_alarm_control_panel.py @@ -379,7 +379,7 @@ async def test_discovery_removal_alarm(hass, mqtt_mock, caplog): data = ( '{ "name": "Beer",' - ' "status_topic": "test_topic",' + ' "state_topic": "test_topic",' ' "command_topic": "test_topic" }' ) @@ -409,12 +409,12 @@ async def test_discovery_update_alarm(hass, mqtt_mock, caplog): data1 = ( '{ "name": "Beer",' - ' "status_topic": "test_topic",' + ' "state_topic": "test_topic",' ' "command_topic": "test_topic" }' ) data2 = ( '{ "name": "Milk",' - ' "status_topic": "test_topic",' + ' "state_topic": "test_topic",' ' "command_topic": "test_topic" }' ) @@ -451,7 +451,7 @@ async def test_discovery_broken(hass, mqtt_mock, caplog): ) data2 = ( '{ "name": "Milk",' - ' "status_topic": "test_topic",' + ' "state_topic": "test_topic",' ' "command_topic": "test_topic" }' ) diff --git a/tests/components/mqtt/test_binary_sensor.py b/tests/components/mqtt/test_binary_sensor.py index 2f8a87b8a9b..3e6e36cd050 100644 --- a/tests/components/mqtt/test_binary_sensor.py +++ b/tests/components/mqtt/test_binary_sensor.py @@ -344,12 +344,12 @@ async def test_discovery_update_attr(hass, mqtt_mock, caplog): await async_start(hass, 'homeassistant', {}, entry) data1 = ( '{ "name": "Beer",' - ' "command_topic": "test_topic",' + ' "state_topic": "test_topic",' ' "json_attributes_topic": "attr-topic1" }' ) data2 = ( '{ "name": "Beer",' - ' "command_topic": "test_topic",' + ' "state_topic": "test_topic",' ' "json_attributes_topic": "attr-topic2" }' ) async_fire_mqtt_message(hass, 'homeassistant/binary_sensor/bla/config', @@ -540,7 +540,6 @@ async def test_entity_device_info_update(hass, mqtt_mock): 'platform': 'mqtt', 'name': 'Test 1', 'state_topic': 'test-topic', - 'command_topic': 'test-command-topic', 'device': { 'identifiers': ['helloworld'], 'connections': [ diff --git a/tests/components/mqtt/test_light.py b/tests/components/mqtt/test_light.py index f501f97331e..cfb0d75d1c7 100644 --- a/tests/components/mqtt/test_light.py +++ b/tests/components/mqtt/test_light.py @@ -1177,13 +1177,13 @@ async def test_unique_id(hass): light.DOMAIN: [{ 'platform': 'mqtt', 'name': 'Test 1', - 'status_topic': 'test-topic', + 'state_topic': 'test-topic', 'command_topic': 'test_topic', 'unique_id': 'TOTALLY_UNIQUE' }, { 'platform': 'mqtt', 'name': 'Test 2', - 'status_topic': 'test-topic', + 'state_topic': 'test-topic', 'command_topic': 'test_topic', 'unique_id': 'TOTALLY_UNIQUE' }] @@ -1200,7 +1200,7 @@ async def test_discovery_removal_light(hass, mqtt_mock, caplog): data = ( '{ "name": "Beer",' - ' "status_topic": "test_topic",' + ' "state_topic": "test_topic",' ' "command_topic": "test_topic" }' ) @@ -1245,12 +1245,12 @@ async def test_discovery_update_light(hass, mqtt_mock, caplog): data1 = ( '{ "name": "Beer",' - ' "status_topic": "test_topic",' + ' "state_topic": "test_topic",' ' "command_topic": "test_topic" }' ) data2 = ( '{ "name": "Milk",' - ' "status_topic": "test_topic",' + ' "state_topic": "test_topic",' ' "command_topic": "test_topic" }' ) @@ -1284,7 +1284,7 @@ async def test_discovery_broken(hass, mqtt_mock, caplog): ) data2 = ( '{ "name": "Milk",' - ' "status_topic": "test_topic",' + ' "state_topic": "test_topic",' ' "command_topic": "test_topic" }' ) diff --git a/tests/components/mqtt/test_light_json.py b/tests/components/mqtt/test_light_json.py index 9533ea5f204..a0ae0ddb2fb 100644 --- a/tests/components/mqtt/test_light_json.py +++ b/tests/components/mqtt/test_light_json.py @@ -649,14 +649,14 @@ async def test_unique_id(hass): 'platform': 'mqtt', 'name': 'Test 1', 'schema': 'json', - 'status_topic': 'test-topic', + 'state_topic': 'test-topic', 'command_topic': 'test_topic', 'unique_id': 'TOTALLY_UNIQUE' }, { 'platform': 'mqtt', 'name': 'Test 2', 'schema': 'json', - 'status_topic': 'test-topic', + 'state_topic': 'test-topic', 'command_topic': 'test_topic', 'unique_id': 'TOTALLY_UNIQUE' }] @@ -714,13 +714,13 @@ async def test_discovery_update_light(hass, mqtt_mock, caplog): data1 = ( '{ "name": "Beer",' ' "schema": "json",' - ' "status_topic": "test_topic",' + ' "state_topic": "test_topic",' ' "command_topic": "test_topic" }' ) data2 = ( '{ "name": "Milk",' ' "schema": "json",' - ' "status_topic": "test_topic",' + ' "state_topic": "test_topic",' ' "command_topic": "test_topic" }' ) @@ -755,7 +755,7 @@ async def test_discovery_broken(hass, mqtt_mock, caplog): data2 = ( '{ "name": "Milk",' ' "schema": "json",' - ' "status_topic": "test_topic",' + ' "state_topic": "test_topic",' ' "command_topic": "test_topic" }' ) diff --git a/tests/components/mqtt/test_light_template.py b/tests/components/mqtt/test_light_template.py index a7147e83b99..2db2bd06aa2 100644 --- a/tests/components/mqtt/test_light_template.py +++ b/tests/components/mqtt/test_light_template.py @@ -265,7 +265,6 @@ async def test_optimistic(hass, mqtt_mock): '{{ blue|d }}', 'command_off_template': 'off', 'effect_list': ['colorloop', 'random'], - 'effect_command_topic': 'test_light_rgb/effect/set', 'qos': 2 } }) @@ -608,7 +607,7 @@ async def test_unique_id(hass): 'platform': 'mqtt', 'name': 'Test 1', 'schema': 'template', - 'status_topic': 'test-topic', + 'state_topic': 'test-topic', 'command_topic': 'test_topic', 'command_on_template': 'on,{{ transition }}', 'command_off_template': 'off,{{ transition|d }}', @@ -617,7 +616,7 @@ async def test_unique_id(hass): 'platform': 'mqtt', 'name': 'Test 2', 'schema': 'template', - 'status_topic': 'test-topic', + 'state_topic': 'test-topic', 'command_topic': 'test_topic', 'unique_id': 'TOTALLY_UNIQUE' }] @@ -679,7 +678,7 @@ async def test_discovery_update_light(hass, mqtt_mock, caplog): data1 = ( '{ "name": "Beer",' ' "schema": "template",' - ' "status_topic": "test_topic",' + ' "state_topic": "test_topic",' ' "command_topic": "test_topic",' ' "command_on_template": "on",' ' "command_off_template": "off"}' @@ -687,7 +686,7 @@ async def test_discovery_update_light(hass, mqtt_mock, caplog): data2 = ( '{ "name": "Milk",' ' "schema": "template",' - ' "status_topic": "test_topic",' + ' "state_topic": "test_topic",' ' "command_topic": "test_topic",' ' "command_on_template": "on",' ' "command_off_template": "off"}' @@ -724,7 +723,7 @@ async def test_discovery_broken(hass, mqtt_mock, caplog): data2 = ( '{ "name": "Milk",' ' "schema": "template",' - ' "status_topic": "test_topic",' + ' "state_topic": "test_topic",' ' "command_topic": "test_topic",' ' "command_on_template": "on",' ' "command_off_template": "off"}' diff --git a/tests/components/mqtt/test_lock.py b/tests/components/mqtt/test_lock.py index c4741445cca..52dd3ecfbdb 100644 --- a/tests/components/mqtt/test_lock.py +++ b/tests/components/mqtt/test_lock.py @@ -246,13 +246,13 @@ async def test_unique_id(hass): lock.DOMAIN: [{ 'platform': 'mqtt', 'name': 'Test 1', - 'status_topic': 'test-topic', + 'state_topic': 'test-topic', 'command_topic': 'test_topic', 'unique_id': 'TOTALLY_UNIQUE' }, { 'platform': 'mqtt', 'name': 'Test 2', - 'status_topic': 'test-topic', + 'state_topic': 'test-topic', 'command_topic': 'test_topic', 'unique_id': 'TOTALLY_UNIQUE' }] diff --git a/tests/components/mqtt/test_sensor.py b/tests/components/mqtt/test_sensor.py index cd637d2aa01..027135e8a7a 100644 --- a/tests/components/mqtt/test_sensor.py +++ b/tests/components/mqtt/test_sensor.py @@ -428,12 +428,12 @@ async def test_discovery_update_attr(hass, mqtt_mock, caplog): await async_start(hass, 'homeassistant', {}, entry) data1 = ( '{ "name": "Beer",' - ' "command_topic": "test_topic",' + ' "state_topic": "test_topic",' ' "json_attributes_topic": "attr-topic1" }' ) data2 = ( '{ "name": "Beer",' - ' "command_topic": "test_topic",' + ' "state_topic": "test_topic",' ' "json_attributes_topic": "attr-topic2" }' ) async_fire_mqtt_message(hass, 'homeassistant/sensor/bla/config', @@ -495,7 +495,7 @@ async def test_discovery_removal_sensor(hass, mqtt_mock, caplog): await async_start(hass, 'homeassistant', {}, entry) data = ( '{ "name": "Beer",' - ' "status_topic": "test_topic" }' + ' "state_topic": "test_topic" }' ) async_fire_mqtt_message(hass, 'homeassistant/sensor/bla/config', data) @@ -517,11 +517,11 @@ async def test_discovery_update_sensor(hass, mqtt_mock, caplog): await async_start(hass, 'homeassistant', {}, entry) data1 = ( '{ "name": "Beer",' - ' "status_topic": "test_topic" }' + ' "state_topic": "test_topic" }' ) data2 = ( '{ "name": "Milk",' - ' "status_topic": "test_topic" }' + ' "state_topic": "test_topic" }' ) async_fire_mqtt_message(hass, 'homeassistant/sensor/bla/config', data1) @@ -626,7 +626,6 @@ async def test_entity_device_info_update(hass, mqtt_mock): 'platform': 'mqtt', 'name': 'Test 1', 'state_topic': 'test-topic', - 'command_topic': 'test-command-topic', 'device': { 'identifiers': ['helloworld'], 'connections': [ diff --git a/tests/components/mqtt/test_switch.py b/tests/components/mqtt/test_switch.py index f1956207a55..7917803aa07 100644 --- a/tests/components/mqtt/test_switch.py +++ b/tests/components/mqtt/test_switch.py @@ -392,7 +392,7 @@ async def test_discovery_removal_switch(hass, mqtt_mock, caplog): data = ( '{ "name": "Beer",' - ' "status_topic": "test_topic",' + ' "state_topic": "test_topic",' ' "command_topic": "test_topic" }' ) @@ -421,12 +421,12 @@ async def test_discovery_update_switch(hass, mqtt_mock, caplog): data1 = ( '{ "name": "Beer",' - ' "status_topic": "test_topic",' + ' "state_topic": "test_topic",' ' "command_topic": "test_topic" }' ) data2 = ( '{ "name": "Milk",' - ' "status_topic": "test_topic",' + ' "state_topic": "test_topic",' ' "command_topic": "test_topic" }' ) @@ -460,7 +460,7 @@ async def test_discovery_broken(hass, mqtt_mock, caplog): ) data2 = ( '{ "name": "Milk",' - ' "status_topic": "test_topic",' + ' "state_topic": "test_topic",' ' "command_topic": "test_topic" }' ) From c5c64e738e8f3f8810f2f4adf6dc2935173c3782 Mon Sep 17 00:00:00 2001 From: Tommy Jonsson Date: Tue, 29 Jan 2019 23:49:33 +0100 Subject: [PATCH 166/222] html5 notifications add VAPID support (#20415) * html5 notifications add VAPID support * fix lint errors * replace httpapi with websocketapi * Address my own comment --- homeassistant/components/notify/html5.py | 59 +++++++++++++++++++----- 1 file changed, 48 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/notify/html5.py b/homeassistant/components/notify/html5.py index 6a486bb6362..17f7e316357 100644 --- a/homeassistant/components/notify/html5.py +++ b/homeassistant/components/notify/html5.py @@ -17,6 +17,7 @@ from voluptuous.humanize import humanize_error from homeassistant.util.json import load_json, save_json from homeassistant.exceptions import HomeAssistantError +from homeassistant.components import websocket_api from homeassistant.components.frontend import add_manifest_json_key from homeassistant.components.http import HomeAssistantView from homeassistant.components.notify import ( @@ -39,10 +40,16 @@ SERVICE_DISMISS = 'html5_dismiss' ATTR_GCM_SENDER_ID = 'gcm_sender_id' ATTR_GCM_API_KEY = 'gcm_api_key' +ATTR_VAPID_PUB_KEY = 'vapid_pub_key' +ATTR_VAPID_PRV_KEY = 'vapid_prv_key' +ATTR_VAPID_EMAIL = 'vapid_email' PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Optional(ATTR_GCM_SENDER_ID): cv.string, vol.Optional(ATTR_GCM_API_KEY): cv.string, + vol.Optional(ATTR_VAPID_PUB_KEY): cv.string, + vol.Optional(ATTR_VAPID_PRV_KEY): cv.string, + vol.Optional(ATTR_VAPID_EMAIL): cv.string, }) ATTR_SUBSCRIPTION = 'subscription' @@ -64,6 +71,11 @@ ATTR_DISMISS = 'dismiss' ATTR_JWT = 'jwt' +WS_TYPE_APPKEY = 'notify/html5/appkey' +SCHEMA_WS_APPKEY = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend({ + vol.Required('type'): WS_TYPE_APPKEY +}) + # The number of days after the moment a notification is sent that a JWT # is valid. JWT_VALID_DAYS = 7 @@ -120,6 +132,18 @@ def get_service(hass, config, discovery_info=None): if registrations is None: return None + vapid_pub_key = config.get(ATTR_VAPID_PUB_KEY) + vapid_prv_key = config.get(ATTR_VAPID_PRV_KEY) + vapid_email = config.get(ATTR_VAPID_EMAIL) + + def websocket_appkey(hass, connection, msg): + connection.send_message( + websocket_api.result_message(msg['id'], vapid_pub_key)) + + hass.components.websocket_api.async_register_command( + WS_TYPE_APPKEY, websocket_appkey, SCHEMA_WS_APPKEY + ) + hass.http.register_view( HTML5PushRegistrationView(registrations, json_path)) hass.http.register_view(HTML5PushCallbackView(registrations)) @@ -132,7 +156,8 @@ def get_service(hass, config, discovery_info=None): ATTR_GCM_SENDER_ID, config.get(ATTR_GCM_SENDER_ID)) return HTML5NotificationService( - hass, gcm_api_key, registrations, json_path) + hass, gcm_api_key, vapid_prv_key, vapid_email, registrations, + json_path) def _load_config(filename): @@ -336,9 +361,12 @@ class HTML5PushCallbackView(HomeAssistantView): class HTML5NotificationService(BaseNotificationService): """Implement the notification service for HTML5.""" - def __init__(self, hass, gcm_key, registrations, json_path): + def __init__(self, hass, gcm_key, vapid_prv, vapid_email, registrations, + json_path): """Initialize the service.""" self._gcm_key = gcm_key + self._vapid_prv = vapid_prv + self._vapid_claims = {"sub": "mailto:{}".format(vapid_email)} self.registrations = registrations self.registrations_json_path = json_path @@ -425,7 +453,7 @@ class HTML5NotificationService(BaseNotificationService): def _push_message(self, payload, **kwargs): """Send the message.""" import jwt - from pywebpush import WebPusher + from pywebpush import WebPusher, webpush timestamp = int(time.time()) @@ -452,14 +480,23 @@ class HTML5NotificationService(BaseNotificationService): jwt_token = jwt.encode(jwt_claims, jwt_secret).decode('utf-8') payload[ATTR_DATA][ATTR_JWT] = jwt_token - # Only pass the gcm key if we're actually using GCM - # If we don't, notifications break on FireFox - gcm_key = self._gcm_key \ - if 'googleapis.com' in info[ATTR_SUBSCRIPTION][ATTR_ENDPOINT] \ - else None - response = WebPusher(info[ATTR_SUBSCRIPTION]).send( - json.dumps(payload), gcm_key=gcm_key, ttl='86400' - ) + if self._vapid_prv and self._vapid_claims: + response = webpush( + info[ATTR_SUBSCRIPTION], + json.dumps(payload), + vapid_private_key=self._vapid_prv, + vapid_claims=self._vapid_claims + ) + else: + # Only pass the gcm key if we're actually using GCM + # If we don't, notifications break on FireFox + gcm_key = self._gcm_key \ + if 'googleapis.com' \ + in info[ATTR_SUBSCRIPTION][ATTR_ENDPOINT] \ + else None + response = WebPusher(info[ATTR_SUBSCRIPTION]).send( + json.dumps(payload), gcm_key=gcm_key, ttl='86400' + ) if response.status_code == 410: _LOGGER.info("Notification channel has expired") From ec4495bd0c1158b25b2a3d21b54ceaa4f6918161 Mon Sep 17 00:00:00 2001 From: mindakas <45339544+mindakas@users.noreply.github.com> Date: Wed, 30 Jan 2019 13:57:14 +0200 Subject: [PATCH 167/222] Bump pymodbus to 1.5.2 (#20582) --- homeassistant/components/modbus.py | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/modbus.py b/homeassistant/components/modbus.py index c8d71af71b4..40ede019c10 100644 --- a/homeassistant/components/modbus.py +++ b/homeassistant/components/modbus.py @@ -16,7 +16,7 @@ from homeassistant.const import ( DOMAIN = 'modbus' -REQUIREMENTS = ['pymodbus==1.3.1'] +REQUIREMENTS = ['pymodbus==1.5.2'] # Type of network CONF_BAUDRATE = 'baudrate' diff --git a/requirements_all.txt b/requirements_all.txt index d94a746c50e..1f478f4bff9 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1122,7 +1122,7 @@ pymitv==1.4.3 pymochad==0.2.0 # homeassistant.components.modbus -pymodbus==1.3.1 +pymodbus==1.5.2 # homeassistant.components.media_player.monoprice pymonoprice==0.3 From 0ef9882e2eba80235c67a38ec67be5dfe1acbeb5 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Wed, 30 Jan 2019 17:09:56 +0100 Subject: [PATCH 168/222] Fix map icons --- homeassistant/components/map.py | 2 +- tests/components/frontend/test_init.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/map.py b/homeassistant/components/map.py index c0184239a1a..d30a7568452 100644 --- a/homeassistant/components/map.py +++ b/homeassistant/components/map.py @@ -10,5 +10,5 @@ DOMAIN = 'map' async def async_setup(hass, config): """Register the built-in map panel.""" await hass.components.frontend.async_register_built_in_panel( - 'map', 'map', 'hass:account-location') + 'map', 'map', 'hass:tooltip-account') return True diff --git a/tests/components/frontend/test_init.py b/tests/components/frontend/test_init.py index 9f386ceb904..b1b9a70d594 100644 --- a/tests/components/frontend/test_init.py +++ b/tests/components/frontend/test_init.py @@ -249,7 +249,7 @@ 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') + 'map', 'Map', 'mdi:tooltip-account') client = await hass_ws_client(hass) await client.send_json({ @@ -264,7 +264,7 @@ async def test_get_panels(hass, hass_ws_client): 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']['icon'] == 'mdi:tooltip-account' assert msg['result']['map']['title'] == 'Map' From a011048a4ec8cfb3a37e59cc3403c9df0b80ae84 Mon Sep 17 00:00:00 2001 From: Alok Saboo Date: Wed, 30 Jan 2019 12:40:40 -0500 Subject: [PATCH 169/222] Change Unifi timeout (#20606) --- homeassistant/components/unifi/controller.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/unifi/controller.py b/homeassistant/components/unifi/controller.py index 9e21956536f..11529cbe171 100644 --- a/homeassistant/components/unifi/controller.py +++ b/homeassistant/components/unifi/controller.py @@ -114,7 +114,7 @@ async def get_controller( ) try: - with async_timeout.timeout(5): + with async_timeout.timeout(10): await controller.login() return controller From 7c9597f8244ff45da715877ead13d04a3966169e Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 30 Jan 2019 09:41:55 -0800 Subject: [PATCH 170/222] Fix area registry config being loaded (#20598) * Fi area registry config being loaded * Mark area_id optional --- homeassistant/components/config/__init__.py | 1 + homeassistant/components/config/device_registry.py | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/config/__init__.py b/homeassistant/components/config/__init__.py index 4154ca337a3..65a4d50be84 100644 --- a/homeassistant/components/config/__init__.py +++ b/homeassistant/components/config/__init__.py @@ -14,6 +14,7 @@ from homeassistant.util.yaml import load_yaml, dump DOMAIN = 'config' DEPENDENCIES = ['http'] SECTIONS = ( + 'area_registry', 'auth', 'auth_provider_homeassistant', 'automation', diff --git a/homeassistant/components/config/device_registry.py b/homeassistant/components/config/device_registry.py index d81bdeb1f06..0677531242a 100644 --- a/homeassistant/components/config/device_registry.py +++ b/homeassistant/components/config/device_registry.py @@ -18,7 +18,7 @@ WS_TYPE_UPDATE = 'config/device_registry/update' SCHEMA_WS_UPDATE = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend({ vol.Required('type'): WS_TYPE_UPDATE, vol.Required('device_id'): str, - vol.Optional('area_id'): str, + vol.Optional('area_id'): vol.Any(str, None), }) From 349de19316b4ed23c03bccb254f094a40c895604 Mon Sep 17 00:00:00 2001 From: starkillerOG Date: Wed, 30 Jan 2019 18:42:18 +0100 Subject: [PATCH 171/222] Philips Hue: add prompt to update bridge/bulb (#20590) * Philips Hue: add prompt to update bridge/bulb * bump aiohue to v1.9.0 * bump aiohue to v1.9.0 * bump aiohue to v1.9.0 --- homeassistant/components/hue/__init__.py | 2 +- homeassistant/components/hue/light.py | 14 ++++++++++---- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 13 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/hue/__init__.py b/homeassistant/components/hue/__init__.py index b10e5bb29de..7618e702d04 100644 --- a/homeassistant/components/hue/__init__.py +++ b/homeassistant/components/hue/__init__.py @@ -19,7 +19,7 @@ from .bridge import HueBridge # Loading the config flow file will register the flow from .config_flow import configured_hosts -REQUIREMENTS = ['aiohue==1.8.0'] +REQUIREMENTS = ['aiohue==1.9.0'] _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/hue/light.py b/homeassistant/components/hue/light.py index 3327d0f9dc2..51e50f629b5 100644 --- a/homeassistant/components/hue/light.py +++ b/homeassistant/components/hue/light.py @@ -230,12 +230,18 @@ class HueLight(Light): self.gamut_typ = self.light.colorgamuttype self.gamut = self.light.colorgamut _LOGGER.debug("Color gamut of %s: %s", self.name, str(self.gamut)) + if self.light.swupdatestate == "readytoinstall": + err = ( + "Please check for software updates of the bridge " + "and/or the bulb: %s, in the Philips Hue App." + ) + _LOGGER.warning(err, self.name) if self.gamut: if not color.check_valid_gamut(self.gamut): - err = "Please check for software updates of the bridge " \ - "and/or bulb in the Philips Hue App, " \ - "Color gamut of %s: %s, not valid, " \ - "setting gamut to None." + err = ( + "Color gamut of %s: %s, not valid, " + "setting gamut to None." + ) _LOGGER.warning(err, self.name, str(self.gamut)) self.gamut_typ = GAMUT_TYPE_UNAVAILABLE self.gamut = None diff --git a/requirements_all.txt b/requirements_all.txt index 1f478f4bff9..6ccf12dba1e 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -115,7 +115,7 @@ aioharmony==0.1.5 aiohttp_cors==0.7.0 # homeassistant.components.hue -aiohue==1.8.0 +aiohue==1.9.0 # homeassistant.components.sensor.iliad_italy aioiliad==0.1.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 2dbd2760c7d..390a2cac46a 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -41,7 +41,7 @@ aioautomatic==0.6.5 aiohttp_cors==0.7.0 # homeassistant.components.hue -aiohue==1.8.0 +aiohue==1.9.0 # homeassistant.components.unifi aiounifi==4 From ed299a9137c9f9348508f4b6df81ea31ea3f72e1 Mon Sep 17 00:00:00 2001 From: choss Date: Wed, 30 Jan 2019 18:44:36 +0100 Subject: [PATCH 172/222] Add support for FRITZ DECT 100 (temp sensor) (#20308) --- homeassistant/components/fritzbox.py | 2 +- homeassistant/components/sensor/fritzbox.py | 77 +++++++++++++++++++++ 2 files changed, 78 insertions(+), 1 deletion(-) create mode 100644 homeassistant/components/sensor/fritzbox.py diff --git a/homeassistant/components/fritzbox.py b/homeassistant/components/fritzbox.py index e6f121799df..ad3c7bc1929 100644 --- a/homeassistant/components/fritzbox.py +++ b/homeassistant/components/fritzbox.py @@ -18,7 +18,7 @@ _LOGGER = logging.getLogger(__name__) REQUIREMENTS = ['pyfritzhome==0.4.0'] -SUPPORTED_DOMAINS = ['binary_sensor', 'climate', 'switch'] +SUPPORTED_DOMAINS = ['binary_sensor', 'climate', 'switch', 'sensor'] DOMAIN = 'fritzbox' diff --git a/homeassistant/components/sensor/fritzbox.py b/homeassistant/components/sensor/fritzbox.py new file mode 100644 index 00000000000..66c515c2bfd --- /dev/null +++ b/homeassistant/components/sensor/fritzbox.py @@ -0,0 +1,77 @@ +""" +Support for AVM Fritz!Box smarthome temperature sensor only devices. + +For more details about this component, please refer to the documentation at +http://home-assistant.io/components/sensor.fritzbox/ +""" +import logging + +import requests + +from homeassistant.components.fritzbox import DOMAIN as FRITZBOX_DOMAIN +from homeassistant.components.fritzbox import ( + ATTR_STATE_DEVICE_LOCKED, ATTR_STATE_LOCKED) +from homeassistant.helpers.entity import Entity +from homeassistant.const import TEMP_CELSIUS + +DEPENDENCIES = ['fritzbox'] + +_LOGGER = logging.getLogger(__name__) + + +def setup_platform(hass, config, add_entities, discovery_info=None): + """Set up the Fritzbox smarthome sensor platform.""" + _LOGGER.debug("Initializing fritzbox temperature sensors") + devices = [] + fritz_list = hass.data[FRITZBOX_DOMAIN] + + for fritz in fritz_list: + device_list = fritz.get_devices() + for device in device_list: + if (device.has_temperature_sensor + and not device.has_switch + and not device.has_thermostat): + devices.append(FritzBoxTempSensor(device, fritz)) + + add_entities(devices) + + +class FritzBoxTempSensor(Entity): + """The entity class for Fritzbox temperature sensors.""" + + def __init__(self, device, fritz): + """Initialize the switch.""" + self._device = device + self._fritz = fritz + + @property + def name(self): + """Return the name of the device.""" + return self._device.name + + @property + def state(self): + """Return the state of the sensor.""" + return self._device.temperature + + @property + def unit_of_measurement(self): + """Return the unit of measurement.""" + return TEMP_CELSIUS + + def update(self): + """Get latest data and states from the device.""" + try: + self._device.update() + except requests.exceptions.HTTPError as ex: + _LOGGER.warning("Fritzhome connection error: %s", ex) + self._fritz.login() + + @property + def device_state_attributes(self): + """Return the state attributes of the device.""" + attrs = { + ATTR_STATE_DEVICE_LOCKED: self._device.device_lock, + ATTR_STATE_LOCKED: self._device.lock, + } + return attrs From 8062f489735fe9359394c488123528238af329a8 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 30 Jan 2019 09:50:32 -0800 Subject: [PATCH 173/222] Add remove commmand to entity registry (#20597) --- .../components/config/entity_registry.py | 47 +++++++++++++++---- homeassistant/helpers/entity_registry.py | 6 +++ .../components/config/test_entity_registry.py | 44 +++++++++++++++++ 3 files changed, 87 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/config/entity_registry.py b/homeassistant/components/config/entity_registry.py index fdac1ad95da..39dd622540d 100644 --- a/homeassistant/components/config/entity_registry.py +++ b/homeassistant/components/config/entity_registry.py @@ -5,7 +5,8 @@ from homeassistant.core import callback from homeassistant.helpers.entity_registry import async_get_registry from homeassistant.components import websocket_api from homeassistant.components.websocket_api.const import ERR_NOT_FOUND -from homeassistant.components.websocket_api.decorators import async_response +from homeassistant.components.websocket_api.decorators import ( + async_response, require_admin) from homeassistant.helpers import config_validation as cv DEPENDENCIES = ['websocket_api'] @@ -30,6 +31,12 @@ SCHEMA_WS_UPDATE = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend({ vol.Optional('new_entity_id'): str, }) +WS_TYPE_REMOVE = 'config/entity_registry/remove' +SCHEMA_WS_REMOVE = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend({ + vol.Required('type'): WS_TYPE_REMOVE, + vol.Required('entity_id'): cv.entity_id +}) + async def async_setup(hass): """Enable the Entity Registry views.""" @@ -45,6 +52,10 @@ async def async_setup(hass): WS_TYPE_UPDATE, websocket_update_entity, SCHEMA_WS_UPDATE ) + hass.components.websocket_api.async_register_command( + WS_TYPE_REMOVE, websocket_remove_entity, + SCHEMA_WS_REMOVE + ) return True @@ -56,14 +67,7 @@ async def websocket_list_entities(hass, connection, msg): """ registry = await async_get_registry(hass) connection.send_message(websocket_api.result_message( - msg['id'], [{ - 'config_entry_id': entry.config_entry_id, - 'device_id': entry.device_id, - 'disabled_by': entry.disabled_by, - 'entity_id': entry.entity_id, - 'name': entry.name, - 'platform': entry.platform, - } for entry in registry.entities.values()] + msg['id'], [_entry_dict(entry) for entry in registry.entities.values()] )) @@ -86,6 +90,7 @@ async def websocket_get_entity(hass, connection, msg): )) +@require_admin @async_response async def websocket_update_entity(hass, connection, msg): """Handle update entity websocket command. @@ -125,10 +130,32 @@ async def websocket_update_entity(hass, connection, msg): )) +@require_admin +@async_response +async def websocket_remove_entity(hass, connection, msg): + """Handle remove entity websocket command. + + Async friendly. + """ + registry = await async_get_registry(hass) + + if msg['entity_id'] not in registry.entities: + connection.send_message(websocket_api.error_message( + msg['id'], ERR_NOT_FOUND, 'Entity not found')) + return + + registry.async_remove(msg['entity_id']) + connection.send_message(websocket_api.result_message(msg['id'])) + + @callback def _entry_dict(entry): """Convert entry to API format.""" return { + 'config_entry_id': entry.config_entry_id, + 'device_id': entry.device_id, + 'disabled_by': entry.disabled_by, 'entity_id': entry.entity_id, - 'name': entry.name + 'name': entry.name, + 'platform': entry.platform, } diff --git a/homeassistant/helpers/entity_registry.py b/homeassistant/helpers/entity_registry.py index e4f266854ef..6ee32f642bc 100644 --- a/homeassistant/helpers/entity_registry.py +++ b/homeassistant/helpers/entity_registry.py @@ -149,6 +149,12 @@ class EntityRegistry: self.async_schedule_save() return entity + @callback + def async_remove(self, entity_id): + """Remove an entity from registry.""" + self.entities.pop(entity_id) + self.async_schedule_save() + @callback def async_update_entity(self, entity_id, *, name=_UNDEF, new_entity_id=_UNDEF): diff --git a/tests/components/config/test_entity_registry.py b/tests/components/config/test_entity_registry.py index cd74faf1843..26903bb256b 100644 --- a/tests/components/config/test_entity_registry.py +++ b/tests/components/config/test_entity_registry.py @@ -82,6 +82,10 @@ async def test_get_entity(hass, client): msg = await client.receive_json() assert msg['result'] == { + 'config_entry_id': None, + 'device_id': None, + 'disabled_by': None, + 'platform': 'test_platform', 'entity_id': 'test_domain.name', 'name': 'Hello World' } @@ -94,6 +98,10 @@ async def test_get_entity(hass, client): msg = await client.receive_json() assert msg['result'] == { + 'config_entry_id': None, + 'device_id': None, + 'disabled_by': None, + 'platform': 'test_platform', 'entity_id': 'test_domain.no_name', 'name': None } @@ -128,6 +136,10 @@ async def test_update_entity_name(hass, client): msg = await client.receive_json() assert msg['result'] == { + 'config_entry_id': None, + 'device_id': None, + 'disabled_by': None, + 'platform': 'test_platform', 'entity_id': 'test_domain.world', 'name': 'after update' } @@ -165,6 +177,10 @@ async def test_update_entity_no_changes(hass, client): msg = await client.receive_json() assert msg['result'] == { + 'config_entry_id': None, + 'device_id': None, + 'disabled_by': None, + 'platform': 'test_platform', 'entity_id': 'test_domain.world', 'name': 'name of entity' } @@ -224,9 +240,37 @@ async def test_update_entity_id(hass, client): msg = await client.receive_json() assert msg['result'] == { + 'config_entry_id': None, + 'device_id': None, + 'disabled_by': None, + 'platform': 'test_platform', 'entity_id': 'test_domain.planet', 'name': None } assert hass.states.get('test_domain.world') is None assert hass.states.get('test_domain.planet') is not None + + +async def test_remove_entity(hass, client): + """Test removing entity.""" + registry = mock_registry(hass, { + 'test_domain.world': RegistryEntry( + entity_id='test_domain.world', + unique_id='1234', + # Using component.async_add_entities is equal to platform "domain" + platform='test_platform', + name='before update' + ) + }) + + await client.send_json({ + 'id': 6, + 'type': 'config/entity_registry/remove', + 'entity_id': 'test_domain.world', + }) + + msg = await client.receive_json() + + assert msg['success'] + assert len(registry.entities) == 0 From 91aa874c0cc8b4e0daf5a268bc2d3ac4c2339fc2 Mon Sep 17 00:00:00 2001 From: Anders Melchiorsen Date: Wed, 30 Jan 2019 19:47:28 +0100 Subject: [PATCH 174/222] Fix LIFX for single-zone strip extensions (#20604) --- homeassistant/components/light/lifx.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/homeassistant/components/light/lifx.py b/homeassistant/components/light/lifx.py index f448d61ee04..f0cd7b7a7fe 100644 --- a/homeassistant/components/light/lifx.py +++ b/homeassistant/components/light/lifx.py @@ -714,3 +714,7 @@ class LIFXStrip(LIFXColor): if resp: zone += 8 top = resp.count + + # We only await multizone responses so don't ask for just one + if zone == top-1: + zone -= 1 From cb07ea0d60acec43778687282504fdf8eb712885 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 30 Jan 2019 10:57:53 -0800 Subject: [PATCH 175/222] RFC: Add system health component (#20436) * Add system health component * Remove stale comment * Fix confusing syntax * Update test_init.py * Address comments * Lint * Move distro check to updater * Convert to websocket * Lint * Make info callback async * Fix tests * Fix tests * Lint * Catch exceptions --- .../components/system_health/__init__.py | 73 ++++++++++++ homeassistant/components/updater.py | 55 +++------ .../components/websocket_api/__init__.py | 11 +- .../components/websocket_api/decorators.py | 14 +++ homeassistant/config.py | 3 + homeassistant/helpers/system_info.py | 36 ++++++ tests/components/system_health/__init__.py | 1 + tests/components/system_health/test_init.py | 105 ++++++++++++++++++ tests/components/test_updater.py | 38 +++---- tests/helpers/test_system_info.py | 12 ++ 10 files changed, 283 insertions(+), 65 deletions(-) create mode 100644 homeassistant/components/system_health/__init__.py create mode 100644 homeassistant/helpers/system_info.py create mode 100644 tests/components/system_health/__init__.py create mode 100644 tests/components/system_health/test_init.py create mode 100644 tests/helpers/test_system_info.py diff --git a/homeassistant/components/system_health/__init__.py b/homeassistant/components/system_health/__init__.py new file mode 100644 index 00000000000..fca433550d7 --- /dev/null +++ b/homeassistant/components/system_health/__init__.py @@ -0,0 +1,73 @@ +"""System health component.""" +import asyncio +from collections import OrderedDict +import logging +from typing import Callable, Dict + +import async_timeout +import voluptuous as vol + +from homeassistant.core import callback +from homeassistant.loader import bind_hass +from homeassistant.helpers.typing import HomeAssistantType, ConfigType +from homeassistant.components import websocket_api + +DEPENDENCIES = ['http'] +DOMAIN = 'system_health' +INFO_CALLBACK_TIMEOUT = 5 +_LOGGER = logging.getLogger(__name__) + + +@bind_hass +@callback +def async_register_info(hass: HomeAssistantType, domain: str, + info_callback: Callable[[HomeAssistantType], Dict]): + """Register an info callback.""" + data = hass.data.setdefault( + DOMAIN, OrderedDict()).setdefault('info', OrderedDict()) + data[domain] = info_callback + + +async def async_setup(hass: HomeAssistantType, config: ConfigType): + """Set up the System Health component.""" + hass.components.websocket_api.async_register_command(handle_info) + return True + + +async def _info_wrapper(hass, info_callback): + """Wrap info callback.""" + try: + with async_timeout.timeout(INFO_CALLBACK_TIMEOUT): + return await info_callback(hass) + except asyncio.TimeoutError: + return { + 'error': 'Fetching info timed out' + } + except Exception as err: # pylint: disable=W0703 + _LOGGER.exception("Error fetching info") + return { + 'error': str(err) + } + + +@websocket_api.async_response +@websocket_api.websocket_command({ + vol.Required('type'): 'system_health/info' +}) +async def handle_info(hass: HomeAssistantType, + connection: websocket_api.ActiveConnection, + msg: Dict): + """Handle an info request.""" + info_callbacks = hass.data.get(DOMAIN, {}).get('info', {}) + data = OrderedDict() + data['homeassistant'] = \ + await hass.helpers.system_info.async_get_system_info() + + if info_callbacks: + for domain, domain_data in zip(info_callbacks, await asyncio.gather(*[ + _info_wrapper(hass, info_callback) for info_callback + in info_callbacks.values() + ])): + data[domain] = domain_data + + connection.send_message(websocket_api.result_message(msg['id'], data)) diff --git a/homeassistant/components/updater.py b/homeassistant/components/updater.py index 2e32960573d..daa85a2425e 100644 --- a/homeassistant/components/updater.py +++ b/homeassistant/components/updater.py @@ -10,21 +10,18 @@ from datetime import timedelta from distutils.version import StrictVersion import json import logging -import os -import platform import uuid import aiohttp import async_timeout import voluptuous as vol -from homeassistant.const import ATTR_FRIENDLY_NAME -from homeassistant.const import __version__ as current_version +from homeassistant.const import ( + ATTR_FRIENDLY_NAME, __version__ as current_version) from homeassistant.helpers import event from homeassistant.helpers.aiohttp_client import async_get_clientsession import homeassistant.helpers.config_validation as cv import homeassistant.util.dt as dt_util -from homeassistant.util.package import is_virtual_env REQUIREMENTS = ['distro==1.3.0'] @@ -124,44 +121,22 @@ async def async_setup(hass, config): return True -async def get_system_info(hass, include_components): - """Return info about the system.""" - info_object = { - 'arch': platform.machine(), - 'dev': 'dev' in current_version, - 'docker': False, - 'os_name': platform.system(), - 'python_version': platform.python_version(), - 'timezone': dt_util.DEFAULT_TIME_ZONE.zone, - 'version': current_version, - 'virtualenv': is_virtual_env(), - 'hassio': hass.components.hassio.is_hassio(), - } - - if include_components: - info_object['components'] = list(hass.config.components) - - if platform.system() == 'Windows': - info_object['os_version'] = platform.win32_ver()[0] - elif platform.system() == 'Darwin': - info_object['os_version'] = platform.mac_ver()[0] - elif platform.system() == 'FreeBSD': - info_object['os_version'] = platform.release() - elif platform.system() == 'Linux': - import distro - linux_dist = await hass.async_add_job( - distro.linux_distribution, False) - info_object['distribution'] = linux_dist[0] - info_object['os_version'] = linux_dist[1] - info_object['docker'] = os.path.isfile('/.dockerenv') - - return info_object - - async def get_newest_version(hass, huuid, include_components): """Get the newest Home Assistant version.""" if huuid: - info_object = await get_system_info(hass, include_components) + info_object = \ + await hass.helpers.system_info.async_get_system_info() + + if include_components: + info_object['components'] = list(hass.config.components) + + import distro + + linux_dist = await hass.async_add_executor_job( + distro.linux_distribution, False) + info_object['distribution'] = linux_dist[0] + info_object['os_version'] = linux_dist[1] + info_object['huuid'] = huuid else: info_object = {} diff --git a/homeassistant/components/websocket_api/__init__.py b/homeassistant/components/websocket_api/__init__.py index 9c67af820f4..48c8f27996a 100644 --- a/homeassistant/components/websocket_api/__init__.py +++ b/homeassistant/components/websocket_api/__init__.py @@ -22,13 +22,22 @@ result_message = messages.result_message async_response = decorators.async_response require_admin = decorators.require_admin ws_require_user = decorators.ws_require_user +websocket_command = decorators.websocket_command # pylint: enable=invalid-name @bind_hass @callback -def async_register_command(hass, command, handler, schema): +def async_register_command(hass, command_or_handler, handler=None, + schema=None): """Register a websocket command.""" + # pylint: disable=protected-access + if handler is None: + handler = command_or_handler + command = handler._ws_command + schema = handler._ws_schema + else: + command = command_or_handler handlers = hass.data.get(DOMAIN) if handlers is None: handlers = hass.data[DOMAIN] = {} diff --git a/homeassistant/components/websocket_api/decorators.py b/homeassistant/components/websocket_api/decorators.py index d91b884541d..08619f6d15f 100644 --- a/homeassistant/components/websocket_api/decorators.py +++ b/homeassistant/components/websocket_api/decorators.py @@ -98,3 +98,17 @@ def ws_require_user( return check_current_user return validator + + +def websocket_command(schema): + """Tag a function as a websocket command.""" + command = schema['type'] + + def decorate(func): + """Decorate ws command function.""" + # pylint: disable=protected-access + func._ws_schema = messages.BASE_COMMAND_MESSAGE_SCHEMA.extend(schema) + func._ws_command = command + return func + + return decorate diff --git a/homeassistant/config.py b/homeassistant/config.py index 3fd138f54e4..2a9f8f64835 100644 --- a/homeassistant/config.py +++ b/homeassistant/config.py @@ -105,6 +105,9 @@ map: # Track the sun sun: +# Allow diagnosing system problems +system_health: + # Sensors sensor: # Weather prediction diff --git a/homeassistant/helpers/system_info.py b/homeassistant/helpers/system_info.py new file mode 100644 index 00000000000..14cf1ff230c --- /dev/null +++ b/homeassistant/helpers/system_info.py @@ -0,0 +1,36 @@ +"""Helper to gather system info.""" +import os +import platform +from typing import Dict + +from homeassistant.const import __version__ as current_version +from homeassistant.loader import bind_hass +from homeassistant.util.package import is_virtual_env +from .typing import HomeAssistantType + + +@bind_hass +async def async_get_system_info(hass: HomeAssistantType) -> Dict: + """Return info about the system.""" + info_object = { + 'version': current_version, + 'dev': 'dev' in current_version, + 'hassio': hass.components.hassio.is_hassio(), + 'virtualenv': is_virtual_env(), + 'python_version': platform.python_version(), + 'docker': False, + 'arch': platform.machine(), + 'timezone': str(hass.config.time_zone), + 'os_name': platform.system(), + } + + if platform.system() == 'Windows': + info_object['os_version'] = platform.win32_ver()[0] + elif platform.system() == 'Darwin': + info_object['os_version'] = platform.mac_ver()[0] + elif platform.system() == 'FreeBSD': + info_object['os_version'] = platform.release() + elif platform.system() == 'Linux': + info_object['docker'] = os.path.isfile('/.dockerenv') + + return info_object diff --git a/tests/components/system_health/__init__.py b/tests/components/system_health/__init__.py new file mode 100644 index 00000000000..d59c20d4da6 --- /dev/null +++ b/tests/components/system_health/__init__.py @@ -0,0 +1 @@ +"""Tests for the system health component.""" diff --git a/tests/components/system_health/test_init.py b/tests/components/system_health/test_init.py new file mode 100644 index 00000000000..e090b11877e --- /dev/null +++ b/tests/components/system_health/test_init.py @@ -0,0 +1,105 @@ +"""Tests for the system health component init.""" +import asyncio +from unittest.mock import Mock + +import pytest + +from homeassistant.setup import async_setup_component + +from tests.common import mock_coro + + +@pytest.fixture +def mock_system_info(hass): + """Mock system info.""" + hass.helpers.system_info.async_get_system_info = Mock( + return_value=mock_coro({'hello': True}) + ) + + +async def test_info_endpoint_return_info(hass, hass_ws_client, + mock_system_info): + """Test that the info endpoint works.""" + assert await async_setup_component(hass, 'system_health', {}) + client = await hass_ws_client(hass) + + resp = await client.send_json({ + 'id': 6, + 'type': 'system_health/info', + }) + resp = await client.receive_json() + assert resp['success'] + data = resp['result'] + + assert len(data) == 1 + data = data['homeassistant'] + assert data == {'hello': True} + + +async def test_info_endpoint_register_callback(hass, hass_ws_client, + mock_system_info): + """Test that the info endpoint allows registering callbacks.""" + async def mock_info(hass): + return {'storage': 'YAML'} + + hass.components.system_health.async_register_info('lovelace', mock_info) + assert await async_setup_component(hass, 'system_health', {}) + client = await hass_ws_client(hass) + + resp = await client.send_json({ + 'id': 6, + 'type': 'system_health/info', + }) + resp = await client.receive_json() + assert resp['success'] + data = resp['result'] + + assert len(data) == 2 + data = data['lovelace'] + assert data == {'storage': 'YAML'} + + +async def test_info_endpoint_register_callback_timeout(hass, hass_ws_client, + mock_system_info): + """Test that the info endpoint timing out.""" + async def mock_info(hass): + raise asyncio.TimeoutError + + hass.components.system_health.async_register_info('lovelace', mock_info) + assert await async_setup_component(hass, 'system_health', {}) + client = await hass_ws_client(hass) + + resp = await client.send_json({ + 'id': 6, + 'type': 'system_health/info', + }) + resp = await client.receive_json() + assert resp['success'] + data = resp['result'] + + assert len(data) == 2 + data = data['lovelace'] + assert data == {'error': 'Fetching info timed out'} + + +async def test_info_endpoint_register_callback_exc(hass, hass_ws_client, + mock_system_info): + """Test that the info endpoint requires auth.""" + async def mock_info(hass): + raise Exception("TEST ERROR") + + hass.components.system_health.async_register_info('lovelace', mock_info) + assert await async_setup_component(hass, 'system_health', {}) + client = await hass_ws_client(hass) + + resp = await client.send_json({ + 'id': 6, + 'type': 'system_health/info', + }) + resp = await client.receive_json() + assert resp['success'] + data = resp['result'] + + assert len(data) == 2 + data = data['lovelace'] + assert data == {'error': 'TEST ERROR'} diff --git a/tests/components/test_updater.py b/tests/components/test_updater.py index 23b669928f4..bde6c3b0c61 100644 --- a/tests/components/test_updater.py +++ b/tests/components/test_updater.py @@ -8,7 +8,8 @@ import pytest from homeassistant.setup import async_setup_component from homeassistant.components import updater import homeassistant.util.dt as dt_util -from tests.common import async_fire_time_changed, mock_coro, mock_component +from tests.common import ( + async_fire_time_changed, mock_coro, mock_component, MockDependency) NEW_VERSION = '10000.0' MOCK_VERSION = '10.0' @@ -23,6 +24,13 @@ MOCK_CONFIG = {updater.DOMAIN: { }} +@pytest.fixture(autouse=True) +def mock_distro(): + """Mock distro dep.""" + with MockDependency('distro'): + yield + + @pytest.fixture def mock_get_newest_version(): """Fixture to mock get_newest_version.""" @@ -99,30 +107,12 @@ def test_disable_reporting(hass, mock_get_uuid, mock_get_newest_version): assert call[1] is None -@asyncio.coroutine -def test_enabled_component_info(hass, mock_get_uuid): - """Test if new entity is created if new version is available.""" - with patch('homeassistant.components.updater.platform.system', - Mock(return_value="junk")): - res = yield from updater.get_system_info(hass, True) - assert 'components' in res, 'Updater failed to generate component list' - - -@asyncio.coroutine -def test_disable_component_info(hass, mock_get_uuid): - """Test if new entity is created if new version is available.""" - with patch('homeassistant.components.updater.platform.system', - Mock(return_value="junk")): - res = yield from updater.get_system_info(hass, False) - assert 'components' not in res, 'Updater failed, components generate' - - @asyncio.coroutine def test_get_newest_version_no_analytics_when_no_huuid(hass, aioclient_mock): """Test we do not gather analytics when no huuid is passed in.""" aioclient_mock.post(updater.UPDATER_URL, json=MOCK_RESPONSE) - with patch('homeassistant.components.updater.get_system_info', + with patch('homeassistant.helpers.system_info.async_get_system_info', side_effect=Exception): res = yield from updater.get_newest_version(hass, None, False) assert res == (MOCK_RESPONSE['version'], @@ -134,7 +124,7 @@ def test_get_newest_version_analytics_when_huuid(hass, aioclient_mock): """Test we do not gather analytics when no huuid is passed in.""" aioclient_mock.post(updater.UPDATER_URL, json=MOCK_RESPONSE) - with patch('homeassistant.components.updater.get_system_info', + with patch('homeassistant.helpers.system_info.async_get_system_info', Mock(return_value=mock_coro({'fake': 'bla'}))): res = yield from updater.get_newest_version(hass, MOCK_HUUID, False) assert res == (MOCK_RESPONSE['version'], @@ -144,7 +134,7 @@ def test_get_newest_version_analytics_when_huuid(hass, aioclient_mock): @asyncio.coroutine def test_error_fetching_new_version_timeout(hass): """Test we do not gather analytics when no huuid is passed in.""" - with patch('homeassistant.components.updater.get_system_info', + with patch('homeassistant.helpers.system_info.async_get_system_info', Mock(return_value=mock_coro({'fake': 'bla'}))), \ patch('async_timeout.timeout', side_effect=asyncio.TimeoutError): res = yield from updater.get_newest_version(hass, MOCK_HUUID, False) @@ -156,7 +146,7 @@ def test_error_fetching_new_version_bad_json(hass, aioclient_mock): """Test we do not gather analytics when no huuid is passed in.""" aioclient_mock.post(updater.UPDATER_URL, text='not json') - with patch('homeassistant.components.updater.get_system_info', + with patch('homeassistant.helpers.system_info.async_get_system_info', Mock(return_value=mock_coro({'fake': 'bla'}))): res = yield from updater.get_newest_version(hass, MOCK_HUUID, False) assert res is None @@ -170,7 +160,7 @@ def test_error_fetching_new_version_invalid_response(hass, aioclient_mock): # 'release-notes' is missing }) - with patch('homeassistant.components.updater.get_system_info', + with patch('homeassistant.helpers.system_info.async_get_system_info', Mock(return_value=mock_coro({'fake': 'bla'}))): res = yield from updater.get_newest_version(hass, MOCK_HUUID, False) assert res is None diff --git a/tests/helpers/test_system_info.py b/tests/helpers/test_system_info.py new file mode 100644 index 00000000000..7f23447e1f4 --- /dev/null +++ b/tests/helpers/test_system_info.py @@ -0,0 +1,12 @@ +"""Tests for the system info helper.""" +import json + +from homeassistant.const import __version__ as current_version + + +async def test_get_system_info(hass): + """Test the get system info.""" + info = await hass.helpers.system_info.async_get_system_info() + assert isinstance(info, dict) + assert info['version'] == current_version + assert json.dumps(info) is not None From 2836ff86fe140ec280ba7f692a8e32a61a35dc65 Mon Sep 17 00:00:00 2001 From: William Scanlon Date: Wed, 30 Jan 2019 14:35:47 -0500 Subject: [PATCH 176/222] Update to the newest python-wink and fix push updates! (#20594) * Update to the newest python-wink and make post session call to Wink to keep pubnub working. * Update __init__.py --- homeassistant/components/wink/__init__.py | 6 ++++-- requirements_all.txt | 2 +- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/wink/__init__.py b/homeassistant/components/wink/__init__.py index 90e717efd9c..3cedb0b126b 100644 --- a/homeassistant/components/wink/__init__.py +++ b/homeassistant/components/wink/__init__.py @@ -25,7 +25,7 @@ from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.event import track_time_interval from homeassistant.util.json import load_json, save_json -REQUIREMENTS = ['python-wink==1.10.1', 'pubnubsub-handler==1.0.3'] +REQUIREMENTS = ['python-wink==1.10.3', 'pubnubsub-handler==1.0.3'] _LOGGER = logging.getLogger(__name__) @@ -358,7 +358,9 @@ def setup(hass, config): time.sleep(1) pywink.set_user_agent(USER_AGENT) _temp_response = pywink.wink_api_fetch() - _LOGGER.debug(str(json.dumps(_temp_response))) + _LOGGER.debug("%s", _temp_response) + _temp_response = pywink.post_session() + _LOGGER.debug("%s", _temp_response) # Call the Wink API every hour to keep PubNub updates flowing track_time_interval(hass, keep_alive_call, timedelta(minutes=60)) diff --git a/requirements_all.txt b/requirements_all.txt index 6ccf12dba1e..4b8942681bd 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1354,7 +1354,7 @@ python-velbus==2.0.21 python-vlc==1.1.2 # homeassistant.components.wink -python-wink==1.10.1 +python-wink==1.10.3 # homeassistant.components.sensor.awair python_awair==0.0.3 From e2cc1564a0d1427ac10547099a637a8e9b32ddf7 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 30 Jan 2019 12:57:56 -0800 Subject: [PATCH 177/222] Add lovelace systeam health (#20592) --- homeassistant/components/lovelace/__init__.py | 52 ++++++++++++++- tests/common.py | 5 ++ tests/components/lovelace/test_init.py | 66 +++++++++++++++++++ 3 files changed, 120 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/lovelace/__init__.py b/homeassistant/components/lovelace/__init__.py index c3254d84a73..b4cb2b18dca 100644 --- a/homeassistant/components/lovelace/__init__.py +++ b/homeassistant/components/lovelace/__init__.py @@ -75,6 +75,9 @@ async def async_setup(hass, config): WS_TYPE_SAVE_CONFIG, websocket_lovelace_save_config, SCHEMA_SAVE_CONFIG) + hass.components.system_health.async_register_info( + DOMAIN, system_health_info) + return True @@ -86,11 +89,22 @@ class LovelaceStorage: self._store = hass.helpers.storage.Store(STORAGE_VERSION, STORAGE_KEY) self._data = None + async def async_get_info(self): + """Return the YAML storage mode.""" + if self._data is None: + await self._load() + + if self._data['config'] is None: + return { + 'mode': 'auto-gen' + } + + return _config_info('storage', self._data['config']) + async def async_load(self, force): """Load config.""" if self._data is None: - data = await self._store.async_load() - self._data = data if data else {'config': None} + await self._load() config = self._data['config'] @@ -102,10 +116,15 @@ class LovelaceStorage: async def async_save(self, config): """Save config.""" if self._data is None: - self._data = {'config': None} + await self._load() self._data['config'] = config await self._store.async_save(self._data) + async def _load(self): + """Load the config.""" + data = await self._store.async_load() + self._data = data if data else {'config': None} + class LovelaceYAML: """Class to handle YAML-based Lovelace config.""" @@ -115,6 +134,19 @@ class LovelaceYAML: self.hass = hass self._cache = None + async def async_get_info(self): + """Return the YAML storage mode.""" + try: + config = await self.async_load(False) + except ConfigNotFound: + return { + 'mode': 'yaml', + 'error': '{} not found'.format( + self.hass.config.path(LOVELACE_CONFIG_FILE)) + } + + return _config_info('yaml', config) + async def async_load(self, force): """Load config.""" return await self.hass.async_add_executor_job(self._load_config, force) @@ -177,3 +209,17 @@ async def websocket_lovelace_config(hass, connection, msg): async def websocket_lovelace_save_config(hass, connection, msg): """Save Lovelace UI configuration.""" await hass.data[DOMAIN].async_save(msg['config']) + + +async def system_health_info(hass): + """Get info for the info page.""" + return await hass.data[DOMAIN].async_get_info() + + +def _config_info(mode, config): + """Generate info about the config.""" + return { + 'mode': mode, + 'resources': len(config.get('resources', [])), + 'views': len(config.get('views', [])) + } diff --git a/tests/common.py b/tests/common.py index 0f9b372c161..3642c5da6ec 100644 --- a/tests/common.py +++ b/tests/common.py @@ -890,3 +890,8 @@ async def flush_store(store): return await store._async_handle_write_data() + + +async def get_system_health_info(hass, domain): + """Get system health info.""" + return await hass.data['system_health']['info'][domain](hass) diff --git a/tests/components/lovelace/test_init.py b/tests/components/lovelace/test_init.py index 20490f8c0cd..7aa4ef0f5b3 100644 --- a/tests/components/lovelace/test_init.py +++ b/tests/components/lovelace/test_init.py @@ -4,6 +4,8 @@ from unittest.mock import patch from homeassistant.setup import async_setup_component from homeassistant.components import frontend, lovelace +from tests.common import get_system_health_info + async def test_lovelace_from_storage(hass, hass_ws_client, hass_storage): """Test we load lovelace config from storage.""" @@ -117,3 +119,67 @@ async def test_lovelace_from_yaml(hass, hass_ws_client): assert response['success'] assert response['result'] == {'hello': 'yo'} + + +async def test_system_health_info_autogen(hass): + """Test system health info endpoint.""" + assert await async_setup_component(hass, 'lovelace', {}) + info = await get_system_health_info(hass, 'lovelace') + assert info == {'mode': 'auto-gen'} + + +async def test_system_health_info_storage(hass, hass_storage): + """Test system health info endpoint.""" + hass_storage[lovelace.STORAGE_KEY] = { + 'key': 'lovelace', + 'version': 1, + 'data': { + 'config': { + 'resources': [], + 'views': [] + } + } + } + assert await async_setup_component(hass, 'lovelace', {}) + info = await get_system_health_info(hass, 'lovelace') + assert info == { + 'mode': 'storage', + 'resources': 0, + 'views': 0, + } + + +async def test_system_health_info_yaml(hass): + """Test system health info endpoint.""" + assert await async_setup_component(hass, 'lovelace', { + 'lovelace': { + 'mode': 'YAML' + } + }) + with patch('homeassistant.components.lovelace.load_yaml', return_value={ + 'views': [ + { + 'cards': [] + } + ] + }): + info = await get_system_health_info(hass, 'lovelace') + assert info == { + 'mode': 'yaml', + 'resources': 0, + 'views': 1, + } + + +async def test_system_health_info_yaml_not_found(hass): + """Test system health info endpoint.""" + assert await async_setup_component(hass, 'lovelace', { + 'lovelace': { + 'mode': 'YAML' + } + }) + info = await get_system_health_info(hass, 'lovelace') + assert info == { + 'mode': 'yaml', + 'error': "{} not found".format(hass.config.path('ui-lovelace.yaml')) + } From 542160fc56dc8aa7485649137740a5052680889e Mon Sep 17 00:00:00 2001 From: Gido Date: Wed, 30 Jan 2019 21:58:41 +0100 Subject: [PATCH 178/222] Add sensor platform for Rova Garbage Collection (#18868) * Adding sensor for SolarEdge Monitoring API support * Adding support for Rova garbage calendar * Added Rova platform to retrieve garbage pickup times * Update async to new syntax Added async_update to sensor class Added Throttle to Rova update function Minor code style changes * Small style fixes * Removed domain * Update debug to error messages Change CONF_MONITORED_VARIABLES to CONF_MONITORED_CONDITIONS Update async update call to normal update * Update requirements to rova v0.0.2 Add address check to see if ROVA collects in the given area * Rename entity names to English Add json_key to Sensor entities * Add device_class to the RovaSensor * Fix pylint and flake8 messages * Add check for None in case collection date is unknown * Return device class constant --- .coveragerc | 1 + homeassistant/components/sensor/rova.py | 149 ++++++++++++++++++++++++ requirements_all.txt | 3 + 3 files changed, 153 insertions(+) create mode 100644 homeassistant/components/sensor/rova.py diff --git a/.coveragerc b/.coveragerc index 5c99318b255..75b8b3b4ff8 100644 --- a/.coveragerc +++ b/.coveragerc @@ -856,6 +856,7 @@ omit = homeassistant/components/sensor/rainbird.py homeassistant/components/sensor/recollect_waste.py homeassistant/components/sensor/ripple.py + homeassistant/components/sensor/rova.py homeassistant/components/sensor/rtorrent.py homeassistant/components/sensor/ruter.py homeassistant/components/sensor/scrape.py diff --git a/homeassistant/components/sensor/rova.py b/homeassistant/components/sensor/rova.py new file mode 100644 index 00000000000..0b7f43f0973 --- /dev/null +++ b/homeassistant/components/sensor/rova.py @@ -0,0 +1,149 @@ +""" +Support for Rova garbage calendar. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/sensor.rova/ +""" + +from datetime import datetime, timedelta +import logging + +import voluptuous as vol + +from homeassistant.components.sensor import PLATFORM_SCHEMA +from homeassistant.const import (CONF_MONITORED_CONDITIONS, CONF_NAME, + DEVICE_CLASS_TIMESTAMP) +import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.entity import Entity +from homeassistant.util import Throttle + +REQUIREMENTS = ['rova==0.0.2'] + +# Config for rova requests. +CONF_ZIP_CODE = 'zip_code' +CONF_HOUSE_NUMBER = 'house_number' + +UPDATE_DELAY = timedelta(hours=12) +SCAN_INTERVAL = timedelta(hours=12) + +# Supported sensor types: +# Key: [json_key, name, icon] +SENSOR_TYPES = { + 'bio': ['gft', 'Biowaste', 'mdi:recycle'], + 'paper': ['papier', 'Paper', 'mdi:recycle'], + 'plastic': ['plasticplus', 'PET', 'mdi:recycle'], + 'residual': ['rest', 'Residual', 'mdi:recycle']} + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Required(CONF_ZIP_CODE): cv.string, + vol.Required(CONF_HOUSE_NUMBER): cv.string, + vol.Optional(CONF_NAME, default='Rova'): cv.string, + vol.Optional(CONF_MONITORED_CONDITIONS, default=['bio']): + vol.All(cv.ensure_list, [vol.In(SENSOR_TYPES)]) +}) + +_LOGGER = logging.getLogger(__name__) + + +def setup_platform(hass, config, add_entities, discovery_info=None): + """Create the Rova data service and sensors.""" + from rova.rova import Rova + from requests.exceptions import HTTPError, ConnectTimeout + + zip_code = config[CONF_ZIP_CODE] + house_number = config[CONF_HOUSE_NUMBER] + platform_name = config[CONF_NAME] + + # Create new Rova object to retrieve data + api = Rova(zip_code, house_number) + + try: + if not api.is_rova_area(): + _LOGGER.error("ROVA does not collect garbage in this area") + return + except (ConnectTimeout, HTTPError): + _LOGGER.error("Could not retrieve details from ROVA API") + return + + # Create rova data service which will retrieve and update the data. + data_service = RovaData(api) + + # Create a new sensor for each garbage type. + entities = [] + for sensor_key in config[CONF_MONITORED_CONDITIONS]: + sensor = RovaSensor(platform_name, sensor_key, data_service) + entities.append(sensor) + + add_entities(entities, True) + + +class RovaSensor(Entity): + """Representation of a Rova sensor.""" + + def __init__(self, platform_name, sensor_key, data_service): + """Initialize the sensor.""" + self.sensor_key = sensor_key + self.platform_name = platform_name + self.data_service = data_service + + self._state = None + + self._json_key = SENSOR_TYPES[self.sensor_key][0] + + @property + def name(self): + """Return the name.""" + return "{}_{}".format(self.platform_name, self.sensor_key) + + @property + def icon(self): + """Return the sensor icon.""" + return SENSOR_TYPES[self.sensor_key][2] + + @property + def device_class(self): + """Return the class of this sensor.""" + return DEVICE_CLASS_TIMESTAMP + + @property + def state(self): + """Return the state of the sensor.""" + return self._state + + def update(self): + """Get the latest data from the sensor and update the state.""" + self.data_service.update() + pickup_date = self.data_service.data.get(self._json_key) + if pickup_date is not None: + self._state = pickup_date.isoformat() + + +class RovaData: + """Get and update the latest data from the Rova API.""" + + def __init__(self, api): + """Initialize the data object.""" + self.api = api + self.data = {} + + @Throttle(UPDATE_DELAY) + def update(self): + """Update the data from the Rova API.""" + from requests.exceptions import HTTPError, ConnectTimeout + + try: + items = self.api.get_calendar_items() + except (ConnectTimeout, HTTPError): + _LOGGER.error("Could not retrieve data, retry again later") + return + + self.data = {} + + for item in items: + date = datetime.strptime(item['Date'], '%Y-%m-%dT%H:%M:%S') + code = item['GarbageTypeCode'].lower() + + if code not in self.data and date > datetime.now(): + self.data[code] = date + + _LOGGER.debug("Updated Rova calendar: %s", self.data) diff --git a/requirements_all.txt b/requirements_all.txt index 4b8942681bd..a59f7f5415e 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1470,6 +1470,9 @@ rocketchat-API==0.6.1 # homeassistant.components.vacuum.roomba roombapy==1.3.1 +# homeassistant.components.sensor.rova +rova==0.0.2 + # homeassistant.components.switch.rpi_rf # rpi-rf==0.9.7 From 3e98aad8a29968b7fe0e3d231d927627ac52e92c Mon Sep 17 00:00:00 2001 From: Alok Saboo Date: Wed, 30 Jan 2019 16:02:23 -0500 Subject: [PATCH 179/222] Added code to Abode Alarm control panel (#20611) --- homeassistant/components/alarm_control_panel/abode.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/alarm_control_panel/abode.py b/homeassistant/components/alarm_control_panel/abode.py index c57666d4fe6..6d4e28243ea 100644 --- a/homeassistant/components/alarm_control_panel/abode.py +++ b/homeassistant/components/alarm_control_panel/abode.py @@ -6,9 +6,9 @@ https://home-assistant.io/components/alarm_control_panel.abode/ """ import logging +import homeassistant.components.alarm_control_panel as alarm from homeassistant.components.abode import CONF_ATTRIBUTION, AbodeDevice from homeassistant.components.abode import DOMAIN as ABODE_DOMAIN -from homeassistant.components.alarm_control_panel import AlarmControlPanel from homeassistant.const import ( ATTR_ATTRIBUTION, STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME, STATE_ALARM_DISARMED) @@ -31,7 +31,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): add_entities(alarm_devices) -class AbodeAlarm(AbodeDevice, AlarmControlPanel): +class AbodeAlarm(AbodeDevice, alarm.AlarmControlPanel): """An alarm_control_panel implementation for Abode.""" def __init__(self, data, device, name): @@ -57,6 +57,11 @@ class AbodeAlarm(AbodeDevice, AlarmControlPanel): state = None return state + @property + def code_format(self): + """Return one or more digits/characters.""" + return alarm.FORMAT_NUMBER + def alarm_disarm(self, code=None): """Send disarm command.""" self._device.set_standby() From ed75549123bd8c409cd8678c41d5ab20ee629c12 Mon Sep 17 00:00:00 2001 From: "David F. Mulcahey" Date: Wed, 30 Jan 2019 16:44:22 -0500 Subject: [PATCH 180/222] ZHA component rewrite part 4 - add device module (#20469) * add device module * spelling * review comments * filter out endpoint id 0 (ZDO) * review comments * change name * remove return --- .coveragerc | 1 + homeassistant/components/zha/core/const.py | 9 + homeassistant/components/zha/core/device.py | 316 ++++++++++++++++++ .../components/zha/core/listeners.py | 35 ++ 4 files changed, 361 insertions(+) create mode 100644 homeassistant/components/zha/core/device.py diff --git a/.coveragerc b/.coveragerc index 75b8b3b4ff8..f1ff7715580 100644 --- a/.coveragerc +++ b/.coveragerc @@ -460,6 +460,7 @@ omit = homeassistant/components/zha/device_entity.py homeassistant/components/zha/core/helpers.py homeassistant/components/zha/core/const.py + homeassistant/components/zha/core/device.py homeassistant/components/zha/core/listeners.py homeassistant/components/zha/core/gateway.py homeassistant/components/*/zha.py diff --git a/homeassistant/components/zha/core/const.py b/homeassistant/components/zha/core/const.py index 47c3982c5d6..3069ebf02db 100644 --- a/homeassistant/components/zha/core/const.py +++ b/homeassistant/components/zha/core/const.py @@ -44,12 +44,21 @@ ATTR_MANUFACTURER = 'manufacturer' ATTR_COMMAND = 'command' ATTR_COMMAND_TYPE = 'command_type' ATTR_ARGS = 'args' +ATTR_ENDPOINT_ID = 'endpoint_id' IN = 'in' OUT = 'out' CLIENT_COMMANDS = 'client_commands' SERVER_COMMANDS = 'server_commands' SERVER = 'server' +IEEE = 'ieee' +MODEL = 'model' +NAME = 'name' + +LISTENER_BATTERY = 'battery' + +SIGNAL_ATTR_UPDATED = 'attribute_updated' +SIGNAL_AVAILABLE = 'available' class RadioType(enum.Enum): diff --git a/homeassistant/components/zha/core/device.py b/homeassistant/components/zha/core/device.py new file mode 100644 index 00000000000..c7dabced24b --- /dev/null +++ b/homeassistant/components/zha/core/device.py @@ -0,0 +1,316 @@ +""" +Device for Zigbee Home Automation. + +For more details about this component, please refer to the documentation at +https://home-assistant.io/components/zha/ +""" +import asyncio +import logging + +from homeassistant.helpers.dispatcher import ( + async_dispatcher_connect, async_dispatcher_send +) +from .const import ( + ATTR_MANUFACTURER, LISTENER_BATTERY, SIGNAL_AVAILABLE, IN, OUT, + ATTR_CLUSTER_ID, ATTR_ATTRIBUTE, ATTR_VALUE, ATTR_COMMAND, SERVER, + ATTR_COMMAND_TYPE, ATTR_ARGS, CLIENT_COMMANDS, SERVER_COMMANDS, + ATTR_ENDPOINT_ID, IEEE, MODEL, NAME +) +from .listeners import EventRelayListener + +_LOGGER = logging.getLogger(__name__) + + +class ZHADevice: + """ZHA Zigbee device object.""" + + def __init__(self, hass, zigpy_device, zha_gateway): + """Initialize the gateway.""" + self.hass = hass + self._zigpy_device = zigpy_device + # Get first non ZDO endpoint id to use to get manufacturer and model + endpoint_ids = zigpy_device.endpoints.keys() + ept_id = next(ept_id for ept_id in endpoint_ids if ept_id != 0) + self._manufacturer = zigpy_device.endpoints[ept_id].manufacturer + self._model = zigpy_device.endpoints[ept_id].model + self._zha_gateway = zha_gateway + self._cluster_listeners = {} + self._relay_listeners = [] + self._all_listeners = [] + self._name = "{} {}".format( + self.manufacturer, + self.model + ) + self._available = False + self._available_signal = "{}_{}_{}".format( + self.name, self.ieee, SIGNAL_AVAILABLE) + self._unsub = async_dispatcher_connect( + self.hass, + self._available_signal, + self.async_initialize + ) + + @property + def name(self): + """Return device name.""" + return self._name + + @property + def ieee(self): + """Return ieee address for device.""" + return self._zigpy_device.ieee + + @property + def manufacturer(self): + """Return ieee address for device.""" + return self._manufacturer + + @property + def model(self): + """Return ieee address for device.""" + return self._model + + @property + def nwk(self): + """Return nwk for device.""" + return self._zigpy_device.nwk + + @property + def lqi(self): + """Return lqi for device.""" + return self._zigpy_device.lqi + + @property + def rssi(self): + """Return rssi for device.""" + return self._zigpy_device.rssi + + @property + def last_seen(self): + """Return last_seen for device.""" + return self._zigpy_device.last_seen + + @property + def manufacturer_code(self): + """Return manufacturer code for device.""" + # will eventually get this directly from Zigpy + return None + + @property + def gateway(self): + """Return the gateway for this device.""" + return self._zha_gateway + + @property + def cluster_listeners(self): + """Return cluster listeners for device.""" + return self._cluster_listeners.values() + + @property + def all_listeners(self): + """Return cluster listeners and relay listeners for device.""" + return self._all_listeners + + @property + def cluster_listener_keys(self): + """Return cluster listeners for device.""" + return self._cluster_listeners.keys() + + @property + def available_signal(self): + """Signal to use to subscribe to device availability changes.""" + return self._available_signal + + @property + def available(self): + """Return True if sensor is available.""" + return self._available + + def update_available(self, available): + """Set sensor availability.""" + if self._available != available and available: + # Update the state the first time the device comes online + async_dispatcher_send( + self.hass, + self._available_signal, + False + ) + async_dispatcher_send( + self.hass, + "{}_{}".format(self._available_signal, 'entity'), + True + ) + self._available = available + + @property + def device_info(self): + """Return a device description for device.""" + ieee = str(self.ieee) + return { + IEEE: ieee, + ATTR_MANUFACTURER: self.manufacturer, + MODEL: self.model, + NAME: self.name or ieee + } + + def add_cluster_listener(self, cluster_listener): + """Add cluster listener to device.""" + # only keep 1 power listener + if cluster_listener.name is LISTENER_BATTERY and \ + LISTENER_BATTERY in self._cluster_listeners: + return + self._all_listeners.append(cluster_listener) + if isinstance(cluster_listener, EventRelayListener): + self._relay_listeners.append(cluster_listener) + else: + self._cluster_listeners[cluster_listener.name] = cluster_listener + + def get_cluster_listener(self, name): + """Get cluster listener by name.""" + return self._cluster_listeners.get(name, None) + + async def async_configure(self): + """Configure the device.""" + _LOGGER.debug('%s: started configuration', self.name) + await self._execute_listener_tasks('async_configure') + _LOGGER.debug('%s: completed configuration', self.name) + + async def async_initialize(self, from_cache): + """Initialize listeners.""" + _LOGGER.debug('%s: started initialization', self.name) + await self._execute_listener_tasks('async_initialize', from_cache) + _LOGGER.debug('%s: completed initialization', self.name) + + async def async_accept_messages(self): + """Start accepting messages from the zigbee network.""" + await self._execute_listener_tasks('accept_messages') + + async def _execute_listener_tasks(self, task_name, *args): + """Gather and execute a set of listener tasks.""" + listener_tasks = [] + for listener in self.all_listeners: + listener_tasks.append( + self._async_create_task(listener, task_name, *args)) + await asyncio.gather(*listener_tasks) + + async def _async_create_task(self, listener, func_name, *args): + """Configure a single listener on this device.""" + try: + await getattr(listener, func_name)(*args) + _LOGGER.debug('%s: listener: %s %s stage succeeded', + self.name, + "{}-{}".format( + listener.name, listener.unique_id), + func_name) + except Exception as ex: # pylint: disable=broad-except + _LOGGER.warning( + '%s listener: %s %s stage failed ex: %s', + self.name, + "{}-{}".format(listener.name, listener.unique_id), + func_name, + ex + ) + + async def async_unsub_dispatcher(self): + """Unsubscribe the dispatcher.""" + if self._unsub: + self._unsub() + + async def get_clusters(self): + """Get all clusters for this device.""" + return { + ep_id: { + IN: endpoint.in_clusters, + OUT: endpoint.out_clusters + } for (ep_id, endpoint) in self._zigpy_device.endpoints.items() + if ep_id != 0 + } + + async def get_cluster(self, endpooint_id, cluster_id, cluster_type=IN): + """Get zigbee cluster from this entity.""" + clusters = await self.get_clusters() + return clusters[endpooint_id][cluster_type][cluster_id] + + async def get_cluster_attributes(self, endpooint_id, cluster_id, + cluster_type=IN): + """Get zigbee attributes for specified cluster.""" + cluster = await self.get_cluster(endpooint_id, cluster_id, + cluster_type) + if cluster is None: + return None + return cluster.attributes + + async def get_cluster_commands(self, endpooint_id, cluster_id, + cluster_type=IN): + """Get zigbee commands for specified cluster.""" + cluster = await self.get_cluster(endpooint_id, cluster_id, + cluster_type) + if cluster is None: + return None + return { + CLIENT_COMMANDS: cluster.client_commands, + SERVER_COMMANDS: cluster.server_commands, + } + + async def write_zigbee_attribute(self, endpooint_id, cluster_id, + attribute, value, cluster_type=IN, + manufacturer=None): + """Write a value to a zigbee attribute for a cluster in this entity.""" + cluster = await self.get_cluster( + endpooint_id, cluster_id, cluster_type) + if cluster is None: + return None + + from zigpy.exceptions import DeliveryError + try: + response = await cluster.write_attributes( + {attribute: value}, + manufacturer=manufacturer + ) + _LOGGER.debug( + 'set: %s for attr: %s to cluster: %s for entity: %s - res: %s', + value, + attribute, + cluster_id, + endpooint_id, + response + ) + return response + except DeliveryError as exc: + _LOGGER.debug( + 'failed to set attribute: %s %s %s %s %s', + '{}: {}'.format(ATTR_VALUE, value), + '{}: {}'.format(ATTR_ATTRIBUTE, attribute), + '{}: {}'.format(ATTR_CLUSTER_ID, cluster_id), + '{}: {}'.format(ATTR_ENDPOINT_ID, endpooint_id), + exc + ) + return None + + async def issue_cluster_command(self, endpooint_id, cluster_id, command, + command_type, args, cluster_type=IN, + manufacturer=None): + """Issue a command against specified zigbee cluster on this entity.""" + cluster = await self.get_cluster( + endpooint_id, cluster_id, cluster_type) + if cluster is None: + return None + response = None + if command_type == SERVER: + response = await cluster.command(command, *args, + manufacturer=manufacturer, + expect_reply=True) + else: + response = await cluster.client_command(command, *args) + + _LOGGER.debug( + 'Issued cluster command: %s %s %s %s %s %s %s', + '{}: {}'.format(ATTR_CLUSTER_ID, cluster_id), + '{}: {}'.format(ATTR_COMMAND, command), + '{}: {}'.format(ATTR_COMMAND_TYPE, command_type), + '{}: {}'.format(ATTR_ARGS, args), + '{}: {}'.format(ATTR_CLUSTER_ID, cluster_type), + '{}: {}'.format(ATTR_MANUFACTURER, manufacturer), + '{}: {}'.format(ATTR_ENDPOINT_ID, endpooint_id) + ) + return response diff --git a/homeassistant/components/zha/core/listeners.py b/homeassistant/components/zha/core/listeners.py index d4fce491563..4f60ea83d6f 100644 --- a/homeassistant/components/zha/core/listeners.py +++ b/homeassistant/components/zha/core/listeners.py @@ -7,6 +7,9 @@ https://home-assistant.io/components/zha/ import logging +from homeassistant.core import callback +from .const import SIGNAL_ATTR_UPDATED + _LOGGER = logging.getLogger(__name__) @@ -108,3 +111,35 @@ class LevelListener(ClusterListener): """Handle attribute updates on this cluster.""" if attrid == self.CURRENT_LEVEL: self._entity.set_level(value) + + +class EventRelayListener(ClusterListener): + """Event relay that can be attached to zigbee clusters.""" + + name = 'event_relay' + + @callback + def attribute_updated(self, attrid, value): + """Handle an attribute updated on this cluster.""" + self.zha_send_event( + self._cluster, + SIGNAL_ATTR_UPDATED, + { + 'attribute_id': attrid, + 'attribute_name': self._cluster.attributes.get( + attrid, + ['Unknown'])[0], + 'value': value + } + ) + + @callback + def cluster_command(self, tsn, command_id, args): + """Handle a cluster command received on this cluster.""" + if self._cluster.server_commands is not None and \ + self._cluster.server_commands.get(command_id) is not None: + self.zha_send_event( + self._cluster, + self._cluster.server_commands.get(command_id)[0], + args + ) From 473bf93973896aa26d5615622a980e09ad461dc4 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 30 Jan 2019 16:50:08 -0800 Subject: [PATCH 181/222] Updated frontend to 20190130.1 --- homeassistant/components/frontend/__init__.py | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/frontend/__init__.py b/homeassistant/components/frontend/__init__.py index f5cc33b63a0..5613bdff3ff 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==20190121.1'] +REQUIREMENTS = ['home-assistant-frontend==20190130.1'] DOMAIN = 'frontend' DEPENDENCIES = ['api', 'websocket_api', 'http', 'system_log', diff --git a/requirements_all.txt b/requirements_all.txt index a59f7f5415e..0d9ea55b502 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -526,7 +526,7 @@ hole==0.3.0 holidays==0.9.9 # homeassistant.components.frontend -home-assistant-frontend==20190121.1 +home-assistant-frontend==20190130.1 # homeassistant.components.zwave homeassistant-pyozw==0.1.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 390a2cac46a..47dfaa69727 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -113,7 +113,7 @@ hdate==0.8.7 holidays==0.9.9 # homeassistant.components.frontend -home-assistant-frontend==20190121.1 +home-assistant-frontend==20190130.1 # homeassistant.components.homekit_controller homekit==0.12.2 From 5a0c707a376ed088996a9def1b9eccdd1a81d5e9 Mon Sep 17 00:00:00 2001 From: Jasper van der Neut - Stulen Date: Thu, 31 Jan 2019 02:12:59 +0100 Subject: [PATCH 182/222] Fix duplicate luftdaten entities (#20226) * Use same data schema for configflow, make sensor_id a positive integer. * Change sensor_id to int and remove duplicate Luftdaten config entries. This fixes #18838, and also fixes the root cause of #19981 and #19622. * Use pure type for boolean. --- .../components/luftdaten/__init__.py | 25 +++++++++++++++++-- .../components/luftdaten/config_flow.py | 15 ++++++++--- 2 files changed, 35 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/luftdaten/__init__.py b/homeassistant/components/luftdaten/__init__.py index b00fca7d3c0..45d75b90f7f 100644 --- a/homeassistant/components/luftdaten/__init__.py +++ b/homeassistant/components/luftdaten/__init__.py @@ -12,13 +12,14 @@ from homeassistant.config_entries import SOURCE_IMPORT from homeassistant.const import ( CONF_MONITORED_CONDITIONS, CONF_SCAN_INTERVAL, CONF_SENSORS, CONF_SHOW_ON_MAP, TEMP_CELSIUS) +from homeassistant.core import callback from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers.aiohttp_client import async_get_clientsession import homeassistant.helpers.config_validation as cv from homeassistant.helpers.dispatcher import async_dispatcher_send from homeassistant.helpers.event import async_track_time_interval -from .config_flow import configured_sensors +from .config_flow import configured_sensors, duplicate_stations from .const import CONF_SENSOR_ID, DEFAULT_SCAN_INTERVAL, DOMAIN REQUIREMENTS = ['luftdaten==0.3.4'] @@ -67,6 +68,14 @@ CONFIG_SCHEMA = vol.Schema({ }, extra=vol.ALLOW_EXTRA) +@callback +def _async_fixup_sensor_id(hass, config_entry, sensor_id): + hass.config_entries.async_update_entry( + config_entry, data={ + **config_entry.data, CONF_SENSOR_ID: int(sensor_id) + }) + + async def async_setup(hass, config): """Set up the Luftdaten component.""" hass.data[DOMAIN] = {} @@ -77,7 +86,7 @@ async def async_setup(hass, config): return True conf = config[DOMAIN] - station_id = conf.get(CONF_SENSOR_ID) + station_id = conf[CONF_SENSOR_ID] if station_id not in configured_sensors(hass): hass.async_create_task( @@ -102,6 +111,18 @@ async def async_setup_entry(hass, config_entry): from luftdaten import Luftdaten from luftdaten.exceptions import LuftdatenError + if not isinstance(config_entry.data[CONF_SENSOR_ID], int): + _async_fixup_sensor_id(hass, config_entry, + config_entry.data[CONF_SENSOR_ID]) + + if (config_entry.data[CONF_SENSOR_ID] in + duplicate_stations(hass) and config_entry.source == SOURCE_IMPORT): + _LOGGER.warning("Removing duplicate sensors for station %s", + config_entry.data[CONF_SENSOR_ID]) + hass.async_create_task(hass.config_entries.async_remove( + config_entry.entry_id)) + return False + session = async_get_clientsession(hass) try: diff --git a/homeassistant/components/luftdaten/config_flow.py b/homeassistant/components/luftdaten/config_flow.py index 33715c3c0c1..b4ebc93da9c 100644 --- a/homeassistant/components/luftdaten/config_flow.py +++ b/homeassistant/components/luftdaten/config_flow.py @@ -7,6 +7,7 @@ from homeassistant import config_entries from homeassistant.const import CONF_SCAN_INTERVAL, CONF_SHOW_ON_MAP from homeassistant.core import callback from homeassistant.helpers import aiohttp_client +import homeassistant.helpers.config_validation as cv from .const import CONF_SENSOR_ID, DEFAULT_SCAN_INTERVAL, DOMAIN @@ -15,10 +16,18 @@ from .const import CONF_SENSOR_ID, DEFAULT_SCAN_INTERVAL, DOMAIN def configured_sensors(hass): """Return a set of configured Luftdaten sensors.""" return set( - '{0}'.format(entry.data[CONF_SENSOR_ID]) + entry.data[CONF_SENSOR_ID] for entry in hass.config_entries.async_entries(DOMAIN)) +@callback +def duplicate_stations(hass): + """Return a set of duplicate configured Luftdaten stations.""" + stations = [int(entry.data[CONF_SENSOR_ID]) + for entry in hass.config_entries.async_entries(DOMAIN)] + return {x for x in stations if stations.count(x) > 1} + + @config_entries.HANDLERS.register(DOMAIN) class LuftDatenFlowHandler(config_entries.ConfigFlow): """Handle a Luftdaten config flow.""" @@ -30,7 +39,7 @@ class LuftDatenFlowHandler(config_entries.ConfigFlow): def _show_form(self, errors=None): """Show the form to the user.""" data_schema = OrderedDict() - data_schema[vol.Required(CONF_SENSOR_ID)] = str + data_schema[vol.Required(CONF_SENSOR_ID)] = cv.positive_int data_schema[vol.Optional(CONF_SHOW_ON_MAP, default=False)] = bool return self.async_show_form( @@ -72,4 +81,4 @@ class LuftDatenFlowHandler(config_entries.ConfigFlow): CONF_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL) user_input.update({CONF_SCAN_INTERVAL: scan_interval.seconds}) - return self.async_create_entry(title=sensor_id, data=user_input) + return self.async_create_entry(title=str(sensor_id), data=user_input) From 69ec7980ad974844420aa3c64157e7e1b078f788 Mon Sep 17 00:00:00 2001 From: Andrew Sayre <6730289+andrewsayre@users.noreply.github.com> Date: Wed, 30 Jan 2019 19:31:59 -0600 Subject: [PATCH 183/222] Add SmartThings component and switch platform (#20148) * Added SmartThings component and switch platform * Corrected comment typos. * Embedded switch platform. * Replaced custom view usage with webhook component. * Replaced urls with tokens in strings. * Fixed line length. * Use generated webhook id instead of static one. * Reuse core constant instead of defining again. * Optimizations in anticipation of future platforms. * Use async_generate_path instead of hard-coded path. * Fixed line length. * Updates per review feedback. * Updates per latest review feedback. --- CODEOWNERS | 1 + .../smartthings/.translations/en.json | 27 ++ .../components/smartthings/__init__.py | 213 +++++++++++++ .../components/smartthings/config_flow.py | 179 +++++++++++ homeassistant/components/smartthings/const.py | 31 ++ .../components/smartthings/smartapp.py | 275 +++++++++++++++++ .../components/smartthings/strings.json | 27 ++ .../components/smartthings/switch.py | 70 +++++ homeassistant/config_entries.py | 1 + requirements_all.txt | 6 + requirements_test_all.txt | 6 + script/gen_requirements_all.py | 2 + tests/components/smartthings/__init__.py | 1 + tests/components/smartthings/conftest.py | 279 ++++++++++++++++++ .../smartthings/test_config_flow.py | 245 +++++++++++++++ tests/components/smartthings/test_init.py | 183 ++++++++++++ tests/components/smartthings/test_smartapp.py | 112 +++++++ tests/components/smartthings/test_switch.py | 135 +++++++++ 18 files changed, 1793 insertions(+) create mode 100644 homeassistant/components/smartthings/.translations/en.json create mode 100644 homeassistant/components/smartthings/__init__.py create mode 100644 homeassistant/components/smartthings/config_flow.py create mode 100644 homeassistant/components/smartthings/const.py create mode 100644 homeassistant/components/smartthings/smartapp.py create mode 100644 homeassistant/components/smartthings/strings.json create mode 100644 homeassistant/components/smartthings/switch.py create mode 100644 tests/components/smartthings/__init__.py create mode 100644 tests/components/smartthings/conftest.py create mode 100644 tests/components/smartthings/test_config_flow.py create mode 100644 tests/components/smartthings/test_init.py create mode 100644 tests/components/smartthings/test_smartapp.py create mode 100644 tests/components/smartthings/test_switch.py diff --git a/CODEOWNERS b/CODEOWNERS index 2a2391186f4..98eaca90076 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -236,6 +236,7 @@ homeassistant/components/*/rfxtrx.py @danielhiversen # S homeassistant/components/simplisafe/* @bachya +homeassistant/components/smartthings/* @andrewsayre # T homeassistant/components/tahoma.py @philklei diff --git a/homeassistant/components/smartthings/.translations/en.json b/homeassistant/components/smartthings/.translations/en.json new file mode 100644 index 00000000000..1fb4e878cb4 --- /dev/null +++ b/homeassistant/components/smartthings/.translations/en.json @@ -0,0 +1,27 @@ +{ + "config": { + "title": "SmartThings", + "step": { + "user": { + "title": "Enter Personal Access Token", + "description": "Please enter a SmartThings [Personal Access Token]({token_url}) that has been created per the [instructions]({component_url}).", + "data": { + "access_token": "Access Token" + } + }, + "wait_install": { + "title": "Install SmartApp", + "description": "Please install the Home Assistant SmartApp in at least one location and click submit." + } + }, + "error": { + "token_invalid_format": "The token must be in the UID/GUID format", + "token_unauthorized": "The token is invalid or no longer authorized.", + "token_forbidden": "The token does not have the required OAuth scopes.", + "token_already_setup": "The token has already been setup.", + "app_setup_error": "Unable to setup the SmartApp. Please try again.", + "app_not_installed": "Please ensure you have installed and authorized the Home Assistant SmartApp and try again.", + "base_url_not_https": "The `base_url` for the `http` component must be configured and start with `https://`." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/smartthings/__init__.py b/homeassistant/components/smartthings/__init__.py new file mode 100644 index 00000000000..c705a3df73e --- /dev/null +++ b/homeassistant/components/smartthings/__init__.py @@ -0,0 +1,213 @@ +"""SmartThings Cloud integration for Home Assistant.""" + +import asyncio +import logging +from typing import Iterable + +from aiohttp.client_exceptions import ( + ClientConnectionError, ClientResponseError) + +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_ACCESS_TOKEN +from homeassistant.exceptions import ConfigEntryNotReady +from homeassistant.helpers.aiohttp_client import async_get_clientsession +from homeassistant.helpers.dispatcher import ( + async_dispatcher_connect, async_dispatcher_send) +from homeassistant.helpers.entity import Entity +from homeassistant.helpers.typing import ConfigType, HomeAssistantType + +from .config_flow import SmartThingsFlowHandler # noqa +from .const import ( + CONF_APP_ID, CONF_INSTALLED_APP_ID, DATA_BROKERS, DATA_MANAGER, DOMAIN, + SIGNAL_SMARTTHINGS_UPDATE, SUPPORTED_PLATFORMS) +from .smartapp import ( + setup_smartapp, setup_smartapp_endpoint, validate_installed_app) + +REQUIREMENTS = ['pysmartapp==0.3.0', 'pysmartthings==0.4.2'] +DEPENDENCIES = ['webhook'] + +_LOGGER = logging.getLogger(__name__) + + +async def async_setup(hass: HomeAssistantType, config: ConfigType): + """Initialize the SmartThings platform.""" + await setup_smartapp_endpoint(hass) + return True + + +async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry): + """Initialize config entry which represents an installed SmartApp.""" + from pysmartthings import SmartThings + + if not hass.config.api.base_url.lower().startswith('https://'): + _LOGGER.warning("The 'base_url' of the 'http' component must be " + "configured and start with 'https://'") + return False + + api = SmartThings(async_get_clientsession(hass), + entry.data[CONF_ACCESS_TOKEN]) + + remove_entry = False + try: + # See if the app is already setup. This occurs when there are + # installs in multiple SmartThings locations (valid use-case) + manager = hass.data[DOMAIN][DATA_MANAGER] + smart_app = manager.smartapps.get(entry.data[CONF_APP_ID]) + if not smart_app: + # Validate and setup the app. + app = await api.app(entry.data[CONF_APP_ID]) + smart_app = setup_smartapp(hass, app) + + # Validate and retrieve the installed app. + installed_app = await validate_installed_app( + api, entry.data[CONF_INSTALLED_APP_ID]) + + # Get devices and their current status + devices = await api.devices( + location_ids=[installed_app.location_id]) + + async def retrieve_device_status(device): + try: + await device.status.refresh() + except ClientResponseError: + _LOGGER.debug("Unable to update status for device: %s (%s), " + "the device will be ignored", + device.label, device.device_id, exc_info=True) + devices.remove(device) + + await asyncio.gather(*[retrieve_device_status(d) + for d in devices.copy()]) + + # Setup device broker + broker = DeviceBroker(hass, devices, + installed_app.installed_app_id) + broker.event_handler_disconnect = \ + smart_app.connect_event(broker.event_handler) + hass.data[DOMAIN][DATA_BROKERS][entry.entry_id] = broker + + except ClientResponseError as ex: + if ex.status in (401, 403): + _LOGGER.exception("Unable to setup config entry '%s' - please " + "reconfigure the integration", entry.title) + remove_entry = True + else: + _LOGGER.debug(ex, exc_info=True) + raise ConfigEntryNotReady + except (ClientConnectionError, RuntimeWarning) as ex: + _LOGGER.debug(ex, exc_info=True) + raise ConfigEntryNotReady + + if remove_entry: + hass.async_create_task( + hass.config_entries.async_remove(entry.entry_id)) + # only create new flow if there isn't a pending one for SmartThings. + flows = hass.config_entries.flow.async_progress() + if not [flow for flow in flows if flow['handler'] == DOMAIN]: + hass.async_create_task( + hass.config_entries.flow.async_init( + DOMAIN, context={'source': 'import'})) + return False + + for component in SUPPORTED_PLATFORMS: + hass.async_create_task(hass.config_entries.async_forward_entry_setup( + entry, component)) + return True + + +async def async_unload_entry(hass: HomeAssistantType, entry: ConfigEntry): + """Unload a config entry.""" + broker = hass.data[DOMAIN][DATA_BROKERS].pop(entry.entry_id, None) + if broker and broker.event_handler_disconnect: + broker.event_handler_disconnect() + + tasks = [hass.config_entries.async_forward_entry_unload(entry, component) + for component in SUPPORTED_PLATFORMS] + return all(await asyncio.gather(*tasks)) + + +class DeviceBroker: + """Manages an individual SmartThings config entry.""" + + def __init__(self, hass: HomeAssistantType, devices: Iterable, + installed_app_id: str): + """Create a new instance of the DeviceBroker.""" + self._hass = hass + self._installed_app_id = installed_app_id + self.devices = {device.device_id: device for device in devices} + self.event_handler_disconnect = None + + async def event_handler(self, req, resp, app): + """Broker for incoming events.""" + from pysmartapp.event import EVENT_TYPE_DEVICE + + # Do not process events received from a different installed app + # under the same parent SmartApp (valid use-scenario) + if req.installed_app_id != self._installed_app_id: + return + + updated_devices = set() + for evt in req.events: + if evt.event_type != EVENT_TYPE_DEVICE: + continue + device = self.devices.get(evt.device_id) + if not device: + continue + device.status.apply_attribute_update( + evt.component_id, evt.capability, evt.attribute, evt.value) + updated_devices.add(device.device_id) + _LOGGER.debug("Update received with %s events and updated %s devices", + len(req.events), len(updated_devices)) + + async_dispatcher_send(self._hass, SIGNAL_SMARTTHINGS_UPDATE, + updated_devices) + + +class SmartThingsEntity(Entity): + """Defines a SmartThings entity.""" + + def __init__(self, device): + """Initialize the instance.""" + self._device = device + self._dispatcher_remove = None + + async def async_added_to_hass(self): + """Device added to hass.""" + async def async_update_state(devices): + """Update device state.""" + if self._device.device_id in devices: + await self.async_update_ha_state(True) + + self._dispatcher_remove = async_dispatcher_connect( + self.hass, SIGNAL_SMARTTHINGS_UPDATE, async_update_state) + + async def async_will_remove_from_hass(self) -> None: + """Disconnect the device when removed.""" + if self._dispatcher_remove: + self._dispatcher_remove() + + @property + def device_info(self): + """Get attributes about the device.""" + return { + 'identifiers': { + (DOMAIN, self._device.device_id) + }, + 'name': self._device.label, + 'model': self._device.device_type_name, + 'manufacturer': 'Unavailable' + } + + @property + def name(self) -> str: + """Return the name of the device.""" + return self._device.label + + @property + def should_poll(self) -> bool: + """No polling needed for this device.""" + return False + + @property + def unique_id(self) -> str: + """Return a unique ID.""" + return self._device.device_id diff --git a/homeassistant/components/smartthings/config_flow.py b/homeassistant/components/smartthings/config_flow.py new file mode 100644 index 00000000000..b280036a615 --- /dev/null +++ b/homeassistant/components/smartthings/config_flow.py @@ -0,0 +1,179 @@ +"""Config flow to configure SmartThings.""" +import logging + +from aiohttp.client_exceptions import ClientResponseError +import voluptuous as vol + +from homeassistant import config_entries +from homeassistant.const import CONF_ACCESS_TOKEN +from homeassistant.helpers.aiohttp_client import async_get_clientsession + +from .const import ( + CONF_APP_ID, CONF_INSTALLED_APP_ID, CONF_LOCATION_ID, DOMAIN, + VAL_UID_MATCHER) +from .smartapp import ( + create_app, find_app, setup_smartapp, setup_smartapp_endpoint, update_app) + +_LOGGER = logging.getLogger(__name__) + + +@config_entries.HANDLERS.register(DOMAIN) +class SmartThingsFlowHandler(config_entries.ConfigFlow): + """ + Handle configuration of SmartThings integrations. + + Any number of integrations are supported. The high level flow follows: + 1) Flow initiated + a) User initiates through the UI + b) Re-configuration of a failed entry setup + 2) Enter access token + a) Check not already setup + b) Validate format + c) Setup SmartApp + 3) Wait for Installation + a) Check user installed into one or more locations + b) Config entries setup for all installations + """ + + VERSION = 1 + CONNECTION_CLASS = config_entries.CONN_CLASS_CLOUD_PUSH + + def __init__(self): + """Create a new instance of the flow handler.""" + self.access_token = None + self.app_id = None + self.api = None + + async def async_step_import(self, user_input=None): + """Occurs when a previously entry setup fails and is re-initiated.""" + return await self.async_step_user(user_input) + + async def async_step_user(self, user_input=None): + """Get access token and validate it.""" + from pysmartthings import SmartThings + + errors = {} + if not self.hass.config.api.base_url.lower().startswith('https://'): + errors['base'] = "base_url_not_https" + return self._show_step_user(errors) + + if user_input is None or CONF_ACCESS_TOKEN not in user_input: + return self._show_step_user(errors) + + self.access_token = user_input.get(CONF_ACCESS_TOKEN, '') + self.api = SmartThings(async_get_clientsession(self.hass), + self.access_token) + + # Ensure token is a UUID + if not VAL_UID_MATCHER.match(self.access_token): + errors[CONF_ACCESS_TOKEN] = "token_invalid_format" + return self._show_step_user(errors) + # Check not already setup in another entry + if any(entry.data.get(CONF_ACCESS_TOKEN) == self.access_token + for entry + in self.hass.config_entries.async_entries(DOMAIN)): + errors[CONF_ACCESS_TOKEN] = "token_already_setup" + return self._show_step_user(errors) + + # Setup end-point + await setup_smartapp_endpoint(self.hass) + + try: + app = await find_app(self.hass, self.api) + if app: + await app.refresh() # load all attributes + await update_app(self.hass, app) + else: + app = await create_app(self.hass, self.api) + setup_smartapp(self.hass, app) + self.app_id = app.app_id + except ClientResponseError as ex: + if ex.status == 401: + errors[CONF_ACCESS_TOKEN] = "token_unauthorized" + elif ex.status == 403: + errors[CONF_ACCESS_TOKEN] = "token_forbidden" + else: + errors['base'] = "app_setup_error" + return self._show_step_user(errors) + except Exception: # pylint:disable=broad-except + errors['base'] = "app_setup_error" + _LOGGER.exception("Unexpected error setting up the SmartApp") + return self._show_step_user(errors) + + return await self.async_step_wait_install() + + async def async_step_wait_install(self, user_input=None): + """Wait for SmartApp installation.""" + from pysmartthings import InstalledAppStatus + + errors = {} + if user_input is None: + return self._show_step_wait_install(errors) + + # Find installed apps that were authorized + installed_apps = [app for app in await self.api.installed_apps( + installed_app_status=InstalledAppStatus.AUTHORIZED) + if app.app_id == self.app_id] + if not installed_apps: + errors['base'] = 'app_not_installed' + return self._show_step_wait_install(errors) + + # User may have installed the SmartApp in more than one SmartThings + # location. Config flows are created for the additional installations + for installed_app in installed_apps[1:]: + self.hass.async_create_task( + self.hass.config_entries.flow.async_init( + DOMAIN, context={'source': 'install'}, + data={ + CONF_APP_ID: installed_app.app_id, + CONF_INSTALLED_APP_ID: installed_app.installed_app_id, + CONF_LOCATION_ID: installed_app.location_id, + CONF_ACCESS_TOKEN: self.access_token + })) + + # return entity for the first one. + installed_app = installed_apps[0] + return await self.async_step_install({ + CONF_APP_ID: installed_app.app_id, + CONF_INSTALLED_APP_ID: installed_app.installed_app_id, + CONF_LOCATION_ID: installed_app.location_id, + CONF_ACCESS_TOKEN: self.access_token + }) + + def _show_step_user(self, errors): + return self.async_show_form( + step_id='user', + data_schema=vol.Schema({ + vol.Required(CONF_ACCESS_TOKEN, + default=self.access_token): str + }), + errors=errors, + description_placeholders={ + 'token_url': 'https://account.smartthings.com/tokens', + 'component_url': + 'https://www.home-assistant.io/components/smartthings/' + } + ) + + def _show_step_wait_install(self, errors): + return self.async_show_form( + step_id='wait_install', + errors=errors + ) + + async def async_step_install(self, data=None): + """ + Create a config entry at completion of a flow. + + Launched when the user completes the flow or when the SmartApp + is installed into an additional location. + """ + from pysmartthings import SmartThings + + if not self.api: + # Launched from the SmartApp install event handler + self.api = SmartThings( + async_get_clientsession(self.hass), data[CONF_ACCESS_TOKEN]) + + location = await self.api.location(data[CONF_LOCATION_ID]) + return self.async_create_entry(title=location.name, data=data) diff --git a/homeassistant/components/smartthings/const.py b/homeassistant/components/smartthings/const.py new file mode 100644 index 00000000000..9a6d96bfab9 --- /dev/null +++ b/homeassistant/components/smartthings/const.py @@ -0,0 +1,31 @@ +"""Constants used by the SmartThings component and platforms.""" +import re + +APP_OAUTH_SCOPES = [ + 'r:devices:*' +] +APP_NAME_PREFIX = 'homeassistant.' +CONF_APP_ID = 'app_id' +CONF_INSTALLED_APP_ID = 'installed_app_id' +CONF_INSTANCE_ID = 'instance_id' +CONF_LOCATION_ID = 'location_id' +DATA_MANAGER = 'manager' +DATA_BROKERS = 'brokers' +DOMAIN = 'smartthings' +SIGNAL_SMARTTHINGS_UPDATE = 'smartthings_update' +SIGNAL_SMARTAPP_PREFIX = 'smartthings_smartap_' +SETTINGS_INSTANCE_ID = "hassInstanceId" +STORAGE_KEY = DOMAIN +STORAGE_VERSION = 1 +SUPPORTED_PLATFORMS = [ + 'switch' +] +SUPPORTED_CAPABILITIES = [ + 'colorControl', + 'colorTemperature', + 'switch', + 'switchLevel' +] +VAL_UID = "^(?:([0-9a-fA-F]{32})|([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]" \ + "{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}))$" +VAL_UID_MATCHER = re.compile(VAL_UID) diff --git a/homeassistant/components/smartthings/smartapp.py b/homeassistant/components/smartthings/smartapp.py new file mode 100644 index 00000000000..9d9dacf8460 --- /dev/null +++ b/homeassistant/components/smartthings/smartapp.py @@ -0,0 +1,275 @@ +""" +SmartApp functionality to receive cloud-push notifications. + +This module defines the functions to manage the SmartApp integration +within the SmartThings ecosystem in order to receive real-time webhook-based +callbacks when device states change. +""" +import asyncio +import functools +import logging +from uuid import uuid4 + +from aiohttp import web + +from homeassistant.components import webhook +from homeassistant.const import CONF_ACCESS_TOKEN, CONF_WEBHOOK_ID +from homeassistant.helpers.aiohttp_client import async_get_clientsession +from homeassistant.helpers.dispatcher import ( + async_dispatcher_connect, async_dispatcher_send) +from homeassistant.helpers.typing import HomeAssistantType + +from .const import ( + APP_NAME_PREFIX, APP_OAUTH_SCOPES, CONF_APP_ID, CONF_INSTALLED_APP_ID, + CONF_INSTANCE_ID, CONF_LOCATION_ID, DATA_BROKERS, DATA_MANAGER, DOMAIN, + SETTINGS_INSTANCE_ID, SIGNAL_SMARTAPP_PREFIX, STORAGE_KEY, STORAGE_VERSION, + SUPPORTED_CAPABILITIES) + +_LOGGER = logging.getLogger(__name__) + + +async def find_app(hass: HomeAssistantType, api): + """Find an existing SmartApp for this installation of hass.""" + apps = await api.apps() + for app in [app for app in apps + if app.app_name.startswith(APP_NAME_PREFIX)]: + # Load settings to compare instance id + settings = await app.settings() + if settings.settings.get(SETTINGS_INSTANCE_ID) == \ + hass.data[DOMAIN][CONF_INSTANCE_ID]: + return app + + +async def validate_installed_app(api, installed_app_id: str): + """ + Ensure the specified installed SmartApp is valid and functioning. + + Query the API for the installed SmartApp and validate that it is tied to + the specified app_id and is in an authorized state. + """ + from pysmartthings import InstalledAppStatus + + installed_app = await api.installed_app(installed_app_id) + if installed_app.installed_app_status != InstalledAppStatus.AUTHORIZED: + raise RuntimeWarning("Installed SmartApp instance '{}' ({}) is not " + "AUTHORIZED but instead {}" + .format(installed_app.display_name, + installed_app.installed_app_id, + installed_app.installed_app_status)) + return installed_app + + +def _get_app_template(hass: HomeAssistantType): + from pysmartthings import APP_TYPE_WEBHOOK, CLASSIFICATION_AUTOMATION + + return { + 'app_name': APP_NAME_PREFIX + str(uuid4()), + 'display_name': 'Home Assistant', + 'description': "Home Assistant at " + hass.config.api.base_url, + 'webhook_target_url': webhook.async_generate_url( + hass, hass.data[DOMAIN][CONF_WEBHOOK_ID]), + 'app_type': APP_TYPE_WEBHOOK, + 'single_instance': True, + 'classifications': [CLASSIFICATION_AUTOMATION] + } + + +async def create_app(hass: HomeAssistantType, api): + """Create a SmartApp for this instance of hass.""" + from pysmartthings import App, AppOAuth, AppSettings + from pysmartapp.const import SETTINGS_APP_ID + + # Create app from template attributes + template = _get_app_template(hass) + app = App() + for key, value in template.items(): + setattr(app, key, value) + app = (await api.create_app(app))[0] + _LOGGER.debug("Created SmartApp '%s' (%s)", app.app_name, app.app_id) + + # Set unique hass id in settings + settings = AppSettings(app.app_id) + settings.settings[SETTINGS_APP_ID] = app.app_id + settings.settings[SETTINGS_INSTANCE_ID] = \ + hass.data[DOMAIN][CONF_INSTANCE_ID] + await api.update_app_settings(settings) + _LOGGER.debug("Updated App Settings for SmartApp '%s' (%s)", + app.app_name, app.app_id) + + # Set oauth scopes + oauth = AppOAuth(app.app_id) + oauth.client_name = 'Home Assistant' + oauth.scope.extend(APP_OAUTH_SCOPES) + await api.update_app_oauth(oauth) + _LOGGER.debug("Updated App OAuth for SmartApp '%s' (%s)", + app.app_name, app.app_id) + return app + + +async def update_app(hass: HomeAssistantType, app): + """Ensure the SmartApp is up-to-date and update if necessary.""" + template = _get_app_template(hass) + template.pop('app_name') # don't update this + update_required = False + for key, value in template.items(): + if getattr(app, key) != value: + update_required = True + setattr(app, key, value) + if update_required: + await app.save() + _LOGGER.debug("SmartApp '%s' (%s) updated with latest settings", + app.app_name, app.app_id) + + +def setup_smartapp(hass, app): + """ + Configure an individual SmartApp in hass. + + Register the SmartApp with the SmartAppManager so that hass will service + lifecycle events (install, event, etc...). A unique SmartApp is created + for each SmartThings account that is configured in hass. + """ + manager = hass.data[DOMAIN][DATA_MANAGER] + smartapp = manager.smartapps.get(app.app_id) + if smartapp: + # already setup + return smartapp + smartapp = manager.register(app.app_id, app.webhook_public_key) + smartapp.name = app.display_name + smartapp.description = app.description + smartapp.permissions.extend(APP_OAUTH_SCOPES) + return smartapp + + +async def setup_smartapp_endpoint(hass: HomeAssistantType): + """ + Configure the SmartApp webhook in hass. + + SmartApps are an extension point within the SmartThings ecosystem and + is used to receive push updates (i.e. device updates) from the cloud. + """ + from pysmartapp import Dispatcher, SmartAppManager + + data = hass.data.get(DOMAIN) + if data: + # already setup + return + + # Get/create config to store a unique id for this hass instance. + store = hass.helpers.storage.Store(STORAGE_VERSION, STORAGE_KEY) + config = await store.async_load() + if not config: + # Create config + config = { + CONF_INSTANCE_ID: str(uuid4()), + CONF_WEBHOOK_ID: webhook.generate_secret() + } + await store.async_save(config) + + # SmartAppManager uses a dispatcher to invoke callbacks when push events + # occur. Use hass' implementation instead of the built-in one. + dispatcher = Dispatcher( + signal_prefix=SIGNAL_SMARTAPP_PREFIX, + connect=functools.partial(async_dispatcher_connect, hass), + send=functools.partial(async_dispatcher_send, hass)) + manager = SmartAppManager( + webhook.async_generate_path(config[CONF_WEBHOOK_ID]), + dispatcher=dispatcher) + manager.connect_install(functools.partial(smartapp_install, hass)) + manager.connect_uninstall(functools.partial(smartapp_uninstall, hass)) + + webhook.async_register(hass, DOMAIN, 'SmartApp', + config[CONF_WEBHOOK_ID], smartapp_webhook) + + hass.data[DOMAIN] = { + DATA_MANAGER: manager, + CONF_INSTANCE_ID: config[CONF_INSTANCE_ID], + DATA_BROKERS: {}, + CONF_WEBHOOK_ID: config[CONF_WEBHOOK_ID] + } + + +async def smartapp_install(hass: HomeAssistantType, req, resp, app): + """ + Handle when a SmartApp is installed by the user into a location. + + Setup subscriptions using the access token SmartThings provided in the + event. An explicit subscription is required for each 'capability' in order + to receive the related attribute updates. Finally, create a config entry + representing the installation if this is not the first installation under + the account. + """ + from pysmartthings import SmartThings, Subscription, SourceType + + # This access token is a temporary 'SmartApp token' that expires in 5 min + # and is used to create subscriptions only. + api = SmartThings(async_get_clientsession(hass), req.auth_token) + + async def create_subscription(target): + sub = Subscription() + sub.installed_app_id = req.installed_app_id + sub.location_id = req.location_id + sub.source_type = SourceType.CAPABILITY + sub.capability = target + try: + await api.create_subscription(sub) + _LOGGER.debug("Created subscription for '%s' under app '%s'", + target, req.installed_app_id) + except Exception: # pylint:disable=broad-except + _LOGGER.exception("Failed to create subscription for '%s' under " + "app '%s'", target, req.installed_app_id) + + tasks = [create_subscription(c) for c in SUPPORTED_CAPABILITIES] + await asyncio.gather(*tasks) + _LOGGER.debug("SmartApp '%s' under parent app '%s' was installed", + req.installed_app_id, app.app_id) + + # The permanent access token is copied from another config flow with the + # same parent app_id. If one is not found, that means the user is within + # the initial config flow and the entry at the conclusion. + access_token = next(( + entry.data.get(CONF_ACCESS_TOKEN) for entry + in hass.config_entries.async_entries(DOMAIN) + if entry.data[CONF_APP_ID] == app.app_id), None) + if access_token: + # Add as job not needed because the current coroutine was invoked + # from the dispatcher and is not being awaited. + await hass.config_entries.flow.async_init( + DOMAIN, context={'source': 'install'}, + data={ + CONF_APP_ID: app.app_id, + CONF_INSTALLED_APP_ID: req.installed_app_id, + CONF_LOCATION_ID: req.location_id, + CONF_ACCESS_TOKEN: access_token + }) + + +async def smartapp_uninstall(hass: HomeAssistantType, req, resp, app): + """ + Handle when a SmartApp is removed from a location by the user. + + Find and delete the config entry representing the integration. + """ + entry = next((entry for entry in hass.config_entries.async_entries(DOMAIN) + if entry.data.get(CONF_INSTALLED_APP_ID) == + req.installed_app_id), + None) + if entry: + _LOGGER.debug("SmartApp '%s' under parent app '%s' was removed", + req.installed_app_id, app.app_id) + # Add as job not needed because the current coroutine was invoked + # from the dispatcher and is not being awaited. + await hass.config_entries.async_remove(entry.entry_id) + + +async def smartapp_webhook(hass: HomeAssistantType, webhook_id: str, request): + """ + Handle a smartapp lifecycle event callback from SmartThings. + + Requests from SmartThings are digitally signed and the SmartAppManager + validates the signature for authenticity. + """ + manager = hass.data[DOMAIN][DATA_MANAGER] + data = await request.json() + result = await manager.handle_request(data, request.headers) + return web.json_response(result) diff --git a/homeassistant/components/smartthings/strings.json b/homeassistant/components/smartthings/strings.json new file mode 100644 index 00000000000..1fb4e878cb4 --- /dev/null +++ b/homeassistant/components/smartthings/strings.json @@ -0,0 +1,27 @@ +{ + "config": { + "title": "SmartThings", + "step": { + "user": { + "title": "Enter Personal Access Token", + "description": "Please enter a SmartThings [Personal Access Token]({token_url}) that has been created per the [instructions]({component_url}).", + "data": { + "access_token": "Access Token" + } + }, + "wait_install": { + "title": "Install SmartApp", + "description": "Please install the Home Assistant SmartApp in at least one location and click submit." + } + }, + "error": { + "token_invalid_format": "The token must be in the UID/GUID format", + "token_unauthorized": "The token is invalid or no longer authorized.", + "token_forbidden": "The token does not have the required OAuth scopes.", + "token_already_setup": "The token has already been setup.", + "app_setup_error": "Unable to setup the SmartApp. Please try again.", + "app_not_installed": "Please ensure you have installed and authorized the Home Assistant SmartApp and try again.", + "base_url_not_https": "The `base_url` for the `http` component must be configured and start with `https://`." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/smartthings/switch.py b/homeassistant/components/smartthings/switch.py new file mode 100644 index 00000000000..1fccfcd3619 --- /dev/null +++ b/homeassistant/components/smartthings/switch.py @@ -0,0 +1,70 @@ +""" +Support for switches through the SmartThings cloud API. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/smartthings.switch/ +""" +from homeassistant.components.switch import SwitchDevice + +from . import SmartThingsEntity +from .const import DATA_BROKERS, DOMAIN + +DEPENDENCIES = ['smartthings'] + + +async def async_setup_platform(hass, config, async_add_entities, + discovery_info=None): + """Platform uses config entry setup.""" + pass + + +async def async_setup_entry(hass, config_entry, async_add_entities): + """Add switches for a config entry.""" + broker = hass.data[DOMAIN][DATA_BROKERS][config_entry.entry_id] + async_add_entities( + [SmartThingsSwitch(device) for device in broker.devices.values() + if is_switch(device)]) + + +def is_switch(device): + """Determine if the device should be represented as a switch.""" + from pysmartthings import Capability + + # Must be able to be turned on/off. + if Capability.switch not in device.capabilities: + return False + # Must not have a capability represented by other types. + non_switch_capabilities = [ + Capability.color_control, + Capability.color_temperature, + Capability.fan_speed, + Capability.switch_level + ] + if any(capability in device.capabilities + for capability in non_switch_capabilities): + return False + + return True + + +class SmartThingsSwitch(SmartThingsEntity, SwitchDevice): + """Define a SmartThings switch.""" + + async def async_turn_off(self, **kwargs) -> None: + """Turn the switch off.""" + await self._device.switch_off(set_status=True) + # State is set optimistically in the command above, therefore update + # the entity state ahead of receiving the confirming push updates + self.async_schedule_update_ha_state() + + async def async_turn_on(self, **kwargs) -> None: + """Turn the switch on.""" + await self._device.switch_on(set_status=True) + # State is set optimistically in the command above, therefore update + # the entity state ahead of receiving the confirming push updates + self.async_schedule_update_ha_state() + + @property + def is_on(self) -> bool: + """Return true if light is on.""" + return self._device.status.switch diff --git a/homeassistant/config_entries.py b/homeassistant/config_entries.py index 159f5651c31..9c4c127f52e 100644 --- a/homeassistant/config_entries.py +++ b/homeassistant/config_entries.py @@ -160,6 +160,7 @@ FLOWS = [ 'point', 'rainmachine', 'simplisafe', + 'smartthings', 'smhi', 'sonos', 'tellduslive', diff --git a/requirements_all.txt b/requirements_all.txt index 0d9ea55b502..de8ad8c7914 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1224,6 +1224,12 @@ pysher==1.0.1 # homeassistant.components.sensor.sma pysma==0.3.1 +# homeassistant.components.smartthings +pysmartapp==0.3.0 + +# homeassistant.components.smartthings +pysmartthings==0.4.2 + # homeassistant.components.device_tracker.snmp # homeassistant.components.sensor.snmp # homeassistant.components.switch.snmp diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 47dfaa69727..a56626734eb 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -210,6 +210,12 @@ pyotp==2.2.6 # homeassistant.components.qwikswitch pyqwikswitch==0.8 +# homeassistant.components.smartthings +pysmartapp==0.3.0 + +# homeassistant.components.smartthings +pysmartthings==0.4.2 + # homeassistant.components.sonos pysonos==0.0.6 diff --git a/script/gen_requirements_all.py b/script/gen_requirements_all.py index 79ba3f8c342..398b2791848 100755 --- a/script/gen_requirements_all.py +++ b/script/gen_requirements_all.py @@ -90,6 +90,8 @@ TEST_REQUIREMENTS = ( 'pynx584', 'pyopenuv', 'pyotp', + 'pysmartapp', + 'pysmartthings', 'pysonos', 'pyqwikswitch', 'PyRMVtransport', diff --git a/tests/components/smartthings/__init__.py b/tests/components/smartthings/__init__.py new file mode 100644 index 00000000000..5a3e9135963 --- /dev/null +++ b/tests/components/smartthings/__init__.py @@ -0,0 +1 @@ +"""Tests for the SmartThings component.""" diff --git a/tests/components/smartthings/conftest.py b/tests/components/smartthings/conftest.py new file mode 100644 index 00000000000..56bb5a62888 --- /dev/null +++ b/tests/components/smartthings/conftest.py @@ -0,0 +1,279 @@ +"""Test configuration and mocks for the SmartThings component.""" +from collections import defaultdict +from unittest.mock import Mock, patch +from uuid import uuid4 + +from pysmartthings import ( + CLASSIFICATION_AUTOMATION, AppEntity, AppSettings, DeviceEntity, + InstalledApp, Location) +from pysmartthings.api import Api +import pytest + +from homeassistant.components import webhook +from homeassistant.components.smartthings.const import ( + APP_NAME_PREFIX, CONF_APP_ID, CONF_INSTALLED_APP_ID, CONF_INSTANCE_ID, + CONF_LOCATION_ID, DOMAIN, SETTINGS_INSTANCE_ID, STORAGE_KEY, + STORAGE_VERSION) +from homeassistant.config_entries import ( + CONN_CLASS_CLOUD_PUSH, SOURCE_USER, ConfigEntry) +from homeassistant.const import CONF_ACCESS_TOKEN, CONF_WEBHOOK_ID +from homeassistant.setup import async_setup_component + +from tests.common import mock_coro + + +@pytest.fixture(autouse=True) +async def setup_component(hass, config_file, hass_storage): + """Load the SmartThing component.""" + hass_storage[STORAGE_KEY] = {'data': config_file, + "version": STORAGE_VERSION} + await async_setup_component(hass, 'smartthings', {}) + hass.config.api.base_url = 'https://test.local' + + +def _create_location(): + loc = Location() + loc.apply_data({ + 'name': 'Test Location', + 'locationId': str(uuid4()) + }) + return loc + + +@pytest.fixture(name='location') +def location_fixture(): + """Fixture for a single location.""" + return _create_location() + + +@pytest.fixture(name='locations') +def locations_fixture(location): + """Fixture for 2 locations.""" + return [location, _create_location()] + + +@pytest.fixture(name="app") +def app_fixture(hass, config_file): + """Fixture for a single app.""" + app = AppEntity(Mock()) + app.apply_data({ + 'appName': APP_NAME_PREFIX + str(uuid4()), + 'appId': str(uuid4()), + 'appType': 'WEBHOOK_SMART_APP', + 'classifications': [CLASSIFICATION_AUTOMATION], + 'displayName': 'Home Assistant', + 'description': "Home Assistant at " + hass.config.api.base_url, + 'singleInstance': True, + 'webhookSmartApp': { + 'targetUrl': webhook.async_generate_url( + hass, hass.data[DOMAIN][CONF_WEBHOOK_ID]), + 'publicKey': ''} + }) + app.refresh = Mock() + app.refresh.return_value = mock_coro() + app.save = Mock() + app.save.return_value = mock_coro() + settings = AppSettings(app.app_id) + settings.settings[SETTINGS_INSTANCE_ID] = config_file[CONF_INSTANCE_ID] + app.settings = Mock() + app.settings.return_value = mock_coro(return_value=settings) + return app + + +@pytest.fixture(name='app_settings') +def app_settings_fixture(app, config_file): + """Fixture for an app settings.""" + settings = AppSettings(app.app_id) + settings.settings[SETTINGS_INSTANCE_ID] = config_file[CONF_INSTANCE_ID] + return settings + + +def _create_installed_app(location_id, app_id): + item = InstalledApp() + item.apply_data(defaultdict(str, { + 'installedAppId': str(uuid4()), + 'installedAppStatus': 'AUTHORIZED', + 'installedAppType': 'UNKNOWN', + 'appId': app_id, + 'locationId': location_id + })) + return item + + +@pytest.fixture(name='installed_app') +def installed_app_fixture(location, app): + """Fixture for a single installed app.""" + return _create_installed_app(location.location_id, app.app_id) + + +@pytest.fixture(name='installed_apps') +def installed_apps_fixture(installed_app, locations, app): + """Fixture for 2 installed apps.""" + return [installed_app, + _create_installed_app(locations[1].location_id, app.app_id)] + + +@pytest.fixture(name='config_file') +def config_file_fixture(): + """Fixture representing the local config file contents.""" + return { + CONF_INSTANCE_ID: str(uuid4()), + CONF_WEBHOOK_ID: webhook.generate_secret() + } + + +@pytest.fixture(name='smartthings_mock') +def smartthings_mock_fixture(locations): + """Fixture to mock smartthings API calls.""" + def _location(location_id): + return mock_coro( + return_value=next(location for location in locations + if location.location_id == location_id)) + + with patch("pysmartthings.SmartThings", autospec=True) as mock: + mock.return_value.location.side_effect = _location + yield mock + + +@pytest.fixture(name='device') +def device_fixture(location): + """Fixture representing devices loaded.""" + item = DeviceEntity(None) + item.status.refresh = Mock() + item.status.refresh.return_value = mock_coro() + item.apply_data({ + "deviceId": "743de49f-036f-4e9c-839a-2f89d57607db", + "name": "GE In-Wall Smart Dimmer", + "label": "Front Porch Lights", + "deviceManufacturerCode": "0063-4944-3038", + "locationId": location.location_id, + "deviceTypeId": "8a9d4b1e3b9b1fe3013b9b206a7f000d", + "deviceTypeName": "Dimmer Switch", + "deviceNetworkType": "ZWAVE", + "components": [ + { + "id": "main", + "capabilities": [ + { + "id": "switch", + "version": 1 + }, + { + "id": "switchLevel", + "version": 1 + }, + { + "id": "refresh", + "version": 1 + }, + { + "id": "indicator", + "version": 1 + }, + { + "id": "sensor", + "version": 1 + }, + { + "id": "actuator", + "version": 1 + }, + { + "id": "healthCheck", + "version": 1 + }, + { + "id": "light", + "version": 1 + } + ] + } + ], + "dth": { + "deviceTypeId": "8a9d4b1e3b9b1fe3013b9b206a7f000d", + "deviceTypeName": "Dimmer Switch", + "deviceNetworkType": "ZWAVE", + "completedSetup": False + }, + "type": "DTH" + }) + return item + + +@pytest.fixture(name='config_entry') +def config_entry_fixture(hass, installed_app, location): + """Fixture representing a config entry.""" + data = { + CONF_ACCESS_TOKEN: str(uuid4()), + CONF_INSTALLED_APP_ID: installed_app.installed_app_id, + CONF_APP_ID: installed_app.app_id, + CONF_LOCATION_ID: location.location_id + } + return ConfigEntry("1", DOMAIN, location.name, data, SOURCE_USER, + CONN_CLASS_CLOUD_PUSH) + + +@pytest.fixture(name="device_factory") +def device_factory_fixture(): + """Fixture for creating mock devices.""" + api = Mock(spec=Api) + api.post_device_command.return_value = mock_coro(return_value={}) + + def _factory(label, capabilities, status: dict = None): + device_data = { + "deviceId": str(uuid4()), + "name": "Device Type Handler Name", + "label": label, + "deviceManufacturerCode": "9135fc86-0929-4436-bf73-5d75f523d9db", + "locationId": "fcd829e9-82f4-45b9-acfd-62fda029af80", + "components": [ + { + "id": "main", + "capabilities": [ + {"id": capability, "version": 1} + for capability in capabilities + ] + } + ], + "dth": { + "deviceTypeId": "b678b29d-2726-4e4f-9c3f-7aa05bd08964", + "deviceTypeName": "Switch", + "deviceNetworkType": "ZWAVE" + }, + "type": "DTH" + } + device = DeviceEntity(api, data=device_data) + if status: + for attribute, value in status.items(): + device.status.apply_attribute_update( + 'main', '', attribute, value) + return device + return _factory + + +@pytest.fixture(name="event_factory") +def event_factory_fixture(): + """Fixture for creating mock devices.""" + def _factory(device_id, event_type="DEVICE_EVENT"): + event = Mock() + event.event_type = event_type + event.device_id = device_id + event.component_id = 'main' + event.capability = '' + event.attribute = 'Updated' + event.value = 'Value' + return event + return _factory + + +@pytest.fixture(name="event_request_factory") +def event_request_factory_fixture(event_factory): + """Fixture for creating mock smartapp event requests.""" + def _factory(device_ids): + request = Mock() + request.installed_app_id = uuid4() + request.events = [event_factory(id) for id in device_ids] + request.events.append(event_factory(uuid4())) + request.events.append(event_factory(device_ids[0], event_type="OTHER")) + return request + return _factory diff --git a/tests/components/smartthings/test_config_flow.py b/tests/components/smartthings/test_config_flow.py new file mode 100644 index 00000000000..4d2a43a52c7 --- /dev/null +++ b/tests/components/smartthings/test_config_flow.py @@ -0,0 +1,245 @@ +"""Tests for the SmartThings config flow module.""" +from unittest.mock import patch +from uuid import uuid4 + +from aiohttp.client_exceptions import ClientResponseError + +from homeassistant import data_entry_flow +from homeassistant.components.smartthings.config_flow import ( + SmartThingsFlowHandler) +from homeassistant.config_entries import ConfigEntry + +from tests.common import mock_coro + + +async def test_step_user(hass): + """Test the access token form is shown for a user initiated flow.""" + flow = SmartThingsFlowHandler() + flow.hass = hass + result = await flow.async_step_user() + + assert result['type'] == data_entry_flow.RESULT_TYPE_FORM + assert result['step_id'] == 'user' + + +async def test_step_init(hass): + """Test the access token form is shown for an init flow.""" + flow = SmartThingsFlowHandler() + flow.hass = hass + result = await flow.async_step_import() + + assert result['type'] == data_entry_flow.RESULT_TYPE_FORM + assert result['step_id'] == 'user' + + +async def test_base_url_not_https(hass): + """Test the base_url parameter starts with https://.""" + hass.config.api.base_url = 'http://0.0.0.0' + flow = SmartThingsFlowHandler() + flow.hass = hass + result = await flow.async_step_import() + + assert result['type'] == data_entry_flow.RESULT_TYPE_FORM + assert result['step_id'] == 'user' + assert result['errors'] == {'base': 'base_url_not_https'} + + +async def test_invalid_token_format(hass): + """Test an error is shown for invalid token formats.""" + flow = SmartThingsFlowHandler() + flow.hass = hass + result = await flow.async_step_user({'access_token': '123456789'}) + + assert result['type'] == data_entry_flow.RESULT_TYPE_FORM + assert result['step_id'] == 'user' + assert result['errors'] == {'access_token': 'token_invalid_format'} + + +async def test_token_already_setup(hass): + """Test an error is shown when the token is already setup.""" + flow = SmartThingsFlowHandler() + flow.hass = hass + token = str(uuid4()) + entries = [ConfigEntry( + version='', domain='', title='', data={'access_token': token}, + source='', connection_class='')] + + with patch.object(hass.config_entries, 'async_entries', + return_value=entries): + result = await flow.async_step_user({'access_token': token}) + + assert result['type'] == data_entry_flow.RESULT_TYPE_FORM + assert result['step_id'] == 'user' + assert result['errors'] == {'access_token': 'token_already_setup'} + + +async def test_token_unauthorized(hass, smartthings_mock): + """Test an error is shown when the token is not authorized.""" + flow = SmartThingsFlowHandler() + flow.hass = hass + + smartthings_mock.return_value.apps.return_value = mock_coro( + exception=ClientResponseError(None, None, status=401)) + + result = await flow.async_step_user({'access_token': str(uuid4())}) + + assert result['type'] == data_entry_flow.RESULT_TYPE_FORM + assert result['step_id'] == 'user' + assert result['errors'] == {'access_token': 'token_unauthorized'} + + +async def test_token_forbidden(hass, smartthings_mock): + """Test an error is shown when the token is forbidden.""" + flow = SmartThingsFlowHandler() + flow.hass = hass + + smartthings_mock.return_value.apps.return_value = mock_coro( + exception=ClientResponseError(None, None, status=403)) + + result = await flow.async_step_user({'access_token': str(uuid4())}) + + assert result['type'] == data_entry_flow.RESULT_TYPE_FORM + assert result['step_id'] == 'user' + assert result['errors'] == {'access_token': 'token_forbidden'} + + +async def test_unknown_api_error(hass, smartthings_mock): + """Test an error is shown when there is an unknown API error.""" + flow = SmartThingsFlowHandler() + flow.hass = hass + + smartthings_mock.return_value.apps.return_value = mock_coro( + exception=ClientResponseError(None, None, status=500)) + + result = await flow.async_step_user({'access_token': str(uuid4())}) + + assert result['type'] == data_entry_flow.RESULT_TYPE_FORM + assert result['step_id'] == 'user' + assert result['errors'] == {'base': 'app_setup_error'} + + +async def test_unknown_error(hass, smartthings_mock): + """Test an error is shown when there is an unknown API error.""" + flow = SmartThingsFlowHandler() + flow.hass = hass + + smartthings_mock.return_value.apps.return_value = mock_coro( + exception=Exception('Unknown error')) + + result = await flow.async_step_user({'access_token': str(uuid4())}) + + assert result['type'] == data_entry_flow.RESULT_TYPE_FORM + assert result['step_id'] == 'user' + assert result['errors'] == {'base': 'app_setup_error'} + + +async def test_app_created_then_show_wait_form(hass, app, smartthings_mock): + """Test SmartApp is created when one does not exist and shows wait form.""" + flow = SmartThingsFlowHandler() + flow.hass = hass + + smartthings = smartthings_mock.return_value + smartthings.apps.return_value = mock_coro(return_value=[]) + smartthings.create_app.return_value = mock_coro(return_value=(app, None)) + smartthings.update_app_settings.return_value = mock_coro() + smartthings.update_app_oauth.return_value = mock_coro() + + result = await flow.async_step_user({'access_token': str(uuid4())}) + + assert result['type'] == data_entry_flow.RESULT_TYPE_FORM + assert result['step_id'] == 'wait_install' + + +async def test_app_updated_then_show_wait_form( + hass, app, smartthings_mock): + """Test SmartApp is updated when an existing is already created.""" + flow = SmartThingsFlowHandler() + flow.hass = hass + + api = smartthings_mock.return_value + api.apps.return_value = mock_coro(return_value=[app]) + + result = await flow.async_step_user({'access_token': str(uuid4())}) + + assert result['type'] == data_entry_flow.RESULT_TYPE_FORM + assert result['step_id'] == 'wait_install' + + +async def test_wait_form_displayed(hass): + """Test the wait for installation form is displayed.""" + flow = SmartThingsFlowHandler() + flow.hass = hass + + result = await flow.async_step_wait_install(None) + + assert result['type'] == data_entry_flow.RESULT_TYPE_FORM + assert result['step_id'] == 'wait_install' + + +async def test_wait_form_displayed_after_checking(hass, smartthings_mock): + """Test error is shown when the user has not installed the app.""" + flow = SmartThingsFlowHandler() + flow.hass = hass + flow.access_token = str(uuid4()) + flow.api = smartthings_mock.return_value + flow.api.installed_apps.return_value = mock_coro(return_value=[]) + + result = await flow.async_step_wait_install({}) + + assert result['type'] == data_entry_flow.RESULT_TYPE_FORM + assert result['step_id'] == 'wait_install' + assert result['errors'] == {'base': 'app_not_installed'} + + +async def test_config_entry_created_when_installed( + hass, location, installed_app, smartthings_mock): + """Test a config entry is created once the app is installed.""" + flow = SmartThingsFlowHandler() + flow.hass = hass + flow.access_token = str(uuid4()) + flow.api = smartthings_mock.return_value + flow.app_id = installed_app.app_id + flow.api.installed_apps.return_value = \ + mock_coro(return_value=[installed_app]) + + result = await flow.async_step_wait_install({}) + + assert result['type'] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result['data']['app_id'] == installed_app.app_id + assert result['data']['installed_app_id'] == \ + installed_app.installed_app_id + assert result['data']['location_id'] == installed_app.location_id + assert result['data']['access_token'] == flow.access_token + assert result['title'] == location.name + + +async def test_multiple_config_entry_created_when_installed( + hass, app, locations, installed_apps, smartthings_mock): + """Test a config entries are created for multiple installs.""" + flow = SmartThingsFlowHandler() + flow.hass = hass + flow.access_token = str(uuid4()) + flow.app_id = app.app_id + flow.api = smartthings_mock.return_value + flow.api.installed_apps.return_value = \ + mock_coro(return_value=installed_apps) + + result = await flow.async_step_wait_install({}) + + assert result['type'] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result['data']['app_id'] == installed_apps[0].app_id + assert result['data']['installed_app_id'] == \ + installed_apps[0].installed_app_id + assert result['data']['location_id'] == installed_apps[0].location_id + assert result['data']['access_token'] == flow.access_token + assert result['title'] == locations[0].name + + await hass.async_block_till_done() + entries = hass.config_entries.async_entries('smartthings') + assert len(entries) == 1 + assert entries[0].data['app_id'] == installed_apps[1].app_id + assert entries[0].data['installed_app_id'] == \ + installed_apps[1].installed_app_id + assert entries[0].data['location_id'] == installed_apps[1].location_id + assert entries[0].data['access_token'] == flow.access_token + assert entries[0].title == locations[1].name diff --git a/tests/components/smartthings/test_init.py b/tests/components/smartthings/test_init.py new file mode 100644 index 00000000000..d20d2d4e047 --- /dev/null +++ b/tests/components/smartthings/test_init.py @@ -0,0 +1,183 @@ +"""Tests for the SmartThings component init module.""" +from unittest.mock import Mock, patch +from uuid import uuid4 + +from aiohttp import ClientConnectionError, ClientResponseError +from pysmartthings import InstalledAppStatus +import pytest + +from homeassistant.components import smartthings +from homeassistant.components.smartthings.const import ( + DATA_BROKERS, DOMAIN, SIGNAL_SMARTTHINGS_UPDATE, SUPPORTED_PLATFORMS) +from homeassistant.exceptions import ConfigEntryNotReady +from homeassistant.helpers.dispatcher import async_dispatcher_connect + +from tests.common import mock_coro + + +async def test_unrecoverable_api_errors_create_new_flow( + hass, config_entry, smartthings_mock): + """ + Test a new config flow is initiated when there are API errors. + + 401 (unauthorized): Occurs when the access token is no longer valid. + 403 (forbidden/not found): Occurs when the app or installed app could + not be retrieved/found (likely deleted?) + """ + api = smartthings_mock.return_value + for error_status in (401, 403): + setattr(hass.config_entries, '_entries', [config_entry]) + api.app.return_value = mock_coro( + exception=ClientResponseError(None, None, + status=error_status)) + + # Assert setup returns false + result = await smartthings.async_setup_entry(hass, config_entry) + assert not result + + # Assert entry was removed and new flow created + await hass.async_block_till_done() + assert not hass.config_entries.async_entries(DOMAIN) + flows = hass.config_entries.flow.async_progress() + assert len(flows) == 1 + assert flows[0]['handler'] == 'smartthings' + assert flows[0]['context'] == {'source': 'import'} + hass.config_entries.flow.async_abort(flows[0]['flow_id']) + + +async def test_recoverable_api_errors_raise_not_ready( + hass, config_entry, smartthings_mock): + """Test config entry not ready raised for recoverable API errors.""" + setattr(hass.config_entries, '_entries', [config_entry]) + api = smartthings_mock.return_value + api.app.return_value = mock_coro( + exception=ClientResponseError(None, None, status=500)) + + with pytest.raises(ConfigEntryNotReady): + await smartthings.async_setup_entry(hass, config_entry) + + +async def test_connection_errors_raise_not_ready( + hass, config_entry, smartthings_mock): + """Test config entry not ready raised for connection errors.""" + setattr(hass.config_entries, '_entries', [config_entry]) + api = smartthings_mock.return_value + api.app.return_value = mock_coro( + exception=ClientConnectionError()) + + with pytest.raises(ConfigEntryNotReady): + await smartthings.async_setup_entry(hass, config_entry) + + +async def test_base_url_no_longer_https_does_not_load( + hass, config_entry, app, smartthings_mock): + """Test base_url no longer valid creates a new flow.""" + hass.config.api.base_url = 'http://0.0.0.0' + setattr(hass.config_entries, '_entries', [config_entry]) + api = smartthings_mock.return_value + api.app.return_value = mock_coro(return_value=app) + + # Assert setup returns false + result = await smartthings.async_setup_entry(hass, config_entry) + assert not result + + +async def test_unauthorized_installed_app_raises_not_ready( + hass, config_entry, app, installed_app, + smartthings_mock): + """Test config entry not ready raised when the app isn't authorized.""" + setattr(hass.config_entries, '_entries', [config_entry]) + setattr(installed_app, '_installed_app_status', + InstalledAppStatus.PENDING) + + api = smartthings_mock.return_value + api.app.return_value = mock_coro(return_value=app) + api.installed_app.return_value = mock_coro(return_value=installed_app) + + with pytest.raises(ConfigEntryNotReady): + await smartthings.async_setup_entry(hass, config_entry) + + +async def test_config_entry_loads_platforms( + hass, config_entry, app, installed_app, + device, smartthings_mock): + """Test config entry loads properly and proxies to platforms.""" + setattr(hass.config_entries, '_entries', [config_entry]) + + api = smartthings_mock.return_value + api.app.return_value = mock_coro(return_value=app) + api.installed_app.return_value = mock_coro(return_value=installed_app) + api.devices.return_value = mock_coro(return_value=[device]) + + with patch.object(hass.config_entries, 'async_forward_entry_setup', + return_value=mock_coro()) as forward_mock: + assert await smartthings.async_setup_entry(hass, config_entry) + # Assert platforms loaded + await hass.async_block_till_done() + assert forward_mock.call_count == len(SUPPORTED_PLATFORMS) + + +async def test_unload_entry(hass, config_entry): + """Test entries are unloaded correctly.""" + broker = Mock() + broker.event_handler_disconnect = Mock() + hass.data[DOMAIN][DATA_BROKERS][config_entry.entry_id] = broker + + with patch.object(hass.config_entries, 'async_forward_entry_unload', + return_value=mock_coro( + return_value=True + )) as forward_mock: + assert await smartthings.async_unload_entry(hass, config_entry) + assert broker.event_handler_disconnect.call_count == 1 + assert config_entry.entry_id not in hass.data[DOMAIN][DATA_BROKERS] + # Assert platforms unloaded + await hass.async_block_till_done() + assert forward_mock.call_count == len(SUPPORTED_PLATFORMS) + + +async def test_event_handler_dispatches_updated_devices( + hass, device_factory, event_request_factory): + """Test the event handler dispatches updated devices.""" + devices = [ + device_factory('Bedroom 1 Switch', ['switch']), + device_factory('Bathroom 1', ['switch']), + device_factory('Sensor', ['motionSensor']), + ] + device_ids = [devices[0].device_id, devices[1].device_id, + devices[2].device_id] + request = event_request_factory(device_ids) + called = False + + def signal(ids): + nonlocal called + called = True + assert device_ids == ids + async_dispatcher_connect(hass, SIGNAL_SMARTTHINGS_UPDATE, signal) + broker = smartthings.DeviceBroker( + hass, devices, request.installed_app_id) + + await broker.event_handler(request, None, None) + await hass.async_block_till_done() + + assert called + for device in devices: + assert device.status.attributes['Updated'] == 'Value' + + +async def test_event_handler_ignores_other_installed_app( + hass, device_factory, event_request_factory): + """Test the event handler dispatches updated devices.""" + device = device_factory('Bedroom 1 Switch', ['switch']) + request = event_request_factory([device.device_id]) + called = False + + def signal(ids): + nonlocal called + called = True + async_dispatcher_connect(hass, SIGNAL_SMARTTHINGS_UPDATE, signal) + broker = smartthings.DeviceBroker(hass, [device], str(uuid4())) + + await broker.event_handler(request, None, None) + await hass.async_block_till_done() + + assert not called diff --git a/tests/components/smartthings/test_smartapp.py b/tests/components/smartthings/test_smartapp.py new file mode 100644 index 00000000000..0f517222c4a --- /dev/null +++ b/tests/components/smartthings/test_smartapp.py @@ -0,0 +1,112 @@ +"""Tests for the smartapp module.""" +from unittest.mock import Mock, patch +from uuid import uuid4 + +from pysmartthings import AppEntity + +from homeassistant.components.smartthings import smartapp +from homeassistant.components.smartthings.const import ( + DATA_MANAGER, DOMAIN, SUPPORTED_CAPABILITIES) + +from tests.common import mock_coro + + +async def test_update_app(hass, app): + """Test update_app does not save if app is current.""" + await smartapp.update_app(hass, app) + assert app.save.call_count == 0 + + +async def test_update_app_updated_needed(hass, app): + """Test update_app updates when an app is needed.""" + mock_app = Mock(spec=AppEntity) + mock_app.app_name = 'Test' + mock_app.refresh.return_value = mock_coro() + mock_app.save.return_value = mock_coro() + + await smartapp.update_app(hass, mock_app) + + assert mock_app.save.call_count == 1 + assert mock_app.app_name == 'Test' + assert mock_app.display_name == app.display_name + assert mock_app.description == app.description + assert mock_app.webhook_target_url == app.webhook_target_url + assert mock_app.app_type == app.app_type + assert mock_app.single_instance == app.single_instance + assert mock_app.classifications == app.classifications + + +async def test_smartapp_install_abort_if_no_other(hass, smartthings_mock): + """Test aborts if no other app was configured already.""" + api = smartthings_mock.return_value + api.create_subscription.return_value = mock_coro() + app = Mock() + app.app_id = uuid4() + request = Mock() + request.installed_app_id = uuid4() + request.auth_token = uuid4() + request.location_id = uuid4() + + await smartapp.smartapp_install(hass, request, None, app) + + entries = hass.config_entries.async_entries('smartthings') + assert not entries + assert api.create_subscription.call_count == \ + len(SUPPORTED_CAPABILITIES) + + +async def test_smartapp_install_creates_flow( + hass, smartthings_mock, config_entry, location): + """Test installation creates flow.""" + # Arrange + setattr(hass.config_entries, '_entries', [config_entry]) + api = smartthings_mock.return_value + api.create_subscription.return_value = mock_coro() + app = Mock() + app.app_id = config_entry.data['app_id'] + request = Mock() + request.installed_app_id = str(uuid4()) + request.auth_token = str(uuid4()) + request.location_id = location.location_id + # Act + await smartapp.smartapp_install(hass, request, None, app) + # Assert + await hass.async_block_till_done() + entries = hass.config_entries.async_entries('smartthings') + assert len(entries) == 2 + assert api.create_subscription.call_count == \ + len(SUPPORTED_CAPABILITIES) + assert entries[1].data['app_id'] == app.app_id + assert entries[1].data['installed_app_id'] == request.installed_app_id + assert entries[1].data['location_id'] == request.location_id + assert entries[1].data['access_token'] == \ + config_entry.data['access_token'] + assert entries[1].title == location.name + + +async def test_smartapp_uninstall(hass, config_entry): + """Test the config entry is unloaded when the app is uninstalled.""" + setattr(hass.config_entries, '_entries', [config_entry]) + app = Mock() + app.app_id = config_entry.data['app_id'] + request = Mock() + request.installed_app_id = config_entry.data['installed_app_id'] + + with patch.object(hass.config_entries, 'async_remove', + return_value=mock_coro()) as remove: + await smartapp.smartapp_uninstall(hass, request, None, app) + assert remove.call_count == 1 + + +async def test_smartapp_webhook(hass): + """Test the smartapp webhook calls the manager.""" + manager = Mock() + manager.handle_request = Mock() + manager.handle_request.return_value = mock_coro(return_value={}) + hass.data[DOMAIN][DATA_MANAGER] = manager + request = Mock() + request.headers = [] + request.json.return_value = mock_coro(return_value={}) + result = await smartapp.smartapp_webhook(hass, '', request) + + assert result.body == b'{}' diff --git a/tests/components/smartthings/test_switch.py b/tests/components/smartthings/test_switch.py new file mode 100644 index 00000000000..7bf8b15af51 --- /dev/null +++ b/tests/components/smartthings/test_switch.py @@ -0,0 +1,135 @@ +""" +Test for the SmartThings switch platform. + +The only mocking required is of the underlying SmartThings API object so +real HTTP calls are not initiated during testing. +""" +from pysmartthings import Attribute, Capability + +from homeassistant.components.smartthings import DeviceBroker, switch +from homeassistant.components.smartthings.const import ( + DATA_BROKERS, DOMAIN, SIGNAL_SMARTTHINGS_UPDATE) +from homeassistant.config_entries import ( + CONN_CLASS_CLOUD_PUSH, SOURCE_USER, ConfigEntry) +from homeassistant.helpers.dispatcher import async_dispatcher_send + + +async def _setup_platform(hass, *devices): + """Set up the SmartThings switch platform and prerequisites.""" + hass.config.components.add(DOMAIN) + broker = DeviceBroker(hass, devices, '') + config_entry = ConfigEntry("1", DOMAIN, "Test", {}, + SOURCE_USER, CONN_CLASS_CLOUD_PUSH) + hass.data[DOMAIN] = { + DATA_BROKERS: { + config_entry.entry_id: broker + } + } + await hass.config_entries.async_forward_entry_setup(config_entry, 'switch') + await hass.async_block_till_done() + return config_entry + + +async def test_async_setup_platform(): + """Test setup platform does nothing (it uses config entries).""" + await switch.async_setup_platform(None, None, None) + + +def test_is_switch(device_factory): + """Test switches are correctly identified.""" + switch_device = device_factory('Switch', [Capability.switch]) + non_switch_devices = [ + device_factory('Light', [Capability.switch, Capability.switch_level]), + device_factory('Fan', [Capability.switch, Capability.fan_speed]), + device_factory('Color Light', [Capability.switch, + Capability.color_control]), + device_factory('Temp Light', [Capability.switch, + Capability.color_temperature]), + device_factory('Unknown', ['Unknown']), + ] + assert switch.is_switch(switch_device) + for non_switch_device in non_switch_devices: + assert not switch.is_switch(non_switch_device) + + +async def test_entity_and_device_attributes(hass, device_factory): + """Test the attributes of the entity are correct.""" + # Arrange + device = device_factory('Switch_1', [Capability.switch], + {Attribute.switch: 'on'}) + entity_registry = await hass.helpers.entity_registry.async_get_registry() + device_registry = await hass.helpers.device_registry.async_get_registry() + # Act + await _setup_platform(hass, device) + # Assert + entity = entity_registry.async_get('switch.switch_1') + assert entity + assert entity.unique_id == device.device_id + device_entry = device_registry.async_get_device( + {(DOMAIN, device.device_id)}, []) + assert device_entry + assert device_entry.name == device.label + assert device_entry.model == device.device_type_name + assert device_entry.manufacturer == 'Unavailable' + + +async def test_turn_off(hass, device_factory): + """Test the switch turns of successfully.""" + # Arrange + device = device_factory('Switch_1', [Capability.switch], + {Attribute.switch: 'on'}) + await _setup_platform(hass, device) + # Act + await hass.services.async_call( + 'switch', 'turn_off', {'entity_id': 'switch.switch_1'}, + blocking=True) + # Assert + state = hass.states.get('switch.switch_1') + assert state is not None + assert state.state == 'off' + + +async def test_turn_on(hass, device_factory): + """Test the switch turns of successfully.""" + # Arrange + device = device_factory('Switch_1', [Capability.switch], + {Attribute.switch: 'off'}) + await _setup_platform(hass, device) + # Act + await hass.services.async_call( + 'switch', 'turn_on', {'entity_id': 'switch.switch_1'}, + blocking=True) + # Assert + state = hass.states.get('switch.switch_1') + assert state is not None + assert state.state == 'on' + + +async def test_update_from_signal(hass, device_factory): + """Test the switch updates when receiving a signal.""" + # Arrange + device = device_factory('Switch_1', [Capability.switch], + {Attribute.switch: 'off'}) + await _setup_platform(hass, device) + await device.switch_on(True) + # Act + async_dispatcher_send(hass, SIGNAL_SMARTTHINGS_UPDATE, + [device.device_id]) + # Assert + await hass.async_block_till_done() + state = hass.states.get('switch.switch_1') + assert state is not None + assert state.state == 'on' + + +async def test_unload_config_entry(hass, device_factory): + """Test the switch is removed when the config entry is unloaded.""" + # Arrange + device = device_factory('Switch', [Capability.switch], + {Attribute.switch: 'on'}) + config_entry = await _setup_platform(hass, device) + # Act + await hass.config_entries.async_forward_entry_unload( + config_entry, 'switch') + # Assert + assert not hass.states.get('switch.switch_1') From c7ff8d4996d4a7e915a9d43c3a0f7a1c1ed77678 Mon Sep 17 00:00:00 2001 From: Diogo Gomes Date: Thu, 31 Jan 2019 01:33:52 +0000 Subject: [PATCH 184/222] fix #20571 (#20589) --- homeassistant/components/sensor/history_stats.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/homeassistant/components/sensor/history_stats.py b/homeassistant/components/sensor/history_stats.py index a8d9276edc8..f5b76c89aba 100644 --- a/homeassistant/components/sensor/history_stats.py +++ b/homeassistant/components/sensor/history_stats.py @@ -282,6 +282,13 @@ class HistoryStatsSensor(Entity): if end is None: end = start + self._duration + if start > dt_util.now(): + # History hasn't been written yet for this period + return + if dt_util.now() < end: + # No point in making stats of the future + end = dt_util.now() + self._period = start, end From 8db215223024005fceedb078f801ca35305a5505 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 30 Jan 2019 17:37:56 -0800 Subject: [PATCH 185/222] Bumped version to 0.87.0b0 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 8701c682920..48253c2eb6e 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -2,7 +2,7 @@ """Constants used by Home Assistant components.""" MAJOR_VERSION = 0 MINOR_VERSION = 87 -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 0400e29f7a887be1e191f0c727f20eaaf3be6b3f Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 1 Feb 2019 12:50:48 -0800 Subject: [PATCH 186/222] Updated frontend to 20190201.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 5613bdff3ff..8f3afef16cd 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==20190130.1'] +REQUIREMENTS = ['home-assistant-frontend==20190201.0'] DOMAIN = 'frontend' DEPENDENCIES = ['api', 'websocket_api', 'http', 'system_log', diff --git a/requirements_all.txt b/requirements_all.txt index de8ad8c7914..4605eb7b870 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -526,7 +526,7 @@ hole==0.3.0 holidays==0.9.9 # homeassistant.components.frontend -home-assistant-frontend==20190130.1 +home-assistant-frontend==20190201.0 # homeassistant.components.zwave homeassistant-pyozw==0.1.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index a56626734eb..ec2c9153137 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -113,7 +113,7 @@ hdate==0.8.7 holidays==0.9.9 # homeassistant.components.frontend -home-assistant-frontend==20190130.1 +home-assistant-frontend==20190201.0 # homeassistant.components.homekit_controller homekit==0.12.2 From 557b745053a086f73643801708be6f6b0735be00 Mon Sep 17 00:00:00 2001 From: emkay82 <37954256+emkay82@users.noreply.github.com> Date: Fri, 1 Feb 2019 14:53:40 +0100 Subject: [PATCH 187/222] Fix pjlink issue (#20510) * Fix issue #16606 Some projectors do not respond to pjlink requests during a short period after the status changes or when its in standby, resulting in pypjlink2 throwing an error. This fix catches these errors. Furthermore, only the status 'on' and 'warm-up' is interpreted as switched on, because 'cooling' is actually a switched off status. * Update pjlink.py Improved error handling * Update pjlink.py Improved error handling * Update pjlink.py Clean up --- .../components/media_player/pjlink.py | 32 ++++++++++++++----- 1 file changed, 24 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/media_player/pjlink.py b/homeassistant/components/media_player/pjlink.py index 168cde4a792..0609b75f98d 100644 --- a/homeassistant/components/media_player/pjlink.py +++ b/homeassistant/components/media_player/pjlink.py @@ -93,15 +93,31 @@ class PjLinkDevice(MediaPlayerDevice): def update(self): """Get the latest state from the device.""" + from pypjlink.projector import ProjectorError with self.projector() as projector: - pwstate = projector.get_power() - if pwstate == 'off': - self._pwstate = STATE_OFF - else: - self._pwstate = STATE_ON - self._muted = projector.get_mute()[1] - self._current_source = \ - format_input_source(*projector.get_input()) + try: + pwstate = projector.get_power() + if pwstate in ('on', 'warm-up'): + self._pwstate = STATE_ON + else: + self._pwstate = STATE_OFF + self._muted = projector.get_mute()[1] + self._current_source = \ + format_input_source(*projector.get_input()) + except KeyError as err: + if str(err) == "'OK'": + self._pwstate = STATE_OFF + self._muted = False + self._current_source = None + else: + raise + except ProjectorError as err: + if str(err) == 'unavailable time': + self._pwstate = STATE_OFF + self._muted = False + self._current_source = None + else: + raise @property def name(self): From ca729b178bd893dbf4d8605c494cb2482e6c7322 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 1 Feb 2019 14:09:23 -0800 Subject: [PATCH 188/222] Fix geofency requiring a configuration.yaml entry (#20631) --- homeassistant/components/geofency/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/geofency/__init__.py b/homeassistant/components/geofency/__init__.py index 239af14add8..f58580b83c7 100644 --- a/homeassistant/components/geofency/__init__.py +++ b/homeassistant/components/geofency/__init__.py @@ -68,8 +68,8 @@ WEBHOOK_SCHEMA = vol.Schema({ async def async_setup(hass, hass_config): """Set up the Geofency component.""" - config = hass_config[DOMAIN] - mobile_beacons = config[CONF_MOBILE_BEACONS] + config = hass_config.get(DOMAIN, {}) + mobile_beacons = config.get(CONF_MOBILE_BEACONS, []) hass.data[DOMAIN] = [slugify(beacon) for beacon in mobile_beacons] return True From 1a5028f56f91740a7f14e7522c2ecfdb6045926d Mon Sep 17 00:00:00 2001 From: Kevin Fronczak Date: Fri, 1 Feb 2019 00:52:30 -0800 Subject: [PATCH 189/222] Upgrade blinkpy to re-enable motion detection (#20651) --- homeassistant/components/blink/__init__.py | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/blink/__init__.py b/homeassistant/components/blink/__init__.py index 57500fcc8a6..82815d11a6e 100644 --- a/homeassistant/components/blink/__init__.py +++ b/homeassistant/components/blink/__init__.py @@ -15,7 +15,7 @@ from homeassistant.const import ( CONF_BINARY_SENSORS, CONF_SENSORS, CONF_FILENAME, CONF_MONITORED_CONDITIONS, TEMP_FAHRENHEIT) -REQUIREMENTS = ['blinkpy==0.11.2'] +REQUIREMENTS = ['blinkpy==0.12.1'] _LOGGER = logging.getLogger(__name__) diff --git a/requirements_all.txt b/requirements_all.txt index 4605eb7b870..22bed82f26b 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -199,7 +199,7 @@ bellows==0.7.0 bimmer_connected==0.5.3 # homeassistant.components.blink -blinkpy==0.11.2 +blinkpy==0.12.1 # homeassistant.components.light.blinksticklight blinkstick==1.1.8 From 47660f9312f8c4307f3f77144c89ebcf8992ba37 Mon Sep 17 00:00:00 2001 From: zewelor Date: Fri, 1 Feb 2019 10:59:05 +0100 Subject: [PATCH 190/222] Fix parsing yeelight custom effects, when not present in config (#20658) --- homeassistant/components/light/yeelight.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/light/yeelight.py b/homeassistant/components/light/yeelight.py index 249f542325f..b678fcd2799 100644 --- a/homeassistant/components/light/yeelight.py +++ b/homeassistant/components/light/yeelight.py @@ -193,8 +193,14 @@ def setup_platform(hass, config, add_entities, discovery_info=None): name = device_config[CONF_NAME] _LOGGER.debug("Adding configured %s", name) - custom_effects = _parse_custom_effects(config[CONF_CUSTOM_EFFECTS]) device = {'name': name, 'ipaddr': ipaddr} + + if CONF_CUSTOM_EFFECTS in config: + custom_effects = \ + _parse_custom_effects(config[CONF_CUSTOM_EFFECTS]) + else: + custom_effects = None + light = YeelightLight(device, device_config, custom_effects=custom_effects) lights.append(light) From c702e1e3c648c7c9d376e66e691a96f5ca89ec78 Mon Sep 17 00:00:00 2001 From: emontnemery Date: Fri, 1 Feb 2019 17:14:02 +0100 Subject: [PATCH 191/222] Add PLATFORM_SCHEMA_BASE support to check_config.py (#20663) --- homeassistant/scripts/check_config.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/homeassistant/scripts/check_config.py b/homeassistant/scripts/check_config.py index 50463c28bd1..67bc97da992 100644 --- a/homeassistant/scripts/check_config.py +++ b/homeassistant/scripts/check_config.py @@ -345,14 +345,21 @@ def check_ha_config_file(hass): _comp_error(ex, domain, config) continue - if not hasattr(component, 'PLATFORM_SCHEMA'): + if (not hasattr(component, 'PLATFORM_SCHEMA') and + not hasattr(component, 'PLATFORM_SCHEMA_BASE')): continue platforms = [] for p_name, p_config in config_per_platform(config, domain): # Validate component specific platform schema try: - p_validated = component.PLATFORM_SCHEMA(p_config) + if hasattr(component, 'PLATFORM_SCHEMA_BASE'): + p_validated = \ + component.PLATFORM_SCHEMA_BASE( # type: ignore + p_config) + else: + p_validated = component.PLATFORM_SCHEMA( # type: ignore + p_config) except vol.Invalid as ex: _comp_error(ex, domain, config) continue From e4d76d5c4428715753fd1c127506e9aec0c2e889 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oliver=20V=C3=B6lker?= Date: Fri, 1 Feb 2019 18:35:49 +0100 Subject: [PATCH 192/222] InfluxDB - change connection test method (#20666) --- homeassistant/components/sensor/influxdb.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/sensor/influxdb.py b/homeassistant/components/sensor/influxdb.py index 9f34580344c..35229c2a805 100644 --- a/homeassistant/components/sensor/influxdb.py +++ b/homeassistant/components/sensor/influxdb.py @@ -111,7 +111,7 @@ class InfluxSensor(Entity): database=database, ssl=influx_conf['ssl'], verify_ssl=influx_conf['verify_ssl']) try: - influx.query("SHOW DIAGNOSTICS;") + influx.query("SHOW SERIES LIMIT 1;") self.connected = True self.data = InfluxSensorData( influx, query.get(CONF_GROUP_FUNCTION), query.get(CONF_FIELD), From 224c2588762e067a6621486a2fa080012508906a Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 1 Feb 2019 14:35:23 -0800 Subject: [PATCH 193/222] Bumped version to 0.87.0b1 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 48253c2eb6e..5204d17f580 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -2,7 +2,7 @@ """Constants used by Home Assistant components.""" MAJOR_VERSION = 0 MINOR_VERSION = 87 -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 61b2f1bff0d79fd081f52935758003abc8d54f54 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sat, 2 Feb 2019 14:02:50 -0800 Subject: [PATCH 194/222] Update translations --- .../ambient_station/.translations/ca.json | 19 ++++++++ .../ambient_station/.translations/ko.json | 19 ++++++++ .../ambient_station/.translations/lb.json | 19 ++++++++ .../ambient_station/.translations/ru.json | 19 ++++++++ .../.translations/zh-Hant.json | 19 ++++++++ .../components/auth/.translations/pl.json | 2 +- .../components/auth/.translations/sl.json | 2 +- .../components/auth/.translations/uk.json | 5 ++ .../daikin/.translations/zh-Hans.json | 19 ++++++++ .../components/deconz/.translations/en.json | 2 +- .../components/deconz/.translations/pl.json | 4 +- .../dialogflow/.translations/sl.json | 4 +- .../dialogflow/.translations/zh-Hans.json | 5 ++ .../emulated_roku/.translations/es.json | 17 +++++++ .../emulated_roku/.translations/ko.json | 2 +- .../emulated_roku/.translations/lb.json | 21 ++++++++ .../emulated_roku/.translations/pl.json | 21 ++++++++ .../emulated_roku/.translations/sl.json | 21 ++++++++ .../emulated_roku/.translations/zh-Hans.json | 17 +++++++ .../emulated_roku/.translations/zh-Hant.json | 21 ++++++++ .../components/esphome/.translations/es.json | 25 ++++++++++ .../components/esphome/.translations/ko.json | 2 +- .../components/esphome/.translations/pl.json | 6 +-- .../esphome/.translations/zh-Hans.json | 27 +++++++++++ .../components/geofency/.translations/es.json | 8 ++++ .../components/geofency/.translations/pl.json | 18 +++++++ .../components/geofency/.translations/sl.json | 18 +++++++ .../geofency/.translations/zh-Hans.json | 15 ++++++ .../geofency/.translations/zh-Hant.json | 18 +++++++ .../gpslogger/.translations/ca.json | 18 +++++++ .../gpslogger/.translations/en.json | 30 ++++++------ .../gpslogger/.translations/es.json | 8 ++++ .../gpslogger/.translations/ko.json | 18 +++++++ .../gpslogger/.translations/lb.json | 18 +++++++ .../gpslogger/.translations/no.json | 18 +++++++ .../gpslogger/.translations/pl.json | 18 +++++++ .../gpslogger/.translations/ru.json | 17 +++++++ .../gpslogger/.translations/sl.json | 18 +++++++ .../gpslogger/.translations/zh-Hans.json | 7 +++ .../gpslogger/.translations/zh-Hant.json | 18 +++++++ .../homematicip_cloud/.translations/sl.json | 6 +-- .../.translations/zh-Hans.json | 2 +- .../components/hue/.translations/pl.json | 2 +- .../components/hue/.translations/sl.json | 2 +- .../components/ifttt/.translations/sl.json | 4 +- .../components/locative/.translations/ca.json | 18 +++++++ .../components/locative/.translations/en.json | 30 ++++++------ .../components/locative/.translations/ko.json | 18 +++++++ .../components/locative/.translations/lb.json | 18 +++++++ .../components/locative/.translations/no.json | 18 +++++++ .../components/locative/.translations/pl.json | 18 +++++++ .../components/locative/.translations/ru.json | 17 +++++++ .../components/locative/.translations/sl.json | 18 +++++++ .../locative/.translations/zh-Hans.json | 15 ++++++ .../locative/.translations/zh-Hant.json | 18 +++++++ .../components/mailgun/.translations/sl.json | 4 +- .../mailgun/.translations/zh-Hans.json | 9 +++- .../components/mqtt/.translations/sl.json | 2 +- .../nest/.translations/zh-Hans.json | 4 +- .../components/openuv/.translations/ca.json | 4 +- .../point/.translations/zh-Hans.json | 11 ++++- .../rainmachine/.translations/zh-Hans.json | 4 +- .../simplisafe/.translations/zh-Hans.json | 2 +- .../smartthings/.translations/ca.json | 27 +++++++++++ .../smartthings/.translations/en.json | 48 +++++++++---------- .../smartthings/.translations/lb.json | 27 +++++++++++ .../smartthings/.translations/pl.json | 10 ++++ .../smartthings/.translations/zh-Hant.json | 27 +++++++++++ .../tellduslive/.translations/ca.json | 4 ++ .../tellduslive/.translations/en.json | 1 + .../tellduslive/.translations/es.json | 10 ++++ .../tellduslive/.translations/ko.json | 4 ++ .../tellduslive/.translations/lb.json | 4 ++ .../tellduslive/.translations/no.json | 4 ++ .../tellduslive/.translations/pl.json | 4 ++ .../tellduslive/.translations/ru.json | 4 ++ .../tellduslive/.translations/sl.json | 4 ++ .../tellduslive/.translations/zh-Hans.json | 24 ++++++++++ .../tellduslive/.translations/zh-Hant.json | 4 ++ .../components/twilio/.translations/sl.json | 4 +- .../twilio/.translations/zh-Hans.json | 13 ++++- .../components/upnp/.translations/sl.json | 2 +- .../upnp/.translations/zh-Hans.json | 8 +++- .../components/zha/.translations/zh-Hans.json | 13 ++++- 84 files changed, 981 insertions(+), 92 deletions(-) create mode 100644 homeassistant/components/ambient_station/.translations/ca.json create mode 100644 homeassistant/components/ambient_station/.translations/ko.json create mode 100644 homeassistant/components/ambient_station/.translations/lb.json create mode 100644 homeassistant/components/ambient_station/.translations/ru.json create mode 100644 homeassistant/components/ambient_station/.translations/zh-Hant.json create mode 100644 homeassistant/components/daikin/.translations/zh-Hans.json create mode 100644 homeassistant/components/emulated_roku/.translations/es.json create mode 100644 homeassistant/components/emulated_roku/.translations/lb.json create mode 100644 homeassistant/components/emulated_roku/.translations/pl.json create mode 100644 homeassistant/components/emulated_roku/.translations/sl.json create mode 100644 homeassistant/components/emulated_roku/.translations/zh-Hans.json create mode 100644 homeassistant/components/emulated_roku/.translations/zh-Hant.json create mode 100644 homeassistant/components/esphome/.translations/es.json create mode 100644 homeassistant/components/esphome/.translations/zh-Hans.json create mode 100644 homeassistant/components/geofency/.translations/es.json create mode 100644 homeassistant/components/geofency/.translations/pl.json create mode 100644 homeassistant/components/geofency/.translations/sl.json create mode 100644 homeassistant/components/geofency/.translations/zh-Hans.json create mode 100644 homeassistant/components/geofency/.translations/zh-Hant.json create mode 100644 homeassistant/components/gpslogger/.translations/ca.json create mode 100644 homeassistant/components/gpslogger/.translations/es.json create mode 100644 homeassistant/components/gpslogger/.translations/ko.json create mode 100644 homeassistant/components/gpslogger/.translations/lb.json create mode 100644 homeassistant/components/gpslogger/.translations/no.json create mode 100644 homeassistant/components/gpslogger/.translations/pl.json create mode 100644 homeassistant/components/gpslogger/.translations/ru.json create mode 100644 homeassistant/components/gpslogger/.translations/sl.json create mode 100644 homeassistant/components/gpslogger/.translations/zh-Hans.json create mode 100644 homeassistant/components/gpslogger/.translations/zh-Hant.json create mode 100644 homeassistant/components/locative/.translations/ca.json create mode 100644 homeassistant/components/locative/.translations/ko.json create mode 100644 homeassistant/components/locative/.translations/lb.json create mode 100644 homeassistant/components/locative/.translations/no.json create mode 100644 homeassistant/components/locative/.translations/pl.json create mode 100644 homeassistant/components/locative/.translations/ru.json create mode 100644 homeassistant/components/locative/.translations/sl.json create mode 100644 homeassistant/components/locative/.translations/zh-Hans.json create mode 100644 homeassistant/components/locative/.translations/zh-Hant.json create mode 100644 homeassistant/components/smartthings/.translations/ca.json create mode 100644 homeassistant/components/smartthings/.translations/lb.json create mode 100644 homeassistant/components/smartthings/.translations/pl.json create mode 100644 homeassistant/components/smartthings/.translations/zh-Hant.json create mode 100644 homeassistant/components/tellduslive/.translations/es.json create mode 100644 homeassistant/components/tellduslive/.translations/zh-Hans.json diff --git a/homeassistant/components/ambient_station/.translations/ca.json b/homeassistant/components/ambient_station/.translations/ca.json new file mode 100644 index 00000000000..d3c451f3e3f --- /dev/null +++ b/homeassistant/components/ambient_station/.translations/ca.json @@ -0,0 +1,19 @@ +{ + "config": { + "error": { + "identifier_exists": "Clau d'aplicaci\u00f3 i/o clau API ja registrada", + "invalid_key": "Clau API i/o clau d'aplicaci\u00f3 inv\u00e0lida/es", + "no_devices": "No s'ha trobat cap dispositiu al compte" + }, + "step": { + "user": { + "data": { + "api_key": "Clau API", + "app_key": "Clau d'aplicaci\u00f3" + }, + "title": "Introdueix la teva informaci\u00f3" + } + }, + "title": "Ambient PWS" + } +} \ No newline at end of file diff --git a/homeassistant/components/ambient_station/.translations/ko.json b/homeassistant/components/ambient_station/.translations/ko.json new file mode 100644 index 00000000000..51a09514159 --- /dev/null +++ b/homeassistant/components/ambient_station/.translations/ko.json @@ -0,0 +1,19 @@ +{ + "config": { + "error": { + "identifier_exists": "Application \ud0a4 \ud639\uc740 API \ud0a4\uac00 \uc774\ubbf8 \ub4f1\ub85d\ub418\uc5c8\uc2b5\ub2c8\ub2e4", + "invalid_key": "Application \ud0a4 \ud639\uc740 API \ud0a4\uac00 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4", + "no_devices": "\uacc4\uc815\uc5d0 \uae30\uae30\uac00 \uc874\uc7ac\ud558\uc9c0 \uc54a\uc2b5\ub2c8\ub2e4" + }, + "step": { + "user": { + "data": { + "api_key": "API \ud0a4", + "app_key": "Application \ud0a4" + }, + "title": "\uc0ac\uc6a9\uc790 \uc815\ubcf4\ub97c \uc785\ub825\ud574 \uc8fc\uc138\uc694" + } + }, + "title": "Ambient PWS" + } +} \ No newline at end of file diff --git a/homeassistant/components/ambient_station/.translations/lb.json b/homeassistant/components/ambient_station/.translations/lb.json new file mode 100644 index 00000000000..0f0d60d4458 --- /dev/null +++ b/homeassistant/components/ambient_station/.translations/lb.json @@ -0,0 +1,19 @@ +{ + "config": { + "error": { + "identifier_exists": "Applikatioun's Schl\u00ebssel an/oder API Schl\u00ebssel ass scho registr\u00e9iert", + "invalid_key": "Ong\u00ebltegen API Schl\u00ebssel an/oder Applikatioun's Schl\u00ebssel", + "no_devices": "Keng Apparater am Kont fonnt" + }, + "step": { + "user": { + "data": { + "api_key": "API Schl\u00ebssel", + "app_key": "Applikatioun's Schl\u00ebssel" + }, + "title": "F\u00ebllt \u00e4r Informatiounen aus" + } + }, + "title": "Ambient PWS" + } +} \ No newline at end of file diff --git a/homeassistant/components/ambient_station/.translations/ru.json b/homeassistant/components/ambient_station/.translations/ru.json new file mode 100644 index 00000000000..d1264010b75 --- /dev/null +++ b/homeassistant/components/ambient_station/.translations/ru.json @@ -0,0 +1,19 @@ +{ + "config": { + "error": { + "identifier_exists": "\u041a\u043b\u044e\u0447 \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u044f \u0438/\u0438\u043b\u0438 \u043a\u043b\u044e\u0447 API \u0443\u0436\u0435 \u0437\u0430\u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0438\u0440\u043e\u0432\u0430\u043d", + "invalid_key": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 \u043a\u043b\u044e\u0447 API \u0438/\u0438\u043b\u0438 \u043a\u043b\u044e\u0447 \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u044f", + "no_devices": "\u0412 \u0443\u0447\u0435\u0442\u043d\u043e\u0439 \u0437\u0430\u043f\u0438\u0441\u0438 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u043d\u0435 \u043d\u0430\u0439\u0434\u0435\u043d\u044b" + }, + "step": { + "user": { + "data": { + "api_key": "\u041a\u043b\u044e\u0447 API", + "app_key": "\u041a\u043b\u044e\u0447 \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u044f" + }, + "title": "Ambient PWS" + } + }, + "title": "Ambient PWS" + } +} \ No newline at end of file diff --git a/homeassistant/components/ambient_station/.translations/zh-Hant.json b/homeassistant/components/ambient_station/.translations/zh-Hant.json new file mode 100644 index 00000000000..7e3ed3ef888 --- /dev/null +++ b/homeassistant/components/ambient_station/.translations/zh-Hant.json @@ -0,0 +1,19 @@ +{ + "config": { + "error": { + "identifier_exists": "API \u5bc6\u9470\u53ca/\u6216\u61c9\u7528\u5bc6\u9470\u5df2\u8a3b\u518a", + "invalid_key": "API \u5bc6\u9470\u53ca/\u6216\u61c9\u7528\u5bc6\u9470\u7121\u6548", + "no_devices": "\u5e33\u865f\u4e2d\u627e\u4e0d\u5230\u4efb\u4f55\u88dd\u7f6e" + }, + "step": { + "user": { + "data": { + "api_key": "API \u5bc6\u9470", + "app_key": "\u61c9\u7528\u5bc6\u9470" + }, + "title": "\u586b\u5beb\u8cc7\u8a0a" + } + }, + "title": "\u74b0\u5883 PWS" + } +} \ No newline at end of file diff --git a/homeassistant/components/auth/.translations/pl.json b/homeassistant/components/auth/.translations/pl.json index 6adaaa019c5..f0e9f7b71ea 100644 --- a/homeassistant/components/auth/.translations/pl.json +++ b/homeassistant/components/auth/.translations/pl.json @@ -9,7 +9,7 @@ }, "step": { "init": { - "description": "Prosz\u0119 wybra\u0107 jedn\u0105 z us\u0142ugi powiadamiania:", + "description": "Prosz\u0119 wybra\u0107 jedn\u0105 us\u0142ug\u0119 powiadamiania:", "title": "Skonfiguruj has\u0142o jednorazowe dostarczone przez komponent powiadomie\u0144" }, "setup": { diff --git a/homeassistant/components/auth/.translations/sl.json b/homeassistant/components/auth/.translations/sl.json index 223dc91a480..f70bb81e700 100644 --- a/homeassistant/components/auth/.translations/sl.json +++ b/homeassistant/components/auth/.translations/sl.json @@ -21,7 +21,7 @@ }, "totp": { "error": { - "invalid_code": "Neveljavna koda, prosimo, poskusite znova. \u010ce dobite to sporo\u010dilo ve\u010dkrat, prosimo poskrbite, da bo ura va\u0161ega Home Assistenta to\u010dna." + "invalid_code": "Neveljavna koda, prosimo, poskusite znova. \u010ce dobite to sporo\u010dilo ve\u010dkrat, prosimo poskrbite, da bo ura va\u0161ega Home Assistanta to\u010dna." }, "step": { "init": { diff --git a/homeassistant/components/auth/.translations/uk.json b/homeassistant/components/auth/.translations/uk.json index 3d4d9a5b151..f826075078e 100644 --- a/homeassistant/components/auth/.translations/uk.json +++ b/homeassistant/components/auth/.translations/uk.json @@ -3,6 +3,11 @@ "notify": { "error": { "invalid_code": "\u041d\u0435\u0432\u0456\u0440\u043d\u0438\u0439 \u043a\u043e\u0434, \u0441\u043f\u0440\u043e\u0431\u0443\u0439\u0442\u0435 \u0449\u0435 \u0440\u0430\u0437." + }, + "step": { + "setup": { + "title": "\u041f\u0435\u0440\u0435\u0432\u0456\u0440\u0442\u0435 \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f" + } } } } diff --git a/homeassistant/components/daikin/.translations/zh-Hans.json b/homeassistant/components/daikin/.translations/zh-Hans.json new file mode 100644 index 00000000000..1330e3a932d --- /dev/null +++ b/homeassistant/components/daikin/.translations/zh-Hans.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "already_configured": "\u8bbe\u5907\u5df2\u914d\u7f6e\u5b8c\u6210", + "device_fail": "\u521b\u5efa\u8bbe\u5907\u65f6\u51fa\u73b0\u610f\u5916\u9519\u8bef\u3002", + "device_timeout": "\u8fde\u63a5\u8bbe\u5907\u8d85\u65f6\u3002" + }, + "step": { + "user": { + "data": { + "host": "\u4e3b\u673a" + }, + "description": "\u8f93\u5165\u60a8\u7684 Daikin \u7a7a\u8c03\u7684IP\u5730\u5740\u3002", + "title": "\u914d\u7f6e Daikin \u7a7a\u8c03" + } + }, + "title": "Daikin \u7a7a\u8c03" + } +} \ No newline at end of file diff --git a/homeassistant/components/deconz/.translations/en.json b/homeassistant/components/deconz/.translations/en.json index 0c60953db56..d8bcc95a115 100644 --- a/homeassistant/components/deconz/.translations/en.json +++ b/homeassistant/components/deconz/.translations/en.json @@ -17,7 +17,7 @@ "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", + "description": "Unlock your deCONZ gateway to register with Home Assistant.\n\n1. Go to deCONZ Settings -> Gateway -> Advanced\n2. Press \"Authenticate app\" button", "title": "Link with deCONZ" }, "options": { diff --git a/homeassistant/components/deconz/.translations/pl.json b/homeassistant/components/deconz/.translations/pl.json index 5dd87d9e462..5a8b710c006 100644 --- a/homeassistant/components/deconz/.translations/pl.json +++ b/homeassistant/components/deconz/.translations/pl.json @@ -12,7 +12,7 @@ "init": { "data": { "host": "Host", - "port": "Port (warto\u015b\u0107 domy\u015blna: \"80\")" + "port": "Port" }, "title": "Zdefiniuj bramk\u0119 deCONZ" }, @@ -28,6 +28,6 @@ "title": "Dodatkowe opcje konfiguracji dla deCONZ" } }, - "title": "deCONZ" + "title": "Brama deCONZ Zigbee" } } \ No newline at end of file diff --git a/homeassistant/components/dialogflow/.translations/sl.json b/homeassistant/components/dialogflow/.translations/sl.json index 597e65a7658..18a476b6870 100644 --- a/homeassistant/components/dialogflow/.translations/sl.json +++ b/homeassistant/components/dialogflow/.translations/sl.json @@ -1,11 +1,11 @@ { "config": { "abort": { - "not_internet_accessible": " \u010ce \u017eelite prejemati sporo\u010dila dialogflow, mora biti Home Assistent dostopen prek interneta.", + "not_internet_accessible": "\u010ce \u017eelite prejemati sporo\u010dila dialogflow, mora biti Home Assistant dostopen prek interneta.", "one_instance_allowed": "Potrebna je samo ena instanca." }, "create_entry": { - "default": "Za po\u0161iljanje dogodkov Home Assistent-u, boste morali nastaviti [webhook z dialogflow]({twilio_url}).\n\nIzpolnite naslednje informacije:\n\n- URL: `{webhook_url}`\n- Metoda: POST\n- Vrsta vsebine: application/x-www-form-urlencoded\n\nGlej [dokumentacijo]({docs_url}) za nadaljna navodila." + "default": "Za po\u0161iljanje dogodkov Home Assistant-u, boste morali nastaviti [webhook z dialogflow]({twilio_url}).\n\nIzpolnite naslednje informacije:\n\n- URL: `{webhook_url}`\n- Metoda: POST\n- Vrsta vsebine: application/x-www-form-urlencoded\n\nGlej [dokumentacijo]({docs_url}) za nadaljna navodila." }, "step": { "user": { diff --git a/homeassistant/components/dialogflow/.translations/zh-Hans.json b/homeassistant/components/dialogflow/.translations/zh-Hans.json index 6eecbed54ac..8a542dd0d62 100644 --- a/homeassistant/components/dialogflow/.translations/zh-Hans.json +++ b/homeassistant/components/dialogflow/.translations/zh-Hans.json @@ -1,10 +1,15 @@ { "config": { + "abort": { + "not_internet_accessible": "\u60a8\u7684 Home Assistant \u5b9e\u4f8b\u9700\u8981\u63a5\u5165\u4e92\u8054\u7f51\u4ee5\u63a5\u6536 Dialogflow \u6d88\u606f\u3002", + "one_instance_allowed": "\u4ec5\u9700\u4e00\u4e2a\u5b9e\u4f8b" + }, "create_entry": { "default": "\u8981\u5411 Home Assistant \u53d1\u9001\u4e8b\u4ef6\uff0c\u60a8\u9700\u8981\u914d\u7f6e [Dialogflow \u7684 Webhook \u96c6\u6210]({dialogflow_url})\u3002\n\n\u586b\u5199\u4ee5\u4e0b\u4fe1\u606f\uff1a\n\n- URL: `{webhook_url}`\n- Method: POST\n- Content Type: application/json\n\n\u8bf7\u53c2\u9605[\u6587\u6863]({docs_url})\u4ee5\u4e86\u89e3\u66f4\u591a\u4fe1\u606f\u3002" }, "step": { "user": { + "description": "\u60a8\u786e\u5b9a\u8981\u8bbe\u7f6e Dialogflow \u5417?", "title": "\u8bbe\u7f6e Dialogflow Webhook" } }, diff --git a/homeassistant/components/emulated_roku/.translations/es.json b/homeassistant/components/emulated_roku/.translations/es.json new file mode 100644 index 00000000000..3491c784c19 --- /dev/null +++ b/homeassistant/components/emulated_roku/.translations/es.json @@ -0,0 +1,17 @@ +{ + "config": { + "abort": { + "name_exists": "El nombre ya existe" + }, + "step": { + "user": { + "data": { + "host_ip": "IP del host", + "listen_port": "Puerto de escucha", + "name": "Nombre" + }, + "title": "Definir la configuraci\u00f3n del servidor" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/emulated_roku/.translations/ko.json b/homeassistant/components/emulated_roku/.translations/ko.json index 54c3e079386..ddee892039f 100644 --- a/homeassistant/components/emulated_roku/.translations/ko.json +++ b/homeassistant/components/emulated_roku/.translations/ko.json @@ -11,7 +11,7 @@ "host_ip": "\ud638\uc2a4\ud2b8 IP", "listen_port": "\uc218\uc2e0 \ud3ec\ud2b8", "name": "\uc774\ub984", - "upnp_bind_multicast": "\uba40\ud2f0 \uce90\uc2a4\ud2b8 \ubc14\uc778\ub4dc (\ucc38/\uac70\uc9d3)" + "upnp_bind_multicast": "\uba40\ud2f0 \uce90\uc2a4\ud2b8 \ud560\ub2f9 (\ucc38/\uac70\uc9d3)" }, "title": "\uc11c\ubc84 \uad6c\uc131 \uc815\uc758" } diff --git a/homeassistant/components/emulated_roku/.translations/lb.json b/homeassistant/components/emulated_roku/.translations/lb.json new file mode 100644 index 00000000000..11d1aa3ff7a --- /dev/null +++ b/homeassistant/components/emulated_roku/.translations/lb.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "name_exists": "Numm g\u00ebtt et schonn" + }, + "step": { + "user": { + "data": { + "advertise_ip": "IP annonc\u00e9ieren", + "advertise_port": "Port annonc\u00e9ieren", + "host_ip": "IP vum Apparat", + "listen_port": "Port lauschteren", + "name": "Numm", + "upnp_bind_multicast": "Multicast abannen (Richteg/Falsch)" + }, + "title": "Server Konfiguratioun d\u00e9fin\u00e9ieren" + } + }, + "title": "EmulatedRoku" + } +} \ No newline at end of file diff --git a/homeassistant/components/emulated_roku/.translations/pl.json b/homeassistant/components/emulated_roku/.translations/pl.json new file mode 100644 index 00000000000..0ed3cc3d14a --- /dev/null +++ b/homeassistant/components/emulated_roku/.translations/pl.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "name_exists": "Nazwa ju\u017c istnieje" + }, + "step": { + "user": { + "data": { + "advertise_ip": "IP rozg\u0142aszania", + "advertise_port": "Port rozg\u0142aszania", + "host_ip": "IP hosta", + "listen_port": "Port nas\u0142uchu", + "name": "Nazwa", + "upnp_bind_multicast": "Powi\u0105\u017c multicast (prawda/fa\u0142sz)" + }, + "title": "Zdefiniuj konfiguracj\u0119 serwera" + } + }, + "title": "EmulatedRoku" + } +} \ No newline at end of file diff --git a/homeassistant/components/emulated_roku/.translations/sl.json b/homeassistant/components/emulated_roku/.translations/sl.json new file mode 100644 index 00000000000..768feb83747 --- /dev/null +++ b/homeassistant/components/emulated_roku/.translations/sl.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "name_exists": "Ime \u017ee obstaja" + }, + "step": { + "user": { + "data": { + "advertise_ip": "Advertise IP", + "advertise_port": "Advertise port", + "host_ip": "IP gostitelja", + "listen_port": "Vrata naprave", + "name": "Ime", + "upnp_bind_multicast": "Vezava multicasta (True / False)" + }, + "title": "Dolo\u010dite konfiguracijo stre\u017enika" + } + }, + "title": "EmulatedRoku" + } +} \ No newline at end of file diff --git a/homeassistant/components/emulated_roku/.translations/zh-Hans.json b/homeassistant/components/emulated_roku/.translations/zh-Hans.json new file mode 100644 index 00000000000..9cb4cc33431 --- /dev/null +++ b/homeassistant/components/emulated_roku/.translations/zh-Hans.json @@ -0,0 +1,17 @@ +{ + "config": { + "abort": { + "name_exists": "\u540d\u79f0\u5df2\u5b58\u5728" + }, + "step": { + "user": { + "data": { + "host_ip": "\u4e3b\u673aIP", + "listen_port": "\u76d1\u542c\u7aef\u53e3", + "name": "\u59d3\u540d" + }, + "title": "\u5b9a\u4e49\u670d\u52a1\u5668\u914d\u7f6e" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/emulated_roku/.translations/zh-Hant.json b/homeassistant/components/emulated_roku/.translations/zh-Hant.json new file mode 100644 index 00000000000..40b4307ae02 --- /dev/null +++ b/homeassistant/components/emulated_roku/.translations/zh-Hant.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "name_exists": "\u8a72\u540d\u7a31\u5df2\u5b58\u5728" + }, + "step": { + "user": { + "data": { + "advertise_ip": "\u5ee3\u64ad\u901a\u8a0a\u57e0", + "advertise_port": "\u5ee3\u64ad\u901a\u8a0a\u57e0", + "host_ip": "\u4e3b\u6a5f IP", + "listen_port": "\u76e3\u807d\u901a\u8a0a\u57e0", + "name": "\u540d\u7a31", + "upnp_bind_multicast": "\u7d81\u5b9a\u7fa4\u64ad\uff08Multicast\uff09True/False" + }, + "title": "\u5b9a\u7fa9\u4f3a\u670d\u5668\u8a2d\u5b9a" + } + }, + "title": "EmulatedRoku" + } +} \ No newline at end of file diff --git a/homeassistant/components/esphome/.translations/es.json b/homeassistant/components/esphome/.translations/es.json new file mode 100644 index 00000000000..8010b330b88 --- /dev/null +++ b/homeassistant/components/esphome/.translations/es.json @@ -0,0 +1,25 @@ +{ + "config": { + "abort": { + "already_configured": "ESP ya est\u00e1 configurado" + }, + "error": { + "invalid_password": "\u00a1Contrase\u00f1a incorrecta!" + }, + "step": { + "authenticate": { + "data": { + "password": "Contrase\u00f1a" + }, + "description": "Escribe la contrase\u00f1a que hayas establecido en tu configuraci\u00f3n.", + "title": "Escribe la contrase\u00f1a" + }, + "user": { + "data": { + "host": "Host", + "port": "Puerto" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/esphome/.translations/ko.json b/homeassistant/components/esphome/.translations/ko.json index 514acbbbf18..24f84851254 100644 --- a/homeassistant/components/esphome/.translations/ko.json +++ b/homeassistant/components/esphome/.translations/ko.json @@ -4,7 +4,7 @@ "already_configured": "ESP \uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4" }, "error": { - "connection_error": "ESP \uc5d0 \uc5f0\uacb0\ud560 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4. YAML \ud30c\uc77c\uc5d0 'api :' \ub97c \uad6c\uc131\ud588\ub294\uc9c0 \ud655\uc778\ud574\uc8fc\uc138\uc694.", + "connection_error": "ESP \uc5d0 \uc5f0\uacb0\ud560 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4. YAML \ud30c\uc77c\uc5d0 'api:' \ub97c \uad6c\uc131\ud588\ub294\uc9c0 \ud655\uc778\ud574\uc8fc\uc138\uc694.", "invalid_password": "\uc798\ubabb\ub41c \ube44\ubc00\ubc88\ud638", "resolve_error": "ESP \uc758 \uc8fc\uc18c\ub97c \ud655\uc778\ud560 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4. \uc774 \uc624\ub958\uac00 \uacc4\uc18d \ubc1c\uc0dd\ud558\uba74 \uace0\uc815 IP \uc8fc\uc18c (https://esphomelib.com/esphomeyaml/components/wifi.html#manual-ips)\ub97c \uc124\uc815\ud574\uc8fc\uc138\uc694" }, diff --git a/homeassistant/components/esphome/.translations/pl.json b/homeassistant/components/esphome/.translations/pl.json index 4f2a8b0e1bb..19fb581eb3f 100644 --- a/homeassistant/components/esphome/.translations/pl.json +++ b/homeassistant/components/esphome/.translations/pl.json @@ -1,10 +1,10 @@ { "config": { "abort": { - "already_configured": "ESP jest ju\u017c skonfigurowany" + "already_configured": "ESP jest ju\u017c skonfigurowane" }, "error": { - "connection_error": "Nie mo\u017cna po\u0142\u0105czy\u0107 si\u0119 z ESP. Upewnij si\u0119, \u017ce Tw\u00f3j plik YAML zawiera lini\u0119 \"api:\".", + "connection_error": "Nie mo\u017cna po\u0142\u0105czy\u0107 si\u0119 z ESP. Upewnij si\u0119, \u017ce Tw\u00f3j plik YAML zawiera lini\u0119 'api:'.", "invalid_password": "Nieprawid\u0142owe has\u0142o!", "resolve_error": "Nie mo\u017cna rozpozna\u0107 adresu ESP. Je\u015bli ten b\u0142\u0105d b\u0119dzie si\u0119 powtarza\u0142, nale\u017cy ustawi\u0107 statyczny adres IP: https://esphomelib.com/esphomeyaml/components/wifi.html#manual-ips" }, @@ -21,7 +21,7 @@ "host": "Host", "port": "Port" }, - "description": "Wprowad\u017a ustawienia po\u0142\u0105czenia swojego [ESPHome] (https://esphomelib.com/) w\u0119z\u0142a.", + "description": "Wprowad\u017a ustawienia po\u0142\u0105czenia swojego [ESPHome](https://esphomelib.com/) w\u0119z\u0142a.", "title": "ESPHome" } }, diff --git a/homeassistant/components/esphome/.translations/zh-Hans.json b/homeassistant/components/esphome/.translations/zh-Hans.json new file mode 100644 index 00000000000..8e5ca59fcef --- /dev/null +++ b/homeassistant/components/esphome/.translations/zh-Hans.json @@ -0,0 +1,27 @@ +{ + "config": { + "error": { + "connection_error": "\u65e0\u6cd5\u8fde\u63a5\u5230ESP\u3002\u8bf7\u786e\u4fdd\u60a8\u7684YAML\u6587\u4ef6\u5305\u542b'api:'\u884c\u3002", + "invalid_password": "\u65e0\u6548\u7684\u5bc6\u7801\uff01", + "resolve_error": "\u65e0\u6cd5\u89e3\u6790ESP\u7684\u5730\u5740\u3002\u5982\u679c\u6b64\u9519\u8bef\u4ecd\u7136\u5b58\u5728\uff0c\u8bf7\u8bbe\u7f6e\u9759\u6001IP\u5730\u5740\uff1ahttps://esphomelib.com/esphomeyaml/components/wifi.html#manual-ips" + }, + "step": { + "authenticate": { + "data": { + "password": "\u5bc6\u7801" + }, + "description": "\u8bf7\u8f93\u5165\u60a8\u5728\u914d\u7f6e\u4e2d\u8bbe\u7f6e\u7684\u5bc6\u7801\u3002", + "title": "\u8f93\u5165\u5bc6\u7801" + }, + "user": { + "data": { + "host": "\u4e3b\u673a", + "port": "\u7aef\u53e3" + }, + "description": "\u8bf7\u8f93\u5165\u60a8\u7684 [ESPHome](https://esphomelib.com/) \u8282\u70b9\u7684\u8fde\u63a5\u8bbe\u7f6e\u3002", + "title": "ESPHome" + } + }, + "title": "ESPHome" + } +} \ No newline at end of file diff --git a/homeassistant/components/geofency/.translations/es.json b/homeassistant/components/geofency/.translations/es.json new file mode 100644 index 00000000000..cd14e21db10 --- /dev/null +++ b/homeassistant/components/geofency/.translations/es.json @@ -0,0 +1,8 @@ +{ + "config": { + "abort": { + "not_internet_accessible": "Tu Home Assistant debe ser accesible desde Internet para recibir mensajes de GPSLogger.", + "one_instance_allowed": "Solo se necesita una instancia." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/geofency/.translations/pl.json b/homeassistant/components/geofency/.translations/pl.json new file mode 100644 index 00000000000..09d93e6911e --- /dev/null +++ b/homeassistant/components/geofency/.translations/pl.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "not_internet_accessible": "Tw\u00f3j Home Assistant musi by\u0107 dost\u0119pny z Internetu, aby odbiera\u0107 komunikaty z Geofency.", + "one_instance_allowed": "Wymagana jest tylko jedna instancja." + }, + "create_entry": { + "default": "Aby wysy\u0142a\u0107 zdarzenia do Home Assistant'a, musisz skonfigurowa\u0107 webhook w Geofency. \n\n Wprowad\u017a nast\u0119puj\u0105ce dane:\n\n - URL: `{webhook_url}` \n - Metoda: POST \n\nZapoznaj si\u0119 z [dokumentacj\u0105]({docs_url}) by pozna\u0107 szczeg\u00f3\u0142y." + }, + "step": { + "user": { + "description": "Czy chcesz skonfigurowa\u0107 Geofency?", + "title": "Konfiguracja Geofency Webhook" + } + }, + "title": "Geofency Webhook" + } +} \ No newline at end of file diff --git a/homeassistant/components/geofency/.translations/sl.json b/homeassistant/components/geofency/.translations/sl.json new file mode 100644 index 00000000000..e56d41d4f1a --- /dev/null +++ b/homeassistant/components/geofency/.translations/sl.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "not_internet_accessible": "Va\u0161 Home Assistant mora biti dostopen prek interneta, da boste lahko prejemali Geofency sporo\u010dila.", + "one_instance_allowed": "Potrebna je samo ena instanca." + }, + "create_entry": { + "default": "\u010ce \u017eelite dogodke poslati v Home Assistant, morate v Geofency-ju nastaviti funkcijo webhook. \n\n Izpolnite naslednje podatke: \n\n - URL: ` {webhook_url} ` \n - Metoda: POST \n\n Za ve\u010d podrobnosti si oglejte [dokumentacijo] ( {docs_url} )." + }, + "step": { + "user": { + "description": "Ali ste prepri\u010dani, da \u017eelite nastaviti geofency webhook?", + "title": "Nastavite Geofency Webhook" + } + }, + "title": "Geofency Webhook" + } +} \ No newline at end of file diff --git a/homeassistant/components/geofency/.translations/zh-Hans.json b/homeassistant/components/geofency/.translations/zh-Hans.json new file mode 100644 index 00000000000..7ab8a128980 --- /dev/null +++ b/homeassistant/components/geofency/.translations/zh-Hans.json @@ -0,0 +1,15 @@ +{ + "config": { + "abort": { + "not_internet_accessible": "\u60a8\u7684 Home Assistant \u5b9e\u4f8b\u9700\u8981\u63a5\u5165\u4e92\u8054\u7f51\u4ee5\u63a5\u6536 Geofency \u6d88\u606f\u3002", + "one_instance_allowed": "\u4ec5\u9700\u4e00\u4e2a\u5b9e\u4f8b" + }, + "step": { + "user": { + "description": "\u60a8\u786e\u5b9a\u8981\u8bbe\u7f6e Geofency Webhook \u5417?", + "title": "\u8bbe\u7f6e Geofency Webhook" + } + }, + "title": "Geofency Webhook" + } +} \ No newline at end of file diff --git a/homeassistant/components/geofency/.translations/zh-Hant.json b/homeassistant/components/geofency/.translations/zh-Hant.json new file mode 100644 index 00000000000..bec33c26d10 --- /dev/null +++ b/homeassistant/components/geofency/.translations/zh-Hant.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "not_internet_accessible": "Home Assistant \u5be6\u4f8b\u5fc5\u9808\u80fd\u5920\u7531\u7db2\u969b\u7db2\u8def\u5b58\u53d6\uff0c\u65b9\u80fd\u63a5\u53d7 Geofency \u8a0a\u606f\u3002", + "one_instance_allowed": "\u50c5\u9700\u8a2d\u5b9a\u4e00\u7d44\u7269\u4ef6\u5373\u53ef\u3002" + }, + "create_entry": { + "default": "\u6b32\u50b3\u9001\u4e8b\u4ef6\u81f3 Home Assistant\uff0c\u5c07\u9700\u65bc Geofency \u5167\u8a2d\u5b9a webhook \u529f\u80fd\u3002\n\n\u8acb\u586b\u5beb\u4e0b\u5217\u8cc7\u8a0a\uff1a\n\n- URL: `{webhook_url}`\n- Method: POST\n\n\u8acb\u53c3\u95b1 [\u6587\u4ef6]({docs_url})\u4ee5\u4e86\u89e3\u66f4\u8a73\u7d30\u8cc7\u6599\u3002" + }, + "step": { + "user": { + "description": "\u662f\u5426\u8981\u8a2d\u5b9a Geofency Webhook\uff1f", + "title": "\u8a2d\u5b9a Geofency Webhook" + } + }, + "title": "Geofency Webhook" + } +} \ No newline at end of file diff --git a/homeassistant/components/gpslogger/.translations/ca.json b/homeassistant/components/gpslogger/.translations/ca.json new file mode 100644 index 00000000000..2d3b08d236e --- /dev/null +++ b/homeassistant/components/gpslogger/.translations/ca.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "not_internet_accessible": "La inst\u00e0ncia de Home Assistant ha de ser accessible des d'Internet per rebre missatges de GPSLogger.", + "one_instance_allowed": "Nom\u00e9s cal una sola inst\u00e0ncia." + }, + "create_entry": { + "default": "Per enviar esdeveniments a Home Assistant, haur\u00e0s de configurar l'opci\u00f3 webhook de GPSLogger.\n\nCompleta la seg\u00fcent informaci\u00f3:\n\n- URL: `{webhook_url}` \n- M\u00e8tode: POST \n\nConsulta la [documentaci\u00f3]({docs_url}) per a m\u00e9s detalls." + }, + "step": { + "user": { + "description": "Est\u00e0s segur que vols configurar el Webhook GPSLogger?", + "title": "Configuraci\u00f3 del Webhook GPSLogger" + } + }, + "title": "Webhook GPSLogger" + } +} \ No newline at end of file diff --git a/homeassistant/components/gpslogger/.translations/en.json b/homeassistant/components/gpslogger/.translations/en.json index d5641ef5db8..ad8f978bc59 100644 --- a/homeassistant/components/gpslogger/.translations/en.json +++ b/homeassistant/components/gpslogger/.translations/en.json @@ -1,18 +1,18 @@ { - "config": { - "title": "GPSLogger Webhook", - "step": { - "user": { - "title": "Set up the GPSLogger Webhook", - "description": "Are you sure you want to set up the GPSLogger Webhook?" - } - }, - "abort": { - "one_instance_allowed": "Only a single instance is necessary.", - "not_internet_accessible": "Your Home Assistant instance needs to be accessible from the internet to receive messages from GPSLogger." - }, - "create_entry": { - "default": "To send events to Home Assistant, you will need to setup the webhook feature in GPSLogger.\n\nFill in the following info:\n\n- URL: `{webhook_url}`\n- Method: POST\n\nSee [the documentation]({docs_url}) for further details." + "config": { + "abort": { + "not_internet_accessible": "Your Home Assistant instance needs to be accessible from the internet to receive messages from GPSLogger.", + "one_instance_allowed": "Only a single instance is necessary." + }, + "create_entry": { + "default": "To send events to Home Assistant, you will need to setup the webhook feature in GPSLogger.\n\nFill in the following info:\n\n- URL: `{webhook_url}`\n- Method: POST\n\nSee [the documentation]({docs_url}) for further details." + }, + "step": { + "user": { + "description": "Are you sure you want to set up the GPSLogger Webhook?", + "title": "Set up the GPSLogger Webhook" + } + }, + "title": "GPSLogger Webhook" } - } } \ No newline at end of file diff --git a/homeassistant/components/gpslogger/.translations/es.json b/homeassistant/components/gpslogger/.translations/es.json new file mode 100644 index 00000000000..cd14e21db10 --- /dev/null +++ b/homeassistant/components/gpslogger/.translations/es.json @@ -0,0 +1,8 @@ +{ + "config": { + "abort": { + "not_internet_accessible": "Tu Home Assistant debe ser accesible desde Internet para recibir mensajes de GPSLogger.", + "one_instance_allowed": "Solo se necesita una instancia." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/gpslogger/.translations/ko.json b/homeassistant/components/gpslogger/.translations/ko.json new file mode 100644 index 00000000000..a65e51d7cae --- /dev/null +++ b/homeassistant/components/gpslogger/.translations/ko.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "not_internet_accessible": "GPSLogger \uba54\uc2dc\uc9c0\ub97c \ubc1b\uc73c\ub824\uba74 \uc778\ud130\ub137\uc5d0\uc11c Home Assistant \uc778\uc2a4\ud134\uc2a4\uc5d0 \uc561\uc138\uc2a4 \ud560 \uc218 \uc788\uc5b4\uc57c \ud569\ub2c8\ub2e4.", + "one_instance_allowed": "\ud558\ub098\uc758 \uc778\uc2a4\ud134\uc2a4\ub9cc \ud544\uc694\ud569\ub2c8\ub2e4." + }, + "create_entry": { + "default": "Home Assistant \ub85c \uc774\ubca4\ud2b8\ub97c \ubcf4\ub0b4\ub824\uba74 GPSLogger \uc5d0\uc11c Webhook \uc744 \uc124\uc815\ud574\uc57c\ud569\ub2c8\ub2e4. \n\n\ub2e4\uc74c \uc815\ubcf4\ub97c \uc785\ub825\ud574 \uc8fc\uc138\uc694. \n\n - URL: `{webhook_url}`\n - Method: POST\n \n \uc790\uc138\ud55c \uc815\ubcf4\ub294 [\uc548\ub0b4]({docs_url}) \ub97c \ucc38\uc870\ud574 \uc8fc\uc138\uc694." + }, + "step": { + "user": { + "description": "GPSLogger Webhook \uc744 \uc124\uc815 \ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?", + "title": "GPSLogger Webhook \uc124\uc815" + } + }, + "title": "GPSLogger Webhook" + } +} \ No newline at end of file diff --git a/homeassistant/components/gpslogger/.translations/lb.json b/homeassistant/components/gpslogger/.translations/lb.json new file mode 100644 index 00000000000..78df911c868 --- /dev/null +++ b/homeassistant/components/gpslogger/.translations/lb.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "not_internet_accessible": "\u00c4r Home Assistant Instanz muss iwwert Internet accessibel si fir GPSLogger Noriichten z'empf\u00e4nken.", + "one_instance_allowed": "N\u00ebmmen eng eenzeg Instanz ass n\u00e9ideg." + }, + "create_entry": { + "default": "Fir Evenementer un Home Assistant ze sch\u00e9cken, muss den Webhook Feature am GPSLogger ageriicht ginn.\n\nF\u00ebllt folgend Informatiounen aus:\n\n- URL: `{webhook_url}`\n- Method: POST\n\nLiest [Dokumentatioun]({docs_url}) fir w\u00e9ider D\u00e9tailer." + }, + "step": { + "user": { + "description": "S\u00e9cher fir GPSLogger Webhook anzeriichten?", + "title": "GPSLogger Webhook ariichten" + } + }, + "title": "GPSLogger Webhook" + } +} \ No newline at end of file diff --git a/homeassistant/components/gpslogger/.translations/no.json b/homeassistant/components/gpslogger/.translations/no.json new file mode 100644 index 00000000000..836b5c8bc68 --- /dev/null +++ b/homeassistant/components/gpslogger/.translations/no.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "not_internet_accessible": "Home Assistant m\u00e5 v\u00e6re tilgjengelig fra internett for \u00e5 kunne motta meldinger fra GPSLogger.", + "one_instance_allowed": "Kun en enkelt forekomst er n\u00f8dvendig." + }, + "create_entry": { + "default": "For \u00e5 sende hendelser til Home Assistant, m\u00e5 du sette opp webhook-funksjonen i GPSLogger. \n\n Fyll ut f\u00f8lgende informasjon: \n\n - URL: `{webhook_url}` \n - Metode: POST \n\n Se [dokumentasjonen]({docs_url}) for ytterligere detaljer." + }, + "step": { + "user": { + "description": "Er du sikker p\u00e5 at du vil sette opp GPSLogger Webhook?", + "title": "Sett opp GPSLogger Webhook" + } + }, + "title": "GPSLogger Webhook" + } +} \ No newline at end of file diff --git a/homeassistant/components/gpslogger/.translations/pl.json b/homeassistant/components/gpslogger/.translations/pl.json new file mode 100644 index 00000000000..3d82ac6fa5a --- /dev/null +++ b/homeassistant/components/gpslogger/.translations/pl.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "not_internet_accessible": "Twoja instancja Home Assistant musi by\u0107 dost\u0119pna z Internetu, aby otrzymywa\u0107 wiadomo\u015bci z GPSlogger.", + "one_instance_allowed": "Wymagana jest tylko jedna instancja." + }, + "create_entry": { + "default": "Aby wysy\u0142a\u0107 lokalizacje do Home Assistant'a, musisz skonfigurowa\u0107 webhook w aplikacji GPSLogger. \n\n Wprowad\u017a nast\u0119puj\u0105ce dane:\n\n - URL: `{webhook_url}` \n - Metoda: POST \n\nZapoznaj si\u0119 z [dokumentacj\u0105]({docs_url}) by pozna\u0107 szczeg\u00f3\u0142y." + }, + "step": { + "user": { + "description": "Czy chcesz skonfigurowa\u0107 Geofency?", + "title": "Konfiguracja Geofency Webhook" + } + }, + "title": "Konfiguracja Geofency Webhook" + } +} \ No newline at end of file diff --git a/homeassistant/components/gpslogger/.translations/ru.json b/homeassistant/components/gpslogger/.translations/ru.json new file mode 100644 index 00000000000..34b7e907288 --- /dev/null +++ b/homeassistant/components/gpslogger/.translations/ru.json @@ -0,0 +1,17 @@ +{ + "config": { + "abort": { + "not_internet_accessible": "\u0412\u0430\u0448 Home Assistant \u0434\u043e\u043b\u0436\u0435\u043d \u0431\u044b\u0442\u044c \u0434\u043e\u0441\u0442\u0443\u043f\u0435\u043d \u0438\u0437 \u0438\u043d\u0442\u0435\u0440\u043d\u0435\u0442\u0430 \u0434\u043b\u044f \u043f\u043e\u043b\u0443\u0447\u0435\u043d\u0438\u044f \u0441\u043e\u043e\u0431\u0449\u0435\u043d\u0438\u0439 GPSLogger." + }, + "create_entry": { + "default": "\u0414\u043b\u044f \u043e\u0442\u043f\u0440\u0430\u0432\u043a\u0438 \u0441\u043e\u0431\u044b\u0442\u0438\u0439 \u0432 Home Assistant \u0432\u044b \u0434\u043e\u043b\u0436\u043d\u044b \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c webhooks \u0434\u043b\u044f GPSLogger.\n\n\u0414\u043b\u044f \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0439\u0442\u0435 \u0441\u043b\u0435\u0434\u0443\u044e\u0449\u0443\u044e \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u044e:\n\n- URL: `{webhook_url}`\n- Method: POST\n\n\u041e\u0437\u043d\u0430\u043a\u043e\u043c\u044c\u0442\u0435\u0441\u044c \u0441 [\u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0430\u0446\u0438\u0435\u0439]({docs_url}) \u0434\u043b\u044f \u043f\u043e\u043b\u0443\u0447\u0435\u043d\u0438\u044f \u0431\u043e\u043b\u0435\u0435 \u043f\u043e\u0434\u0440\u043e\u0431\u043d\u043e\u0439 \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u0438." + }, + "step": { + "user": { + "description": "\u0412\u044b \u0443\u0432\u0435\u0440\u0435\u043d\u044b, \u0447\u0442\u043e \u0445\u043e\u0442\u0438\u0442\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c GPSLogger?", + "title": "GPSLogger" + } + }, + "title": "GPSLogger" + } +} \ No newline at end of file diff --git a/homeassistant/components/gpslogger/.translations/sl.json b/homeassistant/components/gpslogger/.translations/sl.json new file mode 100644 index 00000000000..8e205bef437 --- /dev/null +++ b/homeassistant/components/gpslogger/.translations/sl.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "not_internet_accessible": "Va\u0161 Home Assistant mora biti dostopek prek interneta, da boste lahko prejemali GPSlogger sporo\u010dila.", + "one_instance_allowed": "Potrebna je samo ena instanca." + }, + "create_entry": { + "default": "\u010ce \u017eelite dogodke poslati v Home Assistant, morate v GPSLoggerju nastaviti funkcijo webhook. \n\n Izpolnite naslednje podatke: \n\n - URL: ` {webhook_url} ` \n - Metoda: POST \n\n Za ve\u010d podrobnosti si oglejte [dokumentacijo] ( {docs_url} )." + }, + "step": { + "user": { + "description": "Ali ste prepri\u010dani, da \u017eelite nastaviti GPSloggerWebhook?", + "title": "Nastavite GPSlogger Webhook" + } + }, + "title": "GPSLogger Webhook" + } +} \ No newline at end of file diff --git a/homeassistant/components/gpslogger/.translations/zh-Hans.json b/homeassistant/components/gpslogger/.translations/zh-Hans.json new file mode 100644 index 00000000000..dd5db73f582 --- /dev/null +++ b/homeassistant/components/gpslogger/.translations/zh-Hans.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "one_instance_allowed": "\u53ea\u6709\u4e00\u4e2a\u5b9e\u4f8b\u662f\u5fc5\u9700\u7684\u3002" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/gpslogger/.translations/zh-Hant.json b/homeassistant/components/gpslogger/.translations/zh-Hant.json new file mode 100644 index 00000000000..c9d98da1afc --- /dev/null +++ b/homeassistant/components/gpslogger/.translations/zh-Hant.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "not_internet_accessible": "Home Assistant \u5be6\u4f8b\u5fc5\u9808\u80fd\u5920\u7531\u7db2\u969b\u7db2\u8def\u5b58\u53d6\uff0c\u65b9\u80fd\u63a5\u53d7 GPSLogger \u8a0a\u606f\u3002", + "one_instance_allowed": "\u50c5\u9700\u8a2d\u5b9a\u4e00\u7d44\u7269\u4ef6\u5373\u53ef\u3002" + }, + "create_entry": { + "default": "\u6b32\u50b3\u9001\u4e8b\u4ef6\u81f3 Home Assistant\uff0c\u5c07\u9700\u65bc GPSLogger \u5167\u8a2d\u5b9a webhook \u529f\u80fd\u3002\n\n\u8acb\u586b\u5beb\u4e0b\u5217\u8cc7\u8a0a\uff1a\n\n- URL: `{webhook_url}`\n- Method: POST\n\n\u8acb\u53c3\u95b1 [\u6587\u4ef6]({docs_url})\u4ee5\u4e86\u89e3\u66f4\u8a73\u7d30\u8cc7\u6599\u3002" + }, + "step": { + "user": { + "description": "\u662f\u5426\u8981\u8a2d\u5b9a GPSLogger Webhook\uff1f", + "title": "\u8a2d\u5b9a GPSLogger Webhook" + } + }, + "title": "GPSLogger Webhook" + } +} \ No newline at end of file diff --git a/homeassistant/components/homematicip_cloud/.translations/sl.json b/homeassistant/components/homematicip_cloud/.translations/sl.json index eabb31ac833..cdde0f12d78 100644 --- a/homeassistant/components/homematicip_cloud/.translations/sl.json +++ b/homeassistant/components/homematicip_cloud/.translations/sl.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "Dostopna to\u010dka je \u017ee konfigurirana", + "already_configured": "Dostopna to\u010dka je \u017ee nastavljena", "connection_aborted": "Povezava s stre\u017enikom HMIP ni bila mogo\u010da", "unknown": "Pri\u0161lo je do neznane napake" }, @@ -21,8 +21,8 @@ "title": "Izberite dostopno to\u010dko HomematicIP" }, "link": { - "description": "Pritisnite modro tipko na dostopni to\u010dko in gumb po\u0161lji, da registrirate homematicIP s Home Assistentom. \n\n![Location of button on bridge](/static/images/config_flows/config_homematicip_cloud.png)", - "title": "Pove\u017eite dostopno to\u010dno" + "description": "Pritisnite modro tipko na dostopni to\u010dko in gumb po\u0161lji, da registrirate homematicIP s Home Assistantom. \n\n![Location of button on bridge](/static/images/config_flows/config_homematicip_cloud.png)", + "title": "Pove\u017eite dostopno to\u010dko" } }, "title": "HomematicIP Cloud" diff --git a/homeassistant/components/homematicip_cloud/.translations/zh-Hans.json b/homeassistant/components/homematicip_cloud/.translations/zh-Hans.json index 629ee4347fe..4c2b6268eec 100644 --- a/homeassistant/components/homematicip_cloud/.translations/zh-Hans.json +++ b/homeassistant/components/homematicip_cloud/.translations/zh-Hans.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "\u63a5\u5165\u70b9\u5df2\u7ecf\u914d\u7f6e\u5b8c\u6210", + "already_configured": "\u63a5\u5165\u70b9\u5df2\u914d\u7f6e", "connection_aborted": "\u65e0\u6cd5\u8fde\u63a5\u5230 HMIP \u670d\u52a1\u5668", "unknown": "\u53d1\u751f\u672a\u77e5\u9519\u8bef\u3002" }, diff --git a/homeassistant/components/hue/.translations/pl.json b/homeassistant/components/hue/.translations/pl.json index 784fa0d99a6..63cbbe016a2 100644 --- a/homeassistant/components/hue/.translations/pl.json +++ b/homeassistant/components/hue/.translations/pl.json @@ -24,6 +24,6 @@ "title": "Hub Link" } }, - "title": "Mostek Philips Hue" + "title": "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 05d52d5c37e..7ad7a2e6ade 100644 --- a/homeassistant/components/hue/.translations/sl.json +++ b/homeassistant/components/hue/.translations/sl.json @@ -20,7 +20,7 @@ "title": "Izberite Hue most" }, "link": { - "description": "Pritisnite gumb na mostu, da registrirate Philips Hue s Home Assistentom. \n\n ! [Polo\u017eaj gumba na mostu] (/static/images/config_philips_hue.jpg)", + "description": "Pritisnite gumb na mostu, da registrirate Philips Hue s Home Assistantom. \n\n ! [Polo\u017eaj gumba na mostu] (/static/images/config_philips_hue.jpg)", "title": "Link Hub" } }, diff --git a/homeassistant/components/ifttt/.translations/sl.json b/homeassistant/components/ifttt/.translations/sl.json index f5cc1dc572e..efb966880eb 100644 --- a/homeassistant/components/ifttt/.translations/sl.json +++ b/homeassistant/components/ifttt/.translations/sl.json @@ -1,11 +1,11 @@ { "config": { "abort": { - "not_internet_accessible": "Va\u0161 Home Assistent mora biti dostopek prek interneta, da boste lahko prejemali IFTTT sporo\u010dila.", + "not_internet_accessible": "Va\u0161 Home Assistant mora biti dostopek prek interneta, da boste lahko prejemali IFTTT sporo\u010dila.", "one_instance_allowed": "Potrebna je samo ena instanca." }, "create_entry": { - "default": "\u010ce \u017eelite poslati dogodke Home Assistent-u, boste morali uporabiti akcijo \u00bbNaredi spletno zahtevo\u00ab iz orodja [IFTTT Webhook applet] ( {applet_url} ). \n\n Izpolnite naslednje podatke: \n\n - URL: ` {webhook_url} ` \n - Metoda: POST \n - Vrsta vsebine: application/json \n\n Poglejte si [dokumentacijo] ( {docs_url} ) o tem, kako konfigurirati avtomatizacijo za obdelavo dohodnih podatkov." + "default": "\u010ce \u017eelite poslati dogodke Home Assistant-u, boste morali uporabiti akcijo \u00bbNaredi spletno zahtevo\u00ab iz orodja [IFTTT Webhook applet] ( {applet_url} ). \n\n Izpolnite naslednje podatke: \n\n - URL: ` {webhook_url} ` \n - Metoda: POST \n - Vrsta vsebine: application/json \n\n Poglejte si [dokumentacijo] ( {docs_url} ) o tem, kako konfigurirati avtomatizacijo za obdelavo dohodnih podatkov." }, "step": { "user": { diff --git a/homeassistant/components/locative/.translations/ca.json b/homeassistant/components/locative/.translations/ca.json new file mode 100644 index 00000000000..a08907a51ef --- /dev/null +++ b/homeassistant/components/locative/.translations/ca.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "not_internet_accessible": "La inst\u00e0ncia de Home Assistant ha de ser accessible des d'Internet per rebre missatges de Geofency.", + "one_instance_allowed": "Nom\u00e9s cal una sola inst\u00e0ncia." + }, + "create_entry": { + "default": "Per enviar ubicacions a Home Assistant, haur\u00e0s de configurar l'opci\u00f3 webhook de l'aplicaci\u00f3 Locative.\n\nCompleta la seg\u00fcent informaci\u00f3:\n\n- URL: `{webhook_url}` \n- M\u00e8tode: POST \n\nConsulta la [documentaci\u00f3]({docs_url}) per a m\u00e9s detalls." + }, + "step": { + "user": { + "description": "Est\u00e0s segur que vols configurar el Webhook Locative?", + "title": "Configuraci\u00f3 del Webhook Locative" + } + }, + "title": "Webhook Locative" + } +} \ No newline at end of file diff --git a/homeassistant/components/locative/.translations/en.json b/homeassistant/components/locative/.translations/en.json index b2a538a0fa5..052557408d8 100644 --- a/homeassistant/components/locative/.translations/en.json +++ b/homeassistant/components/locative/.translations/en.json @@ -1,18 +1,18 @@ { - "config": { - "title": "Locative Webhook", - "step": { - "user": { - "title": "Set up the Locative Webhook", - "description": "Are you sure you want to set up the Locative Webhook?" - } - }, - "abort": { - "one_instance_allowed": "Only a single instance is necessary.", - "not_internet_accessible": "Your Home Assistant instance needs to be accessible from the internet to receive messages from Geofency." - }, - "create_entry": { - "default": "To send locations to Home Assistant, you will need to setup the webhook feature in the Locative app.\n\nFill in the following info:\n\n- URL: `{webhook_url}`\n- Method: POST\n\nSee [the documentation]({docs_url}) for further details." + "config": { + "abort": { + "not_internet_accessible": "Your Home Assistant instance needs to be accessible from the internet to receive messages from Geofency.", + "one_instance_allowed": "Only a single instance is necessary." + }, + "create_entry": { + "default": "To send locations to Home Assistant, you will need to setup the webhook feature in the Locative app.\n\nFill in the following info:\n\n- URL: `{webhook_url}`\n- Method: POST\n\nSee [the documentation]({docs_url}) for further details." + }, + "step": { + "user": { + "description": "Are you sure you want to set up the Locative Webhook?", + "title": "Set up the Locative Webhook" + } + }, + "title": "Locative Webhook" } - } } \ No newline at end of file diff --git a/homeassistant/components/locative/.translations/ko.json b/homeassistant/components/locative/.translations/ko.json new file mode 100644 index 00000000000..a57b27cdd75 --- /dev/null +++ b/homeassistant/components/locative/.translations/ko.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "not_internet_accessible": "Locative \uba54\uc2dc\uc9c0\ub97c \ubc1b\uc73c\ub824\uba74 \uc778\ud130\ub137\uc5d0\uc11c Home Assistant \uc778\uc2a4\ud134\uc2a4\uc5d0 \uc561\uc138\uc2a4 \ud560 \uc218 \uc788\uc5b4\uc57c \ud569\ub2c8\ub2e4.", + "one_instance_allowed": "\ud558\ub098\uc758 \uc778\uc2a4\ud134\uc2a4\ub9cc \ud544\uc694\ud569\ub2c8\ub2e4." + }, + "create_entry": { + "default": "Home Assistant \ub85c \uc774\ubca4\ud2b8\ub97c \ubcf4\ub0b4\ub824\uba74 Locative \uc571\uc5d0\uc11c Webhook \uc744 \uc124\uc815\ud574\uc57c\ud569\ub2c8\ub2e4. \n\n\ub2e4\uc74c \uc815\ubcf4\ub97c \uc785\ub825\ud574 \uc8fc\uc138\uc694. \n\n - URL: `{webhook_url}`\n - Method: POST\n \n \uc790\uc138\ud55c \uc815\ubcf4\ub294 [\uc548\ub0b4]({docs_url}) \ub97c \ucc38\uc870\ud574 \uc8fc\uc138\uc694." + }, + "step": { + "user": { + "description": "Locative Webhook \uc744 \uc124\uc815 \ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?", + "title": "Locative Webhook \uc124\uc815" + } + }, + "title": "Locative Webhook" + } +} \ No newline at end of file diff --git a/homeassistant/components/locative/.translations/lb.json b/homeassistant/components/locative/.translations/lb.json new file mode 100644 index 00000000000..25db0ecef81 --- /dev/null +++ b/homeassistant/components/locative/.translations/lb.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "not_internet_accessible": "\u00c4r Home Assistant Instanz muss iwwert Internet accessibel si fir Geofency Noriichten z'empf\u00e4nken.", + "one_instance_allowed": "N\u00ebmmen eng eenzeg Instanz ass n\u00e9ideg." + }, + "create_entry": { + "default": "Fir Plazen un Home Assistant ze sch\u00e9cken, muss den Webhook Feature an der Locative App ageriicht ginn.\n\nF\u00ebllt folgend Informatiounen aus:\n\n- URL: `{webhook_url}`\n- Method: POST\n\nLiest [Dokumentatioun]({docs_url}) fir w\u00e9ider D\u00e9tailer." + }, + "step": { + "user": { + "description": "S\u00e9cher fir Locative Webhook anzeriichten?", + "title": "Locative Webhook ariichten" + } + }, + "title": "Locative Webhook" + } +} \ No newline at end of file diff --git a/homeassistant/components/locative/.translations/no.json b/homeassistant/components/locative/.translations/no.json new file mode 100644 index 00000000000..00e3337dfe1 --- /dev/null +++ b/homeassistant/components/locative/.translations/no.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "not_internet_accessible": "Home Assistant m\u00e5 v\u00e6re tilgjengelig fra internett for \u00e5 kunne motta meldinger fra Geofency.", + "one_instance_allowed": "Kun en enkelt forekomst er n\u00f8dvendig." + }, + "create_entry": { + "default": "For \u00e5 kunne sende steder til Home Assistant, m\u00e5 du sette opp webhook-funksjonen i Locative. \n\n Fyll ut f\u00f8lgende informasjon: \n\n - URL: `{webhook_url}` \n - Metode: POST \n\n Se [dokumentasjonen]({docs_url}) for ytterligere detaljer." + }, + "step": { + "user": { + "description": "Er du sikker p\u00e5 at du vil sette opp Locative Webhook?", + "title": "Sett opp Lokative Webhook" + } + }, + "title": "Lokative Webhook" + } +} \ No newline at end of file diff --git a/homeassistant/components/locative/.translations/pl.json b/homeassistant/components/locative/.translations/pl.json new file mode 100644 index 00000000000..89f6881593a --- /dev/null +++ b/homeassistant/components/locative/.translations/pl.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "not_internet_accessible": "Twoja instancja Home Assistant musi by\u0107 dost\u0119pna z Internetu, aby otrzymywa\u0107 wiadomo\u015bci z Geofency.", + "one_instance_allowed": "Wymagana jest tylko jedna instancja." + }, + "create_entry": { + "default": "Aby wysy\u0142a\u0107 lokalizacje do Home Assistant'a, musisz skonfigurowa\u0107 webhook w aplikacji Locative. \n\n Wprowad\u017a nast\u0119puj\u0105ce dane:\n\n - URL: `{webhook_url}` \n - Metoda: POST \n\nZapoznaj si\u0119 z [dokumentacj\u0105]({docs_url}) by pozna\u0107 szczeg\u00f3\u0142y." + }, + "step": { + "user": { + "description": "Czy na pewno chcesz skonfigurowa\u0107 Locative Webhook?", + "title": "Skonfiguruj Locative Webhook" + } + }, + "title": "Locative Webhook" + } +} \ No newline at end of file diff --git a/homeassistant/components/locative/.translations/ru.json b/homeassistant/components/locative/.translations/ru.json new file mode 100644 index 00000000000..d8b8d55a608 --- /dev/null +++ b/homeassistant/components/locative/.translations/ru.json @@ -0,0 +1,17 @@ +{ + "config": { + "abort": { + "not_internet_accessible": "\u0412\u0430\u0448 Home Assistant \u0434\u043e\u043b\u0436\u0435\u043d \u0431\u044b\u0442\u044c \u0434\u043e\u0441\u0442\u0443\u043f\u0435\u043d \u0438\u0437 \u0438\u043d\u0442\u0435\u0440\u043d\u0435\u0442\u0430 \u0434\u043b\u044f \u043f\u043e\u043b\u0443\u0447\u0435\u043d\u0438\u044f \u0441\u043e\u043e\u0431\u0449\u0435\u043d\u0438\u0439 Locative." + }, + "create_entry": { + "default": "\u0414\u043b\u044f \u043e\u0442\u043f\u0440\u0430\u0432\u043a\u0438 \u0441\u043e\u0431\u044b\u0442\u0438\u0439 \u0432 Home Assistant \u0432\u044b \u0434\u043e\u043b\u0436\u043d\u044b \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c webhooks \u0434\u043b\u044f Locative.\n\n\u0414\u043b\u044f \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0439\u0442\u0435 \u0441\u043b\u0435\u0434\u0443\u044e\u0449\u0443\u044e \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u044e:\n\n- URL: `{webhook_url}`\n- Method: POST\n\n\u041e\u0437\u043d\u0430\u043a\u043e\u043c\u044c\u0442\u0435\u0441\u044c \u0441 [\u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0430\u0446\u0438\u0435\u0439]({docs_url}) \u0434\u043b\u044f \u043f\u043e\u043b\u0443\u0447\u0435\u043d\u0438\u044f \u0431\u043e\u043b\u0435\u0435 \u043f\u043e\u0434\u0440\u043e\u0431\u043d\u043e\u0439 \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u0438." + }, + "step": { + "user": { + "description": "\u0412\u044b \u0443\u0432\u0435\u0440\u0435\u043d\u044b, \u0447\u0442\u043e \u0445\u043e\u0442\u0438\u0442\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c Locative?", + "title": "Locative" + } + }, + "title": "Locative" + } +} \ No newline at end of file diff --git a/homeassistant/components/locative/.translations/sl.json b/homeassistant/components/locative/.translations/sl.json new file mode 100644 index 00000000000..0b0bd45b7d6 --- /dev/null +++ b/homeassistant/components/locative/.translations/sl.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "not_internet_accessible": "Va\u0161 Home Assistant mora biti dostopek prek interneta, da boste lahko prejemali Geofency sporo\u010dila.", + "one_instance_allowed": "Potrebna je samo ena instanca." + }, + "create_entry": { + "default": "Za po\u0161iljanje lokacij v Home Assistant, morate namestiti funkcijo webhook v aplikaciji Locative. \n\n Izpolnite naslednje podatke: \n\n - URL: ` {webhook_url} ` \n - Metoda: POST \n\n Za ve\u010d podrobnosti si oglejte [dokumentacijo] ( {docs_url} )." + }, + "step": { + "user": { + "description": "Ali ste prepri\u010dani, da \u017eelite nastaviti Locative Webhook?", + "title": "Nastavite Locative Webhook" + } + }, + "title": "Locative Webhook" + } +} \ No newline at end of file diff --git a/homeassistant/components/locative/.translations/zh-Hans.json b/homeassistant/components/locative/.translations/zh-Hans.json new file mode 100644 index 00000000000..d98793d96e5 --- /dev/null +++ b/homeassistant/components/locative/.translations/zh-Hans.json @@ -0,0 +1,15 @@ +{ + "config": { + "abort": { + "not_internet_accessible": "\u60a8\u7684Home Assistant\u5b9e\u4f8b\u9700\u8981\u53ef\u4ee5\u4eceInternet\u8bbf\u95ee\u4ee5\u63a5\u6536\u6765\u81eaGeofency\u7684\u6d88\u606f\u3002", + "one_instance_allowed": "\u53ea\u9700\u8981\u4e00\u4e2a\u5b9e\u4f8b\u3002" + }, + "step": { + "user": { + "description": "\u60a8\u786e\u5b9a\u8981\u8bbe\u7f6e\u5b9a\u4f4d Webhook\u5417\uff1f", + "title": "\u8bbe\u7f6e\u5b9a\u4f4d Webhook" + } + }, + "title": "\u5b9a\u4f4d Webhook" + } +} \ No newline at end of file diff --git a/homeassistant/components/locative/.translations/zh-Hant.json b/homeassistant/components/locative/.translations/zh-Hant.json new file mode 100644 index 00000000000..62bb6bb9d96 --- /dev/null +++ b/homeassistant/components/locative/.translations/zh-Hant.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "not_internet_accessible": "Home Assistant \u5be6\u4f8b\u5fc5\u9808\u80fd\u5920\u7531\u7db2\u969b\u7db2\u8def\u5b58\u53d6\uff0c\u65b9\u80fd\u63a5\u53d7 Locative \u8a0a\u606f\u3002", + "one_instance_allowed": "\u50c5\u9700\u8a2d\u5b9a\u4e00\u7d44\u7269\u4ef6\u5373\u53ef\u3002" + }, + "create_entry": { + "default": "\u6b32\u50b3\u9001\u4f4d\u7f6e\u81f3 Home Assistant\uff0c\u5c07\u9700\u65bc Locative App \u5167\u8a2d\u5b9a webhook \u529f\u80fd\u3002\n\n\u8acb\u586b\u5beb\u4e0b\u5217\u8cc7\u8a0a\uff1a\n\n- URL: `{webhook_url}`\n- Method: POST\n\n\u8acb\u53c3\u95b1 [\u6587\u4ef6]({docs_url})\u4ee5\u4e86\u89e3\u66f4\u8a73\u7d30\u8cc7\u6599\u3002" + }, + "step": { + "user": { + "description": "\u662f\u5426\u8981\u8a2d\u5b9a Locative Webhook\uff1f", + "title": "\u8a2d\u5b9a Locative Webhook" + } + }, + "title": "Locative Webhook" + } +} \ No newline at end of file diff --git a/homeassistant/components/mailgun/.translations/sl.json b/homeassistant/components/mailgun/.translations/sl.json index 4eb12d7343c..2f526826d31 100644 --- a/homeassistant/components/mailgun/.translations/sl.json +++ b/homeassistant/components/mailgun/.translations/sl.json @@ -1,11 +1,11 @@ { "config": { "abort": { - "not_internet_accessible": " \u010ce \u017eelite prejemati sporo\u010dila Mailgun, mora biti Home Assistent dostopen prek interneta.", + "not_internet_accessible": "\u010ce \u017eelite prejemati sporo\u010dila Mailgun, mora biti Home Assistant dostopen prek interneta.", "one_instance_allowed": "Potrebna je samo ena instanca." }, "create_entry": { - "default": "Za po\u0161iljanje dogodkov Home Assistentu boste morali nastaviti [Webhooks z Mailgun]({mailgun_url}).\n\nIzpolnite naslednje informacije:\n\n- URL: `{webhook_url}`\n- Metoda: POST\n- Vrsta vsebine: application/json\n\nGlej [dokumentacijo]({docs_url}) o tem, kako nastavite automations za obravnavo dohodnih podatkov." + "default": "Za po\u0161iljanje dogodkov Home Assistantu boste morali nastaviti [Webhooks z Mailgun]({mailgun_url}).\n\nIzpolnite naslednje informacije:\n\n- URL: `{webhook_url}`\n- Metoda: POST\n- Vrsta vsebine: application/json\n\nGlej [dokumentacijo]({docs_url}) o tem, kako nastavite automations za obravnavo dohodnih podatkov." }, "step": { "user": { diff --git a/homeassistant/components/mailgun/.translations/zh-Hans.json b/homeassistant/components/mailgun/.translations/zh-Hans.json index 06c1d3624f4..5dd0a7aeabf 100644 --- a/homeassistant/components/mailgun/.translations/zh-Hans.json +++ b/homeassistant/components/mailgun/.translations/zh-Hans.json @@ -6,6 +6,13 @@ }, "create_entry": { "default": "\u8981\u5411 Home Assistant \u53d1\u9001\u4e8b\u4ef6\uff0c\u60a8\u9700\u8981\u914d\u7f6e [Mailgun \u7684 Webhook]({mailgun_url})\u3002\n\n\u586b\u5199\u4ee5\u4e0b\u4fe1\u606f\uff1a\n\n- URL: `{webhook_url}`\n- Method: POST\n- Content Type: application/json\n\n\u6709\u5173\u5982\u4f55\u914d\u7f6e\u81ea\u52a8\u5316\u4ee5\u5904\u7406\u4f20\u5165\u7684\u6570\u636e\uff0c\u8bf7\u53c2\u9605[\u6587\u6863]({docs_url})\u3002" - } + }, + "step": { + "user": { + "description": "\u60a8\u786e\u5b9a\u8981\u8bbe\u7f6e Mailgun \u5417\uff1f", + "title": "\u8bbe\u7f6e Mailgun Webhook" + } + }, + "title": "Mailgun" } } \ No newline at end of file diff --git a/homeassistant/components/mqtt/.translations/sl.json b/homeassistant/components/mqtt/.translations/sl.json index d8d331449a2..0050d1b040d 100644 --- a/homeassistant/components/mqtt/.translations/sl.json +++ b/homeassistant/components/mqtt/.translations/sl.json @@ -22,7 +22,7 @@ "data": { "discovery": "Omogo\u010di odkrivanje" }, - "description": "\u017delite konfigurirati Home Assistent-a za povezavo s posrednikom MQTT, ki ga ponuja hass.io add-on {addon} ?", + "description": "\u017delite konfigurirati Home Assistant-a za povezavo s posrednikom MQTT, ki ga ponuja hass.io add-on {addon} ?", "title": "MQTT Broker prek dodatka Hass.io" } }, diff --git a/homeassistant/components/nest/.translations/zh-Hans.json b/homeassistant/components/nest/.translations/zh-Hans.json index 05ba5bdf155..0b5cbc989fd 100644 --- a/homeassistant/components/nest/.translations/zh-Hans.json +++ b/homeassistant/components/nest/.translations/zh-Hans.json @@ -24,8 +24,8 @@ "data": { "code": "PIN \u7801" }, - "description": "\u8981\u5173\u8054 Nest \u5e10\u6237\uff0c\u8bf7[\u6388\u6743\u5e10\u6237]({url})\u3002\n\n\u5b8c\u6210\u6388\u6743\u540e\uff0c\u5728\u4e0b\u9762\u7c98\u8d34\u83b7\u5f97\u7684 PIN \u7801\u3002", - "title": "\u5173\u8054 Nest \u5e10\u6237" + "description": "\u8981\u5173\u8054 Nest \u8d26\u6237\uff0c\u8bf7[\u6388\u6743\u8d26\u6237]({url})\u3002\n\n\u5b8c\u6210\u6388\u6743\u540e\uff0c\u5728\u4e0b\u9762\u7c98\u8d34\u83b7\u5f97\u7684 PIN \u7801\u3002", + "title": "\u5173\u8054 Nest \u8d26\u6237" } }, "title": "Nest" diff --git a/homeassistant/components/openuv/.translations/ca.json b/homeassistant/components/openuv/.translations/ca.json index 5cb9a8ce5a5..ad2f391886a 100644 --- a/homeassistant/components/openuv/.translations/ca.json +++ b/homeassistant/components/openuv/.translations/ca.json @@ -2,12 +2,12 @@ "config": { "error": { "identifier_exists": "Les coordenades ja estan registrades", - "invalid_api_key": "Contrasenya API no v\u00e0lida" + "invalid_api_key": "Clau API no v\u00e0lida" }, "step": { "user": { "data": { - "api_key": "Contrasenya API d'OpenUV", + "api_key": "Clau API d'OpenUV", "elevation": "Elevaci\u00f3", "latitude": "Latitud", "longitude": "Longitud" diff --git a/homeassistant/components/point/.translations/zh-Hans.json b/homeassistant/components/point/.translations/zh-Hans.json index 6b5cb91cfeb..ebd2b88b10e 100644 --- a/homeassistant/components/point/.translations/zh-Hans.json +++ b/homeassistant/components/point/.translations/zh-Hans.json @@ -1,8 +1,17 @@ { "config": { + "abort": { + "authorize_url_fail": "\u751f\u6210\u6388\u6743URL\u65f6\u53d1\u751f\u672a\u77e5\u9519\u8bef\u3002", + "authorize_url_timeout": "\u751f\u6210\u6388\u6743URL\u8d85\u65f6" + }, + "error": { + "follow_link": "\u8bf7\u5728\u70b9\u51fb\u63d0\u4ea4\u524d\u6309\u7167\u94fe\u63a5\u8fdb\u884c\u8eab\u4efd\u9a8c\u8bc1", + "no_token": "\u672a\u7ecfMinut\u9a8c\u8bc1" + }, "step": { "auth": { - "description": "\u8bf7\u8bbf\u95ee\u4e0b\u65b9\u7684\u94fe\u63a5\u5e76\u5141\u8bb8\u8bbf\u95ee\u60a8\u7684 Minut \u8d26\u6237\uff0c\u7136\u540e\u56de\u6765\u70b9\u51fb\u4e0b\u9762\u7684\u63d0\u4ea4\u3002\n\n[\u94fe\u63a5]({authorization_url})" + "description": "\u8bf7\u8bbf\u95ee\u4e0b\u65b9\u7684\u94fe\u63a5\u5e76\u5141\u8bb8\u8bbf\u95ee\u60a8\u7684 Minut \u8d26\u6237\uff0c\u7136\u540e\u56de\u6765\u70b9\u51fb\u4e0b\u9762\u7684\u63d0\u4ea4\u3002\n\n[\u94fe\u63a5]({authorization_url})", + "title": "\u8ba4\u8bc1\u70b9" }, "user": { "data": { diff --git a/homeassistant/components/rainmachine/.translations/zh-Hans.json b/homeassistant/components/rainmachine/.translations/zh-Hans.json index 7c6f07a7edd..e7171ca2867 100644 --- a/homeassistant/components/rainmachine/.translations/zh-Hans.json +++ b/homeassistant/components/rainmachine/.translations/zh-Hans.json @@ -1,11 +1,13 @@ { "config": { "error": { - "identifier_exists": "\u5e10\u6237\u5df2\u6ce8\u518c" + "identifier_exists": "\u8d26\u6237\u5df2\u6ce8\u518c", + "invalid_credentials": "\u65e0\u6548\u7684\u8eab\u4efd\u8ba4\u8bc1" }, "step": { "user": { "data": { + "ip_address": "\u4e3b\u673a\u540d\u6216IP\u5730\u5740", "password": "\u5bc6\u7801", "port": "\u7aef\u53e3" }, diff --git a/homeassistant/components/simplisafe/.translations/zh-Hans.json b/homeassistant/components/simplisafe/.translations/zh-Hans.json index 2316f5c7454..4c57baea77f 100644 --- a/homeassistant/components/simplisafe/.translations/zh-Hans.json +++ b/homeassistant/components/simplisafe/.translations/zh-Hans.json @@ -1,7 +1,7 @@ { "config": { "error": { - "identifier_exists": "\u5e10\u6237\u5df2\u6ce8\u518c", + "identifier_exists": "\u8d26\u6237\u5df2\u6ce8\u518c", "invalid_credentials": "\u65e0\u6548\u7684\u8eab\u4efd\u8ba4\u8bc1" }, "step": { diff --git a/homeassistant/components/smartthings/.translations/ca.json b/homeassistant/components/smartthings/.translations/ca.json new file mode 100644 index 00000000000..3c0ca05a8d5 --- /dev/null +++ b/homeassistant/components/smartthings/.translations/ca.json @@ -0,0 +1,27 @@ +{ + "config": { + "error": { + "app_not_installed": "Assegura't que has instal\u00b7lat i autoritzat l'aplicaci\u00f3 SmartApp de Home Assistant i torna-ho a provar.", + "app_setup_error": "No s'ha pogut configurar SmartApp. Siusplau, torna-ho a provar.", + "base_url_not_https": "L'`base_url` per al component `http` ha d'estar configurat i comen\u00e7ar amb `https://`.", + "token_already_setup": "El testimoni d'autenticaci\u00f3 ja ha estat configurat.", + "token_forbidden": "El testimoni d'autenticaci\u00f3 no t\u00e9 cont\u00e9 els apartats OAuth obligatoris.", + "token_invalid_format": "El testimoni d'autenticaci\u00f3 ha d'estar en format UID/GUID", + "token_unauthorized": "El testimoni d'autenticaci\u00f3 no \u00e9s v\u00e0lid o ja no t\u00e9 autoritzaci\u00f3." + }, + "step": { + "user": { + "data": { + "access_token": "Testimoni d'acc\u00e9s" + }, + "description": "Introdueix un [testimoni d'autenticaci\u00f3 d'acc\u00e9s personal] ({token_url}) de SmartThings que s'ha creat a trav\u00e9s les [instruccions] ({component_url}).", + "title": "Introdueix el testimoni d'autenticaci\u00f3 d'acc\u00e9s personal" + }, + "wait_install": { + "description": "Instal\u00b7la l'SmartApp de Home Assistant en almenys una ubicaci\u00f3 i fes clic a Enviar.", + "title": "Instal\u00b7laci\u00f3 de SmartApp" + } + }, + "title": "SmartThings" + } +} \ No newline at end of file diff --git a/homeassistant/components/smartthings/.translations/en.json b/homeassistant/components/smartthings/.translations/en.json index 1fb4e878cb4..f2775b30ae2 100644 --- a/homeassistant/components/smartthings/.translations/en.json +++ b/homeassistant/components/smartthings/.translations/en.json @@ -1,27 +1,27 @@ { - "config": { - "title": "SmartThings", - "step": { - "user": { - "title": "Enter Personal Access Token", - "description": "Please enter a SmartThings [Personal Access Token]({token_url}) that has been created per the [instructions]({component_url}).", - "data": { - "access_token": "Access Token" - } - }, - "wait_install": { - "title": "Install SmartApp", - "description": "Please install the Home Assistant SmartApp in at least one location and click submit." - } - }, - "error": { - "token_invalid_format": "The token must be in the UID/GUID format", - "token_unauthorized": "The token is invalid or no longer authorized.", - "token_forbidden": "The token does not have the required OAuth scopes.", - "token_already_setup": "The token has already been setup.", - "app_setup_error": "Unable to setup the SmartApp. Please try again.", - "app_not_installed": "Please ensure you have installed and authorized the Home Assistant SmartApp and try again.", - "base_url_not_https": "The `base_url` for the `http` component must be configured and start with `https://`." + "config": { + "error": { + "app_not_installed": "Please ensure you have installed and authorized the Home Assistant SmartApp and try again.", + "app_setup_error": "Unable to setup the SmartApp. Please try again.", + "base_url_not_https": "The `base_url` for the `http` component must be configured and start with `https://`.", + "token_already_setup": "The token has already been setup.", + "token_forbidden": "The token does not have the required OAuth scopes.", + "token_invalid_format": "The token must be in the UID/GUID format", + "token_unauthorized": "The token is invalid or no longer authorized." + }, + "step": { + "user": { + "data": { + "access_token": "Access Token" + }, + "description": "Please enter a SmartThings [Personal Access Token]({token_url}) that has been created per the [instructions]({component_url}).", + "title": "Enter Personal Access Token" + }, + "wait_install": { + "description": "Please install the Home Assistant SmartApp in at least one location and click submit.", + "title": "Install SmartApp" + } + }, + "title": "SmartThings" } - } } \ No newline at end of file diff --git a/homeassistant/components/smartthings/.translations/lb.json b/homeassistant/components/smartthings/.translations/lb.json new file mode 100644 index 00000000000..fd59d187314 --- /dev/null +++ b/homeassistant/components/smartthings/.translations/lb.json @@ -0,0 +1,27 @@ +{ + "config": { + "error": { + "app_not_installed": "Stellt w.e.g s\u00e9cher dass d'Home Assistant SmartApp install\u00e9iert an autoris\u00e9iert ass, a prob\u00e9iert nach emol.", + "app_setup_error": "Kann SmartApp net install\u00e9ieren. Prob\u00e9iert w.e.g. nach emol.", + "base_url_not_https": "`base_url` fir den `http` Komponent muss konfigur\u00e9iert sinn a mat `https://`uf\u00e4nken.", + "token_already_setup": "Den Jeton gouf schonn ageriicht.", + "token_forbidden": "De Jeton huet net d\u00e9i n\u00e9ideg OAuth M\u00e9iglechkeeten.", + "token_invalid_format": "De Jeton muss am UID/GUID Format sinn", + "token_unauthorized": "De Jeton ass ong\u00eblteg oder net m\u00e9i autoris\u00e9iert." + }, + "step": { + "user": { + "data": { + "access_token": "Acc\u00e8ss Jeton" + }, + "description": "Gitt w.e.g. ee [Pers\u00e9inlechen Acc\u00e8s Jeton]({token_url}) vu SmartThings an dee via [d'Instruktiounen] ({component_url}) erstallt gouf.", + "title": "Pers\u00e9inlechen Acc\u00e8ss Jeton uginn" + }, + "wait_install": { + "description": "Install\u00e9iert d'Home Assistant SmartApp op mannst ee mol a klickt op Ofsch\u00e9cken.", + "title": "SmartApp install\u00e9ieren" + } + }, + "title": "SmartThings" + } +} \ No newline at end of file diff --git a/homeassistant/components/smartthings/.translations/pl.json b/homeassistant/components/smartthings/.translations/pl.json new file mode 100644 index 00000000000..379cdf699b7 --- /dev/null +++ b/homeassistant/components/smartthings/.translations/pl.json @@ -0,0 +1,10 @@ +{ + "config": { + "step": { + "wait_install": { + "title": "Zainstaluj SmartApp" + } + }, + "title": "SmartThings" + } +} \ No newline at end of file diff --git a/homeassistant/components/smartthings/.translations/zh-Hant.json b/homeassistant/components/smartthings/.translations/zh-Hant.json new file mode 100644 index 00000000000..952eafec60c --- /dev/null +++ b/homeassistant/components/smartthings/.translations/zh-Hant.json @@ -0,0 +1,27 @@ +{ + "config": { + "error": { + "app_not_installed": "\u8acb\u78ba\u8a8d\u5df2\u7d93\u5b89\u88dd\u4e26\u6388\u6b0a Home Assistant Smartapp \u5f8c\u518d\u8a66\u4e00\u6b21\u3002", + "app_setup_error": "\u7121\u6cd5\u8a2d\u5b9a SmartApp\uff0c\u8acb\u518d\u8a66\u4e00\u6b21\u3002", + "base_url_not_https": "\u5fc5\u9808\u8a2d\u5b9a\u300chttp\u300d\u5143\u4ef6\u4e4b\u300cbase_url\u300d\uff0c\u4e26\u4ee5\u300chttps://\u300d\u70ba\u958b\u982d\u3002", + "token_already_setup": "\u5bc6\u9470\u5df2\u8a2d\u5b9a\u904e\u3002", + "token_forbidden": "\u5bc6\u9470\u4e0d\u5177\u6240\u9700\u7684 OAuth \u7bc4\u570d\u3002", + "token_invalid_format": "\u5bc6\u9470\u5fc5\u9808\u70ba UID/GUID \u683c\u5f0f", + "token_unauthorized": "\u5bc6\u9470\u7121\u6548\u6216\u4e0d\u518d\u5177\u6709\u6388\u6b0a\u3002" + }, + "step": { + "user": { + "data": { + "access_token": "\u5b58\u53d6\u5bc6\u9470" + }, + "description": "\u8acb\u8f38\u5165\u8ddf\u8457[ \u6307\u5f15]({component_url})\u6240\u7522\u751f\u7684 SmartThings [\u500b\u4eba\u5b58\u53d6\u5bc6\u9470]({token_url})\u3002", + "title": "\u8f38\u5165\u500b\u4eba\u5b58\u53d6\u5bc6\u9470" + }, + "wait_install": { + "description": "\u8acb\u81f3\u5c11\u65bc\u4e00\u500b\u4f4d\u7f6e\u4e2d\u5b89\u88dd Home Assistant Smartapp\uff0c\u4e26\u9ede\u9078\u50b3\u9001\u3002", + "title": "\u5b89\u88dd SmartApp" + } + }, + "title": "SmartThings" + } +} \ No newline at end of file diff --git a/homeassistant/components/tellduslive/.translations/ca.json b/homeassistant/components/tellduslive/.translations/ca.json index db97b1ad6d8..75915735882 100644 --- a/homeassistant/components/tellduslive/.translations/ca.json +++ b/homeassistant/components/tellduslive/.translations/ca.json @@ -2,10 +2,14 @@ "config": { "abort": { "all_configured": "TelldusLive ja est\u00e0 configurat", + "already_setup": "TelldusLive ja est\u00e0 configurat", "authorize_url_fail": "S'ha produ\u00eft un error desconegut al generar l'URL d'autoritzaci\u00f3.", "authorize_url_timeout": "S'ha acabat el temps d'espera mentre \u00e9s generava l'URL d'autoritzaci\u00f3.", "unknown": "S'ha produ\u00eft un error desconegut" }, + "error": { + "auth_error": "Error d'autenticaci\u00f3, torna-ho a provar" + }, "step": { "auth": { "description": "Passos per enlla\u00e7ar el teu compte de TelldusLive:\n 1. Clica l'enlla\u00e7 de sota.\n 2. Inicia sessi\u00f3 a Telldus Live.\n 3. Autoritza **{app_name}** (clica **Yes**).\n 4. Torna aqu\u00ed i clica **SUBMIT**.\n \n [Enlla\u00e7 al compte de TelldusLive]({auth_url})", diff --git a/homeassistant/components/tellduslive/.translations/en.json b/homeassistant/components/tellduslive/.translations/en.json index 4ed9ef597f4..c2b00561858 100644 --- a/homeassistant/components/tellduslive/.translations/en.json +++ b/homeassistant/components/tellduslive/.translations/en.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "all_configured": "TelldusLive is already configured", "already_setup": "TelldusLive is already configured", "authorize_url_fail": "Unknown error generating an authorize url.", "authorize_url_timeout": "Timeout generating authorize url.", diff --git a/homeassistant/components/tellduslive/.translations/es.json b/homeassistant/components/tellduslive/.translations/es.json new file mode 100644 index 00000000000..4e7de72edc4 --- /dev/null +++ b/homeassistant/components/tellduslive/.translations/es.json @@ -0,0 +1,10 @@ +{ + "config": { + "abort": { + "already_setup": "TelldusLive ya est\u00e1 configurado" + }, + "error": { + "auth_error": "Error de autenticaci\u00f3n, por favor int\u00e9ntalo de nuevo" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tellduslive/.translations/ko.json b/homeassistant/components/tellduslive/.translations/ko.json index a7b68bbf8be..29f64a87cb3 100644 --- a/homeassistant/components/tellduslive/.translations/ko.json +++ b/homeassistant/components/tellduslive/.translations/ko.json @@ -2,10 +2,14 @@ "config": { "abort": { "all_configured": "TelldusLive \uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4", + "already_setup": "TelldusLive \uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4", "authorize_url_fail": "\uc778\uc99d url \uc0dd\uc131\uc5d0 \uc54c \uc218 \uc5c6\ub294 \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4.", "authorize_url_timeout": "\uc778\uc99d url \uc0dd\uc131 \uc2dc\uac04\uc774 \ucd08\uacfc\ub418\uc5c8\uc2b5\ub2c8\ub2e4.", "unknown": "\uc54c \uc218\uc5c6\ub294 \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4" }, + "error": { + "auth_error": "\uc778\uc99d\uc774 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4. \ub2e4\uc2dc \uc2dc\ub3c4\ud574 \uc8fc\uc138\uc694." + }, "step": { "auth": { "description": "TelldusLive \uacc4\uc815\uc744 \uc5f0\uacb0\ud558\ub824\uba74:\n 1. \ud558\ub2e8\uc758 \ub9c1\ud06c\ub97c \ud074\ub9ad\ud574\uc8fc\uc138\uc694\n 2. Telldus Live \uc5d0 \ub85c\uadf8\uc778 \ud558\uc138\uc694\n 3. Authorize **{app_name}** (**Yes** \ub97c \ud074\ub9ad\ud558\uc138\uc694).\n 4. \ub2e4\uc2dc \uc5ec\uae30\ub85c \ub3cc\uc544\uc640\uc11c **SUBMIT** \uc744 \ud074\ub9ad\ud558\uc138\uc694.\n\n [TelldusLive \uacc4\uc815 \uc5f0\uacb0\ud558\uae30]({auth_url})", diff --git a/homeassistant/components/tellduslive/.translations/lb.json b/homeassistant/components/tellduslive/.translations/lb.json index 85de49776c1..5eb4d1b978a 100644 --- a/homeassistant/components/tellduslive/.translations/lb.json +++ b/homeassistant/components/tellduslive/.translations/lb.json @@ -2,10 +2,14 @@ "config": { "abort": { "all_configured": "TelldusLive ass scho konfigur\u00e9iert", + "already_setup": "TelldusLive ass scho konfigur\u00e9iert", "authorize_url_fail": "Onbekannte Feeler beim gener\u00e9ieren vun der Autorisatiouns URL.", "authorize_url_timeout": "Z\u00e4it Iwwerschreidung beim gener\u00e9ieren vun der Autorisatiouns URL.", "unknown": "Onbekannten Fehler opgetrueden" }, + "error": { + "auth_error": "Feeler bei der Authentifikatioun, prob\u00e9iert w.e.g. nach emol" + }, "step": { "auth": { "description": "Fir den TelldusLive Kont ze verbannen:\n1. Klickt op de Link \u00ebnnen\n2. Verbannt iech mat TelldusLive\n3. Autoris\u00e9iert **{app_name}** (klickt **Yes**)\n4. Kommt heihinner zer\u00e9ck a klickt **Ofsch\u00e9cken**\n\n[Tellduslive Kont verbannen]({auth_url})", diff --git a/homeassistant/components/tellduslive/.translations/no.json b/homeassistant/components/tellduslive/.translations/no.json index 5c3d343dd03..2c6439b364f 100644 --- a/homeassistant/components/tellduslive/.translations/no.json +++ b/homeassistant/components/tellduslive/.translations/no.json @@ -2,10 +2,14 @@ "config": { "abort": { "all_configured": "TelldusLive er allerede konfigurert", + "already_setup": "TelldusLive er allerede konfigurert", "authorize_url_fail": "Ukjent feil ved generering av autoriseringsadresse.", "authorize_url_timeout": "Tidsavbrudd ved generering av autoriseringsadresse.", "unknown": "Ukjent feil oppstod" }, + "error": { + "auth_error": "Autentiseringsfeil, vennligst pr\u00f8v igjen" + }, "step": { "auth": { "description": "For \u00e5 koble TelldusLive-kontoen din:\n 1. Klikk p\u00e5 linken under\n 2. Logg inn p\u00e5 Telldus Live \n 3. Tillat **{app_name}** (klikk**Ja**). \n 4. Kom tilbake hit og klikk **SUBMIT**. \n\n [Link TelldusLive-konto]({auth_url})", diff --git a/homeassistant/components/tellduslive/.translations/pl.json b/homeassistant/components/tellduslive/.translations/pl.json index 5ee9ac221a7..9d791e0e786 100644 --- a/homeassistant/components/tellduslive/.translations/pl.json +++ b/homeassistant/components/tellduslive/.translations/pl.json @@ -2,10 +2,14 @@ "config": { "abort": { "all_configured": "TelldusLive jest ju\u017c skonfigurowany", + "already_setup": "TelldusLive jest ju\u017c skonfigurowany", "authorize_url_fail": "Nieznany b\u0142\u0105d podczas generowania url autoryzacji.", "authorize_url_timeout": "Min\u0105\u0142 limit czasu generowania url autoryzacji.", "unknown": "Wyst\u0105pi\u0142 nieznany b\u0142\u0105d" }, + "error": { + "auth_error": "B\u0142\u0105d uwierzytelniania, spr\u00f3buj ponownie" + }, "step": { "auth": { "description": "Aby po\u0142\u0105czy\u0107 konto TelldusLive: \n 1. Kliknij poni\u017cszy link \n 2. Zaloguj si\u0119 do Telldus Live \n 3. Autoryzuj **{app_name}** (kliknij **Tak**). \n 4. Wr\u00f3\u0107 tutaj i kliknij **SUBMIT**. \n\n [Link do konta TelldusLive]({auth_url})", diff --git a/homeassistant/components/tellduslive/.translations/ru.json b/homeassistant/components/tellduslive/.translations/ru.json index 2e319b9400b..80dff6dc88a 100644 --- a/homeassistant/components/tellduslive/.translations/ru.json +++ b/homeassistant/components/tellduslive/.translations/ru.json @@ -2,10 +2,14 @@ "config": { "abort": { "all_configured": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u043a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430.", + "already_setup": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u043a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430.", "authorize_url_fail": "\u041d\u0435\u0438\u0437\u0432\u0435\u0441\u0442\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430 \u043f\u0440\u0438 \u0433\u0435\u043d\u0435\u0440\u0430\u0446\u0438\u0438 \u0441\u0441\u044b\u043b\u043a\u0438 \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0438\u0438.", "authorize_url_timeout": "\u0418\u0441\u0442\u0435\u043a\u043b\u043e \u0432\u0440\u0435\u043c\u044f \u0433\u0435\u043d\u0435\u0440\u0430\u0446\u0438\u0438 \u0441\u0441\u044b\u043b\u043a\u0438 \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0438\u0438.", "unknown": "\u041d\u0435\u0438\u0437\u0432\u0435\u0441\u0442\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430" }, + "error": { + "auth_error": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438, \u043f\u043e\u0436\u0430\u043b\u0443\u0439\u0441\u0442\u0430, \u043f\u043e\u0432\u0442\u043e\u0440\u0438\u0442\u0435 \u043f\u043e\u043f\u044b\u0442\u043a\u0443" + }, "step": { "auth": { "description": "\u0414\u043b\u044f \u0442\u043e\u0433\u043e, \u0447\u0442\u043e\u0431\u044b \u043f\u0440\u0438\u0432\u044f\u0437\u0430\u0442\u044c \u0430\u043a\u043a\u0430\u0443\u043d\u0442 TelldusLive:\n 1. \u041f\u0435\u0440\u0435\u0439\u0434\u0438\u0442\u0435 \u043f\u043e \u0441\u0441\u044b\u043b\u043a\u0435, \u0443\u043a\u0430\u0437\u0430\u043d\u043d\u043e\u0439 \u043d\u0438\u0436\u0435\n 2. \u0412\u043e\u0439\u0434\u0438\u0442\u0435 \u0432 Telldus Live\n 3. Authorize **{app_name}** (\u043d\u0430\u0436\u043c\u0438\u0442\u0435 **Yes**).\n 4. \u0412\u0435\u0440\u043d\u0438\u0442\u0435\u0441\u044c \u0441\u044e\u0434\u0430 \u0438 \u043d\u0430\u0436\u043c\u0438\u0442\u0435 **\u041f\u041e\u0414\u0422\u0412\u0415\u0420\u0414\u0418\u0422\u042c**.\n\n [\u0421\u0441\u044b\u043b\u043a\u0430 \u043d\u0430 TelldusLive]({auth_url})", diff --git a/homeassistant/components/tellduslive/.translations/sl.json b/homeassistant/components/tellduslive/.translations/sl.json index f4b9f0fda98..16e6ddcb5f4 100644 --- a/homeassistant/components/tellduslive/.translations/sl.json +++ b/homeassistant/components/tellduslive/.translations/sl.json @@ -2,10 +2,14 @@ "config": { "abort": { "all_configured": "TelldusLive je \u017ee konfiguriran", + "already_setup": "TelldusLive je \u017ee konfiguriran", "authorize_url_fail": "Neznana napaka pri generiranju potrditvenega URL-ja.", "authorize_url_timeout": "\u010casovna omejitev za generiranje URL-ja je potekla.", "unknown": "Pri\u0161lo je do neznane napake" }, + "error": { + "auth_error": "Napaka pri preverjanju pristnosti, poskusite znova" + }, "step": { "auth": { "description": "\u010ce \u017eelite povezati svoj ra\u010dun TelldusLive: \n 1. Kliknite spodnjo povezavo \n 2. Prijavite se v Telldus Live \n 3. Dovolite ** {app_name} ** (kliknite ** Da **). \n 4. Pridi nazaj in kliknite ** SUBMIT **. \n\n [Link TelldusLive ra\u010dun] ( {auth_url} )", diff --git a/homeassistant/components/tellduslive/.translations/zh-Hans.json b/homeassistant/components/tellduslive/.translations/zh-Hans.json new file mode 100644 index 00000000000..f707b1f15f8 --- /dev/null +++ b/homeassistant/components/tellduslive/.translations/zh-Hans.json @@ -0,0 +1,24 @@ +{ + "config": { + "abort": { + "all_configured": "Tellduslive \u5df2\u914d\u7f6e\u5b8c\u6210", + "authorize_url_fail": "\u751f\u6210\u6388\u6743URL\u65f6\u53d1\u751f\u672a\u77e5\u9519\u8bef\u3002", + "authorize_url_timeout": "\u751f\u6210\u6388\u6743URL\u8d85\u65f6", + "unknown": "\u53d1\u751f\u672a\u77e5\u7684\u9519\u8bef" + }, + "error": { + "auth_error": "\u53cc\u91cd\u8ba4\u8bc1\u5931\u8d25\uff0c\u8bf7\u91cd\u8bd5\u3002" + }, + "step": { + "auth": { + "description": "\u8981\u94fe\u63a5\u60a8\u7684TelldusLive\u8d26\u6237\uff1a \n 1.\u5355\u51fb\u4e0b\u9762\u7684\u94fe\u63a5\n 2.\u767b\u5f55Telldus Live \n 3.\u6388\u6743 **{app_name}** (\u70b9\u51fb **\u662f**)\u3002 \n 4.\u56de\u5230\u8fd9\u91cc\uff0c\u7136\u540e\u70b9\u51fb**\u63d0\u4ea4**\u3002 \n\n [TelldusLive\u8d26\u6237\u94fe\u63a5]({auth_url})" + }, + "user": { + "data": { + "host": "\u4e3b\u673a" + }, + "description": "\u7a7a\u767d" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tellduslive/.translations/zh-Hant.json b/homeassistant/components/tellduslive/.translations/zh-Hant.json index a5e3c652c0c..c632b543634 100644 --- a/homeassistant/components/tellduslive/.translations/zh-Hant.json +++ b/homeassistant/components/tellduslive/.translations/zh-Hant.json @@ -2,10 +2,14 @@ "config": { "abort": { "all_configured": "TelldusLive \u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "already_setup": "TelldusLive \u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", "authorize_url_fail": "\u7522\u751f\u8a8d\u8b49 URL \u6642\u767c\u751f\u672a\u77e5\u932f\u8aa4", "authorize_url_timeout": "\u7522\u751f\u8a8d\u8b49 URL \u6642\u903e\u6642", "unknown": "\u767c\u751f\u672a\u77e5\u932f\u8aa4\u3002" }, + "error": { + "auth_error": "\u8a8d\u8b49\u932f\u8aa4\uff0c\u8acb\u518d\u8a66\u4e00\u6b21\u3002" + }, "step": { "auth": { "description": "\u6b32\u9023\u7d50 TelldusLive \u5e33\u865f\uff1a\n 1. \u9ede\u9078\u4e0b\u65b9\u9023\u7d50\n 2. \u767b\u5165\u81f3 Telldus Live\n 3. \u5c0d **{app_name}** \u9032\u884c\u6388\u6b0a\uff08\u9ede\u9078 **Yes**\uff09\u3002\n 4. \u56de\u5230\u672c\u9801\u9762\u4e26\u9ede\u9078 **\u50b3\u9001**\u3002\n\n [Link TelldusLive account]({auth_url})", diff --git a/homeassistant/components/twilio/.translations/sl.json b/homeassistant/components/twilio/.translations/sl.json index 0321cb05452..86d2c44f11c 100644 --- a/homeassistant/components/twilio/.translations/sl.json +++ b/homeassistant/components/twilio/.translations/sl.json @@ -1,11 +1,11 @@ { "config": { "abort": { - "not_internet_accessible": " \u010ce \u017eelite prejemati sporo\u010dila Twilio, mora biti Home Assistent dostopen prek interneta.", + "not_internet_accessible": "\u010ce \u017eelite prejemati sporo\u010dila Twilio, mora biti Home Assistant dostopen prek interneta.", "one_instance_allowed": "Potrebna je samo ena instanca." }, "create_entry": { - "default": "Za po\u0161iljanje dogodkov Home Assistent-u, boste morali nastaviti [Webhooks z Twilio]({twilio_url}).\n\nIzpolnite naslednje informacije:\n\n- URL: `{webhook_url}`\n- Metoda: POST\n- Vrsta vsebine: application/x-www-form-urlencoded\n\nGlej [dokumentacijo]({docs_url}) o tem, kako nastavite avtomatizacijo za obravnavo dohodnih podatkov." + "default": "Za po\u0161iljanje dogodkov Home Assistant-u, boste morali nastaviti [Webhooks z Twilio]({twilio_url}).\n\nIzpolnite naslednje informacije:\n\n- URL: `{webhook_url}`\n- Metoda: POST\n- Vrsta vsebine: application/x-www-form-urlencoded\n\nGlej [dokumentacijo]({docs_url}) o tem, kako nastavite avtomatizacijo za obravnavo dohodnih podatkov." }, "step": { "user": { diff --git a/homeassistant/components/twilio/.translations/zh-Hans.json b/homeassistant/components/twilio/.translations/zh-Hans.json index e108fe12498..6fda9f0143c 100644 --- a/homeassistant/components/twilio/.translations/zh-Hans.json +++ b/homeassistant/components/twilio/.translations/zh-Hans.json @@ -1,7 +1,18 @@ { "config": { + "abort": { + "not_internet_accessible": "\u60a8\u7684 Home Assistant \u5b9e\u4f8b\u9700\u8981\u63a5\u5165\u4e92\u8054\u7f51\u4ee5\u63a5\u6536 Twilio \u6d88\u606f\u3002", + "one_instance_allowed": "\u4ec5\u9700\u4e00\u4e2a\u5b9e\u4f8b" + }, "create_entry": { "default": "\u8981\u5411 Home Assistant \u53d1\u9001\u4e8b\u4ef6\uff0c\u60a8\u9700\u8981\u914d\u7f6e [Twilio \u7684 Webhook]({twilio_url})\u3002\n\n\u586b\u5199\u4ee5\u4e0b\u4fe1\u606f\uff1a\n\n- URL: `{webhook_url}`\n- Method: POST\n- Content Type: application/json\n\n\u6709\u5173\u5982\u4f55\u914d\u7f6e\u81ea\u52a8\u5316\u4ee5\u5904\u7406\u4f20\u5165\u7684\u6570\u636e\uff0c\u8bf7\u53c2\u9605[\u6587\u6863]({docs_url})\u3002" - } + }, + "step": { + "user": { + "description": "\u60a8\u786e\u5b9a\u8981\u8bbe\u7f6e Twilio \u5417\uff1f", + "title": "\u8bbe\u7f6e Twilio Webhook" + } + }, + "title": "Twilio" } } \ No newline at end of file diff --git a/homeassistant/components/upnp/.translations/sl.json b/homeassistant/components/upnp/.translations/sl.json index 4bf6501bd2a..4c019d8f207 100644 --- a/homeassistant/components/upnp/.translations/sl.json +++ b/homeassistant/components/upnp/.translations/sl.json @@ -24,7 +24,7 @@ }, "user": { "data": { - "enable_port_mapping": "Omogo\u010dajo preslikavo vrat (port mapping) za Home Assistent-a", + "enable_port_mapping": "Omogo\u010dajo preslikavo vrat (port mapping) za Home Assistant-a", "enable_sensors": "Dodaj prometne senzorje", "igd": "UPnP/IGD" }, diff --git a/homeassistant/components/upnp/.translations/zh-Hans.json b/homeassistant/components/upnp/.translations/zh-Hans.json index b16172e97d7..2194a2dc264 100644 --- a/homeassistant/components/upnp/.translations/zh-Hans.json +++ b/homeassistant/components/upnp/.translations/zh-Hans.json @@ -4,9 +4,15 @@ "already_configured": "UPnP/IGD \u5df2\u914d\u7f6e\u5b8c\u6210", "incomplete_device": "\u5ffd\u7565\u4e0d\u5b8c\u6574\u7684 UPnP \u8bbe\u5907", "no_devices_discovered": "\u672a\u53d1\u73b0 UPnP/IGD", - "no_sensors_or_port_mapping": "\u81f3\u5c11\u542f\u7528\u4f20\u611f\u5668\u6216\u7aef\u53e3\u6620\u5c04" + "no_devices_found": "\u6ca1\u6709\u5728\u7f51\u7edc\u4e0a\u627e\u5230 UPnP/IGD \u8bbe\u5907\u3002", + "no_sensors_or_port_mapping": "\u81f3\u5c11\u542f\u7528\u4f20\u611f\u5668\u6216\u7aef\u53e3\u6620\u5c04", + "single_instance_allowed": "UPnP/IGD \u53ea\u9700\u8981\u914d\u7f6e\u4e00\u6b21\u3002" }, "step": { + "confirm": { + "description": "\u60a8\u60f3\u8981\u914d\u7f6e UPnP/IGD \u5417\uff1f", + "title": "UPnP/IGD" + }, "init": { "title": "UPnP/IGD" }, diff --git a/homeassistant/components/zha/.translations/zh-Hans.json b/homeassistant/components/zha/.translations/zh-Hans.json index 8befb2ee114..ce458fa32f1 100644 --- a/homeassistant/components/zha/.translations/zh-Hans.json +++ b/homeassistant/components/zha/.translations/zh-Hans.json @@ -1,11 +1,20 @@ { "config": { + "abort": { + "single_instance_allowed": "\u53ea\u5141\u8bb8\u4e00\u4e2a ZHA \u914d\u7f6e\u3002" + }, + "error": { + "cannot_connect": "\u65e0\u6cd5\u8fde\u63a5\u5230 ZHA \u8bbe\u5907\u3002" + }, "step": { "user": { "data": { "usb_path": "USB \u8bbe\u5907\u8def\u5f84" - } + }, + "description": "\u7a7a\u767d", + "title": "ZHA" } - } + }, + "title": "ZHA" } } \ No newline at end of file From 8988ee5b346652b4f93f97e39f9913052b12695c Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sat, 2 Feb 2019 14:03:13 -0800 Subject: [PATCH 195/222] Updated frontend to 20190202.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 8f3afef16cd..b5836e67ffc 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==20190201.0'] +REQUIREMENTS = ['home-assistant-frontend==20190202.0'] DOMAIN = 'frontend' DEPENDENCIES = ['api', 'websocket_api', 'http', 'system_log', diff --git a/requirements_all.txt b/requirements_all.txt index 22bed82f26b..ea8afeb6a09 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -526,7 +526,7 @@ hole==0.3.0 holidays==0.9.9 # homeassistant.components.frontend -home-assistant-frontend==20190201.0 +home-assistant-frontend==20190202.0 # homeassistant.components.zwave homeassistant-pyozw==0.1.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index ec2c9153137..4ebef8bc1e6 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -113,7 +113,7 @@ hdate==0.8.7 holidays==0.9.9 # homeassistant.components.frontend -home-assistant-frontend==20190201.0 +home-assistant-frontend==20190202.0 # homeassistant.components.homekit_controller homekit==0.12.2 From 785b42ecde6460ba0835b5cf3a01b898854764d9 Mon Sep 17 00:00:00 2001 From: Andrew Sayre <6730289+andrewsayre@users.noreply.github.com> Date: Sat, 2 Feb 2019 09:12:24 -0600 Subject: [PATCH 196/222] Add SmartThings Light platform (#20652) * Add SmartThings Light platform and tests * Cleaned a few awk comments * Updates per review feedback * Switched to super * Changes per review feedback --- homeassistant/components/smartthings/const.py | 1 + homeassistant/components/smartthings/light.py | 215 +++++++++++++ tests/components/smartthings/test_light.py | 293 ++++++++++++++++++ tests/components/smartthings/test_switch.py | 17 +- 4 files changed, 518 insertions(+), 8 deletions(-) create mode 100644 homeassistant/components/smartthings/light.py create mode 100644 tests/components/smartthings/test_light.py diff --git a/homeassistant/components/smartthings/const.py b/homeassistant/components/smartthings/const.py index 9a6d96bfab9..a9f47fc7c72 100644 --- a/homeassistant/components/smartthings/const.py +++ b/homeassistant/components/smartthings/const.py @@ -18,6 +18,7 @@ SETTINGS_INSTANCE_ID = "hassInstanceId" STORAGE_KEY = DOMAIN STORAGE_VERSION = 1 SUPPORTED_PLATFORMS = [ + 'light', 'switch' ] SUPPORTED_CAPABILITIES = [ diff --git a/homeassistant/components/smartthings/light.py b/homeassistant/components/smartthings/light.py new file mode 100644 index 00000000000..8495be62a73 --- /dev/null +++ b/homeassistant/components/smartthings/light.py @@ -0,0 +1,215 @@ +""" +Support for lights through the SmartThings cloud API. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/smartthings.light/ +""" +import asyncio + +from homeassistant.components.light import ( + ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, ATTR_HS_COLOR, ATTR_TRANSITION, + SUPPORT_BRIGHTNESS, SUPPORT_COLOR, SUPPORT_COLOR_TEMP, SUPPORT_TRANSITION, + Light) +import homeassistant.util.color as color_util + +from . import SmartThingsEntity +from .const import DATA_BROKERS, DOMAIN + +DEPENDENCIES = ['smartthings'] + + +async def async_setup_platform( + hass, config, async_add_entities, discovery_info=None): + """Platform uses config entry setup.""" + pass + + +async def async_setup_entry(hass, config_entry, async_add_entities): + """Add lights for a config entry.""" + broker = hass.data[DOMAIN][DATA_BROKERS][config_entry.entry_id] + async_add_entities( + [SmartThingsLight(device) for device in broker.devices.values() + if is_light(device)], True) + + +def is_light(device): + """Determine if the device should be represented as a light.""" + from pysmartthings import Capability + + # Must be able to be turned on/off. + if Capability.switch not in device.capabilities: + return False + # Not a fan (which might also have switch_level) + if Capability.fan_speed in device.capabilities: + return False + # Must have one of these + light_capabilities = [ + Capability.color_control, + Capability.color_temperature, + Capability.switch_level + ] + if any(capability in device.capabilities + for capability in light_capabilities): + return True + return False + + +def convert_scale(value, value_scale, target_scale, round_digits=4): + """Convert a value to a different scale.""" + return round(value * target_scale / value_scale, round_digits) + + +class SmartThingsLight(SmartThingsEntity, Light): + """Define a SmartThings Light.""" + + def __init__(self, device): + """Initialize a SmartThingsLight.""" + super().__init__(device) + self._brightness = None + self._color_temp = None + self._hs_color = None + self._supported_features = self._determine_features() + + def _determine_features(self): + """Get features supported by the device.""" + from pysmartthings.device import Capability + + features = 0 + # Brightness and transition + if Capability.switch_level in self._device.capabilities: + features |= \ + SUPPORT_BRIGHTNESS | SUPPORT_TRANSITION + # Color Temperature + if Capability.color_temperature in self._device.capabilities: + features |= SUPPORT_COLOR_TEMP + # Color + if Capability.color_control in self._device.capabilities: + features |= SUPPORT_COLOR + + return features + + async def async_turn_on(self, **kwargs) -> None: + """Turn the light on.""" + tasks = [] + # Color temperature + if self._supported_features & SUPPORT_COLOR_TEMP \ + and ATTR_COLOR_TEMP in kwargs: + tasks.append(self.async_set_color_temp( + kwargs[ATTR_COLOR_TEMP])) + # Color + if self._supported_features & SUPPORT_COLOR \ + and ATTR_HS_COLOR in kwargs: + tasks.append(self.async_set_color( + kwargs[ATTR_HS_COLOR])) + if tasks: + # Set temp/color first + await asyncio.gather(*tasks) + + # Switch/brightness/transition + if self._supported_features & SUPPORT_BRIGHTNESS \ + and ATTR_BRIGHTNESS in kwargs: + await self.async_set_level( + kwargs[ATTR_BRIGHTNESS], + kwargs.get(ATTR_TRANSITION, 0)) + else: + await self._device.switch_on(set_status=True) + + # State is set optimistically in the commands above, therefore update + # the entity state ahead of receiving the confirming push updates + self.async_schedule_update_ha_state(True) + + async def async_turn_off(self, **kwargs) -> None: + """Turn the light off.""" + # Switch/transition + if self._supported_features & SUPPORT_TRANSITION \ + and ATTR_TRANSITION in kwargs: + await self.async_set_level(0, int(kwargs[ATTR_TRANSITION])) + else: + await self._device.switch_off(set_status=True) + + # State is set optimistically in the commands above, therefore update + # the entity state ahead of receiving the confirming push updates + self.async_schedule_update_ha_state(True) + + async def async_update(self): + """Update entity attributes when the device status has changed.""" + # Brightness and transition + if self._supported_features & SUPPORT_BRIGHTNESS: + self._brightness = convert_scale( + self._device.status.level, 100, 255) + # Color Temperature + if self._supported_features & SUPPORT_COLOR_TEMP: + self._color_temp = color_util.color_temperature_kelvin_to_mired( + self._device.status.color_temperature) + # Color + if self._supported_features & SUPPORT_COLOR: + self._hs_color = ( + convert_scale(self._device.status.hue, 100, 360), + self._device.status.saturation + ) + + async def async_set_color(self, hs_color): + """Set the color of the device.""" + hue = convert_scale(float(hs_color[0]), 360, 100) + hue = max(min(hue, 100.0), 0.0) + saturation = max(min(float(hs_color[1]), 100.0), 0.0) + await self._device.set_color( + hue, saturation, set_status=True) + + async def async_set_color_temp(self, value: float): + """Set the color temperature of the device.""" + kelvin = color_util.color_temperature_mired_to_kelvin(value) + kelvin = max(min(kelvin, 30000.0), 1.0) + await self._device.set_color_temperature( + kelvin, set_status=True) + + async def async_set_level(self, brightness: int, transition: int): + """Set the brightness of the light over transition.""" + level = int(convert_scale(brightness, 255, 100, 0)) + # Due to rounding, set level to 1 (one) so we don't inadvertently + # turn off the light when a low brightness is set. + level = 1 if level == 0 and brightness > 0 else level + level = max(min(level, 100), 0) + duration = int(transition) + await self._device.set_level(level, duration, set_status=True) + + @property + def brightness(self): + """Return the brightness of this light between 0..255.""" + return self._brightness + + @property + def color_temp(self): + """Return the CT color value in mireds.""" + return self._color_temp + + @property + def hs_color(self): + """Return the hue and saturation color value [float, float].""" + return self._hs_color + + @property + def is_on(self) -> bool: + """Return true if light is on.""" + return self._device.status.switch + + @property + def max_mireds(self): + """Return the warmest color_temp that this light supports.""" + # SmartThings does not expose this attribute, instead it's + # implemented within each device-type handler. This value is the + # lowest kelvin found supported across 20+ handlers. + return 500 # 2000K + + @property + def min_mireds(self): + """Return the coldest color_temp that this light supports.""" + # SmartThings does not expose this attribute, instead it's + # implemented within each device-type handler. This value is the + # highest kelvin found supported across 20+ handlers. + return 111 # 9000K + + @property + def supported_features(self) -> int: + """Flag supported features.""" + return self._supported_features diff --git a/tests/components/smartthings/test_light.py b/tests/components/smartthings/test_light.py new file mode 100644 index 00000000000..a4f1103f270 --- /dev/null +++ b/tests/components/smartthings/test_light.py @@ -0,0 +1,293 @@ +""" +Test for the SmartThings light platform. + +The only mocking required is of the underlying SmartThings API object so +real HTTP calls are not initiated during testing. +""" +from pysmartthings import Attribute, Capability +import pytest + +from homeassistant.components.light import ( + ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, ATTR_HS_COLOR, ATTR_TRANSITION, + SUPPORT_BRIGHTNESS, SUPPORT_COLOR, SUPPORT_COLOR_TEMP, SUPPORT_TRANSITION) +from homeassistant.components.smartthings import DeviceBroker, light +from homeassistant.components.smartthings.const import ( + DATA_BROKERS, DOMAIN, SIGNAL_SMARTTHINGS_UPDATE) +from homeassistant.config_entries import ( + CONN_CLASS_CLOUD_PUSH, SOURCE_USER, ConfigEntry) +from homeassistant.const import ATTR_ENTITY_ID, ATTR_SUPPORTED_FEATURES +from homeassistant.helpers.dispatcher import async_dispatcher_send + + +@pytest.fixture(name="light_devices") +def light_devices_fixture(device_factory): + """Fixture returns a set of mock light devices.""" + return [ + device_factory( + "Dimmer 1", + capabilities=[Capability.switch, Capability.switch_level], + status={Attribute.switch: 'on', Attribute.level: 100}), + device_factory( + "Color Dimmer 1", + capabilities=[Capability.switch, Capability.switch_level, + Capability.color_control], + status={Attribute.switch: 'off', Attribute.level: 0, + Attribute.hue: 76.0, Attribute.saturation: 55.0}), + device_factory( + "Color Dimmer 2", + capabilities=[Capability.switch, Capability.switch_level, + Capability.color_control, + Capability.color_temperature], + status={Attribute.switch: 'on', Attribute.level: 100, + Attribute.hue: 76.0, Attribute.saturation: 55.0, + Attribute.color_temperature: 4500}) + ] + + +async def _setup_platform(hass, *devices): + """Set up the SmartThings light platform and prerequisites.""" + hass.config.components.add(DOMAIN) + broker = DeviceBroker(hass, devices, '') + config_entry = ConfigEntry("1", DOMAIN, "Test", {}, + SOURCE_USER, CONN_CLASS_CLOUD_PUSH) + hass.data[DOMAIN] = { + DATA_BROKERS: { + config_entry.entry_id: broker + } + } + await hass.config_entries.async_forward_entry_setup(config_entry, 'light') + await hass.async_block_till_done() + return config_entry + + +async def test_async_setup_platform(): + """Test setup platform does nothing (it uses config entries).""" + await light.async_setup_platform(None, None, None) + + +def test_is_light(device_factory, light_devices): + """Test lights are correctly identified.""" + non_lights = [ + device_factory('Unknown', ['Unknown']), + device_factory("Fan 1", + [Capability.switch, Capability.switch_level, + Capability.fan_speed]), + device_factory("Switch 1", [Capability.switch]), + device_factory("Can't be turned off", + [Capability.switch_level, Capability.color_control, + Capability.color_temperature]) + ] + + for device in light_devices: + assert light.is_light(device), device.name + for device in non_lights: + assert not light.is_light(device), device.name + + +async def test_entity_state(hass, light_devices): + """Tests the state attributes properly match the light types.""" + await _setup_platform(hass, *light_devices) + + # Dimmer 1 + state = hass.states.get('light.dimmer_1') + assert state.state == 'on' + assert state.attributes[ATTR_SUPPORTED_FEATURES] == \ + SUPPORT_BRIGHTNESS | SUPPORT_TRANSITION + assert state.attributes[ATTR_BRIGHTNESS] == 255 + + # Color Dimmer 1 + state = hass.states.get('light.color_dimmer_1') + assert state.state == 'off' + assert state.attributes[ATTR_SUPPORTED_FEATURES] == \ + SUPPORT_BRIGHTNESS | SUPPORT_TRANSITION | SUPPORT_COLOR + + # Color Dimmer 2 + state = hass.states.get('light.color_dimmer_2') + assert state.state == 'on' + assert state.attributes[ATTR_SUPPORTED_FEATURES] == \ + SUPPORT_BRIGHTNESS | SUPPORT_TRANSITION | SUPPORT_COLOR | \ + SUPPORT_COLOR_TEMP + assert state.attributes[ATTR_BRIGHTNESS] == 255 + assert state.attributes[ATTR_HS_COLOR] == (273.6, 55.0) + assert state.attributes[ATTR_COLOR_TEMP] == 222 + + +async def test_entity_and_device_attributes(hass, device_factory): + """Test the attributes of the entity are correct.""" + # Arrange + device = device_factory( + "Light 1", [Capability.switch, Capability.switch_level]) + entity_registry = await hass.helpers.entity_registry.async_get_registry() + device_registry = await hass.helpers.device_registry.async_get_registry() + # Act + await _setup_platform(hass, device) + # Assert + entry = entity_registry.async_get("light.light_1") + assert entry + assert entry.unique_id == device.device_id + + entry = device_registry.async_get_device( + {(DOMAIN, device.device_id)}, []) + assert entry + assert entry.name == device.label + assert entry.model == device.device_type_name + assert entry.manufacturer == 'Unavailable' + + +async def test_turn_off(hass, light_devices): + """Test the light turns of successfully.""" + # Arrange + await _setup_platform(hass, *light_devices) + # Act + await hass.services.async_call( + 'light', 'turn_off', {'entity_id': 'light.color_dimmer_2'}, + blocking=True) + # Assert + state = hass.states.get('light.color_dimmer_2') + assert state is not None + assert state.state == 'off' + + +async def test_turn_off_with_transition(hass, light_devices): + """Test the light turns of successfully with transition.""" + # Arrange + await _setup_platform(hass, *light_devices) + # Act + await hass.services.async_call( + 'light', 'turn_off', + {ATTR_ENTITY_ID: "light.color_dimmer_2", ATTR_TRANSITION: 2}, + blocking=True) + # Assert + state = hass.states.get("light.color_dimmer_2") + assert state is not None + assert state.state == 'off' + + +async def test_turn_on(hass, light_devices): + """Test the light turns of successfully.""" + # Arrange + await _setup_platform(hass, *light_devices) + # Act + await hass.services.async_call( + 'light', 'turn_on', {ATTR_ENTITY_ID: "light.color_dimmer_1"}, + blocking=True) + # Assert + state = hass.states.get("light.color_dimmer_1") + assert state is not None + assert state.state == 'on' + + +async def test_turn_on_with_brightness(hass, light_devices): + """Test the light turns on to the specified brightness.""" + # Arrange + await _setup_platform(hass, *light_devices) + # Act + await hass.services.async_call( + 'light', 'turn_on', + {ATTR_ENTITY_ID: "light.color_dimmer_1", + ATTR_BRIGHTNESS: 75, ATTR_TRANSITION: 2}, + blocking=True) + # Assert + state = hass.states.get("light.color_dimmer_1") + assert state is not None + assert state.state == 'on' + # round-trip rounding error (expected) + assert state.attributes[ATTR_BRIGHTNESS] == 73.95 + + +async def test_turn_on_with_minimal_brightness(hass, light_devices): + """ + Test lights set to lowest brightness when converted scale would be zero. + + SmartThings light brightness is a percentage (0-100), but HASS uses a + 0-255 scale. This tests if a really low value (1-2) is passed, we don't + set the level to zero, which turns off the lights in SmartThings. + """ + # Arrange + await _setup_platform(hass, *light_devices) + # Act + await hass.services.async_call( + 'light', 'turn_on', + {ATTR_ENTITY_ID: "light.color_dimmer_1", + ATTR_BRIGHTNESS: 2}, + blocking=True) + # Assert + state = hass.states.get("light.color_dimmer_1") + assert state is not None + assert state.state == 'on' + # round-trip rounding error (expected) + assert state.attributes[ATTR_BRIGHTNESS] == 2.55 + + +async def test_turn_on_with_color(hass, light_devices): + """Test the light turns on with color.""" + # Arrange + await _setup_platform(hass, *light_devices) + # Act + await hass.services.async_call( + 'light', 'turn_on', + {ATTR_ENTITY_ID: "light.color_dimmer_2", + ATTR_HS_COLOR: (180, 50)}, + blocking=True) + # Assert + state = hass.states.get("light.color_dimmer_2") + assert state is not None + assert state.state == 'on' + assert state.attributes[ATTR_HS_COLOR] == (180, 50) + + +async def test_turn_on_with_color_temp(hass, light_devices): + """Test the light turns on with color temp.""" + # Arrange + await _setup_platform(hass, *light_devices) + # Act + await hass.services.async_call( + 'light', 'turn_on', + {ATTR_ENTITY_ID: "light.color_dimmer_2", + ATTR_COLOR_TEMP: 300}, + blocking=True) + # Assert + state = hass.states.get("light.color_dimmer_2") + assert state is not None + assert state.state == 'on' + assert state.attributes[ATTR_COLOR_TEMP] == 300 + + +async def test_update_from_signal(hass, device_factory): + """Test the light updates when receiving a signal.""" + # Arrange + device = device_factory( + "Color Dimmer 2", + capabilities=[Capability.switch, Capability.switch_level, + Capability.color_control, Capability.color_temperature], + status={Attribute.switch: 'off', Attribute.level: 100, + Attribute.hue: 76.0, Attribute.saturation: 55.0, + Attribute.color_temperature: 4500}) + await _setup_platform(hass, device) + await device.switch_on(True) + # Act + async_dispatcher_send(hass, SIGNAL_SMARTTHINGS_UPDATE, + [device.device_id]) + # Assert + await hass.async_block_till_done() + state = hass.states.get('light.color_dimmer_2') + assert state is not None + assert state.state == 'on' + + +async def test_unload_config_entry(hass, device_factory): + """Test the light is removed when the config entry is unloaded.""" + # Arrange + device = device_factory( + "Color Dimmer 2", + capabilities=[Capability.switch, Capability.switch_level, + Capability.color_control, Capability.color_temperature], + status={Attribute.switch: 'off', Attribute.level: 100, + Attribute.hue: 76.0, Attribute.saturation: 55.0, + Attribute.color_temperature: 4500}) + config_entry = await _setup_platform(hass, device) + # Act + await hass.config_entries.async_forward_entry_unload( + config_entry, 'light') + # Assert + assert not hass.states.get('light.color_dimmer_2') diff --git a/tests/components/smartthings/test_switch.py b/tests/components/smartthings/test_switch.py index 7bf8b15af51..a8013105291 100644 --- a/tests/components/smartthings/test_switch.py +++ b/tests/components/smartthings/test_switch.py @@ -62,15 +62,16 @@ async def test_entity_and_device_attributes(hass, device_factory): # Act await _setup_platform(hass, device) # Assert - entity = entity_registry.async_get('switch.switch_1') - assert entity - assert entity.unique_id == device.device_id - device_entry = device_registry.async_get_device( + entry = entity_registry.async_get('switch.switch_1') + assert entry + assert entry.unique_id == device.device_id + + entry = device_registry.async_get_device( {(DOMAIN, device.device_id)}, []) - assert device_entry - assert device_entry.name == device.label - assert device_entry.model == device.device_type_name - assert device_entry.manufacturer == 'Unavailable' + assert entry + assert entry.name == device.label + assert entry.model == device.device_type_name + assert entry.manufacturer == 'Unavailable' async def test_turn_off(hass, device_factory): From c0fd22c2856d6015cef97b3d786b3a68116b53f0 Mon Sep 17 00:00:00 2001 From: Rohan Kapoor Date: Fri, 1 Feb 2019 00:40:27 -0800 Subject: [PATCH 197/222] Fix allow extra in locative webhook schema validation (#20657) * Allow extra in locative webhook schema validation (fixes #20566) * Remove extra attribute --- homeassistant/components/locative/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/locative/__init__.py b/homeassistant/components/locative/__init__.py index 195eacf17c2..1f7f9c3a686 100644 --- a/homeassistant/components/locative/__init__.py +++ b/homeassistant/components/locative/__init__.py @@ -48,8 +48,8 @@ WEBHOOK_SCHEMA = vol.All( vol.Required(ATTR_LONGITUDE): cv.longitude, vol.Required(ATTR_DEVICE_ID): cv.string, vol.Required(ATTR_TRIGGER): cv.string, - vol.Optional(ATTR_ID): vol.All(cv.string, _id) - }), + vol.Optional(ATTR_ID): vol.All(cv.string, _id), + }, extra=vol.ALLOW_EXTRA), _validate_test_mode ) From b669e1498a3060b35e3dc3fe7d1271ffb9a1aca9 Mon Sep 17 00:00:00 2001 From: Andrew Sayre <6730289+andrewsayre@users.noreply.github.com> Date: Sat, 2 Feb 2019 16:04:29 -0600 Subject: [PATCH 198/222] Add SmartThings Fan platform (#20681) * Add SmartThings fan * Removed unnecessary update method * Corrected usage of async_schedule_update_ha_state * Clean-up/optimization --- homeassistant/components/smartthings/const.py | 2 + homeassistant/components/smartthings/fan.py | 96 ++++++++ tests/components/smartthings/test_fan.py | 213 ++++++++++++++++++ 3 files changed, 311 insertions(+) create mode 100644 homeassistant/components/smartthings/fan.py create mode 100644 tests/components/smartthings/test_fan.py diff --git a/homeassistant/components/smartthings/const.py b/homeassistant/components/smartthings/const.py index a9f47fc7c72..2834de4dcf1 100644 --- a/homeassistant/components/smartthings/const.py +++ b/homeassistant/components/smartthings/const.py @@ -18,12 +18,14 @@ SETTINGS_INSTANCE_ID = "hassInstanceId" STORAGE_KEY = DOMAIN STORAGE_VERSION = 1 SUPPORTED_PLATFORMS = [ + 'fan', 'light', 'switch' ] SUPPORTED_CAPABILITIES = [ 'colorControl', 'colorTemperature', + 'fanSpeed', 'switch', 'switchLevel' ] diff --git a/homeassistant/components/smartthings/fan.py b/homeassistant/components/smartthings/fan.py new file mode 100644 index 00000000000..7862736e60b --- /dev/null +++ b/homeassistant/components/smartthings/fan.py @@ -0,0 +1,96 @@ +""" +Support for fans through the SmartThings cloud API. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/smartthings.fan/ +""" + +from homeassistant.components.fan import ( + SPEED_HIGH, SPEED_LOW, SPEED_MEDIUM, SPEED_OFF, SUPPORT_SET_SPEED, + FanEntity) + +from . import SmartThingsEntity +from .const import DATA_BROKERS, DOMAIN + +DEPENDENCIES = ['smartthings'] + +VALUE_TO_SPEED = { + 0: SPEED_OFF, + 1: SPEED_LOW, + 2: SPEED_MEDIUM, + 3: SPEED_HIGH, +} +SPEED_TO_VALUE = { + v: k for k, v in VALUE_TO_SPEED.items()} + + +async def async_setup_platform( + hass, config, async_add_entities, discovery_info=None): + """Platform uses config entry setup.""" + pass + + +async def async_setup_entry(hass, config_entry, async_add_entities): + """Add fans for a config entry.""" + broker = hass.data[DOMAIN][DATA_BROKERS][config_entry.entry_id] + async_add_entities( + [SmartThingsFan(device) for device in broker.devices.values() + if is_fan(device)]) + + +def is_fan(device): + """Determine if the device should be represented as a fan.""" + from pysmartthings import Capability + # Must have switch and fan_speed + return all(capability in device.capabilities + for capability in [Capability.switch, Capability.fan_speed]) + + +class SmartThingsFan(SmartThingsEntity, FanEntity): + """Define a SmartThings Fan.""" + + async def async_set_speed(self, speed: str): + """Set the speed of the fan.""" + value = SPEED_TO_VALUE[speed] + await self._device.set_fan_speed(value, set_status=True) + # State is set optimistically in the command above, therefore update + # the entity state ahead of receiving the confirming push updates + self.async_schedule_update_ha_state() + + async def async_turn_on(self, speed: str = None, **kwargs) -> None: + """Turn the fan on.""" + if speed is not None: + value = SPEED_TO_VALUE[speed] + await self._device.set_fan_speed(value, set_status=True) + else: + await self._device.switch_on(set_status=True) + # State is set optimistically in the commands above, therefore update + # the entity state ahead of receiving the confirming push updates + self.async_schedule_update_ha_state() + + async def async_turn_off(self, **kwargs) -> None: + """Turn the fan off.""" + await self._device.switch_off(set_status=True) + # State is set optimistically in the command above, therefore update + # the entity state ahead of receiving the confirming push updates + self.async_schedule_update_ha_state() + + @property + def is_on(self) -> bool: + """Return true if fan is on.""" + return self._device.status.switch + + @property + def speed(self) -> str: + """Return the current speed.""" + return VALUE_TO_SPEED[self._device.status.fan_speed] + + @property + def speed_list(self) -> list: + """Get the list of available speeds.""" + return [SPEED_OFF, SPEED_LOW, SPEED_MEDIUM, SPEED_HIGH] + + @property + def supported_features(self) -> int: + """Flag supported features.""" + return SUPPORT_SET_SPEED diff --git a/tests/components/smartthings/test_fan.py b/tests/components/smartthings/test_fan.py new file mode 100644 index 00000000000..99627e866d9 --- /dev/null +++ b/tests/components/smartthings/test_fan.py @@ -0,0 +1,213 @@ +""" +Test for the SmartThings fan platform. + +The only mocking required is of the underlying SmartThings API object so +real HTTP calls are not initiated during testing. +""" +from pysmartthings import Attribute, Capability + +from homeassistant.components.fan import ( + ATTR_SPEED, ATTR_SPEED_LIST, SPEED_HIGH, SPEED_LOW, SPEED_MEDIUM, + SPEED_OFF, SUPPORT_SET_SPEED) +from homeassistant.components.smartthings import DeviceBroker, fan +from homeassistant.components.smartthings.const import ( + DATA_BROKERS, DOMAIN, SIGNAL_SMARTTHINGS_UPDATE) +from homeassistant.config_entries import ( + CONN_CLASS_CLOUD_PUSH, SOURCE_USER, ConfigEntry) +from homeassistant.const import ATTR_ENTITY_ID, ATTR_SUPPORTED_FEATURES +from homeassistant.helpers.dispatcher import async_dispatcher_send + + +async def _setup_platform(hass, *devices): + """Set up the SmartThings fan platform and prerequisites.""" + hass.config.components.add(DOMAIN) + broker = DeviceBroker(hass, devices, '') + config_entry = ConfigEntry("1", DOMAIN, "Test", {}, + SOURCE_USER, CONN_CLASS_CLOUD_PUSH) + hass.data[DOMAIN] = { + DATA_BROKERS: { + config_entry.entry_id: broker + } + } + await hass.config_entries.async_forward_entry_setup(config_entry, 'fan') + await hass.async_block_till_done() + return config_entry + + +async def test_async_setup_platform(): + """Test setup platform does nothing (it uses config entries).""" + await fan.async_setup_platform(None, None, None) + + +def test_is_fan(device_factory): + """Test fans are correctly identified.""" + non_fans = [ + device_factory('Unknown', ['Unknown']), + device_factory("Switch 1", [Capability.switch]), + device_factory("Non-Switchable Fan", [Capability.fan_speed]), + device_factory("Color Light", + [Capability.switch, Capability.switch_level, + Capability.color_control, + Capability.color_temperature]) + ] + fan_device = device_factory( + "Fan 1", [Capability.switch, Capability.switch_level, + Capability.fan_speed]) + + assert fan.is_fan(fan_device), fan_device.name + for device in non_fans: + assert not fan.is_fan(device), device.name + + +async def test_entity_state(hass, device_factory): + """Tests the state attributes properly match the fan types.""" + device = device_factory( + "Fan 1", + capabilities=[Capability.switch, Capability.fan_speed], + status={Attribute.switch: 'on', Attribute.fan_speed: 2}) + await _setup_platform(hass, device) + + # Dimmer 1 + state = hass.states.get('fan.fan_1') + assert state.state == 'on' + assert state.attributes[ATTR_SUPPORTED_FEATURES] == SUPPORT_SET_SPEED + assert state.attributes[ATTR_SPEED] == SPEED_MEDIUM + assert state.attributes[ATTR_SPEED_LIST] == \ + [SPEED_OFF, SPEED_LOW, SPEED_MEDIUM, SPEED_HIGH] + + +async def test_entity_and_device_attributes(hass, device_factory): + """Test the attributes of the entity are correct.""" + # Arrange + device = device_factory( + "Fan 1", + capabilities=[Capability.switch, Capability.fan_speed], + status={Attribute.switch: 'on', Attribute.fan_speed: 2}) + await _setup_platform(hass, device) + entity_registry = await hass.helpers.entity_registry.async_get_registry() + device_registry = await hass.helpers.device_registry.async_get_registry() + # Act + await _setup_platform(hass, device) + # Assert + entry = entity_registry.async_get("fan.fan_1") + assert entry + assert entry.unique_id == device.device_id + + entry = device_registry.async_get_device( + {(DOMAIN, device.device_id)}, []) + assert entry + assert entry.name == device.label + assert entry.model == device.device_type_name + assert entry.manufacturer == 'Unavailable' + + +async def test_turn_off(hass, device_factory): + """Test the fan turns of successfully.""" + # Arrange + device = device_factory( + "Fan 1", + capabilities=[Capability.switch, Capability.fan_speed], + status={Attribute.switch: 'on', Attribute.fan_speed: 2}) + await _setup_platform(hass, device) + # Act + await hass.services.async_call( + 'fan', 'turn_off', {'entity_id': 'fan.fan_1'}, + blocking=True) + # Assert + state = hass.states.get('fan.fan_1') + assert state is not None + assert state.state == 'off' + + +async def test_turn_on(hass, device_factory): + """Test the fan turns of successfully.""" + # Arrange + device = device_factory( + "Fan 1", + capabilities=[Capability.switch, Capability.fan_speed], + status={Attribute.switch: 'off', Attribute.fan_speed: 0}) + await _setup_platform(hass, device) + # Act + await hass.services.async_call( + 'fan', 'turn_on', {ATTR_ENTITY_ID: "fan.fan_1"}, + blocking=True) + # Assert + state = hass.states.get("fan.fan_1") + assert state is not None + assert state.state == 'on' + + +async def test_turn_on_with_speed(hass, device_factory): + """Test the fan turns on to the specified speed.""" + # Arrange + device = device_factory( + "Fan 1", + capabilities=[Capability.switch, Capability.fan_speed], + status={Attribute.switch: 'off', Attribute.fan_speed: 0}) + await _setup_platform(hass, device) + # Act + await hass.services.async_call( + 'fan', 'turn_on', + {ATTR_ENTITY_ID: "fan.fan_1", + ATTR_SPEED: SPEED_HIGH}, + blocking=True) + # Assert + state = hass.states.get("fan.fan_1") + assert state is not None + assert state.state == 'on' + assert state.attributes[ATTR_SPEED] == SPEED_HIGH + + +async def test_set_speed(hass, device_factory): + """Test setting to specific fan speed.""" + # Arrange + device = device_factory( + "Fan 1", + capabilities=[Capability.switch, Capability.fan_speed], + status={Attribute.switch: 'off', Attribute.fan_speed: 0}) + await _setup_platform(hass, device) + # Act + await hass.services.async_call( + 'fan', 'set_speed', + {ATTR_ENTITY_ID: "fan.fan_1", + ATTR_SPEED: SPEED_HIGH}, + blocking=True) + # Assert + state = hass.states.get("fan.fan_1") + assert state is not None + assert state.state == 'on' + assert state.attributes[ATTR_SPEED] == SPEED_HIGH + + +async def test_update_from_signal(hass, device_factory): + """Test the fan updates when receiving a signal.""" + # Arrange + device = device_factory( + "Fan 1", + capabilities=[Capability.switch, Capability.fan_speed], + status={Attribute.switch: 'off', Attribute.fan_speed: 0}) + await _setup_platform(hass, device) + await device.switch_on(True) + # Act + async_dispatcher_send(hass, SIGNAL_SMARTTHINGS_UPDATE, + [device.device_id]) + # Assert + await hass.async_block_till_done() + state = hass.states.get('fan.fan_1') + assert state is not None + assert state.state == 'on' + + +async def test_unload_config_entry(hass, device_factory): + """Test the fan is removed when the config entry is unloaded.""" + # Arrange + device = device_factory( + "Fan 1", + capabilities=[Capability.switch, Capability.fan_speed], + status={Attribute.switch: 'off', Attribute.fan_speed: 0}) + config_entry = await _setup_platform(hass, device) + # Act + await hass.config_entries.async_forward_entry_unload( + config_entry, 'fan') + # Assert + assert not hass.states.get('fan.fan_1') From 4255f2c62fe924f579f9971655c4eda5cdf89e80 Mon Sep 17 00:00:00 2001 From: emontnemery Date: Sat, 2 Feb 2019 18:31:28 +0100 Subject: [PATCH 199/222] Add entity_namespace to PLATFORM_SCHEMA (#20693) * Add entity_namespace to base platform schema * Add test * Fix --- homeassistant/helpers/config_validation.py | 4 ++- tests/test_setup.py | 29 ++++++++++++++++++++++ 2 files changed, 32 insertions(+), 1 deletion(-) diff --git a/homeassistant/helpers/config_validation.py b/homeassistant/helpers/config_validation.py index f3371a26725..b148a875398 100644 --- a/homeassistant/helpers/config_validation.py +++ b/homeassistant/helpers/config_validation.py @@ -16,7 +16,7 @@ from homeassistant.const import ( CONF_ALIAS, CONF_ENTITY_ID, CONF_VALUE_TEMPLATE, WEEKDAYS, CONF_CONDITION, CONF_BELOW, CONF_ABOVE, CONF_TIMEOUT, SUN_EVENT_SUNSET, SUN_EVENT_SUNRISE, CONF_UNIT_SYSTEM_IMPERIAL, CONF_UNIT_SYSTEM_METRIC, - ENTITY_MATCH_ALL) + ENTITY_MATCH_ALL, CONF_ENTITY_NAMESPACE) from homeassistant.core import valid_entity_id, split_entity_id from homeassistant.exceptions import TemplateError import homeassistant.util.dt as dt_util @@ -554,12 +554,14 @@ def key_dependency(key, dependency): PLATFORM_SCHEMA = vol.Schema({ vol.Required(CONF_PLATFORM): string, + vol.Optional(CONF_ENTITY_NAMESPACE): string, vol.Optional(CONF_SCAN_INTERVAL): time_period }, extra=vol.ALLOW_EXTRA) # This will replace PLATFORM_SCHEMA once all base components are updated PLATFORM_SCHEMA_2 = vol.Schema({ vol.Required(CONF_PLATFORM): string, + vol.Optional(CONF_ENTITY_NAMESPACE): string, vol.Optional(CONF_SCAN_INTERVAL): time_period }) diff --git a/tests/test_setup.py b/tests/test_setup.py index 6d2cc770013..6d0d2a35847 100644 --- a/tests/test_setup.py +++ b/tests/test_setup.py @@ -259,6 +259,7 @@ class TestSetup: assert setup.setup_component(self.hass, 'platform_conf', { 'platform_conf': { # fail: no extra keys allowed + 'platform': 'whatever', 'hello': 'world', 'invalid': 'extra', } @@ -284,6 +285,34 @@ class TestSetup: self.hass.data.pop(setup.DATA_SETUP) self.hass.config.components.remove('platform_conf') + def test_validate_platform_config_4(self): + """Test entity_namespace in PLATFORM_SCHEMA.""" + component_schema = PLATFORM_SCHEMA_BASE + platform_schema = PLATFORM_SCHEMA + loader.set_component( + self.hass, + 'platform_conf', + MockModule('platform_conf', + platform_schema_base=component_schema)) + + loader.set_component( + self.hass, + 'platform_conf.whatever', + MockPlatform('whatever', + platform_schema=platform_schema)) + + with assert_setup_component(1): + assert setup.setup_component(self.hass, 'platform_conf', { + 'platform_conf': { + # pass: entity_namespace accepted by PLATFORM_SCHEMA + 'platform': 'whatever', + 'entity_namespace': 'yummy', + } + }) + + self.hass.data.pop(setup.DATA_SETUP) + self.hass.config.components.remove('platform_conf') + def test_component_not_found(self): """setup_component should not crash if component doesn't exist.""" assert not setup.setup_component(self.hass, 'non_existing') From d3e011ff505058ec569e00b39fff89f97e02f4ef Mon Sep 17 00:00:00 2001 From: Andrew Sayre <6730289+andrewsayre@users.noreply.github.com> Date: Sat, 2 Feb 2019 16:06:30 -0600 Subject: [PATCH 200/222] Add SmartThings Binary Sensor platform (#20699) * Add SmartThings binary_sensor platform * Fixed comment typo. --- .../components/smartthings/binary_sensor.py | 81 ++++++++++++++ homeassistant/components/smartthings/const.py | 12 ++- .../smartthings/test_binary_sensor.py | 100 ++++++++++++++++++ 3 files changed, 192 insertions(+), 1 deletion(-) create mode 100644 homeassistant/components/smartthings/binary_sensor.py create mode 100644 tests/components/smartthings/test_binary_sensor.py diff --git a/homeassistant/components/smartthings/binary_sensor.py b/homeassistant/components/smartthings/binary_sensor.py new file mode 100644 index 00000000000..045944ccfa9 --- /dev/null +++ b/homeassistant/components/smartthings/binary_sensor.py @@ -0,0 +1,81 @@ +""" +Support for binary sensors through the SmartThings cloud API. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/smartthings.binary_sensor/ +""" +from homeassistant.components.binary_sensor import BinarySensorDevice + +from . import SmartThingsEntity +from .const import DATA_BROKERS, DOMAIN + +DEPENDENCIES = ['smartthings'] + +CAPABILITY_TO_ATTRIB = { + 'accelerationSensor': 'acceleration', + 'contactSensor': 'contact', + 'filterStatus': 'filterStatus', + 'motionSensor': 'motion', + 'presenceSensor': 'presence', + 'soundSensor': 'sound', + 'tamperAlert': 'tamper', + 'valve': 'valve', + 'waterSensor': 'water' +} +ATTRIB_TO_CLASS = { + 'acceleration': 'moving', + 'contact': 'opening', + 'filterStatus': 'problem', + 'motion': 'motion', + 'presence': 'presence', + 'sound': 'sound', + 'tamper': 'problem', + 'valve': 'opening', + 'water': 'moisture' +} + + +async def async_setup_platform(hass, config, async_add_entities, + discovery_info=None): + """Platform uses config entry setup.""" + pass + + +async def async_setup_entry(hass, config_entry, async_add_entities): + """Add binary sensors for a config entry.""" + broker = hass.data[DOMAIN][DATA_BROKERS][config_entry.entry_id] + sensors = [] + for device in broker.devices.values(): + for capability, attrib in CAPABILITY_TO_ATTRIB.items(): + if capability in device.capabilities: + sensors.append(SmartThingsBinarySensor(device, attrib)) + async_add_entities(sensors) + + +class SmartThingsBinarySensor(SmartThingsEntity, BinarySensorDevice): + """Define a SmartThings Binary Sensor.""" + + def __init__(self, device, attribute): + """Init the class.""" + super().__init__(device) + self._attribute = attribute + + @property + def name(self) -> str: + """Return the name of the binary sensor.""" + return '{} {}'.format(self._device.label, self._attribute) + + @property + def unique_id(self) -> str: + """Return a unique ID.""" + return '{}.{}'.format(self._device.device_id, self._attribute) + + @property + def is_on(self): + """Return true if the binary sensor is on.""" + return self._device.status.is_on(self._attribute) + + @property + def device_class(self): + """Return the class of this device.""" + return ATTRIB_TO_CLASS[self._attribute] diff --git a/homeassistant/components/smartthings/const.py b/homeassistant/components/smartthings/const.py index 2834de4dcf1..f545f84832d 100644 --- a/homeassistant/components/smartthings/const.py +++ b/homeassistant/components/smartthings/const.py @@ -18,16 +18,26 @@ SETTINGS_INSTANCE_ID = "hassInstanceId" STORAGE_KEY = DOMAIN STORAGE_VERSION = 1 SUPPORTED_PLATFORMS = [ + 'binary_sensor', 'fan', 'light', 'switch' ] SUPPORTED_CAPABILITIES = [ + 'accelerationSensor', 'colorControl', 'colorTemperature', + 'contactSensor', 'fanSpeed', + 'filterStatus', + 'motionSensor', + 'presenceSensor', + 'soundSensor', 'switch', - 'switchLevel' + 'switchLevel', + 'tamperAlert', + 'valve', + 'waterSensor' ] VAL_UID = "^(?:([0-9a-fA-F]{32})|([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]" \ "{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}))$" diff --git a/tests/components/smartthings/test_binary_sensor.py b/tests/components/smartthings/test_binary_sensor.py new file mode 100644 index 00000000000..2e0c46842b0 --- /dev/null +++ b/tests/components/smartthings/test_binary_sensor.py @@ -0,0 +1,100 @@ +""" +Test for the SmartThings binary_sensor platform. + +The only mocking required is of the underlying SmartThings API object so +real HTTP calls are not initiated during testing. +""" +from pysmartthings import Attribute, Capability + +from homeassistant.components.smartthings import DeviceBroker, binary_sensor +from homeassistant.components.smartthings.const import ( + DATA_BROKERS, DOMAIN, SIGNAL_SMARTTHINGS_UPDATE) +from homeassistant.config_entries import ( + CONN_CLASS_CLOUD_PUSH, SOURCE_USER, ConfigEntry) +from homeassistant.const import ATTR_FRIENDLY_NAME +from homeassistant.helpers.dispatcher import async_dispatcher_send + + +async def _setup_platform(hass, *devices): + """Set up the SmartThings binary_sensor platform and prerequisites.""" + hass.config.components.add(DOMAIN) + broker = DeviceBroker(hass, devices, '') + config_entry = ConfigEntry("1", DOMAIN, "Test", {}, + SOURCE_USER, CONN_CLASS_CLOUD_PUSH) + hass.data[DOMAIN] = { + DATA_BROKERS: { + config_entry.entry_id: broker + } + } + await hass.config_entries.async_forward_entry_setup( + config_entry, 'binary_sensor') + await hass.async_block_till_done() + return config_entry + + +async def test_async_setup_platform(): + """Test setup platform does nothing (it uses config entries).""" + await binary_sensor.async_setup_platform(None, None, None) + + +async def test_entity_state(hass, device_factory): + """Tests the state attributes properly match the light types.""" + device = device_factory('Motion Sensor 1', [Capability.motion_sensor], + {Attribute.motion: 'inactive'}) + await _setup_platform(hass, device) + state = hass.states.get('binary_sensor.motion_sensor_1_motion') + assert state.state == 'off' + assert state.attributes[ATTR_FRIENDLY_NAME] ==\ + device.label + ' ' + Attribute.motion + + +async def test_entity_and_device_attributes(hass, device_factory): + """Test the attributes of the entity are correct.""" + # Arrange + device = device_factory('Motion Sensor 1', [Capability.motion_sensor], + {Attribute.motion: 'inactive'}) + entity_registry = await hass.helpers.entity_registry.async_get_registry() + device_registry = await hass.helpers.device_registry.async_get_registry() + # Act + await _setup_platform(hass, device) + # Assert + entity = entity_registry.async_get('binary_sensor.motion_sensor_1_motion') + assert entity + assert entity.unique_id == device.device_id + '.' + Attribute.motion + device_entry = device_registry.async_get_device( + {(DOMAIN, device.device_id)}, []) + assert device_entry + assert device_entry.name == device.label + assert device_entry.model == device.device_type_name + assert device_entry.manufacturer == 'Unavailable' + + +async def test_update_from_signal(hass, device_factory): + """Test the binary_sensor updates when receiving a signal.""" + # Arrange + device = device_factory('Motion Sensor 1', [Capability.motion_sensor], + {Attribute.motion: 'inactive'}) + await _setup_platform(hass, device) + device.status.apply_attribute_update( + 'main', Capability.motion_sensor, Attribute.motion, 'active') + # Act + async_dispatcher_send(hass, SIGNAL_SMARTTHINGS_UPDATE, + [device.device_id]) + # Assert + await hass.async_block_till_done() + state = hass.states.get('binary_sensor.motion_sensor_1_motion') + assert state is not None + assert state.state == 'on' + + +async def test_unload_config_entry(hass, device_factory): + """Test the binary_sensor is removed when the config entry is unloaded.""" + # Arrange + device = device_factory('Motion Sensor 1', [Capability.motion_sensor], + {Attribute.motion: 'inactive'}) + config_entry = await _setup_platform(hass, device) + # Act + await hass.config_entries.async_forward_entry_unload( + config_entry, 'binary_sensor') + # Assert + assert not hass.states.get('binary_sensor.motion_sensor_1_motion') From f73cb0eba5319039c6792fda6aa400c550c06be2 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sat, 2 Feb 2019 14:09:55 -0800 Subject: [PATCH 201/222] Bumped version to 0.87.0b2 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 5204d17f580..c1c0451fc21 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -2,7 +2,7 @@ """Constants used by Home Assistant components.""" MAJOR_VERSION = 0 MINOR_VERSION = 87 -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 e5835eb7c847a6e00f51c04840d59bc0ca2a2102 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sat, 2 Feb 2019 02:52:34 -0800 Subject: [PATCH 202/222] Remove fingerprint middleware (#20682) * Remove fingerprint middleware * Lint --- homeassistant/components/http/__init__.py | 6 ++---- homeassistant/components/http/static.py | 23 +---------------------- 2 files changed, 3 insertions(+), 26 deletions(-) diff --git a/homeassistant/components/http/__init__.py b/homeassistant/components/http/__init__.py index d43ba989f28..02b9affefd4 100644 --- a/homeassistant/components/http/__init__.py +++ b/homeassistant/components/http/__init__.py @@ -25,8 +25,7 @@ from .auth import setup_auth from .ban import setup_bans from .cors import setup_cors from .real_ip import setup_real_ip -from .static import ( - CachingFileResponse, CachingStaticResource, staticresource_middleware) +from .static import CachingFileResponse, CachingStaticResource # Import as alias from .const import KEY_AUTHENTICATED, KEY_REAL_IP # noqa @@ -192,8 +191,7 @@ class HomeAssistantHTTP: use_x_forwarded_for, trusted_proxies, trusted_networks, login_threshold, is_ban_enabled, ssl_profile): """Initialize the HTTP Home Assistant server.""" - app = self.app = web.Application( - middlewares=[staticresource_middleware]) + app = self.app = web.Application(middlewares=[]) # This order matters setup_real_ip(app, use_x_forwarded_for, trusted_proxies) diff --git a/homeassistant/components/http/static.py b/homeassistant/components/http/static.py index 8b28a7cf288..54e72c88ff3 100644 --- a/homeassistant/components/http/static.py +++ b/homeassistant/components/http/static.py @@ -1,15 +1,10 @@ """Static file handling for HTTP component.""" - -import re - from aiohttp import hdrs -from aiohttp.web import FileResponse, middleware +from aiohttp.web import FileResponse from aiohttp.web_exceptions import HTTPNotFound from aiohttp.web_urldispatcher import StaticResource from yarl import URL -_FINGERPRINT = re.compile(r'^(.+)-[a-z0-9]{32}\.(\w+)$', re.IGNORECASE) - class CachingStaticResource(StaticResource): """Static Resource handler that will add cache headers.""" @@ -56,19 +51,3 @@ class CachingFileResponse(FileResponse): # Overwriting like this because __init__ can change implementation. self._sendfile = sendfile - - -@middleware -async def staticresource_middleware(request, handler): - """Middleware to strip out fingerprint from fingerprinted assets.""" - path = request.path - if not path.startswith('/static/') and not path.startswith('/frontend'): - return await handler(request) - - fingerprinted = _FINGERPRINT.match(request.match_info['filename']) - - if fingerprinted: - request.match_info['filename'] = \ - '{}.{}'.format(*fingerprinted.groups()) - - return await handler(request) From e98486876254aab7222669a15acf0553e75ed40e Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sat, 2 Feb 2019 16:32:26 -0800 Subject: [PATCH 203/222] Bumped version to 0.87.0b3 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index c1c0451fc21..9503aa9947d 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -2,7 +2,7 @@ """Constants used by Home Assistant components.""" MAJOR_VERSION = 0 MINOR_VERSION = 87 -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 e4d45bf53a98ced7732bff516e1cced86917ce0e Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 1 Feb 2019 16:31:53 -0800 Subject: [PATCH 204/222] Test is broken --- tests/components/sensor/test_history_stats.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/components/sensor/test_history_stats.py b/tests/components/sensor/test_history_stats.py index 67cacb29880..a739325847f 100644 --- a/tests/components/sensor/test_history_stats.py +++ b/tests/components/sensor/test_history_stats.py @@ -70,9 +70,9 @@ class TestHistoryStatsSensor(unittest.TestCase): assert sensor1_start.second == 0 # End = 02:01:00 - assert sensor1_end.hour == 2 - assert sensor1_end.minute == 1 - assert sensor1_end.second == 0 + # assert sensor1_end.hour == 2 + # assert sensor1_end.minute == 1 + # assert sensor1_end.second == 0 # Start = 21:59:00 assert sensor2_start.hour == 21 From c9f64af85a41018d223aa16b9e81f586e39d5dce Mon Sep 17 00:00:00 2001 From: Diogo Gomes Date: Sat, 2 Feb 2019 06:11:50 +0000 Subject: [PATCH 205/222] fix test commented in #20678 (#20680) --- tests/components/sensor/test_history_stats.py | 37 +++++++++++-------- 1 file changed, 21 insertions(+), 16 deletions(-) diff --git a/tests/components/sensor/test_history_stats.py b/tests/components/sensor/test_history_stats.py index a739325847f..28d01de4b34 100644 --- a/tests/components/sensor/test_history_stats.py +++ b/tests/components/sensor/test_history_stats.py @@ -1,8 +1,11 @@ """The test for the History Statistics sensor platform.""" # pylint: disable=protected-access -from datetime import timedelta +from datetime import datetime, timedelta import unittest from unittest.mock import patch +import pytest +import pytz +from homeassistant.helpers import template from homeassistant.const import STATE_UNKNOWN from homeassistant.setup import setup_component @@ -12,7 +15,6 @@ from homeassistant.helpers.template import Template import homeassistant.util.dt as dt_util from tests.common import init_recorder_component, get_test_home_assistant -import pytest class TestHistoryStatsSensor(unittest.TestCase): @@ -50,19 +52,22 @@ class TestHistoryStatsSensor(unittest.TestCase): def test_period_parsing(self): """Test the conversion from templates to period.""" - today = Template('{{ now().replace(hour=0).replace(minute=0)' - '.replace(second=0) }}', self.hass) - duration = timedelta(hours=2, minutes=1) + now = datetime(2019, 1, 1, 23, 30, 0, tzinfo=pytz.utc) + with patch.dict(template.ENV.globals, {'now': lambda: now}): + print(dt_util.now()) + today = Template('{{ now().replace(hour=0).replace(minute=0)' + '.replace(second=0) }}', self.hass) + duration = timedelta(hours=2, minutes=1) - sensor1 = HistoryStatsSensor( - self.hass, 'test', 'on', today, None, duration, 'time', 'test') - sensor2 = HistoryStatsSensor( - self.hass, 'test', 'on', None, today, duration, 'time', 'test') + sensor1 = HistoryStatsSensor( + self.hass, 'test', 'on', today, None, duration, 'time', 'test') + sensor2 = HistoryStatsSensor( + self.hass, 'test', 'on', None, today, duration, 'time', 'test') - sensor1.update_period() - sensor1_start, sensor1_end = sensor1._period - sensor2.update_period() - sensor2_start, sensor2_end = sensor2._period + sensor1.update_period() + sensor1_start, sensor1_end = sensor1._period + sensor2.update_period() + sensor2_start, sensor2_end = sensor2._period # Start = 00:00:00 assert sensor1_start.hour == 0 @@ -70,9 +75,9 @@ class TestHistoryStatsSensor(unittest.TestCase): assert sensor1_start.second == 0 # End = 02:01:00 - # assert sensor1_end.hour == 2 - # assert sensor1_end.minute == 1 - # assert sensor1_end.second == 0 + assert sensor1_end.hour == 2 + assert sensor1_end.minute == 1 + assert sensor1_end.second == 0 # Start = 21:59:00 assert sensor2_start.hour == 21 From b0200cdbfe9524e26469e07927ef7d104b77c912 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sat, 2 Feb 2019 20:28:03 -0800 Subject: [PATCH 206/222] Bumped version to 0.87.0b4 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 9503aa9947d..0a57706c547 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -2,7 +2,7 @@ """Constants used by Home Assistant components.""" MAJOR_VERSION = 0 MINOR_VERSION = 87 -PATCH_VERSION = '0b3' +PATCH_VERSION = '0b4' __short_version__ = '{}.{}'.format(MAJOR_VERSION, MINOR_VERSION) __version__ = '{}.{}'.format(__short_version__, PATCH_VERSION) REQUIRED_PYTHON_VER = (3, 5, 3) From 5f2d209decd5928240e870405d11bb79978d137f Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sun, 3 Feb 2019 11:32:15 -0800 Subject: [PATCH 207/222] Updated frontend to 20190203.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 b5836e67ffc..46652b4d7b0 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==20190202.0'] +REQUIREMENTS = ['home-assistant-frontend==20190203.0'] DOMAIN = 'frontend' DEPENDENCIES = ['api', 'websocket_api', 'http', 'system_log', diff --git a/requirements_all.txt b/requirements_all.txt index ea8afeb6a09..bccd39d91f4 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -526,7 +526,7 @@ hole==0.3.0 holidays==0.9.9 # homeassistant.components.frontend -home-assistant-frontend==20190202.0 +home-assistant-frontend==20190203.0 # homeassistant.components.zwave homeassistant-pyozw==0.1.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 4ebef8bc1e6..df2d5496441 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -113,7 +113,7 @@ hdate==0.8.7 holidays==0.9.9 # homeassistant.components.frontend -home-assistant-frontend==20190202.0 +home-assistant-frontend==20190203.0 # homeassistant.components.homekit_controller homekit==0.12.2 From 9a13aafeea30a2836ee99d016f420ded753d3658 Mon Sep 17 00:00:00 2001 From: Andrew Sayre <6730289+andrewsayre@users.noreply.github.com> Date: Sun, 3 Feb 2019 00:08:37 -0600 Subject: [PATCH 208/222] Add SmartThings button support via events (#20707) * Add event support for buttons * binary_sensor test clean-up --- .../components/smartthings/__init__.py | 15 ++++++++- homeassistant/components/smartthings/const.py | 2 ++ tests/components/smartthings/conftest.py | 22 ++++++++----- .../smartthings/test_binary_sensor.py | 31 +++++++++++++------ tests/components/smartthings/test_init.py | 31 ++++++++++++++++++- 5 files changed, 82 insertions(+), 19 deletions(-) diff --git a/homeassistant/components/smartthings/__init__.py b/homeassistant/components/smartthings/__init__.py index c705a3df73e..d86524ef62b 100644 --- a/homeassistant/components/smartthings/__init__.py +++ b/homeassistant/components/smartthings/__init__.py @@ -19,7 +19,7 @@ from homeassistant.helpers.typing import ConfigType, HomeAssistantType from .config_flow import SmartThingsFlowHandler # noqa from .const import ( CONF_APP_ID, CONF_INSTALLED_APP_ID, DATA_BROKERS, DATA_MANAGER, DOMAIN, - SIGNAL_SMARTTHINGS_UPDATE, SUPPORTED_PLATFORMS) + EVENT_BUTTON, SIGNAL_SMARTTHINGS_UPDATE, SUPPORTED_PLATFORMS) from .smartapp import ( setup_smartapp, setup_smartapp_endpoint, validate_installed_app) @@ -154,6 +154,19 @@ class DeviceBroker: continue device.status.apply_attribute_update( evt.component_id, evt.capability, evt.attribute, evt.value) + + # Fire events for buttons + if evt.capability == 'button' and evt.attribute == 'button': + data = { + 'component_id': evt.component_id, + 'device_id': evt.device_id, + 'location_id': evt.location_id, + 'value': evt.value, + 'name': device.label + } + self._hass.bus.async_fire(EVENT_BUTTON, data) + _LOGGER.debug("Fired button event: %s", data) + updated_devices.add(device.device_id) _LOGGER.debug("Update received with %s events and updated %s devices", len(req.events), len(updated_devices)) diff --git a/homeassistant/components/smartthings/const.py b/homeassistant/components/smartthings/const.py index f545f84832d..3d0e5cb95f8 100644 --- a/homeassistant/components/smartthings/const.py +++ b/homeassistant/components/smartthings/const.py @@ -12,6 +12,7 @@ CONF_LOCATION_ID = 'location_id' DATA_MANAGER = 'manager' DATA_BROKERS = 'brokers' DOMAIN = 'smartthings' +EVENT_BUTTON = "smartthings.button" SIGNAL_SMARTTHINGS_UPDATE = 'smartthings_update' SIGNAL_SMARTAPP_PREFIX = 'smartthings_smartap_' SETTINGS_INSTANCE_ID = "hassInstanceId" @@ -25,6 +26,7 @@ SUPPORTED_PLATFORMS = [ ] SUPPORTED_CAPABILITIES = [ 'accelerationSensor', + 'button', 'colorControl', 'colorTemperature', 'contactSensor', diff --git a/tests/components/smartthings/conftest.py b/tests/components/smartthings/conftest.py index 56bb5a62888..7358e05f346 100644 --- a/tests/components/smartthings/conftest.py +++ b/tests/components/smartthings/conftest.py @@ -254,14 +254,16 @@ def device_factory_fixture(): @pytest.fixture(name="event_factory") def event_factory_fixture(): """Fixture for creating mock devices.""" - def _factory(device_id, event_type="DEVICE_EVENT"): + def _factory(device_id, event_type="DEVICE_EVENT", capability='', + attribute='Updated', value='Value'): event = Mock() event.event_type = event_type event.device_id = device_id event.component_id = 'main' - event.capability = '' - event.attribute = 'Updated' - event.value = 'Value' + event.capability = capability + event.attribute = attribute + event.value = value + event.location_id = str(uuid4()) return event return _factory @@ -269,11 +271,15 @@ def event_factory_fixture(): @pytest.fixture(name="event_request_factory") def event_request_factory_fixture(event_factory): """Fixture for creating mock smartapp event requests.""" - def _factory(device_ids): + def _factory(device_ids=None, events=None): request = Mock() request.installed_app_id = uuid4() - request.events = [event_factory(id) for id in device_ids] - request.events.append(event_factory(uuid4())) - request.events.append(event_factory(device_ids[0], event_type="OTHER")) + if events is None: + events = [] + if device_ids: + events.extend([event_factory(id) for id in device_ids]) + events.append(event_factory(uuid4())) + events.append(event_factory(device_ids[0], event_type="OTHER")) + request.events = events return request return _factory diff --git a/tests/components/smartthings/test_binary_sensor.py b/tests/components/smartthings/test_binary_sensor.py index 2e0c46842b0..92d891c06d6 100644 --- a/tests/components/smartthings/test_binary_sensor.py +++ b/tests/components/smartthings/test_binary_sensor.py @@ -6,9 +6,10 @@ real HTTP calls are not initiated during testing. """ from pysmartthings import Attribute, Capability +from homeassistant.components.binary_sensor import DEVICE_CLASSES from homeassistant.components.smartthings import DeviceBroker, binary_sensor from homeassistant.components.smartthings.const import ( - DATA_BROKERS, DOMAIN, SIGNAL_SMARTTHINGS_UPDATE) + DATA_BROKERS, DOMAIN, SIGNAL_SMARTTHINGS_UPDATE, SUPPORTED_CAPABILITIES) from homeassistant.config_entries import ( CONN_CLASS_CLOUD_PUSH, SOURCE_USER, ConfigEntry) from homeassistant.const import ATTR_FRIENDLY_NAME @@ -32,6 +33,18 @@ async def _setup_platform(hass, *devices): return config_entry +async def test_mapping_integrity(): + """Test ensures the map dicts have proper integrity.""" + # Ensure every CAPABILITY_TO_ATTRIB key is in SUPPORTED_CAPABILITIES + # Ensure every CAPABILITY_TO_ATTRIB value is in ATTRIB_TO_CLASS keys + for capability, attrib in binary_sensor.CAPABILITY_TO_ATTRIB.items(): + assert capability in SUPPORTED_CAPABILITIES, capability + assert attrib in binary_sensor.ATTRIB_TO_CLASS.keys(), attrib + # Ensure every ATTRIB_TO_CLASS value is in DEVICE_CLASSES + for device_class in binary_sensor.ATTRIB_TO_CLASS.values(): + assert device_class in DEVICE_CLASSES + + async def test_async_setup_platform(): """Test setup platform does nothing (it uses config entries).""" await binary_sensor.async_setup_platform(None, None, None) @@ -58,15 +71,15 @@ async def test_entity_and_device_attributes(hass, device_factory): # Act await _setup_platform(hass, device) # Assert - entity = entity_registry.async_get('binary_sensor.motion_sensor_1_motion') - assert entity - assert entity.unique_id == device.device_id + '.' + Attribute.motion - device_entry = device_registry.async_get_device( + entry = entity_registry.async_get('binary_sensor.motion_sensor_1_motion') + assert entry + assert entry.unique_id == device.device_id + '.' + Attribute.motion + entry = device_registry.async_get_device( {(DOMAIN, device.device_id)}, []) - assert device_entry - assert device_entry.name == device.label - assert device_entry.model == device.device_type_name - assert device_entry.manufacturer == 'Unavailable' + assert entry + assert entry.name == device.label + assert entry.model == device.device_type_name + assert entry.manufacturer == 'Unavailable' async def test_update_from_signal(hass, device_factory): diff --git a/tests/components/smartthings/test_init.py b/tests/components/smartthings/test_init.py index d20d2d4e047..4aef42c1b6f 100644 --- a/tests/components/smartthings/test_init.py +++ b/tests/components/smartthings/test_init.py @@ -8,7 +8,8 @@ import pytest from homeassistant.components import smartthings from homeassistant.components.smartthings.const import ( - DATA_BROKERS, DOMAIN, SIGNAL_SMARTTHINGS_UPDATE, SUPPORTED_PLATFORMS) + DATA_BROKERS, DOMAIN, EVENT_BUTTON, SIGNAL_SMARTTHINGS_UPDATE, + SUPPORTED_PLATFORMS) from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers.dispatcher import async_dispatcher_connect @@ -181,3 +182,31 @@ async def test_event_handler_ignores_other_installed_app( await hass.async_block_till_done() assert not called + + +async def test_event_handler_fires_button_events( + hass, device_factory, event_factory, event_request_factory): + """Test the event handler fires button events.""" + device = device_factory('Button 1', ['button']) + event = event_factory(device.device_id, capability='button', + attribute='button', value='pushed') + request = event_request_factory(events=[event]) + called = False + + def handler(evt): + nonlocal called + called = True + assert evt.data == { + 'component_id': 'main', + 'device_id': device.device_id, + 'location_id': event.location_id, + 'value': 'pushed', + 'name': device.label + } + hass.bus.async_listen(EVENT_BUTTON, handler) + broker = smartthings.DeviceBroker( + hass, [device], request.installed_app_id) + await broker.event_handler(request, None, None) + await hass.async_block_till_done() + + assert called From e1509bcc0c82e8a984481eba6cfcdf996d6c6dda Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Sun, 3 Feb 2019 16:23:30 -0700 Subject: [PATCH 209/222] Fix temperature unit conversion in Ambient PWS (#20723) --- .../components/ambient_station/__init__.py | 75 +++++++++---------- .../components/ambient_station/const.py | 3 - .../components/ambient_station/sensor.py | 21 +----- 3 files changed, 38 insertions(+), 61 deletions(-) diff --git a/homeassistant/components/ambient_station/__init__.py b/homeassistant/components/ambient_station/__init__.py index 788927a2700..0991336f42a 100644 --- a/homeassistant/components/ambient_station/__init__.py +++ b/homeassistant/components/ambient_station/__init__.py @@ -11,7 +11,7 @@ import voluptuous as vol from homeassistant.config_entries import SOURCE_IMPORT from homeassistant.const import ( ATTR_NAME, ATTR_LOCATION, CONF_API_KEY, CONF_MONITORED_CONDITIONS, - CONF_UNIT_SYSTEM, EVENT_HOMEASSISTANT_STOP) + EVENT_HOMEASSISTANT_STOP) from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers import aiohttp_client, config_validation as cv from homeassistant.helpers.dispatcher import async_dispatcher_send @@ -19,8 +19,7 @@ from homeassistant.helpers.event import async_call_later from .config_flow import configured_instances from .const import ( - ATTR_LAST_DATA, CONF_APP_KEY, DATA_CLIENT, DOMAIN, TOPIC_UPDATE, UNITS_SI, - UNITS_US) + ATTR_LAST_DATA, CONF_APP_KEY, DATA_CLIENT, DOMAIN, TOPIC_UPDATE) REQUIREMENTS = ['aioambient==0.1.0'] _LOGGER = logging.getLogger(__name__) @@ -28,36 +27,36 @@ _LOGGER = logging.getLogger(__name__) DEFAULT_SOCKET_MIN_RETRY = 15 SENSOR_TYPES = { - '24hourrainin': ['24 Hr Rain', 'in'], - 'baromabsin': ['Abs Pressure', 'inHg'], - 'baromrelin': ['Rel Pressure', 'inHg'], - 'battout': ['Battery', ''], - 'co2': ['co2', 'ppm'], - 'dailyrainin': ['Daily Rain', 'in'], - 'dewPoint': ['Dew Point', ['°F', '°C']], - 'eventrainin': ['Event Rain', 'in'], - 'feelsLike': ['Feels Like', ['°F', '°C']], - 'hourlyrainin': ['Hourly Rain Rate', 'in/hr'], - 'humidity': ['Humidity', '%'], - 'humidityin': ['Humidity In', '%'], - 'lastRain': ['Last Rain', ''], - 'maxdailygust': ['Max Gust', 'mph'], - 'monthlyrainin': ['Monthly Rain', 'in'], - 'solarradiation': ['Solar Rad', 'W/m^2'], - 'tempf': ['Temp', ['°F', '°C']], - 'tempinf': ['Inside Temp', ['°F', '°C']], - 'totalrainin': ['Lifetime Rain', 'in'], - 'uv': ['uv', 'Index'], - 'weeklyrainin': ['Weekly Rain', 'in'], - 'winddir': ['Wind Dir', '°'], - 'winddir_avg10m': ['Wind Dir Avg 10m', '°'], - 'winddir_avg2m': ['Wind Dir Avg 2m', 'mph'], - 'windgustdir': ['Gust Dir', '°'], - 'windgustmph': ['Wind Gust', 'mph'], - 'windspdmph_avg10m': ['Wind Avg 10m', 'mph'], - 'windspdmph_avg2m': ['Wind Avg 2m', 'mph'], - 'windspeedmph': ['Wind Speed', 'mph'], - 'yearlyrainin': ['Yearly Rain', 'in'], + '24hourrainin': ('24 Hr Rain', 'in'), + 'baromabsin': ('Abs Pressure', 'inHg'), + 'baromrelin': ('Rel Pressure', 'inHg'), + 'battout': ('Battery', ''), + 'co2': ('co2', 'ppm'), + 'dailyrainin': ('Daily Rain', 'in'), + 'dewPoint': ('Dew Point', '°F'), + 'eventrainin': ('Event Rain', 'in'), + 'feelsLike': ('Feels Like', '°F'), + 'hourlyrainin': ('Hourly Rain Rate', 'in/hr'), + 'humidity': ('Humidity', '%'), + 'humidityin': ('Humidity In', '%'), + 'lastRain': ('Last Rain', ''), + 'maxdailygust': ('Max Gust', 'mph'), + 'monthlyrainin': ('Monthly Rain', 'in'), + 'solarradiation': ('Solar Rad', 'W/m^2'), + 'tempf': ('Temp', '°F'), + 'tempinf': ('Inside Temp', '°F'), + 'totalrainin': ('Lifetime Rain', 'in'), + 'uv': ('uv', 'Index'), + 'weeklyrainin': ('Weekly Rain', 'in'), + 'winddir': ('Wind Dir', '°'), + 'winddir_avg10m': ('Wind Dir Avg 10m', '°'), + 'winddir_avg2m': ('Wind Dir Avg 2m', 'mph'), + 'windgustdir': ('Gust Dir', '°'), + 'windgustmph': ('Wind Gust', 'mph'), + 'windspdmph_avg10m': ('Wind Avg 10m', 'mph'), + 'windspdmph_avg2m': ('Wind Avg 2m', 'mph'), + 'windspeedmph': ('Wind Speed', 'mph'), + 'yearlyrainin': ('Yearly Rain', 'in'), } CONFIG_SCHEMA = vol.Schema({ @@ -70,8 +69,6 @@ CONFIG_SCHEMA = vol.Schema({ vol.Optional( CONF_MONITORED_CONDITIONS, default=list(SENSOR_TYPES)): vol.All(cv.ensure_list, [vol.In(SENSOR_TYPES)]), - vol.Optional(CONF_UNIT_SYSTEM): - vol.In([UNITS_SI, UNITS_US]), }) }, extra=vol.ALLOW_EXTRA) @@ -111,8 +108,7 @@ async def async_setup_entry(hass, config_entry): config_entry.data[CONF_API_KEY], config_entry.data[CONF_APP_KEY], session), config_entry.data.get( - CONF_MONITORED_CONDITIONS, list(SENSOR_TYPES)), - config_entry.data.get(CONF_UNIT_SYSTEM)) + CONF_MONITORED_CONDITIONS, list(SENSOR_TYPES))) hass.loop.create_task(ambient.ws_connect()) hass.data[DOMAIN][DATA_CLIENT][config_entry.entry_id] = ambient except WebsocketConnectionError as err: @@ -139,9 +135,7 @@ async def async_unload_entry(hass, config_entry): class AmbientStation: """Define a class to handle the Ambient websocket.""" - def __init__( - self, hass, config_entry, client, monitored_conditions, - unit_system): + def __init__(self, hass, config_entry, client, monitored_conditions): """Initialize.""" self._config_entry = config_entry self._hass = hass @@ -149,7 +143,6 @@ class AmbientStation: self.client = client self.monitored_conditions = monitored_conditions self.stations = {} - self.unit_system = unit_system async def ws_connect(self): """Register handlers and connect to the websocket.""" diff --git a/homeassistant/components/ambient_station/const.py b/homeassistant/components/ambient_station/const.py index df2c5462e66..75606a1c699 100644 --- a/homeassistant/components/ambient_station/const.py +++ b/homeassistant/components/ambient_station/const.py @@ -8,6 +8,3 @@ CONF_APP_KEY = 'app_key' DATA_CLIENT = 'data_client' TOPIC_UPDATE = 'update' - -UNITS_SI = 'si' -UNITS_US = 'us' diff --git a/homeassistant/components/ambient_station/sensor.py b/homeassistant/components/ambient_station/sensor.py index d2d89233472..9e0833e3441 100644 --- a/homeassistant/components/ambient_station/sensor.py +++ b/homeassistant/components/ambient_station/sensor.py @@ -12,14 +12,11 @@ from homeassistant.const import ATTR_NAME from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_connect -from .const import ( - ATTR_LAST_DATA, DATA_CLIENT, DOMAIN, TOPIC_UPDATE, UNITS_SI, UNITS_US) +from .const import ATTR_LAST_DATA, DATA_CLIENT, DOMAIN, TOPIC_UPDATE DEPENDENCIES = ['ambient_station'] _LOGGER = logging.getLogger(__name__) -UNIT_SYSTEM = {UNITS_US: 0, UNITS_SI: 1} - async def async_setup_platform( hass, config, async_add_entities, discovery_info=None): @@ -31,20 +28,10 @@ async def async_setup_entry(hass, entry, async_add_entities): """Set up an Ambient PWS sensor based on a config entry.""" ambient = hass.data[DOMAIN][DATA_CLIENT][entry.entry_id] - if ambient.unit_system: - sys_units = ambient.unit_system - elif hass.config.units.is_metric: - sys_units = UNITS_SI - else: - sys_units = UNITS_US - sensor_list = [] for mac_address, station in ambient.stations.items(): for condition in ambient.monitored_conditions: name, unit = SENSOR_TYPES[condition] - if isinstance(unit, list): - unit = unit[UNIT_SYSTEM[sys_units]] - sensor_list.append( AmbientWeatherSensor( ambient, mac_address, station[ATTR_NAME], condition, name, @@ -58,7 +45,7 @@ class AmbientWeatherSensor(Entity): def __init__( self, ambient, mac_address, station_name, sensor_type, sensor_name, - units): + unit): """Initialize the sensor.""" self._ambient = ambient self._async_unsub_dispatcher_connect = None @@ -67,7 +54,7 @@ class AmbientWeatherSensor(Entity): self._sensor_type = sensor_type self._state = None self._station_name = station_name - self._units = units + self._unit = unit @property def name(self): @@ -87,7 +74,7 @@ class AmbientWeatherSensor(Entity): @property def unit_of_measurement(self): """Return the unit of measurement.""" - return self._units + return self._unit @property def unique_id(self): From 027fcf269b785f2ac473a309c776f1cd6907c9e6 Mon Sep 17 00:00:00 2001 From: David Lie Date: Sun, 3 Feb 2019 17:47:38 -0500 Subject: [PATCH 210/222] Revert pyfoscam back to libpyfoscam (#20727) * Change foscam python library to pyfoscam, which is more up to date and has several critical bug fixes. * Update requirements_all.txt to match. * Inserting automatically generated requirements.txt * Revert changes until pyfoscam captures recent bug fixes. The pyfoscam version pulled by pip is currently broken. * Updated requirements_all.txt based on changing pyfoscam back to libpyfoscam. --- homeassistant/components/camera/foscam.py | 4 ++-- requirements_all.txt | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/camera/foscam.py b/homeassistant/components/camera/foscam.py index 173e115cbaf..ceec57f7755 100644 --- a/homeassistant/components/camera/foscam.py +++ b/homeassistant/components/camera/foscam.py @@ -15,7 +15,7 @@ from homeassistant.helpers import config_validation as cv _LOGGER = logging.getLogger(__name__) -REQUIREMENTS = ['pyfoscam==1.2'] +REQUIREMENTS = ['libpyfoscam==1.0'] CONF_IP = 'ip' @@ -43,7 +43,7 @@ class FoscamCam(Camera): def __init__(self, device_info): """Initialize a Foscam camera.""" - from foscam import FoscamCamera + from libpyfoscam import FoscamCamera super(FoscamCam, self).__init__() diff --git a/requirements_all.txt b/requirements_all.txt index bccd39d91f4..33216d7603e 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -605,6 +605,9 @@ libnacl==1.6.1 # homeassistant.components.dyson libpurecoollink==0.4.2 +# homeassistant.components.camera.foscam +libpyfoscam==1.0 + # homeassistant.components.device_tracker.mikrotik librouteros==2.2.0 @@ -1014,9 +1017,6 @@ pyflunearyou==1.0.1 # homeassistant.components.light.futurenow pyfnip==0.2 -# homeassistant.components.camera.foscam -pyfoscam==1.2 - # homeassistant.components.fritzbox pyfritzhome==0.4.0 From 7d334783de50d8ad0b40ad4baaac169e3e84092f Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sun, 3 Feb 2019 15:27:14 -0800 Subject: [PATCH 211/222] Bumped version to 0.87.0b5 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 0a57706c547..a44d90551cb 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -2,7 +2,7 @@ """Constants used by Home Assistant components.""" MAJOR_VERSION = 0 MINOR_VERSION = 87 -PATCH_VERSION = '0b4' +PATCH_VERSION = '0b5' __short_version__ = '{}.{}'.format(MAJOR_VERSION, MINOR_VERSION) __version__ = '{}.{}'.format(__short_version__, PATCH_VERSION) REQUIRED_PYTHON_VER = (3, 5, 3) From 207a050dbaad95957a25752593b312081ea09fa2 Mon Sep 17 00:00:00 2001 From: Jason Hu Date: Mon, 4 Feb 2019 09:57:22 -0800 Subject: [PATCH 212/222] Fix ffmpeg v4 stream issue (#20314) * Add ffmpeg version * Add ffmpeg stream content type * Change ffmpeg camera stream content type * Change ffmpeg stream content type * Lint * Add a none guard * Fix * Fix * Update onvif.py * Fix version match regrex * Fix regrex * Upgrade ha-ffmpeg to 1.11 * Lint * Get ffmpeg version in ffmpeg component setup --- homeassistant/components/camera/amcrest.py | 2 +- homeassistant/components/camera/arlo.py | 2 +- homeassistant/components/camera/canary.py | 2 +- homeassistant/components/camera/ffmpeg.py | 2 +- homeassistant/components/camera/onvif.py | 5 ++-- homeassistant/components/camera/ring.py | 2 +- homeassistant/components/camera/xiaomi.py | 2 +- homeassistant/components/camera/yi.py | 2 +- homeassistant/components/ffmpeg.py | 30 +++++++++++++++++++++- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 11 files changed, 41 insertions(+), 12 deletions(-) diff --git a/homeassistant/components/camera/amcrest.py b/homeassistant/components/camera/amcrest.py index 4ba527b4805..3b3368c2f5c 100644 --- a/homeassistant/components/camera/amcrest.py +++ b/homeassistant/components/camera/amcrest.py @@ -82,7 +82,7 @@ class AmcrestCam(Camera): try: return await async_aiohttp_proxy_stream( self.hass, request, stream, - 'multipart/x-mixed-replace;boundary=ffserver') + self._ffmpeg.ffmpeg_stream_content_type) finally: await stream.close() diff --git a/homeassistant/components/camera/arlo.py b/homeassistant/components/camera/arlo.py index d56616218e7..7857995b4af 100644 --- a/homeassistant/components/camera/arlo.py +++ b/homeassistant/components/camera/arlo.py @@ -104,7 +104,7 @@ class ArloCam(Camera): try: return await async_aiohttp_proxy_stream( self.hass, request, stream, - 'multipart/x-mixed-replace;boundary=ffserver') + self._ffmpeg.ffmpeg_stream_content_type) finally: await stream.close() diff --git a/homeassistant/components/camera/canary.py b/homeassistant/components/camera/canary.py index 7a83e2da4d1..eb0c8f3fc6d 100644 --- a/homeassistant/components/camera/canary.py +++ b/homeassistant/components/camera/canary.py @@ -101,7 +101,7 @@ class CanaryCamera(Camera): try: return await async_aiohttp_proxy_stream( self.hass, request, stream, - 'multipart/x-mixed-replace;boundary=ffserver') + self._ffmpeg.ffmpeg_stream_content_type) finally: await stream.close() diff --git a/homeassistant/components/camera/ffmpeg.py b/homeassistant/components/camera/ffmpeg.py index 6bd68b05bb5..db9e73f3e1b 100644 --- a/homeassistant/components/camera/ffmpeg.py +++ b/homeassistant/components/camera/ffmpeg.py @@ -68,7 +68,7 @@ class FFmpegCamera(Camera): try: return await async_aiohttp_proxy_stream( self.hass, request, stream, - 'multipart/x-mixed-replace;boundary=ffserver') + self._manager.ffmpeg_stream_content_type) finally: await stream.close() diff --git a/homeassistant/components/camera/onvif.py b/homeassistant/components/camera/onvif.py index d1afd39ca7b..da0bae7c50b 100644 --- a/homeassistant/components/camera/onvif.py +++ b/homeassistant/components/camera/onvif.py @@ -213,7 +213,8 @@ class ONVIFHassCamera(Camera): if not self._input: return None - stream = CameraMjpeg(self.hass.data[DATA_FFMPEG].binary, + ffmpeg_manager = self.hass.data[DATA_FFMPEG] + stream = CameraMjpeg(ffmpeg_manager.binary, loop=self.hass.loop) await stream.open_camera( self._input, extra_cmd=self._ffmpeg_arguments) @@ -221,7 +222,7 @@ class ONVIFHassCamera(Camera): try: return await async_aiohttp_proxy_stream( self.hass, request, stream, - 'multipart/x-mixed-replace;boundary=ffserver') + ffmpeg_manager.ffmpeg_stream_content_type) finally: await stream.close() diff --git a/homeassistant/components/camera/ring.py b/homeassistant/components/camera/ring.py index ad351fb59cf..da1119281b3 100644 --- a/homeassistant/components/camera/ring.py +++ b/homeassistant/components/camera/ring.py @@ -142,7 +142,7 @@ class RingCam(Camera): try: return await async_aiohttp_proxy_stream( self.hass, request, stream, - 'multipart/x-mixed-replace;boundary=ffserver') + self._ffmpeg.ffmpeg_stream_content_type) finally: await stream.close() diff --git a/homeassistant/components/camera/xiaomi.py b/homeassistant/components/camera/xiaomi.py index 207dd17ed9b..93e9dd4a07c 100644 --- a/homeassistant/components/camera/xiaomi.py +++ b/homeassistant/components/camera/xiaomi.py @@ -161,6 +161,6 @@ class XiaomiCamera(Camera): try: return await async_aiohttp_proxy_stream( self.hass, request, stream, - 'multipart/x-mixed-replace;boundary=ffserver') + self._manager.ffmpeg_stream_content_type) finally: await stream.close() diff --git a/homeassistant/components/camera/yi.py b/homeassistant/components/camera/yi.py index 8b5b865ee57..7d731d2a433 100644 --- a/homeassistant/components/camera/yi.py +++ b/homeassistant/components/camera/yi.py @@ -147,6 +147,6 @@ class YiCamera(Camera): try: return await async_aiohttp_proxy_stream( self.hass, request, stream, - 'multipart/x-mixed-replace;boundary=ffserver') + self._manager.ffmpeg_stream_content_type) finally: await stream.close() diff --git a/homeassistant/components/ffmpeg.py b/homeassistant/components/ffmpeg.py index a2f0ca19231..3184b5a5d54 100644 --- a/homeassistant/components/ffmpeg.py +++ b/homeassistant/components/ffmpeg.py @@ -5,6 +5,7 @@ For more details about this component, please refer to the documentation at https://home-assistant.io/components/ffmpeg/ """ import logging +import re import voluptuous as vol @@ -16,7 +17,7 @@ from homeassistant.helpers.dispatcher import ( import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity -REQUIREMENTS = ['ha-ffmpeg==1.9'] +REQUIREMENTS = ['ha-ffmpeg==1.11'] DOMAIN = 'ffmpeg' @@ -60,6 +61,8 @@ async def async_setup(hass, config): conf.get(CONF_FFMPEG_BIN, DEFAULT_BINARY) ) + await manager.async_get_version() + # Register service async def async_service_handle(service): """Handle service ffmpeg process.""" @@ -96,12 +99,37 @@ class FFmpegManager: self.hass = hass self._cache = {} self._bin = ffmpeg_bin + self._version = None + self._major_version = None @property def binary(self): """Return ffmpeg binary from config.""" return self._bin + async def async_get_version(self): + """Return ffmpeg version.""" + from haffmpeg.tools import FFVersion + + ffversion = FFVersion(self._bin, self.hass.loop) + self._version = await ffversion.get_version() + + self._major_version = None + if self._version is not None: + result = re.search(r"(\d+)\.", self._version) + if result is not None: + self._major_version = int(result.group(1)) + + return self._version, self._major_version + + @property + def ffmpeg_stream_content_type(self): + """Return HTTP content type for ffmpeg stream.""" + if self._major_version is not None and self._major_version > 3: + return 'multipart/x-mixed-replace;boundary=ffmpeg' + + return 'multipart/x-mixed-replace;boundary=ffserver' + class FFmpegBase(Entity): """Interface object for FFmpeg.""" diff --git a/requirements_all.txt b/requirements_all.txt index 33216d7603e..a7ea2f5d7d5 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -487,7 +487,7 @@ greenwavereality==0.5.1 gstreamer-player==1.1.2 # homeassistant.components.ffmpeg -ha-ffmpeg==1.9 +ha-ffmpeg==1.11 # homeassistant.components.media_player.philips_js ha-philipsjs==0.0.5 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index df2d5496441..ec1ece765f9 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -98,7 +98,7 @@ geojson_client==0.3 georss_client==0.5 # homeassistant.components.ffmpeg -ha-ffmpeg==1.9 +ha-ffmpeg==1.11 # homeassistant.components.hangouts hangups==0.4.6 From 94ab5dca7f0851a1d141186f84809d43f96278d9 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 4 Feb 2019 01:14:30 -0800 Subject: [PATCH 213/222] Improve cloud error handling (#20729) * Improve cloud error handling * Lint --- homeassistant/components/cloud/http_api.py | 9 ++++++--- tests/components/cloud/test_http_api.py | 12 ++++++++++++ 2 files changed, 18 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/cloud/http_api.py b/homeassistant/components/cloud/http_api.py index 03a77c08d4b..a2825eb6d7b 100644 --- a/homeassistant/components/cloud/http_api.py +++ b/homeassistant/components/cloud/http_api.py @@ -107,13 +107,16 @@ def _handle_cloud_errors(handler): result = await handler(view, request, *args, **kwargs) return result - except (auth_api.CloudError, asyncio.TimeoutError) as err: + except Exception as err: # pylint: disable=broad-except err_info = _CLOUD_ERRORS.get(err.__class__) if err_info is None: + _LOGGER.exception( + "Unexpected error processing request for %s", request.path) err_info = (502, 'Unexpected error: {}'.format(err)) status, msg = err_info - return view.json_message(msg, status_code=status, - message_code=err.__class__.__name__) + return view.json_message( + msg, status_code=status, + message_code=err.__class__.__name__.lower()) return error_handler diff --git a/tests/components/cloud/test_http_api.py b/tests/components/cloud/test_http_api.py index 84d35f4bdd8..06de6bf0b59 100644 --- a/tests/components/cloud/test_http_api.py +++ b/tests/components/cloud/test_http_api.py @@ -111,6 +111,18 @@ def test_login_view(hass, cloud_client, mock_cognito): assert result_pass == 'my_password' +async def test_login_view_random_exception(cloud_client): + """Try logging in with invalid JSON.""" + with patch('async_timeout.timeout', side_effect=ValueError('Boom')): + req = await cloud_client.post('/api/cloud/login', json={ + 'email': 'my_username', + 'password': 'my_password' + }) + assert req.status == 502 + resp = await req.json() + assert resp == {'code': 'valueerror', 'message': 'Unexpected error: Boom'} + + @asyncio.coroutine def test_login_view_invalid_json(cloud_client): """Try logging in with invalid JSON.""" From cfa03a408e21ed7e3d5a6b32561dc8db2b47a245 Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Mon, 4 Feb 2019 19:58:38 +0100 Subject: [PATCH 214/222] Fix cloud webhook body (#20739) * Bugfix cloud webhooks text response * address comments * Fix lint --- homeassistant/components/cloud/iot.py | 7 +++---- homeassistant/components/cloud/utils.py | 13 +++++++++++++ homeassistant/util/aiohttp.py | 10 ---------- tests/components/cloud/test_utils.py | 24 ++++++++++++++++++++++++ tests/util/test_aiohttp.py | 21 --------------------- 5 files changed, 40 insertions(+), 35 deletions(-) create mode 100644 homeassistant/components/cloud/utils.py create mode 100644 tests/components/cloud/test_utils.py diff --git a/homeassistant/components/cloud/iot.py b/homeassistant/components/cloud/iot.py index ed24fe48d40..d725cb309bc 100644 --- a/homeassistant/components/cloud/iot.py +++ b/homeassistant/components/cloud/iot.py @@ -12,9 +12,10 @@ from homeassistant.components.alexa import smart_home as alexa from homeassistant.components.google_assistant import smart_home as ga from homeassistant.core import callback from homeassistant.util.decorator import Registry -from homeassistant.util.aiohttp import MockRequest, serialize_response +from homeassistant.util.aiohttp import MockRequest from homeassistant.helpers.aiohttp_client import async_get_clientsession from . import auth_api +from . import utils from .const import MESSAGE_EXPIRATION, MESSAGE_AUTH_FAIL HANDLERS = Registry() @@ -360,10 +361,8 @@ async def async_handle_webhook(hass, cloud, payload): response = await hass.components.webhook.async_handle_webhook( found['webhook_id'], request) - response_dict = serialize_response(response) + response_dict = utils.aiohttp_serialize_response(response) body = response_dict.get('body') - if body: - body = body.decode('utf-8') return { 'body': body, diff --git a/homeassistant/components/cloud/utils.py b/homeassistant/components/cloud/utils.py new file mode 100644 index 00000000000..da1d3809989 --- /dev/null +++ b/homeassistant/components/cloud/utils.py @@ -0,0 +1,13 @@ +"""Helper functions for cloud components.""" +from typing import Any, Dict + +from aiohttp import web + + +def aiohttp_serialize_response(response: web.Response) -> Dict[str, Any]: + """Serialize an aiohttp response to a dictionary.""" + return { + 'status': response.status, + 'body': response.text, + 'headers': dict(response.headers), + } diff --git a/homeassistant/util/aiohttp.py b/homeassistant/util/aiohttp.py index d648ed43110..16fea129573 100644 --- a/homeassistant/util/aiohttp.py +++ b/homeassistant/util/aiohttp.py @@ -3,7 +3,6 @@ import json from urllib.parse import parse_qsl from typing import Any, Dict, Optional -from aiohttp import web from multidict import CIMultiDict, MultiDict @@ -42,12 +41,3 @@ class MockRequest: async def text(self) -> str: """Return the body as text.""" return self._text - - -def serialize_response(response: web.Response) -> Dict[str, Any]: - """Serialize an aiohttp response to a dictionary.""" - return { - 'status': response.status, - 'body': response.body, - 'headers': dict(response.headers), - } diff --git a/tests/components/cloud/test_utils.py b/tests/components/cloud/test_utils.py new file mode 100644 index 00000000000..24de4ce6214 --- /dev/null +++ b/tests/components/cloud/test_utils.py @@ -0,0 +1,24 @@ +"""Test aiohttp request helper.""" +from aiohttp import web + +from homeassistant.components.cloud import utils + + +def test_serialize_text(): + """Test serializing a text response.""" + response = web.Response(status=201, text='Hello') + assert utils.aiohttp_serialize_response(response) == { + 'status': 201, + 'body': 'Hello', + 'headers': {'Content-Type': 'text/plain; charset=utf-8'}, + } + + +def test_serialize_json(): + """Test serializing a JSON response.""" + response = web.json_response({"how": "what"}) + assert utils.aiohttp_serialize_response(response) == { + 'status': 200, + 'body': '{"how": "what"}', + 'headers': {'Content-Type': 'application/json; charset=utf-8'}, + } diff --git a/tests/util/test_aiohttp.py b/tests/util/test_aiohttp.py index 8f528376cce..5df1582da32 100644 --- a/tests/util/test_aiohttp.py +++ b/tests/util/test_aiohttp.py @@ -1,5 +1,4 @@ """Test aiohttp request helper.""" -from aiohttp import web from homeassistant.util import aiohttp @@ -32,23 +31,3 @@ async def test_request_post_query(): assert request.query == { 'get': 'true' } - - -def test_serialize_text(): - """Test serializing a text response.""" - response = web.Response(status=201, text='Hello') - assert aiohttp.serialize_response(response) == { - 'status': 201, - 'body': b'Hello', - 'headers': {'Content-Type': 'text/plain; charset=utf-8'}, - } - - -def test_serialize_json(): - """Test serializing a JSON response.""" - response = web.json_response({"how": "what"}) - assert aiohttp.serialize_response(response) == { - 'status': 200, - 'body': b'{"how": "what"}', - 'headers': {'Content-Type': 'application/json; charset=utf-8'}, - } From a8b4467763f116b7cdb35a1cc44f79451f4754ce Mon Sep 17 00:00:00 2001 From: Jason Hu Date: Mon, 4 Feb 2019 10:58:06 -0800 Subject: [PATCH 215/222] Fix the line reference in config error message (#20743) * Fix the line reference in config error message * Fix platform config validation * Fix test * Handle error in error handling routine --- homeassistant/config.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/homeassistant/config.py b/homeassistant/config.py index 2a9f8f64835..5dbf226ca25 100644 --- a/homeassistant/config.py +++ b/homeassistant/config.py @@ -446,7 +446,11 @@ def _format_config_error(ex: vol.Invalid, domain: str, config: Dict) -> str: else: message += '{}.'.format(humanize_error(config, ex)) - domain_config = config.get(domain, config) + try: + domain_config = config.get(domain, config) + except AttributeError: + domain_config = config + message += " (See {}, line {}). ".format( getattr(domain_config, '__config_file__', '?'), getattr(domain_config, '__line__', '?')) @@ -759,7 +763,7 @@ def async_process_component_config( p_validated = component.PLATFORM_SCHEMA( # type: ignore p_config) except vol.Invalid as ex: - async_log_exception(ex, domain, config, hass) + async_log_exception(ex, domain, p_config, hass) continue # Not all platform components follow same pattern for platforms @@ -779,10 +783,10 @@ def async_process_component_config( # pylint: disable=no-member try: p_validated = platform.PLATFORM_SCHEMA( # type: ignore - p_validated) + p_config) except vol.Invalid as ex: async_log_exception(ex, '{}.{}'.format(domain, p_name), - p_validated, hass) + p_config, hass) continue platforms.append(p_validated) From 7f762105497fa775df6620add5e74780591363a4 Mon Sep 17 00:00:00 2001 From: Steven Looman Date: Tue, 5 Feb 2019 01:36:25 +0100 Subject: [PATCH 216/222] Upgrade to async_upnp_client==0.14.4 (#20751) --- homeassistant/components/media_player/dlna_dmr.py | 2 +- homeassistant/components/upnp/__init__.py | 2 +- requirements_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/media_player/dlna_dmr.py b/homeassistant/components/media_player/dlna_dmr.py index 0121f6e98d3..802b2b597fc 100644 --- a/homeassistant/components/media_player/dlna_dmr.py +++ b/homeassistant/components/media_player/dlna_dmr.py @@ -26,7 +26,7 @@ from homeassistant.helpers.aiohttp_client import async_get_clientsession import homeassistant.helpers.config_validation as cv from homeassistant.util import get_local_ip -REQUIREMENTS = ['async-upnp-client==0.14.3'] +REQUIREMENTS = ['async-upnp-client==0.14.4'] _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/upnp/__init__.py b/homeassistant/components/upnp/__init__.py index 494c39c06ba..2a1b8c52d79 100644 --- a/homeassistant/components/upnp/__init__.py +++ b/homeassistant/components/upnp/__init__.py @@ -29,7 +29,7 @@ from .const import LOGGER as _LOGGER from .device import Device -REQUIREMENTS = ['async-upnp-client==0.14.3'] +REQUIREMENTS = ['async-upnp-client==0.14.4'] NOTIFICATION_ID = 'upnp_notification' NOTIFICATION_TITLE = 'UPnP/IGD Setup' diff --git a/requirements_all.txt b/requirements_all.txt index a7ea2f5d7d5..80755957f17 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -167,7 +167,7 @@ asterisk_mbox==0.5.0 # homeassistant.components.upnp # homeassistant.components.media_player.dlna_dmr -async-upnp-client==0.14.3 +async-upnp-client==0.14.4 # homeassistant.components.light.avion # avion==0.10 From 3d75e1c2994d2c23b4ae5c316d05e915c3211f00 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 5 Feb 2019 01:45:03 -0800 Subject: [PATCH 217/222] Keep cloud tokens always valid (#20762) * Keep auth token always valid * Remove unused refresh_auth message * Capture EndpointConnectionError * Lint --- homeassistant/components/cloud/__init__.py | 3 +- homeassistant/components/cloud/auth_api.py | 73 +++++++++++++++++++--- homeassistant/components/cloud/iot.py | 30 +++++++-- tests/components/cloud/test_auth_api.py | 29 +++++++++ tests/components/cloud/test_iot.py | 23 +------ 5 files changed, 122 insertions(+), 36 deletions(-) diff --git a/homeassistant/components/cloud/__init__.py b/homeassistant/components/cloud/__init__.py index d938dd20e67..98e649e1742 100644 --- a/homeassistant/components/cloud/__init__.py +++ b/homeassistant/components/cloud/__init__.py @@ -106,6 +106,7 @@ async def async_setup(hass, config): ) cloud = hass.data[DOMAIN] = Cloud(hass, **kwargs) + await auth_api.async_setup(hass, cloud) hass.bus.async_listen_once(EVENT_HOMEASSISTANT_START, cloud.async_start) await http_api.async_setup(hass) return True @@ -263,7 +264,7 @@ class Cloud: self.access_token = info['access_token'] self.refresh_token = info['refresh_token'] - self.hass.add_job(self.iot.connect()) + self.hass.async_create_task(self.iot.connect()) def _decode_claims(self, token): # pylint: disable=no-self-use """Decode the claims in a token.""" diff --git a/homeassistant/components/cloud/auth_api.py b/homeassistant/components/cloud/auth_api.py index 954d28b803f..6019dac87b9 100644 --- a/homeassistant/components/cloud/auth_api.py +++ b/homeassistant/components/cloud/auth_api.py @@ -1,4 +1,10 @@ """Package to communicate with the authentication API.""" +import asyncio +import logging +import random + + +_LOGGER = logging.getLogger(__name__) class CloudError(Exception): @@ -39,6 +45,40 @@ AWS_EXCEPTIONS = { } +async def async_setup(hass, cloud): + """Configure the auth api.""" + refresh_task = None + + async def handle_token_refresh(): + """Handle Cloud access token refresh.""" + sleep_time = 5 + sleep_time = random.randint(2400, 3600) + while True: + try: + await asyncio.sleep(sleep_time) + await hass.async_add_executor_job(renew_access_token, cloud) + except CloudError as err: + _LOGGER.error("Can't refresh cloud token: %s", err) + except asyncio.CancelledError: + # Task is canceled, stop it. + break + + sleep_time = random.randint(3100, 3600) + + async def on_connect(): + """When the instance is connected.""" + nonlocal refresh_task + refresh_task = hass.async_create_task(handle_token_refresh()) + + async def on_disconnect(): + """When the instance is disconnected.""" + nonlocal refresh_task + refresh_task.cancel() + + cloud.iot.register_on_connect(on_connect) + cloud.iot.register_on_disconnect(on_disconnect) + + def _map_aws_exception(err): """Map AWS exception to our exceptions.""" ex = AWS_EXCEPTIONS.get(err.response['Error']['Code'], UnknownError) @@ -47,7 +87,7 @@ def _map_aws_exception(err): def register(cloud, email, password): """Register a new account.""" - from botocore.exceptions import ClientError + from botocore.exceptions import ClientError, EndpointConnectionError cognito = _cognito(cloud) # Workaround for bug in Warrant. PR with fix: @@ -55,13 +95,16 @@ def register(cloud, email, password): cognito.add_base_attributes() try: cognito.register(email, password) + except ClientError as err: raise _map_aws_exception(err) + except EndpointConnectionError: + raise UnknownError() def resend_email_confirm(cloud, email): """Resend email confirmation.""" - from botocore.exceptions import ClientError + from botocore.exceptions import ClientError, EndpointConnectionError cognito = _cognito(cloud, username=email) @@ -72,18 +115,23 @@ def resend_email_confirm(cloud, email): ) except ClientError as err: raise _map_aws_exception(err) + except EndpointConnectionError: + raise UnknownError() def forgot_password(cloud, email): """Initialize forgotten password flow.""" - from botocore.exceptions import ClientError + from botocore.exceptions import ClientError, EndpointConnectionError cognito = _cognito(cloud, username=email) try: cognito.initiate_forgot_password() + except ClientError as err: raise _map_aws_exception(err) + except EndpointConnectionError: + raise UnknownError() def login(cloud, email, password): @@ -97,7 +145,7 @@ def login(cloud, email, password): def check_token(cloud): """Check that the token is valid and verify if needed.""" - from botocore.exceptions import ClientError + from botocore.exceptions import ClientError, EndpointConnectionError cognito = _cognito( cloud, @@ -109,13 +157,17 @@ def check_token(cloud): cloud.id_token = cognito.id_token cloud.access_token = cognito.access_token cloud.write_user_info() + except ClientError as err: raise _map_aws_exception(err) + except EndpointConnectionError: + raise UnknownError() + def renew_access_token(cloud): """Renew access token.""" - from botocore.exceptions import ClientError + from botocore.exceptions import ClientError, EndpointConnectionError cognito = _cognito( cloud, @@ -127,13 +179,17 @@ def renew_access_token(cloud): cloud.id_token = cognito.id_token cloud.access_token = cognito.access_token cloud.write_user_info() + except ClientError as err: raise _map_aws_exception(err) + except EndpointConnectionError: + raise UnknownError() + def _authenticate(cloud, email, password): """Log in and return an authenticated Cognito instance.""" - from botocore.exceptions import ClientError + from botocore.exceptions import ClientError, EndpointConnectionError from warrant.exceptions import ForceChangePasswordException assert not cloud.is_logged_in, 'Cannot login if already logged in.' @@ -145,11 +201,14 @@ def _authenticate(cloud, email, password): return cognito except ForceChangePasswordException: - raise PasswordChangeRequired + raise PasswordChangeRequired() except ClientError as err: raise _map_aws_exception(err) + except EndpointConnectionError: + raise UnknownError() + def _cognito(cloud, **kwargs): """Get the client credentials.""" diff --git a/homeassistant/components/cloud/iot.py b/homeassistant/components/cloud/iot.py index d725cb309bc..055c4dbaa64 100644 --- a/homeassistant/components/cloud/iot.py +++ b/homeassistant/components/cloud/iot.py @@ -62,12 +62,18 @@ class CloudIoT: # Local code waiting for a response self._response_handler = {} self._on_connect = [] + self._on_disconnect = [] @callback def register_on_connect(self, on_connect_cb): """Register an async on_connect callback.""" self._on_connect.append(on_connect_cb) + @callback + def register_on_disconnect(self, on_disconnect_cb): + """Register an async on_disconnect callback.""" + self._on_disconnect.append(on_disconnect_cb) + @property def connected(self): """Return if we're currently connected.""" @@ -102,6 +108,17 @@ class CloudIoT: # Still adding it here to make sure we can always reconnect _LOGGER.exception("Unexpected error") + if self.state == STATE_CONNECTED and self._on_disconnect: + try: + yield from asyncio.wait([ + cb() for cb in self._on_disconnect + ]) + except Exception: # pylint: disable=broad-except + # Safety net. This should never hit. + # Still adding it here to make sure we don't break the flow + _LOGGER.exception( + "Unexpected error in on_disconnect callbacks") + if self.close_requested: break @@ -192,7 +209,13 @@ class CloudIoT: self.state = STATE_CONNECTED if self._on_connect: - yield from asyncio.wait([cb() for cb in self._on_connect]) + try: + yield from asyncio.wait([cb() for cb in self._on_connect]) + except Exception: # pylint: disable=broad-except + # Safety net. This should never hit. + # Still adding it here to make sure we don't break the flow + _LOGGER.exception( + "Unexpected error in on_connect callbacks") while not client.closed: msg = yield from client.receive() @@ -326,11 +349,6 @@ async def async_handle_cloud(hass, cloud, payload): await cloud.logout() _LOGGER.error("You have been logged out from Home Assistant cloud: %s", payload['reason']) - elif action == 'refresh_auth': - # Refresh the auth token between now and payload['seconds'] - hass.helpers.event.async_call_later( - random.randint(0, payload['seconds']), - lambda now: auth_api.check_token(cloud)) else: _LOGGER.warning("Received unknown cloud action: %s", action) diff --git a/tests/components/cloud/test_auth_api.py b/tests/components/cloud/test_auth_api.py index a50a4d796aa..bdf9939cb2b 100644 --- a/tests/components/cloud/test_auth_api.py +++ b/tests/components/cloud/test_auth_api.py @@ -1,4 +1,5 @@ """Tests for the tools to communicate with the cloud.""" +import asyncio from unittest.mock import MagicMock, patch from botocore.exceptions import ClientError @@ -165,3 +166,31 @@ def test_check_token_raises(mock_cognito): assert cloud.id_token != mock_cognito.id_token assert cloud.access_token != mock_cognito.access_token assert len(cloud.write_user_info.mock_calls) == 0 + + +async def test_async_setup(hass): + """Test async setup.""" + cloud = MagicMock() + await auth_api.async_setup(hass, cloud) + assert len(cloud.iot.mock_calls) == 2 + on_connect = cloud.iot.mock_calls[0][1][0] + on_disconnect = cloud.iot.mock_calls[1][1][0] + + with patch('random.randint', return_value=0), patch( + 'homeassistant.components.cloud.auth_api.renew_access_token' + ) as mock_renew: + await on_connect() + # Let handle token sleep once + await asyncio.sleep(0) + # Let handle token refresh token + await asyncio.sleep(0) + + assert len(mock_renew.mock_calls) == 1 + assert mock_renew.mock_calls[0][1][0] is cloud + + await on_disconnect() + + # Make sure task is no longer being called + await asyncio.sleep(0) + await asyncio.sleep(0) + assert len(mock_renew.mock_calls) == 1 diff --git a/tests/components/cloud/test_iot.py b/tests/components/cloud/test_iot.py index 1a528f8cedf..10a94f46833 100644 --- a/tests/components/cloud/test_iot.py +++ b/tests/components/cloud/test_iot.py @@ -10,9 +10,8 @@ from homeassistant.components.cloud import ( Cloud, iot, auth_api, MODE_DEV) from homeassistant.components.cloud.const import ( PREF_ENABLE_ALEXA, PREF_ENABLE_GOOGLE) -from homeassistant.util import dt as dt_util from tests.components.alexa import test_smart_home as test_alexa -from tests.common import mock_coro, async_fire_time_changed +from tests.common import mock_coro from . import mock_cloud_prefs @@ -158,26 +157,6 @@ async def test_handling_core_messages_logout(hass, mock_cloud): assert len(mock_cloud.logout.mock_calls) == 1 -async def test_handling_core_messages_refresh_auth(hass, mock_cloud): - """Test handling core messages.""" - mock_cloud.hass = hass - with patch('random.randint', return_value=0) as mock_rand, patch( - 'homeassistant.components.cloud.auth_api.check_token' - ) as mock_check: - await iot.async_handle_cloud(hass, mock_cloud, { - 'action': 'refresh_auth', - 'seconds': 230, - }) - async_fire_time_changed(hass, dt_util.utcnow()) - await hass.async_block_till_done() - - assert len(mock_rand.mock_calls) == 1 - assert mock_rand.mock_calls[0][1] == (0, 230) - - assert len(mock_check.mock_calls) == 1 - assert mock_check.mock_calls[0][1][0] is mock_cloud - - @asyncio.coroutine def test_cloud_getting_disconnected_by_server(mock_client, caplog, mock_cloud): """Test server disconnecting instance.""" From e8dfc326d3f4fa1749bc73fce7adbd245da81390 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 5 Feb 2019 08:04:49 -0800 Subject: [PATCH 218/222] Bumped version to 0.87.0b6 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index a44d90551cb..3549dee7fc6 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -2,7 +2,7 @@ """Constants used by Home Assistant components.""" MAJOR_VERSION = 0 MINOR_VERSION = 87 -PATCH_VERSION = '0b5' +PATCH_VERSION = '0b6' __short_version__ = '{}.{}'.format(MAJOR_VERSION, MINOR_VERSION) __version__ = '{}.{}'.format(__short_version__, PATCH_VERSION) REQUIRED_PYTHON_VER = (3, 5, 3) From a6bcb515f93379e2827a9796004a6dba566009b0 Mon Sep 17 00:00:00 2001 From: Fredrik Erlandsson Date: Wed, 6 Feb 2019 00:20:23 +0100 Subject: [PATCH 219/222] Fix tellduslive responsiveness (#20603) * use async_call_later for update * no need to timeout * fixes * move init tasks to hass.loop * version bump of tellduslive * fixes for @MartinHjelmare * fixes task cancel * don't return from new client --- .../components/tellduslive/__init__.py | 65 ++++++++++--------- requirements_all.txt | 2 +- 2 files changed, 36 insertions(+), 31 deletions(-) diff --git a/homeassistant/components/tellduslive/__init__.py b/homeassistant/components/tellduslive/__init__.py index d9cd1be59da..2a57a78ee9e 100644 --- a/homeassistant/components/tellduslive/__init__.py +++ b/homeassistant/components/tellduslive/__init__.py @@ -5,27 +5,26 @@ For more details about this component, please refer to the documentation at https://home-assistant.io/components/tellduslive/ """ import asyncio -from datetime import timedelta from functools import partial import logging import voluptuous as vol from homeassistant import config_entries -import homeassistant.helpers.config_validation as cv from homeassistant.const import CONF_UPDATE_INTERVAL +import homeassistant.helpers.config_validation as cv from homeassistant.helpers.dispatcher import async_dispatcher_send -from homeassistant.helpers.event import async_track_time_interval +from homeassistant.helpers.event import async_call_later from . import config_flow # noqa pylint_disable=unused-import from .const import ( - CONF_HOST, DOMAIN, KEY_HOST, KEY_SCAN_INTERVAL, - KEY_SESSION, MIN_UPDATE_INTERVAL, NOT_SO_PRIVATE_KEY, PUBLIC_KEY, - SCAN_INTERVAL, SIGNAL_UPDATE_ENTITY, TELLDUS_DISCOVERY_NEW) + CONF_HOST, DOMAIN, KEY_HOST, KEY_SCAN_INTERVAL, KEY_SESSION, + MIN_UPDATE_INTERVAL, NOT_SO_PRIVATE_KEY, PUBLIC_KEY, SCAN_INTERVAL, + SIGNAL_UPDATE_ENTITY, TELLDUS_DISCOVERY_NEW) APPLICATION_NAME = 'Home Assistant' -REQUIREMENTS = ['tellduslive==0.10.8'] +REQUIREMENTS = ['tellduslive==0.10.10'] _LOGGER = logging.getLogger(__name__) @@ -45,6 +44,7 @@ CONFIG_SCHEMA = vol.Schema( DATA_CONFIG_ENTRY_LOCK = 'tellduslive_config_entry_lock' CONFIG_ENTRY_IS_SETUP = 'telldus_config_entry_is_setup' +NEW_CLIENT_TASK = 'telldus_new_client_task' INTERVAL_TRACKER = '{}_INTERVAL'.format(DOMAIN) @@ -71,33 +71,30 @@ async def async_setup_entry(hass, entry): hass.data[DATA_CONFIG_ENTRY_LOCK] = asyncio.Lock() hass.data[CONFIG_ENTRY_IS_SETUP] = set() - - client = TelldusLiveClient(hass, entry, session) - hass.data[DOMAIN] = client - await async_add_hubs(hass, client, entry.entry_id) - hass.async_create_task(client.update()) - - interval = timedelta(seconds=entry.data[KEY_SCAN_INTERVAL]) - _LOGGER.debug('Update interval %s', interval) - hass.data[INTERVAL_TRACKER] = async_track_time_interval( - hass, client.update, interval) + hass.data[NEW_CLIENT_TASK] = hass.loop.create_task( + async_new_client(hass, session, entry)) return True -async def async_add_hubs(hass, client, entry_id): +async def async_new_client(hass, session, entry): """Add the hubs associated with the current client to device_registry.""" + interval = entry.data[KEY_SCAN_INTERVAL] + _LOGGER.debug('Update interval %s seconds.', interval) + client = TelldusLiveClient(hass, entry, session, interval) + hass.data[DOMAIN] = client dev_reg = await hass.helpers.device_registry.async_get_registry() for hub in await client.async_get_hubs(): _LOGGER.debug("Connected hub %s", hub['name']) dev_reg.async_get_or_create( - config_entry_id=entry_id, + config_entry_id=entry.entry_id, identifiers={(DOMAIN, hub['id'])}, manufacturer='Telldus', name=hub['name'], model=hub['type'], sw_version=hub['version'], ) + await client.update() async def async_setup(hass, config): @@ -118,6 +115,8 @@ async def async_setup(hass, config): async def async_unload_entry(hass, config_entry): """Unload a config entry.""" + if not hass.data[NEW_CLIENT_TASK].done(): + hass.data[NEW_CLIENT_TASK].cancel() interval_tracker = hass.data.pop(INTERVAL_TRACKER) interval_tracker() await asyncio.wait([ @@ -132,7 +131,7 @@ async def async_unload_entry(hass, config_entry): class TelldusLiveClient: """Get the latest data and update the states.""" - def __init__(self, hass, config_entry, session): + def __init__(self, hass, config_entry, session, interval): """Initialize the Tellus data object.""" self._known_devices = set() self._device_infos = {} @@ -140,6 +139,7 @@ class TelldusLiveClient: self._hass = hass self._config_entry = config_entry self._client = session + self._interval = interval async def async_get_hubs(self): """Return hubs registered for the user.""" @@ -195,16 +195,21 @@ class TelldusLiveClient: async def update(self, *args): """Periodically poll the servers for current state.""" - if not await self._hass.async_add_executor_job(self._client.update): - _LOGGER.warning('Failed request') - - dev_ids = {dev.device_id for dev in self._client.devices} - new_devices = dev_ids - self._known_devices - # just await each discover as `gather` use up all HTTPAdapter pools - for d_id in new_devices: - await self._discover(d_id) - self._known_devices |= new_devices - async_dispatcher_send(self._hass, SIGNAL_UPDATE_ENTITY) + try: + if not await self._hass.async_add_executor_job( + self._client.update): + _LOGGER.warning('Failed request') + return + dev_ids = {dev.device_id for dev in self._client.devices} + new_devices = dev_ids - self._known_devices + # just await each discover as `gather` use up all HTTPAdapter pools + for d_id in new_devices: + await self._discover(d_id) + self._known_devices |= new_devices + async_dispatcher_send(self._hass, SIGNAL_UPDATE_ENTITY) + finally: + self._hass.data[INTERVAL_TRACKER] = async_call_later( + self._hass, self._interval, self.update) def device(self, device_id): """Return device representation.""" diff --git a/requirements_all.txt b/requirements_all.txt index 80755957f17..9e6fc503908 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1628,7 +1628,7 @@ tellcore-net==0.4 tellcore-py==1.1.2 # homeassistant.components.tellduslive -tellduslive==0.10.8 +tellduslive==0.10.10 # homeassistant.components.media_player.lg_soundbar temescal==0.1 From 58b7905276d7986bed4c90618574c851d77e9de5 Mon Sep 17 00:00:00 2001 From: Erik Hendrix Date: Mon, 4 Feb 2019 16:48:35 -0700 Subject: [PATCH 220/222] Update version for pymyq to 1.1.0 Update version of pymyq to 1.1.0; this version brings improved functionality, reducing errors for retrieving current state for the MyQ covers. --- homeassistant/components/cover/myq.py | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/cover/myq.py b/homeassistant/components/cover/myq.py index bdff232fec9..b2587c06512 100644 --- a/homeassistant/components/cover/myq.py +++ b/homeassistant/components/cover/myq.py @@ -15,7 +15,7 @@ from homeassistant.const import ( STATE_OPEN, STATE_OPENING) from homeassistant.helpers import aiohttp_client, config_validation as cv -REQUIREMENTS = ['pymyq==1.0.0'] +REQUIREMENTS = ['pymyq==1.1.0'] _LOGGER = logging.getLogger(__name__) MYQ_TO_HASS = { diff --git a/requirements_all.txt b/requirements_all.txt index 9e6fc503908..39015bfb972 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1131,7 +1131,7 @@ pymonoprice==0.3 pymusiccast==0.1.6 # homeassistant.components.cover.myq -pymyq==1.0.0 +pymyq==1.1.0 # homeassistant.components.mysensors pymysensors==0.18.0 From 9912e0fc48d3c0b2847898752522d8b153776021 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 6 Feb 2019 09:50:48 -0800 Subject: [PATCH 221/222] Make sure Locative doesn't submit invalid device IDs (#20784) --- homeassistant/components/locative/device_tracker.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/locative/device_tracker.py b/homeassistant/components/locative/device_tracker.py index 20808c773f0..78090914b2c 100644 --- a/homeassistant/components/locative/device_tracker.py +++ b/homeassistant/components/locative/device_tracker.py @@ -11,6 +11,7 @@ from homeassistant.components.device_tracker import \ from homeassistant.components.locative import DOMAIN as LOCATIVE_DOMAIN from homeassistant.components.locative import TRACKER_UPDATE from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.util import slugify _LOGGER = logging.getLogger(__name__) @@ -24,7 +25,7 @@ async def async_setup_entry(hass, entry, async_see): async def _set_location(device, gps_location, location_name): """Fire HA event to set location.""" await async_see( - dev_id=device, + dev_id=slugify(device), gps=gps_location, location_name=location_name ) From 180689fb047f84a64f171c9683e29edadc7e8776 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 6 Feb 2019 11:49:56 -0800 Subject: [PATCH 222/222] Bumped version to 0.87.0 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 3549dee7fc6..3a260501e32 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -2,7 +2,7 @@ """Constants used by Home Assistant components.""" MAJOR_VERSION = 0 MINOR_VERSION = 87 -PATCH_VERSION = '0b6' +PATCH_VERSION = '0' __short_version__ = '{}.{}'.format(MAJOR_VERSION, MINOR_VERSION) __version__ = '{}.{}'.format(__short_version__, PATCH_VERSION) REQUIRED_PYTHON_VER = (3, 5, 3)