Exemple #1
0
async def test_update_named_service_events_manual_accessory_auto_requires():
    accessories = Accessories()
    accessory = Accessory.create_with_info(
        name="TestLight",
        manufacturer="Test Mfr",
        model="Test Bulb",
        serial_number="1234",
        firmware_revision="1.1",
    )
    service = accessory.add_service(ServicesTypes.LIGHTBULB,
                                    name="Light Strip",
                                    add_required=True)
    on_char = service[CharacteristicsTypes.ON]
    accessories.add_accessory(accessory)

    controller = FakeController()
    pairing = await controller.add_paired_device(accessories, "alias")

    callback = mock.Mock()
    await pairing.subscribe([(accessory.aid, on_char.iid)])
    pairing.dispatcher_connect(callback)

    # Simulate that the state was changed on the device itself.
    pairing.testing.update_named_service("Light Strip",
                                         {CharacteristicsTypes.ON: True})

    assert callback.call_args_list == [
        mock.call({(accessory.aid, on_char.iid): {
                       "value": 1
                   }})
    ]
Exemple #2
0
async def setup_test_accessories(hass, accessories):
    """Load a fake homekit device based on captured JSON profile."""
    fake_controller = await setup_platform(hass)

    pairing_id = "00:00:00:00:00:00"

    accessories_obj = Accessories()
    for accessory in accessories:
        accessories_obj.add_accessory(accessory)
    pairing = await fake_controller.add_paired_device(accessories_obj,
                                                      pairing_id)

    config_entry = MockConfigEntry(
        version=1,
        domain="homekit_controller",
        entry_id="TestData",
        data={"AccessoryPairingID": pairing_id},
        title="test",
    )
    config_entry.add_to_hass(hass)

    await hass.config_entries.async_setup(config_entry.entry_id)
    await hass.async_block_till_done()

    return config_entry, pairing
Exemple #3
0
async def device_config_changed(hass, accessories):
    """Discover new devices added to Home Assistant at runtime."""
    # Update the accessories our FakePairing knows about
    controller = hass.data[CONTROLLER]
    pairing = controller.pairings["00:00:00:00:00:00"]

    accessories_obj = Accessories()
    for accessory in accessories:
        accessories_obj.add_accessory(accessory)
    pairing.accessories = accessories_obj

    discovery_info = {
        "name": "TestDevice",
        "host": "127.0.0.1",
        "port": 8080,
        "properties": {
            "md": "TestDevice",
            "id": "00:00:00:00:00:00",
            "c#": "2",
            "sf": "0",
        },
    }

    # Config Flow will abort and notify us if the discovery event is of
    # interest - in this case c# has incremented
    flow = config_flow.HomekitControllerFlowHandler()
    flow.hass = hass
    flow.context = {}
    result = await flow.async_step_zeroconf(discovery_info)
    assert result["type"] == "abort"
    assert result["reason"] == "already_configured"

    # Wait for services to reconfigure
    await hass.async_block_till_done()
    await hass.async_block_till_done()
Exemple #4
0
async def device_config_changed(hass, accessories):
    """Discover new devices added to Home Assistant at runtime."""
    # Update the accessories our FakePairing knows about
    controller = hass.data[CONTROLLER]
    pairing = controller.pairings["00:00:00:00:00:00"]

    accessories_obj = Accessories()
    for accessory in accessories:
        accessories_obj.add_accessory(accessory)
    pairing._accessories_state = AccessoriesState(
        accessories_obj, pairing.config_num + 1
    )
    pairing._async_description_update(
        HomeKitService(
            name="TestDevice.local",
            id="00:00:00:00:00:00",
            model="",
            config_num=2,
            state_num=3,
            feature_flags=0,
            status_flags=0,
            category=1,
            protocol_version="1.0",
            type="_hap._tcp.local.",
            address="127.0.0.1",
            addresses=["127.0.0.1"],
            port=8080,
        )
    )

    # Wait for services to reconfigure
    await hass.async_block_till_done()
    await hass.async_block_till_done()
async def test_parse_old_homekit_json(hass):
    """Test migrating original .homekit/hk-00:00:00:00:00:00 files."""
    accessory = Accessory.create_with_info("TestDevice", "example.com", "Test",
                                           "0001", "0.1")
    service = accessory.add_service(ServicesTypes.LIGHTBULB)
    on_char = service.add_char(CharacteristicsTypes.ON)
    on_char.value = 0

    accessories = Accessories()
    accessories.add_accessory(accessory)

    fake_controller = await setup_platform(hass)
    pairing = await fake_controller.add_paired_device(accessories,
                                                      "00:00:00:00:00:00")
    pairing.pairing_data = {"AccessoryPairingID": "00:00:00:00:00:00"}

    mock_path = mock.Mock()
    mock_path.exists.side_effect = [False, True]

    mock_listdir = mock.Mock()
    mock_listdir.return_value = ["hk-00:00:00:00:00:00", "pairings.json"]

    read_data = {"AccessoryPairingID": "00:00:00:00:00:00"}
    mock_open = mock.mock_open(read_data=json.dumps(read_data))

    discovery_info = {
        "name": "TestDevice",
        "host": "127.0.0.1",
        "port": 8080,
        "properties": {
            "md": "TestDevice",
            "id": "00:00:00:00:00:00",
            "c#": 1,
            "sf": 0
        },
    }

    flow = _setup_flow_handler(hass)

    pairing_cls_imp = (
        "homeassistant.components.homekit_controller.config_flow.IpPairing")

    with mock.patch(pairing_cls_imp) as pairing_cls:
        pairing_cls.return_value = pairing
        with mock.patch("builtins.open", mock_open):
            with mock.patch("os.path", mock_path):
                with mock.patch("os.listdir", mock_listdir):
                    result = await flow.async_step_zeroconf(discovery_info)

    assert result["type"] == "create_entry"
    assert result["title"] == "TestDevice"
    assert result["data"]["AccessoryPairingID"] == "00:00:00:00:00:00"
    assert flow.context == {
        "hkid": "00:00:00:00:00:00",
        "title_placeholders": {
            "name": "TestDevice"
        },
        "unique_id": "00:00:00:00:00:00",
    }
Exemple #6
0
    def __init__(self, hass, config_entry, pairing_data):
        """Initialise a generic HomeKit device."""

        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 = hass.data[CONTROLLER].load_pairing(
            self.pairing_data["AccessoryPairingID"], self.pairing_data)

        self.accessories = None
        self.config_num = 0

        self.entity_map = Accessories()

        # 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 = []

        # A map of aid -> device_id
        # Useful when routing events to triggers
        self.devices = {}

        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

        self.watchable_characteristics = []

        self.pairing.dispatcher_connect(self.process_new_events)
Exemple #7
0
async def setup_accessories_from_file(hass, path):
    """Load an collection of accessory defs from JSON data."""
    accessories_fixture = await hass.async_add_executor_job(
        load_fixture, os.path.join("homekit_controller", path))
    accessories_json = json.loads(accessories_fixture)
    accessories = Accessories.from_list(accessories_json)
    return accessories
Exemple #8
0
def test_linked_services():
    a = Accessories.from_file("tests/fixtures/hue_bridge.json").aid(
        6623462389072572)

    service = a.services.first(
        service_type=ServicesTypes.STATELESS_PROGRAMMABLE_SWITCH)
    assert len(service.linked) > 0
    assert service.linked[0].type == "000000CC-0000-1000-8000-0026BB765291"
Exemple #9
0
def test_linked_services():
    a = Accessories.from_file("tests/fixtures/hue_bridge.json").aid(
        6623462389072572)

    service = a.services.first(
        service_type=ServicesTypes.STATELESS_PROGRAMMABLE_SWITCH)
    assert len(service.linked) > 0
    assert service.linked[0].short_type == ServicesTypes.SERVICE_LABEL
Exemple #10
0
def test_process_changes():
    accessories = Accessories.from_file("tests/fixtures/koogeek_ls1.json")

    on_char = accessories.aid(1).characteristics.iid(8)
    assert on_char.value is False

    accessories.process_changes({(1, 8): {"value": True}})

    assert on_char.value is True
Exemple #11
0
def setup_mock_accessory(controller):
    """Add a bridge accessory to a test controller."""
    bridge = Accessories()

    accessory = Accessory.create_with_info(
        name="Koogeek-LS1-20833F",
        manufacturer="Koogeek",
        model="LS1",
        serial_number="12345",
        firmware_revision="1.1",
    )

    service = accessory.add_service(ServicesTypes.LIGHTBULB)
    on_char = service.add_char(CharacteristicsTypes.ON)
    on_char.value = 0

    bridge.add_accessory(accessory)

    return controller.add_device(bridge)
Exemple #12
0
def test_tlv8_struct():
    a = Accessories.from_file(
        "tests/fixtures/home_assistant_bridge_camera.json")
    service = a.aid(2018094878).services.iid(11)

    streaming_status = service.value(CharacteristicsTypes.STREAMING_STATUS)
    assert streaming_status.status == StreamingStatusValues.AVAILABLE

    video_stream_config = service.value(
        CharacteristicsTypes.SUPPORTED_VIDEO_STREAM_CONFIGURATION)

    assert video_stream_config == SupportedVideoStreamConfiguration(config=[
        VideoConfigConfiguration(
            codec_type=VideoCodecTypeValues.H264,
            codec_params=[
                VideoCodecParameters(
                    profile_id=ProfileIDValues.HIGH_PROFILE,
                    level=ProfileSupportLevelValues.FOUR,
                    packetization_mode=PacketizationModeValues.
                    NON_INTERLEAVED_MODE,
                    cvo_enabled=None,
                    cvo_id=None,
                )
            ],
            video_attrs=[VideoAttrs(width=1920, height=1080, fps=30)],
        )
    ])

    audio_stream_config = service.value(
        CharacteristicsTypes.SUPPORTED_AUDIO_CONFIGURATION)

    assert audio_stream_config == SupportedAudioStreamConfiguration(
        config=[
            AudioCodecConfiguration(
                codec=AudioCodecValues.OPUS,
                parameters=[
                    AudioCodecParameters(
                        audio_channels=1,
                        bit_rate=BitRateValues.VARIABLE,
                        sample_rate=SampleRateValues.SIXTEEN_KHZ,
                        rtp_time=None,
                    )
                ],
            )
        ],
        comfort_noise=0,
    )

    rtp_config = service.value(
        CharacteristicsTypes.SUPPORTED_RTP_CONFIGURATION)

    assert rtp_config == [
        SupportedRTPConfiguration(
            srtp_crypto_suite=SRTPCryptoSuiteValues.AES_CM_128_HMAC_SHA1_80, ),
    ]
Exemple #13
0
async def test_pairing():
    accessories = Accessories.from_file("tests/fixtures/koogeek_ls1.json")
    controller = FakeController()
    device = controller.add_device(accessories)

    discovery = await controller.find_ip_by_device_id(device.device_id)
    finish_pairing = await discovery.start_pairing("alias")
    pairing = await finish_pairing("111-22-333")

    chars_and_services = await pairing.list_accessories_and_characteristics()
    assert isinstance(chars_and_services, list)
Exemple #14
0
def test_get_by_name():
    name = "Hue dimmer switch button 3"
    a = Accessories.from_file("tests/fixtures/hue_bridge.json").aid(
        6623462389072572)

    service = a.services.first(
        service_type=ServicesTypes.STATELESS_PROGRAMMABLE_SWITCH,
        characteristics={CharacteristicsTypes.NAME: name},
    )

    assert service[CharacteristicsTypes.NAME].value == name
Exemple #15
0
def test_set_value():
    """
    At the moment a bunch of tests in Home Assistant rely on Char.set_value
    """
    accessories = Accessories.from_file("tests/fixtures/koogeek_ls1.json")

    on_char = accessories.aid(1).characteristics.iid(8)
    assert on_char.value is False

    on_char.value = True

    assert on_char.value is True
Exemple #16
0
async def test_update_aid_iid_events():
    accessories = Accessories.from_file("tests/fixtures/koogeek_ls1.json")
    controller = FakeController()
    pairing = await controller.add_paired_device(accessories, "alias")

    callback = mock.Mock()
    await pairing.subscribe([(1, 8)])
    pairing.dispatcher_connect(callback)

    # Simulate that the state was changed on the device itself.
    pairing.testing.update_aid_iid([(1, 8, True)])

    assert callback.call_args_list == [mock.call({(1, 8): {"value": 1}})]
Exemple #17
0
def test_tlv8_struct_bare_array():
    a = Accessories.from_file("tests/fixtures/camera.json")
    service = a.aid(1).services.iid(16)

    rtp_config = service.value(
        CharacteristicsTypes.SUPPORTED_RTP_CONFIGURATION)

    assert rtp_config == [
        SupportedRTPConfiguration(
            srtp_crypto_suite=SRTPCryptoSuiteValues.DISABLED, ),
        SupportedRTPConfiguration(
            srtp_crypto_suite=SRTPCryptoSuiteValues.AES_CM_128_HMAC_SHA1_80, ),
    ]
Exemple #18
0
async def test_events_are_filtered():
    accessories = Accessories.from_file("tests/fixtures/koogeek_ls1.json")
    controller = FakeController()
    pairing = await controller.add_paired_device(accessories, "alias")

    callback = mock.Mock()
    await pairing.subscribe([(1, 10)])
    pairing.dispatcher_connect(callback)

    # Simulate that the state was changed on the device itself.
    pairing.testing.update_named_service("Light Strip",
                                         {CharacteristicsTypes.ON: True})

    assert callback.call_args_list == []
Exemple #19
0
def test_build_update():
    name = "Hue dimmer switch button 3"

    a = Accessories.from_file("tests/fixtures/hue_bridge.json").aid(
        6623462389072572)

    service = a.services.first(
        service_type=ServicesTypes.STATELESS_PROGRAMMABLE_SWITCH,
        characteristics={CharacteristicsTypes.NAME: name},
    )

    payload = service.build_update({CharacteristicsTypes.NAME: "Fred"})

    assert payload == [(6623462389072572, 588410716196, "Fred")]
Exemple #20
0
def test_tlv8_struct_re_encode():
    a = Accessories.from_file("tests/fixtures/camera.json")
    service = a.aid(1).services.iid(16)

    video_stream_config = service.value(
        CharacteristicsTypes.SUPPORTED_VIDEO_STREAM_CONFIGURATION)

    raw = base64.b64decode(
        "AcUBAQACHQEBAAAAAQEBAAABAQICAQAAAAIBAQAAAgECAwEAAwsBAoAHAgI4BAMBHgAAAwsBAgAFAgL"
        "AAwMBHgAAAwsBAgAFAgLQAgMBHgAAAwsBAgAEAgIAAwMBHgAAAwsBAoACAgLgAQMBHgAAAwsBAoACAg"
        "JoAQMBHgAAAwsBAuABAgJoAQMBHgAAAwsBAuABAgIOAQMBHgAAAwsBAkABAgLwAAMBHgAAAwsBAkABA"
        "gLwAAMBDwAAAwsBAkABAgK0AAMBHg==")

    assert raw == video_stream_config.encode()
Exemple #21
0
def test_order_by():
    a = Accessories.from_file("tests/fixtures/hue_bridge.json").aid(
        6623462389072572)

    buttons = a.services.filter(
        service_type=ServicesTypes.STATELESS_PROGRAMMABLE_SWITCH,
        order_by=(CharacteristicsTypes.SERVICE_LABEL_INDEX,
                  CharacteristicsTypes.NAME),
    )

    assert buttons[0].value(CharacteristicsTypes.SERVICE_LABEL_INDEX) == 1
    assert buttons[1].value(CharacteristicsTypes.SERVICE_LABEL_INDEX) == 2
    assert buttons[2].value(CharacteristicsTypes.SERVICE_LABEL_INDEX) == 3
    assert buttons[3].value(CharacteristicsTypes.SERVICE_LABEL_INDEX) == 4
Exemple #22
0
async def test_put_failure():
    accessories = Accessories.from_file("tests/fixtures/koogeek_ls1.json")

    char = accessories.aid(1).characteristics.iid(10)
    char.status = HapStatusCode.UNABLE_TO_COMMUNICATE

    controller = FakeController()
    device = controller.add_device(accessories)

    discovery = await controller.find_ip_by_device_id(device.description.id)
    finish_pairing = await discovery.start_pairing("alias")
    pairing = await finish_pairing("111-22-333")

    chars = await pairing.put_characteristics([(1, 10, 1)])
    assert chars == {(1, 10): {"status": -70402}}
Exemple #23
0
def test_build_update_minStep_clamping_lennox():
    a = Accessories.from_file("tests/fixtures/lennox_e30.json").aid(1)
    service = a.services.iid(100)

    assertions = [
        (27.23, 27.0),
        (27.6, 27.5),
        (27.26, 27.5),
        (27.9, 28.0),
    ]

    for left, right in assertions:
        payload = service.build_update(
            {CharacteristicsTypes.TEMPERATURE_TARGET: left})
        assert payload == [(1, 104, right)]
Exemple #24
0
def test_get_by_vendor_characteristic_types():
    spray_level = 5

    a = Accessories.from_file("tests/fixtures/vocolinc_flowerbud.json").aid(1)

    service = a.services.first(
        service_type=ServicesTypes.HUMIDIFIER_DEHUMIDIFIER, )

    found = service.has(
        CharacteristicsTypes.VENDOR_VOCOLINC_HUMIDIFIER_SPRAY_LEVEL)
    assert found is True

    char = service.characteristics.first(char_types=[
        CharacteristicsTypes.VENDOR_VOCOLINC_HUMIDIFIER_SPRAY_LEVEL
    ])
    assert char.value == spray_level
Exemple #25
0
def test_build_update_minStep_clamping_ecobee():
    a = Accessories.from_file("tests/fixtures/ecobee3.json").aid(1)
    service = a.services.iid(16)

    assertions = [
        (27.23, 27.2),
        (27.6, 27.6),
        (27.26, 27.3),
        (27.9, 27.9),
        (27.95, 28.0),
    ]

    for left, right in assertions:
        payload = service.build_update(
            {CharacteristicsTypes.TEMPERATURE_TARGET: left})
        assert payload == [(1, 20, right)]
Exemple #26
0
async def test_get_and_set():
    accessories = Accessories.from_file("tests/fixtures/koogeek_ls1.json")
    controller = FakeController()
    device = controller.add_device(accessories)

    discovery = await controller.find_ip_by_device_id(device.device_id)
    finish_pairing = await discovery.start_pairing("alias")
    pairing = await finish_pairing("111-22-333")

    chars = await pairing.get_characteristics([(1, 10)])
    assert chars == {(1, 10): {"value": 0}}

    chars = await pairing.put_characteristics([(1, 10, 1)])
    assert chars == {}

    chars = await pairing.get_characteristics([(1, 10)])
    assert chars == {(1, 10): {"value": 1}}
    async def async_refresh_entity_map(self, config_num: int) -> bool:
        """Handle setup of a HomeKit accessory."""
        try:
            self.accessories = await self.pairing.list_accessories_and_characteristics(
            )
        except AccessoryDisconnectedError:
            # If we fail to refresh this data then we will naturally retry
            # later when Bonjour spots c# is still not up to date.
            return False

        self.entity_map = Accessories.from_list(self.accessories)

        self.hass.data[ENTITY_MAP].async_create_or_update_map(
            self.unique_id, config_num, self.accessories)

        self.config_num = config_num
        self.hass.async_create_task(self.async_process_entity_map())

        return True
Exemple #28
0
def test_process_changes_error():
    accessories = Accessories.from_file("tests/fixtures/koogeek_ls1.json")

    on_char = accessories.aid(1).characteristics.iid(8)
    assert on_char.value is False
    assert on_char.status == HapStatusCode.SUCCESS

    accessories.process_changes({
        (1, 8): {
            "status": HapStatusCode.UNABLE_TO_COMMUNICATE.value
        }
    })

    assert on_char.value is False
    assert on_char.status == HapStatusCode.UNABLE_TO_COMMUNICATE

    accessories.process_changes({(1, 8): {"value": True}})
    assert on_char.value is True
    assert on_char.status == HapStatusCode.SUCCESS
Exemple #29
0
def test_idevices_switch():
    a = Accessories.from_file("tests/fixtures/idevices_switch.json").aid(1)
    assert a.name == "iDevices Switch"
    assert a.model == "IDEV0001"
    assert a.manufacturer == "iDevices LLC"
    assert a.serial_number == "00080685"
    assert a.firmware_revision == "1.2.1"

    service = a.services.first(
        service_type=ServicesTypes.ACCESSORY_INFORMATION)

    char = next(iter(service.characteristics))
    assert char.iid == 2
    assert char.perms == ["pr"]
    assert char.format == "string"
    assert char.value == "iDevices Switch"
    assert char.description == "Name"
    assert char.maxLen == 64

    assert service.has(char.type)
    async def async_setup(self) -> bool:
        """Prepare to use a paired HomeKit device in Home Assistant."""
        cache = self.hass.data[ENTITY_MAP].get_map(self.unique_id)
        if not cache:
            if await self.async_refresh_entity_map(self.config_num):
                self._polling_interval_remover = async_track_time_interval(
                    self.hass, self.async_update, DEFAULT_SCAN_INTERVAL)
                return True
            return False

        self.accessories = cache["accessories"]
        self.config_num = cache["config_num"]

        self.entity_map = Accessories.from_list(self.accessories)

        self._polling_interval_remover = async_track_time_interval(
            self.hass, self.async_update, DEFAULT_SCAN_INTERVAL)

        self.hass.async_create_task(self.async_process_entity_map())

        return True