2019-02-14 16:01:46 +01:00
|
|
|
"""Support for Homekit device discovery."""
|
2018-04-13 19:25:35 +02:00
|
|
|
import logging
|
2020-02-25 12:09:04 +01:00
|
|
|
import os
|
2018-04-13 19:25:35 +02:00
|
|
|
|
2020-02-24 10:55:33 +01:00
|
|
|
import aiohomekit
|
2020-03-10 12:06:44 +01:00
|
|
|
from aiohomekit.model import Accessory
|
2020-02-24 10:55:33 +01:00
|
|
|
from aiohomekit.model.characteristics import CharacteristicsTypes
|
2020-03-10 12:06:44 +01:00
|
|
|
from aiohomekit.model.services import Service, ServicesTypes
|
2019-08-14 18:14:15 +02:00
|
|
|
|
2019-07-22 18:22:44 +02:00
|
|
|
from homeassistant.core import callback
|
2019-05-13 08:56:05 +02:00
|
|
|
from homeassistant.exceptions import ConfigEntryNotReady
|
2019-05-17 08:41:21 +02:00
|
|
|
from homeassistant.helpers import device_registry as dr
|
2019-12-08 17:50:57 +01:00
|
|
|
from homeassistant.helpers.entity import Entity
|
2018-04-13 19:25:35 +02:00
|
|
|
|
2019-12-19 09:45:23 +01:00
|
|
|
from .config_flow import normalize_hkid
|
2020-03-10 12:06:44 +01:00
|
|
|
from .connection import HKDevice
|
2019-12-08 17:50:57 +01:00
|
|
|
from .const import CONTROLLER, DOMAIN, ENTITY_MAP, KNOWN_DEVICES
|
2019-04-18 17:55:34 +02:00
|
|
|
from .storage import EntityMapStorage
|
2019-03-07 04:44:52 +01:00
|
|
|
|
2018-04-13 19:25:35 +02:00
|
|
|
_LOGGER = logging.getLogger(__name__)
|
|
|
|
|
2018-10-04 09:25:05 +02:00
|
|
|
|
2019-01-28 17:21:20 +01:00
|
|
|
def escape_characteristic_name(char_name):
|
|
|
|
"""Escape any dash or dots in a characteristics name."""
|
2019-07-31 21:25:30 +02:00
|
|
|
return char_name.replace("-", "_").replace(".", "_")
|
2019-01-28 17:21:20 +01:00
|
|
|
|
|
|
|
|
2018-04-13 19:25:35 +02:00
|
|
|
class HomeKitEntity(Entity):
|
|
|
|
"""Representation of a Home Assistant HomeKit device."""
|
|
|
|
|
|
|
|
def __init__(self, accessory, devinfo):
|
|
|
|
"""Initialise a generic HomeKit device."""
|
2018-10-04 09:25:05 +02:00
|
|
|
self._accessory = accessory
|
2019-07-31 21:25:30 +02:00
|
|
|
self._aid = devinfo["aid"]
|
|
|
|
self._iid = devinfo["iid"]
|
2018-04-13 19:25:35 +02:00
|
|
|
self._features = 0
|
|
|
|
self._chars = {}
|
2019-01-28 17:21:20 +01:00
|
|
|
self.setup()
|
|
|
|
|
2019-07-22 18:22:44 +02:00
|
|
|
self._signals = []
|
|
|
|
|
2020-03-10 12:06:44 +01:00
|
|
|
@property
|
|
|
|
def accessory(self) -> Accessory:
|
|
|
|
"""Return an Accessory model that this entity is attached to."""
|
|
|
|
return self._accessory.entity_map.aid(self._aid)
|
|
|
|
|
|
|
|
@property
|
|
|
|
def accessory_info(self) -> Service:
|
|
|
|
"""Information about the make and model of an accessory."""
|
|
|
|
return self.accessory.services.first(
|
|
|
|
service_type=ServicesTypes.ACCESSORY_INFORMATION
|
|
|
|
)
|
|
|
|
|
|
|
|
@property
|
|
|
|
def service(self) -> Service:
|
|
|
|
"""Return a Service model that this entity is attached to."""
|
|
|
|
return self.accessory.services.iid(self._iid)
|
|
|
|
|
2019-07-22 18:22:44 +02:00
|
|
|
async def async_added_to_hass(self):
|
|
|
|
"""Entity added to hass."""
|
|
|
|
self._signals.append(
|
|
|
|
self.hass.helpers.dispatcher.async_dispatcher_connect(
|
2019-07-31 21:25:30 +02:00
|
|
|
self._accessory.signal_state_updated, self.async_state_changed
|
2019-07-22 18:22:44 +02:00
|
|
|
)
|
|
|
|
)
|
|
|
|
|
2019-07-31 21:25:30 +02:00
|
|
|
self._accessory.add_pollable_characteristics(self.pollable_characteristics)
|
2020-02-26 19:35:53 +01:00
|
|
|
self._accessory.add_watchable_characteristics(self.watchable_characteristics)
|
2019-07-22 18:22:44 +02:00
|
|
|
|
|
|
|
async def async_will_remove_from_hass(self):
|
|
|
|
"""Prepare to be removed from hass."""
|
2019-07-31 21:25:30 +02:00
|
|
|
self._accessory.remove_pollable_characteristics(self._aid)
|
2020-02-26 19:35:53 +01:00
|
|
|
self._accessory.remove_watchable_characteristics(self._aid)
|
2019-07-22 18:22:44 +02:00
|
|
|
|
|
|
|
for signal_remove in self._signals:
|
|
|
|
signal_remove()
|
|
|
|
self._signals.clear()
|
|
|
|
|
|
|
|
@property
|
|
|
|
def should_poll(self) -> bool:
|
|
|
|
"""Return False.
|
|
|
|
|
|
|
|
Data update is triggered from HKDevice.
|
|
|
|
"""
|
|
|
|
return False
|
|
|
|
|
2019-01-28 17:21:20 +01:00
|
|
|
def setup(self):
|
2020-01-31 17:33:00 +01:00
|
|
|
"""Configure an entity baed on its HomeKit characteristics metadata."""
|
2019-01-28 17:21:20 +01:00
|
|
|
get_uuid = CharacteristicsTypes.get_uuid
|
2019-07-31 21:25:30 +02:00
|
|
|
characteristic_types = [get_uuid(c) for c in self.get_characteristic_types()]
|
2019-01-28 17:21:20 +01:00
|
|
|
|
2019-07-22 18:22:44 +02:00
|
|
|
self.pollable_characteristics = []
|
2020-02-26 19:35:53 +01:00
|
|
|
self.watchable_characteristics = []
|
2019-01-28 17:21:20 +01:00
|
|
|
self._chars = {}
|
|
|
|
self._char_names = {}
|
|
|
|
|
2020-03-10 12:06:44 +01:00
|
|
|
# Setup events and/or polling for characteristics directly attached to this entity
|
|
|
|
for char in self.service.characteristics:
|
|
|
|
if char.type not in characteristic_types:
|
2019-01-28 17:21:20 +01:00
|
|
|
continue
|
2020-03-10 12:06:44 +01:00
|
|
|
self._setup_characteristic(char.to_accessory_and_service_list())
|
|
|
|
|
|
|
|
# Setup events and/or polling for characteristics attached to sub-services of this
|
|
|
|
# entity (like an INPUT_SOURCE).
|
|
|
|
for service in self.accessory.services.filter(parent_service=self.service):
|
|
|
|
for char in service.characteristics:
|
|
|
|
if char.type not in characteristic_types:
|
2019-01-28 17:21:20 +01:00
|
|
|
continue
|
2020-03-10 12:06:44 +01:00
|
|
|
self._setup_characteristic(char.to_accessory_and_service_list())
|
2020-03-06 16:47:40 +01:00
|
|
|
|
2019-01-28 17:21:20 +01:00
|
|
|
def _setup_characteristic(self, char):
|
|
|
|
"""Configure an entity based on a HomeKit characteristics metadata."""
|
|
|
|
# Build up a list of (aid, iid) tuples to poll on update()
|
2020-02-24 10:55:33 +01:00
|
|
|
if "pr" in char["perms"]:
|
|
|
|
self.pollable_characteristics.append((self._aid, char["iid"]))
|
2019-01-28 17:21:20 +01:00
|
|
|
|
2020-02-26 19:35:53 +01:00
|
|
|
# Build up a list of (aid, iid) tuples to subscribe to
|
|
|
|
if "ev" in char["perms"]:
|
|
|
|
self.watchable_characteristics.append((self._aid, char["iid"]))
|
|
|
|
|
2019-01-28 17:21:20 +01:00
|
|
|
# Build a map of ctype -> iid
|
2019-07-31 21:25:30 +02:00
|
|
|
short_name = CharacteristicsTypes.get_short(char["type"])
|
|
|
|
self._chars[short_name] = char["iid"]
|
|
|
|
self._char_names[char["iid"]] = short_name
|
2019-01-28 17:21:20 +01:00
|
|
|
|
|
|
|
# Callback to allow entity to configure itself based on this
|
|
|
|
# characteristics metadata (valid values, value ranges, features, etc)
|
|
|
|
setup_fn_name = escape_characteristic_name(short_name)
|
2019-09-03 17:27:14 +02:00
|
|
|
setup_fn = getattr(self, f"_setup_{setup_fn_name}", None)
|
2019-01-28 17:21:20 +01:00
|
|
|
if not setup_fn:
|
|
|
|
return
|
|
|
|
setup_fn(char)
|
2018-04-13 19:25:35 +02:00
|
|
|
|
2020-01-06 16:35:10 +01:00
|
|
|
def get_hk_char_value(self, characteristic_type):
|
|
|
|
"""Return the value for a given characteristic type enum."""
|
|
|
|
state = self._accessory.current_state.get(self._aid)
|
|
|
|
if not state:
|
|
|
|
return None
|
|
|
|
char = self._chars.get(CharacteristicsTypes.get_short(characteristic_type))
|
|
|
|
if not char:
|
|
|
|
return None
|
|
|
|
return state.get(char, {}).get("value")
|
|
|
|
|
2019-07-22 18:22:44 +02:00
|
|
|
@callback
|
|
|
|
def async_state_changed(self):
|
|
|
|
"""Collect new data from bridge and update the entity state in hass."""
|
|
|
|
accessory_state = self._accessory.current_state.get(self._aid, {})
|
|
|
|
for iid, result in accessory_state.items():
|
2020-01-31 17:33:00 +01:00
|
|
|
# No value so don't process this result
|
2019-07-31 21:25:30 +02:00
|
|
|
if "value" not in result:
|
2018-04-13 19:25:35 +02:00
|
|
|
continue
|
2019-08-01 17:35:19 +02:00
|
|
|
|
|
|
|
# Unknown iid - this is probably for a sibling service that is part
|
|
|
|
# of the same physical accessory. Ignore it.
|
|
|
|
if iid not in self._char_names:
|
|
|
|
continue
|
|
|
|
|
2019-01-28 21:27:26 +01:00
|
|
|
# Callback to update the entity with this characteristic value
|
|
|
|
char_name = escape_characteristic_name(self._char_names[iid])
|
2019-09-03 17:27:14 +02:00
|
|
|
update_fn = getattr(self, f"_update_{char_name}", None)
|
2019-01-28 21:27:26 +01:00
|
|
|
if not update_fn:
|
|
|
|
continue
|
2019-08-01 17:35:19 +02:00
|
|
|
|
2019-07-31 21:25:30 +02:00
|
|
|
update_fn(result["value"])
|
2018-04-13 19:25:35 +02:00
|
|
|
|
2019-07-22 18:22:44 +02:00
|
|
|
self.async_write_ha_state()
|
|
|
|
|
2018-04-13 19:25:35 +02:00
|
|
|
@property
|
|
|
|
def unique_id(self):
|
|
|
|
"""Return the ID of this device."""
|
2020-03-10 12:06:44 +01:00
|
|
|
serial = self.accessory_info.value(CharacteristicsTypes.SERIAL_NUMBER)
|
2019-09-03 17:27:14 +02:00
|
|
|
return f"homekit-{serial}-{self._iid}"
|
2018-04-13 19:25:35 +02:00
|
|
|
|
|
|
|
@property
|
|
|
|
def name(self):
|
|
|
|
"""Return the name of the device if any."""
|
2020-03-10 12:06:44 +01:00
|
|
|
return self.accessory_info.value(CharacteristicsTypes.NAME)
|
2018-04-13 19:25:35 +02:00
|
|
|
|
2018-10-04 09:25:05 +02:00
|
|
|
@property
|
|
|
|
def available(self) -> bool:
|
|
|
|
"""Return True if entity is available."""
|
2019-07-22 18:22:44 +02:00
|
|
|
return self._accessory.available
|
2018-10-04 09:25:05 +02:00
|
|
|
|
2019-05-17 08:41:21 +02:00
|
|
|
@property
|
|
|
|
def device_info(self):
|
|
|
|
"""Return the device info."""
|
2020-03-10 12:06:44 +01:00
|
|
|
info = self.accessory_info
|
|
|
|
accessory_serial = info.value(CharacteristicsTypes.SERIAL_NUMBER)
|
2019-05-17 08:41:21 +02:00
|
|
|
|
|
|
|
device_info = {
|
2019-07-31 21:25:30 +02:00
|
|
|
"identifiers": {(DOMAIN, "serial-number", accessory_serial)},
|
2020-03-10 12:06:44 +01:00
|
|
|
"name": info.value(CharacteristicsTypes.NAME),
|
|
|
|
"manufacturer": info.value(CharacteristicsTypes.MANUFACTURER, ""),
|
|
|
|
"model": info.value(CharacteristicsTypes.MODEL, ""),
|
|
|
|
"sw_version": info.value(CharacteristicsTypes.FIRMWARE_REVISION, ""),
|
2019-05-17 08:41:21 +02:00
|
|
|
}
|
|
|
|
|
2019-06-10 18:10:44 +02:00
|
|
|
# Some devices only have a single accessory - we don't add a
|
|
|
|
# via_device otherwise it would be self referential.
|
2019-07-31 21:25:30 +02:00
|
|
|
bridge_serial = self._accessory.connection_info["serial-number"]
|
2019-05-17 08:41:21 +02:00
|
|
|
if accessory_serial != bridge_serial:
|
2019-07-31 21:25:30 +02:00
|
|
|
device_info["via_device"] = (DOMAIN, "serial-number", bridge_serial)
|
2019-05-17 08:41:21 +02:00
|
|
|
|
|
|
|
return device_info
|
|
|
|
|
2019-01-28 17:21:20 +01:00
|
|
|
def get_characteristic_types(self):
|
|
|
|
"""Define the homekit characteristics the entity cares about."""
|
|
|
|
raise NotImplementedError
|
|
|
|
|
2018-04-13 19:25:35 +02:00
|
|
|
|
2019-05-13 08:56:05 +02:00
|
|
|
async def async_setup_entry(hass, entry):
|
|
|
|
"""Set up a HomeKit connection on a config entry."""
|
|
|
|
conn = HKDevice(hass, entry, entry.data)
|
|
|
|
hass.data[KNOWN_DEVICES][conn.unique_id] = conn
|
|
|
|
|
2019-12-19 09:45:23 +01:00
|
|
|
# For backwards compat
|
|
|
|
if entry.unique_id is None:
|
|
|
|
hass.config_entries.async_update_entry(
|
|
|
|
entry, unique_id=normalize_hkid(conn.unique_id)
|
|
|
|
)
|
|
|
|
|
2019-05-13 08:56:05 +02:00
|
|
|
if not await conn.async_setup():
|
|
|
|
del hass.data[KNOWN_DEVICES][conn.unique_id]
|
|
|
|
raise ConfigEntryNotReady
|
|
|
|
|
2019-05-17 08:41:21 +02:00
|
|
|
conn_info = conn.connection_info
|
|
|
|
|
|
|
|
device_registry = await dr.async_get_registry(hass)
|
|
|
|
device_registry.async_get_or_create(
|
|
|
|
config_entry_id=entry.entry_id,
|
|
|
|
identifiers={
|
2019-07-31 21:25:30 +02:00
|
|
|
(DOMAIN, "serial-number", conn_info["serial-number"]),
|
|
|
|
(DOMAIN, "accessory-id", conn.unique_id),
|
2019-05-17 08:41:21 +02:00
|
|
|
},
|
|
|
|
name=conn.name,
|
2019-07-31 21:25:30 +02:00
|
|
|
manufacturer=conn_info.get("manufacturer"),
|
|
|
|
model=conn_info.get("model"),
|
|
|
|
sw_version=conn_info.get("firmware.revision"),
|
2019-05-17 08:41:21 +02:00
|
|
|
)
|
|
|
|
|
2019-05-13 08:56:05 +02:00
|
|
|
return True
|
|
|
|
|
|
|
|
|
2019-04-18 17:55:34 +02:00
|
|
|
async def async_setup(hass, config):
|
2018-04-13 19:25:35 +02:00
|
|
|
"""Set up for Homekit devices."""
|
2019-04-18 17:55:34 +02:00
|
|
|
map_storage = hass.data[ENTITY_MAP] = EntityMapStorage(hass)
|
|
|
|
await map_storage.async_initialize()
|
|
|
|
|
2020-02-24 10:55:33 +01:00
|
|
|
hass.data[CONTROLLER] = aiohomekit.Controller()
|
2018-04-13 19:25:35 +02:00
|
|
|
hass.data[KNOWN_DEVICES] = {}
|
2019-04-18 17:55:34 +02:00
|
|
|
|
2020-02-25 12:09:04 +01:00
|
|
|
dothomekit_dir = hass.config.path(".homekit")
|
|
|
|
if os.path.exists(dothomekit_dir):
|
|
|
|
_LOGGER.warning(
|
|
|
|
(
|
|
|
|
"Legacy homekit_controller state found in %s. Support for reading "
|
|
|
|
"the folder is deprecated and will be removed in 0.109.0."
|
|
|
|
),
|
|
|
|
dothomekit_dir,
|
|
|
|
)
|
|
|
|
|
2018-04-13 19:25:35 +02:00
|
|
|
return True
|
2019-04-18 17:55:34 +02:00
|
|
|
|
|
|
|
|
2019-07-22 18:22:44 +02:00
|
|
|
async def async_unload_entry(hass, entry):
|
|
|
|
"""Disconnect from HomeKit devices before unloading entry."""
|
2019-07-31 21:25:30 +02:00
|
|
|
hkid = entry.data["AccessoryPairingID"]
|
2019-07-22 18:22:44 +02:00
|
|
|
|
|
|
|
if hkid in hass.data[KNOWN_DEVICES]:
|
|
|
|
connection = hass.data[KNOWN_DEVICES][hkid]
|
|
|
|
await connection.async_unload()
|
|
|
|
|
|
|
|
return True
|
|
|
|
|
|
|
|
|
2019-04-18 17:55:34 +02:00
|
|
|
async def async_remove_entry(hass, entry):
|
|
|
|
"""Cleanup caches before removing config entry."""
|
2019-07-31 21:25:30 +02:00
|
|
|
hkid = entry.data["AccessoryPairingID"]
|
2019-04-18 17:55:34 +02:00
|
|
|
hass.data[ENTITY_MAP].async_delete_map(hkid)
|