From 33aead73521a851d7865b42a40b5ac7240356448 Mon Sep 17 00:00:00 2001 From: Mariusz Pacek Date: Sun, 25 Oct 2026 09:20:35 +0100 Subject: [PATCH] refactor: remove the CWU heater, the house has none The app showed the controller's heater signal as a device, a detail and a statistic. Drop it from the API (heaterOn, heaterHours) and the UI; the gateway still records output_heater as raw data. Co-Authored-By: Claude Opus 5.5 --- backend/README.md | 5 ++--- .../adapters/influx/InfluxSamples.java | 12 +++++------ .../home/backend/application/DayTally.java | 9 +++------ .../backend/application/HeatPumpService.java | 8 +------- .../home/backend/domain/HeatPumpSample.java | 2 +- .../home/backend/domain/HeatPumpView.java | 2 +- .../java/home/backend/domain/StatsView.java | 4 ++-- .../backend/adapters/http/ApiServerTest.java | 12 +++++------ .../influx/InfluxHistoryRepositoryTest.java | 6 +++--- .../application/ProgramServiceTest.java | 4 ++-- .../backend/application/ServicesTest.java | 10 +--------- .../backend/application/StatsServiceTest.java | 20 +++++++++---------- .../test/resources/api/heat-pump-unread.json | 1 - backend/src/test/resources/api/heat-pump.json | 1 - backend/src/test/resources/api/stats.json | 8 -------- .../influx/influx-query-heat-pump.json | 2 +- docs/frisko/rx910-se21.md | 2 +- docs/spec.md | 7 +++---- frontend/src/Details.tsx | 3 +-- frontend/src/History.test.tsx | 6 +----- frontend/src/Overview.test.tsx | 20 ------------------- frontend/src/Overview.tsx | 4 ++-- frontend/src/Stats.tsx | 7 +++---- frontend/src/format.ts | 3 --- frontend/src/types.ts | 3 --- 25 files changed, 49 insertions(+), 112 deletions(-) diff --git a/backend/README.md b/backend/README.md index fe6ad7c..94327db 100644 --- a/backend/README.md +++ b/backend/README.md @@ -10,7 +10,7 @@ the three values are null (`pi-sensor-stale.json`). `GET /api/stats?range=7d|30d` (example `src/test/resources/api/stats.json`; 400 otherwise, 502 when InfluxDB is down) is the heat pump per local day (Europe/Warsaw, today included) from the raw `heat_pump` polls: `days[]` with `date`, `compressorHours`, `compressorStarts`, `cwuHours`, `coHours`, -`heaterHours`, `avgOutdoorTemp`, `minCwuTemp`, `maxCwuTemp` (all null on a day without polls) and `totals` over the range. A poll holds its outputs +`avgOutdoorTemp`, `minCwuTemp`, `maxCwuTemp` (all null on a day without polls) and `totals` over the range. A poll holds its outputs until the next one, at most 600 s, so gaps add no hours; a start is a poll above 0 after a poll at 0, so the 300 s polling misses short cycles. `GET /api/program?days=14` (7 to 30, default 14; 400 otherwise, 502 when InfluxDB is down; example `src/test/resources/api/program.json`) is the weekly @@ -18,8 +18,7 @@ program as the history shows it, not read from the controller: `weekdays[]` from `comfort` (intervals `from`/`to`, local "HH:MM", "24:00" at midnight), `observedDays`, `agreeingDays` and `confidence` (`confirmed`: at least two days of that weekday agree within 5 minutes, `seen_once`, `no_data`). A boundary is the middle between the two polls where the period changes (`Periods`), rounded to 5 minutes; an override, ambiguous setpoints (e.g. during our writes) or a gap over 600 s drop the interval. -Today is left out. `/api/heat-pump` has `periods` (`cwu`, `room`: `comfort`, `economy`, `unknown`, `override`; `program`, `programText`), -and `heaterOn` is also true while `zb_state` = 2. +Today is left out. `/api/heat-pump` has `periods` (`cwu`, `room`: `comfort`, `economy`, `unknown`, `override`; `program`, `programText`). `GET /api/battery` is the Deye battery, example `src/test/resources/api/battery.json`: live `socPct`, `powerW` (+ discharge / − charge), `voltageV`, `currentA`, `temperatureC`, `today` and `total` energy (`chargeKwh`, `dischargeKwh`), null when the Deye is stale (older than 10 min or down). diff --git a/backend/src/main/java/home/backend/adapters/influx/InfluxSamples.java b/backend/src/main/java/home/backend/adapters/influx/InfluxSamples.java index ab2131c..325ab7e 100644 --- a/backend/src/main/java/home/backend/adapters/influx/InfluxSamples.java +++ b/backend/src/main/java/home/backend/adapters/influx/InfluxSamples.java @@ -22,7 +22,7 @@ final class InfluxSamples { } static String query(long fromMs, long toMs) { - return "SELECT output_compressor, output_cwu, output_co, output_heater, outdoor_temp, cwu_temp," + return "SELECT output_compressor, output_cwu, output_co, outdoor_temp, cwu_temp," + " program, target_room_temp, target_cwu_temp, setpoint_co_economy, setpoint_co_comfort," + " setpoint_cwu_economy, setpoint_cwu_comfort" + " FROM \"heat_pump\" WHERE time >= " + fromMs + "ms AND time < " + toMs + "ms"; @@ -43,8 +43,8 @@ final class InfluxSamples { for (JsonNode series : result.path("series")) { for (JsonNode row : series.path("values")) { samples.add(new HeatPumpSample(Instant.ofEpochMilli(row.get(0).asLong()), - number(row, 1), number(row, 2), number(row, 3), number(row, 4), - number(row, 5), number(row, 6), periods(row))); + number(row, 1), number(row, 2), number(row, 3), + number(row, 4), number(row, 5), periods(row))); } } } @@ -52,9 +52,9 @@ final class InfluxSamples { } private static Periods periods(JsonNode row) { - var targets = new Targets(number(row, 8), null, number(row, 9)); - var setpoints = new Setpoints(whole(row, 10), whole(row, 11), whole(row, 12), whole(row, 13)); - return Periods.of(whole(row, 7), targets, setpoints); + var targets = new Targets(number(row, 7), null, number(row, 8)); + var setpoints = new Setpoints(whole(row, 9), whole(row, 10), whole(row, 11), whole(row, 12)); + return Periods.of(whole(row, 6), targets, setpoints); } private static Integer whole(JsonNode row, int column) { diff --git a/backend/src/main/java/home/backend/application/DayTally.java b/backend/src/main/java/home/backend/application/DayTally.java index 26f1f2e..6b81452 100644 --- a/backend/src/main/java/home/backend/application/DayTally.java +++ b/backend/src/main/java/home/backend/application/DayTally.java @@ -13,7 +13,6 @@ final class DayTally { private double compressorSeconds; private double cwuSeconds; private double coSeconds; - private double heaterSeconds; private int starts; private int samples; private double outdoorSum; @@ -39,7 +38,6 @@ final class DayTally { compressorSeconds += on(s.compressor()) ? seconds : 0; cwuSeconds += on(s.cwu()) ? seconds : 0; coSeconds += on(s.co()) ? seconds : 0; - heaterSeconds += on(s.heater()) ? seconds : 0; } void start() { @@ -50,7 +48,6 @@ final class DayTally { compressorSeconds += other.compressorSeconds; cwuSeconds += other.cwuSeconds; coSeconds += other.coSeconds; - heaterSeconds += other.heaterSeconds; starts += other.starts; samples += other.samples; outdoorSum += other.outdoorSum; @@ -63,15 +60,15 @@ final class DayTally { Day day(LocalDate date) { if (samples == 0) { - return new Day(date, null, null, null, null, null, null, null, null); + return new Day(date, null, null, null, null, null, null, null); } return new Day(date, hours(compressorSeconds), starts, hours(cwuSeconds), hours(coSeconds), - hours(heaterSeconds), avgOutdoor(), minCwu, maxCwu); + avgOutdoor(), minCwu, maxCwu); } Totals totals() { return new Totals(hours(compressorSeconds), starts, hours(cwuSeconds), hours(coSeconds), - hours(heaterSeconds), avgOutdoor(), minCwu, maxCwu); + avgOutdoor(), minCwu, maxCwu); } static boolean on(Double output) { diff --git a/backend/src/main/java/home/backend/application/HeatPumpService.java b/backend/src/main/java/home/backend/application/HeatPumpService.java index 6b2ee2c..c83d3fc 100644 --- a/backend/src/main/java/home/backend/application/HeatPumpService.java +++ b/backend/src/main/java/home/backend/application/HeatPumpService.java @@ -23,7 +23,6 @@ import static home.backend.application.SnapshotFields.whole; public class HeatPumpService { private static final String DATABASE = "Dom_Pompa"; - private static final int HEATER_STATE = 2; private static final State[] STATES = State.values(); private final GatewayClient gateway; @@ -49,7 +48,7 @@ public class HeatPumpService { tenths(f, "target_cwu_temp")); var setpoints = new Setpoints(whole(f, "setpoint_co_economy"), whole(f, "setpoint_co_comfort"), whole(f, "setpoint_cwu_economy"), whole(f, "setpoint_cwu_comfort")); - return new HeatPumpView(time, stale, state(f), whole(f, "output_compressor"), heaterOn(f), + return new HeatPumpView(time, stale, state(f), whole(f, "output_compressor"), on(f, "alarm") || on(f, "alarm_code"), alarmCode, alarmText(alarmCode), new Pumps(on(f, "output_co"), on(f, "output_buffer"), on(f, "output_circulation"), on(f, "output_ground"), on(f, "output_cwu")), @@ -60,11 +59,6 @@ public class HeatPumpService { targets, setpoints, Periods.of(whole(f, "program"), targets, setpoints)); } - /** The heater output, or zb_state 2, which is the CWU heater too. */ - private static boolean heaterOn(Map f) { - return on(f, "output_heater") || Integer.valueOf(HEATER_STATE).equals(whole(f, "zb_state")); - } - /** Codes of register 4011 as the vendor app names them; unverified on our device. */ private static String alarmText(Integer code) { if (code == null || code == 0) { diff --git a/backend/src/main/java/home/backend/domain/HeatPumpSample.java b/backend/src/main/java/home/backend/domain/HeatPumpSample.java index 7bc065d..36ebbcf 100644 --- a/backend/src/main/java/home/backend/domain/HeatPumpSample.java +++ b/backend/src/main/java/home/backend/domain/HeatPumpSample.java @@ -3,6 +3,6 @@ package home.backend.domain; import java.time.Instant; /** One poll of the heat pump from InfluxDB; a field the poll did not carry is null. */ -public record HeatPumpSample(Instant t, Double compressor, Double cwu, Double co, Double heater, +public record HeatPumpSample(Instant t, Double compressor, Double cwu, Double co, Double outdoorTemp, Double cwuTemp, Periods periods) { } diff --git a/backend/src/main/java/home/backend/domain/HeatPumpView.java b/backend/src/main/java/home/backend/domain/HeatPumpView.java index 0081c92..9462e5b 100644 --- a/backend/src/main/java/home/backend/domain/HeatPumpView.java +++ b/backend/src/main/java/home/backend/domain/HeatPumpView.java @@ -6,7 +6,7 @@ import java.time.Instant; * What the heat pump does now (F1). Without a reading from the gateway {@code time} and the values are * null; {@code stale} is true when the device is down or the reading is older than 2 × 300 s. */ -public record HeatPumpView(Instant time, boolean stale, State state, Integer compressorPercent, boolean heaterOn, +public record HeatPumpView(Instant time, boolean stale, State state, Integer compressorPercent, boolean alarm, Integer alarmCode, String alarmText, Pumps pumps, Temperatures temperatures, Targets targets, Setpoints setpoints, Periods periods) { diff --git a/backend/src/main/java/home/backend/domain/StatsView.java b/backend/src/main/java/home/backend/domain/StatsView.java index e764964..dbce57c 100644 --- a/backend/src/main/java/home/backend/domain/StatsView.java +++ b/backend/src/main/java/home/backend/domain/StatsView.java @@ -10,10 +10,10 @@ import java.util.List; public record StatsView(String range, List days, Totals totals) { public record Day(LocalDate date, Double compressorHours, Integer compressorStarts, Double cwuHours, - Double coHours, Double heaterHours, Double avgOutdoorTemp, Double minCwuTemp, Double maxCwuTemp) { + Double coHours, Double avgOutdoorTemp, Double minCwuTemp, Double maxCwuTemp) { } public record Totals(double compressorHours, int compressorStarts, double cwuHours, double coHours, - double heaterHours, Double avgOutdoorTemp, Double minCwuTemp, Double maxCwuTemp) { + Double avgOutdoorTemp, Double minCwuTemp, Double maxCwuTemp) { } } diff --git a/backend/src/test/java/home/backend/adapters/http/ApiServerTest.java b/backend/src/test/java/home/backend/adapters/http/ApiServerTest.java index c861c35..ced4d7d 100644 --- a/backend/src/test/java/home/backend/adapters/http/ApiServerTest.java +++ b/backend/src/test/java/home/backend/adapters/http/ApiServerTest.java @@ -121,10 +121,10 @@ class ApiServerTest { throw new IOException("influx down"); } return List.of( - new HeatPumpSample(Instant.parse("2026-10-23T10:00:00Z"), 0.0, 0.0, 0.0, 0.0, 8.0, 44.0, Periods.UNKNOWN), - new HeatPumpSample(Instant.parse("2026-10-23T10:05:00Z"), 100.0, 100.0, 0.0, 0.0, 8.5, 43.0, Periods.UNKNOWN), - new HeatPumpSample(Instant.parse("2026-10-23T10:10:00Z"), 100.0, 0.0, 0.0, 100.0, 9.0, 45.0, Periods.UNKNOWN), - new HeatPumpSample(Instant.parse("2026-10-23T10:15:00Z"), 0.0, 0.0, 0.0, 0.0, 9.0, 46.0, Periods.UNKNOWN)); + new HeatPumpSample(Instant.parse("2026-10-23T10:00:00Z"), 0.0, 0.0, 0.0, 8.0, 44.0, Periods.UNKNOWN), + new HeatPumpSample(Instant.parse("2026-10-23T10:05:00Z"), 100.0, 100.0, 0.0, 8.5, 43.0, Periods.UNKNOWN), + new HeatPumpSample(Instant.parse("2026-10-23T10:10:00Z"), 100.0, 0.0, 0.0, 9.0, 45.0, Periods.UNKNOWN), + new HeatPumpSample(Instant.parse("2026-10-23T10:15:00Z"), 0.0, 0.0, 0.0, 9.0, 46.0, Periods.UNKNOWN)); }; verdict = Status.APPLIED; restoreOutcome = Outcome.RESTORED; @@ -256,7 +256,7 @@ class ApiServerTest { "co_buffer_temp", 16.9, "cwu_temp", 13.6, "ground_in_temp", 16.0, "ground_out_temp", 14.3, "pump_outlet_temp", 19.7, "outdoor_avg_temp", 4.8, "target_room_temp", 18.0)); pump.putAll(Map.of("target_co_temp", 31.0, "target_cwu_temp", 8.0, "pump_mode", 0.0, - "output_compressor", 0.0, "output_heater", 0.0, "alarm", 0.0, + "output_compressor", 0.0, "alarm", 0.0, "setpoint_co_economy", 18.0, "setpoint_co_comfort", 19.0, "setpoint_cwu_economy", 8.0)); pump.putAll(Map.of("setpoint_cwu_comfort", 45.0, "output_co", 0.0, "output_buffer", 0.0, "output_circulation", 0.0, "output_ground", 0.0, "output_cwu", 0.0, "program", 1.0)); @@ -543,7 +543,7 @@ class ApiServerTest { List polls = new ArrayList<>(); for (int minute = 20; minute <= 125; minute += 5) { Instant t = Instant.parse("2026-10-23T09:00:00Z").plus(Duration.ofMinutes(minute)); - polls.add(new HeatPumpSample(t, null, null, null, null, null, null, + polls.add(new HeatPumpSample(t, null, null, null, null, null, minute >= 25 && minute <= 60 ? comfort : economy)); } return polls; diff --git a/backend/src/test/java/home/backend/adapters/influx/InfluxHistoryRepositoryTest.java b/backend/src/test/java/home/backend/adapters/influx/InfluxHistoryRepositoryTest.java index 9319758..8c24169 100644 --- a/backend/src/test/java/home/backend/adapters/influx/InfluxHistoryRepositoryTest.java +++ b/backend/src/test/java/home/backend/adapters/influx/InfluxHistoryRepositoryTest.java @@ -218,16 +218,16 @@ class InfluxHistoryRepositoryTest { List samples = repository(TIMEOUT).samples(FROM, TO); assertEquals("Dom_Pompa", requests.get(0).get("db")); - assertEquals("SELECT output_compressor, output_cwu, output_co, output_heater, outdoor_temp, cwu_temp," + assertEquals("SELECT output_compressor, output_cwu, output_co, outdoor_temp, cwu_temp," + " program, target_room_temp, target_cwu_temp, setpoint_co_economy, setpoint_co_comfort," + " setpoint_cwu_economy, setpoint_cwu_comfort" + " FROM \"heat_pump\" WHERE time >= 1792800000000ms AND time < 1792821600000ms", requests.get(0).get("q")); assertEquals(3, samples.size()); - assertEquals(new HeatPumpSample(Instant.ofEpochMilli(1792771500000L), 100.0, 100.0, 0.0, 0.0, 8.5, 43.9, + assertEquals(new HeatPumpSample(Instant.ofEpochMilli(1792771500000L), 100.0, 100.0, 0.0, 8.5, 43.9, new Periods(Period.COMFORT, Period.COMFORT, 1, null)), samples.get(1)); assertEquals(Period.ECONOMY, samples.get(0).periods().cwu()); - assertEquals(new HeatPumpSample(Instant.ofEpochMilli(1792771800000L), 50.0, 0.0, 100.0, 100.0, null, null, Periods.UNKNOWN), + assertEquals(new HeatPumpSample(Instant.ofEpochMilli(1792771800000L), 50.0, 0.0, 100.0, null, null, Periods.UNKNOWN), samples.get(2)); } diff --git a/backend/src/test/java/home/backend/application/ProgramServiceTest.java b/backend/src/test/java/home/backend/application/ProgramServiceTest.java index 7e39d45..131fbe3 100644 --- a/backend/src/test/java/home/backend/application/ProgramServiceTest.java +++ b/backend/src/test/java/home/backend/application/ProgramServiceTest.java @@ -41,7 +41,7 @@ class ProgramServiceTest { } private static HeatPumpSample poll(String utc, Periods periods) { - return new HeatPumpSample(Instant.parse(utc), null, null, null, null, null, null, periods); + return new HeatPumpSample(Instant.parse(utc), null, null, null, null, null, periods); } /** CWU economy 8, comfort 43; room economy 18, comfort 19, as on the real controller. */ @@ -67,7 +67,7 @@ class ProgramServiceTest { List samples = new ArrayList<>(); Instant end = Instant.parse(to + ":00Z"); for (Instant t = Instant.parse(from + ":00Z"); !t.isAfter(end); t = t.plus(Duration.ofMinutes(POLL_MINUTES))) { - samples.add(new HeatPumpSample(t, null, null, null, null, null, null, periods)); + samples.add(new HeatPumpSample(t, null, null, null, null, null, periods)); } return samples; } diff --git a/backend/src/test/java/home/backend/application/ServicesTest.java b/backend/src/test/java/home/backend/application/ServicesTest.java index 746533b..4c6303d 100644 --- a/backend/src/test/java/home/backend/application/ServicesTest.java +++ b/backend/src/test/java/home/backend/application/ServicesTest.java @@ -72,13 +72,6 @@ class ServicesTest { assertEquals(State.HEATING_BOTH, heatPump(Map.of("pump_mode", 3.0)).state()); } - @Test - void heatPumpHeaterIsOnForTheOutputOrForZbState2() throws IOException { - assertTrue(heatPump(Map.of("output_heater", 100.0)).heaterOn()); - assertTrue(heatPump(Map.of("output_heater", 0.0, "zb_state", 2.0)).heaterOn()); - assertFalse(heatPump(Map.of("output_heater", 0.0, "zb_state", 0.0)).heaterOn()); - } - @Test void heatPumpPeriodsComeFromTargetsAndSetpoints() throws IOException { HeatPumpView view = heatPump(Map.of("program", 1.0, "target_cwu_temp", 43.0, "setpoint_cwu_economy", 8.0, @@ -107,12 +100,11 @@ class ServicesTest { @Test void heatPumpMapsOutputsAlarmAndTime() throws IOException { - HeatPumpView view = heatPump(Map.of("output_compressor", 66.0, "output_heater", 100.0, + HeatPumpView view = heatPump(Map.of("output_compressor", 66.0, "output_cwu", 100.0, "output_co", 0.0, "alarm", 4.0, "setpoint_cwu_economy", 8.0)); assertEquals(READ, view.time()); assertEquals(66, view.compressorPercent()); - assertTrue(view.heaterOn()); assertTrue(view.alarm()); assertTrue(view.pumps().cwu()); assertFalse(view.pumps().co()); diff --git a/backend/src/test/java/home/backend/application/StatsServiceTest.java b/backend/src/test/java/home/backend/application/StatsServiceTest.java index 89a4347..77c0fda 100644 --- a/backend/src/test/java/home/backend/application/StatsServiceTest.java +++ b/backend/src/test/java/home/backend/application/StatsServiceTest.java @@ -36,7 +36,7 @@ class StatsServiceTest { } private static HeatPumpSample compressor(String time, double stage) { - return new HeatPumpSample(Instant.parse(time), stage, 0.0, 0.0, 0.0, null, null, Periods.UNKNOWN); + return new HeatPumpSample(Instant.parse(time), stage, 0.0, 0.0, null, null, Periods.UNKNOWN); } private static Day day(StatsView view, String date) { @@ -159,15 +159,14 @@ class StatsServiceTest { @Test void sumsEachOutputSeparately() throws IOException { StatsView view = stats("7d", NOW, - new HeatPumpSample(Instant.parse("2026-10-23T10:00:00Z"), 100.0, 100.0, 0.0, 0.0, 10.0, 40.0, Periods.UNKNOWN), - new HeatPumpSample(Instant.parse("2026-10-23T10:05:00Z"), 100.0, 0.0, 100.0, 100.0, 12.0, 38.0, Periods.UNKNOWN), - new HeatPumpSample(Instant.parse("2026-10-23T10:10:00Z"), 0.0, 0.0, 0.0, 0.0, null, null, Periods.UNKNOWN)); + new HeatPumpSample(Instant.parse("2026-10-23T10:00:00Z"), 100.0, 100.0, 0.0, 10.0, 40.0, Periods.UNKNOWN), + new HeatPumpSample(Instant.parse("2026-10-23T10:05:00Z"), 100.0, 0.0, 100.0, 12.0, 38.0, Periods.UNKNOWN), + new HeatPumpSample(Instant.parse("2026-10-23T10:10:00Z"), 0.0, 0.0, 0.0, null, null, Periods.UNKNOWN)); Day d = day(view, "2026-10-23"); assertEquals(0.17, d.compressorHours()); assertEquals(0.08, d.cwuHours()); assertEquals(0.08, d.coHours()); - assertEquals(0.08, d.heaterHours()); assertEquals(11.0, d.avgOutdoorTemp()); assertEquals(38.0, d.minCwuTemp()); assertEquals(40.0, d.maxCwuTemp()); @@ -176,13 +175,12 @@ class StatsServiceTest { @Test void totalsCoverTheWholeRangeWithTheTemperatureOfAllPolls() throws IOException { StatsView view = stats("7d", NOW, - new HeatPumpSample(Instant.parse("2026-10-22T10:00:00Z"), 100.0, 0.0, 0.0, 0.0, 10.0, 30.0, Periods.UNKNOWN), - new HeatPumpSample(Instant.parse("2026-10-22T10:05:00Z"), 0.0, 0.0, 0.0, 0.0, 10.0, 31.0, Periods.UNKNOWN), - new HeatPumpSample(Instant.parse("2026-10-23T10:00:00Z"), 100.0, 0.0, 0.0, 100.0, 4.0, 45.0, Periods.UNKNOWN), - new HeatPumpSample(Instant.parse("2026-10-23T10:05:00Z"), 0.0, 0.0, 0.0, 0.0, 4.0, 44.0, Periods.UNKNOWN)); + new HeatPumpSample(Instant.parse("2026-10-22T10:00:00Z"), 100.0, 0.0, 0.0, 10.0, 30.0, Periods.UNKNOWN), + new HeatPumpSample(Instant.parse("2026-10-22T10:05:00Z"), 0.0, 0.0, 0.0, 10.0, 31.0, Periods.UNKNOWN), + new HeatPumpSample(Instant.parse("2026-10-23T10:00:00Z"), 100.0, 0.0, 0.0, 4.0, 45.0, Periods.UNKNOWN), + new HeatPumpSample(Instant.parse("2026-10-23T10:05:00Z"), 0.0, 0.0, 0.0, 4.0, 44.0, Periods.UNKNOWN)); assertEquals(0.17, view.totals().compressorHours()); - assertEquals(0.08, view.totals().heaterHours()); assertEquals(7.0, view.totals().avgOutdoorTemp()); assertEquals(30.0, view.totals().minCwuTemp()); assertEquals(45.0, view.totals().maxCwuTemp()); @@ -191,7 +189,7 @@ class StatsServiceTest { @Test void aMissingFieldCountsAsOffAndUnknownStateIsNoStart() throws IOException { StatsView view = stats("7d", NOW, - new HeatPumpSample(Instant.parse("2026-10-23T10:00:00Z"), null, null, null, null, null, null, Periods.UNKNOWN), + new HeatPumpSample(Instant.parse("2026-10-23T10:00:00Z"), null, null, null, null, null, Periods.UNKNOWN), compressor("2026-10-23T10:05:00Z", 100)); assertEquals(0, day(view, "2026-10-23").compressorStarts()); diff --git a/backend/src/test/resources/api/heat-pump-unread.json b/backend/src/test/resources/api/heat-pump-unread.json index 6c43313..f88d64e 100644 --- a/backend/src/test/resources/api/heat-pump-unread.json +++ b/backend/src/test/resources/api/heat-pump-unread.json @@ -3,7 +3,6 @@ "stale": true, "state": null, "compressorPercent": null, - "heaterOn": false, "alarm": false, "alarmCode": null, "alarmText": null, diff --git a/backend/src/test/resources/api/heat-pump.json b/backend/src/test/resources/api/heat-pump.json index 44f0177..9dc7dfc 100644 --- a/backend/src/test/resources/api/heat-pump.json +++ b/backend/src/test/resources/api/heat-pump.json @@ -3,7 +3,6 @@ "stale": false, "state": "IDLE", "compressorPercent": 0, - "heaterOn": false, "alarm": false, "alarmCode": null, "alarmText": null, diff --git a/backend/src/test/resources/api/stats.json b/backend/src/test/resources/api/stats.json index 5724766..d286a32 100644 --- a/backend/src/test/resources/api/stats.json +++ b/backend/src/test/resources/api/stats.json @@ -7,7 +7,6 @@ "compressorStarts": null, "cwuHours": null, "coHours": null, - "heaterHours": null, "avgOutdoorTemp": null, "minCwuTemp": null, "maxCwuTemp": null @@ -18,7 +17,6 @@ "compressorStarts": null, "cwuHours": null, "coHours": null, - "heaterHours": null, "avgOutdoorTemp": null, "minCwuTemp": null, "maxCwuTemp": null @@ -29,7 +27,6 @@ "compressorStarts": null, "cwuHours": null, "coHours": null, - "heaterHours": null, "avgOutdoorTemp": null, "minCwuTemp": null, "maxCwuTemp": null @@ -40,7 +37,6 @@ "compressorStarts": null, "cwuHours": null, "coHours": null, - "heaterHours": null, "avgOutdoorTemp": null, "minCwuTemp": null, "maxCwuTemp": null @@ -51,7 +47,6 @@ "compressorStarts": null, "cwuHours": null, "coHours": null, - "heaterHours": null, "avgOutdoorTemp": null, "minCwuTemp": null, "maxCwuTemp": null @@ -62,7 +57,6 @@ "compressorStarts": 1, "cwuHours": 0.08, "coHours": 0.0, - "heaterHours": 0.08, "avgOutdoorTemp": 8.6, "minCwuTemp": 43.0, "maxCwuTemp": 46.0 @@ -73,7 +67,6 @@ "compressorStarts": null, "cwuHours": null, "coHours": null, - "heaterHours": null, "avgOutdoorTemp": null, "minCwuTemp": null, "maxCwuTemp": null @@ -84,7 +77,6 @@ "compressorStarts": 1, "cwuHours": 0.08, "coHours": 0.0, - "heaterHours": 0.08, "avgOutdoorTemp": 8.6, "minCwuTemp": 43.0, "maxCwuTemp": 46.0 diff --git a/backend/src/test/resources/influx/influx-query-heat-pump.json b/backend/src/test/resources/influx/influx-query-heat-pump.json index d0d3258..c6b75fc 100644 --- a/backend/src/test/resources/influx/influx-query-heat-pump.json +++ b/backend/src/test/resources/influx/influx-query-heat-pump.json @@ -1 +1 @@ -{"results":[{"statement_id":0,"series":[{"name":"heat_pump","columns":["time","output_compressor","output_cwu","output_co","output_heater","outdoor_temp","cwu_temp","program","target_room_temp","target_cwu_temp","setpoint_co_economy","setpoint_co_comfort","setpoint_cwu_economy","setpoint_cwu_comfort"],"values":[[1792771200000,0,0,0,0,8.5,44.2,1,18.0,8.0,18,19,8,43],[1792771500000,100,100,0,0,8.5,43.9,1,19.0,43.0,18,19,8,43],[1792771800000,50,0,100,100,null,null,null,null,null,null,null,null,null]]}]}]} +{"results":[{"statement_id":0,"series":[{"name":"heat_pump","columns":["time","output_compressor","output_cwu","output_co","outdoor_temp","cwu_temp","program","target_room_temp","target_cwu_temp","setpoint_co_economy","setpoint_co_comfort","setpoint_cwu_economy","setpoint_cwu_comfort"],"values":[[1792771200000,0,0,0,8.5,44.2,1,18.0,8.0,18,19,8,43],[1792771500000,100,100,0,8.5,43.9,1,19.0,43.0,18,19,8,43],[1792771800000,50,0,100,null,null,null,null,null,null,null,null,null]]}]}]} diff --git a/docs/frisko/rx910-se21.md b/docs/frisko/rx910-se21.md index 6abb5ae..819afc9 100644 --- a/docs/frisko/rx910-se21.md +++ b/docs/frisko/rx910-se21.md @@ -27,7 +27,7 @@ unless marked otherwise. | Outputs | 4112 CO, 4113 buffer, 4114 compressor (0/33/66/100), 4115 heater signal (heater not connected), 4116 circulation, 4117 ground, 4118 CWU | 0 or 100 | | Setpoints (F3) | 4131/4132 CO economy/comfort (5–35), 4143/4144 CWU economy/comfort (5–70) | whole °C | | Protection | 4158 TmaxPc = 52 °C, 4149 TmaxCWU = 55, 4148 HistCWU = 5, 4146 PriorCWU = 1, 4147 heater signal delay = 20 | read 2026-10-23 | -| Compressor | 4166 zwlZSt(II) = 20 min (stage 2 delay, matches history), 4165 zwlZSt(..) = 10 (likely stage 3, never seen), 4162 t_wylSpr = 5 min (minimum off, matches the 18:20 stop / 18:25 restart), 4160 t_zwlZalSpr = 20, 4161 t_zwlWylPdz = 20, 4163 t_minSpr = 160, 4164 t_maxSpr = 230 (units unknown); 4157 TminDz = −3, 4159 ∆TmaxDz = 10 | FC3 read 2026-10-25 | +| Compressor | 4166 zwlZSt(II) = 30 min (stage 2 delay; was 20, matching history, until the owner-approved one-time write of 2026-10-25, read back 30), 4165 zwlZSt(..) = 10 (likely stage 3, never seen), 4162 t_wylSpr = 5 min (minimum off, matches the 18:20 stop / 18:25 restart), 4160 t_zwlZalSpr = 20, 4161 t_zwlWylPdz = 20, 4163 t_minSpr = 160, 4164 t_maxSpr = 230 (units unknown); 4157 TminDz = −3, 4159 ∆TmaxDz = 10 | FC3 read 2026-10-25, 4166 written and read back 2026-10-25 | | CWU extras | 4150 TpodbCWU = 5, 4151 t_podbCWU = 15, disinfection 4152–4154 = 7 days / 60 °C / 60 min (off: 4145 = 0), 4155/4156 t_zal/wylCCW = 2/2; 4246, 4247 read garbage (not on this firmware) | FC3 read 2026-10-25 | Unknown: 4119 `WyCHL` reads −14308. Vendor-app findings, all unverified: [app-knowledge.md](app-knowledge.md). diff --git a/docs/spec.md b/docs/spec.md index 64c9663..d7fbbf5 100644 --- a/docs/spec.md +++ b/docs/spec.md @@ -74,11 +74,10 @@ Layers in both apps, dependencies point inwards only: Functional: - F1 Live state of the heat pump: idle / heating CWU / heating CO, compressor stage, pumps, - heater, alarm, current targets and temperatures; the BME280 reading (temperature, humidity, + alarm, current targets and temperatures; the BME280 reading (temperature, humidity, pressure) of the living room. An "Odśwież" button asks for one extra poll of each device, at most once - a minute per device (the MK01 takes one client). It also shows the period (comfort or economy) of CWU and room, - and the controller's heater signal (the heater is not connected). -- F2 History for the last 7 days, and per-day heat pump statistics for 7 or 30 days (compressor, CWU, CO and heater hours, + a minute per device (the MK01 takes one client). It also shows the period (comfort or economy) of CWU and room. +- F2 History for the last 7 days, and per-day heat pump statistics for 7 or 30 days (compressor, CWU and CO hours, starts "at least" because polls are 5 minutes apart). - F3 Change four setpoints: economy and comfort temperature of CWU (4143, 4144) and CO (4131, 4132). Any value within limits, or one tap on a named preset. Limits and presets live in diff --git a/frontend/src/Details.tsx b/frontend/src/Details.tsx index 8690055..e0d57f8 100644 --- a/frontend/src/Details.tsx +++ b/frontend/src/Details.tsx @@ -1,5 +1,5 @@ import type { ReactNode } from "react"; -import { celsius, degrees, HEATER_SIGNAL, number, periodText } from "./format"; +import { celsius, degrees, number, periodText } from "./format"; import type { HeatPump, PiSensor } from "./types"; import { Card, Row } from "./ui"; @@ -37,7 +37,6 @@ export function TemperatureDetails({ hp, pi }: { hp: HeatPump; pi: PiSensor | un - {hp.heaterOn && } diff --git a/frontend/src/History.test.tsx b/frontend/src/History.test.tsx index 1afaa14..8b88d8e 100644 --- a/frontend/src/History.test.tsx +++ b/frontend/src/History.test.tsx @@ -50,15 +50,11 @@ describe("Historia views", () => { }); describe("Statystyki", () => { - it("shows the totals and the starts as a minimum, with the heater signal neutral", async () => { + it("shows the totals and the starts as a minimum", async () => { await open(); expect(await screen.findByText("Załączenia sprężarki, co najmniej")).toBeInTheDocument(); expect(screen.getAllByText("0,2 h").length).toBeGreaterThan(0); - const tile = screen.getByText("Sygnał grzałki (niepodłączona)").closest("div")!; - expect(tile).toHaveTextContent("0,1 h"); - expect(tile.className).not.toMatch(/alarm|run/); - expect(screen.queryByText("Prąd z grzałki jest drogi")).toBeNull(); expect(screen.queryByRole("status")).toBeNull(); }); diff --git a/frontend/src/Overview.test.tsx b/frontend/src/Overview.test.tsx index a90fbba..9b85159 100644 --- a/frontend/src/Overview.test.tsx +++ b/frontend/src/Overview.test.tsx @@ -174,24 +174,4 @@ describe("Overview", () => { expect((await screen.findByRole("region", { name: "Teraz" })).children[0]).toHaveTextContent(/^Ciepła woda13,6 °Ccel 8,0 °C$/); }); - - it("shows the heater as the controller's signal, without a warning or a power figure", async () => { - serveHeatPump({ ...fresh(example("heat-pump")), heaterOn: true }); - const Overview = await overview(); - render(); - - expect(await screen.findByText("sprężarka, grzałka: sygnał sterownika (niepodłączona)")).toBeInTheDocument(); - expect(screen.getByText("Grzałka").closest("section")).toHaveTextContent("sygnał sterownika (niepodłączona)"); - expect(screen.queryByText(/Grzałka elektryczna/)).toBeNull(); - expect(screen.queryByText(/0,9 kW/)).toBeNull(); - }); - - it("says nothing about the heater while its output is off", async () => { - serveHeatPump(fresh(example("heat-pump"))); - const Overview = await overview(); - render(); - - expect(await screen.findByText("sprężarka")).toBeInTheDocument(); - expect(screen.queryByText("Grzałka")).toBeNull(); - }); }); diff --git a/frontend/src/Overview.tsx b/frontend/src/Overview.tsx index 0b17bb5..10bc6e0 100644 --- a/frontend/src/Overview.tsx +++ b/frontend/src/Overview.tsx @@ -1,5 +1,5 @@ import type { ReactNode } from "react"; -import { celsius, HEATER_SIGNAL, hour, longDate, number, percent, periodText, power } from "./format"; +import { celsius, hour, longDate, number, percent, periodText, power } from "./format"; import { Icon, type IconName } from "./icons"; import { flowing } from "./flow"; import { usePoll } from "./poll"; @@ -108,7 +108,7 @@ function Tile({ icon, title, detail, value, status }: { function DeviceTiles({ hp }: { hp: HeatPump }) { return (
- 0)} /> (d.compressorStarts ?? 0) > TAKTING_STARTS_PER_DAY); } -function Tile({ label, value, wide = false }: { label: string; value: string; wide?: boolean }) { +function Tile({ label, value }: { label: string; value: string }) { return ( -
+

{value}

{label}

@@ -40,12 +40,11 @@ function Tile({ label, value, wide = false }: { label: string; value: string; wi function Totals({ stats }: { stats: StatsData }) { const t = stats.totals; return ( -
+
-
); } diff --git a/frontend/src/format.ts b/frontend/src/format.ts index cfc3cb3..c51428f 100644 --- a/frontend/src/format.ts +++ b/frontend/src/format.ts @@ -104,6 +104,3 @@ export function periodText(period: Period, programText: string | null): string | if (period === "override") return programText ? `program: ${programText}` : "program inny niż tygodniowy"; return null; } - -/** The electric heater is not wired (owner, 2026-10-25): output 4115 is only the controller's signal. */ -export const HEATER_SIGNAL = "sygnał sterownika (niepodłączona)"; diff --git a/frontend/src/types.ts b/frontend/src/types.ts index 41f79a6..3ce662d 100644 --- a/frontend/src/types.ts +++ b/frontend/src/types.ts @@ -16,7 +16,6 @@ export interface HeatPump { stale: boolean; state: PumpState | null; compressorPercent: number | null; - heaterOn: boolean | null; alarm: boolean; alarmCode: number | null; alarmText: string | null; @@ -260,7 +259,6 @@ export interface StatsDay { compressorStarts: number | null; cwuHours: number | null; coHours: number | null; - heaterHours: number | null; avgOutdoorTemp: number | null; minCwuTemp: number | null; maxCwuTemp: number | null; @@ -271,7 +269,6 @@ export interface StatsTotals { compressorStarts: number; cwuHours: number; coHours: number; - heaterHours: number; avgOutdoorTemp: number | null; minCwuTemp: number | null; maxCwuTemp: number | null;