Add initial camera support to homekit_controller (#43100)
parent
dc8db033b9
commit
cc396b9736
|
@ -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)
|
||||
|
|
|
@ -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)
|
|
@ -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:
|
||||
|
|
|
@ -37,4 +37,5 @@ HOMEKIT_ACCESSORY_DISPATCH = {
|
|||
"occupancy": "binary_sensor",
|
||||
"television": "media_player",
|
||||
"valve": "switch",
|
||||
"camera-rtp-stream-management": "camera",
|
||||
}
|
||||
|
|
|
@ -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."
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
|
@ -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
Loading…
Reference in New Issue