Add support for event entity motion sensors to HomeKit (#121123)

This commit is contained in:
J. Nick Koston 2024-07-04 04:50:50 -05:00 committed by GitHub
parent d429bcef16
commit 67a4c2c884
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 145 additions and 16 deletions

View file

@ -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
)

View file

@ -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,

View file

@ -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,
},
)

View file

@ -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: