Esempio n. 1
0
    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)
Esempio n. 2
0
    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)
Esempio n. 3
0
    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)])
Esempio n. 4
0
 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)
Esempio n. 5
0
    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'
Esempio n. 6
0
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
Esempio n. 8
0
    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))
Esempio n. 9
0
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
Esempio n. 10
0
        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)
Esempio n. 11
0
    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'
Esempio n. 12
0
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