import dataclasses
import logging
from httpx import AsyncClient
_LOGGER = logging.getLogger(__name__)
@dataclasses.dataclass(frozen=True, kw_only=True)
class Value:
key: str
register: str
VALUES: tuple[Value, ...] = (
# Asetusarvot > Menoveden minimiraja
Value(key="l1_supply_temperature_minimum", register="S_54_85"),
# Asetusarvot > Menoveden maksimiraja
Value(key="l1_supply_temperature_maximum", register="S_55_85"),
# Asetusarvot > L1 Säätökäyrä
# n = negative, p = positive
Value(key="l1_heating_curve_n_20", register="S_67_85"),
Value(key="l1_heating_curve_n_10", register="S_69_85"),
Value(key="l1_heating_curve_zero", register="S_71_85"),
Value(key="l1_heating_curve_p_10", register="S_73_85"),
Value(key="l1_heating_curve_p_20", register="S_75_85"),
# Asetusarvot > Huonelämpötila
Value(key="l1_target_room_temperature", register="S_81_85"),
# Asetusarvot > Lämmönpudotus (huonelämpö)
Value(key="l1_temperature_drop", register="S_87_85"),
# Asetusarvot > Suuri lämmönpudotus (huonelämpö)
Value(key="l1_temperature_drop_big", register="S_88_85"),
# Ohjaustavat
# 0 = Automaatti
# 3 = Pakko-ohjaus, norm. lämpötaso
# 1 = Pakko-ohjaus, lämmönpudotus
# 2 = Pakko-ohjaus, suuri lämmönpudotus
# 6 = Käsiajo, sähköinen
# 5 = Alasajo
Value(key="l1_operation_mode", register="S_59_85"),
Value(key="l1_manual_drive_valve_position", register="S_92_85"),
# Mittaukset > Ulkolämpötila
Value(key="outside_temperature", register="S_227_85"),
# Mittaukset > L1 Menoveden lämpötila
Value(key="l1_supply_temperature", register="S_259_85"),
# Mittaukset > L1 Huonelämpötila
Value(key="l1_room_temperature", register="S_284_85"),
# Mittaukset > Huonelämpökaukoasetus TMR/SP
Value(key="l1_tmrsp", register="S_274_85"),
# Mittaukset > L1 Venttiilin asento
Value(key="l1_valve_position", register="S_272_85"),
# EH-800 > Kotona/Poissa
# 0 = Kotona
# 1 = Poissa
# 2 = Ei K/P-ohjausta
Value(key="home_away", register="S_135_85"),
# EH-800 > Huonelämpötila
Value(key="room_temperature", register="S_261_85"),
# EH-800 > Huonelämpötilan hienosäätö
Value(key="room_temperature_fine_adjustment", register="S_102_85"),
# EH-800 > Lämpötaso:: (UI placement is strange)
Value(key="l1_operation_mode_str", register="S_1000_0"),
)
@dataclasses.dataclass(frozen=True, kw_only=True)
class OperationMode:
name: str
value: int
OPERATION_MODES: tuple[OperationMode, ...] = (
OperationMode(name="Automatic", value=0),
OperationMode(name="Forced - Normal", value=3),
OperationMode(name="Forced - Drop", value=1),
OperationMode(name="Forced - Big Drop", value=2),
OperationMode(name="Manual", value=6),
OperationMode(name="Off", value=5),
)
class EH800:
def __init__(self, host: str, port: int, username: str, password: str) -> None:
self._uri = f"http://{host}:{port}"
self._login = f"uid={username};pwd={password};"
self._client = AsyncClient()
self._request_query = "request?"
for value in VALUES:
self._request_query += f"{value.register};"
self.data = {}
async def _refresh_login(self) -> bool:
"""
Refresh login.
Logs an error if the login failed.
"""
r = await self._client.get(f"{self._uri}/login?{self._login}")
if r.text[:-1] == "login?result=ok;":
return True
_LOGGER.error("Login error")
return False
async def _request_values(self) -> bool:
"""
Request values from the API.
"""
r = await self._client.get(f"{self._uri}/{self._request_query}")
if r.status_code != 200:
_LOGGER.error("unexpected return code %s", r.status_code)
return False
# Remove suffix and prefix so we end up with key=val pairs separated by
# semicolons: key=val;key2=val;key3=val
text = r.text.removeprefix("request?").removesuffix("\x00").removesuffix(";")
pairs = text.split(";")
for pair in pairs:
kv = pair.split("=")
# Only process those that returned something
if len(kv) == 2:
# Find the data key for the register
data_key = [value.key for value in VALUES if value.register == kv[0]][0]
self.data[data_key] = kv[1]
else:
_LOGGER.warning("register %s didn't return a value", str(kv[0]))
return True
async def _update_value(self, value: Value, new_value) -> None:
"""
Update a value via the API.
Checks the API return value and logs an error if the value update failed.
"""
r = await self._client.get(f"{self._uri}/update?{value.register}={new_value};")
eq_index = r.text.find("=")
sc_index = r.text.find(";")
got_value = r.text[eq_index + 1 : sc_index]
if str(got_value) != str(new_value):
_LOGGER.error(
"Value update failed, got '%s', wanted '%s'", got_value, new_value
)
else:
# Update data to match the new value
self.data[value.key] = new_value
async def update(self) -> bool:
"""Update data values from the API."""
if not await self._refresh_login():
return False
if not await self._request_values():
return False
return True
async def update_value(self, key, new_value) -> None:
"""Update a value via the API."""
value = [value for value in VALUES if value.key == key][0]
_LOGGER.debug(
"Updating '%s' (%s) to '%s'", value.key, value.register, new_value
)
await self._update_value(value, new_value)