Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions homeassistant/components/homekit_controller/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,8 @@ def __init__(self, accessory, devinfo):

self._signals = []

super().__init__()

@property
def accessory(self) -> Accessory:
"""Return an Accessory model that this entity is attached to."""
Expand Down Expand Up @@ -171,6 +173,16 @@ def get_characteristic_types(self):
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)
Expand Down
50 changes: 50 additions & 0 deletions homeassistant/components/homekit_controller/camera.py
Original file line number Diff line number Diff line change
@@ -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)
20 changes: 19 additions & 1 deletion homeassistant/components/homekit_controller/connection.py
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,9 @@ def __init__(self, hass, config_entry, pairing_data):

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 = []

Expand Down Expand Up @@ -289,14 +292,29 @@ async def async_refresh_entity_map(self, config_num):

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:
Expand Down
1 change: 1 addition & 0 deletions homeassistant/components/homekit_controller/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,4 +37,5 @@
"occupancy": "binary_sensor",
"television": "media_player",
"valve": "switch",
"camera-rtp-stream-management": "camera",
}
Original file line number Diff line number Diff line change
Expand Up @@ -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"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Don't forget to mark the dependency upgrade in the MR template.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Of course happy to do this, but the PR template says to only check 1 box.

],
"zeroconf": [
"_hap._tcp.local."
Expand Down
2 changes: 1 addition & 1 deletion requirements_all.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion requirements_test_all.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
@@ -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
29 changes: 29 additions & 0 deletions tests/components/homekit_controller/test_camera.py
Original file line number Diff line number Diff line change
@@ -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)
Loading