Skip to content

Commit

Permalink
add infloor valve, blind and sensor entities, adjustments for oauth h…
Browse files Browse the repository at this point in the history
…andling of different client ids (#220)
  • Loading branch information
thepiwo authored Apr 7, 2024
1 parent 835e16d commit 928b6cc
Show file tree
Hide file tree
Showing 7 changed files with 131 additions and 33 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
# Secrets
/.env
access_token.json
access_token_*.json

# Cache
__pycache__
Expand Down
11 changes: 7 additions & 4 deletions iolite_client/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -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))
44 changes: 43 additions & 1 deletion iolite_client/entity.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -53,14 +79,30 @@ 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,
identifier: str,
name: str,
current_temp: float,
target_temp: float,
window_open: bool,
window_open: Optional[bool],
):
super().__init__(identifier, name)
self.current_temp = current_temp
Expand Down
59 changes: 49 additions & 10 deletions iolite_client/entity_factory.py
Original file line number Diff line number Diff line change
@@ -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


Expand Down Expand Up @@ -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,
Expand All @@ -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)
Expand Down
22 changes: 12 additions & 10 deletions iolite_client/oauth_handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,21 +16,21 @@

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,
}
)

@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,
}
Expand All @@ -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:
"""
Expand All @@ -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)
)
Expand All @@ -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)
)
Expand All @@ -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:
"""
Expand All @@ -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),
Expand All @@ -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),
Expand Down
6 changes: 3 additions & 3 deletions iolite_client/request_handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
}
],
},
Expand Down
21 changes: 16 additions & 5 deletions scripts/example.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,15 @@
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()
env.read_env()

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)
Expand All @@ -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()
Expand All @@ -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
Expand All @@ -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")

Expand Down

0 comments on commit 928b6cc

Please sign in to comment.