From 5a46adfebf6a4cd6073b8f11ec12c7fa554ef643 Mon Sep 17 00:00:00 2001 From: "David F. Mulcahey" Date: Fri, 17 Jan 2020 13:06:10 -0500 Subject: [PATCH 1/9] add multistate back (#30889) --- homeassistant/components/zha/core/const.py | 1 + homeassistant/components/zha/sensor.py | 13 +++++++++++++ 2 files changed, 14 insertions(+) diff --git a/homeassistant/components/zha/core/const.py b/homeassistant/components/zha/core/const.py index 61be496fa1c..848f5805ad5 100644 --- a/homeassistant/components/zha/core/const.py +++ b/homeassistant/components/zha/core/const.py @@ -56,6 +56,7 @@ CHANNEL_HUMIDITY = "humidity" CHANNEL_IAS_WD = "ias_wd" CHANNEL_ILLUMINANCE = "illuminance" CHANNEL_LEVEL = ATTR_LEVEL +CHANNEL_MULTISTATE_INPUT = "multistate_input" CHANNEL_OCCUPANCY = "occupancy" CHANNEL_ON_OFF = "on_off" CHANNEL_POWER_CONFIGURATION = "power" diff --git a/homeassistant/components/zha/sensor.py b/homeassistant/components/zha/sensor.py index 3b73a9793c9..ce02bf11d9d 100644 --- a/homeassistant/components/zha/sensor.py +++ b/homeassistant/components/zha/sensor.py @@ -26,6 +26,7 @@ from .core.const import ( CHANNEL_ELECTRICAL_MEASUREMENT, CHANNEL_HUMIDITY, CHANNEL_ILLUMINANCE, + CHANNEL_MULTISTATE_INPUT, CHANNEL_POWER_CONFIGURATION, CHANNEL_PRESSURE, CHANNEL_SMARTENERGY_METERING, @@ -227,6 +228,18 @@ class ElectricalMeasurement(Sensor): return round(value * self._channel.multiplier / self._channel.divisor) +@STRICT_MATCH(channel_names=CHANNEL_MULTISTATE_INPUT) +class Text(Sensor): + """Sensor that displays string values.""" + + _device_class = None + _unit = None + + def formatter(self, value) -> str: + """Return string value.""" + return value + + @STRICT_MATCH(generic_ids=CHANNEL_ST_HUMIDITY_CLUSTER) @STRICT_MATCH(channel_names=CHANNEL_HUMIDITY) class Humidity(Sensor): From 586566e6ab11e0de9c13c83c3686648d6a5b902c Mon Sep 17 00:00:00 2001 From: SukramJ Date: Fri, 17 Jan 2020 18:37:32 +0100 Subject: [PATCH 2/9] Fix missing switch groups of HomematicIP Cloud (#30903) --- homeassistant/components/homematicip_cloud/switch.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/homematicip_cloud/switch.py b/homeassistant/components/homematicip_cloud/switch.py index 8f3f6a3a177..6fdb0b8c95c 100644 --- a/homeassistant/components/homematicip_cloud/switch.py +++ b/homeassistant/components/homematicip_cloud/switch.py @@ -13,7 +13,7 @@ from homematicip.aio.device import ( AsyncPrintedCircuitBoardSwitch2, AsyncPrintedCircuitBoardSwitchBattery, ) -from homematicip.aio.group import AsyncSwitchingGroup +from homematicip.aio.group import AsyncExtendedLinkedSwitchingGroup, AsyncSwitchingGroup from homeassistant.components.switch import SwitchDevice from homeassistant.config_entries import ConfigEntry @@ -67,7 +67,7 @@ async def async_setup_entry( entities.append(HomematicipMultiSwitch(hap, device, channel)) for group in hap.home.groups: - if isinstance(group, AsyncSwitchingGroup): + if isinstance(group, (AsyncExtendedLinkedSwitchingGroup, AsyncSwitchingGroup)): entities.append(HomematicipGroupSwitch(hap, group)) if entities: From 6ac33e5c7b7f9f83b806382b211d344522c1488e Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Sat, 18 Jan 2020 00:28:34 +0100 Subject: [PATCH 3/9] Fix issue with group unique id when normalising bridge id (#30904) --- homeassistant/components/deconz/__init__.py | 11 +++++++++-- homeassistant/components/deconz/config_flow.py | 4 ++-- homeassistant/components/deconz/const.py | 3 ++- homeassistant/components/deconz/light.py | 7 ++++++- homeassistant/components/deconz/services.py | 12 ++++++------ tests/components/deconz/test_services.py | 6 +++--- 6 files changed, 28 insertions(+), 15 deletions(-) diff --git a/homeassistant/components/deconz/__init__.py b/homeassistant/components/deconz/__init__.py index 096bc6c2904..507b48da9db 100644 --- a/homeassistant/components/deconz/__init__.py +++ b/homeassistant/components/deconz/__init__.py @@ -1,10 +1,11 @@ """Support for deCONZ devices.""" import voluptuous as vol +from homeassistant.config_entries import _UNDEF from homeassistant.const import EVENT_HOMEASSISTANT_STOP from .config_flow import get_master_gateway -from .const import CONF_MASTER_GATEWAY, DOMAIN +from .const import CONF_BRIDGE_ID, CONF_GROUP_ID_BASE, CONF_MASTER_GATEWAY, DOMAIN from .gateway import DeconzGateway from .services import async_setup_services, async_unload_services @@ -37,8 +38,14 @@ async def async_setup_entry(hass, config_entry): # 0.104 introduced config entry unique id, this makes upgrading possible if config_entry.unique_id is None: + + new_data = _UNDEF + if CONF_BRIDGE_ID in config_entry.data: + new_data = dict(config_entry.data) + new_data[CONF_GROUP_ID_BASE] = config_entry.data[CONF_BRIDGE_ID] + hass.config_entries.async_update_entry( - config_entry, unique_id=gateway.api.config.bridgeid + config_entry, unique_id=gateway.api.config.bridgeid, data=new_data ) hass.data[DOMAIN][config_entry.unique_id] = gateway diff --git a/homeassistant/components/deconz/config_flow.py b/homeassistant/components/deconz/config_flow.py index dd37cc31fae..5a9ef232e61 100644 --- a/homeassistant/components/deconz/config_flow.py +++ b/homeassistant/components/deconz/config_flow.py @@ -21,7 +21,7 @@ from homeassistant.helpers import aiohttp_client from .const import ( CONF_ALLOW_CLIP_SENSOR, CONF_ALLOW_DECONZ_GROUPS, - CONF_BRIDGEID, + CONF_BRIDGE_ID, DEFAULT_ALLOW_CLIP_SENSOR, DEFAULT_ALLOW_DECONZ_GROUPS, DEFAULT_PORT, @@ -74,7 +74,7 @@ class DeconzFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): if user_input is not None: for bridge in self.bridges: if bridge[CONF_HOST] == user_input[CONF_HOST]: - self.bridge_id = bridge[CONF_BRIDGEID] + self.bridge_id = bridge[CONF_BRIDGE_ID] self.deconz_config = { CONF_HOST: bridge[CONF_HOST], CONF_PORT: bridge[CONF_PORT], diff --git a/homeassistant/components/deconz/const.py b/homeassistant/components/deconz/const.py index 41ef80b367f..e951e61fde7 100644 --- a/homeassistant/components/deconz/const.py +++ b/homeassistant/components/deconz/const.py @@ -5,7 +5,8 @@ _LOGGER = logging.getLogger(__package__) DOMAIN = "deconz" -CONF_BRIDGEID = "bridgeid" +CONF_BRIDGE_ID = "bridgeid" +CONF_GROUP_ID_BASE = "group_id_base" DEFAULT_PORT = 80 DEFAULT_ALLOW_CLIP_SENSOR = False diff --git a/homeassistant/components/deconz/light.py b/homeassistant/components/deconz/light.py index af708a15391..15d3b828741 100644 --- a/homeassistant/components/deconz/light.py +++ b/homeassistant/components/deconz/light.py @@ -22,6 +22,7 @@ from homeassistant.helpers.dispatcher import async_dispatcher_connect import homeassistant.util.color as color_util from .const import ( + CONF_GROUP_ID_BASE, COVER_TYPES, DOMAIN as DECONZ_DOMAIN, NEW_GROUP, @@ -205,7 +206,11 @@ class DeconzGroup(DeconzLight): """Set up group and create an unique id.""" super().__init__(device, gateway) - self._unique_id = f"{self.gateway.api.config.bridgeid}-{self._device.deconz_id}" + group_id_base = self.gateway.config_entry.unique_id + if CONF_GROUP_ID_BASE in self.gateway.config_entry.data: + group_id_base = self.gateway.config_entry.data[CONF_GROUP_ID_BASE] + + self._unique_id = f"{group_id_base}-{self._device.deconz_id}" @property def unique_id(self): diff --git a/homeassistant/components/deconz/services.py b/homeassistant/components/deconz/services.py index 9d133acdb1d..f20ff65c434 100644 --- a/homeassistant/components/deconz/services.py +++ b/homeassistant/components/deconz/services.py @@ -6,7 +6,7 @@ from homeassistant.helpers import config_validation as cv from .config_flow import get_master_gateway from .const import ( _LOGGER, - CONF_BRIDGEID, + CONF_BRIDGE_ID, DOMAIN, NEW_GROUP, NEW_LIGHT, @@ -27,14 +27,14 @@ SERVICE_CONFIGURE_DEVICE_SCHEMA = vol.All( vol.Optional(SERVICE_ENTITY): cv.entity_id, vol.Optional(SERVICE_FIELD): cv.matches_regex("/.*"), vol.Required(SERVICE_DATA): dict, - vol.Optional(CONF_BRIDGEID): str, + vol.Optional(CONF_BRIDGE_ID): str, } ), cv.has_at_least_one_key(SERVICE_ENTITY, SERVICE_FIELD), ) SERVICE_DEVICE_REFRESH = "device_refresh" -SERVICE_DEVICE_REFRESH_SCHEMA = vol.All(vol.Schema({vol.Optional(CONF_BRIDGEID): str})) +SERVICE_DEVICE_REFRESH_SCHEMA = vol.All(vol.Schema({vol.Optional(CONF_BRIDGE_ID): str})) async def async_setup_services(hass): @@ -97,7 +97,7 @@ async def async_configure_service(hass, data): See Dresden Elektroniks REST API documentation for details: http://dresden-elektronik.github.io/deconz-rest-doc/rest/ """ - bridgeid = data.get(CONF_BRIDGEID) + bridgeid = data.get(CONF_BRIDGE_ID) field = data.get(SERVICE_FIELD, "") entity_id = data.get(SERVICE_ENTITY) data = data[SERVICE_DATA] @@ -119,8 +119,8 @@ async def async_configure_service(hass, data): async def async_refresh_devices_service(hass, data): """Refresh available devices from deCONZ.""" gateway = get_master_gateway(hass) - if CONF_BRIDGEID in data: - gateway = hass.data[DOMAIN][data[CONF_BRIDGEID]] + if CONF_BRIDGE_ID in data: + gateway = hass.data[DOMAIN][data[CONF_BRIDGE_ID]] groups = set(gateway.api.groups.keys()) lights = set(gateway.api.lights.keys()) diff --git a/tests/components/deconz/test_services.py b/tests/components/deconz/test_services.py index f77a74006e7..07985e4d9f4 100644 --- a/tests/components/deconz/test_services.py +++ b/tests/components/deconz/test_services.py @@ -5,7 +5,7 @@ import pytest import voluptuous as vol from homeassistant.components import deconz -from homeassistant.components.deconz.const import CONF_BRIDGEID +from homeassistant.components.deconz.const import CONF_BRIDGE_ID from .test_gateway import BRIDGEID, setup_deconz_integration @@ -91,7 +91,7 @@ async def test_configure_service_with_field(hass): data = { deconz.services.SERVICE_FIELD: "/light/2", - CONF_BRIDGEID: BRIDGEID, + CONF_BRIDGE_ID: BRIDGEID, deconz.services.SERVICE_DATA: {"on": True, "attr1": 10, "attr2": 20}, } @@ -180,7 +180,7 @@ async def test_service_refresh_devices(hass): """Test that service can refresh devices.""" gateway = await setup_deconz_integration(hass) - data = {CONF_BRIDGEID: BRIDGEID} + data = {CONF_BRIDGE_ID: BRIDGEID} with patch( "pydeconz.DeconzSession.request", From 6053d02e443bfcd6675a09345d9a5798f79c0175 Mon Sep 17 00:00:00 2001 From: ochlocracy <5885236+ochlocracy@users.noreply.github.com> Date: Fri, 17 Jan 2020 18:04:46 -0500 Subject: [PATCH 4/9] Fix Alexa semantics for covers with tilt support. (#30911) * Fix Alexa semantics for covers with tilt support. * Clarify wording. * Korrect grammar. --- .../components/alexa/capabilities.py | 94 +++++++--- homeassistant/components/alexa/entities.py | 4 +- homeassistant/components/alexa/handlers.py | 8 +- homeassistant/components/alexa/resources.py | 15 +- tests/components/alexa/test_smart_home.py | 176 ++++++++++++------ 5 files changed, 207 insertions(+), 90 deletions(-) diff --git a/homeassistant/components/alexa/capabilities.py b/homeassistant/components/alexa/capabilities.py index 26d07760747..1dddc815d01 100644 --- a/homeassistant/components/alexa/capabilities.py +++ b/homeassistant/components/alexa/capabilities.py @@ -1073,6 +1073,15 @@ class AlexaSecurityPanelController(AlexaCapability): class AlexaModeController(AlexaCapability): """Implements Alexa.ModeController. + The instance property must be unique across ModeController, RangeController, ToggleController within the same device. + The instance property should be a concatenated string of device domain period and single word. + e.g. fan.speed & fan.direction. + + The instance property must not contain words from other instance property strings within the same device. + e.g. Instance property cover.position & cover.tilt_position will cause the Alexa.Discovery directive to fail. + + An instance property string value may be reused for different devices. + https://developer.amazon.com/docs/device-apis/alexa-modecontroller.html """ @@ -1183,28 +1192,38 @@ class AlexaModeController(AlexaCapability): def semantics(self): """Build and return semantics object.""" + supported = self.entity.attributes.get(ATTR_SUPPORTED_FEATURES, 0) # Cover Position if self.instance == f"{cover.DOMAIN}.{cover.ATTR_POSITION}": + lower_labels = [AlexaSemantics.ACTION_LOWER] + raise_labels = [AlexaSemantics.ACTION_RAISE] self._semantics = AlexaSemantics() + + # Add open/close semantics if tilt is not supported. + if not supported & cover.SUPPORT_SET_TILT_POSITION: + lower_labels.append(AlexaSemantics.ACTION_CLOSE) + raise_labels.append(AlexaSemantics.ACTION_OPEN) + self._semantics.add_states_to_value( + [AlexaSemantics.STATES_CLOSED], + f"{cover.ATTR_POSITION}.{cover.STATE_CLOSED}", + ) + self._semantics.add_states_to_value( + [AlexaSemantics.STATES_OPEN], + f"{cover.ATTR_POSITION}.{cover.STATE_OPEN}", + ) + self._semantics.add_action_to_directive( - [AlexaSemantics.ACTION_CLOSE, AlexaSemantics.ACTION_LOWER], + lower_labels, "SetMode", {"mode": f"{cover.ATTR_POSITION}.{cover.STATE_CLOSED}"}, ) self._semantics.add_action_to_directive( - [AlexaSemantics.ACTION_OPEN, AlexaSemantics.ACTION_RAISE], + raise_labels, "SetMode", {"mode": f"{cover.ATTR_POSITION}.{cover.STATE_OPEN}"}, ) - self._semantics.add_states_to_value( - [AlexaSemantics.STATES_CLOSED], - f"{cover.ATTR_POSITION}.{cover.STATE_CLOSED}", - ) - self._semantics.add_states_to_value( - [AlexaSemantics.STATES_OPEN], - f"{cover.ATTR_POSITION}.{cover.STATE_OPEN}", - ) + return self._semantics.serialize_semantics() return None @@ -1213,6 +1232,15 @@ class AlexaModeController(AlexaCapability): class AlexaRangeController(AlexaCapability): """Implements Alexa.RangeController. + The instance property must be unique across ModeController, RangeController, ToggleController within the same device. + The instance property should be a concatenated string of device domain period and single word. + e.g. fan.speed & fan.direction. + + The instance property must not contain words from other instance property strings within the same device. + e.g. Instance property cover.position & cover.tilt_position will cause the Alexa.Discovery directive to fail. + + An instance property string value may be reused for different devices. + https://developer.amazon.com/docs/device-apis/alexa-rangecontroller.html """ @@ -1268,8 +1296,8 @@ class AlexaRangeController(AlexaCapability): if self.instance == f"{cover.DOMAIN}.{cover.ATTR_POSITION}": return self.entity.attributes.get(cover.ATTR_CURRENT_POSITION) - # Cover Tilt Position - if self.instance == f"{cover.DOMAIN}.{cover.ATTR_TILT_POSITION}": + # Cover Tilt + if self.instance == f"{cover.DOMAIN}.tilt": return self.entity.attributes.get(cover.ATTR_CURRENT_TILT_POSITION) # Input Number Value @@ -1321,10 +1349,10 @@ class AlexaRangeController(AlexaCapability): ) return self._resource.serialize_capability_resources() - # Cover Tilt Position Resources - if self.instance == f"{cover.DOMAIN}.{cover.ATTR_TILT_POSITION}": + # Cover Tilt Resources + if self.instance == f"{cover.DOMAIN}.tilt": self._resource = AlexaPresetResource( - ["Tilt Position", AlexaGlobalCatalog.SETTING_OPENING], + ["Tilt", "Angle", AlexaGlobalCatalog.SETTING_DIRECTION], min_value=0, max_value=100, precision=1, @@ -1358,24 +1386,35 @@ class AlexaRangeController(AlexaCapability): def semantics(self): """Build and return semantics object.""" + supported = self.entity.attributes.get(ATTR_SUPPORTED_FEATURES, 0) # Cover Position if self.instance == f"{cover.DOMAIN}.{cover.ATTR_POSITION}": + lower_labels = [AlexaSemantics.ACTION_LOWER] + raise_labels = [AlexaSemantics.ACTION_RAISE] self._semantics = AlexaSemantics() + + # Add open/close semantics if tilt is not supported. + if not supported & cover.SUPPORT_SET_TILT_POSITION: + lower_labels.append(AlexaSemantics.ACTION_CLOSE) + raise_labels.append(AlexaSemantics.ACTION_OPEN) + self._semantics.add_states_to_value( + [AlexaSemantics.STATES_CLOSED], value=0 + ) + self._semantics.add_states_to_range( + [AlexaSemantics.STATES_OPEN], min_value=1, max_value=100 + ) + self._semantics.add_action_to_directive( - [AlexaSemantics.ACTION_LOWER], "SetRangeValue", {"rangeValue": 0} + lower_labels, "SetRangeValue", {"rangeValue": 0} ) self._semantics.add_action_to_directive( - [AlexaSemantics.ACTION_RAISE], "SetRangeValue", {"rangeValue": 100} - ) - self._semantics.add_states_to_value([AlexaSemantics.STATES_CLOSED], value=0) - self._semantics.add_states_to_range( - [AlexaSemantics.STATES_OPEN], min_value=1, max_value=100 + raise_labels, "SetRangeValue", {"rangeValue": 100} ) return self._semantics.serialize_semantics() - # Cover Tilt Position - if self.instance == f"{cover.DOMAIN}.{cover.ATTR_TILT_POSITION}": + # Cover Tilt + if self.instance == f"{cover.DOMAIN}.tilt": self._semantics = AlexaSemantics() self._semantics.add_action_to_directive( [AlexaSemantics.ACTION_CLOSE], "SetRangeValue", {"rangeValue": 0} @@ -1395,6 +1434,15 @@ class AlexaRangeController(AlexaCapability): class AlexaToggleController(AlexaCapability): """Implements Alexa.ToggleController. + The instance property must be unique across ModeController, RangeController, ToggleController within the same device. + The instance property should be a concatenated string of device domain period and single word. + e.g. fan.speed & fan.direction. + + The instance property must not contain words from other instance property strings within the same device. + e.g. Instance property cover.position & cover.tilt_position will cause the Alexa.Discovery directive to fail. + + An instance property string value may be reused for different devices. + https://developer.amazon.com/docs/device-apis/alexa-togglecontroller.html """ diff --git a/homeassistant/components/alexa/entities.py b/homeassistant/components/alexa/entities.py index d6fa0415640..084231f0090 100644 --- a/homeassistant/components/alexa/entities.py +++ b/homeassistant/components/alexa/entities.py @@ -404,9 +404,7 @@ class CoverCapabilities(AlexaEntity): self.entity, instance=f"{cover.DOMAIN}.{cover.ATTR_POSITION}" ) if supported & cover.SUPPORT_SET_TILT_POSITION: - yield AlexaRangeController( - self.entity, instance=f"{cover.DOMAIN}.{cover.ATTR_TILT_POSITION}" - ) + yield AlexaRangeController(self.entity, instance=f"{cover.DOMAIN}.tilt") yield AlexaEndpointHealth(self.hass, self.entity) yield Alexa(self.hass) diff --git a/homeassistant/components/alexa/handlers.py b/homeassistant/components/alexa/handlers.py index 74c1b24d42b..1cb8980b0b1 100644 --- a/homeassistant/components/alexa/handlers.py +++ b/homeassistant/components/alexa/handlers.py @@ -1118,8 +1118,8 @@ async def async_api_set_range(hass, config, directive, context): service = cover.SERVICE_SET_COVER_POSITION data[cover.ATTR_POSITION] = range_value - # Cover Tilt Position - elif instance == f"{cover.DOMAIN}.{cover.ATTR_TILT_POSITION}": + # Cover Tilt + elif instance == f"{cover.DOMAIN}.tilt": range_value = int(range_value) if range_value == 0: service = cover.SERVICE_CLOSE_COVER_TILT @@ -1192,8 +1192,8 @@ async def async_api_adjust_range(hass, config, directive, context): 100, max(0, range_delta + current) ) - # Cover Tilt Position - elif instance == f"{cover.DOMAIN}.{cover.ATTR_TILT_POSITION}": + # Cover Tilt + elif instance == f"{cover.DOMAIN}.tilt": range_delta = int(range_delta) service = SERVICE_SET_COVER_TILT_POSITION current = entity.attributes.get(cover.ATTR_TILT_POSITION) diff --git a/homeassistant/components/alexa/resources.py b/homeassistant/components/alexa/resources.py index 09927321c36..d2580f3bfea 100644 --- a/homeassistant/components/alexa/resources.py +++ b/homeassistant/components/alexa/resources.py @@ -190,7 +190,12 @@ class AlexaGlobalCatalog: class AlexaCapabilityResource: - """Base class for Alexa capabilityResources, ModeResources, and presetResources objects. + """Base class for Alexa capabilityResources, modeResources, and presetResources objects. + + Resources objects labels must be unique across all modeResources and presetResources within the same device. + To provide support for all supported locales, include one label from the AlexaGlobalCatalog in the labels array. + You cannot use any words from the following list as friendly names: + https://developer.amazon.com/docs/alexa/device-apis/resources-and-assets.html#names-you-cannot-use https://developer.amazon.com/docs/device-apis/resources-and-assets.html#capability-resources """ @@ -312,6 +317,14 @@ class AlexaSemantics: Semantics is supported for the following interfaces only: ModeController, RangeController, and ToggleController. + Semantics stateMappings are only supported for one interface of the same type on the same device. If a device has + multiple RangeControllers only one interface may use stateMappings otherwise discovery will fail. + + You can support semantics actionMappings on different controllers for the same device, however each controller must + support different phrases. For example, you can support "raise" on a RangeController, and "open" on a ModeController, + but you can't support "open" on both RangeController and ModeController. Semantics stateMappings are only supported + for one interface on the same device. + https://developer.amazon.com/docs/device-apis/alexa-discovery.html#semantics-object """ diff --git a/tests/components/alexa/test_smart_home.py b/tests/components/alexa/test_smart_home.py index 37301c3555e..51b1ed83982 100644 --- a/tests/components/alexa/test_smart_home.py +++ b/tests/components/alexa/test_smart_home.py @@ -132,7 +132,7 @@ def get_capability(capabilities, capability_name, instance=None): for capability in capabilities: if instance and capability["instance"] == instance: return capability - elif capability["interface"] == capability_name: + if not instance and capability["interface"] == capability_name: return capability return None @@ -1427,6 +1427,36 @@ async def test_cover_position_range(hass): assert supported_range["maximumValue"] == 100 assert supported_range["precision"] == 1 + # Assert for Position Semantics + position_semantics = range_capability["semantics"] + assert position_semantics is not None + + position_action_mappings = position_semantics["actionMappings"] + assert position_action_mappings is not None + assert { + "@type": "ActionsToDirective", + "actions": ["Alexa.Actions.Lower", "Alexa.Actions.Close"], + "directive": {"name": "SetRangeValue", "payload": {"rangeValue": 0}}, + } in position_action_mappings + assert { + "@type": "ActionsToDirective", + "actions": ["Alexa.Actions.Raise", "Alexa.Actions.Open"], + "directive": {"name": "SetRangeValue", "payload": {"rangeValue": 100}}, + } in position_action_mappings + + position_state_mappings = position_semantics["stateMappings"] + assert position_state_mappings is not None + assert { + "@type": "StatesToValue", + "states": ["Alexa.States.Closed"], + "value": 0, + } in position_state_mappings + assert { + "@type": "StatesToRange", + "states": ["Alexa.States.Open"], + "range": {"minimumValue": 1, "maximumValue": 100}, + } in position_state_mappings + call, _ = await assert_request_calls_service( "Alexa.RangeController", "SetRangeValue", @@ -2454,16 +2484,37 @@ async def test_cover_position_mode(hass): }, } in supported_modes - semantics = mode_capability["semantics"] - assert semantics is not None + # Assert for Position Semantics + position_semantics = mode_capability["semantics"] + assert position_semantics is not None - action_mappings = semantics["actionMappings"] - assert action_mappings is not None + position_action_mappings = position_semantics["actionMappings"] + assert position_action_mappings is not None + assert { + "@type": "ActionsToDirective", + "actions": ["Alexa.Actions.Lower", "Alexa.Actions.Close"], + "directive": {"name": "SetMode", "payload": {"mode": "position.closed"}}, + } in position_action_mappings + assert { + "@type": "ActionsToDirective", + "actions": ["Alexa.Actions.Raise", "Alexa.Actions.Open"], + "directive": {"name": "SetMode", "payload": {"mode": "position.open"}}, + } in position_action_mappings - state_mappings = semantics["stateMappings"] - assert state_mappings is not None + position_state_mappings = position_semantics["stateMappings"] + assert position_state_mappings is not None + assert { + "@type": "StatesToValue", + "states": ["Alexa.States.Closed"], + "value": "position.closed", + } in position_state_mappings + assert { + "@type": "StatesToValue", + "states": ["Alexa.States.Open"], + "value": "position.open", + } in position_state_mappings - call, msg = await assert_request_calls_service( + _, msg = await assert_request_calls_service( "Alexa.ModeController", "SetMode", "cover#test_mode", @@ -2477,7 +2528,7 @@ async def test_cover_position_mode(hass): assert properties["namespace"] == "Alexa.ModeController" assert properties["value"] == "position.closed" - call, msg = await assert_request_calls_service( + _, msg = await assert_request_calls_service( "Alexa.ModeController", "SetMode", "cover#test_mode", @@ -2491,7 +2542,7 @@ async def test_cover_position_mode(hass): assert properties["namespace"] == "Alexa.ModeController" assert properties["value"] == "position.open" - call, msg = await assert_request_calls_service( + _, msg = await assert_request_calls_service( "Alexa.ModeController", "SetMode", "cover#test_mode", @@ -2611,7 +2662,7 @@ async def test_cover_tilt_position_range(hass): range_capability = get_capability(capabilities, "Alexa.RangeController") assert range_capability is not None - assert range_capability["instance"] == "cover.tilt_position" + assert range_capability["instance"] == "cover.tilt" semantics = range_capability["semantics"] assert semantics is not None @@ -2629,7 +2680,7 @@ async def test_cover_tilt_position_range(hass): "cover.set_cover_tilt_position", hass, payload={"rangeValue": "50"}, - instance="cover.tilt_position", + instance="cover.tilt", ) assert call.data["position"] == 50 @@ -2640,7 +2691,7 @@ async def test_cover_tilt_position_range(hass): "cover.close_cover_tilt", hass, payload={"rangeValue": "0"}, - instance="cover.tilt_position", + instance="cover.tilt", ) properties = msg["context"]["properties"][0] assert properties["name"] == "rangeValue" @@ -2654,7 +2705,7 @@ async def test_cover_tilt_position_range(hass): "cover.open_cover_tilt", hass, payload={"rangeValue": "100"}, - instance="cover.tilt_position", + instance="cover.tilt", ) properties = msg["context"]["properties"][0] assert properties["name"] == "rangeValue" @@ -2670,12 +2721,12 @@ async def test_cover_tilt_position_range(hass): False, "cover.set_cover_tilt_position", "tilt_position", - instance="cover.tilt_position", + instance="cover.tilt", ) -async def test_cover_semantics(hass): - """Test cover discovery and semantics.""" +async def test_cover_semantics_position_and_tilt(hass): + """Test cover discovery and semantics with position and tilt support.""" device = ( "cover.test_semantics", "open", @@ -2697,50 +2748,57 @@ async def test_cover_semantics(hass): appliance, "Alexa.RangeController", "Alexa.EndpointHealth", "Alexa" ) - for range_instance in ("cover.position", "cover.tilt_position"): - range_capability = get_capability( - capabilities, "Alexa.RangeController", range_instance - ) - semantics = range_capability["semantics"] - assert semantics is not None + # Assert for Position Semantics + position_capability = get_capability( + capabilities, "Alexa.RangeController", "cover.position" + ) + position_semantics = position_capability["semantics"] + assert position_semantics is not None - action_mappings = semantics["actionMappings"] - assert action_mappings is not None - if range_instance == "cover.position": - assert { - "@type": "ActionsToDirective", - "actions": ["Alexa.Actions.Lower"], - "directive": {"name": "SetRangeValue", "payload": {"rangeValue": 0}}, - } in action_mappings - assert { - "@type": "ActionsToDirective", - "actions": ["Alexa.Actions.Raise"], - "directive": {"name": "SetRangeValue", "payload": {"rangeValue": 100}}, - } in action_mappings - elif range_instance == "cover.position": - assert { - "@type": "ActionsToDirective", - "actions": ["Alexa.Actions.Close"], - "directive": {"name": "SetRangeValue", "payload": {"rangeValue": 0}}, - } in action_mappings - assert { - "@type": "ActionsToDirective", - "actions": ["Alexa.Actions.Open"], - "directive": {"name": "SetRangeValue", "payload": {"rangeValue": 100}}, - } in action_mappings + position_action_mappings = position_semantics["actionMappings"] + assert position_action_mappings is not None + assert { + "@type": "ActionsToDirective", + "actions": ["Alexa.Actions.Lower"], + "directive": {"name": "SetRangeValue", "payload": {"rangeValue": 0}}, + } in position_action_mappings + assert { + "@type": "ActionsToDirective", + "actions": ["Alexa.Actions.Raise"], + "directive": {"name": "SetRangeValue", "payload": {"rangeValue": 100}}, + } in position_action_mappings - state_mappings = semantics["stateMappings"] - assert state_mappings is not None - assert { - "@type": "StatesToValue", - "states": ["Alexa.States.Closed"], - "value": 0, - } in state_mappings - assert { - "@type": "StatesToRange", - "states": ["Alexa.States.Open"], - "range": {"minimumValue": 1, "maximumValue": 100}, - } in state_mappings + # Assert for Tilt Semantics + tilt_capability = get_capability( + capabilities, "Alexa.RangeController", "cover.tilt" + ) + tilt_semantics = tilt_capability["semantics"] + assert tilt_semantics is not None + tilt_action_mappings = tilt_semantics["actionMappings"] + assert tilt_action_mappings is not None + assert { + "@type": "ActionsToDirective", + "actions": ["Alexa.Actions.Close"], + "directive": {"name": "SetRangeValue", "payload": {"rangeValue": 0}}, + } in tilt_action_mappings + assert { + "@type": "ActionsToDirective", + "actions": ["Alexa.Actions.Open"], + "directive": {"name": "SetRangeValue", "payload": {"rangeValue": 100}}, + } in tilt_action_mappings + + tilt_state_mappings = tilt_semantics["stateMappings"] + assert tilt_state_mappings is not None + assert { + "@type": "StatesToValue", + "states": ["Alexa.States.Closed"], + "value": 0, + } in tilt_state_mappings + assert { + "@type": "StatesToRange", + "states": ["Alexa.States.Open"], + "range": {"minimumValue": 1, "maximumValue": 100}, + } in tilt_state_mappings async def test_input_number(hass): From f9b48844e69dab8d1549f2b85144725bb83ed5e7 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 17 Jan 2020 14:54:32 -0800 Subject: [PATCH 5/9] camera endpoint likes to timeout, catch it. (#30919) --- homeassistant/components/ring/camera.py | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/ring/camera.py b/homeassistant/components/ring/camera.py index 07d87c85714..1526a915482 100644 --- a/homeassistant/components/ring/camera.py +++ b/homeassistant/components/ring/camera.py @@ -6,6 +6,7 @@ import logging from haffmpeg.camera import CameraMjpeg from haffmpeg.tools import IMAGE_JPEG, ImageFrame +import requests from homeassistant.components.camera import Camera from homeassistant.components.ffmpeg import DATA_FFMPEG @@ -146,9 +147,15 @@ class RingCam(RingEntityMixin, Camera): ): return - video_url = await self.hass.async_add_executor_job( - self._device.recording_url, self._last_event["id"] - ) + try: + video_url = await self.hass.async_add_executor_job( + self._device.recording_url, self._last_event["id"] + ) + except requests.Timeout: + _LOGGER.warning( + "Time out fetching recording url for camera %s", self.entity_id + ) + video_url = None if video_url: self._last_video_id = self._last_event["id"] From 07a0bc4abe5f3d337e90afd6421d2ce37d6f8000 Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Sat, 18 Jan 2020 00:33:46 +0100 Subject: [PATCH 6/9] Fix service device refresh calling state update (#30920) --- .../components/deconz/binary_sensor.py | 8 ++++--- .../components/deconz/deconz_device.py | 5 +++- .../components/deconz/deconz_event.py | 24 +++++++++++-------- homeassistant/components/deconz/manifest.json | 10 +++++--- homeassistant/components/deconz/sensor.py | 18 ++++++++------ homeassistant/components/deconz/services.py | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 8 files changed, 44 insertions(+), 27 deletions(-) diff --git a/homeassistant/components/deconz/binary_sensor.py b/homeassistant/components/deconz/binary_sensor.py index 6261473bb0e..225a28f52f8 100644 --- a/homeassistant/components/deconz/binary_sensor.py +++ b/homeassistant/components/deconz/binary_sensor.py @@ -54,11 +54,13 @@ class DeconzBinarySensor(DeconzDevice, BinarySensorDevice): """Representation of a deCONZ binary sensor.""" @callback - def async_update_callback(self, force_update=False): + def async_update_callback(self, force_update=False, ignore_update=False): """Update the sensor's state.""" - changed = set(self._device.changed_keys) + if ignore_update: + return + keys = {"on", "reachable", "state"} - if force_update or any(key in changed for key in keys): + if force_update or self._device.changed_keys.intersection(keys): self.async_schedule_update_ha_state() @property diff --git a/homeassistant/components/deconz/deconz_device.py b/homeassistant/components/deconz/deconz_device.py index 68daee6cf26..4ac3e6cd379 100644 --- a/homeassistant/components/deconz/deconz_device.py +++ b/homeassistant/components/deconz/deconz_device.py @@ -91,8 +91,11 @@ class DeconzDevice(DeconzBase, Entity): unsub_dispatcher() @callback - def async_update_callback(self, force_update=False): + def async_update_callback(self, force_update=False, ignore_update=False): """Update the device's state.""" + if ignore_update: + return + self.async_schedule_update_ha_state() @property diff --git a/homeassistant/components/deconz/deconz_event.py b/homeassistant/components/deconz/deconz_event.py index 3c2442994a5..527e8d2ab7a 100644 --- a/homeassistant/components/deconz/deconz_event.py +++ b/homeassistant/components/deconz/deconz_event.py @@ -39,17 +39,21 @@ class DeconzEvent(DeconzBase): self._device = None @callback - def async_update_callback(self, force_update=False): + def async_update_callback(self, force_update=False, ignore_update=False): """Fire the event if reason is that state is updated.""" - if "state" in self._device.changed_keys: - data = { - CONF_ID: self.event_id, - CONF_UNIQUE_ID: self.serial, - CONF_EVENT: self._device.state, - } - if self._device.gesture: - data[CONF_GESTURE] = self._device.gesture - self.gateway.hass.bus.async_fire(CONF_DECONZ_EVENT, data) + if ignore_update or "state" not in self._device.changed_keys: + return + + data = { + CONF_ID: self.event_id, + CONF_UNIQUE_ID: self.serial, + CONF_EVENT: self._device.state, + } + + if self._device.gesture: + data[CONF_GESTURE] = self._device.gesture + + self.gateway.hass.bus.async_fire(CONF_DECONZ_EVENT, data) async def async_update_device_registry(self): """Update device registry.""" diff --git a/homeassistant/components/deconz/manifest.json b/homeassistant/components/deconz/manifest.json index a327d7106fc..f448e9105c8 100644 --- a/homeassistant/components/deconz/manifest.json +++ b/homeassistant/components/deconz/manifest.json @@ -3,13 +3,17 @@ "name": "deCONZ", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/deconz", - "requirements": ["pydeconz==67"], + "requirements": [ + "pydeconz==68" + ], "ssdp": [ { "manufacturer": "Royal Philips Electronics" } ], "dependencies": [], - "codeowners": ["@kane610"], + "codeowners": [ + "@kane610" + ], "quality_scale": "platinum" -} +} \ No newline at end of file diff --git a/homeassistant/components/deconz/sensor.py b/homeassistant/components/deconz/sensor.py index 8194dd145dc..8261f03e902 100644 --- a/homeassistant/components/deconz/sensor.py +++ b/homeassistant/components/deconz/sensor.py @@ -97,11 +97,13 @@ class DeconzSensor(DeconzDevice): """Representation of a deCONZ sensor.""" @callback - def async_update_callback(self, force_update=False): + def async_update_callback(self, force_update=False, ignore_update=False): """Update the sensor's state.""" - changed = set(self._device.changed_keys) + if ignore_update: + return + keys = {"on", "reachable", "state"} - if force_update or any(key in changed for key in keys): + if force_update or self._device.changed_keys.intersection(keys): self.async_schedule_update_ha_state() @property @@ -155,11 +157,13 @@ class DeconzBattery(DeconzDevice): """Battery class for when a device is only represented as an event.""" @callback - def async_update_callback(self, force_update=False): + def async_update_callback(self, force_update=False, ignore_update=False): """Update the battery's state, if needed.""" - changed = set(self._device.changed_keys) + if ignore_update: + return + keys = {"battery", "reachable"} - if force_update or any(key in changed for key in keys): + if force_update or self._device.changed_keys.intersection(keys): self.async_schedule_update_ha_state() @property @@ -217,7 +221,7 @@ class DeconzSensorStateTracker: self.sensor = None @callback - def async_update_callback(self): + def async_update_callback(self, ignore_update=False): """Sensor state updated.""" if "battery" in self.sensor.changed_keys: async_dispatcher_send( diff --git a/homeassistant/components/deconz/services.py b/homeassistant/components/deconz/services.py index f20ff65c434..f893b9880fd 100644 --- a/homeassistant/components/deconz/services.py +++ b/homeassistant/components/deconz/services.py @@ -127,7 +127,7 @@ async def async_refresh_devices_service(hass, data): scenes = set(gateway.api.scenes.keys()) sensors = set(gateway.api.sensors.keys()) - await gateway.api.refresh_state() + await gateway.api.refresh_state(ignore_update=True) gateway.async_add_device_callback( NEW_GROUP, diff --git a/requirements_all.txt b/requirements_all.txt index f59edf2b4b8..508ff673ad0 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1194,7 +1194,7 @@ pydaikin==1.6.1 pydanfossair==0.1.0 # homeassistant.components.deconz -pydeconz==67 +pydeconz==68 # homeassistant.components.delijn pydelijn==0.5.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index d7f44aed82b..fcd7b1df821 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -414,7 +414,7 @@ pycoolmasternet==0.0.4 pydaikin==1.6.1 # homeassistant.components.deconz -pydeconz==67 +pydeconz==68 # homeassistant.components.zwave pydispatcher==2.0.5 From 353010712f63cb02d176ef186ed6901d915a3a2e Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Fri, 17 Jan 2020 23:54:53 +0100 Subject: [PATCH 7/9] pdated frontend to 20200108.2 (#30921) --- homeassistant/components/frontend/manifest.json | 10 +++------- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 6 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/frontend/manifest.json b/homeassistant/components/frontend/manifest.json index 1bc1900ee94..bd732a7a0a1 100644 --- a/homeassistant/components/frontend/manifest.json +++ b/homeassistant/components/frontend/manifest.json @@ -2,9 +2,7 @@ "domain": "frontend", "name": "Home Assistant Frontend", "documentation": "https://www.home-assistant.io/integrations/frontend", - "requirements": [ - "home-assistant-frontend==20200108.0" - ], + "requirements": ["home-assistant-frontend==20200108.2"], "dependencies": [ "api", "auth", @@ -14,8 +12,6 @@ "system_log", "websocket_api" ], - "codeowners": [ - "@home-assistant/frontend" - ], + "codeowners": ["@home-assistant/frontend"], "quality_scale": "internal" -} \ No newline at end of file +} diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 2cc4e1c65d6..c3e7dcea692 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -11,7 +11,7 @@ cryptography==2.8 defusedxml==0.6.0 distro==1.4.0 hass-nabucasa==0.31 -home-assistant-frontend==20200108.0 +home-assistant-frontend==20200108.2 importlib-metadata==1.3.0 jinja2>=2.10.3 netdisco==2.6.0 diff --git a/requirements_all.txt b/requirements_all.txt index 508ff673ad0..bddb1908b85 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -679,7 +679,7 @@ hole==0.5.0 holidays==0.9.12 # homeassistant.components.frontend -home-assistant-frontend==20200108.0 +home-assistant-frontend==20200108.2 # homeassistant.components.zwave homeassistant-pyozw==0.1.7 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index fcd7b1df821..cc9f3e97e85 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -244,7 +244,7 @@ hole==0.5.0 holidays==0.9.12 # homeassistant.components.frontend -home-assistant-frontend==20200108.0 +home-assistant-frontend==20200108.2 # homeassistant.components.zwave homeassistant-pyozw==0.1.7 From 2b733917a4354124c22794c1dd60d451efa460a2 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 17 Jan 2020 15:38:38 -0800 Subject: [PATCH 8/9] Fix hue accepting filename (#30924) --- homeassistant/components/hue/__init__.py | 7 +++++-- tests/components/hue/test_init.py | 23 +++++++++++++++-------- 2 files changed, 20 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/hue/__init__.py b/homeassistant/components/hue/__init__.py index b294a811c61..7349f4fe6a6 100644 --- a/homeassistant/components/hue/__init__.py +++ b/homeassistant/components/hue/__init__.py @@ -36,6 +36,7 @@ BRIDGE_CONFIG_SCHEMA = vol.Schema( vol.Optional( CONF_ALLOW_HUE_GROUPS, default=DEFAULT_ALLOW_HUE_GROUPS ): cv.boolean, + vol.Optional("filename"): str, } ) @@ -46,8 +47,10 @@ CONFIG_SCHEMA = vol.Schema( vol.Optional(CONF_BRIDGES): vol.All( cv.ensure_list, [ - cv.deprecated("filename", invalidation_version="0.106.0"), - vol.All(BRIDGE_CONFIG_SCHEMA), + vol.All( + cv.deprecated("filename", invalidation_version="0.106.0"), + BRIDGE_CONFIG_SCHEMA, + ), ], ) } diff --git a/tests/components/hue/test_init.py b/tests/components/hue/test_init.py index 2e147fd097b..35e1ba689b4 100644 --- a/tests/components/hue/test_init.py +++ b/tests/components/hue/test_init.py @@ -29,12 +29,14 @@ async def test_setup_defined_hosts_known_auth(hass): hue.DOMAIN, { hue.DOMAIN: { - hue.CONF_BRIDGES: { - hue.CONF_HOST: "0.0.0.0", - hue.CONF_ALLOW_HUE_GROUPS: False, - hue.CONF_ALLOW_UNREACHABLE: True, - "filename": "bla", - } + hue.CONF_BRIDGES: [ + { + hue.CONF_HOST: "0.0.0.0", + hue.CONF_ALLOW_HUE_GROUPS: False, + hue.CONF_ALLOW_UNREACHABLE: True, + }, + {hue.CONF_HOST: "1.1.1.1", "filename": "bla"}, + ] } }, ) @@ -42,7 +44,7 @@ async def test_setup_defined_hosts_known_auth(hass): ) # Flow started for discovered bridge - assert len(hass.config_entries.flow.async_progress()) == 0 + assert len(hass.config_entries.flow.async_progress()) == 1 # Config stored for domain. assert hass.data[hue.DATA_CONFIGS] == { @@ -50,8 +52,13 @@ async def test_setup_defined_hosts_known_auth(hass): hue.CONF_HOST: "0.0.0.0", hue.CONF_ALLOW_HUE_GROUPS: False, hue.CONF_ALLOW_UNREACHABLE: True, + }, + "1.1.1.1": { + hue.CONF_HOST: "1.1.1.1", + hue.CONF_ALLOW_HUE_GROUPS: True, + hue.CONF_ALLOW_UNREACHABLE: False, "filename": "bla", - } + }, } From 2c915af348d182825141ab687d04e0be2515df9a Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 17 Jan 2020 15:48:27 -0800 Subject: [PATCH 9/9] Bumped version to 0.104.2 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 77e681432db..e2dfdf5ef64 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -1,7 +1,7 @@ """Constants used by Home Assistant components.""" MAJOR_VERSION = 0 MINOR_VERSION = 104 -PATCH_VERSION = "1" +PATCH_VERSION = "2" __short_version__ = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__ = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER = (3, 7, 0)