"""Python library to connect deCONZ and Home Assistant to work together.""" from collections.abc import Callable import logging from typing import Any, Final, TypedDict import aiohttp from .errors import RequestError, ResponseError, raise_error LOGGER = logging.getLogger(__name__) URL_DISCOVER: Final = "https://phoscon.de/discover" class DiscoveredBridge(TypedDict): """Discovered bridge type.""" id: str host: str mac: str name: str port: int async def delete_api_key( session: aiohttp.ClientSession, host: str, port: int, api_key: str ) -> None: """Delete API key from deCONZ.""" url = f"http://{host}:{port}/api/{api_key}/config/whitelist/{api_key}" response = await request(session.delete, url) LOGGER.info(response) async def delete_all_keys( session: aiohttp.ClientSession, host: str, port: int, api_key: str, api_keys: list[str] = [], ) -> None: """Delete all API keys except for the ones provided to the method.""" url = f"http://{host}:{port}/api/{api_key}/config" response = await request(session.get, url) api_keys.append(api_key) for key in response["whitelist"]: if key not in api_keys: await delete_api_key(session, host, port, key) async def get_bridge_id( session: aiohttp.ClientSession, host: str, port: int, api_key: str ) -> str: """Get bridge id for bridge.""" url = f"http://{host}:{port}/api/{api_key}/config" response = await request(session.get, url) bridge_id = normalize_bridge_id(response["bridgeid"]) LOGGER.info("Bridge id: %s", bridge_id) return bridge_id async def discovery(session: aiohttp.ClientSession) -> list[DiscoveredBridge]: """Find bridges allowing gateway discovery.""" response: list[dict[str, Any]] = await request(session.get, URL_DISCOVER) LOGGER.info("Discovered the following bridges: %s.", response) return [ DiscoveredBridge( { "id": normalize_bridge_id(bridge["id"]), "host": bridge["internalipaddress"], "port": bridge["internalport"], "mac": bridge.get("macaddress", ""), "name": bridge.get("name", ""), } ) for bridge in response ] async def request( session: Callable[[Any], Any], url: str, **kwargs: Any, ) -> Any: """Do a web request and manage response.""" LOGGER.debug("Sending %s to %s", kwargs, url) try: res = await session(url, **kwargs) if res.content_type != "application/json": raise ResponseError(f"Invalid content type: {res.content_type}") response = await res.json() LOGGER.debug("HTTP request response: %s", response) _raise_on_error(response) return response except aiohttp.client_exceptions.ClientError as err: raise RequestError(f"Error requesting data from {url}: {err}") from None def _raise_on_error(data: list[dict[str, Any]] | dict[str, Any]) -> None: """Check response for error message.""" if isinstance(data, list) and data: data = data[0] if isinstance(data, dict) and "error" in data: raise_error(data["error"]) def normalize_bridge_id(bridge_id: str) -> str: """Normalize a bridge identifier.""" bridge_id = bridge_id.upper() # discovery: contains 4 extra characters in the middle: "FFFF" if len(bridge_id) == 16 and bridge_id[6:10] == "FFFF": return bridge_id[0:6] + bridge_id[-6:] # deCONZ config API contains right ID. if len(bridge_id) == 12: return bridge_id LOGGER.warning("Received unexpected bridge id: %s", bridge_id) return bridge_id