Add support for event entity motion sensors to HomeKit (#121123)
This commit is contained in:
parent
d429bcef16
commit
67a4c2c884
4 changed files with 145 additions and 16 deletions
|
@ -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
|
||||
)
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
},
|
||||
)
|
||||
|
||||
|
|
|
@ -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:
|
||||
|
|
Loading…
Add table
Reference in a new issue