diff --git a/homeassistant/components/homekit/__init__.py b/homeassistant/components/homekit/__init__.py index 7e1bbad70b4..37927758862 100644 --- a/homeassistant/components/homekit/__init__.py +++ b/homeassistant/components/homekit/__init__.py @@ -163,6 +163,7 @@ BATTERY_CHARGING_SENSOR = ( BinarySensorDeviceClass.BATTERY_CHARGING, ) BATTERY_SENSOR = (SENSOR_DOMAIN, SensorDeviceClass.BATTERY) +MOTION_EVENT_SENSOR = (EVENT_DOMAIN, EventDeviceClass.MOTION) MOTION_SENSOR = (BINARY_SENSOR_DOMAIN, BinarySensorDeviceClass.MOTION) DOORBELL_EVENT_SENSOR = (EVENT_DOMAIN, EventDeviceClass.DOORBELL) DOORBELL_BINARY_SENSOR = (BINARY_SENSOR_DOMAIN, BinarySensorDeviceClass.OCCUPANCY) @@ -1121,7 +1122,11 @@ class HomeKit: ) if domain == CAMERA_DOMAIN: - if motion_binary_sensor_entity_id := lookup.get(MOTION_SENSOR): + if motion_event_entity_id := lookup.get(MOTION_EVENT_SENSOR): + config[entity_id].setdefault( + CONF_LINKED_MOTION_SENSOR, motion_event_entity_id + ) + elif motion_binary_sensor_entity_id := lookup.get(MOTION_SENSOR): config[entity_id].setdefault( CONF_LINKED_MOTION_SENSOR, motion_binary_sensor_entity_id ) diff --git a/homeassistant/components/homekit/type_cameras.py b/homeassistant/components/homekit/type_cameras.py index ca3a2f0d021..40fd6b2aade 100644 --- a/homeassistant/components/homekit/type_cameras.py +++ b/homeassistant/components/homekit/type_cameras.py @@ -222,15 +222,19 @@ class Camera(HomeAccessory, PyhapCamera): # type: ignore[misc] ) self._char_motion_detected = None - self.linked_motion_sensor = self.config.get(CONF_LINKED_MOTION_SENSOR) - if self.linked_motion_sensor: - state = self.hass.states.get(self.linked_motion_sensor) - if state: + self.linked_motion_sensor: str | None = self.config.get( + CONF_LINKED_MOTION_SENSOR + ) + self.motion_is_event = False + if linked_motion_sensor := self.linked_motion_sensor: + self.motion_is_event = linked_motion_sensor.startswith("event.") + if state := self.hass.states.get(linked_motion_sensor): serv_motion = self.add_preload_service(SERV_MOTION_SENSOR) self._char_motion_detected = serv_motion.configure_char( CHAR_MOTION_DETECTED, value=False ) - self._async_update_motion_state(state) + if not self.motion_is_event: + self._async_update_motion_state(state) self._char_doorbell_detected = None self._char_doorbell_detected_switch = None @@ -309,12 +313,26 @@ class Camera(HomeAccessory, PyhapCamera): # type: ignore[misc] if not new_state: return - detected = new_state.state == STATE_ON - assert self._char_motion_detected - if self._char_motion_detected.value == detected: + state = new_state.state + char = self._char_motion_detected + assert char is not None + if self.motion_is_event: + if state in (STATE_UNKNOWN, STATE_UNAVAILABLE): + return + _LOGGER.debug( + "%s: Set linked motion %s sensor to True/False", + self.entity_id, + self.linked_motion_sensor, + ) + char.set_value(True) + char.set_value(False) return - self._char_motion_detected.set_value(detected) + detected = state == STATE_ON + if char.value == detected: + return + + char.set_value(detected) _LOGGER.debug( "%s: Set linked motion %s sensor to %d", self.entity_id, diff --git a/tests/components/homekit/test_homekit.py b/tests/components/homekit/test_homekit.py index da755dc26f3..45da90b5446 100644 --- a/tests/components/homekit/test_homekit.py +++ b/tests/components/homekit/test_homekit.py @@ -1939,12 +1939,21 @@ async def test_homekit_ignored_missing_devices( ) +@pytest.mark.parametrize( + ("domain", "device_class"), + [ + ("binary_sensor", BinarySensorDeviceClass.MOTION), + ("event", EventDeviceClass.MOTION), + ], +) @pytest.mark.usefixtures("mock_async_zeroconf") async def test_homekit_finds_linked_motion_sensors( hass: HomeAssistant, hk_driver, device_registry: dr.DeviceRegistry, entity_registry: er.EntityRegistry, + domain: str, + device_class: EventDeviceClass | BinarySensorDeviceClass, ) -> None: """Test HomeKit start method.""" entry = await async_init_integration(hass) @@ -1964,21 +1973,21 @@ async def test_homekit_finds_linked_motion_sensors( connections={(dr.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, ) - binary_motion_sensor = entity_registry.async_get_or_create( - "binary_sensor", + entry = entity_registry.async_get_or_create( + domain, "camera", "motion_sensor", device_id=device_entry.id, - original_device_class=BinarySensorDeviceClass.MOTION, + original_device_class=device_class, ) camera = entity_registry.async_get_or_create( "camera", "camera", "demo", device_id=device_entry.id ) hass.states.async_set( - binary_motion_sensor.entity_id, + entry.entity_id, STATE_ON, - {ATTR_DEVICE_CLASS: BinarySensorDeviceClass.MOTION}, + {ATTR_DEVICE_CLASS: device_class}, ) hass.states.async_set(camera.entity_id, STATE_ON) @@ -2001,7 +2010,7 @@ async def test_homekit_finds_linked_motion_sensors( "model": "Camera Server", "platform": "test", "sw_version": "0.16.0", - "linked_motion_sensor": "binary_sensor.camera_motion_sensor", + "linked_motion_sensor": entry.entity_id, }, ) diff --git a/tests/components/homekit/test_type_cameras.py b/tests/components/homekit/test_type_cameras.py index 510af680eaa..fd5d1835641 100644 --- a/tests/components/homekit/test_type_cameras.py +++ b/tests/components/homekit/test_type_cameras.py @@ -795,6 +795,103 @@ async def test_camera_with_linked_motion_sensor( assert char.value is True +async def test_camera_with_linked_motion_event( + hass: HomeAssistant, run_driver, events +) -> None: + """Test a camera with a linked motion event entity can update.""" + await async_setup_component(hass, ffmpeg.DOMAIN, {ffmpeg.DOMAIN: {}}) + await async_setup_component( + hass, camera.DOMAIN, {camera.DOMAIN: {"platform": "demo"}} + ) + await hass.async_block_till_done() + motion_entity_id = "event.motion" + + hass.states.async_set( + motion_entity_id, + dt_util.utcnow().isoformat(), + {ATTR_DEVICE_CLASS: EventDeviceClass.MOTION}, + ) + await hass.async_block_till_done() + entity_id = "camera.demo_camera" + + hass.states.async_set(entity_id, None) + await hass.async_block_till_done() + acc = Camera( + hass, + run_driver, + "Camera", + entity_id, + 2, + { + CONF_STREAM_SOURCE: "/dev/null", + CONF_SUPPORT_AUDIO: True, + CONF_VIDEO_CODEC: VIDEO_CODEC_H264_OMX, + CONF_AUDIO_CODEC: AUDIO_CODEC_COPY, + CONF_LINKED_MOTION_SENSOR: motion_entity_id, + }, + ) + bridge = HomeBridge("hass", run_driver, "Test Bridge") + bridge.add_accessory(acc) + + acc.run() + + assert acc.aid == 2 + assert acc.category == 17 # Camera + + service = acc.get_service(SERV_MOTION_SENSOR) + assert service + char = service.get_characteristic(CHAR_MOTION_DETECTED) + assert char + + assert char.value is False + broker = MagicMock() + char.broker = broker + + hass.states.async_set( + motion_entity_id, STATE_UNKNOWN, {ATTR_DEVICE_CLASS: EventDeviceClass.MOTION} + ) + await hass.async_block_till_done() + assert len(broker.mock_calls) == 0 + broker.reset_mock() + assert char.value is False + + char.set_value(True) + fire_time = dt_util.utcnow().isoformat() + hass.states.async_set( + motion_entity_id, fire_time, {ATTR_DEVICE_CLASS: EventDeviceClass.MOTION} + ) + await hass.async_block_till_done() + assert len(broker.mock_calls) == 4 + broker.reset_mock() + assert char.value is False + + hass.states.async_set( + motion_entity_id, + fire_time, + {ATTR_DEVICE_CLASS: EventDeviceClass.MOTION}, + force_update=True, + ) + await hass.async_block_till_done() + assert len(broker.mock_calls) == 0 + broker.reset_mock() + + hass.states.async_set( + motion_entity_id, + fire_time, + {ATTR_DEVICE_CLASS: EventDeviceClass.MOTION, "other": "attr"}, + ) + await hass.async_block_till_done() + assert len(broker.mock_calls) == 0 + broker.reset_mock() + # Ensure we do not throw when the linked + # motion sensor is removed + hass.states.async_remove(motion_entity_id) + await hass.async_block_till_done() + acc.run() + await hass.async_block_till_done() + assert char.value is False + + async def test_camera_with_a_missing_linked_motion_sensor( hass: HomeAssistant, run_driver, events ) -> None: