Add support for HomeKit doorbell (#38419)
* Add support for HomeKit doorbell * Update homeassistant/components/homekit/type_cameras.py Co-authored-by: J. Nick Koston <nick@koston.org> * Update homeassistant/components/homekit/type_cameras.py Co-authored-by: J. Nick Koston <nick@koston.org> * add speaker service for doorbells * fixed test as doorbell char requires null value * removed null value for doorbell presses. and removed broken override of default values Co-authored-by: J. Nick Koston <nick@koston.org>
This commit is contained in:
parent
bb69aba051
commit
9d0f58009e
5 changed files with 179 additions and 10 deletions
|
@ -11,6 +11,7 @@ from homeassistant.components import zeroconf
|
|||
from homeassistant.components.binary_sensor import (
|
||||
DEVICE_CLASS_BATTERY_CHARGING,
|
||||
DEVICE_CLASS_MOTION,
|
||||
DEVICE_CLASS_OCCUPANCY,
|
||||
DOMAIN as BINARY_SENSOR_DOMAIN,
|
||||
)
|
||||
from homeassistant.components.camera import DOMAIN as CAMERA_DOMAIN
|
||||
|
@ -55,6 +56,7 @@ from .const import (
|
|||
CONF_FILTER,
|
||||
CONF_LINKED_BATTERY_CHARGING_SENSOR,
|
||||
CONF_LINKED_BATTERY_SENSOR,
|
||||
CONF_LINKED_DOORBELL_SENSOR,
|
||||
CONF_LINKED_HUMIDITY_SENSOR,
|
||||
CONF_LINKED_MOTION_SENSOR,
|
||||
CONF_SAFE_MODE,
|
||||
|
@ -487,6 +489,7 @@ class HomeKit:
|
|||
{
|
||||
(BINARY_SENSOR_DOMAIN, DEVICE_CLASS_BATTERY_CHARGING),
|
||||
(BINARY_SENSOR_DOMAIN, DEVICE_CLASS_MOTION),
|
||||
(BINARY_SENSOR_DOMAIN, DEVICE_CLASS_OCCUPANCY),
|
||||
(SENSOR_DOMAIN, DEVICE_CLASS_BATTERY),
|
||||
(SENSOR_DOMAIN, DEVICE_CLASS_HUMIDITY),
|
||||
}
|
||||
|
@ -631,6 +634,13 @@ class HomeKit:
|
|||
self._config.setdefault(state.entity_id, {}).setdefault(
|
||||
CONF_LINKED_MOTION_SENSOR, motion_binary_sensor_entity_id,
|
||||
)
|
||||
doorbell_binary_sensor_entity_id = device_lookup[ent_reg_ent.device_id].get(
|
||||
(BINARY_SENSOR_DOMAIN, DEVICE_CLASS_OCCUPANCY)
|
||||
)
|
||||
if doorbell_binary_sensor_entity_id:
|
||||
self._config.setdefault(state.entity_id, {}).setdefault(
|
||||
CONF_LINKED_DOORBELL_SENSOR, doorbell_binary_sensor_entity_id,
|
||||
)
|
||||
|
||||
if state.entity_id.startswith(f"{HUMIDIFIER_DOMAIN}."):
|
||||
current_humidity_sensor_entity_id = device_lookup[
|
||||
|
|
|
@ -41,6 +41,7 @@ CONF_FEATURE_LIST = "feature_list"
|
|||
CONF_FILTER = "filter"
|
||||
CONF_LINKED_BATTERY_SENSOR = "linked_battery_sensor"
|
||||
CONF_LINKED_BATTERY_CHARGING_SENSOR = "linked_battery_charging_sensor"
|
||||
CONF_LINKED_DOORBELL_SENSOR = "linked_doorbell_sensor"
|
||||
CONF_LINKED_MOTION_SENSOR = "linked_motion_sensor"
|
||||
CONF_LINKED_HUMIDITY_SENSOR = "linked_humidity_sensor"
|
||||
CONF_LOW_BATTERY_THRESHOLD = "low_battery_threshold"
|
||||
|
@ -112,6 +113,7 @@ SERV_CAMERA_RTP_STREAM_MANAGEMENT = "CameraRTPStreamManagement"
|
|||
SERV_CARBON_DIOXIDE_SENSOR = "CarbonDioxideSensor"
|
||||
SERV_CARBON_MONOXIDE_SENSOR = "CarbonMonoxideSensor"
|
||||
SERV_CONTACT_SENSOR = "ContactSensor"
|
||||
SERV_DOORBELL = "Doorbell"
|
||||
SERV_FANV2 = "Fanv2"
|
||||
SERV_GARAGE_DOOR_OPENER = "GarageDoorOpener"
|
||||
SERV_HUMIDIFIER_DEHUMIDIFIER = "HumidifierDehumidifier"
|
||||
|
@ -126,6 +128,7 @@ SERV_OCCUPANCY_SENSOR = "OccupancySensor"
|
|||
SERV_OUTLET = "Outlet"
|
||||
SERV_SECURITY_SYSTEM = "SecuritySystem"
|
||||
SERV_SMOKE_SENSOR = "SmokeSensor"
|
||||
SERV_SPEAKER = "Speaker"
|
||||
SERV_SWITCH = "Switch"
|
||||
SERV_TELEVISION = "Television"
|
||||
SERV_TELEVISION_SPEAKER = "TelevisionSpeaker"
|
||||
|
@ -184,6 +187,7 @@ CHAR_OCCUPANCY_DETECTED = "OccupancyDetected"
|
|||
CHAR_ON = "On"
|
||||
CHAR_OUTLET_IN_USE = "OutletInUse"
|
||||
CHAR_POSITION_STATE = "PositionState"
|
||||
CHAR_PROGRAMMABLE_SWITCH_EVENT = "ProgrammableSwitchEvent"
|
||||
CHAR_REMOTE_KEY = "RemoteKey"
|
||||
CHAR_ROTATION_DIRECTION = "RotationDirection"
|
||||
CHAR_ROTATION_SPEED = "RotationSpeed"
|
||||
|
|
|
@ -23,9 +23,12 @@ from homeassistant.util import get_local_ip
|
|||
from .accessories import TYPES, HomeAccessory
|
||||
from .const import (
|
||||
CHAR_MOTION_DETECTED,
|
||||
CHAR_MUTE,
|
||||
CHAR_PROGRAMMABLE_SWITCH_EVENT,
|
||||
CONF_AUDIO_CODEC,
|
||||
CONF_AUDIO_MAP,
|
||||
CONF_AUDIO_PACKET_SIZE,
|
||||
CONF_LINKED_DOORBELL_SENSOR,
|
||||
CONF_LINKED_MOTION_SENSOR,
|
||||
CONF_MAX_FPS,
|
||||
CONF_MAX_HEIGHT,
|
||||
|
@ -48,13 +51,18 @@ from .const import (
|
|||
DEFAULT_VIDEO_CODEC,
|
||||
DEFAULT_VIDEO_MAP,
|
||||
DEFAULT_VIDEO_PACKET_SIZE,
|
||||
SERV_DOORBELL,
|
||||
SERV_MOTION_SENSOR,
|
||||
SERV_SPEAKER,
|
||||
)
|
||||
from .img_util import scale_jpeg_camera_image
|
||||
from .util import pid_is_alive
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
DOORBELL_SINGLE_PRESS = 0
|
||||
DOORBELL_DOUBLE_PRESS = 1
|
||||
DOORBELL_LONG_PRESS = 2
|
||||
|
||||
VIDEO_OUTPUT = (
|
||||
"-map {v_map} -an "
|
||||
|
@ -190,18 +198,32 @@ class Camera(HomeAccessory, PyhapCamera):
|
|||
category=CATEGORY_CAMERA,
|
||||
options=options,
|
||||
)
|
||||
|
||||
self._char_motion_detected = None
|
||||
self.linked_motion_sensor = self.config.get(CONF_LINKED_MOTION_SENSOR)
|
||||
if not self.linked_motion_sensor:
|
||||
return
|
||||
state = self.hass.states.get(self.linked_motion_sensor)
|
||||
if not state:
|
||||
return
|
||||
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 self.linked_motion_sensor:
|
||||
state = self.hass.states.get(self.linked_motion_sensor)
|
||||
if state:
|
||||
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)
|
||||
|
||||
self._char_doorbell_detected = None
|
||||
self.linked_doorbell_sensor = self.config.get(CONF_LINKED_DOORBELL_SENSOR)
|
||||
if self.linked_doorbell_sensor:
|
||||
state = self.hass.states.get(self.linked_doorbell_sensor)
|
||||
if state:
|
||||
serv_doorbell = self.add_preload_service(SERV_DOORBELL)
|
||||
self.set_primary_service(serv_doorbell)
|
||||
self._char_doorbell_detected = serv_doorbell.configure_char(
|
||||
CHAR_PROGRAMMABLE_SWITCH_EVENT, value=0,
|
||||
)
|
||||
serv_speaker = self.add_preload_service(SERV_SPEAKER)
|
||||
serv_speaker.configure_char(CHAR_MUTE, value=0)
|
||||
|
||||
self._async_update_doorbell_state(state)
|
||||
|
||||
async def run_handler(self):
|
||||
"""Handle accessory driver started event.
|
||||
|
@ -215,6 +237,13 @@ class Camera(HomeAccessory, PyhapCamera):
|
|||
self._async_update_motion_state_event,
|
||||
)
|
||||
|
||||
if self._char_doorbell_detected:
|
||||
async_track_state_change_event(
|
||||
self.hass,
|
||||
[self.linked_doorbell_sensor],
|
||||
self._async_update_doorbell_state_event,
|
||||
)
|
||||
|
||||
await super().run_handler()
|
||||
|
||||
@callback
|
||||
|
@ -240,6 +269,26 @@ class Camera(HomeAccessory, PyhapCamera):
|
|||
detected,
|
||||
)
|
||||
|
||||
@callback
|
||||
def _async_update_doorbell_state_event(self, event):
|
||||
"""Handle state change event listener callback."""
|
||||
self._async_update_doorbell_state(event.data.get("new_state"))
|
||||
|
||||
@callback
|
||||
def _async_update_doorbell_state(self, new_state):
|
||||
"""Handle link doorbell sensor state change to update HomeKit value."""
|
||||
if not new_state:
|
||||
return
|
||||
|
||||
if new_state.state == STATE_ON:
|
||||
self._char_doorbell_detected.set_value(DOORBELL_SINGLE_PRESS)
|
||||
_LOGGER.debug(
|
||||
"%s: Set linked doorbell %s sensor to %d",
|
||||
self.entity_id,
|
||||
self.linked_doorbell_sensor,
|
||||
DOORBELL_SINGLE_PRESS,
|
||||
)
|
||||
|
||||
@callback
|
||||
def async_update_state(self, new_state):
|
||||
"""Handle state change to update HomeKit value."""
|
||||
|
|
|
@ -34,6 +34,7 @@ from .const import (
|
|||
CONF_FEATURE_LIST,
|
||||
CONF_LINKED_BATTERY_CHARGING_SENSOR,
|
||||
CONF_LINKED_BATTERY_SENSOR,
|
||||
CONF_LINKED_DOORBELL_SENSOR,
|
||||
CONF_LINKED_HUMIDITY_SENSOR,
|
||||
CONF_LINKED_MOTION_SENSOR,
|
||||
CONF_LOW_BATTERY_THRESHOLD,
|
||||
|
@ -127,6 +128,9 @@ CAMERA_SCHEMA = BASIC_INFO_SCHEMA.extend(
|
|||
CONF_VIDEO_PACKET_SIZE, default=DEFAULT_VIDEO_PACKET_SIZE
|
||||
): cv.positive_int,
|
||||
vol.Optional(CONF_LINKED_MOTION_SENSOR): cv.entity_domain(binary_sensor.DOMAIN),
|
||||
vol.Optional(CONF_LINKED_DOORBELL_SENSOR): cv.entity_domain(
|
||||
binary_sensor.DOMAIN
|
||||
),
|
||||
}
|
||||
)
|
||||
|
||||
|
|
|
@ -10,12 +10,16 @@ from homeassistant.components.homekit.accessories import HomeBridge
|
|||
from homeassistant.components.homekit.const import (
|
||||
AUDIO_CODEC_COPY,
|
||||
CHAR_MOTION_DETECTED,
|
||||
CHAR_PROGRAMMABLE_SWITCH_EVENT,
|
||||
CONF_AUDIO_CODEC,
|
||||
CONF_LINKED_DOORBELL_SENSOR,
|
||||
CONF_LINKED_MOTION_SENSOR,
|
||||
CONF_STREAM_SOURCE,
|
||||
CONF_SUPPORT_AUDIO,
|
||||
CONF_VIDEO_CODEC,
|
||||
DEVICE_CLASS_MOTION,
|
||||
DEVICE_CLASS_OCCUPANCY,
|
||||
SERV_DOORBELL,
|
||||
SERV_MOTION_SENSOR,
|
||||
VIDEO_CODEC_COPY,
|
||||
VIDEO_CODEC_H264_OMX,
|
||||
|
@ -601,3 +605,101 @@ async def test_camera_with_a_missing_linked_motion_sensor(hass, run_driver, even
|
|||
assert acc.category == 17 # Camera
|
||||
|
||||
assert not acc.get_service(SERV_MOTION_SENSOR)
|
||||
|
||||
|
||||
async def test_camera_with_linked_doorbell_sensor(hass, run_driver, events):
|
||||
"""Test a camera with a linked doorbell sensor 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()
|
||||
doorbell_entity_id = "binary_sensor.doorbell"
|
||||
|
||||
hass.states.async_set(
|
||||
doorbell_entity_id, STATE_ON, {ATTR_DEVICE_CLASS: DEVICE_CLASS_OCCUPANCY}
|
||||
)
|
||||
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_DOORBELL_SENSOR: doorbell_entity_id,
|
||||
},
|
||||
)
|
||||
bridge = HomeBridge("hass", run_driver, "Test Bridge")
|
||||
bridge.add_accessory(acc)
|
||||
|
||||
await acc.run_handler()
|
||||
|
||||
assert acc.aid == 2
|
||||
assert acc.category == 17 # Camera
|
||||
|
||||
service = acc.get_service(SERV_DOORBELL)
|
||||
assert service
|
||||
char = service.get_characteristic(CHAR_PROGRAMMABLE_SWITCH_EVENT)
|
||||
assert char
|
||||
|
||||
assert char.value == 0
|
||||
|
||||
hass.states.async_set(
|
||||
doorbell_entity_id, STATE_OFF, {ATTR_DEVICE_CLASS: DEVICE_CLASS_OCCUPANCY}
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
assert char.value == 0
|
||||
|
||||
char.set_value(True)
|
||||
hass.states.async_set(
|
||||
doorbell_entity_id, STATE_ON, {ATTR_DEVICE_CLASS: DEVICE_CLASS_OCCUPANCY}
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
assert char.value == 0
|
||||
|
||||
# Ensure we do not throw when the linked
|
||||
# doorbell sensor is removed
|
||||
hass.states.async_remove(doorbell_entity_id)
|
||||
await hass.async_block_till_done()
|
||||
await acc.run_handler()
|
||||
await hass.async_block_till_done()
|
||||
assert char.value == 0
|
||||
|
||||
|
||||
async def test_camera_with_a_missing_linked_doorbell_sensor(hass, run_driver, events):
|
||||
"""Test a camera with a configured linked doorbell sensor that is missing."""
|
||||
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()
|
||||
doorbell_entity_id = "binary_sensor.doorbell"
|
||||
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_LINKED_DOORBELL_SENSOR: doorbell_entity_id},
|
||||
)
|
||||
bridge = HomeBridge("hass", run_driver, "Test Bridge")
|
||||
bridge.add_accessory(acc)
|
||||
|
||||
await acc.run_handler()
|
||||
|
||||
assert acc.aid == 2
|
||||
assert acc.category == 17 # Camera
|
||||
|
||||
assert not acc.get_service(SERV_DOORBELL)
|
||||
|
|
Loading…
Add table
Reference in a new issue