From 4a89fba6f9a2f7b5db1247032fcae4b93d0bbf49 Mon Sep 17 00:00:00 2001 From: Jc2k Date: Tue, 25 Feb 2020 22:01:03 +0000 Subject: [PATCH] Add homekit_controller occupancy sensor (#32188) --- .../homekit_controller/binary_sensor.py | 28 +++ .../components/homekit_controller/const.py | 1 + .../specific_devices/test_ecobee_occupancy.py | 37 +++ .../homekit_controller/test_binary_sensor.py | 25 ++ .../homekit_controller/ecobee_occupancy.json | 236 ++++++++++++++++++ 5 files changed, 327 insertions(+) create mode 100644 tests/components/homekit_controller/specific_devices/test_ecobee_occupancy.py create mode 100644 tests/fixtures/homekit_controller/ecobee_occupancy.json diff --git a/homeassistant/components/homekit_controller/binary_sensor.py b/homeassistant/components/homekit_controller/binary_sensor.py index 39e37fab68e..7ca7f7a5711 100644 --- a/homeassistant/components/homekit_controller/binary_sensor.py +++ b/homeassistant/components/homekit_controller/binary_sensor.py @@ -5,6 +5,7 @@ from aiohomekit.model.characteristics import CharacteristicsTypes from homeassistant.components.binary_sensor import ( DEVICE_CLASS_MOTION, + DEVICE_CLASS_OCCUPANCY, DEVICE_CLASS_OPENING, DEVICE_CLASS_SMOKE, BinarySensorDevice, @@ -94,10 +95,37 @@ class HomeKitSmokeSensor(HomeKitEntity, BinarySensorDevice): return self._state == 1 +class HomeKitOccupancySensor(HomeKitEntity, BinarySensorDevice): + """Representation of a Homekit smoke sensor.""" + + def __init__(self, *args): + """Initialise the entity.""" + super().__init__(*args) + self._state = None + + @property + def device_class(self) -> str: + """Return the class of this sensor.""" + return DEVICE_CLASS_OCCUPANCY + + def get_characteristic_types(self): + """Define the homekit characteristics the entity is tracking.""" + return [CharacteristicsTypes.OCCUPANCY_DETECTED] + + def _update_occupancy_detected(self, value): + self._state = value + + @property + def is_on(self): + """Return true if smoke is currently detected.""" + return self._state == 1 + + ENTITY_TYPES = { "motion": HomeKitMotionSensor, "contact": HomeKitContactSensor, "smoke": HomeKitSmokeSensor, + "occupancy": HomeKitOccupancySensor, } diff --git a/homeassistant/components/homekit_controller/const.py b/homeassistant/components/homekit_controller/const.py index 684f83ba5d4..9c750b17e8f 100644 --- a/homeassistant/components/homekit_controller/const.py +++ b/homeassistant/components/homekit_controller/const.py @@ -30,4 +30,5 @@ HOMEKIT_ACCESSORY_DISPATCH = { "fan": "fan", "fanv2": "fan", "air-quality": "air_quality", + "occupancy": "binary_sensor", } diff --git a/tests/components/homekit_controller/specific_devices/test_ecobee_occupancy.py b/tests/components/homekit_controller/specific_devices/test_ecobee_occupancy.py new file mode 100644 index 00000000000..b1a8c0a636f --- /dev/null +++ b/tests/components/homekit_controller/specific_devices/test_ecobee_occupancy.py @@ -0,0 +1,37 @@ +""" +Regression tests for Ecobee occupancy. + +https://github.com/home-assistant/home-assistant/issues/31827 +""" + +from tests.components.homekit_controller.common import ( + Helper, + setup_accessories_from_file, + setup_test_accessories, +) + + +async def test_ecobee_occupancy_setup(hass): + """Test that an Ecbobee occupancy sensor be correctly setup in HA.""" + accessories = await setup_accessories_from_file(hass, "ecobee_occupancy.json") + config_entry, pairing = await setup_test_accessories(hass, accessories) + + entity_registry = await hass.helpers.entity_registry.async_get_registry() + + sensor = entity_registry.async_get("binary_sensor.master_fan") + assert sensor.unique_id == "homekit-111111111111-56" + + sensor_helper = Helper( + hass, "binary_sensor.master_fan", pairing, accessories[0], config_entry + ) + sensor_state = await sensor_helper.poll_and_get_state() + assert sensor_state.attributes["friendly_name"] == "Master Fan" + + device_registry = await hass.helpers.device_registry.async_get_registry() + + device = device_registry.async_get(sensor.device_id) + assert device.manufacturer == "ecobee Inc." + assert device.name == "Master Fan" + assert device.model == "ecobee Switch+" + assert device.sw_version == "4.5.130201" + assert device.via_device_id is None diff --git a/tests/components/homekit_controller/test_binary_sensor.py b/tests/components/homekit_controller/test_binary_sensor.py index 5107ae32bd5..8817ed5c22d 100644 --- a/tests/components/homekit_controller/test_binary_sensor.py +++ b/tests/components/homekit_controller/test_binary_sensor.py @@ -4,6 +4,7 @@ from aiohomekit.model.services import ServicesTypes from homeassistant.components.binary_sensor import ( DEVICE_CLASS_MOTION, + DEVICE_CLASS_OCCUPANCY, DEVICE_CLASS_OPENING, DEVICE_CLASS_SMOKE, ) @@ -13,6 +14,7 @@ from tests.components.homekit_controller.common import setup_test_component MOTION_DETECTED = ("motion", "motion-detected") CONTACT_STATE = ("contact", "contact-state") SMOKE_DETECTED = ("smoke", "smoke-detected") +OCCUPANCY_DETECTED = ("occupancy", "occupancy-detected") def create_motion_sensor_service(accessory): @@ -82,3 +84,26 @@ async def test_smoke_sensor_read_state(hass, utcnow): assert state.state == "on" assert state.attributes["device_class"] == DEVICE_CLASS_SMOKE + + +def create_occupancy_sensor_service(accessory): + """Define occupancy characteristics.""" + service = accessory.add_service(ServicesTypes.OCCUPANCY_SENSOR) + + cur_state = service.add_char(CharacteristicsTypes.OCCUPANCY_DETECTED) + cur_state.value = 0 + + +async def test_occupancy_sensor_read_state(hass, utcnow): + """Test that we can read the state of a HomeKit occupancy sensor accessory.""" + helper = await setup_test_component(hass, create_occupancy_sensor_service) + + helper.characteristics[OCCUPANCY_DETECTED].value = False + state = await helper.poll_and_get_state() + assert state.state == "off" + + helper.characteristics[OCCUPANCY_DETECTED].value = True + state = await helper.poll_and_get_state() + assert state.state == "on" + + assert state.attributes["device_class"] == DEVICE_CLASS_OCCUPANCY diff --git a/tests/fixtures/homekit_controller/ecobee_occupancy.json b/tests/fixtures/homekit_controller/ecobee_occupancy.json new file mode 100644 index 00000000000..78c98599961 --- /dev/null +++ b/tests/fixtures/homekit_controller/ecobee_occupancy.json @@ -0,0 +1,236 @@ +[ + { + "aid": 1, + "services": [ + { + "characteristics": [ + { + "format": "string", + "iid": 2, + "perms": [ + "pr" + ], + "type": "00000023-0000-1000-8000-0026BB765291", + "value": "Master Fan" + }, + { + "format": "string", + "iid": 3, + "perms": [ + "pr" + ], + "type": "00000020-0000-1000-8000-0026BB765291", + "value": "ecobee Inc." + }, + { + "format": "string", + "iid": 4, + "perms": [ + "pr" + ], + "type": "00000030-0000-1000-8000-0026BB765291", + "value": "111111111111" + }, + { + "format": "string", + "iid": 5, + "perms": [ + "pr" + ], + "type": "00000021-0000-1000-8000-0026BB765291", + "value": "ecobee Switch+" + }, + { + "format": "bool", + "iid": 6, + "perms": [ + "pw" + ], + "type": "00000014-0000-1000-8000-0026BB765291" + }, + { + "format": "string", + "iid": 8, + "perms": [ + "pr" + ], + "type": "00000052-0000-1000-8000-0026BB765291", + "value": "4.5.130201" + }, + { + "format": "uint32", + "iid": 9, + "perms": [ + "pr", + "ev" + ], + "type": "000000A6-0000-1000-8000-0026BB765291", + "value": 0 + } + ], + "iid": 1, + "stype": "accessory-information", + "type": "0000003E-0000-1000-8000-0026BB765291" + }, + { + "characteristics": [ + { + "format": "string", + "iid": 31, + "maxLen": 64, + "perms": [ + "pr" + ], + "type": "00000037-0000-1000-8000-0026BB765291", + "value": "1.1.0" + } + ], + "iid": 30, + "stype": "service", + "type": "000000A2-0000-1000-8000-0026BB765291" + }, + { + "characteristics": [ + { + "format": "bool", + "iid": 17, + "perms": [ + "pr", + "pw", + "ev" + ], + "type": "00000025-0000-1000-8000-0026BB765291", + "value": false + }, + { + "format": "string", + "iid": 18, + "perms": [ + "pr" + ], + "type": "00000023-0000-1000-8000-0026BB765291", + "value": "Master Fan" + } + ], + "iid": 16, + "primary": true, + "stype": "switch", + "type": "00000049-0000-1000-8000-0026BB765291" + }, + { + "characteristics": [ + { + "format": "float", + "iid": 20, + "maxValue": 100000, + "minValue": 0, + "perms": [ + "pr", + "ev" + ], + "type": "0000006B-0000-1000-8000-0026BB765291", + "unit": "lux", + "value": 0 + }, + { + "format": "string", + "iid": 21, + "perms": [ + "pr" + ], + "type": "00000023-0000-1000-8000-0026BB765291", + "value": "Master Fan" + } + ], + "iid": 27, + "stype": "light", + "type": "00000084-0000-1000-8000-0026BB765291" + }, + { + "characteristics": [ + { + "format": "bool", + "iid": 66, + "perms": [ + "pr", + "ev" + ], + "type": "00000022-0000-1000-8000-0026BB765291", + "value": false + }, + { + "format": "string", + "iid": 28, + "perms": [ + "pr" + ], + "type": "00000023-0000-1000-8000-0026BB765291", + "value": "Master Fan" + } + ], + "iid": 56, + "stype": "motion", + "type": "00000085-0000-1000-8000-0026BB765291" + }, + { + "characteristics": [ + { + "format": "uint8", + "iid": 65, + "maxValue": 1, + "minStep": 1, + "minValue": 0, + "perms": [ + "pr", + "ev" + ], + "type": "00000071-0000-1000-8000-0026BB765291", + "value": 0 + }, + { + "format": "string", + "iid": 29, + "perms": [ + "pr" + ], + "type": "00000023-0000-1000-8000-0026BB765291", + "value": "Master Fan" + } + ], + "iid": 57, + "stype": "occupancy", + "type": "00000086-0000-1000-8000-0026BB765291" + }, + { + "characteristics": [ + { + "format": "float", + "iid": 19, + "maxValue": 100, + "minStep": 0.1, + "minValue": 0, + "perms": [ + "pr", + "ev" + ], + "type": "00000011-0000-1000-8000-0026BB765291", + "unit": "celsius", + "value": 25.6 + }, + { + "format": "string", + "iid": 22, + "perms": [ + "pr" + ], + "type": "00000023-0000-1000-8000-0026BB765291", + "value": "Master Fan" + } + ], + "iid": 55, + "stype": "temperature", + "type": "0000008A-0000-1000-8000-0026BB765291" + } + ] + } +] \ No newline at end of file