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 }}) ]
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
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()
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", }
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)
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
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"
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
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
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)
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, ), ]
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)
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
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
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}})]
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, ), ]
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 == []
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")]
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()
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
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}}
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)]
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
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)]
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
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
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