From 9a6188433a3b4adf86f528b76d04b206594e8948 Mon Sep 17 00:00:00 2001 From: MistaP <7313597+Mista-P@users.noreply.github.com> Date: Sun, 11 Oct 2026 18:18:22 +0100 Subject: [PATCH 1/3] fix(capabilities): a hot-water cylinder is not room climate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A live ASHW cylinder (EDL200UK-630RF) reports its water target, 45 °C, in ActiveSetPointTemperature, which made capabilities_for call it room climate and keep every room-heater feature. Clients then gave it a thermostat with a 7-30 °C range, room temperature, open window, EcoStart and setback. Only a room temperature now makes a hot-water appliance room climate. Without one, the room-heater features default to off unless its status reports them (the live cylinder sends null for all of them), and so do the room setpoint and setback writes. Co-Authored-By: Claude Opus 5.5 --- dimplex_controller/capabilities.py | 31 ++++++++------ tests/test_capabilities.py | 68 ++++++++++++++++++++++++++++++ 2 files changed, 86 insertions(+), 13 deletions(-) diff --git a/dimplex_controller/capabilities.py b/dimplex_controller/capabilities.py index fcf69cb..4a4152b 100644 --- a/dimplex_controller/capabilities.py +++ b/dimplex_controller/capabilities.py @@ -162,15 +162,23 @@ def capabilities_for( heat_pump = True hot_water = True - boost = True - away = True + if status is not None and status.AvailableHotWater is not None: + hot_water = True + + # A cylinder reports its *water* target in ActiveSetPointTemperature (45 °C + # on a live ASHW cylinder), so only a room temperature makes it room climate. + # Without one, the room-heater features default to off — a live cylinder + # sends null for every one of them — unless its status reports them. + climate = not hot_water or (status is not None and status.RoomTemperature is not None) + + boost = climate + away = climate advance = True - open_window = True - eco_start = True - setback_read = True - frost = True + open_window = climate + eco_start = climate + setback_read = climate + frost = climate timer = True - climate = not hot_water # cylinder-only appliances are not room climate if status is not None: if status.BoostDuration is not None or status.BoostTemperature is not None: @@ -183,10 +191,6 @@ def capabilities_for( eco_start = True if status.SetbackEnabled is not None or status.SetbackTemperature is not None: setback_read = True - if status.AvailableHotWater is not None: - hot_water = True - if status.RoomTemperature is not None or status.ActiveSetPointTemperature is not None: - climate = True # Advance only means something for a scheduled room heater; a cylinder has # no "next comfort period" to jump to. @@ -200,10 +204,11 @@ def capabilities_for( open_window=open_window, eco_start=eco_start, setback_read=setback_read, - setback_write=True, + setback_write=climate, frost=frost, timer=timer, - setpoint_write=True, + # The room setpoint endpoint; cylinders have their own (Set*TemperatureHwc). + setpoint_write=climate, energy_meter=energy_meter, storage=storage, hot_water=hot_water, diff --git a/tests/test_capabilities.py b/tests/test_capabilities.py index 51047a0..724b9c5 100644 --- a/tests/test_capabilities.py +++ b/tests/test_capabilities.py @@ -107,3 +107,71 @@ def test_as_dict_includes_presets(): data = caps.as_dict() assert data["boost"] is False assert data["climate_presets"] == ["comfort", "away"] + + +# GetApplianceOverview row from a live ASHW cylinder (EDL200UK-630RF). +_LIVE_CYLINDER = { + "HubId": "h1", + "ApplianceId": "a1", + "ZoneId": "z1", + "StatusTwo": 0, + "ApplianceModes": 8192, + "RoomTemperature": None, + "ActiveSetPointTemperature": 45, + "BoostDuration": 0, + "BoostTemperature": None, + "AwayDateTime": None, + "AwayTemperature": None, + "OpenWindowEnabled": None, + "EcoStartEnabled": None, + "SetbackEnabled": None, + "SetbackTemperature": None, + "AvailableHotWater": 336, + "Capacity": 200, + "ErrorCode": 0, + "WarningCode": 0, +} + + +def _ashw() -> Appliance: + return Appliance( + ApplianceId="a1", + ApplianceType="ASHW Cylinder", + ApplianceModel="EDL200UK-630RF", + ZoneId="z1", + FriendlyName="Hot Water", + ZoneName="Hot water", + ) + + +def test_live_cylinder_is_not_room_climate(): + """Its water target in ActiveSetPointTemperature must not make it a thermostat.""" + caps = capabilities_for(_ashw(), status=ApplianceStatus.model_validate(_LIVE_CYLINDER)) + assert caps.hot_water is True and caps.heat_pump is True + assert caps.climate is False + for flag in ( + "away", + "advance", + "open_window", + "eco_start", + "setback_read", + "setback_write", + "frost", + "setpoint_write", + ): + assert getattr(caps, flag) is False, flag + # BoostDuration is reported (0), and a cylinder does have a boost. + assert caps.boost is True + + +def test_cylinder_with_a_room_temperature_is_room_climate(): + status = ApplianceStatus.model_validate({**_LIVE_CYLINDER, "RoomTemperature": 19}) + caps = capabilities_for(_ashw(), status=status) + assert caps.climate is True + assert caps.open_window is True and caps.setpoint_write is True + + +def test_room_heater_keeps_every_feature_without_status(): + caps = capabilities_for() + assert caps.climate is True + assert caps.open_window and caps.eco_start and caps.away and caps.frost and caps.setpoint_write From 0ae226a8ddcf0a46a751538faf37cd2521a62362 Mon Sep 17 00:00:00 2001 From: MistaP <7313597+Mista-P@users.noreply.github.com> Date: Sun, 11 Oct 2026 18:18:22 +0100 Subject: [PATCH 2/3] feat(hot-water): cylinder status fields, schedule model and holiday From a live ASHW cylinder (EDL200UK-630RF): - ApplianceStatus declares the cylinder fields the overview carries: Capacity, HeatingUp, IsGreenEnergyActive, HygieneFrequency, HygieneTemperature, HolidayNumOfDays and the always-null rest. - get_heat_pump_hot_water_schedule returns HeatPumpHwcSchedule. The reply has no TimerMode and one {DayOfWeek, StartTime, Mode} per period, so the TimerModeSettings parse failed on real hardware. - set_hot_water_holiday engages or clears Holiday. Verified: status 8192 -> 8200 with HolidayNumOfDays set and heating stopped, then back to 8192. - set_heat_pump_hot_water_schedule documents that the endpoint answers 403 to every body tried. Co-Authored-By: Claude Opus 5.5 --- dimplex_controller/__init__.py | 4 ++ dimplex_controller/client.py | 54 +++++++++++++++++++-- dimplex_controller/models.py | 40 ++++++++++++++++ tests/test_client_methods.py | 87 ++++++++++++++++++++++++++++------ tests/test_models.py | 33 +++++++++++++ 5 files changed, 198 insertions(+), 20 deletions(-) diff --git a/dimplex_controller/__init__.py b/dimplex_controller/__init__.py index 2493596..193c404 100644 --- a/dimplex_controller/__init__.py +++ b/dimplex_controller/__init__.py @@ -45,6 +45,8 @@ ApplianceModeStatus, ApplianceStatus, AutomaticProvisioning, + HeatPumpHwcPeriod, + HeatPumpHwcSchedule, Hub, HygieneFrequency, ProductModel, @@ -83,6 +85,8 @@ "SetbackStatus", "TimerMode", "TimerModeSettings", + "HeatPumpHwcPeriod", + "HeatPumpHwcSchedule", "TimerPeriod", "AutomaticProvisioning", "ProductModel", diff --git a/dimplex_controller/client.py b/dimplex_controller/client.py index 58f480a..3c89aeb 100644 --- a/dimplex_controller/client.py +++ b/dimplex_controller/client.py @@ -3,6 +3,7 @@ import asyncio import json import logging +import math import random from datetime import datetime, timedelta, timezone from typing import Any @@ -36,6 +37,7 @@ ApplianceModeSettings, ApplianceModeStatus, ApplianceStatus, + HeatPumpHwcSchedule, Hub, HygieneFrequency, ProductModel, @@ -840,6 +842,42 @@ async def set_hot_water_mode( endpoint=endpoint, ) + async def set_hot_water_holiday( + self, + hub_id: str, + appliance_ids: list[str], + until: datetime | None = None, + *, + enable: bool = True, + heat_pump: bool = False, + ) -> None: + """Put a cylinder on Holiday (standby) until ``until``, or clear it. + + Verified on a live ASHW cylinder (EDL200UK-630RF) with ``heat_pump=True``: + engaging moved ``ApplianceModes`` 8192 → 8200 and set + ``HolidayNumOfDays``, heating stopped, and clearing restored 8192. The + cylinder counts holidays in whole days, so ``until`` is also sent as a + rounded-up day count; a caller that wants a time of day has to clear + Holiday itself at that time. + """ + days = 0 + if enable: + if until is None: + raise ValueError("a holiday needs an end time") + end = until if until.tzinfo is not None else until.replace(tzinfo=timezone.utc) + seconds = (end - datetime.now(timezone.utc)).total_seconds() + days = max(1, math.ceil(seconds / 86400)) + endpoint = "/RemoteControl/SetApplianceModeHeatPumpHwc" if heat_pump else "/RemoteControl/SetApplianceModeHwc" + await self.set_mode_flag( + hub_id, + appliance_ids, + ApplianceModeFlag.HOLIDAY, + enable=enable, + until=until, + number_of_days=days, + endpoint=endpoint, + ) + async def set_hot_water_boost_temperature( self, hub_id: str, appliance_ids: list[str], temperature: float, *, enable: bool = True ) -> None: @@ -902,10 +940,12 @@ async def set_hot_water_hygiene( endpoint=endpoint, ) - async def get_heat_pump_hot_water_schedule(self, hub_id: str, appliance_id: str) -> TimerModeSettings: - """Read an ASHW cylinder's schedule. + async def get_heat_pump_hot_water_schedule(self, hub_id: str, appliance_id: str) -> HeatPumpHwcSchedule: + """Read an ASHW cylinder's weekly schedule. - .. warning:: Untested — APK-confirmed only. + Verified on a live ASHW cylinder (EDL200UK-630RF). The reply does not fit + :class:`TimerModeSettings` — there is no ``TimerMode`` and each period is + ``{DayOfWeek, StartTime, Mode}`` — hence :class:`HeatPumpHwcSchedule`. """ payload = {"HubId": hub_id, "ApplianceId": appliance_id} data = await self._request( @@ -913,12 +953,16 @@ async def get_heat_pump_hot_water_schedule(self, hub_id: str, appliance_id: str) "/RemoteControl/ApiGetTimerModeDetailsForHeatPumpHwcAppliance", json=payload, ) - return TimerModeSettings.model_validate(data) + return HeatPumpHwcSchedule.model_validate(data) async def set_heat_pump_hot_water_schedule(self, settings: TimerModeSettings) -> TimerModeSettings: """Write an ASHW cylinder's schedule periods. - .. warning:: Untested — APK-confirmed only. + .. warning:: Does not work yet. On a live ASHW cylinder this endpoint + answered HTTP 403 to every body tried — wrapped in ``TimerModeSettings`` + or ``Settings``, with or without ``TimerMode``, integer or ``HH:MM:SS`` + times, and app versions up to 3.0.0 — while unwrapped bodies got 400. + The request the official app sends has not been captured. """ payload = {"TimerModeSettings": settings.model_dump(mode="json")} await self._request("POST", "/RemoteControl/UpdateHeatPumpHwcSchedulePeriods", json=payload) diff --git a/dimplex_controller/models.py b/dimplex_controller/models.py index 75e1f61..9dadc7a 100644 --- a/dimplex_controller/models.py +++ b/dimplex_controller/models.py @@ -255,6 +255,31 @@ class TimerModeSettings(BaseModel): TimerPeriods: list[TimerPeriod] = Field(default_factory=list) +class HeatPumpHwcPeriod(BaseModel): + """One period of an ASHW cylinder's weekly schedule. + + A live cylinder returns one ``{DayOfWeek, StartTime: 0, Mode: 2}`` per day. + ``DayOfWeek`` is .NET's (0 = Sunday). ``StartTime`` is an integer, presumed + minutes after midnight, and what each ``Mode`` value means is not yet known. + """ + + DayOfWeek: int + StartTime: int + Mode: int + + +class HeatPumpHwcSchedule(BaseModel): + """``ApiGetTimerModeDetailsForHeatPumpHwcAppliance`` reply. + + Unlike the room-heater :class:`TimerModeSettings` it has no ``TimerMode`` + and its periods carry a mode instead of an end time and temperature. + """ + + HubId: str + ApplianceId: str + TimerPeriods: list[HeatPumpHwcPeriod] = Field(default_factory=list) + + class UserContext(BaseModel): Id: str EmailAddress: str | None = None @@ -286,6 +311,21 @@ class ApplianceStatus(BaseModel): LockStatus: int | None = None ErrorCode: str | None = None WarningCode: str | None = None + # Hot-water cylinder fields, as a live ASHW cylinder (EDL200UK-630RF) sends + # them. AvailableHotWater above is litres of usable hot water (the app shows + # it as "336 L Hot water"). + Capacity: int | None = None # tank size, litres + HeatingUp: bool | None = None + IsGreenEnergyActive: bool | None = None + HygieneFrequency: int | None = None # EFrequency, see HygieneFrequency + HygieneTemperature: float | None = None + HolidayNumOfDays: int | None = None + # Present in the same reply, always null on the cylinder seen so far. + DelayedStartTime: str | None = None + DsmStatus: int | None = None + ModifiedPower: float | None = None + ConnectedChargeProfiles: Any = None + StandaloneChargeProfiles: Any = None @field_validator("ErrorCode", "WarningCode", mode="before") @classmethod diff --git a/tests/test_client_methods.py b/tests/test_client_methods.py index c9a7aab..bde24f6 100644 --- a/tests/test_client_methods.py +++ b/tests/test_client_methods.py @@ -2,8 +2,9 @@ from __future__ import annotations +import json import logging -from datetime import datetime, timezone +from datetime import datetime, timedelta, timezone import aiohttp import pytest @@ -12,9 +13,12 @@ from dimplex_controller.const import NO_SETPOINT_SENTINEL, NULL_DATETIME from dimplex_controller.models import ( ApplianceModeFlag, + HeatPumpHwcPeriod, + HeatPumpHwcSchedule, HygieneFrequency, SetbackStatus, TimerMode, + TimerModeSettings, TimerPeriod, Zone, ) @@ -492,28 +496,81 @@ async def handler(request): ] -@pytest.mark.asyncio -async def test_heat_pump_hot_water_schedule_round_trip(aresponses): - """The ASHW schedule is read then written back through its own endpoints.""" - captured: dict = {} +# The reply a live ASHW cylinder (EDL200UK-630RF) gave: one period per day. +_ASHW_SCHEDULE = { + "HubId": "hub-1", + "ApplianceId": "a-1", + "TimerPeriods": [{"DayOfWeek": d, "StartTime": 0, "Mode": 2} for d in (1, 2, 3, 4, 5, 6, 0)], +} - async def write_handler(request): - captured["body"] = await request.json() - return _json(aresponses) +@pytest.mark.asyncio +async def test_heat_pump_hot_water_schedule_reads_the_live_shape(aresponses): + """The ASHW schedule has no TimerMode and a Mode per period.""" aresponses.add( HOST, "/api/RemoteControl/ApiGetTimerModeDetailsForHeatPumpHwcAppliance", "POST", - _json(aresponses, _TIMER_BODY), + _json(aresponses, json.dumps(_ASHW_SCHEDULE)), ) + async with aiohttp.ClientSession() as session: + schedule = await _authed(session).get_heat_pump_hot_water_schedule("hub-1", "a-1") + + assert isinstance(schedule, HeatPumpHwcSchedule) + assert len(schedule.TimerPeriods) == 7 + assert schedule.TimerPeriods[-1] == HeatPumpHwcPeriod(DayOfWeek=0, StartTime=0, Mode=2) + + +@pytest.mark.asyncio +async def test_heat_pump_hot_water_schedule_write_posts_timer_mode_settings(aresponses): + """The write is unchanged (and answers 403 on a live cylinder; see its docstring).""" + captured: dict = {} + + async def write_handler(request): + captured["body"] = await request.json() + return _json(aresponses) + aresponses.add(HOST, "/api/RemoteControl/UpdateHeatPumpHwcSchedulePeriods", "POST", write_handler) + settings = TimerModeSettings.model_validate(json.loads(_TIMER_BODY)) + async with aiohttp.ClientSession() as session: + await _authed(session).set_heat_pump_hot_water_schedule(settings) + + assert captured["body"]["TimerModeSettings"]["ApplianceId"] == settings.ApplianceId + +@pytest.mark.asyncio +async def test_set_hot_water_holiday_sends_holiday_with_rounded_up_days(aresponses): + """The body that engaged and cleared Holiday on a live ASHW cylinder.""" + bodies: list[tuple[str, dict]] = [] + + async def handler(request): + bodies.append((request.path, await request.json())) + return _json(aresponses, "true") + + aresponses.add(HOST, "/api/RemoteControl/SetApplianceModeHeatPumpHwc", "POST", handler) + aresponses.add(HOST, "/api/RemoteControl/SetApplianceModeHeatPumpHwc", "POST", handler) + aresponses.add(HOST, "/api/RemoteControl/SetApplianceModeHwc", "POST", handler) + + until = datetime.now(timezone.utc).replace(microsecond=0) + timedelta(days=2, hours=3) async with aiohttp.ClientSession() as session: client = _authed(session) - settings = await client.get_heat_pump_hot_water_schedule("hub-1", "a-1") - settings.TimerPeriods[0].Temperature = 55.0 - await client.set_heat_pump_hot_water_schedule(settings) - - assert len(settings.TimerPeriods) == 2 - assert captured["body"]["TimerModeSettings"]["TimerPeriods"][0]["Temperature"] == 55.0 + await client.set_hot_water_holiday("hub-1", ["a-1"], until, heat_pump=True) + await client.set_hot_water_holiday("hub-1", ["a-1"], enable=False, heat_pump=True) + await client.set_hot_water_holiday("hub-1", ["a-1"], until) + with pytest.raises(ValueError): + await client.set_hot_water_holiday("hub-1", ["a-1"]) + + path, on = bodies[0] + assert path == "/api/RemoteControl/SetApplianceModeHeatPumpHwc" + assert on["ApplianceIds"] == ["a-1"] + assert on["Settings"]["ApplianceModes"] == int(ApplianceModeFlag.HOLIDAY) + assert on["Settings"]["Status"] == 1 + assert on["Settings"]["NumberOfDays"] == 3 + assert on["Settings"]["Date"] == until.replace(tzinfo=None).isoformat() + + _, off = bodies[1] + assert off["Settings"]["Status"] == 0 + assert off["Settings"]["NumberOfDays"] == 0 + assert off["Settings"]["Date"] == NULL_DATETIME + + assert bodies[2][0] == "/api/RemoteControl/SetApplianceModeHwc" diff --git a/tests/test_models.py b/tests/test_models.py index be78905..7992b2a 100644 --- a/tests/test_models.py +++ b/tests/test_models.py @@ -414,3 +414,36 @@ def test_timer_period_empty_time_returns_none(): period = TimerPeriod(DayOfWeek=0, StartTime="", EndTime="", Temperature=18.0) assert period.start_time_obj is None assert period.end_time_obj is None + + +def test_appliance_status_keeps_cylinder_fields(): + """A live ASHW cylinder's overview row, including the cylinder-only fields.""" + status = ApplianceStatus.model_validate( + { + "HubId": "h1", + "ApplianceId": "a1", + "ZoneId": "z1", + "ApplianceModes": 8192, + "ActiveSetPointTemperature": 45, + "AvailableHotWater": 336, + "HygieneTemperature": None, + "HygieneFrequency": 7, + "HolidayNumOfDays": 0, + "Capacity": 200, + "ErrorCode": 0, + "WarningCode": 0, + "IsGreenEnergyActive": True, + "HeatingUp": False, + "ConnectedChargeProfiles": None, + "StandaloneChargeProfiles": None, + "ModifiedPower": None, + "DsmStatus": None, + "DelayedStartTime": None, + } + ) + assert status.Capacity == 200 + assert status.IsGreenEnergyActive is True + assert status.HeatingUp is False + assert status.HygieneFrequency == HygieneFrequency.WEEKLY + assert status.HolidayNumOfDays == 0 + assert status.has_mode(ApplianceModeFlag.NORMAL) From 1a897dc17d8bd4149017e2c68d274db9193117a1 Mon Sep 17 00:00:00 2001 From: MistaP <7313597+Mista-P@users.noreply.github.com> Date: Sun, 11 Oct 2026 18:18:22 +0100 Subject: [PATCH 3/3] docs(hot-water): record what an ASHW cylinder returned Mark the heat-pump cylinder endpoints with what was validated live on an EDL200UK-630RF (holiday, schedule read) and what was not (schedule write answers 403; the temperature, boost and hygiene writes are untested). Co-Authored-By: Claude Opus 5.5 --- README.md | 4 ++- docs/decompiled-api-reference.md | 49 +++++++++++++++++++++++--------- openapi.yaml | 6 ++-- 3 files changed, 41 insertions(+), 18 deletions(-) diff --git a/README.md b/README.md index 6ff3065..2097934 100644 --- a/README.md +++ b/README.md @@ -259,7 +259,9 @@ Main client class. Construct with an `aiohttp.ClientSession` and a `refresh_toke | `set_setback_temperature(...)` | Write the setback temperature (untested). | | `set_eco_start(hub_id, appliance_ids, enable)` | Toggle EcoStart. | | `set_open_window_detection(hub_id, appliance_ids, enable)` | Toggle Open Window Detection. | -| `set_hot_water_*(...)` / `*_heat_pump_hot_water_schedule(...)` | Hot-water cylinder surface (untested — no hardware). | +| `set_hot_water_holiday(hub_id, appliance_ids, until, *, enable, heat_pump)` | Cylinder Holiday on/off (validated on an ASHW cylinder). | +| `get_heat_pump_hot_water_schedule(...)` | ASHW cylinder schedule as `HeatPumpHwcSchedule` (validated). Writing it answers 403. | +| `set_hot_water_*(...)` | Remaining cylinder controls (untested — no hardware). | | `get_tsi_energy_report(hub_id, ...)` | Returns `TsiEnergyReport`. | | `capabilities_for(appliance, *, status, product)` | Derive an `ApplianceCapabilities` matrix. | | `export_tokens()` / `apply_tokens(bundle)` | Token persistence helpers. | diff --git a/docs/decompiled-api-reference.md b/docs/decompiled-api-reference.md index 2dce7ca..4c95404 100644 --- a/docs/decompiled-api-reference.md +++ b/docs/decompiled-api-reference.md @@ -150,21 +150,39 @@ the control/reporting-relevant ones are listed here. Method names are the > `SetApplianceSetpointTemperature` for setpoints and `SetApplianceMode` > (FrostProtect) for "off". -### RemoteControl — hot-water cylinders (❌ not owned — APK-confirmed only) +### RemoteControl — hot-water cylinders (partly validated on an ASHW cylinder) Applies to `WaterHeater` and heat-pump `ASHW Cylinder` models. All reuse `SetApplianceModeRequest` / `ApplianceModeSettings`. -| Endpoint | Method | Mode used | 📦 | -| --- | --- | --- | --- | -| `/RemoteControl/SetApplianceModeHwc` | `SetModeForHWAppliances` | Boost/Normal/… | ✅ | -| `/RemoteControl/SetBoostTemperatureHwc` | `SetHWBoostTemperature` | `Boost`, Temperature | ✅ | -| `/RemoteControl/SetNormalTemperatureHwc` | `SetHWNormalTemperature` | `Normal`, Temperature | ✅ | -| `/RemoteControl/SetHygieneSettingsHwc` | `SetHWHygieneSettings` | `Hygiene`, Temperature, `Frequency` | ✅ | -| `/RemoteControl/SetApplianceModeHeatPumpHwc` | `SetModeForHeatPumpHWAppliances` | — | ✅ | -| `/RemoteControl/SetHygieneSettingsHeatPumpHwc` | `SetHeatPumpHWCHygieneSettings` | `Hygiene` | ✅ | -| `/RemoteControl/ApiGetTimerModeDetailsForHeatPumpHwcAppliance` | `ApiGetTimerModeDetailsForHeatPumpHwcAppliance` → details | `{HubId, ApplianceId}` | ✅ | -| `/RemoteControl/UpdateHeatPumpHwcSchedulePeriods` | schedule edit | — | ✅ | +| Endpoint | Method | Mode used | 📦 | 🔬 live (EDL200UK-630RF) | +| --- | --- | --- | --- | --- | +| `/RemoteControl/SetApplianceModeHwc` | `SetModeForHWAppliances` | Boost/Normal/… | ✅ | — not an ASHW endpoint | +| `/RemoteControl/SetBoostTemperatureHwc` | `SetHWBoostTemperature` | `Boost`, Temperature | ✅ | — not tested | +| `/RemoteControl/SetNormalTemperatureHwc` | `SetHWNormalTemperature` | `Normal`, Temperature | ✅ | — not tested | +| `/RemoteControl/SetHygieneSettingsHwc` | `SetHWHygieneSettings` | `Hygiene`, Temperature, `Frequency` | ✅ | — not tested | +| `/RemoteControl/SetApplianceModeHeatPumpHwc` | `SetModeForHeatPumpHWAppliances` | `Holiday` | ✅ | ✅ Holiday on/off (`set_hot_water_holiday`) | +| `/RemoteControl/SetHygieneSettingsHeatPumpHwc` | `SetHeatPumpHWCHygieneSettings` | `Hygiene` | ✅ | — not tested | +| `/RemoteControl/ApiGetTimerModeDetailsForHeatPumpHwcAppliance` | `ApiGetTimerModeDetailsForHeatPumpHwcAppliance` → details | `{HubId, ApplianceId}` | ✅ | ✅ reads (`HeatPumpHwcSchedule`) | +| `/RemoteControl/UpdateHeatPumpHwcSchedulePeriods` | schedule edit | — | ✅ | ❌ HTTP 403 to every body tried | + +What a live ASHW cylinder returned: + +- **Overview:** `ApplianceModes` 8192 (`Normal`, which the app shows as "Timer"), + `ActiveSetPointTemperature` 45 (the *water* target), `AvailableHotWater` 336 (litres — + the app shows "336 L Hot water"), `Capacity` 200, `HeatingUp`, `IsGreenEnergyActive`, + `HygieneFrequency` 7, `HolidayNumOfDays`, and `ErrorCode`/`WarningCode` as **integers**. + Every room-heater field (`RoomTemperature`, `OpenWindowEnabled`, `EcoStartEnabled`, setback, + away) is `null`. +- **Holiday:** `SetApplianceModeHeatPumpHwc` with `ApplianceModes` = 8, `Status` 1, + `Date` and `NumberOfDays` moved the status to 8200 with `HolidayNumOfDays` set and heating + stopped; `Status` 0 restored 8192. +- **Schedule:** `{HubId, ApplianceId, TimerPeriods: [{DayOfWeek, StartTime: 0, Mode: 2}]}` — + one period per day, no `TimerMode`. What `Mode` values mean, and whether `StartTime` is + minutes after midnight, is not yet known. +- **Schedule write:** wrapped bodies (`TimerModeSettings` or `Settings`, with or without + `TimerMode`, integer or `HH:MM:SS` times, `app_version` up to 3.0.0) all got **403**; + unwrapped bodies got **400**. The app's own request has not been captured. ### Reports / profiles / structure (context) @@ -211,10 +229,10 @@ Every ❌ in the "before 0.13.0" column above is now addressed: | "Off" | `set_frost_protect()` / `turn_off()` | | Advance / Manual / Eco modes | `set_advance()`, `set_manual()`, `set_eco_mode()` | | Setback write | `set_setback_temperature()`; capability `setback_write=True` | -| HWC / heat-pump HWC | `set_hot_water_*`, `get/set_heat_pump_hot_water_schedule` — implemented but ❌ untested | +| HWC / heat-pump HWC | `set_hot_water_*`, `get/set_heat_pump_hot_water_schedule` — implemented; Holiday and the schedule read since validated on an ASHW cylinder | | Temperature range | `MODE_TEMP_MIN`/`MAX` = 7–30 °C for Boost/Frost/Manual/Eco; capability `min_temp` corrected from 5 → 7 | -Still open: live validation of `SetSetbackTemperature` and the entire HWC surface. +Still open: live validation of `SetSetbackTemperature` and the HWC temperature, boost and hygiene writes. ### What 0.13.1 changed @@ -254,7 +272,10 @@ Still open: live validation of `SetSetbackTemperature` and the entire HWC surfac - **Additionally validated live on a QM100RF:** Boost/Away flag values, the `Status` engage/clear semantics, `BoostDuration`, and the dedicated `SetApplianceSetpointTemperature` endpoint. -- **APK-only (no hardware):** all HWC / heat-pump-HWC endpoints and `SetSetbackTemperature`. +- **Additionally validated live on an ASHW cylinder (EDL200UK-630RF):** the cylinder overview + fields, Holiday on/off via `SetApplianceModeHeatPumpHwc`, and the heat-pump schedule read. + `UpdateHeatPumpHwcSchedulePeriods` answers 403. +- **APK-only (no hardware):** the remaining HWC endpoints and `SetSetbackTemperature`. - **Resolved:** whether a *raised* Away target (>7) is honoured on Quantum — it is. Away is a settable **7–18 °C** setback that *defaults* to 7 (anti-freeze), so the common case is a low setpoint by design; a higher chosen value applies up to 18, diff --git a/openapi.yaml b/openapi.yaml index f741700..6b9f228 100644 --- a/openapi.yaml +++ b/openapi.yaml @@ -633,7 +633,7 @@ paths: /RemoteControl/SetApplianceModeHeatPumpHwc: post: - summary: Set an ASHW (heat-pump) cylinder mode (untested) + summary: Set an ASHW (heat-pump) cylinder mode (Holiday validated live) requestBody: $ref: "#/components/requestBodies/ApplianceModeRequest" responses: @@ -651,7 +651,7 @@ paths: /RemoteControl/ApiGetTimerModeDetailsForHeatPumpHwcAppliance: post: - summary: Read an ASHW cylinder's schedule (untested) + summary: Read an ASHW cylinder's schedule (validated live) requestBody: required: true content: @@ -673,7 +673,7 @@ paths: /RemoteControl/UpdateHeatPumpHwcSchedulePeriods: post: - summary: Write an ASHW cylinder's schedule periods (untested) + summary: Write an ASHW cylinder's schedule periods (answers 403 on a live cylinder) requestBody: required: true content: