diff --git a/homeassistant/components/alexa/smart_home.py b/homeassistant/components/alexa/smart_home.py index 1558a1bf218..beb1a9c15ee 100644 --- a/homeassistant/components/alexa/smart_home.py +++ b/homeassistant/components/alexa/smart_home.py @@ -27,8 +27,9 @@ from homeassistant.const import ( CONF_NAME, SERVICE_LOCK, SERVICE_MEDIA_NEXT_TRACK, SERVICE_MEDIA_PAUSE, SERVICE_MEDIA_PLAY, SERVICE_MEDIA_PREVIOUS_TRACK, SERVICE_MEDIA_STOP, SERVICE_SET_COVER_POSITION, SERVICE_TURN_OFF, SERVICE_TURN_ON, - SERVICE_UNLOCK, SERVICE_VOLUME_SET, STATE_LOCKED, STATE_ON, STATE_UNLOCKED, - TEMP_CELSIUS, TEMP_FAHRENHEIT, MATCH_ALL) + SERVICE_UNLOCK, SERVICE_VOLUME_SET, STATE_LOCKED, STATE_ON, + STATE_UNAVAILABLE, STATE_UNLOCKED, TEMP_CELSIUS, TEMP_FAHRENHEIT, + MATCH_ALL) import homeassistant.core as ha import homeassistant.util.color as color_util from homeassistant.util.decorator import Registry @@ -393,6 +394,37 @@ class _AlexaInterface: } +class _AlexaEndpointHealth(_AlexaInterface): + """Implements Alexa.EndpointHealth. + + https://developer.amazon.com/docs/smarthome/state-reporting-for-a-smart-home-skill.html#report-state-when-alexa-requests-it + """ + + def __init__(self, hass, entity): + super().__init__(entity) + self.hass = hass + + def name(self): + return 'Alexa.EndpointHealth' + + def properties_supported(self): + return [{'name': 'connectivity'}] + + def properties_proactively_reported(self): + return False + + def properties_retrievable(self): + return True + + def get_property(self, name): + if name != 'connectivity': + raise _UnsupportedProperty(name) + + if self.entity.state == STATE_UNAVAILABLE: + return {'value': 'UNREACHABLE'} + return {'value': 'OK'} + + class _AlexaPowerController(_AlexaInterface): """Implements Alexa.PowerController. @@ -769,7 +801,8 @@ class _GenericCapabilities(_AlexaEntity): return [_DisplayCategory.OTHER] def interfaces(self): - return [_AlexaPowerController(self.entity)] + return [_AlexaPowerController(self.entity), + _AlexaEndpointHealth(self.hass, self.entity)] @ENTITY_ADAPTERS.register(switch.DOMAIN) @@ -778,7 +811,8 @@ class _SwitchCapabilities(_AlexaEntity): return [_DisplayCategory.SWITCH] def interfaces(self): - return [_AlexaPowerController(self.entity)] + return [_AlexaPowerController(self.entity), + _AlexaEndpointHealth(self.hass, self.entity)] @ENTITY_ADAPTERS.register(climate.DOMAIN) @@ -792,6 +826,7 @@ class _ClimateCapabilities(_AlexaEntity): yield _AlexaPowerController(self.entity) yield _AlexaThermostatController(self.hass, self.entity) yield _AlexaTemperatureSensor(self.hass, self.entity) + yield _AlexaEndpointHealth(self.hass, self.entity) @ENTITY_ADAPTERS.register(cover.DOMAIN) @@ -804,6 +839,7 @@ class _CoverCapabilities(_AlexaEntity): supported = self.entity.attributes.get(ATTR_SUPPORTED_FEATURES, 0) if supported & cover.SUPPORT_SET_POSITION: yield _AlexaPercentageController(self.entity) + yield _AlexaEndpointHealth(self.hass, self.entity) @ENTITY_ADAPTERS.register(light.DOMAIN) @@ -821,6 +857,7 @@ class _LightCapabilities(_AlexaEntity): yield _AlexaColorController(self.entity) if supported & light.SUPPORT_COLOR_TEMP: yield _AlexaColorTemperatureController(self.entity) + yield _AlexaEndpointHealth(self.hass, self.entity) @ENTITY_ADAPTERS.register(fan.DOMAIN) @@ -833,6 +870,7 @@ class _FanCapabilities(_AlexaEntity): supported = self.entity.attributes.get(ATTR_SUPPORTED_FEATURES, 0) if supported & fan.SUPPORT_SET_SPEED: yield _AlexaPercentageController(self.entity) + yield _AlexaEndpointHealth(self.hass, self.entity) @ENTITY_ADAPTERS.register(lock.DOMAIN) @@ -841,7 +879,8 @@ class _LockCapabilities(_AlexaEntity): return [_DisplayCategory.SMARTLOCK] def interfaces(self): - return [_AlexaLockController(self.entity)] + return [_AlexaLockController(self.entity), + _AlexaEndpointHealth(self.hass, self.entity)] @ENTITY_ADAPTERS.register(media_player.DOMAIN) @@ -851,6 +890,7 @@ class _MediaPlayerCapabilities(_AlexaEntity): def interfaces(self): yield _AlexaPowerController(self.entity) + yield _AlexaEndpointHealth(self.hass, self.entity) supported = self.entity.attributes.get(ATTR_SUPPORTED_FEATURES, 0) if supported & media_player.SUPPORT_VOLUME_SET: @@ -913,6 +953,7 @@ class _SensorCapabilities(_AlexaEntity): TEMP_CELSIUS, ): yield _AlexaTemperatureSensor(self.hass, self.entity) + yield _AlexaEndpointHealth(self.hass, self.entity) @ENTITY_ADAPTERS.register(binary_sensor.DOMAIN) @@ -934,6 +975,8 @@ class _BinarySensorCapabilities(_AlexaEntity): elif sensor_type is self.TYPE_MOTION: yield _AlexaMotionSensor(self.hass, self.entity) + yield _AlexaEndpointHealth(self.hass, self.entity) + def get_type(self): """Return the type of binary sensor.""" attrs = self.entity.attributes diff --git a/tests/components/alexa/test_smart_home.py b/tests/components/alexa/test_smart_home.py index 93551076461..536112e9b0b 100644 --- a/tests/components/alexa/test_smart_home.py +++ b/tests/components/alexa/test_smart_home.py @@ -153,6 +153,15 @@ async def discovery_test(device, hass, expected_endpoints=1): return None +def get_capability(capabilities, capability_name): + """Search a set of capabilities for a specific one.""" + for capability in capabilities: + if capability['interface'] == capability_name: + return capability + + return None + + def assert_endpoint_capabilities(endpoint, *interfaces): """Assert the endpoint supports the given interfaces. @@ -176,7 +185,11 @@ async def test_switch(hass, events): assert appliance['endpointId'] == 'switch#test' assert appliance['displayCategories'][0] == "SWITCH" assert appliance['friendlyName'] == "Test switch" - assert_endpoint_capabilities(appliance, 'Alexa.PowerController') + assert_endpoint_capabilities( + appliance, + 'Alexa.PowerController', + 'Alexa.EndpointHealth', + ) await assert_power_controller_works( 'switch#test', @@ -196,7 +209,11 @@ async def test_light(hass): assert appliance['endpointId'] == 'light#test_1' assert appliance['displayCategories'][0] == "LIGHT" assert appliance['friendlyName'] == "Test light 1" - assert_endpoint_capabilities(appliance, 'Alexa.PowerController') + assert_endpoint_capabilities( + appliance, + 'Alexa.PowerController', + 'Alexa.EndpointHealth', + ) await assert_power_controller_works( 'light#test_1', @@ -222,6 +239,7 @@ async def test_dimmable_light(hass): appliance, 'Alexa.BrightnessController', 'Alexa.PowerController', + 'Alexa.EndpointHealth', ) properties = await reported_properties(hass, 'light#test_2') @@ -260,6 +278,7 @@ async def test_color_light(hass): 'Alexa.PowerController', 'Alexa.ColorController', 'Alexa.ColorTemperatureController', + 'Alexa.EndpointHealth', ) # IncreaseColorTemperature and DecreaseColorTemperature have their own @@ -277,7 +296,8 @@ async def test_script(hass): (capability,) = assert_endpoint_capabilities( appliance, - 'Alexa.SceneController') + 'Alexa.SceneController', + ) assert not capability['supportsDeactivation'] await assert_scene_controller_works( @@ -299,7 +319,8 @@ async def test_cancelable_script(hass): assert appliance['endpointId'] == 'script#test_2' (capability,) = assert_endpoint_capabilities( appliance, - 'Alexa.SceneController') + 'Alexa.SceneController', + ) assert capability['supportsDeactivation'] await assert_scene_controller_works( @@ -321,7 +342,11 @@ async def test_input_boolean(hass): assert appliance['endpointId'] == 'input_boolean#test' assert appliance['displayCategories'][0] == "OTHER" assert appliance['friendlyName'] == "Test input boolean" - assert_endpoint_capabilities(appliance, 'Alexa.PowerController') + assert_endpoint_capabilities( + appliance, + 'Alexa.PowerController', + 'Alexa.EndpointHealth', + ) await assert_power_controller_works( 'input_boolean#test', @@ -341,7 +366,8 @@ async def test_scene(hass): (capability,) = assert_endpoint_capabilities( appliance, - 'Alexa.SceneController') + 'Alexa.SceneController' + ) assert not capability['supportsDeactivation'] await assert_scene_controller_works( @@ -359,7 +385,11 @@ async def test_fan(hass): assert appliance['endpointId'] == 'fan#test_1' assert appliance['displayCategories'][0] == "OTHER" assert appliance['friendlyName'] == "Test fan 1" - assert_endpoint_capabilities(appliance, 'Alexa.PowerController') + assert_endpoint_capabilities( + appliance, + 'Alexa.PowerController', + 'Alexa.EndpointHealth', + ) async def test_variable_fan(hass): @@ -386,6 +416,7 @@ async def test_variable_fan(hass): appliance, 'Alexa.PercentageController', 'Alexa.PowerController', + 'Alexa.EndpointHealth', ) call, _ = await assert_request_calls_service( @@ -412,7 +443,11 @@ async def test_lock(hass): assert appliance['endpointId'] == 'lock#test' assert appliance['displayCategories'][0] == "SMARTLOCK" assert appliance['friendlyName'] == "Test lock" - assert_endpoint_capabilities(appliance, 'Alexa.LockController') + assert_endpoint_capabilities( + appliance, + 'Alexa.LockController', + 'Alexa.EndpointHealth', + ) _, msg = await assert_request_calls_service( 'Alexa.LockController', 'Lock', 'lock#test', @@ -449,6 +484,7 @@ async def test_media_player(hass): 'Alexa.Speaker', 'Alexa.StepSpeaker', 'Alexa.PlaybackController', + 'Alexa.EndpointHealth', ) await assert_power_controller_works( @@ -546,7 +582,11 @@ async def test_alert(hass): assert appliance['endpointId'] == 'alert#test' assert appliance['displayCategories'][0] == "OTHER" assert appliance['friendlyName'] == "Test alert" - assert_endpoint_capabilities(appliance, 'Alexa.PowerController') + assert_endpoint_capabilities( + appliance, + 'Alexa.PowerController', + 'Alexa.EndpointHealth', + ) await assert_power_controller_works( 'alert#test', @@ -563,7 +603,11 @@ async def test_automation(hass): assert appliance['endpointId'] == 'automation#test' assert appliance['displayCategories'][0] == "OTHER" assert appliance['friendlyName'] == "Test automation" - assert_endpoint_capabilities(appliance, 'Alexa.PowerController') + assert_endpoint_capabilities( + appliance, + 'Alexa.PowerController', + 'Alexa.EndpointHealth', + ) await assert_power_controller_works( 'automation#test', @@ -580,7 +624,11 @@ async def test_group(hass): assert appliance['endpointId'] == 'group#test' assert appliance['displayCategories'][0] == "OTHER" assert appliance['friendlyName'] == "Test group" - assert_endpoint_capabilities(appliance, 'Alexa.PowerController') + assert_endpoint_capabilities( + appliance, + 'Alexa.PowerController', + 'Alexa.EndpointHealth', + ) await assert_power_controller_works( 'group#test', @@ -609,6 +657,7 @@ async def test_cover(hass): appliance, 'Alexa.PercentageController', 'Alexa.PowerController', + 'Alexa.EndpointHealth', ) await assert_power_controller_works( @@ -675,11 +724,16 @@ async def test_temp_sensor(hass): assert appliance['displayCategories'][0] == 'TEMPERATURE_SENSOR' assert appliance['friendlyName'] == 'Test Temp Sensor' - (capability,) = assert_endpoint_capabilities( + capabilities = assert_endpoint_capabilities( appliance, - 'Alexa.TemperatureSensor') - assert capability['interface'] == 'Alexa.TemperatureSensor' - properties = capability['properties'] + 'Alexa.TemperatureSensor', + 'Alexa.EndpointHealth', + ) + + temp_sensor_capability = get_capability(capabilities, + 'Alexa.TemperatureSensor') + assert temp_sensor_capability is not None + properties = temp_sensor_capability['properties'] assert properties['retrievable'] is True assert {'name': 'temperature'} in properties['supported'] @@ -704,11 +758,16 @@ async def test_contact_sensor(hass): assert appliance['displayCategories'][0] == 'CONTACT_SENSOR' assert appliance['friendlyName'] == 'Test Contact Sensor' - (capability,) = assert_endpoint_capabilities( + capabilities = assert_endpoint_capabilities( appliance, - 'Alexa.ContactSensor') - assert capability['interface'] == 'Alexa.ContactSensor' - properties = capability['properties'] + 'Alexa.ContactSensor', + 'Alexa.EndpointHealth', + ) + + contact_sensor_capability = get_capability(capabilities, + 'Alexa.ContactSensor') + assert contact_sensor_capability is not None + properties = contact_sensor_capability['properties'] assert properties['retrievable'] is True assert {'name': 'detectionState'} in properties['supported'] @@ -717,6 +776,9 @@ async def test_contact_sensor(hass): properties.assert_equal('Alexa.ContactSensor', 'detectionState', 'DETECTED') + properties.assert_equal('Alexa.EndpointHealth', 'connectivity', + {'value': 'OK'}) + async def test_motion_sensor(hass): """Test motion sensor discovery.""" @@ -734,11 +796,16 @@ async def test_motion_sensor(hass): assert appliance['displayCategories'][0] == 'MOTION_SENSOR' assert appliance['friendlyName'] == 'Test Motion Sensor' - (capability,) = assert_endpoint_capabilities( + capabilities = assert_endpoint_capabilities( appliance, - 'Alexa.MotionSensor') - assert capability['interface'] == 'Alexa.MotionSensor' - properties = capability['properties'] + 'Alexa.MotionSensor', + 'Alexa.EndpointHealth', + ) + + motion_sensor_capability = get_capability(capabilities, + 'Alexa.MotionSensor') + assert motion_sensor_capability is not None + properties = motion_sensor_capability['properties'] assert properties['retrievable'] is True assert {'name': 'detectionState'} in properties['supported'] @@ -787,6 +854,7 @@ async def test_thermostat(hass): appliance, 'Alexa.ThermostatController', 'Alexa.TemperatureSensor', + 'Alexa.EndpointHealth', ) properties = await reported_properties( @@ -1486,9 +1554,11 @@ async def test_entity_config(hass): assert appliance['displayCategories'][0] == "SWITCH" assert appliance['friendlyName'] == "Config name" assert appliance['description'] == "Config description" - assert len(appliance['capabilities']) == 1 - assert appliance['capabilities'][-1]['interface'] == \ - 'Alexa.PowerController' + assert_endpoint_capabilities( + appliance, + 'Alexa.PowerController', + 'Alexa.EndpointHealth', + ) async def test_unsupported_domain(hass): @@ -1651,6 +1721,38 @@ async def test_disabled(hass): assert msg['payload']['type'] == 'BRIDGE_UNREACHABLE' +async def test_endpoint_good_health(hass): + """Test endpoint health reporting.""" + device = ( + 'binary_sensor.test_contact', + 'on', + { + 'friendly_name': "Test Contact Sensor", + 'device_class': 'door', + } + ) + await discovery_test(device, hass) + properties = await reported_properties(hass, 'binary_sensor#test_contact') + properties.assert_equal('Alexa.EndpointHealth', 'connectivity', + {'value': 'OK'}) + + +async def test_endpoint_bad_health(hass): + """Test endpoint health reporting.""" + device = ( + 'binary_sensor.test_contact', + 'unavailable', + { + 'friendly_name': "Test Contact Sensor", + 'device_class': 'door', + } + ) + await discovery_test(device, hass) + properties = await reported_properties(hass, 'binary_sensor#test_contact') + properties.assert_equal('Alexa.EndpointHealth', 'connectivity', + {'value': 'UNREACHABLE'}) + + async def test_report_state(hass, aioclient_mock): """Test proactive state reports.""" aioclient_mock.post(TEST_URL, json={'data': 'is irrelevant'})