Case: oma talo — Osa 17

Written by

in

Lämpöpumppu siirtyy final gate -polkuun: kolme hiljaista ohitusta ja yksi onnenkantamoinen

Edellisessä osassa watchdog-kerros sai ensimmäisen oikean saaliinsa. Tässä osassa otetaan seuraava askel: lämpöpumpun fyysinen ohjaus siirretään uuteen final gate -arkkitehtuuriin — samaan, jossa sähköauton lataus on jo kulkenut jonkin aikaa.

Päivän aikana löytyi kolme tapaa, joilla järjestelmä olisi voinut epäonnistua täysin hiljaa, ja yksi hetki, jota ei olisi voinut käsikirjoittaa paremmin: Thermia päätti aloittaa oikean käyttövesijakson kesken testien ja validoi koko päätösketjun livenä ennen kuin mitään oli vielä kytketty päälle.

Lähtötilanne: miksi juuri nyt

EnergyHubin ohjausarkkitehtuurin tavoitetila on ollut selvä jo B-vaiheen alusta:

Decision Engine
  → lease (eh.lease.v1)
  → final gate (eh.final_gate.v1)
  → Home Assistant → releet
  → fyysinen laite
  → telemetria / readback
  → observer

Sähköauton latauspolku kulkee tätä reittiä jo tuotannossa, stale-suojineen ja rollback-mekanismeineen. Lämpöpumppu sen sijaan on ohjautunut edelleen vanhaa suoraa komentopolkua (energyhub/command/hp/mode), vaikka päätöksenteon ja havainnoinnin pohja — lease-julkaisu, final gate -laskenta shadow-tilassa ja HA:n havaintokerros — on ollut valmiina.

Seuraava iso kehitysaskel olisi ennustepohjainen optimointi: sää, aurinko, lämmöntarve. Mutta ennustlogiikka tekee järjestelmästä aktiivisemman, ja ennen kuin järjestelmälle antaa lisää päätösvaltaa, sen käsien pitää toimia deterministisesti. Siksi HP final gate live ennen ennusteita.

Ajoitus oli myös tarkoituksella tämä. Kesällä Thermia Calibra ajaa vain kylpyhuoneen lattialämmitystä ja käyttövettä, yhteensä noin 100 minuuttia päivässä. Jos jokin menee pieleen, seuraukset ovat ”käyttövesi lämpiää tunnin myöhässä”, eivät ”talo kylmenee”. Tällaista arkkitehtuurisiirtoa ei kannata tehdä ensimmäistä kertaa tammikuussa.

Esiehto: hiljainen hylkäys näkyväksi (hiljainen ohitus #1)

Ennen mitään live-muutosta piti korjata löydös edellisestä työpaketista: B3-gaten cache hylkäsi väärällä schemalla tulevat lease- ja lock-viestit täysin hiljaa. Shadow-tilassa se oli pelkkä havaintopuute. Live-tilassa se olisi tarkoittanut, että lämpöpumpun komento jää hiljaisesti menemättä läpi ja pumppu jää edelliseen tilaansa — ilman yhtäkään lokiriviä.

Korjaus oli pieni mutta periaatteellisesti tärkeä: node.warn jokaisesta viestistä, jonka schema ei ole odotettu (eh.lease.v1 / eh.lock.v1), 60 sekunnin throttlella ettei 15 sekunnin tick-sykli spämmää lokia. Testattiin julkaisemalla tahallaan rikkinäinen eh.lock.BAD ja eh.lease.BAD — molemmat tuottivat varoituksen, final gate jatkoi normaalisti.

Tämä on sama periaate kuin osan 15 ”komento ≠ toteuma”, mutta askelta aiemmin ketjussa: validointi, joka hylkää hiljaa, on pahempi kuin validointi joka puuttuu, koska se antaa väärän turvallisuudentunteen.

Valmistelu HA:ssa — ja patchauksen nöyryyttävä oppitunti

Varsinainen valmistelupaketti (B5d-1b) toi Home Assistantiin:

  • input_datetime.d_hp_final_gate_last_seen ja input_boolean.c_hp_final_gate_stale -helperit
  • fyysisen SG-ohjauksen ehdot uuden energyhub_hp_control_path-valitsimen (legacy / shadow / final_gate) mukaisiksi
  • final gate live -kuluttajan, joka toimii vain final_gate-polussa ja validoi scheman, targetin, komennon ja tuoreuden (< 300 s)
  • minimaalisen stale-suojan: jos final gate -viesti vanhenee, pumppu normaliin
  • rollback-automaation: polunvaihto pois final_gatesta pakottaa SG-releet normaliksi readback-varmistuksella
  • HA-restart-normalisoinnin: käynnistys final_gate-polussa → normal, kunnes tuore viesti saapuu

Kuulostaa suoraviivaiselta. Toteutus ei ollut.

Muutokset tehdään tässä projektissa aina Python-patch-skripteillä, jotka kaatuvat jos kohdetta ei löydy — ei koskaan käsin editoiden. Tänään se periaate maksoi itsensä takaisin korkojen kera, koska kolme patch-yritystä epäonnistui peräkkäin:

  1. Ensimmäinen skripti haki pitkää täsmämerkkijonoa, joka ei vastannut tiedoston todellista sisältöä merkilleen. Kaatui turvallisesti ennen kirjoitusta.
  2. Toinen, pidempi skripti meni terminaaliin liitettäessä rikki — heredoc-paste katkeili ja rivit sekoittuivat. Osa muutoksista ajautui, osa ei, ja lopputuloksena YAML-sisennykset menivät sekaisin. Tiedosto palautettiin varmuuskopiosta.
  3. Kolmas yritys kaatui siihen, että automaatiolohkojen sisennys olikin 2 välilyöntiä eikä 4 — asia, jonka yksi repr()-tulostus paljasti sekunnissa.

Lopullinen toimiva resepti oli tylsä mutta luotettava:

pienet patchit yksi kerrallaan
  → python3 -m py_compile ennen ajoa (paljastaa rikkinäisen pasten)
  → rakenteellinen rivihaku, ei pitkiä täsmäankkureita
  → sisennys luetaan kohderivistä, ei kovakoodata
  → nl -ba -katselmointi jokaisen patchin jälkeen
  → HA config check ennen restarttia, aina

Tärkein havainto koko sotkusta: jokainen epäonnistunut patch kaatui ennen write_text-kutsua. Kohdetiedosto ei muuttunut kertaakaan vahingossa. ”Kaadu ennen kirjoitusta” -periaate muutti kolme epäonnistumista harmittomiksi iteraatioiksi sen sijaan, että ne olisivat olleet kolme korjattavaa tuotantovirhettä.

Legionella-sensorin nimivirhe (hiljainen ohitus #2)

Live-kuluttajan turvasäännöissä on ehto: jos Thermia ajaa legionellajaksoa, EnergyHub ei koske pumppuun mihinkään. Legionella-ajo omistaa pumpun.

Alkuperäinen patch-luonnos käytti sensoria sensor.modbus_m_hp_running_priority ja arvoa 7. Ennen ajoa tehtiin ristiintarkistus Node-REDin State Collectoria vasten — ja hyvä niin. Node-RED laskee legionella-tilan täsmälleen samalla logiikalla (running_priority === 7), mutta HA:n todellinen sensorinimi onkin sensor.modbus_m_hp_running_first_priority.

Väärällä nimellä template olisi palauttanut oletusarvon int(-1), ehto hp_rp == 7 ei olisi lauennut koskaan, eikä mikään olisi kertonut sitä. Legionella-suoja olisi ollut olemassa YAML:ssa, näyttänyt oikealta katselmoinnissa ja ollut käytännössä pois päältä.

Tämä on päivän toinen hiljainen ohitus, ja mekanismi on opettavainen: oletusarvo validointilogiikassa muuttaa puuttuvan datan ”kaikki ok” -tilaksi. int(-1) on turvallinen laskennassa mutta vaarallinen ehdossa. Sama kuvio kuin schema-hylkäys, eri kerroksessa.

Control path -peili: payload ei saa valehdella

Yksi asia jäi vielä häiritsemään ennen live-testiä. B3-gaten julkaisema final gate -payload sisälsi kovakoodatut kentät:

json

"shadow": true,
"control_path": "legacy",
"physical_control": false

HA olisi voinut ohjata pumppua näistä kentistä välittämättä — ne olivat puhdasta raportointia. Mutta live-testissä lokit ja Grafana olisivat valehdelleet: fyysinen ohjaus käynnissä, payload väittää shadow-tilaa. Auditointikelvoton tilanne.

Ratkaisu (B5d-1c-0a) oli pieni peilausketju:

HA: input_select.energyhub_hp_control_path muuttuu
  → retained MQTT: energyhub/config/hp_control_path (eh.control_path.v1)
  → Node-RED lukee topicin global-contextiin
  → B3 payload: control_path, physical_control ja shadow todellisesta tilasta

Rajaus oli tärkeä ja se kirjattiin eksplisiittisesti: tämä on peili, ei ehto. Gate ei saa päätellä mitään omasta julkaisustaan — muuten syntyisi kehäriippuvuus, jossa raportointikenttä alkaa ohjata sitä mitä sen pitäisi vain kuvata. Retained-viesti taas varmistaa, että Node-RED saa tilan heti restartin jälkeen eikä jää arvailemaan.

Sivulöytönä: ensimmäinen versio patchista kirjoitti flows.jsonin pretty-printattuna ja git-diff paisui 1154 riviin. Compact-muotoon palauttamisen jälkeen todellinen muutos oli 2 riviä. Minifioitu yksirivinen flows.json on säilytettävä konventiona juuri tämän takia — diffit pysyvät auditoitavina.

Onnenkantamoinen: Thermia validoi arkkitehtuurin itse

Sitten tapahtui jotain, mitä ei voi suunnitella. Kesken preflight-tarkistusten — control path edelleen turvallisesti legacyssä — talon käyttövesi ehti jäähtyä triggerirajalle ja Decision Engine käynnisti oikean DHW-boostin. MQTT-kuuntelussa näkyi livenä koko ketju:

energyhub/lease/hp_sg_mode:
  mode: "boost"
  reason: "DHW: boost päällä 2 min"
  ttl_s: 120

energyhub/final_gate/hp_sg_mode:
  lease_valid: true
  lease_mode: "boost"
  would_command: "boost"
  blocked: false

Oikea tuotantopäätös kulki Decision Enginestä leaseksi, leasesta gaten läpi ja gate laski would_command: boost — kaikki shadow-tilassa, mitään ohjaamatta. Neljä minuuttia myöhemmin boost päättyi ja ketju palautui normaliin yhtä siististi.

Tämä vastasi kertalaakista päivän tärkeimpään avoimeen kysymykseen: menevätkö lämpöpumpun päätökset oikeasti final gate -ketjuun, vai jäisikö live-polku pysyvästi normal-tilaan koska boostit kulkevat vanhaa reittiä? Vastaus tuli todellisella datalla ennen kuin yhtäkään testikomentoa oli lähetetty. Shadow-arkkitehtuurin koko idea tiivistyi tähän hetkeen: järjestelmä todisti toimivuutensa tarkkailemalla itseään tuotannossa.

KUVA 1: Päätös vs toteuma -paneeli — DHW-jakso näkyy piikkinä n. klo 15.50
KUVA 2: Thermia-paneeli — käyttövesi 47,7 °C → 57,9 °C, kompressori 60 %

Live-testi kolmessa vaiheessa

Kun DHW-jakso oli ohi ja SG-releet todistetusti pois päältä, tehtiin varsinainen live-testi. Ei yhtenä isona kytkentänä vaan kolmena erillisenä, kumpaankin suuntaan todennettuna vaiheena.

Vaihe 1 — normal-only live. energyhub_hp_control_path käännettiin final_gateen. Noin minuutin sisällä retained payload päivittyi:

json

"shadow": false,
"control_path": "final_gate",
"physical_control": true,
"would_command": "normal"

SG-releet pysyivät pois päältä, stale-lippu ei noussut, watchdog pysyi vihreänä. Fyysinen ohjausvalta oli nyt final gate -polulla — ja polku ei tehnyt mitään, koska päätös oli normal. Juuri niin kuin pitää.

Vaihe 2 — boost ja readback. Injektoitiin lyhyt validi boost-lease (eh.lease.v1, ttl 180 s, priority 90). Releet menivät päälle välittömästi: SG1 ON + SG2 ON, mikä on Thermian relekartassa Boost. Minuutin kuluttua palautus normaliin ja releet pois. Testin tavoite oli nimenomaan reletason readback, ei pumpun lämpövaste — Thermian SG ready -signaalien noin viiden minuutin vaikutusviive tarkoitti, ettei kompressori ehtinyt edes reagoida. Tässä testissä se oli ominaisuus, ei puute: saatiin puhdas rele-todennus ilman turhaa kompressorisykliä.

Testissä paljastui samalla arkkitehtuurin sisäänrakennettu itsekorjautuvuus: manuaalinen testilease ei jäänyt voimaan 180 sekunniksi, koska Decision Engine yliajaa leasen omalla julkaisullaan minuutin sykleissä. Käsin injektoitu tila ei voi jäädä kummittelemaan — järjestelmän oma päätös voittaa aina seuraavalla kierroksella. Retained-zombie-incidenttien jälkeen tämä on täsmälleen haluttu käytös.

Vaihe 3 — rollback. Polku takaisin legacyyn yhdellä valinnalla. Rollback-automaatio pakotti releet normaliksi, payload palasi shadow/legacy-tilaan, ja heti perään ajettu legacy-komentotesti varmisti, ettei uusi final_gate-estoehto rikkonut vanhaa polkua. Watchdog vihreä koko ajan.

Shelly-releiden kommunikaatiovirheet

Testisarja paljasti yhden kovennettavan kohdan. energyhub_hp_force_sg_normal -skripti, joka pakottaa SG-releet normaliin (ja jota käytetään stale-, rollback- ja restart-tilanteissa), kirjasi kahdesti Shelly Pro 2 -releen kommunikaatiovirheen ja hetkellisen unavailable-tilan. Lopputila oli molemmilla kerroilla oikea — releet menivät pois — mutta virheilmoitus tuli silti.

Shellyjen lyhyitä unavailable-jaksoja on tässä järjestelmässä esiintynyt alusta asti ja niitä tulee tiheään; juurisyy (Wi-Fi, mDNS, CoIoT-asetukset tai jokin muu) on oma selvityksensä. Final gate -siirron kannalta oleellinen johtopäätös on kuitenkin tämä: turvakriittinen normalisointipolku ei saa nojata siihen, että rele sattuu olemaan tavoitettavissa juuri sillä sekunnilla. Skriptin retry-logiikka pelasti tilanteen nyt, mutta se piti kovettaa kunnolla ennen kuin lämpöpumpun voi jättää final gate -polkuun pysyvästi.

Force-normal kovennetaan (B5d-1d)

Kovennus tehtiin omana työpakettinaan ennen pysyvää siirtoa. Ydinperiaate oli, ettei turvakriittinen normalisointi saa uskoa yhtä yritystä: skripti tekee nyt useamman hallitun retryn kasvavalla välillä, todentaa lopputilan releen readbackista eikä pelkästä käskyn onnistumisesta, ja kirjaa virheen vain jos releet eivät oikeasti palaudu normaliin. Ohimenevä unavailable-jakso, joka korjaantuu seuraavalla yrityksellä, ei enää tuota valehälytystä — mutta aito jumi tuottaa.

Lisäksi HA:n oma palvelukutsu ei ollut ainoa reitti releeseen. Skripti sai rinnalleen HA:sta riippumattoman Shelly RPC -fallbackin: jos normalisointi HA:n kautta ei mene läpi retryjen sisällä, skripti ottaa suoran yhteyden Shelly Pro 2:een ja pakottaa lähdöt normaliksi sitä kautta. Turvakriittinen polku ei saa kaatua siihen, että yksi väliporras — tässä Home Assistant — sattuu olemaan hetken tavoittamattomissa.

Kovennus todennettiin kaikissa kolmessa käyttötilanteessa, joissa force-normal laukeaa: final gate -viestin vanhenemisessa, rollbackissa polunvaihdon yhteydessä ja HA:n restartissa. Kussakin releet päätyivät readbackilla varmistettuun normaliin, retryt näkyivät lokissa hallittuina eikä yksikään ohimenevä unavailable-jakso enää synnyttänyt virhekirjausta.

Pysyvä siirto (B5d-1e)

Vasta tämän jälkeen lämpöpumppu jätettiin final gate -polkuun pysyvästi. Siirto ei ollut uusi koodimuutos vaan tietoinen päätös olla palaamatta legacyyn testien päätteeksi: energyhub_hp_control_path jäi final_gateen, ja HA-restart-normalisointi yhdessä kovennetun force-normal-polun kanssa takaa, että käynnistyksen jälkeen pumppu on normaalissa kunnes tuore final gate -viesti saapuu.

Seurantajakson aikana kaikki todelliset DHW-boostit — samat, joista onnenkantamoinen shadow-validointi aiemmin syntyi — kulkivat nyt final gate -ketjun läpi fyysiseen ohjaukseen asti, palautuivat normaliin jakson päätyttyä, eikä stale-lippu tai watchdog noussut kertaakaan. Lämpöpumppu ohjautuu nyt samaa reittiä kuin sähköauton lataus: Decision Engine → lease → final gate → HA → SG-releet → Thermia → readback → observer. Vanha suora komentopolku (energyhub/command/hp/mode) jäi olemassa fallbackina mutta ei enää ole normaalin ohjauksen reitti.

Missä ollaan nyt

Testisarja hyväksyttiin kaikilta osin, force-normal on kovennettu ja lämpöpumppu on pysyvästi final gate -polulla:

B5d-0   schema-varoitukset          ✅ commitoitu
B5d-1b  HA live -polun valmistelu   ✅ commitoitu
B5d-1c-0a control path -peili       ✅ commitoitu
B5d-1c-1 normal-only live           ✅ testattu
B5d-1c-2 boost + readback           ✅ testattu
B5d-1c-3 rollback + legacy-todennus ✅ testattu
B5d-1d  force_sg_normal-kovennus    ✅ commitoitu
B5d-1e  pysyvä final gate -jakso    ✅ tuotannossa

Opit

Hiljainen epäonnistuminen on pahin epäonnistumisen laji. Yhden päivän aikana löytyi kolme erillistä mekanismia, joilla järjestelmä olisi voinut ohittaa turvalogiikkaa täysin äänettömästi: schema-validointi joka hylkää lokittamatta, sensorinimi jonka oletusarvo neutraloi ehdon, ja patch-skripti joka ajautuu vain osittain. Yksikään ei olisi näkynyt missään ennen kuin vahinko olisi tapahtunut. Kaikki kolme löytyivät samalla reseptillä: ristiintarkista oletukset todellista järjestelmää vasten ennen kuin nojaat niihin.

Peili ei saa olla ehto. Observability-kenttien pitää kuvata tilaa, ei ohjata sitä. Kun raportointi ja päätöksenteko sekoittuvat, syntyy kehäriippuvuuksia joita on mahdoton debugata.

Shadow-vaihe maksaa itsensä takaisin. Kuukausien varovainen rinnakkaisajo huipentui hetkeen, jossa oikea tuotantopäätös validoi koko ketjun ennen ensimmäistäkään live-komentoa. Se ei ollut tuuria — se oli arkkitehtuuri, joka teki tuurista mahdollisen.

Turvakriittinen polku ei saa nojata yhteen yritykseen eikä yhteen väliportaaseen. Force-normal-kovennus tiivistyy tähän: useampi retry, readbackilla varmistettu lopputila ja HA:sta riippumaton suora RPC-fallback. Normalisointi, joka toimii vain kun kaikki väliportaat sattuvat vastaamaan, ei ole turvamekanismi vaan onnenkauppa.

Sama SG ready -pohjainen ohjausmalli toimii Thermian lisäksi useimmissa moderneissa maalämpöpumpuissa — Nibe, Vaillant ja Daikin tukevat vastaavia SG ready -signaaleja — joten arkkitehtuuri on siirrettävissä laitemerkistä riippumatta. Fyysisenä rajapintana tässä toimii Shelly Pro 2 -rele ja tiedonsiirtona MQTT retained -viestit Mosquitto-brokerin kautta.


Lämpöpumppu ohjautuu nyt lopullisesti samaa deterministististä reittiä kuin sähköauton lataus, ja käsien vakauden päälle voi vihdoin alkaa rakentaa sitä, mitä varten koko siirto tehtiin: ennustepohjaista optimointia, joka antaa järjestelmälle enemmän päätösvaltaa vasta kun sen toimeenpano on todistetusti hallinnassa.

Mutta ennen sitä tässä sarjassa on vielä yksi tunnustus tehtävänä. Tämän artikkelin alussa todettiin ohimennen, että sähköauton latauspolku kulkee final gate -arkkitehtuuria ”jo tuotannossa, stale-suojineen ja rollback-mekanismeineen”. Se piti paikkansa vähemmän kuin kirjoitushetkellä uskoin — ja sen selvittäminen alkoi releestä, joka sahasi kellontarkasti kaksi minuuttia päällä, kolme pois.

Siitä kertoo seuraava osa.

Piditkö artikkelista?

Seuraa blogia myös Blogit.fi:ssä, niin löydät uudet kirjoitukset helposti.

Seuraa blogia Blogit.fi:ssä