Merge PR #104: refactor: remove the CWU heater, the house has none
This commit is contained in:
commit
5bda4cb7ec
25 files changed
+49
-112
No files matched your search
+2
-3
@@ -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).
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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<String, Double> 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) {
|
||||
|
||||
@@ -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) {
|
||||
}
|
||||
@@ -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) {
|
||||
|
||||
|
||||
@@ -10,10 +10,10 @@ import java.util.List;
|
||||
public record StatsView(String range, List<Day> 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) {
|
||||
}
|
||||
}
|
||||
@@ -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<HeatPumpSample> 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;
|
||||
|
||||
@@ -218,16 +218,16 @@ class InfluxHistoryRepositoryTest {
|
||||
List<HeatPumpSample> 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));
|
||||
}
|
||||
|
||||
|
||||
@@ -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<HeatPumpSample> 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;
|
||||
}
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
"stale": true,
|
||||
"state": null,
|
||||
"compressorPercent": null,
|
||||
"heaterOn": false,
|
||||
"alarm": false,
|
||||
"alarmCode": null,
|
||||
"alarmText": null,
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
"stale": false,
|
||||
"state": "IDLE",
|
||||
"compressorPercent": 0,
|
||||
"heaterOn": false,
|
||||
"alarm": false,
|
||||
"alarmCode": null,
|
||||
"alarmText": null,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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]]}]}]}
|
||||
@@ -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).
|
||||
|
||||
+3
-4
@@ -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
|
||||
|
||||
@@ -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
|
||||
<Row label="Bufor CO" value={celsius(t.coBuffer)} />
|
||||
<Row label="Bufor CO, cel" value={celsius(targets.co)} />
|
||||
<Row label="Wyjście pompy" value={celsius(t.pumpOutlet)} />
|
||||
{hp.heaterOn && <Row label="Grzałka" value={HEATER_SIGNAL} />}
|
||||
</Group>
|
||||
<Group title="Dolne źródło">
|
||||
<Row label="Do pompy" value={celsius(t.groundIn)} />
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
|
||||
|
||||
@@ -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<HeatPump>("heat-pump")), heaterOn: true });
|
||||
const Overview = await overview();
|
||||
render(<Overview />);
|
||||
|
||||
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(<Overview />);
|
||||
|
||||
expect(await screen.findByText("sprężarka")).toBeInTheDocument();
|
||||
expect(screen.queryByText("Grzałka")).toBeNull();
|
||||
});
|
||||
});
|
||||
@@ -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 (
|
||||
<section aria-label="Stan urządzeń" className="grid gap-3 sm:grid-cols-2 sm:gap-4">
|
||||
<Tile icon="fan" title="Pompa ciepła" detail={hp.heaterOn ? `sprężarka, grzałka: ${HEATER_SIGNAL}` : "sprężarka"}
|
||||
<Tile icon="fan" title="Pompa ciepła" detail="sprężarka"
|
||||
value={hp.compressorPercent == null ? "—" : `${hp.compressorPercent} %`}
|
||||
status={running(hp.compressorPercent == null ? null : hp.compressorPercent > 0)} />
|
||||
<Tile icon="tank" title="Zbiornik CWU" detail={`cyrkulacja ${onOff(hp.pumps.circulation, "pracuje", "stoi")}`}
|
||||
|
||||
@@ -28,9 +28,9 @@ export function takting(days: StatsDay[]) {
|
||||
return days.some((d) => (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 (
|
||||
<div className={`rounded-xl border border-edge bg-page p-3 ${wide ? "col-span-2 lg:col-span-1" : ""}`}>
|
||||
<div className="rounded-xl border border-edge bg-page p-3">
|
||||
<p className="text-2xl font-bold tabular-nums">{value}</p>
|
||||
<p className="text-sm text-soft">{label}</p>
|
||||
</div>
|
||||
@@ -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 (
|
||||
<div className="mb-4 grid grid-cols-2 gap-3 lg:grid-cols-5">
|
||||
<div className="mb-4 grid grid-cols-2 gap-3 lg:grid-cols-4">
|
||||
<Tile label="Sprężarka" value={hours(t.compressorHours)} />
|
||||
<Tile label="Załączenia sprężarki, co najmniej" value={number(t.compressorStarts)} />
|
||||
<Tile label="Ciepła woda (CWU)" value={hours(t.cwuHours)} />
|
||||
<Tile label="Ogrzewanie (CO)" value={hours(t.coHours)} />
|
||||
<Tile label="Sygnał grzałki (niepodłączona)" value={hours(t.heaterHours)} wide />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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)";
|
||||
@@ -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;
|
||||
|
||||
Reference in new issue
Block a user