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:
Austin Drummond 2020-07-31 15:41:36 -04:00 committed by GitHub
parent bb69aba051
commit 9d0f58009e
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 179 additions and 10 deletions

View file

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

View file

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

View file

@ -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."""

View file

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

View file

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