async def async_import_legacy_pairing(self, discovery_props, pairing_data): """Migrate a legacy pairing to config entries.""" hkid = discovery_props["id"] existing = find_existing_host(self.hass, hkid) if existing: _LOGGER.info( ( "Legacy configuration for homekit accessory %s" "not loaded as already migrated" ), hkid, ) return self.async_abort(reason="already_configured") _LOGGER.info( ( "Legacy configuration %s for homekit" "accessory migrated to config entries" ), hkid, ) pairing = IpPairing(pairing_data) return await self._entry_from_accessory(pairing)
def __init__(self, hass, config_entry, pairing_data): """Initialise a generic HomeKit device.""" from homekit.controller.ip_implementation import IpPairing self.hass = hass self.config_entry = config_entry # We copy pairing_data because homekit_python may mutate it, but we # don't want to mutate a dict owned by a config entry. self.pairing_data = pairing_data.copy() self.pairing = IpPairing(self.pairing_data) self.accessories = {} self.config_num = 0 # A list of callbacks that turn HK service metadata into entities self.listeners = [] # The platorms we have forwarded the config entry so far. If a new # accessory is added to a bridge we may have to load additional # platforms. We don't want to load all platforms up front if its just # a lightbulb. And we dont want to forward a config entry twice # (triggers a Config entry already set up error) self.platforms = set() # This just tracks aid/iid pairs so we know if a HK service has been # mapped to a HA entity. self.entities = [] # There are multiple entities sharing a single connection - only # allow one entity to use pairing at once. self.pairing_lock = asyncio.Lock(loop=hass.loop)
def test_put_characteristics_sends_minimal_json(self): """ The tado internet bridge will fail if a there are spaces in the json data transmitted to it. An iPhone client sends minimalistic json with the whitespace stripped out: b'{"characteristics":[{"aid":2,"iid":12,"ev":true},...,{"aid":2,"iid":17,"ev":true}]}' https://github.com/jlusiardi/homekit_python/issues/181 https://github.com/jlusiardi/homekit_python/pull/182 """ session = __class__.prepare_mock_function() pairing = IpPairing(session.pairing_data) pairing.session = session pairing.put_characteristics([(1, 2, 3)])
def finish_pairing(pin): Controller.check_pin_format(pin) try: pairing = perform_pair_setup_part2(pin, str(uuid.uuid4()), write_fun, salt, pub_key) finally: conn.close() pairing['AccessoryIP'] = connection_data['ip'] pairing['AccessoryPort'] = connection_data['port'] pairing['Connection'] = 'IP' self.pairings[alias] = IpPairing(pairing)
def test_get_events_sends_minimal_json(self): """ The tado internet bridge will fail if a there are spaces in the json data transmitted to it. An iPhone client sends minimalistic json with the whitespace stripped out: b'{"characteristics":[{"aid":2,"iid":12,"ev":true},...,{"aid":2,"iid":17,"ev":true}]}' https://github.com/jlusiardi/homekit_python/issues/181 https://github.com/jlusiardi/homekit_python/pull/182 """ session = __class__.prepare_mock_function() pairing = IpPairing(session.pairing_data) pairing.session = session pairing.get_events([(1, 2)], lambda *_: None) assert session.put.call_args[0][0] == '/characteristics' payload = session.put.call_args[0][1] assert ' ' not in payload, 'Regression of https://github.com/jlusiardi/homekit_python/issues/181'
async def async_setup(hass, config): """Set up for Homekit devices.""" # pylint: disable=import-error import homekit from homekit.controller.ip_implementation import IpPairing map_storage = hass.data[ENTITY_MAP] = EntityMapStorage(hass) await map_storage.async_initialize() hass.data[CONTROLLER] = controller = homekit.Controller() old_pairings = await hass.async_add_executor_job(load_old_pairings, hass) for hkid, pairing_data in old_pairings.items(): controller.pairings[hkid] = IpPairing(pairing_data) def discovery_dispatch(service, discovery_info): """Dispatcher for Homekit discovery events.""" # model, id host = discovery_info['host'] port = discovery_info['port'] # 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#']) if model in HOMEKIT_IGNORE: return # 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 \ device.pairing is not None: device.refresh_entity_map(config_num) return _LOGGER.debug('Discovered unique device %s', hkid) device = HKDevice(hass, host, port, model, hkid, config_num, config) device.setup() hass.data[KNOWN_DEVICES] = {} await hass.async_add_executor_job(discovery.listen, hass, SERVICE_HOMEKIT, discovery_dispatch) return True
def __init__(self, opp, config_entry, pairing_data): """Initialise a generic HomeKit device.""" self.opp = opp self.config_entry = config_entry # We copy pairing_data because homekit_python may mutate it, but we # don't want to mutate a dict owned by a config entry. self.pairing_data = pairing_data.copy() self.pairing = IpPairing(self.pairing_data) self.accessories = {} self.config_num = 0 # A list of callbacks that turn HK service metadata into entities self.listeners = [] # The platorms we have forwarded the config entry so far. If a new # accessory is added to a bridge we may have to load additional # platforms. We don't want to load all platforms up front if its just # a lightbulb. And we don't want to forward a config entry twice # (triggers a Config entry already set up error) self.platforms = set() # This just tracks aid/iid pairs so we know if a HK service has been # mapped to a HA entity. self.entities = [] # There are multiple entities sharing a single connection - only # allow one entity to use pairing at once. self.pairing_lock = asyncio.Lock() self.available = True self.signal_state_updated = "_".join( (DOMAIN, self.unique_id, "state_updated")) # Current values of all characteristics homekit_controller is tracking. # Key is a (accessory_id, characteristic_id) tuple. self.current_state = {} self.pollable_characteristics = [] # If this is set polling is active and can be disabled by calling # this method. self._polling_interval_remover = None # Never allow concurrent polling of the same accessory or bridge self._polling_lock = asyncio.Lock() self._polling_lock_warned = False
def load_data(self, filename): """ Loads the pairing data of the controller from a file. :param filename: the file name of the pairing data :raises ConfigLoadingError: if the config could not be loaded. The reason is given in the message. :raises TransportNotSupportedError: if the dependencies for the selected transport are not installed """ try: with open(filename, 'r') as input_fp: data = json.load(input_fp) for pairing_id in data: if 'Connection' not in data[pairing_id]: # This is a pre BLE entry in the file with the pairing data, hence it is for an IP based # accessory. So we set the connection type (in case save data is used everything will be fine) # and also issue a warning data[pairing_id]['Connection'] = 'IP' self.logger.warning( 'Loaded pairing for %s with missing connection type. Assume this is IP based.', pairing_id) if data[pairing_id]['Connection'] == 'IP': if not IP_TRANSPORT_SUPPORTED: raise TransportNotSupportedError('IP') self.pairings[pairing_id] = IpPairing(data[pairing_id]) elif data[pairing_id]['Connection'] == 'BLE': if not BLE_TRANSPORT_SUPPORTED: raise TransportNotSupportedError('BLE') self.pairings[pairing_id] = BlePairing( data[pairing_id], self.ble_adapter) elif data[pairing_id][ 'Connection'] == 'ADDITIONAL_PAIRING': self.pairings[pairing_id] = AdditionalPairing( data[pairing_id]) else: # ignore anything else, issue warning self.logger.warning( 'could not load pairing %s of type "%s"', pairing_id, data[pairing_id]['Connection']) except PermissionError: raise ConfigLoadingError( 'Could not open "{f}" due to missing permissions'.format( f=filename)) except JSONDecodeError: raise ConfigLoadingError( 'Cannot parse "{f}" as JSON file'.format(f=filename)) except FileNotFoundError: raise ConfigLoadingError( 'Could not open "{f}" because it does not exist'.format( f=filename))
def setup(hass, config): """Set up for Homekit devices.""" # pylint: disable=import-error import homekit from homekit.controller.ip_implementation import IpPairing hass.data[CONTROLLER] = controller = homekit.Controller() for hkid, pairing_data in load_old_pairings(hass).items(): controller.pairings[hkid] = IpPairing(pairing_data) def discovery_dispatch(service, discovery_info): """Dispatcher for Homekit discovery events.""" # model, id host = discovery_info['host'] port = discovery_info['port'] # 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#']) if model != BRID_HK_MODEL: return # 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 \ device.pairing is not None: device.accessory_setup() return _LOGGER.debug('Discovered Brid Air Purifier: %s', hkid) HKDevice(hass, host, port, model, hkid, config_num, config) hass.data[KNOWN_DEVICES] = {} hass.data[const.KNOWN_ENTITIES] = {} discovery.listen(hass, SERVICE_HOMEKIT, discovery_dispatch) return True
def finish_pairing(pin): Controller.check_pin_format(pin) try: state_machine = perform_pair_setup_part2(pin, str(uuid.uuid4()), salt, pub_key) request, expected = state_machine.send(None) while True: try: response = write_fun(request, expected) request, expected = state_machine.send(response) except StopIteration as result: pairing = result.value break finally: conn.close() pairing['AccessoryIP'] = connection_data['ip'] pairing['AccessoryPort'] = connection_data['port'] pairing['Connection'] = 'IP' self.pairings[alias] = IpPairing(pairing)
def test_requests_only_send_params_for_true_case(self): """ The tado internet bridge will fail if a GET request has what it considers to be unexpected request parameters. An iPhone client sends requests like `/characteristics?id=1.10`. It doesn't transmit `ev=0` or any other falsies. https://github.com/home-assistant/home-assistant/issues/16971 https://github.com/jlusiardi/homekit_python/pull/132 """ pairing = IpPairing({}) with mock.patch.object(pairing, 'session') as session: session.get.return_value.read.return_value = b'{"characteristics": []}' pairing.get_characteristics([(1, 2)]) assert session.get.call_args[0][0] == '/characteristics?id=1.2' pairing.get_characteristics([(1, 2)], include_meta=True) assert session.get.call_args[0][ 0] == '/characteristics?id=1.2&meta=1'
def setup(hass, config): """Set up for Homekit devices.""" # pylint: disable=import-error import homekit from homekit.controller.ip_implementation import IpPairing hass.data[CONTROLLER] = controller = homekit.Controller() data_dir = os.path.join(hass.config.path(), HOMEKIT_DIR) if not os.path.isdir(data_dir): os.mkdir(data_dir) pairing_file = os.path.join(data_dir, PAIRING_FILE) if os.path.exists(pairing_file): controller.load_data(pairing_file) # Migrate any existing pairings to the new internal homekit_python format for device in os.listdir(data_dir): if not device.startswith('hk-'): continue alias = device[3:] if alias in controller.pairings: continue with open(os.path.join(data_dir, device)) as pairing_data_fp: pairing_data = json.load(pairing_data_fp) controller.pairings[alias] = IpPairing(pairing_data) controller.save_data(pairing_file) def discovery_dispatch(service, discovery_info): """Dispatcher for Homekit discovery events.""" # model, id host = discovery_info['host'] port = discovery_info['port'] # 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#']) if model in HOMEKIT_IGNORE: return # 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 \ device.pairing_info is not None: device.accessory_setup() return _LOGGER.debug('Discovered unique device %s', hkid) device = HKDevice(hass, host, port, model, hkid, config_num, config) hass.data[KNOWN_DEVICES][hkid] = device hass.data[KNOWN_ACCESSORIES] = {} hass.data[KNOWN_DEVICES] = {} discovery.listen(hass, SERVICE_HOMEKIT, discovery_dispatch) return True