Merge PR #100: fix: the CWU heater is not connected, show its output as a controller signal
CI / Check (gateway, backend, frontend) (push) Failing after 1m15s
CI / Check (gateway, backend, frontend) (push) Failing after 1m15s
This commit is contained in:
commit
eec29666eb
10 files changed
+44
-71
No files matched your search
@@ -48,7 +48,14 @@ Verified from history, 2026-10-24, 167 samples. Rule: compare the target with th
|
||||
so comfort periods start one hour early in local time (about 10:25 and 16:55) until the clock is set.
|
||||
The app should not assume program times are local civil time.
|
||||
|
||||
## Heater (output_heater 4115, zb_state 4228)
|
||||
## Heater signal (output_heater 4115, zb_state 4228)
|
||||
|
||||
The electric CWU heater is NOT physically connected (owner, 2026-10-25). Output 4115 = 100 and
|
||||
`zb_state` 2 only mean the controller's heater signal after 20 min of a CWU cycle; nothing is heated
|
||||
by it. Below, "heater" means that signal. The meter step at +20 min is real but its cause is
|
||||
unexplained; hypotheses: a compressor stage change (the controller does it at the same delay), another
|
||||
load on the same relay or phase, a circulation or pump switched with the signal. Do not read it as
|
||||
heater power. To test: log stage 4114 and the pumps' outputs around +20 min and compare the per-phase step.
|
||||
|
||||
Episodes in `heat_pump` (10-24); meter steps from `electricity.PRAD2` `total_power_active`.
|
||||
|
||||
@@ -71,11 +78,11 @@ Verified from history:
|
||||
outlet 51.7, compressor and heater off, outlet 42.1 five minutes later, cwu 42.2 below target).
|
||||
The compressor restarts ~18:25:30 (stage 33) without the heater (not 20 min) and stops at 18:30 when
|
||||
cwu reaches 43.0. Evening heater runs last 1-10 min (meter), ending at the outlet limit.
|
||||
- Energy at ~0.9 kW: episode 1 about 0.3 kWh; an evening run 0.02-0.15 kWh. Small next to the
|
||||
compressor (+1.4-1.7 kW at stage 33, about +3 kW with the pumps at 66).
|
||||
- The 0.67-0.96 kW step lasts as long as the signal; it is small next to the compressor (+1.4-1.7 kW
|
||||
at stage 33, about +3 kW with the pumps at 66).
|
||||
|
||||
Hypothesis: the heater delay is measured from the compressor start of a CWU request, not from a
|
||||
temperature condition. A cycle that finishes within 20 min never uses it.
|
||||
Hypothesis: the signal delay is measured from the compressor start of a CWU request, not from a
|
||||
temperature condition. A cycle that finishes within 20 min never raises it.
|
||||
|
||||
## CWU heating cycles (legacy POMPA, 2026-09-10..10-24, outlet and cwu rise)
|
||||
|
||||
|
||||
@@ -24,9 +24,9 @@ unless marked otherwise.
|
||||
| Temperatures | 4065 outdoor, 4066 indoor, 4067 CO/buffer, 4069 CWU, 4070/4071 ground in/out, 4072 pump outlet, 4199 avg outdoor | X10 |
|
||||
| Targets now | 4201 room, 4202 CO supply, 4203 CWU | X10; show economy vs comfort period |
|
||||
| State | 4227 pump (0 stop, 1 CO, 2 CWU, 3 both), 4226 program, 4221 alarm, 4073 pressure switch | |
|
||||
| Outputs | 4112 CO, 4113 buffer, 4114 compressor (0/33/66/100), 4115 heater, 4116 circulation, 4117 ground, 4118 CWU | 0 or 100 |
|
||||
| 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 delay = 20 | read 2026-10-23 |
|
||||
| Protection | 4158 TmaxPc = 52 °C, 4149 TmaxCWU = 55, 4148 HistCWU = 5, 4146 PriorCWU = 1, 4147 heater signal delay = 20 | read 2026-10-23 |
|
||||
|
||||
Unknown: 4119 `WyCHL` reads −14308. Vendor-app findings, all unverified: [app-knowledge.md](app-knowledge.md).
|
||||
|
||||
@@ -44,9 +44,10 @@ the Frisko app. Party mode (4140 = 2, hours in 4142) is a simpler "heat now" ove
|
||||
Slow morning rises with the pump off come from the solar collector.
|
||||
- The pump outlet tops out at ~52 °C (TmaxPc), so the pump alone reaches ~47–48 °C in the tank.
|
||||
Higher CWU values in history occur only in summer (solar).
|
||||
- Setting CWU above what the pump reaches risks the electric heater (after the delay) and
|
||||
- Setting CWU above what the pump reaches raises the controller's heater signal (after the delay; the heater is not connected) and risks
|
||||
pressure-switch trips; three trips in one cycle latch a fault until manual reset (MR208-PC+ docs).
|
||||
- Frisko phone app: direct Modbus TCP, LAN only, 200–300 ms between requests, 5 s timeout.
|
||||
- History analysis of 2026-10 ([history-2026-10.md](history-2026-10.md)): the heater joins 20 min into a
|
||||
CWU cycle (4147); `zb_state` 2 = heater; 4227 is unreliable for "heating CWU"; the controller clock
|
||||
- History analysis of 2026-10 ([history-2026-10.md](history-2026-10.md)): the heater signal comes 20 min into a
|
||||
CWU cycle (4147), `zb_state` 2 = that signal. The heater is not connected (owner, 2026-10-25); the meter
|
||||
step at +20 min is unexplained; 4227 is unreliable for "heating CWU"; the controller clock
|
||||
does not follow DST; period = target compared with the setpoints, valid only with 4226 = 1.
|
||||
+1
-1
@@ -77,7 +77,7 @@ Functional:
|
||||
heater, 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 a warning while the electric heater runs.
|
||||
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,
|
||||
starts "at least" because polls are 5 minutes apart).
|
||||
- F3 Change four setpoints: economy and comfort temperature of CWU (4143, 4144) and CO
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { ReactNode } from "react";
|
||||
import { celsius, degrees, number, periodText } from "./format";
|
||||
import { celsius, degrees, HEATER_SIGNAL, number, periodText } from "./format";
|
||||
import type { HeatPump, PiSensor } from "./types";
|
||||
import { Card, Row } from "./ui";
|
||||
|
||||
@@ -37,6 +37,7 @@ 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,25 +50,18 @@ describe("Historia views", () => {
|
||||
});
|
||||
|
||||
describe("Statystyki", () => {
|
||||
it("shows the totals, the starts as a minimum and the heater highlighted when it ran", async () => {
|
||||
it("shows the totals and the starts as a minimum, with the heater signal neutral", async () => {
|
||||
await open();
|
||||
|
||||
expect(await screen.findByText("Załączenia sprężarki, co najmniej")).toBeInTheDocument();
|
||||
expect(screen.getAllByText("0,2 h").length).toBeGreaterThan(0);
|
||||
expect(screen.getByText("Grzałka").closest("[data-alert]")).toHaveAttribute("data-alert", "true");
|
||||
expect(screen.getByText("Prąd z grzałki jest drogi")).toBeInTheDocument();
|
||||
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();
|
||||
});
|
||||
|
||||
it("does not highlight a heater that stayed off", async () => {
|
||||
const stats = example<Stats>("stats");
|
||||
stats.totals.heaterHours = 0;
|
||||
await open(stats);
|
||||
|
||||
await screen.findByText("Grzałka");
|
||||
expect(screen.getByText("Grzałka").closest("[data-alert]")).toHaveAttribute("data-alert", "false");
|
||||
});
|
||||
|
||||
it("asks for 30 days when chosen", async () => {
|
||||
const fetch = await open();
|
||||
await screen.findByText("Sprężarka");
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { render, screen, within } from "@testing-library/react";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { example, serve } from "./fixtures";
|
||||
import type { HeatPump } from "./types";
|
||||
@@ -176,32 +175,23 @@ describe("Overview", () => {
|
||||
expect((await screen.findByRole("region", { name: "Teraz" })).children[0]).toHaveTextContent(/^Ciepła woda13,6 °Ccel 8,0 °C$/);
|
||||
});
|
||||
|
||||
it("warns in amber while the heater runs and explains it on a tap", async () => {
|
||||
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 />);
|
||||
|
||||
const summary = await screen.findByText("Grzałka elektryczna pracuje (ok. 0,9 kW)");
|
||||
const explanation = screen.getByText(/Dołącza, gdy grzanie wody trwa dłużej niż 20 min/);
|
||||
expect(explanation).not.toBeVisible();
|
||||
expect(summary.closest("[role=alert]")!.className).toContain("amber");
|
||||
|
||||
await userEvent.click(summary);
|
||||
expect(explanation).toBeVisible();
|
||||
expect(explanation).toHaveTextContent("ustawienie sterownika 4147");
|
||||
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("shows no heater warning while the heater is off or the reading is stale", async () => {
|
||||
it("says nothing about the heater while its output is off", async () => {
|
||||
serveHeatPump(fresh(example("heat-pump")));
|
||||
const Overview = await overview();
|
||||
const { unmount } = render(<Overview />);
|
||||
await screen.findByRole("region", { name: "Teraz" });
|
||||
expect(screen.queryByText(/Grzałka elektryczna/)).toBeNull();
|
||||
unmount();
|
||||
|
||||
serveHeatPump({ ...fresh(example<HeatPump>("heat-pump")), heaterOn: true, stale: true });
|
||||
render(<Overview />);
|
||||
await screen.findByRole("region", { name: "Teraz" });
|
||||
expect(screen.queryByText(/Grzałka elektryczna/)).toBeNull();
|
||||
|
||||
expect(await screen.findByText("sprężarka")).toBeInTheDocument();
|
||||
expect(screen.queryByText("Grzałka")).toBeNull();
|
||||
});
|
||||
});
|
||||
@@ -1,12 +1,12 @@
|
||||
import type { ReactNode } from "react";
|
||||
import { celsius, hour, longDate, number, percent, periodText, power } from "./format";
|
||||
import { celsius, HEATER_SIGNAL, hour, longDate, number, percent, periodText, power } from "./format";
|
||||
import { Icon, type IconName } from "./icons";
|
||||
import { flowing } from "./flow";
|
||||
import { usePoll } from "./poll";
|
||||
import { Schematic } from "./Schematic";
|
||||
import { Legend, TimeChart, toRows, type Line } from "./TimeChart";
|
||||
import type { Energy, HeatPump, History, PiSensor, PumpState, Weather } from "./types";
|
||||
import { Banner, Card, Dot, Freshness, Loading, PageTitle, Pill } from "./ui";
|
||||
import { Card, Dot, Freshness, Loading, PageTitle, Pill } from "./ui";
|
||||
import { EnergyToday, gridText } from "./Energy";
|
||||
import { TemperatureDetails } from "./Details";
|
||||
import { useWeather, WeatherCard, WeatherIcon, WeatherWarnings } from "./Weather";
|
||||
@@ -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={`sprężarka, grzałka ${onOff(hp.heaterOn, "włączona", "wyłączona")}`}
|
||||
<Tile icon="fan" title="Pompa ciepła" detail={hp.heaterOn ? `sprężarka, grzałka: ${HEATER_SIGNAL}` : "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")}`}
|
||||
@@ -125,7 +125,6 @@ function Devices({ hp }: { hp: HeatPump }) {
|
||||
const compressor = hp.compressorPercent == null ? null : hp.compressorPercent > 0;
|
||||
const items: [string, boolean | null, string][] = [
|
||||
["Sprężarka", compressor, compressor ? percent(hp.compressorPercent) : "stoi"],
|
||||
["Grzałka", hp.heaterOn, onOff(hp.heaterOn, "włączona", "wyłączona")],
|
||||
...Object.entries(PUMP_NAMES).map(([key, name]): [string, boolean | null, string] => {
|
||||
const on = hp.pumps[key as keyof typeof PUMP_NAMES];
|
||||
return [name, on, onOff(on, "pracuje", "stoi")];
|
||||
@@ -168,21 +167,6 @@ function TemperatureChart() {
|
||||
);
|
||||
}
|
||||
|
||||
// Amber, not the alarm red: the heater is expensive, not broken. The explanation opens on a tap.
|
||||
function HeaterWarning() {
|
||||
return (
|
||||
<Banner tone="warn" compact>
|
||||
<details>
|
||||
<summary className="flex min-h-8 cursor-pointer items-center gap-2">
|
||||
<Icon name="alert" className="size-5 shrink-0" />
|
||||
<span>Grzałka elektryczna pracuje (ok. 0,9 kW)</span>
|
||||
</summary>
|
||||
<p className="mt-1 font-normal">Dołącza, gdy grzanie wody trwa dłużej niż 20 min (ustawienie sterownika 4147).</p>
|
||||
</details>
|
||||
</Banner>
|
||||
);
|
||||
}
|
||||
|
||||
function NoReading() {
|
||||
return (
|
||||
<Card>
|
||||
@@ -214,7 +198,6 @@ export function Overview() {
|
||||
<span>ALARM pompy ciepła. {hp.alarmText ? `${hp.alarmText}. ` : ""}Sprawdź sterownik w domu.</span>
|
||||
</div>
|
||||
)}
|
||||
{hp.heaterOn && !hp.stale && <HeaterWarning />}
|
||||
{weather && <WeatherWarnings warnings={weather.warnings} />}
|
||||
{hp.time == null ? <NoReading /> : (
|
||||
<div className="grid gap-4 lg:grid-cols-3">
|
||||
|
||||
@@ -115,7 +115,6 @@ export function Schematic({ hp, pvPowerW }: { hp: HeatPump; pvPowerW: number | n
|
||||
<Glow id="cwu" x={639} y={402} w={90} h={216} colour="#f97316" on={cwu} />
|
||||
<Glow id="buffer" x={746} y={489} w={70} h={134} colour="#ef4444" on={buffer} />
|
||||
{compressor && <circle className="beat" cx={575} cy={462} r={6} fill="var(--color-run)" />}
|
||||
{hp.heaterOn && <rect className="glow" x={518} y={560} width={78} height={14} rx={7} fill="#ef4444" />}
|
||||
|
||||
{/* Warmth rising from the floor while the CO pump runs */}
|
||||
{co && [960, 1080, 1200, 1300].map((x, i) => (
|
||||
|
||||
@@ -28,14 +28,11 @@ export function takting(days: StatsDay[]) {
|
||||
return days.some((d) => (d.compressorStarts ?? 0) > TAKTING_STARTS_PER_DAY);
|
||||
}
|
||||
|
||||
function Tile({ label, value, hint, alert = false, wide = false }: {
|
||||
label: string; value: string; hint?: string; alert?: boolean; wide?: boolean;
|
||||
}) {
|
||||
function Tile({ label, value, wide = false }: { label: string; value: string; wide?: boolean }) {
|
||||
return (
|
||||
<div data-alert={alert} className={`rounded-xl border p-3 ${wide ? "col-span-2 lg:col-span-1" : ""} ${alert ? "border-alarm bg-run-soft" : "border-edge bg-page"}`}>
|
||||
<div className={`rounded-xl border border-edge bg-page p-3 ${wide ? "col-span-2 lg:col-span-1" : ""}`}>
|
||||
<p className="text-2xl font-bold tabular-nums">{value}</p>
|
||||
<p className="text-sm text-soft">{label}</p>
|
||||
{hint && <p className="text-sm text-soft">{hint}</p>}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -48,8 +45,7 @@ function Totals({ stats }: { stats: StatsData }) {
|
||||
<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="Grzałka" value={hours(t.heaterHours)} wide alert={t.heaterHours > 0}
|
||||
hint={t.heaterHours > 0 ? "Prąd z grzałki jest drogi" : undefined} />
|
||||
<Tile label="Sygnał grzałki (niepodłączona)" value={hours(t.heaterHours)} wide />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -104,3 +104,6 @@ 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)";
|
||||
Reference in new issue
Block a user