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
59 changes: 44 additions & 15 deletions homeassistant/components/homekit/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,13 +34,18 @@
CONF_INCLUDE_ENTITIES,
convert_filter,
)
from homeassistant.loader import async_get_integration
from homeassistant.util import get_local_ip

from .accessories import get_accessory
from .aidmanager import AccessoryAidStorage
from .const import (
AID_STORAGE,
ATTR_DISPLAY_NAME,
ATTR_INTERGRATION,
ATTR_MANUFACTURER,
ATTR_MODEL,
ATTR_SOFTWARE_VERSION,
ATTR_VALUE,
BRIDGE_NAME,
CONF_ADVERTISE_IP,
Expand Down Expand Up @@ -200,7 +205,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry):
aid_storage = AccessoryAidStorage(hass, entry.entry_id)

await aid_storage.async_initialize()
# These are yaml only
# ip_address and advertise_ip are yaml only
ip_address = conf.get(CONF_IP_ADDRESS)
advertise_ip = conf.get(CONF_ADVERTISE_IP)

Expand Down Expand Up @@ -494,6 +499,7 @@ async def async_start(self, *args):
self.status = STATUS_WAIT

ent_reg = await entity_registry.async_get_registry(self.hass)
dev_reg = await device_registry.async_get_registry(self.hass)

device_lookup = ent_reg.async_get_device_class_lookup(
{
Expand All @@ -507,16 +513,24 @@ async def async_start(self, *args):
if not self._filter(state.entity_id):
continue

self._async_configure_linked_battery_sensors(ent_reg, device_lookup, state)
ent_reg_ent = ent_reg.async_get(state.entity_id)
if ent_reg_ent:
await self._async_set_device_info_attributes(
ent_reg_ent, dev_reg, state.entity_id
)
self._async_configure_linked_battery_sensors(
ent_reg_ent, device_lookup, state
)

bridged_states.append(state)

self._async_register_bridge(dev_reg)
await self.hass.async_add_executor_job(self._start, bridged_states)
await self._async_register_bridge()

async def _async_register_bridge(self):
@callback
def _async_register_bridge(self, dev_reg):
"""Register the bridge as a device so homekit_controller and exclude it from discovery."""
registry = await device_registry.async_get_registry(self.hass)
registry.async_get_or_create(
dev_reg.async_get_or_create(
config_entry_id=self._entry_id,
connections={
(device_registry.CONNECTION_NETWORK_MAC, self.driver.state.mac)
Expand Down Expand Up @@ -567,21 +581,21 @@ async def async_stop(self, *args):
self.hass.add_job(self.driver.stop)

@callback
def _async_configure_linked_battery_sensors(self, ent_reg, device_lookup, state):
entry = ent_reg.async_get(state.entity_id)

def _async_configure_linked_battery_sensors(
self, ent_reg_ent, device_lookup, state
):
if (
entry is None
or entry.device_id is None
or entry.device_id not in device_lookup
or entry.device_class
ent_reg_ent is None
or ent_reg_ent.device_id is None
or ent_reg_ent.device_id not in device_lookup
or ent_reg_ent.device_class
in (DEVICE_CLASS_BATTERY_CHARGING, DEVICE_CLASS_BATTERY)
):
return

if ATTR_BATTERY_CHARGING not in state.attributes:
battery_charging_binary_sensor_entity_id = device_lookup[
entry.device_id
ent_reg_ent.device_id
].get(("binary_sensor", DEVICE_CLASS_BATTERY_CHARGING))
if battery_charging_binary_sensor_entity_id:
self._config.setdefault(state.entity_id, {}).setdefault(
Expand All @@ -590,14 +604,29 @@ def _async_configure_linked_battery_sensors(self, ent_reg, device_lookup, state)
)

if ATTR_BATTERY_LEVEL not in state.attributes:
battery_sensor_entity_id = device_lookup[entry.device_id].get(
battery_sensor_entity_id = device_lookup[ent_reg_ent.device_id].get(
("sensor", DEVICE_CLASS_BATTERY)
)
if battery_sensor_entity_id:
self._config.setdefault(state.entity_id, {}).setdefault(
CONF_LINKED_BATTERY_SENSOR, battery_sensor_entity_id
)

async def _async_set_device_info_attributes(self, ent_reg_ent, dev_reg, entity_id):
"""Set attributes that will be used for homekit device info."""
ent_cfg = self._config.setdefault(entity_id, {})
if ent_reg_ent.device_id:
dev_reg_ent = dev_reg.async_get(ent_reg_ent.device_id)
if dev_reg_ent.manufacturer:
ent_cfg[ATTR_MANUFACTURER] = dev_reg_ent.manufacturer
if dev_reg_ent.model:
ent_cfg[ATTR_MODEL] = dev_reg_ent.model
if dev_reg_ent.sw_version:
ent_cfg[ATTR_SOFTWARE_VERSION] = dev_reg_ent.sw_version
if ATTR_MANUFACTURER not in ent_cfg:
integration = await async_get_integration(self.hass, ent_reg_ent.platform)
ent_cfg[ATTR_INTERGRATION] = integration.name


class HomeKitPairingQRView(HomeAssistantView):
"""Display the homekit pairing code at a protected url."""
Expand Down
29 changes: 25 additions & 4 deletions homeassistant/components/homekit/accessories.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,10 @@

from .const import (
ATTR_DISPLAY_NAME,
ATTR_INTERGRATION,
ATTR_MANUFACTURER,
ATTR_MODEL,
ATTR_SOFTWARE_VERSION,
ATTR_VALUE,
BRIDGE_MODEL,
BRIDGE_SERIAL_NUMBER,
Expand Down Expand Up @@ -235,15 +239,32 @@ def __init__(
):
"""Initialize a Accessory object."""
super().__init__(driver=driver, display_name=name, aid=aid, *args, **kwargs)
model = split_entity_id(entity_id)[0].replace("_", " ").title()
self.config = config or {}
domain = split_entity_id(entity_id)[0].replace("_", " ")

if ATTR_MANUFACTURER in self.config:
manufacturer = self.config[ATTR_MANUFACTURER]
elif ATTR_INTERGRATION in self.config:
manufacturer = self.config[ATTR_INTERGRATION].replace("_", " ").title()
else:
manufacturer = f"{MANUFACTURER} {domain}".title()
if ATTR_MODEL in self.config:
model = self.config[ATTR_MODEL]
else:
model = domain.title()
if ATTR_SOFTWARE_VERSION in self.config:
sw_version = self.config[ATTR_SOFTWARE_VERSION]
else:
sw_version = __version__

self.set_info_service(
firmware_revision=__version__,
manufacturer=MANUFACTURER,
manufacturer=manufacturer,
model=model,
serial_number=entity_id,
firmware_revision=sw_version,
)

self.category = category
self.config = config or {}
self.entity_id = entity_id
self.hass = hass
self.debounce = {}
Expand Down
5 changes: 5 additions & 0 deletions homeassistant/components/homekit/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,10 @@
# #### Attributes ####
ATTR_DISPLAY_NAME = "display_name"
ATTR_VALUE = "value"
ATTR_INTERGRATION = "platform"
ATTR_MANUFACTURER = "manufacturer"
ATTR_MODEL = "model"
ATTR_SOFTWARE_VERSION = "sw_version"

# #### Config ####
CONF_ADVERTISE_IP = "advertise_ip"
Expand Down Expand Up @@ -50,6 +54,7 @@
CONF_VIDEO_PACKET_SIZE = "video_packet_size"

# #### Config Defaults ####
DEFAULT_SUPPORT_AUDIO = False
DEFAULT_AUDIO_CODEC = AUDIO_CODEC_OPUS
DEFAULT_AUDIO_MAP = "0:a:0"
DEFAULT_AUDIO_PACKET_SIZE = 188
Expand Down
39 changes: 32 additions & 7 deletions homeassistant/components/homekit/type_cameras.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,10 +33,20 @@
CONF_VIDEO_CODEC,
CONF_VIDEO_MAP,
CONF_VIDEO_PACKET_SIZE,
DEFAULT_AUDIO_CODEC,
DEFAULT_AUDIO_MAP,
DEFAULT_AUDIO_PACKET_SIZE,
DEFAULT_MAX_FPS,
DEFAULT_MAX_HEIGHT,
DEFAULT_MAX_WIDTH,
DEFAULT_SUPPORT_AUDIO,
DEFAULT_VIDEO_CODEC,
DEFAULT_VIDEO_MAP,
DEFAULT_VIDEO_PACKET_SIZE,
SERV_CAMERA_RTP_STREAM_MANAGEMENT,
)
from .img_util import scale_jpeg_camera_image
from .util import CAMERA_SCHEMA, pid_is_alive
from .util import pid_is_alive

_LOGGER = logging.getLogger(__name__)

Expand Down Expand Up @@ -94,6 +104,19 @@
FFMPEG_PID = "ffmpeg_pid"
SESSION_ID = "session_id"

CONFIG_DEFAULTS = {
CONF_SUPPORT_AUDIO: DEFAULT_SUPPORT_AUDIO,
CONF_MAX_WIDTH: DEFAULT_MAX_WIDTH,
CONF_MAX_HEIGHT: DEFAULT_MAX_HEIGHT,
CONF_MAX_FPS: DEFAULT_MAX_FPS,
CONF_AUDIO_CODEC: DEFAULT_AUDIO_CODEC,
CONF_AUDIO_MAP: DEFAULT_AUDIO_MAP,
CONF_VIDEO_MAP: DEFAULT_VIDEO_MAP,
CONF_VIDEO_CODEC: DEFAULT_VIDEO_CODEC,
CONF_AUDIO_PACKET_SIZE: DEFAULT_AUDIO_PACKET_SIZE,
CONF_VIDEO_PACKET_SIZE: DEFAULT_VIDEO_PACKET_SIZE,
}


@TYPES.register("Camera")
class Camera(HomeAccessory, PyhapCamera):
Expand All @@ -104,11 +127,13 @@ def __init__(self, hass, driver, name, entity_id, aid, config):
self._ffmpeg = hass.data[DATA_FFMPEG]
self._cur_session = None
self._camera = hass.data[DOMAIN_CAMERA]
config_w_defaults = CAMERA_SCHEMA(config)
for config_key in CONFIG_DEFAULTS:
if config_key not in config:
config[config_key] = CONFIG_DEFAULTS[config_key]

max_fps = config_w_defaults[CONF_MAX_FPS]
max_width = config_w_defaults[CONF_MAX_WIDTH]
max_height = config_w_defaults[CONF_MAX_HEIGHT]
max_fps = config[CONF_MAX_FPS]
max_width = config[CONF_MAX_WIDTH]
max_height = config[CONF_MAX_HEIGHT]
resolutions = [
(w, h, fps)
for w, h, fps in SLOW_RESOLUTIONS
Expand Down Expand Up @@ -136,7 +161,7 @@ def __init__(self, hass, driver, name, entity_id, aid, config):
}
audio_options = {"codecs": [{"type": "OPUS", "samplerate": 24}]}

stream_address = config_w_defaults.get(CONF_STREAM_ADDRESS, get_local_ip())
stream_address = config.get(CONF_STREAM_ADDRESS, get_local_ip())

options = {
"video": video_options,
Expand All @@ -151,7 +176,7 @@ def __init__(self, hass, driver, name, entity_id, aid, config):
name,
entity_id,
aid,
config_w_defaults,
config,
category=CATEGORY_CAMERA,
options=options,
)
Expand Down
3 changes: 2 additions & 1 deletion homeassistant/components/homekit/util.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@
DEFAULT_MAX_FPS,
DEFAULT_MAX_HEIGHT,
DEFAULT_MAX_WIDTH,
DEFAULT_SUPPORT_AUDIO,
DEFAULT_VIDEO_CODEC,
DEFAULT_VIDEO_MAP,
DEFAULT_VIDEO_PACKET_SIZE,
Expand Down Expand Up @@ -98,7 +99,7 @@
vol.Optional(CONF_AUDIO_CODEC, default=DEFAULT_AUDIO_CODEC): vol.In(
VALID_AUDIO_CODECS
),
vol.Optional(CONF_SUPPORT_AUDIO, default=False): cv.boolean,
vol.Optional(CONF_SUPPORT_AUDIO, default=DEFAULT_SUPPORT_AUDIO): cv.boolean,
vol.Optional(CONF_MAX_WIDTH, default=DEFAULT_MAX_WIDTH): cv.positive_int,
vol.Optional(CONF_MAX_HEIGHT, default=DEFAULT_MAX_HEIGHT): cv.positive_int,
vol.Optional(CONF_MAX_FPS, default=DEFAULT_MAX_FPS): cv.positive_int,
Expand Down
52 changes: 43 additions & 9 deletions tests/components/homekit/test_accessories.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,10 @@
)
from homeassistant.components.homekit.const import (
ATTR_DISPLAY_NAME,
ATTR_INTERGRATION,
ATTR_MANUFACTURER,
ATTR_MODEL,
ATTR_SOFTWARE_VERSION,
ATTR_VALUE,
BRIDGE_MODEL,
BRIDGE_NAME,
Expand Down Expand Up @@ -80,11 +84,17 @@ def demo_func(*args):

async def test_home_accessory(hass, hk_driver):
"""Test HomeAccessory class."""
entity_id = "homekit.accessory"
entity_id = "sensor.accessory"
entity_id2 = "light.accessory"

hass.states.async_set(entity_id, None)
hass.states.async_set(entity_id2, None)

await hass.async_block_till_done()

acc = HomeAccessory(hass, hk_driver, "Home Accessory", entity_id, 2, None)
acc = HomeAccessory(
hass, hk_driver, "Home Accessory", entity_id, 2, {"platform": "isy994"}
)
assert acc.hass == hass
assert acc.display_name == "Home Accessory"
assert acc.aid == 2
Expand All @@ -93,9 +103,35 @@ async def test_home_accessory(hass, hk_driver):
serv = acc.services[0] # SERV_ACCESSORY_INFO
assert serv.display_name == SERV_ACCESSORY_INFO
assert serv.get_characteristic(CHAR_NAME).value == "Home Accessory"
assert serv.get_characteristic(CHAR_MANUFACTURER).value == MANUFACTURER
assert serv.get_characteristic(CHAR_MODEL).value == "Homekit"
assert serv.get_characteristic(CHAR_SERIAL_NUMBER).value == "homekit.accessory"
assert serv.get_characteristic(CHAR_MANUFACTURER).value == "Isy994"
assert serv.get_characteristic(CHAR_MODEL).value == "Sensor"
assert serv.get_characteristic(CHAR_SERIAL_NUMBER).value == "sensor.accessory"

acc2 = HomeAccessory(hass, hk_driver, "Home Accessory", entity_id2, 3, {})
serv = acc2.services[0] # SERV_ACCESSORY_INFO
assert serv.get_characteristic(CHAR_NAME).value == "Home Accessory"
assert serv.get_characteristic(CHAR_MANUFACTURER).value == f"{MANUFACTURER} Light"
assert serv.get_characteristic(CHAR_MODEL).value == "Light"
assert serv.get_characteristic(CHAR_SERIAL_NUMBER).value == "light.accessory"

acc3 = HomeAccessory(
hass,
hk_driver,
"Home Accessory",
entity_id2,
3,
{
ATTR_MODEL: "Awesome",
ATTR_MANUFACTURER: "Lux Brands",
ATTR_SOFTWARE_VERSION: "0.4.3",
ATTR_INTERGRATION: "luxe",
},
)
serv = acc3.services[0] # SERV_ACCESSORY_INFO
assert serv.get_characteristic(CHAR_NAME).value == "Home Accessory"
assert serv.get_characteristic(CHAR_MANUFACTURER).value == "Lux Brands"
assert serv.get_characteristic(CHAR_MODEL).value == "Awesome"
assert serv.get_characteristic(CHAR_SERIAL_NUMBER).value == "light.accessory"

hass.states.async_set(entity_id, "on")
await hass.async_block_till_done()
Expand Down Expand Up @@ -441,9 +477,7 @@ async def test_battery_appears_after_startup(hass, hk_driver, caplog):
hass.states.async_set(entity_id, None, {})
await hass.async_block_till_done()

acc = HomeAccessory(
hass, hk_driver, "Accessory without battery", entity_id, 2, None
)
acc = HomeAccessory(hass, hk_driver, "Accessory without battery", entity_id, 2, {})
assert acc._char_battery is None

with patch(
Expand All @@ -469,7 +503,7 @@ async def test_call_service(hass, hk_driver, events):
hass.states.async_set(entity_id, None)
await hass.async_block_till_done()

acc = HomeAccessory(hass, hk_driver, "Home Accessory", entity_id, 2, None)
acc = HomeAccessory(hass, hk_driver, "Home Accessory", entity_id, 2, {})
call_service = async_mock_service(hass, "cover", "open_cover")

test_domain = "cover"
Expand Down
Loading