Regulate the cabin by blower speed from a stored heater setpoint #91
Loading…
Reference in a new issue
No description provided.
Delete branch "feat/heater-cabin-control"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
The Flow 5D has no thermostat, so the node now regulates the cabin by blower speed.
CMD_HEATERsetpoints are stored (keysetpoint, 0.1 °C, 50 to 300, default 200) and reported inSTATE_HEATER. Each Si7021 reading runs a pure PI controller (blower_control.h) whose level goes to the blower motor with origin automation. This replaces the edge logic from #90.Priority: heater link lost gives full speed, heater blower off gives motor off and a reset, cabin unknown or fault gives full speed, otherwise PI. Kp, Ki and the minimum level are untuned starting values and need the bench. A manual command holds for the ten second lockout, then the next reading overrides it.
Stacks on #89 and #90.
Tested:
pio test -e native(494 tests) andpio run -e node_heater. Not tested on hardware.Tickets: CCS-WHP-13, CCS-WHP-14. The Home Assistant side is CCS-HA-56.
Two small gaps; nothing blocking.
@ -92,0 +117,4 @@const float secondsSinceReading = (now - lastCabinReadingMs) / 1000.0f;lastCabinSequence = cabinSensor.sequence();lastCabinReadingMs = now;const uint16_t level = blowerControl.update(heater.linkLost(), heater.blowerOn(), cabinSensor.temperature(), heater.setpoint(), secondsSinceReading);request()/setOn(false) now run every reading, even when the level has not changed. Each goes through noteChange(), so the fan likely re-broadcasts state every ~10 s, including while idle at 0. Only call when the level differs from the last one applied (the old code guarded on change). Not verified against noteChange().
@ -253,0 +258,4 @@uint16_t stored = 0;TEST_ASSERT_TRUE(rig.store.load("setpoint", stored));TEST_ASSERT_EQUAL_UINT16(220, stored);}Only the happy path is tested. Missing: clamp to 50/300, a sentinel setpoint leaving the stored value alone, and begin() reloading a stored value (and rejecting an out-of-range one). Each is a branch added in this commit that nothing would catch if broken.
One finding, in src/node_heater/main.cpp loop(): BlowerControl::update runs only when cabinSensor.sequence() changes, and the Si7021 reads every 10 s (conversionIntervalMs). So link loss (full speed), blowerOn going true or false, and a setpoint change all take up to 10 s to reach the motor. The old code reacted on the same loop pass. The link-loss path is the one that matters, because it is the safety default. Fix: call update() every loop. Keep dtSeconds tied to elapsed time since the last call, or advance the integral only when the sequence changes.
Three findings on
eca1885.@ -92,0 +116,4 @@const uint32_t now = ctrl::nowMs();const bool newReading = cabinSensor.sequence() != lastCabinSequence;const float secondsSinceReading = newReading ? (now - lastCabinReadingMs) / 1000.0f : 0.0f;if (newReading) {lastCabinReadingMs starts at 0, so the first reading integrates dt = uptime (all the time since boot, not one interval). Seed it from now on the first reading, or skip integration for it.
@ -92,0 +122,4 @@}const uint16_t level = blowerControl.update(heater.linkLost(), heater.blowerOn(), cabinSensor.temperature(), heater.setpoint(), secondsSinceReading);if ((newReading || level != appliedBlowerLevel) && !blowerMotor.lockedOut(now)) {appliedBlowerLevel = level;Link loss (full blower, a heater-protection action) is sent as Origin::Automation and gated by !lockedOut. A manual blower change in the last 10 s therefore delays the full-speed response until the lockout ends. Origin::Failsafe overrides the lockout (origin.h), so send it as Failsafe when heater.linkLost(). Not new in this commit, but the new guard makes it explicit.
@ -0,0 +35,4 @@ctrl::BlowerControl control;TEST_ASSERT_EQUAL_UINT16(0, control.update(false, true, cabinAt(200), setpoint, readingIntervalSeconds));TEST_ASSERT_EQUAL_UINT16(1000, control.update(true, true, cabinAt(200), setpoint, 0.0f));}This test passes on the old code too: linkLost returns maxLevel before dt is read. It does not cover the change (dt 0 freezes the integral; main.cpp only passes dt on a new reading). Replace it with one that proves the freeze, e.g. a reading with dt>0 that builds an integral, then update(..., 0.0f) twice and assert the level is unchanged. The main.cpp gating (write only on new reading or changed level) is untested.
No findings on
7d9de0a.