"""Code to handle the communication with Livisi Smart home controllers.""" from __future__ import annotations from typing import Any import uuid from aiohttp.client import ClientSession from .errors import ( IncorrectIpAddressException, ShcUnreachableException, WrongCredentialException, TokenExpiredException, ) from .const import ( AUTH_GRANT_TYPE, AUTH_PASSWORD, AUTH_USERNAME, AUTHENTICATION_HEADERS, CLASSIC_PORT, LOCATION, CAPABILITY_MAP, CAPABILITY_CONFIG, REQUEST_TIMEOUT, USERNAME, ) ERRORS = {1: Exception} class AioLivisi: """Handles the communication with the Livisi Smart Home controller.""" instance = None def __init__( self, web_session: ClientSession = None, auth_headers: dict[str, Any] = None ) -> None: self._web_session: ClientSession = web_session self._auth_headers: dict[str, Any] = auth_headers self._token: str = "" self._livisi_connection_data: dict[str, str] = None async def async_set_token( self, livisi_connection_data: dict[str, str] = None ) -> None: """Set the JWT from the LIVISI Smart Home Controller.""" access_data: dict = {} try: if self._livisi_connection_data is not None: self._livisi_connection_data = livisi_connection_data access_data = await self.async_get_jwt_token(livisi_connection_data) self.token = access_data["access_token"] self._auth_headers = { "authorization": f"Bearer {self.token}", "Content-type": "application/json", "Accept": "*/*", } except Exception as error: if len(access_data) == 0: raise IncorrectIpAddressException from error elif access_data["errorcode"] == 2009: raise WrongCredentialException from error else: raise ShcUnreachableException from error async def async_send_authorized_request( self, method, url: str, payload=None, ) -> dict: """Make a request to the Livisi Smart Home controller.""" ip_address = self._livisi_connection_data["ip_address"] path = f"http://{ip_address}:{CLASSIC_PORT}/{url}" return await self.async_send_request(method, path, payload, self._auth_headers) async def async_send_unauthorized_request( self, method, url: str, headers, payload=None, ) -> dict: """Send a request without JWT token.""" return await self.async_send_request(method, url, payload, headers) async def async_get_jwt_token(self, livisi_connection_data: dict[str, str]): """Send a request for getting the JWT token.""" login_credentials = { AUTH_USERNAME: USERNAME, AUTH_PASSWORD: livisi_connection_data["password"], AUTH_GRANT_TYPE: "password", } headers = AUTHENTICATION_HEADERS self._livisi_connection_data = livisi_connection_data ip_address = self._livisi_connection_data["ip_address"] return await self.async_send_request( "post", url=f"http://{ip_address}:{CLASSIC_PORT}/auth/token", payload=login_credentials, headers=headers, ) async def async_send_request( self, method, url: str, payload=None, headers=None ) -> dict: """Send a request to the Livisi Smart Home controller.""" try: response = await self.__async_send_request(method, url, payload, headers) except Exception: response = await self.__async_send_request(method, url, payload, headers) if "errorcode" in response: if response["errorcode"] == 2007: raise TokenExpiredException return response async def __async_send_request( self, method, url: str, payload=None, headers=None ) -> dict: async with self._web_session.request( method, url, json=payload, headers=headers, ssl=False, timeout=REQUEST_TIMEOUT, ) as res: data = await res.json() return data async def async_get_controller(self) -> dict[str, Any]: """Get Livisi Smart Home controller data.""" return await self.async_get_controller_status() async def async_get_controller_status(self) -> dict[str, Any]: """Get Livisi Smart Home controller status.""" shc_info = await self.async_send_authorized_request("get", url="status") return shc_info async def async_get_devices( self, ) -> list[dict[str, Any]]: """Send a request for getting the devices.""" devices = await self.async_send_authorized_request("get", url="device") capabilities = await self.async_send_authorized_request("get", url="capability") capability_map = {} capability_config = {} for capability in capabilities: if "device" in capability: device_id = capability["device"].split("/")[-1] if device_id not in capability_map: capability_map[device_id] = {} capability_config[device_id] = {} capability_map[device_id][capability["type"]] = ( "/capability/" + capability["id"] ) if "config" in capability: capability_config[device_id][capability["type"]] = capability["config"] for device in devices: device_id = device["id"] device[CAPABILITY_MAP] = capability_map.get(device_id, {}) device[CAPABILITY_CONFIG] = capability_config.get(device_id, {}) for device in devices.copy(): if LOCATION in device and device.get(LOCATION) is not None: device[LOCATION] = device[LOCATION].removeprefix("/location/") return devices async def async_get_device_state(self, capability) -> dict[str, Any] | None: """Get the state of the device.""" url = f"{capability}/state" try: return await self.async_send_authorized_request("get", url) except Exception: return None async def async_pss_set_state(self, capability_id, is_on: bool) -> dict[str, Any]: """Set the PSS state.""" set_state_payload: dict[str, Any] = { "id": uuid.uuid4().hex, "type": "SetState", "namespace": "core.RWE", "target": capability_id, "params": {"onState": {"type": "Constant", "value": is_on}}, } return await self.async_send_authorized_request( "post", "action", payload=set_state_payload ) async def async_set_onstate(self, capability_id, is_on: bool) -> dict[str, Any]: """Set the onState for devices that support it.""" set_state_payload: dict[str, Any] = { "id": uuid.uuid4().hex, "type": "SetState", "namespace": "core.RWE", "target": capability_id, "params": {"onState": {"type": "Constant", "value": is_on}}, } return await self.async_send_authorized_request( "post", "action", payload=set_state_payload ) async def async_variable_set_value( self, capability_id, value: bool ) -> dict[str, Any]: """Set the boolean variable state.""" set_value_payload: dict[str, Any] = { "id": uuid.uuid4().hex, "type": "SetState", "namespace": "core.RWE", "target": capability_id, "params": {"value": {"type": "Constant", "value": value}}, } return await self.async_send_authorized_request( "post", "action", payload=set_value_payload ) async def async_vrcc_set_temperature( self, capability_id, target_temperature: float, is_avatar: bool ) -> dict[str, Any]: """Set the Virtual Climate Control state.""" if is_avatar: params = "setpointTemperature" else: params = "pointTemperature" set_state_payload: dict[str, Any] = { "id": uuid.uuid4().hex, "type": "SetState", "namespace": "core.RWE", "target": capability_id, "params": {params: {"type": "Constant", "value": target_temperature}}, } return await self.async_send_authorized_request( "post", "action", payload=set_state_payload ) async def async_get_all_rooms(self) -> dict[str, Any]: """Get all the rooms from LIVISI configuration.""" return await self.async_send_authorized_request("get", "location") @property def livisi_connection_data(self): """Return the connection data.""" return self._livisi_connection_data @livisi_connection_data.setter def livisi_connection_data(self, new_value): self._livisi_connection_data = new_value @property def token(self): """Return the token.""" return self._token @token.setter def token(self, new_value): self._token = new_value @property def web_session(self): """Return the web session.""" return self._web_session @web_session.setter def web_session(self, new_value): self._web_session = new_value