Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
122 commits
Select commit Hold shift + click to select a range
6a178c9
Fix safety defects in the decision path; replace the flow-temperature…
enoch85 Jul 12, 2026
a017d0e
Add a reference thermal-battery controller to the simulation harness
enoch85 Jul 12, 2026
e09a82b
Let the building fabric be used as thermal storage
enoch85 Jul 12, 2026
2e67dd4
Make the decision-scenario scripts runnable again
enoch85 Jul 12, 2026
29dbd2c
Classify a price quarter by what it costs, not only by where it ranks
enoch85 Jul 12, 2026
2b40326
Stop counting the airflow heat twice
enoch85 Jul 12, 2026
46632d9
Help the slow house first, not last
enoch85 Jul 12, 2026
08cf128
Stop waiting forever for a heat pump that is never coming
enoch85 Jul 12, 2026
5202be9
Do not ask a saturated compressor for more heat
enoch85 Jul 12, 2026
d451cd2
Let a slow house look further ahead than a fast one
enoch85 Jul 12, 2026
441a054
Size the pre-heat against the fabric it has to charge
enoch85 Jul 12, 2026
4a8a0e3
Let only the control loop drive the heat pump, one writer at a time
enoch85 Jul 12, 2026
d111114
Stop trusting a sensor that has stopped talking
enoch85 Jul 12, 2026
fc90261
Make the rulebook describe the code that exists
enoch85 Jul 12, 2026
603bb05
Make the climate-zone tables say what the code computes
enoch85 Jul 12, 2026
c9c14f8
Stop the architecture doc from teaching a bug that was already fixed
enoch85 Jul 12, 2026
2baaff0
Stop three architecture docs from describing code that was never written
enoch85 Jul 12, 2026
035bd0d
Put the evidence for the safety limits in the repository
enoch85 Jul 12, 2026
3eb9bf8
Stop calling a forecast a meter reading, and a price a sum of money
enoch85 Jul 12, 2026
8282ddb
Pass Home Assistant the types its APIs are checking for
enoch85 Jul 12, 2026
f2217d6
Stop the reload listener from reasoning from a false premise
enoch85 Jul 12, 2026
d5856fe
Stop the rulebook teaching a flow-temperature model that was removed
enoch85 Jul 12, 2026
124814d
Let the sensors speak the language Home Assistant is running in
enoch85 Jul 12, 2026
1298c09
Tell the user which setting was wrong, instead of "Unknown error occu…
enoch85 Jul 12, 2026
bf30028
Keep the boost cooldown out of reach of the reload button
enoch85 Jul 12, 2026
95b0539
Do not leave a coordinator driving the pump when setup fails
enoch85 Jul 12, 2026
8e9a397
Stop inventing an electricity price when there is none
enoch85 Jul 12, 2026
6d6e0a0
Stop crediting the flow-temperature model this project removed
enoch85 Jul 12, 2026
b2ccaf4
Make it possible to report what the pump actually did
enoch85 Jul 12, 2026
83eb90d
Check every document, because the wrong number was in five of them
enoch85 Jul 12, 2026
e7c29b7
Give the absolute degree-minute floor one definition
enoch85 Jul 12, 2026
1584b9c
Stop offering a temperature the system will spend the winter fighting
enoch85 Jul 12, 2026
51b37e6
Do not drive the pump on a reading nobody has confirmed for hours
enoch85 Jul 13, 2026
79246a2
Stop treating a missing price as the cheapest quarter of the day
enoch85 Jul 13, 2026
ff06597
Stop billing the owner for power nobody measured
enoch85 Jul 13, 2026
0733333
Give the ladder back its missing rung, and bill only what the grid de…
enoch85 Jul 13, 2026
77af3be
Stop hot water starting at a degree minute that aborts it on the next…
enoch85 Jul 13, 2026
7485bc0
Let the shower win, but not against a house that is freezing
enoch85 Jul 13, 2026
ebc8591
Give the simulator a house whose pump can actually run out
enoch85 Jul 13, 2026
94d4f03
Revert "Do not leave a coordinator driving the pump when setup fails"
enoch85 Jul 13, 2026
cd171ec
Revert "Give the simulator a house whose pump can actually run out"
enoch85 Jul 13, 2026
4eae7b1
Revert "Size the pre-heat against the fabric it has to charge"
enoch85 Jul 13, 2026
b732cca
Revert "Let the building fabric be used as thermal storage"
enoch85 Jul 13, 2026
93e627b
Revert "Classify a price quarter by what it costs, not only by where …
enoch85 Jul 13, 2026
f545ebc
Read the sources, and find two errors that were hiding each other
enoch85 Jul 13, 2026
19d9592
The fix for two cancelling errors was itself two cancelling errors
enoch85 Jul 13, 2026
0d25cbf
A milliwatt sensor was being billed as megawatts
enoch85 Jul 13, 2026
a7daf6e
Peak protection never fired for anyone without a whole-house meter
enoch85 Jul 13, 2026
376c78c
A NIBE with no room sensor was coasted to a stop on a number nobody m…
enoch85 Jul 13, 2026
58689c5
The simulator could not fail, so it could not detect. Now it can.
enoch85 Jul 13, 2026
98b8ea4
int(-1.9) is -1, so the pump always did less than the engine asked
enoch85 Jul 13, 2026
cf96018
The emergency ladder fired in July
enoch85 Jul 13, 2026
9ac479e
A percentile cannot see a price
enoch85 Jul 13, 2026
27a12fe
Two units nobody checked: 240 V that cannot exist, and Fahrenheit rea…
enoch85 Jul 13, 2026
1b7b88f
The plausibility band was applied to every sensor except the one that…
enoch85 Jul 13, 2026
2a0d109
Five tests asserted a literal against itself
enoch85 Jul 13, 2026
0c6f7d8
A worse peak reported a bigger saving
enoch85 Jul 13, 2026
91b3275
Our hot-water boost outlived us
enoch85 Jul 13, 2026
d047dfb
The guard against rapid fan cycling was exactly one tick long
enoch85 Jul 13, 2026
c1888dc
On an S-series pump, hot-water optimisation did nothing and said so i…
enoch85 Jul 13, 2026
e307d6f
A negative price is still a price
enoch85 Jul 13, 2026
0bd5c80
A setpoint is not a measurement, and only the domain can tell them apart
enoch85 Jul 13, 2026
900dfda
The prediction gates counted samples and spoke in hours, and the two …
enoch85 Jul 13, 2026
d6d0642
Twenty-seven constants that nothing reads, and three that name servic…
enoch85 Jul 13, 2026
6993ac5
Twenty-one tests for overshoot protection, and not one of them called…
enoch85 Jul 13, 2026
b51bee0
A naive datetime in the emergency layer, and a history nobody reads
enoch85 Jul 14, 2026
6b0c195
"Hard floor, always heat below this" - and it does not, and it must not
enoch85 Jul 14, 2026
1d1e0cc
The cold-snap trigger was reading the weather from six hours ago
enoch85 Jul 14, 2026
aeeee53
A saturated compressor is a positive feedback trap - recorded, not fixed
enoch85 Jul 14, 2026
8c7a007
Ten tests that break at a daylight-saving transition, and all ten wer…
enoch85 Jul 14, 2026
5dd6167
My "first-law audit" was an identity. It could not fail, so it could …
enoch85 Jul 14, 2026
7c007cc
Untrack the simulator output the gitignore already says is not committed
enoch85 Jul 14, 2026
d605a6a
My savings figure was measured against a badly tuned pump
enoch85 Jul 14, 2026
2970b50
A green run over silent code is not evidence, so the harness now says…
enoch85 Jul 14, 2026
c88434d
The recovery ladder only ever fires in the run that fails
enoch85 Jul 14, 2026
8401965
Correct a claim of mine that the previous commit proved was worthless
enoch85 Jul 14, 2026
620adaa
Clamp the degree-minute triggers on the number the layers actually read
enoch85 Jul 14, 2026
9e7b76a
Make the simulator's energy audit capable of failing, and stop it des…
enoch85 Jul 14, 2026
c0da762
The savings figure was the night weighting, compared against itself
enoch85 Jul 14, 2026
0fd3461
An impossible power reading is not a peak, and my test for it defende…
enoch85 Jul 14, 2026
abd7b21
The guard that stopped the optimiser buying at the day's highest pric…
enoch85 Jul 14, 2026
d8bba52
The floor I put under the cost layers was a bang-bang controller
enoch85 Jul 14, 2026
0472629
Two tests that could not fail, and two defects that were cancelling e…
enoch85 Jul 14, 2026
2cdd1aa
The heat-pump models were invented, and I published a month of kWh an…
enoch85 Jul 14, 2026
3f7345f
The immersion heater was one invented number standing for five machines
enoch85 Jul 14, 2026
6d5d097
The houses were invented too, and the sizing decided what the simulat…
enoch85 Jul 14, 2026
a6eecb9
The tariff bills the hour. EffektGuard defended a peak nobody is bill…
enoch85 Jul 14, 2026
04f65d8
Every number in the plant model now says where it came from
enoch85 Jul 14, 2026
54f5d1b
Leaving the weather dropdown blank silently switched off the main con…
enoch85 Jul 14, 2026
37f2fef
On the night the clocks go back, an hour of the month's peak was deleted
enoch85 Jul 14, 2026
920f82b
The simulator could not have seen a DST day, and when it could, it li…
enoch85 Jul 14, 2026
c9a9ecb
The simulator was validating an implementation nobody runs
enoch85 Jul 14, 2026
71eac6d
An hour the meter slept through was billed as the month's peak
enoch85 Jul 14, 2026
d372231
The integration was unloaded, and then it wrote one more offset to th…
enoch85 Jul 14, 2026
c937be2
The hot-water boost we started, and then disowned
enoch85 Jul 14, 2026
6c99a63
I asserted the wrong trigger four times, and never once ran it
enoch85 Jul 14, 2026
9a10f08
Turning the thermostat off did not turn the optimiser off
enoch85 Jul 14, 2026
022b8ff
A view that nobody tells about the change is not a view
enoch85 Jul 14, 2026
3523d9b
Cut the retelling, keep the evidence
enoch85 Jul 14, 2026
eef8a15
One COP curve, one import block, one place for the numbers
enoch85 Jul 14, 2026
2eb8c38
A week of real prices, recorded whether or not anyone is watching
enoch85 Jul 14, 2026
b178fe3
Two watchers, one CSV, and a box that starts nothing on boot
enoch85 Jul 14, 2026
5f7fdfe
The week was watching a heat pump that was not there
enoch85 Jul 14, 2026
f1d2e0f
Actuator path: apply-only state, forced OFF neutralization
enoch85 Jul 14, 2026
83437f1
A store from the quarter-hour era no longer breaks setup, and a day g…
enoch85 Jul 14, 2026
8964f86
The actuator commit shipped red, and its own OFF test never ran
enoch85 Jul 14, 2026
6087b2d
The airflow sensor called a method this branch had deleted
enoch85 Jul 14, 2026
372b29b
A user's hot-water boost is not the price optimizer's to cancel
enoch85 Jul 14, 2026
150dd95
A billing hour's provenance is every sample's, not the closing cycle's
enoch85 Jul 14, 2026
6b878e2
The simulated elpatron fires where NIBE arms it, and the F2040 keeps …
enoch85 Jul 14, 2026
6d7f668
The slab charges at the rate its own eigenvalues say, and a doc canno…
enoch85 Jul 14, 2026
48c5556
Diagnostics tell the truth, teardown follows HA, and a spike is not a…
enoch85 Jul 14, 2026
5917e29
The last wall-clock subtraction, and the last citation to a document …
enoch85 Jul 14, 2026
32479aa
Cut the second retelling too
enoch85 Jul 14, 2026
a329722
Dead knobs deleted, one quarter constant, and gates that can actually…
enoch85 Jul 14, 2026
e4cebe6
Every production line justifies itself, or it is gone
enoch85 Jul 15, 2026
1ee55a8
The audit's scaffolding comes down: 51 debug tests deleted, every kep…
enoch85 Jul 15, 2026
5e59e4d
A helper is a valid hot-water actuator (issue #18)
enoch85 Jul 15, 2026
945a922
Carry the v0.5.0 release version
enoch85 Jul 15, 2026
926760c
The register gets whole degrees only: int() was the design, not the bug
enoch85 Jul 15, 2026
65116a9
The owner's tariff bills the 15-minute period, and now everything agr…
enoch85 Jul 16, 2026
2813fb0
A real arctic January, and the machine the manual says cannot be there
enoch85 Jul 16, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
337 changes: 237 additions & 100 deletions .github/copilot-instructions.md

Large diffs are not rendered by default.

9 changes: 4 additions & 5 deletions .github/workflows/validate.yml
Original file line number Diff line number Diff line change
Expand Up @@ -38,11 +38,10 @@ jobs:
run: |
pytest tests/ -v --tb=short

# Commented out until ready for HACS publication
# - name: HACS validation
# uses: hacs/action@main
# with:
# category: integration
- name: HACS validation
uses: hacs/action@main
with:
category: integration

- name: Hassfest validation
uses: home-assistant/actions/hassfest@master
87 changes: 61 additions & 26 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
<img src="icons/logo.png" alt="EffektGuard Logo" width="200"/>

[![hacs_badge](https://img.shields.io/badge/HACS-Default-41BDF5.svg)](https://github.com/hacs/integration)
![Version](https://img.shields.io/badge/version-0.5.0-beta.1-blue)
![Version](https://img.shields.io/badge/version-0.5.0-blue)
![HA](https://img.shields.io/badge/Home%20Assistant-2025.10%2B-blue)
[![Sponsor on GitHub](https://img.shields.io/badge/sponsor-GitHub%20Sponsors-1f425f?logo=github&style=for-the-badge)](https://github.com/sponsors/enoch85)

Expand Down Expand Up @@ -40,30 +40,50 @@ Currently supports NIBE heat pumps via MyUplink integration, with plans to add s
- **Proactive debt prevention** - trend-based future DM prediction
- **Effect tariff** (peak avoidance) - predictive 15-min protection
- **Prediction/Learning** (self-tuning) - learned thermal model for pre-heating
- **Weather compensation** (mathematical flow temp) - André Kühne + Timbones formulas
- **Weather compensation** (mathematical flow temp) - the **EN 442 emitter law**
- **Weather prediction** (pre-heating) - time-aware cold snap protection
- **Spot price** (cost reduction) - forward-looking optimization with adaptive horizon
- **Comfort** (tolerance) - reactive temperature correction

### 🌍 Global Climate Adaptation
Automatic latitude-based zone detection (Arctic to Mediterranean):
- **Extreme Cold** (66.5°N+): Kiruna, Tromsø - DM -800 to -1200 normal
- **Very Cold** (60.5-66.5°N): Luleå, Umeå - DM -600 to -1000 normal
- **Cold** (56-60.5°N): Stockholm, Oslo, Helsinki - DM -450 to -700 normal
- **Moderate Cold** (54.5-56°N): Copenhagen, Malmö - DM -300 to -500 normal
- **Standard** (<54.5°N): Paris, London - DM -200 to -350 normal

**No configuration needed** - uses Home Assistant latitude. DM -1500 absolute maximum enforced globally.

### 🧠 Self-Learning Capability
Learns your building over 7-14 days:
Automatic latitude-based zone detection (Arctic to Mediterranean). The ranges below are each zone's
**base** range — what is normal **at that zone's own winter average**. They shift with the outdoor
temperature, by 20 DM per degree:

| Zone | Latitude | Winter avg | Base normal DM |
|---|---|---|---|
| **Extreme Cold** | 66.5°N+ | −20 °C | −800 to −1200 |
| **Very Cold** | 60.5–66.5°N | −12 °C | −600 to −1000 |
| **Cold** | 56–60.5°N | −8 °C | −450 to −700 |
| **Moderate Cold** | 54.5–56°N | −1 °C | −300 to −500 |
| **Standard** | <54.5°N | 0 °C | −200 to −350 |

So Stockholm's normal range is −450 to −700 **at −8 °C**, and −490 to −740 at −10 °C. Quoting the
base range as if it applied at every temperature is how four of this project's own documents came
to be wrong; see `docs/CLIMATE_ZONES.md` for the full tables.

**No configuration needed** — uses Home Assistant's latitude.

**DM −1500 is the absolute floor**, enforced globally. ⚠️ It is *not* the number that governs a real
F750: the pump's own "start addition" (menu 4.9.3) fires at **−700**, so the immersion heater engages
and works DM back up long before −1500 is approached. See `docs/research/01_degree_minutes.md`.

### 🧠 Self-Learning Capability (observing only — not yet driving your pump)
EffektGuard continuously observes your building and estimates:
- **UFH type detection** - concrete slab (6h lag) vs timber (2-3h lag) vs radiators (<1h lag)
- **Thermal mass** - building heat storage capacity (kWh/°C)
- **Heat loss coefficient** - envelope performance (W/°C)
- **Heating efficiency** - system response to offset changes (°C/°C)
- **Weather patterns** - seasonal adaptation with unusual weather detection

Predictive pre-heating uses learned parameters for intelligent load shifting.
**These estimates do not currently influence control.** Learned parameters are used for
pre-heating only once the model's confidence exceeds 70%, and confidence cannot presently
reach that: the indoor temperature is read to 0.1 °C every 5 minutes, and a building's
response over 5 minutes is smaller than the sensor's own resolution. Nothing reliable can be
learned from that, so nothing is claimed from it.

Control today is physics-based and deterministic — the heating curve, the EN 442 emitter law,
weather compensation and the degree-minute safety net — none of which depends on learning.

### ⚡ Effect Tariff Optimization
Native 15-minute (quarterly) integration:
Expand All @@ -82,7 +102,8 @@ Multi-factor forward-looking optimization combining:

### 🌡️ Weather Compensation (Mathematical)
Physics-based flow temperature optimization:
- **André Kühne formula** - validated across manufacturers (Vaillant, Daikin, NIBE, etc.)
- **EN 442 emitter law** (EN 442-1 §3.31, EN 12831, EN 1264) - validated against NIBE's own
published curve 9: it lands **0.20 °C** from it, where a straight line is out by **2.37 °C**
- **Timbones method** - radiator-specific calculations (BS EN442)
- **UFH adjustments** - concrete slab (-8°C), timber (-5°C)
- **Climate-aware margins** - automatic safety headroom by zone
Expand Down Expand Up @@ -272,22 +293,36 @@ Native quarterly (15-min) price periods:
- **Auto-discovery** - finds price entity automatically

### Weather Compensation Math

The flow temperature follows the **EN 442 emitter law** (`utils/emitter.py`) — the emitter's own
characteristic curve, not a linear offset from outdoor temperature:

```python
# André Kühne formula (universal)
TFlow = 2.55 × (HC × (Tset - Tout))^0.78 + Tset
# Demand reaches zero at the BALANCE POINT, not at room temperature:
# bodies, appliances and the sun cover the losses until several degrees below the setpoint.
balance = T_room - INTERNAL_GAINS_W / heat_loss_coefficient

# Timbones method (radiator-specific)
TFlow = ((Pin / Pout)^(1/1.3) × (DTout / DTin)) × (Tset - Tout) + Tset
phi = (balance - T_out) / (balance - T_out_design) # dimensionless relative load
T_flow = T_room + dT_design * phi**(1/n) + spread/2 # n = 1.3 radiators, 1.1 UFH
```
Combined with climate-aware safety margins (0.0-2.5°C by zone).

### Self-Learning Timeline
- **Day 1-3**: Low confidence (0.0-0.3), conservative defaults
- **Day 4-7**: Medium confidence (0.3-0.7), starts using learned params
- **Day 8-14**: High confidence (0.7-1.0), fully optimized
- **Ongoing**: Continuous refinement, seasonal adaptation
The spread is held **constant** — a heat pump modulates its circulator, it does not run a
fixed-speed pump. Combined with climate-aware safety margins (0.0–2.5 °C by zone).

### Self-Learning Status

Observations are recorded every 5 minutes into a rolling 672-entry window — **56 hours**, not
the week the window was sized for. Confidence is scored from observation count, data
consistency and time span, and learned parameters are used for pre-heating only above 70%.

At the present observation cadence that threshold is not reachable, and the integration says so
rather than pretending otherwise: a 0.1 °C indoor sensor sampled every 5 minutes quantises the
building's response into steps of 1.2 °C/h, which is larger than any real heating rate. The
consistency term is therefore honestly zero and confidence settles around 0.47.

672 observations (1 week @ 15-min) minimum for reliable learning.
**Learning does not drive your heat pump today.** Making it do so means sampling slowly enough
for the signal to exceed the sensor's resolution — a deliberate change, not a tuning tweak,
because it would put a learned model in the control path of real heating equipment.

## Documentation

Expand Down
Loading