def test_clamp_enum_valid_vals(): a = Accessory() tv_service = a.add_service(service_type=ServicesTypes.TELEVISION) char = tv_service.add_char( CharacteristicsTypes.REMOTE_KEY, valid_values=[RemoteKeyValues.PLAY_PAUSE], min_value=None, max_value=None, ) valid_vals = clamp_enum_to_char(RemoteKeyValues, char) assert valid_vals == {RemoteKeyValues.PLAY_PAUSE}
def test_clamp_enum_min_max_single_press(): a = Accessory() tv_service = a.add_service(service_type=ServicesTypes.STATELESS_PROGRAMMABLE_SWITCH) char = tv_service.add_char( CharacteristicsTypes.INPUT_EVENT, valid_values=None, min_value=InputEventValues.SINGLE_PRESS, max_value=InputEventValues.SINGLE_PRESS, ) valid_vals = clamp_enum_to_char(InputEventValues, char) assert valid_vals == {InputEventValues.SINGLE_PRESS}
def test_clamp_enum_min_max_unclamped_button_press(): a = Accessory() tv_service = a.add_service(service_type=ServicesTypes.STATELESS_PROGRAMMABLE_SWITCH) char = tv_service.add_char( CharacteristicsTypes.INPUT_EVENT, ) valid_vals = clamp_enum_to_char(InputEventValues, char) assert valid_vals == { InputEventValues.SINGLE_PRESS, InputEventValues.DOUBLE_PRESS, InputEventValues.LONG_PRESS, }
async def setup_test_component(hass, setup_accessory, capitalize=False, suffix=None): """Load a fake homekit accessory based on a homekit accessory model. If capitalize is True, property names will be in upper case. If suffix is set, entityId will include the suffix """ accessory = Accessory.create_with_info("TestDevice", "example.com", "Test", "0001", "0.1") setup_accessory(accessory) domain = None for service in accessory.services: service_name = ServicesTypes.get_short(service.type) if service_name in HOMEKIT_ACCESSORY_DISPATCH: domain = HOMEKIT_ACCESSORY_DISPATCH[service_name] break assert domain, "Cannot map test homekit services to Home Assistant domain" config_entry, pairing = await setup_test_accessories(hass, [accessory]) entity = "testdevice" if suffix is None else f"testdevice_{suffix}" return Helper(hass, ".".join((domain, entity)), pairing, accessory, config_entry)
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 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", }
async def test_ble_device_only_checks_is_available(hass, controller): """Test a BLE device only checks is_available.""" is_available = False class FakeBLEPairing(FakePairing): """Fake BLE pairing that can flip is_available.""" @property def transport(self): return Transport.BLE @property def is_connected(self): return False @property def is_available(self): nonlocal is_available return is_available async def async_populate_accessories_state(self, *args, **kwargs): nonlocal is_available if not is_available: raise AccessoryNotFoundError("any") await super().async_populate_accessories_state(*args, **kwargs) async def get_characteristics(self, chars, *args, **kwargs): nonlocal is_available if not is_available: raise AccessoryNotFoundError("any") return {} accessory = Accessory.create_with_info("TestDevice", "example.com", "Test", "0001", "0.1") create_alive_service(accessory) with patch("aiohomekit.testing.FakePairing", FakeBLEPairing): await async_setup_component(hass, DOMAIN, {}) config_entry, _ = await setup_test_accessories_with_controller( hass, [accessory], controller) await hass.async_block_till_done() assert config_entry.state == ConfigEntryState.SETUP_RETRY is_available = True async_fire_time_changed(hass, utcnow() + timedelta(seconds=10)) await hass.async_block_till_done() assert config_entry.state == ConfigEntryState.LOADED assert hass.states.get("light.testdevice").state == STATE_OFF is_available = False async_fire_time_changed(hass, utcnow() + timedelta(hours=1)) assert hass.states.get("light.testdevice").state == STATE_UNAVAILABLE is_available = True async_fire_time_changed(hass, utcnow() + timedelta(hours=1)) assert hass.states.get("light.testdevice").state == STATE_OFF
async def controller_and_unpaired_accessory(request, loop): config_file = tempfile.NamedTemporaryFile() config_file.write("""{ "accessory_ltpk": "7986cf939de8986f428744e36ed72d86189bea46b4dcdc8d9d79a3e4fceb92b9", "accessory_ltsk": "3d99f3e959a1f93af4056966f858074b2a1fdec1c5fd84a51ea96f9fa004156a", "accessory_pairing_id": "12:34:56:00:01:0A", "accessory_pin": "031-45-154", "c#": 1, "category": "Lightbulb", "host_ip": "127.0.0.1", "host_port": 51842, "name": "unittestLight", "unsuccessful_tries": 0 }""".encode()) config_file.flush() # Make sure get_id() numbers are stable between tests model_mixin.id_counter = 0 httpd = AccessoryServer(config_file.name, None) accessory = Accessory.create_with_info("Testlicht", "lusiardi.de", "Demoserver", "0001", "0.1") lightBulbService = accessory.add_service(ServicesTypes.LIGHTBULB) lightBulbService.add_char(CharacteristicsTypes.ON, value=False) httpd.add_accessory(accessory) t = threading.Thread(target=httpd.serve_forever) t.start() controller = Controller() for i in range(10): if port_ready(51842): break time.sleep(1) with mock.patch("aiohomekit.zeroconf._find_data_for_device_id") as find: find.return_value = { "address": "127.0.0.1", "port": 51842, "id": "12:34:56:00:01:0A", } with mock.patch.object(controller, "load_data", lambda x: None): with mock.patch("aiohomekit.__main__.Controller") as c: c.return_value = controller yield controller try: await asyncio.shield(controller.shutdown()) except asyncio.CancelledError: pass httpd.shutdown() t.join()
def create_service_with_ecobee_mode(accessory: Accessory): """Define a thermostat with ecobee mode characteristics.""" service = accessory.add_service(ServicesTypes.THERMOSTAT, add_required=True) current_mode = service.add_char( CharacteristicsTypes.VENDOR_ECOBEE_CURRENT_MODE) current_mode.value = 0 current_mode.perms.append("ev") service.add_char(CharacteristicsTypes.VENDOR_ECOBEE_SET_HOLD_SCHEDULE) return service
async def test_offline_device_raises(hass, controller): """Test an offline device raises ConfigEntryNotReady.""" is_connected = False class OfflineFakePairing(FakePairing): """Fake pairing that can flip is_connected.""" @property def is_connected(self): nonlocal is_connected return is_connected @property def is_available(self): return self.is_connected async def async_populate_accessories_state(self, *args, **kwargs): nonlocal is_connected if not is_connected: raise AccessoryNotFoundError("any") await super().async_populate_accessories_state(*args, **kwargs) async def get_characteristics(self, chars, *args, **kwargs): nonlocal is_connected if not is_connected: raise AccessoryNotFoundError("any") return {} accessory = Accessory.create_with_info("TestDevice", "example.com", "Test", "0001", "0.1") create_alive_service(accessory) with patch("aiohomekit.testing.FakePairing", OfflineFakePairing): await async_setup_component(hass, DOMAIN, {}) config_entry, _ = await setup_test_accessories_with_controller( hass, [accessory], controller) await hass.async_block_till_done() assert config_entry.state == ConfigEntryState.SETUP_RETRY is_connected = True async_fire_time_changed(hass, utcnow() + timedelta(seconds=10)) await hass.async_block_till_done() assert config_entry.state == ConfigEntryState.LOADED assert hass.states.get("light.testdevice").state == STATE_OFF
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 __init__( self, accessory: Accessory, service_type: str, name: str | None = None, add_required: bool = False, ): self.type = normalize_uuid(service_type) self.accessory = accessory self.iid = accessory.get_next_id() self.characteristics = Characteristics() self.characteristics_by_type = {} self.linked = [] if name: char = self.add_char(CharacteristicsTypes.NAME) char.set_value(name) if add_required: for required in services[self.type]["required"]: if required not in self.characteristics_by_type: self.add_char(required)
async def controller_and_paired_accessory(request, loop): config_file = tempfile.NamedTemporaryFile() config_file.write("""{ "accessory_ltpk": "7986cf939de8986f428744e36ed72d86189bea46b4dcdc8d9d79a3e4fceb92b9", "accessory_ltsk": "3d99f3e959a1f93af4056966f858074b2a1fdec1c5fd84a51ea96f9fa004156a", "accessory_pairing_id": "12:34:56:00:01:0A", "accessory_pin": "031-45-154", "c#": 1, "category": "Lightbulb", "host_ip": "127.0.0.1", "host_port": 51842, "name": "unittestLight", "peers": { "decc6fa3-de3e-41c9-adba-ef7409821bfc": { "admin": true, "key": "d708df2fbf4a8779669f0ccd43f4962d6d49e4274f88b1292f822edc3bcf8ed8" } }, "unsuccessful_tries": 0 }""".encode()) config_file.flush() # Make sure get_id() numbers are stable between tests model_mixin.id_counter = 0 httpd = AccessoryServer(config_file.name, None) accessory = Accessory.create_with_info("Testlicht", "lusiardi.de", "Demoserver", "0001", "0.1") lightBulbService = accessory.add_service(ServicesTypes.LIGHTBULB) lightBulbService.add_char(CharacteristicsTypes.ON, value=False) httpd.add_accessory(accessory) t = threading.Thread(target=httpd.serve_forever) t.start() controller_file = tempfile.NamedTemporaryFile() controller_file.write("""{ "alias": { "Connection": "IP", "iOSDeviceLTPK": "d708df2fbf4a8779669f0ccd43f4962d6d49e4274f88b1292f822edc3bcf8ed8", "iOSPairingId": "decc6fa3-de3e-41c9-adba-ef7409821bfc", "AccessoryLTPK": "7986cf939de8986f428744e36ed72d86189bea46b4dcdc8d9d79a3e4fceb92b9", "AccessoryPairingID": "12:34:56:00:01:0A", "AccessoryPort": 51842, "AccessoryIP": "127.0.0.1", "iOSDeviceLTSK": "fa45f082ef87efc6c8c8d043d74084a3ea923a2253e323a7eb9917b4090c2fcc" } }""".encode()) controller_file.flush() controller = Controller() controller.load_data(controller_file.name) config_file.close() for i in range(10): if port_ready(51842): break time.sleep(1) with mock.patch("aiohomekit.zeroconf._find_data_for_device_id") as find: find.return_value = { "address": "127.0.0.1", "port": 51842, "id": "12:34:56:00:01:0A", } with mock.patch.object(controller, "load_data", lambda x: None): with mock.patch("aiohomekit.__main__.Controller") as c: c.return_value = controller yield controller try: await asyncio.shield(controller.shutdown()) except asyncio.CancelledError: pass httpd.shutdown() t.join()