From 928b6ccfb98682477a6740a3fe579700e0533722 Mon Sep 17 00:00:00 2001 From: Philipp Piwo Date: Sun, 7 Apr 2024 20:15:31 +0200 Subject: [PATCH] add infloor valve, blind and sensor entities, adjustments for oauth handling of different client ids (#220) --- .gitignore | 1 + iolite_client/client.py | 11 +++--- iolite_client/entity.py | 44 +++++++++++++++++++++++- iolite_client/entity_factory.py | 59 ++++++++++++++++++++++++++------ iolite_client/oauth_handler.py | 22 ++++++------ iolite_client/request_handler.py | 6 ++-- scripts/example.py | 21 +++++++++--- 7 files changed, 131 insertions(+), 33 deletions(-) diff --git a/.gitignore b/.gitignore index 94923b8..07c43ae 100644 --- a/.gitignore +++ b/.gitignore @@ -5,6 +5,7 @@ # Secrets /.env access_token.json +access_token_*.json # Cache __pycache__ diff --git a/iolite_client/client.py b/iolite_client/client.py index 8790d59..86fea2e 100755 --- a/iolite_client/client.py +++ b/iolite_client/client.py @@ -304,9 +304,12 @@ def discover(self): """Discovers the entities registered within the heating system.""" asyncio.run(self.async_discover()) - async def async_set_temp(self, device, temp: float): - request = self.request_handler.get_action_request(device, temp) + async def async_set_property(self, device, property: str, value: float): + request = self.request_handler.get_action_request(device, property, value) await asyncio.create_task(self._fetch_application([request])) - def set_temp(self, device, temp: float): - asyncio.run(self.async_set_temp(device, temp)) + def set_temp(self, device, value: float): + asyncio.run(self.async_set_property(device, 'heatingTemperatureSetting', value)) + + def set_blind_level(self, device, value: float): + asyncio.run(self.async_set_property(device, 'blindLevel', value)) diff --git a/iolite_client/entity.py b/iolite_client/entity.py index 39692a1..257da07 100644 --- a/iolite_client/entity.py +++ b/iolite_client/entity.py @@ -29,6 +29,32 @@ def get_type(cls) -> str: class Switch(Device): pass +class Blind(Device): + def __init__( + self, + identifier: str, + name: str, + place_identifier: str, + manufacturer: str, + blind_level: int, + ): + super().__init__(identifier, name, place_identifier, manufacturer) + self.blind_level = blind_level + +class HumiditySensor(Device): + def __init__( + self, + identifier: str, + name: str, + place_identifier: str, + manufacturer: str, + current_env_temp: float, + humidity_level: float, + ): + super().__init__(identifier, name, place_identifier, manufacturer) + self.current_env_temp = current_env_temp + self.humidity_level = humidity_level + class Lamp(Device): pass @@ -53,6 +79,22 @@ def __init__( self.current_env_temp = current_env_temp +class InFloorValve(Device): + def __init__( + self, + identifier: str, + name: str, + place_identifier: str, + manufacturer: str, + current_env_temp: float, + heating_temperature_setting: float, + device_status: str, + ): + super().__init__(identifier, name, place_identifier, manufacturer) + self.heating_temperature_setting = heating_temperature_setting + self.device_status = device_status + self.current_env_temp = current_env_temp + class Heating(Entity): def __init__( self, @@ -60,7 +102,7 @@ def __init__( name: str, current_temp: float, target_temp: float, - window_open: bool, + window_open: Optional[bool], ): super().__init__(identifier, name) self.current_temp = current_temp diff --git a/iolite_client/entity_factory.py b/iolite_client/entity_factory.py index d927b23..e65e2e9 100644 --- a/iolite_client/entity_factory.py +++ b/iolite_client/entity_factory.py @@ -1,4 +1,4 @@ -from iolite_client.entity import Device, Heating, Lamp, RadiatorValve, Room, Switch +from iolite_client.entity import Device, Heating, Lamp, RadiatorValve, InFloorValve, Room, Switch, Blind, HumiditySensor from iolite_client.exceptions import UnsupportedDeviceError @@ -42,14 +42,15 @@ def create_heating(payload: dict) -> Heating: return Heating( payload["id"], payload["name"], - payload["currentTemperature"], + payload.get("currentTemperature", None), payload["targetTemperature"], - payload["windowOpen"], + payload.get("windowOpen", None), ) def _create_device(identifier: str, type_name: str, payload: dict): place_identifier = payload["placeIdentifier"] + model_name = payload["modelName"] if type_name == "Lamp": return Lamp( identifier, @@ -73,21 +74,59 @@ def _create_device(identifier: str, type_name: str, payload: dict): ) elif type_name == "Heater": properties = payload["properties"] + current_env_temp = _get_prop(properties, "currentEnvironmentTemperature") + + if model_name.startswith("38de6001c3ad"): + heating_temperature_setting = _get_prop(properties, "heatingTemperatureSetting") + device_status = _get_prop(properties, "deviceStatus") + return InFloorValve( + identifier, + payload["friendlyName"], + place_identifier, + payload["manufacturer"], + current_env_temp, + heating_temperature_setting, + device_status, + ) + + else: + battery_level = _get_prop(properties, "batteryLevel") + heating_mode = _get_prop(properties, "heatingMode") + valve_position = _get_prop(properties, "valvePosition") + + return RadiatorValve( + identifier, + payload["friendlyName"], + place_identifier, + payload["manufacturer"], + current_env_temp, + battery_level, + heating_mode, + valve_position, + ) + elif type_name == "Blind": + properties = payload["properties"] + blind_level = _get_prop(properties, "blindLevel") + return Blind( + identifier, + payload["friendlyName"], + place_identifier, + payload["manufacturer"], + blind_level + ) + elif type_name == "HumiditySensor": + properties = payload["properties"] current_env_temp = _get_prop(properties, "currentEnvironmentTemperature") - battery_level = _get_prop(properties, "batteryLevel") - heating_mode = _get_prop(properties, "heatingMode") - valve_position = _get_prop(properties, "valvePosition") + humidity_level = _get_prop(properties, "humidityLevel") - return RadiatorValve( + return HumiditySensor( identifier, payload["friendlyName"], place_identifier, payload["manufacturer"], current_env_temp, - battery_level, - heating_mode, - valve_position, + humidity_level ) else: raise UnsupportedDeviceError(type_name, identifier, payload) diff --git a/iolite_client/oauth_handler.py b/iolite_client/oauth_handler.py index 828fd2a..3e4b6cf 100644 --- a/iolite_client/oauth_handler.py +++ b/iolite_client/oauth_handler.py @@ -16,10 +16,10 @@ class OAuthHandlerHelper: @staticmethod - def get_access_token_query(code: str, name: str) -> str: + def get_access_token_query(code: str, name: str, client_id: str) -> str: return urlencode( { - "client_id": CLIENT_ID, + "client_id": client_id, "grant_type": "authorization_code", "code": code, "name": name, @@ -27,10 +27,10 @@ def get_access_token_query(code: str, name: str) -> str: ) @staticmethod - def get_new_access_token_query(refresh_token: str) -> str: + def get_new_access_token_query(refresh_token: str, client_id: str) -> str: return urlencode( { - "client_id": CLIENT_ID, + "client_id": client_id, "grant_type": "refresh_token", "refresh_token": refresh_token, } @@ -53,9 +53,10 @@ def add_expires_at(token: dict) -> dict: class OAuthHandler: - def __init__(self, username: str, password: str): + def __init__(self, username: str, password: str, client_id: str = CLIENT_ID): self.username = username self.password = password + self.client_id = client_id def get_access_token(self, code: str, name: str) -> dict: """ @@ -64,7 +65,7 @@ def get_access_token(self, code: str, name: str) -> dict: :param name: The name of the device being paired :return: """ - query = OAuthHandlerHelper.get_access_token_query(code, name) + query = OAuthHandlerHelper.get_access_token_query(code, name, self.client_id) response = requests.post( f"{BASE_URL}/ui/token?{query}", auth=(self.username, self.password) ) @@ -77,7 +78,7 @@ def get_new_access_token(self, refresh_token: str) -> dict: :param refresh_token: The refresh token :return: dict containing access token, and new refresh token """ - query = OAuthHandlerHelper.get_new_access_token_query(refresh_token) + query = OAuthHandlerHelper.get_new_access_token_query(refresh_token, self.client_id) response = requests.post( f"{BASE_URL}/ui/token?{query}", auth=(self.username, self.password) ) @@ -100,11 +101,12 @@ def get_sid(self, access_token: str) -> str: class AsyncOAuthHandler: def __init__( - self, username: str, password: str, web_session: aiohttp.ClientSession + self, username: str, password: str, web_session: aiohttp.ClientSession, client_id: str = CLIENT_ID ): self.username = username self.password = password self.web_session = web_session + self.client_id = client_id async def get_access_token(self, code: str, name: str) -> dict: """ @@ -113,7 +115,7 @@ async def get_access_token(self, code: str, name: str) -> dict: :param name: The name of the device being paired :return: """ - query = OAuthHandlerHelper.get_access_token_query(code, name) + query = OAuthHandlerHelper.get_access_token_query(code, name, self.client_id) response = await self.web_session.post( f"{BASE_URL}/ui/token?{query}", auth=aiohttp.BasicAuth(self.username, self.password), @@ -127,7 +129,7 @@ async def get_new_access_token(self, refresh_token: str) -> dict: :param refresh_token: The refresh token :return: dict containing access token, and new refresh token """ - query = OAuthHandlerHelper.get_new_access_token_query(refresh_token) + query = OAuthHandlerHelper.get_new_access_token_query(refresh_token, self.client_id) response = await self.web_session.post( f"{BASE_URL}/ui/token?{query}", auth=aiohttp.BasicAuth(self.username, self.password), diff --git a/iolite_client/request_handler.py b/iolite_client/request_handler.py index 22155d8..daa300f 100644 --- a/iolite_client/request_handler.py +++ b/iolite_client/request_handler.py @@ -34,18 +34,18 @@ def get_subscribe_request(self, object_query: str) -> dict: return request - def get_action_request(self, device_id: str, temp: float) -> dict: + def get_action_request(self, device_id: str, property: str, value: float) -> dict: request = self._build_request( ClassMap.ActionRequest.value, { "modelID": "http://iolite.de#Environment", "class": ClassMap.ActionRequest.value, - "objectQuery": f"devices[id='{device_id}']/properties[name='heatingTemperatureSetting']", + "objectQuery": f"devices[id='{device_id}']/properties[name='{property}']", "actionName": "requestValueUpdate", "parameters": [ { "class": "ValueParameter", - "value": temp, + "value": value, } ], }, diff --git a/scripts/example.py b/scripts/example.py index ab88f10..54e1659 100644 --- a/scripts/example.py +++ b/scripts/example.py @@ -4,7 +4,7 @@ from environs import Env from iolite_client.client import Client -from iolite_client.entity import RadiatorValve +from iolite_client.entity import RadiatorValve, Blind, HumiditySensor, InFloorValve from iolite_client.oauth_handler import LocalOAuthStorage, OAuthHandler, OAuthWrapper env = Env() @@ -12,6 +12,7 @@ USERNAME = env("HTTP_USERNAME") PASSWORD = env("HTTP_PASSWORD") +CLIENT_ID = env("CLIENT_ID") CODE = env("CODE") NAME = env("NAME") LOG_LEVEL = env.log_level("LOG_LEVEL", logging.INFO) @@ -21,7 +22,7 @@ # Get SID oauth_storage = LocalOAuthStorage(".") -oauth_handler = OAuthHandler(USERNAME, PASSWORD) +oauth_handler = OAuthHandler(USERNAME, PASSWORD, CLIENT_ID) oauth_wrapper = OAuthWrapper(oauth_handler, oauth_storage) access_token = oauth_storage.fetch_access_token() @@ -34,7 +35,8 @@ print("------------------") print(f"URL: https://remote.iolite.de/ui/?SID={sid}") print(f"User: {USERNAME}") -print(f"Pass: {PASSWORD}") +print(f"Password: {PASSWORD}") +print(f"Client Id: {CLIENT_ID}") print("------------------") # Init client @@ -47,17 +49,26 @@ logger.info("Finished discovery") for room in client.discovered.get_rooms(): - print(f"{room.name} has {len(room.devices)} devices") + + print(f"\n{room.name} has {len(room.devices)} devices") if room.heating: print( f"Current temp: {room.heating.current_temp}, target: {room.heating.target_temp}" ) for device in room.devices.values(): - print(f"- {device.name}") + print(f"- {device.name} {device.get_type()}") if isinstance(device, RadiatorValve): print(f" - current: {device.current_env_temp}") print(f" - mode: {device.heating_mode}") + if isinstance(device, InFloorValve): + print(f" - current: {device.current_env_temp}") + print(f" - setting: {device.heating_temperature_setting}") + if isinstance(device, Blind): + print(f" - blind level: {device.blind_level}") + if isinstance(device, HumiditySensor): + print(f" - temp: {device.current_env_temp}") + print(f" - humidity: {device.humidity_level}") bathroom = client.discovered.find_room_by_name("Bathroom")