From b9ad19acbf15ca93e1646f47720ac4ee0208dab3 Mon Sep 17 00:00:00 2001 From: Erik Date: Sun, 2 Dec 2018 17:00:31 +0100 Subject: [PATCH 001/304] Add JSON attribute topic to MQTT binary sensor Add MqttAttributes mixin --- .../components/binary_sensor/mqtt.py | 14 ++-- homeassistant/components/mqtt/__init__.py | 65 +++++++++++++++++++ tests/components/binary_sensor/test_mqtt.py | 63 +++++++++++++++++- 3 files changed, 136 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/binary_sensor/mqtt.py b/homeassistant/components/binary_sensor/mqtt.py index acbad0d0419..d2a2be88172 100644 --- a/homeassistant/components/binary_sensor/mqtt.py +++ b/homeassistant/components/binary_sensor/mqtt.py @@ -16,10 +16,10 @@ from homeassistant.const import ( CONF_FORCE_UPDATE, CONF_NAME, CONF_VALUE_TEMPLATE, CONF_PAYLOAD_ON, CONF_PAYLOAD_OFF, CONF_DEVICE_CLASS, CONF_DEVICE) from homeassistant.components.mqtt import ( - ATTR_DISCOVERY_HASH, CONF_STATE_TOPIC, CONF_AVAILABILITY_TOPIC, + ATTR_DISCOVERY_HASH, CONF_AVAILABILITY_TOPIC, CONF_STATE_TOPIC, CONF_PAYLOAD_AVAILABLE, CONF_PAYLOAD_NOT_AVAILABLE, CONF_QOS, - MqttAvailability, MqttDiscoveryUpdate, MqttEntityDeviceInfo, - subscription) + MqttAttributes, MqttAvailability, MqttDiscoveryUpdate, + MqttEntityDeviceInfo, subscription) from homeassistant.components.mqtt.discovery import MQTT_DISCOVERY_NEW import homeassistant.helpers.config_validation as cv from homeassistant.helpers.dispatcher import async_dispatcher_connect @@ -49,7 +49,8 @@ PLATFORM_SCHEMA = mqtt.MQTT_RO_PLATFORM_SCHEMA.extend({ # This is an exception because MQTT is a message transport, not a protocol 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, @@ -76,7 +77,7 @@ async def _async_setup_entity(config, async_add_entities, discovery_hash=None): async_add_entities([MqttBinarySensor(config, discovery_hash)]) -class MqttBinarySensor(MqttAvailability, MqttDiscoveryUpdate, +class MqttBinarySensor(MqttAttributes, MqttAvailability, MqttDiscoveryUpdate, MqttEntityDeviceInfo, BinarySensorDevice): """Representation a binary sensor that is updated by MQTT.""" @@ -94,6 +95,7 @@ class MqttBinarySensor(MqttAvailability, MqttDiscoveryUpdate, qos = config.get(CONF_QOS) device_config = config.get(CONF_DEVICE) + MqttAttributes.__init__(self, config) MqttAvailability.__init__(self, availability_topic, qos, payload_available, payload_not_available) MqttDiscoveryUpdate.__init__(self, discovery_hash, @@ -109,6 +111,7 @@ class MqttBinarySensor(MqttAvailability, MqttDiscoveryUpdate, """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() @@ -164,6 +167,7 @@ class MqttBinarySensor(MqttAvailability, MqttDiscoveryUpdate, 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) @property diff --git a/homeassistant/components/mqtt/__init__.py b/homeassistant/components/mqtt/__init__.py index 7ff32a79142..11b837113c5 100644 --- a/homeassistant/components/mqtt/__init__.py +++ b/homeassistant/components/mqtt/__init__.py @@ -6,6 +6,7 @@ https://home-assistant.io/components/mqtt/ """ import asyncio from itertools import groupby +import json import logging from operator import attrgetter import os @@ -70,6 +71,7 @@ CONF_COMMAND_TOPIC = 'command_topic' CONF_AVAILABILITY_TOPIC = 'availability_topic' CONF_PAYLOAD_AVAILABLE = 'payload_available' CONF_PAYLOAD_NOT_AVAILABLE = 'payload_not_available' +CONF_JSON_ATTRS_TOPIC = 'json_attributes_topic' CONF_QOS = 'qos' CONF_RETAIN = 'retain' @@ -224,6 +226,10 @@ MQTT_ENTITY_DEVICE_INFO_SCHEMA = vol.All(vol.Schema({ vol.Optional(CONF_SW_VERSION): cv.string, }), validate_device_has_at_least_one_identifier) +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) # Sensor type platforms subscribe to MQTT events @@ -820,6 +826,65 @@ def _match_topic(subscription: str, topic: str) -> bool: return False +class MqttAttributes(Entity): + """Mixin used for platforms that support JSON attributes.""" + + def __init__(self, config: dict) -> None: + """Initialize the JSON attributes mixin.""" + self._attributes = None + self._attributes_sub_state = None + self._attributes_config = config + + async def async_added_to_hass(self) -> None: + """Subscribe MQTT events. + + This method must be run in the event loop and returns a coroutine. + """ + await self._attributes_subscribe_topics() + + async def attributes_discovery_update(self, config: dict): + """Handle updated discovery message.""" + self._attributes_config = config + await self._attributes_subscribe_topics() + + async def _attributes_subscribe_topics(self): + """(Re)Subscribe to topics.""" + from .subscription import async_subscribe_topics + + @callback + def attributes_message_received(topic: str, + payload: SubscribePayloadType, + qos: int) -> None: + try: + json_dict = json.loads(payload) + if isinstance(json_dict, dict): + self._attributes = json_dict + self.async_schedule_update_ha_state() + else: + _LOGGER.debug("JSON result was not a dictionary") + self._attributes = None + except ValueError: + _LOGGER.debug("Erroneous JSON: %s", payload) + self._attributes = None + + self._attributes_sub_state = await async_subscribe_topics( + self.hass, self._attributes_sub_state, + {'attributes_topic': { + 'topic': self._attributes_config.get(CONF_JSON_ATTRS_TOPIC), + 'msg_callback': attributes_message_received, + 'qos': self._attributes_config.get(CONF_QOS)}}) + + async def async_will_remove_from_hass(self): + """Unsubscribe when removed.""" + from .subscription import async_unsubscribe_topics + await async_unsubscribe_topics(self.hass, self._attributes_sub_state) + + @property + def device_state_attributes(self): + """Return the state attributes.""" + return self._attributes + + class MqttAvailability(Entity): """Mixin used for platforms that report availability.""" diff --git a/tests/components/binary_sensor/test_mqtt.py b/tests/components/binary_sensor/test_mqtt.py index 71d179211a2..a4357eefed8 100644 --- a/tests/components/binary_sensor/test_mqtt.py +++ b/tests/components/binary_sensor/test_mqtt.py @@ -1,7 +1,7 @@ """The tests for the MQTT binary sensor platform.""" import json import unittest -from unittest.mock import Mock +from unittest.mock import Mock, patch from datetime import timedelta import homeassistant.core as ha @@ -256,6 +256,67 @@ 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') + + assert '100' == \ + state.attributes.get('val') + + @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' + } + }) + + 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.debug.assert_called_with( + 'JSON result was not a dictionary') + + @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' + } + }) + + fire_mqtt_message(self.hass, 'attr-topic', 'This is not JSON') + self.hass.block_till_done() + + state = self.hass.states.get('binary_sensor.test') + assert state.attributes.get('val') is None + mock_logger.debug.assert_called_with( + 'Erroneous JSON: %s', 'This is not JSON') + async def test_unique_id(hass): """Test unique id option only creates one sensor per unique_id.""" From 21197fb9689ee63813560b0238453a2dbd14a9af Mon Sep 17 00:00:00 2001 From: Erik Date: Tue, 4 Dec 2018 10:55:30 +0100 Subject: [PATCH 002/304] Review comments --- homeassistant/components/mqtt/__init__.py | 7 ++++--- tests/components/binary_sensor/test_mqtt.py | 4 ++-- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/mqtt/__init__.py b/homeassistant/components/mqtt/__init__.py index 11b837113c5..aa63adb72d8 100644 --- a/homeassistant/components/mqtt/__init__.py +++ b/homeassistant/components/mqtt/__init__.py @@ -840,6 +840,7 @@ class MqttAttributes(Entity): This method must be run in the event loop and returns a coroutine. """ + await super().async_added_to_hass() await self._attributes_subscribe_topics() async def attributes_discovery_update(self, config: dict): @@ -861,15 +862,15 @@ class MqttAttributes(Entity): self._attributes = json_dict self.async_schedule_update_ha_state() else: - _LOGGER.debug("JSON result was not a dictionary") + _LOGGER.warning("JSON result was not a dictionary") self._attributes = None except ValueError: - _LOGGER.debug("Erroneous JSON: %s", payload) + _LOGGER.warning("Erroneous JSON: %s", payload) self._attributes = None self._attributes_sub_state = await async_subscribe_topics( self.hass, self._attributes_sub_state, - {'attributes_topic': { + {CONF_JSON_ATTRS_TOPIC: { 'topic': self._attributes_config.get(CONF_JSON_ATTRS_TOPIC), 'msg_callback': attributes_message_received, 'qos': self._attributes_config.get(CONF_QOS)}}) diff --git a/tests/components/binary_sensor/test_mqtt.py b/tests/components/binary_sensor/test_mqtt.py index a4357eefed8..74c7d32927b 100644 --- a/tests/components/binary_sensor/test_mqtt.py +++ b/tests/components/binary_sensor/test_mqtt.py @@ -293,7 +293,7 @@ class TestSensorMQTT(unittest.TestCase): state = self.hass.states.get('binary_sensor.test') assert state.attributes.get('val') is None - mock_logger.debug.assert_called_with( + mock_logger.warning.assert_called_with( 'JSON result was not a dictionary') @patch('homeassistant.components.mqtt._LOGGER') @@ -314,7 +314,7 @@ class TestSensorMQTT(unittest.TestCase): state = self.hass.states.get('binary_sensor.test') assert state.attributes.get('val') is None - mock_logger.debug.assert_called_with( + mock_logger.warning.assert_called_with( 'Erroneous JSON: %s', 'This is not JSON') From 30c77b9e64662ebfe219083ed30e8a4506372f23 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 6 Dec 2018 09:36:44 +0100 Subject: [PATCH 003/304] Bumped version to 0.85.0.dev0 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index b4a94d318f6..3f24da30a0a 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -1,7 +1,7 @@ # coding: utf-8 """Constants used by Home Assistant components.""" MAJOR_VERSION = 0 -MINOR_VERSION = 84 +MINOR_VERSION = 85 PATCH_VERSION = '0.dev0' __short_version__ = '{}.{}'.format(MAJOR_VERSION, MINOR_VERSION) __version__ = '{}.{}'.format(__short_version__, PATCH_VERSION) From 04c7d5c128c61bc26caf6950ccb231cb27faacac Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 6 Dec 2018 10:38:26 +0100 Subject: [PATCH 004/304] Revert #17745 (#19064) --- homeassistant/components/google_assistant/http.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/google_assistant/http.py b/homeassistant/components/google_assistant/http.py index d688491fe89..f0294c3bcb2 100644 --- a/homeassistant/components/google_assistant/http.py +++ b/homeassistant/components/google_assistant/http.py @@ -48,7 +48,7 @@ def async_register_http(hass, cfg): entity_config.get(entity.entity_id, {}).get(CONF_EXPOSE) domain_exposed_by_default = \ - expose_by_default or entity.domain in exposed_domains + expose_by_default and entity.domain in exposed_domains # Expose an entity if the entity's domain is exposed by default and # the configuration doesn't explicitly exclude it from being From 1be440a72b79c96d633ed796b16f802e6aa0a1f2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ville=20Skytt=C3=A4?= Date: Thu, 6 Dec 2018 12:54:44 +0200 Subject: [PATCH 005/304] Upgrade pylint to 2.2.2 (#18750) * Upgrade to 2.2.0 * simplifiable-if-expression fixes * duplicate-string-formatting-argument fixes * unused-import fixes * Upgrade to 2.2.1 * Remove no longer needed disable * Upgrade to 2.2.2 --- .../components/binary_sensor/alarmdecoder.py | 16 ++++++++-------- .../components/binary_sensor/mystrom.py | 3 +-- homeassistant/components/frontend/__init__.py | 4 ++-- homeassistant/components/homekit/__init__.py | 2 +- homeassistant/components/homekit/type_fans.py | 2 +- .../components/media_player/bluesound.py | 2 +- .../components/media_player/ue_smart_radio.py | 2 +- .../components/owntracks/config_flow.py | 3 +-- homeassistant/components/sensor/bme680.py | 6 ++---- homeassistant/components/sensor/miflora.py | 2 +- homeassistant/components/sensor/mitemp_bt.py | 2 +- homeassistant/components/sensor/mvglive.py | 8 ++++---- homeassistant/components/sensor/statistics.py | 3 +-- homeassistant/components/spaceapi.py | 2 +- homeassistant/components/switch/raspihats.py | 4 ++-- homeassistant/config.py | 7 ++++--- homeassistant/helpers/intent.py | 3 +-- requirements_test.txt | 2 +- requirements_test_all.txt | 2 +- 19 files changed, 35 insertions(+), 40 deletions(-) diff --git a/homeassistant/components/binary_sensor/alarmdecoder.py b/homeassistant/components/binary_sensor/alarmdecoder.py index 1b50d6c6c72..f7a42e9b831 100644 --- a/homeassistant/components/binary_sensor/alarmdecoder.py +++ b/homeassistant/components/binary_sensor/alarmdecoder.py @@ -92,14 +92,14 @@ class AlarmDecoderBinarySensor(BinarySensorDevice): """Return the state attributes.""" attr = {} if self._rfid and self._rfstate is not None: - attr[ATTR_RF_BIT0] = True if self._rfstate & 0x01 else False - attr[ATTR_RF_LOW_BAT] = True if self._rfstate & 0x02 else False - attr[ATTR_RF_SUPERVISED] = True if self._rfstate & 0x04 else False - attr[ATTR_RF_BIT3] = True if self._rfstate & 0x08 else False - attr[ATTR_RF_LOOP3] = True if self._rfstate & 0x10 else False - attr[ATTR_RF_LOOP2] = True if self._rfstate & 0x20 else False - attr[ATTR_RF_LOOP4] = True if self._rfstate & 0x40 else False - attr[ATTR_RF_LOOP1] = True if self._rfstate & 0x80 else False + attr[ATTR_RF_BIT0] = bool(self._rfstate & 0x01) + attr[ATTR_RF_LOW_BAT] = bool(self._rfstate & 0x02) + attr[ATTR_RF_SUPERVISED] = bool(self._rfstate & 0x04) + attr[ATTR_RF_BIT3] = bool(self._rfstate & 0x08) + attr[ATTR_RF_LOOP3] = bool(self._rfstate & 0x10) + attr[ATTR_RF_LOOP2] = bool(self._rfstate & 0x20) + attr[ATTR_RF_LOOP4] = bool(self._rfstate & 0x40) + attr[ATTR_RF_LOOP1] = bool(self._rfstate & 0x80) return attr @property diff --git a/homeassistant/components/binary_sensor/mystrom.py b/homeassistant/components/binary_sensor/mystrom.py index 5785ed464fd..4927be27eb3 100644 --- a/homeassistant/components/binary_sensor/mystrom.py +++ b/homeassistant/components/binary_sensor/mystrom.py @@ -61,8 +61,7 @@ class MyStromView(HomeAssistantView): '{}_{}'.format(button_id, button_action)) self.add_entities([self.buttons[entity_id]]) else: - new_state = True if self.buttons[entity_id].state == 'off' \ - else False + new_state = self.buttons[entity_id].state == 'off' self.buttons[entity_id].async_on_update(new_state) diff --git a/homeassistant/components/frontend/__init__.py b/homeassistant/components/frontend/__init__.py index 43a4839bf43..77c98ab6aa2 100644 --- a/homeassistant/components/frontend/__init__.py +++ b/homeassistant/components/frontend/__init__.py @@ -55,8 +55,8 @@ MANIFEST_JSON = { for size in (192, 384, 512, 1024): MANIFEST_JSON['icons'].append({ - 'src': '/static/icons/favicon-{}x{}.png'.format(size, size), - 'sizes': '{}x{}'.format(size, size), + 'src': '/static/icons/favicon-{size}x{size}.png'.format(size=size), + 'sizes': '{size}x{size}'.format(size=size), 'type': 'image/png' }) diff --git a/homeassistant/components/homekit/__init__.py b/homeassistant/components/homekit/__init__.py index da8daf50f2a..c8aea5f8fb3 100644 --- a/homeassistant/components/homekit/__init__.py +++ b/homeassistant/components/homekit/__init__.py @@ -245,7 +245,7 @@ class HomeKit(): return self.status = STATUS_WAIT - # pylint: disable=unused-variable + # pylint: disable=unused-import from . import ( # noqa F401 type_covers, type_fans, type_lights, type_locks, type_media_players, type_security_systems, type_sensors, diff --git a/homeassistant/components/homekit/type_fans.py b/homeassistant/components/homekit/type_fans.py index 5a860ed21c8..2b4e55c4c8d 100644 --- a/homeassistant/components/homekit/type_fans.py +++ b/homeassistant/components/homekit/type_fans.py @@ -78,7 +78,7 @@ class Fan(HomeAccessory): """Set state if call came from HomeKit.""" _LOGGER.debug('%s: Set oscillating to %d', self.entity_id, value) self._flag[CHAR_SWING_MODE] = True - oscillating = True if value == 1 else False + oscillating = value == 1 params = {ATTR_ENTITY_ID: self.entity_id, ATTR_OSCILLATING: oscillating} self.call_service(DOMAIN, SERVICE_OSCILLATE, params, oscillating) diff --git a/homeassistant/components/media_player/bluesound.py b/homeassistant/components/media_player/bluesound.py index f4ed62b15cd..998f559bc8a 100644 --- a/homeassistant/components/media_player/bluesound.py +++ b/homeassistant/components/media_player/bluesound.py @@ -779,7 +779,7 @@ class BluesoundPlayer(MediaPlayerDevice): @property def shuffle(self): """Return true if shuffle is active.""" - return True if self._status.get('shuffle', '0') == '1' else False + return self._status.get('shuffle', '0') == '1' async def async_join(self, master): """Join the player to a group.""" diff --git a/homeassistant/components/media_player/ue_smart_radio.py b/homeassistant/components/media_player/ue_smart_radio.py index 066972aaa25..75f6d92a98c 100644 --- a/homeassistant/components/media_player/ue_smart_radio.py +++ b/homeassistant/components/media_player/ue_smart_radio.py @@ -136,7 +136,7 @@ class UERadioDevice(MediaPlayerDevice): @property def is_volume_muted(self): """Boolean if volume is currently muted.""" - return True if self._volume <= 0 else False + return self._volume <= 0 @property def volume_level(self): diff --git a/homeassistant/components/owntracks/config_flow.py b/homeassistant/components/owntracks/config_flow.py index 8cf19e84bcd..6818efbbf75 100644 --- a/homeassistant/components/owntracks/config_flow.py +++ b/homeassistant/components/owntracks/config_flow.py @@ -9,8 +9,7 @@ CONF_SECRET = 'secret' def supports_encryption(): """Test if we support encryption.""" try: - # pylint: disable=unused-variable - import libnacl # noqa + import libnacl # noqa pylint: disable=unused-import return True except OSError: return False diff --git a/homeassistant/components/sensor/bme680.py b/homeassistant/components/sensor/bme680.py index cbcb7f1080e..64d28c25bf0 100644 --- a/homeassistant/components/sensor/bme680.py +++ b/homeassistant/components/sensor/bme680.py @@ -179,10 +179,8 @@ def _setup_bme680(config): sensor_handler = BME680Handler( sensor, - True if ( - SENSOR_GAS in config[CONF_MONITORED_CONDITIONS] or - SENSOR_AQ in config[CONF_MONITORED_CONDITIONS] - ) else False, + (SENSOR_GAS in config[CONF_MONITORED_CONDITIONS] or + SENSOR_AQ in config[CONF_MONITORED_CONDITIONS]), config[CONF_AQ_BURN_IN_TIME], config[CONF_AQ_HUM_BASELINE], config[CONF_AQ_HUM_WEIGHTING] diff --git a/homeassistant/components/sensor/miflora.py b/homeassistant/components/sensor/miflora.py index 74bb8261609..412b339caf3 100644 --- a/homeassistant/components/sensor/miflora.py +++ b/homeassistant/components/sensor/miflora.py @@ -55,7 +55,7 @@ async def async_setup_platform(hass, config, async_add_entities, """Set up the MiFlora sensor.""" from miflora import miflora_poller try: - import bluepy.btle # noqa: F401 pylint: disable=unused-variable + import bluepy.btle # noqa: F401 pylint: disable=unused-import from btlewrap import BluepyBackend backend = BluepyBackend except ImportError: diff --git a/homeassistant/components/sensor/mitemp_bt.py b/homeassistant/components/sensor/mitemp_bt.py index 2ae5c29b043..15e225fd2c0 100644 --- a/homeassistant/components/sensor/mitemp_bt.py +++ b/homeassistant/components/sensor/mitemp_bt.py @@ -60,7 +60,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the MiTempBt sensor.""" from mitemp_bt import mitemp_bt_poller try: - import bluepy.btle # noqa: F401 pylint: disable=unused-variable + import bluepy.btle # noqa: F401 pylint: disable=unused-import from btlewrap import BluepyBackend backend = BluepyBackend except ImportError: diff --git a/homeassistant/components/sensor/mvglive.py b/homeassistant/components/sensor/mvglive.py index 8634e4f4570..5ec66aafe2f 100644 --- a/homeassistant/components/sensor/mvglive.py +++ b/homeassistant/components/sensor/mvglive.py @@ -147,10 +147,10 @@ class MVGLiveData: self._products = products self._timeoffset = timeoffset self._number = number - self._include_ubahn = True if 'U-Bahn' in self._products else False - self._include_tram = True if 'Tram' in self._products else False - self._include_bus = True if 'Bus' in self._products else False - self._include_sbahn = True if 'S-Bahn' in self._products else False + self._include_ubahn = 'U-Bahn' in self._products + self._include_tram = 'Tram' in self._products + self._include_bus = 'Bus' in self._products + self._include_sbahn = 'S-Bahn' in self._products self.mvg = MVGLive.MVGLive() self.departures = [] diff --git a/homeassistant/components/sensor/statistics.py b/homeassistant/components/sensor/statistics.py index e011121f4a2..01c783dc1db 100644 --- a/homeassistant/components/sensor/statistics.py +++ b/homeassistant/components/sensor/statistics.py @@ -80,8 +80,7 @@ class StatisticsSensor(Entity): precision): """Initialize the Statistics sensor.""" self._entity_id = entity_id - self.is_binary = True if self._entity_id.split('.')[0] == \ - 'binary_sensor' else False + self.is_binary = self._entity_id.split('.')[0] == 'binary_sensor' if not self.is_binary: self._name = '{} {}'.format(name, ATTR_MEAN) else: diff --git a/homeassistant/components/spaceapi.py b/homeassistant/components/spaceapi.py index eaf1508071a..fa2e5e8e1ea 100644 --- a/homeassistant/components/spaceapi.py +++ b/homeassistant/components/spaceapi.py @@ -129,7 +129,7 @@ class APISpaceApiView(HomeAssistantView): if space_state is not None: state = { - ATTR_OPEN: False if space_state.state == 'off' else True, + ATTR_OPEN: space_state.state != 'off', ATTR_LASTCHANGE: dt_util.as_timestamp(space_state.last_updated), } diff --git a/homeassistant/components/switch/raspihats.py b/homeassistant/components/switch/raspihats.py index c697d7042a6..b422efea2ff 100644 --- a/homeassistant/components/switch/raspihats.py +++ b/homeassistant/components/switch/raspihats.py @@ -123,7 +123,7 @@ class I2CHatSwitch(ToggleEntity): def turn_on(self, **kwargs): """Turn the device on.""" try: - state = True if self._invert_logic is False else False + state = self._invert_logic is False self.I2C_HATS_MANAGER.write_dq(self._address, self._channel, state) self.schedule_update_ha_state() except I2CHatsException as ex: @@ -132,7 +132,7 @@ class I2CHatSwitch(ToggleEntity): def turn_off(self, **kwargs): """Turn the device off.""" try: - state = False if self._invert_logic is False else True + state = self._invert_logic is not False self.I2C_HATS_MANAGER.write_dq(self._address, self._channel, state) self.schedule_update_ha_state() except I2CHatsException as ex: diff --git a/homeassistant/config.py b/homeassistant/config.py index 4fc77bd81cd..10d3ce21a00 100644 --- a/homeassistant/config.py +++ b/homeassistant/config.py @@ -437,9 +437,10 @@ def _format_config_error(ex: vol.Invalid, domain: str, config: Dict) -> str: """ message = "Invalid config for [{}]: ".format(domain) if 'extra keys not allowed' in ex.error_message: - message += '[{}] is an invalid option for [{}]. Check: {}->{}.'\ - .format(ex.path[-1], domain, domain, - '->'.join(str(m) for m in ex.path)) + message += '[{option}] is an invalid option for [{domain}]. ' \ + 'Check: {domain}->{path}.'.format( + option=ex.path[-1], domain=domain, + path='->'.join(str(m) for m in ex.path)) else: message += '{}.'.format(humanize_error(config, ex)) diff --git a/homeassistant/helpers/intent.py b/homeassistant/helpers/intent.py index d942aabccce..f4d57ce86cd 100644 --- a/homeassistant/helpers/intent.py +++ b/homeassistant/helpers/intent.py @@ -68,8 +68,7 @@ async def async_handle(hass: HomeAssistantType, platform: str, intent_type, err) raise InvalidSlotInfo( 'Received invalid slot info for {}'.format(intent_type)) from err - # https://github.com/PyCQA/pylint/issues/2284 - except IntentHandleError: # pylint: disable=try-except-raise + except IntentHandleError: raise except Exception as err: raise IntentUnexpectedError( diff --git a/requirements_test.txt b/requirements_test.txt index 8d761c1e614..d9c52bbd053 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -8,7 +8,7 @@ flake8==3.6.0 mock-open==1.3.1 mypy==0.641 pydocstyle==2.1.1 -pylint==2.1.1 +pylint==2.2.2 pytest-aiohttp==0.3.0 pytest-cov==2.6.0 pytest-sugar==0.9.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index c63fb9a1200..5105350739a 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -9,7 +9,7 @@ flake8==3.6.0 mock-open==1.3.1 mypy==0.641 pydocstyle==2.1.1 -pylint==2.1.1 +pylint==2.2.2 pytest-aiohttp==0.3.0 pytest-cov==2.6.0 pytest-sugar==0.9.2 From c9316192691e096397964ac30b85b1919b9443f1 Mon Sep 17 00:00:00 2001 From: Sean Wilson Date: Thu, 6 Dec 2018 10:25:59 -0500 Subject: [PATCH 006/304] Add CM17A support (#19041) * Add CM17A support. * Update log entry --- homeassistant/components/light/x10.py | 26 +++++++++++++++++++------- 1 file changed, 19 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/light/x10.py b/homeassistant/components/light/x10.py index ef2211a4469..9618a13a1a9 100644 --- a/homeassistant/components/light/x10.py +++ b/homeassistant/components/light/x10.py @@ -41,24 +41,26 @@ def get_unit_status(code): def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the x10 Light platform.""" + is_cm11a = True try: x10_command('info') except CalledProcessError as err: - _LOGGER.error(err.output) - return False + _LOGGER.info("Assuming that the device is CM17A: %s", err.output) + is_cm11a = False - add_entities(X10Light(light) for light in config[CONF_DEVICES]) + add_entities(X10Light(light, is_cm11a) for light in config[CONF_DEVICES]) class X10Light(Light): """Representation of an X10 Light.""" - def __init__(self, light): + def __init__(self, light, is_cm11a): """Initialize an X10 Light.""" self._name = light['name'] self._id = light['id'] self._brightness = 0 self._state = False + self._is_cm11a = is_cm11a @property def name(self): @@ -82,15 +84,25 @@ class X10Light(Light): def turn_on(self, **kwargs): """Instruct the light to turn on.""" - x10_command('on ' + self._id) + if self._is_cm11a: + x10_command('on ' + self._id) + else: + x10_command('fon ' + self._id) self._brightness = kwargs.get(ATTR_BRIGHTNESS, 255) self._state = True def turn_off(self, **kwargs): """Instruct the light to turn off.""" - x10_command('off ' + self._id) + if self._is_cm11a: + x10_command('off ' + self._id) + else: + x10_command('foff ' + self._id) self._state = False def update(self): """Fetch update state.""" - self._state = bool(get_unit_status(self._id)) + if self._is_cm11a: + self._state = bool(get_unit_status(self._id)) + else: + # Not supported on CM17A + pass From dd92318762e685e016a7f27ba1f05dd79302df7e Mon Sep 17 00:00:00 2001 From: Mike Miller Date: Thu, 6 Dec 2018 18:05:15 +0200 Subject: [PATCH 007/304] Fix missing colorTemperatureInKelvin from Alexa responses (#19069) * Fix missing colorTemperatureInKelvin from Alexa responses * Update smart_home.py * Add test --- homeassistant/components/alexa/smart_home.py | 14 ++++++++++++++ tests/components/alexa/test_smart_home.py | 19 +++++++++++++++++++ 2 files changed, 33 insertions(+) diff --git a/homeassistant/components/alexa/smart_home.py b/homeassistant/components/alexa/smart_home.py index 2a61533a2b9..f06b853087f 100644 --- a/homeassistant/components/alexa/smart_home.py +++ b/homeassistant/components/alexa/smart_home.py @@ -504,6 +504,20 @@ class _AlexaColorTemperatureController(_AlexaInterface): def name(self): return 'Alexa.ColorTemperatureController' + def properties_supported(self): + return [{'name': 'colorTemperatureInKelvin'}] + + def properties_retrievable(self): + return True + + def get_property(self, name): + if name != 'colorTemperatureInKelvin': + raise _UnsupportedProperty(name) + if 'color_temp' in self.entity.attributes: + return color_util.color_temperature_mired_to_kelvin( + self.entity.attributes['color_temp']) + return 0 + class _AlexaPercentageController(_AlexaInterface): """Implements Alexa.PercentageController. diff --git a/tests/components/alexa/test_smart_home.py b/tests/components/alexa/test_smart_home.py index 3cfb8068177..ddf66d1c617 100644 --- a/tests/components/alexa/test_smart_home.py +++ b/tests/components/alexa/test_smart_home.py @@ -1354,6 +1354,25 @@ async def test_report_colored_light_state(hass): }) +async def test_report_colored_temp_light_state(hass): + """Test ColorTemperatureController reports color temp correctly.""" + hass.states.async_set( + 'light.test_on', 'on', {'friendly_name': "Test light On", + 'color_temp': 240, + 'supported_features': 2}) + hass.states.async_set( + 'light.test_off', 'off', {'friendly_name': "Test light Off", + 'supported_features': 2}) + + properties = await reported_properties(hass, 'light.test_on') + properties.assert_equal('Alexa.ColorTemperatureController', + 'colorTemperatureInKelvin', 4166) + + properties = await reported_properties(hass, 'light.test_off') + properties.assert_equal('Alexa.ColorTemperatureController', + 'colorTemperatureInKelvin', 0) + + async def reported_properties(hass, endpoint): """Use ReportState to get properties and return them. From 0a3af545fe4cefb6b5695e9109de8a345d3e680a Mon Sep 17 00:00:00 2001 From: Anders Melchiorsen Date: Fri, 7 Dec 2018 07:06:35 +0100 Subject: [PATCH 008/304] Upgrade aiolifx to 0.6.7 (#19077) --- homeassistant/components/lifx/__init__.py | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/lifx/__init__.py b/homeassistant/components/lifx/__init__.py index 52df3d47ca1..f2713197ed1 100644 --- a/homeassistant/components/lifx/__init__.py +++ b/homeassistant/components/lifx/__init__.py @@ -8,7 +8,7 @@ from homeassistant.components.light import DOMAIN as LIGHT_DOMAIN DOMAIN = 'lifx' -REQUIREMENTS = ['aiolifx==0.6.6'] +REQUIREMENTS = ['aiolifx==0.6.7'] CONF_SERVER = 'server' CONF_BROADCAST = 'broadcast' diff --git a/requirements_all.txt b/requirements_all.txt index c56bf8888e5..1d0d0fb918f 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -112,7 +112,7 @@ aiohue==1.5.0 aioimaplib==0.7.13 # homeassistant.components.lifx -aiolifx==0.6.6 +aiolifx==0.6.7 # homeassistant.components.light.lifx aiolifx_effects==0.2.1 From 455508deac02233a5de34f79f6369f4edbe36e4e Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Fri, 7 Dec 2018 07:09:05 +0100 Subject: [PATCH 009/304] Force refresh Lovelace (#19073) * Force refresh Lovelace * Check config on load * Update __init__.py * Update __init__.py --- homeassistant/components/lovelace/__init__.py | 44 +++++++++++-------- 1 file changed, 26 insertions(+), 18 deletions(-) diff --git a/homeassistant/components/lovelace/__init__.py b/homeassistant/components/lovelace/__init__.py index 0d9b6a6d9fe..f6a8a3fd688 100644 --- a/homeassistant/components/lovelace/__init__.py +++ b/homeassistant/components/lovelace/__init__.py @@ -48,6 +48,7 @@ WS_TYPE_DELETE_VIEW = 'lovelace/config/view/delete' SCHEMA_GET_LOVELACE_UI = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend({ vol.Required('type'): vol.Any(WS_TYPE_GET_LOVELACE_UI, OLD_WS_TYPE_GET_LOVELACE_UI), + vol.Optional('force', default=False): bool, }) SCHEMA_MIGRATE_CONFIG = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend({ @@ -144,12 +145,12 @@ class DuplicateIdError(HomeAssistantError): """Duplicate ID's.""" -def load_config(hass) -> JSON_TYPE: +def load_config(hass, force: bool) -> JSON_TYPE: """Load a YAML file.""" fname = hass.config.path(LOVELACE_CONFIG_FILE) # Check for a cached version of the config - if LOVELACE_DATA in hass.data: + if not force and LOVELACE_DATA in hass.data: config, last_update = hass.data[LOVELACE_DATA] modtime = os.path.getmtime(fname) if config and last_update > modtime: @@ -158,23 +159,29 @@ def load_config(hass) -> JSON_TYPE: config = yaml.load_yaml(fname, False) seen_card_ids = set() seen_view_ids = set() + if 'views' in config and not isinstance(config['views'], list): + raise HomeAssistantError("Views should be a list.") for view in config.get('views', []): - view_id = view.get('id') - if view_id: - view_id = str(view_id) - if view_id in seen_view_ids: - raise DuplicateIdError( - 'ID `{}` has multiple occurances in views'.format(view_id)) - seen_view_ids.add(view_id) + if 'id' in view and not isinstance(view['id'], (str, int)): + raise HomeAssistantError( + "Your config contains view(s) with invalid ID(s).") + view_id = str(view.get('id', '')) + if view_id in seen_view_ids: + raise DuplicateIdError( + 'ID `{}` has multiple occurances in views'.format(view_id)) + seen_view_ids.add(view_id) + if 'cards' in view and not isinstance(view['cards'], list): + raise HomeAssistantError("Cards should be a list.") for card in view.get('cards', []): - card_id = card.get('id') - if card_id: - card_id = str(card_id) - if card_id in seen_card_ids: - raise DuplicateIdError( - 'ID `{}` has multiple occurances in cards' - .format(card_id)) - seen_card_ids.add(card_id) + if 'id' in card and not isinstance(card['id'], (str, int)): + raise HomeAssistantError( + "Your config contains card(s) with invalid ID(s).") + card_id = str(card.get('id', '')) + if card_id in seen_card_ids: + raise DuplicateIdError( + 'ID `{}` has multiple occurances in cards' + .format(card_id)) + seen_card_ids.add(card_id) hass.data[LOVELACE_DATA] = (config, time.time()) return config @@ -539,7 +546,8 @@ def handle_yaml_errors(func): @handle_yaml_errors async def websocket_lovelace_config(hass, connection, msg): """Send Lovelace UI config over WebSocket configuration.""" - return await hass.async_add_executor_job(load_config, hass) + return await hass.async_add_executor_job(load_config, hass, + msg.get('force', False)) @websocket_api.async_response From ce736e7ba1e58cf0d3427c88c39e922376a05063 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 7 Dec 2018 07:12:59 +0100 Subject: [PATCH 010/304] Updated frontend to 20181207.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 77c98ab6aa2..36fbe14aefd 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==20181205.0'] +REQUIREMENTS = ['home-assistant-frontend==20181207.0'] DOMAIN = 'frontend' DEPENDENCIES = ['api', 'websocket_api', 'http', 'system_log', diff --git a/requirements_all.txt b/requirements_all.txt index 1d0d0fb918f..8bcbd449635 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -493,7 +493,7 @@ hole==0.3.0 holidays==0.9.8 # homeassistant.components.frontend -home-assistant-frontend==20181205.0 +home-assistant-frontend==20181207.0 # homeassistant.components.zwave homeassistant-pyozw==0.1.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 5105350739a..2512d74e044 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -101,7 +101,7 @@ hdate==0.7.5 holidays==0.9.8 # homeassistant.components.frontend -home-assistant-frontend==20181205.0 +home-assistant-frontend==20181207.0 # homeassistant.components.homematicip_cloud homematicip==0.9.8 From 471d94b6cdda1d20e1ef284815d4d05663c4eed9 Mon Sep 17 00:00:00 2001 From: phnx Date: Fri, 7 Dec 2018 01:15:04 -0500 Subject: [PATCH 011/304] home-assistant/home-assistant#18645: Fix climate mode mapping. --- homeassistant/components/google_assistant/trait.py | 5 ++++- tests/components/google_assistant/__init__.py | 2 +- tests/components/google_assistant/test_google_assistant.py | 4 ++-- tests/components/google_assistant/test_trait.py | 6 +++--- 4 files changed, 10 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/google_assistant/trait.py b/homeassistant/components/google_assistant/trait.py index f2cb819fcc9..05c5e5dcde9 100644 --- a/homeassistant/components/google_assistant/trait.py +++ b/homeassistant/components/google_assistant/trait.py @@ -517,7 +517,10 @@ class TemperatureSettingTrait(_Trait): climate.STATE_HEAT: 'heat', climate.STATE_COOL: 'cool', climate.STATE_OFF: 'off', - climate.STATE_AUTO: 'heatcool', + climate.STATE_AUTO: 'auto', + climate.STATE_FAN_ONLY: 'fan-only', + climate.STATE_DRY: 'dry', + climate.STATE_ECO: 'eco' } google_to_hass = {value: key for key, value in hass_to_google.items()} diff --git a/tests/components/google_assistant/__init__.py b/tests/components/google_assistant/__init__.py index 03cc327a5c5..949960598d6 100644 --- a/tests/components/google_assistant/__init__.py +++ b/tests/components/google_assistant/__init__.py @@ -223,7 +223,7 @@ DEMO_DEVICES = [{ 'type': 'action.devices.types.THERMOSTAT', 'willReportState': False, 'attributes': { - 'availableThermostatModes': 'heat,cool,heatcool,off', + 'availableThermostatModes': 'heat,cool,auto,off', 'thermostatTemperatureUnit': 'C', }, }, { diff --git a/tests/components/google_assistant/test_google_assistant.py b/tests/components/google_assistant/test_google_assistant.py index 89e9090da98..0da2781a01f 100644 --- a/tests/components/google_assistant/test_google_assistant.py +++ b/tests/components/google_assistant/test_google_assistant.py @@ -214,7 +214,7 @@ def test_query_climate_request(hass_fixture, assistant_client, auth_header): 'online': True, 'thermostatTemperatureSetpointHigh': 24, 'thermostatTemperatureAmbient': 23, - 'thermostatMode': 'heatcool', + 'thermostatMode': 'auto', 'thermostatTemperatureSetpointLow': 21 } assert devices['climate.hvac'] == { @@ -271,7 +271,7 @@ def test_query_climate_request_f(hass_fixture, assistant_client, auth_header): 'online': True, 'thermostatTemperatureSetpointHigh': -4.4, 'thermostatTemperatureAmbient': -5, - 'thermostatMode': 'heatcool', + 'thermostatMode': 'auto', 'thermostatTemperatureSetpointLow': -6.1, } assert devices['climate.hvac'] == { diff --git a/tests/components/google_assistant/test_trait.py b/tests/components/google_assistant/test_trait.py index e9169c9bbbe..cb709ed084c 100644 --- a/tests/components/google_assistant/test_trait.py +++ b/tests/components/google_assistant/test_trait.py @@ -679,11 +679,11 @@ async def test_temperature_setting_climate_range(hass): climate.ATTR_MAX_TEMP: 80 }), BASIC_CONFIG) assert trt.sync_attributes() == { - 'availableThermostatModes': 'off,cool,heat,heatcool', + 'availableThermostatModes': 'off,cool,heat,auto', 'thermostatTemperatureUnit': 'F', } assert trt.query_attributes() == { - 'thermostatMode': 'heatcool', + 'thermostatMode': 'auto', 'thermostatTemperatureAmbient': 21.1, 'thermostatHumidityAmbient': 25, 'thermostatTemperatureSetpointLow': 18.3, @@ -709,7 +709,7 @@ async def test_temperature_setting_climate_range(hass): calls = async_mock_service( hass, climate.DOMAIN, climate.SERVICE_SET_OPERATION_MODE) await trt.execute(trait.COMMAND_THERMOSTAT_SET_MODE, { - 'thermostatMode': 'heatcool', + 'thermostatMode': 'auto', }) assert len(calls) == 1 assert calls[0].data == { From 8a62bc92376996d6e9a1c4d66d2e4448c8245b01 Mon Sep 17 00:00:00 2001 From: ehendrix23 Date: Thu, 6 Dec 2018 23:26:49 -0700 Subject: [PATCH 012/304] Set directv unavailable state when errors returned for longer then a minute (#19014) * Fix unavailable Change setting to unavailable when getting request exceptions only after 1 minute has past since 1st occurrence. * Put common code in _check_state_available Put common code to determine if available should be set to False in method _check_state_available --- .../components/media_player/directv.py | 40 ++++++++++++++++--- tests/components/media_player/test_directv.py | 24 ++++++++++- 2 files changed, 57 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/media_player/directv.py b/homeassistant/components/media_player/directv.py index d8c67e372b2..707014328c6 100644 --- a/homeassistant/components/media_player/directv.py +++ b/homeassistant/components/media_player/directv.py @@ -133,6 +133,7 @@ class DirecTvDevice(MediaPlayerDevice): self._is_client = device != '0' self._assumed_state = None self._available = False + self._first_error_timestamp = None if self._is_client: _LOGGER.debug("Created DirecTV client %s for device %s", @@ -142,7 +143,7 @@ class DirecTvDevice(MediaPlayerDevice): def update(self): """Retrieve latest state.""" - _LOGGER.debug("Updating status for %s", self._name) + _LOGGER.debug("%s: Updating status", self.entity_id) try: self._available = True self._is_standby = self.dtv.get_standby() @@ -156,6 +157,7 @@ class DirecTvDevice(MediaPlayerDevice): else: self._current = self.dtv.get_tuned() if self._current['status']['code'] == 200: + self._first_error_timestamp = None self._is_recorded = self._current.get('uniqueId')\ is not None self._paused = self._last_position == \ @@ -165,15 +167,41 @@ class DirecTvDevice(MediaPlayerDevice): self._last_update = dt_util.utcnow() if not self._paused \ or self._last_update is None else self._last_update else: - self._available = False + # If an error is received then only set to unavailable if + # this started at least 1 minute ago. + log_message = "{}: Invalid status {} received".format( + self.entity_id, + self._current['status']['code'] + ) + if self._check_state_available(): + _LOGGER.debug(log_message) + else: + _LOGGER.error(log_message) + except requests.RequestException as ex: - _LOGGER.error("Request error trying to update current status for" - " %s. %s", self._name, ex) - self._available = False - except Exception: + _LOGGER.error("%s: Request error trying to update current status: " + "%s", self.entity_id, ex) + self._check_state_available() + + except Exception as ex: + _LOGGER.error("%s: Exception trying to update current status: %s", + self.entity_id, ex) self._available = False + if not self._first_error_timestamp: + self._first_error_timestamp = dt_util.utcnow() raise + def _check_state_available(self): + """Set to unavailable if issue been occurring over 1 minute.""" + if not self._first_error_timestamp: + self._first_error_timestamp = dt_util.utcnow() + else: + tdelta = dt_util.utcnow() - self._first_error_timestamp + if tdelta.total_seconds() >= 60: + self._available = False + + return self._available + @property def device_state_attributes(self): """Return device specific state attributes.""" diff --git a/tests/components/media_player/test_directv.py b/tests/components/media_player/test_directv.py index 951f1319cc0..d8e561d8d2a 100644 --- a/tests/components/media_player/test_directv.py +++ b/tests/components/media_player/test_directv.py @@ -515,7 +515,7 @@ async def test_available(hass, platforms, main_dtv, mock_now): state = hass.states.get(MAIN_ENTITY_ID) assert state.state != STATE_UNAVAILABLE - # Make update fail (i.e. DVR offline) + # Make update fail 1st time next_update = next_update + timedelta(minutes=5) with patch.object( main_dtv, 'get_standby', side_effect=requests.RequestException), \ @@ -523,6 +523,28 @@ async def test_available(hass, platforms, main_dtv, mock_now): async_fire_time_changed(hass, next_update) await hass.async_block_till_done() + state = hass.states.get(MAIN_ENTITY_ID) + assert state.state != STATE_UNAVAILABLE + + # Make update fail 2nd time within 1 minute + next_update = next_update + timedelta(seconds=30) + with patch.object( + main_dtv, 'get_standby', side_effect=requests.RequestException), \ + patch('homeassistant.util.dt.utcnow', return_value=next_update): + async_fire_time_changed(hass, next_update) + await hass.async_block_till_done() + + state = hass.states.get(MAIN_ENTITY_ID) + assert state.state != STATE_UNAVAILABLE + + # Make update fail 3rd time more then a minute after 1st failure + next_update = next_update + timedelta(minutes=1) + with patch.object( + main_dtv, 'get_standby', side_effect=requests.RequestException), \ + patch('homeassistant.util.dt.utcnow', return_value=next_update): + async_fire_time_changed(hass, next_update) + await hass.async_block_till_done() + state = hass.states.get(MAIN_ENTITY_ID) assert state.state == STATE_UNAVAILABLE From def4e89372de5232b571b5137a555acc69c7400e Mon Sep 17 00:00:00 2001 From: Matthew Garrett Date: Thu, 6 Dec 2018 22:32:21 -0800 Subject: [PATCH 013/304] Bump lakeside requirement to support more Eufy devices (#19080) The T1203 works fine with the existing protocol. --- homeassistant/components/eufy.py | 3 ++- requirements_all.txt | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/eufy.py b/homeassistant/components/eufy.py index 31a4dddd424..c1166f8cf7b 100644 --- a/homeassistant/components/eufy.py +++ b/homeassistant/components/eufy.py @@ -15,7 +15,7 @@ from homeassistant.helpers import discovery import homeassistant.helpers.config_validation as cv -REQUIREMENTS = ['lakeside==0.10'] +REQUIREMENTS = ['lakeside==0.11'] _LOGGER = logging.getLogger(__name__) @@ -43,6 +43,7 @@ EUFY_DISPATCH = { 'T1013': 'light', 'T1201': 'switch', 'T1202': 'switch', + 'T1203': 'switch', 'T1211': 'switch' } diff --git a/requirements_all.txt b/requirements_all.txt index 8bcbd449635..fd45ca3da40 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -564,7 +564,7 @@ kiwiki-client==0.1.1 konnected==0.1.4 # homeassistant.components.eufy -lakeside==0.10 +lakeside==0.11 # homeassistant.components.owntracks libnacl==1.6.1 From 24d0aa3f55009ac8bcd81e7c1d5f95d36ad09d15 Mon Sep 17 00:00:00 2001 From: phnx Date: Fri, 7 Dec 2018 01:50:48 -0500 Subject: [PATCH 014/304] home-assistant/home-assistant#18645: Remove un-used constants. --- homeassistant/components/google_assistant/const.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/homeassistant/components/google_assistant/const.py b/homeassistant/components/google_assistant/const.py index aca960f9c0a..bfeb0fcadf5 100644 --- a/homeassistant/components/google_assistant/const.py +++ b/homeassistant/components/google_assistant/const.py @@ -19,8 +19,6 @@ DEFAULT_EXPOSED_DOMAINS = [ 'media_player', 'scene', 'script', 'switch', 'vacuum', 'lock', ] DEFAULT_ALLOW_UNLOCK = False -CLIMATE_MODE_HEATCOOL = 'heatcool' -CLIMATE_SUPPORTED_MODES = {'heat', 'cool', 'off', 'on', CLIMATE_MODE_HEATCOOL} PREFIX_TYPES = 'action.devices.types.' TYPE_LIGHT = PREFIX_TYPES + 'LIGHT' From 5bf6951311029f99d770aff6479f7284ad5980db Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pierre=20St=C3=A5hl?= Date: Fri, 7 Dec 2018 11:06:38 +0100 Subject: [PATCH 015/304] Upgrade pyatv to 0.3.12 (#19085) --- homeassistant/components/apple_tv.py | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/apple_tv.py b/homeassistant/components/apple_tv.py index ff17b6d5e39..73cabdfbae6 100644 --- a/homeassistant/components/apple_tv.py +++ b/homeassistant/components/apple_tv.py @@ -16,7 +16,7 @@ from homeassistant.helpers import discovery from homeassistant.helpers.aiohttp_client import async_get_clientsession import homeassistant.helpers.config_validation as cv -REQUIREMENTS = ['pyatv==0.3.11'] +REQUIREMENTS = ['pyatv==0.3.12'] _LOGGER = logging.getLogger(__name__) diff --git a/requirements_all.txt b/requirements_all.txt index fd45ca3da40..08355236f11 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -861,7 +861,7 @@ pyarlo==0.2.2 pyatmo==1.4 # homeassistant.components.apple_tv -pyatv==0.3.11 +pyatv==0.3.12 # homeassistant.components.device_tracker.bbox # homeassistant.components.sensor.bbox From 7edd241059c3047082cc635a23b073db0bee32f8 Mon Sep 17 00:00:00 2001 From: speedmann Date: Fri, 7 Dec 2018 11:08:41 +0100 Subject: [PATCH 016/304] Automatically detect if ipv4/ipv6 is used for cert_expiry (#18916) * Automatically detect if ipv4/ipv6 is used for cert_expiry Fixes #18818 Python sockets use ipv4 per default. If the domain which should be checked only has an ipv6 record, socket creation errors out with `[Errno -2] Name or service not known` This fix tries to guess the protocol family and creates the socket with the correct family type * Fix line length violation --- homeassistant/components/sensor/cert_expiry.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/sensor/cert_expiry.py b/homeassistant/components/sensor/cert_expiry.py index df48ebbf41c..a04a631f2e9 100644 --- a/homeassistant/components/sensor/cert_expiry.py +++ b/homeassistant/components/sensor/cert_expiry.py @@ -85,8 +85,10 @@ class SSLCertificate(Entity): """Fetch the certificate information.""" try: ctx = ssl.create_default_context() + host_info = socket.getaddrinfo(self.server_name, self.server_port) + family = host_info[0][0] sock = ctx.wrap_socket( - socket.socket(), server_hostname=self.server_name) + socket.socket(family=family), server_hostname=self.server_name) sock.settimeout(TIMEOUT) sock.connect((self.server_name, self.server_port)) except socket.gaierror: From 385f0298bd483f2939f1811f81c227f277a207dc Mon Sep 17 00:00:00 2001 From: phnx Date: Fri, 7 Dec 2018 10:00:56 -0500 Subject: [PATCH 017/304] home-assistant/home-assistant#18645: revert heat-cool -> auto change --- homeassistant/components/google_assistant/trait.py | 2 +- tests/components/google_assistant/__init__.py | 2 +- tests/components/google_assistant/test_google_assistant.py | 4 ++-- tests/components/google_assistant/test_trait.py | 6 +++--- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/google_assistant/trait.py b/homeassistant/components/google_assistant/trait.py index 05c5e5dcde9..99c0f222933 100644 --- a/homeassistant/components/google_assistant/trait.py +++ b/homeassistant/components/google_assistant/trait.py @@ -517,7 +517,7 @@ class TemperatureSettingTrait(_Trait): climate.STATE_HEAT: 'heat', climate.STATE_COOL: 'cool', climate.STATE_OFF: 'off', - climate.STATE_AUTO: 'auto', + climate.STATE_AUTO: 'heatcool', climate.STATE_FAN_ONLY: 'fan-only', climate.STATE_DRY: 'dry', climate.STATE_ECO: 'eco' diff --git a/tests/components/google_assistant/__init__.py b/tests/components/google_assistant/__init__.py index 949960598d6..03cc327a5c5 100644 --- a/tests/components/google_assistant/__init__.py +++ b/tests/components/google_assistant/__init__.py @@ -223,7 +223,7 @@ DEMO_DEVICES = [{ 'type': 'action.devices.types.THERMOSTAT', 'willReportState': False, 'attributes': { - 'availableThermostatModes': 'heat,cool,auto,off', + 'availableThermostatModes': 'heat,cool,heatcool,off', 'thermostatTemperatureUnit': 'C', }, }, { diff --git a/tests/components/google_assistant/test_google_assistant.py b/tests/components/google_assistant/test_google_assistant.py index 0da2781a01f..89e9090da98 100644 --- a/tests/components/google_assistant/test_google_assistant.py +++ b/tests/components/google_assistant/test_google_assistant.py @@ -214,7 +214,7 @@ def test_query_climate_request(hass_fixture, assistant_client, auth_header): 'online': True, 'thermostatTemperatureSetpointHigh': 24, 'thermostatTemperatureAmbient': 23, - 'thermostatMode': 'auto', + 'thermostatMode': 'heatcool', 'thermostatTemperatureSetpointLow': 21 } assert devices['climate.hvac'] == { @@ -271,7 +271,7 @@ def test_query_climate_request_f(hass_fixture, assistant_client, auth_header): 'online': True, 'thermostatTemperatureSetpointHigh': -4.4, 'thermostatTemperatureAmbient': -5, - 'thermostatMode': 'auto', + 'thermostatMode': 'heatcool', 'thermostatTemperatureSetpointLow': -6.1, } assert devices['climate.hvac'] == { diff --git a/tests/components/google_assistant/test_trait.py b/tests/components/google_assistant/test_trait.py index cb709ed084c..e9169c9bbbe 100644 --- a/tests/components/google_assistant/test_trait.py +++ b/tests/components/google_assistant/test_trait.py @@ -679,11 +679,11 @@ async def test_temperature_setting_climate_range(hass): climate.ATTR_MAX_TEMP: 80 }), BASIC_CONFIG) assert trt.sync_attributes() == { - 'availableThermostatModes': 'off,cool,heat,auto', + 'availableThermostatModes': 'off,cool,heat,heatcool', 'thermostatTemperatureUnit': 'F', } assert trt.query_attributes() == { - 'thermostatMode': 'auto', + 'thermostatMode': 'heatcool', 'thermostatTemperatureAmbient': 21.1, 'thermostatHumidityAmbient': 25, 'thermostatTemperatureSetpointLow': 18.3, @@ -709,7 +709,7 @@ async def test_temperature_setting_climate_range(hass): calls = async_mock_service( hass, climate.DOMAIN, climate.SERVICE_SET_OPERATION_MODE) await trt.execute(trait.COMMAND_THERMOSTAT_SET_MODE, { - 'thermostatMode': 'auto', + 'thermostatMode': 'heatcool', }) assert len(calls) == 1 assert calls[0].data == { From e567e3d4e71e732f19d4d83eb7eeebb9a7c0ffa2 Mon Sep 17 00:00:00 2001 From: Nick Horvath Date: Fri, 7 Dec 2018 13:20:05 -0500 Subject: [PATCH 018/304] Bump skybellpy version to fix api issue (#19100) --- homeassistant/components/skybell.py | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/skybell.py b/homeassistant/components/skybell.py index 3f27c91e7c5..b3c3b63bd84 100644 --- a/homeassistant/components/skybell.py +++ b/homeassistant/components/skybell.py @@ -14,7 +14,7 @@ from homeassistant.const import ( import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity -REQUIREMENTS = ['skybellpy==0.1.2'] +REQUIREMENTS = ['skybellpy==0.2.0'] _LOGGER = logging.getLogger(__name__) diff --git a/requirements_all.txt b/requirements_all.txt index 08355236f11..332623a0179 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1431,7 +1431,7 @@ simplisafe-python==3.1.14 sisyphus-control==2.1 # homeassistant.components.skybell -skybellpy==0.1.2 +skybellpy==0.2.0 # homeassistant.components.notify.slack slacker==0.11.0 From a58b3aad59e4aa00e05f8e166f8118b2271d6b6b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20H=C3=B8yer=20Iversen?= Date: Fri, 7 Dec 2018 19:33:06 +0100 Subject: [PATCH 019/304] Upgrade Tibber lib (#19098) --- 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 fce4312ad68..8462b646a22 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.8.5'] +REQUIREMENTS = ['pyTibber==0.8.6'] DOMAIN = 'tibber' diff --git a/requirements_all.txt b/requirements_all.txt index 332623a0179..df6ba3f8133 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -834,7 +834,7 @@ pyRFXtrx==0.23 pySwitchmate==0.4.4 # homeassistant.components.tibber -pyTibber==0.8.5 +pyTibber==0.8.6 # homeassistant.components.switch.dlink pyW215==0.6.0 From 05586de51f0148216bcf2f4e91ebaadd1bef53d9 Mon Sep 17 00:00:00 2001 From: Andrew Hayworth Date: Fri, 7 Dec 2018 14:17:34 -0600 Subject: [PATCH 020/304] Set lock status correctly for Schlage BE469 Z-Wave locks (#18737) * Set lock status correctly for Schlage BE469 Z-Wave locks PR #17386 attempted to improve the state of z-wave lock tracking for some problematic models. However, it operated under a flawed assumptions. Namely, that we can always trust `self.values` to have fresh data, and that the Schlage BE469 sends alarm reports after every lock event. We can't trust `self.values`, and the Schlage is very broken. :) When we receive a notification from the driver about a state change, we call `update_properties` - but we can (and do!) have _stale_ properties left over from previous updates. #17386 really works best if you start from a clean slate each time. However, `update_properties` is called on every value update, and we don't get a reason why. Moreover, values that weren't just refreshed are not removed. So blindly looking at something like `self.values.access_control` when deciding to apply a workaround is not going to always be correct - it may or may not be, depending on what happened in the past. For the sad case of the BE469, here are the Z-Wave events that happen under various circumstances: RF Lock / Unlock: - Send: door lock command set - Receive: door lock report - Send: door lock command get - Receive: door lock report Manual lock / Unlock: - Receive: alarm - Send: door lock command get - Receive: door lock report Keypad lock / Unlock: - Receive: alarm - Send: door lock command get - Receive: door lock report Thus, this PR introduces yet another work around - we track the current and last z-wave command that the driver saw, and make assumptions based on the sequence of events. This seems to be the most reliable way to go - simply asking the driver to refresh various states doesn't clear out alarms the way you would expect; this model doesn't support the access control logging commands; and trying to manually clear out alarm state when calling RF lock/unlock was tricky. The lock state, when the z-wave network restarts, may look out of sync for a few minutes. However, after the full network restart is complete, everything looks good in my testing. * Fix linter --- homeassistant/components/lock/zwave.py | 58 ++++++++++++++++++++------ tests/components/lock/test_zwave.py | 46 +++++++++++++++++++- 2 files changed, 90 insertions(+), 14 deletions(-) diff --git a/homeassistant/components/lock/zwave.py b/homeassistant/components/lock/zwave.py index 796c62377f1..b4bb233c9cc 100644 --- a/homeassistant/components/lock/zwave.py +++ b/homeassistant/components/lock/zwave.py @@ -29,8 +29,9 @@ SERVICE_CLEAR_USERCODE = 'clear_usercode' POLYCONTROL = 0x10E DANALOCK_V2_BTZE = 0x2 POLYCONTROL_DANALOCK_V2_BTZE_LOCK = (POLYCONTROL, DANALOCK_V2_BTZE) -WORKAROUND_V2BTZE = 'v2btze' -WORKAROUND_DEVICE_STATE = 'state' +WORKAROUND_V2BTZE = 1 +WORKAROUND_DEVICE_STATE = 2 +WORKAROUND_TRACK_MESSAGE = 4 DEVICE_MAPPINGS = { POLYCONTROL_DANALOCK_V2_BTZE_LOCK: WORKAROUND_V2BTZE, @@ -43,7 +44,7 @@ DEVICE_MAPPINGS = { # Yale YRD220 (as reported by adrum in PR #17386) (0x0109, 0x0000): WORKAROUND_DEVICE_STATE, # Schlage BE469 - (0x003B, 0x5044): WORKAROUND_DEVICE_STATE, + (0x003B, 0x5044): WORKAROUND_DEVICE_STATE | WORKAROUND_TRACK_MESSAGE, # Schlage FE599NX (0x003B, 0x504C): WORKAROUND_DEVICE_STATE, } @@ -51,13 +52,15 @@ DEVICE_MAPPINGS = { LOCK_NOTIFICATION = { '1': 'Manual Lock', '2': 'Manual Unlock', - '3': 'RF Lock', - '4': 'RF Unlock', '5': 'Keypad Lock', '6': 'Keypad Unlock', '11': 'Lock Jammed', '254': 'Unknown Event' } +NOTIFICATION_RF_LOCK = '3' +NOTIFICATION_RF_UNLOCK = '4' +LOCK_NOTIFICATION[NOTIFICATION_RF_LOCK] = 'RF Lock' +LOCK_NOTIFICATION[NOTIFICATION_RF_UNLOCK] = 'RF Unlock' LOCK_ALARM_TYPE = { '9': 'Deadbolt Jammed', @@ -66,8 +69,6 @@ LOCK_ALARM_TYPE = { '19': 'Unlocked with Keypad by user ', '21': 'Manually Locked ', '22': 'Manually Unlocked ', - '24': 'Locked by RF', - '25': 'Unlocked by RF', '27': 'Auto re-lock', '33': 'User deleted: ', '112': 'Master code changed or User added: ', @@ -79,6 +80,10 @@ LOCK_ALARM_TYPE = { '168': 'Critical Battery Level', '169': 'Battery too low to operate' } +ALARM_RF_LOCK = '24' +ALARM_RF_UNLOCK = '25' +LOCK_ALARM_TYPE[ALARM_RF_LOCK] = 'Locked by RF' +LOCK_ALARM_TYPE[ALARM_RF_UNLOCK] = 'Unlocked by RF' MANUAL_LOCK_ALARM_LEVEL = { '1': 'by Key Cylinder or Inside thumb turn', @@ -229,6 +234,8 @@ class ZwaveLock(zwave.ZWaveDeviceEntity, LockDevice): self._lock_status = None self._v2btze = None self._state_workaround = False + self._track_message_workaround = False + self._previous_message = None # Enable appropriate workaround flags for our device # Make sure that we have values for the key before converting to int @@ -237,26 +244,30 @@ class ZwaveLock(zwave.ZWaveDeviceEntity, LockDevice): specific_sensor_key = (int(self.node.manufacturer_id, 16), int(self.node.product_id, 16)) if specific_sensor_key in DEVICE_MAPPINGS: - if DEVICE_MAPPINGS[specific_sensor_key] == WORKAROUND_V2BTZE: + workaround = DEVICE_MAPPINGS[specific_sensor_key] + if workaround & WORKAROUND_V2BTZE: self._v2btze = 1 _LOGGER.debug("Polycontrol Danalock v2 BTZE " "workaround enabled") - if DEVICE_MAPPINGS[specific_sensor_key] == \ - WORKAROUND_DEVICE_STATE: + if workaround & WORKAROUND_DEVICE_STATE: self._state_workaround = True _LOGGER.debug( "Notification device state workaround enabled") + if workaround & WORKAROUND_TRACK_MESSAGE: + self._track_message_workaround = True + _LOGGER.debug("Message tracking workaround enabled") self.update_properties() def update_properties(self): """Handle data changes for node values.""" self._state = self.values.primary.data - _LOGGER.debug("Lock state set from Bool value and is %s", self._state) + _LOGGER.debug("lock state set to %s", self._state) if self.values.access_control: notification_data = self.values.access_control.data self._notification = LOCK_NOTIFICATION.get(str(notification_data)) if self._state_workaround: self._state = LOCK_STATUS.get(str(notification_data)) + _LOGGER.debug("workaround: lock state set to %s", self._state) if self._v2btze: if self.values.v2btze_advanced and \ self.values.v2btze_advanced.data == CONFIG_ADVANCED: @@ -265,16 +276,37 @@ class ZwaveLock(zwave.ZWaveDeviceEntity, LockDevice): "Lock state set from Access Control value and is %s, " "get=%s", str(notification_data), self.state) + if self._track_message_workaround: + this_message = self.node.stats['lastReceivedMessage'][5] + + if this_message == zwave.const.COMMAND_CLASS_DOOR_LOCK: + self._state = self.values.primary.data + _LOGGER.debug("set state to %s based on message tracking", + self._state) + if self._previous_message == \ + zwave.const.COMMAND_CLASS_DOOR_LOCK: + if self._state: + self._notification = \ + LOCK_NOTIFICATION[NOTIFICATION_RF_LOCK] + self._lock_status = \ + LOCK_ALARM_TYPE[ALARM_RF_LOCK] + else: + self._notification = \ + LOCK_NOTIFICATION[NOTIFICATION_RF_UNLOCK] + self._lock_status = \ + LOCK_ALARM_TYPE[ALARM_RF_UNLOCK] + return + + self._previous_message = this_message + if not self.values.alarm_type: return alarm_type = self.values.alarm_type.data - _LOGGER.debug("Lock alarm_type is %s", str(alarm_type)) if self.values.alarm_level: alarm_level = self.values.alarm_level.data else: alarm_level = None - _LOGGER.debug("Lock alarm_level is %s", str(alarm_level)) if not alarm_type: return diff --git a/tests/components/lock/test_zwave.py b/tests/components/lock/test_zwave.py index 3955538273b..484e4796759 100644 --- a/tests/components/lock/test_zwave.py +++ b/tests/components/lock/test_zwave.py @@ -62,7 +62,7 @@ def test_lock_value_changed(mock_openzwave): assert device.is_locked -def test_lock_value_changed_workaround(mock_openzwave): +def test_lock_state_workaround(mock_openzwave): """Test value changed for Z-Wave lock using notification state.""" node = MockNode(manufacturer_id='0090', product_id='0440') values = MockEntityValues( @@ -78,6 +78,50 @@ def test_lock_value_changed_workaround(mock_openzwave): assert not device.is_locked +def test_track_message_workaround(mock_openzwave): + """Test value changed for Z-Wave lock by alarm-clearing workaround.""" + node = MockNode(manufacturer_id='003B', product_id='5044', + stats={'lastReceivedMessage': [0] * 6}) + values = MockEntityValues( + primary=MockValue(data=True, node=node), + access_control=None, + alarm_type=None, + alarm_level=None, + ) + + # Here we simulate an RF lock. The first zwave.get_device will call + # update properties, simulating the first DoorLock report. We then trigger + # a change, simulating the openzwave automatic refreshing behavior (which + # is enabled for at least the lock that needs this workaround) + node.stats['lastReceivedMessage'][5] = const.COMMAND_CLASS_DOOR_LOCK + device = zwave.get_device(node=node, values=values) + value_changed(values.primary) + assert device.is_locked + assert device.device_state_attributes[zwave.ATTR_NOTIFICATION] == 'RF Lock' + + # Simulate a keypad unlock. We trigger a value_changed() which simulates + # the Alarm notification received from the lock. Then, we trigger + # value_changed() to simulate the automatic refreshing behavior. + values.access_control = MockValue(data=6, node=node) + values.alarm_type = MockValue(data=19, node=node) + values.alarm_level = MockValue(data=3, node=node) + node.stats['lastReceivedMessage'][5] = const.COMMAND_CLASS_ALARM + value_changed(values.access_control) + node.stats['lastReceivedMessage'][5] = const.COMMAND_CLASS_DOOR_LOCK + values.primary.data = False + value_changed(values.primary) + assert not device.is_locked + assert device.device_state_attributes[zwave.ATTR_LOCK_STATUS] == \ + 'Unlocked with Keypad by user 3' + + # Again, simulate an RF lock. + device.lock() + node.stats['lastReceivedMessage'][5] = const.COMMAND_CLASS_DOOR_LOCK + value_changed(values.primary) + assert device.is_locked + assert device.device_state_attributes[zwave.ATTR_NOTIFICATION] == 'RF Lock' + + def test_v2btze_value_changed(mock_openzwave): """Test value changed for v2btze Z-Wave lock.""" node = MockNode(manufacturer_id='010e', product_id='0002') From 65c2a257369572ef348879258aa2996de7e109df Mon Sep 17 00:00:00 2001 From: Sebastian Muszynski Date: Fri, 7 Dec 2018 23:40:48 +0100 Subject: [PATCH 021/304] Support next generation of the Xiaomi Mi Smart Plug (chuangmi.plug.hmi205) (#19071) * Add next generation of the Xiaomi Mi Smart Plug (chuangmi.plug.hmi205) * Fix linting --- homeassistant/components/switch/xiaomi_miio.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/switch/xiaomi_miio.py b/homeassistant/components/switch/xiaomi_miio.py index 125f89f5040..9db13446752 100644 --- a/homeassistant/components/switch/xiaomi_miio.py +++ b/homeassistant/components/switch/xiaomi_miio.py @@ -39,6 +39,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ 'chuangmi.plug.m1', 'chuangmi.plug.v2', 'chuangmi.plug.v3', + 'chuangmi.plug.hmi205', ]), }) @@ -146,7 +147,8 @@ async def async_setup_platform(hass, config, async_add_entities, device = XiaomiPowerStripSwitch(name, plug, model, unique_id) devices.append(device) hass.data[DATA_KEY][host] = device - elif model in ['chuangmi.plug.m1', 'chuangmi.plug.v2']: + elif model in ['chuangmi.plug.m1', 'chuangmi.plug.v2', + 'chuangmi.plug.hmi205']: from miio import ChuangmiPlug plug = ChuangmiPlug(host, token, model=model) device = XiaomiPlugGenericSwitch(name, plug, model, unique_id) From ece7b498ed22583cd298b84bf6331af6cda6aa18 Mon Sep 17 00:00:00 2001 From: Sebastian Muszynski Date: Sat, 8 Dec 2018 08:28:39 +0100 Subject: [PATCH 022/304] Fix the Xiaomi Aqara Cube rotate event of the LAN protocol 2.0 (Closes: #18199) (#19104) --- homeassistant/components/binary_sensor/xiaomi_aqara.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/homeassistant/components/binary_sensor/xiaomi_aqara.py b/homeassistant/components/binary_sensor/xiaomi_aqara.py index 550bdaac172..584d56c2e68 100644 --- a/homeassistant/components/binary_sensor/xiaomi_aqara.py +++ b/homeassistant/components/binary_sensor/xiaomi_aqara.py @@ -467,4 +467,12 @@ class XiaomiCube(XiaomiBinarySensor): }) self._last_action = 'rotate' + if 'rotate_degree' in data: + self._hass.bus.fire('xiaomi_aqara.cube_action', { + 'entity_id': self.entity_id, + 'action_type': 'rotate', + 'action_value': float(data['rotate_degree'].replace(",", ".")) + }) + self._last_action = 'rotate' + return True From f2f649680f02f9b2edee9a3c60a456d05255313f Mon Sep 17 00:00:00 2001 From: Sebastian Muszynski Date: Sat, 8 Dec 2018 08:45:03 +0100 Subject: [PATCH 023/304] Don't avoid async_schedule_update_ha_state by returning false (#19102) --- homeassistant/components/binary_sensor/xiaomi_aqara.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/homeassistant/components/binary_sensor/xiaomi_aqara.py b/homeassistant/components/binary_sensor/xiaomi_aqara.py index 584d56c2e68..614b7253f2e 100644 --- a/homeassistant/components/binary_sensor/xiaomi_aqara.py +++ b/homeassistant/components/binary_sensor/xiaomi_aqara.py @@ -423,9 +423,7 @@ class XiaomiButton(XiaomiBinarySensor): }) self._last_action = click_type - if value in ['long_click_press', 'long_click_release']: - return True - return False + return True class XiaomiCube(XiaomiBinarySensor): From ffe83d9ab1fc7058b7aefef9a5a281e7e12e1217 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20H=C3=B8yer=20Iversen?= Date: Sat, 8 Dec 2018 11:38:42 +0100 Subject: [PATCH 024/304] Upgrade Mill library (#19117) --- 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 5ea48614f6b..48b15400a43 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.2.9'] +REQUIREMENTS = ['millheater==0.3.0'] _LOGGER = logging.getLogger(__name__) diff --git a/requirements_all.txt b/requirements_all.txt index df6ba3f8133..aa2e52e13ba 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -644,7 +644,7 @@ mficlient==0.3.0 miflora==0.4.0 # homeassistant.components.climate.mill -millheater==0.2.9 +millheater==0.3.0 # homeassistant.components.sensor.mitemp_bt mitemp_bt==0.0.1 From 2134331e2ba597c5a0de098130105e0df82cf0f1 Mon Sep 17 00:00:00 2001 From: Sebastian Muszynski Date: Sat, 8 Dec 2018 14:49:14 +0100 Subject: [PATCH 025/304] Add Philips Moonlight Bedside Lamp support (#18496) * Add Philips Moonlight Bedside Lamp support * Update comment * Make hound happy * Wrap the call that could raise the exception only * Remote blank line * Use updated python-miio API --- homeassistant/components/light/xiaomi_miio.py | 272 ++++++++++++------ 1 file changed, 180 insertions(+), 92 deletions(-) diff --git a/homeassistant/components/light/xiaomi_miio.py b/homeassistant/components/light/xiaomi_miio.py index 9e650562fe8..62433ca9f97 100644 --- a/homeassistant/components/light/xiaomi_miio.py +++ b/homeassistant/components/light/xiaomi_miio.py @@ -1,5 +1,7 @@ """ -Support for Xiaomi Philips Lights (LED Ball & Ceiling Lamp, Eyecare Lamp 2). +Support for Xiaomi Philips Lights. + +LED Ball, Candle, Downlight, Ceiling, Eyecare 2, Bedside & Desklamp Lamp. For more details about this platform, please refer to the documentation https://home-assistant.io/components/light.xiaomi_miio/ @@ -19,7 +21,7 @@ from homeassistant.components.light import ( from homeassistant.const import CONF_HOST, CONF_NAME, CONF_TOKEN from homeassistant.exceptions import PlatformNotReady import homeassistant.helpers.config_validation as cv -from homeassistant.util import dt +from homeassistant.util import color, dt REQUIREMENTS = ['python-miio==0.4.4', 'construct==2.9.45'] @@ -38,6 +40,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ ['philips.light.sread1', 'philips.light.ceiling', 'philips.light.zyceiling', + 'philips.light.moonlight', 'philips.light.bulb', 'philips.light.candle', 'philips.light.candle2', @@ -63,6 +66,13 @@ ATTR_AUTOMATIC_COLOR_TEMPERATURE = 'automatic_color_temperature' ATTR_REMINDER = 'reminder' ATTR_EYECARE_MODE = 'eyecare_mode' +# Moonlight +ATTR_SLEEP_ASSISTANT = 'sleep_assistant' +ATTR_SLEEP_OFF_TIME = 'sleep_off_time' +ATTR_TOTAL_ASSISTANT_SLEEP_TIME = 'total_assistant_sleep_time' +ATTR_BRAND_SLEEP = 'brand_sleep' +ATTR_BRAND = 'brand' + SERVICE_SET_SCENE = 'xiaomi_miio_set_scene' SERVICE_SET_DELAYED_TURN_OFF = 'xiaomi_miio_set_delayed_turn_off' SERVICE_REMINDER_ON = 'xiaomi_miio_reminder_on' @@ -151,6 +161,12 @@ async def async_setup_platform(hass, config, async_add_entities, device = XiaomiPhilipsCeilingLamp(name, light, model, unique_id) devices.append(device) hass.data[DATA_KEY][host] = device + elif model == 'philips.light.moonlight': + from miio import PhilipsMoonlight + light = PhilipsMoonlight(host, token) + device = XiaomiPhilipsMoonlightLamp(name, light, model, unique_id) + devices.append(device) + hass.data[DATA_KEY][host] = device elif model in ['philips.light.bulb', 'philips.light.candle', 'philips.light.candle2', @@ -307,15 +323,15 @@ class XiaomiPhilipsAbstractLight(Light): from miio import DeviceException try: state = await self.hass.async_add_executor_job(self._light.status) - _LOGGER.debug("Got new state: %s", state) - - self._available = True - self._state = state.is_on - self._brightness = ceil((255 / 100.0) * state.brightness) - except DeviceException as ex: self._available = False _LOGGER.error("Got exception while fetching the state: %s", ex) + return + + _LOGGER.debug("Got new state: %s", state) + self._available = True + self._state = state.is_on + self._brightness = ceil((255 / 100.0) * state.brightness) class XiaomiPhilipsGenericLight(XiaomiPhilipsAbstractLight): @@ -335,25 +351,25 @@ class XiaomiPhilipsGenericLight(XiaomiPhilipsAbstractLight): from miio import DeviceException try: state = await self.hass.async_add_executor_job(self._light.status) - _LOGGER.debug("Got new state: %s", state) - - self._available = True - self._state = state.is_on - self._brightness = ceil((255 / 100.0) * state.brightness) - - delayed_turn_off = self.delayed_turn_off_timestamp( - state.delay_off_countdown, - dt.utcnow(), - self._state_attrs[ATTR_DELAYED_TURN_OFF]) - - self._state_attrs.update({ - ATTR_SCENE: state.scene, - ATTR_DELAYED_TURN_OFF: delayed_turn_off, - }) - except DeviceException as ex: self._available = False _LOGGER.error("Got exception while fetching the state: %s", ex) + return + + _LOGGER.debug("Got new state: %s", state) + self._available = True + self._state = state.is_on + self._brightness = ceil((255 / 100.0) * state.brightness) + + delayed_turn_off = self.delayed_turn_off_timestamp( + state.delay_off_countdown, + dt.utcnow(), + self._state_attrs[ATTR_DELAYED_TURN_OFF]) + + self._state_attrs.update({ + ATTR_SCENE: state.scene, + ATTR_DELAYED_TURN_OFF: delayed_turn_off, + }) async def async_set_scene(self, scene: int = 1): """Set the fixed scene.""" @@ -485,29 +501,29 @@ class XiaomiPhilipsBulb(XiaomiPhilipsGenericLight): from miio import DeviceException try: state = await self.hass.async_add_executor_job(self._light.status) - _LOGGER.debug("Got new state: %s", state) - - self._available = True - self._state = state.is_on - self._brightness = ceil((255 / 100.0) * state.brightness) - self._color_temp = self.translate( - state.color_temperature, - CCT_MIN, CCT_MAX, - self.max_mireds, self.min_mireds) - - delayed_turn_off = self.delayed_turn_off_timestamp( - state.delay_off_countdown, - dt.utcnow(), - self._state_attrs[ATTR_DELAYED_TURN_OFF]) - - self._state_attrs.update({ - ATTR_SCENE: state.scene, - ATTR_DELAYED_TURN_OFF: delayed_turn_off, - }) - except DeviceException as ex: self._available = False _LOGGER.error("Got exception while fetching the state: %s", ex) + return + + _LOGGER.debug("Got new state: %s", state) + self._available = True + self._state = state.is_on + self._brightness = ceil((255 / 100.0) * state.brightness) + self._color_temp = self.translate( + state.color_temperature, + CCT_MIN, CCT_MAX, + self.max_mireds, self.min_mireds) + + delayed_turn_off = self.delayed_turn_off_timestamp( + state.delay_off_countdown, + dt.utcnow(), + self._state_attrs[ATTR_DELAYED_TURN_OFF]) + + self._state_attrs.update({ + ATTR_SCENE: state.scene, + ATTR_DELAYED_TURN_OFF: delayed_turn_off, + }) @staticmethod def translate(value, left_min, left_max, right_min, right_max): @@ -545,32 +561,32 @@ class XiaomiPhilipsCeilingLamp(XiaomiPhilipsBulb): from miio import DeviceException try: state = await self.hass.async_add_executor_job(self._light.status) - _LOGGER.debug("Got new state: %s", state) - - self._available = True - self._state = state.is_on - self._brightness = ceil((255 / 100.0) * state.brightness) - self._color_temp = self.translate( - state.color_temperature, - CCT_MIN, CCT_MAX, - self.max_mireds, self.min_mireds) - - delayed_turn_off = self.delayed_turn_off_timestamp( - state.delay_off_countdown, - dt.utcnow(), - self._state_attrs[ATTR_DELAYED_TURN_OFF]) - - self._state_attrs.update({ - ATTR_SCENE: state.scene, - ATTR_DELAYED_TURN_OFF: delayed_turn_off, - ATTR_NIGHT_LIGHT_MODE: state.smart_night_light, - ATTR_AUTOMATIC_COLOR_TEMPERATURE: - state.automatic_color_temperature, - }) - except DeviceException as ex: self._available = False _LOGGER.error("Got exception while fetching the state: %s", ex) + return + + _LOGGER.debug("Got new state: %s", state) + self._available = True + self._state = state.is_on + self._brightness = ceil((255 / 100.0) * state.brightness) + self._color_temp = self.translate( + state.color_temperature, + CCT_MIN, CCT_MAX, + self.max_mireds, self.min_mireds) + + delayed_turn_off = self.delayed_turn_off_timestamp( + state.delay_off_countdown, + dt.utcnow(), + self._state_attrs[ATTR_DELAYED_TURN_OFF]) + + self._state_attrs.update({ + ATTR_SCENE: state.scene, + ATTR_DELAYED_TURN_OFF: delayed_turn_off, + ATTR_NIGHT_LIGHT_MODE: state.smart_night_light, + ATTR_AUTOMATIC_COLOR_TEMPERATURE: + state.automatic_color_temperature, + }) class XiaomiPhilipsEyecareLamp(XiaomiPhilipsGenericLight): @@ -591,28 +607,28 @@ class XiaomiPhilipsEyecareLamp(XiaomiPhilipsGenericLight): from miio import DeviceException try: state = await self.hass.async_add_executor_job(self._light.status) - _LOGGER.debug("Got new state: %s", state) - - self._available = True - self._state = state.is_on - self._brightness = ceil((255 / 100.0) * state.brightness) - - delayed_turn_off = self.delayed_turn_off_timestamp( - state.delay_off_countdown, - dt.utcnow(), - self._state_attrs[ATTR_DELAYED_TURN_OFF]) - - self._state_attrs.update({ - ATTR_SCENE: state.scene, - ATTR_DELAYED_TURN_OFF: delayed_turn_off, - ATTR_REMINDER: state.reminder, - ATTR_NIGHT_LIGHT_MODE: state.smart_night_light, - ATTR_EYECARE_MODE: state.eyecare, - }) - except DeviceException as ex: self._available = False _LOGGER.error("Got exception while fetching the state: %s", ex) + return + + _LOGGER.debug("Got new state: %s", state) + self._available = True + self._state = state.is_on + self._brightness = ceil((255 / 100.0) * state.brightness) + + delayed_turn_off = self.delayed_turn_off_timestamp( + state.delay_off_countdown, + dt.utcnow(), + self._state_attrs[ATTR_DELAYED_TURN_OFF]) + + self._state_attrs.update({ + ATTR_SCENE: state.scene, + ATTR_DELAYED_TURN_OFF: delayed_turn_off, + ATTR_REMINDER: state.reminder, + ATTR_NIGHT_LIGHT_MODE: state.smart_night_light, + ATTR_EYECARE_MODE: state.eyecare, + }) async def async_set_delayed_turn_off(self, time_period: timedelta): """Set delayed turn off.""" @@ -719,12 +735,84 @@ class XiaomiPhilipsEyecareLampAmbientLight(XiaomiPhilipsAbstractLight): from miio import DeviceException try: state = await self.hass.async_add_executor_job(self._light.status) - _LOGGER.debug("Got new state: %s", state) - - self._available = True - self._state = state.ambient - self._brightness = ceil((255 / 100.0) * state.ambient_brightness) - except DeviceException as ex: self._available = False _LOGGER.error("Got exception while fetching the state: %s", ex) + return + + _LOGGER.debug("Got new state: %s", state) + self._available = True + self._state = state.ambient + self._brightness = ceil((255 / 100.0) * state.ambient_brightness) + + +class XiaomiPhilipsMoonlightLamp(XiaomiPhilipsBulb): + """Representation of a Xiaomi Philips Zhirui Bedside Lamp.""" + + def __init__(self, name, light, model, unique_id): + """Initialize the light device.""" + super().__init__(name, light, model, unique_id) + + self._hs_color = None + self._state_attrs.pop(ATTR_DELAYED_TURN_OFF) + self._state_attrs.update({ + ATTR_SLEEP_ASSISTANT: None, + ATTR_SLEEP_OFF_TIME: None, + ATTR_TOTAL_ASSISTANT_SLEEP_TIME: None, + ATTR_BRAND_SLEEP: None, + ATTR_BRAND: None, + }) + + @property + def min_mireds(self): + """Return the coldest color_temp that this light supports.""" + return 153 + + @property + def max_mireds(self): + """Return the warmest color_temp that this light supports.""" + return 588 + + @property + def hs_color(self) -> tuple: + """Return the hs color value.""" + return self._hs_color + + @property + def supported_features(self): + """Return the supported features.""" + return SUPPORT_BRIGHTNESS | SUPPORT_COLOR_TEMP + + async def async_update(self): + """Fetch state from the device.""" + from miio import DeviceException + try: + state = await self.hass.async_add_executor_job(self._light.status) + except DeviceException as ex: + self._available = False + _LOGGER.error("Got exception while fetching the state: %s", ex) + return + + _LOGGER.debug("Got new state: %s", state) + self._available = True + self._state = state.is_on + self._brightness = ceil((255 / 100.0) * state.brightness) + self._color_temp = self.translate( + state.color_temperature, + CCT_MIN, CCT_MAX, + self.max_mireds, self.min_mireds) + self._hs_color = color.color_RGB_to_hs(*state.rgb) + + self._state_attrs.update({ + ATTR_SCENE: state.scene, + ATTR_SLEEP_ASSISTANT: state.sleep_assistant, + ATTR_SLEEP_OFF_TIME: state.sleep_off_time, + ATTR_TOTAL_ASSISTANT_SLEEP_TIME: + state.total_assistant_sleep_time, + ATTR_BRAND_SLEEP: state.brand_sleep, + ATTR_BRAND: state.brand, + }) + + async def async_set_delayed_turn_off(self, time_period: timedelta): + """Set delayed turn off. Unsupported.""" + return From 6e55c2a3453e02523a684987019f2bff0eb79895 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ab=C3=ADlio=20Costa?= Date: Sat, 8 Dec 2018 14:16:16 -0600 Subject: [PATCH 026/304] update edp_redy version (#19078) * update edp_redy version * update requirements_all.txt --- homeassistant/components/edp_redy.py | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/edp_redy.py b/homeassistant/components/edp_redy.py index 210d7eb6afc..10780103613 100644 --- a/homeassistant/components/edp_redy.py +++ b/homeassistant/components/edp_redy.py @@ -26,7 +26,7 @@ EDP_REDY = 'edp_redy' DATA_UPDATE_TOPIC = '{0}_data_update'.format(DOMAIN) UPDATE_INTERVAL = 60 -REQUIREMENTS = ['edp_redy==0.0.2'] +REQUIREMENTS = ['edp_redy==0.0.3'] CONFIG_SCHEMA = vol.Schema({ DOMAIN: vol.Schema({ diff --git a/requirements_all.txt b/requirements_all.txt index aa2e52e13ba..7907b7c0f8d 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -325,7 +325,7 @@ dsmr_parser==0.12 dweepy==0.3.0 # homeassistant.components.edp_redy -edp_redy==0.0.2 +edp_redy==0.0.3 # homeassistant.components.media_player.horizon einder==0.3.1 From fd5b92b2fb777d3ff9e0b6ef32d338538be330ae Mon Sep 17 00:00:00 2001 From: edif30 Date: Sat, 8 Dec 2018 20:39:51 -0500 Subject: [PATCH 027/304] Update Google Assistant services description and request sync timeout (#19113) * Fix google assistant request sync service call * More descriptive services.yaml * Update services.yaml * Update __init__.py * Update request sync service call timeout Change from 5s to 15s to allow Google to respond. 5s was too short. The service would sync but the service call would time out and throw the error. --- homeassistant/components/google_assistant/__init__.py | 2 +- homeassistant/components/google_assistant/services.yaml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/google_assistant/__init__.py b/homeassistant/components/google_assistant/__init__.py index bf0c72ec1c8..c0dff15d888 100644 --- a/homeassistant/components/google_assistant/__init__.py +++ b/homeassistant/components/google_assistant/__init__.py @@ -67,7 +67,7 @@ async def async_setup(hass: HomeAssistant, yaml_config: Dict[str, Any]): """Handle request sync service calls.""" websession = async_get_clientsession(hass) try: - with async_timeout.timeout(5, loop=hass.loop): + with async_timeout.timeout(15, loop=hass.loop): agent_user_id = call.data.get('agent_user_id') or \ call.context.user_id res = await websession.post( diff --git a/homeassistant/components/google_assistant/services.yaml b/homeassistant/components/google_assistant/services.yaml index 7d3af71ac2b..33a52c8ef60 100644 --- a/homeassistant/components/google_assistant/services.yaml +++ b/homeassistant/components/google_assistant/services.yaml @@ -2,4 +2,4 @@ request_sync: description: Send a request_sync command to Google. fields: agent_user_id: - description: Optional. Only needed for automations. Specific Home Assistant user id to sync with Google Assistant. Do not need when you call this service through Home Assistant front end or API. Used in automation script or other place where context.user_id is missing. + description: "Optional. Only needed for automations. Specific Home Assistant user id (not username, ID in configuration > users > under username) to sync with Google Assistant. Do not need when you call this service through Home Assistant front end or API. Used in automation script or other place where context.user_id is missing." From 30064655c26f1e335e09f6e5a6f81d13a9974e49 Mon Sep 17 00:00:00 2001 From: arigilder <43716164+arigilder@users.noreply.github.com> Date: Sat, 8 Dec 2018 23:27:01 -0500 Subject: [PATCH 028/304] Remove marking device tracker stale if state is stale (#19133) --- homeassistant/components/device_tracker/__init__.py | 1 - 1 file changed, 1 deletion(-) diff --git a/homeassistant/components/device_tracker/__init__.py b/homeassistant/components/device_tracker/__init__.py index 16d9022c98f..202883713c7 100644 --- a/homeassistant/components/device_tracker/__init__.py +++ b/homeassistant/components/device_tracker/__init__.py @@ -383,7 +383,6 @@ class DeviceTracker: for device in self.devices.values(): if (device.track and device.last_update_home) and \ device.stale(now): - device.mark_stale() self.hass.async_create_task(device.async_update_ha_state(True)) async def async_setup_tracked_device(self): From 4b4f51fb6f65c6ad7602266ba29deb43b06f6622 Mon Sep 17 00:00:00 2001 From: Bas Schipper Date: Sun, 9 Dec 2018 10:33:39 +0100 Subject: [PATCH 029/304] Fixed doorbird config without events (empty list) (#19121) --- homeassistant/components/doorbird.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/doorbird.py b/homeassistant/components/doorbird.py index 578855011cc..42d14205e75 100644 --- a/homeassistant/components/doorbird.py +++ b/homeassistant/components/doorbird.py @@ -104,7 +104,7 @@ def setup(hass, config): return False # Subscribe to doorbell or motion events - if events is not None: + if events: doorstation.update_schedule(hass) hass.data[DOMAIN] = doorstations From 4d4967d0dd115199981067512d08b4baa2b14c59 Mon Sep 17 00:00:00 2001 From: Ludovico de Nittis Date: Sun, 9 Dec 2018 11:08:39 +0100 Subject: [PATCH 030/304] Add code support for iAlarm (#19124) --- .../components/alarm_control_panel/ialarm.py | 38 +++++++++++++++---- 1 file changed, 31 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/alarm_control_panel/ialarm.py b/homeassistant/components/alarm_control_panel/ialarm.py index efc7436e21b..6115edf406e 100644 --- a/homeassistant/components/alarm_control_panel/ialarm.py +++ b/homeassistant/components/alarm_control_panel/ialarm.py @@ -5,14 +5,16 @@ For more details about this platform, please refer to the documentation at https://home-assistant.io/components/alarm_control_panel.ialarm/ """ import logging +import re import voluptuous as vol import homeassistant.components.alarm_control_panel as alarm from homeassistant.components.alarm_control_panel import PLATFORM_SCHEMA from homeassistant.const import ( - CONF_HOST, CONF_NAME, CONF_PASSWORD, CONF_USERNAME, STATE_ALARM_ARMED_AWAY, - STATE_ALARM_ARMED_HOME, STATE_ALARM_DISARMED, STATE_ALARM_TRIGGERED) + CONF_CODE, CONF_HOST, CONF_NAME, CONF_PASSWORD, CONF_USERNAME, + STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME, STATE_ALARM_DISARMED, + STATE_ALARM_TRIGGERED) import homeassistant.helpers.config_validation as cv REQUIREMENTS = ['pyialarm==0.3'] @@ -36,6 +38,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Required(CONF_HOST): vol.All(cv.string, no_application_protocol), vol.Required(CONF_PASSWORD): cv.string, vol.Required(CONF_USERNAME): cv.string, + vol.Optional(CONF_CODE): cv.positive_int, vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, }) @@ -43,23 +46,25 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ def setup_platform(hass, config, add_entities, discovery_info=None): """Set up an iAlarm control panel.""" name = config.get(CONF_NAME) + code = config.get(CONF_CODE) username = config.get(CONF_USERNAME) password = config.get(CONF_PASSWORD) host = config.get(CONF_HOST) url = 'http://{}'.format(host) - ialarm = IAlarmPanel(name, username, password, url) + ialarm = IAlarmPanel(name, code, username, password, url) add_entities([ialarm], True) class IAlarmPanel(alarm.AlarmControlPanel): """Representation of an iAlarm status.""" - def __init__(self, name, username, password, url): + def __init__(self, name, code, username, password, url): """Initialize the iAlarm status.""" from pyialarm import IAlarm self._name = name + self._code = str(code) if code else None self._username = username self._password = password self._url = url @@ -71,6 +76,15 @@ class IAlarmPanel(alarm.AlarmControlPanel): """Return the name of the device.""" return self._name + @property + def code_format(self): + """Return one or more digits/characters.""" + if self._code is None: + return None + if isinstance(self._code, str) and re.search('^\\d+$', self._code): + return 'Number' + return 'Any' + @property def state(self): """Return the state of the device.""" @@ -98,12 +112,22 @@ class IAlarmPanel(alarm.AlarmControlPanel): def alarm_disarm(self, code=None): """Send disarm command.""" - self._client.disarm() + if self._validate_code(code): + self._client.disarm() def alarm_arm_away(self, code=None): """Send arm away command.""" - self._client.arm_away() + if self._validate_code(code): + self._client.arm_away() def alarm_arm_home(self, code=None): """Send arm home command.""" - self._client.arm_stay() + if self._validate_code(code): + self._client.arm_stay() + + def _validate_code(self, code): + """Validate given code.""" + check = self._code is None or code == self._code + if not check: + _LOGGER.warning("Wrong code entered") + return check From 863edfd66017e6e541221f07399fa4ec5c31c9f5 Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Sun, 9 Dec 2018 11:34:53 +0100 Subject: [PATCH 031/304] Upgrade slacker to 0.12.0 --- homeassistant/components/notify/slack.py | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/notify/slack.py b/homeassistant/components/notify/slack.py index 599633ff5ff..8e23c9f4fa0 100644 --- a/homeassistant/components/notify/slack.py +++ b/homeassistant/components/notify/slack.py @@ -17,7 +17,7 @@ from homeassistant.components.notify import ( BaseNotificationService) from homeassistant.const import (CONF_API_KEY, CONF_USERNAME, CONF_ICON) -REQUIREMENTS = ['slacker==0.11.0'] +REQUIREMENTS = ['slacker==0.12.0'] _LOGGER = logging.getLogger(__name__) diff --git a/requirements_all.txt b/requirements_all.txt index 7907b7c0f8d..b4fdd09723b 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1434,7 +1434,7 @@ sisyphus-control==2.1 skybellpy==0.2.0 # homeassistant.components.notify.slack -slacker==0.11.0 +slacker==0.12.0 # homeassistant.components.sleepiq sleepyq==0.6 From dbbbfaa86975f67b167231db36088d5b51335412 Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Sun, 9 Dec 2018 12:38:42 +0100 Subject: [PATCH 032/304] Upgrade youtube_dl to 2018.12.03 (#19139) --- 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 296c6c8d75d..0aac40d9f33 100644 --- a/homeassistant/components/media_extractor.py +++ b/homeassistant/components/media_extractor.py @@ -14,7 +14,7 @@ from homeassistant.components.media_player import ( SERVICE_PLAY_MEDIA) from homeassistant.helpers import config_validation as cv -REQUIREMENTS = ['youtube_dl==2018.11.23'] +REQUIREMENTS = ['youtube_dl==2018.12.03'] _LOGGER = logging.getLogger(__name__) diff --git a/requirements_all.txt b/requirements_all.txt index 7907b7c0f8d..cc7b0ee0aca 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1662,7 +1662,7 @@ yeelight==0.4.3 yeelightsunflower==0.0.10 # homeassistant.components.media_extractor -youtube_dl==2018.11.23 +youtube_dl==2018.12.03 # homeassistant.components.light.zengge zengge==0.2 From ce998cdc87ff1e804d465b26f93b643b8ba042fd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ville=20Skytt=C3=A4?= Date: Sun, 9 Dec 2018 19:19:13 +0200 Subject: [PATCH 033/304] Upgrade upcloud-api to 0.4.3 --- homeassistant/components/upcloud.py | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/upcloud.py b/homeassistant/components/upcloud.py index a0b61f86e56..ca0f554bd39 100644 --- a/homeassistant/components/upcloud.py +++ b/homeassistant/components/upcloud.py @@ -19,7 +19,7 @@ from homeassistant.helpers.dispatcher import ( from homeassistant.helpers.entity import Entity from homeassistant.helpers.event import track_time_interval -REQUIREMENTS = ['upcloud-api==0.4.2'] +REQUIREMENTS = ['upcloud-api==0.4.3'] _LOGGER = logging.getLogger(__name__) diff --git a/requirements_all.txt b/requirements_all.txt index cc7b0ee0aca..d8678675943 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1573,7 +1573,7 @@ twilio==6.19.1 uber_rides==0.6.0 # homeassistant.components.upcloud -upcloud-api==0.4.2 +upcloud-api==0.4.3 # homeassistant.components.sensor.ups upsmychoice==1.0.6 From 7d3a962f7342221f782a045199bdf992650e1aee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ville=20Skytt=C3=A4?= Date: Sun, 9 Dec 2018 21:22:08 +0200 Subject: [PATCH 034/304] Upgrade mypy to 0.650 (#19150) * Upgrade to 0.650 * Remove no longer needed type: ignore --- homeassistant/util/async_.py | 2 +- requirements_test.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/util/async_.py b/homeassistant/util/async_.py index 0185128abac..a4ad0e98a2e 100644 --- a/homeassistant/util/async_.py +++ b/homeassistant/util/async_.py @@ -28,7 +28,7 @@ except AttributeError: try: return loop.run_until_complete(main) finally: - asyncio.set_event_loop(None) # type: ignore # not a bug + asyncio.set_event_loop(None) loop.close() diff --git a/requirements_test.txt b/requirements_test.txt index d9c52bbd053..1cadd996c9c 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -6,7 +6,7 @@ coveralls==1.2.0 flake8-docstrings==1.3.0 flake8==3.6.0 mock-open==1.3.1 -mypy==0.641 +mypy==0.650 pydocstyle==2.1.1 pylint==2.2.2 pytest-aiohttp==0.3.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 2512d74e044..4f77f457289 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -7,7 +7,7 @@ coveralls==1.2.0 flake8-docstrings==1.3.0 flake8==3.6.0 mock-open==1.3.1 -mypy==0.641 +mypy==0.650 pydocstyle==2.1.1 pylint==2.2.2 pytest-aiohttp==0.3.0 From 5e65e27bda0d89b84112d308ee9d2fb32aa1f474 Mon Sep 17 00:00:00 2001 From: Lukas Barth Date: Sun, 9 Dec 2018 23:22:33 +0100 Subject: [PATCH 035/304] Update geizhals dependency (#19152) --- homeassistant/components/sensor/geizhals.py | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/sensor/geizhals.py b/homeassistant/components/sensor/geizhals.py index 7d215fb6baf..654ad0ccafb 100644 --- a/homeassistant/components/sensor/geizhals.py +++ b/homeassistant/components/sensor/geizhals.py @@ -15,7 +15,7 @@ from homeassistant.util import Throttle from homeassistant.helpers.entity import Entity from homeassistant.const import CONF_NAME -REQUIREMENTS = ['geizhals==0.0.7'] +REQUIREMENTS = ['geizhals==0.0.9'] _LOGGER = logging.getLogger(__name__) diff --git a/requirements_all.txt b/requirements_all.txt index cc7b0ee0aca..220614d385b 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -416,7 +416,7 @@ gTTS-token==1.1.3 gearbest_parser==1.0.7 # homeassistant.components.sensor.geizhals -geizhals==0.0.7 +geizhals==0.0.9 # homeassistant.components.geo_location.geo_json_events # homeassistant.components.geo_location.nsw_rural_fire_service_feed From 4c04fe652c80765397456523583c1f91405642b2 Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Sun, 9 Dec 2018 23:22:56 +0100 Subject: [PATCH 036/304] Upgrade sphinx-autodoc-typehints to 1.5.2 (#19140) --- requirements_docs.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements_docs.txt b/requirements_docs.txt index 7fd779d4231..5a11383a17c 100644 --- a/requirements_docs.txt +++ b/requirements_docs.txt @@ -1,3 +1,3 @@ Sphinx==1.8.2 -sphinx-autodoc-typehints==1.5.1 +sphinx-autodoc-typehints==1.5.2 sphinx-autodoc-annotation==1.0.post1 From 866c2ca994a656cbad1b4f5e66ccf568b2b238de Mon Sep 17 00:00:00 2001 From: clayton craft Date: Sun, 9 Dec 2018 16:27:31 -0600 Subject: [PATCH 037/304] Update radiotherm to 2.0.0 and handle change in tstat error detection (#19107) * radiotherm: bump version to 2.0.0 * radiotherm: change handling of transient errors from tstat Radiotherm 2.0.0 now throws an exception when a transient error is detected, instead of returning -1 for the field where the error was detected. This change supports handling the exception. --- homeassistant/components/climate/radiotherm.py | 14 ++++++++------ requirements_all.txt | 2 +- 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/climate/radiotherm.py b/homeassistant/components/climate/radiotherm.py index f914b9b4762..f0423d32c96 100644 --- a/homeassistant/components/climate/radiotherm.py +++ b/homeassistant/components/climate/radiotherm.py @@ -17,7 +17,7 @@ from homeassistant.const import ( CONF_HOST, TEMP_FAHRENHEIT, ATTR_TEMPERATURE, PRECISION_HALVES) import homeassistant.helpers.config_validation as cv -REQUIREMENTS = ['radiotherm==1.4.1'] +REQUIREMENTS = ['radiotherm==2.0.0'] _LOGGER = logging.getLogger(__name__) @@ -235,13 +235,15 @@ class RadioThermostat(ClimateDevice): self._name = self.device.name['raw'] # Request the current state from the thermostat. - data = self.device.tstat['raw'] + import radiotherm + try: + data = self.device.tstat['raw'] + except radiotherm.validate.RadiothermTstatError: + _LOGGER.error('%s (%s) was busy (invalid value returned)', + self._name, self.device.host) + return current_temp = data['temp'] - if current_temp == -1: - _LOGGER.error('%s (%s) was busy (temp == -1)', self._name, - self.device.host) - return # Map thermostat values into various STATE_ flags. self._current_temperature = current_temp diff --git a/requirements_all.txt b/requirements_all.txt index 27863a99aec..ec22bccfa9a 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1352,7 +1352,7 @@ quantum-gateway==0.0.3 rachiopy==0.1.3 # homeassistant.components.climate.radiotherm -radiotherm==1.4.1 +radiotherm==2.0.0 # homeassistant.components.raincloud raincloudy==0.0.5 From a744dc270b4bff3e498540c60f7716571e66dbc4 Mon Sep 17 00:00:00 2001 From: Yaron de Leeuw Date: Sun, 9 Dec 2018 17:32:48 -0500 Subject: [PATCH 038/304] Update pygtfs to upstream's 0.1.5 (#19151) This works by adding `?check_same_thread=False` to the sqlite connection string, as suggested by robbiet480@. It upgrades pygtfs from homeassitant's forked 0.1.3 version to 0.1.5. Fixes #15725 --- homeassistant/components/sensor/gtfs.py | 6 +++--- requirements_all.txt | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/sensor/gtfs.py b/homeassistant/components/sensor/gtfs.py index 633a50f15c1..3ccc60457b6 100644 --- a/homeassistant/components/sensor/gtfs.py +++ b/homeassistant/components/sensor/gtfs.py @@ -16,7 +16,7 @@ from homeassistant.const import CONF_NAME from homeassistant.helpers.entity import Entity import homeassistant.helpers.config_validation as cv -REQUIREMENTS = ['pygtfs-homeassistant==0.1.3.dev0'] +REQUIREMENTS = ['pygtfs==0.1.5'] _LOGGER = logging.getLogger(__name__) @@ -169,9 +169,9 @@ def setup_platform(hass, config, add_entities, discovery_info=None): import pygtfs - split_file_name = os.path.splitext(data) + (gtfs_root, _) = os.path.splitext(data) - sqlite_file = "{}.sqlite".format(split_file_name[0]) + sqlite_file = "{}.sqlite?check_same_thread=False".format(gtfs_root) joined_path = os.path.join(gtfs_dir, sqlite_file) gtfs = pygtfs.Schedule(joined_path) diff --git a/requirements_all.txt b/requirements_all.txt index ec22bccfa9a..0606252b18d 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -963,7 +963,7 @@ pygatt==3.2.0 pygogogate2==0.1.1 # homeassistant.components.sensor.gtfs -pygtfs-homeassistant==0.1.3.dev0 +pygtfs==0.1.5 # homeassistant.components.remote.harmony pyharmony==1.0.20 From a521b885bf14e14a75791a2d7411aafac2d6416d Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 10 Dec 2018 08:57:17 +0100 Subject: [PATCH 039/304] Lovelace using storage (#19101) * Add MVP * Remove unused code * Fix * Add force back * Fix tests * Storage keyed * Error out when storage doesnt find config * Use old load_yaml * Set config for panel correct * Use instance cache var * Make config option --- homeassistant/components/frontend/__init__.py | 3 +- homeassistant/components/lovelace/__init__.py | 675 ++------------- tests/components/lovelace/test_init.py | 812 ++---------------- 3 files changed, 177 insertions(+), 1313 deletions(-) diff --git a/homeassistant/components/frontend/__init__.py b/homeassistant/components/frontend/__init__.py index 36fbe14aefd..f14a3b0b324 100644 --- a/homeassistant/components/frontend/__init__.py +++ b/homeassistant/components/frontend/__init__.py @@ -250,8 +250,7 @@ async def async_setup(hass, config): await asyncio.wait( [async_register_built_in_panel(hass, panel) for panel in ( 'dev-event', 'dev-info', 'dev-service', 'dev-state', - 'dev-template', 'dev-mqtt', 'kiosk', 'lovelace', - 'states', 'profile')], + 'dev-template', 'dev-mqtt', 'kiosk', 'states', 'profile')], loop=hass.loop) hass.data[DATA_FINALIZE_PANEL] = async_finalize_panel diff --git a/homeassistant/components/lovelace/__init__.py b/homeassistant/components/lovelace/__init__.py index f6a8a3fd688..68c322b3956 100644 --- a/homeassistant/components/lovelace/__init__.py +++ b/homeassistant/components/lovelace/__init__.py @@ -7,507 +7,139 @@ at https://www.home-assistant.io/lovelace/ from functools import wraps import logging import os -from typing import Dict, List, Union import time -import uuid import voluptuous as vol from homeassistant.components import websocket_api from homeassistant.exceptions import HomeAssistantError -import homeassistant.util.ruamel_yaml as yaml +from homeassistant.util.yaml import load_yaml _LOGGER = logging.getLogger(__name__) DOMAIN = 'lovelace' -LOVELACE_DATA = 'lovelace' +STORAGE_KEY = DOMAIN +STORAGE_VERSION = 1 +CONF_MODE = 'mode' +MODE_YAML = 'yaml' +MODE_STORAGE = 'storage' + +CONFIG_SCHEMA = vol.Schema({ + DOMAIN: vol.Schema({ + vol.Optional(CONF_MODE, default=MODE_STORAGE): + vol.All(vol.Lower, vol.In([MODE_YAML, MODE_STORAGE])), + }), +}, extra=vol.ALLOW_EXTRA) + LOVELACE_CONFIG_FILE = 'ui-lovelace.yaml' -JSON_TYPE = Union[List, Dict, str] # pylint: disable=invalid-name -FORMAT_YAML = 'yaml' -FORMAT_JSON = 'json' - -OLD_WS_TYPE_GET_LOVELACE_UI = 'frontend/lovelace_config' WS_TYPE_GET_LOVELACE_UI = 'lovelace/config' -WS_TYPE_MIGRATE_CONFIG = 'lovelace/config/migrate' WS_TYPE_SAVE_CONFIG = 'lovelace/config/save' -WS_TYPE_GET_CARD = 'lovelace/config/card/get' -WS_TYPE_UPDATE_CARD = 'lovelace/config/card/update' -WS_TYPE_ADD_CARD = 'lovelace/config/card/add' -WS_TYPE_MOVE_CARD = 'lovelace/config/card/move' -WS_TYPE_DELETE_CARD = 'lovelace/config/card/delete' - -WS_TYPE_GET_VIEW = 'lovelace/config/view/get' -WS_TYPE_UPDATE_VIEW = 'lovelace/config/view/update' -WS_TYPE_ADD_VIEW = 'lovelace/config/view/add' -WS_TYPE_MOVE_VIEW = 'lovelace/config/view/move' -WS_TYPE_DELETE_VIEW = 'lovelace/config/view/delete' - SCHEMA_GET_LOVELACE_UI = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend({ - vol.Required('type'): - vol.Any(WS_TYPE_GET_LOVELACE_UI, OLD_WS_TYPE_GET_LOVELACE_UI), + vol.Required('type'): WS_TYPE_GET_LOVELACE_UI, vol.Optional('force', default=False): bool, }) -SCHEMA_MIGRATE_CONFIG = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend({ - vol.Required('type'): WS_TYPE_MIGRATE_CONFIG, -}) - SCHEMA_SAVE_CONFIG = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend({ vol.Required('type'): WS_TYPE_SAVE_CONFIG, vol.Required('config'): vol.Any(str, dict), - vol.Optional('format', default=FORMAT_JSON): - vol.Any(FORMAT_JSON, FORMAT_YAML), -}) - -SCHEMA_GET_CARD = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend({ - vol.Required('type'): WS_TYPE_GET_CARD, - vol.Required('card_id'): str, - vol.Optional('format', default=FORMAT_YAML): - vol.Any(FORMAT_JSON, FORMAT_YAML), -}) - -SCHEMA_UPDATE_CARD = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend({ - vol.Required('type'): WS_TYPE_UPDATE_CARD, - vol.Required('card_id'): str, - vol.Required('card_config'): vol.Any(str, dict), - vol.Optional('format', default=FORMAT_YAML): - vol.Any(FORMAT_JSON, FORMAT_YAML), -}) - -SCHEMA_ADD_CARD = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend({ - vol.Required('type'): WS_TYPE_ADD_CARD, - vol.Required('view_id'): str, - vol.Required('card_config'): vol.Any(str, dict), - vol.Optional('position'): int, - vol.Optional('format', default=FORMAT_YAML): - vol.Any(FORMAT_JSON, FORMAT_YAML), -}) - -SCHEMA_MOVE_CARD = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend({ - vol.Required('type'): WS_TYPE_MOVE_CARD, - vol.Required('card_id'): str, - vol.Optional('new_position'): int, - vol.Optional('new_view_id'): str, -}) - -SCHEMA_DELETE_CARD = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend({ - vol.Required('type'): WS_TYPE_DELETE_CARD, - vol.Required('card_id'): str, -}) - -SCHEMA_GET_VIEW = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend({ - vol.Required('type'): WS_TYPE_GET_VIEW, - vol.Required('view_id'): str, - vol.Optional('format', default=FORMAT_YAML): vol.Any(FORMAT_JSON, - FORMAT_YAML), -}) - -SCHEMA_UPDATE_VIEW = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend({ - vol.Required('type'): WS_TYPE_UPDATE_VIEW, - vol.Required('view_id'): str, - vol.Required('view_config'): vol.Any(str, dict), - vol.Optional('format', default=FORMAT_YAML): vol.Any(FORMAT_JSON, - FORMAT_YAML), -}) - -SCHEMA_ADD_VIEW = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend({ - vol.Required('type'): WS_TYPE_ADD_VIEW, - vol.Required('view_config'): vol.Any(str, dict), - vol.Optional('position'): int, - vol.Optional('format', default=FORMAT_YAML): vol.Any(FORMAT_JSON, - FORMAT_YAML), -}) - -SCHEMA_MOVE_VIEW = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend({ - vol.Required('type'): WS_TYPE_MOVE_VIEW, - vol.Required('view_id'): str, - vol.Required('new_position'): int, -}) - -SCHEMA_DELETE_VIEW = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend({ - vol.Required('type'): WS_TYPE_DELETE_VIEW, - vol.Required('view_id'): str, }) -class CardNotFoundError(HomeAssistantError): - """Card not found in data.""" - - -class ViewNotFoundError(HomeAssistantError): - """View not found in data.""" - - -class DuplicateIdError(HomeAssistantError): - """Duplicate ID's.""" - - -def load_config(hass, force: bool) -> JSON_TYPE: - """Load a YAML file.""" - fname = hass.config.path(LOVELACE_CONFIG_FILE) - - # Check for a cached version of the config - if not force and LOVELACE_DATA in hass.data: - config, last_update = hass.data[LOVELACE_DATA] - modtime = os.path.getmtime(fname) - if config and last_update > modtime: - return config - - config = yaml.load_yaml(fname, False) - seen_card_ids = set() - seen_view_ids = set() - if 'views' in config and not isinstance(config['views'], list): - raise HomeAssistantError("Views should be a list.") - for view in config.get('views', []): - if 'id' in view and not isinstance(view['id'], (str, int)): - raise HomeAssistantError( - "Your config contains view(s) with invalid ID(s).") - view_id = str(view.get('id', '')) - if view_id in seen_view_ids: - raise DuplicateIdError( - 'ID `{}` has multiple occurances in views'.format(view_id)) - seen_view_ids.add(view_id) - if 'cards' in view and not isinstance(view['cards'], list): - raise HomeAssistantError("Cards should be a list.") - for card in view.get('cards', []): - if 'id' in card and not isinstance(card['id'], (str, int)): - raise HomeAssistantError( - "Your config contains card(s) with invalid ID(s).") - card_id = str(card.get('id', '')) - if card_id in seen_card_ids: - raise DuplicateIdError( - 'ID `{}` has multiple occurances in cards' - .format(card_id)) - seen_card_ids.add(card_id) - hass.data[LOVELACE_DATA] = (config, time.time()) - return config - - -def migrate_config(fname: str) -> None: - """Add id to views and cards if not present and check duplicates.""" - config = yaml.load_yaml(fname, True) - updated = False - seen_card_ids = set() - seen_view_ids = set() - index = 0 - for view in config.get('views', []): - view_id = str(view.get('id', '')) - if not view_id: - updated = True - view.insert(0, 'id', index, comment="Automatically created id") - else: - if view_id in seen_view_ids: - raise DuplicateIdError( - 'ID `{}` has multiple occurrences in views'.format( - view_id)) - seen_view_ids.add(view_id) - for card in view.get('cards', []): - card_id = str(card.get('id', '')) - if not card_id: - updated = True - card.insert(0, 'id', uuid.uuid4().hex, - comment="Automatically created id") - else: - if card_id in seen_card_ids: - raise DuplicateIdError( - 'ID `{}` has multiple occurrences in cards' - .format(card_id)) - seen_card_ids.add(card_id) - index += 1 - if updated: - yaml.save_yaml(fname, config) - - -def save_config(fname: str, config, data_format: str = FORMAT_JSON) -> None: - """Save config to file.""" - if data_format == FORMAT_YAML: - config = yaml.yaml_to_object(config) - yaml.save_yaml(fname, config) - - -def get_card(fname: str, card_id: str, data_format: str = FORMAT_YAML)\ - -> JSON_TYPE: - """Load a specific card config for id.""" - round_trip = data_format == FORMAT_YAML - - config = yaml.load_yaml(fname, round_trip) - - for view in config.get('views', []): - for card in view.get('cards', []): - if str(card.get('id', '')) != card_id: - continue - if data_format == FORMAT_YAML: - return yaml.object_to_yaml(card) - return card - - raise CardNotFoundError( - "Card with ID: {} was not found in {}.".format(card_id, fname)) - - -def update_card(fname: str, card_id: str, card_config: str, - data_format: str = FORMAT_YAML) -> None: - """Save a specific card config for id.""" - config = yaml.load_yaml(fname, True) - for view in config.get('views', []): - for card in view.get('cards', []): - if str(card.get('id', '')) != card_id: - continue - if data_format == FORMAT_YAML: - card_config = yaml.yaml_to_object(card_config) - card.clear() - card.update(card_config) - yaml.save_yaml(fname, config) - return - - raise CardNotFoundError( - "Card with ID: {} was not found in {}.".format(card_id, fname)) - - -def add_card(fname: str, view_id: str, card_config: str, - position: int = None, data_format: str = FORMAT_YAML) -> None: - """Add a card to a view.""" - config = yaml.load_yaml(fname, True) - for view in config.get('views', []): - if str(view.get('id', '')) != view_id: - continue - cards = view.get('cards', []) - if not cards and 'cards' in view: - del view['cards'] - if data_format == FORMAT_YAML: - card_config = yaml.yaml_to_object(card_config) - if 'id' not in card_config: - card_config['id'] = uuid.uuid4().hex - if position is None: - cards.append(card_config) - else: - cards.insert(position, card_config) - if 'cards' not in view: - view['cards'] = cards - yaml.save_yaml(fname, config) - return - - raise ViewNotFoundError( - "View with ID: {} was not found in {}.".format(view_id, fname)) - - -def move_card(fname: str, card_id: str, position: int = None) -> None: - """Move a card to a different position.""" - if position is None: - raise HomeAssistantError( - 'Position is required if view is not specified.') - config = yaml.load_yaml(fname, True) - for view in config.get('views', []): - for card in view.get('cards', []): - if str(card.get('id', '')) != card_id: - continue - cards = view.get('cards') - cards.insert(position, cards.pop(cards.index(card))) - yaml.save_yaml(fname, config) - return - - raise CardNotFoundError( - "Card with ID: {} was not found in {}.".format(card_id, fname)) - - -def move_card_view(fname: str, card_id: str, view_id: str, - position: int = None) -> None: - """Move a card to a different view.""" - config = yaml.load_yaml(fname, True) - for view in config.get('views', []): - if str(view.get('id', '')) == view_id: - destination = view.get('cards') - for card in view.get('cards'): - if str(card.get('id', '')) != card_id: - continue - origin = view.get('cards') - card_to_move = card - - if 'destination' not in locals(): - raise ViewNotFoundError( - "View with ID: {} was not found in {}.".format(view_id, fname)) - if 'card_to_move' not in locals(): - raise CardNotFoundError( - "Card with ID: {} was not found in {}.".format(card_id, fname)) - - origin.pop(origin.index(card_to_move)) - - if position is None: - destination.append(card_to_move) - else: - destination.insert(position, card_to_move) - - yaml.save_yaml(fname, config) - - -def delete_card(fname: str, card_id: str) -> None: - """Delete a card from view.""" - config = yaml.load_yaml(fname, True) - for view in config.get('views', []): - for card in view.get('cards', []): - if str(card.get('id', '')) != card_id: - continue - cards = view.get('cards') - cards.pop(cards.index(card)) - yaml.save_yaml(fname, config) - return - - raise CardNotFoundError( - "Card with ID: {} was not found in {}.".format(card_id, fname)) - - -def get_view(fname: str, view_id: str, data_format: str = FORMAT_YAML) -> None: - """Get view without it's cards.""" - round_trip = data_format == FORMAT_YAML - config = yaml.load_yaml(fname, round_trip) - found = None - for view in config.get('views', []): - if str(view.get('id', '')) == view_id: - found = view - break - if found is None: - raise ViewNotFoundError( - "View with ID: {} was not found in {}.".format(view_id, fname)) - - del found['cards'] - if data_format == FORMAT_YAML: - return yaml.object_to_yaml(found) - return found - - -def update_view(fname: str, view_id: str, view_config, data_format: - str = FORMAT_YAML) -> None: - """Update view.""" - config = yaml.load_yaml(fname, True) - found = None - for view in config.get('views', []): - if str(view.get('id', '')) == view_id: - found = view - break - if found is None: - raise ViewNotFoundError( - "View with ID: {} was not found in {}.".format(view_id, fname)) - if data_format == FORMAT_YAML: - view_config = yaml.yaml_to_object(view_config) - if not view_config.get('cards') and found.get('cards'): - view_config['cards'] = found.get('cards', []) - if not view_config.get('badges') and found.get('badges'): - view_config['badges'] = found.get('badges', []) - found.clear() - found.update(view_config) - yaml.save_yaml(fname, config) - - -def add_view(fname: str, view_config: str, - position: int = None, data_format: str = FORMAT_YAML) -> None: - """Add a view.""" - config = yaml.load_yaml(fname, True) - views = config.get('views', []) - if data_format == FORMAT_YAML: - view_config = yaml.yaml_to_object(view_config) - if 'id' not in view_config: - view_config['id'] = uuid.uuid4().hex - if position is None: - views.append(view_config) - else: - views.insert(position, view_config) - if 'views' not in config: - config['views'] = views - yaml.save_yaml(fname, config) - - -def move_view(fname: str, view_id: str, position: int) -> None: - """Move a view to a different position.""" - config = yaml.load_yaml(fname, True) - views = config.get('views', []) - found = None - for view in views: - if str(view.get('id', '')) == view_id: - found = view - break - if found is None: - raise ViewNotFoundError( - "View with ID: {} was not found in {}.".format(view_id, fname)) - - views.insert(position, views.pop(views.index(found))) - yaml.save_yaml(fname, config) - - -def delete_view(fname: str, view_id: str) -> None: - """Delete a view.""" - config = yaml.load_yaml(fname, True) - views = config.get('views', []) - found = None - for view in views: - if str(view.get('id', '')) == view_id: - found = view - break - if found is None: - raise ViewNotFoundError( - "View with ID: {} was not found in {}.".format(view_id, fname)) - - views.pop(views.index(found)) - yaml.save_yaml(fname, config) +class ConfigNotFound(HomeAssistantError): + """When no config available.""" async def async_setup(hass, config): """Set up the Lovelace commands.""" - # Backwards compat. Added in 0.80. Remove after 0.85 - hass.components.websocket_api.async_register_command( - OLD_WS_TYPE_GET_LOVELACE_UI, websocket_lovelace_config, - SCHEMA_GET_LOVELACE_UI) + # Pass in default to `get` because defaults not set if loaded as dep + mode = config.get(DOMAIN, {}).get(CONF_MODE, MODE_STORAGE) + + await hass.components.frontend.async_register_built_in_panel( + DOMAIN, config={ + 'mode': mode + }) + + if mode == MODE_YAML: + hass.data[DOMAIN] = LovelaceYAML(hass) + else: + hass.data[DOMAIN] = LovelaceStorage(hass) hass.components.websocket_api.async_register_command( WS_TYPE_GET_LOVELACE_UI, websocket_lovelace_config, SCHEMA_GET_LOVELACE_UI) - hass.components.websocket_api.async_register_command( - WS_TYPE_MIGRATE_CONFIG, websocket_lovelace_migrate_config, - SCHEMA_MIGRATE_CONFIG) - hass.components.websocket_api.async_register_command( WS_TYPE_SAVE_CONFIG, websocket_lovelace_save_config, SCHEMA_SAVE_CONFIG) - hass.components.websocket_api.async_register_command( - WS_TYPE_GET_CARD, websocket_lovelace_get_card, SCHEMA_GET_CARD) - - hass.components.websocket_api.async_register_command( - WS_TYPE_UPDATE_CARD, websocket_lovelace_update_card, - SCHEMA_UPDATE_CARD) - - hass.components.websocket_api.async_register_command( - WS_TYPE_ADD_CARD, websocket_lovelace_add_card, SCHEMA_ADD_CARD) - - hass.components.websocket_api.async_register_command( - WS_TYPE_MOVE_CARD, websocket_lovelace_move_card, SCHEMA_MOVE_CARD) - - hass.components.websocket_api.async_register_command( - WS_TYPE_DELETE_CARD, websocket_lovelace_delete_card, - SCHEMA_DELETE_CARD) - - hass.components.websocket_api.async_register_command( - WS_TYPE_GET_VIEW, websocket_lovelace_get_view, SCHEMA_GET_VIEW) - - hass.components.websocket_api.async_register_command( - WS_TYPE_UPDATE_VIEW, websocket_lovelace_update_view, - SCHEMA_UPDATE_VIEW) - - hass.components.websocket_api.async_register_command( - WS_TYPE_ADD_VIEW, websocket_lovelace_add_view, SCHEMA_ADD_VIEW) - - hass.components.websocket_api.async_register_command( - WS_TYPE_MOVE_VIEW, websocket_lovelace_move_view, SCHEMA_MOVE_VIEW) - - hass.components.websocket_api.async_register_command( - WS_TYPE_DELETE_VIEW, websocket_lovelace_delete_view, - SCHEMA_DELETE_VIEW) - return True +class LovelaceStorage: + """Class to handle Storage based Lovelace config.""" + + def __init__(self, hass): + """Initialize Lovelace config based on storage helper.""" + self._store = hass.helpers.storage.Store(STORAGE_VERSION, STORAGE_KEY) + self._data = None + + 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} + + config = self._data['config'] + + if config is None: + raise ConfigNotFound + + return config + + async def async_save(self, config): + """Save config.""" + self._data = {'config': config} + await self._store.async_save(config) + + +class LovelaceYAML: + """Class to handle YAML-based Lovelace config.""" + + def __init__(self, hass): + """Initialize the YAML config.""" + self.hass = hass + self._cache = None + + async def async_load(self, force): + """Load config.""" + return await self.hass.async_add_executor_job(self._load_config, force) + + def _load_config(self, force): + """Load the actual config.""" + fname = self.hass.config.path(LOVELACE_CONFIG_FILE) + # Check for a cached version of the config + if not force and self._cache is not None: + config, last_update = self._cache + modtime = os.path.getmtime(fname) + if config and last_update > modtime: + return config + + try: + config = load_yaml(fname) + except FileNotFoundError: + raise ConfigNotFound from None + + self._cache = (config, time.time()) + return config + + async def async_save(self, config): + """Save config.""" + raise HomeAssistantError('Not supported') + + def handle_yaml_errors(func): """Handle error with WebSocket calls.""" @wraps(func) @@ -518,19 +150,8 @@ def handle_yaml_errors(func): message = websocket_api.result_message( msg['id'], result ) - except FileNotFoundError: - error = ('file_not_found', - 'Could not find ui-lovelace.yaml in your config dir.') - except yaml.UnsupportedYamlError as err: - error = 'unsupported_error', str(err) - except yaml.WriteError as err: - error = 'write_error', str(err) - except DuplicateIdError as err: - error = 'duplicate_id', str(err) - except CardNotFoundError as err: - error = 'card_not_found', str(err) - except ViewNotFoundError as err: - error = 'view_not_found', str(err) + except ConfigNotFound: + error = 'config_not_found', 'No config found.' except HomeAssistantError as err: error = 'error', str(err) @@ -546,117 +167,11 @@ def handle_yaml_errors(func): @handle_yaml_errors async def websocket_lovelace_config(hass, connection, msg): """Send Lovelace UI config over WebSocket configuration.""" - return await hass.async_add_executor_job(load_config, hass, - msg.get('force', False)) - - -@websocket_api.async_response -@handle_yaml_errors -async def websocket_lovelace_migrate_config(hass, connection, msg): - """Migrate Lovelace UI configuration.""" - return await hass.async_add_executor_job( - migrate_config, hass.config.path(LOVELACE_CONFIG_FILE)) + return await hass.data[DOMAIN].async_load(msg['force']) @websocket_api.async_response @handle_yaml_errors async def websocket_lovelace_save_config(hass, connection, msg): """Save Lovelace UI configuration.""" - return await hass.async_add_executor_job( - save_config, hass.config.path(LOVELACE_CONFIG_FILE), msg['config'], - msg.get('format', FORMAT_JSON)) - - -@websocket_api.async_response -@handle_yaml_errors -async def websocket_lovelace_get_card(hass, connection, msg): - """Send Lovelace card config over WebSocket configuration.""" - return await hass.async_add_executor_job( - get_card, hass.config.path(LOVELACE_CONFIG_FILE), msg['card_id'], - msg.get('format', FORMAT_YAML)) - - -@websocket_api.async_response -@handle_yaml_errors -async def websocket_lovelace_update_card(hass, connection, msg): - """Receive Lovelace card configuration over WebSocket and save.""" - return await hass.async_add_executor_job( - update_card, hass.config.path(LOVELACE_CONFIG_FILE), - msg['card_id'], msg['card_config'], msg.get('format', FORMAT_YAML)) - - -@websocket_api.async_response -@handle_yaml_errors -async def websocket_lovelace_add_card(hass, connection, msg): - """Add new card to view over WebSocket and save.""" - return await hass.async_add_executor_job( - add_card, hass.config.path(LOVELACE_CONFIG_FILE), - msg['view_id'], msg['card_config'], msg.get('position'), - msg.get('format', FORMAT_YAML)) - - -@websocket_api.async_response -@handle_yaml_errors -async def websocket_lovelace_move_card(hass, connection, msg): - """Move card to different position over WebSocket and save.""" - if 'new_view_id' in msg: - return await hass.async_add_executor_job( - move_card_view, hass.config.path(LOVELACE_CONFIG_FILE), - msg['card_id'], msg['new_view_id'], msg.get('new_position')) - - return await hass.async_add_executor_job( - move_card, hass.config.path(LOVELACE_CONFIG_FILE), - msg['card_id'], msg.get('new_position')) - - -@websocket_api.async_response -@handle_yaml_errors -async def websocket_lovelace_delete_card(hass, connection, msg): - """Delete card from Lovelace over WebSocket and save.""" - return await hass.async_add_executor_job( - delete_card, hass.config.path(LOVELACE_CONFIG_FILE), msg['card_id']) - - -@websocket_api.async_response -@handle_yaml_errors -async def websocket_lovelace_get_view(hass, connection, msg): - """Send Lovelace view config over WebSocket config.""" - return await hass.async_add_executor_job( - get_view, hass.config.path(LOVELACE_CONFIG_FILE), msg['view_id'], - msg.get('format', FORMAT_YAML)) - - -@websocket_api.async_response -@handle_yaml_errors -async def websocket_lovelace_update_view(hass, connection, msg): - """Receive Lovelace card config over WebSocket and save.""" - return await hass.async_add_executor_job( - update_view, hass.config.path(LOVELACE_CONFIG_FILE), - msg['view_id'], msg['view_config'], msg.get('format', FORMAT_YAML)) - - -@websocket_api.async_response -@handle_yaml_errors -async def websocket_lovelace_add_view(hass, connection, msg): - """Add new view over WebSocket and save.""" - return await hass.async_add_executor_job( - add_view, hass.config.path(LOVELACE_CONFIG_FILE), - msg['view_config'], msg.get('position'), - msg.get('format', FORMAT_YAML)) - - -@websocket_api.async_response -@handle_yaml_errors -async def websocket_lovelace_move_view(hass, connection, msg): - """Move view to different position over WebSocket and save.""" - return await hass.async_add_executor_job( - move_view, hass.config.path(LOVELACE_CONFIG_FILE), - msg['view_id'], msg['new_position']) - - -@websocket_api.async_response -@handle_yaml_errors -async def websocket_lovelace_delete_view(hass, connection, msg): - """Delete card from Lovelace over WebSocket and save.""" - return await hass.async_add_executor_job( - delete_view, hass.config.path(LOVELACE_CONFIG_FILE), msg['view_id']) + await hass.data[DOMAIN].async_save(msg['config']) diff --git a/tests/components/lovelace/test_init.py b/tests/components/lovelace/test_init.py index e296d14c6f8..ea856b464c3 100644 --- a/tests/components/lovelace/test_init.py +++ b/tests/components/lovelace/test_init.py @@ -1,748 +1,98 @@ """Test the Lovelace initialization.""" from unittest.mock import patch -from ruamel.yaml import YAML -from homeassistant.exceptions import HomeAssistantError from homeassistant.setup import async_setup_component -from homeassistant.components.websocket_api.const import TYPE_RESULT -from homeassistant.components.lovelace import migrate_config -from homeassistant.util.ruamel_yaml import UnsupportedYamlError - -TEST_YAML_A = """\ -title: My Awesome Home -# Include external resources -resources: - - url: /local/my-custom-card.js - type: js - - url: /local/my-webfont.css - type: css - -# Exclude entities from "Unused entities" view -excluded_entities: - - weblink.router -views: - # View tab title. - - title: Example - # Optional unique id for direct access /lovelace/${id} - id: example - # Optional background (overwrites the global background). - background: radial-gradient(crimson, skyblue) - # Each view can have a different theme applied. - theme: dark-mode - # The cards to show on this view. - cards: - # The filter card will filter entities for their state - - type: entity-filter - entities: - - device_tracker.paulus - - device_tracker.anne_there - state_filter: - - 'home' - card: - type: glance - title: People that are home - - # The picture entity card will represent an entity with a picture - - type: picture-entity - image: https://www.home-assistant.io/images/default-social.png - entity: light.bed_light - - # Specify a tab icon if you want the view tab to be an icon. - - icon: mdi:home-assistant - # Title of the view. Will be used as the tooltip for tab icon - title: Second view - cards: - - id: test - type: entities - title: Test card - # Entities card will take a list of entities and show their state. - - type: entities - # Title of the entities card - title: Example - # The entities here will be shown in the same order as specified. - # Each entry is an entity ID or a map with extra options. - entities: - - light.kitchen - - switch.ac - - entity: light.living_room - # Override the name to use - name: LR Lights - - # The markdown card will render markdown text. - - type: markdown - title: Lovelace - content: > - Welcome to your **Lovelace UI**. -""" - -TEST_YAML_B = """\ -title: Home -views: - - title: Dashboard - id: dashboard - icon: mdi:home - cards: - - id: testid - type: vertical-stack - cards: - - type: picture-entity - entity: group.sample - name: Sample - image: /local/images/sample.jpg - tap_action: toggle -""" - -# Test data that can not be loaded as YAML -TEST_BAD_YAML = """\ -title: Home -views: - - title: Dashboard - icon: mdi:home - cards: - - id: testid - type: vertical-stack -""" - -# Test unsupported YAML -TEST_UNSUP_YAML = """\ -title: Home -views: - - title: Dashboard - icon: mdi:home - cards: !include cards.yaml -""" +from homeassistant.components import frontend, lovelace -def test_add_id(): - """Test if id is added.""" - yaml = YAML(typ='rt') +async def test_lovelace_from_storage(hass, hass_ws_client, hass_storage): + """Test we load lovelace config from storage.""" + assert await async_setup_component(hass, 'lovelace', {}) + assert hass.data[frontend.DATA_PANELS]['lovelace'].config == { + 'mode': 'storage' + } - fname = "dummy.yaml" - with patch('homeassistant.util.ruamel_yaml.load_yaml', - return_value=yaml.load(TEST_YAML_A)), \ - patch('homeassistant.util.ruamel_yaml.save_yaml') \ - as save_yaml_mock: - migrate_config(fname) - - result = save_yaml_mock.call_args_list[0][0][1] - assert 'id' in result['views'][0]['cards'][0] - assert 'id' in result['views'][1] - - -def test_id_not_changed(): - """Test if id is not changed if already exists.""" - yaml = YAML(typ='rt') - - fname = "dummy.yaml" - with patch('homeassistant.util.ruamel_yaml.load_yaml', - return_value=yaml.load(TEST_YAML_B)), \ - patch('homeassistant.util.ruamel_yaml.save_yaml') \ - as save_yaml_mock: - migrate_config(fname) - assert save_yaml_mock.call_count == 0 - - -async def test_deprecated_lovelace_ui(hass, hass_ws_client): - """Test lovelace_ui command.""" - await async_setup_component(hass, 'lovelace') client = await hass_ws_client(hass) - with patch('homeassistant.components.lovelace.load_config', - return_value={'hello': 'world'}): - await client.send_json({ - 'id': 5, - 'type': 'frontend/lovelace_config', - }) - msg = await client.receive_json() + # Fetch data + await client.send_json({ + 'id': 5, + 'type': 'lovelace/config' + }) + response = await client.receive_json() + assert not response['success'] + assert response['error']['code'] == 'config_not_found' - assert msg['id'] == 5 - assert msg['type'] == TYPE_RESULT - assert msg['success'] - assert msg['result'] == {'hello': 'world'} + # 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'] == { + 'yo': 'hello' + } + + # Load new config + await client.send_json({ + 'id': 7, + 'type': 'lovelace/config' + }) + response = await client.receive_json() + assert response['success'] + + assert response['result'] == { + 'yo': 'hello' + } -async def test_deprecated_lovelace_ui_not_found(hass, hass_ws_client): - """Test lovelace_ui command cannot find file.""" - await async_setup_component(hass, 'lovelace') +async def test_lovelace_from_yaml(hass, hass_ws_client): + """Test we load lovelace config from yaml.""" + assert await async_setup_component(hass, 'lovelace', { + 'lovelace': { + 'mode': 'YAML' + } + }) + assert hass.data[frontend.DATA_PANELS]['lovelace'].config == { + 'mode': 'yaml' + } + client = await hass_ws_client(hass) - with patch('homeassistant.components.lovelace.load_config', - side_effect=FileNotFoundError): + # Fetch data + await client.send_json({ + 'id': 5, + 'type': 'lovelace/config' + }) + response = await client.receive_json() + assert not response['success'] + + assert response['error']['code'] == 'config_not_found' + + # Store new config not allowed + await client.send_json({ + 'id': 6, + 'type': 'lovelace/config/save', + 'config': { + 'yo': 'hello' + } + }) + response = await client.receive_json() + assert not response['success'] + + # Patch data + with patch('homeassistant.components.lovelace.load_yaml', return_value={ + 'hello': 'yo' + }): await client.send_json({ - 'id': 5, - 'type': 'frontend/lovelace_config', + 'id': 7, + 'type': 'lovelace/config' }) - msg = await client.receive_json() + response = await client.receive_json() - assert msg['id'] == 5 - assert msg['type'] == TYPE_RESULT - assert msg['success'] is False - assert msg['error']['code'] == 'file_not_found' - - -async def test_deprecated_lovelace_ui_load_err(hass, hass_ws_client): - """Test lovelace_ui command cannot find file.""" - await async_setup_component(hass, 'lovelace') - client = await hass_ws_client(hass) - - with patch('homeassistant.components.lovelace.load_config', - side_effect=HomeAssistantError): - await client.send_json({ - 'id': 5, - 'type': 'frontend/lovelace_config', - }) - msg = await client.receive_json() - - assert msg['id'] == 5 - assert msg['type'] == TYPE_RESULT - assert msg['success'] is False - assert msg['error']['code'] == 'error' - - -async def test_lovelace_ui(hass, hass_ws_client): - """Test lovelace_ui command.""" - await async_setup_component(hass, 'lovelace') - client = await hass_ws_client(hass) - - with patch('homeassistant.components.lovelace.load_config', - return_value={'hello': 'world'}): - await client.send_json({ - 'id': 5, - 'type': 'lovelace/config', - }) - msg = await client.receive_json() - - assert msg['id'] == 5 - assert msg['type'] == TYPE_RESULT - assert msg['success'] - assert msg['result'] == {'hello': 'world'} - - -async def test_lovelace_ui_not_found(hass, hass_ws_client): - """Test lovelace_ui command cannot find file.""" - await async_setup_component(hass, 'lovelace') - client = await hass_ws_client(hass) - - with patch('homeassistant.components.lovelace.load_config', - side_effect=FileNotFoundError): - await client.send_json({ - 'id': 5, - 'type': 'lovelace/config', - }) - msg = await client.receive_json() - - assert msg['id'] == 5 - assert msg['type'] == TYPE_RESULT - assert msg['success'] is False - assert msg['error']['code'] == 'file_not_found' - - -async def test_lovelace_ui_load_err(hass, hass_ws_client): - """Test lovelace_ui command load error.""" - await async_setup_component(hass, 'lovelace') - client = await hass_ws_client(hass) - - with patch('homeassistant.components.lovelace.load_config', - side_effect=HomeAssistantError): - await client.send_json({ - 'id': 5, - 'type': 'lovelace/config', - }) - msg = await client.receive_json() - - assert msg['id'] == 5 - assert msg['type'] == TYPE_RESULT - assert msg['success'] is False - assert msg['error']['code'] == 'error' - - -async def test_lovelace_ui_load_json_err(hass, hass_ws_client): - """Test lovelace_ui command load error.""" - await async_setup_component(hass, 'lovelace') - client = await hass_ws_client(hass) - - with patch('homeassistant.components.lovelace.load_config', - side_effect=UnsupportedYamlError): - await client.send_json({ - 'id': 5, - 'type': 'lovelace/config', - }) - msg = await client.receive_json() - - assert msg['id'] == 5 - assert msg['type'] == TYPE_RESULT - assert msg['success'] is False - assert msg['error']['code'] == 'unsupported_error' - - -async def test_lovelace_get_card(hass, hass_ws_client): - """Test get_card command.""" - await async_setup_component(hass, 'lovelace') - client = await hass_ws_client(hass) - yaml = YAML(typ='rt') - - with patch('homeassistant.util.ruamel_yaml.load_yaml', - return_value=yaml.load(TEST_YAML_A)): - await client.send_json({ - 'id': 5, - 'type': 'lovelace/config/card/get', - 'card_id': 'test', - }) - msg = await client.receive_json() - - assert msg['id'] == 5 - assert msg['type'] == TYPE_RESULT - assert msg['success'] - assert msg['result'] == 'id: test\ntype: entities\ntitle: Test card\n' - - -async def test_lovelace_get_card_not_found(hass, hass_ws_client): - """Test get_card command cannot find card.""" - await async_setup_component(hass, 'lovelace') - client = await hass_ws_client(hass) - yaml = YAML(typ='rt') - - with patch('homeassistant.util.ruamel_yaml.load_yaml', - return_value=yaml.load(TEST_YAML_A)): - await client.send_json({ - 'id': 5, - 'type': 'lovelace/config/card/get', - 'card_id': 'not_found', - }) - msg = await client.receive_json() - - assert msg['id'] == 5 - assert msg['type'] == TYPE_RESULT - assert msg['success'] is False - assert msg['error']['code'] == 'card_not_found' - - -async def test_lovelace_get_card_bad_yaml(hass, hass_ws_client): - """Test get_card command bad yaml.""" - await async_setup_component(hass, 'lovelace') - client = await hass_ws_client(hass) - - with patch('homeassistant.util.ruamel_yaml.load_yaml', - side_effect=HomeAssistantError): - await client.send_json({ - 'id': 5, - 'type': 'lovelace/config/card/get', - 'card_id': 'testid', - }) - msg = await client.receive_json() - - assert msg['id'] == 5 - assert msg['type'] == TYPE_RESULT - assert msg['success'] is False - assert msg['error']['code'] == 'error' - - -async def test_lovelace_update_card(hass, hass_ws_client): - """Test update_card command.""" - await async_setup_component(hass, 'lovelace') - client = await hass_ws_client(hass) - yaml = YAML(typ='rt') - - with patch('homeassistant.util.ruamel_yaml.load_yaml', - return_value=yaml.load(TEST_YAML_A)), \ - patch('homeassistant.util.ruamel_yaml.save_yaml') \ - as save_yaml_mock: - await client.send_json({ - 'id': 5, - 'type': 'lovelace/config/card/update', - 'card_id': 'test', - 'card_config': 'id: test\ntype: glance\n', - }) - msg = await client.receive_json() - - result = save_yaml_mock.call_args_list[0][0][1] - assert result.mlget(['views', 1, 'cards', 0, 'type'], - list_ok=True) == 'glance' - assert msg['id'] == 5 - assert msg['type'] == TYPE_RESULT - assert msg['success'] - - -async def test_lovelace_update_card_not_found(hass, hass_ws_client): - """Test update_card command cannot find card.""" - await async_setup_component(hass, 'lovelace') - client = await hass_ws_client(hass) - yaml = YAML(typ='rt') - - with patch('homeassistant.util.ruamel_yaml.load_yaml', - return_value=yaml.load(TEST_YAML_A)): - await client.send_json({ - 'id': 5, - 'type': 'lovelace/config/card/update', - 'card_id': 'not_found', - 'card_config': 'id: test\ntype: glance\n', - }) - msg = await client.receive_json() - - assert msg['id'] == 5 - assert msg['type'] == TYPE_RESULT - assert msg['success'] is False - assert msg['error']['code'] == 'card_not_found' - - -async def test_lovelace_update_card_bad_yaml(hass, hass_ws_client): - """Test update_card command bad yaml.""" - await async_setup_component(hass, 'lovelace') - client = await hass_ws_client(hass) - yaml = YAML(typ='rt') - - with patch('homeassistant.util.ruamel_yaml.load_yaml', - return_value=yaml.load(TEST_YAML_A)), \ - patch('homeassistant.util.ruamel_yaml.yaml_to_object', - side_effect=HomeAssistantError): - await client.send_json({ - 'id': 5, - 'type': 'lovelace/config/card/update', - 'card_id': 'test', - 'card_config': 'id: test\ntype: glance\n', - }) - msg = await client.receive_json() - - assert msg['id'] == 5 - assert msg['type'] == TYPE_RESULT - assert msg['success'] is False - assert msg['error']['code'] == 'error' - - -async def test_lovelace_add_card(hass, hass_ws_client): - """Test add_card command.""" - await async_setup_component(hass, 'lovelace') - client = await hass_ws_client(hass) - yaml = YAML(typ='rt') - - with patch('homeassistant.util.ruamel_yaml.load_yaml', - return_value=yaml.load(TEST_YAML_A)), \ - patch('homeassistant.util.ruamel_yaml.save_yaml') \ - as save_yaml_mock: - await client.send_json({ - 'id': 5, - 'type': 'lovelace/config/card/add', - 'view_id': 'example', - 'card_config': 'id: test\ntype: added\n', - }) - msg = await client.receive_json() - - result = save_yaml_mock.call_args_list[0][0][1] - assert result.mlget(['views', 0, 'cards', 2, 'type'], - list_ok=True) == 'added' - assert msg['id'] == 5 - assert msg['type'] == TYPE_RESULT - assert msg['success'] - - -async def test_lovelace_add_card_position(hass, hass_ws_client): - """Test add_card command.""" - await async_setup_component(hass, 'lovelace') - client = await hass_ws_client(hass) - yaml = YAML(typ='rt') - - with patch('homeassistant.util.ruamel_yaml.load_yaml', - return_value=yaml.load(TEST_YAML_A)), \ - patch('homeassistant.util.ruamel_yaml.save_yaml') \ - as save_yaml_mock: - await client.send_json({ - 'id': 5, - 'type': 'lovelace/config/card/add', - 'view_id': 'example', - 'position': 0, - 'card_config': 'id: test\ntype: added\n', - }) - msg = await client.receive_json() - - result = save_yaml_mock.call_args_list[0][0][1] - assert result.mlget(['views', 0, 'cards', 0, 'type'], - list_ok=True) == 'added' - assert msg['id'] == 5 - assert msg['type'] == TYPE_RESULT - assert msg['success'] - - -async def test_lovelace_move_card_position(hass, hass_ws_client): - """Test move_card command.""" - await async_setup_component(hass, 'lovelace') - client = await hass_ws_client(hass) - yaml = YAML(typ='rt') - - with patch('homeassistant.util.ruamel_yaml.load_yaml', - return_value=yaml.load(TEST_YAML_A)), \ - patch('homeassistant.util.ruamel_yaml.save_yaml') \ - as save_yaml_mock: - await client.send_json({ - 'id': 5, - 'type': 'lovelace/config/card/move', - 'card_id': 'test', - 'new_position': 2, - }) - msg = await client.receive_json() - - result = save_yaml_mock.call_args_list[0][0][1] - assert result.mlget(['views', 1, 'cards', 2, 'title'], - list_ok=True) == 'Test card' - assert msg['id'] == 5 - assert msg['type'] == TYPE_RESULT - assert msg['success'] - - -async def test_lovelace_move_card_view(hass, hass_ws_client): - """Test move_card to view command.""" - await async_setup_component(hass, 'lovelace') - client = await hass_ws_client(hass) - yaml = YAML(typ='rt') - - with patch('homeassistant.util.ruamel_yaml.load_yaml', - return_value=yaml.load(TEST_YAML_A)), \ - patch('homeassistant.util.ruamel_yaml.save_yaml') \ - as save_yaml_mock: - await client.send_json({ - 'id': 5, - 'type': 'lovelace/config/card/move', - 'card_id': 'test', - 'new_view_id': 'example', - }) - msg = await client.receive_json() - - result = save_yaml_mock.call_args_list[0][0][1] - assert result.mlget(['views', 0, 'cards', 2, 'title'], - list_ok=True) == 'Test card' - assert msg['id'] == 5 - assert msg['type'] == TYPE_RESULT - assert msg['success'] - - -async def test_lovelace_move_card_view_position(hass, hass_ws_client): - """Test move_card to view with position command.""" - await async_setup_component(hass, 'lovelace') - client = await hass_ws_client(hass) - yaml = YAML(typ='rt') - - with patch('homeassistant.util.ruamel_yaml.load_yaml', - return_value=yaml.load(TEST_YAML_A)), \ - patch('homeassistant.util.ruamel_yaml.save_yaml') \ - as save_yaml_mock: - await client.send_json({ - 'id': 5, - 'type': 'lovelace/config/card/move', - 'card_id': 'test', - 'new_view_id': 'example', - 'new_position': 1, - }) - msg = await client.receive_json() - - result = save_yaml_mock.call_args_list[0][0][1] - assert result.mlget(['views', 0, 'cards', 1, 'title'], - list_ok=True) == 'Test card' - assert msg['id'] == 5 - assert msg['type'] == TYPE_RESULT - assert msg['success'] - - -async def test_lovelace_delete_card(hass, hass_ws_client): - """Test delete_card command.""" - await async_setup_component(hass, 'lovelace') - client = await hass_ws_client(hass) - yaml = YAML(typ='rt') - - with patch('homeassistant.util.ruamel_yaml.load_yaml', - return_value=yaml.load(TEST_YAML_A)), \ - patch('homeassistant.util.ruamel_yaml.save_yaml') \ - as save_yaml_mock: - await client.send_json({ - 'id': 5, - 'type': 'lovelace/config/card/delete', - 'card_id': 'test', - }) - msg = await client.receive_json() - - result = save_yaml_mock.call_args_list[0][0][1] - cards = result.mlget(['views', 1, 'cards'], list_ok=True) - assert len(cards) == 2 - assert cards[0]['title'] == 'Example' - assert msg['id'] == 5 - assert msg['type'] == TYPE_RESULT - assert msg['success'] - - -async def test_lovelace_get_view(hass, hass_ws_client): - """Test get_view command.""" - await async_setup_component(hass, 'lovelace') - client = await hass_ws_client(hass) - yaml = YAML(typ='rt') - - with patch('homeassistant.util.ruamel_yaml.load_yaml', - return_value=yaml.load(TEST_YAML_A)): - await client.send_json({ - 'id': 5, - 'type': 'lovelace/config/view/get', - 'view_id': 'example', - }) - msg = await client.receive_json() - - assert msg['id'] == 5 - assert msg['type'] == TYPE_RESULT - assert msg['success'] - assert "".join(msg['result'].split()) == "".join('title: Example\n # \ - Optional unique id for direct\ - access /lovelace/${id}\nid: example\n # Optional\ - background (overwrites the global background).\n\ - background: radial-gradient(crimson, skyblue)\n\ - # Each view can have a different theme applied.\n\ - theme: dark-mode\n'.split()) - - -async def test_lovelace_get_view_not_found(hass, hass_ws_client): - """Test get_card command cannot find card.""" - await async_setup_component(hass, 'lovelace') - client = await hass_ws_client(hass) - yaml = YAML(typ='rt') - - with patch('homeassistant.util.ruamel_yaml.load_yaml', - return_value=yaml.load(TEST_YAML_A)): - await client.send_json({ - 'id': 5, - 'type': 'lovelace/config/view/get', - 'view_id': 'not_found', - }) - msg = await client.receive_json() - - assert msg['id'] == 5 - assert msg['type'] == TYPE_RESULT - assert msg['success'] is False - assert msg['error']['code'] == 'view_not_found' - - -async def test_lovelace_update_view(hass, hass_ws_client): - """Test update_view command.""" - await async_setup_component(hass, 'lovelace') - client = await hass_ws_client(hass) - yaml = YAML(typ='rt') - origyaml = yaml.load(TEST_YAML_A) - - with patch('homeassistant.util.ruamel_yaml.load_yaml', - return_value=origyaml), \ - patch('homeassistant.util.ruamel_yaml.save_yaml') \ - as save_yaml_mock: - await client.send_json({ - 'id': 5, - 'type': 'lovelace/config/view/update', - 'view_id': 'example', - 'view_config': 'id: example2\ntitle: New title\n', - }) - msg = await client.receive_json() - - result = save_yaml_mock.call_args_list[0][0][1] - orig_view = origyaml.mlget(['views', 0], list_ok=True) - new_view = result.mlget(['views', 0], list_ok=True) - assert new_view['title'] == 'New title' - assert new_view['cards'] == orig_view['cards'] - assert 'theme' not in new_view - assert msg['id'] == 5 - assert msg['type'] == TYPE_RESULT - assert msg['success'] - - -async def test_lovelace_add_view(hass, hass_ws_client): - """Test add_view command.""" - await async_setup_component(hass, 'lovelace') - client = await hass_ws_client(hass) - yaml = YAML(typ='rt') - - with patch('homeassistant.util.ruamel_yaml.load_yaml', - return_value=yaml.load(TEST_YAML_A)), \ - patch('homeassistant.util.ruamel_yaml.save_yaml') \ - as save_yaml_mock: - await client.send_json({ - 'id': 5, - 'type': 'lovelace/config/view/add', - 'view_config': 'id: test\ntitle: added\n', - }) - msg = await client.receive_json() - - result = save_yaml_mock.call_args_list[0][0][1] - assert result.mlget(['views', 2, 'title'], - list_ok=True) == 'added' - assert msg['id'] == 5 - assert msg['type'] == TYPE_RESULT - assert msg['success'] - - -async def test_lovelace_add_view_position(hass, hass_ws_client): - """Test add_view command with position.""" - await async_setup_component(hass, 'lovelace') - client = await hass_ws_client(hass) - yaml = YAML(typ='rt') - - with patch('homeassistant.util.ruamel_yaml.load_yaml', - return_value=yaml.load(TEST_YAML_A)), \ - patch('homeassistant.util.ruamel_yaml.save_yaml') \ - as save_yaml_mock: - await client.send_json({ - 'id': 5, - 'type': 'lovelace/config/view/add', - 'position': 0, - 'view_config': 'id: test\ntitle: added\n', - }) - msg = await client.receive_json() - - result = save_yaml_mock.call_args_list[0][0][1] - assert result.mlget(['views', 0, 'title'], - list_ok=True) == 'added' - assert msg['id'] == 5 - assert msg['type'] == TYPE_RESULT - assert msg['success'] - - -async def test_lovelace_move_view_position(hass, hass_ws_client): - """Test move_view command.""" - await async_setup_component(hass, 'lovelace') - client = await hass_ws_client(hass) - yaml = YAML(typ='rt') - - with patch('homeassistant.util.ruamel_yaml.load_yaml', - return_value=yaml.load(TEST_YAML_A)), \ - patch('homeassistant.util.ruamel_yaml.save_yaml') \ - as save_yaml_mock: - await client.send_json({ - 'id': 5, - 'type': 'lovelace/config/view/move', - 'view_id': 'example', - 'new_position': 1, - }) - msg = await client.receive_json() - - result = save_yaml_mock.call_args_list[0][0][1] - assert result.mlget(['views', 1, 'title'], - list_ok=True) == 'Example' - assert msg['id'] == 5 - assert msg['type'] == TYPE_RESULT - assert msg['success'] - - -async def test_lovelace_delete_view(hass, hass_ws_client): - """Test delete_card command.""" - await async_setup_component(hass, 'lovelace') - client = await hass_ws_client(hass) - yaml = YAML(typ='rt') - - with patch('homeassistant.util.ruamel_yaml.load_yaml', - return_value=yaml.load(TEST_YAML_A)), \ - patch('homeassistant.util.ruamel_yaml.save_yaml') \ - as save_yaml_mock: - await client.send_json({ - 'id': 5, - 'type': 'lovelace/config/view/delete', - 'view_id': 'example', - }) - msg = await client.receive_json() - - result = save_yaml_mock.call_args_list[0][0][1] - views = result.get('views', []) - assert len(views) == 1 - assert views[0]['title'] == 'Second view' - assert msg['id'] == 5 - assert msg['type'] == TYPE_RESULT - assert msg['success'] + assert response['success'] + assert response['result'] == {'hello': 'yo'} From faab0aa9df99aec25b879e4bfe99eb36082ea22d Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 10 Dec 2018 09:53:53 +0100 Subject: [PATCH 040/304] Updated frontend to 20181210.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 f14a3b0b324..cd592b25993 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==20181207.0'] +REQUIREMENTS = ['home-assistant-frontend==20181210.0'] DOMAIN = 'frontend' DEPENDENCIES = ['api', 'websocket_api', 'http', 'system_log', diff --git a/requirements_all.txt b/requirements_all.txt index 0606252b18d..5168bc29745 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -493,7 +493,7 @@ hole==0.3.0 holidays==0.9.8 # homeassistant.components.frontend -home-assistant-frontend==20181207.0 +home-assistant-frontend==20181210.0 # homeassistant.components.zwave homeassistant-pyozw==0.1.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 4f77f457289..df4ea066991 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -101,7 +101,7 @@ hdate==0.7.5 holidays==0.9.8 # homeassistant.components.frontend -home-assistant-frontend==20181207.0 +home-assistant-frontend==20181210.0 # homeassistant.components.homematicip_cloud homematicip==0.9.8 From fe2d24c240c4f90f7c6965caaf5785573537687a Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 10 Dec 2018 09:54:12 +0100 Subject: [PATCH 041/304] Update translations --- .../components/auth/.translations/ca.json | 16 +++++++-------- .../components/cast/.translations/ca.json | 2 +- .../components/cast/.translations/hu.json | 3 ++- .../components/deconz/.translations/ca.json | 4 ++-- .../components/deconz/.translations/hu.json | 6 ++++-- .../dialogflow/.translations/ca.json | 8 ++++---- .../dialogflow/.translations/hu.json | 5 +++++ .../homematicip_cloud/.translations/ca.json | 12 +++++------ .../components/hue/.translations/ca.json | 8 ++++---- .../components/ifttt/.translations/ca.json | 8 ++++---- .../components/ifttt/.translations/hu.json | 3 ++- .../components/ios/.translations/ca.json | 2 +- .../components/lifx/.translations/ca.json | 2 +- .../luftdaten/.translations/ca.json | 4 ++-- .../components/mailgun/.translations/ca.json | 8 ++++---- .../components/mailgun/.translations/hu.json | 15 ++++++++++++++ .../components/mailgun/.translations/no.json | 2 +- .../components/mqtt/.translations/ca.json | 10 +++++----- .../components/nest/.translations/ca.json | 14 ++++++------- .../components/nest/.translations/hu.json | 3 +++ .../components/openuv/.translations/ca.json | 2 +- .../owntracks/.translations/ca.json | 6 +++--- .../owntracks/.translations/hu.json | 6 ++++++ .../components/point/.translations/ca.json | 12 +++++------ .../components/point/.translations/hu.json | 17 ++++++++++++++++ .../rainmachine/.translations/ca.json | 2 +- .../simplisafe/.translations/ca.json | 2 +- .../simplisafe/.translations/hu.json | 4 +++- .../components/smhi/.translations/ca.json | 2 +- .../components/smhi/.translations/hu.json | 9 ++++++--- .../components/sonos/.translations/ca.json | 2 +- .../components/tradfri/.translations/ca.json | 8 ++++---- .../components/tradfri/.translations/hu.json | 1 + .../components/twilio/.translations/ca.json | 8 ++++---- .../components/twilio/.translations/hu.json | 7 +++++++ .../components/unifi/.translations/ca.json | 2 +- .../components/unifi/.translations/hu.json | 2 ++ .../components/upnp/.translations/ca.json | 2 +- .../components/upnp/.translations/hu.json | 4 ++++ .../components/zha/.translations/ca.json | 4 ++-- .../components/zha/.translations/hu.json | 20 +++++++++++++++++++ .../components/zone/.translations/ca.json | 2 +- .../components/zwave/.translations/ca.json | 8 ++++---- .../components/zwave/.translations/hu.json | 4 +++- 44 files changed, 181 insertions(+), 90 deletions(-) create mode 100644 homeassistant/components/mailgun/.translations/hu.json create mode 100644 homeassistant/components/point/.translations/hu.json create mode 100644 homeassistant/components/twilio/.translations/hu.json create mode 100644 homeassistant/components/zha/.translations/hu.json diff --git a/homeassistant/components/auth/.translations/ca.json b/homeassistant/components/auth/.translations/ca.json index 236352a9018..e5ece421a0b 100644 --- a/homeassistant/components/auth/.translations/ca.json +++ b/homeassistant/components/auth/.translations/ca.json @@ -5,28 +5,28 @@ "no_available_service": "No hi ha serveis de notificaci\u00f3 disponibles." }, "error": { - "invalid_code": "Codi inv\u00e0lid, si us plau torni a provar-ho." + "invalid_code": "Codi inv\u00e0lid, si us plau torna a provar-ho." }, "step": { "init": { - "description": "Seleccioneu un dels serveis de notificaci\u00f3:", - "title": "Configureu una contrasenya d'un sol \u00fas a trav\u00e9s del component de notificacions" + "description": "Selecciona un dels serveis de notificaci\u00f3:", + "title": "Configuraci\u00f3 d'una contrasenya d'un sol \u00fas a trav\u00e9s del component de notificacions" }, "setup": { - "description": "S'ha enviat una contrasenya d'un sol \u00fas mitjan\u00e7ant **notify.{notify_service}**. Introdu\u00efu-la a continuaci\u00f3:", - "title": "Verifiqueu la configuraci\u00f3" + "description": "S'ha enviat una contrasenya d'un sol \u00fas mitjan\u00e7ant **notify.{notify_service}**. Introdueix-la a continuaci\u00f3:", + "title": "Verificaci\u00f3 de la configuraci\u00f3" } }, "title": "Contrasenya d'un sol \u00fas del servei de notificacions" }, "totp": { "error": { - "invalid_code": "Codi inv\u00e0lid, si us plau torni a provar-ho. Si obteniu aquest error repetidament, assegureu-vos que la data i hora de Home Assistant sigui correcta i precisa." + "invalid_code": "Codi inv\u00e0lid, si us plau torna a provar-ho. Si obtens aquest error repetidament, assegura't que la data i hora de Home Assistant siguin correctes i acurades." }, "step": { "init": { - "description": "Per activar la verificaci\u00f3 en dos passos mitjan\u00e7ant contrasenyes d'un sol \u00fas basades en temps, escanegeu el codi QR amb la vostre aplicaci\u00f3 de verificaci\u00f3. Si no en teniu cap, us recomanem [Google Authenticator](https://support.google.com/accounts/answer/1066447) o b\u00e9 [Authy](https://authy.com/). \n\n {qr_code} \n \nDespr\u00e9s d'escanejar el codi QR, introdu\u00efu el codi de sis d\u00edgits proporcionat per l'aplicaci\u00f3. Si teniu problemes per escanejar el codi QR, feu una configuraci\u00f3 manual amb el codi **`{code}`**.", - "title": "Configureu la verificaci\u00f3 en dos passos utilitzant TOTP" + "description": "Per activar la verificaci\u00f3 en dos passos mitjan\u00e7ant contrasenyes d'un sol \u00fas basades en temps, escaneja el codi QR amb la teva aplicaci\u00f3 de verificaci\u00f3. Si no en tens cap, et recomanem [Google Authenticator](https://support.google.com/accounts/answer/1066447) o b\u00e9 [Authy](https://authy.com/). \n\n {qr_code} \n \nDespr\u00e9s d'escanejar el codi QR, introdueix el codi de sis d\u00edgits proporcionat per l'aplicaci\u00f3. Si tens problemes per escanejar el codi QR, fes una configuraci\u00f3 manual amb el codi **`{code}`**.", + "title": "Configura la verificaci\u00f3 en dos passos utilitzant TOTP" } }, "title": "TOTP" diff --git a/homeassistant/components/cast/.translations/ca.json b/homeassistant/components/cast/.translations/ca.json index 570cc7fdc00..26236397dec 100644 --- a/homeassistant/components/cast/.translations/ca.json +++ b/homeassistant/components/cast/.translations/ca.json @@ -6,7 +6,7 @@ }, "step": { "confirm": { - "description": "Voleu configurar Google Cast?", + "description": "Vols configurar Google Cast?", "title": "Google Cast" } }, diff --git a/homeassistant/components/cast/.translations/hu.json b/homeassistant/components/cast/.translations/hu.json index f59a1b43ef1..66dc4ea8dd8 100644 --- a/homeassistant/components/cast/.translations/hu.json +++ b/homeassistant/components/cast/.translations/hu.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "no_devices_found": "Nem tal\u00e1lhat\u00f3k Google Cast eszk\u00f6z\u00f6k a h\u00e1l\u00f3zaton." + "no_devices_found": "Nem tal\u00e1lhat\u00f3k Google Cast eszk\u00f6z\u00f6k a h\u00e1l\u00f3zaton.", + "single_instance_allowed": "Csak egyetlen Google Cast konfigur\u00e1ci\u00f3 sz\u00fcks\u00e9ges." }, "step": { "confirm": { diff --git a/homeassistant/components/deconz/.translations/ca.json b/homeassistant/components/deconz/.translations/ca.json index a3aa5491e23..87189a93806 100644 --- a/homeassistant/components/deconz/.translations/ca.json +++ b/homeassistant/components/deconz/.translations/ca.json @@ -14,7 +14,7 @@ "host": "Amfitri\u00f3", "port": "Port" }, - "title": "Definiu la passarel\u00b7la deCONZ" + "title": "Definici\u00f3 de la passarel\u00b7la deCONZ" }, "link": { "description": "Desbloqueja la teva passarel\u00b7la d'enlla\u00e7 deCONZ per a registrar-te amb Home Assistant.\n\n1. V\u00e9s a la configuraci\u00f3 del sistema deCONZ\n2. Prem el bot\u00f3 \"Desbloquejar passarel\u00b7la\"", @@ -23,7 +23,7 @@ "options": { "data": { "allow_clip_sensor": "Permet la importaci\u00f3 de sensors virtuals", - "allow_deconz_groups": "Permet la importaci\u00f3 de grups deCONZ" + "allow_deconz_groups": "Permetre la importaci\u00f3 de grups deCONZ" }, "title": "Opcions de configuraci\u00f3 addicionals per deCONZ" } diff --git a/homeassistant/components/deconz/.translations/hu.json b/homeassistant/components/deconz/.translations/hu.json index ca2466e9921..fbb5c26ba04 100644 --- a/homeassistant/components/deconz/.translations/hu.json +++ b/homeassistant/components/deconz/.translations/hu.json @@ -22,8 +22,10 @@ }, "options": { "data": { - "allow_clip_sensor": "Virtu\u00e1lis szenzorok import\u00e1l\u00e1s\u00e1nak enged\u00e9lyez\u00e9se" - } + "allow_clip_sensor": "Virtu\u00e1lis szenzorok import\u00e1l\u00e1s\u00e1nak enged\u00e9lyez\u00e9se", + "allow_deconz_groups": "deCONZ csoportok import\u00e1l\u00e1s\u00e1nak enged\u00e9lyez\u00e9se" + }, + "title": "Extra be\u00e1ll\u00edt\u00e1si lehet\u0151s\u00e9gek a deCONZhoz" } }, "title": "deCONZ Zigbee gateway" diff --git a/homeassistant/components/dialogflow/.translations/ca.json b/homeassistant/components/dialogflow/.translations/ca.json index ffc10269776..f6dfc9399c2 100644 --- a/homeassistant/components/dialogflow/.translations/ca.json +++ b/homeassistant/components/dialogflow/.translations/ca.json @@ -1,16 +1,16 @@ { "config": { "abort": { - "not_internet_accessible": "La vostra inst\u00e0ncia de Home Assistant ha de ser accessible des d'Internet per rebre missatges de Dialogflow.", + "not_internet_accessible": "La inst\u00e0ncia de Home Assistant ha de ser accessible des d'Internet per rebre missatges de Dialogflow.", "one_instance_allowed": "Nom\u00e9s cal una sola inst\u00e0ncia." }, "create_entry": { - "default": "Per enviar esdeveniments a Home Assistant, haureu de configurar [integraci\u00f3 webhook de Dialogflow]({dialogflow_url}). \n\n Ompliu la informaci\u00f3 seg\u00fcent: \n\n - URL: `{webhook_url}` \n - M\u00e8tode: POST \n - Tipus de contingut: application/json\n\nVegeu [la documentaci\u00f3]({docs_url}) per a m\u00e9s detalls." + "default": "Per enviar esdeveniments a Home Assistant, haur\u00e0s de configurar la [integraci\u00f3 webhook de Dialogflow]({dialogflow_url}). \n\n Completa la seg\u00fcent informaci\u00f3: \n\n- URL: `{webhook_url}` \n- M\u00e8tode: POST \n- Tipus de contingut: application/json\n\nConsulta la [documentaci\u00f3]({docs_url}) per a m\u00e9s detalls." }, "step": { "user": { - "description": "Esteu segur que voleu configurar Dialogflow?", - "title": "Configureu el Webhook de Dialogflow" + "description": "Est\u00e0s segur que vols configurar Dialogflow?", + "title": "Configuraci\u00f3 del Webhook de Dialogflow" } }, "title": "Dialogflow" diff --git a/homeassistant/components/dialogflow/.translations/hu.json b/homeassistant/components/dialogflow/.translations/hu.json index 89e8205bb09..89889fd6048 100644 --- a/homeassistant/components/dialogflow/.translations/hu.json +++ b/homeassistant/components/dialogflow/.translations/hu.json @@ -1,7 +1,12 @@ { "config": { + "abort": { + "not_internet_accessible": "A Home Assistant rendszerednek el\u00e9rhet\u0151nek kell lennie az internetr\u0151l a Dialogflow \u00fczenetek fogad\u00e1s\u00e1hoz.", + "one_instance_allowed": "Csak egyetlen konfigur\u00e1ci\u00f3 sz\u00fcks\u00e9ges." + }, "step": { "user": { + "description": "Biztosan be szeretn\u00e9d \u00e1ll\u00edtani az Dialogflowt?", "title": "Dialogflow Webhook be\u00e1ll\u00edt\u00e1sa" } }, diff --git a/homeassistant/components/homematicip_cloud/.translations/ca.json b/homeassistant/components/homematicip_cloud/.translations/ca.json index 9ad495c720a..a1c33a10e93 100644 --- a/homeassistant/components/homematicip_cloud/.translations/ca.json +++ b/homeassistant/components/homematicip_cloud/.translations/ca.json @@ -7,9 +7,9 @@ }, "error": { "invalid_pin": "Codi PIN inv\u00e0lid, torna-ho a provar.", - "press_the_button": "Si us plau, premeu el bot\u00f3 blau.", - "register_failed": "Error al registrar, torneu-ho a provar.", - "timeout_button": "Temps d'espera per pr\u00e9mer el bot\u00f3 blau esgotat, torneu-ho a provar." + "press_the_button": "Si us plau, prem el bot\u00f3 blau.", + "register_failed": "Error al registrar, torna-ho a provar.", + "timeout_button": "El temps d'espera m\u00e0xim per pr\u00e9mer el bot\u00f3 blau s'ha esgotat, torna-ho a provar." }, "step": { "init": { @@ -18,11 +18,11 @@ "name": "Nom (opcional, s'utilitza com a nom prefix per a tots els dispositius)", "pin": "Codi PIN (opcional)" }, - "title": "Trieu el punt d'acc\u00e9s HomematicIP" + "title": "Tria el punt d'acc\u00e9s HomematicIP" }, "link": { - "description": "Premeu el bot\u00f3 blau del punt d'acc\u00e9s i el bot\u00f3 de enviar per registrar HomematicIP amb Home Assistent. \n\n![Ubicaci\u00f3 del bot\u00f3 al pont](/static/images/config_flows/config_homematicip_cloud.png)", - "title": "Enlla\u00e7ar punt d'acc\u00e9s" + "description": "Prem el bot\u00f3 blau del punt d'acc\u00e9s i el bot\u00f3 d'enviar per registrar HomematicIP amb Home Assistent. \n\n![Ubicaci\u00f3 del bot\u00f3 al pont](/static/images/config_flows/config_homematicip_cloud.png)", + "title": "Enlla\u00e7 amb punt d'acc\u00e9s" } }, "title": "HomematicIP Cloud" diff --git a/homeassistant/components/hue/.translations/ca.json b/homeassistant/components/hue/.translations/ca.json index 6c41eed5467..a37d4ef1518 100644 --- a/homeassistant/components/hue/.translations/ca.json +++ b/homeassistant/components/hue/.translations/ca.json @@ -3,24 +3,24 @@ "abort": { "all_configured": "Tots els enlla\u00e7os Philips Hue ja estan configurats", "already_configured": "L'enlla\u00e7 ja est\u00e0 configurat", - "cannot_connect": "No es pot connectar amb l'enlla\u00e7", + "cannot_connect": "No s'ha pogut connectar amb l'enlla\u00e7", "discover_timeout": "No s'han pogut descobrir enlla\u00e7os Hue", "no_bridges": "No s'han trobat enlla\u00e7os Philips Hue", "unknown": "S'ha produ\u00eft un error desconegut" }, "error": { "linking": "S'ha produ\u00eft un error desconegut al vincular.", - "register_failed": "No s'ha pogut registrar, torneu-ho a provar" + "register_failed": "No s'ha pogut registrar, torna-ho a provar" }, "step": { "init": { "data": { "host": "Amfitri\u00f3" }, - "title": "Tria l'enlla\u00e7 Hue" + "title": "Tria de l'enlla\u00e7 Hue" }, "link": { - "description": "Premeu el bot\u00f3 de l'ella\u00e7 per registrar Philips Hue amb Home Assistant. \n\n ![Ubicaci\u00f3 del bot\u00f3 al pont](/static/images/config_philips_hue.jpg)", + "description": "Prem el bot\u00f3 de l'enlla\u00e7 per registrar Philips Hue amb Home Assistant. \n\n ![Ubicaci\u00f3 del bot\u00f3 al pont](/static/images/config_philips_hue.jpg)", "title": "Vincular concentrador" } }, diff --git a/homeassistant/components/ifttt/.translations/ca.json b/homeassistant/components/ifttt/.translations/ca.json index aadd66902b6..597328a2ee4 100644 --- a/homeassistant/components/ifttt/.translations/ca.json +++ b/homeassistant/components/ifttt/.translations/ca.json @@ -1,16 +1,16 @@ { "config": { "abort": { - "not_internet_accessible": "La vostra inst\u00e0ncia de Home Assistant ha de ser accessible des d'Internet per rebre missatges de IFTTT.", + "not_internet_accessible": "La inst\u00e0ncia de Home Assistant ha de ser accessible des d'Internet per rebre missatges de IFTTT.", "one_instance_allowed": "Nom\u00e9s cal una sola inst\u00e0ncia." }, "create_entry": { - "default": "Per enviar esdeveniments a Home Assistant, necessitareu utilitzar l'acci\u00f3 \"Make a web resquest\" de [IFTTT Webhook applet]({applet_url}). \n\n Ompliu la informaci\u00f3 seg\u00fcent: \n\n - URL: `{webhook_url}` \n - Method: POST \n - Content Type: application/json \n\nVegeu [la documentaci\u00f3]({docs_url}) sobre com configurar els automatismes per gestionar les dades entrants." + "default": "Per enviar esdeveniments a Home Assistant, necessitar\u00e0s utilitzar l'acci\u00f3 \"Make a web resquest\" de [IFTTT Webhook applet]({applet_url}). \n\nCompleta la seg\u00fcent informaci\u00f3: \n\n- URL: `{webhook_url}` \n- Method: POST \n- Content Type: application/json \n\nConsulta la [documentaci\u00f3]({docs_url}) sobre com configurar els automatismes per gestionar dades entrants." }, "step": { "user": { - "description": "Esteu segur que voleu configurar IFTTT?", - "title": "Configureu la miniaplicaci\u00f3 Webhook de IFTTT" + "description": "Est\u00e0s segur que vols configurar IFTTT?", + "title": "Configuraci\u00f3 de la miniaplicaci\u00f3 Webhook de IFTTT" } }, "title": "IFTTT" diff --git a/homeassistant/components/ifttt/.translations/hu.json b/homeassistant/components/ifttt/.translations/hu.json index 6ecf654ff47..3c4ec66e9a3 100644 --- a/homeassistant/components/ifttt/.translations/hu.json +++ b/homeassistant/components/ifttt/.translations/hu.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "not_internet_accessible": "A Home Assistant-nek el\u00e9rhet\u0151nek kell lennie az internetr\u0151l az IFTTT \u00fczenetek fogad\u00e1s\u00e1hoz." + "not_internet_accessible": "A Home Assistant-nek el\u00e9rhet\u0151nek kell lennie az internetr\u0151l az IFTTT \u00fczenetek fogad\u00e1s\u00e1hoz.", + "one_instance_allowed": "Csak egyetlen konfigur\u00e1ci\u00f3 sz\u00fcks\u00e9ges." }, "step": { "user": { diff --git a/homeassistant/components/ios/.translations/ca.json b/homeassistant/components/ios/.translations/ca.json index 1b1ed732ab3..dcbffdcebd0 100644 --- a/homeassistant/components/ios/.translations/ca.json +++ b/homeassistant/components/ios/.translations/ca.json @@ -5,7 +5,7 @@ }, "step": { "confirm": { - "description": "Voleu configurar el component Home Assistant iOS?", + "description": "Vols configurar el component Home Assistant iOS?", "title": "Home Assistant iOS" } }, diff --git a/homeassistant/components/lifx/.translations/ca.json b/homeassistant/components/lifx/.translations/ca.json index b3896d49e1d..e8ef5bd31bc 100644 --- a/homeassistant/components/lifx/.translations/ca.json +++ b/homeassistant/components/lifx/.translations/ca.json @@ -6,7 +6,7 @@ }, "step": { "confirm": { - "description": "Voleu configurar LIFX?", + "description": "Vols configurar LIFX?", "title": "LIFX" } }, diff --git a/homeassistant/components/luftdaten/.translations/ca.json b/homeassistant/components/luftdaten/.translations/ca.json index 1254b41bddf..b00c1b2e3e3 100644 --- a/homeassistant/components/luftdaten/.translations/ca.json +++ b/homeassistant/components/luftdaten/.translations/ca.json @@ -8,10 +8,10 @@ "step": { "user": { "data": { - "show_on_map": "Mostra al mapa", + "show_on_map": "Mostrar al mapa", "station_id": "Identificador del sensor Luftdaten" }, - "title": "Crear Luftdaten" + "title": "Configuraci\u00f3 de Luftdaten" } }, "title": "Luftdaten" diff --git a/homeassistant/components/mailgun/.translations/ca.json b/homeassistant/components/mailgun/.translations/ca.json index fcb087e6885..f43467de7d9 100644 --- a/homeassistant/components/mailgun/.translations/ca.json +++ b/homeassistant/components/mailgun/.translations/ca.json @@ -1,16 +1,16 @@ { "config": { "abort": { - "not_internet_accessible": "La vostra inst\u00e0ncia de Home Assistant ha de ser accessible des d'Internet per rebre missatges de Mailgun.", + "not_internet_accessible": "La inst\u00e0ncia de Home Assistant ha de ser accessible des d'Internet per rebre missatges de Mailgun.", "one_instance_allowed": "Nom\u00e9s cal una sola inst\u00e0ncia." }, "create_entry": { - "default": "Per enviar esdeveniments a Home Assistant, haureu de configurar [Webhooks amb Mailgun]({mailgun_url}). \n\n Ompliu la informaci\u00f3 seg\u00fcent: \n\n - URL: `{webhook_url}` \n - M\u00e8tode: POST \n - Tipus de contingut: application/x-www-form-urlencoded\n\nVegeu [la documentaci\u00f3]({docs_url}) sobre com configurar els automatismes per gestionar les dades entrants." + "default": "Per enviar esdeveniments a Home Assistant, haur\u00e0s de configurar [Webhooks amb Mailgun]({mailgun_url}). \n\nCompleta la seg\u00fcent informaci\u00f3: \n\n- URL: `{webhook_url}` \n- M\u00e8tode: POST \n- Tipus de contingut: application/x-www-form-urlencoded\n\nConsulta la [documentaci\u00f3]({docs_url}) sobre com configurar els automatismes per gestionar dades entrants." }, "step": { "user": { - "description": "Esteu segur que voleu configurar Mailgun?", - "title": "Configureu el Webhook de Mailgun" + "description": "Est\u00e0s segur que vols configurar Mailgun?", + "title": "Configuraci\u00f3 del Webhook de Mailgun" } }, "title": "Mailgun" diff --git a/homeassistant/components/mailgun/.translations/hu.json b/homeassistant/components/mailgun/.translations/hu.json new file mode 100644 index 00000000000..975c106a26f --- /dev/null +++ b/homeassistant/components/mailgun/.translations/hu.json @@ -0,0 +1,15 @@ +{ + "config": { + "abort": { + "not_internet_accessible": "A Home Assistant rendszerednek el\u00e9rhet\u0151nek kell lennie az internetr\u0151l a Mailgun \u00fczenetek fogad\u00e1s\u00e1hoz.", + "one_instance_allowed": "Csak egyetlen konfigur\u00e1ci\u00f3 sz\u00fcks\u00e9ges." + }, + "step": { + "user": { + "description": "Biztosan be szeretn\u00e9d \u00e1ll\u00edtani a Mailgunt?", + "title": "Mailgun Webhook be\u00e1ll\u00edt\u00e1sa" + } + }, + "title": "Mailgun" + } +} \ No newline at end of file diff --git a/homeassistant/components/mailgun/.translations/no.json b/homeassistant/components/mailgun/.translations/no.json index e1254910542..91c616b69af 100644 --- a/homeassistant/components/mailgun/.translations/no.json +++ b/homeassistant/components/mailgun/.translations/no.json @@ -5,7 +5,7 @@ "one_instance_allowed": "Kun \u00e9n enkelt forekomst er n\u00f8dvendig." }, "create_entry": { - "default": "For \u00e5 sende hendelser til Home Assistant, m\u00e5 du sette opp [Webhooks with Mailgun]({mailgun_url}).\n\nFyll ut f\u00f8lgende informasjon:\n\n- URL: `{webhook_url}`\n- Metode: POST\n- Innholdstype: application/x-www-form-urlencoded\n\nSe [dokumentasjonen]({docs_url}) om hvordan du konfigurerer automatiseringer for \u00e5 h\u00e5ndtere innkommende data." + "default": "For \u00e5 sende hendelser til Home Assistant, m\u00e5 du sette opp [Webhooks with Mailgun]({mailgun_url}).\n\nFyll ut f\u00f8lgende informasjon:\n\n- URL: `{webhook_url}`\n- Metode: POST\n- Innholdstype: application/json\n\nSe [dokumentasjonen]({docs_url}) om hvordan du konfigurerer automatiseringer for \u00e5 h\u00e5ndtere innkommende data." }, "step": { "user": { diff --git a/homeassistant/components/mqtt/.translations/ca.json b/homeassistant/components/mqtt/.translations/ca.json index 72a2636fb60..1fc3ea628bb 100644 --- a/homeassistant/components/mqtt/.translations/ca.json +++ b/homeassistant/components/mqtt/.translations/ca.json @@ -4,25 +4,25 @@ "single_instance_allowed": "Nom\u00e9s es permet una \u00fanica configuraci\u00f3 de MQTT." }, "error": { - "cannot_connect": "No es pot connectar amb el broker." + "cannot_connect": "No s'ha pogut connectar amb el broker." }, "step": { "broker": { "data": { "broker": "Broker", - "discovery": "Habilita descobriment autom\u00e0tic", + "discovery": "Habilita el descobriment autom\u00e0tic", "password": "Contrasenya", "port": "Port", "username": "Nom d'usuari" }, - "description": "Introdu\u00efu la informaci\u00f3 de connexi\u00f3 del vostre broker MQTT.", + "description": "Introdueix la informaci\u00f3 de connexi\u00f3 del teu broker MQTT.", "title": "MQTT" }, "hassio_confirm": { "data": { - "discovery": "Habilita descobriment autom\u00e0tic" + "discovery": "Habilitar descobriment autom\u00e0tic" }, - "description": "Voleu configurar Home Assistant perqu\u00e8 es connecti amb el broker MQTT proporcionat pel complement de hass.io {addon}?", + "description": "Vols configurar Home Assistant perqu\u00e8 es connecti amb el broker MQTT proporcionat pel complement de hass.io {addon}?", "title": "Broker MQTT a trav\u00e9s del complement de Hass.io" } }, diff --git a/homeassistant/components/nest/.translations/ca.json b/homeassistant/components/nest/.translations/ca.json index e15d0106da8..179c8f20951 100644 --- a/homeassistant/components/nest/.translations/ca.json +++ b/homeassistant/components/nest/.translations/ca.json @@ -1,10 +1,10 @@ { "config": { "abort": { - "already_setup": "Nom\u00e9s podeu configurar un \u00fanic compte Nest.", + "already_setup": "Nom\u00e9s pots configurar un \u00fanic compte Nest.", "authorize_url_fail": "S'ha produ\u00eft un error desconegut al generar l'URL d'autoritzaci\u00f3.", - "authorize_url_timeout": "Temps d'espera generant l'URL d'autoritzaci\u00f3 esgotat.", - "no_flows": "Necessiteu configurar Nest abans de poder autenticar-vos-hi. [Llegiu les instruccions](https://www.home-assistant.io/components/nest/)." + "authorize_url_timeout": "El temps d'espera m\u00e0xim per generar l'URL d'autoritzaci\u00f3 s'ha esgotat.", + "no_flows": "Necessites configurar Nest abans de poder autenticar-t'hi. Llegeix les [instruccions](https://www.home-assistant.io/components/nest/)." }, "error": { "internal_error": "Error intern al validar el codi", @@ -17,15 +17,15 @@ "data": { "flow_impl": "Prove\u00efdor" }, - "description": "Trieu a trav\u00e9s de quin prove\u00efdor d'autenticaci\u00f3 us voleu autenticar amb Nest.", + "description": "Tria a amb quin prove\u00efdor d'autenticaci\u00f3 vols autenticar-te amb Nest.", "title": "Prove\u00efdor d'autenticaci\u00f3" }, "link": { "data": { - "code": "Codi pin" + "code": "Codi PIN" }, - "description": "Per enlla\u00e7ar el vostre compte de Nest, [autoritzeu el vostre compte] ({url}). \n\nDespr\u00e9s de l'autoritzaci\u00f3, copieu i enganxeu el codi pin que es mostra a sota.", - "title": "Enlla\u00e7ar compte de Nest" + "description": "Per enlla\u00e7ar el teu compte de Nest, [autoritza el vostre compte]({url}). \n\nDespr\u00e9s de l'autoritzaci\u00f3, copia i enganxa el codi pin que es mostra a sota.", + "title": "Enlla\u00e7 amb el compte de Nest" } }, "title": "Nest" diff --git a/homeassistant/components/nest/.translations/hu.json b/homeassistant/components/nest/.translations/hu.json index aa99b46e576..e24c38f8608 100644 --- a/homeassistant/components/nest/.translations/hu.json +++ b/homeassistant/components/nest/.translations/hu.json @@ -2,9 +2,11 @@ "config": { "abort": { "already_setup": "Csak egy Nest-fi\u00f3kot konfigur\u00e1lhat.", + "authorize_url_fail": "Ismeretlen hiba t\u00f6rt\u00e9nt a hiteles\u00edt\u00e9si link gener\u00e1l\u00e1sa sor\u00e1n.", "authorize_url_timeout": "Id\u0151t\u00fall\u00e9p\u00e9s az \u00e9rv\u00e9nyes\u00edt\u00e9si url gener\u00e1l\u00e1sa sor\u00e1n." }, "error": { + "internal_error": "Bels\u0151 hiba t\u00f6rt\u00e9nt a k\u00f3d valid\u00e1l\u00e1s\u00e1n\u00e1l", "invalid_code": "\u00c9rv\u00e9nytelen k\u00f3d", "timeout": "Id\u0151t\u00fall\u00e9p\u00e9s a k\u00f3d \u00e9rv\u00e9nyes\u00edt\u00e9se sor\u00e1n.", "unknown": "Ismeretlen hiba t\u00f6rt\u00e9nt a k\u00f3d \u00e9rv\u00e9nyes\u00edt\u00e9se sor\u00e1n" @@ -14,6 +16,7 @@ "data": { "flow_impl": "Szolg\u00e1ltat\u00f3" }, + "description": "V\u00e1laszd ki, hogy melyik hiteles\u00edt\u00e9si szolg\u00e1ltat\u00f3n\u00e1l szeretn\u00e9d hiteles\u00edteni a Nestet.", "title": "Hiteles\u00edt\u00e9si Szolg\u00e1ltat\u00f3" }, "link": { diff --git a/homeassistant/components/openuv/.translations/ca.json b/homeassistant/components/openuv/.translations/ca.json index 4a6cf526921..5cb9a8ce5a5 100644 --- a/homeassistant/components/openuv/.translations/ca.json +++ b/homeassistant/components/openuv/.translations/ca.json @@ -12,7 +12,7 @@ "latitude": "Latitud", "longitude": "Longitud" }, - "title": "Introdu\u00efu la vostra informaci\u00f3" + "title": "Introdueix la teva informaci\u00f3" } }, "title": "OpenUV" diff --git a/homeassistant/components/owntracks/.translations/ca.json b/homeassistant/components/owntracks/.translations/ca.json index 438148f414c..c733f0f12cc 100644 --- a/homeassistant/components/owntracks/.translations/ca.json +++ b/homeassistant/components/owntracks/.translations/ca.json @@ -4,12 +4,12 @@ "one_instance_allowed": "Nom\u00e9s cal una sola inst\u00e0ncia." }, "create_entry": { - "default": "\n\nPer Android: obre [l'app OwnTracks]({android_url}), ves a prefer\u00e8ncies -> connexi\u00f3, i posa els par\u00e0metres seguents:\n - Mode: Private HTTP\n - Host: {webhook_url}\n - Identification:\n - Username: ``\n - Device ID: ``\n\nPer iOS: obre [l'app OwnTracks]({ios_url}), clica l'icona (i) a dalt a l'esquerra -> configuraci\u00f3 (settings), i posa els par\u00e0metres settings:\n - Mode: HTTP\n - URL: {webhook_url}\n - Turn on authentication\n - UserID: ``\n\n{secret}\n\nVegeu [the documentation]({docs_url}) per a m\u00e9s informaci\u00f3." + "default": "\n\nPer Android: obre [l'app d'OwnTracks]({android_url}), ves a prefer\u00e8ncies -> connexi\u00f3, i posa els par\u00e0metres seguents:\n - Mode: Private HTTP\n - Host: {webhook_url}\n - Identification:\n - Username: ``\n - Device ID: ``\n\nPer iOS: obre [l'app d'OwnTracks]({ios_url}), clica l'icona (i) a dalt a l'esquerra -> configuraci\u00f3 (settings), i posa els par\u00e0metres seg\u00fcents:\n - Mode: HTTP\n - URL: {webhook_url}\n - Turn on authentication\n - UserID: ``\n\n{secret}\n\nConsulta [la documentaci\u00f3]({docs_url}) per a m\u00e9s informaci\u00f3." }, "step": { "user": { - "description": "Esteu segur que voleu configurar OwnTracks?", - "title": "Configureu OwnTracks" + "description": "Est\u00e0s segur que vols configurar l'OwnTracks?", + "title": "Configuraci\u00f3 d'OwnTracks" } }, "title": "OwnTracks" diff --git a/homeassistant/components/owntracks/.translations/hu.json b/homeassistant/components/owntracks/.translations/hu.json index 9c4e46a28bf..a82843bef53 100644 --- a/homeassistant/components/owntracks/.translations/hu.json +++ b/homeassistant/components/owntracks/.translations/hu.json @@ -1,5 +1,11 @@ { "config": { + "abort": { + "one_instance_allowed": "Csak egyetlen konfigur\u00e1ci\u00f3 sz\u00fcks\u00e9ges." + }, + "create_entry": { + "default": "\n\nAndroidon, nyisd meg [az OwnTracks appot]({android_url}), menj a preferences -> connectionre. V\u00e1ltoztasd meg a al\u00e1bbi be\u00e1ll\u00edt\u00e1sokat:\n - Mode: Private HTTP\n - Host: {webhook_url}\n - Identification:\n - Username: ``\n - Device ID: ``\n\niOS-en, nyisd meg [az OwnTracks appot]({ios_url}), kattints az (i) ikonra bal oldalon fel\u00fcl -> settings. V\u00e1ltoztasd meg az al\u00e1bbi be\u00e1ll\u00edt\u00e1sokat:\n - Mode: HTTP\n - URL: {webhook_url}\n - Turn on authentication\n - UserID: ``\n\n{secret}\n\nN\u00e9zd meg [a dokument\u00e1ci\u00f3t]({docs_url}) tov\u00e1bbi inform\u00e1ci\u00f3k\u00e9rt." + }, "step": { "user": { "description": "Biztosan be szeretn\u00e9d \u00e1ll\u00edtani az Owntracks-t?", diff --git a/homeassistant/components/point/.translations/ca.json b/homeassistant/components/point/.translations/ca.json index 6298b29f268..6a66735e6d0 100644 --- a/homeassistant/components/point/.translations/ca.json +++ b/homeassistant/components/point/.translations/ca.json @@ -1,29 +1,29 @@ { "config": { "abort": { - "already_setup": "Nom\u00e9s podeu configurar un compte de Point.", + "already_setup": "Nom\u00e9s pots configurar un compte de Point.", "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.", "external_setup": "Point s'ha configurat correctament des d'un altre lloc.", - "no_flows": "Necessiteu configurar Point abans de poder autenticar-vos-hi. [Llegiu les instruccions](https://www.home-assistant.io/components/point/)." + "no_flows": "Necessites configurar Point abans de poder autenticar-t'hi. [Llegiu les instruccions](https://www.home-assistant.io/components/point/)." }, "create_entry": { - "default": "Autenticaci\u00f3 exitosa amb Minut per als vostres dispositiu/s Point." + "default": "Autenticaci\u00f3 exitosa amb Minut per als teus dispositiu/s Point." }, "error": { - "follow_link": "Si us plau seguiu l'enlla\u00e7 i autentiqueu-vos abans de pr\u00e9mer Enviar", + "follow_link": "Si us plau v\u00e9s a l'enlla\u00e7 i autentica't abans de pr\u00e9mer Enviar", "no_token": "No s'ha autenticat amb Minut" }, "step": { "auth": { - "description": "Aneu a l'enlla\u00e7 seg\u00fcent i Accepta l'acc\u00e9s al vostre compte de Minut, despr\u00e9s torneu i premeu Enviar (a sota). \n\n[Enlla\u00e7]({authorization_url})", + "description": "V\u00e9s a l'enlla\u00e7 seg\u00fcent i Accepta l'acc\u00e9s al teu compte de Minut, despr\u00e9s torna i prem Enviar (a sota). \n\n[Enlla\u00e7]({authorization_url})", "title": "Autenticar Point" }, "user": { "data": { "flow_impl": "Prove\u00efdor" }, - "description": "Trieu a trav\u00e9s de quin prove\u00efdor d'autenticaci\u00f3 us voleu autenticar amb Point.", + "description": "Tria a trav\u00e9s de quin prove\u00efdor d'autenticaci\u00f3 vols autenticar-te amb Point.", "title": "Prove\u00efdor d'autenticaci\u00f3" } }, diff --git a/homeassistant/components/point/.translations/hu.json b/homeassistant/components/point/.translations/hu.json new file mode 100644 index 00000000000..2d52069d5ba --- /dev/null +++ b/homeassistant/components/point/.translations/hu.json @@ -0,0 +1,17 @@ +{ + "config": { + "step": { + "auth": { + "title": "Point hiteles\u00edt\u00e9se" + }, + "user": { + "data": { + "flow_impl": "Szolg\u00e1ltat\u00f3" + }, + "description": "V\u00e1laszd ki, hogy melyik hiteles\u00edt\u00e9si szolg\u00e1ltat\u00f3n\u00e1l szeretn\u00e9d hiteles\u00edteni a Pointot.", + "title": "Hiteles\u00edt\u00e9si Szolg\u00e1ltat\u00f3" + } + }, + "title": "Minut Point" + } +} \ No newline at end of file diff --git a/homeassistant/components/rainmachine/.translations/ca.json b/homeassistant/components/rainmachine/.translations/ca.json index 7a1459cff6b..60458f1469e 100644 --- a/homeassistant/components/rainmachine/.translations/ca.json +++ b/homeassistant/components/rainmachine/.translations/ca.json @@ -11,7 +11,7 @@ "password": "Contrasenya", "port": "Port" }, - "title": "Introdu\u00efu la vostra informaci\u00f3" + "title": "Introdueix la teva informaci\u00f3" } }, "title": "RainMachine" diff --git a/homeassistant/components/simplisafe/.translations/ca.json b/homeassistant/components/simplisafe/.translations/ca.json index 1662162c439..a02c3a5e28e 100644 --- a/homeassistant/components/simplisafe/.translations/ca.json +++ b/homeassistant/components/simplisafe/.translations/ca.json @@ -11,7 +11,7 @@ "password": "Contrasenya", "username": "Correu electr\u00f2nic" }, - "title": "Introdu\u00efu la vostra informaci\u00f3" + "title": "Introdueix la teva informaci\u00f3" } }, "title": "SimpliSafe" diff --git a/homeassistant/components/simplisafe/.translations/hu.json b/homeassistant/components/simplisafe/.translations/hu.json index 103bf4e18d0..613b5565470 100644 --- a/homeassistant/components/simplisafe/.translations/hu.json +++ b/homeassistant/components/simplisafe/.translations/hu.json @@ -1,6 +1,7 @@ { "config": { "error": { + "identifier_exists": "Fi\u00f3k m\u00e1r regisztr\u00e1lva van", "invalid_credentials": "\u00c9rv\u00e9nytelen hiteles\u00edt\u0151 adatok" }, "step": { @@ -11,6 +12,7 @@ }, "title": "T\u00f6ltsd ki az adataid" } - } + }, + "title": "SimpliSafe" } } \ No newline at end of file diff --git a/homeassistant/components/smhi/.translations/ca.json b/homeassistant/components/smhi/.translations/ca.json index 23b6a2934f0..e265df40217 100644 --- a/homeassistant/components/smhi/.translations/ca.json +++ b/homeassistant/components/smhi/.translations/ca.json @@ -2,7 +2,7 @@ "config": { "error": { "name_exists": "El nom ja existeix", - "wrong_location": "Ubicaci\u00f3 nom\u00e9s a Su\u00e8cia" + "wrong_location": "La ubicaci\u00f3 ha d'estar a Su\u00e8cia" }, "step": { "user": { diff --git a/homeassistant/components/smhi/.translations/hu.json b/homeassistant/components/smhi/.translations/hu.json index 86fed8933ef..425cf927631 100644 --- a/homeassistant/components/smhi/.translations/hu.json +++ b/homeassistant/components/smhi/.translations/hu.json @@ -1,14 +1,17 @@ { "config": { "error": { - "name_exists": "A n\u00e9v m\u00e1r l\u00e9tezik" + "name_exists": "A n\u00e9v m\u00e1r l\u00e9tezik", + "wrong_location": "Csak sv\u00e9dorsz\u00e1gi helysz\u00edn megengedett" }, "step": { "user": { "data": { "latitude": "Sz\u00e9less\u00e9g", - "longitude": "Hossz\u00fas\u00e1g" - } + "longitude": "Hossz\u00fas\u00e1g", + "name": "N\u00e9v" + }, + "title": "Helysz\u00edn Sv\u00e9dorsz\u00e1gban" } } } diff --git a/homeassistant/components/sonos/.translations/ca.json b/homeassistant/components/sonos/.translations/ca.json index a6f1f99a379..67fd26f1b5a 100644 --- a/homeassistant/components/sonos/.translations/ca.json +++ b/homeassistant/components/sonos/.translations/ca.json @@ -6,7 +6,7 @@ }, "step": { "confirm": { - "description": "Voleu configurar Sonos?", + "description": "Vols configurar Sonos?", "title": "Sonos" } }, diff --git a/homeassistant/components/tradfri/.translations/ca.json b/homeassistant/components/tradfri/.translations/ca.json index acbbb275fc3..22d70092f0d 100644 --- a/homeassistant/components/tradfri/.translations/ca.json +++ b/homeassistant/components/tradfri/.translations/ca.json @@ -4,8 +4,8 @@ "already_configured": "L'enlla\u00e7 ja est\u00e0 configurat" }, "error": { - "cannot_connect": "No es pot connectar amb la passarel\u00b7la d'enlla\u00e7", - "invalid_key": "Ha fallat el registre amb la clau proporcionada. Si aix\u00f2 continua passant, intenteu reiniciar la passarel\u00b7la d'enlla\u00e7.", + "cannot_connect": "No s'ha pogut connectar a la passarel\u00b7la d'enlla\u00e7", + "invalid_key": "Ha fallat el registre amb la clau proporcionada. Si aix\u00f2 continua passant, intenta reiniciar la passarel\u00b7la d'enlla\u00e7.", "timeout": "S'ha acabat el temps d'espera durant la validaci\u00f3 del codi." }, "step": { @@ -14,8 +14,8 @@ "host": "Amfitri\u00f3", "security_code": "Codi de seguretat" }, - "description": "Podeu trobar el codi de seguretat a la part posterior de la vostra passarel\u00b7la d'enlla\u00e7.", - "title": "Introdu\u00efu el codi de seguretat" + "description": "Pots trobar el codi de seguretat a la part posterior de la teva passarel\u00b7la d'enlla\u00e7.", + "title": "Introdueix el codi de seguretat" } }, "title": "IKEA TR\u00c5DFRI" diff --git a/homeassistant/components/tradfri/.translations/hu.json b/homeassistant/components/tradfri/.translations/hu.json index dc7c033d41d..88ff9e6104b 100644 --- a/homeassistant/components/tradfri/.translations/hu.json +++ b/homeassistant/components/tradfri/.translations/hu.json @@ -5,6 +5,7 @@ }, "error": { "cannot_connect": "Nem siker\u00fclt csatlakozni a gatewayhez.", + "invalid_key": "Nem siker\u00fclt regisztr\u00e1lni a megadott kulcs seg\u00edts\u00e9g\u00e9vel. Ha ez t\u00f6bbsz\u00f6r megt\u00f6rt\u00e9nik, pr\u00f3b\u00e1lja meg \u00fajraind\u00edtani a gatewayt.", "timeout": "Id\u0151t\u00fall\u00e9p\u00e9s a k\u00f3d \u00e9rv\u00e9nyes\u00edt\u00e9se sor\u00e1n." }, "step": { diff --git a/homeassistant/components/twilio/.translations/ca.json b/homeassistant/components/twilio/.translations/ca.json index 6f9e22bfd40..324ab0dd69a 100644 --- a/homeassistant/components/twilio/.translations/ca.json +++ b/homeassistant/components/twilio/.translations/ca.json @@ -1,16 +1,16 @@ { "config": { "abort": { - "not_internet_accessible": "La vostra inst\u00e0ncia de Home Assistant ha de ser accessible des d'Internet per rebre missatges de Twilio.", + "not_internet_accessible": "La inst\u00e0ncia de Home Assistant ha de ser accessible des d'Internet per rebre missatges de Twilio.", "one_instance_allowed": "Nom\u00e9s cal una sola inst\u00e0ncia." }, "create_entry": { - "default": "Per enviar esdeveniments a Home Assistant, haureu de configurar [Webhooks amb Twilio]({twilio_url}).\n\n Ompliu la informaci\u00f3 seg\u00fcent: \n\n - URL: `{webhook_url}` \n - M\u00e8tode: POST \n - Tipus de contingut: application/x-www-form-urlencoded\n\nVegeu [la documentaci\u00f3]({docs_url}) sobre com configurar els automatismes per gestionar les dades entrants." + "default": "Per enviar esdeveniments a Home Assistant, haur\u00e0s de configurar [Webhooks amb Twilio]({twilio_url}).\n\nCompleta la seg\u00fcent informaci\u00f3 : \n\n- URL: `{webhook_url}` \n- M\u00e8tode: POST \n- Tipus de contingut: application/x-www-form-urlencoded\n\nConsulta la [documentaci\u00f3]({docs_url}) sobre com configurar els automatismes per gestionar dades entrants." }, "step": { "user": { - "description": "Esteu segur que voleu configurar Twilio?", - "title": "Configureu el Webhook de Twilio" + "description": "Est\u00e0s segur que vols configurar Twilio?", + "title": "Configuraci\u00f3 del Webhook de Twilio" } }, "title": "Twilio" diff --git a/homeassistant/components/twilio/.translations/hu.json b/homeassistant/components/twilio/.translations/hu.json new file mode 100644 index 00000000000..257dd24f082 --- /dev/null +++ b/homeassistant/components/twilio/.translations/hu.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "one_instance_allowed": "Csak egyetlen konfigur\u00e1ci\u00f3 sz\u00fcks\u00e9ges." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/unifi/.translations/ca.json b/homeassistant/components/unifi/.translations/ca.json index 77d859627dc..442d82d9a3f 100644 --- a/homeassistant/components/unifi/.translations/ca.json +++ b/homeassistant/components/unifi/.translations/ca.json @@ -18,7 +18,7 @@ "username": "Nom d'usuari", "verify_ssl": "El controlador est\u00e0 utilitzant un certificat adequat" }, - "title": "Configura el controlador UniFi" + "title": "Configuraci\u00f3 del controlador UniFi" } }, "title": "Controlador UniFi" diff --git a/homeassistant/components/unifi/.translations/hu.json b/homeassistant/components/unifi/.translations/hu.json index 06104c6ed6c..4a664a40c74 100644 --- a/homeassistant/components/unifi/.translations/hu.json +++ b/homeassistant/components/unifi/.translations/hu.json @@ -10,8 +10,10 @@ "step": { "user": { "data": { + "host": "Host", "password": "Jelsz\u00f3", "port": "Port", + "site": "Site azonos\u00edt\u00f3", "username": "Felhaszn\u00e1l\u00f3n\u00e9v" } } diff --git a/homeassistant/components/upnp/.translations/ca.json b/homeassistant/components/upnp/.translations/ca.json index ab09dbc5bda..5f2606a448f 100644 --- a/homeassistant/components/upnp/.translations/ca.json +++ b/homeassistant/components/upnp/.translations/ca.json @@ -13,7 +13,7 @@ "user": { "data": { "enable_port_mapping": "Activa l'assignaci\u00f3 de ports per a Home Assistant", - "enable_sensors": "Afegiu sensors de tr\u00e0nsit", + "enable_sensors": "Afegeix sensors de tr\u00e0nsit", "igd": "UPnP/IGD" }, "title": "Opcions de configuraci\u00f3 per a UPnP/IGD" diff --git a/homeassistant/components/upnp/.translations/hu.json b/homeassistant/components/upnp/.translations/hu.json index fc0225cc534..466c80f9e56 100644 --- a/homeassistant/components/upnp/.translations/hu.json +++ b/homeassistant/components/upnp/.translations/hu.json @@ -1,5 +1,9 @@ { "config": { + "error": { + "one": "hiba", + "other": "" + }, "step": { "init": { "title": "UPnP/IGD" diff --git a/homeassistant/components/zha/.translations/ca.json b/homeassistant/components/zha/.translations/ca.json index 1feac454c45..635d0ecbde2 100644 --- a/homeassistant/components/zha/.translations/ca.json +++ b/homeassistant/components/zha/.translations/ca.json @@ -4,13 +4,13 @@ "single_instance_allowed": "Nom\u00e9s es permet una \u00fanica configuraci\u00f3 de ZHA." }, "error": { - "cannot_connect": "No es pot connectar amb el dispositiu ZHA." + "cannot_connect": "No s'ha pogut connectar amb el dispositiu ZHA." }, "step": { "user": { "data": { "radio_type": "Tipus de r\u00e0dio", - "usb_path": "Ruta del port USB amb el dispositiu" + "usb_path": "Ruta del port USB al dispositiu" }, "title": "ZHA" } diff --git a/homeassistant/components/zha/.translations/hu.json b/homeassistant/components/zha/.translations/hu.json new file mode 100644 index 00000000000..11b2a9fc833 --- /dev/null +++ b/homeassistant/components/zha/.translations/hu.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "Csak egyetlen ZHA konfigur\u00e1ci\u00f3 megengedett." + }, + "error": { + "cannot_connect": "Nem lehet csatlakozni a ZHA eszk\u00f6zh\u00f6z." + }, + "step": { + "user": { + "data": { + "radio_type": "R\u00e1di\u00f3 t\u00edpusa", + "usb_path": "USB eszk\u00f6z el\u00e9r\u00e9si \u00fat" + }, + "title": "ZHA" + } + }, + "title": "ZHA" + } +} \ No newline at end of file diff --git a/homeassistant/components/zone/.translations/ca.json b/homeassistant/components/zone/.translations/ca.json index 1676c8f3906..aa8296b92df 100644 --- a/homeassistant/components/zone/.translations/ca.json +++ b/homeassistant/components/zone/.translations/ca.json @@ -13,7 +13,7 @@ "passive": "Passiu", "radius": "Radi" }, - "title": "Defineix els par\u00e0metres de la zona" + "title": "Definici\u00f3 dels par\u00e0metres de la zona" } }, "title": "Zona" diff --git a/homeassistant/components/zwave/.translations/ca.json b/homeassistant/components/zwave/.translations/ca.json index 7849f34bbf9..bbf303a1f5e 100644 --- a/homeassistant/components/zwave/.translations/ca.json +++ b/homeassistant/components/zwave/.translations/ca.json @@ -5,16 +5,16 @@ "one_instance_only": "El component nom\u00e9s admet una inst\u00e0ncia de Z-Wave" }, "error": { - "option_error": "Ha fallat la validaci\u00f3 de Z-Wave. \u00c9s correcta la ruta al port USB on hi ha la mem\u00f2ria?" + "option_error": "Ha fallat la validaci\u00f3 de Z-Wave. \u00c9s correcta la ruta al port USB on hi ha connectat el dispositiu?" }, "step": { "user": { "data": { - "network_key": "Clau de xarxa (deixeu-ho en blanc per generar-la autom\u00e0ticament)", + "network_key": "Clau de xarxa (deixa-ho en blanc per generar-la autom\u00e0ticament)", "usb_path": "Ruta del port USB" }, - "description": "Consulteu https://www.home-assistant.io/docs/z-wave/installation/ per obtenir informaci\u00f3 sobre les variables de configuraci\u00f3", - "title": "Configureu Z-Wave" + "description": "Consulta https://www.home-assistant.io/docs/z-wave/installation/ per obtenir informaci\u00f3 sobre les variables de configuraci\u00f3", + "title": "Configuraci\u00f3 de Z-Wave" } }, "title": "Z-Wave" diff --git a/homeassistant/components/zwave/.translations/hu.json b/homeassistant/components/zwave/.translations/hu.json index e2acc5f9115..e326c5152a6 100644 --- a/homeassistant/components/zwave/.translations/hu.json +++ b/homeassistant/components/zwave/.translations/hu.json @@ -8,7 +8,9 @@ "data": { "network_key": "H\u00e1l\u00f3zati kulcs (hagyja \u00fcresen az automatikus gener\u00e1l\u00e1shoz)", "usb_path": "USB el\u00e9r\u00e9si \u00fat" - } + }, + "description": "A konfigur\u00e1ci\u00f3s v\u00e1ltoz\u00f3kr\u00f3l az inform\u00e1ci\u00f3kat l\u00e1sd a https://www.home-assistant.io/docs/z-wave/installation/ oldalon.", + "title": "Z-Wave be\u00e1ll\u00edt\u00e1sa" } }, "title": "Z-Wave" From fdea9cb426271c7a4c8b3d738394ce5ee2b976c6 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 10 Dec 2018 12:24:56 +0100 Subject: [PATCH 042/304] Drop OwnTracks bad packets (#19161) --- .../components/owntracks/__init__.py | 22 ++++++++++----- tests/components/owntracks/test_init.py | 27 ++++++++++++++++--- 2 files changed, 38 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/owntracks/__init__.py b/homeassistant/components/owntracks/__init__.py index 0bb7a2390b7..7dc88be9764 100644 --- a/homeassistant/components/owntracks/__init__.py +++ b/homeassistant/components/owntracks/__init__.py @@ -118,9 +118,18 @@ async def async_connect_mqtt(hass, component): async def handle_webhook(hass, webhook_id, request): - """Handle webhook callback.""" + """Handle webhook callback. + + iOS sets the "topic" as part of the payload. + Android does not set a topic but adds headers to the request. + """ context = hass.data[DOMAIN]['context'] - message = await request.json() + + try: + message = await request.json() + except ValueError: + _LOGGER.warning('Received invalid JSON from OwnTracks') + return json_response([]) # Android doesn't populate topic if 'topic' not in message: @@ -129,11 +138,10 @@ async def handle_webhook(hass, webhook_id, request): device = headers.get('X-Limit-D', user) if user is None: - _LOGGER.warning('Set a username in Connection -> Identification') - return json_response( - {'error': 'You need to supply username.'}, - status=400 - ) + _LOGGER.warning('No topic or user found in message. If on Android,' + ' set a username in Connection -> Identification') + # Keep it as a 200 response so the incorrect packet is discarded + return json_response([]) topic_base = re.sub('/#$', '', context.mqtt_topic) message['topic'] = '{}/{}/{}'.format(topic_base, user, device) diff --git a/tests/components/owntracks/test_init.py b/tests/components/owntracks/test_init.py index ba362da905a..3d2d8d03e7c 100644 --- a/tests/components/owntracks/test_init.py +++ b/tests/components/owntracks/test_init.py @@ -110,7 +110,7 @@ def test_handle_value_error(mock_client): @asyncio.coroutine -def test_returns_error_missing_username(mock_client): +def test_returns_error_missing_username(mock_client, caplog): """Test that an error is returned when username is missing.""" resp = yield from mock_client.post( '/api/webhook/owntracks_test', @@ -120,10 +120,29 @@ def test_returns_error_missing_username(mock_client): } ) - assert resp.status == 400 - + # Needs to be 200 or OwnTracks keeps retrying bad packet. + assert resp.status == 200 json = yield from resp.json() - assert json == {'error': 'You need to supply username.'} + assert json == [] + assert 'No topic or user found' in caplog.text + + +@asyncio.coroutine +def test_returns_error_incorrect_json(mock_client, caplog): + """Test that an error is returned when username is missing.""" + resp = yield from mock_client.post( + '/api/webhook/owntracks_test', + data='not json', + headers={ + 'X-Limit-d': 'Pixel', + } + ) + + # Needs to be 200 or OwnTracks keeps retrying bad packet. + assert resp.status == 200 + json = yield from resp.json() + assert json == [] + assert 'invalid JSON' in caplog.text @asyncio.coroutine From da338f2c1ac644734833763cd1cac9920fd14e87 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 10 Dec 2018 12:25:08 +0100 Subject: [PATCH 043/304] Fix lovelace save (#19162) --- homeassistant/components/lovelace/__init__.py | 4 ++-- tests/components/lovelace/test_init.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/lovelace/__init__.py b/homeassistant/components/lovelace/__init__.py index 68c322b3956..e6f122bce19 100644 --- a/homeassistant/components/lovelace/__init__.py +++ b/homeassistant/components/lovelace/__init__.py @@ -101,8 +101,8 @@ class LovelaceStorage: async def async_save(self, config): """Save config.""" - self._data = {'config': config} - await self._store.async_save(config) + self._data['config'] = config + await self._store.async_save(self._data) class LovelaceYAML: diff --git a/tests/components/lovelace/test_init.py b/tests/components/lovelace/test_init.py index ea856b464c3..15548b28cfb 100644 --- a/tests/components/lovelace/test_init.py +++ b/tests/components/lovelace/test_init.py @@ -34,7 +34,7 @@ async def test_lovelace_from_storage(hass, hass_ws_client, hass_storage): response = await client.receive_json() assert response['success'] assert hass_storage[lovelace.STORAGE_KEY]['data'] == { - 'yo': 'hello' + 'config': {'yo': 'hello'} } # Load new config From df2f476c671fe76eba0bd619c60c6071e47f9754 Mon Sep 17 00:00:00 2001 From: Eric Nagley Date: Mon, 10 Dec 2018 06:31:52 -0500 Subject: [PATCH 044/304] Google assistant fix target temp for *F values. (#19083) * home-assistant/home-assistant#18524 : Add rounding to *F temps * home-assistant/home-assistant#18524 : Linting * simplify round behavior * fix trailing whitespace (thanks github editor) --- homeassistant/components/google_assistant/trait.py | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/google_assistant/trait.py b/homeassistant/components/google_assistant/trait.py index f2cb819fcc9..e0776d4c636 100644 --- a/homeassistant/components/google_assistant/trait.py +++ b/homeassistant/components/google_assistant/trait.py @@ -588,8 +588,11 @@ class TemperatureSettingTrait(_Trait): max_temp = self.state.attributes[climate.ATTR_MAX_TEMP] if command == COMMAND_THERMOSTAT_TEMPERATURE_SETPOINT: - temp = temp_util.convert(params['thermostatTemperatureSetpoint'], - TEMP_CELSIUS, unit) + temp = temp_util.convert( + params['thermostatTemperatureSetpoint'], TEMP_CELSIUS, + unit) + if unit == TEMP_FAHRENHEIT: + temp = round(temp) if temp < min_temp or temp > max_temp: raise SmartHomeError( @@ -607,6 +610,8 @@ class TemperatureSettingTrait(_Trait): temp_high = temp_util.convert( params['thermostatTemperatureSetpointHigh'], TEMP_CELSIUS, unit) + if unit == TEMP_FAHRENHEIT: + temp_high = round(temp_high) if temp_high < min_temp or temp_high > max_temp: raise SmartHomeError( @@ -615,7 +620,10 @@ class TemperatureSettingTrait(_Trait): "{} and {}".format(min_temp, max_temp)) temp_low = temp_util.convert( - params['thermostatTemperatureSetpointLow'], TEMP_CELSIUS, unit) + params['thermostatTemperatureSetpointLow'], TEMP_CELSIUS, + unit) + if unit == TEMP_FAHRENHEIT: + temp_low = round(temp_low) if temp_low < min_temp or temp_low > max_temp: raise SmartHomeError( From 3cf8610cb324db6951a443e6c938a0c95583c7f1 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 10 Dec 2018 12:50:09 +0100 Subject: [PATCH 045/304] Updated frontend to 20181210.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 cd592b25993..25bda7091c3 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==20181210.0'] +REQUIREMENTS = ['home-assistant-frontend==20181210.1'] DOMAIN = 'frontend' DEPENDENCIES = ['api', 'websocket_api', 'http', 'system_log', diff --git a/requirements_all.txt b/requirements_all.txt index 5168bc29745..fbbf9590c37 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -493,7 +493,7 @@ hole==0.3.0 holidays==0.9.8 # homeassistant.components.frontend -home-assistant-frontend==20181210.0 +home-assistant-frontend==20181210.1 # homeassistant.components.zwave homeassistant-pyozw==0.1.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index df4ea066991..d8bb60ace9b 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -101,7 +101,7 @@ hdate==0.7.5 holidays==0.9.8 # homeassistant.components.frontend -home-assistant-frontend==20181210.0 +home-assistant-frontend==20181210.1 # homeassistant.components.homematicip_cloud homematicip==0.9.8 From 59581786d380ab8d7f5e9aa06f893032fe454216 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 10 Dec 2018 12:58:51 +0100 Subject: [PATCH 046/304] Add raw service data to event (#19163) --- homeassistant/core.py | 6 ++++-- tests/test_core.py | 27 ++++++++++++++++++++++++++- 2 files changed, 30 insertions(+), 3 deletions(-) diff --git a/homeassistant/core.py b/homeassistant/core.py index 2a40d604ee0..37d1134ef29 100644 --- a/homeassistant/core.py +++ b/homeassistant/core.py @@ -1098,9 +1098,11 @@ class ServiceRegistry: raise ServiceNotFound(domain, service) from None if handler.schema: - service_data = handler.schema(service_data) + processed_data = handler.schema(service_data) + else: + processed_data = service_data - service_call = ServiceCall(domain, service, service_data, context) + service_call = ServiceCall(domain, service, processed_data, context) self._hass.bus.async_fire(EVENT_CALL_SERVICE, { ATTR_DOMAIN: domain.lower(), diff --git a/tests/test_core.py b/tests/test_core.py index 724233cbf98..5ee9f5cdb05 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -8,6 +8,7 @@ from unittest.mock import patch, MagicMock from datetime import datetime, timedelta from tempfile import TemporaryDirectory +import voluptuous as vol import pytz import pytest @@ -21,7 +22,7 @@ from homeassistant.const import ( __version__, EVENT_STATE_CHANGED, ATTR_FRIENDLY_NAME, CONF_UNIT_SYSTEM, ATTR_NOW, EVENT_TIME_CHANGED, EVENT_TIMER_OUT_OF_SYNC, ATTR_SECONDS, EVENT_HOMEASSISTANT_STOP, EVENT_HOMEASSISTANT_CLOSE, - EVENT_SERVICE_REGISTERED, EVENT_SERVICE_REMOVED) + EVENT_SERVICE_REGISTERED, EVENT_SERVICE_REMOVED, EVENT_CALL_SERVICE) from tests.common import get_test_home_assistant, async_mock_service @@ -1000,3 +1001,27 @@ async def test_service_executed_with_subservices(hass): assert len(calls) == 4 assert [call.service for call in calls] == [ 'outer', 'inner', 'inner', 'outer'] + + +async def test_service_call_event_contains_original_data(hass): + """Test that service call event contains original data.""" + events = [] + + @ha.callback + def callback(event): + events.append(event) + + hass.bus.async_listen(EVENT_CALL_SERVICE, callback) + + calls = async_mock_service(hass, 'test', 'service', vol.Schema({ + 'number': vol.Coerce(int) + })) + + await hass.services.async_call('test', 'service', { + 'number': '23' + }, blocking=True) + await hass.async_block_till_done() + assert len(events) == 1 + assert events[0].data['service_data']['number'] == '23' + assert len(calls) == 1 + assert calls[0].data['number'] == 23 From f4f42176bdfce4fbf3d16c0646f607b83c01e0f0 Mon Sep 17 00:00:00 2001 From: "David F. Mulcahey" Date: Mon, 10 Dec 2018 07:59:50 -0800 Subject: [PATCH 047/304] ZHA - Event foundation (#19095) * event foundation * add missing periods to comments * reworked so that entities don't fire events * lint * review comments --- homeassistant/components/binary_sensor/zha.py | 8 +++ homeassistant/components/zha/__init__.py | 33 ++++++++- homeassistant/components/zha/const.py | 7 ++ .../components/zha/entities/entity.py | 5 ++ homeassistant/components/zha/event.py | 69 +++++++++++++++++++ 5 files changed, 121 insertions(+), 1 deletion(-) create mode 100644 homeassistant/components/zha/event.py diff --git a/homeassistant/components/binary_sensor/zha.py b/homeassistant/components/binary_sensor/zha.py index 62c57f0288b..aa1f4eb2f86 100644 --- a/homeassistant/components/binary_sensor/zha.py +++ b/homeassistant/components/binary_sensor/zha.py @@ -190,6 +190,10 @@ class Remote(ZhaEntity, BinarySensorDevice): """Handle ZDO commands on this cluster.""" pass + def zha_send_event(self, cluster, command, args): + """Relay entity events to hass.""" + pass # don't let entities fire events + class LevelListener: """Listener for the LevelControl Zigbee cluster.""" @@ -220,6 +224,10 @@ class Remote(ZhaEntity, BinarySensorDevice): """Handle ZDO commands on this cluster.""" pass + def zha_send_event(self, cluster, command, args): + """Relay entity events to hass.""" + pass # don't let entities fire events + def __init__(self, **kwargs): """Initialize Switch.""" super().__init__(**kwargs) diff --git a/homeassistant/components/zha/__init__.py b/homeassistant/components/zha/__init__.py index fb909b6fedf..41659ae47df 100644 --- a/homeassistant/components/zha/__init__.py +++ b/homeassistant/components/zha/__init__.py @@ -7,6 +7,7 @@ https://home-assistant.io/components/zha/ import collections import logging import os +import types import voluptuous as vol @@ -20,12 +21,14 @@ 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 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) + DEFAULT_RADIO_TYPE, DOMAIN, ZHA_DISCOVERY_NEW, RadioType, + EVENTABLE_CLUSTERS, DATA_ZHA_CORE_EVENTS) REQUIREMENTS = [ 'bellows==0.7.0', @@ -130,6 +133,19 @@ async def async_setup_entry(hass, config_entry): database = config[CONF_DATABASE] else: database = os.path.join(hass.config.config_dir, DEFAULT_DATABASE_NAME) + + # patch zigpy listener to prevent flooding logs with warnings due to + # how zigpy implemented its listeners + from zigpy.appdb import ClusterPersistingListener + + def zha_send_event(self, cluster, command, args): + pass + + ClusterPersistingListener.zha_send_event = types.MethodType( + zha_send_event, + ClusterPersistingListener + ) + APPLICATION_CONTROLLER = ControllerApplication(radio, database) listener = ApplicationListener(hass, config) APPLICATION_CONTROLLER.add_listener(listener) @@ -205,6 +221,9 @@ async def async_unload_entry(hass, config_entry): for entity_id in entity_ids: await component.async_remove_entity(entity_id) + # clean up events + hass.data[DATA_ZHA][DATA_ZHA_CORE_EVENTS].clear() + _LOGGER.debug("Closing zha radio") hass.data[DATA_ZHA][DATA_ZHA_RADIO].close() @@ -221,6 +240,7 @@ class ApplicationListener: self._config = config self._component = EntityComponent(_LOGGER, DOMAIN, hass) self._device_registry = collections.defaultdict(list) + self._events = {} zha_const.populate_data() for component in COMPONENTS: @@ -228,6 +248,7 @@ class ApplicationListener: 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. @@ -256,6 +277,8 @@ class ApplicationListener: """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) async def async_device_initialized(self, device, join): """Handle device joined and basic information discovered (async).""" @@ -362,6 +385,14 @@ class ApplicationListener: 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: []}) + self._events[cluster.endpoint.device.ieee].append(ZhaEvent( + self._hass, + cluster + )) + if cluster.cluster_id in profile_clusters: return diff --git a/homeassistant/components/zha/const.py b/homeassistant/components/zha/const.py index 9efa847b50c..7da6f826c44 100644 --- a/homeassistant/components/zha/const.py +++ b/homeassistant/components/zha/const.py @@ -13,6 +13,7 @@ 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 = [ @@ -53,6 +54,7 @@ SINGLE_INPUT_CLUSTER_DEVICE_CLASS = {} SINGLE_OUTPUT_CLUSTER_DEVICE_CLASS = {} CUSTOM_CLUSTER_MAPPINGS = {} COMPONENT_CLUSTERS = {} +EVENTABLE_CLUSTERS = [] def populate_data(): @@ -70,6 +72,11 @@ def populate_data(): 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', diff --git a/homeassistant/components/zha/entities/entity.py b/homeassistant/components/zha/entities/entity.py index da8f615a665..920c90a4cd1 100644 --- a/homeassistant/components/zha/entities/entity.py +++ b/homeassistant/components/zha/entities/entity.py @@ -103,3 +103,8 @@ class ZhaEntity(entity.Entity): 'name': self._device_state_attributes['friendly_name'], 'via_hub': (DOMAIN, self.hass.data[DATA_ZHA][DATA_ZHA_BRIDGE_ID]), } + + @callback + def zha_send_event(self, cluster, command, args): + """Relay entity events to hass.""" + pass # don't relay events from entities diff --git a/homeassistant/components/zha/event.py b/homeassistant/components/zha/event.py new file mode 100644 index 00000000000..20175dd097f --- /dev/null +++ b/homeassistant/components/zha/event.py @@ -0,0 +1,69 @@ +""" +Event for Zigbee Home Automation. + +For more details about this component, please refer to the documentation at +https://home-assistant.io/components/zha/ +""" +import logging + +from homeassistant.core import EventOrigin, callback +from homeassistant.util import slugify + +_LOGGER = logging.getLogger(__name__) + + +class ZhaEvent(): + """A base class for ZHA events.""" + + def __init__(self, hass, cluster, **kwargs): + """Init ZHA event.""" + self._hass = hass + self._cluster = cluster + cluster.add_listener(self) + ieee = cluster.endpoint.device.ieee + ieeetail = ''.join(['%02x' % (o, ) for o in ieee[-4:]]) + endpoint = cluster.endpoint + if endpoint.manufacturer and endpoint.model is not None: + self._unique_id = "{}.{}_{}_{}_{}{}".format( + 'zha_event', + slugify(endpoint.manufacturer), + slugify(endpoint.model), + ieeetail, + cluster.endpoint.endpoint_id, + kwargs.get('entity_suffix', ''), + ) + else: + self._unique_id = "{}.zha_{}_{}{}".format( + 'zha_event', + ieeetail, + cluster.endpoint.endpoint_id, + kwargs.get('entity_suffix', ''), + ) + + @callback + def attribute_updated(self, attribute, value): + """Handle an attribute updated on this cluster.""" + pass + + @callback + def zdo_command(self, tsn, command_id, args): + """Handle a ZDO command received on this cluster.""" + pass + + @callback + def cluster_command(self, tsn, command_id, args): + """Handle a cluster command received on this cluster.""" + pass + + @callback + def zha_send_event(self, cluster, command, args): + """Relay entity events to hass.""" + self._hass.bus.async_fire( + 'zha_event', + { + 'unique_id': self._unique_id, + 'command': command, + 'args': args + }, + EventOrigin.remote + ) From 92e19f6001566d098b5ed4c570950aad2420e093 Mon Sep 17 00:00:00 2001 From: Fredrik Erlandsson Date: Mon, 10 Dec 2018 18:44:45 +0100 Subject: [PATCH 048/304] TelldusLive config flow (#18758) * update TelldusLive to use config flow * fixes from Martin * Update homeassistant/components/tellduslive/config_flow.py Co-Authored-By: fredrike * revert changes in entry.py * tox tests * tox fixes * woof woof (fix for hound) * lint ignore * unload entry * coverall toxtests * fix some toxtests --- .../components/binary_sensor/tellduslive.py | 31 +- homeassistant/components/cover/tellduslive.py | 28 +- homeassistant/components/discovery.py | 2 +- homeassistant/components/light/tellduslive.py | 29 +- .../components/sensor/tellduslive.py | 28 +- .../components/switch/tellduslive.py | 30 +- .../tellduslive/.translations/en.json | 24 ++ .../components/tellduslive/__init__.py | 320 +++++++----------- .../components/tellduslive/config_flow.py | 150 ++++++++ homeassistant/components/tellduslive/const.py | 29 ++ .../components/tellduslive/strings.json | 24 ++ homeassistant/config_entries.py | 1 + tests/components/tellduslive/__init__.py | 1 + .../tellduslive/test_config_flow.py | 223 ++++++++++++ 14 files changed, 685 insertions(+), 235 deletions(-) create mode 100644 homeassistant/components/tellduslive/.translations/en.json create mode 100644 homeassistant/components/tellduslive/config_flow.py create mode 100644 homeassistant/components/tellduslive/strings.json create mode 100644 tests/components/tellduslive/__init__.py create mode 100644 tests/components/tellduslive/test_config_flow.py diff --git a/homeassistant/components/binary_sensor/tellduslive.py b/homeassistant/components/binary_sensor/tellduslive.py index 7f60e40c68b..f6ed85db132 100644 --- a/homeassistant/components/binary_sensor/tellduslive.py +++ b/homeassistant/components/binary_sensor/tellduslive.py @@ -9,22 +9,35 @@ https://home-assistant.io/components/binary_sensor.tellduslive/ """ import logging -from homeassistant.components import tellduslive +from homeassistant.components import binary_sensor, tellduslive from homeassistant.components.binary_sensor import BinarySensorDevice from homeassistant.components.tellduslive.entry import TelldusLiveEntity +from homeassistant.helpers.dispatcher import async_dispatcher_connect _LOGGER = logging.getLogger(__name__) def setup_platform(hass, config, add_entities, discovery_info=None): - """Set up Tellstick sensors.""" - if discovery_info is None: - return - client = hass.data[tellduslive.DOMAIN] - add_entities( - TelldusLiveSensor(client, binary_sensor) - for binary_sensor in discovery_info - ) + """Old way of setting up TelldusLive. + + Can only be called when a user accidentally mentions the platform in their + config. But even in that case it would have been ignored. + """ + pass + + +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up tellduslive sensors dynamically.""" + async def async_discover_binary_sensor(device_id): + """Discover and add a discovered sensor.""" + client = hass.data[tellduslive.DOMAIN] + async_add_entities([TelldusLiveSensor(client, device_id)]) + + async_dispatcher_connect( + hass, + tellduslive.TELLDUS_DISCOVERY_NEW.format(binary_sensor.DOMAIN, + tellduslive.DOMAIN), + async_discover_binary_sensor) class TelldusLiveSensor(TelldusLiveEntity, BinarySensorDevice): diff --git a/homeassistant/components/cover/tellduslive.py b/homeassistant/components/cover/tellduslive.py index 67affdae04e..1879c88c83c 100644 --- a/homeassistant/components/cover/tellduslive.py +++ b/homeassistant/components/cover/tellduslive.py @@ -8,20 +8,36 @@ https://home-assistant.io/components/cover.tellduslive/ """ import logging -from homeassistant.components import tellduslive +from homeassistant.components import cover, tellduslive from homeassistant.components.cover import CoverDevice from homeassistant.components.tellduslive.entry import TelldusLiveEntity +from homeassistant.helpers.dispatcher import async_dispatcher_connect _LOGGER = logging.getLogger(__name__) def setup_platform(hass, config, add_entities, discovery_info=None): - """Set up the Telldus Live covers.""" - if discovery_info is None: - return + """Old way of setting up TelldusLive. - client = hass.data[tellduslive.DOMAIN] - add_entities(TelldusLiveCover(client, cover) for cover in discovery_info) + Can only be called when a user accidentally mentions the platform in their + config. But even in that case it would have been ignored. + """ + pass + + +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up tellduslive sensors dynamically.""" + async def async_discover_cover(device_id): + """Discover and add a discovered sensor.""" + client = hass.data[tellduslive.DOMAIN] + async_add_entities([TelldusLiveCover(client, device_id)]) + + async_dispatcher_connect( + hass, + tellduslive.TELLDUS_DISCOVERY_NEW.format(cover.DOMAIN, + tellduslive.DOMAIN), + async_discover_cover, + ) class TelldusLiveCover(TelldusLiveEntity, CoverDevice): diff --git a/homeassistant/components/discovery.py b/homeassistant/components/discovery.py index bbf40c73070..00805bd76b8 100644 --- a/homeassistant/components/discovery.py +++ b/homeassistant/components/discovery.py @@ -49,6 +49,7 @@ CONFIG_ENTRY_HANDLERS = { SERVICE_DECONZ: 'deconz', 'google_cast': 'cast', SERVICE_HUE: 'hue', + SERVICE_TELLDUSLIVE: 'tellduslive', SERVICE_IKEA_TRADFRI: 'tradfri', 'sonos': 'sonos', } @@ -62,7 +63,6 @@ SERVICE_HANDLERS = { SERVICE_APPLE_TV: ('apple_tv', None), SERVICE_WINK: ('wink', None), SERVICE_XIAOMI_GW: ('xiaomi_aqara', None), - SERVICE_TELLDUSLIVE: ('tellduslive', None), SERVICE_DAIKIN: ('daikin', None), SERVICE_SABNZBD: ('sabnzbd', None), SERVICE_SAMSUNG_PRINTER: ('sensor', 'syncthru'), diff --git a/homeassistant/components/light/tellduslive.py b/homeassistant/components/light/tellduslive.py index 8601fe3cf1f..3f14b34ea78 100644 --- a/homeassistant/components/light/tellduslive.py +++ b/homeassistant/components/light/tellduslive.py @@ -8,20 +8,37 @@ https://home-assistant.io/components/light.tellduslive/ """ import logging -from homeassistant.components import tellduslive +from homeassistant.components import light, tellduslive from homeassistant.components.light import ( ATTR_BRIGHTNESS, SUPPORT_BRIGHTNESS, Light) from homeassistant.components.tellduslive.entry import TelldusLiveEntity +from homeassistant.helpers.dispatcher import async_dispatcher_connect _LOGGER = logging.getLogger(__name__) def setup_platform(hass, config, add_entities, discovery_info=None): - """Set up the Tellstick Net lights.""" - if discovery_info is None: - return - client = hass.data[tellduslive.DOMAIN] - add_entities(TelldusLiveLight(client, light) for light in discovery_info) + """Old way of setting up TelldusLive. + + Can only be called when a user accidentally mentions the platform in their + config. But even in that case it would have been ignored. + """ + pass + + +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up tellduslive sensors dynamically.""" + async def async_discover_light(device_id): + """Discover and add a discovered sensor.""" + client = hass.data[tellduslive.DOMAIN] + async_add_entities([TelldusLiveLight(client, device_id)]) + + async_dispatcher_connect( + hass, + tellduslive.TELLDUS_DISCOVERY_NEW.format(light.DOMAIN, + tellduslive.DOMAIN), + async_discover_light, + ) class TelldusLiveLight(TelldusLiveEntity, Light): diff --git a/homeassistant/components/sensor/tellduslive.py b/homeassistant/components/sensor/tellduslive.py index 4afff115b9d..7d67dcfb38f 100644 --- a/homeassistant/components/sensor/tellduslive.py +++ b/homeassistant/components/sensor/tellduslive.py @@ -6,11 +6,12 @@ https://home-assistant.io/components/sensor.tellduslive/ """ import logging -from homeassistant.components import tellduslive +from homeassistant.components import sensor, tellduslive from homeassistant.components.tellduslive.entry import TelldusLiveEntity from homeassistant.const import ( DEVICE_CLASS_HUMIDITY, DEVICE_CLASS_ILLUMINANCE, DEVICE_CLASS_TEMPERATURE, TEMP_CELSIUS) +from homeassistant.helpers.dispatcher import async_dispatcher_connect _LOGGER = logging.getLogger(__name__) @@ -46,12 +47,25 @@ SENSOR_TYPES = { def setup_platform(hass, config, add_entities, discovery_info=None): - """Set up the Tellstick sensors.""" - if discovery_info is None: - return - client = hass.data[tellduslive.DOMAIN] - add_entities( - TelldusLiveSensor(client, sensor) for sensor in discovery_info) + """Old way of setting up TelldusLive. + + Can only be called when a user accidentally mentions the platform in their + config. But even in that case it would have been ignored. + """ + pass + + +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up tellduslive sensors dynamically.""" + async def async_discover_sensor(device_id): + """Discover and add a discovered sensor.""" + client = hass.data[tellduslive.DOMAIN] + async_add_entities([TelldusLiveSensor(client, device_id)]) + + async_dispatcher_connect( + hass, + tellduslive.TELLDUS_DISCOVERY_NEW.format( + sensor.DOMAIN, tellduslive.DOMAIN), async_discover_sensor) class TelldusLiveSensor(TelldusLiveEntity): diff --git a/homeassistant/components/switch/tellduslive.py b/homeassistant/components/switch/tellduslive.py index ed4f825f5ac..5c04e872623 100644 --- a/homeassistant/components/switch/tellduslive.py +++ b/homeassistant/components/switch/tellduslive.py @@ -9,20 +9,36 @@ https://home-assistant.io/components/switch.tellduslive/ """ import logging -from homeassistant.components import tellduslive +from homeassistant.components import switch, tellduslive from homeassistant.components.tellduslive.entry import TelldusLiveEntity +from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity import ToggleEntity _LOGGER = logging.getLogger(__name__) def setup_platform(hass, config, add_entities, discovery_info=None): - """Set up Tellstick switches.""" - if discovery_info is None: - return - client = hass.data[tellduslive.DOMAIN] - add_entities( - TelldusLiveSwitch(client, switch) for switch in discovery_info) + """Old way of setting up TelldusLive. + + Can only be called when a user accidentally mentions the platform in their + config. But even in that case it would have been ignored. + """ + pass + + +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up tellduslive sensors dynamically.""" + async def async_discover_switch(device_id): + """Discover and add a discovered sensor.""" + client = hass.data[tellduslive.DOMAIN] + async_add_entities([TelldusLiveSwitch(client, device_id)]) + + async_dispatcher_connect( + hass, + tellduslive.TELLDUS_DISCOVERY_NEW.format(switch.DOMAIN, + tellduslive.DOMAIN), + async_discover_switch, + ) class TelldusLiveSwitch(TelldusLiveEntity, ToggleEntity): diff --git a/homeassistant/components/tellduslive/.translations/en.json b/homeassistant/components/tellduslive/.translations/en.json new file mode 100644 index 00000000000..ef7db098419 --- /dev/null +++ b/homeassistant/components/tellduslive/.translations/en.json @@ -0,0 +1,24 @@ +{ + "config": { + "title": "Telldus Live", + "step": { + "user": { + "title": "Pick endpoint.", + "description": "", + "data": { + "host": "Host" + } + }, + "auth": { + "title": "Authenticate against TelldusLive", + "description": "To link your TelldusLive account:\n 1. Click the link below\n 2. Login to Telldus Live\n 3. Authorize **{app_name}** (click **Yes**).\n 4. Come back here and click **SUBMIT**.\n\n [Link TelldusLive account]({auth_url})" + } + }, + "abort": { + "authorize_url_timeout": "Timeout generating authorize url.", + "authorize_url_fail": "Unknown error generating an authorize url.", + "all_configured": "TelldusLive is already configured", + "unknown": "Unknown error occurred" + } + } + } diff --git a/homeassistant/components/tellduslive/__init__.py b/homeassistant/components/tellduslive/__init__.py index 89e74464489..b17c2cb3c46 100644 --- a/homeassistant/components/tellduslive/__init__.py +++ b/homeassistant/components/tellduslive/__init__.py @@ -4,20 +4,22 @@ Support for Telldus Live. For more details about this component, please refer to the documentation at https://home-assistant.io/components/tellduslive/ """ +import asyncio from datetime import timedelta import logging import voluptuous as vol -from homeassistant.components.discovery import SERVICE_TELLDUSLIVE -from homeassistant.const import CONF_HOST, CONF_TOKEN -from homeassistant.helpers import discovery +from homeassistant import config_entries import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.dispatcher import dispatcher_send -from homeassistant.helpers.event import track_time_interval -from homeassistant.util.json import load_json, save_json +from homeassistant.helpers.dispatcher import async_dispatcher_send +from homeassistant.helpers.event import async_track_time_interval -from .const import DOMAIN, SIGNAL_UPDATE_ENTITY +from . import config_flow # noqa pylint_disable=unused-import +from .const import ( + CONF_HOST, CONF_UPDATE_INTERVAL, 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' @@ -25,229 +27,149 @@ REQUIREMENTS = ['tellduslive==0.10.4'] _LOGGER = logging.getLogger(__name__) -TELLLDUS_CONFIG_FILE = 'tellduslive.conf' -KEY_CONFIG = 'tellduslive_config' +CONFIG_SCHEMA = vol.Schema( + { + DOMAIN: + vol.Schema({ + vol.Optional(CONF_HOST, default=DOMAIN): + cv.string, + vol.Optional(CONF_UPDATE_INTERVAL, default=SCAN_INTERVAL): + (vol.All(cv.time_period, vol.Clamp(min=MIN_UPDATE_INTERVAL))) + }), + }, + extra=vol.ALLOW_EXTRA, +) -CONF_TOKEN_SECRET = 'token_secret' -CONF_UPDATE_INTERVAL = 'update_interval' +DATA_CONFIG_ENTRY_LOCK = 'tellduslive_config_entry_lock' +CONFIG_ENTRY_IS_SETUP = 'telldus_config_entry_is_setup' -PUBLIC_KEY = 'THUPUNECH5YEQA3RE6UYUPRUZ2DUGUGA' -NOT_SO_PRIVATE_KEY = 'PHES7U2RADREWAFEBUSTUBAWRASWUTUS' - -MIN_UPDATE_INTERVAL = timedelta(seconds=5) -DEFAULT_UPDATE_INTERVAL = timedelta(minutes=1) - -CONFIG_SCHEMA = vol.Schema({ - DOMAIN: vol.Schema({ - vol.Optional(CONF_HOST): cv.string, - vol.Optional(CONF_UPDATE_INTERVAL, default=DEFAULT_UPDATE_INTERVAL): ( - vol.All(cv.time_period, vol.Clamp(min=MIN_UPDATE_INTERVAL))) - }), -}, extra=vol.ALLOW_EXTRA) - -CONFIG_INSTRUCTIONS = """ -To link your TelldusLive account: - -1. Click the link below - -2. Login to Telldus Live - -3. Authorize {app_name}. - -4. Click the Confirm button. - -[Link TelldusLive account]({auth_url}) -""" +INTERVAL_TRACKER = '{}_INTERVAL'.format(DOMAIN) -def setup(hass, config, session=None): - """Set up the Telldus Live component.""" - from tellduslive import Session, supports_local_api - config_filename = hass.config.path(TELLLDUS_CONFIG_FILE) - conf = load_json(config_filename) +async def async_setup_entry(hass, entry): + """Create a tellduslive session.""" + from tellduslive import Session + conf = entry.data[KEY_SESSION] - def request_configuration(host=None): - """Request TelldusLive authorization.""" - configurator = hass.components.configurator - hass.data.setdefault(KEY_CONFIG, {}) - data_key = host or DOMAIN - - # Configuration already in progress - if hass.data[KEY_CONFIG].get(data_key): - return - - _LOGGER.info('Configuring TelldusLive %s', - 'local client: {}'.format(host) if host else - 'cloud service') - - session = Session(public_key=PUBLIC_KEY, - private_key=NOT_SO_PRIVATE_KEY, - host=host, - application=APPLICATION_NAME) - - auth_url = session.authorize_url - if not auth_url: - _LOGGER.warning('Failed to retrieve authorization URL') - return - - _LOGGER.debug('Got authorization URL %s', auth_url) - - def configuration_callback(callback_data): - """Handle the submitted configuration.""" - session.authorize() - res = setup(hass, config, session) - if not res: - configurator.notify_errors( - hass.data[KEY_CONFIG].get(data_key), - 'Unable to connect.') - return - - conf.update( - {host: {CONF_HOST: host, - CONF_TOKEN: session.access_token}} if host else - {DOMAIN: {CONF_TOKEN: session.access_token, - CONF_TOKEN_SECRET: session.access_token_secret}}) - save_json(config_filename, conf) - # Close all open configurators: for now, we only support one - # tellstick device, and configuration via either cloud service - # or via local API, not both at the same time - for instance in hass.data[KEY_CONFIG].values(): - configurator.request_done(instance) - - hass.data[KEY_CONFIG][data_key] = \ - configurator.request_config( - 'TelldusLive ({})'.format( - 'LocalAPI' if host - else 'Cloud service'), - configuration_callback, - description=CONFIG_INSTRUCTIONS.format( - app_name=APPLICATION_NAME, - auth_url=auth_url), - submit_caption='Confirm', - entity_picture='/static/images/logo_tellduslive.png', - ) - - def tellstick_discovered(service, info): - """Run when a Tellstick is discovered.""" - _LOGGER.info('Discovered tellstick device') - - if DOMAIN in hass.data: - _LOGGER.debug('Tellstick already configured') - return - - host, device = info[:2] - - if not supports_local_api(device): - _LOGGER.debug('Tellstick does not support local API') - # Configure the cloud service - hass.add_job(request_configuration) - return - - _LOGGER.debug('Tellstick does support local API') - - # Ignore any known devices - if conf and host in conf: - _LOGGER.debug('Discovered already known device: %s', host) - return - - # Offer configuration of both live and local API - request_configuration() - request_configuration(host) - - discovery.listen(hass, SERVICE_TELLDUSLIVE, tellstick_discovered) - - if session: - _LOGGER.debug('Continuing setup configured by configurator') - elif conf and CONF_HOST in next(iter(conf.values())): - # For now, only one local device is supported - _LOGGER.debug('Using Local API pre-configured by configurator') - session = Session(**next(iter(conf.values()))) - elif DOMAIN in conf: - _LOGGER.debug('Using TelldusLive cloud service ' - 'pre-configured by configurator') - session = Session(PUBLIC_KEY, NOT_SO_PRIVATE_KEY, - application=APPLICATION_NAME, **conf[DOMAIN]) - elif config.get(DOMAIN): - _LOGGER.info('Found entry in configuration.yaml. ' - 'Requesting TelldusLive cloud service configuration') - request_configuration() - - if CONF_HOST in config.get(DOMAIN, {}): - _LOGGER.info('Found TelldusLive host entry in configuration.yaml. ' - 'Requesting Telldus Local API configuration') - request_configuration(config.get(DOMAIN).get(CONF_HOST)) - - return True + if KEY_HOST in conf: + session = Session(**conf) else: - _LOGGER.info('Tellstick discovered, awaiting discovery callback') - return True + session = Session( + PUBLIC_KEY, + NOT_SO_PRIVATE_KEY, + application=APPLICATION_NAME, + **conf, + ) if not session.is_authorized: _LOGGER.error('Authentication Error') return False - client = TelldusLiveClient(hass, config, session) + hass.data[DATA_CONFIG_ENTRY_LOCK] = asyncio.Lock() + hass.data[CONFIG_ENTRY_IS_SETUP] = set() + + client = TelldusLiveClient(hass, entry, session) hass.data[DOMAIN] = client - client.update() - interval = config.get(DOMAIN, {}).get(CONF_UPDATE_INTERVAL, - DEFAULT_UPDATE_INTERVAL) + await client.update() + + interval = timedelta(seconds=entry.data[KEY_SCAN_INTERVAL]) _LOGGER.debug('Update interval %s', interval) - track_time_interval(hass, client.update, interval) + hass.data[INTERVAL_TRACKER] = async_track_time_interval( + hass, client.update, interval) return True +async def async_setup(hass, config): + """Set up the Telldus Live component.""" + if DOMAIN not in config: + return True + + hass.async_create_task( + hass.config_entries.flow.async_init( + DOMAIN, + context={'source': config_entries.SOURCE_IMPORT}, + data={ + KEY_HOST: config[DOMAIN].get(CONF_HOST), + KEY_SCAN_INTERVAL: config[DOMAIN].get(CONF_UPDATE_INTERVAL), + })) + return True + + +async def async_unload_entry(hass, config_entry): + """Unload a config entry.""" + interval_tracker = hass.data.pop(INTERVAL_TRACKER) + interval_tracker() + await asyncio.wait([ + hass.config_entries.async_forward_entry_unload(config_entry, component) + for component in hass.data.pop(CONFIG_ENTRY_IS_SETUP) + ]) + del hass.data[DOMAIN] + del hass.data[DATA_CONFIG_ENTRY_LOCK] + return True + + class TelldusLiveClient: """Get the latest data and update the states.""" - def __init__(self, hass, config, session): + def __init__(self, hass, config_entry, session): """Initialize the Tellus data object.""" self._known_devices = set() self._hass = hass - self._config = config + self._config_entry = config_entry self._client = session - def update(self, *args): - """Update local list of devices.""" + @staticmethod + def identify_device(device): + """Find out what type of HA component to create.""" + if device.is_sensor: + return 'sensor' + from tellduslive import (DIM, UP, TURNON) + if device.methods & DIM: + return 'light' + if device.methods & UP: + return 'cover' + if device.methods & TURNON: + return 'switch' + if device.methods == 0: + return 'binary_sensor' + _LOGGER.warning("Unidentified device type (methods: %d)", + device.methods) + return 'switch' + + async def _discover(self, device_id): + """Discover the component.""" + device = self._client.device(device_id) + component = self.identify_device(device) + async with self._hass.data[DATA_CONFIG_ENTRY_LOCK]: + if component not in self._hass.data[CONFIG_ENTRY_IS_SETUP]: + await self._hass.config_entries.async_forward_entry_setup( + self._config_entry, component) + self._hass.data[CONFIG_ENTRY_IS_SETUP].add(component) + device_ids = [] + if device.is_sensor: + for item in device.items: + device_ids.append((device.device_id, item.name, item.scale)) + else: + device_ids.append(device_id) + for _id in device_ids: + async_dispatcher_send( + self._hass, TELLDUS_DISCOVERY_NEW.format(component, DOMAIN), + _id) + + async def update(self, *args): + """Periodically poll the servers for current state.""" _LOGGER.debug('Updating') if not self._client.update(): _LOGGER.warning('Failed request') - def identify_device(device): - """Find out what type of HA component to create.""" - from tellduslive import (DIM, UP, TURNON) - if device.methods & DIM: - return 'light' - if device.methods & UP: - return 'cover' - if device.methods & TURNON: - return 'switch' - if device.methods == 0: - return 'binary_sensor' - _LOGGER.warning( - "Unidentified device type (methods: %d)", device.methods) - return 'switch' - - def discover(device_id, component): - """Discover the component.""" - discovery.load_platform( - self._hass, component, DOMAIN, [device_id], self._config) - - for device in self._client.devices: - if device.device_id in self._known_devices: - continue - if device.is_sensor: - for item in device.items: - discover((device.device_id, item.name, item.scale), - 'sensor') - else: - discover(device.device_id, - identify_device(device)) - self._known_devices.add(device.device_id) - - dispatcher_send(self._hass, SIGNAL_UPDATE_ENTITY) + dev_ids = {dev.device_id for dev in self._client.devices} + new_devices = dev_ids - self._known_devices + await asyncio.gather(*[self._discover(d_id) for d_id in new_devices]) + self._known_devices |= new_devices + async_dispatcher_send(self._hass, SIGNAL_UPDATE_ENTITY) def device(self, device_id): """Return device representation.""" diff --git a/homeassistant/components/tellduslive/config_flow.py b/homeassistant/components/tellduslive/config_flow.py new file mode 100644 index 00000000000..64260b6047c --- /dev/null +++ b/homeassistant/components/tellduslive/config_flow.py @@ -0,0 +1,150 @@ +"""Config flow for Tellduslive.""" +import asyncio +import logging +import os + +import async_timeout +import voluptuous as vol + +from homeassistant import config_entries +from homeassistant.util.json import load_json + +from .const import ( + APPLICATION_NAME, CLOUD_NAME, DOMAIN, KEY_HOST, KEY_SCAN_INTERVAL, + KEY_SESSION, NOT_SO_PRIVATE_KEY, PUBLIC_KEY, SCAN_INTERVAL, + TELLDUS_CONFIG_FILE) + +KEY_TOKEN = 'token' +KEY_TOKEN_SECRET = 'token_secret' + +_LOGGER = logging.getLogger(__name__) + + +@config_entries.HANDLERS.register('tellduslive') +class FlowHandler(config_entries.ConfigFlow): + """Handle a config flow.""" + + VERSION = 1 + CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_POLL + + def __init__(self): + """Init config flow.""" + self._hosts = [CLOUD_NAME] + self._host = None + self._session = None + self._scan_interval = SCAN_INTERVAL + + def _get_auth_url(self): + return self._session.authorize_url + + async def async_step_user(self, user_input=None): + """Let user select host or cloud.""" + if self.hass.config_entries.async_entries(DOMAIN): + return self.async_abort(reason='already_setup') + + if user_input is not None or len(self._hosts) == 1: + if user_input is not None and user_input[KEY_HOST] != CLOUD_NAME: + self._host = user_input[KEY_HOST] + return await self.async_step_auth() + + return self.async_show_form( + step_id='user', + data_schema=vol.Schema({ + vol.Required(KEY_HOST): + vol.In(list(self._hosts)) + })) + + async def async_step_auth(self, user_input=None): + """Handle the submitted configuration.""" + if not self._session: + from tellduslive import Session + self._session = Session( + public_key=PUBLIC_KEY, + private_key=NOT_SO_PRIVATE_KEY, + host=self._host, + application=APPLICATION_NAME, + ) + + if user_input is not None and self._session.authorize(): + host = self._host or CLOUD_NAME + if self._host: + session = { + KEY_HOST: host, + KEY_TOKEN: self._session.access_token + } + else: + session = { + KEY_TOKEN: self._session.access_token, + KEY_TOKEN_SECRET: self._session.access_token_secret + } + return self.async_create_entry( + title=host, data={ + KEY_HOST: host, + KEY_SCAN_INTERVAL: self._scan_interval.seconds, + KEY_SESSION: session, + }) + + try: + with async_timeout.timeout(10): + auth_url = await self.hass.async_add_executor_job( + self._get_auth_url) + except asyncio.TimeoutError: + return self.async_abort(reason='authorize_url_timeout') + except Exception: # pylint: disable=broad-except + _LOGGER.exception("Unexpected error generating auth url") + return self.async_abort(reason='authorize_url_fail') + + _LOGGER.debug('Got authorization URL %s', auth_url) + return self.async_show_form( + step_id='auth', + description_placeholders={ + 'app_name': APPLICATION_NAME, + 'auth_url': auth_url, + }, + ) + + async def async_step_discovery(self, user_input): + """Run when a Tellstick is discovered.""" + from tellduslive import supports_local_api + _LOGGER.info('Discovered tellstick device: %s', user_input) + # Ignore any known devices + for entry in self._async_current_entries(): + if entry.data[KEY_HOST] == user_input[0]: + return self.async_abort(reason='already_configured') + + if not supports_local_api(user_input[1]): + _LOGGER.debug('Tellstick does not support local API') + # Configure the cloud service + return await self.async_step_auth() + + self._hosts.append(user_input[0]) + return await self.async_step_user() + + async def async_step_import(self, user_input): + """Import a config entry.""" + if self.hass.config_entries.async_entries(DOMAIN): + return self.async_abort(reason='already_setup') + + self._scan_interval = user_input[KEY_SCAN_INTERVAL] + if user_input[KEY_HOST] != DOMAIN: + self._hosts.append(user_input[KEY_HOST]) + + if not await self.hass.async_add_executor_job( + os.path.isfile, self.hass.config.path(TELLDUS_CONFIG_FILE)): + return await self.async_step_user() + + conf = await self.hass.async_add_executor_job( + load_json, self.hass.config.path(TELLDUS_CONFIG_FILE)) + host = next(iter(conf)) + + if user_input[KEY_HOST] != host: + return await self.async_step_user() + + host = CLOUD_NAME if host == 'tellduslive' else host + return self.async_create_entry( + title=host, + data={ + KEY_HOST: host, + KEY_SCAN_INTERVAL: self._scan_interval.seconds, + KEY_SESSION: next(iter(conf.values())), + }) diff --git a/homeassistant/components/tellduslive/const.py b/homeassistant/components/tellduslive/const.py index a4ef33af518..81b3abefdee 100644 --- a/homeassistant/components/tellduslive/const.py +++ b/homeassistant/components/tellduslive/const.py @@ -1,5 +1,34 @@ """Consts used by TelldusLive.""" +from datetime import timedelta + +from homeassistant.const import ( # noqa pylint: disable=unused-import + ATTR_BATTERY_LEVEL, CONF_HOST, CONF_TOKEN, DEVICE_DEFAULT_NAME) + +APPLICATION_NAME = 'Home Assistant' DOMAIN = 'tellduslive' +TELLDUS_CONFIG_FILE = 'tellduslive.conf' +KEY_CONFIG = 'tellduslive_config' + SIGNAL_UPDATE_ENTITY = 'tellduslive_update' + +KEY_HOST = 'host' +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' + +MIN_UPDATE_INTERVAL = timedelta(seconds=5) +SCAN_INTERVAL = timedelta(minutes=1) + +ATTR_LAST_UPDATED = 'time_last_updated' + +SIGNAL_UPDATE_ENTITY = 'tellduslive_update' +TELLDUS_DISCOVERY_NEW = 'telldus_new_{}_{}' + +CLOUD_NAME = 'Cloud API' diff --git a/homeassistant/components/tellduslive/strings.json b/homeassistant/components/tellduslive/strings.json new file mode 100644 index 00000000000..7be98213222 --- /dev/null +++ b/homeassistant/components/tellduslive/strings.json @@ -0,0 +1,24 @@ +{ + "config": { + "title": "Telldus Live", + "step": { + "user": { + "title": "Pick endpoint.", + "description": "", + "data": { + "host": "Host" + } + }, + "auth": { + "title": "Authenticate against TelldusLive", + "description": "To link your TelldusLive account:\n 1. Click the link below\n 2. Login to Telldus Live\n 3. Authorize **{app_name}** (click **Yes**).\n 4. Come back here and click **SUBMIT**.\n\n [Link TelldusLive account]({auth_url})" + } + }, + "abort": { + "authorize_url_timeout": "Timeout generating authorize url.", + "authorize_url_fail": "Unknown error generating an authorize url.", + "all_configured": "TelldusLive is already configured", + "unknown": "Unknown error occurred" + } + } +} diff --git a/homeassistant/config_entries.py b/homeassistant/config_entries.py index 5c6ced5756f..39270b36108 100644 --- a/homeassistant/config_entries.py +++ b/homeassistant/config_entries.py @@ -155,6 +155,7 @@ FLOWS = [ 'simplisafe', 'smhi', 'sonos', + 'tellduslive', 'tradfri', 'twilio', 'unifi', diff --git a/tests/components/tellduslive/__init__.py b/tests/components/tellduslive/__init__.py new file mode 100644 index 00000000000..4ed4babc1c8 --- /dev/null +++ b/tests/components/tellduslive/__init__.py @@ -0,0 +1 @@ +"""Tests for the TelldusLive component.""" diff --git a/tests/components/tellduslive/test_config_flow.py b/tests/components/tellduslive/test_config_flow.py new file mode 100644 index 00000000000..ba569a9f149 --- /dev/null +++ b/tests/components/tellduslive/test_config_flow.py @@ -0,0 +1,223 @@ +# flake8: noqa pylint: skip-file +"""Tests for the TelldusLive config flow.""" +import asyncio +from unittest.mock import Mock, patch + +import pytest + +from homeassistant import data_entry_flow +from homeassistant.components.tellduslive import ( + APPLICATION_NAME, DOMAIN, KEY_HOST, KEY_SCAN_INTERVAL, SCAN_INTERVAL, + config_flow) + +from tests.common import MockConfigEntry, MockDependency, mock_coro + + +def init_config_flow(hass, side_effect=None): + """Init a configuration flow.""" + flow = config_flow.FlowHandler() + flow.hass = hass + if side_effect: + flow._get_auth_url = Mock(side_effect=side_effect) + return flow + + +@pytest.fixture +def supports_local_api(): + """Set TelldusLive supports_local_api.""" + return True + + +@pytest.fixture +def authorize(): + """Set TelldusLive authorize.""" + return True + + +@pytest.fixture +def mock_tellduslive(supports_local_api, authorize): + """Mock tellduslive.""" + with MockDependency('tellduslive') as mock_tellduslive_: + mock_tellduslive_.supports_local_api.return_value = supports_local_api + mock_tellduslive_.Session().authorize.return_value = authorize + mock_tellduslive_.Session().access_token = 'token' + mock_tellduslive_.Session().access_token_secret = 'token_secret' + mock_tellduslive_.Session().authorize_url = 'https://example.com' + yield mock_tellduslive_ + + +async def test_abort_if_already_setup(hass): + """Test we abort if TelldusLive is already setup.""" + flow = init_config_flow(hass) + + with patch.object(hass.config_entries, 'async_entries', return_value=[{}]): + result = await flow.async_step_user() + assert result['type'] == data_entry_flow.RESULT_TYPE_ABORT + assert result['reason'] == 'already_setup' + + with patch.object(hass.config_entries, 'async_entries', return_value=[{}]): + result = await flow.async_step_import(None) + assert result['type'] == data_entry_flow.RESULT_TYPE_ABORT + assert result['reason'] == 'already_setup' + + +async def test_full_flow_implementation(hass, mock_tellduslive): + """Test registering an implementation and finishing flow works.""" + flow = init_config_flow(hass) + result = await flow.async_step_discovery(['localhost', 'tellstick']) + assert result['type'] == data_entry_flow.RESULT_TYPE_FORM + assert result['step_id'] == 'user' + + result = await flow.async_step_user() + assert result['type'] == data_entry_flow.RESULT_TYPE_FORM + assert result['step_id'] == 'user' + + result = await flow.async_step_user({'host': 'localhost'}) + assert result['type'] == data_entry_flow.RESULT_TYPE_FORM + assert result['step_id'] == 'auth' + assert result['description_placeholders'] == { + 'auth_url': 'https://example.com', + 'app_name': APPLICATION_NAME, + } + + result = await flow.async_step_auth('') + assert result['type'] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result['title'] == 'localhost' + assert result['data']['host'] == 'localhost' + assert result['data']['scan_interval'] == 60 + assert result['data']['session'] == {'token': 'token', 'host': 'localhost'} + + +async def test_step_import(hass, mock_tellduslive): + """Test that we trigger auth when configuring from import.""" + flow = init_config_flow(hass) + + result = await flow.async_step_import({ + KEY_HOST: DOMAIN, + KEY_SCAN_INTERVAL: 0, + }) + assert result['type'] == data_entry_flow.RESULT_TYPE_FORM + assert result['step_id'] == 'auth' + + +async def test_step_import_add_host(hass, mock_tellduslive): + """Test that we add host and trigger user when configuring from import.""" + flow = init_config_flow(hass) + + result = await flow.async_step_import({ + KEY_HOST: 'localhost', + KEY_SCAN_INTERVAL: 0, + }) + assert result['type'] == data_entry_flow.RESULT_TYPE_FORM + assert result['step_id'] == 'user' + + +async def test_step_import_no_config_file(hass, mock_tellduslive): + """Test that we trigger user with no config_file configuring from import.""" + flow = init_config_flow(hass) + + result = await flow.async_step_import({ KEY_HOST: 'localhost', KEY_SCAN_INTERVAL: 0, }) + assert result['type'] == data_entry_flow.RESULT_TYPE_FORM + assert result['step_id'] == 'user' + + +async def test_step_import_load_json_matching_host(hass, mock_tellduslive): + """Test that we add host and trigger user when configuring from import.""" + flow = init_config_flow(hass) + + with patch('homeassistant.components.tellduslive.config_flow.load_json', + return_value={'tellduslive': {}}), \ + patch('os.path.isfile'): + result = await flow.async_step_import({ KEY_HOST: 'Cloud API', KEY_SCAN_INTERVAL: 0, }) + assert result['type'] == data_entry_flow.RESULT_TYPE_FORM + assert result['step_id'] == 'user' + + +async def test_step_import_load_json(hass, mock_tellduslive): + """Test that we create entry when configuring from import.""" + flow = init_config_flow(hass) + + with patch('homeassistant.components.tellduslive.config_flow.load_json', + return_value={'localhost': {}}), \ + patch('os.path.isfile'): + result = await flow.async_step_import({ KEY_HOST: 'localhost', KEY_SCAN_INTERVAL: SCAN_INTERVAL, }) + assert result['type'] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result['title'] == 'localhost' + assert result['data']['host'] == 'localhost' + assert result['data']['scan_interval'] == 60 + assert result['data']['session'] == {} + + +@pytest.mark.parametrize('supports_local_api', [False]) +async def test_step_disco_no_local_api(hass, mock_tellduslive): + """Test that we trigger when configuring from discovery, not supporting local api.""" + flow = init_config_flow(hass) + + result = await flow.async_step_discovery(['localhost', 'tellstick']) + assert result['type'] == data_entry_flow.RESULT_TYPE_FORM + assert result['step_id'] == 'auth' + + +async def test_step_auth(hass, mock_tellduslive): + """Test that create cloud entity from auth.""" + flow = init_config_flow(hass) + + result = await flow.async_step_auth(['localhost', 'tellstick']) + assert result['type'] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result['title'] == 'Cloud API' + assert result['data']['host'] == 'Cloud API' + assert result['data']['scan_interval'] == 60 + assert result['data']['session'] == { + 'token': 'token', + 'token_secret': 'token_secret', + } + + +@pytest.mark.parametrize('authorize', [False]) +async def test_wrong_auth_flow_implementation(hass, mock_tellduslive): + """Test wrong auth.""" + flow = init_config_flow(hass) + + await flow.async_step_user() + result = await flow.async_step_auth('') + assert result['type'] == data_entry_flow.RESULT_TYPE_FORM + assert result['step_id'] == 'auth' + + +async def test_not_pick_host_if_only_one(hass, mock_tellduslive): + """Test not picking host if we have just one.""" + flow = init_config_flow(hass) + + result = await flow.async_step_user() + assert result['type'] == data_entry_flow.RESULT_TYPE_FORM + assert result['step_id'] == 'auth' + + +async def test_abort_if_timeout_generating_auth_url(hass, mock_tellduslive): + """Test abort if generating authorize url timeout.""" + flow = init_config_flow(hass, side_effect=asyncio.TimeoutError) + + result = await flow.async_step_user() + assert result['type'] == data_entry_flow.RESULT_TYPE_ABORT + assert result['reason'] == 'authorize_url_timeout' + + +async def test_abort_if_exception_generating_auth_url(hass, mock_tellduslive): + """Test we abort if generating authorize url blows up.""" + flow = init_config_flow(hass, side_effect=ValueError) + + result = await flow.async_step_user() + assert result['type'] == data_entry_flow.RESULT_TYPE_ABORT + assert result['reason'] == 'authorize_url_fail' + +async def test_discovery_already_configured(hass, mock_tellduslive): + """Test abort if alredy configured fires from discovery.""" + MockConfigEntry( + domain='tellduslive', + data={'host': 'some-host'} + ).add_to_hass(hass) + flow = init_config_flow(hass) + + result = await flow.async_step_discovery(['some-host', '']) + assert result['type'] == data_entry_flow.RESULT_TYPE_ABORT + assert result['reason'] == 'already_configured' From 557720b094dbee67ca69a39cd38d6fc1dca75885 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 11 Dec 2018 06:50:54 +0100 Subject: [PATCH 049/304] Fix cloud defaults (#19172) --- homeassistant/components/cloud/__init__.py | 3 +-- homeassistant/components/cloud/prefs.py | 8 +++----- tests/components/cloud/test_cloudhooks.py | 2 +- tests/components/cloud/test_iot.py | 2 +- 4 files changed, 6 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/cloud/__init__.py b/homeassistant/components/cloud/__init__.py index 329f83768ce..fd5b413043e 100644 --- a/homeassistant/components/cloud/__init__.py +++ b/homeassistant/components/cloud/__init__.py @@ -252,8 +252,7 @@ class Cloud: return json.loads(file.read()) info = await self.hass.async_add_job(load_config) - - await self.prefs.async_initialize(bool(info)) + await self.prefs.async_initialize() if info is None: return diff --git a/homeassistant/components/cloud/prefs.py b/homeassistant/components/cloud/prefs.py index c4aa43c91d2..32362df2fa9 100644 --- a/homeassistant/components/cloud/prefs.py +++ b/homeassistant/components/cloud/prefs.py @@ -16,19 +16,17 @@ class CloudPreferences: self._store = hass.helpers.storage.Store(STORAGE_VERSION, STORAGE_KEY) self._prefs = None - async def async_initialize(self, logged_in): + async def async_initialize(self): """Finish initializing the preferences.""" prefs = await self._store.async_load() if prefs is None: - # Backwards compat: we enable alexa/google if already logged in prefs = { - PREF_ENABLE_ALEXA: logged_in, - PREF_ENABLE_GOOGLE: logged_in, + PREF_ENABLE_ALEXA: True, + PREF_ENABLE_GOOGLE: True, PREF_GOOGLE_ALLOW_UNLOCK: False, PREF_CLOUDHOOKS: {} } - await self._store.async_save(prefs) self._prefs = prefs diff --git a/tests/components/cloud/test_cloudhooks.py b/tests/components/cloud/test_cloudhooks.py index b65046331a7..9306a6c6ef3 100644 --- a/tests/components/cloud/test_cloudhooks.py +++ b/tests/components/cloud/test_cloudhooks.py @@ -17,7 +17,7 @@ def mock_cloudhooks(hass): cloud.iot = Mock(async_send_message=Mock(return_value=mock_coro())) cloud.cloudhook_create_url = 'https://webhook-create.url' cloud.prefs = prefs.CloudPreferences(hass) - hass.loop.run_until_complete(cloud.prefs.async_initialize(True)) + hass.loop.run_until_complete(cloud.prefs.async_initialize()) return cloudhooks.Cloudhooks(cloud) diff --git a/tests/components/cloud/test_iot.py b/tests/components/cloud/test_iot.py index b11de7da4e4..2133a803aef 100644 --- a/tests/components/cloud/test_iot.py +++ b/tests/components/cloud/test_iot.py @@ -411,7 +411,7 @@ async def test_refresh_token_expired(hass): async def test_webhook_msg(hass): """Test webhook msg.""" cloud = Cloud(hass, MODE_DEV, None, None) - await cloud.prefs.async_initialize(True) + await cloud.prefs.async_initialize() await cloud.prefs.async_update(cloudhooks={ 'hello': { 'webhook_id': 'mock-webhook-id', From ab8cf4f1e43f79746d2beb7a51074b38d299616a Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 11 Dec 2018 10:29:55 +0100 Subject: [PATCH 050/304] Updated frontend to 20181211.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 25bda7091c3..9d4997004f1 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==20181210.1'] +REQUIREMENTS = ['home-assistant-frontend==20181211.0'] DOMAIN = 'frontend' DEPENDENCIES = ['api', 'websocket_api', 'http', 'system_log', diff --git a/requirements_all.txt b/requirements_all.txt index fbbf9590c37..861aa6a60d6 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -493,7 +493,7 @@ hole==0.3.0 holidays==0.9.8 # homeassistant.components.frontend -home-assistant-frontend==20181210.1 +home-assistant-frontend==20181211.0 # homeassistant.components.zwave homeassistant-pyozw==0.1.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index d8bb60ace9b..129a7910b0a 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -101,7 +101,7 @@ hdate==0.7.5 holidays==0.9.8 # homeassistant.components.frontend -home-assistant-frontend==20181210.1 +home-assistant-frontend==20181211.0 # homeassistant.components.homematicip_cloud homematicip==0.9.8 From eb4a44535c3d29c71b36f8f7c2e8c7f36ef005e1 Mon Sep 17 00:00:00 2001 From: Jonathan Keljo Date: Tue, 11 Dec 2018 02:34:03 -0800 Subject: [PATCH 051/304] Enable alarmdecoder to see open/close state of bypassed RF zones when armed (#18477) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Enable alarmdecoder to see open/close state of bypassed zones when armed The alarmdecoder component already reported RF state bits as attributes. If the user knows which loop is set up for the zone in the alarm panel, they can use that information to tell whether the zone is open or closed even when the system is armed by monitoring the appropriate attribute. That’s awkward, so this commit enables the user to simply configure which loop is used and the component will update the state itself. * Simplify, also it's more correct to treat it as a state change rather than a permanent state, since it's possible the decoder might miss some events. * Remove relative import --- homeassistant/components/alarmdecoder.py | 3 +++ .../components/binary_sensor/alarmdecoder.py | 11 ++++++++--- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/alarmdecoder.py b/homeassistant/components/alarmdecoder.py index 1377b2a6c3a..92eab728210 100644 --- a/homeassistant/components/alarmdecoder.py +++ b/homeassistant/components/alarmdecoder.py @@ -32,6 +32,7 @@ CONF_DEVICE_TYPE = 'type' CONF_PANEL_DISPLAY = 'panel_display' CONF_ZONE_NAME = 'name' CONF_ZONE_TYPE = 'type' +CONF_ZONE_LOOP = 'loop' CONF_ZONE_RFID = 'rfid' CONF_ZONES = 'zones' CONF_RELAY_ADDR = 'relayaddr' @@ -75,6 +76,8 @@ ZONE_SCHEMA = vol.Schema({ vol.Optional(CONF_ZONE_TYPE, default=DEFAULT_ZONE_TYPE): vol.Any(DEVICE_CLASSES_SCHEMA), vol.Optional(CONF_ZONE_RFID): cv.string, + vol.Optional(CONF_ZONE_LOOP): + vol.All(vol.Coerce(int), vol.Range(min=1, max=4)), vol.Inclusive(CONF_RELAY_ADDR, 'relaylocation', 'Relay address and channel must exist together'): cv.byte, vol.Inclusive(CONF_RELAY_CHAN, 'relaylocation', diff --git a/homeassistant/components/binary_sensor/alarmdecoder.py b/homeassistant/components/binary_sensor/alarmdecoder.py index f7a42e9b831..d8fddeaa540 100644 --- a/homeassistant/components/binary_sensor/alarmdecoder.py +++ b/homeassistant/components/binary_sensor/alarmdecoder.py @@ -9,7 +9,7 @@ import logging from homeassistant.components.binary_sensor import BinarySensorDevice from homeassistant.components.alarmdecoder import ( ZONE_SCHEMA, CONF_ZONES, CONF_ZONE_NAME, CONF_ZONE_TYPE, - CONF_ZONE_RFID, SIGNAL_ZONE_FAULT, SIGNAL_ZONE_RESTORE, + CONF_ZONE_RFID, CONF_ZONE_LOOP, SIGNAL_ZONE_FAULT, SIGNAL_ZONE_RESTORE, SIGNAL_RFX_MESSAGE, SIGNAL_REL_MESSAGE, CONF_RELAY_ADDR, CONF_RELAY_CHAN) @@ -37,10 +37,12 @@ def setup_platform(hass, config, add_entities, discovery_info=None): zone_type = device_config_data[CONF_ZONE_TYPE] zone_name = device_config_data[CONF_ZONE_NAME] zone_rfid = device_config_data.get(CONF_ZONE_RFID) + zone_loop = device_config_data.get(CONF_ZONE_LOOP) relay_addr = device_config_data.get(CONF_RELAY_ADDR) relay_chan = device_config_data.get(CONF_RELAY_CHAN) device = AlarmDecoderBinarySensor( - zone_num, zone_name, zone_type, zone_rfid, relay_addr, relay_chan) + zone_num, zone_name, zone_type, zone_rfid, zone_loop, relay_addr, + relay_chan) devices.append(device) add_entities(devices) @@ -51,7 +53,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): class AlarmDecoderBinarySensor(BinarySensorDevice): """Representation of an AlarmDecoder binary sensor.""" - def __init__(self, zone_number, zone_name, zone_type, zone_rfid, + def __init__(self, zone_number, zone_name, zone_type, zone_rfid, zone_loop, relay_addr, relay_chan): """Initialize the binary_sensor.""" self._zone_number = zone_number @@ -59,6 +61,7 @@ class AlarmDecoderBinarySensor(BinarySensorDevice): self._state = None self._name = zone_name self._rfid = zone_rfid + self._loop = zone_loop self._rfstate = None self._relay_addr = relay_addr self._relay_chan = relay_chan @@ -128,6 +131,8 @@ class AlarmDecoderBinarySensor(BinarySensorDevice): """Update RF state.""" if self._rfid and message and message.serial_number == self._rfid: self._rfstate = message.value + if self._loop: + self._state = 1 if message.loop[self._loop - 1] else 0 self.schedule_update_ha_state() def _rel_message_callback(self, message): From 3e7b908a61323de8819086ddf11a989df52c4ea6 Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Tue, 11 Dec 2018 14:12:27 +0100 Subject: [PATCH 052/304] Add SCAN_INTERVAL (#19186) * Add SCAN_INTERVAL * More clean-up --- homeassistant/components/weather/__init__.py | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/weather/__init__.py b/homeassistant/components/weather/__init__.py index 725c7f609a7..b9cb300ee21 100644 --- a/homeassistant/components/weather/__init__.py +++ b/homeassistant/components/weather/__init__.py @@ -4,6 +4,7 @@ Weather component that handles meteorological data for your location. For more details about this component, please refer to the documentation at https://home-assistant.io/components/weather/ """ +from datetime import timedelta import logging from homeassistant.helpers.entity_component import EntityComponent @@ -14,11 +15,6 @@ from homeassistant.helpers.entity import Entity _LOGGER = logging.getLogger(__name__) -DEPENDENCIES = [] -DOMAIN = 'weather' - -ENTITY_ID_FORMAT = DOMAIN + '.{}' - ATTR_CONDITION_CLASS = 'condition_class' ATTR_FORECAST = 'forecast' ATTR_FORECAST_CONDITION = 'condition' @@ -26,8 +22,8 @@ ATTR_FORECAST_PRECIPITATION = 'precipitation' ATTR_FORECAST_TEMP = 'temperature' ATTR_FORECAST_TEMP_LOW = 'templow' ATTR_FORECAST_TIME = 'datetime' -ATTR_FORECAST_WIND_SPEED = 'wind_speed' ATTR_FORECAST_WIND_BEARING = 'wind_bearing' +ATTR_FORECAST_WIND_SPEED = 'wind_speed' ATTR_WEATHER_ATTRIBUTION = 'attribution' ATTR_WEATHER_HUMIDITY = 'humidity' ATTR_WEATHER_OZONE = 'ozone' @@ -37,10 +33,17 @@ ATTR_WEATHER_VISIBILITY = 'visibility' ATTR_WEATHER_WIND_BEARING = 'wind_bearing' ATTR_WEATHER_WIND_SPEED = 'wind_speed' +DOMAIN = 'weather' + +ENTITY_ID_FORMAT = DOMAIN + '.{}' + +SCAN_INTERVAL = timedelta(seconds=30) + async def async_setup(hass, config): """Set up the weather component.""" - component = hass.data[DOMAIN] = EntityComponent(_LOGGER, DOMAIN, hass) + component = hass.data[DOMAIN] = EntityComponent( + _LOGGER, DOMAIN, hass, SCAN_INTERVAL) await component.async_setup(config) return True From 1f8156e26c2d9c6b604946a7903ee9c0316adbb5 Mon Sep 17 00:00:00 2001 From: Erik Date: Tue, 11 Dec 2018 17:05:49 +0100 Subject: [PATCH 053/304] Fail if new entity_id is in hass.states --- homeassistant/components/config/entity_registry.py | 2 +- homeassistant/helpers/entity_registry.py | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/config/entity_registry.py b/homeassistant/components/config/entity_registry.py index 1ede76d0fd8..15eed32a8cd 100644 --- a/homeassistant/components/config/entity_registry.py +++ b/homeassistant/components/config/entity_registry.py @@ -88,7 +88,7 @@ async def websocket_get_entity(hass, connection, msg): @async_response async def websocket_update_entity(hass, connection, msg): - """Handle get camera thumbnail websocket command. + """Handle update entity websocket command. Async friendly. """ diff --git a/homeassistant/helpers/entity_registry.py b/homeassistant/helpers/entity_registry.py index 57c8bcf0af8..fdd9f178321 100644 --- a/homeassistant/helpers/entity_registry.py +++ b/homeassistant/helpers/entity_registry.py @@ -171,7 +171,9 @@ class EntityRegistry: changes['device_id'] = device_id if new_entity_id is not _UNDEF and new_entity_id != old.entity_id: - if self.async_is_registered(new_entity_id): + if (self.async_is_registered(new_entity_id) or new_entity_id in + self.hass.states.async_entity_ids( + split_entity_id(entity_id)[0])): raise ValueError('Entity is already registered') if not valid_entity_id(new_entity_id): From 61ca9bb8e439513c5068aff3c414f888dd2a156c Mon Sep 17 00:00:00 2001 From: javicalle <31999997+javicalle@users.noreply.github.com> Date: Tue, 11 Dec 2018 17:20:30 +0100 Subject: [PATCH 054/304] Restore states for RFLink devices (#18816) * Merge branch 'master' of https://github.com/home-assistant/home-assistant into dev # Conflicts: # homeassistant/components/binary_sensor/point.py # homeassistant/components/cloud/__init__.py # homeassistant/components/cloud/prefs.py # homeassistant/components/frontend/__init__.py # homeassistant/components/light/fibaro.py # homeassistant/components/logbook.py # homeassistant/components/point/__init__.py # homeassistant/config_entries.py # homeassistant/const.py # homeassistant/helpers/service.py # requirements_all.txt # requirements_test_all.txt * one 'async_get_last_state' refactor left behind * Remove RestoreEntity inheritance (already in parent class) * # pylint: disable=too-many-ancestors * code predictor can be a bitch * lint corrections * # pylint: disable=too-many-ancestors * recover from dict[key] * Remove all 'coroutine' decorator, replace for 'async def' Replace all 'yield from' for 'await' Replace 'hass.async_add_job' for 'hass.async_create_task' --- homeassistant/components/cover/rflink.py | 14 +- homeassistant/components/light/rflink.py | 24 ++ homeassistant/components/rflink.py | 14 +- homeassistant/components/switch/rflink.py | 1 + tests/components/cover/test_rflink.py | 497 ++++++++++++++++++++++ tests/components/light/test_rflink.py | 70 ++- tests/components/switch/test_rflink.py | 45 +- 7 files changed, 649 insertions(+), 16 deletions(-) create mode 100644 tests/components/cover/test_rflink.py diff --git a/homeassistant/components/cover/rflink.py b/homeassistant/components/cover/rflink.py index 353cccc7d4f..cdc7cac3adb 100644 --- a/homeassistant/components/cover/rflink.py +++ b/homeassistant/components/cover/rflink.py @@ -15,8 +15,8 @@ from homeassistant.components.rflink import ( from homeassistant.components.cover import ( CoverDevice, PLATFORM_SCHEMA) import homeassistant.helpers.config_validation as cv -from homeassistant.const import CONF_NAME - +from homeassistant.helpers.restore_state import RestoreEntity +from homeassistant.const import CONF_NAME, STATE_OPEN DEPENDENCIES = ['rflink'] @@ -60,9 +60,17 @@ async def async_setup_platform(hass, config, async_add_entities, async_add_entities(devices_from_config(config)) -class RflinkCover(RflinkCommand, CoverDevice): +class RflinkCover(RflinkCommand, CoverDevice, RestoreEntity): """Rflink entity which can switch on/stop/off (eg: cover).""" + async def async_added_to_hass(self): + """Restore RFLink cover state (OPEN/CLOSE).""" + await super().async_added_to_hass() + + old_state = await self.async_get_last_state() + if old_state is not None: + self._state = old_state.state == STATE_OPEN + def _handle_event(self, event): """Adjust state if Rflink picks up a remote command for this device.""" self.cancel_queued_send_commands() diff --git a/homeassistant/components/light/rflink.py b/homeassistant/components/light/rflink.py index 3b60280c582..ef389bb84f9 100644 --- a/homeassistant/components/light/rflink.py +++ b/homeassistant/components/light/rflink.py @@ -148,17 +148,29 @@ async def async_setup_platform(hass, config, async_add_entities, hass.data[DATA_DEVICE_REGISTER][EVENT_KEY_COMMAND] = add_new_device +# pylint: disable=too-many-ancestors class RflinkLight(SwitchableRflinkDevice, Light): """Representation of a Rflink light.""" pass +# pylint: disable=too-many-ancestors class DimmableRflinkLight(SwitchableRflinkDevice, Light): """Rflink light device that support dimming.""" _brightness = 255 + async def async_added_to_hass(self): + """Restore RFLink light brightness attribute.""" + await super().async_added_to_hass() + + old_state = await self.async_get_last_state() + if old_state is not None and \ + old_state.attributes.get(ATTR_BRIGHTNESS) is not None: + # restore also brightness in dimmables devices + self._brightness = int(old_state.attributes[ATTR_BRIGHTNESS]) + async def async_turn_on(self, **kwargs): """Turn the device on.""" if ATTR_BRIGHTNESS in kwargs: @@ -179,6 +191,7 @@ class DimmableRflinkLight(SwitchableRflinkDevice, Light): return SUPPORT_BRIGHTNESS +# pylint: disable=too-many-ancestors class HybridRflinkLight(SwitchableRflinkDevice, Light): """Rflink light device that sends out both dim and on/off commands. @@ -196,6 +209,16 @@ class HybridRflinkLight(SwitchableRflinkDevice, Light): _brightness = 255 + async def async_added_to_hass(self): + """Restore RFLink light brightness attribute.""" + await super().async_added_to_hass() + + old_state = await self.async_get_last_state() + if old_state is not None and \ + old_state.attributes.get(ATTR_BRIGHTNESS) is not None: + # restore also brightness in dimmables devices + self._brightness = int(old_state.attributes[ATTR_BRIGHTNESS]) + async def async_turn_on(self, **kwargs): """Turn the device on and set dim level.""" if ATTR_BRIGHTNESS in kwargs: @@ -222,6 +245,7 @@ class HybridRflinkLight(SwitchableRflinkDevice, Light): return SUPPORT_BRIGHTNESS +# pylint: disable=too-many-ancestors class ToggleRflinkLight(SwitchableRflinkDevice, Light): """Rflink light device which sends out only 'on' commands. diff --git a/homeassistant/components/rflink.py b/homeassistant/components/rflink.py index b3c58da1076..89392b8565f 100644 --- a/homeassistant/components/rflink.py +++ b/homeassistant/components/rflink.py @@ -13,7 +13,7 @@ import voluptuous as vol from homeassistant.const import ( ATTR_ENTITY_ID, CONF_COMMAND, CONF_HOST, CONF_PORT, - EVENT_HOMEASSISTANT_STOP) + STATE_ON, EVENT_HOMEASSISTANT_STOP) from homeassistant.core import CoreState, callback from homeassistant.exceptions import HomeAssistantError import homeassistant.helpers.config_validation as cv @@ -21,7 +21,7 @@ from homeassistant.helpers.deprecation import get_deprecated from homeassistant.helpers.entity import Entity from homeassistant.helpers.dispatcher import ( async_dispatcher_send, async_dispatcher_connect) - +from homeassistant.helpers.restore_state import RestoreEntity REQUIREMENTS = ['rflink==0.0.37'] @@ -499,9 +499,17 @@ class RflinkCommand(RflinkDevice): self._async_send_command(cmd, repetitions - 1)) -class SwitchableRflinkDevice(RflinkCommand): +class SwitchableRflinkDevice(RflinkCommand, RestoreEntity): """Rflink entity which can switch on/off (eg: light, switch).""" + async def async_added_to_hass(self): + """Restore RFLink device state (ON/OFF).""" + await super().async_added_to_hass() + + old_state = await self.async_get_last_state() + if old_state is not None: + self._state = old_state.state == STATE_ON + def _handle_event(self, event): """Adjust state if Rflink picks up a remote command for this device.""" self.cancel_queued_send_commands() diff --git a/homeassistant/components/switch/rflink.py b/homeassistant/components/switch/rflink.py index 51bf5543584..25e4e367fdf 100644 --- a/homeassistant/components/switch/rflink.py +++ b/homeassistant/components/switch/rflink.py @@ -67,6 +67,7 @@ async def async_setup_platform(hass, config, async_add_entities, async_add_entities(devices_from_config(config)) +# pylint: disable=too-many-ancestors class RflinkSwitch(SwitchableRflinkDevice, SwitchDevice): """Representation of a Rflink switch.""" diff --git a/tests/components/cover/test_rflink.py b/tests/components/cover/test_rflink.py new file mode 100644 index 00000000000..4f88d24d97f --- /dev/null +++ b/tests/components/cover/test_rflink.py @@ -0,0 +1,497 @@ +"""Test for RFLink cover components. + +Test setup of RFLink covers component/platform. State tracking and +control of RFLink cover devices. + +""" + +import logging + +from homeassistant.components.rflink import EVENT_BUTTON_PRESSED +from homeassistant.const import ( + SERVICE_OPEN_COVER, SERVICE_CLOSE_COVER, + STATE_OPEN, STATE_CLOSED, ATTR_ENTITY_ID) +from homeassistant.core import callback, State, CoreState + +from tests.common import mock_restore_cache +from ..test_rflink import mock_rflink + +DOMAIN = 'cover' + +CONFIG = { + 'rflink': { + 'port': '/dev/ttyABC0', + 'ignore_devices': ['ignore_wildcard_*', 'ignore_cover'], + }, + DOMAIN: { + 'platform': 'rflink', + 'devices': { + 'protocol_0_0': { + 'name': 'test', + 'aliases': ['test_alias_0_0'], + }, + 'cover_0_0': { + 'name': 'dim_test', + }, + 'cover_0_1': { + 'name': 'cover_test', + } + }, + }, +} + +_LOGGER = logging.getLogger(__name__) + + +async def test_default_setup(hass, monkeypatch): + """Test all basic functionality of the RFLink cover component.""" + # setup mocking rflink module + event_callback, create, protocol, _ = await mock_rflink( + hass, CONFIG, DOMAIN, monkeypatch) + + # make sure arguments are passed + assert create.call_args_list[0][1]['ignore'] + + # test default state of cover loaded from config + cover_initial = hass.states.get(DOMAIN + '.test') + assert cover_initial.state == STATE_CLOSED + assert cover_initial.attributes['assumed_state'] + + # cover should follow state of the hardware device by interpreting + # incoming events for its name and aliases + + # mock incoming command event for this device + event_callback({ + 'id': 'protocol_0_0', + 'command': 'up', + }) + await hass.async_block_till_done() + + cover_after_first_command = hass.states.get(DOMAIN + '.test') + assert cover_after_first_command.state == STATE_OPEN + # not sure why, but cover have always assumed_state=true + assert cover_after_first_command.attributes.get('assumed_state') + + # mock incoming command event for this device + event_callback({ + 'id': 'protocol_0_0', + 'command': 'down', + }) + await hass.async_block_till_done() + + assert hass.states.get(DOMAIN + '.test').state == STATE_CLOSED + + # should respond to group command + event_callback({ + 'id': 'protocol_0_0', + 'command': 'allon', + }) + await hass.async_block_till_done() + + cover_after_first_command = hass.states.get(DOMAIN + '.test') + assert cover_after_first_command.state == STATE_OPEN + + # should respond to group command + event_callback({ + 'id': 'protocol_0_0', + 'command': 'alloff', + }) + await hass.async_block_till_done() + + assert hass.states.get(DOMAIN + '.test').state == STATE_CLOSED + + # test following aliases + # mock incoming command event for this device alias + event_callback({ + 'id': 'test_alias_0_0', + 'command': 'up', + }) + await hass.async_block_till_done() + + assert hass.states.get(DOMAIN + '.test').state == STATE_OPEN + + # test changing state from HA propagates to RFLink + hass.async_create_task( + hass.services.async_call(DOMAIN, SERVICE_CLOSE_COVER, + {ATTR_ENTITY_ID: DOMAIN + '.test'})) + await hass.async_block_till_done() + assert hass.states.get(DOMAIN + '.test').state == STATE_CLOSED + assert protocol.send_command_ack.call_args_list[0][0][0] == 'protocol_0_0' + assert protocol.send_command_ack.call_args_list[0][0][1] == 'DOWN' + + hass.async_create_task( + hass.services.async_call(DOMAIN, SERVICE_OPEN_COVER, + {ATTR_ENTITY_ID: DOMAIN + '.test'})) + await hass.async_block_till_done() + assert hass.states.get(DOMAIN + '.test').state == STATE_OPEN + assert protocol.send_command_ack.call_args_list[1][0][1] == 'UP' + + +async def test_firing_bus_event(hass, monkeypatch): + """Incoming RFLink command events should be put on the HA event bus.""" + config = { + 'rflink': { + 'port': '/dev/ttyABC0', + }, + DOMAIN: { + 'platform': 'rflink', + 'devices': { + 'protocol_0_0': { + 'name': 'test', + 'aliases': ['test_alias_0_0'], + 'fire_event': True, + }, + }, + }, + } + + # setup mocking rflink module + event_callback, _, _, _ = await mock_rflink( + hass, config, DOMAIN, monkeypatch) + + calls = [] + + @callback + def listener(event): + calls.append(event) + hass.bus.async_listen_once(EVENT_BUTTON_PRESSED, listener) + + # test event for new unconfigured sensor + event_callback({ + 'id': 'protocol_0_0', + 'command': 'down', + }) + await hass.async_block_till_done() + + assert calls[0].data == {'state': 'down', 'entity_id': DOMAIN + '.test'} + + +async def test_signal_repetitions(hass, monkeypatch): + """Command should be sent amount of configured repetitions.""" + config = { + 'rflink': { + 'port': '/dev/ttyABC0', + }, + DOMAIN: { + 'platform': 'rflink', + 'device_defaults': { + 'signal_repetitions': 3, + }, + 'devices': { + 'protocol_0_0': { + 'name': 'test', + 'signal_repetitions': 2, + }, + 'protocol_0_1': { + 'name': 'test1', + }, + }, + }, + } + + # setup mocking rflink module + _, _, protocol, _ = await mock_rflink(hass, config, DOMAIN, monkeypatch) + + # test if signal repetition is performed according to configuration + hass.async_create_task( + hass.services.async_call(DOMAIN, SERVICE_OPEN_COVER, + {ATTR_ENTITY_ID: DOMAIN + '.test'})) + + # wait for commands and repetitions to finish + await hass.async_block_till_done() + + assert protocol.send_command_ack.call_count == 2 + + # test if default apply to configured devices + hass.async_create_task( + hass.services.async_call(DOMAIN, SERVICE_OPEN_COVER, + {ATTR_ENTITY_ID: DOMAIN + '.test1'})) + + # wait for commands and repetitions to finish + await hass.async_block_till_done() + + assert protocol.send_command_ack.call_count == 5 + + +async def test_signal_repetitions_alternation(hass, monkeypatch): + """Simultaneously switching entities must alternate repetitions.""" + config = { + 'rflink': { + 'port': '/dev/ttyABC0', + }, + DOMAIN: { + 'platform': 'rflink', + 'devices': { + 'protocol_0_0': { + 'name': 'test', + 'signal_repetitions': 2, + }, + 'protocol_0_1': { + 'name': 'test1', + 'signal_repetitions': 2, + }, + }, + }, + } + + # setup mocking rflink module + _, _, protocol, _ = await mock_rflink( + hass, config, DOMAIN, monkeypatch) + + hass.async_create_task( + hass.services.async_call(DOMAIN, SERVICE_CLOSE_COVER, + {ATTR_ENTITY_ID: DOMAIN + '.test'})) + hass.async_create_task( + hass.services.async_call(DOMAIN, SERVICE_CLOSE_COVER, + {ATTR_ENTITY_ID: DOMAIN + '.test1'})) + + await hass.async_block_till_done() + + assert protocol.send_command_ack.call_args_list[0][0][0] == 'protocol_0_0' + assert protocol.send_command_ack.call_args_list[1][0][0] == 'protocol_0_1' + assert protocol.send_command_ack.call_args_list[2][0][0] == 'protocol_0_0' + assert protocol.send_command_ack.call_args_list[3][0][0] == 'protocol_0_1' + + +async def test_signal_repetitions_cancelling(hass, monkeypatch): + """Cancel outstanding repetitions when state changed.""" + config = { + 'rflink': { + 'port': '/dev/ttyABC0', + }, + DOMAIN: { + 'platform': 'rflink', + 'devices': { + 'protocol_0_0': { + 'name': 'test', + 'signal_repetitions': 3, + }, + }, + }, + } + + # setup mocking rflink module + _, _, protocol, _ = await mock_rflink( + hass, config, DOMAIN, monkeypatch) + + hass.async_create_task( + hass.services.async_call(DOMAIN, SERVICE_CLOSE_COVER, + {ATTR_ENTITY_ID: DOMAIN + '.test'})) + + hass.async_create_task( + hass.services.async_call(DOMAIN, SERVICE_OPEN_COVER, + {ATTR_ENTITY_ID: DOMAIN + '.test'})) + + await hass.async_block_till_done() + + assert protocol.send_command_ack.call_args_list[0][0][1] == 'DOWN' + assert protocol.send_command_ack.call_args_list[1][0][1] == 'UP' + assert protocol.send_command_ack.call_args_list[2][0][1] == 'UP' + assert protocol.send_command_ack.call_args_list[3][0][1] == 'UP' + + +async def test_group_alias(hass, monkeypatch): + """Group aliases should only respond to group commands (allon/alloff).""" + config = { + 'rflink': { + 'port': '/dev/ttyABC0', + }, + DOMAIN: { + 'platform': 'rflink', + 'devices': { + 'protocol_0_0': { + 'name': 'test', + 'group_aliases': ['test_group_0_0'], + }, + }, + }, + } + + # setup mocking rflink module + event_callback, _, _, _ = await mock_rflink( + hass, config, DOMAIN, monkeypatch) + + assert hass.states.get(DOMAIN + '.test').state == STATE_CLOSED + + # test sending group command to group alias + event_callback({ + 'id': 'test_group_0_0', + 'command': 'allon', + }) + await hass.async_block_till_done() + + assert hass.states.get(DOMAIN + '.test').state == STATE_OPEN + + # test sending group command to group alias + event_callback({ + 'id': 'test_group_0_0', + 'command': 'down', + }) + await hass.async_block_till_done() + + assert hass.states.get(DOMAIN + '.test').state == STATE_OPEN + + +async def test_nogroup_alias(hass, monkeypatch): + """Non group aliases should not respond to group commands.""" + config = { + 'rflink': { + 'port': '/dev/ttyABC0', + }, + DOMAIN: { + 'platform': 'rflink', + 'devices': { + 'protocol_0_0': { + 'name': 'test', + 'nogroup_aliases': ['test_nogroup_0_0'], + }, + }, + }, + } + + # setup mocking rflink module + event_callback, _, _, _ = await mock_rflink( + hass, config, DOMAIN, monkeypatch) + + assert hass.states.get(DOMAIN + '.test').state == STATE_CLOSED + + # test sending group command to nogroup alias + event_callback({ + 'id': 'test_nogroup_0_0', + 'command': 'allon', + }) + await hass.async_block_till_done() + # should not affect state + assert hass.states.get(DOMAIN + '.test').state == STATE_CLOSED + + # test sending group command to nogroup alias + event_callback({ + 'id': 'test_nogroup_0_0', + 'command': 'up', + }) + await hass.async_block_till_done() + # should affect state + assert hass.states.get(DOMAIN + '.test').state == STATE_OPEN + + +async def test_nogroup_device_id(hass, monkeypatch): + """Device id that do not respond to group commands (allon/alloff).""" + config = { + 'rflink': { + 'port': '/dev/ttyABC0', + }, + DOMAIN: { + 'platform': 'rflink', + 'devices': { + 'test_nogroup_0_0': { + 'name': 'test', + 'group': False, + }, + }, + }, + } + + # setup mocking rflink module + event_callback, _, _, _ = await mock_rflink( + hass, config, DOMAIN, monkeypatch) + + assert hass.states.get(DOMAIN + '.test').state == STATE_CLOSED + + # test sending group command to nogroup + event_callback({ + 'id': 'test_nogroup_0_0', + 'command': 'allon', + }) + await hass.async_block_till_done() + # should not affect state + assert hass.states.get(DOMAIN + '.test').state == STATE_CLOSED + + # test sending group command to nogroup + event_callback({ + 'id': 'test_nogroup_0_0', + 'command': 'up', + }) + await hass.async_block_till_done() + # should affect state + assert hass.states.get(DOMAIN + '.test').state == STATE_OPEN + + +async def test_disable_automatic_add(hass, monkeypatch): + """If disabled new devices should not be automatically added.""" + config = { + 'rflink': { + 'port': '/dev/ttyABC0', + }, + DOMAIN: { + 'platform': 'rflink', + 'automatic_add': False, + }, + } + + # setup mocking rflink module + event_callback, _, _, _ = await mock_rflink( + hass, config, DOMAIN, monkeypatch) + + # test event for new unconfigured sensor + event_callback({ + 'id': 'protocol_0_0', + 'command': 'down', + }) + await hass.async_block_till_done() + + # make sure new device is not added + assert not hass.states.get(DOMAIN + '.protocol_0_0') + + +async def test_restore_state(hass, monkeypatch): + """Ensure states are restored on startup.""" + config = { + 'rflink': { + 'port': '/dev/ttyABC0', + }, + DOMAIN: { + 'platform': 'rflink', + 'devices': { + 'RTS_12345678_0': { + 'name': 'c1', + }, + 'test_restore_2': { + 'name': 'c2', + }, + 'test_restore_3': { + 'name': 'c3', + }, + 'test_restore_4': { + 'name': 'c4', + }, + }, + }, + } + + mock_restore_cache(hass, ( + State(DOMAIN + '.c1', STATE_OPEN, ), + State(DOMAIN + '.c2', STATE_CLOSED, ), + )) + + hass.state = CoreState.starting + + # setup mocking rflink module + _, _, _, _ = await mock_rflink(hass, config, DOMAIN, monkeypatch) + + state = hass.states.get(DOMAIN + '.c1') + assert state + assert state.state == STATE_OPEN + + state = hass.states.get(DOMAIN + '.c2') + assert state + assert state.state == STATE_CLOSED + + state = hass.states.get(DOMAIN + '.c3') + assert state + assert state.state == STATE_CLOSED + + # not cached cover must default values + state = hass.states.get(DOMAIN + '.c4') + assert state + assert state.state == STATE_CLOSED + assert state.attributes['assumed_state'] diff --git a/tests/components/light/test_rflink.py b/tests/components/light/test_rflink.py index c55c16077e0..e77e4c0ff44 100644 --- a/tests/components/light/test_rflink.py +++ b/tests/components/light/test_rflink.py @@ -1,7 +1,7 @@ -"""Test for RFlink light components. +"""Test for RFLink light components. -Test setup of rflink lights component/platform. State tracking and -control of Rflink switch devices. +Test setup of RFLink lights component/platform. State tracking and +control of RFLink switch devices. """ @@ -10,9 +10,10 @@ import asyncio from homeassistant.components.light import ATTR_BRIGHTNESS from homeassistant.components.rflink import EVENT_BUTTON_PRESSED from homeassistant.const import ( - ATTR_ENTITY_ID, SERVICE_TURN_OFF, SERVICE_TURN_ON) -from homeassistant.core import callback + ATTR_ENTITY_ID, SERVICE_TURN_OFF, SERVICE_TURN_ON, STATE_ON, STATE_OFF) +from homeassistant.core import callback, State, CoreState +from tests.common import mock_restore_cache from ..test_rflink import mock_rflink DOMAIN = 'light' @@ -44,7 +45,7 @@ CONFIG = { @asyncio.coroutine def test_default_setup(hass, monkeypatch): - """Test all basic functionality of the rflink switch component.""" + """Test all basic functionality of the RFLink switch component.""" # setup mocking rflink module event_callback, create, protocol, _ = yield from mock_rflink( hass, CONFIG, DOMAIN, monkeypatch) @@ -119,7 +120,7 @@ def test_default_setup(hass, monkeypatch): assert hass.states.get(DOMAIN + '.protocol2_0_1').state == 'on' - # test changing state from HA propagates to Rflink + # test changing state from HA propagates to RFLink hass.async_add_job( hass.services.async_call(DOMAIN, SERVICE_TURN_OFF, {ATTR_ENTITY_ID: DOMAIN + '.test'})) @@ -175,7 +176,7 @@ def test_default_setup(hass, monkeypatch): @asyncio.coroutine def test_firing_bus_event(hass, monkeypatch): - """Incoming Rflink command events should be put on the HA event bus.""" + """Incoming RFLink command events should be put on the HA event bus.""" config = { 'rflink': { 'port': '/dev/ttyABC0', @@ -560,3 +561,56 @@ def test_disable_automatic_add(hass, monkeypatch): # make sure new device is not added assert not hass.states.get(DOMAIN + '.protocol_0_0') + + +@asyncio.coroutine +def test_restore_state(hass, monkeypatch): + """Ensure states are restored on startup.""" + config = { + 'rflink': { + 'port': '/dev/ttyABC0', + }, + DOMAIN: { + 'platform': 'rflink', + 'devices': { + 'NewKaku_12345678_0': { + 'name': 'l1', + 'type': 'hybrid', + }, + 'test_restore_2': { + 'name': 'l2', + }, + 'test_restore_3': { + 'name': 'l3', + }, + }, + }, + } + + mock_restore_cache(hass, ( + State(DOMAIN + '.l1', STATE_ON, {ATTR_BRIGHTNESS: "123", }), + State(DOMAIN + '.l2', STATE_ON, {ATTR_BRIGHTNESS: "321", }), + State(DOMAIN + '.l3', STATE_OFF, ), + )) + + hass.state = CoreState.starting + + # setup mocking rflink module + _, _, _, _ = yield from mock_rflink(hass, config, DOMAIN, monkeypatch) + + # dimmable light must restore brightness + state = hass.states.get(DOMAIN + '.l1') + assert state + assert state.state == STATE_ON + assert state.attributes[ATTR_BRIGHTNESS] == 123 + + # normal light do NOT must restore brightness + state = hass.states.get(DOMAIN + '.l2') + assert state + assert state.state == STATE_ON + assert not state.attributes.get(ATTR_BRIGHTNESS) + + # OFF state also restores (or not) + state = hass.states.get(DOMAIN + '.l3') + assert state + assert state.state == STATE_OFF diff --git a/tests/components/switch/test_rflink.py b/tests/components/switch/test_rflink.py index 77a6b572e96..8603545b563 100644 --- a/tests/components/switch/test_rflink.py +++ b/tests/components/switch/test_rflink.py @@ -9,9 +9,10 @@ import asyncio from homeassistant.components.rflink import EVENT_BUTTON_PRESSED from homeassistant.const import ( - ATTR_ENTITY_ID, SERVICE_TURN_OFF, SERVICE_TURN_ON) -from homeassistant.core import callback + ATTR_ENTITY_ID, SERVICE_TURN_OFF, SERVICE_TURN_ON, STATE_ON, STATE_OFF) +from homeassistant.core import callback, State, CoreState +from tests.common import mock_restore_cache from ..test_rflink import mock_rflink DOMAIN = 'switch' @@ -310,3 +311,43 @@ def test_not_firing_default(hass, monkeypatch): yield from hass.async_block_till_done() assert not calls, 'an event has been fired' + + +@asyncio.coroutine +def test_restore_state(hass, monkeypatch): + """Ensure states are restored on startup.""" + config = { + 'rflink': { + 'port': '/dev/ttyABC0', + }, + DOMAIN: { + 'platform': 'rflink', + 'devices': { + 'test': { + 'name': 's1', + 'aliases': ['test_alias_0_0'], + }, + 'switch_test': { + 'name': 's2', + } + } + } + } + + mock_restore_cache(hass, ( + State(DOMAIN + '.s1', STATE_ON, ), + State(DOMAIN + '.s2', STATE_OFF, ), + )) + + hass.state = CoreState.starting + + # setup mocking rflink module + _, _, _, _ = yield from mock_rflink(hass, config, DOMAIN, monkeypatch) + + state = hass.states.get(DOMAIN + '.s1') + assert state + assert state.state == STATE_ON + + state = hass.states.get(DOMAIN + '.s2') + assert state + assert state.state == STATE_OFF From c20322232ab0e500c2e3952fc784c0064f397e74 Mon Sep 17 00:00:00 2001 From: Fredrik Erlandsson Date: Tue, 11 Dec 2018 18:17:45 +0100 Subject: [PATCH 055/304] Move daikin to package (#19187) --- homeassistant/components/{daikin.py => daikin/__init__.py} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename homeassistant/components/{daikin.py => daikin/__init__.py} (100%) diff --git a/homeassistant/components/daikin.py b/homeassistant/components/daikin/__init__.py similarity index 100% rename from homeassistant/components/daikin.py rename to homeassistant/components/daikin/__init__.py From c7492b0feb39f6bfe8c28e28f5aa521854c5d1ee Mon Sep 17 00:00:00 2001 From: Erik Date: Tue, 11 Dec 2018 20:20:57 +0100 Subject: [PATCH 056/304] Move check to websocket --- homeassistant/components/config/entity_registry.py | 7 ++++++- homeassistant/helpers/entity_registry.py | 4 +--- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/config/entity_registry.py b/homeassistant/components/config/entity_registry.py index 15eed32a8cd..13a968dd101 100644 --- a/homeassistant/components/config/entity_registry.py +++ b/homeassistant/components/config/entity_registry.py @@ -1,7 +1,7 @@ """HTTP views to interact with the entity registry.""" import voluptuous as vol -from homeassistant.core import callback +from homeassistant.core import callback, split_entity_id 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 @@ -106,6 +106,11 @@ async def websocket_update_entity(hass, connection, msg): if 'new_entity_id' in msg: changes['new_entity_id'] = msg['new_entity_id'] + if (msg['new_entity_id'] in hass.states.async_entity_ids( + split_entity_id(msg['new_entity_id'])[0])): + connection.send_message(websocket_api.error_message( + msg['id'], ERR_NOT_FOUND, 'Entity is already registered')) + return try: if changes: diff --git a/homeassistant/helpers/entity_registry.py b/homeassistant/helpers/entity_registry.py index fdd9f178321..57c8bcf0af8 100644 --- a/homeassistant/helpers/entity_registry.py +++ b/homeassistant/helpers/entity_registry.py @@ -171,9 +171,7 @@ class EntityRegistry: changes['device_id'] = device_id if new_entity_id is not _UNDEF and new_entity_id != old.entity_id: - if (self.async_is_registered(new_entity_id) or new_entity_id in - self.hass.states.async_entity_ids( - split_entity_id(entity_id)[0])): + if self.async_is_registered(new_entity_id): raise ValueError('Entity is already registered') if not valid_entity_id(new_entity_id): From d1eb5da5f40f53da221e3c6e5d8115d11df70ec5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20H=C3=B8yer=20Iversen?= Date: Wed, 12 Dec 2018 09:16:20 +0100 Subject: [PATCH 057/304] Update switchbot library (#19202) --- homeassistant/components/switch/switchbot.py | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/switch/switchbot.py b/homeassistant/components/switch/switchbot.py index 9682a4444aa..9cd2927d832 100644 --- a/homeassistant/components/switch/switchbot.py +++ b/homeassistant/components/switch/switchbot.py @@ -12,7 +12,7 @@ import homeassistant.helpers.config_validation as cv from homeassistant.components.switch import SwitchDevice, PLATFORM_SCHEMA from homeassistant.const import CONF_NAME, CONF_MAC -REQUIREMENTS = ['PySwitchbot==0.4'] +REQUIREMENTS = ['PySwitchbot==0.5'] _LOGGER = logging.getLogger(__name__) diff --git a/requirements_all.txt b/requirements_all.txt index 861aa6a60d6..db2165edcf6 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -54,7 +54,7 @@ PyQRCode==1.2.1 PyRMVtransport==0.1.3 # homeassistant.components.switch.switchbot -PySwitchbot==0.4 +PySwitchbot==0.5 # homeassistant.components.sensor.transport_nsw PyTransportNSW==0.1.1 From c0cd2d48ece830bef0cf457e07e70c562837348e Mon Sep 17 00:00:00 2001 From: Fredrik Erlandsson Date: Wed, 12 Dec 2018 09:29:45 +0100 Subject: [PATCH 058/304] add unique_id to SMHI (#19183) --- homeassistant/components/weather/smhi.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/homeassistant/components/weather/smhi.py b/homeassistant/components/weather/smhi.py index c686b5c90e9..94873b03bd6 100644 --- a/homeassistant/components/weather/smhi.py +++ b/homeassistant/components/weather/smhi.py @@ -96,6 +96,11 @@ class SmhiWeather(WeatherEntity): self._fail_count = 0 self._smhi_api = Smhi(self._longitude, self._latitude, session=session) + @property + def unique_id(self) -> str: + """Return a unique id.""" + return '{}, {}'.format(self._latitude, self._longitude) + @Throttle(MIN_TIME_BETWEEN_UPDATES) async def async_update(self) -> None: """Refresh the forecast data from SMHI weather API.""" From 6f4657fe022504c33ebb09f17a08d33b1fcb76ee Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 12 Dec 2018 11:44:50 +0100 Subject: [PATCH 059/304] Revert PR #18602 (#19188) --- homeassistant/components/camera/mjpeg.py | 20 +++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/camera/mjpeg.py b/homeassistant/components/camera/mjpeg.py index 5c6d7e18075..2819b0e6ec4 100644 --- a/homeassistant/components/camera/mjpeg.py +++ b/homeassistant/components/camera/mjpeg.py @@ -59,15 +59,21 @@ async def async_setup_platform(hass, config, async_add_entities, def extract_image_from_mjpeg(stream): """Take in a MJPEG stream object, return the jpg from it.""" - data = bytes() - data_start = b"\xff\xd8" - data_end = b"\xff\xd9" - for chunk in stream: - end_idx = chunk.find(data_end) - if end_idx != -1: - return data[data.find(data_start):] + chunk[:end_idx + 2] + data = b'' + for chunk in stream: data += chunk + jpg_end = data.find(b'\xff\xd9') + + if jpg_end == -1: + continue + + jpg_start = data.find(b'\xff\xd8') + + if jpg_start == -1: + continue + + return data[jpg_start:jpg_end + 2] class MjpegCamera(Camera): From 7de509dc76242424251547fb77b2bbaf0e04b389 Mon Sep 17 00:00:00 2001 From: Jason Hunter Date: Wed, 12 Dec 2018 09:54:25 -0500 Subject: [PATCH 060/304] Add automation and script events to logbook event types (#19219) --- homeassistant/components/logbook.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/logbook.py b/homeassistant/components/logbook.py index 78da5733a06..79ee728ddd7 100644 --- a/homeassistant/components/logbook.py +++ b/homeassistant/components/logbook.py @@ -60,7 +60,8 @@ CONFIG_SCHEMA = vol.Schema({ ALL_EVENT_TYPES = [ EVENT_STATE_CHANGED, EVENT_LOGBOOK_ENTRY, EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP, - EVENT_ALEXA_SMART_HOME, EVENT_HOMEKIT_CHANGED + EVENT_ALEXA_SMART_HOME, EVENT_HOMEKIT_CHANGED, + EVENT_AUTOMATION_TRIGGERED, EVENT_SCRIPT_STARTED ] LOG_MESSAGE_SCHEMA = vol.Schema({ From 4984030871e4eb43f62c9b94dde35b85a7d250d7 Mon Sep 17 00:00:00 2001 From: Lukas Barth Date: Wed, 12 Dec 2018 16:11:18 +0100 Subject: [PATCH 061/304] Fix geizhals crash if no price found (#19197) * Fix geizhals crash if no price found * Return None on unknown price. * Linting * Linting the linting --- homeassistant/components/sensor/geizhals.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/homeassistant/components/sensor/geizhals.py b/homeassistant/components/sensor/geizhals.py index 654ad0ccafb..66cab473f49 100644 --- a/homeassistant/components/sensor/geizhals.py +++ b/homeassistant/components/sensor/geizhals.py @@ -80,6 +80,9 @@ class Geizwatch(Entity): @property def state(self): """Return the best price of the selected product.""" + if not self._device.prices: + return None + return self._device.prices[0] @property From 0e868deedd59bfef4e09bf610a85042d3933a19b Mon Sep 17 00:00:00 2001 From: Erik Date: Wed, 12 Dec 2018 16:26:44 +0100 Subject: [PATCH 062/304] Add JSON attribute topic to MQTT sensor --- homeassistant/components/sensor/mqtt.py | 18 ++++++-- tests/components/sensor/test_mqtt.py | 61 +++++++++++++++++++++++++ 2 files changed, 74 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/sensor/mqtt.py b/homeassistant/components/sensor/mqtt.py index 7d0908c5645..74da2a18563 100644 --- a/homeassistant/components/sensor/mqtt.py +++ b/homeassistant/components/sensor/mqtt.py @@ -14,8 +14,8 @@ import voluptuous as vol from homeassistant.core import callback from homeassistant.components import sensor from homeassistant.components.mqtt import ( - ATTR_DISCOVERY_HASH, CONF_AVAILABILITY_TOPIC, CONF_STATE_TOPIC, - CONF_PAYLOAD_AVAILABLE, CONF_PAYLOAD_NOT_AVAILABLE, CONF_QOS, + ATTR_DISCOVERY_HASH, CONF_AVAILABILITY_TOPIC, CONF_PAYLOAD_AVAILABLE, + CONF_PAYLOAD_NOT_AVAILABLE, CONF_QOS, CONF_STATE_TOPIC, MqttAttributes, MqttAvailability, MqttDiscoveryUpdate, MqttEntityDeviceInfo, subscription) from homeassistant.components.mqtt.discovery import MQTT_DISCOVERY_NEW from homeassistant.components.sensor import DEVICE_CLASSES_SCHEMA @@ -52,7 +52,8 @@ PLATFORM_SCHEMA = mqtt.MQTT_RO_PLATFORM_SCHEMA.extend({ # This is an exception because MQTT is a message transport, not a protocol. 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, @@ -80,8 +81,8 @@ async def _async_setup_entity(config: ConfigType, async_add_entities, async_add_entities([MqttSensor(config, discovery_hash)]) -class MqttSensor(MqttAvailability, MqttDiscoveryUpdate, MqttEntityDeviceInfo, - Entity): +class MqttSensor(MqttAttributes, MqttAvailability, MqttDiscoveryUpdate, + MqttEntityDeviceInfo, Entity): """Representation of a sensor that can be updated using MQTT.""" def __init__(self, config, discovery_hash): @@ -99,6 +100,11 @@ class MqttSensor(MqttAvailability, MqttDiscoveryUpdate, MqttEntityDeviceInfo, qos = config.get(CONF_QOS) device_config = config.get(CONF_DEVICE) + if CONF_JSON_ATTRS in config: + _LOGGER.warning('configuration variable "json_attributes" is ' + 'deprecated, replace with "json_attributes_topic"') + + MqttAttributes.__init__(self, config) MqttAvailability.__init__(self, availability_topic, qos, payload_available, payload_not_available) MqttDiscoveryUpdate.__init__(self, discovery_hash, @@ -114,6 +120,7 @@ class MqttSensor(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() @@ -172,6 +179,7 @@ class MqttSensor(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) @callback diff --git a/tests/components/sensor/test_mqtt.py b/tests/components/sensor/test_mqtt.py index 78de05e1ff3..f4f92df2153 100644 --- a/tests/components/sensor/test_mqtt.py +++ b/tests/components/sensor/test_mqtt.py @@ -333,6 +333,67 @@ 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): From d03dfd985bff83e726e52a7ba1934d7e2278aed3 Mon Sep 17 00:00:00 2001 From: Erik Date: Wed, 12 Dec 2018 16:30:42 +0100 Subject: [PATCH 063/304] Review comments --- homeassistant/components/config/entity_registry.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/config/entity_registry.py b/homeassistant/components/config/entity_registry.py index 13a968dd101..71833a2e42d 100644 --- a/homeassistant/components/config/entity_registry.py +++ b/homeassistant/components/config/entity_registry.py @@ -1,7 +1,7 @@ """HTTP views to interact with the entity registry.""" import voluptuous as vol -from homeassistant.core import callback, split_entity_id +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 @@ -106,10 +106,9 @@ async def websocket_update_entity(hass, connection, msg): if 'new_entity_id' in msg: changes['new_entity_id'] = msg['new_entity_id'] - if (msg['new_entity_id'] in hass.states.async_entity_ids( - split_entity_id(msg['new_entity_id'])[0])): + if hass.states.get(msg['new_entity_id']) is not None: connection.send_message(websocket_api.error_message( - msg['id'], ERR_NOT_FOUND, 'Entity is already registered')) + msg['id'], 'invalid_info', 'Entity is already registered')) return try: From 031ee71adffa5cd1cda13b7402cad6d6c9b81ed9 Mon Sep 17 00:00:00 2001 From: "David F. Mulcahey" Date: Wed, 12 Dec 2018 11:06:22 -0500 Subject: [PATCH 064/304] Add ZHA device handler library (#19099) * event foundation * implement quirks * lock zha-quirks version * allow quirks handling to be toggled on and off * revert event commit * disable warning * update requirements_all * Remove fix in favor of #19141 #19141 should be what ultimately corrects this issue. * review comment --- .coveragerc | 1 + homeassistant/components/zha/__init__.py | 13 +++++++++++-- homeassistant/components/zha/const.py | 1 + requirements_all.txt | 3 +++ 4 files changed, 16 insertions(+), 2 deletions(-) diff --git a/.coveragerc b/.coveragerc index 8d98a0c23e0..a33a58f3571 100644 --- a/.coveragerc +++ b/.coveragerc @@ -407,6 +407,7 @@ omit = homeassistant/components/zha/__init__.py homeassistant/components/zha/const.py + homeassistant/components/zha/event.py homeassistant/components/zha/entities/* homeassistant/components/zha/helpers.py homeassistant/components/*/zha.py diff --git a/homeassistant/components/zha/__init__.py b/homeassistant/components/zha/__init__.py index 41659ae47df..e685deed1c6 100644 --- a/homeassistant/components/zha/__init__.py +++ b/homeassistant/components/zha/__init__.py @@ -28,12 +28,13 @@ from .const import ( 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) + EVENTABLE_CLUSTERS, DATA_ZHA_CORE_EVENTS, ENABLE_QUIRKS) REQUIREMENTS = [ 'bellows==0.7.0', 'zigpy==0.2.0', 'zigpy-xbee==0.1.1', + 'zha-quirks==0.0.5' ] DEVICE_CONFIG_SCHEMA_ENTRY = vol.Schema({ @@ -51,6 +52,7 @@ CONFIG_SCHEMA = vol.Schema({ vol.Optional(CONF_DATABASE): cv.string, vol.Optional(CONF_DEVICE_CONFIG, default={}): vol.Schema({cv.string: DEVICE_CONFIG_SCHEMA_ENTRY}), + vol.Optional(ENABLE_QUIRKS, default=True): cv.boolean, }) }, extra=vol.ALLOW_EXTRA) @@ -94,7 +96,8 @@ async def async_setup(hass, config): context={'source': config_entries.SOURCE_IMPORT}, data={ CONF_USB_PATH: conf[CONF_USB_PATH], - CONF_RADIO_TYPE: conf.get(CONF_RADIO_TYPE).value + CONF_RADIO_TYPE: conf.get(CONF_RADIO_TYPE).value, + ENABLE_QUIRKS: conf[ENABLE_QUIRKS] } )) return True @@ -105,6 +108,12 @@ async def async_setup_entry(hass, config_entry): Will automatically load components to support devices found on the network. """ + if config_entry.data.get(ENABLE_QUIRKS): + # needs to be done here so that the ZHA module is finished loading + # before zhaquirks is imported + # pylint: disable=W0611, W0612 + import zhaquirks # noqa + global APPLICATION_CONTROLLER hass.data[DATA_ZHA] = hass.data.get(DATA_ZHA, {}) diff --git a/homeassistant/components/zha/const.py b/homeassistant/components/zha/const.py index 7da6f826c44..a148eccf53f 100644 --- a/homeassistant/components/zha/const.py +++ b/homeassistant/components/zha/const.py @@ -30,6 +30,7 @@ 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 diff --git a/requirements_all.txt b/requirements_all.txt index db2165edcf6..ff6cc9cee1d 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1670,6 +1670,9 @@ zengge==0.2 # homeassistant.components.zeroconf zeroconf==0.21.3 +# homeassistant.components.zha +zha-quirks==0.0.5 + # homeassistant.components.climate.zhong_hong zhong_hong_hvac==1.0.9 From 7d9e2577131e121a365d632de650753d073584b5 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 12 Dec 2018 17:17:27 +0100 Subject: [PATCH 065/304] Fix owntracks topic in encrypted ios (#19220) * Fix owntracks topic * Warn if per-topic secret and using HTTP --- homeassistant/components/device_tracker/owntracks.py | 9 +++++++-- homeassistant/components/owntracks/__init__.py | 9 +++++---- 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/device_tracker/owntracks.py b/homeassistant/components/device_tracker/owntracks.py index ae2b9d6146b..e85ebbe6fe1 100644 --- a/homeassistant/components/device_tracker/owntracks.py +++ b/homeassistant/components/device_tracker/owntracks.py @@ -316,14 +316,19 @@ async def async_handle_waypoints_message(hass, context, message): @HANDLERS.register('encrypted') async def async_handle_encrypted_message(hass, context, message): """Handle an encrypted message.""" - plaintext_payload = _decrypt_payload(context.secret, message['topic'], + if 'topic' not in message and isinstance(context.secret, dict): + _LOGGER.error("You cannot set per topic secrets when using HTTP") + return + + plaintext_payload = _decrypt_payload(context.secret, message.get('topic'), message['data']) if plaintext_payload is None: return decrypted = json.loads(plaintext_payload) - decrypted['topic'] = message['topic'] + if 'topic' in message and 'topic' not in decrypted: + decrypted['topic'] = message['topic'] await async_handle_message(hass, context, decrypted) diff --git a/homeassistant/components/owntracks/__init__.py b/homeassistant/components/owntracks/__init__.py index 7dc88be9764..5e6a99741e8 100644 --- a/homeassistant/components/owntracks/__init__.py +++ b/homeassistant/components/owntracks/__init__.py @@ -137,15 +137,16 @@ async def handle_webhook(hass, webhook_id, request): user = headers.get('X-Limit-U') device = headers.get('X-Limit-D', user) - if user is None: + if user: + topic_base = re.sub('/#$', '', context.mqtt_topic) + message['topic'] = '{}/{}/{}'.format(topic_base, user, device) + + elif message['_type'] != 'encrypted': _LOGGER.warning('No topic or user found in message. If on Android,' ' set a username in Connection -> Identification') # Keep it as a 200 response so the incorrect packet is discarded return json_response([]) - topic_base = re.sub('/#$', '', context.mqtt_topic) - message['topic'] = '{}/{}/{}'.format(topic_base, user, device) - hass.helpers.dispatcher.async_dispatcher_send( DOMAIN, hass, context, message) return json_response([]) From f8438e96d11805b6a70e6a385de2048f32ce7769 Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Wed, 12 Dec 2018 11:07:06 -0700 Subject: [PATCH 066/304] Add package data attribute to 17track.net summary sensors (#19213) * 17track.net: Add package data attribute to summary sensors * Member comments --- .../components/sensor/seventeentrack.py | 22 +++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/sensor/seventeentrack.py b/homeassistant/components/sensor/seventeentrack.py index 7c5dba3b0e1..c77b934fbad 100644 --- a/homeassistant/components/sensor/seventeentrack.py +++ b/homeassistant/components/sensor/seventeentrack.py @@ -21,9 +21,12 @@ REQUIREMENTS = ['py17track==2.1.1'] _LOGGER = logging.getLogger(__name__) ATTR_DESTINATION_COUNTRY = 'destination_country' +ATTR_FRIENDLY_NAME = 'friendly_name' ATTR_INFO_TEXT = 'info_text' ATTR_ORIGIN_COUNTRY = 'origin_country' +ATTR_PACKAGES = 'packages' ATTR_PACKAGE_TYPE = 'package_type' +ATTR_STATUS = 'status' ATTR_TRACKING_INFO_LANGUAGE = 'tracking_info_language' ATTR_TRACKING_NUMBER = 'tracking_number' @@ -117,7 +120,7 @@ class SeventeenTrackSummarySensor(Entity): @property def name(self): """Return the name.""" - return '17track Packages {0}'.format(self._status) + return 'Seventeentrack Packages {0}'.format(self._status) @property def state(self): @@ -139,6 +142,21 @@ class SeventeenTrackSummarySensor(Entity): """Update the sensor.""" await self._data.async_update() + package_data = [] + for package in self._data.packages: + if package.status != self._status: + continue + + package_data.append({ + ATTR_FRIENDLY_NAME: package.friendly_name, + ATTR_INFO_TEXT: package.info_text, + ATTR_STATUS: package.status, + ATTR_TRACKING_NUMBER: package.tracking_number, + }) + + if package_data: + self._attrs[ATTR_PACKAGES] = package_data + self._state = self._data.summary.get(self._status) @@ -186,7 +204,7 @@ class SeventeenTrackPackageSensor(Entity): name = self._friendly_name if not name: name = self._tracking_number - return '17track Package: {0}'.format(name) + return 'Seventeentrack Package: {0}'.format(name) @property def state(self): From 8c6b9b57cd83bbc48fcac7d3146171770fefae9f Mon Sep 17 00:00:00 2001 From: kennedyshead Date: Wed, 12 Dec 2018 20:24:44 +0100 Subject: [PATCH 067/304] Bump aioasuswrt (#19229) * bump aioasuswrt version * run gen_requirements --- 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 719e857c751..959d39038e2 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.13'] +REQUIREMENTS = ['aioasuswrt==1.1.15'] _LOGGER = logging.getLogger(__name__) diff --git a/requirements_all.txt b/requirements_all.txt index ff6cc9cee1d..3ab4bafe3ea 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -87,7 +87,7 @@ abodepy==0.14.0 afsapi==0.0.4 # homeassistant.components.asuswrt -aioasuswrt==1.1.13 +aioasuswrt==1.1.15 # homeassistant.components.device_tracker.automatic aioautomatic==0.6.5 From 2fc0dfecb12d6212249b45a7725c71ba2d944623 Mon Sep 17 00:00:00 2001 From: Teemu R Date: Wed, 12 Dec 2018 23:05:55 +0100 Subject: [PATCH 068/304] Convert songpal to use asynchronous websocket for state updates (#19129) * Add websocket-based non-polling variant for songpal * linting fixes * changes based on Martin's feedback * Fix linting * add backoff timer for reconnects, fix variable naming (I thought that this wouldn't matter for internals..) * Remove poll configuration variable * bump the version just to be sure, the previous release lacked a version file (required for setup.py) --- .../components/media_player/songpal.py | 118 +++++++++++++++--- requirements_all.txt | 2 +- 2 files changed, 100 insertions(+), 20 deletions(-) diff --git a/homeassistant/components/media_player/songpal.py b/homeassistant/components/media_player/songpal.py index 83b10997c31..c79470a82fe 100644 --- a/homeassistant/components/media_player/songpal.py +++ b/homeassistant/components/media_player/songpal.py @@ -4,7 +4,9 @@ Support for Songpal-enabled (Sony) media devices. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/media_player.songpal/ """ +import asyncio import logging +from collections import OrderedDict import voluptuous as vol @@ -12,11 +14,12 @@ from homeassistant.components.media_player import ( DOMAIN, PLATFORM_SCHEMA, SUPPORT_SELECT_SOURCE, SUPPORT_TURN_OFF, SUPPORT_TURN_ON, SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET, SUPPORT_VOLUME_STEP, MediaPlayerDevice) -from homeassistant.const import ATTR_ENTITY_ID, CONF_NAME, STATE_OFF, STATE_ON +from homeassistant.const import ( + ATTR_ENTITY_ID, CONF_NAME, STATE_OFF, STATE_ON, EVENT_HOMEASSISTANT_STOP) from homeassistant.exceptions import PlatformNotReady import homeassistant.helpers.config_validation as cv -REQUIREMENTS = ['python-songpal==0.0.8'] +REQUIREMENTS = ['python-songpal==0.0.9.1'] _LOGGER = logging.getLogger(__name__) @@ -62,7 +65,11 @@ async def async_setup_platform( else: name = config.get(CONF_NAME) endpoint = config.get(CONF_ENDPOINT) - device = SongpalDevice(name, endpoint) + device = SongpalDevice(name, endpoint, poll=False) + + if endpoint in hass.data[PLATFORM]: + _LOGGER.debug("The endpoint exists already, skipping setup.") + return try: await device.initialize() @@ -96,12 +103,13 @@ async def async_setup_platform( class SongpalDevice(MediaPlayerDevice): """Class representing a Songpal device.""" - def __init__(self, name, endpoint): + def __init__(self, name, endpoint, poll=False): """Init.""" - import songpal + from songpal import Device self._name = name - self.endpoint = endpoint - self.dev = songpal.Device(self.endpoint) + self._endpoint = endpoint + self._poll = poll + self.dev = Device(self._endpoint) self._sysinfo = None self._state = False @@ -114,13 +122,79 @@ class SongpalDevice(MediaPlayerDevice): self._volume = 0 self._is_muted = False - self._sources = [] + self._active_source = None + self._sources = {} + + @property + def should_poll(self): + """Return True if the device should be polled.""" + return self._poll async def initialize(self): """Initialize the device.""" await self.dev.get_supported_methods() self._sysinfo = await self.dev.get_system_info() + async def async_activate_websocket(self): + """Activate websocket for listening if wanted.""" + _LOGGER.info("Activating websocket connection..") + from songpal import (VolumeChange, ContentChange, + PowerChange, ConnectChange) + + async def _volume_changed(volume: VolumeChange): + _LOGGER.debug("Volume changed: %s", volume) + self._volume = volume.volume + self._is_muted = volume.mute + await self.async_update_ha_state() + + async def _source_changed(content: ContentChange): + _LOGGER.debug("Source changed: %s", content) + if content.is_input: + self._active_source = self._sources[content.source] + _LOGGER.debug("New active source: %s", self._active_source) + await self.async_update_ha_state() + else: + _LOGGER.warning("Got non-handled content change: %s", + content) + + async def _power_changed(power: PowerChange): + _LOGGER.debug("Power changed: %s", power) + self._state = power.status + await self.async_update_ha_state() + + async def _try_reconnect(connect: ConnectChange): + _LOGGER.error("Got disconnected with %s, trying to reconnect.", + connect.exception) + self._available = False + self.dev.clear_notification_callbacks() + await self.async_update_ha_state() + + # Try to reconnect forever, a successful reconnect will initialize + # the websocket connection again. + delay = 10 + while not self._available: + _LOGGER.debug("Trying to reconnect in %s seconds", delay) + await asyncio.sleep(delay) + # We need to inform HA about the state in case we are coming + # back from a disconnected state. + await self.async_update_ha_state(force_refresh=True) + delay = min(2*delay, 300) + + self.dev.on_notification(VolumeChange, _volume_changed) + self.dev.on_notification(ContentChange, _source_changed) + self.dev.on_notification(PowerChange, _power_changed) + self.dev.on_notification(ConnectChange, _try_reconnect) + + async def listen_events(): + await self.dev.listen_notifications() + + async def handle_stop(event): + await self.dev.stop_listen_notifications() + + self.hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, handle_stop) + + self.hass.loop.create_task(listen_events()) + @property def name(self): """Return name of the device.""" @@ -169,18 +243,28 @@ class SongpalDevice(MediaPlayerDevice): inputs = await self.dev.get_inputs() _LOGGER.debug("Got ins: %s", inputs) - self._sources = inputs + + self._sources = OrderedDict() + for input_ in inputs: + self._sources[input_.uri] = input_ + if input_.active: + self._active_source = input_ + + _LOGGER.debug("Active source: %s", self._active_source) self._available = True + + # activate notifications if wanted + if not self._poll: + await self.hass.async_create_task( + self.async_activate_websocket()) except SongpalException as ex: - # if we were available, print out the exception - if self._available: - _LOGGER.error("Got an exception: %s", ex) + _LOGGER.error("Unable to update: %s", ex) self._available = False async def async_select_source(self, source): """Select source.""" - for out in self._sources: + for out in self._sources.values(): if out.title == source: await out.activate() return @@ -190,7 +274,7 @@ class SongpalDevice(MediaPlayerDevice): @property def source_list(self): """Return list of available sources.""" - return [x.title for x in self._sources] + return [src.title for src in self._sources.values()] @property def state(self): @@ -202,11 +286,7 @@ class SongpalDevice(MediaPlayerDevice): @property def source(self): """Return currently active source.""" - for out in self._sources: - if out.active: - return out.title - - return None + return self._active_source.title @property def volume_level(self): diff --git a/requirements_all.txt b/requirements_all.txt index 3ab4bafe3ea..acf668f2703 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1256,7 +1256,7 @@ python-roku==3.1.5 python-sochain-api==0.0.2 # homeassistant.components.media_player.songpal -python-songpal==0.0.8 +python-songpal==0.0.9.1 # homeassistant.components.sensor.synologydsm python-synology==0.2.0 From 56c7e78cf2f64e5d537ab8e3c0d34719a441f2c2 Mon Sep 17 00:00:00 2001 From: Andrey Kupreychik Date: Thu, 13 Dec 2018 16:01:41 +0700 Subject: [PATCH 069/304] Bumped NDMS2 client to 0.0.6 (#19244) --- homeassistant/components/device_tracker/keenetic_ndms2.py | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/device_tracker/keenetic_ndms2.py b/homeassistant/components/device_tracker/keenetic_ndms2.py index e9f9791b9f6..b8c2124ff0f 100644 --- a/homeassistant/components/device_tracker/keenetic_ndms2.py +++ b/homeassistant/components/device_tracker/keenetic_ndms2.py @@ -15,7 +15,7 @@ from homeassistant.const import ( CONF_HOST, CONF_PORT, CONF_PASSWORD, CONF_USERNAME ) -REQUIREMENTS = ['ndms2_client==0.0.5'] +REQUIREMENTS = ['ndms2_client==0.0.6'] _LOGGER = logging.getLogger(__name__) diff --git a/requirements_all.txt b/requirements_all.txt index acf668f2703..25b32500734 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -672,7 +672,7 @@ nad_receiver==0.0.9 nanoleaf==0.4.1 # homeassistant.components.device_tracker.keenetic_ndms2 -ndms2_client==0.0.5 +ndms2_client==0.0.6 # homeassistant.components.sensor.netdata netdata==0.1.2 From 8ea0a8d40b2471166bf419b3dbed4b7efbcb4bd9 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 13 Dec 2018 10:07:59 +0100 Subject: [PATCH 070/304] RFC: Deprecate auto target all for services and introduce entity_id: * (#19006) * Deprecate auto target all * Match on word 'all' --- .../alarm_control_panel/__init__.py | 2 +- .../components/automation/__init__.py | 4 +-- homeassistant/components/camera/__init__.py | 2 +- homeassistant/components/climate/__init__.py | 18 +++++----- homeassistant/components/counter/__init__.py | 2 +- homeassistant/components/cover/__init__.py | 2 +- homeassistant/components/fan/__init__.py | 12 +++---- homeassistant/components/group/__init__.py | 2 +- .../components/image_processing/__init__.py | 2 +- homeassistant/components/light/__init__.py | 6 ++-- homeassistant/components/lock/__init__.py | 2 +- .../components/media_player/__init__.py | 2 +- homeassistant/components/remote/__init__.py | 2 +- homeassistant/components/scene/__init__.py | 2 +- homeassistant/components/switch/__init__.py | 2 +- homeassistant/components/vacuum/__init__.py | 2 +- .../components/water_heater/__init__.py | 8 ++--- homeassistant/const.py | 3 ++ homeassistant/helpers/config_validation.py | 9 ++++- homeassistant/helpers/entity_component.py | 12 +++++-- homeassistant/helpers/service.py | 9 +++-- tests/helpers/test_config_validation.py | 13 +++++++ tests/helpers/test_entity_component.py | 34 +++++++++++++++++++ tests/helpers/test_service.py | 32 +++++++++++++++++ 24 files changed, 143 insertions(+), 41 deletions(-) diff --git a/homeassistant/components/alarm_control_panel/__init__.py b/homeassistant/components/alarm_control_panel/__init__.py index a42e6e880b5..ad8520118b4 100644 --- a/homeassistant/components/alarm_control_panel/__init__.py +++ b/homeassistant/components/alarm_control_panel/__init__.py @@ -25,7 +25,7 @@ ATTR_CHANGED_BY = 'changed_by' ENTITY_ID_FORMAT = DOMAIN + '.{}' ALARM_SERVICE_SCHEMA = vol.Schema({ - vol.Optional(ATTR_ENTITY_ID): cv.entity_ids, + vol.Optional(ATTR_ENTITY_ID): cv.comp_entity_ids, vol.Optional(ATTR_CODE): cv.string, }) diff --git a/homeassistant/components/automation/__init__.py b/homeassistant/components/automation/__init__.py index 4a2df399e0a..d062376f2a8 100644 --- a/homeassistant/components/automation/__init__.py +++ b/homeassistant/components/automation/__init__.py @@ -94,11 +94,11 @@ PLATFORM_SCHEMA = vol.Schema({ }) SERVICE_SCHEMA = vol.Schema({ - vol.Optional(ATTR_ENTITY_ID): cv.entity_ids, + vol.Optional(ATTR_ENTITY_ID): cv.comp_entity_ids, }) TRIGGER_SERVICE_SCHEMA = vol.Schema({ - vol.Required(ATTR_ENTITY_ID): cv.entity_ids, + vol.Required(ATTR_ENTITY_ID): cv.comp_entity_ids, vol.Optional(ATTR_VARIABLES, default={}): dict, }) diff --git a/homeassistant/components/camera/__init__.py b/homeassistant/components/camera/__init__.py index 0463b172d7a..653d0315ad4 100644 --- a/homeassistant/components/camera/__init__.py +++ b/homeassistant/components/camera/__init__.py @@ -61,7 +61,7 @@ FALLBACK_STREAM_INTERVAL = 1 # seconds MIN_STREAM_INTERVAL = 0.5 # seconds CAMERA_SERVICE_SCHEMA = vol.Schema({ - vol.Optional(ATTR_ENTITY_ID): cv.entity_ids, + vol.Optional(ATTR_ENTITY_ID): cv.comp_entity_ids, }) CAMERA_SERVICE_SNAPSHOT = CAMERA_SERVICE_SCHEMA.extend({ diff --git a/homeassistant/components/climate/__init__.py b/homeassistant/components/climate/__init__.py index 4b73e24fb41..d116a885319 100644 --- a/homeassistant/components/climate/__init__.py +++ b/homeassistant/components/climate/__init__.py @@ -92,15 +92,15 @@ CONVERTIBLE_ATTRIBUTE = [ _LOGGER = logging.getLogger(__name__) ON_OFF_SERVICE_SCHEMA = vol.Schema({ - vol.Optional(ATTR_ENTITY_ID): cv.entity_ids, + vol.Optional(ATTR_ENTITY_ID): cv.comp_entity_ids, }) SET_AWAY_MODE_SCHEMA = vol.Schema({ - vol.Optional(ATTR_ENTITY_ID): cv.entity_ids, + vol.Optional(ATTR_ENTITY_ID): cv.comp_entity_ids, vol.Required(ATTR_AWAY_MODE): cv.boolean, }) SET_AUX_HEAT_SCHEMA = vol.Schema({ - vol.Optional(ATTR_ENTITY_ID): cv.entity_ids, + vol.Optional(ATTR_ENTITY_ID): cv.comp_entity_ids, vol.Required(ATTR_AUX_HEAT): cv.boolean, }) SET_TEMPERATURE_SCHEMA = vol.Schema(vol.All( @@ -110,28 +110,28 @@ SET_TEMPERATURE_SCHEMA = vol.Schema(vol.All( vol.Exclusive(ATTR_TEMPERATURE, 'temperature'): vol.Coerce(float), vol.Inclusive(ATTR_TARGET_TEMP_HIGH, 'temperature'): vol.Coerce(float), vol.Inclusive(ATTR_TARGET_TEMP_LOW, 'temperature'): vol.Coerce(float), - vol.Optional(ATTR_ENTITY_ID): cv.entity_ids, + vol.Optional(ATTR_ENTITY_ID): cv.comp_entity_ids, vol.Optional(ATTR_OPERATION_MODE): cv.string, } )) SET_FAN_MODE_SCHEMA = vol.Schema({ - vol.Optional(ATTR_ENTITY_ID): cv.entity_ids, + vol.Optional(ATTR_ENTITY_ID): cv.comp_entity_ids, vol.Required(ATTR_FAN_MODE): cv.string, }) SET_HOLD_MODE_SCHEMA = vol.Schema({ - vol.Optional(ATTR_ENTITY_ID): cv.entity_ids, + vol.Optional(ATTR_ENTITY_ID): cv.comp_entity_ids, vol.Required(ATTR_HOLD_MODE): cv.string, }) SET_OPERATION_MODE_SCHEMA = vol.Schema({ - vol.Optional(ATTR_ENTITY_ID): cv.entity_ids, + vol.Optional(ATTR_ENTITY_ID): cv.comp_entity_ids, vol.Required(ATTR_OPERATION_MODE): cv.string, }) SET_HUMIDITY_SCHEMA = vol.Schema({ - vol.Optional(ATTR_ENTITY_ID): cv.entity_ids, + vol.Optional(ATTR_ENTITY_ID): cv.comp_entity_ids, vol.Required(ATTR_HUMIDITY): vol.Coerce(float), }) SET_SWING_MODE_SCHEMA = vol.Schema({ - vol.Optional(ATTR_ENTITY_ID): cv.entity_ids, + vol.Optional(ATTR_ENTITY_ID): cv.comp_entity_ids, vol.Required(ATTR_SWING_MODE): cv.string, }) diff --git a/homeassistant/components/counter/__init__.py b/homeassistant/components/counter/__init__.py index 228870489a2..cd3a29df2b6 100644 --- a/homeassistant/components/counter/__init__.py +++ b/homeassistant/components/counter/__init__.py @@ -33,7 +33,7 @@ SERVICE_INCREMENT = 'increment' SERVICE_RESET = 'reset' SERVICE_SCHEMA = vol.Schema({ - vol.Optional(ATTR_ENTITY_ID): cv.entity_ids, + vol.Optional(ATTR_ENTITY_ID): cv.comp_entity_ids, }) CONFIG_SCHEMA = vol.Schema({ diff --git a/homeassistant/components/cover/__init__.py b/homeassistant/components/cover/__init__.py index ec11b139f6b..ef8fcc42302 100644 --- a/homeassistant/components/cover/__init__.py +++ b/homeassistant/components/cover/__init__.py @@ -60,7 +60,7 @@ INTENT_OPEN_COVER = 'HassOpenCover' INTENT_CLOSE_COVER = 'HassCloseCover' COVER_SERVICE_SCHEMA = vol.Schema({ - vol.Optional(ATTR_ENTITY_ID): cv.entity_ids, + vol.Optional(ATTR_ENTITY_ID): cv.comp_entity_ids, }) COVER_SET_COVER_POSITION_SCHEMA = COVER_SERVICE_SCHEMA.extend({ diff --git a/homeassistant/components/fan/__init__.py b/homeassistant/components/fan/__init__.py index 36b075747e0..a54d52f4b12 100644 --- a/homeassistant/components/fan/__init__.py +++ b/homeassistant/components/fan/__init__.py @@ -61,30 +61,30 @@ PROP_TO_ATTR = { } # type: dict FAN_SET_SPEED_SCHEMA = vol.Schema({ - vol.Required(ATTR_ENTITY_ID): cv.entity_ids, + vol.Required(ATTR_ENTITY_ID): cv.comp_entity_ids, vol.Required(ATTR_SPEED): cv.string }) # type: dict FAN_TURN_ON_SCHEMA = vol.Schema({ - vol.Required(ATTR_ENTITY_ID): cv.entity_ids, + vol.Required(ATTR_ENTITY_ID): cv.comp_entity_ids, vol.Optional(ATTR_SPEED): cv.string }) # type: dict FAN_TURN_OFF_SCHEMA = vol.Schema({ - vol.Optional(ATTR_ENTITY_ID): cv.entity_ids + vol.Optional(ATTR_ENTITY_ID): cv.comp_entity_ids }) # type: dict FAN_OSCILLATE_SCHEMA = vol.Schema({ - vol.Required(ATTR_ENTITY_ID): cv.entity_ids, + vol.Required(ATTR_ENTITY_ID): cv.comp_entity_ids, vol.Required(ATTR_OSCILLATING): cv.boolean }) # type: dict FAN_TOGGLE_SCHEMA = vol.Schema({ - vol.Required(ATTR_ENTITY_ID): cv.entity_ids + vol.Required(ATTR_ENTITY_ID): cv.comp_entity_ids }) FAN_SET_DIRECTION_SCHEMA = vol.Schema({ - vol.Required(ATTR_ENTITY_ID): cv.entity_ids, + vol.Required(ATTR_ENTITY_ID): cv.comp_entity_ids, vol.Optional(ATTR_DIRECTION): cv.string }) # type: dict diff --git a/homeassistant/components/group/__init__.py b/homeassistant/components/group/__init__.py index 15a3816c559..b6dcd65fc2c 100644 --- a/homeassistant/components/group/__init__.py +++ b/homeassistant/components/group/__init__.py @@ -49,7 +49,7 @@ SERVICE_REMOVE = 'remove' CONTROL_TYPES = vol.In(['hidden', None]) SET_VISIBILITY_SERVICE_SCHEMA = vol.Schema({ - vol.Optional(ATTR_ENTITY_ID): cv.entity_ids, + vol.Optional(ATTR_ENTITY_ID): cv.comp_entity_ids, vol.Required(ATTR_VISIBLE): cv.boolean }) diff --git a/homeassistant/components/image_processing/__init__.py b/homeassistant/components/image_processing/__init__.py index 72a4a8155e2..b2cbb2b2391 100644 --- a/homeassistant/components/image_processing/__init__.py +++ b/homeassistant/components/image_processing/__init__.py @@ -62,7 +62,7 @@ PLATFORM_SCHEMA = cv.PLATFORM_SCHEMA.extend({ }) SERVICE_SCAN_SCHEMA = vol.Schema({ - vol.Optional(ATTR_ENTITY_ID): cv.entity_ids, + vol.Optional(ATTR_ENTITY_ID): cv.comp_entity_ids, }) diff --git a/homeassistant/components/light/__init__.py b/homeassistant/components/light/__init__.py index 41dbbcd6d0c..aa12e562515 100644 --- a/homeassistant/components/light/__init__.py +++ b/homeassistant/components/light/__init__.py @@ -88,7 +88,7 @@ VALID_BRIGHTNESS = vol.All(vol.Coerce(int), vol.Clamp(min=0, max=255)) VALID_BRIGHTNESS_PCT = vol.All(vol.Coerce(float), vol.Range(min=0, max=100)) LIGHT_TURN_ON_SCHEMA = vol.Schema({ - ATTR_ENTITY_ID: cv.entity_ids, + ATTR_ENTITY_ID: cv.comp_entity_ids, vol.Exclusive(ATTR_PROFILE, COLOR_GROUP): cv.string, ATTR_TRANSITION: VALID_TRANSITION, ATTR_BRIGHTNESS: VALID_BRIGHTNESS, @@ -115,13 +115,13 @@ LIGHT_TURN_ON_SCHEMA = vol.Schema({ }) LIGHT_TURN_OFF_SCHEMA = vol.Schema({ - ATTR_ENTITY_ID: cv.entity_ids, + ATTR_ENTITY_ID: cv.comp_entity_ids, ATTR_TRANSITION: VALID_TRANSITION, ATTR_FLASH: vol.In([FLASH_SHORT, FLASH_LONG]), }) LIGHT_TOGGLE_SCHEMA = vol.Schema({ - ATTR_ENTITY_ID: cv.entity_ids, + ATTR_ENTITY_ID: cv.comp_entity_ids, ATTR_TRANSITION: VALID_TRANSITION, }) diff --git a/homeassistant/components/lock/__init__.py b/homeassistant/components/lock/__init__.py index 22923602dc2..72e87f763d2 100644 --- a/homeassistant/components/lock/__init__.py +++ b/homeassistant/components/lock/__init__.py @@ -34,7 +34,7 @@ GROUP_NAME_ALL_LOCKS = 'all locks' MIN_TIME_BETWEEN_SCANS = timedelta(seconds=10) LOCK_SERVICE_SCHEMA = vol.Schema({ - vol.Optional(ATTR_ENTITY_ID): cv.entity_ids, + vol.Optional(ATTR_ENTITY_ID): cv.comp_entity_ids, vol.Optional(ATTR_CODE): cv.string, }) diff --git a/homeassistant/components/media_player/__init__.py b/homeassistant/components/media_player/__init__.py index 8530a01d3e6..cd109cce7d3 100644 --- a/homeassistant/components/media_player/__init__.py +++ b/homeassistant/components/media_player/__init__.py @@ -117,7 +117,7 @@ SUPPORT_SELECT_SOUND_MODE = 65536 # Service call validation schemas MEDIA_PLAYER_SCHEMA = vol.Schema({ - ATTR_ENTITY_ID: cv.entity_ids, + ATTR_ENTITY_ID: cv.comp_entity_ids, }) MEDIA_PLAYER_SET_VOLUME_SCHEMA = MEDIA_PLAYER_SCHEMA.extend({ diff --git a/homeassistant/components/remote/__init__.py b/homeassistant/components/remote/__init__.py index 4fc491e57e8..162cb41d92e 100644 --- a/homeassistant/components/remote/__init__.py +++ b/homeassistant/components/remote/__init__.py @@ -46,7 +46,7 @@ DEFAULT_NUM_REPEATS = 1 DEFAULT_DELAY_SECS = 0.4 REMOTE_SERVICE_SCHEMA = vol.Schema({ - vol.Optional(ATTR_ENTITY_ID): cv.entity_ids, + vol.Optional(ATTR_ENTITY_ID): cv.comp_entity_ids, }) REMOTE_SERVICE_ACTIVITY_SCHEMA = REMOTE_SERVICE_SCHEMA.extend({ diff --git a/homeassistant/components/scene/__init__.py b/homeassistant/components/scene/__init__.py index 2bcb1c8e16d..b3ab5228875 100644 --- a/homeassistant/components/scene/__init__.py +++ b/homeassistant/components/scene/__init__.py @@ -56,7 +56,7 @@ PLATFORM_SCHEMA = vol.Schema( ), extra=vol.ALLOW_EXTRA) SCENE_SERVICE_SCHEMA = vol.Schema({ - vol.Optional(ATTR_ENTITY_ID): cv.entity_ids, + vol.Required(ATTR_ENTITY_ID): cv.entity_ids, }) diff --git a/homeassistant/components/switch/__init__.py b/homeassistant/components/switch/__init__.py index 1adabe4b57e..513ebbcb5ea 100644 --- a/homeassistant/components/switch/__init__.py +++ b/homeassistant/components/switch/__init__.py @@ -39,7 +39,7 @@ PROP_TO_ATTR = { } SWITCH_SERVICE_SCHEMA = vol.Schema({ - vol.Optional(ATTR_ENTITY_ID): cv.entity_ids, + vol.Optional(ATTR_ENTITY_ID): cv.comp_entity_ids, }) _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/vacuum/__init__.py b/homeassistant/components/vacuum/__init__.py index 212e6bd648f..4799e945be0 100644 --- a/homeassistant/components/vacuum/__init__.py +++ b/homeassistant/components/vacuum/__init__.py @@ -49,7 +49,7 @@ SERVICE_PAUSE = 'pause' SERVICE_STOP = 'stop' VACUUM_SERVICE_SCHEMA = vol.Schema({ - vol.Optional(ATTR_ENTITY_ID): cv.entity_ids, + vol.Optional(ATTR_ENTITY_ID): cv.comp_entity_ids, }) VACUUM_SET_FAN_SPEED_SERVICE_SCHEMA = VACUUM_SERVICE_SCHEMA.extend({ diff --git a/homeassistant/components/water_heater/__init__.py b/homeassistant/components/water_heater/__init__.py index 92dbebc4421..fee2846e8d5 100644 --- a/homeassistant/components/water_heater/__init__.py +++ b/homeassistant/components/water_heater/__init__.py @@ -57,22 +57,22 @@ CONVERTIBLE_ATTRIBUTE = [ _LOGGER = logging.getLogger(__name__) ON_OFF_SERVICE_SCHEMA = vol.Schema({ - vol.Optional(ATTR_ENTITY_ID): cv.entity_ids, + vol.Optional(ATTR_ENTITY_ID): cv.comp_entity_ids, }) SET_AWAY_MODE_SCHEMA = vol.Schema({ - vol.Optional(ATTR_ENTITY_ID): cv.entity_ids, + vol.Optional(ATTR_ENTITY_ID): cv.comp_entity_ids, vol.Required(ATTR_AWAY_MODE): cv.boolean, }) SET_TEMPERATURE_SCHEMA = vol.Schema(vol.All( { vol.Required(ATTR_TEMPERATURE, 'temperature'): vol.Coerce(float), - vol.Optional(ATTR_ENTITY_ID): cv.entity_ids, + vol.Optional(ATTR_ENTITY_ID): cv.comp_entity_ids, vol.Optional(ATTR_OPERATION_MODE): cv.string, } )) SET_OPERATION_MODE_SCHEMA = vol.Schema({ - vol.Optional(ATTR_ENTITY_ID): cv.entity_ids, + vol.Optional(ATTR_ENTITY_ID): cv.comp_entity_ids, vol.Required(ATTR_OPERATION_MODE): cv.string, }) diff --git a/homeassistant/const.py b/homeassistant/const.py index 3f24da30a0a..a03b5fe52bc 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -13,6 +13,9 @@ PLATFORM_FORMAT = '{}.{}' # Can be used to specify a catch all when registering state or event listeners. MATCH_ALL = '*' +# Entity target all constant +ENTITY_MATCH_ALL = 'all' + # If no name is specified DEVICE_DEFAULT_NAME = 'Unnamed Device' diff --git a/homeassistant/helpers/config_validation.py b/homeassistant/helpers/config_validation.py index 5c49a1b50e1..c14f4e4fadb 100644 --- a/homeassistant/helpers/config_validation.py +++ b/homeassistant/helpers/config_validation.py @@ -15,7 +15,8 @@ from homeassistant.const import ( CONF_PLATFORM, CONF_SCAN_INTERVAL, TEMP_CELSIUS, TEMP_FAHRENHEIT, 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) + SUN_EVENT_SUNRISE, CONF_UNIT_SYSTEM_IMPERIAL, CONF_UNIT_SYSTEM_METRIC, + ENTITY_MATCH_ALL) from homeassistant.core import valid_entity_id, split_entity_id from homeassistant.exceptions import TemplateError import homeassistant.util.dt as dt_util @@ -161,6 +162,12 @@ def entity_ids(value: Union[str, Sequence]) -> Sequence[str]: return [entity_id(ent_id) for ent_id in value] +comp_entity_ids = vol.Any( + vol.All(vol.Lower, ENTITY_MATCH_ALL), + entity_ids +) + + def entity_domain(domain: str): """Validate that entity belong to domain.""" def validate(value: Any) -> str: diff --git a/homeassistant/helpers/entity_component.py b/homeassistant/helpers/entity_component.py index 982c92510a9..ce876991097 100644 --- a/homeassistant/helpers/entity_component.py +++ b/homeassistant/helpers/entity_component.py @@ -7,7 +7,7 @@ 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) + ATTR_ENTITY_ID, CONF_SCAN_INTERVAL, CONF_ENTITY_NAMESPACE, MATCH_ALL) from homeassistant.core import callback from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import config_per_platform, discovery @@ -161,7 +161,15 @@ class EntityComponent: This method must be run in the event loop. """ - if ATTR_ENTITY_ID not in service.data: + data_ent_id = service.data.get(ATTR_ENTITY_ID) + + if data_ent_id in (None, MATCH_ALL): + if data_ent_id is None: + 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) + return [entity for entity in self.entities if entity.available] entity_ids = set(extract_entity_ids(self.hass, service, expand_group)) diff --git a/homeassistant/helpers/service.py b/homeassistant/helpers/service.py index e8068f57286..f51d0f8b248 100644 --- a/homeassistant/helpers/service.py +++ b/homeassistant/helpers/service.py @@ -6,7 +6,7 @@ from os import path import voluptuous as vol from homeassistant.auth.permissions.const import POLICY_CONTROL -from homeassistant.const import ATTR_ENTITY_ID +from homeassistant.const import ATTR_ENTITY_ID, ENTITY_MATCH_ALL import homeassistant.core as ha from homeassistant.exceptions import TemplateError, Unauthorized, UnknownUser from homeassistant.helpers import template @@ -197,7 +197,12 @@ async def entity_service_call(hass, platforms, func, call): entity_perms = None # Are we trying to target all entities - target_all_entities = ATTR_ENTITY_ID not in call.data + if ATTR_ENTITY_ID in call.data: + target_all_entities = call.data[ATTR_ENTITY_ID] == ENTITY_MATCH_ALL + else: + _LOGGER.warning('Not passing an entity ID to a service to target all ' + 'entities is deprecated. Use instead: entity_id: "*"') + target_all_entities = True if not target_all_entities: # A set of entities we're trying to target. diff --git a/tests/helpers/test_config_validation.py b/tests/helpers/test_config_validation.py index cfd84dbc3b3..412882f0a01 100644 --- a/tests/helpers/test_config_validation.py +++ b/tests/helpers/test_config_validation.py @@ -584,3 +584,16 @@ def test_is_regex(): valid_re = ".*" schema(valid_re) + + +def test_comp_entity_ids(): + """Test config validation for component entity IDs.""" + schema = vol.Schema(cv.comp_entity_ids) + + for valid in ('ALL', 'all', 'AlL', 'light.kitchen', ['light.kitchen'], + ['light.kitchen', 'light.ceiling'], []): + schema(valid) + + for invalid in (['light.kitchen', 'not-entity-id'], '*', ''): + with pytest.raises(vol.Invalid): + schema(invalid) diff --git a/tests/helpers/test_entity_component.py b/tests/helpers/test_entity_component.py index 7562a38d268..8f54f0ee5bc 100644 --- a/tests/helpers/test_entity_component.py +++ b/tests/helpers/test_entity_component.py @@ -452,3 +452,37 @@ async def test_set_service_race(hass): await hass.async_block_till_done() assert not exception + + +async def test_extract_all_omit_entity_id(hass, caplog): + """Test extract all with None and *.""" + component = EntityComponent(_LOGGER, DOMAIN, hass) + await component.async_add_entities([ + MockEntity(name='test_1'), + MockEntity(name='test_2'), + ]) + + call = ha.ServiceCall('test', 'service') + + assert ['test_domain.test_1', 'test_domain.test_2'] == \ + sorted(ent.entity_id for ent in + component.async_extract_from_service(call)) + assert ('Not passing an entity ID to a service to target all entities is ' + 'deprecated') in caplog.text + + +async def test_extract_all_use_match_all(hass, caplog): + """Test extract all with None and *.""" + component = EntityComponent(_LOGGER, DOMAIN, hass) + await component.async_add_entities([ + MockEntity(name='test_1'), + MockEntity(name='test_2'), + ]) + + call = ha.ServiceCall('test', 'service', {'entity_id': '*'}) + + assert ['test_domain.test_1', 'test_domain.test_2'] == \ + sorted(ent.entity_id for ent in + component.async_extract_from_service(call)) + assert ('Not passing an entity ID to a service to target all entities is ' + 'deprecated') not in caplog.text diff --git a/tests/helpers/test_service.py b/tests/helpers/test_service.py index 8fca7df69c1..35e89fc5218 100644 --- a/tests/helpers/test_service.py +++ b/tests/helpers/test_service.py @@ -306,3 +306,35 @@ async def test_call_no_context_target_specific( assert len(mock_service_platform_call.mock_calls) == 1 entities = mock_service_platform_call.mock_calls[0][1][2] assert entities == [mock_entities['light.kitchen']] + + +async def test_call_with_match_all(hass, mock_service_platform_call, + mock_entities, caplog): + """Check we only target allowed entities if targetting all.""" + await service.entity_service_call(hass, [ + Mock(entities=mock_entities) + ], Mock(), ha.ServiceCall('test_domain', 'test_service', { + 'entity_id': 'all' + })) + + assert len(mock_service_platform_call.mock_calls) == 1 + entities = mock_service_platform_call.mock_calls[0][1][2] + assert entities == [ + mock_entities['light.kitchen'], mock_entities['light.living_room']] + assert ('Not passing an entity ID to a service to target ' + 'all entities is deprecated') not in caplog.text + + +async def test_call_with_omit_entity_id(hass, mock_service_platform_call, + mock_entities, caplog): + """Check we only target allowed entities if targetting all.""" + await service.entity_service_call(hass, [ + Mock(entities=mock_entities) + ], Mock(), ha.ServiceCall('test_domain', 'test_service')) + + assert len(mock_service_platform_call.mock_calls) == 1 + entities = mock_service_platform_call.mock_calls[0][1][2] + assert entities == [ + mock_entities['light.kitchen'], mock_entities['light.living_room']] + assert ('Not passing an entity ID to a service to target ' + 'all entities is deprecated') in caplog.text From 9d9e11372b6df9399c33d4854e14e12086d00b29 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 13 Dec 2018 12:21:16 +0100 Subject: [PATCH 071/304] Make automations log errors (#18965) --- .../components/automation/__init__.py | 10 ++++- homeassistant/helpers/script.py | 44 +++++++++++++++++-- homeassistant/helpers/service.py | 4 ++ tests/components/automation/test_init.py | 21 +++++++++ tests/helpers/test_script.py | 35 +++++++++++++++ 5 files changed, 110 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/automation/__init__.py b/homeassistant/components/automation/__init__.py index d062376f2a8..6c9b04f9fa2 100644 --- a/homeassistant/components/automation/__init__.py +++ b/homeassistant/components/automation/__init__.py @@ -375,7 +375,15 @@ def _async_get_action(hass, config, name): async def action(entity_id, variables, context): """Execute an action.""" _LOGGER.info('Executing %s', name) - await script_obj.async_run(variables, context) + hass.components.logbook.async_log_entry( + name, 'has been triggered', DOMAIN, entity_id) + + try: + await script_obj.async_run(variables, context) + except Exception as err: # pylint: disable=broad-except + script_obj.async_log_exception( + _LOGGER, + 'Error while executing automation {}'.format(entity_id), err) return action diff --git a/homeassistant/helpers/script.py b/homeassistant/helpers/script.py index 088882df608..e4693c3cd3b 100644 --- a/homeassistant/helpers/script.py +++ b/homeassistant/helpers/script.py @@ -85,6 +85,7 @@ class Script(): self.name = name self._change_listener = change_listener self._cur = -1 + self._exception_step = None self.last_action = None self.last_triggered = None self.can_cancel = any(CONF_DELAY in action or CONF_WAIT_TEMPLATE @@ -136,10 +137,9 @@ class Script(): return except _StopScript: break - except Exception as err: + except Exception: # Store the step that had an exception - # pylint: disable=protected-access - err._script_step = cur + self._exception_step = cur # Set script to not running self._cur = -1 self.last_action = None @@ -166,6 +166,44 @@ class Script(): if self._change_listener: self.hass.async_add_job(self._change_listener) + @callback + def async_log_exception(self, logger, message_base, exception): + """Log an exception for this script. + + Should only be called on exceptions raised by this scripts async_run. + """ + # pylint: disable=protected-access + step = self._exception_step + action = self.sequence[step] + action_type = _determine_action(action) + + error = None + meth = logger.error + + if isinstance(exception, vol.Invalid): + error_desc = "Invalid data" + + elif isinstance(exception, exceptions.TemplateError): + error_desc = "Error rendering template" + + elif isinstance(exception, exceptions.Unauthorized): + error_desc = "Unauthorized" + + elif isinstance(exception, exceptions.ServiceNotFound): + error_desc = "Service not found" + + else: + # Print the full stack trace, unknown error + error_desc = 'Unknown error' + meth = logger.exception + error = "" + + if error is None: + error = str(exception) + + meth("%s. %s for %s at pos %s: %s", + message_base, error_desc, action_type, step + 1, error) + async def _handle_action(self, action, variables, context): """Handle an action.""" await self._actions[_determine_action(action)]( diff --git a/homeassistant/helpers/service.py b/homeassistant/helpers/service.py index f51d0f8b248..66488fbec3d 100644 --- a/homeassistant/helpers/service.py +++ b/homeassistant/helpers/service.py @@ -55,9 +55,13 @@ async def async_call_from_config(hass, config, blocking=False, variables=None, variables) domain_service = cv.service(domain_service) except TemplateError as ex: + if blocking: + raise _LOGGER.error('Error rendering service name template: %s', ex) return except vol.Invalid: + if blocking: + raise _LOGGER.error('Template rendered invalid service: %s', domain_service) return diff --git a/tests/components/automation/test_init.py b/tests/components/automation/test_init.py index a01b48b9190..12c97507a13 100644 --- a/tests/components/automation/test_init.py +++ b/tests/components/automation/test_init.py @@ -864,3 +864,24 @@ def test_automation_not_trigger_on_bootstrap(hass): assert len(calls) == 1 assert ['hello.world'] == calls[0].data.get(ATTR_ENTITY_ID) + + +async def test_automation_with_error_in_script(hass, caplog): + """Test automation with an error in script.""" + assert await async_setup_component(hass, automation.DOMAIN, { + automation.DOMAIN: { + 'alias': 'hello', + 'trigger': { + 'platform': 'event', + 'event_type': 'test_event', + }, + 'action': { + 'service': 'test.automation', + 'entity_id': 'hello.world' + } + } + }) + + hass.bus.async_fire('test_event') + await hass.async_block_till_done() + assert 'Service test.automation not found' in caplog.text diff --git a/tests/helpers/test_script.py b/tests/helpers/test_script.py index 887a147c417..d04044d9b60 100644 --- a/tests/helpers/test_script.py +++ b/tests/helpers/test_script.py @@ -4,6 +4,7 @@ from datetime import timedelta from unittest import mock import unittest +import jinja2 import voluptuous as vol import pytest @@ -798,6 +799,7 @@ async def test_propagate_error_service_not_found(hass): await script_obj.async_run() assert len(events) == 0 + assert script_obj._cur == -1 async def test_propagate_error_invalid_service_data(hass): @@ -829,6 +831,7 @@ async def test_propagate_error_invalid_service_data(hass): assert len(events) == 0 assert len(calls) == 0 + assert script_obj._cur == -1 async def test_propagate_error_service_exception(hass): @@ -859,3 +862,35 @@ async def test_propagate_error_service_exception(hass): assert len(events) == 0 assert len(calls) == 0 + assert script_obj._cur == -1 + + +def test_log_exception(): + """Test logged output.""" + script_obj = script.Script(None, cv.SCRIPT_SCHEMA([ + {'service': 'test.script'}, + {'event': 'test_event'}])) + script_obj._exception_step = 1 + + for exc, msg in ( + (vol.Invalid("Invalid number"), 'Invalid data'), + (exceptions.TemplateError(jinja2.TemplateError('Unclosed bracket')), + 'Error rendering template'), + (exceptions.Unauthorized(), 'Unauthorized'), + (exceptions.ServiceNotFound('light', 'turn_on'), 'Service not found'), + (ValueError("Cannot parse JSON"), 'Unknown error'), + ): + logger = mock.Mock() + script_obj.async_log_exception(logger, 'Test error', exc) + + assert len(logger.mock_calls) == 1 + p_format, p_msg_base, p_error_desc, p_action_type, p_step, p_error = \ + logger.mock_calls[0][1] + + assert p_error_desc == msg + assert p_action_type == script.ACTION_FIRE_EVENT + assert p_step == 2 + if isinstance(exc, ValueError): + assert p_error == "" + else: + assert p_error == str(exc) From 6766d25e62e7f886fe329037b9d76ebd7e8cce8e Mon Sep 17 00:00:00 2001 From: Erik Eriksson <8228319+molobrakos@users.noreply.github.com> Date: Thu, 13 Dec 2018 12:25:40 +0100 Subject: [PATCH 072/304] Re-use connection-pool (#19249) Re-use connection-pool of VOC --- homeassistant/components/volvooncall.py | 14 +++++++++----- requirements_all.txt | 2 +- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/volvooncall.py b/homeassistant/components/volvooncall.py index 75339171cbc..8e9fd087564 100644 --- a/homeassistant/components/volvooncall.py +++ b/homeassistant/components/volvooncall.py @@ -15,6 +15,7 @@ from homeassistant.helpers import discovery import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity from homeassistant.helpers.event import async_track_point_in_utc_time +from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.dispatcher import ( async_dispatcher_send, async_dispatcher_connect) @@ -24,7 +25,7 @@ DOMAIN = 'volvooncall' DATA_KEY = DOMAIN -REQUIREMENTS = ['volvooncall==0.7.11'] +REQUIREMENTS = ['volvooncall==0.8.2'] _LOGGER = logging.getLogger(__name__) @@ -106,12 +107,15 @@ CONFIG_SCHEMA = vol.Schema({ async def async_setup(hass, config): """Set up the Volvo On Call component.""" + session = async_get_clientsession(hass) + from volvooncall import Connection connection = Connection( - config[DOMAIN].get(CONF_USERNAME), - config[DOMAIN].get(CONF_PASSWORD), - config[DOMAIN].get(CONF_SERVICE_URL), - config[DOMAIN].get(CONF_REGION)) + session=session, + username=config[DOMAIN].get(CONF_USERNAME), + password=config[DOMAIN].get(CONF_PASSWORD), + service_url=config[DOMAIN].get(CONF_SERVICE_URL), + region=config[DOMAIN].get(CONF_REGION)) interval = config[DOMAIN].get(CONF_UPDATE_INTERVAL) diff --git a/requirements_all.txt b/requirements_all.txt index 25b32500734..fe71b03fbd6 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1591,7 +1591,7 @@ venstarcolortouch==0.6 volkszaehler==0.1.2 # homeassistant.components.volvooncall -volvooncall==0.7.11 +volvooncall==0.8.2 # homeassistant.components.verisure vsure==1.5.2 From 7436c0fe428e169e2aaf142d4142a01514db9afd Mon Sep 17 00:00:00 2001 From: emontnemery Date: Thu, 13 Dec 2018 15:51:50 +0100 Subject: [PATCH 073/304] Add device registry to MQTT light (#19013) --- .../components/light/mqtt/schema_basic.py | 13 ++-- .../components/light/mqtt/schema_json.py | 14 ++-- .../components/light/mqtt/schema_template.py | 21 ++++-- tests/components/light/test_mqtt.py | 65 ++++++++++++++++- tests/components/light/test_mqtt_json.py | 70 +++++++++++++++++- tests/components/light/test_mqtt_template.py | 72 ++++++++++++++++++- 6 files changed, 239 insertions(+), 16 deletions(-) diff --git a/homeassistant/components/light/mqtt/schema_basic.py b/homeassistant/components/light/mqtt/schema_basic.py index 74f3dbdec91..91fe2549051 100644 --- a/homeassistant/components/light/mqtt/schema_basic.py +++ b/homeassistant/components/light/mqtt/schema_basic.py @@ -15,13 +15,13 @@ from homeassistant.components.light import ( ATTR_WHITE_VALUE, Light, SUPPORT_BRIGHTNESS, SUPPORT_COLOR_TEMP, SUPPORT_EFFECT, SUPPORT_COLOR, SUPPORT_WHITE_VALUE) from homeassistant.const import ( - CONF_BRIGHTNESS, CONF_COLOR_TEMP, CONF_EFFECT, CONF_HS, CONF_NAME, - CONF_OPTIMISTIC, CONF_PAYLOAD_OFF, CONF_PAYLOAD_ON, STATE_ON, + CONF_BRIGHTNESS, CONF_COLOR_TEMP, CONF_DEVICE, CONF_EFFECT, CONF_HS, + CONF_NAME, CONF_OPTIMISTIC, CONF_PAYLOAD_OFF, CONF_PAYLOAD_ON, STATE_ON, CONF_RGB, CONF_STATE, CONF_VALUE_TEMPLATE, CONF_WHITE_VALUE, CONF_XY) from homeassistant.components.mqtt import ( CONF_AVAILABILITY_TOPIC, CONF_COMMAND_TOPIC, CONF_PAYLOAD_AVAILABLE, CONF_PAYLOAD_NOT_AVAILABLE, CONF_QOS, CONF_RETAIN, CONF_STATE_TOPIC, - MqttAvailability, MqttDiscoveryUpdate, subscription) + 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 @@ -105,6 +105,7 @@ PLATFORM_SCHEMA_BASIC = mqtt.MQTT_RW_PLATFORM_SCHEMA.extend({ vol.Optional(CONF_XY_VALUE_TEMPLATE): cv.template, 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) @@ -117,7 +118,9 @@ async def async_setup_entity_basic(hass, config, async_add_entities, async_add_entities([MqttLight(config, discovery_hash)]) -class MqttLight(MqttAvailability, MqttDiscoveryUpdate, Light, RestoreEntity): +# pylint: disable=too-many-ancestors +class MqttLight(MqttAvailability, MqttDiscoveryUpdate, MqttEntityDeviceInfo, + Light, RestoreEntity): """Representation of a MQTT light.""" def __init__(self, config, discovery_hash): @@ -151,11 +154,13 @@ class MqttLight(MqttAvailability, MqttDiscoveryUpdate, Light, RestoreEntity): payload_available = config.get(CONF_PAYLOAD_AVAILABLE) payload_not_available = config.get(CONF_PAYLOAD_NOT_AVAILABLE) qos = config.get(CONF_QOS) + device_config = config.get(CONF_DEVICE) MqttAvailability.__init__(self, availability_topic, qos, payload_available, payload_not_available) MqttDiscoveryUpdate.__init__(self, discovery_hash, self.discovery_update) + MqttEntityDeviceInfo.__init__(self, device_config) async def async_added_to_hass(self): """Subscribe to MQTT events.""" diff --git a/homeassistant/components/light/mqtt/schema_json.py b/homeassistant/components/light/mqtt/schema_json.py index 8a72f7b1f89..0f2fd5ddf60 100644 --- a/homeassistant/components/light/mqtt/schema_json.py +++ b/homeassistant/components/light/mqtt/schema_json.py @@ -18,10 +18,10 @@ from homeassistant.components.light import ( from homeassistant.components.mqtt import ( CONF_AVAILABILITY_TOPIC, CONF_COMMAND_TOPIC, CONF_PAYLOAD_AVAILABLE, CONF_PAYLOAD_NOT_AVAILABLE, CONF_QOS, CONF_RETAIN, CONF_STATE_TOPIC, - MqttAvailability, MqttDiscoveryUpdate, subscription) + MqttAvailability, MqttDiscoveryUpdate, MqttEntityDeviceInfo, subscription) from homeassistant.const import ( - CONF_BRIGHTNESS, CONF_COLOR_TEMP, CONF_EFFECT, CONF_NAME, CONF_OPTIMISTIC, - CONF_RGB, CONF_WHITE_VALUE, CONF_XY, STATE_ON) + CONF_BRIGHTNESS, CONF_COLOR_TEMP, CONF_DEVICE, CONF_EFFECT, CONF_NAME, + CONF_OPTIMISTIC, CONF_RGB, CONF_WHITE_VALUE, CONF_XY, STATE_ON) from homeassistant.core import callback import homeassistant.helpers.config_validation as cv from homeassistant.helpers.restore_state import RestoreEntity @@ -80,6 +80,7 @@ PLATFORM_SCHEMA_JSON = mqtt.MQTT_RW_PLATFORM_SCHEMA.extend({ vol.Optional(CONF_XY, default=DEFAULT_XY): cv.boolean, vol.Optional(CONF_HS, default=DEFAULT_HS): cv.boolean, vol.Required(CONF_COMMAND_TOPIC): mqtt.valid_publish_topic, + vol.Optional(CONF_DEVICE): mqtt.MQTT_ENTITY_DEVICE_INFO_SCHEMA, }).extend(mqtt.MQTT_AVAILABILITY_SCHEMA.schema) @@ -89,8 +90,9 @@ async def async_setup_entity_json(hass: HomeAssistantType, config: ConfigType, async_add_entities([MqttLightJson(config, discovery_hash)]) -class MqttLightJson(MqttAvailability, MqttDiscoveryUpdate, Light, - RestoreEntity): +# pylint: disable=too-many-ancestors +class MqttLightJson(MqttAvailability, MqttDiscoveryUpdate, + MqttEntityDeviceInfo, Light, RestoreEntity): """Representation of a MQTT JSON light.""" def __init__(self, config, discovery_hash): @@ -116,11 +118,13 @@ class MqttLightJson(MqttAvailability, MqttDiscoveryUpdate, Light, payload_available = config.get(CONF_PAYLOAD_AVAILABLE) payload_not_available = config.get(CONF_PAYLOAD_NOT_AVAILABLE) qos = config.get(CONF_QOS) + device_config = config.get(CONF_DEVICE) MqttAvailability.__init__(self, availability_topic, qos, payload_available, payload_not_available) MqttDiscoveryUpdate.__init__(self, discovery_hash, self.discovery_update) + MqttEntityDeviceInfo.__init__(self, device_config) async def async_added_to_hass(self): """Subscribe to MQTT events.""" diff --git a/homeassistant/components/light/mqtt/schema_template.py b/homeassistant/components/light/mqtt/schema_template.py index 419472d1927..4626ab2c7c2 100644 --- a/homeassistant/components/light/mqtt/schema_template.py +++ b/homeassistant/components/light/mqtt/schema_template.py @@ -14,11 +14,12 @@ from homeassistant.components.light import ( ATTR_HS_COLOR, ATTR_TRANSITION, ATTR_WHITE_VALUE, Light, SUPPORT_BRIGHTNESS, SUPPORT_COLOR_TEMP, SUPPORT_EFFECT, SUPPORT_FLASH, SUPPORT_COLOR, SUPPORT_TRANSITION, SUPPORT_WHITE_VALUE) -from homeassistant.const import CONF_NAME, CONF_OPTIMISTIC, STATE_ON, STATE_OFF +from homeassistant.const import ( + CONF_DEVICE, CONF_NAME, CONF_OPTIMISTIC, STATE_ON, STATE_OFF) from homeassistant.components.mqtt import ( CONF_AVAILABILITY_TOPIC, CONF_STATE_TOPIC, CONF_COMMAND_TOPIC, CONF_PAYLOAD_AVAILABLE, CONF_PAYLOAD_NOT_AVAILABLE, CONF_QOS, CONF_RETAIN, - MqttAvailability, MqttDiscoveryUpdate, subscription) + 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 @@ -43,6 +44,7 @@ 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, @@ -63,6 +65,8 @@ PLATFORM_SCHEMA_TEMPLATE = mqtt.MQTT_RW_PLATFORM_SCHEMA.extend({ vol.Required(CONF_COMMAND_TOPIC): mqtt.valid_publish_topic, vol.Optional(CONF_QOS, default=mqtt.DEFAULT_QOS): 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) @@ -72,8 +76,9 @@ async def async_setup_entity_template(hass, config, async_add_entities, async_add_entities([MqttTemplate(config, discovery_hash)]) -class MqttTemplate(MqttAvailability, MqttDiscoveryUpdate, Light, - RestoreEntity): +# pylint: disable=too-many-ancestors +class MqttTemplate(MqttAvailability, MqttDiscoveryUpdate, MqttEntityDeviceInfo, + Light, RestoreEntity): """Representation of a MQTT Template light.""" def __init__(self, config, discovery_hash): @@ -91,6 +96,7 @@ class MqttTemplate(MqttAvailability, MqttDiscoveryUpdate, Light, self._white_value = None self._hs = None self._effect = None + self._unique_id = config.get(CONF_UNIQUE_ID) # Load config self._setup_from_config(config) @@ -99,11 +105,13 @@ class MqttTemplate(MqttAvailability, MqttDiscoveryUpdate, Light, payload_available = config.get(CONF_PAYLOAD_AVAILABLE) payload_not_available = config.get(CONF_PAYLOAD_NOT_AVAILABLE) qos = config.get(CONF_QOS) + device_config = config.get(CONF_DEVICE) MqttAvailability.__init__(self, availability_topic, qos, payload_available, payload_not_available) MqttDiscoveryUpdate.__init__(self, discovery_hash, self.discovery_update) + MqttEntityDeviceInfo.__init__(self, device_config) async def async_added_to_hass(self): """Subscribe to MQTT events.""" @@ -302,6 +310,11 @@ class MqttTemplate(MqttAvailability, MqttDiscoveryUpdate, Light, """Return the name of the entity.""" return self._config.get(CONF_NAME) + @property + def unique_id(self): + """Return a unique ID.""" + return self._unique_id + @property def is_on(self): """Return True if entity is on.""" diff --git a/tests/components/light/test_mqtt.py b/tests/components/light/test_mqtt.py index 9e4fa3ebc79..80c872f55b9 100644 --- a/tests/components/light/test_mqtt.py +++ b/tests/components/light/test_mqtt.py @@ -153,6 +153,7 @@ light: payload_off: "off" """ +import json from unittest import mock from unittest.mock import patch @@ -165,7 +166,7 @@ import homeassistant.core as ha from tests.common import ( assert_setup_component, async_fire_mqtt_message, - mock_coro, MockConfigEntry) + async_mock_mqtt_component, mock_coro, MockConfigEntry) from tests.components.light import common @@ -1038,6 +1039,29 @@ async def test_custom_availability_payload(hass, mqtt_mock): assert STATE_UNAVAILABLE == state.state +async def test_unique_id(hass): + """Test unique id option only creates one light per unique_id.""" + await async_mock_mqtt_component(hass) + assert await async_setup_component(hass, light.DOMAIN, { + light.DOMAIN: [{ + 'platform': 'mqtt', + 'name': 'Test 1', + 'status_topic': 'test-topic', + 'command_topic': 'test_topic', + 'unique_id': 'TOTALLY_UNIQUE' + }, { + 'platform': 'mqtt', + 'name': 'Test 2', + 'status_topic': 'test-topic', + 'command_topic': 'test_topic', + 'unique_id': 'TOTALLY_UNIQUE' + }] + }) + async_fire_mqtt_message(hass, 'test-topic', 'payload') + await hass.async_block_till_done() + assert len(hass.states.async_entity_ids(light.DOMAIN)) == 1 + + async def test_discovery_removal_light(hass, mqtt_mock, caplog): """Test removal of discovered light.""" entry = MockConfigEntry(domain=mqtt.DOMAIN) @@ -1117,3 +1141,42 @@ async def test_discovery_update_light(hass, mqtt_mock, caplog): assert state.name == 'Milk' state = hass.states.get('light.milk') 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) + entry.add_to_hass(hass) + await async_start(hass, 'homeassistant', {}, entry) + registry = await hass.helpers.device_registry.async_get_registry() + + data = json.dumps({ + 'platform': 'mqtt', + 'name': 'Test 1', + 'state_topic': 'test-topic', + 'command_topic': 'test-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' + }) + 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.identifiers == {('mqtt', 'helloworld')} + assert device.connections == {('mac', "02:5b:26:a8:dc:12")} + assert device.manufacturer == 'Whatever' + assert device.name == 'Beer' + assert device.model == 'Glass' + assert device.sw_version == '0.1-beta' diff --git a/tests/components/light/test_mqtt_json.py b/tests/components/light/test_mqtt_json.py index 8567dfd7921..a4906cb586f 100644 --- a/tests/components/light/test_mqtt_json.py +++ b/tests/components/light/test_mqtt_json.py @@ -87,6 +87,7 @@ light: brightness: true brightness_scale: 99 """ +import json from unittest.mock import patch from homeassistant.setup import async_setup_component @@ -97,7 +98,9 @@ from homeassistant.components import light, mqtt from homeassistant.components.mqtt.discovery import async_start import homeassistant.core as ha -from tests.common import mock_coro, async_fire_mqtt_message, MockConfigEntry +from tests.common import ( + mock_coro, async_fire_mqtt_message, async_mock_mqtt_component, + MockConfigEntry) async def test_fail_setup_if_no_command_topic(hass, mqtt_mock): @@ -533,6 +536,31 @@ async def test_custom_availability_payload(hass, mqtt_mock): assert STATE_UNAVAILABLE == state.state +async def test_unique_id(hass): + """Test unique id option only creates one light per unique_id.""" + await async_mock_mqtt_component(hass) + assert await async_setup_component(hass, light.DOMAIN, { + light.DOMAIN: [{ + 'platform': 'mqtt', + 'name': 'Test 1', + 'schema': 'json', + 'status_topic': 'test-topic', + 'command_topic': 'test_topic', + 'unique_id': 'TOTALLY_UNIQUE' + }, { + 'platform': 'mqtt', + 'name': 'Test 2', + 'schema': 'json', + 'status_topic': 'test-topic', + 'command_topic': 'test_topic', + 'unique_id': 'TOTALLY_UNIQUE' + }] + }) + async_fire_mqtt_message(hass, 'test-topic', 'payload') + await hass.async_block_till_done() + assert len(hass.states.async_entity_ids(light.DOMAIN)) == 1 + + async def test_discovery_removal(hass, mqtt_mock, caplog): """Test removal of discovered mqtt_json lights.""" entry = MockConfigEntry(domain=mqtt.DOMAIN) @@ -609,3 +637,43 @@ async def test_discovery_update_light(hass, mqtt_mock, caplog): assert state.name == 'Milk' state = hass.states.get('light.milk') 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) + entry.add_to_hass(hass) + await async_start(hass, 'homeassistant', {}, entry) + registry = await hass.helpers.device_registry.async_get_registry() + + data = json.dumps({ + 'platform': 'mqtt', + 'name': 'Test 1', + 'schema': 'json', + 'state_topic': 'test-topic', + 'command_topic': 'test-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' + }) + 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.identifiers == {('mqtt', 'helloworld')} + assert device.connections == {('mac', "02:5b:26:a8:dc:12")} + assert device.manufacturer == 'Whatever' + assert device.name == 'Beer' + assert device.model == 'Glass' + assert device.sw_version == '0.1-beta' diff --git a/tests/components/light/test_mqtt_template.py b/tests/components/light/test_mqtt_template.py index ce4a5f5a2e6..088b233ac4b 100644 --- a/tests/components/light/test_mqtt_template.py +++ b/tests/components/light/test_mqtt_template.py @@ -26,6 +26,7 @@ If your light doesn't support white value feature, omit `white_value_template`. If your light doesn't support RGB feature, omit `(red|green|blue)_template`. """ +import json from unittest.mock import patch from homeassistant.setup import async_setup_component @@ -37,7 +38,7 @@ import homeassistant.core as ha from tests.common import ( async_fire_mqtt_message, assert_setup_component, mock_coro, - MockConfigEntry) + async_mock_mqtt_component, MockConfigEntry) async def test_setup_fails(hass, mqtt_mock): @@ -484,6 +485,33 @@ async def test_custom_availability_payload(hass, mqtt_mock): assert STATE_UNAVAILABLE == state.state +async def test_unique_id(hass): + """Test unique id option only creates one light per unique_id.""" + await async_mock_mqtt_component(hass) + assert await async_setup_component(hass, light.DOMAIN, { + light.DOMAIN: [{ + 'platform': 'mqtt', + 'name': 'Test 1', + 'schema': 'template', + 'status_topic': 'test-topic', + 'command_topic': 'test_topic', + 'command_on_template': 'on,{{ transition }}', + 'command_off_template': 'off,{{ transition|d }}', + 'unique_id': 'TOTALLY_UNIQUE' + }, { + 'platform': 'mqtt', + 'name': 'Test 2', + 'schema': 'template', + 'status_topic': 'test-topic', + 'command_topic': 'test_topic', + 'unique_id': 'TOTALLY_UNIQUE' + }] + }) + async_fire_mqtt_message(hass, 'test-topic', 'payload') + await hass.async_block_till_done() + assert len(hass.states.async_entity_ids(light.DOMAIN)) == 1 + + async def test_discovery(hass, mqtt_mock, caplog): """Test removal of discovered mqtt_json lights.""" entry = MockConfigEntry(domain=mqtt.DOMAIN) @@ -562,3 +590,45 @@ async def test_discovery_update_light(hass, mqtt_mock, caplog): assert state.name == 'Milk' state = hass.states.get('light.milk') 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) + entry.add_to_hass(hass) + await async_start(hass, 'homeassistant', {}, entry) + registry = await hass.helpers.device_registry.async_get_registry() + + data = json.dumps({ + 'platform': 'mqtt', + 'name': 'Test 1', + 'schema': 'template', + 'state_topic': 'test-topic', + 'command_topic': 'test-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' + }) + 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.identifiers == {('mqtt', 'helloworld')} + assert device.connections == {('mac', "02:5b:26:a8:dc:12")} + assert device.manufacturer == 'Whatever' + assert device.name == 'Beer' + assert device.model == 'Glass' + assert device.sw_version == '0.1-beta' From 90df932fe13d2686d99065e0baaedcbe805a0b22 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 13 Dec 2018 15:30:20 +0100 Subject: [PATCH 074/304] Check admin permission before able to manage config entries --- homeassistant/auth/permissions/const.py | 1 + .../components/config/config_entries.py | 37 +++++ homeassistant/exceptions.py | 6 + .../components/config/test_config_entries.py | 146 ++++++++++++++++++ 4 files changed, 190 insertions(+) diff --git a/homeassistant/auth/permissions/const.py b/homeassistant/auth/permissions/const.py index e60879881c1..d390d010dee 100644 --- a/homeassistant/auth/permissions/const.py +++ b/homeassistant/auth/permissions/const.py @@ -1,5 +1,6 @@ """Permission constants.""" CAT_ENTITIES = 'entities' +CAT_CONFIG_ENTRIES = 'config_entries' SUBCAT_ALL = 'all' POLICY_READ = 'read' diff --git a/homeassistant/components/config/config_entries.py b/homeassistant/components/config/config_entries.py index 644990d7185..68890a79ca6 100644 --- a/homeassistant/components/config/config_entries.py +++ b/homeassistant/components/config/config_entries.py @@ -1,7 +1,9 @@ """Http views to control the config manager.""" from homeassistant import config_entries, data_entry_flow +from homeassistant.auth.permissions.const import CAT_CONFIG_ENTRIES from homeassistant.components.http import HomeAssistantView +from homeassistant.exceptions import Unauthorized from homeassistant.helpers.data_entry_flow import ( FlowManagerIndexView, FlowManagerResourceView) @@ -63,6 +65,9 @@ class ConfigManagerEntryResourceView(HomeAssistantView): async def delete(self, request, entry_id): """Delete a config entry.""" + if not request['hass_user'].is_admin: + raise Unauthorized(config_entry_id=entry_id, permission='remove') + hass = request.app['hass'] try: @@ -85,12 +90,26 @@ class ConfigManagerFlowIndexView(FlowManagerIndexView): Example of a non-user initiated flow is a discovered Hue hub that requires user interaction to finish setup. """ + if not request['hass_user'].is_admin: + raise Unauthorized( + perm_category=CAT_CONFIG_ENTRIES, permission='add') + hass = request.app['hass'] return self.json([ flw for flw in hass.config_entries.flow.async_progress() if flw['context']['source'] != config_entries.SOURCE_USER]) + # pylint: disable=arguments-differ + async def post(self, request): + """Handle a POST request.""" + if not request['hass_user'].is_admin: + raise Unauthorized( + perm_category=CAT_CONFIG_ENTRIES, permission='add') + + # pylint: disable=no-value-for-parameter + return await super().post(request) + class ConfigManagerFlowResourceView(FlowManagerResourceView): """View to interact with the flow manager.""" @@ -98,6 +117,24 @@ class ConfigManagerFlowResourceView(FlowManagerResourceView): url = '/api/config/config_entries/flow/{flow_id}' name = 'api:config:config_entries:flow:resource' + async def get(self, request, flow_id): + """Get the current state of a data_entry_flow.""" + if not request['hass_user'].is_admin: + raise Unauthorized( + perm_category=CAT_CONFIG_ENTRIES, permission='add') + + return await super().get(request, flow_id) + + # pylint: disable=arguments-differ + async def post(self, request, flow_id): + """Handle a POST request.""" + if not request['hass_user'].is_admin: + raise Unauthorized( + perm_category=CAT_CONFIG_ENTRIES, permission='add') + + # pylint: disable=no-value-for-parameter + return await super().post(request, flow_id) + class ConfigManagerAvailableFlowView(HomeAssistantView): """View to query available flows.""" diff --git a/homeassistant/exceptions.py b/homeassistant/exceptions.py index 5e2ab4988b1..aadee3e792b 100644 --- a/homeassistant/exceptions.py +++ b/homeassistant/exceptions.py @@ -47,12 +47,18 @@ class Unauthorized(HomeAssistantError): def __init__(self, context: Optional['Context'] = None, user_id: Optional[str] = None, entity_id: Optional[str] = None, + config_entry_id: Optional[str] = None, + perm_category: Optional[str] = None, permission: Optional[Tuple[str]] = None) -> None: """Unauthorized error.""" super().__init__(self.__class__.__name__) self.context = context self.user_id = user_id self.entity_id = entity_id + self.config_entry_id = config_entry_id + # Not all actions have an ID (like adding config entry) + # We then use this fallback to know what category was unauth + self.perm_category = perm_category self.permission = permission diff --git a/tests/components/config/test_config_entries.py b/tests/components/config/test_config_entries.py index 0b36cc6bc87..709dbce3c16 100644 --- a/tests/components/config/test_config_entries.py +++ b/tests/components/config/test_config_entries.py @@ -84,6 +84,17 @@ def test_remove_entry(hass, client): assert len(hass.config_entries.async_entries()) == 0 +async def test_remove_entry_unauth(hass, client, hass_admin_user): + """Test removing an entry via the API.""" + hass_admin_user.groups = [] + entry = MockConfigEntry(domain='demo', state=core_ce.ENTRY_STATE_LOADED) + entry.add_to_hass(hass) + resp = await client.delete( + '/api/config/config_entries/entry/{}'.format(entry.entry_id)) + assert resp.status == 401 + assert len(hass.config_entries.async_entries()) == 1 + + @asyncio.coroutine def test_available_flows(hass, client): """Test querying the available flows.""" @@ -155,6 +166,35 @@ def test_initialize_flow(hass, client): } +async def test_initialize_flow_unauth(hass, client, hass_admin_user): + """Test we can initialize a flow.""" + hass_admin_user.groups = [] + + class TestFlow(core_ce.ConfigFlow): + @asyncio.coroutine + def async_step_user(self, user_input=None): + schema = OrderedDict() + schema[vol.Required('username')] = str + schema[vol.Required('password')] = str + + return self.async_show_form( + step_id='user', + data_schema=schema, + description_placeholders={ + 'url': 'https://example.com', + }, + errors={ + 'username': 'Should be unique.' + } + ) + + with patch.dict(HANDLERS, {'test': TestFlow}): + resp = await client.post('/api/config/config_entries/flow', + json={'handler': 'test'}) + + assert resp.status == 401 + + @asyncio.coroutine def test_abort(hass, client): """Test a flow that aborts.""" @@ -273,6 +313,58 @@ def test_two_step_flow(hass, client): } +async def test_continue_flow_unauth(hass, client, hass_admin_user): + """Test we can't finish a two step flow.""" + set_component( + hass, 'test', + MockModule('test', async_setup_entry=mock_coro_func(True))) + + class TestFlow(core_ce.ConfigFlow): + VERSION = 1 + + @asyncio.coroutine + def async_step_user(self, user_input=None): + return self.async_show_form( + step_id='account', + data_schema=vol.Schema({ + 'user_title': str + })) + + @asyncio.coroutine + def async_step_account(self, user_input=None): + return self.async_create_entry( + title=user_input['user_title'], + data={'secret': 'account_token'}, + ) + + with patch.dict(HANDLERS, {'test': TestFlow}): + resp = await client.post('/api/config/config_entries/flow', + json={'handler': 'test'}) + assert resp.status == 200 + data = await resp.json() + flow_id = data.pop('flow_id') + assert data == { + 'type': 'form', + 'handler': 'test', + 'step_id': 'account', + 'data_schema': [ + { + 'name': 'user_title', + 'type': 'string' + } + ], + 'description_placeholders': None, + 'errors': None + } + + hass_admin_user.groups = [] + + resp = await client.post( + '/api/config/config_entries/flow/{}'.format(flow_id), + json={'user_title': 'user-title'}) + assert resp.status == 401 + + @asyncio.coroutine def test_get_progress_index(hass, client): """Test querying for the flows that are in progress.""" @@ -305,6 +397,29 @@ def test_get_progress_index(hass, client): ] +async def test_get_progress_index_unauth(hass, client, hass_admin_user): + """Test we can't get flows that are in progress.""" + hass_admin_user.groups = [] + + class TestFlow(core_ce.ConfigFlow): + VERSION = 5 + + async def async_step_hassio(self, info): + return (await self.async_step_account()) + + async def async_step_account(self, user_input=None): + return self.async_show_form( + step_id='account', + ) + + with patch.dict(HANDLERS, {'test': TestFlow}): + form = await hass.config_entries.flow.async_init( + 'test', context={'source': 'hassio'}) + + resp = await client.get('/api/config/config_entries/flow') + assert resp.status == 401 + + @asyncio.coroutine def test_get_progress_flow(hass, client): """Test we can query the API for same result as we get from init a flow.""" @@ -337,3 +452,34 @@ def test_get_progress_flow(hass, client): data2 = yield from resp2.json() assert data == data2 + + +async def test_get_progress_flow(hass, client, hass_admin_user): + """Test we can query the API for same result as we get from init a flow.""" + class TestFlow(core_ce.ConfigFlow): + async def async_step_user(self, user_input=None): + schema = OrderedDict() + schema[vol.Required('username')] = str + schema[vol.Required('password')] = str + + return self.async_show_form( + step_id='user', + data_schema=schema, + errors={ + 'username': 'Should be unique.' + } + ) + + with patch.dict(HANDLERS, {'test': TestFlow}): + resp = await client.post('/api/config/config_entries/flow', + json={'handler': 'test'}) + + assert resp.status == 200 + data = await resp.json() + + hass_admin_user.groups = [] + + resp2 = await client.get( + '/api/config/config_entries/flow/{}'.format(data['flow_id'])) + + assert resp2.status == 401 From 0fa71862965c67d02d4399300cc72c1b672dd9f0 Mon Sep 17 00:00:00 2001 From: Sander Geerts Date: Thu, 13 Dec 2018 16:31:14 +0100 Subject: [PATCH 075/304] Support for the Harman Kardon AVR (#18471) * Feature: support for the HK AVR * Remove testcode * Feature: support for the HK AVR * Remove testcode * Added checklist * Review fixes whitespaces * Lint fixes * Review fixes, add current source * Remove unused imports * Review fixes; State constants, dict[key] * More review fixes, Unknown state and Sources * Review fix; rename devices to entities --- .coveragerc | 1 + .../media_player/harman_kardon_avr.py | 132 ++++++++++++++++++ requirements_all.txt | 3 + 3 files changed, 136 insertions(+) create mode 100644 homeassistant/components/media_player/harman_kardon_avr.py diff --git a/.coveragerc b/.coveragerc index a33a58f3571..61459d0a777 100644 --- a/.coveragerc +++ b/.coveragerc @@ -597,6 +597,7 @@ omit = homeassistant/components/media_player/frontier_silicon.py homeassistant/components/media_player/gpmdp.py homeassistant/components/media_player/gstreamer.py + homeassistant/components/media_player/harman_kardon_avr.py homeassistant/components/media_player/horizon.py homeassistant/components/media_player/itunes.py homeassistant/components/media_player/kodi.py diff --git a/homeassistant/components/media_player/harman_kardon_avr.py b/homeassistant/components/media_player/harman_kardon_avr.py new file mode 100644 index 00000000000..46d1dd4d698 --- /dev/null +++ b/homeassistant/components/media_player/harman_kardon_avr.py @@ -0,0 +1,132 @@ +""" +Support for interface with an Harman/Kardon or JBL AVR. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/media_player.harman_kardon_avr/ +""" +import logging + +import voluptuous as vol + +import homeassistant.helpers.config_validation as cv +from homeassistant.components.media_player import ( + SUPPORT_TURN_OFF, SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_STEP, + PLATFORM_SCHEMA, SUPPORT_TURN_ON, SUPPORT_SELECT_SOURCE, + MediaPlayerDevice) +from homeassistant.const import ( + CONF_HOST, CONF_NAME, CONF_PORT, STATE_OFF, STATE_ON) + +REQUIREMENTS = ['hkavr==0.0.5'] + +_LOGGER = logging.getLogger(__name__) + +DEFAULT_NAME = 'Harman Kardon AVR' +DEFAULT_PORT = 10025 + +SUPPORT_HARMAN_KARDON_AVR = SUPPORT_VOLUME_STEP | SUPPORT_VOLUME_MUTE | \ + SUPPORT_TURN_OFF | SUPPORT_TURN_ON | \ + SUPPORT_SELECT_SOURCE + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Required(CONF_HOST): cv.string, + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, +}) + + +def setup_platform(hass, config, add_entities, discover_info=None): + """Set up the AVR platform.""" + import hkavr + + name = config[CONF_NAME] + host = config[CONF_HOST] + port = config[CONF_PORT] + + avr = hkavr.HkAVR(host, port, name) + avr_device = HkAvrDevice(avr) + + add_entities([avr_device], True) + + +class HkAvrDevice(MediaPlayerDevice): + """Representation of a Harman Kardon AVR / JBL AVR TV.""" + + def __init__(self, avr): + """Initialize a new HarmanKardonAVR.""" + self._avr = avr + + self._name = avr.name + self._host = avr.host + self._port = avr.port + + self._source_list = avr.sources + + self._state = None + self._muted = avr.muted + self._current_source = avr.current_source + + def update(self): + """Update the state of this media_player.""" + if self._avr.is_on(): + self._state = STATE_ON + elif self._avr.is_off(): + self._state = STATE_OFF + else: + self._state = None + + self._muted = self._avr.muted + self._current_source = self._avr.current_source + + @property + def name(self): + """Return the name of the device.""" + return self._name + + @property + def state(self): + """Return the state of the device.""" + return self._state + + @property + def is_volume_muted(self): + """Muted status not available.""" + return self._muted + + @property + def source(self): + """Return the current input source.""" + return self._current_source + + @property + def source_list(self): + """Available sources.""" + return self._source_list + + @property + def supported_features(self): + """Flag media player features that are supported.""" + return SUPPORT_HARMAN_KARDON_AVR + + def turn_on(self): + """Turn the AVR on.""" + self._avr.power_on() + + def turn_off(self): + """Turn off the AVR.""" + self._avr.power_off() + + def select_source(self, source): + """Select input source.""" + return self._avr.select_source(source) + + def volume_up(self): + """Volume up the AVR.""" + return self._avr.volume_up() + + def volume_down(self): + """Volume down AVR.""" + return self._avr.volume_down() + + def mute_volume(self, mute): + """Send mute command.""" + return self._avr.mute(mute) diff --git a/requirements_all.txt b/requirements_all.txt index fe71b03fbd6..9dcb0e9f59b 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -483,6 +483,9 @@ hikvision==0.4 # homeassistant.components.notify.hipchat hipnotify==1.0.8 +# homeassistant.components.media_player.harman_kardon_avr +hkavr==0.0.5 + # homeassistant.components.hlk_sw16 hlk-sw16==0.0.6 From 9f790325bb6732528941f4547d59a6702cd7c5de Mon Sep 17 00:00:00 2001 From: Fredrik Erlandsson Date: Thu, 13 Dec 2018 16:40:56 +0100 Subject: [PATCH 076/304] Fix point sensor discovery (#19245) --- homeassistant/components/binary_sensor/point.py | 5 ++--- homeassistant/components/sensor/point.py | 6 +++--- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/binary_sensor/point.py b/homeassistant/components/binary_sensor/point.py index 29488d08130..1bd97ce2747 100644 --- a/homeassistant/components/binary_sensor/point.py +++ b/homeassistant/components/binary_sensor/point.py @@ -7,8 +7,7 @@ https://home-assistant.io/components/binary_sensor.point/ import logging -from homeassistant.components.binary_sensor import ( - DOMAIN as PARENT_DOMAIN, BinarySensorDevice) +from homeassistant.components.binary_sensor import DOMAIN, BinarySensorDevice from homeassistant.components.point import MinutPointEntity from homeassistant.components.point.const import ( DOMAIN as POINT_DOMAIN, POINT_DISCOVERY_NEW, SIGNAL_WEBHOOK) @@ -49,7 +48,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): for device_class in EVENTS), True) async_dispatcher_connect( - hass, POINT_DISCOVERY_NEW.format(PARENT_DOMAIN, POINT_DOMAIN), + hass, POINT_DISCOVERY_NEW.format(DOMAIN, POINT_DOMAIN), async_discover_sensor) diff --git a/homeassistant/components/sensor/point.py b/homeassistant/components/sensor/point.py index 1bb46827602..9413cf163d9 100644 --- a/homeassistant/components/sensor/point.py +++ b/homeassistant/components/sensor/point.py @@ -6,10 +6,10 @@ https://home-assistant.io/components/sensor.point/ """ import logging -from homeassistant.components.point import ( - DOMAIN as PARENT_DOMAIN, MinutPointEntity) +from homeassistant.components.point import MinutPointEntity from homeassistant.components.point.const import ( DOMAIN as POINT_DOMAIN, POINT_DISCOVERY_NEW) +from homeassistant.components.sensor import DOMAIN from homeassistant.const import ( DEVICE_CLASS_HUMIDITY, DEVICE_CLASS_PRESSURE, DEVICE_CLASS_TEMPERATURE, TEMP_CELSIUS) @@ -38,7 +38,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): for sensor_type in SENSOR_TYPES), True) async_dispatcher_connect( - hass, POINT_DISCOVERY_NEW.format(PARENT_DOMAIN, POINT_DOMAIN), + hass, POINT_DISCOVERY_NEW.format(DOMAIN, POINT_DOMAIN), async_discover_sensor) From 66aa7d0e680ab88ccd562f2919f33e39ad41657c Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Thu, 13 Dec 2018 16:43:59 +0100 Subject: [PATCH 077/304] Fix list (fixes #19235) (#19258) --- homeassistant/components/tts/amazon_polly.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/tts/amazon_polly.py b/homeassistant/components/tts/amazon_polly.py index e3f5b7407cd..1dfb741bb42 100644 --- a/homeassistant/components/tts/amazon_polly.py +++ b/homeassistant/components/tts/amazon_polly.py @@ -5,6 +5,7 @@ For more details about this component, please refer to the documentation at https://home-assistant.io/components/tts.amazon_polly/ """ import logging + import voluptuous as vol from homeassistant.components.tts import Provider, PLATFORM_SCHEMA @@ -56,7 +57,7 @@ SUPPORTED_VOICES = [ 'Cristiano', 'Ines', # Portuguese, European 'Carmen', # Romanian 'Maxim', 'Tatyana', # Russian - 'Enrique', 'Conchita', 'Lucia' # Spanish European + 'Enrique', 'Conchita', 'Lucia', # Spanish European 'Mia', # Spanish Mexican 'Miguel', 'Penelope', # Spanish US 'Astrid', # Swedish From 9efb90d23cbe81985729afc72817d82b5e1f685a Mon Sep 17 00:00:00 2001 From: Tom Harris Date: Thu, 13 Dec 2018 07:52:12 -0800 Subject: [PATCH 078/304] Resolve IOLinc sensor name (#19050) --- homeassistant/components/binary_sensor/insteon.py | 3 ++- homeassistant/components/insteon/__init__.py | 2 +- requirements_all.txt | 2 +- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/binary_sensor/insteon.py b/homeassistant/components/binary_sensor/insteon.py index 009de676bf3..5b0a291e92b 100644 --- a/homeassistant/components/binary_sensor/insteon.py +++ b/homeassistant/components/binary_sensor/insteon.py @@ -14,6 +14,7 @@ DEPENDENCIES = ['insteon'] _LOGGER = logging.getLogger(__name__) SENSOR_TYPES = {'openClosedSensor': 'opening', + 'ioLincSensor': 'opening', 'motionSensor': 'motion', 'doorSensor': 'door', 'wetLeakSensor': 'moisture', @@ -58,7 +59,7 @@ class InsteonBinarySensor(InsteonEntity, BinarySensorDevice): on_val = bool(self._insteon_device_state.value) if self._insteon_device_state.name in ['lightSensor', - 'openClosedSensor']: + 'ioLincSensor']: return not on_val return on_val diff --git a/homeassistant/components/insteon/__init__.py b/homeassistant/components/insteon/__init__.py index 14d43cbcaee..a9edbaed3b6 100644 --- a/homeassistant/components/insteon/__init__.py +++ b/homeassistant/components/insteon/__init__.py @@ -19,7 +19,7 @@ import homeassistant.helpers.config_validation as cv from homeassistant.helpers import discovery from homeassistant.helpers.entity import Entity -REQUIREMENTS = ['insteonplm==0.15.1'] +REQUIREMENTS = ['insteonplm==0.15.2'] _LOGGER = logging.getLogger(__name__) diff --git a/requirements_all.txt b/requirements_all.txt index 9dcb0e9f59b..ef62831167e 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -536,7 +536,7 @@ ihcsdk==2.2.0 influxdb==5.2.0 # homeassistant.components.insteon -insteonplm==0.15.1 +insteonplm==0.15.2 # homeassistant.components.sensor.iperf3 iperf3==0.1.10 From 85e6f92c5ab00fa871c339e7c42595c4e06ca585 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 13 Dec 2018 20:08:31 +0100 Subject: [PATCH 079/304] Lint --- .../components/config/test_config_entries.py | 26 ++++--------------- 1 file changed, 5 insertions(+), 21 deletions(-) diff --git a/tests/components/config/test_config_entries.py b/tests/components/config/test_config_entries.py index 709dbce3c16..be73906c1bf 100644 --- a/tests/components/config/test_config_entries.py +++ b/tests/components/config/test_config_entries.py @@ -190,7 +190,7 @@ async def test_initialize_flow_unauth(hass, client, hass_admin_user): with patch.dict(HANDLERS, {'test': TestFlow}): resp = await client.post('/api/config/config_entries/flow', - json={'handler': 'test'}) + json={'handler': 'test'}) assert resp.status == 401 @@ -339,7 +339,7 @@ async def test_continue_flow_unauth(hass, client, hass_admin_user): with patch.dict(HANDLERS, {'test': TestFlow}): resp = await client.post('/api/config/config_entries/flow', - json={'handler': 'test'}) + json={'handler': 'test'}) assert resp.status == 200 data = await resp.json() flow_id = data.pop('flow_id') @@ -400,22 +400,6 @@ def test_get_progress_index(hass, client): async def test_get_progress_index_unauth(hass, client, hass_admin_user): """Test we can't get flows that are in progress.""" hass_admin_user.groups = [] - - class TestFlow(core_ce.ConfigFlow): - VERSION = 5 - - async def async_step_hassio(self, info): - return (await self.async_step_account()) - - async def async_step_account(self, user_input=None): - return self.async_show_form( - step_id='account', - ) - - with patch.dict(HANDLERS, {'test': TestFlow}): - form = await hass.config_entries.flow.async_init( - 'test', context={'source': 'hassio'}) - resp = await client.get('/api/config/config_entries/flow') assert resp.status == 401 @@ -454,8 +438,8 @@ def test_get_progress_flow(hass, client): assert data == data2 -async def test_get_progress_flow(hass, client, hass_admin_user): - """Test we can query the API for same result as we get from init a flow.""" +async def test_get_progress_flow_unauth(hass, client, hass_admin_user): + """Test we can can't query the API for result of flow.""" class TestFlow(core_ce.ConfigFlow): async def async_step_user(self, user_input=None): schema = OrderedDict() @@ -472,7 +456,7 @@ async def test_get_progress_flow(hass, client, hass_admin_user): with patch.dict(HANDLERS, {'test': TestFlow}): resp = await client.post('/api/config/config_entries/flow', - json={'handler': 'test'}) + json={'handler': 'test'}) assert resp.status == 200 data = await resp.json() From 34cfdb4e35ce4c02db44cc210b31d5e3e9de9ebe Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 13 Dec 2018 20:56:48 +0100 Subject: [PATCH 080/304] Fix OwnTracks deadlocking (#19260) * Fix OwnTracks deadlocking * Fix deadlock --- homeassistant/components/owntracks/__init__.py | 2 +- tests/components/device_tracker/test_owntracks.py | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/owntracks/__init__.py b/homeassistant/components/owntracks/__init__.py index 5e6a99741e8..d0ba27aeddd 100644 --- a/homeassistant/components/owntracks/__init__.py +++ b/homeassistant/components/owntracks/__init__.py @@ -18,7 +18,7 @@ from .config_flow import CONF_SECRET DOMAIN = "owntracks" REQUIREMENTS = ['libnacl==1.6.1'] -DEPENDENCIES = ['device_tracker', 'webhook'] +DEPENDENCIES = ['webhook'] CONF_MAX_GPS_ACCURACY = 'max_gps_accuracy' CONF_WAYPOINT_IMPORT = 'waypoints' diff --git a/tests/components/device_tracker/test_owntracks.py b/tests/components/device_tracker/test_owntracks.py index 6f457f30ed0..68c1f1e8766 100644 --- a/tests/components/device_tracker/test_owntracks.py +++ b/tests/components/device_tracker/test_owntracks.py @@ -277,6 +277,8 @@ def setup_comp(hass): """Initialize components.""" mock_component(hass, 'group') mock_component(hass, 'zone') + hass.loop.run_until_complete(async_setup_component( + hass, 'device_tracker', {})) hass.loop.run_until_complete(async_mock_mqtt_component(hass)) hass.states.async_set( From f60f9bae005b93870ea3cd8aaa1ae0aba9b7eff2 Mon Sep 17 00:00:00 2001 From: damarco Date: Thu, 13 Dec 2018 23:08:35 +0100 Subject: [PATCH 081/304] Always add friendly name attribute to ZHA entities (#19141) * Always add friendly name attribute * Only change device_info name --- homeassistant/components/zha/entities/entity.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/zha/entities/entity.py b/homeassistant/components/zha/entities/entity.py index 920c90a4cd1..0d99324f78b 100644 --- a/homeassistant/components/zha/entities/entity.py +++ b/homeassistant/components/zha/entities/entity.py @@ -100,7 +100,7 @@ class ZhaEntity(entity.Entity): 'identifiers': {(DOMAIN, ieee)}, 'manufacturer': self._endpoint.manufacturer, 'model': self._endpoint.model, - 'name': self._device_state_attributes['friendly_name'], + 'name': self._device_state_attributes.get('friendly_name', ieee), 'via_hub': (DOMAIN, self.hass.data[DATA_ZHA][DATA_ZHA_BRIDGE_ID]), } From e8ec74b9441d3a77861d8038a7781f3b8d6aaa26 Mon Sep 17 00:00:00 2001 From: Rohan Kapoor Date: Thu, 13 Dec 2018 23:10:54 -0800 Subject: [PATCH 082/304] Expose ZoneMinder availability to Home Assistant (#18946) * Expose ZoneMinder availability to Home Assistant * Bump zm-py to 0.2.0 with the availability changes published --- homeassistant/components/camera/zoneminder.py | 7 +++++++ homeassistant/components/sensor/zoneminder.py | 16 +++++++++++++++- homeassistant/components/zoneminder/__init__.py | 2 +- requirements_all.txt | 2 +- 4 files changed, 24 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/camera/zoneminder.py b/homeassistant/components/camera/zoneminder.py index 55d8d91d3ee..e2be7912387 100644 --- a/homeassistant/components/camera/zoneminder.py +++ b/homeassistant/components/camera/zoneminder.py @@ -44,6 +44,7 @@ class ZoneMinderCamera(MjpegCamera): } super().__init__(device_info) self._is_recording = None + self._is_available = None self._monitor = monitor @property @@ -55,8 +56,14 @@ class ZoneMinderCamera(MjpegCamera): """Update our recording state from the ZM API.""" _LOGGER.debug("Updating camera state for monitor %i", self._monitor.id) self._is_recording = self._monitor.is_recording + self._is_available = self._monitor.is_available @property def is_recording(self): """Return whether the monitor is in alarm mode.""" return self._is_recording + + @property + def available(self): + """Return True if entity is available.""" + return self._is_available diff --git a/homeassistant/components/sensor/zoneminder.py b/homeassistant/components/sensor/zoneminder.py index 8e4b57f1f38..16513dc58de 100644 --- a/homeassistant/components/sensor/zoneminder.py +++ b/homeassistant/components/sensor/zoneminder.py @@ -64,7 +64,8 @@ class ZMSensorMonitors(Entity): def __init__(self, monitor): """Initialize monitor sensor.""" self._monitor = monitor - self._state = monitor.function.value + self._state = None + self._is_available = None @property def name(self): @@ -76,6 +77,11 @@ class ZMSensorMonitors(Entity): """Return the state of the sensor.""" return self._state + @property + def available(self): + """Return True if Monitor is available.""" + return self._is_available + def update(self): """Update the sensor.""" state = self._monitor.function @@ -83,6 +89,7 @@ class ZMSensorMonitors(Entity): self._state = None else: self._state = state.value + self._is_available = self._monitor.is_available class ZMSensorEvents(Entity): @@ -123,6 +130,7 @@ class ZMSensorRunState(Entity): def __init__(self, client): """Initialize run state sensor.""" self._state = None + self._is_available = None self._client = client @property @@ -135,6 +143,12 @@ class ZMSensorRunState(Entity): """Return the state of the sensor.""" return self._state + @property + def available(self): + """Return True if ZoneMinder is available.""" + return self._is_available + def update(self): """Update the sensor.""" self._state = self._client.get_active_state() + self._is_available = self._client.is_available diff --git a/homeassistant/components/zoneminder/__init__.py b/homeassistant/components/zoneminder/__init__.py index e5d0c7a5a92..262ce35d3cb 100644 --- a/homeassistant/components/zoneminder/__init__.py +++ b/homeassistant/components/zoneminder/__init__.py @@ -15,7 +15,7 @@ import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) -REQUIREMENTS = ['zm-py==0.1.0'] +REQUIREMENTS = ['zm-py==0.2.0'] CONF_PATH_ZMS = 'path_zms' diff --git a/requirements_all.txt b/requirements_all.txt index ef62831167e..1baa47c33d6 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1689,4 +1689,4 @@ zigpy-xbee==0.1.1 zigpy==0.2.0 # homeassistant.components.zoneminder -zm-py==0.1.0 +zm-py==0.2.0 From ddbfdf14e9878016951939c6b38637ee49495895 Mon Sep 17 00:00:00 2001 From: Heine Furubotten Date: Fri, 14 Dec 2018 08:33:46 +0100 Subject: [PATCH 083/304] Upgraded enturclient to 0.1.2 (#19267) --- homeassistant/components/sensor/entur_public_transport.py | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/sensor/entur_public_transport.py b/homeassistant/components/sensor/entur_public_transport.py index 01fb22f675c..4d50a8f5f79 100644 --- a/homeassistant/components/sensor/entur_public_transport.py +++ b/homeassistant/components/sensor/entur_public_transport.py @@ -18,7 +18,7 @@ from homeassistant.helpers.entity import Entity from homeassistant.util import Throttle import homeassistant.util.dt as dt_util -REQUIREMENTS = ['enturclient==0.1.0'] +REQUIREMENTS = ['enturclient==0.1.2'] _LOGGER = logging.getLogger(__name__) diff --git a/requirements_all.txt b/requirements_all.txt index 1baa47c33d6..83547d373cf 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -340,7 +340,7 @@ elkm1-lib==0.7.13 enocean==0.40 # homeassistant.components.sensor.entur_public_transport -enturclient==0.1.0 +enturclient==0.1.2 # homeassistant.components.sensor.envirophat # envirophat==0.0.6 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 129a7910b0a..3c69013765d 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -59,7 +59,7 @@ defusedxml==0.5.0 dsmr_parser==0.12 # homeassistant.components.sensor.entur_public_transport -enturclient==0.1.0 +enturclient==0.1.2 # homeassistant.components.sensor.season ephem==3.7.6.0 From 74a93fe7648c38499ea988b5720745e8e04f999a Mon Sep 17 00:00:00 2001 From: bremor <34525505+bremor@users.noreply.github.com> Date: Fri, 14 Dec 2018 18:42:01 +1100 Subject: [PATCH 084/304] Synology chat add verify ssl (#19276) * Update synology_chat.py * Added verify_ssl option to notify.synology_chat Python requests will verify ssl by default, this configuration options allows the user to specify if they want to verify ssl or not. Non breaking change, default is True - do verify ssl. --- homeassistant/components/notify/synology_chat.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/notify/synology_chat.py b/homeassistant/components/notify/synology_chat.py index 3fbb7823dc0..922631b4045 100644 --- a/homeassistant/components/notify/synology_chat.py +++ b/homeassistant/components/notify/synology_chat.py @@ -12,13 +12,14 @@ import voluptuous as vol from homeassistant.components.notify import ( BaseNotificationService, PLATFORM_SCHEMA, ATTR_DATA) -from homeassistant.const import CONF_RESOURCE +from homeassistant.const import CONF_RESOURCE, CONF_VERIFY_SSL import homeassistant.helpers.config_validation as cv ATTR_FILE_URL = 'file_url' PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Required(CONF_RESOURCE): cv.url, + vol.Optional(CONF_VERIFY_SSL, default=True): cv.boolean, }) _LOGGER = logging.getLogger(__name__) @@ -27,16 +28,18 @@ _LOGGER = logging.getLogger(__name__) def get_service(hass, config, discovery_info=None): """Get the Synology Chat notification service.""" resource = config.get(CONF_RESOURCE) + verify_ssl = config.get(CONF_VERIFY_SSL) - return SynologyChatNotificationService(resource) + return SynologyChatNotificationService(resource, verify_ssl) class SynologyChatNotificationService(BaseNotificationService): """Implementation of a notification service for Synology Chat.""" - def __init__(self, resource): + def __init__(self, resource, verify_ssl): """Initialize the service.""" self._resource = resource + self._verify_ssl = verify_ssl def send_message(self, message="", **kwargs): """Send a message to a user.""" @@ -52,7 +55,8 @@ class SynologyChatNotificationService(BaseNotificationService): to_send = 'payload={}'.format(json.dumps(data)) - response = requests.post(self._resource, data=to_send, timeout=10) + response = requests.post(self._resource, data=to_send, timeout=10, + verify=self._verify_ssl) if response.status_code not in (200, 201): _LOGGER.exception( From 377c61203d2e684b9b8e113eb120213e85c4487f Mon Sep 17 00:00:00 2001 From: Eric Nagley Date: Fri, 14 Dec 2018 02:52:29 -0500 Subject: [PATCH 085/304] Fix call to super() (#19279) * home-assistant/home-assistant#19273: fix call to super() * home-assistant/home-assistant#19273: adjust to python3 standards. * home-assistant/home-assistant#19273: remove bad test. --- homeassistant/components/light/lutron.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/light/lutron.py b/homeassistant/components/light/lutron.py index ee08e532ce7..359ef0114c5 100644 --- a/homeassistant/components/light/lutron.py +++ b/homeassistant/components/light/lutron.py @@ -42,7 +42,7 @@ class LutronLight(LutronDevice, Light): def __init__(self, area_name, lutron_device, controller): """Initialize the light.""" self._prev_brightness = None - super().__init__(self, area_name, lutron_device, controller) + super().__init__(area_name, lutron_device, controller) @property def supported_features(self): @@ -75,8 +75,7 @@ class LutronLight(LutronDevice, Light): @property def device_state_attributes(self): """Return the state attributes.""" - attr = {} - attr['lutron_integration_id'] = self._lutron_device.id + attr = {'lutron_integration_id': self._lutron_device.id} return attr @property From 4a23d4c7d30d1b8a6d72095784b84c69913db0da Mon Sep 17 00:00:00 2001 From: Thibault Maekelbergh Date: Fri, 14 Dec 2018 09:52:34 +0100 Subject: [PATCH 086/304] Add NMBS (Belgian railway) sensor platform (#18610) * Add custom component to core * Add pyrail to reqs * Format & lint * Sort nmbs.py into place on coveragerc * Only set up station live if provided * Only set up sensor if config is provided * Fix line too long linting error * PR Remarks * Add docstrings * Fix hound line to long error * Fix quotes * Rebase coveragerc * PR Review * Init empty attrs * Dont include delay if there is none * PR review * Safer check * Rebase reqs * Generate req * Update homeassistant/components/sensor/nmbs.py Co-Authored-By: thibmaek * PR remarks --- .coveragerc | 1 + homeassistant/components/sensor/nmbs.py | 220 ++++++++++++++++++++++++ requirements_all.txt | 3 + 3 files changed, 224 insertions(+) create mode 100644 homeassistant/components/sensor/nmbs.py diff --git a/.coveragerc b/.coveragerc index 61459d0a777..b62c7202abd 100644 --- a/.coveragerc +++ b/.coveragerc @@ -771,6 +771,7 @@ omit = homeassistant/components/sensor/netdata.py homeassistant/components/sensor/netdata_public.py homeassistant/components/sensor/neurio_energy.py + homeassistant/components/sensor/nmbs.py homeassistant/components/sensor/noaa_tides.py homeassistant/components/sensor/nsw_fuel_station.py homeassistant/components/sensor/nut.py diff --git a/homeassistant/components/sensor/nmbs.py b/homeassistant/components/sensor/nmbs.py new file mode 100644 index 00000000000..e13ca18af5f --- /dev/null +++ b/homeassistant/components/sensor/nmbs.py @@ -0,0 +1,220 @@ +""" +Get ride details and liveboard details for NMBS (Belgian railway). + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/sensor.nmbs/ +""" +import logging + +import voluptuous as vol + +from homeassistant.components.sensor import PLATFORM_SCHEMA +from homeassistant.const import CONF_NAME, ATTR_ATTRIBUTION +import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.entity import Entity +import homeassistant.util.dt as dt_util + +_LOGGER = logging.getLogger(__name__) + +DEFAULT_NAME = 'NMBS' +DEFAULT_NAME_LIVE = "NMBS Live" + +DEFAULT_ICON = "mdi:train" +DEFAULT_ICON_ALERT = "mdi:alert-octagon" + +CONF_STATION_FROM = 'station_from' +CONF_STATION_TO = 'station_to' +CONF_STATION_LIVE = 'station_live' + +REQUIREMENTS = ["pyrail==0.0.3"] + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Required(CONF_STATION_FROM): cv.string, + vol.Required(CONF_STATION_TO): cv.string, + vol.Optional(CONF_STATION_LIVE): cv.string, + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, +}) + + +def get_time_until(departure_time=None): + """Calculate the time between now and a train's departure time.""" + if departure_time is None: + return 0 + + delta = dt_util.utc_from_timestamp(int(departure_time)) - dt_util.now() + return round((delta.total_seconds() / 60)) + + +def get_delay_in_minutes(delay=0): + """Get the delay in minutes from a delay in seconds.""" + return round((int(delay) / 60)) + + +def get_ride_duration(departure_time, arrival_time, delay=0): + """Calculate the total travel time in minutes.""" + duration = dt_util.utc_from_timestamp( + int(arrival_time)) - dt_util.utc_from_timestamp(int(departure_time)) + duration_time = int(round((duration.total_seconds() / 60))) + return duration_time + get_delay_in_minutes(delay) + + +def setup_platform(hass, config, add_entities, discovery_info=None): + """Set up the NMBS sensor with iRail API.""" + from pyrail import iRail + api_client = iRail() + + name = config[CONF_NAME] + station_from = config[CONF_STATION_FROM] + station_to = config[CONF_STATION_TO] + station_live = config.get(CONF_STATION_LIVE) + + sensors = [NMBSSensor(name, station_from, station_to, api_client)] + + if station_live is not None: + sensors.append(NMBSLiveBoard(station_live, api_client)) + + add_entities(sensors, True) + + +class NMBSLiveBoard(Entity): + """Get the next train from a station's liveboard.""" + + def __init__(self, live_station, api_client): + """Initialize the sensor for getting liveboard data.""" + self._station = live_station + self._api_client = api_client + self._attrs = {} + self._state = None + + @property + def name(self): + """Return the sensor default name.""" + return DEFAULT_NAME_LIVE + + @property + def icon(self): + """Return the default icon or an alert icon if delays.""" + if self._attrs is not None and int(self._attrs['delay']) > 0: + return DEFAULT_ICON_ALERT + + return DEFAULT_ICON + + @property + def state(self): + """Return sensor state.""" + return self._state + + @property + def device_state_attributes(self): + """Return the sensor attributes if data is available.""" + if self._state is None or self._attrs is None: + return None + + delay = get_delay_in_minutes(self._attrs["delay"]) + departure = get_time_until(self._attrs['time']) + + attrs = { + 'departure': "In {} minutes".format(departure), + 'extra_train': int(self._attrs['isExtra']) > 0, + 'occupancy': self._attrs['occupancy']['name'], + 'vehicle_id': self._attrs['vehicle'], + ATTR_ATTRIBUTION: "https://api.irail.be/", + } + + if delay > 0: + attrs['delay'] = "{} minutes".format(delay) + + return attrs + + def update(self): + """Set the state equal to the next departure.""" + liveboard = self._api_client.get_liveboard(self._station) + next_departure = liveboard['departures']['departure'][0] + + self._attrs = next_departure + self._state = "Track {} - {}".format( + next_departure['platform'], next_departure['station']) + + +class NMBSSensor(Entity): + """Get the the total travel time for a given connection.""" + + def __init__(self, name, station_from, station_to, api_client): + """Initialize the NMBS connection sensor.""" + self._name = name + self._station_from = station_from + self._station_to = station_to + self._api_client = api_client + self._attrs = {} + self._state = None + + @property + def name(self): + """Return the name of the sensor.""" + return self._name + + @property + def unit_of_measurement(self): + """Return the unit of measurement.""" + return 'min' + + @property + def icon(self): + """Return the sensor default icon or an alert icon if any delay.""" + if self._attrs is not None: + delay = get_delay_in_minutes(self._attrs['departure']['delay']) + if delay > 0: + return "mdi:alert-octagon" + + return "mdi:train" + + @property + def device_state_attributes(self): + """Return sensor attributes if data is available.""" + if self._state is None or self._attrs is None: + return None + + delay = get_delay_in_minutes(self._attrs['departure']['delay']) + departure = get_time_until(self._attrs['departure']['time']) + + attrs = { + 'departure': "In {} minutes".format(departure), + 'direction': self._attrs['departure']['direction']['name'], + 'occupancy': self._attrs['departure']['occupancy']['name'], + "platform_arriving": self._attrs['arrival']['platform'], + "platform_departing": self._attrs['departure']['platform'], + "vehicle_id": self._attrs['departure']['vehicle'], + ATTR_ATTRIBUTION: "https://api.irail.be/", + } + + if delay > 0: + attrs['delay'] = "{} minutes".format(delay) + + return attrs + + @property + def state(self): + """Return the state of the device.""" + return self._state + + def update(self): + """Set the state to the duration of a connection.""" + connections = self._api_client.get_connections( + self._station_from, self._station_to) + + next_connection = None + + if int(connections['connection'][0]['departure']['left']) > 0: + next_connection = connections['connection'][1] + else: + next_connection = connections['connection'][0] + + self._attrs = next_connection + + duration = get_ride_duration( + next_connection['departure']['time'], + next_connection['arrival']['time'], + next_connection['departure']['delay'], + ) + + self._state = duration diff --git a/requirements_all.txt b/requirements_all.txt index 83547d373cf..6545f703738 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1120,6 +1120,9 @@ pypollencom==2.2.2 # homeassistant.components.qwikswitch pyqwikswitch==0.8 +# homeassistant.components.sensor.nmbs +pyrail==0.0.3 + # homeassistant.components.rainbird pyrainbird==0.1.6 From 0eb0faff0353782547c0c54c2338790e86314e2b Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 14 Dec 2018 10:18:36 +0100 Subject: [PATCH 087/304] Add permission check to light service (#19259) --- homeassistant/components/light/__init__.py | 17 +++++++++++++++++ tests/components/light/test_init.py | 22 ++++++++++++++++++++++ 2 files changed, 39 insertions(+) diff --git a/homeassistant/components/light/__init__.py b/homeassistant/components/light/__init__.py index aa12e562515..a16d1aaf87e 100644 --- a/homeassistant/components/light/__init__.py +++ b/homeassistant/components/light/__init__.py @@ -12,11 +12,13 @@ import os import voluptuous as vol +from homeassistant.auth.permissions.const import POLICY_CONTROL from homeassistant.components.group import \ ENTITY_ID_FORMAT as GROUP_ENTITY_ID_FORMAT from homeassistant.const import ( ATTR_ENTITY_ID, SERVICE_TOGGLE, SERVICE_TURN_OFF, SERVICE_TURN_ON, STATE_ON) +from homeassistant.exceptions import UnknownUser, Unauthorized import homeassistant.helpers.config_validation as cv from homeassistant.helpers.config_validation import PLATFORM_SCHEMA # noqa from homeassistant.helpers.entity import ToggleEntity @@ -256,6 +258,21 @@ async def async_setup(hass, config): target_lights = component.async_extract_from_service(service) params.pop(ATTR_ENTITY_ID, None) + if service.context.user_id: + user = await hass.auth.async_get_user(service.context.user_id) + if user is None: + raise UnknownUser(context=service.context) + + entity_perms = user.permissions.check_entity + + for light in target_lights: + if not entity_perms(light, POLICY_CONTROL): + raise Unauthorized( + context=service.context, + entity_id=light, + permission=POLICY_CONTROL + ) + preprocess_turn_on_alternatives(params) update_tasks = [] diff --git a/tests/components/light/test_init.py b/tests/components/light/test_init.py index 09474a5ad06..28d688b2080 100644 --- a/tests/components/light/test_init.py +++ b/tests/components/light/test_init.py @@ -5,7 +5,10 @@ import unittest.mock as mock import os from io import StringIO +import pytest + from homeassistant import core, loader +from homeassistant.exceptions import Unauthorized from homeassistant.setup import setup_component, async_setup_component from homeassistant.const import ( ATTR_ENTITY_ID, STATE_ON, STATE_OFF, CONF_PLATFORM, @@ -495,3 +498,22 @@ async def test_light_context(hass, hass_admin_user): assert state2 is not None assert state.state != state2.state assert state2.context.user_id == hass_admin_user.id + + +async def test_light_turn_on_auth(hass, hass_admin_user): + """Test that light context works.""" + assert await async_setup_component(hass, 'light', { + 'light': { + 'platform': 'test' + } + }) + + state = hass.states.get('light.ceiling') + assert state is not None + + hass_admin_user.mock_policy({}) + + with pytest.raises(Unauthorized): + await hass.services.async_call('light', 'turn_on', { + 'entity_id': state.entity_id, + }, True, core.Context(user_id=hass_admin_user.id)) From 4f988182587b8b82fdaf1495f37a024730505860 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 14 Dec 2018 10:19:27 +0100 Subject: [PATCH 088/304] Rename is_owner decorator to is_admin (#19266) * Rename is_owner decorator to is_admin * Update test_auth.py --- homeassistant/components/config/auth.py | 6 ++-- .../config/auth_provider_homeassistant.py | 6 ++-- .../components/websocket_api/__init__.py | 2 +- .../components/websocket_api/decorators.py | 17 +++++---- tests/components/config/test_auth.py | 26 ++++++-------- .../test_auth_provider_homeassistant.py | 36 +++++-------------- tests/conftest.py | 10 +++++- 7 files changed, 42 insertions(+), 61 deletions(-) diff --git a/homeassistant/components/config/auth.py b/homeassistant/components/config/auth.py index ec83918e9f0..625dbefbbb3 100644 --- a/homeassistant/components/config/auth.py +++ b/homeassistant/components/config/auth.py @@ -39,7 +39,7 @@ async def async_setup(hass): return True -@websocket_api.require_owner +@websocket_api.require_admin @websocket_api.async_response async def websocket_list(hass, connection, msg): """Return a list of users.""" @@ -49,7 +49,7 @@ async def websocket_list(hass, connection, msg): websocket_api.result_message(msg['id'], result)) -@websocket_api.require_owner +@websocket_api.require_admin @websocket_api.async_response async def websocket_delete(hass, connection, msg): """Delete a user.""" @@ -72,7 +72,7 @@ async def websocket_delete(hass, connection, msg): websocket_api.result_message(msg['id'])) -@websocket_api.require_owner +@websocket_api.require_admin @websocket_api.async_response async def websocket_create(hass, connection, msg): """Create a user.""" diff --git a/homeassistant/components/config/auth_provider_homeassistant.py b/homeassistant/components/config/auth_provider_homeassistant.py index 3495a959f49..5455277aa78 100644 --- a/homeassistant/components/config/auth_provider_homeassistant.py +++ b/homeassistant/components/config/auth_provider_homeassistant.py @@ -3,7 +3,6 @@ import voluptuous as vol from homeassistant.auth.providers import homeassistant as auth_ha from homeassistant.components import websocket_api -from homeassistant.components.websocket_api.decorators import require_owner WS_TYPE_CREATE = 'config/auth_provider/homeassistant/create' @@ -54,7 +53,7 @@ def _get_provider(hass): raise RuntimeError('Provider not found') -@require_owner +@websocket_api.require_admin @websocket_api.async_response async def websocket_create(hass, connection, msg): """Create credentials and attach to a user.""" @@ -91,7 +90,7 @@ async def websocket_create(hass, connection, msg): connection.send_message(websocket_api.result_message(msg['id'])) -@require_owner +@websocket_api.require_admin @websocket_api.async_response async def websocket_delete(hass, connection, msg): """Delete username and related credential.""" @@ -123,6 +122,7 @@ async def websocket_delete(hass, connection, msg): websocket_api.result_message(msg['id'])) +@websocket_api.require_admin @websocket_api.async_response async def websocket_change_password(hass, connection, msg): """Change user password.""" diff --git a/homeassistant/components/websocket_api/__init__.py b/homeassistant/components/websocket_api/__init__.py index 90c802423ce..9c67af820f4 100644 --- a/homeassistant/components/websocket_api/__init__.py +++ b/homeassistant/components/websocket_api/__init__.py @@ -20,7 +20,7 @@ BASE_COMMAND_MESSAGE_SCHEMA = messages.BASE_COMMAND_MESSAGE_SCHEMA error_message = messages.error_message result_message = messages.result_message async_response = decorators.async_response -require_owner = decorators.require_owner +require_admin = decorators.require_admin ws_require_user = decorators.ws_require_user # pylint: enable=invalid-name diff --git a/homeassistant/components/websocket_api/decorators.py b/homeassistant/components/websocket_api/decorators.py index 34250202a5e..d91b884541d 100644 --- a/homeassistant/components/websocket_api/decorators.py +++ b/homeassistant/components/websocket_api/decorators.py @@ -3,6 +3,7 @@ from functools import wraps import logging from homeassistant.core import callback +from homeassistant.exceptions import Unauthorized from . import messages @@ -30,21 +31,19 @@ def async_response(func): return schedule_handler -def require_owner(func): - """Websocket decorator to require user to be an owner.""" +def require_admin(func): + """Websocket decorator to require user to be an admin.""" @wraps(func) - def with_owner(hass, connection, msg): - """Check owner and call function.""" + def with_admin(hass, connection, msg): + """Check admin and call function.""" user = connection.user - if user is None or not user.is_owner: - connection.send_message(messages.error_message( - msg['id'], 'unauthorized', 'This command is for owners only.')) - return + if user is None or not user.is_admin: + raise Unauthorized() func(hass, connection, msg) - return with_owner + return with_admin def ws_require_user( diff --git a/tests/components/config/test_auth.py b/tests/components/config/test_auth.py index b5e0a8c9197..5cc7b4bd82e 100644 --- a/tests/components/config/test_auth.py +++ b/tests/components/config/test_auth.py @@ -13,9 +13,10 @@ def setup_config(hass, aiohttp_client): hass.loop.run_until_complete(auth_config.async_setup(hass)) -async def test_list_requires_owner(hass, hass_ws_client, hass_access_token): +async def test_list_requires_admin(hass, hass_ws_client, + hass_read_only_access_token): """Test get users requires auth.""" - client = await hass_ws_client(hass, hass_access_token) + client = await hass_ws_client(hass, hass_read_only_access_token) await client.send_json({ 'id': 5, @@ -109,9 +110,10 @@ async def test_list(hass, hass_ws_client, hass_admin_user): } -async def test_delete_requires_owner(hass, hass_ws_client, hass_access_token): +async def test_delete_requires_admin(hass, hass_ws_client, + hass_read_only_access_token): """Test delete command requires an owner.""" - client = await hass_ws_client(hass, hass_access_token) + client = await hass_ws_client(hass, hass_read_only_access_token) await client.send_json({ 'id': 5, @@ -139,15 +141,12 @@ async def test_delete_unable_self_account(hass, hass_ws_client, result = await client.receive_json() assert not result['success'], result - assert result['error']['code'] == 'unauthorized' + assert result['error']['code'] == 'no_delete_self' async def test_delete_unknown_user(hass, hass_ws_client, hass_access_token): """Test we cannot delete an unknown user.""" client = await hass_ws_client(hass, hass_access_token) - refresh_token = await hass.auth.async_validate_access_token( - hass_access_token) - refresh_token.user.is_owner = True await client.send_json({ 'id': 5, @@ -163,9 +162,6 @@ async def test_delete_unknown_user(hass, hass_ws_client, hass_access_token): async def test_delete(hass, hass_ws_client, hass_access_token): """Test delete command works.""" client = await hass_ws_client(hass, hass_access_token) - refresh_token = await hass.auth.async_validate_access_token( - hass_access_token) - refresh_token.user.is_owner = True test_user = MockUser( id='efg', ).add_to_hass(hass) @@ -186,9 +182,6 @@ async def test_delete(hass, hass_ws_client, hass_access_token): async def test_create(hass, hass_ws_client, hass_access_token): """Test create command works.""" client = await hass_ws_client(hass, hass_access_token) - refresh_token = await hass.auth.async_validate_access_token( - hass_access_token) - refresh_token.user.is_owner = True assert len(await hass.auth.async_get_users()) == 1 @@ -210,9 +203,10 @@ async def test_create(hass, hass_ws_client, hass_access_token): assert not user.system_generated -async def test_create_requires_owner(hass, hass_ws_client, hass_access_token): +async def test_create_requires_admin(hass, hass_ws_client, + hass_read_only_access_token): """Test create command requires an owner.""" - client = await hass_ws_client(hass, hass_access_token) + client = await hass_ws_client(hass, hass_read_only_access_token) await client.send_json({ 'id': 5, diff --git a/tests/components/config/test_auth_provider_homeassistant.py b/tests/components/config/test_auth_provider_homeassistant.py index a374083c2ab..a4c4c5a3c5a 100644 --- a/tests/components/config/test_auth_provider_homeassistant.py +++ b/tests/components/config/test_auth_provider_homeassistant.py @@ -22,9 +22,6 @@ async def test_create_auth_system_generated_user(hass, hass_access_token, """Test we can't add auth to system generated users.""" system_user = MockUser(system_generated=True).add_to_hass(hass) client = await hass_ws_client(hass, hass_access_token) - refresh_token = await hass.auth.async_validate_access_token( - hass_access_token) - refresh_token.user.is_owner = True await client.send_json({ 'id': 5, @@ -49,9 +46,6 @@ async def test_create_auth_unknown_user(hass_ws_client, hass, hass_access_token): """Test create pointing at unknown user.""" client = await hass_ws_client(hass, hass_access_token) - refresh_token = await hass.auth.async_validate_access_token( - hass_access_token) - refresh_token.user.is_owner = True await client.send_json({ 'id': 5, @@ -67,10 +61,10 @@ async def test_create_auth_unknown_user(hass_ws_client, hass, assert result['error']['code'] == 'not_found' -async def test_create_auth_requires_owner(hass, hass_ws_client, - hass_access_token): - """Test create requires owner to call API.""" - client = await hass_ws_client(hass, hass_access_token) +async def test_create_auth_requires_admin(hass, hass_ws_client, + hass_read_only_access_token): + """Test create requires admin to call API.""" + client = await hass_ws_client(hass, hass_read_only_access_token) await client.send_json({ 'id': 5, @@ -90,9 +84,6 @@ async def test_create_auth(hass, hass_ws_client, hass_access_token, """Test create auth command works.""" client = await hass_ws_client(hass, hass_access_token) user = MockUser().add_to_hass(hass) - refresh_token = await hass.auth.async_validate_access_token( - hass_access_token) - refresh_token.user.is_owner = True assert len(user.credentials) == 0 @@ -123,9 +114,6 @@ async def test_create_auth_duplicate_username(hass, hass_ws_client, """Test we can't create auth with a duplicate username.""" client = await hass_ws_client(hass, hass_access_token) user = MockUser().add_to_hass(hass) - refresh_token = await hass.auth.async_validate_access_token( - hass_access_token) - refresh_token.user.is_owner = True hass_storage[prov_ha.STORAGE_KEY] = { 'version': 1, @@ -153,9 +141,6 @@ async def test_delete_removes_just_auth(hass_ws_client, hass, hass_storage, hass_access_token): """Test deleting an auth without being connected to a user.""" client = await hass_ws_client(hass, hass_access_token) - refresh_token = await hass.auth.async_validate_access_token( - hass_access_token) - refresh_token.user.is_owner = True hass_storage[prov_ha.STORAGE_KEY] = { 'version': 1, @@ -181,9 +166,6 @@ async def test_delete_removes_credential(hass, hass_ws_client, hass_access_token, hass_storage): """Test deleting auth that is connected to a user.""" client = await hass_ws_client(hass, hass_access_token) - refresh_token = await hass.auth.async_validate_access_token( - hass_access_token) - refresh_token.user.is_owner = True user = MockUser().add_to_hass(hass) user.credentials.append( @@ -210,9 +192,10 @@ async def test_delete_removes_credential(hass, hass_ws_client, assert len(hass_storage[prov_ha.STORAGE_KEY]['data']['users']) == 0 -async def test_delete_requires_owner(hass, hass_ws_client, hass_access_token): - """Test delete requires owner.""" - client = await hass_ws_client(hass, hass_access_token) +async def test_delete_requires_admin(hass, hass_ws_client, + hass_read_only_access_token): + """Test delete requires admin.""" + client = await hass_ws_client(hass, hass_read_only_access_token) await client.send_json({ 'id': 5, @@ -228,9 +211,6 @@ async def test_delete_requires_owner(hass, hass_ws_client, hass_access_token): async def test_delete_unknown_auth(hass, hass_ws_client, hass_access_token): """Test trying to delete an unknown auth username.""" client = await hass_ws_client(hass, hass_access_token) - refresh_token = await hass.auth.async_validate_access_token( - hass_access_token) - refresh_token.user.is_owner = True await client.send_json({ 'id': 5, diff --git a/tests/conftest.py b/tests/conftest.py index 82ae596fb48..528ad195195 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -142,7 +142,7 @@ def hass_access_token(hass, hass_admin_user): """Return an access token to access Home Assistant.""" refresh_token = hass.loop.run_until_complete( hass.auth.async_create_refresh_token(hass_admin_user, CLIENT_ID)) - yield hass.auth.async_create_access_token(refresh_token) + return hass.auth.async_create_access_token(refresh_token) @pytest.fixture @@ -167,6 +167,14 @@ def hass_read_only_user(hass, local_auth): return MockUser(groups=[read_only_group]).add_to_hass(hass) +@pytest.fixture +def hass_read_only_access_token(hass, hass_read_only_user): + """Return a Home Assistant read only user.""" + refresh_token = hass.loop.run_until_complete( + hass.auth.async_create_refresh_token(hass_read_only_user, CLIENT_ID)) + return hass.auth.async_create_access_token(refresh_token) + + @pytest.fixture def legacy_auth(hass): """Load legacy API password provider.""" From fb680bc1e4c2d045b541895fd852a1d39004a812 Mon Sep 17 00:00:00 2001 From: Luca Angemi Date: Fri, 14 Dec 2018 10:25:02 +0100 Subject: [PATCH 089/304] Add automation and script events to logbook filter events (#19253) * Add automation and script events to logbook filter events * Update logbook.py * Update logbook.py * Update logbook tests * Update test_logbook.py * Update test_logbook.py * Update test_logbook.py * Update test_logbook.py --- homeassistant/components/logbook.py | 8 ++++++ tests/components/test_logbook.py | 42 ++++++++++++++++++++++++----- 2 files changed, 43 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/logbook.py b/homeassistant/components/logbook.py index 79ee728ddd7..0c6608e3572 100644 --- a/homeassistant/components/logbook.py +++ b/homeassistant/components/logbook.py @@ -469,6 +469,14 @@ def _exclude_events(events, entities_filter): domain = event.data.get(ATTR_DOMAIN) entity_id = event.data.get(ATTR_ENTITY_ID) + elif event.event_type == EVENT_AUTOMATION_TRIGGERED: + domain = 'automation' + entity_id = event.data.get(ATTR_ENTITY_ID) + + elif event.event_type == EVENT_SCRIPT_STARTED: + domain = 'script' + entity_id = event.data.get(ATTR_ENTITY_ID) + elif event.event_type == EVENT_ALEXA_SMART_HOME: domain = 'alexa' diff --git a/tests/components/test_logbook.py b/tests/components/test_logbook.py index 321a16ae64e..c8ade907dd3 100644 --- a/tests/components/test_logbook.py +++ b/tests/components/test_logbook.py @@ -265,22 +265,50 @@ class TestComponentLogbook(unittest.TestCase): def test_exclude_automation_events(self): """Test if automation entries can be excluded by entity_id.""" name = 'My Automation Rule' - message = 'has been triggered' domain = 'automation' entity_id = 'automation.my_automation_rule' entity_id2 = 'automation.my_automation_rule_2' entity_id2 = 'sensor.blu' - eventA = ha.Event(logbook.EVENT_LOGBOOK_ENTRY, { + eventA = ha.Event(logbook.EVENT_AUTOMATION_TRIGGERED, { logbook.ATTR_NAME: name, - logbook.ATTR_MESSAGE: message, - logbook.ATTR_DOMAIN: domain, logbook.ATTR_ENTITY_ID: entity_id, }) - eventB = ha.Event(logbook.EVENT_LOGBOOK_ENTRY, { + eventB = ha.Event(logbook.EVENT_AUTOMATION_TRIGGERED, { + logbook.ATTR_NAME: name, + logbook.ATTR_ENTITY_ID: entity_id2, + }) + + config = logbook.CONFIG_SCHEMA({ + ha.DOMAIN: {}, + logbook.DOMAIN: {logbook.CONF_EXCLUDE: { + logbook.CONF_ENTITIES: [entity_id, ]}}}) + events = logbook._exclude_events( + (ha.Event(EVENT_HOMEASSISTANT_STOP), eventA, eventB), + logbook._generate_filter_from_config(config[logbook.DOMAIN])) + entries = list(logbook.humanify(self.hass, events)) + + assert 2 == len(entries) + self.assert_entry( + entries[0], name='Home Assistant', message='stopped', + domain=ha.DOMAIN) + self.assert_entry( + entries[1], name=name, domain=domain, entity_id=entity_id2) + + def test_exclude_script_events(self): + """Test if script start can be excluded by entity_id.""" + name = 'My Script Rule' + domain = 'script' + entity_id = 'script.my_script' + entity_id2 = 'script.my_script_2' + entity_id2 = 'sensor.blu' + + eventA = ha.Event(logbook.EVENT_SCRIPT_STARTED, { + logbook.ATTR_NAME: name, + logbook.ATTR_ENTITY_ID: entity_id, + }) + eventB = ha.Event(logbook.EVENT_SCRIPT_STARTED, { logbook.ATTR_NAME: name, - logbook.ATTR_MESSAGE: message, - logbook.ATTR_DOMAIN: domain, logbook.ATTR_ENTITY_ID: entity_id2, }) From 7a7c2ad416f716c59112ee91032461c770c52dfa Mon Sep 17 00:00:00 2001 From: emontnemery Date: Fri, 14 Dec 2018 10:33:37 +0100 Subject: [PATCH 090/304] Fix race in entity_platform.async_add_entities (#19222) --- homeassistant/helpers/entity_platform.py | 3 ++- homeassistant/helpers/entity_registry.py | 5 +++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/homeassistant/helpers/entity_platform.py b/homeassistant/helpers/entity_platform.py index ece0fbd071a..9c76d244138 100644 --- a/homeassistant/helpers/entity_platform.py +++ b/homeassistant/helpers/entity_platform.py @@ -297,7 +297,8 @@ class EntityPlatform: self.domain, self.platform_name, entity.unique_id, suggested_object_id=suggested_object_id, config_entry_id=config_entry_id, - device_id=device_id) + device_id=device_id, + known_object_ids=self.entities.keys()) if entry.disabled: self.logger.info( diff --git a/homeassistant/helpers/entity_registry.py b/homeassistant/helpers/entity_registry.py index 57c8bcf0af8..8216681496b 100644 --- a/homeassistant/helpers/entity_registry.py +++ b/homeassistant/helpers/entity_registry.py @@ -117,7 +117,7 @@ class EntityRegistry: @callback def async_get_or_create(self, domain, platform, unique_id, *, suggested_object_id=None, config_entry_id=None, - device_id=None): + device_id=None, known_object_ids=None): """Get entity. Create if it doesn't exist.""" entity_id = self.async_get_entity_id(domain, platform, unique_id) if entity_id: @@ -126,7 +126,8 @@ class EntityRegistry: device_id=device_id) entity_id = self.async_generate_entity_id( - domain, suggested_object_id or '{}_{}'.format(platform, unique_id)) + domain, suggested_object_id or '{}_{}'.format(platform, unique_id), + known_object_ids) entity = RegistryEntry( entity_id=entity_id, From e886576a641ccdc7f4bdea01a7c26cbfb89036c4 Mon Sep 17 00:00:00 2001 From: Eric Nagley Date: Fri, 14 Dec 2018 06:41:09 -0500 Subject: [PATCH 091/304] home-assistant/home-assistant#17333: update to use DOMAIN constants and standards. (#19242) --- homeassistant/components/climate/eq3btsmart.py | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/climate/eq3btsmart.py b/homeassistant/components/climate/eq3btsmart.py index bb0a9d4b810..1eaaaa9d34e 100644 --- a/homeassistant/components/climate/eq3btsmart.py +++ b/homeassistant/components/climate/eq3btsmart.py @@ -9,7 +9,8 @@ import logging import voluptuous as vol from homeassistant.components.climate import ( - STATE_ON, STATE_OFF, STATE_AUTO, PLATFORM_SCHEMA, ClimateDevice, + STATE_ON, STATE_OFF, STATE_HEAT, STATE_MANUAL, STATE_ECO, PLATFORM_SCHEMA, + ClimateDevice, SUPPORT_TARGET_TEMPERATURE, SUPPORT_OPERATION_MODE, SUPPORT_AWAY_MODE, SUPPORT_ON_OFF) from homeassistant.const import ( @@ -21,8 +22,6 @@ REQUIREMENTS = ['python-eq3bt==0.1.9', 'construct==2.9.45'] _LOGGER = logging.getLogger(__name__) STATE_BOOST = 'boost' -STATE_AWAY = 'away' -STATE_MANUAL = 'manual' ATTR_STATE_WINDOW_OPEN = 'window_open' ATTR_STATE_VALVE = 'valve' @@ -65,10 +64,10 @@ class EQ3BTSmartThermostat(ClimateDevice): self.modes = { eq3.Mode.Open: STATE_ON, eq3.Mode.Closed: STATE_OFF, - eq3.Mode.Auto: STATE_AUTO, + eq3.Mode.Auto: STATE_HEAT, eq3.Mode.Manual: STATE_MANUAL, eq3.Mode.Boost: STATE_BOOST, - eq3.Mode.Away: STATE_AWAY, + eq3.Mode.Away: STATE_ECO, } self.reverse_modes = {v: k for k, v in self.modes.items()} @@ -140,20 +139,20 @@ class EQ3BTSmartThermostat(ClimateDevice): def turn_away_mode_off(self): """Away mode off turns to AUTO mode.""" - self.set_operation_mode(STATE_AUTO) + self.set_operation_mode(STATE_HEAT) def turn_away_mode_on(self): """Set away mode on.""" - self.set_operation_mode(STATE_AWAY) + self.set_operation_mode(STATE_ECO) @property def is_away_mode_on(self): """Return if we are away.""" - return self.current_operation == STATE_AWAY + return self.current_operation == STATE_ECO def turn_on(self): """Turn device on.""" - self.set_operation_mode(STATE_AUTO) + self.set_operation_mode(STATE_HEAT) def turn_off(self): """Turn device off.""" From b97f0c02617de35cce0e55352c7fde11c5b11da3 Mon Sep 17 00:00:00 2001 From: emontnemery Date: Fri, 14 Dec 2018 13:00:37 +0100 Subject: [PATCH 092/304] Make variable `entity_id` available to value_template for MQTT binary sensor (#19195) * MQTT binary_sensor: Make variable `entity_id` available to value_template * Review comments * Add testcase --- .../components/binary_sensor/mqtt.py | 2 +- homeassistant/helpers/template.py | 9 ++++--- tests/components/binary_sensor/test_mqtt.py | 27 +++++++++++++++++++ 3 files changed, 33 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/binary_sensor/mqtt.py b/homeassistant/components/binary_sensor/mqtt.py index d2a2be88172..f4b0459fb07 100644 --- a/homeassistant/components/binary_sensor/mqtt.py +++ b/homeassistant/components/binary_sensor/mqtt.py @@ -135,7 +135,7 @@ class MqttBinarySensor(MqttAttributes, MqttAvailability, MqttDiscoveryUpdate, value_template = self._config.get(CONF_VALUE_TEMPLATE) if value_template is not None: payload = value_template.async_render_with_possible_json_value( - payload) + payload, variables={'entity_id': self.entity_id}) if payload == self._config.get(CONF_PAYLOAD_ON): self._state = True elif payload == self._config.get(CONF_PAYLOAD_OFF): diff --git a/homeassistant/helpers/template.py b/homeassistant/helpers/template.py index 2173f972cba..e82302dfd3b 100644 --- a/homeassistant/helpers/template.py +++ b/homeassistant/helpers/template.py @@ -149,7 +149,8 @@ class Template: error_value).result() def async_render_with_possible_json_value(self, value, - error_value=_SENTINEL): + error_value=_SENTINEL, + variables=None): """Render template with value exposed. If valid JSON will expose value_json too. @@ -159,9 +160,9 @@ class Template: if self._compiled is None: self._ensure_compiled() - variables = { - 'value': value - } + variables = dict(variables or {}) + variables['value'] = value + try: variables['value_json'] = json.loads(value) except ValueError: diff --git a/tests/components/binary_sensor/test_mqtt.py b/tests/components/binary_sensor/test_mqtt.py index 74c7d32927b..ce9293ba256 100644 --- a/tests/components/binary_sensor/test_mqtt.py +++ b/tests/components/binary_sensor/test_mqtt.py @@ -58,6 +58,33 @@ class TestSensorMQTT(unittest.TestCase): state = self.hass.states.get('binary_sensor.test') assert STATE_OFF == state.state + def test_setting_sensor_value_via_mqtt_message_and_template(self): + """Test the setting of the value via MQTT.""" + assert setup_component(self.hass, binary_sensor.DOMAIN, { + binary_sensor.DOMAIN: { + 'platform': 'mqtt', + 'name': 'test', + 'state_topic': 'test-topic', + 'payload_on': 'ON', + 'payload_off': 'OFF', + 'value_template': '{%if is_state(entity_id,\"on\")-%}OFF' + '{%-else-%}ON{%-endif%}' + } + }) + + state = self.hass.states.get('binary_sensor.test') + assert STATE_OFF == state.state + + fire_mqtt_message(self.hass, 'test-topic', '') + self.hass.block_till_done() + state = self.hass.states.get('binary_sensor.test') + assert STATE_ON == state.state + + fire_mqtt_message(self.hass, 'test-topic', '') + self.hass.block_till_done() + state = self.hass.states.get('binary_sensor.test') + assert STATE_OFF == state.state + def test_valid_device_class(self): """Test the setting of a valid sensor class.""" assert setup_component(self.hass, binary_sensor.DOMAIN, { From f95bd9c78f3ac04642f4ce3ae60dd74ca9ea9a34 Mon Sep 17 00:00:00 2001 From: MaxG88 Date: Fri, 14 Dec 2018 07:14:32 -0500 Subject: [PATCH 093/304] Set unavailable when unreachable (#19012) * Turn GPMDP Off When Unavailable * Update requirements_all.txt * Specified Exception Type * Update gpmdp.py --- .../components/media_player/gpmdp.py | 44 ++++++++++++------- requirements_all.txt | 2 +- 2 files changed, 28 insertions(+), 18 deletions(-) diff --git a/homeassistant/components/media_player/gpmdp.py b/homeassistant/components/media_player/gpmdp.py index b16eb8d417a..b4ede671d52 100644 --- a/homeassistant/components/media_player/gpmdp.py +++ b/homeassistant/components/media_player/gpmdp.py @@ -20,7 +20,7 @@ from homeassistant.const import ( import homeassistant.helpers.config_validation as cv from homeassistant.util.json import load_json, save_json -REQUIREMENTS = ['websocket-client==0.37.0'] +REQUIREMENTS = ['websocket-client==0.54.0'] _CONFIGURING = {} _LOGGER = logging.getLogger(__name__) @@ -155,6 +155,7 @@ class GPMDP(MediaPlayerDevice): self._duration = None self._volume = None self._request_id = 0 + self._available = True def get_ws(self): """Check if the websocket is setup and connected.""" @@ -200,22 +201,31 @@ class GPMDP(MediaPlayerDevice): def update(self): """Get the latest details from the player.""" time.sleep(1) - playstate = self.send_gpmdp_msg('playback', 'getPlaybackState') - if playstate is None: - return - self._status = PLAYBACK_DICT[str(playstate['value'])] - time_data = self.send_gpmdp_msg('playback', 'getCurrentTime') - if time_data is not None: - self._seek_position = int(time_data['value'] / 1000) - track_data = self.send_gpmdp_msg('playback', 'getCurrentTrack') - if track_data is not None: - self._title = track_data['value']['title'] - self._artist = track_data['value']['artist'] - self._albumart = track_data['value']['albumArt'] - self._duration = int(track_data['value']['duration'] / 1000) - volume_data = self.send_gpmdp_msg('volume', 'getVolume') - if volume_data is not None: - self._volume = volume_data['value'] / 100 + try: + self._available = True + playstate = self.send_gpmdp_msg('playback', 'getPlaybackState') + if playstate is None: + return + self._status = PLAYBACK_DICT[str(playstate['value'])] + time_data = self.send_gpmdp_msg('playback', 'getCurrentTime') + if time_data is not None: + self._seek_position = int(time_data['value'] / 1000) + track_data = self.send_gpmdp_msg('playback', 'getCurrentTrack') + if track_data is not None: + self._title = track_data['value']['title'] + self._artist = track_data['value']['artist'] + self._albumart = track_data['value']['albumArt'] + self._duration = int(track_data['value']['duration'] / 1000) + volume_data = self.send_gpmdp_msg('volume', 'getVolume') + if volume_data is not None: + self._volume = volume_data['value'] / 100 + except OSError: + self._available = False + + @property + def available(self): + """Return if media player is available.""" + return self._available @property def media_content_type(self): diff --git a/requirements_all.txt b/requirements_all.txt index 6545f703738..6327ad3e2a7 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1627,7 +1627,7 @@ watchdog==0.8.3 waterfurnace==1.0.0 # homeassistant.components.media_player.gpmdp -websocket-client==0.37.0 +websocket-client==0.54.0 # homeassistant.components.media_player.webostv websockets==6.0 From 004179775cbf0fdf4f4c9eba86223b29def531d9 Mon Sep 17 00:00:00 2001 From: Erik Eriksson <8228319+molobrakos@users.noreply.github.com> Date: Fri, 14 Dec 2018 13:25:28 +0100 Subject: [PATCH 094/304] Updated ELIQ Online sensor to async API (#19248) * Updated ELIQ Online sensor to async API * Remove use of STATE_UNKNOWN --- homeassistant/components/sensor/eliqonline.py | 26 ++++++++++++------- requirements_all.txt | 2 +- 2 files changed, 17 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/sensor/eliqonline.py b/homeassistant/components/sensor/eliqonline.py index a2b1a4071c1..8e750a8d5e1 100644 --- a/homeassistant/components/sensor/eliqonline.py +++ b/homeassistant/components/sensor/eliqonline.py @@ -10,11 +10,12 @@ import logging import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA -from homeassistant.const import (CONF_ACCESS_TOKEN, CONF_NAME, STATE_UNKNOWN) +from homeassistant.const import (CONF_ACCESS_TOKEN, CONF_NAME) from homeassistant.helpers.entity import Entity import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.aiohttp_client import async_get_clientsession -REQUIREMENTS = ['eliqonline==1.0.14'] +REQUIREMENTS = ['eliqonline==1.2.2'] _LOGGER = logging.getLogger(__name__) @@ -35,24 +36,27 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ }) -def setup_platform(hass, config, add_entities, discovery_info=None): +async def async_setup_platform(hass, config, async_add_entities, + discovery_info=None): """Set up the ELIQ Online sensor.""" import eliqonline access_token = config.get(CONF_ACCESS_TOKEN) name = config.get(CONF_NAME, DEFAULT_NAME) channel_id = config.get(CONF_CHANNEL_ID) + session = async_get_clientsession(hass) - api = eliqonline.API(access_token) + api = eliqonline.API(session=session, + access_token=access_token) try: _LOGGER.debug("Probing for access to ELIQ Online API") - api.get_data_now(channelid=channel_id) + await api.get_data_now(channelid=channel_id) except OSError as error: _LOGGER.error("Could not access the ELIQ Online API: %s", error) return False - add_entities([EliqSensor(api, channel_id, name)], True) + async_add_entities([EliqSensor(api, channel_id, name)], True) class EliqSensor(Entity): @@ -61,7 +65,7 @@ class EliqSensor(Entity): def __init__(self, api, channel_id, name): """Initialize the sensor.""" self._name = name - self._state = STATE_UNKNOWN + self._state = None self._api = api self._channel_id = channel_id @@ -85,12 +89,14 @@ class EliqSensor(Entity): """Return the state of the device.""" return self._state - def update(self): + async def async_update(self): """Get the latest data.""" try: - response = self._api.get_data_now(channelid=self._channel_id) - self._state = int(response.power) + response = await self._api.get_data_now(channelid=self._channel_id) + self._state = int(response["power"]) _LOGGER.debug("Updated power from server %d W", self._state) + except KeyError: + _LOGGER.warning("Invalid response from ELIQ Online API") except OSError as error: _LOGGER.warning("Could not connect to the ELIQ Online API: %s", error) diff --git a/requirements_all.txt b/requirements_all.txt index 6327ad3e2a7..ba282603cd0 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -331,7 +331,7 @@ edp_redy==0.0.3 einder==0.3.1 # homeassistant.components.sensor.eliqonline -eliqonline==1.0.14 +eliqonline==1.2.2 # homeassistant.components.elkm1 elkm1-lib==0.7.13 From b88cf6485001cb7db0a84f4959cc52940cdeea86 Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Fri, 14 Dec 2018 13:32:58 +0100 Subject: [PATCH 095/304] Add air pollutants component (#18707) * Add air pollutants component * Fix lint issue * Update docstrings * Revert change * Remove entries * Remove imports * Fix variable and other fixes * Change tests * Set SCAN_INTERVAL --- .../components/air_pollutants/__init__.py | 176 ++++++++++++++++++ .../components/air_pollutants/demo.py | 56 ++++++ homeassistant/components/demo.py | 1 + tests/components/air_pollutants/__init__.py | 1 + .../air_pollutants/test_air_pollutants.py | 42 +++++ 5 files changed, 276 insertions(+) create mode 100644 homeassistant/components/air_pollutants/__init__.py create mode 100644 homeassistant/components/air_pollutants/demo.py create mode 100644 tests/components/air_pollutants/__init__.py create mode 100644 tests/components/air_pollutants/test_air_pollutants.py diff --git a/homeassistant/components/air_pollutants/__init__.py b/homeassistant/components/air_pollutants/__init__.py new file mode 100644 index 00000000000..7391e8c9975 --- /dev/null +++ b/homeassistant/components/air_pollutants/__init__.py @@ -0,0 +1,176 @@ +""" +Component for handling Air Pollutants data for your location. + +For more details about this component, please refer to the documentation at +https://home-assistant.io/components/air_pollutants/ +""" +from datetime import timedelta +import logging + +from homeassistant.helpers.entity_component import EntityComponent +from homeassistant.helpers.config_validation import PLATFORM_SCHEMA # noqa +from homeassistant.helpers.entity import Entity + +_LOGGER = logging.getLogger(__name__) + +ATTR_AIR_POLLUTANTS_AQI = 'air_quality_index' +ATTR_AIR_POLLUTANTS_ATTRIBUTION = 'attribution' +ATTR_AIR_POLLUTANTS_C02 = 'carbon_dioxide' +ATTR_AIR_POLLUTANTS_CO = 'carbon_monoxide' +ATTR_AIR_POLLUTANTS_N2O = 'nitrogen_oxide' +ATTR_AIR_POLLUTANTS_NO = 'nitrogen_monoxide' +ATTR_AIR_POLLUTANTS_NO2 = 'nitrogen_dioxide' +ATTR_AIR_POLLUTANTS_OZONE = 'ozone' +ATTR_AIR_POLLUTANTS_PM_0_1 = 'particulate_matter_0_1' +ATTR_AIR_POLLUTANTS_PM_10 = 'particulate_matter_10' +ATTR_AIR_POLLUTANTS_PM_2_5 = 'particulate_matter_2_5' +ATTR_AIR_POLLUTANTS_SO2 = 'sulphur_dioxide' + +DOMAIN = 'air_pollutants' + +ENTITY_ID_FORMAT = DOMAIN + '.{}' + +SCAN_INTERVAL = timedelta(seconds=30) + + +async def async_setup(hass, config): + """Set up the air pollutants component.""" + component = hass.data[DOMAIN] = EntityComponent( + _LOGGER, DOMAIN, hass, SCAN_INTERVAL) + await component.async_setup(config) + return True + + +async def async_setup_entry(hass, entry): + """Set up a config entry.""" + return await hass.data[DOMAIN].async_setup_entry(entry) + + +async def async_unload_entry(hass, entry): + """Unload a config entry.""" + return await hass.data[DOMAIN].async_unload_entry(entry) + + +class AirPollutantsEntity(Entity): + """ABC for air pollutants data.""" + + @property + def particulate_matter_2_5(self): + """Return the particulate matter 2.5 level.""" + raise NotImplementedError() + + @property + def particulate_matter_10(self): + """Return the particulate matter 10 level.""" + return None + + @property + def particulate_matter_0_1(self): + """Return the particulate matter 0.1 level.""" + return None + + @property + def temperature_unit(self): + """Return the unit of measurement of the temperature.""" + return None + + @property + def air_quality_index(self): + """Return the Air Quality Index (AQI).""" + return None + + @property + def ozone(self): + """Return the O3 (ozone) level.""" + return None + + @property + def carbon_monoxide(self): + """Return the CO (carbon monoxide) level.""" + return None + + @property + def carbon_dioxide(self): + """Return the CO2 (carbon dioxide) level.""" + return None + + @property + def attribution(self): + """Return the attribution.""" + return None + + @property + def sulphur_dioxide(self): + """Return the SO2 (sulphur dioxide) level.""" + return None + + @property + def nitrogen_oxide(self): + """Return the N2O (nitrogen oxide) level.""" + return None + + @property + def nitrogen_monoxide(self): + """Return the NO (nitrogen monoxide) level.""" + return None + + @property + def nitrogen_dioxide(self): + """Return the NO2 (nitrogen dioxide) level.""" + return None + + @property + def state_attributes(self): + """Return the state attributes.""" + data = {} + + air_quality_index = self.air_quality_index + if air_quality_index is not None: + data[ATTR_AIR_POLLUTANTS_AQI] = air_quality_index + + ozone = self.ozone + if ozone is not None: + data[ATTR_AIR_POLLUTANTS_OZONE] = ozone + + particulate_matter_0_1 = self.particulate_matter_0_1 + if particulate_matter_0_1 is not None: + data[ATTR_AIR_POLLUTANTS_PM_0_1] = particulate_matter_0_1 + + particulate_matter_10 = self.particulate_matter_10 + if particulate_matter_10 is not None: + data[ATTR_AIR_POLLUTANTS_PM_10] = particulate_matter_10 + + sulphur_dioxide = self.sulphur_dioxide + if sulphur_dioxide is not None: + data[ATTR_AIR_POLLUTANTS_SO2] = sulphur_dioxide + + nitrogen_oxide = self.nitrogen_oxide + if nitrogen_oxide is not None: + data[ATTR_AIR_POLLUTANTS_N2O] = nitrogen_oxide + + nitrogen_monoxide = self.nitrogen_monoxide + if nitrogen_monoxide is not None: + data[ATTR_AIR_POLLUTANTS_NO] = nitrogen_monoxide + + nitrogen_dioxide = self.nitrogen_dioxide + if nitrogen_dioxide is not None: + data[ATTR_AIR_POLLUTANTS_NO2] = nitrogen_dioxide + + carbon_dioxide = self.carbon_dioxide + if carbon_dioxide is not None: + data[ATTR_AIR_POLLUTANTS_C02] = carbon_dioxide + + carbon_monoxide = self.carbon_monoxide + if carbon_monoxide is not None: + data[ATTR_AIR_POLLUTANTS_CO] = carbon_monoxide + + attribution = self.attribution + if attribution is not None: + data[ATTR_AIR_POLLUTANTS_ATTRIBUTION] = attribution + + return data + + @property + def state(self): + """Return the current state.""" + return self.particulate_matter_2_5 diff --git a/homeassistant/components/air_pollutants/demo.py b/homeassistant/components/air_pollutants/demo.py new file mode 100644 index 00000000000..06c407d8608 --- /dev/null +++ b/homeassistant/components/air_pollutants/demo.py @@ -0,0 +1,56 @@ +""" +Demo platform that offers fake air pollutants data. + +For more details about this platform, please refer to the documentation +https://home-assistant.io/components/demo/ +""" +from homeassistant.components.air_pollutants import AirPollutantsEntity + + +def setup_platform(hass, config, add_entities, discovery_info=None): + """Set up the Air Pollutants.""" + add_entities([ + DemoAirPollutants('Home', 14, 23, 100), + DemoAirPollutants('Office', 4, 16, None) + ]) + + +class DemoAirPollutants(AirPollutantsEntity): + """Representation of Air Pollutants data.""" + + def __init__(self, name, pm_2_5, pm_10, n2o): + """Initialize the Demo Air Pollutants.""" + self._name = name + self._pm_2_5 = pm_2_5 + self._pm_10 = pm_10 + self._n2o = n2o + + @property + def name(self): + """Return the name of the sensor.""" + return '{} {}'.format('Demo Air Pollutants', self._name) + + @property + def should_poll(self): + """No polling needed for Demo Air Pollutants.""" + return False + + @property + def particulate_matter_2_5(self): + """Return the particulate matter 2.5 level.""" + return self._pm_2_5 + + @property + def particulate_matter_10(self): + """Return the particulate matter 10 level.""" + return self._pm_10 + + @property + def nitrogen_oxide(self): + """Return the nitrogen oxide (N2O) level.""" + return self._n2o + + @property + def attribution(self): + """Return the attribution.""" + return 'Powered by Home Assistant' diff --git a/homeassistant/components/demo.py b/homeassistant/components/demo.py index 8999087a137..d1bca45400b 100644 --- a/homeassistant/components/demo.py +++ b/homeassistant/components/demo.py @@ -15,6 +15,7 @@ DEPENDENCIES = ['conversation', 'introduction', 'zone'] DOMAIN = 'demo' COMPONENTS_WITH_DEMO_PLATFORM = [ + 'air_pollutants', 'alarm_control_panel', 'binary_sensor', 'calendar', diff --git a/tests/components/air_pollutants/__init__.py b/tests/components/air_pollutants/__init__.py new file mode 100644 index 00000000000..98af2395a1f --- /dev/null +++ b/tests/components/air_pollutants/__init__.py @@ -0,0 +1 @@ +"""The tests for Air Pollutants platforms.""" diff --git a/tests/components/air_pollutants/test_air_pollutants.py b/tests/components/air_pollutants/test_air_pollutants.py new file mode 100644 index 00000000000..bbbd85b3a0c --- /dev/null +++ b/tests/components/air_pollutants/test_air_pollutants.py @@ -0,0 +1,42 @@ +"""The tests for the Air Pollutants component.""" +from homeassistant.components.air_pollutants import ( + ATTR_AIR_POLLUTANTS_ATTRIBUTION, ATTR_AIR_POLLUTANTS_N2O, + ATTR_AIR_POLLUTANTS_OZONE, ATTR_AIR_POLLUTANTS_PM_10) +from homeassistant.setup import async_setup_component + + +async def test_state(hass): + """Test Air Pollutants state.""" + config = { + 'air_pollutants': { + 'platform': 'demo', + } + } + + assert await async_setup_component(hass, 'air_pollutants', config) + + state = hass.states.get('air_pollutants.demo_air_pollutants_home') + assert state is not None + + assert state.state == '14' + + +async def test_attributes(hass): + """Test Air Pollutants attributes.""" + config = { + 'air_pollutants': { + 'platform': 'demo', + } + } + + assert await async_setup_component(hass, 'air_pollutants', config) + + state = hass.states.get('air_pollutants.demo_air_pollutants_office') + assert state is not None + + data = state.attributes + assert data.get(ATTR_AIR_POLLUTANTS_PM_10) == 16 + assert data.get(ATTR_AIR_POLLUTANTS_N2O) is None + assert data.get(ATTR_AIR_POLLUTANTS_OZONE) is None + assert data.get(ATTR_AIR_POLLUTANTS_ATTRIBUTION) == \ + 'Powered by Home Assistant' From 7f9cc104476025e01d6b818ff3687e0ed64cd4cc Mon Sep 17 00:00:00 2001 From: pbalogh77 Date: Fri, 14 Dec 2018 13:35:12 +0100 Subject: [PATCH 096/304] Device config for Fibaro hub integration (#19171) --- .../components/binary_sensor/fibaro.py | 6 ++ homeassistant/components/fibaro.py | 56 +++++++++++++------ homeassistant/components/light/fibaro.py | 41 ++++++++++---- 3 files changed, 77 insertions(+), 26 deletions(-) diff --git a/homeassistant/components/binary_sensor/fibaro.py b/homeassistant/components/binary_sensor/fibaro.py index ae8029e13f8..8af2bde10ad 100644 --- a/homeassistant/components/binary_sensor/fibaro.py +++ b/homeassistant/components/binary_sensor/fibaro.py @@ -10,6 +10,7 @@ from homeassistant.components.binary_sensor import ( BinarySensorDevice, ENTITY_ID_FORMAT) from homeassistant.components.fibaro import ( FIBARO_CONTROLLER, FIBARO_DEVICES, FibaroDevice) +from homeassistant.const import (CONF_DEVICE_CLASS, CONF_ICON) DEPENDENCIES = ['fibaro'] @@ -45,6 +46,7 @@ class FibaroBinarySensor(FibaroDevice, BinarySensorDevice): super().__init__(fibaro_device, controller) self.entity_id = ENTITY_ID_FORMAT.format(self.ha_id) stype = None + devconf = fibaro_device.device_config if fibaro_device.type in SENSOR_TYPES: stype = fibaro_device.type elif fibaro_device.baseType in SENSOR_TYPES: @@ -55,6 +57,10 @@ class FibaroBinarySensor(FibaroDevice, BinarySensorDevice): else: self._device_class = None self._icon = None + # device_config overrides: + self._device_class = devconf.get(CONF_DEVICE_CLASS, + self._device_class) + self._icon = devconf.get(CONF_ICON, self._icon) @property def icon(self): diff --git a/homeassistant/components/fibaro.py b/homeassistant/components/fibaro.py index 5813b194890..d506f6c471d 100644 --- a/homeassistant/components/fibaro.py +++ b/homeassistant/components/fibaro.py @@ -10,13 +10,14 @@ from collections import defaultdict from typing import Optional import voluptuous as vol -from homeassistant.const import (ATTR_ARMED, ATTR_BATTERY_LEVEL, - CONF_PASSWORD, CONF_URL, CONF_USERNAME, - EVENT_HOMEASSISTANT_STOP) -import homeassistant.helpers.config_validation as cv -from homeassistant.util import convert, slugify +from homeassistant.const import ( + ATTR_ARMED, ATTR_BATTERY_LEVEL, CONF_DEVICE_CLASS, + CONF_EXCLUDE, CONF_ICON, CONF_PASSWORD, CONF_URL, CONF_USERNAME, + CONF_WHITE_VALUE, EVENT_HOMEASSISTANT_STOP) from homeassistant.helpers import discovery +import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity +from homeassistant.util import convert, slugify REQUIREMENTS = ['fiblary3==0.1.7'] @@ -27,6 +28,10 @@ FIBARO_CONTROLLER = 'fibaro_controller' ATTR_CURRENT_POWER_W = "current_power_w" ATTR_CURRENT_ENERGY_KWH = "current_energy_kwh" CONF_PLUGINS = "plugins" +CONF_DIMMING = "dimming" +CONF_COLOR = "color" +CONF_RESET_COLOR = "reset_color" +CONF_DEVICE_CONFIG = "device_config" FIBARO_COMPONENTS = ['binary_sensor', 'cover', 'light', 'scene', 'sensor', 'switch'] @@ -49,12 +54,26 @@ FIBARO_TYPEMAP = { 'com.fibaro.securitySensor': 'binary_sensor' } +DEVICE_CONFIG_SCHEMA_ENTRY = vol.Schema({ + vol.Optional(CONF_DIMMING): cv.boolean, + vol.Optional(CONF_COLOR): cv.boolean, + vol.Optional(CONF_WHITE_VALUE): cv.boolean, + vol.Optional(CONF_RESET_COLOR): cv.boolean, + vol.Optional(CONF_DEVICE_CLASS): cv.string, + vol.Optional(CONF_ICON): cv.string, +}) + +FIBARO_ID_LIST_SCHEMA = vol.Schema([cv.string]) + CONFIG_SCHEMA = vol.Schema({ DOMAIN: vol.Schema({ vol.Required(CONF_PASSWORD): cv.string, vol.Required(CONF_USERNAME): cv.string, vol.Required(CONF_URL): cv.url, vol.Optional(CONF_PLUGINS, default=False): cv.boolean, + vol.Optional(CONF_EXCLUDE, default=[]): FIBARO_ID_LIST_SCHEMA, + vol.Optional(CONF_DEVICE_CONFIG, default={}): + vol.Schema({cv.string: DEVICE_CONFIG_SCHEMA_ENTRY}) }) }, extra=vol.ALLOW_EXTRA) @@ -62,19 +81,20 @@ CONFIG_SCHEMA = vol.Schema({ class FibaroController(): """Initiate Fibaro Controller Class.""" - _room_map = None # Dict for mapping roomId to room object - _device_map = None # Dict for mapping deviceId to device object - fibaro_devices = None # List of devices by type - _callbacks = {} # Dict of update value callbacks by deviceId - _client = None # Fiblary's Client object for communication - _state_handler = None # Fiblary's StateHandler object - _import_plugins = None # Whether to import devices from plugins - - def __init__(self, username, password, url, import_plugins): + def __init__(self, username, password, url, import_plugins, config): """Initialize the Fibaro controller.""" from fiblary3.client.v4.client import Client as FibaroClient self._client = FibaroClient(url, username, password) self._scene_map = None + # Whether to import devices from plugins + self._import_plugins = import_plugins + self._device_config = config[CONF_DEVICE_CONFIG] + self._room_map = None # Mapping roomId to room object + self._device_map = None # Mapping deviceId to device object + self.fibaro_devices = None # List of devices by type + self._callbacks = {} # Update value callbacks by deviceId + self._state_handler = None # Fiblary's StateHandler object + self._excluded_devices = config.get(CONF_EXCLUDE, []) self.hub_serial = None # Unique serial number of the hub def connect(self): @@ -210,8 +230,11 @@ class FibaroController(): slugify(room_name), slugify(device.name), device.id) if device.enabled and \ ('isPlugin' not in device or - (not device.isPlugin or self._import_plugins)): + (not device.isPlugin or self._import_plugins)) and \ + device.ha_id not in self._excluded_devices: device.mapped_type = self._map_device_to_type(device) + device.device_config = \ + self._device_config.get(device.ha_id, {}) else: device.mapped_type = None if device.mapped_type: @@ -233,7 +256,8 @@ def setup(hass, config): FibaroController(config[DOMAIN][CONF_USERNAME], config[DOMAIN][CONF_PASSWORD], config[DOMAIN][CONF_URL], - config[DOMAIN][CONF_PLUGINS]) + config[DOMAIN][CONF_PLUGINS], + config[DOMAIN]) def stop_fibaro(event): """Stop Fibaro Thread.""" diff --git a/homeassistant/components/light/fibaro.py b/homeassistant/components/light/fibaro.py index 636e4376ae2..6f2842524a2 100644 --- a/homeassistant/components/light/fibaro.py +++ b/homeassistant/components/light/fibaro.py @@ -9,12 +9,15 @@ import logging import asyncio from functools import partial +from homeassistant.const import ( + CONF_WHITE_VALUE) +from homeassistant.components.fibaro import ( + FIBARO_CONTROLLER, FIBARO_DEVICES, FibaroDevice, + CONF_DIMMING, CONF_COLOR, CONF_RESET_COLOR) from homeassistant.components.light import ( ATTR_BRIGHTNESS, ATTR_HS_COLOR, ATTR_WHITE_VALUE, ENTITY_ID_FORMAT, SUPPORT_BRIGHTNESS, SUPPORT_COLOR, SUPPORT_WHITE_VALUE, Light) import homeassistant.util.color as color_util -from homeassistant.components.fibaro import ( - FIBARO_CONTROLLER, FIBARO_DEVICES, FibaroDevice) _LOGGER = logging.getLogger(__name__) @@ -56,20 +59,28 @@ class FibaroLight(FibaroDevice, Light): def __init__(self, fibaro_device, controller): """Initialize the light.""" - self._supported_flags = 0 - self._last_brightness = 0 - self._color = (0, 0) self._brightness = None + self._color = (0, 0) + self._last_brightness = 0 + self._supported_flags = 0 + self._update_lock = asyncio.Lock() self._white = 0 - self._update_lock = asyncio.Lock() - if 'levelChange' in fibaro_device.interfaces: + devconf = fibaro_device.device_config + self._reset_color = devconf.get(CONF_RESET_COLOR, False) + supports_color = 'color' in fibaro_device.properties and \ + 'setColor' in fibaro_device.actions + supports_dimming = 'levelChange' in fibaro_device.interfaces + supports_white_v = 'setW' in fibaro_device.actions + + # Configuration can overrride default capability detection + if devconf.get(CONF_DIMMING, supports_dimming): self._supported_flags |= SUPPORT_BRIGHTNESS - if 'color' in fibaro_device.properties and \ - 'setColor' in fibaro_device.actions: + if devconf.get(CONF_COLOR, supports_color): self._supported_flags |= SUPPORT_COLOR - if 'setW' in fibaro_device.actions: + if devconf.get(CONF_WHITE_VALUE, supports_white_v): self._supported_flags |= SUPPORT_WHITE_VALUE + super().__init__(fibaro_device, controller) self.entity_id = ENTITY_ID_FORMAT.format(self.ha_id) @@ -117,6 +128,12 @@ class FibaroLight(FibaroDevice, Light): self._brightness = scaleto100(target_brightness) if self._supported_flags & SUPPORT_COLOR: + if self._reset_color and \ + kwargs.get(ATTR_WHITE_VALUE) is None and \ + kwargs.get(ATTR_HS_COLOR) is None and \ + kwargs.get(ATTR_BRIGHTNESS) is None: + self._color = (100, 0) + # Update based on parameters self._white = kwargs.get(ATTR_WHITE_VALUE, self._white) self._color = kwargs.get(ATTR_HS_COLOR, self._color) @@ -131,6 +148,10 @@ class FibaroLight(FibaroDevice, Light): self.set_level(int(self._brightness)) return + if self._reset_color: + bri255 = scaleto255(self._brightness) + self.call_set_color(bri255, bri255, bri255, bri255) + if self._supported_flags & SUPPORT_BRIGHTNESS: self.set_level(int(self._brightness)) return From a30921e67d9795243e7b5549908ab2608da776c5 Mon Sep 17 00:00:00 2001 From: Brian Towles Date: Fri, 14 Dec 2018 07:02:06 -0600 Subject: [PATCH 097/304] Set InsteonEntity name to be combo of description and address. (#17262) * Set InsteonEntity name to be combo of description and address. ie "LampLinc Dimmer 26453a" or "Keypad Dimmer 291abb_2" Using a centralized network name * Updated the name to have a more contextual references for device functions then just the group id. * Cleanup for hound * Removed the _generate_network_address function. Not used anymore * Cleanup for lint * clean for hound * Moved descriptor mapper to be a class variable of the InsteonEntity in order to fix lib loading issue for lint * Well, moved DescriptorMapper instance to a function variable now... * fix hound * Lint Cleanup * Clean up docstrings * Simplify Label lookup based on state name --- homeassistant/components/insteon/__init__.py | 75 +++++++++++++++++--- 1 file changed, 66 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/insteon/__init__.py b/homeassistant/components/insteon/__init__.py index a9edbaed3b6..e82e47dc5f4 100644 --- a/homeassistant/components/insteon/__init__.py +++ b/homeassistant/components/insteon/__init__.py @@ -10,12 +10,12 @@ from typing import Dict import voluptuous as vol -from homeassistant.core import callback +import homeassistant.helpers.config_validation as cv from homeassistant.const import (CONF_PORT, EVENT_HOMEASSISTANT_STOP, CONF_PLATFORM, CONF_ENTITY_ID, CONF_HOST) -import homeassistant.helpers.config_validation as cv +from homeassistant.core import callback from homeassistant.helpers import discovery from homeassistant.helpers.entity import Entity @@ -91,7 +91,7 @@ CONF_DEVICE_OVERRIDE_SCHEMA = vol.All( vol.Optional(CONF_FIRMWARE): cv.byte, vol.Optional(CONF_PRODUCT_KEY): cv.byte, vol.Optional(CONF_PLATFORM): cv.string, - })) + })) CONF_X10_SCHEMA = vol.All( vol.Schema({ @@ -147,6 +147,45 @@ X10_HOUSECODE_SCHEMA = vol.Schema({ vol.Required(SRV_HOUSECODE): vol.In(HOUSECODES), }) +STATE_NAME_LABEL_MAP = { + 'keypadButtonA': 'Button A', + 'keypadButtonB': 'Button B', + 'keypadButtonC': 'Button C', + 'keypadButtonD': 'Button D', + 'keypadButtonE': 'Button E', + 'keypadButtonF': 'Button F', + 'keypadButtonG': 'Button G', + 'keypadButtonH': 'Button H', + 'keypadButtonMain': 'Main', + 'onOffButtonA': 'Button A', + 'onOffButtonB': 'Button B', + 'onOffButtonC': 'Button C', + 'onOffButtonD': 'Button D', + 'onOffButtonE': 'Button E', + 'onOffButtonF': 'Button F', + 'onOffButtonG': 'Button G', + 'onOffButtonH': 'Button H', + 'onOffButtonMain': 'Main', + 'fanOnLevel': 'Fan', + 'lightOnLevel': 'Light', + 'coolSetPoint': 'Cool Set', + 'heatSetPoint': 'HeatSet', + 'statusReport': 'Status', + 'generalSensor': 'Sensor', + 'motionSensor': 'Motion', + 'lightSensor': 'Light', + 'batterySensor': 'Battery', + 'dryLeakSensor': 'Dry', + 'wetLeakSensor': 'Wet', + 'heartbeatLeakSensor': 'Heartbeat', + 'openClosedRelay': 'Relay', + 'openClosedSensor': 'Sensor', + 'lightOnOff': 'Light', + 'outletTopOnOff': 'Top', + 'outletBottomOnOff': 'Bottom', + 'coverOpenLevel': 'Cover', +} + async def async_setup(hass, config): """Set up the connection to the modem.""" @@ -478,12 +517,20 @@ class InsteonEntity(Entity): @property def name(self): """Return the name of the node (used for Entity_ID).""" - name = '' - if self._insteon_device_state.group == 0x01: - name = self._insteon_device.id - else: - name = '{:s}_{:d}'.format(self._insteon_device.id, - self._insteon_device_state.group) + # Set a base description + description = self._insteon_device.description + if self._insteon_device.description is None: + description = 'Unknown Device' + + # Get an extension label if there is one + extension = self._get_label() + if extension: + extension = ' ' + extension + name = '{:s} {:s}{:s}'.format( + description, + self._insteon_device.address.human, + extension + ) return name @property @@ -526,6 +573,16 @@ class InsteonEntity(Entity): """All-Link Database loaded for the device.""" self.print_aldb() + def _get_label(self): + """Get the device label for grouped devices.""" + label = '' + if len(self._insteon_device.states) > 1: + if self._insteon_device_state.name in STATE_NAME_LABEL_MAP: + label = STATE_NAME_LABEL_MAP[self._insteon_device_state.name] + else: + label = 'Group {:d}'.format(self.group) + return label + def print_aldb_to_log(aldb): """Print the All-Link Database to the log file.""" From 027920ff52e875b177f4a156add649d7df6ed42a Mon Sep 17 00:00:00 2001 From: liaanvdm <43240119+liaanvdm@users.noreply.github.com> Date: Fri, 14 Dec 2018 15:04:04 +0200 Subject: [PATCH 098/304] Fix restore state for manual alarm control panel (#19284) * Fixed manual alarm control panel restore state * Revert "Fixed manual alarm control panel restore state" This reverts commit 61c9faf434a8bb276133578a0811100a796784ca. * Fixed manual alarm control panel's state restore --- .../components/alarm_control_panel/manual.py | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/alarm_control_panel/manual.py b/homeassistant/components/alarm_control_panel/manual.py index 0a79d74d686..0bbbd0689e2 100644 --- a/homeassistant/components/alarm_control_panel/manual.py +++ b/homeassistant/components/alarm_control_panel/manual.py @@ -310,7 +310,15 @@ class ManualAlarm(alarm.AlarmControlPanel, RestoreEntity): async def async_added_to_hass(self): """Run when entity about to be added to hass.""" + await super().async_added_to_hass() state = await self.async_get_last_state() if state: - self._state = state.state - self._state_ts = state.last_updated + if state.state == STATE_ALARM_PENDING and \ + hasattr(state, 'attributes') and \ + state.attributes['pre_pending_state']: + # If in pending state, we return to the pre_pending_state + self._state = state.attributes['pre_pending_state'] + self._state_ts = dt_util.utcnow() + else: + self._state = state.state + self._state_ts = state.last_updated From 9c62149b0048d391cac9d5f817cb997344964cf9 Mon Sep 17 00:00:00 2001 From: Colin Harrington Date: Fri, 14 Dec 2018 07:42:04 -0600 Subject: [PATCH 099/304] Adding support for Plum Lightpad (#16576) * Adding basic Plum Lightpad support - https://plumlife.com/ * Used Const values is_on is a bool * Added LightpadPowerMeter Sensor to the plum_lightpad platform * Moved to async setup, Introduced a PlumManager, events, subscription, Light and Power meter working * Added PlumMotionSensor * Added Glow Ring support * Updated plum library and re-normalized * set the glow-ring's icon * Naming the glow ring * Formatting and linting * Cleaned up a number of linting issues. Left a number of documentation warnings * setup_platform migrated to async_setup_platform Plum discovery run as a job * bumped plumlightpad version * On shutdown disconnect the telnet session from each plum lightpad * Cleanup & formatting. Worked on parallell cloud update * Moved setup from async to non-async * Utilize async_call_later from the helpers * Cleanedup and linted, down to documentation & one #TODO * Remove commented out debug lines * Fixed Linting issues * Remove TODO * Updated comments & fixed Linting issues * Added plumlightpad to requirements_all.txt * Fixing imports with isort * Added components to .coveragerc * Added PLUM_DATA constant for accessing hass.data[PLUM_DATA] * used dictionary syntax vs get(...) for config * Linting needed an additonal line * Fully async_setup now. removed @callback utilize bus events for detecting new devices found. * Upgraded to plumlightpad 0.0.10 * Removed extra unused PLATFORM_SCHEMA declarations * Moved listener attachment to `async_added_to_hass` and removed unused properties & device_state_attributes * Utilized Discovery when devices were located * Linting and cleanup * used `hass.async_create_task` instead of `hass.async_add_job` per Martin * Removed redundant criteria in if block * Without discovery info, there is no need to setup * Better state management and async on/off for Glow Ring * renamed async_set_config back to set_config, fixed cleanup callback and Plum Initialization * Fixed flake8 linting issues * plumlightpad package update * Add 'motion' device_class to Motion Sensor * Fixed last known Linting issue * let homeassistant handle setting the brightness state * String formatting vs concatenation * use shared aiohttp session from homeassistant * Updating to use new formatting style * looks like @cleanup isn't neccesary * ditch the serial awaits * Ensure async_add_entities is only called once per async_setup_platform * Creating tasks to wait for vs coroutines * Remove unused white component in the GlowRing * Used local variables for GlowRing colors & added a setter for the hs_color property to keep the values in sync * Linted and added docstring * Update the documentation path to point to the component page * Removed the extra sensor and binary_sensor platforms as requested. (To be added in later PRs) * Update plum_lightpad.py * Update plum_lightpad.py --- .coveragerc | 3 + .../components/light/plum_lightpad.py | 185 ++++++++++++++++++ homeassistant/components/plum_lightpad.py | 76 +++++++ requirements_all.txt | 3 + 4 files changed, 267 insertions(+) create mode 100644 homeassistant/components/light/plum_lightpad.py create mode 100644 homeassistant/components/plum_lightpad.py diff --git a/.coveragerc b/.coveragerc index b62c7202abd..212affd29b5 100644 --- a/.coveragerc +++ b/.coveragerc @@ -265,6 +265,9 @@ omit = homeassistant/components/openuv/__init__.py homeassistant/components/*/openuv.py + homeassistant/components/plum_lightpad.py + homeassistant/components/*/plum_lightpad.py + homeassistant/components/pilight.py homeassistant/components/*/pilight.py diff --git a/homeassistant/components/light/plum_lightpad.py b/homeassistant/components/light/plum_lightpad.py new file mode 100644 index 00000000000..fa15c842deb --- /dev/null +++ b/homeassistant/components/light/plum_lightpad.py @@ -0,0 +1,185 @@ +""" +Support for Plum Lightpad switches. + +For more details about this component, please refer to the documentation at +https://home-assistant.io/components/light.plum_lightpad/ +""" +from homeassistant.components.light import ( + ATTR_BRIGHTNESS, ATTR_HS_COLOR, SUPPORT_BRIGHTNESS, SUPPORT_COLOR, Light) +from homeassistant.components.plum_lightpad import PLUM_DATA +import homeassistant.util.color as color_util + +DEPENDENCIES = ['plum_lightpad'] + + +async def async_setup_platform(hass, config, async_add_entities, + discovery_info=None): + """Initialize the Plum Lightpad Light and GlowRing.""" + if discovery_info is None: + return + + plum = hass.data[PLUM_DATA] + + entities = [] + + if 'lpid' in discovery_info: + lightpad = plum.get_lightpad(discovery_info['lpid']) + entities.append(GlowRing(lightpad=lightpad)) + + if 'llid' in discovery_info: + logical_load = plum.get_load(discovery_info['llid']) + entities.append(PlumLight(load=logical_load)) + + if entities: + async_add_entities(entities) + + +class PlumLight(Light): + """Represenation of a Plum Lightpad dimmer.""" + + def __init__(self, load): + """Initialize the light.""" + self._load = load + self._brightness = load.level + + async def async_added_to_hass(self): + """Subscribe to dimmerchange events.""" + self._load.add_event_listener('dimmerchange', self.dimmerchange) + + def dimmerchange(self, event): + """Change event handler updating the brightness.""" + self._brightness = event['level'] + self.schedule_update_ha_state() + + @property + def should_poll(self): + """No polling needed.""" + return False + + @property + def name(self): + """Return the name of the switch if any.""" + return self._load.name + + @property + def brightness(self) -> int: + """Return the brightness of this switch between 0..255.""" + return self._brightness + + @property + def is_on(self) -> bool: + """Return true if light is on.""" + return self._brightness > 0 + + @property + def supported_features(self): + """Flag supported features.""" + if self._load.dimmable: + return SUPPORT_BRIGHTNESS + return None + + async def async_turn_on(self, **kwargs): + """Turn the light on.""" + if ATTR_BRIGHTNESS in kwargs: + await self._load.turn_on(kwargs[ATTR_BRIGHTNESS]) + else: + await self._load.turn_on() + + async def async_turn_off(self, **kwargs): + """Turn the light off.""" + await self._load.turn_off() + + +class GlowRing(Light): + """Represenation of a Plum Lightpad dimmer glow ring.""" + + def __init__(self, lightpad): + """Initialize the light.""" + self._lightpad = lightpad + self._name = '{} Glow Ring'.format(lightpad.friendly_name) + + self._state = lightpad.glow_enabled + self._brightness = lightpad.glow_intensity * 255.0 + + self._red = lightpad.glow_color['red'] + self._green = lightpad.glow_color['green'] + self._blue = lightpad.glow_color['blue'] + + async def async_added_to_hass(self): + """Subscribe to configchange events.""" + self._lightpad.add_event_listener('configchange', + self.configchange_event) + + def configchange_event(self, event): + """Handle Configuration change event.""" + config = event['changes'] + + self._state = config['glowEnabled'] + self._brightness = config['glowIntensity'] * 255.0 + + self._red = config['glowColor']['red'] + self._green = config['glowColor']['green'] + self._blue = config['glowColor']['blue'] + + self.schedule_update_ha_state() + + @property + def hs_color(self): + """Return the hue and saturation color value [float, float].""" + return color_util.color_RGB_to_hs(self._red, self._green, self._blue) + + @property + def should_poll(self): + """No polling needed.""" + return False + + @property + def name(self): + """Return the name of the switch if any.""" + return self._name + + @property + def brightness(self) -> int: + """Return the brightness of this switch between 0..255.""" + return self._brightness + + @property + def glow_intensity(self): + """Brightness in float form.""" + return self._brightness / 255.0 + + @property + def is_on(self) -> bool: + """Return true if light is on.""" + return self._state + + @property + def icon(self): + """Return the crop-portait icon representing the glow ring.""" + return 'mdi:crop-portrait' + + @property + def supported_features(self): + """Flag supported features.""" + return SUPPORT_BRIGHTNESS | SUPPORT_COLOR + + async def async_turn_on(self, **kwargs): + """Turn the light on.""" + if ATTR_BRIGHTNESS in kwargs: + await self._lightpad.set_config( + {"glowIntensity": kwargs[ATTR_BRIGHTNESS]}) + elif ATTR_HS_COLOR in kwargs: + hs_color = kwargs[ATTR_HS_COLOR] + red, green, blue = color_util.color_hs_to_RGB(*hs_color) + await self._lightpad.set_glow_color(red, green, blue, 0) + else: + await self._lightpad.set_config({"glowEnabled": True}) + + async def async_turn_off(self, **kwargs): + """Turn the light off.""" + if ATTR_BRIGHTNESS in kwargs: + await self._lightpad.set_config( + {"glowIntensity": kwargs[ATTR_BRIGHTNESS]}) + else: + await self._lightpad.set_config( + {"glowEnabled": False}) diff --git a/homeassistant/components/plum_lightpad.py b/homeassistant/components/plum_lightpad.py new file mode 100644 index 00000000000..979f257f25f --- /dev/null +++ b/homeassistant/components/plum_lightpad.py @@ -0,0 +1,76 @@ +""" +Support for Plum Lightpad switches. + +For more details about this component, please refer to the documentation at +https://home-assistant.io/components/plum_lightpad +""" +import asyncio +import logging + +import voluptuous as vol + +from homeassistant.const import ( + CONF_PASSWORD, CONF_USERNAME, EVENT_HOMEASSISTANT_STOP) +from homeassistant.helpers import discovery +from homeassistant.helpers.aiohttp_client import async_get_clientsession +import homeassistant.helpers.config_validation as cv + +REQUIREMENTS = ['plumlightpad==0.0.11'] + +_LOGGER = logging.getLogger(__name__) + +DOMAIN = 'plum_lightpad' + +CONFIG_SCHEMA = vol.Schema({ + DOMAIN: vol.Schema({ + vol.Required(CONF_USERNAME): cv.string, + vol.Required(CONF_PASSWORD): cv.string, + }), +}, extra=vol.ALLOW_EXTRA) + +PLUM_DATA = 'plum' + + +async def async_setup(hass, config): + """Plum Lightpad Platform initialization.""" + from plumlightpad import Plum + + conf = config[DOMAIN] + plum = Plum(conf[CONF_USERNAME], conf[CONF_PASSWORD]) + + hass.data[PLUM_DATA] = plum + + def cleanup(event): + """Clean up resources.""" + plum.cleanup() + + hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, cleanup) + + cloud_web_sesison = async_get_clientsession(hass, verify_ssl=True) + await plum.loadCloudData(cloud_web_sesison) + + async def new_load(device): + """Load light and sensor platforms when LogicalLoad is detected.""" + await asyncio.wait([ + hass.async_create_task( + discovery.async_load_platform( + hass, 'light', DOMAIN, + discovered=device, hass_config=conf)) + ]) + + async def new_lightpad(device): + """Load light and binary sensor platforms when Lightpad detected.""" + await asyncio.wait([ + hass.async_create_task( + discovery.async_load_platform( + hass, 'light', DOMAIN, + discovered=device, hass_config=conf)) + ]) + + device_web_session = async_get_clientsession(hass, verify_ssl=False) + hass.async_create_task( + plum.discover(hass.loop, + loadListener=new_load, lightpadListener=new_lightpad, + websession=device_web_session)) + + return True diff --git a/requirements_all.txt b/requirements_all.txt index ba282603cd0..41a8025fee1 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -767,6 +767,9 @@ pizzapi==0.0.3 # homeassistant.components.sensor.plex plexapi==3.0.6 +# homeassistant.components.plum_lightpad +plumlightpad==0.0.11 + # homeassistant.components.sensor.mhz19 # homeassistant.components.sensor.serial_pm pmsensor==0.4 From 1c147b5c5f0de12044c02fe76053e3736d171862 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ville=20Skytt=C3=A4?= Date: Fri, 14 Dec 2018 15:51:13 +0200 Subject: [PATCH 100/304] huawei_lte: Fetch only required data (#17618) When debug logging is enabled, still fetch everything in order to provide indication of available/supported data to users, as instructed in docs. --- .../components/device_tracker/huawei_lte.py | 5 ++- homeassistant/components/huawei_lte.py | 42 +++++++++++++++---- homeassistant/components/sensor/huawei_lte.py | 1 + 3 files changed, 38 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/device_tracker/huawei_lte.py b/homeassistant/components/device_tracker/huawei_lte.py index 4b4eb3f001a..d30a413898f 100644 --- a/homeassistant/components/device_tracker/huawei_lte.py +++ b/homeassistant/components/device_tracker/huawei_lte.py @@ -23,10 +23,13 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Optional(CONF_URL): cv.url, }) +HOSTS_PATH = "wlan_host_list.Hosts" + def get_scanner(hass, config): """Get a Huawei LTE router scanner.""" data = hass.data[DATA_KEY].get_data(config) + data.subscribe(HOSTS_PATH) return HuaweiLteScanner(data) @@ -43,7 +46,7 @@ class HuaweiLteScanner(DeviceScanner): self.data.update() self._hosts = { x["MacAddress"]: x - for x in self.data["wlan_host_list.Hosts.Host"] + for x in self.data[HOSTS_PATH + ".Host"] if x.get("MacAddress") } return list(self._hosts) diff --git a/homeassistant/components/huawei_lte.py b/homeassistant/components/huawei_lte.py index e8d3bc8b3a1..9ed5b9b73c7 100644 --- a/homeassistant/components/huawei_lte.py +++ b/homeassistant/components/huawei_lte.py @@ -50,6 +50,14 @@ class RouterData: traffic_statistics = attr.ib(init=False, factory=dict) wlan_host_list = attr.ib(init=False, factory=dict) + _subscriptions = attr.ib(init=False, factory=set) + + def __attrs_post_init__(self) -> None: + """Fetch device information once, for serial number in @unique_ids.""" + self.subscribe("device_information") + self._update() + self.unsubscribe("device_information") + def __getitem__(self, path: str): """ Get value corresponding to a dotted path. @@ -65,17 +73,34 @@ class RouterData: raise KeyError from err return reduce(operator.getitem, rest, data) + def subscribe(self, path: str) -> None: + """Subscribe to given router data entries.""" + self._subscriptions.add(path.split(".")[0]) + + def unsubscribe(self, path: str) -> None: + """Unsubscribe from given router data entries.""" + self._subscriptions.discard(path.split(".")[0]) + @Throttle(MIN_TIME_BETWEEN_UPDATES) def update(self) -> None: """Call API to update data.""" - self.device_information = self.client.device.information() - _LOGGER.debug("device_information=%s", self.device_information) - self.device_signal = self.client.device.signal() - _LOGGER.debug("device_signal=%s", self.device_signal) - self.traffic_statistics = self.client.monitoring.traffic_statistics() - _LOGGER.debug("traffic_statistics=%s", self.traffic_statistics) - self.wlan_host_list = self.client.wlan.host_list() - _LOGGER.debug("wlan_host_list=%s", self.wlan_host_list) + self._update() + + def _update(self) -> None: + debugging = _LOGGER.isEnabledFor(logging.DEBUG) + if debugging or "device_information" in self._subscriptions: + self.device_information = self.client.device.information() + _LOGGER.debug("device_information=%s", self.device_information) + if debugging or "device_signal" in self._subscriptions: + self.device_signal = self.client.device.signal() + _LOGGER.debug("device_signal=%s", self.device_signal) + if debugging or "traffic_statistics" in self._subscriptions: + self.traffic_statistics = \ + self.client.monitoring.traffic_statistics() + _LOGGER.debug("traffic_statistics=%s", self.traffic_statistics) + if debugging or "wlan_host_list" in self._subscriptions: + self.wlan_host_list = self.client.wlan.host_list() + _LOGGER.debug("wlan_host_list=%s", self.wlan_host_list) @attr.s @@ -120,7 +145,6 @@ def _setup_lte(hass, lte_config) -> None: client = Client(connection) data = RouterData(client) - data.update() hass.data[DATA_KEY].data[url] = data def cleanup(event): diff --git a/homeassistant/components/sensor/huawei_lte.py b/homeassistant/components/sensor/huawei_lte.py index 5ff5f9b38ae..ae376045544 100644 --- a/homeassistant/components/sensor/huawei_lte.py +++ b/homeassistant/components/sensor/huawei_lte.py @@ -117,6 +117,7 @@ def setup_platform( data = hass.data[DATA_KEY].get_data(config) sensors = [] for path in config.get(CONF_MONITORED_CONDITIONS): + data.subscribe(path) sensors.append(HuaweiLteSensor( data, path, SENSOR_META.get(path, {}))) add_entities(sensors, True) From d83f20f7437667f109e158186f8dbc121ecc82d4 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 14 Dec 2018 17:58:45 +0100 Subject: [PATCH 101/304] Fix tests --- tests/components/test_huawei_lte.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tests/components/test_huawei_lte.py b/tests/components/test_huawei_lte.py index 0fe803208ff..aa3e94e93bf 100644 --- a/tests/components/test_huawei_lte.py +++ b/tests/components/test_huawei_lte.py @@ -1,4 +1,6 @@ """Huawei LTE component tests.""" +from unittest.mock import Mock + import pytest from homeassistant.components import huawei_lte @@ -7,7 +9,7 @@ from homeassistant.components import huawei_lte @pytest.fixture(autouse=True) def routerdata(): """Set up a router data for testing.""" - rd = huawei_lte.RouterData(None) + rd = huawei_lte.RouterData(Mock()) rd.device_information = { 'SoftwareVersion': '1.0', 'nested': {'foo': 'bar'}, From d60b7d46b78f4f5c5d213107bde54e5ed7ab3efc Mon Sep 17 00:00:00 2001 From: tmd224 <41244312+tmd224@users.noreply.github.com> Date: Fri, 14 Dec 2018 17:53:49 -0500 Subject: [PATCH 102/304] Add Ambient Weather PWS Sensor component (#18551) * Adding ambient_station.py sensor and updating requirements file * Cleaning up code and fixing flake8 warnings * Fixing flake8 and pylint warnings * Adding ambient_station.py sensor and updating requirements file * Cleaning up code and fixing flake8 warnings * Fixing flake8 and pylint warnings * Fixing bug, things are working well now * removing nosetests file * Adding changes as requested in pull request #18551 * Updating with more change requests from PR code review * Removing SCAN_INTERVAL from PLATFORM_SCHEMA * Removing unused import * Adding platform schema validation for monitored_conditions and conf_units * Updating link to documentation in doc-string. File already named in doc repo * Only setup platform if component can successfully communicate with API * Inverting check for platform setup success --- .coveragerc | 1 + .../components/sensor/ambient_station.py | 212 ++++++++++++++++++ requirements_all.txt | 3 + 3 files changed, 216 insertions(+) create mode 100644 homeassistant/components/sensor/ambient_station.py diff --git a/.coveragerc b/.coveragerc index 212affd29b5..1987dd0b63f 100644 --- a/.coveragerc +++ b/.coveragerc @@ -687,6 +687,7 @@ omit = homeassistant/components/scene/lifx_cloud.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/homeassistant/components/sensor/ambient_station.py b/homeassistant/components/sensor/ambient_station.py new file mode 100644 index 00000000000..0dfbbb63244 --- /dev/null +++ b/homeassistant/components/sensor/ambient_station.py @@ -0,0 +1,212 @@ +""" +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/requirements_all.txt b/requirements_all.txt index 41a8025fee1..5e723325e5f 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -132,6 +132,9 @@ 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 From 70dbbbd9743525864069726376984b00eca70d16 Mon Sep 17 00:00:00 2001 From: Charles Garwood Date: Fri, 14 Dec 2018 19:06:20 -0500 Subject: [PATCH 103/304] Add note to issue template regarding frontend issues (#19295) --- .github/ISSUE_TEMPLATE.md | 1 + .github/ISSUE_TEMPLATE/Bug_report.md | 1 + 2 files changed, 2 insertions(+) diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md index 8772a136eb3..e58659c876a 100644 --- a/.github/ISSUE_TEMPLATE.md +++ b/.github/ISSUE_TEMPLATE.md @@ -1,6 +1,7 @@