async def test_discovery_ignored_hk_bridge(opp, controller): """Ensure we ignore homekit bridges and accessories created by the homekit integration.""" device = setup_mock_accessory(controller) discovery_info = get_device_discovery_info(device) config_entry = MockConfigEntry(domain=config_flow.HOMEKIT_BRIDGE_DOMAIN, data={}) config_entry.add_to_opp(opp) formatted_mac = device_registry.format_mac("AA:BB:CC:DD:EE:FF") dev_reg = mock_device_registry(opp) dev_reg.async_get_or_create( config_entry_id=config_entry.entry_id, connections={(device_registry.CONNECTION_NETWORK_MAC, formatted_mac)}, ) discovery_info["properties"]["id"] = "AA:BB:CC:DD:EE:FF" # Device is discovered result = await opp.config_entries.flow.async_init( "homekit_controller", context={"source": config_entries.SOURCE_ZEROCONF}, data=discovery_info, ) assert result["type"] == "abort" assert result["reason"] == "ignored_model"
async def async_step_ssdp(self, discovery_info): """Handle a discovered UniFi device.""" parsed_url = urlparse(discovery_info[ssdp.ATTR_SSDP_LOCATION]) model_description = discovery_info[ssdp.ATTR_UPNP_MODEL_DESCRIPTION] mac_address = format_mac(discovery_info[ssdp.ATTR_UPNP_SERIAL]) self.config = { CONF_HOST: parsed_url.hostname, } self._async_abort_entries_match({CONF_HOST: self.config[CONF_HOST]}) await self.async_set_unique_id(mac_address) self._abort_if_unique_id_configured(updates=self.config) self.context["title_placeholders"] = { CONF_HOST: self.config[CONF_HOST], CONF_SITE_ID: DEFAULT_SITE_ID, } port = MODEL_PORTS.get(model_description) if port is not None: self.config[CONF_PORT] = port return await self.async_step_user()
def __init__(self, device, device_type, xiaomi_hub, config_entry): """Initialize the Xiaomi device.""" self._state = None self._is_available = True self._sid = device["sid"] self._model = device["model"] self._protocol = device["proto"] self._name = f"{device_type}_{self._sid}" self._device_name = f"{self._model}_{self._sid}" self._type = device_type self._write_to_hub = xiaomi_hub.write_to_hub self._get_from_hub = xiaomi_hub.get_from_hub self._extra_state_attributes = {} self._remove_unavailability_tracker = None self._xiaomi_hub = xiaomi_hub self.parse_data(device["data"], device["raw_data"]) self.parse_voltage(device["data"]) if hasattr(self, "_data_key") and self._data_key: # pylint: disable=no-member self._unique_id = ( f"{self._data_key}{self._sid}" # pylint: disable=no-member ) else: self._unique_id = f"{self._type}{self._sid}" self._gateway_id = config_entry.unique_id if config_entry.data[CONF_MAC] == format_mac(self._sid): # this entity belongs to the gateway itself self._is_gateway = True self._device_id = config_entry.unique_id else: # this entity is connected through zigbee self._is_gateway = False self._device_id = self._sid
def _async_register_bridge(self): """Register the bridge as a device so homekit_controller and exclude it from discovery.""" dev_reg = device_registry.async_get(self.opp) formatted_mac = device_registry.format_mac(self.driver.state.mac) # Connections and identifiers are both used here. # # connections exists so homekit_controller can know the # virtual mac address of the bridge and know to not offer # it via discovery. # # identifiers is used as well since the virtual mac may change # because it will not survive manual pairing resets (deleting state file) # which we have trained users to do over the past few years # because this was the way you had to fix homekit when pairing # failed. # connection = (device_registry.CONNECTION_NETWORK_MAC, formatted_mac) identifier = (DOMAIN, self._entry_id, BRIDGE_SERIAL_NUMBER) self._async_purge_old_bridges(dev_reg, identifier, connection) is_accessory_mode = self._homekit_mode == HOMEKIT_MODE_ACCESSORY hk_mode_name = "Accessory" if is_accessory_mode else "Bridge" dev_reg.async_get_or_create( config_entry_id=self._entry_id, identifiers={identifier}, connections={connection}, manufacturer=MANUFACTURER, name=accessory_friendly_name(self._entry_title, self.driver.accessory), model=f"HomeKit {hk_mode_name}", entry_type="service", )
async def async_migrate_entry(opp, config_entry): """Migrate old entry.""" _LOGGER.debug("Migrating from version %s", config_entry.version) # Flatten configuration but keep old data if user rollbacks OPP prior to 0.106 if config_entry.version == 1: unique_id = config_entry.data[CONF_MAC] data = {**config_entry.data, **config_entry.data[CONF_DEVICE]} opp.config_entries.async_update_entry( config_entry, unique_id=unique_id, data=data ) config_entry.version = 2 # Normalise MAC address of device which also affects entity unique IDs if config_entry.version == 2: old_unique_id = config_entry.unique_id new_unique_id = format_mac(old_unique_id) @callback def update_unique_id(entity_entry): """Update unique ID of entity entry.""" return { "new_unique_id": entity_entry.unique_id.replace( old_unique_id, new_unique_id ) } if old_unique_id != new_unique_id: await async_migrate_entries(opp, config_entry.entry_id, update_unique_id) opp.config_entries.async_update_entry(config_entry, unique_id=new_unique_id) _LOGGER.info("Migration to version %s successful", config_entry.version) return True
async def async_step_user(self, user_input=None): """Handle a flow initiated by the user.""" errors = {} if user_input is not None: host = user_input[CONF_HOST] name = user_input[CONF_NAME] self._async_abort_entries_match({CONF_HOST: host}) mac_address, error = await self._async_try_connect(host) if error is None: await self.async_set_unique_id(format_mac(mac_address)) self._abort_if_unique_id_configured(updates={CONF_HOST: host}) return self.async_create_entry( title=name, data={CONF_HOST: host, CONF_NAME: name}, ) errors["base"] = error user_input = user_input or {} return self.async_show_form( step_id="user", data_schema=vol.Schema( { vol.Required( CONF_HOST, default=user_input.get(CONF_HOST) or "" ): str, vol.Optional( CONF_NAME, default=user_input.get(CONF_NAME) or DEFAULT_NAME ): str, } ), errors=errors, )
async def async_step_dhcp(self, discovery_info): """Handle dhcp discovery.""" self._async_abort_entries_match( {CONF_HOST: discovery_info[IP_ADDRESS]}) formatted_mac = format_mac(discovery_info[MAC_ADDRESS]) await self.async_set_unique_id(format_mac(formatted_mac)) self._abort_if_unique_id_configured( updates={CONF_HOST: discovery_info[IP_ADDRESS]}) self.host = discovery_info[HOSTNAME] self.mac = formatted_mac self.ip_address = discovery_info[IP_ADDRESS] self.context["title_placeholders"] = { "ip": self.ip_address, "mac": self.mac } return await self.async_step_user()
async def async_step_user(self, user_input=None): """Handle a Axis config flow start. Manage device specific parameters. """ errors = {} if user_input is not None: try: device = await get_device( self.opp, host=user_input[CONF_HOST], port=user_input[CONF_PORT], username=user_input[CONF_USERNAME], password=user_input[CONF_PASSWORD], ) self.serial = device.vapix.serial_number await self.async_set_unique_id(format_mac(self.serial)) self._abort_if_unique_id_configured( updates={ CONF_HOST: user_input[CONF_HOST], CONF_PORT: user_input[CONF_PORT], CONF_USERNAME: user_input[CONF_USERNAME], CONF_PASSWORD: user_input[CONF_PASSWORD], } ) self.device_config = { CONF_HOST: user_input[CONF_HOST], CONF_PORT: user_input[CONF_PORT], CONF_USERNAME: user_input[CONF_USERNAME], CONF_PASSWORD: user_input[CONF_PASSWORD], CONF_MODEL: device.vapix.product_number, } return await self._create_entry() except AuthenticationRequired: errors["base"] = "invalid_auth" except CannotConnect: errors["base"] = "cannot_connect" data = self.discovery_schema or { vol.Required(CONF_HOST): str, vol.Required(CONF_USERNAME): str, vol.Required(CONF_PASSWORD): str, vol.Required(CONF_PORT, default=DEFAULT_PORT): int, } return self.async_show_form( step_id="user", description_placeholders=self.device_config, data_schema=vol.Schema(data), errors=errors, )
async def async_step_zeroconf(self, discovery_info: DiscoveryInfoType): """Handle a flow initialized by zeroconf discovery.""" LOGGER.debug("Samsung device found via ZEROCONF: %s", discovery_info) self._mac = format_mac(discovery_info[ATTR_PROPERTIES]["deviceid"]) self._host = discovery_info[CONF_HOST] await self._async_start_discovery() await self._async_set_device_unique_id() self.context["title_placeholders"] = {"device": self._title} return await self.async_step_confirm()
async def async_step_dhcp(self, discovery_info: dict): """Prepare configuration for a DHCP discovered Axis device.""" return await self._process_discovered_device( { CONF_HOST: discovery_info[IP_ADDRESS], CONF_MAC: format_mac(discovery_info.get(MAC_ADDRESS, "")), CONF_NAME: discovery_info.get(HOSTNAME), CONF_PORT: DEFAULT_PORT, } )
async def async_step_zeroconf(self, discovery_info: dict): """Prepare configuration for a Zeroconf discovered Axis device.""" return await self._process_discovered_device( { CONF_HOST: discovery_info[CONF_HOST], CONF_MAC: format_mac(discovery_info["properties"]["macaddress"]), CONF_NAME: discovery_info["name"].split(".", 1)[0], CONF_PORT: discovery_info[CONF_PORT], } )
async def async_step_ssdp(self, discovery_info: dict): """Prepare configuration for a SSDP discovered Axis device.""" url = urlsplit(discovery_info["presentationURL"]) return await self._process_discovered_device( { CONF_HOST: url.hostname, CONF_MAC: format_mac(discovery_info["serialNumber"]), CONF_NAME: f"{discovery_info['friendlyName']}", CONF_PORT: url.port, } )
async def async_setup_entry(opp: OpenPeerPower, entry: ConfigEntry) -> bool: """Set up Bosch SHC from a config entry.""" data = entry.data zeroconf = await async_get_instance(opp) try: session = await opp.async_add_executor_job( SHCSession, data[CONF_HOST], data[CONF_SSL_CERTIFICATE], data[CONF_SSL_KEY], False, zeroconf, ) except SHCAuthenticationError as err: raise ConfigEntryAuthFailed from err except SHCConnectionError as err: raise ConfigEntryNotReady from err shc_info = session.information if shc_info.updateState.name == "UPDATE_AVAILABLE": _LOGGER.warning( "Please check for software updates in the Bosch Smart Home App") opp.data.setdefault(DOMAIN, {}) opp.data[DOMAIN][entry.entry_id] = { DATA_SESSION: session, } device_registry = dr.async_get(opp) device_registry.async_get_or_create( config_entry_id=entry.entry_id, connections={(dr.CONNECTION_NETWORK_MAC, dr.format_mac(shc_info.unique_id))}, identifiers={(DOMAIN, shc_info.unique_id)}, manufacturer="Bosch", name=entry.title, model="SmartHomeController", sw_version=shc_info.version, ) opp.config_entries.async_setup_platforms(entry, PLATFORMS) async def stop_polling(event): """Stop polling service.""" await opp.async_add_executor_job(session.stop_polling) await opp.async_add_executor_job(session.start_polling) opp.data[DOMAIN][ entry.entry_id][DATA_POLLING_HANDLER] = opp.bus.async_listen_once( EVENT_OPENPEERPOWER_STOP, stop_polling) return True
async def async_step_zeroconf(self, discovery_info): """Handle zeroconf discovery.""" name = discovery_info.get("name") self.host = discovery_info.get("host") self.mac = discovery_info.get("properties", {}).get("mac") if self.mac is None: poch = discovery_info.get("properties", {}).get("poch", "") result = search(r"mac=\w+", poch) if result is not None: self.mac = result.group(0).split("=")[1] if not name or not self.host or not self.mac: return self.async_abort(reason="not_xiaomi_miio") self.mac = format_mac(self.mac) # Check which device is discovered. for gateway_model in MODELS_GATEWAY: if name.startswith(gateway_model.replace(".", "-")): unique_id = self.mac await self.async_set_unique_id(unique_id) self._abort_if_unique_id_configured({CONF_HOST: self.host}) self.context.update( {"title_placeholders": { "name": f"Gateway {self.host}" }}) return await self.async_step_device() for device_model in MODELS_ALL_DEVICES: if name.startswith(device_model.replace(".", "-")): unique_id = self.mac await self.async_set_unique_id(unique_id) self._abort_if_unique_id_configured({CONF_HOST: self.host}) self.context.update({ "title_placeholders": { "name": f"{device_model} {self.host}" } }) return await self.async_step_device() # Discovered device is not yet supported _LOGGER.debug( "Not yet supported Xiaomi Miio device '%s' discovered with host %s", name, self.host, ) return self.async_abort(reason="not_xiaomi_miio")
async def _async_get_and_check_device_info(self): """Try to get the device info.""" info = await async_get_device_info(self.opp, self._bridge, self._host) if not info: raise data_entry_flow.AbortFlow(RESULT_NOT_SUPPORTED) dev_info = info.get("device", {}) device_type = dev_info.get("type") if device_type != "Samsung SmartTV": raise data_entry_flow.AbortFlow(RESULT_NOT_SUPPORTED) self._model = dev_info.get("modelName") name = dev_info.get("name") self._name = name.replace("[TV] ", "") if name else device_type self._title = f"{self._name} ({self._model})" self._udn = _strip_uuid(dev_info.get("udn", info["id"])) if dev_info.get("networkType") == "wireless" and dev_info.get( "wifiMac"): self._mac = format_mac(dev_info.get("wifiMac")) self._device_info = info
async def async_step_user(self, user_input=None): """Handle the initial step.""" errors = {} if user_input is not None: hub = Control4Validator( user_input["host"], user_input["username"], user_input["password"], self.opp, ) try: if not await hub.authenticate(): raise InvalidAuth if not await hub.connect_to_director(): raise CannotConnect except InvalidAuth: errors["base"] = "invalid_auth" except CannotConnect: errors["base"] = "cannot_connect" except Exception: # pylint: disable=broad-except _LOGGER.exception("Unexpected exception") errors["base"] = "unknown" if not errors: controller_unique_id = hub.controller_unique_id mac = (controller_unique_id.split("_", 3))[2] formatted_mac = format_mac(mac) await self.async_set_unique_id(formatted_mac) self._abort_if_unique_id_configured() return self.async_create_entry( title=controller_unique_id, data={ CONF_HOST: user_input["host"], CONF_USERNAME: user_input["username"], CONF_PASSWORD: user_input["password"], CONF_CONTROLLER_UNIQUE_ID: controller_unique_id, }, ) return self.async_show_form( step_id="user", data_schema=DATA_SCHEMA, errors=errors )
async def test_dhcp_device_not_supported(opp): """Test DHCP discovery flow that fails because the device is not supported.""" await setup.async_setup_component(opp, "persistent_notification", {}) device = get_device("Kitchen") mock_api = device.get_mock_api() with patch(DEVICE_HELLO, return_value=mock_api): result = await opp.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_DHCP}, data={ HOSTNAME: "broadlink", IP_ADDRESS: device.host, MAC_ADDRESS: device_registry.format_mac(device.mac), }, ) assert result["type"] == "abort" assert result["reason"] == "not_supported"
def __init__( self, opp, name, host, mac_address, off_action, broadcast_address, broadcast_port, ): """Initialize the WOL switch.""" self._opp = opp self._name = name self._host = host self._mac_address = mac_address self._broadcast_address = broadcast_address self._broadcast_port = broadcast_port domain = __name__.split(".")[-2] self._off_script = Script(opp, off_action, name, domain) if off_action else None self._state = False self._assumed_state = host is None self._unique_id = dr.format_mac(mac_address)
async def test_dhcp_can_finish(opp): """Test DHCP discovery flow can finish right away.""" await setup.async_setup_component(opp, "persistent_notification", {}) device = get_device("Living Room") device.host = "1.2.3.4" mock_api = device.get_mock_api() with patch(DEVICE_HELLO, return_value=mock_api): result = await opp.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_DHCP}, data={ HOSTNAME: "broadlink", IP_ADDRESS: "1.2.3.4", MAC_ADDRESS: device_registry.format_mac(device.mac), }, ) await opp.async_block_till_done() assert result["type"] == "form" assert result["step_id"] == "finish" result2 = await opp.config_entries.flow.async_configure( result["flow_id"], {}, ) await opp.async_block_till_done() assert result2["type"] == "create_entry" assert result2["title"] == "Living Room" assert result2["data"] == { "host": "1.2.3.4", "mac": "34ea34b43b5a", "timeout": 10, "type": 24374, }
async def test_discovery_does_not_ignore_non_homekit(opp, controller): """Do not ignore devices that are not from the homekit integration.""" device = setup_mock_accessory(controller) discovery_info = get_device_discovery_info(device) config_entry = MockConfigEntry(domain="not_homekit", data={}) config_entry.add_to_opp(opp) formatted_mac = device_registry.format_mac("AA:BB:CC:DD:EE:FF") dev_reg = mock_device_registry(opp) dev_reg.async_get_or_create( config_entry_id=config_entry.entry_id, connections={(device_registry.CONNECTION_NETWORK_MAC, formatted_mac)}, ) discovery_info["properties"]["id"] = "AA:BB:CC:DD:EE:FF" # Device is discovered result = await opp.config_entries.flow.async_init( "homekit_controller", context={"source": config_entries.SOURCE_ZEROCONF}, data=discovery_info, ) assert result["type"] == "form"
def _format_mac(mac_address): """Format a mac address for matching.""" return format_mac(mac_address).replace(":", "")
async def test_setup_entry(opp): """Test successful setup of entry.""" await setup_axis_integration(opp) assert len(opp.data[AXIS_DOMAIN]) == 1 assert format_mac(MAC) in opp.data[AXIS_DOMAIN]
async def test_homekit_start(opp, hk_driver, mock_zeroconf, device_reg): """Test HomeKit start method.""" entry = await async_init_integration(opp) homekit = _mock_homekit(opp, entry, HOMEKIT_MODE_BRIDGE) homekit.bridge = Mock() homekit.bridge.accessories = [] homekit.driver = hk_driver acc = Accessory(hk_driver, "any") homekit.driver.accessory = acc connection = (device_registry.CONNECTION_NETWORK_MAC, "AA:BB:CC:DD:EE:FF") bridge_with_wrong_mac = device_reg.async_get_or_create( config_entry_id=entry.entry_id, connections={connection}, manufacturer="Any", name="Any", model="Open Peer Power HomeKit Bridge", ) opp.states.async_set("light.demo", "on") opp.states.async_set("light.demo2", "on") state = opp.states.async_all()[0] with patch( f"{PATH_HOMEKIT}.HomeKit.add_bridge_accessory" ) as mock_add_acc, patch( f"{PATH_HOMEKIT}.show_setup_message") as mock_setup_msg, patch( "pyhap.accessory_driver.AccessoryDriver.async_start" ) as hk_driver_start: await homekit.async_start() await opp.async_block_till_done() mock_add_acc.assert_any_call(state) mock_setup_msg.assert_called_with(opp, entry.entry_id, "Mock Title (Open Peer Power Bridge)", ANY, ANY) assert hk_driver_start.called assert homekit.status == STATUS_RUNNING # Test start() if already started hk_driver_start.reset_mock() await homekit.async_start() await opp.async_block_till_done() assert not hk_driver_start.called assert device_reg.async_get(bridge_with_wrong_mac.id) is None device = device_reg.async_get_device({(DOMAIN, entry.entry_id, BRIDGE_SERIAL_NUMBER)}) assert device formatted_mac = device_registry.format_mac(homekit.driver.state.mac) assert (device_registry.CONNECTION_NETWORK_MAC, formatted_mac) in device.connections # Start again to make sure the registry entry is kept homekit.status = STATUS_READY with patch( f"{PATH_HOMEKIT}.HomeKit.add_bridge_accessory" ) as mock_add_acc, patch( f"{PATH_HOMEKIT}.show_setup_message") as mock_setup_msg, patch( "pyhap.accessory_driver.AccessoryDriver.async_start" ) as hk_driver_start: await homekit.async_start() device = device_reg.async_get_device({(DOMAIN, entry.entry_id, BRIDGE_SERIAL_NUMBER)}) assert device formatted_mac = device_registry.format_mac(homekit.driver.state.mac) assert (device_registry.CONNECTION_NETWORK_MAC, formatted_mac) in device.connections assert len(device_reg.devices) == 1 assert homekit.driver.state.config_version == 2
async def async_step_device(self, user_input=None): """Handle a flow initialized by the user to configure a xiaomi miio device.""" errors = {} if user_input is not None: token = user_input[CONF_TOKEN] model = user_input.get(CONF_MODEL) if user_input.get(CONF_HOST): self.host = user_input[CONF_HOST] # Try to connect to a Xiaomi Device. connect_device_class = ConnectXiaomiDevice(self.opp) await connect_device_class.async_connect_device(self.host, token) device_info = connect_device_class.device_info if model is None and device_info is not None: model = device_info.model if model is not None: if self.mac is None and device_info is not None: self.mac = format_mac(device_info.mac_address) # Setup Gateways for gateway_model in MODELS_GATEWAY: if model.startswith(gateway_model): unique_id = self.mac await self.async_set_unique_id(unique_id, raise_on_progress=False) self._abort_if_unique_id_configured() return self.async_create_entry( title=DEFAULT_GATEWAY_NAME, data={ CONF_FLOW_TYPE: CONF_GATEWAY, CONF_HOST: self.host, CONF_TOKEN: token, CONF_MODEL: model, CONF_MAC: self.mac, }, ) # Setup all other Miio Devices name = user_input.get(CONF_NAME, model) for device_model in MODELS_ALL_DEVICES: if model.startswith(device_model): unique_id = self.mac await self.async_set_unique_id(unique_id, raise_on_progress=False) self._abort_if_unique_id_configured() return self.async_create_entry( title=name, data={ CONF_FLOW_TYPE: CONF_DEVICE, CONF_HOST: self.host, CONF_TOKEN: token, CONF_MODEL: model, CONF_MAC: self.mac, }, ) errors["base"] = "unknown_device" else: errors["base"] = "cannot_connect" if self.host: schema = vol.Schema(DEVICE_SETTINGS) else: schema = DEVICE_CONFIG if errors: schema = schema.extend(DEVICE_MODEL_CONFIG) return self.async_show_form(step_id="device", data_schema=schema, errors=errors)