Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
66 changes: 58 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ Asynchronous Python client for communicating with a OJ Microline Thermostat.

## About

A Python package to control OJ Microline thermostats. It currently supports the WD5 series (OWD5, MWD5) and WG4 series (UWG4, AWG4).
A Python package to control OJ Microline thermostats. It currently supports the WD5 series (OWD5, MWD5), WG4 series (UWG4, AWG4), and WG5 series.

## Installation

Expand Down Expand Up @@ -63,24 +63,34 @@ These fields are only available on WD5-series thermostats; for others, they may
| :------- | :--- | :---------- |
| `thermostat_id` | int | The unique identifier for this thermostat. |
| `adaptive_mode` | boolean | If on then then the thermostat automatically changes heating start times to ensure that the required temperature has been reached at the beginning of any specific event. |
| `open_window_detection` | boolean | If on then the thermostat shuts off the heating for 30 minutes if an open window is detected. |
| `open_window_detection` | boolean | If on then the thermostat shuts off the heating for 30 minutes if an open window is detected. Also available on WG5-series thermostats. |
| `daylight_saving_active` | boolean | If on, the "Daylight Saving Time" function of the thermostat will automatically adjust the clock to the daylight saving time for the "Region" chosen. |
| `sensor_mode` | integer | The currently set sensor mode of the thermostat, see below. |
| `temperature_floor` | integer | The temperature measured by the floor sensor. |
| `temperature_room` | integer | The temperature measured by the room sensor. |
| `sensor_mode` | integer | The currently set sensor mode of the thermostat, see below. Also available on WG5-series thermostats. |
| `temperature_floor` | integer | The temperature measured by the floor sensor. Also available on WG5-series thermostats. |
| `temperature_room` | integer | The temperature measured by the room sensor. Also available on WG5-series thermostats. |
| `boost_temperature` | integer | If the regulation mode is set to boost mode, the thermostat will target this temperature. |
| `boost_end_time` | datetime | If the regulation mode is set to boost mode, it will end at this time. |
| `frost_protection_temperature` | integer | If the regulation mode is set to frost protection mode, the thermostat will target this temperature. |
| `schedule` | Schedule | The schedule the thermostat currently uses. (This *could* be supported by WG4-series thermostats, it simply isn't implemented.) |
| `frost_protection_temperature` | integer | If the regulation mode is set to frost protection mode, the thermostat will target this temperature. Also available on WG5-series thermostats. |
| `schedule` | Schedule/dict | The schedule the thermostat currently uses. On WD5 this is a `Schedule` object; on WG5 it is a raw dict from the API. |
| `energy` | list | The energy usage in kWh for the current day and the six previous days. (Note that the integrated tariff calculation needs to be disabled.) |

These fields are only available on WG4-series thermostats; for others, they may be `None`:
These fields are available on WG4 and WG5-series thermostats; for others, they may be `None`:

| Variable | Type | Description |
| :------- | :--- | :---------- |
| `temperature` | integer | The current temperature; the thermostat uses the room sensor or floor sensor based on its configuration. Avoid using this directly; instead, call the `get_current_temperature()` method which also works for WD5-series thermostats. |
| `set_point_temperature` | integer | The temperature the thermostat is targeting. Avoid using this directly; instead, call the `get_target_temperature()` method which also works for WD5-series thermostats. |

These fields are only available on WG5-series thermostats; for others, they may be `None`:

| Variable | Type | Description |
| :------- | :--- | :---------- |
| `building_id` | string | The building UUID this thermostat belongs to. |
| `zone_uuid` | string | The zone UUID this thermostat belongs to (WG5 uses UUIDs rather than integer zone IDs). |
| `is_in_standby` | boolean | Whether the thermostat is in standby (frost protection) mode. |
| `schedule_id` | string | The UUID of the schedule assigned to this thermostat. |
| `schedule_name` | string | The name of the schedule assigned to this thermostat. |

#### Regulation modes

| Integer | Constant | Description |
Expand Down Expand Up @@ -115,6 +125,8 @@ Keep in mind that certain thermostats only support a subset of these modes; be s

## Usage

### WD5 Example

```python
import asyncio
from time import sleep
Expand Down Expand Up @@ -173,6 +185,44 @@ async def main():
await client.set_regulation_mode(resource, REGULATION_SCHEDULE)


if __name__ == "__main__":
asyncio.run(main())
```

### WG5 Example

```python
import asyncio

from ojmicroline_thermostat import OJMicroline, Thermostat
from ojmicroline_thermostat.wg5 import WG5API
from ojmicroline_thermostat.const import (
REGULATION_MANUAL,
REGULATION_SCHEDULE,
)


async def main():
"""Show example on using the OJMicroline client with a WG5 thermostat."""
async with OJMicroline(
api=WG5API(
username="<your-username>",
password="<your-password>",
),
) as client:
thermostats: list[Thermostat] = await client.get_thermostats()

for resource in thermostats:
print(f"{resource.name}: {resource.get_current_temperature() / 100}°C")
print(f" Target: {resource.get_target_temperature() / 100}°C")

# Set to manual mode at 25°C
await client.set_regulation_mode(resource, REGULATION_MANUAL, 2500)

# Set back to schedule
await client.set_regulation_mode(resource, REGULATION_SCHEDULE)


if __name__ == "__main__":
asyncio.run(main())
```
Expand Down
2 changes: 2 additions & 0 deletions ojmicroline_thermostat/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,12 @@
from .ojmicroline import OJMicroline
from .wd5 import WD5API
from .wg4 import WG4API
from .wg5 import WG5API

__all__ = [
"WD5API",
"WG4API",
"WG5API",
"OJMicroline",
"OJMicrolineAuthError",
"OJMicrolineConnectionError",
Expand Down
9 changes: 9 additions & 0 deletions ojmicroline_thermostat/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,12 @@
WD5_DATETIME_FORMAT = "%Y-%m-%dT%H:%M:%S"
WG4_DATETIME_FORMAT = "%d/%m/%Y %H:%M:%S %z"
COMFORT_DURATION = 240

WG5_SENSOR_MAP: dict[str, int] = {
"Floor": SENSOR_FLOOR,
"Room": SENSOR_ROOM,
"RoomFloor": SENSOR_ROOM_FLOOR,
}

WG5_MODE_SCHEDULE = 1
WG5_MODE_HOLD = 2
85 changes: 83 additions & 2 deletions ojmicroline_thermostat/models/thermostat.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
from __future__ import annotations

from dataclasses import dataclass
from datetime import datetime, timedelta
from datetime import UTC, datetime, timedelta
from math import ceil
from time import gmtime, strftime
from typing import Any
Expand All @@ -22,6 +22,7 @@
SENSOR_ROOM,
SENSOR_ROOM_FLOOR,
WG4_DATETIME_FORMAT,
WG5_SENSOR_MAP,
)

from .schedule import Schedule
Expand Down Expand Up @@ -66,10 +67,17 @@ class Thermostat:
boost_temperature: int | None = None
energy: list[float] | None = None

# WG4-only fields:
# WG4/WG5 fields:
temperature: int | None = None
set_point_temperature: int | None = None

# WG5-only fields:
building_id: str | None = None
zone_uuid: str | None = None
is_in_standby: bool | None = None
schedule_id: str | None = None
schedule_name: str | None = None

@classmethod
def from_wd5_json(cls, data: dict[str, Any]) -> Thermostat:
"""Return a new Thermostat instance based on JSON from the WD5-series API.
Expand Down Expand Up @@ -178,6 +186,79 @@ def from_wg4_json(cls, data: dict[str, Any]) -> Thermostat:
vacation_temperature=data["VacationTemperature"],
)

@classmethod
def from_wg5_json(
cls,
data: dict[str, Any],
building_id: str,
zone_name: str,
*,
away_active: bool = False,
) -> Thermostat:
"""Return a new Thermostat instance based on JSON from the WG5-series API.

Args:
----
data: The thermostat control JSON data from the API.
building_id: The building UUID this thermostat belongs to.
zone_name: The zone name this thermostat belongs to.
away_active: Whether away mode is active for the building.

Returns:
-------
A Thermostat Object.

"""
mode = data["mode"]
setpoint_centideg = round(data["setpoint"] * 100)

if away_active or mode.get("isAwayActive"):
regulation_mode = REGULATION_VACATION
elif mode["isInStandby"]:
regulation_mode = REGULATION_FROST_PROTECTION
elif mode.get("fallbackMode") == "Auto":
regulation_mode = REGULATION_SCHEDULE
else:
regulation_mode = REGULATION_MANUAL

return cls(
model="WG5",
serial_number=data["id"],
software_version="",
zone_name=zone_name,
zone_id=0,
zone_uuid=data["zoneId"],
building_id=building_id,
name=data["name"],
online=data["isOnline"],
heating=data["isHeatRelayActive"],
regulation_mode=regulation_mode,
supported_regulation_modes=[
REGULATION_SCHEDULE,
REGULATION_MANUAL,
REGULATION_COMFORT,
REGULATION_VACATION,
REGULATION_FROST_PROTECTION,
],
min_temperature=round(data["minimumPossibleSetPoint"] * 100),
max_temperature=round(data["maximumPossibleSetPoint"] * 100),
temperature=round(data["currentTemperature"] * 100),
set_point_temperature=setpoint_centideg,
manual_temperature=setpoint_centideg,
comfort_temperature=setpoint_centideg,
comfort_end_time=datetime.min.replace(tzinfo=UTC),
last_primary_mode_is_auto=mode.get("fallbackMode") == "Auto",
frost_protection_temperature=round(
data["frostProtectionTemperature"] * 100
),
sensor_mode=WG5_SENSOR_MAP.get(data.get("sensorApplication", "")),
open_window_detection=data.get("isOpenWindowDetected", False),
vacation_mode=away_active or mode.get("isAwayActive", False),
is_in_standby=mode["isInStandby"],
schedule_id=data.get("scheduleData", {}).get("scheduleId"),
schedule_name=data.get("scheduleData", {}).get("scheduleName"),
)

def get_target_temperature(self) -> int:
"""Return the target temperature for the thermostat.

Expand Down
Loading
Loading