Add initial camera support to homekit_controller (#43100)

pull/43230/head
Jc2k 2020-11-14 12:07:22 +00:00 committed by GitHub
parent dc8db033b9
commit cc396b9736
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 2240 additions and 4 deletions

View File

@ -38,6 +38,8 @@ class HomeKitEntity(Entity):
self._signals = []
super().__init__()
@property
def accessory(self) -> Accessory:
"""Return an Accessory model that this entity is attached to."""
@ -171,6 +173,16 @@ class HomeKitEntity(Entity):
raise NotImplementedError
class AccessoryEntity(HomeKitEntity):
"""A HomeKit entity that is related to an entire accessory rather than a specific service or characteristic."""
@property
def unique_id(self) -> str:
"""Return the ID of this device."""
serial = self.accessory_info.value(CharacteristicsTypes.SERIAL_NUMBER)
return f"homekit-{serial}-aid:{self._aid}"
async def async_setup_entry(hass, entry):
"""Set up a HomeKit connection on a config entry."""
conn = HKDevice(hass, entry, entry.data)

View File

@ -0,0 +1,50 @@
"""Support for Homekit cameras."""
from aiohomekit.model.services import ServicesTypes
from homeassistant.components.camera import Camera
from homeassistant.core import callback
from . import KNOWN_DEVICES, AccessoryEntity
class HomeKitCamera(AccessoryEntity, Camera):
"""Representation of a Homekit camera."""
# content_type = "image/jpeg"
def get_characteristic_types(self):
"""Define the homekit characteristics the entity is tracking."""
return []
@property
def state(self):
"""Return the current state of the camera."""
return "idle"
async def async_camera_image(self):
"""Return a jpeg with the current camera snapshot."""
return await self._accessory.pairing.image(
self._aid,
640,
480,
)
async def async_setup_entry(hass, config_entry, async_add_entities):
"""Set up Homekit sensors."""
hkid = config_entry.data["AccessoryPairingID"]
conn = hass.data[KNOWN_DEVICES][hkid]
@callback
def async_add_accessory(accessory):
stream_mgmt = accessory.services.first(
service_type=ServicesTypes.CAMERA_RTP_STREAM_MANAGEMENT
)
if not stream_mgmt:
return
info = {"aid": accessory.aid, "iid": stream_mgmt.iid}
async_add_entities([HomeKitCamera(conn, info)], True)
return True
conn.add_accessory_factory(async_add_accessory)

View File

@ -76,6 +76,9 @@ class HKDevice:
self.entity_map = Accessories()
# A list of callbacks that turn HK accessories into entities
self.accessory_factories = []
# A list of callbacks that turn HK service metadata into entities
self.listeners = []
@ -289,14 +292,29 @@ class HKDevice:
return True
def add_accessory_factory(self, add_entities_cb):
"""Add a callback to run when discovering new entities for accessories."""
self.accessory_factories.append(add_entities_cb)
self._add_new_entities_for_accessory([add_entities_cb])
def _add_new_entities_for_accessory(self, handlers):
for accessory in self.entity_map.accessories:
for handler in handlers:
if (accessory.aid, None) in self.entities:
continue
if handler(accessory):
self.entities.append((accessory.aid, None))
break
def add_listener(self, add_entities_cb):
"""Add a callback to run when discovering new entities."""
"""Add a callback to run when discovering new entities for services."""
self.listeners.append(add_entities_cb)
self._add_new_entities([add_entities_cb])
def add_entities(self):
"""Process the entity map and create HA entities."""
self._add_new_entities(self.listeners)
self._add_new_entities_for_accessory(self.accessory_factories)
def _add_new_entities(self, callbacks):
for accessory in self.accessories:

View File

@ -37,4 +37,5 @@ HOMEKIT_ACCESSORY_DISPATCH = {
"occupancy": "binary_sensor",
"television": "media_player",
"valve": "switch",
"camera-rtp-stream-management": "camera",
}

View File

@ -4,7 +4,7 @@
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/homekit_controller",
"requirements": [
"aiohomekit==0.2.54"
"aiohomekit==0.2.57"
],
"zeroconf": [
"_hap._tcp.local."

View File

@ -178,7 +178,7 @@ aioguardian==1.0.1
aioharmony==0.2.6
# homeassistant.components.homekit_controller
aiohomekit==0.2.54
aiohomekit==0.2.57
# homeassistant.components.emulated_hue
# homeassistant.components.http

View File

@ -109,7 +109,7 @@ aioguardian==1.0.1
aioharmony==0.2.6
# homeassistant.components.homekit_controller
aiohomekit==0.2.54
aiohomekit==0.2.57
# homeassistant.components.emulated_hue
# homeassistant.components.http

View File

@ -0,0 +1,53 @@
"""Test against characteristics captured from a eufycam."""
from tests.components.homekit_controller.common import (
Helper,
setup_accessories_from_file,
setup_test_accessories,
)
async def test_eufycam_setup(hass):
"""Test that a eufycam can be correctly setup in HA."""
accessories = await setup_accessories_from_file(hass, "anker_eufycam.json")
config_entry, pairing = await setup_test_accessories(hass, accessories)
entity_registry = await hass.helpers.entity_registry.async_get_registry()
# Check that the camera is correctly found and set up
camera_id = "camera.eufycam2_0000"
camera = entity_registry.async_get(camera_id)
assert camera.unique_id == "homekit-A0000A000000000D-aid:4"
camera_helper = Helper(
hass,
"camera.eufycam2_0000",
pairing,
accessories[0],
config_entry,
)
camera_state = await camera_helper.poll_and_get_state()
assert camera_state.attributes["friendly_name"] == "eufyCam2-0000"
assert camera_state.state == "idle"
assert camera_state.attributes["supported_features"] == 0
device_registry = await hass.helpers.device_registry.async_get_registry()
device = device_registry.async_get(camera.device_id)
assert device.manufacturer == "Anker"
assert device.name == "eufyCam2-0000"
assert device.model == "T8113"
assert device.sw_version == "1.6.7"
# These cameras are via a bridge, so via should be set
assert device.via_device_id is not None
cameras_count = 0
for state in hass.states.async_all():
if state.entity_id.startswith("camera."):
cameras_count += 1
# There are multiple rtsp services, we only want to create 1
# camera entity per accessory, not 1 camera per service.
assert cameras_count == 3

View File

@ -0,0 +1,29 @@
"""Basic checks for HomeKit cameras."""
import base64
from aiohomekit.model.services import ServicesTypes
from aiohomekit.testing import FAKE_CAMERA_IMAGE
from homeassistant.components import camera
from tests.components.homekit_controller.common import setup_test_component
def create_camera(accessory):
"""Define camera characteristics."""
accessory.add_service(ServicesTypes.CAMERA_RTP_STREAM_MANAGEMENT)
async def test_read_state(hass, utcnow):
"""Test reading the state of a HomeKit camera."""
helper = await setup_test_component(hass, create_camera)
state = await helper.poll_and_get_state()
assert state.state == "idle"
async def test_get_image(hass, utcnow):
"""Test getting a JPEG from a camera."""
helper = await setup_test_component(hass, create_camera)
image = await camera.async_get_image(hass, helper.entity_id)
assert image.content == base64.b64decode(FAKE_CAMERA_IMAGE)

File diff suppressed because it is too large Load Diff