2019-02-14 16:01:46 +01:00
|
|
|
"""Support for Homekit device discovery."""
|
2018-04-13 19:25:35 +02:00
|
|
|
import logging
|
|
|
|
|
|
|
|
from homeassistant.components.discovery import SERVICE_HOMEKIT
|
|
|
|
from homeassistant.helpers import discovery
|
|
|
|
from homeassistant.helpers.entity import Entity
|
|
|
|
|
2019-03-28 00:36:50 +01:00
|
|
|
from .config_flow import load_old_pairings
|
2019-03-28 04:16:00 +01:00
|
|
|
from .connection import get_accessory_information, HKDevice
|
2019-03-07 04:44:52 +01:00
|
|
|
from .const import (
|
2019-04-18 17:55:34 +02:00
|
|
|
CONTROLLER, ENTITY_MAP, KNOWN_DEVICES
|
2019-03-07 04:44:52 +01:00
|
|
|
)
|
2019-03-28 04:01:10 +01:00
|
|
|
from .const import DOMAIN # noqa: pylint: disable=unused-import
|
2019-04-18 17:55:34 +02:00
|
|
|
from .storage import EntityMapStorage
|
2019-03-07 04:44:52 +01:00
|
|
|
|
2018-07-12 11:52:37 +02:00
|
|
|
HOMEKIT_IGNORE = [
|
|
|
|
'BSB002',
|
|
|
|
'Home Assistant Bridge',
|
2019-02-14 16:01:46 +01:00
|
|
|
'TRADFRI gateway',
|
2018-07-12 11:52:37 +02: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."""
|
|
|
|
return char_name.replace('-', '_').replace('.', '_')
|
|
|
|
|
|
|
|
|
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."""
|
2019-03-13 02:45:34 +01:00
|
|
|
self._available = True
|
2018-10-04 09:25:05 +02:00
|
|
|
self._accessory = accessory
|
2018-04-13 19:25:35 +02:00
|
|
|
self._aid = devinfo['aid']
|
|
|
|
self._iid = devinfo['iid']
|
|
|
|
self._features = 0
|
|
|
|
self._chars = {}
|
2019-01-28 17:21:20 +01:00
|
|
|
self.setup()
|
|
|
|
|
|
|
|
def setup(self):
|
|
|
|
"""Configure an entity baed on its HomeKit characterstics metadata."""
|
|
|
|
# pylint: disable=import-error
|
|
|
|
from homekit.model.characteristics import CharacteristicsTypes
|
|
|
|
|
2019-04-18 17:55:34 +02:00
|
|
|
accessories = self._accessory.accessories
|
2019-01-28 17:21:20 +01:00
|
|
|
|
|
|
|
get_uuid = CharacteristicsTypes.get_uuid
|
|
|
|
characteristic_types = [
|
|
|
|
get_uuid(c) for c in self.get_characteristic_types()
|
|
|
|
]
|
|
|
|
|
|
|
|
self._chars_to_poll = []
|
|
|
|
self._chars = {}
|
|
|
|
self._char_names = {}
|
|
|
|
|
2019-04-18 17:55:34 +02:00
|
|
|
for accessory in accessories:
|
2019-01-28 17:21:20 +01:00
|
|
|
if accessory['aid'] != self._aid:
|
|
|
|
continue
|
2019-03-19 20:04:20 +01:00
|
|
|
self._accessory_info = get_accessory_information(accessory)
|
2019-01-28 17:21:20 +01:00
|
|
|
for service in accessory['services']:
|
|
|
|
if service['iid'] != self._iid:
|
|
|
|
continue
|
|
|
|
for char in service['characteristics']:
|
2019-02-08 11:00:51 +01:00
|
|
|
try:
|
|
|
|
uuid = CharacteristicsTypes.get_uuid(char['type'])
|
|
|
|
except KeyError:
|
|
|
|
# If a KeyError is raised its a non-standard
|
|
|
|
# characteristic. We must ignore it in this case.
|
|
|
|
continue
|
2019-01-28 17:21:20 +01:00
|
|
|
if uuid not in characteristic_types:
|
|
|
|
continue
|
|
|
|
self._setup_characteristic(char)
|
|
|
|
|
|
|
|
def _setup_characteristic(self, char):
|
|
|
|
"""Configure an entity based on a HomeKit characteristics metadata."""
|
|
|
|
# pylint: disable=import-error
|
|
|
|
from homekit.model.characteristics import CharacteristicsTypes
|
|
|
|
|
|
|
|
# Build up a list of (aid, iid) tuples to poll on update()
|
|
|
|
self._chars_to_poll.append((self._aid, char['iid']))
|
|
|
|
|
|
|
|
# Build a map of ctype -> iid
|
|
|
|
short_name = CharacteristicsTypes.get_short(char['type'])
|
|
|
|
self._chars[short_name] = char['iid']
|
|
|
|
self._char_names[char['iid']] = short_name
|
|
|
|
|
|
|
|
# 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)
|
|
|
|
setup_fn = getattr(self, '_setup_{}'.format(setup_fn_name), None)
|
|
|
|
if not setup_fn:
|
|
|
|
return
|
2019-01-28 21:27:26 +01:00
|
|
|
# pylint: disable=not-callable
|
2019-01-28 17:21:20 +01:00
|
|
|
setup_fn(char)
|
2018-04-13 19:25:35 +02:00
|
|
|
|
2019-03-11 19:59:41 +01:00
|
|
|
async def async_update(self):
|
2018-04-13 19:25:35 +02:00
|
|
|
"""Obtain a HomeKit device's state."""
|
2019-01-23 20:44:21 +01:00
|
|
|
# pylint: disable=import-error
|
2019-03-13 02:45:34 +01:00
|
|
|
from homekit.exceptions import (
|
|
|
|
AccessoryDisconnectedError, AccessoryNotFoundError)
|
2019-01-23 20:44:21 +01:00
|
|
|
|
2018-10-04 09:25:05 +02:00
|
|
|
try:
|
2019-03-11 19:59:41 +01:00
|
|
|
new_values_dict = await self._accessory.get_characteristics(
|
|
|
|
self._chars_to_poll
|
|
|
|
)
|
2019-03-13 02:45:34 +01:00
|
|
|
except AccessoryNotFoundError:
|
|
|
|
# Not only did the connection fail, but also the accessory is not
|
|
|
|
# visible on the network.
|
|
|
|
self._available = False
|
|
|
|
return
|
2019-01-23 20:44:21 +01:00
|
|
|
except AccessoryDisconnectedError:
|
2019-03-13 02:45:34 +01:00
|
|
|
# Temporary connection failure. Device is still available but our
|
|
|
|
# connection was dropped.
|
2018-10-04 09:25:05 +02:00
|
|
|
return
|
2019-01-28 21:27:26 +01:00
|
|
|
|
2019-03-13 02:45:34 +01:00
|
|
|
self._available = True
|
|
|
|
|
2019-01-28 21:27:26 +01:00
|
|
|
for (_, iid), result in new_values_dict.items():
|
|
|
|
if 'value' not in result:
|
2018-04-13 19:25:35 +02:00
|
|
|
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])
|
|
|
|
update_fn = getattr(self, '_update_{}'.format(char_name), None)
|
|
|
|
if not update_fn:
|
|
|
|
continue
|
|
|
|
# pylint: disable=not-callable
|
|
|
|
update_fn(result['value'])
|
2018-04-13 19:25:35 +02:00
|
|
|
|
|
|
|
@property
|
|
|
|
def unique_id(self):
|
|
|
|
"""Return the ID of this device."""
|
2019-03-19 20:04:20 +01:00
|
|
|
serial = self._accessory_info['serial-number']
|
|
|
|
return "homekit-{}-{}".format(serial, self._iid)
|
2018-04-13 19:25:35 +02:00
|
|
|
|
|
|
|
@property
|
|
|
|
def name(self):
|
|
|
|
"""Return the name of the device if any."""
|
2019-03-19 20:04:20 +01:00
|
|
|
return self._accessory_info.get('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-03-13 02:45:34 +01:00
|
|
|
return self._available
|
2018-10-04 09:25:05 +02:00
|
|
|
|
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-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."""
|
2018-12-24 22:13:17 +01:00
|
|
|
# pylint: disable=import-error
|
|
|
|
import homekit
|
2019-03-12 21:54:08 +01:00
|
|
|
from homekit.controller.ip_implementation import IpPairing
|
2018-12-24 22:13:17 +01:00
|
|
|
|
2019-04-18 17:55:34 +02:00
|
|
|
map_storage = hass.data[ENTITY_MAP] = EntityMapStorage(hass)
|
|
|
|
await map_storage.async_initialize()
|
|
|
|
|
2018-12-24 22:13:17 +01:00
|
|
|
hass.data[CONTROLLER] = controller = homekit.Controller()
|
|
|
|
|
2019-03-28 00:36:50 +01:00
|
|
|
for hkid, pairing_data in load_old_pairings(hass).items():
|
|
|
|
controller.pairings[hkid] = IpPairing(pairing_data)
|
2018-12-24 22:13:17 +01:00
|
|
|
|
2018-04-13 19:25:35 +02:00
|
|
|
def discovery_dispatch(service, discovery_info):
|
|
|
|
"""Dispatcher for Homekit discovery events."""
|
|
|
|
# model, id
|
|
|
|
host = discovery_info['host']
|
|
|
|
port = discovery_info['port']
|
2019-02-24 13:56:52 +01:00
|
|
|
|
|
|
|
# Fold property keys to lower case, making them effectively
|
|
|
|
# case-insensitive. Some HomeKit devices capitalize them.
|
|
|
|
properties = {
|
|
|
|
key.lower(): value
|
|
|
|
for (key, value) in discovery_info['properties'].items()
|
|
|
|
}
|
|
|
|
|
|
|
|
model = properties['md']
|
|
|
|
hkid = properties['id']
|
|
|
|
config_num = int(properties['c#'])
|
2018-04-13 19:25:35 +02:00
|
|
|
|
2018-07-12 11:52:37 +02:00
|
|
|
if model in HOMEKIT_IGNORE:
|
|
|
|
return
|
|
|
|
|
2018-04-13 19:25:35 +02:00
|
|
|
# Only register a device once, but rescan if the config has changed
|
|
|
|
if hkid in hass.data[KNOWN_DEVICES]:
|
|
|
|
device = hass.data[KNOWN_DEVICES][hkid]
|
|
|
|
if config_num > device.config_num and \
|
2019-04-17 19:02:51 +02:00
|
|
|
device.pairing is not None:
|
2019-04-18 17:55:34 +02:00
|
|
|
device.refresh_entity_map(config_num)
|
2018-04-13 19:25:35 +02:00
|
|
|
return
|
|
|
|
|
|
|
|
_LOGGER.debug('Discovered unique device %s', hkid)
|
2019-04-18 17:55:34 +02:00
|
|
|
device = HKDevice(hass, host, port, model, hkid, config_num, config)
|
|
|
|
device.setup()
|
2018-04-13 19:25:35 +02:00
|
|
|
|
|
|
|
hass.data[KNOWN_DEVICES] = {}
|
2019-04-18 17:55:34 +02:00
|
|
|
|
|
|
|
await hass.async_add_executor_job(
|
|
|
|
discovery.listen, hass, SERVICE_HOMEKIT, discovery_dispatch)
|
|
|
|
|
2018-04-13 19:25:35 +02:00
|
|
|
return True
|
2019-04-18 17:55:34 +02:00
|
|
|
|
|
|
|
|
|
|
|
async def async_remove_entry(hass, entry):
|
|
|
|
"""Cleanup caches before removing config entry."""
|
|
|
|
hkid = entry.data['AccessoryPairingID']
|
|
|
|
hass.data[ENTITY_MAP].async_delete_map(hkid)
|