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,
|
BinarySensorDeviceClass.BATTERY_CHARGING,
|
||||||
)
|
)
|
||||||
BATTERY_SENSOR = (SENSOR_DOMAIN, SensorDeviceClass.BATTERY)
|
BATTERY_SENSOR = (SENSOR_DOMAIN, SensorDeviceClass.BATTERY)
|
||||||
|
MOTION_EVENT_SENSOR = (EVENT_DOMAIN, EventDeviceClass.MOTION)
|
||||||
MOTION_SENSOR = (BINARY_SENSOR_DOMAIN, BinarySensorDeviceClass.MOTION)
|
MOTION_SENSOR = (BINARY_SENSOR_DOMAIN, BinarySensorDeviceClass.MOTION)
|
||||||
DOORBELL_EVENT_SENSOR = (EVENT_DOMAIN, EventDeviceClass.DOORBELL)
|
DOORBELL_EVENT_SENSOR = (EVENT_DOMAIN, EventDeviceClass.DOORBELL)
|
||||||
DOORBELL_BINARY_SENSOR = (BINARY_SENSOR_DOMAIN, BinarySensorDeviceClass.OCCUPANCY)
|
DOORBELL_BINARY_SENSOR = (BINARY_SENSOR_DOMAIN, BinarySensorDeviceClass.OCCUPANCY)
|
||||||
|
@ -1121,7 +1122,11 @@ class HomeKit:
|
||||||
)
|
)
|
||||||
|
|
||||||
if domain == CAMERA_DOMAIN:
|
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(
|
config[entity_id].setdefault(
|
||||||
CONF_LINKED_MOTION_SENSOR, motion_binary_sensor_entity_id
|
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._char_motion_detected = None
|
||||||
self.linked_motion_sensor = self.config.get(CONF_LINKED_MOTION_SENSOR)
|
self.linked_motion_sensor: str | None = self.config.get(
|
||||||
if self.linked_motion_sensor:
|
CONF_LINKED_MOTION_SENSOR
|
||||||
state = self.hass.states.get(self.linked_motion_sensor)
|
)
|
||||||
if state:
|
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)
|
serv_motion = self.add_preload_service(SERV_MOTION_SENSOR)
|
||||||
self._char_motion_detected = serv_motion.configure_char(
|
self._char_motion_detected = serv_motion.configure_char(
|
||||||
CHAR_MOTION_DETECTED, value=False
|
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 = None
|
||||||
self._char_doorbell_detected_switch = None
|
self._char_doorbell_detected_switch = None
|
||||||
|
@ -309,12 +313,26 @@ class Camera(HomeAccessory, PyhapCamera): # type: ignore[misc]
|
||||||
if not new_state:
|
if not new_state:
|
||||||
return
|
return
|
||||||
|
|
||||||
detected = new_state.state == STATE_ON
|
state = new_state.state
|
||||||
assert self._char_motion_detected
|
char = self._char_motion_detected
|
||||||
if self._char_motion_detected.value == 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
|
return
|
||||||
|
|
||||||
self._char_motion_detected.set_value(detected)
|
detected = state == STATE_ON
|
||||||
|
if char.value == detected:
|
||||||
|
return
|
||||||
|
|
||||||
|
char.set_value(detected)
|
||||||
_LOGGER.debug(
|
_LOGGER.debug(
|
||||||
"%s: Set linked motion %s sensor to %d",
|
"%s: Set linked motion %s sensor to %d",
|
||||||
self.entity_id,
|
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")
|
@pytest.mark.usefixtures("mock_async_zeroconf")
|
||||||
async def test_homekit_finds_linked_motion_sensors(
|
async def test_homekit_finds_linked_motion_sensors(
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
hk_driver,
|
hk_driver,
|
||||||
device_registry: dr.DeviceRegistry,
|
device_registry: dr.DeviceRegistry,
|
||||||
entity_registry: er.EntityRegistry,
|
entity_registry: er.EntityRegistry,
|
||||||
|
domain: str,
|
||||||
|
device_class: EventDeviceClass | BinarySensorDeviceClass,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Test HomeKit start method."""
|
"""Test HomeKit start method."""
|
||||||
entry = await async_init_integration(hass)
|
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")},
|
connections={(dr.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")},
|
||||||
)
|
)
|
||||||
|
|
||||||
binary_motion_sensor = entity_registry.async_get_or_create(
|
entry = entity_registry.async_get_or_create(
|
||||||
"binary_sensor",
|
domain,
|
||||||
"camera",
|
"camera",
|
||||||
"motion_sensor",
|
"motion_sensor",
|
||||||
device_id=device_entry.id,
|
device_id=device_entry.id,
|
||||||
original_device_class=BinarySensorDeviceClass.MOTION,
|
original_device_class=device_class,
|
||||||
)
|
)
|
||||||
camera = entity_registry.async_get_or_create(
|
camera = entity_registry.async_get_or_create(
|
||||||
"camera", "camera", "demo", device_id=device_entry.id
|
"camera", "camera", "demo", device_id=device_entry.id
|
||||||
)
|
)
|
||||||
|
|
||||||
hass.states.async_set(
|
hass.states.async_set(
|
||||||
binary_motion_sensor.entity_id,
|
entry.entity_id,
|
||||||
STATE_ON,
|
STATE_ON,
|
||||||
{ATTR_DEVICE_CLASS: BinarySensorDeviceClass.MOTION},
|
{ATTR_DEVICE_CLASS: device_class},
|
||||||
)
|
)
|
||||||
hass.states.async_set(camera.entity_id, STATE_ON)
|
hass.states.async_set(camera.entity_id, STATE_ON)
|
||||||
|
|
||||||
|
@ -2001,7 +2010,7 @@ async def test_homekit_finds_linked_motion_sensors(
|
||||||
"model": "Camera Server",
|
"model": "Camera Server",
|
||||||
"platform": "test",
|
"platform": "test",
|
||||||
"sw_version": "0.16.0",
|
"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
|
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(
|
async def test_camera_with_a_missing_linked_motion_sensor(
|
||||||
hass: HomeAssistant, run_driver, events
|
hass: HomeAssistant, run_driver, events
|
||||||
) -> None:
|
) -> None:
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue