Kategoria: HEOMF

  • Case: oma talo — Osa 20

    Voimapirtti: kun teknisesti toimiva paneeli ei vielä ollut hyvä käyttöliittymä

    Edellisessä osassa rakennettiin käsiohjauksen turvallinen selkäranka: tabletti, joka ei saa ohjata suoraan yhtään laitetta, vaan lähettää pelkkiä pyyntöjä samaan päätösketjuun kuin automaation omat päätökset. Se toimi. Pyyntö validoitiin, intent syntyi, resolveri päätti ja final gate toimeenpani — ja kaikki tämä noin kolmessasadassa millisekunnissa.

    Ja silti se ei ollut valmis käyttöliittymä. Se oli insinöörin näkymä, joka sattui roikkumaan seinällä: ruudulla oli teknisiä tiloja, syykoodeja ja rele-termejä, jotka minä ymmärsin mutta jotka eivät kuulu keittiön seinälle koko perheen käyttöön. Tämä osa kertoo siitä, mitä tapahtui kun edellisen osan pyyntörajapinnan päälle rakennettiin oikea käyttöliittymä — ja miksi se työ oli enemmän rajanvetoa kuin pikselinviilausta.

    Paneelilla on nyt nimi, Voimapirtti, ja se on tuotannossa Lenovo Tab M10 -tabletilla keittiön seinällä. Neljä pääsivua — Koti, Sähköauto, Lämpö ja Energia — sekä ylätunnisteesta avautuva Diagnostiikka. Fully Kiosk Browser lukittuna yhteen osoitteeseen, tumma teema, 1280 × 800 -näyttö, luettava metrin päästä. Koko käyttöliittymä on paikallinen: se toimii kotiverkossa ilman pilvipalvelua, ja sen takana on sama Home Assistant– ja Node-RED -pohjainen EnergyHub-kotiautomaatio, jota tämä sarja on rakentanut osa osalta.

    Tärkein raja: esityskerros ei ole ohjauskerros

    Osassa 19 vedettiin luottamusraja: tabletti on epäluotettava ehdottaja, joka saa pyytää mutta ei päättää. Voimapirtti vetää toisen, yhtä tärkeän rajan: käyttöliittymä on renderöintikerros, ei päätöksentekijä. Se näyttää, pyytää ja vahvistaa. Se ei laske sähkön hinnan perusteella latausvartteja, ei laske sulakerajoja, ei ohjaa Thermia-lämpöpumppua suoraan eikä hallitse minkään incidentin elinkaarta.

    Tämä kuulostaa itsestäänselvältä, mutta juuri tämä raja erottaa käyttöliittymän insinöörinäkymästä. Kun paneeli ei omista mitään logiikkaa, sen tehtäväksi jää yksi vaikea asia: kääntää järjestelmän tekninen tila ihmisen ymmärtämäksi tilaksi. Ja se on oma taitolajinsa, jota edellinen tekninen paneeli ei osannut.

    Ketju on sama kuin ennenkin. Selain hakee kanonisen tilan Node-REDin tarjoamalta /energyhub/status-reitiltä noin viiden sekunnin välein, hinta- ja energiatelemetrian omalta /api/voimapirtti/energy-reitiltä ja käyttötilan omalta /api/home_mode/status-reitiltä. JavaScript-adapteri muuntaa backendin kentät renderöintisnapshotiksi. Kirjoituspolku menee edelleen napista pyynnöksi:

    Voimapirtti-painike → Node-RED HTTP -adapteri → energyhub/manual/request (eh.manual.v1) → B3 request validator → domainin Manual Request Manager / Home Mode Manager → Priority Resolver → final gate → fyysinen komento ja readback → /energyhub/status vahvistaa käyttöliittymälle

    Uutta ei ole ketju vaan se, mitä sen molemmissa päissä tehdään: mitä käyttäjälle näytetään ja miten pyyntö puetaan.

    Yksi luku, joka opetti koko projektin: HTTP 200 ei ole vahvistus

    Jos jokin yksittäinen oivallus kiteyttää eron teknisen paneelin ja käyttöliittymän välillä, se on tämä. Kun käyttäjä painaa ”Lataa nyt”, HTTP-vastaus palaa, 200 tai 202, millisekunneissa. Houkutus on näyttää nappi heti aktiivisena — pyyntöhän meni läpi.

    Mutta pyyntö läpi ei ole komento toteutunut. B3 voi hyväksyä pyynnön, Manual Request Manager luoda intentin, ja silti Priority Resolver päättää final gaten ehdoilla toisin — tai rele ja readback päivittyvät vasta sekuntien viiveellä. Siksi Voimapirtti ei koskaan merkitse komentoa aktiiviseksi HTTP-vastauksen perusteella. Painike kulkee tilojen läpi — vapaa, lähetetään, vastaanotettu — ja muuttuu aktiiviseksi vasta kun kanoninen status vahvistaa saman request_id:n. Palvelin omistaa tunnisteen, palauttaa sen 202-vastauksessa, ja selain odottaa näkevänsä juuri sen tunnisteen statuksessa ennen kuin uskoo mitään tapahtuneen.

    Tämä on täsmälleen sama ”komento ≠ toteuma” -periaate, joka on kulkenut koko tämän sarjan läpi releistä lähtien. Nyt se päti käyttöliittymään: paneeli näyttää vahvistettua tilaa, ei toiveita. Sama koskee käyttövettä — boost näkyy aktiivisena vasta kun status vahvistaa dhw.manual.active-tilan — ja sisälämpötavoitetta, joka katsotaan tallennetuksi vasta kun status palauttaa saman heating.target_c-arvon takaisin. Miinus ja plus muuttavat vain paikallista luonnosta; mitään ei lähde ennen ”Tallenna tavoite” -painallusta, eikä tavoite näy hyväksyttynä ennen kuin backend vahvistaa sen.

    Puuttuva arvo on viiva, ei nolla

    Toinen periaate näyttää pikkutarkalta mutta on käyttöpaneelissa turvallisuuskysymys. Jos jokin datalähde ei ole saatavilla, Voimapirtti ei näytä nollaa — se näyttää viivan tai ”Ei tietoa”. Nolla on oikea mittausarvo: 0,0 kW aurinkoa on eri asia kuin ”aurinkodataa ei tule”. Jos puuttuva arvo esitetään nollana, käyttäjä tekee päätöksiä datasta jota ei ole.

    Tämä periaate näkyy juuri nyt Voimapirtin Lämpö-sivulla konkreettisemmin kuin haluaisin. Sivun huonelämpölista näyttää neljä Netatmo-huonetta — Keittiö, Olohuone, Makuuhuone, Alakerta — ja kaikkien kohdalla lukee tällä hetkellä ”Ei tietoa”, vaikka Thermia-anturi ja ulkolämpötila päivittyvät normaalisti. Syy ei ole Voimapirtissä. Samat lämpötilalähteet ovat parhaillaan toisen, rinnakkaisen kehitystyön alla, ja niiden telemetria on hetkellisesti poikki. Voimapirtti tekee juuri sen mitä pitää: se ei keksi nollaa eikä näytä vanhaa arvoa tuoreena, vaan kertoo rehellisesti, ettei tietoa nyt ole. Kun lähde palaa, rivit täyttyvät itsestään. Tyhjä kohta ruudulla on tässä ominaisuus, ei vika — ja hyvä muistutus siitä, miksi rinnakkaiset kehitystyöt pidetään tarkasti erillään, mihin palaan lopussa.

    Neljä sivua ja se, mitä kullakin näytetään

    Koti-sivu on kodin energiavirta: aurinko, koti ja verkko kolmena solmuna, ja virtaus haarautuu sen mukaan, tuottaako talo yli oman kulutuksen vai ottaako verkosta. Yläkulmassa lukee suunta selväkielisenä — ”Aurinko → koti + verkko” — ja oikealla on tiivis tilannekortisto: käyttötila, sähköauton varaustaso, lämpö ja käyttövesi. Alapalkissa päivän neljä energialukemaa: tuotettu, kulutettu, verkosta ja verkkoon. Nämä neljä muodostavat taseen, ja se tase tarkistetaan — tuotettu plus verkosta on kulutettu plus verkkoon.

    KUVA 1. Koti-sivu tuotannossa: aurinko syöttää samaan aikaan taloa ja verkkoa, oikean laidan kortit kokoavat sähköauton, lämmön ja käyttöveden yhteen silmäykseen. Yläreunan ”Kotona · normaali automatiikka” kertoo aktiivisen käyttötilan.

    Sähköauto-sivu näyttää varaustason, latausvalmiuden, hinnan ja toimintamatka-arvion, ja alareunassa on neljä isoa käskyä: lataa nyt, estä lataus, 80 % aamuksi ja 100 % aamuksi. ”Latausvalmius päällä” on tässä tarkoituksella eri asia kuin ”lataa” — rele voi olla kytkettynä ilman että auto ottaa tehoa, ja tämä sarja on jo kertaalleen käsitellyt sen, miksi näiden erottaminen on tärkeää. Sivun yläkulmassa lukee, kulkeeko ohjaus final gaten kautta.

    KUVA 2. Sähköauto-sivu: auto on 62 prosentissa ja latausvalmius on päällä halvalla hinnalla, mutta ”Latausvalmius päällä” ei tarkoita että virtaa otetaan juuri nyt. Yläkulman ”ENERGYHUB · FINAL GATE” kertoo ohjauksen kulkevan turvakerroksen läpi; alarivin painikkeet ovat käsiohjauksen sisääntulo.

    Lämpö-sivu kokoaa lämpövyöhykkeet, sisä- ja ulkolämmön, sisälämpötilatavoitteen säädön ja käyttöveden. Tavoitteen säätö on 18–23 °C yhden asteen askelin, ja sen alla lukee rehellisesti järjestelmän tila: ”Tavoiteohjaus estyy: control_mode_shadow”. Sisälämpötavoitteen fyysinen vaikutus on nimittäin edelleen shadow-tilassa — koko ketju käyttöliittymästä kanoniseen tilaan toimii, mutta comfort wheeliin ei vielä kosketa, koska sisälämpötila on hidas prosessi jota ei voi validoida heinäkuisella minuuttitestillä. Paneeli näyttää tämän tilan suoraan sen sijaan että teeskentelisi ohjaavansa.

    KUVA 3. Lämpö-sivu juuri sinä päivänä, jona neljän huoneen anturidata oli poikki rinnakkaisen kehitystyön takia: rivit näyttävät ”Ei tietoa”, eivät nollaa. Thermia-anturi ja ulkolämpötila päivittyvät normaalisti, sisälämpötavoite on tallennettu ja lämmityksen tavoiteohjaus lukee rehellisesti ”estyy: control_mode_shadow”.

    Energia-sivu on päivän spot-hintakäyrä, nykyiset päätökset domaineittain ja aktiivisen charge_by-tilauksen varatut vartit. Se on read-only. Tässä on rehellinen puute, jonka paneeli näyttää sellaisenaan: normaalin automaation yleistä 96 vartin EV-suunnitelmaa backend ei vielä julkaise, joten sivulla lukee ”current decision only” silloin kun aktiivista aikataulutettua latausta ei ole. Paneeli ei keksi suunnitelmaa jota backend ei anna.

    KUVA 4. Energia-sivu: 96 vartin spot-hintakäyrä pystyviiva merkitsee nykyhetkeä, ja kortit kertovat kunkin domainin nykyisen päätöksen — EV ”lataus sallittu nyt”, käyttövesi ”opportunistic”, lämmitys ”normal”. ”EV-latauksen varatut vartit” on tyhjä, koska aktiivista charge_by-suunnitelmaa ei juuri nyt ole.

    Kotona ja Poissa: käyttötila, jolla on omistaja

    Uusin toiminnallinen lisä on käyttötila. Koti-sivun yläosassa on kaksiasentoinen valinta: Kotona tai Poissa. Se näyttää yksinkertaiselta kytkimeltä, mutta sen alla on sama omistajuusajattelu kuin kaikessa muussakin.

    Poissa ei ole löyhä ”säästötila”. Se julkaisee tunnistettavan 24 tunnin charge_block-pyynnön ja uusii sen tunnin välein — eli sammuttaa sähköauton latauksen ja pitää sen sammuksissa niin kauan kuin tila on Poissa. Kotona taas palauttaa normaalin automatiikan poistamalla vain käyttötilan itsensä tekemän eston. Tämä rajaus on tärkeä: Kotona ei saa tyhjentää käyttäjän erikseen tekemää muuta käsiohjausta. Jos olet käsin estänyt latauksen jostain omasta syystäsi, Kotona-nappi ei kumoa sitä — se koskee vain omaa estoaan. Jokaisella estolla on oma source ja request_id, ja tila purkaa vain sen mitä se itse omistaa.

    Live-testissä tämä toimi juuri niin: Poissa sammutti EV-releen alle minuutissa, ja Kotona antoi releen kytkeytyä heti takaisin, jos normaali automaatio pyysi latausta.

    Käyttötila v1:llä on tietoinen rajaus, jonka paneeli ei piilota: se vaikuttaa toistaiseksi vain sähköauton lataukseen. Jos Poissa syrjäyttää aktiivisen 80/100 % aamuksi -tilauksen, tilausta ei vielä palauteta automaattisesti Kotona-tilaan palattaessa. Paluuaika sekä lämmitys- ja käyttövesivaikutukset ovat jatkokehitystä — eikä niitä kannata tehdä ennen kuin on määritelty tarkasti, mitä Poissa oikeasti muuttaa kussakin domainissa ja kuka sen omistaa.

    Utility meter, joka näytti järkevää nollaa

    Yksi VP4.2:n konkreettisista korjauksista ansaitsee oman kappaleensa, koska se on juuri sitä hiljaisten vikojen luokkaa, josta tämä sarja on kirjoittanut aiemminkin. Päivän energialukemat — verkosta ja verkkoon tänään, eli pörssisähkön ja aurinkosähkön tase — nojasivat aiemmin Home Assistantin utility meter -antureihin, jotka jäivät keskiyön nollaan eivätkä seuranneet varsinaisia P1-mittarilähteitä. Ne näyttivät järkevää nollaa. Nolla on uskottava luku, ja juuri siksi vika oli hankala huomata: mittari näytti toimivalta, koska sen arvo oli täysin mahdollinen.

    Vika paljastui vasta seuraavan vuorokauden aikana: lukema ei kertynyt, vaikka verkosta selvästi otettiin ja verkkoon syötettiin. Korjaus vaihtoi lähteeksi kumulatiiviset HomeWizard-mittarit, jotka eivät nollaudu jaksoittain ja ovat aina saatavilla; ne kalibroitiin korjauspäivän toteutuneisiin arvoihin, ja Voimapirtti siirrettiin lukemaan niitä. Todennus oli karkea mutta kiistaton: lähteen 7 kWh:n muutoksen piti näkyä mittarin 7 kWh:n muutoksena — ja näkyi. Utility meter voi siis näyttää järkevää nollaa, vaikka data ei kerry. Siksi lähteen ja johdetun anturin päivitystapahtumat on verrattava keskenään, ei tuijotettava pelkkää lopputulosta.

    Muut opit tien varrelta

    Käyttöliittymän rakentaminen tuotti oman kokoelmansa hiljaisia sudenkuoppia, jotka kannattaa nimetä:

    CSS-luokkanimen törmäys voi piilottaa koko dokumentin. Yksi väärä luokkanimi teki ruudusta valkoisen — ei virheilmoitusta, ei mitään, pelkkä tyhjä valkoinen näyttö. Virheansan pitää näyttää syy valkoisen ruudun sijaan, muuten vika on mahdoton paikantaa seinällä olevalta tabletilta.

    Android WebView voi olla vanhempi kuin kehityskoneen selain. Sivu, joka toimii moitteetta läppärillä, voi kaatua tabletin selainmoottorissa vanhempaan JavaScript-ominaisuuteen. Yhteensopivuus on testattava sillä laitteella, jolla paneeli oikeasti pyörii, ei sillä jolla se kehitetään.

    Home Assistantin YAML-avain, friendly name, unique_id ja entity_id ovat eri asioita. Sensorin oikea runtime-nimi ei ole se, miltä se YAML:ssa näyttää — se pitää tarkistaa entity registrystä tai recorderista.

    Puuttuva current-state-node ei saa jättää HTTP-pyyntöä auki. Telemetria-adapteri tarvitsee timeoutin ja fallback-vastauksen, muuten yksi puuttuva solmu jättää selaimen odottamaan ikuisesti.

    Opit

    Käyttöliittymän tärkein työ oli kääntäminen, ei koristelu. Tekninen paneeli näytti järjestelmän tilan sellaisenaan. Voimapirtti kääntää sen ihmisen kielelle — ”Latausvalmius päällä”, ”Halpa/aurinko”, ”Poissa · EV OFF” — ilman että se piilottaa mitään olennaista. Vaikein osa ei ollut fonttien ja värien valinta vaan sen päättäminen, mikä tila ansaitsee mitäkin sanan.

    Esityskerros ei saa omistaa logiikkaa. Kun paneeli ei laske hintoja, sulakerajoja eikä ohjaa laitteita, se ei voi myöskään erehtyä niissä. Sama rajapinta, joka syöttää tabletille tilan, voidaan antaa mille tahansa muulle esittäjälle ilman että kukaan saa laiteoikeuksia.

    HTTP 200 ei ole komentovahvistus. Aktiivinen tai valmis tila todennetaan request_id:n ja kanonisen statuksen kautta. Tämä periaate maksoi itsensä takaisin jokaisessa napissa: käyttäjä näkee vahvistetun tilan, ei sitä että pyyntö lähti.

    Puuttuva arvo on viiva, ei nolla. Nolla on oikea mittausarvo, ja sen esittäminen puuttuvan datan tilalla on valhe, jonka varassa käyttäjä tekee päätöksiä. Lämpö-sivun tyhjät huonerivit ovat juuri nyt tämän periaatteen elävä todiste.

    Kaksi rinnakkaista kehitystyötä eivät saa muokata samaa flows.json-tiedostoa keskeneräisinä. Voimapirtti lukittiin tuotantoon omaksi committikseen, ja sen rinnalla kulkeva telemetriatyö rakennetaan tuon lukitun HEADin päälle — ei samaan installeriin, backupiin eikä committiin. Juuri tästä syystä Lämpö-sivun anturit ovat tänään hiljaa: toinen työ omistaa ne hetken, ja raja pidetään puhtaana tarkoituksella. Looginen työ commitoidaan ensin, ja seuraava asennus sidotaan uuteen HEADiin.

    Mitä vielä puuttuu

    Rehellisyyden nimissä lista on tälläkin kertaa pitkä. Käyttötilan ja uusittavan EV-eston restart-regressio on vielä ajettava läpi: Lenovon, Node-REDin, Home Assistantin ja tabletin pitää selvitä uudelleenkäynnistyksestä niin, että Kotona/Poissa ja eston uusiutuminen palautuvat oikein. Offline-regressio — Wi-Fi poikki ja takaisin — pitää todentaa niin, että UI näyttää OFFLINEn, lukitsee komennot ja palautuu itsestään. Normaalin EV-automaation 96 vartin suunnitelma puuttuu Energia-sivulta, samoin PV-lähteen yhtenäinen tuoreustieto. Poissa-tilan paluuaika, laajemmat käyttötilat (vieras, yö, pitkä poissaolo) ja käyttötilan vaikutus lämmitykseen ja käyttöveteen ovat kaikki jatkokehitystä, joka odottaa selkeitä omistajuussääntöjä. Ja se F17-siirtymäluokitus: EV-eston purkamisen jälkeen resolveri voi hetkellisesti kirjata decided_no_relay -poikkeaman, vaikka paluu automatiikalle toimii — normaali rele/readback-siirtymä pitää osata erottaa todellisesta häiriöstä, ettei paneeli näytä turhaa järjestelmävirhettä.

    Mutta nykytila kelpaa tuotantoon: Voimapirtti pyörii seinällä, sähköauton ja käyttöveden käsiohjaus, sisälämpötavoitteen asetus ja Kotona/Poissa toimivat päästä päähän, päivän energiat kertyvät oikein — ja tabletilla ei edelleenkään ole oikeuksia yhteenkään releeseen.


    Seuraavassa osassa mennään sen taakse, mitä Voimapirtin ”80 % aamuksi” -nappi todella käynnistää. Määräaikaan sidottu lataus näyttää napilta, mutta sen takana on suunnitelma, joka lukee lähtöajan, varaustason ja huomisen varttihinnat ja rakentaa niistä latauksen halvimmille varteille — ilman että se lupaa liikaa niistä prosenteista, joita auton oma latauselektroniikka hidastaa loppua kohden. Siitä, ja siitä miksi auto täyteen aamukuudeksi on hankalampi lupaus kuin miltä se kuulostaa, kertoo osa 21.

    Piditkö artikkelista?

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

    Seuraa blogia Blogit.fi:ssä
  • Case: oma talo — Osa 19

    Käsiohjaus: tabletti, joka ei saa ohjata mitään

    EnergyHubia on tähän asti käytetty insinöörin työkaluilla: SSH:lla, mosquitto_pub-komennoilla ja Home Assistantin kehittäjätilalla. Se toimii, mutta sillä on hintansa. Kun safety trip jäi kesäkuussa päälle, kuittaus onnistui lopulta ilman SSH:takin — mutta vain MQTT-komennoilla ja HA:n Developer Tools -näkymässä, eli ei millään, mitä kehtaisi kutsua käyttöliittymäksi. Siitä lähtien tiekartalla on lukenut: järjestelmä tarvitsee oikean käyttöpaneelin.

    Nyt se on seinällä. Android-tabletti, Fully Kiosk Browser lukittuna yhteen osoitteeseen, tumma teema ja kuusi osiota. Paneelista voi käynnistää sähköauton latauksen puolesta tunnista neljään tuntiin, estää latauksen, tilata auton 80 tai 100 prosenttiin aamukuudeksi, käynnistää käyttövesiboostin ja asettaa talon sisälämpötilatavoitteen.

    Tämän osan tärkein asia ei kuitenkaan ole se, mitä paneelilla voi tehdä, vaan se, mitä sillä ei voi: tabletti ei saa ohjata suoraan yhtään laitetta. Ei relettä, ei lämpöpumppua, ei yhtään Home Assistantin palvelukutsua. Jokainen napinpainallus on pelkkä pyyntö. Tämä osa kertoo siitä turvallisesta pyyntöarkkitehtuurista — ei niinkään käyttöliittymän ulkoasusta, joka sai sittemmin oman kunnollisen käsittelynsä ja josta kirjoitan erikseen seuraavassa osassa.

    Tärkein päätös tehtiin ennen ensimmäistäkään nappia

    Ennen kuin riviäkään käyttöliittymäkoodia kirjoitettiin, piti ratkaista, mikä seinäpaneeli järjestelmän silmissä on: suora ohjain, joka kytkee releitä? Home Assistantin dashboard, joka kääntelee helpereitä? MQTT-asiakas, joka julkaisee komentoja? Vai pelkkä pyyntöjen lähettäjä?

    Valinta oli viimeinen, ja se oli koko paketin tärkein arkkitehtuuripäätös. Käsiohjaus rakennettiin erottamalla viisi asiaa toisistaan: käyttäjän pyyntö, järjestelmän hyväksymä aikomus eli intent, resolverin tekemä päätös, fyysinen toteutus ja toteutuksen kuittaus. Käsiohjaus ei tarkoita turvakerrosten ohittamista — se tarkoittaa yhtä uutta sisääntuloa samaan päätösketjuun.

    Fully Kiosk
        ↓
    Käyttäjän pyyntö
        ↓
    Validointi ja käyttöoikeudet
        ↓
    Manual intent / canonical state
        ↓
    Priority Resolver
        ↓
    Safety, capability, admission ja final_gate
        ↓
    Home Assistant / Shelly / Thermia
        ↓
    Readback, observability ja käyttöliittymän status

    Prioriteettijärjestys on tarkkaan mietitty: käsiohjaus voittaa spot-hinnan ja normaalin optimoinnin — nappi on kovempi argumentti kuin halpa vartti. Mutta se ei voita safety tripiä, vanhentunutta vaihedataa, sulake- ja kuormarajoja, capability-estoja, final_gatea eikä readback-valvontaa.

    Teollisuusautomaatiossa tämä on itsestäänselvyys: HMI-paneeli ei ohjaa lähtöjä suoraan, vaan kirjoittaa asetusarvoja ja pyyntöjä logiikalle, joka omistaa lukitukset. Kotiautomaatiossa sama raja on aivan yhtä arvokas — ja sivutuotteena syntyi jotain isompaa. Seinäpaneeli on järjestelmän näkökulmasta epäluotettava ehdottaja, ja täsmälleen sama rajapinta voidaan myöhemmin antaa vaikka paikalliselle kielimallille tai ilmoitusjärjestelmälle ilman, että kenellekään jaetaan laiteoikeuksia.

    Käyttöliittymä ei asu Home Assistantissa

    Fully Kiosk on vain lukittu selain. Varsinainen käyttöliittymä on Node-REDin tarjoama HTML-, CSS- ja JavaScript-sivu:

    GET /energyhub          → Render Operator UI    → HTML
    GET /energyhub/status   → Operator status JSON  → JSON

    Reitti lyheni olennaisesti. Vaihtoehto olisi ollut Fully Kiosk → HA:n Lovelace-dashboard → helperit → Node-RED → EnergyHub; nykyinen kulkee Fully Kiosk → Node-RED → EnergyHub. Home Assistant on yhä mukana laiteintegraatioissa ja joidenkin canonical state -arvojen omistajana, mutta paneelin liikenne ei kierrä sen kautta.

    Status-JSON on valmiiksi koottu näkymä: resolverin viimeisin päätös, EV:n päätös ja fyysinen tila, manual intentit, vaihevalvonnan tila, telemetrian tuoreus, sisälämpötilatavoite ja Thermian huonelämpömittaus, eston syy ja yleinen operator_ok. Selaimen ei tarvitse yhdistellä MQTT-topiceja tai päätellä, mikä tila on tärkein — Node-RED tekee renderöintiin tarkoitetun tilamallin valmiiksi.

    Tuoreus otetaan vakavasti: jokaiselle datalähteelle on oma ikä- ja stale-kynnys, vastaukset lähtevät Cache-Control: no-store -otsakkeella, ja yläpalkissa juokseva ”päätösikä” kertoo jatkuvasti, kuinka vanha resolverin viimeisin päätös on. Vanhentunut mutta uskottavan näköinen data on käyttöpaneelissa vaarallisempaa kuin puuttuva data.

    KUVA 1: Päävalikko: tilarivi, kuusi osiota ja ”Nyt käynnissä” -yhteenveto. Alapalkki näyttää aina, onko käsiohjauksia aktiivisena.

    Miksi selain puhuu HTTP:tä eikä MQTT:tä

    Alkuperäisessä suunnitelmassa tabletti olisi ollut suora MQTT-over-WebSocket-asiakas, ja se polku rakennettiinkin valmiiksi. Nykyinen Operator UI käyttää silti pääasiassa HTTP:tä: selain → Node-REDin HTTP-endpoint → paikallinen MQTT.

    Syyt ovat käytännöllisiä. MQTT-tunnuksia ei tarvitse upottaa selaimen JavaScriptiin, Node-RED tarkistaa parametrit ennen julkaisua, käyttöliittymä ei tarvitse omaa MQTT-tilakonetta, statusdata kootaan palvelimella, ja CORS-, reconnect- ja retained-viestien käsittely jää palvelinpuolelle. WebSocket-rakenne jäi turvattuna varalle tulevaa käyttöä varten.

    MQTT lukkojen taakse

    Samassa yhteydessä koko brokerin ovet käytiin läpi. Mosquitton normaali portti 1883 sidottiin pelkkään loopbackiin — lähiverkon tai Tailscalen asiakas ei voi enää liittyä anonyymisti ja julkaista vaikkapa command-, lock- tai safety-topiceihin. Node-RED, Home Assistant ja paikalliset skriptit käyttävät edelleen osoitetta 127.0.0.1:1883.

    Seinäpaneelia varten on erillinen WebSocket-listener portissa 9001: ei anonyymejä yhteyksiä, oma käyttäjä ja salasana, 64 KiB:n pakettiraja ja tiukka ACL:

    Saa kirjoittaa:  energyhub/manual/request
                     energyhub/ui/wall_panel/online
    
    Saa lukea:       energyhub/ui/wall_panel/status
                     energyhub/manual/reject

    Kaikki muu on kiellettyä: intent-, command-, final_gate-, safety- ja capability-topicit sekä laitteiden suorat ohjaukset. Vaikka joku saisi tabletin MQTT-tunnukset käsiinsä, niillä voi lähettää vain pyyntöjä — samoja, jotka validoidaan joka tapauksessa.

    Pyynnön anatomia

    Kaikki kolme domainia — sähköauto, käyttövesi ja sisälämpötila — kulkevat saman sisääntulon kautta: energyhub/manual/request, schemalla eh.manual.v1. Näin EV-pyyntö näyttää:

    json

    {
      "schema": "eh.manual.v1",
      "ts": "2026-07-19T11:34:15.577Z",
      "request_id": "operator-ui-ev-charge-block-...",
      "source": "operator_http_ui",
      "actor": "wall_tablet",
      "domain": "ev",
      "intent": "charge_block",
      "requested_duration_s": 3600,
      "payload": { "ui": "energyhub_operator", "endpoint": "charge_block" }
    }

    B3-validator tarkistaa scheman, kenttien kelvollisuuden, aikaleiman, sallitut domain–intent-yhdistelmät, payloadin rakenteen ja arvoalueet. Kaksi tarkistusta on periaatteellisesti muita tärkeämpiä: pyyntö ei saa sisältää asiakkaan itse määräämää expires_at– tai ttl_s-arvoa, eikä sillä voi kirjoittaa command- tai final_gate-tasolle.

    Toisin sanoen tabletin kello ei päätä mitään. Selain saa pyytää kestoa tai semanttista deadlinea, mutta lopullisen päättymisajan laskee Manual Request Manager palvelimen kellolla. Väärässä ajassa oleva tai manipuloitu asiakas ei voi jättää käsiohjausta voimaan liian pitkäksi aikaa.

    Hylätyt pyynnöt julkaistaan omaan energyhub/manual/reject-topiciin request_id:n ja syyn kera — unsupported_intent, invalid_ts, target_c_out_of_range ja niin edelleen. Hyväksytyt reititetään domainin mukaan: EV ja käyttövesi omille Manual Request Managereilleen, sisälämpötila omalle sovittimelleen.

    Sähköauto: lataa nyt, estä, lataa aamuksi

    EV:n Manual Request Manager omistaa käsiohjauksen koko elinkaaren: deduplikoinnin, rate limitin, vanhan intentin korvaamisen, expiryn ja tyhjennyksen. Hyväksytty käsiohjaus julkaistaan retained-intenttinä topiciin energyhub/manual/intent/ev tiloilla active, superseded, expired ja cleared. Retained-tila tarkoittaa, että Node-REDin restart ei hukkaa aktiivista käsiohjausta — ja palvelimen laskema expiry tarkoittaa, ettei vanhentunut intent myöskään herää restartissa henkiin.

    Toteutetut intentit ja niiden rajat:

    • charge_now — lataus päälle heti, enintään 4 h (p_manual_ev_charge_now_max_s = 14400)
    • charge_block — lataus estoon, enintään 24 h
    • charge_by — tavoite-SOC määräaikaan mennessä; paneelin napit tarjoavat 80 % ja 100 % seuraavaan kello kuuteen. Deadline lasketaan backendissä Europe/Helsinki-ajassa kesä- ja talviaika huomioiden.
    • clear — palauttaa päätöksenteon normaalille optimoinnille.

    Resolverissa manuaali-intent ei kirjoita relettä, vaan muuttaa ehdokaspäätöstä, joka kulkee normaalin ketjun läpi:

    safety / vaihedata / capability
        ↓
    manual EV intent
        ↓
    charge_by-suunnitelma tai charge_now / charge_block
        ↓
    normaali hinta- ja PV-ohjaus
        ↓
    EV start admission
        ↓
    final_gate

    Observabilityssä näkyy koko ajan rinnakkain, mitä normaali logiikka olisi tehnyt ja mitä käsiohjaus muutti:

    json

    {
      "active": true,
      "intent": "charge_block",
      "manual_decision": false,
      "normal_decision": true,
      "would_change_decision": true,
      "blocked_by": null,
      "live_applied": true,
      "live_decision": false
    }

    Yhdestä viestistä selviää, mitä käyttäjä pyysi, muuttiko pyyntö päätöksen, toteutettiinko se — ja jos ei, mikä turvakerros esti.

    Käyttöönotto tehtiin samalla kaavalla kuin final gate aikanaan: ensin State Collector alkoi lukea intentin ja resolveri laski shadow-päätöksen, tuloksia verrattiin normaaliin päätökseen, ja vasta sitten charge_now ja charge_block kytkettiin live-polkuun. Sen jälkeen vanha s.evManualOverride-reitti poistettiin kokonaan — kaksi rinnakkaista käsiohjausjärjestelmää olisi ollut pahempi kuin ei yhtään. Neljän tunnin charge_now-regressiotestissä intent vanheni ajallaan ja päätös palasi optimoinnille itsestään.

    charge_by on näistä kolmesta se, jolla on eniten liikkuvia osia: se ei ole kytkin vaan määräaikaan sidottu suunnitelma, joka lataa halvimmilla varteilla mutta ehtii silti tavoite-SOC:hen deadlineen mennessä. Sen shadow-vertailu ja ensimmäiset yön yli -ajot 80 ja 100 prosentin tavoitteilla on jo ajettu läpi — mukaan lukien 100 prosentin erikoistapaus, jossa auton oma latauselektroniikka hidastaa loppua kohden ja viimeiset prosentit on osattava varata suunnitelmaan. Suunnittelulogiikka ansaitsee kuitenkin oman osansa, joten tässä se pysyy yhtenä käsiohjausintenttinä muiden joukossa; latausennusteesta ja varttihintojen lookaheadista kirjoitan tarkemmin myöhemmin.

    Painikkeissakin on omat varmistuksensa. HTTP-backendissä on enable-lippu, jolla napit asennettiin ensin kuivaharjoittelutilaan: backend palautti, mitä se olisi julkaissut, mutta ei vielä lähettänyt MQTT-pyyntöä. Ja itse paneelissa EV-backend on oletuksena lukittu — vahinkokosketus näytöltä ei lähde mihinkään ennen kuin lukitus avataan, ja aktiivisesta backendista varoitetaan erikseen.

    KUVA 2: Auto/EV-näkymä: SOC, latausprofiili, admission-tila ja käsikäytön napit. Alareunassa oletuslukituksen ilmoitus.

    Käyttövesi: komento ei ole valmistuminen

    Käyttövedellä on oma MRM ja oma retained intent (energyhub/manual/intent/dhw), päätoimintona boost_now. Rakenne on sama kuin EV:llä, mutta yksi ero tekee siitä kiinnostavamman: käyttövesiboost on prosessi, ei kytkin. Pelkkä komennon lähettäminen ei tarkoita, että vesi lämpeni.

    Siksi resolveri ja observability seuraavat koko ajon ajan Thermian running priorityä, Shellyn tehomittausta, kompressorin toimintaa, veden lämpötilaa ja boostin ikää — ja intent päätyy terminal-tilaan, joka on completed, expired, cancelled tai failed sen mukaan, mitä oikeasti tapahtui.

    Tässä lunastuu myös osan 18 lopussa annettu lupaus, joskin eri muodossa kuin se annettiin. Ensimmäinen oikea DHW-boost koko uuden arkkitehtuurin läpi on nyt ajettu: pyyntö → gate → MRM → resolveri → final_gate → Thermia → readback → terminal state completed, syynä manual_dhw_completed_heating_confirmed. Se ei tosin toteutunut niin, ettei kukaan koskenut mihinkään — vaan täsmälleen päinvastoin. Joku painoi nappia. Järjestelmä hoiti loput ja osasi vielä todistaa mittauksista, että vesi todella lämpeni.

    KUVA 3: Käyttövesinäkymä kesken Thermian oman käyttövesiajon: priority 3, kompressori 45 %, mitattu teho 2.2 kW. Boost-napit alhaalla.

    Sisälämpötila: pysyvä asetus, ei lease

    Kolmas domain rikkoi kaavan. Sisälämpötilatavoite ei ole määräaikainen käsiohjaus samalla tavalla kuin charge_now — se on pysyvä asetus, jolla ei ole expiryä. Siksi sille ei rakennettu retained intent -omistajaa, vaan canonical state -ketju:

    manual request
    → Indoor Target Command Adapter
    → energyhub/command/indoor_target/set
    → Home Assistant (input_number = canonical owner)
    → energyhub/state/indoor_target

    Olennainen yksityiskohta: HTTP-vastaus 202 Accepted ei todista mitään. Node-RED ei pidä command-viestiä onnistuneena kuittauksena, vaan odottaa Home Assistantilta vahvistettua state-viestiä — ja vasta se päivittää käyttöliittymän. Paneeli ei koskaan näytä tavoitetta hyväksyttynä vain siksi, että pyyntö lähti.

    Tavoitealue on 18–23 °C yhden asteen askelin, kokonaislukuina — samalla logiikalla kuin Thermian omassa käyttöliittymässä. Resolveri lukee tavoitteen rinnalla Thermian huonelämpömittausta, jonka tuoreusraja on 180 sekuntia: jos mittaus on stale, live-säätöä ei saa tehdä.

    Ja juuri live-säätö on se, mitä ei vielä tehdä. Koko ketju käyttöliittymästä canonical stateen ja resolverin shadow-laskentaan toimii, mutta fyysinen vaikutus Thermian comfort wheeliin on tarkoituksella pidetty shadow-tilassa. Syy ei ole keskeneräisyys vaan fysiikka: sisälämpötila on hidas prosessi, jota ei voi validoida muutaman minuutin testillä heinäkuussa. Ennen liveä pitää testata ainakin lämmityskausi, kesä-idle, kylmäpakko, stale mittaus, molempien järjestelmien restartit, comfort wheelin readback ja liian tiheiden säätöjen esto.

    KUVA 4: Lämmitysnäkymä: tavoite tallennettu, ohjaustila shadow. Comfort wheeliä ei vielä kosketa.

    Kadonnut minuutti

    Kun kaikki edellä kuvattu toimi, jäljelle jäi yksi ärsyttävä asia: napit tuntuivat hitailta. Pyyntö validoitiin ja intent syntyi millisekunneissa, mutta käyttöliittymän lopullinen vahvistus odotti resolveria — ja flow-kartoitus varmisti, että Priority Resolverin ainoa käynnistäjä oli 60 sekunnin inject. Vahvistus tuli satunnaisesti 0–60 sekunnin viiveellä. Käyttöliittymässä se tuntuu ikuisuudelta.

    Ilmeinen ratkaisu olisi ollut nopeuttaa sykli jatkuvaksi parin sekunnin silmukaksi. Sitä ei tehty — jatkuva pyöritys olisi ollut pelkkää kuormaa ilman tarvetta. Sen sijaan minuuttisyklin rinnalle lisättiin tapahtumapohjainen ajo:

    EV/DHW intent muuttui  → suodatus → 250 ms settle → resolveri
    state/indoor_target    →            250 ms settle → resolveri

    Sisälämpötilan triggeri lähtee tarkoituksella vasta canonical state -viestistä — ei pyynnöstä eikä command-viestistä — jotta nopeakin vahvistus perustuu vahvistettuun tilaan. Ja 250 millisekunnin settle on olemassa siksi, että sama MQTT-viesti haarautuu yhtä aikaa State Collectorille ja fast triggerille: pieni viive varmistaa, että resolveri lukee uuden snapshotin eikä edellistä.

    Toteutus paljasti kaksi kaksoisajoa, ja tässä kohtaa olisi ollut helppo lisätä yleinen debounce ja unohtaa koko asia. Sen sijaan syyt selvitettiin. Ensimmäinen: fast trigger oli kytketty wildcard-tilaukseen energyhub/#, ja samassa broker-yhteydessä oli jo tarkka tilaus samalle intent-topicille — sama viesti toimitettiin kahdesti. Korjaus oli siirtää triggerit tarkkoihin topiceihin ja antaa käsiohjausketjulle kokonaan oma MQTT client ID (nodered_c1_manual). Toinen: kun uusi intent korvasi vanhan, MRM julkaisi tarkoituksella sekä superseded- että active-tapahtuman — molemmat oikeita, mutta resolveria ei tarvitse ajaa audit-tapahtumasta, kun uusi active tulee heti perässä. Nyt superseded suodatetaan; active, expired, cleared ja tombstone ajavat resolverin.

    Lopputulos baseline-commitissa 05d5690 (”Add event-driven fast ACK for manual controls”):

    manual/request       14:34:15.581
    active intent        14:34:15.626
    resolver decision    14:34:15.930

    Pyynnöstä päätökseen 349 millisekuntia. Sisälämpötilalla, canonical state -kierroksen kautta, 314 millisekuntia. Minuuttisykli jäi paikalleen expiryjen varmistajaksi, normaalin optimoinnin ajajaksi ja turvaverkoksi siltä varalta, että tapahtumaviesti jäisi joskus käsittelemättä.

    Opit

    Käyttöliittymäprojektin tärkein päätös ei koskenut käyttöliittymää. Värit, fontit ja napit ovat helppoja. Vaikea ja ratkaiseva kysymys oli luottamusraja: mitä tabletti saa olla. ”Epäluotettava ehdottaja” kuulostaa vähättelyltä, mutta on oikeasti vapauttava rooli — sen ansiosta samaa rajapintaa voi jatkossa tarjota mille tahansa ehdottajalle ilman uusia laiteoikeuksia.

    Asiakkaan kelloon ei luoteta koskaan. Selain saa pyytää kestoa, mutta expiry lasketaan aina palvelimella. Tämä yksi sääntö poistaa kokonaisen luokan vikoja: väärän kellonajan, aikavyöhykevirheen, kesäajan siirtymän ja tahallisen manipuloinnin.

    Retained intent ja palvelimen expiry yhdessä antavat restartin kestävän mutta zombittoman käsiohjauksen. Aktiivinen käsiohjaus selviää Node-REDin restartista, mutta vanhentunut ei herää henkiin. Molemmat puolet ovat yhtä tärkeitä — tämän sarjan lukijat muistavat, mitä retained-viestien zombit ovat ennen saaneet aikaan.

    Kaksi rinnakkaista käsiohjausjärjestelmää on pahempi kuin ei yhtään. Uusi kerros ajettiin ensin shadow’na, sitten liveen — ja legacy-polku poistettiin heti perään, kokonaan.

    Kahdentumat selitetään, ei vaimenneta. Yleinen debounce olisi peittänyt molemmat kaksoisajot muttei selittänyt kumpaakaan. Nyt tiedetään, että syitä oli kaksi ja ne olivat eri mekanismeja — ja kumpikin on korjattu juurisyystään.

    Komento ei ole toteuma. Käyttövesiboostin onnistuminen todetaan mittauksista, ei lähetetystä komennosta. Sama periaate, joka on kulkenut tämän sarjan läpi releistä lähtien, päti myös käyttöliittymään: paneeli näyttää vahvistettua tilaa, ei toiveita.

    Mitä vielä puuttuu

    Rehellisyyden nimissä lista on edelleen pitkä. Sisälämpötilatavoitteen fyysinen live-aktivointi odottaa lämmityskautta. Home/Away paluuaikoineen on validatorissa valmisteltu mutta toteuttamatta — eikä sitä kannata tehdä ennen kuin on määritelty tarkasti, mitä Away oikeasti muuttaa, sillä pelkkä boolean ei riitä, kun vaikutukset ulottuvat autoon, käyttöveteen ja lämmitykseen. charge_by tarvitsee kunnollisen regressio- ja soak-testisarjan: puuttuva tai stale SOC, auto ei kotona, deadline lähellä, huomisen hinnat puuttuvat, replan kesken ajon, kellonsiirtoyö, restartit kesken suunnitelman. Request_id-pohjainen kuittausketju (received → validated → active → applied → blocked → completed) viimeistelisi käyttöliittymän kertomaan täsmälleen, missä vaiheessa pyyntö kulloinkin menee. Direct WebSocket -polun lopullinen rooli — heartbeat, push-status vai poisto hyökkäyspinnan pienentämiseksi — on myös päättämättä, samoin käsiohjaustapahtumien kytkentä tulevaan incident- ja notifier-kerrokseen. Ja jos Node-REDin portti 1880 joskus avataan luotetun verkon ulkopuolelle, HTTP-rajapinta tarvitsee oman suojauskerroksensa: reverse proxyn, HTTPS:n, autentikoinnin ja rate limitin. Nykyisellään se pysyy sisäverkossa.

    Mutta nykytila kelpaa: sähköauton ja käyttöveden käsiohjaus sekä sisälämpötilatavoitteen asetus toimivat päästä päähän, päätös syntyy noin kolmessasadassa millisekunnissa — ja tabletilla ei edelleenkään ole oikeuksia yhteenkään releeseen.


    Ja juuri tähän pyyntöarkkitehtuuriin nojaa se, mikä tästä seuraavaksi kasvoi. Tekninen paneeli toimi, mutta se oli yhä insinöörin näkymä: teknisiä tiloja, syykoodeja ja rele-termejä ruudulla, joka roikkuu keittiön seinällä koko perheen käytettävänä. Seuraava askel oli erottaa esityskerros ohjauskerroksesta kokonaan — antaa samalle turvalliselle pyyntörajapinnalle käyttöliittymä, joka kääntää järjestelmän tilan ihmisen kielelle sen sijaan, että näyttäisi sen sellaisenaan. Siitä, ja siitä miksi teknisesti toimiva paneeli ei vielä ollut hyvä käyttöliittymä, kertoo seuraava osa.

    Piditkö artikkelista?

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

    Seuraa blogia Blogit.fi:ssä
  • Case: oma talo — Osa 18

    Kaksi minuuttia päällä, kolme pois: kun oma turvamekanismi sahasi EV-relettä

    Edellisessä osassa kirjoitin, että sähköauton latauspolku kulkee final gate -arkkitehtuuria ”jo tuotannossa, stale-suojineen ja rollback-mekanismeineen”. Se ei pitänyt paikkaansa. Kirjoitushetkellä uskoin niin — ja juuri se tekee tästä osasta kertomisen arvoisen. Tämä on tarina siitä, miten yhden illan releensahaus paljasti kolme päällekkäistä vikaa, joista yksikään ei ollut se, jota ensin epäiltiin. Ja siitä, miten edellisen vian korjaus oli luonut seuraavan.

    Ilta 12.7.: kellontarkka sahaus

    Sunnuntai-iltana kymmenen jälkeen huomasin Home Assistantin lokista, että sähköauton latausrele oli alkanut käyttäytyä oudosti. Ei ylikuormaa, ei saunaa, ei mitään ilmeistä syytä — sauna, EV ja käyttövesi olivat olleet yhtä aikaa päällä pari tuntia aiemmin, ja lataus oli päättynyt silloin täsmälleen niin kuin pitää. Nyt rele sahasi:

    21.32.16  ON      (OFF jatkunut klo 19.19 alkaen)
    21.33.16  OFF     ON-jakso 60 s
    21.36.17  ON      OFF 181 s
    21.38.16  OFF     ON 119 s
    21.41.16  ON      OFF 180 s
    21.43.16  OFF     ON 120 s
    21.46.16  ON      OFF 180 s
    21.48.16  OFF     ON 120 s
    ...

    Ensimmäisen jakson jälkeen rytmi on lähes sekunnilleen sama: 120 sekuntia päällä, 180 sekuntia pois, kokonaisjakso tasan 300 sekuntia. Ja jokainen tapahtuma osuu sekuntiruudukkoon :16 — samaan kohtaan minuuttia, jossa Node-REDin päätössykli pyörähtää.

    Tällainen kellontarkkuus sulkee heti pois satunnaiset selitykset: Wi-Fi-katkot, releviat ja kuormanvaihtelut eivät toimi metronomin tahtiin. Jokin ohjelmallinen logiikka teki tätä tarkoituksella — se vain ei ollut se logiikka, jonka piti.

    Silmukalla ei myöskään ollut mitään poistumisehtoa. Se olisi jatkanut releen ja auton latauselektroniikan kulutusta niin kauan kuin käynnistysehto pysyi voimassa.

    KUVA 1: Shelly-releen tilahistoria — sahauskuvio klo 21.32 alkaen

    Aamu: todisteet talteen ennen kuin mihinkään kosketaan

    Ensimmäinen toimenpide seuraavana aamuna ei ollut korjaus vaan jäädytys. Ennen yhtäkään restarttia kaikki haihtuva aineisto kerättiin talteen incident-hakemistoon: kolmen kontin (Home Assistant, Node-RED, Mosquitto) lokit tapahtumaikkunasta, retained MQTT -tilannekuva ja Home Assistantin tietokannan tilamuutokset sekunnilleen. Docker-lokit ja retained-viestit eivät odota — restartti olisi tuhonnut ison osan todisteista.

    Tämä kuulostaa byrokratialta, mutta osoittautui ratkaisevaksi. Koko juurisyy löytyi lopulta aineistosta, joka olisi kadonnut ensimmäisessä ”kokeillaan restarttia” -refleksissä.

    Kolme hypoteesia — kaksi väärää

    Vianetsintä eteni tutulla kahden tekoälyn työtavalla: toinen ehdottaa, toinen kyseenalaistaa, ja evidenssi ratkaisee. Tällä kertaa molemmat ehtivät olla väärässä ennen kuin data voitti.

    Ensimmäinen hypoteesi luki rytmiä suoraan: 120 sekunnin ON-jakso näyttää vahvistusikkunalta ja 180 sekunnin tauko cooldownilta — järjestelmä yrittää käynnistää latausta, ei havaitse sitä ja yrittää uudelleen. Looginen tarina, mutta koodista ei löytynyt yhtään 120 tai 180 sekunnin ajastinta. EV-ohjauksen viiveparametrit ovat 5, 15 ja 30 minuuttia.

    Toinen hypoteesi tarttui siihen, että lease-arkkitehtuurin TTL on täsmälleen 120 sekuntia — sama kuin ON-jakson pituus. Tuskin sattumaa? Sattumaa oli. Päätöslokin syykentät osoittivat, että lease vain peilasi ylävirran päätöstä, joka oli jo ehtinyt vaihtua.

    Kolmas jälki löytyi Node-REDin päätöslokista, minuutti minuutilta:

    21:36  EV: true   | Alle 1 snt: lataus aina paalla
    21:37  EV: true   | Alle 1 snt: lataus aina paalla
    21:38  EV: false  | Load admission: EV start denied,
                        profile=nissan_leaf_1p_16a_l1,
                        L1 predicted 32.7A > 29.5A

    Ilta oli poikkeuksellisen halpa, joten lataushalu oli jatkuvasti päällä. Mutta joka viides minuutti käynnistysluvan tarkastaja — load admission, joka rakennettiin osana käsikäyttöpakettia estämään ylikuormakäynnistykset — kielsi latauksen, jonka se itse oli juuri sallinut.

    Ja tässä on savuava ase: 32,7 A miinus Nissan Leafin 16 A latausprofiili on 16,7 A. Eli mitattu L1-vaihevirta sisälsi jo käynnissä olevan latauksen — ja admission lisäsi saman kuorman laskelmaansa toiseen kertaan. Järjestelmä kielsi latauksen käynnistämisen, koska lataus oli käynnissä.

    Miksi admission luuli relettä sammuneeksi

    Kaksoislaskenta edellyttää, että admission luulee releen olevan pois päältä. Miksi se luuli niin, vaikka Home Assistant näki releen päällä?

    Node-RED ei lue Shellyn reletilaa suoraan. Se saa tiedon MQTT-telemetriaviestistä, jonka Home Assistant julkaisee — ja tuon automaation triggereinä olivat vain auton sensorimuutokset ja viiden minuutin aikataulu. Releen tilamuutos ei ollut triggerinä lainkaan. Readback saattoi siis olla viisi minuuttia vanha.

    Ja nyt tulee yksityiskohta, joka teki silmukasta itseään ylläpitävän: ON-jaksot osuivat minuuteille 36–38, 41–43, 46–48 ja niin edelleen. Viiden minuutin telemetriasnapshotit taas laukesivat minuuteilla 35, 40, 45. Rele ei ollut kertaakaan päällä sillä hetkellä, kun tilannekuva otettiin. Node-REDin käsitys ”rele on pois” pysyi teknisesti oikeana joka mittaushetkellä — ja käytännössä väärässä koko tapahtuman ajan. Vaihelukko, jota ei olisi voinut suunnitella ilkeämmäksi.

    Havaintokerros huusi tätä koko ajan. Osassa 15 rakennettu päätös vastaan toteuma -valvonta kirjasi jokaisen ON-jakson aikana varoituksen:

    [warn] F17 poikkeama: ev=decided_no_relay

    ”Päätin releen päälle, mutta relettä ei näy.” F17 ei ohjaa mitään — se vain havainnoi — ja juuri siksi se jäi lokiin todisteeksi, joka osoitti suoraan readback-polkuun.

    Miksi suojat eivät suojanneet

    Järjestelmässä on 30 minuutin minimikäyntiaika ja 15 minuutin sammutusviive nimenomaan sahauksen estämiseksi. Miksi ne eivät toimineet?

    Koska osan 15 opetuksen mukaisesti ajastimet sidottiin mitattuun toteumaan, ei komentoon. Periaate on oikea — mutta kun mittaus ei koskaan rekisteröinyt releen käynnistymistä, ajastimet eivät koskaan lähteneet käyntiin. Yksi väärä datalähde kytki koko suojakerroksen pois päältä.

    Koodista löytyi vielä tuttu vikaluokka pahentamassa tilannetta:

    javascript

    const relayActual = payload.shelly_relay_on || false;

    Jos kenttä puuttuu viestistä, || muuttaa sen hiljaa muotoon ”rele on fyysisesti pois”. Sama falsy-arvojen sudenkuoppa, joka siivottiin ??-operaattorilla muualta koodista jo aiemmin — yksi esiintymä oli jäänyt, ja tietenkin juuri turvalogiikan tulopuolelle. Puuttuva tieto ei ole sama asia kuin kielteinen tieto.

    Hiljainen ohitus #4: ohjauspolku oli palautunut legacyyn

    Kesken juurisyyanalyysin paljastui vielä neljäs asia, joka jatkaa suoraan edellisen osan hiljaisten ohitusten sarjaa: sähköauton control path -valitsin oli tilassa legacy. Final gate -polku, jonka piti olla tuotannossa, oli hiljaa palautunut vanhaan suoraan ohjaukseen — todennäköisesti Home Assistantin restartin yhteydessä, koska valitsimen tilaa ei oltu tehty restartin kestäväksi.

    Tämä ei aiheuttanut sahausta. Final gate peilasi legacy-päätöstä uskollisesti, ja sahaus olisi tapahtunut kummassakin polussa, koska vika oli päätöksenteossa eikä toimeenpanossa. Mutta se tarkoitti, että edellisen osan väite EV:n final gate -tuotantokäytöstä oli kirjoitushetkellä jo vanhentunut — eikä mikään hälyttänyt siitä. Raportointi näytti oikealta, todellisuus oli toinen.

    Korjausketju: viisi committia, viisi periaatetta

    Korjaus tehtiin omassa haarassaan viitenä erillisenä committina, jokainen todennettuna ennen seuraavaa:

    6bc6cb5  EV-telemetria julkaistaan releen tilamuutoksesta
    308427b  Readback tri-stateksi + tuoreusaikaleima
    c7f7e33  Admission vain komentoreunalla + deny-cooldown
    5bccfe8  Control path säilyy restartien yli
    aa2022a  HA–MQTT–Node-RED control path -synkronointi

    Jokainen commit vastaa yhtä periaatetta:

    Tapahtumat, ei aikataulut. Releen tilamuutos lisättiin telemetria-automaation triggeriksi. Readback päivittyy nyt sekunneissa, ei pahimmillaan viidessä minuutissa.

    Tuntematon ei ole epätosi. Readback on nyt kolmitilainen (true / false / unknown) ja sillä on tuoreusaikaleima. Kenttä luetaan vain jos se todella on viestissä — ||-oletusarvon sijaan. Vanhentunut tieto ei enää esiinny varmana tietona.

    Käynnistyslupa tarkistetaan käynnistettäessä. Admission ajetaan vain silloin, kun julkaistu komento on oikeasti vaihtumassa pois-tilasta päälle — ei joka päätössyklillä käynnissä olevaa latausta vastaan. Ja reuna johdetaan omasta komennosta, ei mitatusta releestä: juuri se mittaus tässä petti, eikä korjaus saa nojata korjattavaan. Kielteisen päätöksen jälkeen uutta yritystä hillitsee cooldown, joten epäonnistunut käynnistys ei muutu tykitykseksi.

    Konfiguraatio ei saa haihtua. Control path -valitsin palautuu restartissa oikeaan tilaansa, ja HA:n, MQTT:n ja Node-REDin käsitys polusta pidetään synkronissa retained-viestillä — samalla peiliperiaatteella kuin lämpöpumpun puolella edellisessä osassa.

    Lopuksi koko ketju todennettiin Home Assistantin restartin yli: polku pysyi final gate -tilassa, rele pysyi hallinnassa, eikä seurannassa näkynyt yhtään F17-poikkeamaa tai admission-kieltoa. Vasta tämän jälkeen haara yhdistettiin.

    Chatter-vahti: sahaus itsessään pitää havaita

    Yksi asia jäi korjausketjun jälkeen vaivaamaan enemmän kuin muut. Tämä sahaus kulutti releen koskettimia ja auton latauselektroniikkaa yli puolen tunnin ajan ilman että yksikään hälytys laukesi. Watchdog-kerros tunnistaa hiljaiset viat kuten vanhentuneen varmuuskopion, mutta releen tiheä tilanvaihtelu oli vikaluokka, jota mikään ei vielä valvonut. Juurisyy oli poissa, mutta jos jokin tuleva vika synnyttäisi vastaavan oskillaation, se saisi taas jyllätä huomaamatta.

    Siksi korjausten perään rakennettiin oma chatter-vahti: se laskee releen tilanvaihdot liukuvassa ikkunassa ja nostaa varoituksen, jos vaihtoja kertyy enemmän kuin normaali ohjaus koskaan tuottaisi. Kynnys asetettiin tämän incidentin todellista rytmiä vasten — 300 sekunnin jakso tuottaa kaksi vaihtoa, terve ohjaus selvästi vähemmän — jättäen silti marginaalin normaaleille käynnistys- ja sammutussekvensseille. Vahti ei estä sahausta, se tekee siitä äänekkään: sama filosofia kuin F17:llä, mutta nyt tähdättynä juuri siihen vikaluokkaan, joka tällä kertaa pääsi yllättämään.

    Samalla toteutettiin toinen avoimeksi jäänyt vahti: control path -ristiriitavahti. Jos odotettu ja raportoitu ohjauspolku erkanevat — kuten hiljaisessa ohituksessa #4, jossa polku oli livahtanut legacyyn kenenkään huomaamatta — siitä kuuluu nyt ääni. Se on halvin mahdollinen vakuutus juuri sitä vastaan, että dokumentaatio ja todellisuus erkanevat hiljaa.

    Täysi lataussessio: korjausten lopullinen hyväksyntä

    Viimeinen avoin asia oli suoraviivaisin: kaikki edellä oli todennettu injektoiduilla leaseilla, restart-testeillä ja lokianalyysillä, mutta oikea, alusta loppuun ajettu lataussessio korjatun arkkitehtuurin läpi oli vielä ajamatta. Se ajettiin seuraavalla halvalla yöllä — auto kaapelissa, charge_now päällä, lataus 100 prosenttiin asti.

    Sessio meni läpi juuri niin kuin piti. Rele kävi päälle kertaalleen käynnistyksessä, readback rekisteröi sen sekunneissa, minimikäyntiaika ja sammutusviive lähtivät nyt oikeasti käyntiin toteuman perusteella, eikä admission enää kieltänyt käynnissä olevaa latausta itseään vastaan. F17-poikkeamia ei tullut yhtään, chatter-vahti pysyi hiljaa, control path pysyi final_gatessa koko session ajan. Lataus eteni tasaisesti täyteen ja päättyi hallitusti, kun auton oma latauselektroniikka ilmoitti valmistumisesta — ei releen sahaukseen vaan siihen, että työ oli tehty.

    Tässä yhteydessä kovennettiin vielä releen ja todellisen lataustehon yhteispeliä. Aiemmin järjestelmä nojasi vahvasti reletilaan päätellessään, latautuuko auto oikeasti. Nyt rinnalle otettiin Shellyn tehomittaus: rele päällä ilman tehoa pitkään on eri tila kuin rele päällä ja teho virtaa, ja näiden erottaminen sulki pois kokonaisen luokan väärintulkintoja — esimerkiksi sen, luuleeko järjestelmä latauksen käynnistyneen pelkän relekomennon perusteella.

    Charge_by: auto täyteen määräaikaan mennessä

    Onnistunut täysi sessio avasi oven sille, mikä oli ollut tiekartalla pisimpään: määräaikaan sidottu lataus. charge_now ja charge_block ovat yksinkertaisia — lataa nyt, älä lataa — mutta todellinen tarve on useimmiten ”auto riittävän täyteen aamuksi, mahdollisimman halvalla”. Se on charge_by.

    Perusidea on, että käyttäjä antaa tavoite-SOC:n ja määräajan, ja järjestelmä rakentaa lähtöajan, nykyisen varaustason ja huomisen varttihintojen perusteella suunnitelman, joka lataa vain halvimmilla varteilla mutta ehtii silti tavoitteeseen deadlineen mennessä. Suunnitelma lasketaan uudelleen sitä mukaa kun hintadata täydentyy ja varaustaso muuttuu, eikä se sido latausta kiinteään kellonaikaan vaan hintaan.

    charge_by kytkettiin tuotantoon samalla varovaisuudella kuin final gate aikanaan: ensin suunnitelma laskettiin shadow-tilassa ja sitä verrattiin siihen, mitä yksinkertainen halpuuskynnys olisi tehnyt, ja vasta kun suunnitelma näytti tekevän järkeviä valintoja oikealla hintadatalla, se kytkettiin ohjaamaan latausta. Sen jälkeen se ajettiin läpi oikeissa yön yli -latauksissa: 80 prosentin tavoite aamukuudeksi meni läpi ilman käsin puuttumista, ja 100 prosentin tavoite paljasti oman erikoistapauksensa — auton oma latauselektroniikka hidastaa loppua kohden niin, että viimeiset prosentit kestävät suhteettoman kauan, mikä piti ottaa suunnitelmassa huomioon ettei deadline lipsu. Nämä ovat oma tarinansa, ja niistä kirjoitan tarkemmin myöhemmin omassa osassaan — tässä riittää todeta, että korjattu readback-polku oli edellytys koko charge_by:lle: määräaikaan luottava suunnitelma on täsmälleen niin luotettava kuin sen käsitys siitä, latautuuko auto juuri nyt.

    Opit

    Turvamekanismissa voi olla vika. Load admission rakennettiin estämään ylikuormakäynnistykset — ja se toimi täsmälleen suunnitellusti, väärän lähtötiedon varassa. Ylikuormatestin räpsyminen synnytti admissionin; admission synnytti seuraavan räpsymisen. Jokainen uusi suojakerros on myös uusi vikaantumiskerros, ja se pitää suunnitella samalla vakavuudella.

    Vanhentunut tieto + joka syklillä toistuva portti = oskillaatio. Teollisuusautomaatiossa ilmiö tunnetaan nimellä hunting: säätöpiiri, jonka takaisinkytkentä laahaa, alkaa heilua omaan tahtiinsa. Sama pätee kotiautomaatioon, oli alustana sitten Home Assistant, Node-RED tai mikä tahansa releohjattu latauslogiikka. Jos päätös tehdään uudelleen joka syklillä ja sen lähtötieto päivittyy hitaammin kuin päätöksen seuraus, oskillaatio ei ole riski vaan ajan kysymys.

    Mittausten aikarakenne on osa järjestelmää. Viiden minuutin telemetria ja viiden minuutin sahausjakso lukittuivat vaiheeseen niin, ettei readback nähnyt relettä päällä kertaakaan. Näytteenottotaajuus ei ollut vain ”vähän hidas” — se oli täsmälleen väärä.

    Havaintokerros maksoi itsensä takaisin taas. F17 ja final gaten shadow-loki eivät estäneet vikaa, mutta ilman niitä juurisyy olisi jäänyt arvailuksi. Päätöshistoria syykenttineen on halvin vakuutus, jonka tähän järjestelmään on rakennettu — ja saman logiikan mukaisesti sahaus ja polkuristiriita saivat perään omat vahtinsa, jotta seuraava vastaava vika ei enää pääse jylläämään hiljaa.

    Ja se tunnustus: dokumentaatio vanhenee hiljaa siinä missä konfiguraatiokin. Edellisen osan väite EV-polusta oli totta kirjoitettaessa vain paperilla. Nyt se on totta myös restartin jälkeen — ja siitä pitää huolen kone, ei muistinvarainen olettamus. Sama korjattu polku kantoi lopulta ensimmäisen täyden latauksen, charge_by:n ja tehonseurannan kovennuksen; se, mikä alkoi kellontarkasta sahauksesta, päättyi latausketjuun johon voi luottaa määräaikaa myöten.


    Seuraavassa osassa EnergyHub saa kasvot: seinään kiinnitetty tabletti, josta voi käynnistää sähköauton latauksen, estää sen tai tilata käyttövesiboostin. Sen tärkein ominaisuus on, ettei se saa ohjata suoraan yhtään laitetta — jokainen napinpainallus on pelkkä pyyntö, joka kulkee samojen turvakerrosten läpi kuin automaation omat päätökset. Ja se ensimmäinen oikea DHW-boost koko uuden arkkitehtuurin läpi toteutuu sekin — ei tosin niin, ettei kukaan koske mihinkään, vaan päinvastoin: joku painaa nappia.

    Piditkö artikkelista?

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

    Seuraa blogia Blogit.fi:ssä
  • Case: oma talo — Osa 17

    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ä
  • Case: oma talo — Osa 16

    Kun kukaan ei katso: vahtikoira hiljaisille vioille

    Tämä on kuudestoista osa sarjasta, joka dokumentoi EnergyHub-kotiautomaatiojärjestelmän rakentamisen vanhaan rintamamiestaloon. Edellinen osa kertoi systemaattisesta vikojen läpikäynnistä ennen käyttöönottoa. Se päättyi lupaukseen kertoa seuraavaksi operoinnista — siitä, miten energianhallintajärjestelmää pidetään pystyssä silloin, kun kukaan ei aktiivisesti katso.

    Tämä osa on siitä. Se alkaa viasta, jota en huomannut kahteen viikkoon — ja päättyy samaan vikaan, mutta toisella lopputuloksella.

    Varmuuskopio, joka oli hiljaa rikki

    Järjestelmästä otetaan automaattinen varmuuskopio joka yö kello kolme. Arkisto pakataan, tarkistussumma lasketaan, tiedosto ladataan pilveen. Olin rakentanut tämän varmuuskopioinnin kuukausia sitten ja lakannut ajattelemasta sitä — juuri niin kuin varmuuskopion kuuluukin toimia.

    Sitten eräänä päivänä satuin katsomaan varmuuskopiohakemistoa. Tuorein arkisto oli kahden viikon takaa.

    Skripti oli ajautunut joka yö. Ajastin oli tehnyt työnsä. Mutta itse varmuuskopiointi epäonnistui hiljaa: eräs komento vaati oikeuksia, joita ajastettu ajo ei voinut antaa, ja koska skriptistä puuttui rivi joka olisi pysäyttänyt sen ensimmäiseen virheeseen, se jatkoi loppuun asti ja raportoi onnistuneensa. Loki näytti siistiltä. Paluukoodi oli nolla — merkki onnistumisesta. Vain tiedostojen puuttuminen kertoi, ettei mitään ollut oikeasti tallennettu kahteen viikkoon.

    Tämä on täsmälleen sama vikaluokka josta edellinen osa kertoi: hiljainen epäonnistuminen. Mikään ei kaadu, mikään ei hälytä, ja virhe elää niin kauan kuin kukaan ei satu katsomaan oikeaa paikkaa oikeaan aikaan. Erona vain se, että nyt vika ei ollut ohjauslogiikassa vaan ylläpidossa — ja se osui juuri siihen mekanismiin, jonka koko tarkoitus on suojata katastrofilta.

    Korjasin varmuuskopiointiskriptin. Mutta oikea opetus ei ollut korjaus. Se oli kysymys, jonka vika nosti esiin: mitä muuta on hiljaa rikki, mistä en tiedä?

    Se mitä ei mittaa, ei ole olemassa

    Kotiautomaatiojärjestelmä koostuu joukosta osasia, jotka kaikki voivat pettää itsenäisesti. Home Assistant integroi laitteet. Node-RED tekee optimointipäätökset. MQTT-viestiväylä välittää tiedot. InfluxDB tallentaa historian, Grafana piirtää sen. Kaikki pyörii pienessä palvelimessa nurkassa, eikä kukaan katso sitä päivittäin — se on koko pointti. Automaation kuuluu olla näkymätön.

    Mutta näkymättömyydellä on hinta. Jos järjestelmä ei kerro itsestään mitään, ainoa tapa tietää että se voi hyvin on tarkistaa se käsin. Ja käsin tarkistaminen ei skaalaudu — sitä ei yksinkertaisesti tule tehtyä, ennen kuin jokin menee niin pahasti pieleen että se huomataan muuta kautta. Kylmä suihku, pimeä huone, kahden viikon aukko varmuuskopioissa.

    Ratkaisu ei ole katsoa useammin. Ratkaisu on rakentaa monitorointi, joka on osa järjestelmää itseään: järjestelmä joka kertoo omasta voinnistaan — ja erillinen valvoja, joka huomaa jos se lakkaa kertomasta.

    Sydämensyke

    Ensimmäinen kerros on sydämensyke. Node-RED — järjestelmän aivot, joka tekee energianhallinnan optimointipäätökset — julkaisee säännöllisin väliajoin viestin, joka kertoo yhden asian: minä olen elossa.

    Mutta pelkkä ”olen elossa” ei riitä, ja tämä on tärkeä oivallus. Prosessi voi olla pystyssä ja silti kyvytön tekemään työnsä, jos sen syötteet ovat pysähtyneet. Jos sähkön hintatieto on tunteja vanhaa tai lämpöpumpun mittaukset ovat lakanneet päivittymästä, järjestelmä on teknisesti hengissä mutta käytännössä sokea. Se tekisi päätöksiä vanhentuneen tiedon varassa — mikä on usein vaarallisempaa kuin päätösten tekemättä jättäminen.

    Siksi sydämensyke kertoo prosessin hengissäolon lisäksi sen, kuinka tuoretta jokainen keskeinen syöte on: milloin sähkömittarilta tuli viimeksi lukema, milloin sähkön pörssihinnat päivittyivät, milloin lämpöpumppu ja aurinkopaneelit raportoivat, ja milloin optimointilogiikka viimeksi ajoi läpi. Syke ei ole ”olen elossa” vaan ”olen elossa ja näen”. Se julkaistaan viestiväylälle muutaman sekunnin välein, ja se on perusta kaikelle mitä seuraa.

    Vahtikoira järjestelmän ulkopuolella

    Toinen kerros on valvoja — vahtikoira, joka on tarkoituksella järjestelmän ulkopuolella.

    Tämä sijoittelu on olennainen, ja se on monitoroinnin tärkein yksittäinen suunnitteluperiaate. Sydämensyke tulee Node-REDistä, mutta juuri Node-REDin kaatuminen on yksi tärkeimmistä asioista jotka pitää havaita. Jos valvoja eläisi saman prosessin sisällä jota se valvoo, se kaatuisi yhdessä sen kanssa eikä kukaan saisi tietää. Siksi vahtikoira ajaa palvelimella itsenäisenä käyttöjärjestelmän ajastamana prosessina, Node-REDin, Home Assistantin ja koko muun konttipohjaisen järjestelmän ulkopuolella. Se herää minuutin välein, tarkistaa terveyden ja kirjaa havaintonsa.

    Mitä se tarkistaa? Ovatko kaikki palvelut pystyssä. Onko sydämensyke tuore vai onko se vaiennut. Ovatko syötteet ajan tasalla. Milloin viimeisin varmuuskopio syntyi — se sama tarkistus, jonka puuttuminen aloitti tämän koko tarinan. Ja paljonko levytilaa on jäljellä, koska täyttyvä levy on hidas, hiljainen tapa kaataa koko kotiautomaatiojärjestelmä.

    Tärkein sääntö: vahtikoira ei pure — vielä

    Vahtikoiran voisi kuvitella toimivan: se havaitsee ongelman ja korjaa sen — käynnistää kaatuneen palvelun uudelleen, palauttaa laitteet turvatilaan. Se on lopullinen tavoite. Mutta se ei ole se, mistä aloitin, ja syy on periaatteellinen.

    Valvoja, joka tekee automaattisia korjauksia, on itsessään vaarallinen, jos sen käsitys ”ongelmasta” on väärä. Väärin viritetty vahtikoira, joka käynnistää palveluita uudelleen turhaan tai palauttaa laitteita turvatilaan silloin kun kaikki on kunnossa, aiheuttaa enemmän häiriötä kuin se estää. Ja ennen kuin sille antaa vallan toimia, pitää tietää tarkalleen miltä normaali näyttää — muuten se ei osaa erottaa normaalia poikkeavasta.

    Siksi ensimmäinen versio on tarkoituksella hampaaton. Se katsoo, se kirjaa, se raportoi — mutta se ei kajoa mihinkään. Sen ainoa tehtävä on kerätä tietoa siitä, miten järjestelmä käyttäytyy tavallisesti: kuinka tuoreita syötteet ovat normaalisti, kuinka paljon ne vaihtelevat, mikä on tavallista ja mikä poikkeavaa.

    Ja tämä maltti osoittautui heti hyödylliseksi. Kun annoin vahtikoiran kerätä dataa vuorokauden yli ja katsoin sitten mitä se oli nähnyt, syötteiden tuoreudesta paljastui selkeä kuvio: normaalitilassa mittaustiedot olivat lähes aina alle minuutin vanhoja, ja jakauman yläraja asettui johdonmukaisesti noin 59 sekuntiin — ei koskaan sen yli. Jos olisin arvannut varoitusrajan etukäteen, olisin todennäköisesti asettanut sen 60 sekuntiin, mikä on juuri normaalin ylärajalla — ja se olisi tuottanut turhia hälytyksiä lähes joka päivä. Datan perusteella nostin varoitusrajan 90 sekuntiin ja varsinaisen vikarajan 180 sekuntiin, kolmeen mittaussykliin. Nyt raja laukeaa vasta aidosta poikkeamasta, ei normaalin vaihtelun huipusta. Tätä ei olisi voinut päätellä pöydän takana — se piti mitata.

    Tämä on sama periaate jolla koko järjestelmä otettiin käyttöön: havainnoi ensin, toimi vasta sitten. Vahtikoira aloittaa katsojana, ei tekijänä.

    Mitä testi paljasti

    Ennen kuin luotin vahtikoiraan, testasin sen: pysäytin Node-REDin tahallani ja katsoin, mitä tapahtuu.

    Vahtikoira huomasi katkon välittömästi ja merkitsi tilan vialliseksi. Ja mikä tärkeintä — se ei tehnyt mitään muuta. Node-RED pysyi alhaalla, koska tässä vaiheessa vahtikoiralla ei ole valtaa käynnistää sitä. Se on juuri se käyttäytyminen jonka halusin varmistaa: valvoja havaitsee, mutta ei toimi. Käynnistin Node-REDin itse takaisin, ja sydämensyke palasi.

    Tämä testi oli tärkeä paitsi toimivuuden varmistamiseksi, myös siksi että se pakotti minut näkemään vian sellaisena kuin vahtikoira sen näkee. Kun katko oli päällä, palvelu näkyi alhaalla, sydämensykkeen ”viimeksi nähty” -aika alkoi kasvaa, ja optimointilogiikan viimeisin ajo jäi taakse. Kaikki kolme osaa kertoivat saman tarinan eri kulmasta — ja juuri se moniulotteisuus on syy, miksi rakensin sykkeen kertomaan muutakin kuin pelkän hengissäolon. Yksi signaali voi valehdella; kolme rinnakkaista signaalia piirtää totuuden.

    Kun yksi mittari nikottelee

    Yksi hienovarainen suunnittelukysymys ansaitsee oman huomionsa, koska se on juuri sellainen kohta jossa naiivi toteutus pettää.

    Kun vahtikoira valvoo useaa syötettä — sähkömittari, hinnat, lämpöpumppu, aurinkopaneelit — herää kysymys: milloin se hälyttää? Ilmeinen vastaus olisi ”kun ne kaikki ovat vanhentuneet”. Mutta se olisi vaarallinen virhe. Jos vaatisin että kaikki syötteet vanhenevat yhtä aikaa ennen hälytystä, niin tilanne jossa vain yksi integraatio kuolee — vaikkapa lämpöpumpun tiedonsiirto katkeaa mutta kaikki muu jatkaa normaalisti — jäisi täysin huomaamatta. Ja juuri se on todennäköisin vikatapaus: yksittäinen yhteys irtoaa, ei koko väylä kerralla.

    Ratkaisu on kaksitasoinen. Jos yksittäinen syöte vanhenee, se on varoituksen arvoinen — jokin integraatio on pulassa, ja vahtikoira kertoo tarkalleen mikä. Jos kaikki syötteet vanhenevat yhtä aikaa, se on vakavampi merkki: koko optimointisykli tai viestiväylä on jumissa. Molemmat havaitaan, mutta eri painoarvolla. Sokea piste, jossa yksittäisen laitteen hiljainen kuolema katoaa muiden terveyden taakse, on suljettu.

    Samaan tapaan yksittäinen hetkellinen piikki ei saa laukaista hälytystä. Jos jokin syöte sattuu olemaan hetken tavallista vanhempi yhden tarkistuksen ajan, se on kohinaa, ei vikaa. Vahtikoira vaatii että poikkeava tila jatkuu useamman peräkkäisen tarkistuksen ajan ennen kuin se nostaa hälytyksen — ja vastaavasti se ei julista tilaa terveeksi heti ensimmäisestä normaalista lukemasta, jottei vilkkuva, epävakaasti nikotteleva vika pääse piiloutumaan hälytyksen ja normaalin väliin sahaamalla. Nämä ovat pieniä yksityiskohtia, mutta juuri ne erottavat monitorointijärjestelmän joka on hyödyllinen sellaisesta joka huutaa sudesta niin usein, ettei sitä enää kuunnella.

    Nyt kaikki jää talteen

    Yksi asia puuttui vielä. Vahtikoira raportoi terveyden minuutin välein, mutta raportti katosi heti — se oli hetken tilannekuva, ei historiaa. Jos halusin tietää miten syötteet käyttäytyivät viime yönä, tietoa ei ollut mistä katsoa.

    Niinpä kytkin sekä sydämensykkeen että vahtikoiran raportit samaan aikasarjatietokantaan, johon järjestelmä muutenkin tallentaa historiansa — InfluxDB:hen, jonka päälle Grafana piirtää kuvaajat. Nyt jokainen syke ja jokainen terveystarkistus jää talteen, ja niistä syntyy oma valvontanäkymänsä: järjestelmän terveyden kojelauta, joka näyttää yhdellä silmäyksellä ovatko palvelut pystyssä, kuinka tuoreita syötteet ovat, milloin viimeisin varmuuskopio otettiin ja paljonko levytilaa on jäljellä.

    KUVA 1: System Health -kojelauta yleisnäkymänä — palvelut, telemetria-ikien aikasarja, varmuuskopion ikä, levytila

    Juuri tätä kertyvää dataa valvojan rajojen asettaminen vaatii — ja kuten edellä kuvattu varoitusrajan tarina osoitti, sitä ei voi arvata etukäteen. Se pitää mitata. Kojelauta teki myös oman hyödyllisen palveluksensa: kun näkee järjestelmän terveyden piirrettynä, huomaa myös monitoroinnin omat aukot. Muutama puute paljastui vasta siinä vaiheessa kun data piirtyi ruudulle — mikä on hyvä muistutus siitä, että näkyvyyden rakentaminen tekee näkyväksi myös sen, mitä ei vielä mitata.

    Ja sitten, ennen kuin ehdin edes julkaista tätä artikkelia, vahtikoira teki ensimmäisen aidon löytönsä.

    Ensimmäinen aito saalis

    Ajattelin, että ensimmäinen todellinen havainto antaisi odottaa itseään viikkoja. Se tuli päivissä — ja tavalla, joka oli melkein liian osuva ollakseen totta.

    Osana tätä samaa työtä tein tietoturvasiivouksen: järjestelmän tietokantaan oli aikanaan luotu yksi ylioikeuksinen pääsytunniste, jota useampi palvelu käytti yhteisesti, ja se korvattiin rajatuilla, palvelukohtaisilla tunnisteilla. Oikea ja tarpeellinen parannus. Mutta sillä oli sivuvaikutus, jota en huomannut: yöllinen varmuuskopiointiskripti haki tietokantaosuuttaan varten juuri sitä vanhaa tunnistetta. Kun tunniste mitätöitiin, skripti alkoi saada joka yö kirjautumisvirheen. Ajastin ajoi, virhe kirjautui lokiin, jota kukaan ei lukenut. Täsmälleen sama hiljainen vikaluokka kuin tämän artikkelin alussa — nyt vain oman parannukseni aiheuttamana.

    Erona edelliseen kertaan: nyt joku katsoi. Huomasin vian, koska kaksi mittaria oli eri mieltä. Vahtikoiran raportoima varmuuskopion ikä kasvoi kohti 56:ta tuntia, kun kojelaudan toinen paneeli näytti tyytyväisenä vihreää kymmentä tuntia. Ristiriita itsessään oli hälytys. Selitys löytyi kaivamalla: järjestelmässä on kaksi erillistä varmuuskopioprosessia — palvelukohtainen ja koko järjestelmän kattava — ja paneeli seurasi sitä joka toimi, vahtikoira sitä joka oli rikki.

    Vahtikoira oli oikeassa, ja syy on periaatteellinen. Se ei kysynyt varmuuskopioprosessilta miten meni, vaan mittasi levyllä olevan varmuuskopiotiedoston todellisen iän. Tämä on sama periaate, joka on kulkenut läpi koko sarjan ohjauspuolella:

    Komento ei ole toteuma. Lokirivi on väite. Tiedosto levyllä on tosiasia. Valvo tosiasioita.

    Vika eli alle kaksi vuorokautta ennen kuin se jäi kiinni. Edellinen vastaava eli kaksi viikkoa — ja senkin löytyminen oli puhdasta sattumaa. Se on koko tämän monitorointikerroksen arvo yhdessä vertailussa.

    KUVA 2:Varmuuskopion ikä kasvoi, vaikka järjestelmä näytti muuten toimivan. Korjauksen jälkeen ikä putosi takaisin lähelle nollaa. Tämä on juuri se hiljainen vikaluokka, jota vahtikoiran pitää havaita.

    Korjaus itsessään tarjosi vielä yhden muistutuksen. Sen aikana yksi apuskripti kaatui virheeseen, jonka syntaksitarkistus oli jo ehtinyt hylätä — mutta ajoin skriptin silti. Tarkistus, joka ei pysäytä, ei ole tarkistus. Sama kuri, joka koskee järjestelmän muutoksia, koskee myös sen korjauksia.

    Kuka valvoo valvojaa?

    Kojelauta paljasti vielä yhden asian, joka ansaitsee rehellisen maininnan: monitoroinnin omat viat.

    Yksi kojelaudan paneeleista — sähkömittauksen valvonnan tila — alkoi vilkkua. Välillä se näytti vihreää, välillä oranssia ”ei viestiä”. Data sen takana sahasi tasaisesti kahden arvon väliä, ilman mitään yhteyttä järjestelmän todelliseen tilaan. Juurisyy ei ollut sähkömittarissa vaan valvontatavassa: valvoja katsoi, sattuuko viesti näkymään juuri sen lyhyessä kuunteluikkunassa — puhdas ajoitusarpajainen, jossa noin joka kolmas tarkistus voitti.

    Monitorointi on itsekin ohjelmisto, ja sillä on omat buginsa. Valvoja, joka välillä valehtelee, on pahempi kuin valvoja jota ei ole — koska siihen joko lakataan luottamasta, tai pahempaa, sen ilmoituksiin turrutaan. Korjaus noudattaa täsmälleen samaa mallia, joka pelasti varmuuskopiovalvonnan: oikea kysymys ei ole ”näinkö viestin juuri nyt” vaan ”kuinka vanha viimeisin luotettava viesti on”. Ja siihen asti kojelaudan puolella sama vaimennus kuin muuallakin: yksittäinen ohi mennyt näyte ei käännä tilaa — vasta jatkuva hiljaisuus.

    Näkyvyyden rakentaminen teki näkyväksi myös valvonnan omat puutteet. Se ei ole häpeä vaan ominaisuus: järjestelmä, joka paljastaa omat vikansa, on tervein mahdollinen.

    Kaksi tekoälyä, kuten ennenkin

    Kuten edellisessä osassa, käytin myös tässä kahden tekoälyavustajan työnjakoa: toinen auttoi toteutuksessa, toinen katselmoi suunnitelmia ja etsi sokeita pisteitä. Useampi tämän osan tärkeimmistä yksityiskohdista — kaksitasoinen hälytyslogiikka yksittäisen ja yhtäaikaisen vanhenemisen välillä, sekä vaatimus ettei vilkkuva tila jää piiloon — nousi nimenomaan katselmoinnista. Lopulliset päätökset ja testit jäivät ihmiselle, mutta yhden hengen projektissa toinen näkökulma on arvokas — varsinkin silloin, kun rakennetaan turvakerroksia, joiden pitää toimia myös silloin kun muu järjestelmä ei toimi.

    Operointi on oma taitonsa

    Tämän sarjan alkuosat kertoivat järjestelmän rakentamisesta: miten laitteet liitetään, miten ohjauslogiikka kirjoitetaan, miten optimointi tehdään. Tämä osa kertoi jostain muusta — järjestelmän pitämisestä pystyssä sen jälkeen kun se on rakennettu. Ne ovat eri taitoja, ja jälkimmäinen jää usein huomiotta, kunnes jokin menee hiljaa rikki.

    Hiljainen vika on operoinnin keskeisin vihollinen, koska se ei ilmoita itsestään. Kylmä suihku, kahden viikon aukko varmuuskopioissa, pysähtynyt integraatio jota kukaan ei huomaa — ne kaikki jakavat saman piirteen: mikään ei kaadu, mikään ei huuda. Ratkaisu ei ole valppaus, koska ihminen ei jaksa katsoa loputtomiin. Ratkaisu on rakenne: järjestelmä joka kertoo omasta voinnistaan, ja valvoja joka huomaa hiljaisuuden. Ja rakenne ehti todistaa arvonsa ennen kuin tämä artikkeli ehti julkaisuun — sen ensimmäinen aito saalis oli hiljaa rikkoutunut varmuuskopiointi, täsmälleen se vika, jota vastaan se rakennettiin.

    Tärkein periaate koko työssä oli silti maltti: valvoja aloittaa katsojana, ei tekijänä, ja saa hampaat vasta kun normaali on mitattu.

    Se seuraava askel — hampaiden antaminen valvojalle — on oma tarinansa. Kun järjestelmä osaa luotettavasti havaita että jokin on vialla, seuraava kysymys on: saako se korjata sen itse? Ja jos saa, miten varmistetaan ettei korjaus itse aiheuta vahinkoa — ettei vahtikoira pure väärää jalkaa? Se on fail-safe-arkkitehtuurin aihe, ja se on tulevan osan tarina.


    Seuraavaksi: Osa 17 — lämpöpumppu siirtyy final gate -polkuun. Kolme tapaa epäonnistua täysin hiljaa, yksi onnenkantamoinen jossa Thermia validoi koko päätösketjun itse kesken testien — ja miksi mikään ei ole valmista ennen kuin se on todennettu restartin yli.

    Piditkö artikkelista?

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

    Seuraa blogia Blogit.fi:ssä
  • Case: oma talo — Osa 15

    Kun käyttöönotto paljasti piiloviat: FMA-kovennus

    Tämä on viidestoista osa sarjasta, joka dokumentoi EnergyHub-kotiautomaatiojärjestelmän rakentamisen vanhaan rintamamiestaloon. Edellinen osa pysähtyi hetkeen ennen live-käyttöönottoa: Home Assistant- ja Node-RED-pohjainen energianhallintajärjestelmä oli pyörinyt shadow-moodissa viikkoja, päätöslogiikka oli käyty läpi, ja vaiheistettu käyttöönottosuunnitelma oli valmis. Lupasin kertoa seuraavaksi, miten vaiheistus eteni käytännössä ja mitä tapahtui ensimmäisten viikkojen aikana.

    Lyhyt vastaus: vaiheistus eteni jokseenkin suunnitellusti. Mutta käyttöönotto paljasti vikoja, joita shadow-moodi ei voinut näyttää — eikä siksi, että ne olisi tehty huolimattomasti, vaan siksi, että ne olivat luonteeltaan sellaisia, että ne aktivoituvat vasta kun järjestelmä ottaa oikean kontrollin oikeista laitteista.

    Tämä osa kertoo niistä vioista ja siitä, miten ne käytiin systemaattisesti läpi. Se on tarina menetelmästä nimeltä FMA — failure mode analysis — ja siitä, miksi se kannatti tehdä juuri käyttöönoton kynnyksellä eikä vasta sitten, kun jokin oli mennyt pahasti pieleen.

    Miksi shadow-moodi ei riitä

    Shadow-moodi on hyvä turvaverkko. Järjestelmä laskee päätökset mutta ei toteuta niitä; jos logiikassa on virhe, se näkyy lokissa eikä laitteen käyttäytymisessä. Viikkojen shadow-ajo antoi luottamuksen siihen, että päätöslogiikan iso kuva on kunnossa.

    Mutta shadow-moodilla on sokea piste. Se ei voi paljastaa vikoja, jotka syntyvät vuorovaikutuksesta oikean laiteohjauksen kanssa. Kun järjestelmä vain laskee eikä ohjaa, se ei koskaan joudu tilanteeseen, jossa lähetetty komento ja toteutunut tila eroavat toisistaan. Se ei joudu palautumaan turvakatkaisusta, koska se ei koskaan laukaise sellaista. Se ei joudu käsittelemään tilannetta, jossa vanha retained-viesti elää MQTT-brokerissa ohjauspäivityksen jälkeen, koska shadow-moodissa kukaan ei reagoi siihen viestiin.

    Toisin sanoen: vaarallisin vikaluokka tällaisessa älykodin ohjausjärjestelmässä on hiljainen vika. Mikään ei kaadu. Lokissa ei näy virhettä. Laite vain käyttäytyy väärin — tai jää käyttäytymättä — ja jos kukaan ei satu katsomaan oikeaa mittaria oikealla hetkellä, virhe voi elää päiviä.

    Juuri tällaisten vikojen löytämiseksi tein FMA:n.

    Mikä FMA on

    FMA — failure mode analysis eli vika- ja vaikutusanalyysi — on menetelmä, joka on tuttu teollisuusautomaatiosta ja turvallisuuskriittisestä suunnittelusta. Idea on yksinkertainen: sen sijaan että odotetaan vikojen ilmenevän itsestään, käydään järjestelmä läpi ja kysytään jokaisesta osasta järjestelmällisesti, miten tämä voi epäonnistua. Ei ”epäonnistuuko”, vaan ”miten”.

    Kävin EnergyHubin läpi tällä kysymyksellä ja tunnistin 17 erillistä vikatilaa — failure modea, joista käytän merkintää F1–F17. Ne priorisoitiin ja toteutettiin seitsemässä erässä. Jokainen korjaus tehtiin omana muutoksenaan, omalla testillään, ja jokainen katselmoitiin erikseen. Käytin tähän kahden tekoälyn työnjakoa: toinen analysoi ja toteutti muutokset, toinen teki arkkitehtuurikatselmuksen ja staattisen tarkistuksen. Tästä työnjaosta lisää myöhemmin.

    Vioista hahmottui kolme toistuvaa perusluokkaa. Ne ovat tämän artikkelin punainen lanka, koska ne paljastavat jotain olennaista siitä, millaisia ovat hajautetun ohjausjärjestelmän tyypilliset heikkoudet.

    Vikaluokka 1: jämähtänyt viesti

    MQTT — viestiväylä, jolla kotiautomaation osat puhuvat keskenään — tukee niin sanottuja retained-viestejä. Kun viesti merkitään retained-tilaan, broker säilyttää sen ja antaa sen jokaiselle uudelle tilaajalle, joka liittyy kanavalle. Tämä on erinomainen ominaisuus tilatiedolle: kun järjestelmä käynnistyy uudelleen, se saa heti tietää viimeisimmän tunnetun moodin, eikä jää sokeaksi siihen asti, kunnes seuraava päivitys sattuu tulemaan.

    Mutta sama ominaisuus on miina. Jos retained-viesti kantaa vanhaa, väärää tilaa, se ei katoa itsestään. Se elää brokerissa, kunnes joku julkaisee tilalle uuden. Ja jos järjestelmän jokin osa lukee tuon vanhan viestin ja toimii sen mukaan, syntyy se mitä kutsun zombieksi: tila, joka on jo nollattu yhdessä paikassa mutta herää henkiin toisesta.

    Tämä luokka tuli näkyviin dramaattisimmin turvakatkaisun yhteydessä, johon palaan tuonnempana. Mutta sen perusmuoto oli yksinkertainen, ja sen korjaus opetti periaatteen, joka toistui läpi koko työn.

    Vikaluokka 2: hiljainen epäonnistuminen

    Tämä on se vaarallisin luokka, jonka jo mainitsin. Hiljainen epäonnistuminen tarkoittaa tilannetta, jossa toiminto ei tee mitä piti, mutta mikään ei ilmoita siitä.

    Otetaan konkreettinen esimerkki. Järjestelmä lämmittää käyttöveden kerran päivässä, ja se pitää kirjaa siitä, onko vesi jo lämmitetty (dhw_heated_today). Tämä lippu nollataan joka yö, jotta seuraavana päivänä vesi taas lämmitetään. Alkuperäisessä toteutuksessa nollaus tapahtui ajastettuna kello 00:05.

    Mutta entä jos Node-RED — ohjelmisto, joka tämän nollauksen tekee — sattuu olemaan käynnistymässä uudelleen juuri kello 00:05? Päivitys, uudelleenkäynnistys, mikä tahansa. Silloin nollaus jää tapahtumatta. Lippu jää eilisen true-arvoon. Ja seuraavana päivänä järjestelmä päättelee, että vesi on jo lämmitetty — eikä lämmitä sitä. Mikään ei kaadu. Lokissa ei lue virhettä. Vain kylmä suihku seuraavana aamuna kertoo, että jokin meni pieleen.

    Korjaus oli muuttaa nollaus ajastetusta päivämääräpohjaiseksi: järjestelmä tarkistaa joka ohjaussyklillä, onko päivä vaihtunut edellisestä nollauksesta, ja nollaa liput silloin. Tämä tarkistus selviää uudelleenkäynnistyksestä, koska se ei riipu yhdestä ainoasta hetkestä.

    Tämän luokan korjauksia oli useita. Sähkön hintaohjauksen tyyppisuojaus: tyhjä hintalista johti aiemmin tilanteeseen, jossa vertailu ”onko nyt halpa tunti” palautti aina epätoden, ja sähköauton lataus jäi tapahtumatta halvalla — hiljaa. Käyttövesiboostin uusintayrityksen esto: jos boost epäonnistui, sen laukaisuehdot jäivät voimaan ja se yritti käynnistyä uudelleen loputtomasti. Lämpöpumpun asetusarvon vahvistus: lämpöpumpulle lähetetty comfort wheel -arvo merkittiin ”lähetetyksi” jo lähetyshetkellä, ei silloin kun lämpöpumppu oikeasti vahvisti ottaneensa sen vastaan — joten jos komento ei mennyt perille, sitä ei lähetetty uudelleen.

    Kaikilla näillä on sama rakenne: toiminto epäonnistuu tavalla, joka ei näy mitenkään, ellei satu katsomaan oikeaa arvoa oikealla hetkellä.

    Vikaluokka 3: kaksi omistajaa

    Kolmas luokka on hienovaraisin ja arkkitehtonisesti opettavaisin. Se syntyy, kun samalla tilatiedolla on kaksi lähdettä.

    EnergyHubissa järjestelmän toimintamoodi — normaali, loma, hätätila — kirjoitettiin alun perin kahdesta paikasta. Oli oma dedikoitu kanava moodille, ja sen lisäksi moodi kulki mukana isommassa ”koontiviestissä”, joka kokosi yhteen järjestelmän eri tilatietoja. Kaksi kirjoittajaa, sama muuttuja.

    Ongelma syntyy ristiriidassa. Jos moodi vaihtui omalla kanavallaan, mutta koontiviesti ehti tulla vielä vanhalla moodilla, lopputulos riippui siitä, kumpi viesti sattui saapumaan viimeisenä. Moodi saattoi flipata takaisin vanhaan arvoon ilman, että kukaan oli sitä pyytänyt. Epädeterministinen tila — eli tila, jota ei voi luotettavasti ennustaa — on ohjausjärjestelmässä myrkkyä.

    Korjaus oli periaatteessa yksinkertainen: yksi tila, yksi omistaja. Moodille määrättiin yksi ainoa lähde — sen oma dedikoitu kanava — ja koontiviestin moodikenttä jäi pelkäksi raportoinniksi, jota mikään ohjauslogiikka ei enää lue. Perustelu omistajan valinnalle oli vahva: oma kanava julkaistaan retained-tilassa, joten se selviää uudelleenkäynnistyksestä omillaan eikä tarvitse koontiviestiä varmistuksekseen.

    Tämä periaate — yksi totuuslähde per tila — osoittautui työn tärkeimmäksi opetukseksi. Ja kuten kohta näkyy, sen rikkomukset olivat sitkeämpiä kuin uskoin.

    Huipentuma: turvakatkaisu, joka ei vapautunut

    Sitten tuli oikea tapahtuma, joka kokosi kaikki kolme vikaluokkaa yhteen.

    Eräänä kesäkuun päivänä sauna ja auton lataus olivat päällä yhtä aikaa. L1-vaihe ylitti rajan, ja järjestelmän sulakevalvonta (Safety Guardian) teki täsmälleen oikein: laukaisi turvakatkaisun, esti sähköauton latauksen ja siirsi lämpöpumpun suojaavaan tilaan. Tämä oli järjestelmän tärkein tehtävä, ja se onnistui.

    Mutta sitten ylikuormitus poistui — sauna sammui — eikä järjestelmä palautunut normaaliin. Seuraavana päivänä se oli yhä hätätilassa: lataus estetty, lämpöpumppu lukittu suojaavaan tilaan, vaikka mitään ylivirtaa ei enää ollut. Turvakatkaisu oli tehnyt työnsä, mutta vapautus ei toiminut.

    Tässä tuli kiinnostava osa. Vian juuria ei ollut yksi vaan kolme, ja ne edustivat kukin eri vikaluokkaa.

    Ensimmäinen oli looginen umpikuja. Turvavalvonnan koodissa oli ehto, joka sanoi käytännössä ”jos turvakatkaisu on aktiivinen, älä tee mitään muuta”. Mutta automaattinen vapautuslogiikka oli sijoitettu tämän ehdon jälkeen. Eli niin kauan kuin katkaisu oli päällä, koodi ei koskaan päässyt riville, joka olisi vapauttanut sen. Klassinen kuollut koodi: looginen lukko, joka esti oman avaimensa käytön.

    Toinen oli zombie — vikaluokka 1 puhtaimmillaan. Se sama koontiviesti kantoi turvakatkaisun tilaa, ja eräs Node-REDin osa luki sitä takaisin järjestelmän globaaliin tilaan. Niinpä vaikka turvakatkaisu nollattiin omalla kanavallaan, vanha koontiviesti herätti sen henkiin. Tämä oli täsmälleen sama arkkitehtuurivirhe kuin moodin kaksoisomistus — yksi tila, kaksi lähdettä — vain eri muuttujassa.

    Kolmas oli ajoitusvirhe vapautuksessa. Kun vapautuslogiikka vihdoin korjattiin toimivaksi, se lähetti välittömästi luvan lataukselle. Mutta jos sauna oli yhä päällä, tämä olisi voinut laukaista uuden ylivirran heti — vapautus olisi aiheuttanut juuri sen tilanteen, jota vastaan se suojasi.

    Kaikki kolme korjattiin. Vapautuslogiikka siirrettiin ulos umpikujasta. Zombie-lähde katkaistiin niin, että turvakatkaisua luetaan vain sen omalta kanavalta. Ja vapautus tehtiin asteittaiseksi, niin ettei se palauta täyttä kuormaa kerralla.

    Tämä yksi tapahtuma on koko artikkelin tiivistymä. Se osoitti, että hyväkin järjestelmä voi epäonnistua monella tavalla yhtä aikaa, että viat eivät aina ole riippumattomia toisistaan, ja että sama arkkitehtuurivirhe voi piileskellä useassa paikassa odottamassa eri laukaisinta.

    Sama virhe kolmessa kerroksessa

    Turvakatkaisun zombie oli sukua moodin kaksoisomistukselle, ja sen korjaaminen paljasti, miten sitkeitä nämä arkkitehtuurivirheet ovat.

    Ensin korjattiin moodin kaksoisomistus. Sitten korjattiin turvakatkaisun lukija — Node-RED ei enää lukenut sitä koontiviestistä. Tämä riitti poistamaan toiminnallisen vaaran. Mutta koontiviesti julkaisi yhä turvakatkaisun tilaa, vaikka kukaan ei enää sitä lukenut ohjaukseen. Kenttä oli muuttunut vaarattomaksi, mutta se rikkoi yhä periaatetta — ja niin kauan kuin se oli viestissä mukana, mikä tahansa tuleva lukija voisi vahingossa herättää saman zombien henkiin.

    Niinpä se poistettiin kokonaan. Sama arkkitehtuurivirhe oli siis korjattava kolmessa kerroksessa: moodissa, Node-REDin lukijassa ja koontiviestin julkaisussa. Ja jokainen kerros löytyi vasta, kun edellinen oli korjattu — kuin sipulin kuoria.

    Tästä jäi käteen periaate, jota pidän nyt yhtenä työn arvokkaimmista: kun korjaat tilan, jolla on ollut useita lähteitä, tarkista kaikki lähteet — sekä julkaisu- että lukupuoli — älä vain sitä, jossa oire ensin näkyi. Yhden kerroksen korjaaminen tuntuu valmiilta, mutta jos toinen kerros yhä elää, vika palaa eri laukaisimella.

    Kahden tekoälyn työnjako

    Mainitsin aiemmin, että käytin kahta tekoälyä eri rooleissa. Tämä osoittautui yllättävän tehokkaaksi tavaksi tehdä tällaista työtä, ja se ansaitsee oman huomionsa.

    Claude AI toimi toteuttajana: se analysoi viat, kirjoitti korjaukset, ajoi ne kohdejärjestelmään etäyhteyden yli ja varmensi tulokset oikeasta järjestelmästä. ChatGPT toimi katselmoijana: se teki staattisen arkkitehtuurikatselmuksen, etsi epäkohtia rajapinnoista ja haastoi toteuttajan ratkaisuja.

    Työnjako tuotti aitoa lisäarvoa. Useita olennaisia tarkennuksia tuli nimenomaan katselmoijalta. Esimerkiksi käyttöveden todellisen toteuman tunnistaminen ei saanut nojata pelkkään yhteen signaaliin, vaan useamman mittauksen yhdistelmään — tämä oli katselmoijan huomio, ja se esti virhepäätelmiä. Samoin periaate, että muutosskriptin pitää kaatua äänekkäästi, jos se ei löydä korjattavaa kohtaa, sen sijaan että jatkaisi hiljaa kuin mitään ei olisi — tämä oli katselmoijan oivallus, ja se esti tilanteita, joissa olisi luultu korjauksen menneen läpi, vaikka tiedosto jäi muuttumatta.

    En väitä, että kaksi tekoälyä korvaa kokeneen ihmiskatselmoijan. Mutta yhden hengen projektissa, jossa toista paria silmiä ei muuten ole, kahden eri mallin vastakkainasettelu on selvästi parempi kuin yksi malli, joka katselmoi omaa työtään.

    Mitä tästä jäi käteen

    FMA-työ käytiin loppuun: kaikki 17 vikatilaa käsiteltiin. Lopputulos ei ole ”valmis järjestelmä” — sellaista ei olekaan — vaan järjestelmä, jonka tunnetut heikkoudet on käyty läpi ja korjattu yksi kerrallaan, ja jonka korjausten perustelut on dokumentoitu niin, että ne voi jäljittää.

    Läpileikkaava periaate kiteytyi yhteen lauseeseen: päätöksenteon pitää perustua mitattuun tai vahvistettuun toteumaan, ei oletettuun komentoon. Järjestelmä ei saa luottaa siihen, että koska se lähetti käskyn, käsky myös toteutui. Sen pitää katsoa, mitä oikeasti tapahtui — lukea mittari, vahvistaa rele, tarkistaa toteuma — ja tehdä päätöksensä sen perusteella.

    Tämä kuulostaa itsestäänselvyydeltä. Se ei ole. Suuri osa hiljaisista vioista syntyi nimenomaan siitä, että jossain kohden oletettiin komennon riittävän todisteeksi toteumasta. Ero komennon ja toteuman välillä on koko tämän työn ydin.

    Ja ehkä tärkein metatason opetus: ”JSON kelpaa” ei riitä. Useamman kerran kävi niin, että korjaus näytti oikealta — tiedosto validoitui, viesti näytti numeroita — mutta tulos oli silti väärä, koska sitä ei verrattu todellisuuteen. Eräskin aikavyöhykevirhe paljastui vain siksi, että tarkistin kellonajan oikeasta järjestelmästä ennen kuin hyväksyin muutoksen. Lopputulos pitää aina verrata todellisuuteen, ei pelkkään muotoon.

    Mitä on vielä edessä

    FMA teki järjestelmästä turvallisemman, mutta ei vielä täysin käytettävän eikä optimaalista. Kolme isoa kokonaisuutta on yhä edessä, ja ne määräävät, milloin EnergyHub voi todella korvata vanhan järjestelmän.

    Operaattorin näkymä — hallintapaneeli, jolla järjestelmää voi ohjata ja resetoida ilman komentoriviyhteyttä — on käytettävyyden ehto. Tämän turvakatkaisutapaus teki kipeän selväksi: vapautus vaati käsityönä komentoja, joita kenenkään ei pitäisi joutua muistamaan ulkoa.

    Manuaaliset ohitukset — mahdollisuus ottaa yksittäinen laite hetkeksi pois automatiikalta — tekevät järjestelmästä käytettävän arjessa, jossa kaikkea ei voi eikä pidä automatisoida.

    Lämmityskauden ohjaus — sääennusteen, ulkolämpötilan, talon lämpömassan ja lämpöpumpun hyötysuhteen yhdistäminen päätöslogiikkaan — on se, mikä tekee järjestelmästä täyden korvaajan vanhalle. Se on talven aihe, ja siihen palataan myöhemmissä osissa.

    Näistä jälkimmäinen — lämmityskauden ohjaus — on samalla suurin ja kiinnostavin. Se vie kodin energianhallinnan reaaliaikaisesta reagoinnista kohti ennakointia: ei vain ”onko sähkö nyt halpaa”, vaan ”kannattaako lämmittää nyt varastoon, koska huomenna on kylmää ja kallista”.

    Lopuksi

    Aloin tätä sarjaa dokumentoidakseni järjestelmän rakentamisen. Tämä osa kertoo jostain hieman toisesta: siitä hetkestä, kun valmiilta näyttävä järjestelmä joutuu kosketuksiin todellisuuden kanssa ja paljastaa, ettei se ollutkaan niin valmis. Se ei ole epäonnistumisen tarina. Se on tarina siitä, miten vioista tehdään näkyviä ennen kuin ne ehtivät tehdä vahinkoa — ja miten yksi systemaattinen läpikäynti tuotti enemmän luottamusta järjestelmään kuin viikkojen huoleton ajo olisi koskaan tuottanut.

    Käyttöönotto ei paljastanut, että järjestelmä oli huono. Se paljasti, mitä järjestelmästä ei voinut tietää ennen kuin se otti vastuun. Ja sen tiedon hankkiminen — hallitusti, kerros kerrokselta — oli juuri se, mihin koko vaiheistettu käyttöönotto tähtäsi.

    FMA:n jälkeen työ jatkui suoraan seuraavaan puutteeseen, jonka se paljasti. Jos Node-RED kaatuu tai viestiväylä katkeaa, laitteet jäävät viimeiseen ohjattuun tilaan — eikä mikään huomaa sitä. Tätä varten järjestelmään rakennettiin vahtikoira: ensin sydämensyke, jolla ohjauslogiikka kertoo säännöllisesti olevansa hengissä ja syötteidensä olevan tuoreita, ja sen päälle erillinen valvoja, joka seuraa koko järjestelmän terveyttä. Nämä ovat seuraavan osan aihe.


    Seuraavaksi: Osa 16 — Operointi. Varmuuskopiot, vahtikoirat ja hiljaiset viat: miten kotiautomaatiojärjestelmää pidetään pystyssä, kun kukaan ei katso.

    Piditkö artikkelista?

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

    Seuraa blogia Blogit.fi:ssä
  • Case: oma talo — Osa 14

    Ennen käyttöönottoa — ohjauslogiikan läpikäynti ja kehityskohteet

    Tämä on neljästoista osa sarjasta joka dokumentoi EnergyHub-järjestelmän rakentamisen vanhaan rintamamiestaloon. Edelliset osat ovat kuvanneet arkkitehtuurin, komponentit ja integraatiot. Tässä osassa pysähdytään ennen live-käyttöönottoa: käydään läpi mitä on rakennettu, missä on tunnettuja puutteita ja mitä pitää vielä tehdä ennen kuin uusi järjestelmä ottaa täyden kontrollin.


    Järjestelmä on pyörinyt shadow-moodissa useita viikkoja. Home Assistant ja Node-RED tekevät päätöksiä — mutta eivät toteuta niitä. Vanhat automaatiot ohjaavat laitteita edelleen. Uusi järjestelmä seuraa, laskee ja kirjaa.

    Tämä on tarkoituksellinen valinta. Shadow-moodi on turvaverkko: jos päätöslogiikassa on virhe, se näkyy lokissa eikä laitteen käyttäytymisessä. Ennen live-käyttöönottoa on hyvä pysähtyä ja katsoa mitä on opittu.

    Mikä toimii hyvin

    Datarkerros on vakaa. Kaikki mittaukset kulkevat luotettavasti: P1-mittari, Sungrow Modbus, Thermia Modbus, Shelly-mittarit ja Nissan API. MQTT-väylä välittää tiedot Node-REDille ja InfluxDB tallentaa historian Grafana-visualisointia varten.

    Safety Guardian toimii. Vaihevirtojen valvonta 10 sekunnin syklissä on osoittautunut toimivaksi. 25.5.2026 tapahtuma — L1-vaihe nousi 23,8 ampeerin, Guardian laukaisi peak_protection-moodin, EV-lataus estettiin — meni täsmälleen suunnitelman mukaan. Laukaisu 23,8 ampeerin kohdalla voi herättää kysymyksen miksi reagoitiin ennen 25 ampeerin pääsulakkeen rajaa. Vastaus on ennakointi: Safety Guardian toimii ennakkovaran varassa, ei sulakkeen nimellisrajan mukaan. 25 ampeerin gG-sulake kestää lyhyet ylitykset hyvin — tavoitteena on estää pitkäkestoinen kuormitus, ei reagoida sulakkeen palamiseen. Laukaisu tapahtui jo 23,8 ampeerin kohdalla vaikka pääsulakkeet ovat 25 ampeeria: tämä on tarkoituksellista. Safety Guardian perustuu ennakoivaan rajaan, ei sulakkeen nimellisrajan ylittymiseen. Konservatiivinen varoitusraja 22,5 ampeeria antaa aikaa reagoida ennen kuin todellinen sulakeriski syntyy.

    PV-rajoituslogiikka on testattu. Sungrow-invertterin Modbus-ohjaus toimii. Dynaaminen rajoituslaskenta omakäytön perusteella on validoitu käytännössä. DC-potentiaaliaukko on dokumentoitu ja ymmärretty.

    EV-telemetria on kunnossa. Kolmen tietolähteen arkkitehtuuri — Nissan API, ShellyPro3EM63, ShellyPro1 — toimii suunnitelman mukaan. Tapering-käyrä on mitattu: lataus täydellä 3,6 kW teholla SOC 97 %:iin asti, sen jälkeen ~75 minuutin laskeva vaihe.

    Thermia-integraatio on validoitu. EVU/Boost-ohjaus Shelly Pro 2:n kautta toimii. Boost trigger-and-release on testattu käytännössä. Comfort wheel kirjoitetaan Modbus-rekisteriin suoraan.

    Tunnettuja puutteita ja kehityskohteita

    1. Ohjauslogiikan parametrit ovat hajallaan

    Hintakynnykset, lämpötilatavoitteet ja muut ohjauspisteet ovat tällä hetkellä osittain Node-REDin koodissa, osittain 00_core.yaml:ssa. Ennen live-käyttöönottoa kaikki moodikohtaiset parametrit pitää koota yhteen paikkaan josta käyttäjä voi muuttaa niitä ilman koodin muokkaamista.

    Tavoitetila: 00_core.yaml sisältää kaikki säädettävät parametrit selkeästi dokumentoituna. Node-RED lukee ne telemetrian kautta. Ei hardkoodattuja arvoja. Koska kaikki säädöt ovat yhdessä paikassa, virhekin vaikuttaa laajasti — parametrimuutokset versioidaan ja dokumentoidaan ennen käyttöönottoa. Koska kaikki säädöt ovat yhdessä paikassa, yksittäinen virhe vaikuttaa laajasti — siksi parametrimuutokset dokumentoidaan ja versioidaan.

    2. Operating moodit eivät ole viimeisteltyjä

    Normal, peak_protection ja emergency toimivat. Mutta vacation, guest, cheap_energy ja solar_maximize ovat osittain toteutettuja. Erityisesti vacation-moodi — joka tarvitsee omat lämpötilaminimit käyttövedelle ja huonelämpötilalle — on kesken.

    3. EV-latauksen ajoitusoptimointi puuttuu

    Nykyinen logiikka reagoi reaaliaikaiseen tilanteeseen: hintaan, aurinkoon, sulakkeeseen. Se ei osaa vastata kysymykseen ”milloin kannattaa aloittaa lataus jotta akku on täynnä klo 7:00”. Tähän tarvitaan lähtöaikaohjaus ja tapering-mallin integrointi päätöslogiikkaan.

    Tämä on tiedostettu rajoite. Se ei estä live-käyttöönottoa — nykyinen kynnyslogiikka toimii — mutta se on selkeä kehityskohde.

    4. Watchdog puuttuu — live-käyttöönoton ehto

    Jos Node-RED kaatuu tai MQTT-yhteys katkeaa, laitteet jäävät viimeiseen ohjattuun tilaan. Tällä hetkellä ei ole mekanismia joka havaitsisi tämän ja palautuisi turvalliseen tilaan automaattisesti. Watchdog on suunniteltu mutta ei toteutettu.

    Tämä on lähes ehdoton vaatimus ennen EV- ja HP-ohjauksen live-käyttöönottoa. Ilman watchdogia järjestelmähäiriö voi jättää lämpöpumpun EVU-tilaan tai EV-latauksen estetyksi ilman että kukaan huomaa. Watchdog toteutetaan ennen vaihetta 2.

    5. Sääennusteet ja lämpöinertia eivät ole mukana

    Optimointi perustuu tämän hetken dataan. Huomisen kylmyys ei vaikuta tämän päivän lämmityspäätöksiin. Talon lämpömassa — kuinka kauan se pysyy lämpimänä ilman lämmitystä — ei ole mallinnettu. Nämä ovat seuraavan kehitysvaiheen aiheita.

    Shadow-moodin havainnot

    Muutaman viikon shadow-moodi on tuottanut ensimmäisiä vertailuhavaintoja.

    Hintaohjauksen ero: Uusi järjestelmä olisi useammin estänyt lämpöpumpun kalliina tunteina. Vanha järjestelmä reagoi hintaan hitaammin koska sillä ei ole yhtä tarkkaa hintadataa käytössä reaaliajassa.

    EV-latauksen ero: Uusi järjestelmä ottaa sulaketilanteen huomioon ennen latauksen sallimista. Vanhassa järjestelmässä EV-lataus on yksinkertaisempi ON/OFF-päätös ilman vaihevirtatietoisuutta.

    PV-rajoitus: Vanha järjestelmä rajoittaa invertteria negatiivisilla hinnoilla kiinteällä prosentilla. Uusi laskee rajoituksen dynaamisesti omakäytön perusteella — tulos on useimmiten erilainen.

    Nämä erot ovat pieniä yksittäisinä päivinä. Kumulatiivinen vaikutus näkyy vasta kuukausien datasta.

    Live-käyttöönoton suunnitelma

    Live-käyttöönotto tehdään vaiheittain, ei yhdellä kertaa.

    Vaihe 1: PV-rajoitus live Ensimmäisenä otetaan live-tilaan PV-rajoitus negatiivisilla hinnoilla. Se on yksisuuntainen — rajoittaa invertteria — eikä ohjaa mitään fyysistä relettä. Pienin riski.

    Vaihe 2: HP EVU live Toisena lämpöpumpun EVU-ohjaus kalliina tunteina. Tämä on suurin yksittäinen vaikutus taloudelliseen optimointiin. EVU on konservatiivinen — käytännön testien perusteella ohjaus toimii ennakoivasti eikä ole aiheuttanut äkillisiä katkaisuja käynnissä olevalle kompressorille.

    Vaihe 3: EV-lataus live Kolmantena EV-latauksen ohjaus. Tässä vaiheessa Safety Guardian on jo osoittanut toimivuutensa ja EV-telemetria on vakiintunut.

    Vaihe 4: HP Boost live Viimeisenä lämpöpumpun Boost-ohjaus käyttöveden ajoittamiseen. Tämä vaatii eniten luottamusta järjestelmän logiikkaan koska se aktiivisesti käynnistää toimenpiteitä eikä vain estä niitä.

    Jokaisen vaiheen jälkeen seurataan vähintään viikko ennen seuraavaa vaihetta ja tarkistetaan hyväksymiskriteerit:

    • ei virheellisiä ohjauksia viikon aikana
    • observability-loki vastaa toteutunutta laitekäyttäytymistä
    • manuaalinen ohitus toimii odotetusti
    • vanha automaatio ei taistele uutta vastaan Jokaiselle vaiheelle on rollback-suunnitelma: jos poikkeama havaitaan — virheellinen ohjaus, odottamaton laitekäyttäytyminen tai lokin ja todellisuuden ristiriita — kyseinen live-flag palautetaan false-tilaan ja vanha automaatio jatkaa ohjausta. Palautus on yhden muuttujan asia ja tapahtuu sekunteissa.

    Mitä live-käyttöönotto ei tarkoita

    Live-käyttöönotto ei tarkoita että vanha järjestelmä sammutetaan. Molemmat pyörivät rinnakkain — uusi ottaa kontrollin yksi osa-alue kerrallaan. Shadow/live-mekanismi on juuri tätä varten: energyhub_hp_live_control ja energyhub_ev_live_control -flagit ohjaavat kumpi järjestelmä on aktiivinen.

    Live-käyttöönotto ei myöskään tarkoita että optimointi on valmis. Se tarkoittaa että arkkitehtuuri on riittävän vakaa kantaakseen vastuun — ja että voidaan alkaa kerätä oikeaa dataa oikeista päätöksistä.

    Live-käyttöönoton tärkein tavoite ei ole säästö. Se on ohjausvastuun siirto hallitusti yhdelle arkkitehtuurille — pois hajautetuista automaatioista, kohti keskitettyä päätöksentekoa joka tietää mitä tekee ja miksi.

    Teoria ja todellisuus kohtaavat vasta kun järjestelmä ohjaa oikeita laitteita oikeilla päätöksillä. Tässä vaiheessa EnergyHub ei ole enää kokeilu, mutta se ei ole vielä valmis optimointijärjestelmä. Se on hallitusti käyttöönotettava ohjausarkkitehtuuri.


    Seuraavaksi: Osa 15 — Live-käyttöönotto. Miten vaiheistus eteni käytännössä ja mitä tapahtui ensimmäisten viikkojen aikana.

    Piditkö artikkelista?

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

    Seuraa blogia Blogit.fi:ssä
  • Case: oma talo — Osa 13

    Sähköauton latauksen optimointi EnergyHubissa

    Tämä on kolmastoista osa sarjasta joka dokumentoi EnergyHub-järjestelmän rakentamisen vanhaan rintamamiestaloon. Edellisessä osassa käsiteltiin Thermia-lämpöpumpun Modbus-ohjaus. Tässä osassa tarkastellaan kahta toisiinsa kytkeytyvää kokonaisuutta: sähköauton latausta ja aurinkoinvertterin ohjausta — ja sitä miten ne toimivat yhdessä energiajärjestelmässä.


    Sähköauto on kotitalouden energiankäytön kannalta poikkeuksellinen kuorma. Se on suuri — tyypillisesti 11 kW kolmivaiheisena, tässä asennuksessa 3,6 kW Nissan Leafin yksivaiheisen latauksen vuoksi — mutta täysin siirrettävissä. Toisin kuin lämmitys tai valaistus, auton lataus ei ole sidottu kellonaikaan. Se voidaan tehdä milloin tahansa yön tai päivän aikana, kunhan auto on kotona ja johdossa.

    Tämä joustavuus tekee EV-latauksesta optimoinnin kannalta kiinnostavimman yksittäisen kuorman. Mutta se tekee siitä myös vaikeimman — koska ajoituksen optimointi vaatii tietoa jota ei aina ole saatavilla: milloin auto tarvitaan seuraavaksi, paljonko akkua tarvitaan, ja miten latausteho käyttäytyy akun loppupuolella.

    Kolme tietolähdettä

    EnergyHubin EV-integraatio perustuu kolmeen tietolähteeseen joilla on tarkasti rajatut roolit.

    Nissan API tarjoaa akun varaustason (SOC), arvioidun latausajan ja auton yleisen tilan. Se ei ole reaaliaikainen — tiedot päivittyvät vain kun auto on hereillä: latauksen alussa ja lopussa, HVAC-käynnistyksen yhteydessä tai kun update-nappia painetaan manuaalisesti. Nissan API:n lataus-tila (charging) on epäluotettava — se voi näyttää ”ei lataa” vaikka auto lataa.

    ShellyPro3EM63 mittaa EV-pistokkeen todellisen latausvirran reaaliajassa. Tämä on latauksen luotettava tilan lähde. Kun Phase A teho ylittää 100 W ja ohjausrele on päällä, auto lataa. Kun teho on lähellä nollaa mutta rele on edelleen päällä, akku on täynnä tai lataus on estynyt.

    ShellyPro1 ohjaa erillistä 25 ampeerin relettä joka sallii tai estää latauksen fyysisesti. Node-RED lähettää ohjauskomennon MQTT:n kautta ShellyPro1:lle, joka kytkee releen. Laturi on Walle 16 (3-vaiheinen 11 kW), mutta Nissan Leaf latautuu yksivaiheisesti — kaikki latausteho tulee Phase A:lta, noin 3,6 kW.

    Nissan API    → SOC, arvioitu latausaika (ei reaaliaikainen)
    ShellyPro3EM63 → todellinen latausteho (reaaliaikainen, luotettava)
    ShellyPro1     → latauksen ohjausrele

    Kolmen lähteen yhdistelmä ratkaisee ongelman jota mikään niistä ei yksin ratkaise.

    Nissan API:n rajoitteet käytännössä

    29.5.2026 klo 15:20 EV-telemetria näytti:

    json

    {
      "soc_pct": 70,
      "charging": false,
      "shelly_power_w": 3699,
      "shelly_relay_on": true
    }

    Nissan API sanoi charging: false. ShellyPro3EM63 mittasi 3699 W. Auto latasi täydellä teholla.

    Tämä ei ole bugi — se on Nissan API:n suunniteltu toiminta. API päivittää latauksen tilan vain kun auto raportoi muutoksesta pilveen. Shelly mittaa pistokkeen tehon jatkuvasti.

    Tämä on se syy miksi EnergyHubissa latauksen tila ei koskaan perustu Nissan API:n charging-arvoon. Se perustuu Shellyn mittaukseen.

    Pääsulakkeet — turvallisuus ennen hintaa

    EV on kotitalouden suurin yksittäinen kuorma. Walle 16 -laturi Nissan Leafin yksivaiheisella latauksella ottaa ~3,6 kW yhdeltä vaiheelta — noin 16 ampeeria. Kun tähän lisätään talon muu kulutus, 25 ampeerin pääsulake on jo lähellä.

    Tämä on yksi tärkeimmistä syistä miksi EV-latausta pitää ohjata älykkäästi eikä vain kytkeä päälle halvalla tunnilla. Jos lämpöpumppu käy täydellä teholla ja EV lataa yhtä aikaa, L1-vaihe voi ylittää 25 ampeerin rajan.

    EnergyHubissa vaihekohtainen virta mitataan 10 sekunnin välein. Safety Guardian valvoo rajoja:

    • 22,5 A — varoitustaso, peak_protection aktivoituu
    • 24,5 A — turvalaukaisin, EV-lataus katkaistaan välittömästi

    EV-lataus on ensimmäinen kuorma joka pudotetaan kun sulake uhkaa — se on suuri, ei-kriittinen ja helposti ohjattava. Turvallisuus menee aina hinnan edelle.

    Latauspäätöslogiikka

    Priority Resolver (EnergyHubin keskitetty päätöksentekokerros) tekee EV-latauksen päätöksen samassa syklissä kaiken muun kanssa. Logiikka etenee tuttuun tapaan — turvallisuus ensin, talous viimeisenä.

    javascript

    // Luotettava latauksen tila Shellystä
    s.evActuallyCharging = s.shellyPowerW > 100 && s.shellyRelayOn
    s.evFull = s.shellyPowerW < 100 && s.shellyRelayOn
    
    // Latauspäätös
    if (!s.cap.evChargeAllowed) {
        decisions.ev = false  // capability estää
    } else if (s.isPeakLoad) {
        decisions.ev = false  // sulake uhkaa
    } else if (s.isNegativePrice) {
        decisions.ev = true   // negatiivinen hinta — lataa
    } else if (s.isSolarExcess && s.evSocPct < 95) {
        decisions.ev = true   // aurinkoylijäämä — käytä itse
    } else if (s.isCheapPrice && s.evSocPct < 90) {
        decisions.ev = true   // halpa hetki — lataa
    } else if (s.evSocPct < 20) {
        decisions.ev = true   // akku liian tyhjä — lataa aina
    } else {
        decisions.ev = false  // ei tarvetta
    }

    SOC-raja on tärkeä: lataus ei jatku jos akku on jo riittävän täynnä. ”Halpa hinta” ei tarkoita että ladataan vaikka akussa on jo 85 % — se tarkoittaa että käytetään halpa hetki hyväksi jos latausta tarvitaan.

    Aurinkoylijäämä ja EV

    Aurinkoylijäämä on EV-latauksen kannalta paras tilanne. Aurinko tuottaa enemmän kuin talo kuluttaa, ylijäämä menisi verkkoon — EV voi käyttää sen.

    EnergyHub laskee aurinkoylijäämän reaaliajassa:

    javascript

    // gridPowerW on negatiivinen kun myydään verkkoon
    s.solarExcessW = Math.max(0, -s.gridPowerW)  // vienti verkkoon = ylijäämä
    s.isSolarExcess = s.solarExcessW > 2000       // yli 2 kW ylijäämä

    Kun ylijäämää on yli 2 kW ja EV tarvitsee latausta, lataus sallitaan. EV:n 3,6 kW kulutus nostaa talon omaa kulutusta — verkkoon menevä ylijäämä pienenee ja aurinko kattaa suuremman osan omasta kulutuksesta.

    Tässä on aurinkoinvertterin ja EV-latauksen koordinoinnin ydin: ne optimoidaan yhdessä, ei erikseen. Sama Priority Resolver joka päättää PV-rajoituksesta päättää myös EV-latauksesta. Ne eivät taistele keskenään.

    PV-rajoitus ja EV yhtä aikaa

    Negatiivisilla hinnoilla tilanne on mielenkiintoinen. PV-rajoitus on aktiivinen — invertteri rajoitettu esimerkiksi 25 %:iin. Samaan aikaan EV haluaa ladata.

    Priority Resolver ratkaisee tämän koordinoidusti: jos EV lataa, se nostaa talon omaa kulutusta. Kasvanut omakäyttö tarkoittaa että PV-rajoitusta voidaan löysätä — invertteri saa tuottaa enemmän koska enemmän menee omaan käyttöön eikä verkkoon.

    javascript

    // Laske PV-rajoitus ottaen huomioon EV-lataus
    const evLoad = decisions.ev ? 3600 : 0
    const ownLoad = Math.max(300, s.pvPowerW + Math.min(0, s.gridPowerW) + evLoad)
    const pct = Math.max(10, Math.min(95, Math.round((ownLoad / s.pvPowerW) * 100)))
    decisions.pvCurtailPct = pct

    Käytännössä algoritmi arvioi kuinka paljon aurinkotuotannosta käytetään paikallisesti — oma kulutus plus EV-lataus — ja säätää invertterin tehorajan sen mukaan. Mitä enemmän omaa kulutusta, sitä korkeammalle rajoitus asetetaan ja sitä enemmän aurinko saa tuottaa.

    EV-lataus ja PV-rajoitus lasketaan samassa päätössyklissä. Tulos: aurinko tuottaa enemmän, EV lataa aurinkosähköllä, verkkoon ei mene negatiivisella hinnalla myytävää sähköä.

    Latauksen ajoittaminen — nykyinen tila

    Nykyisessä toteutuksessa EV-lataus perustuu reaaliaikaiseen tilanteeseen: hintaan, aurinkoon ja sulaketilanteeseen. Järjestelmä tekee päätöksen joka minuutti sen hetkisen datan perusteella.

    Tämä on toimiva mutta ei optimaalinen ratkaisu. Se ei osaa vastata kysymykseen: ”milloin kannattaa aloittaa lataus jotta akku on täynnä klo 7:00 halvimmilla tunneilla?”

    Vastaukseen tarvitaan kaksi asiaa jotka ovat vielä kehitysvaiheessa:

    Tapering-malli — Nissan Leaf 39 kWh ei lataudu tasaisella 3,6 kW teholla koko ajan. Latausteho laskee akun loppupuolella, arviolta noin 90 % SOC:n jälkeen. Ilman tätä mallia ei voi laskea tarkasti milloin lataus valmistuu.

    Lähtöaikaohjaus — käyttäjä asettaa milloin auto tarvitaan. Järjestelmä laskee milloin lataus pitää viimeistään aloittaa jotta akku on täynnä ajoissa. Tätä varten tarvitaan kolme ominaisuutta:

    • ”Täysi akku klo 06:00” -painike
    • Säädettävä lähtöaika
    • ”Lataa nyt täyteen” -ohitus

    Nämä ominaisuudet odottavat tapering-mallin valmistumista.

    Tapering — mitattu käyrä

    29.5.2026 tehtiin täydellinen latausmittaus ShellyPro3EM63:lla. Data kattaa SOC 85 %:sta täyteen. Lataus alkoi noin 70 %:sta mutta jatkui keskeytymättä loppuun.

    SOCLataustehoHuomio
    85–95 %~3 710–3 780 WTasainen täysi teho
    98 %~2 870 WTapering alkaa
    100 %~630 WLoppuvaihe, jatkuu ~60 min

    Tapering alkaa noin 97–98 % SOC:ssa. Tämä on huomattavasti myöhemmin kuin monilla muilla EV:llä — Leaf lataa täydellä teholla poikkeuksellisen pitkään. Tapering-vaiheen kesto on noin 60–75 minuuttia.

    Datan perusteella voidaan rakentaa kaksivaiheinen malli:

    javascript

    function estimateChargeTime(socPct, targetPct) {
        const capacity = 39       // kWh
        const taperingStart = 97  // % — mitattu
        const fullPower = 3.75    // kW — mitattu keskiarvo
        const taperingDuration = 75  // min — mitattu
    
        if (targetPct <= taperingStart) {
            // Tasainen lataus
            const energyNeeded = (targetPct - socPct) / 100 * capacity
            return energyNeeded / fullPower * 60  // minuuttia
        } else {
            // Tasainen osuus + tapering
            const flatEnergy = Math.max(0, taperingStart - socPct) / 100 * capacity
            const flatTime = flatEnergy / fullPower * 60
            const taperingPct = targetPct - Math.max(socPct, taperingStart)
            const taperingTime = taperingPct / (100 - taperingStart) * taperingDuration
            return flatTime + taperingTime
        }
    }
    
    // Esimerkki: SOC 70 % → 100 %
    // Tasainen osuus: (97-70)/100 * 39 / 3.75 * 60 = ~168 min
    // Tapering: 75 min
    // Yhteensä: ~243 min = noin 4h (Nissan API näytti 4.5h — linjassa)

    Tämä malli on riittävän tarkka lähtöaikaohjauksen laskentaan. Yksittäinen mittaus ei riitä lopulliseksi malliksi — tarvitaan useampi lataus eri SOC-tasoilta — mutta suuntaviivat ovat selvillä.

    Miksi EV on vaikein kuorma optimoida

    Lämpöpumppua optimoidaan talon lämpömassaa vasten — rakennuksen lämpötila muuttuu hitaasti ja puskuri on suuri. Aurinkoenergiaa optimoidaan hetkellistä tilannetta vasten — päätökset tehdään minuuteissa.

    EV on erilainen. Optimointihorisontti on tunteja — ”milloin auto tarvitaan seuraavaksi” — mutta tieto tulevaisuudesta on epävarmaa. Käyttäjä voi muuttaa suunnitelmiaan. Auto voi olla poissa odottamatta.

    Tämä tekee EV-optimoinnista ajoitusongelman eikä pelkän kynnysongelman. Ratkaisu vaatii:

    • Tietoa nykyisestä SOC:sta (Nissan API)
    • Tietoa latausdynamiikasta (tapering-malli)
    • Tietoa tulevista hinnoista (Nordpool day-ahead)
    • Tietoa auton tarvitsimisjasta (käyttäjän syöte)

    Kolme ensimmäistä on saatavilla tai rakenteilla. Neljäs odottaa käyttöliittymää.

    Mitä on rakennettu, mitä tulee

    Toimii nyt:

    • EV-latauksen sallinta/esto MQTT-komennolla
    • Reaaliaikainen latauksen tila Shellystä
    • SOC-tietoisuus Nissan API:sta
    • Aurinkoylijäämän hyödyntäminen lataukseen
    • Hintaohjattu lataus (kynnyslogiikka)
    • Automaattinen Leaf-tietojen päivitys latauksen alkaessa ja loppuessa

    Kehitysvaiheessa:

    • Tapering-malli (tarvitaan mittausdata)
    • Lähtöaikaohjaus (tarvitaan tapering-malli)
    • EV-UI: ”täysi klo 06:00”, säädettävä lähtöaika, ”lataa nyt” -ohitus

    Arkkitehtuurinen johtopäätös: EV-lataus on yksinkertaiselta näyttävä ongelma joka piilottaa sisäänsä ajoitusongelmat, epäluotettavat tietolähteet ja koordinointitarpeen muiden kuormien kanssa. Yksittäinen automaatio ei riitä — tarvitaan Priority Resolver joka näkee koko tilanteen.


    Seuraavaksi: Osa 14 — Käyttöönotto ja häiriöt. Mitä tapahtui kun järjestelmä otettiin käyttöön — ja mitä opittiin.

    Piditkö artikkelista?

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

    Seuraa blogia Blogit.fi:ssä
  • Case: oma talo — Osa 12

    Thermia Modbus-ohjaus

    Tämä on kahdestoista osa sarjasta joka dokumentoi EnergyHub-järjestelmän rakentamisen vanhaan rintamamiestaloon. Edellisessä osassa käytiin läpi konfliktit ja niiden ratkaisu. Tässä osassa tarkastellaan maalämpöpumpun ohjauksen käytännön toteutusta — mitä Modbus antaa, mitä EVU/Boost-signaalit tekevät ja missä kulkee raja jonka yli ei pidä mennä.


    Maalämpöpumppu on kotitalouden energiankäytön kannalta yksi tärkeimmistä laitteista. Se lämmittää talon, tuottaa käyttöveden ja kuluttaa merkittävän osan sähköstä. Siksi se on myös yksi houkuttelevimmista ohjauksen kohteista — ja samalla yksi varovaisimmin lähestyttävistä.

    Thermia Calibra 12 on osoittautunut hyväksi kumppanina EnergyHub-ajattelulle. Se ei ole suljettu musta laatikko — Modbus TCP antaa laajan näkymän pumpun sisälle. Mutta se on myös opettanut tärkeän läksyn: ohjaaminen ja hallitseminen ovat eri asioita. Sama periaate pätee muidenkin valmistajien pumppuihin: esimerkiksi Nibe, Daikin, Mitsubishi ja Vaillant tarjoavat vastaavia Smart Grid Ready -tuloja ja Modbus-rajapintoja, joten tämän osan ajattelumalli on siirrettävissä laitemerkistä riippumatta — rekisteriosoitteet ja tilakoodit vain vaihtuvat.

    Mitä Modbus antaa

    Thermian Modbus-integraatio tarjoaa huomattavan määrän dataa. Luettavissa on muun muassa:

    • Ulkolämpötila, menovesi, paluuvesi, käyttöveden lämpötila
    • Kompressorin kierrosnopeus prosentteina
    • Nykyinen käyttötila: lämmitys, käyttövesi, sulatus, anti-legionella, valmiustila
    • Jonossa olevat vaatimukset prioriteettijärjestyksessä
    • Käyttötuntilaskurit: kompressori, käyttövesi, lisälämmitin
    • Comfort wheel -asetus (sisälämpötilan tavoitearvo)

    Käyttötila on erityisen arvokas. Rekisteristä näkee reaaliajassa onko pumppu lämmitysajolla (prioriteetti 4), käyttövesiajolla (3), sulatuksessa (2) vai anti-legionella-syklissä (7). Käytetyt prioriteettiarvot perustuvat Thermian viralliseen Modbus-dokumentaatioon sekä käytännön validointiin tässä järjestelmässä. Tämä mahdollistaa älykkään ohjauksen joka ei taistele pumpun omaa logiikkaa vastaan — se seuraa mitä pumppu tekee ja toimii sen mukaan. Jos pumppu on jo anti-legionella-syklissä (rekisteri 30002 palauttaa arvon 7) tai käyttövesiajolla (arvo 3), EnergyHub ei käynnistä uutta Boost-ohjausta. Ulkoinen automaatio ei taistele sisälogiikkaa vastaan — se näkee sen tilan ja väistää.

    EM3-laajennusmoduuli ja neljä tilaa

    Thermia Calibra 12:ssa on EM3-laajennusmoduuli joka tarjoaa älykkään sähköverkkotoiminnon — neljä eri ohjaustilaa kahden digitaalisen tulon (SG1 ja SG2) yhdistelmillä:

    SG1=0, SG2=0  →  Normal    — normaali toiminta
    SG1=1, SG2=0  →  EVU       — esto: kompressori ja lisälämmitin pysäytetään
    SG1=0, SG2=1  →  Comfort   — käyttövesi korkeammilla lämpötiloilla
    SG1=1, SG2=1  →  Boost     — käynnistää käyttöveden korkeat lähtöarvot,
                                  laukaisee bakteerineston tarvittaessa

    EnergyHubissa nämä signaalit tuotetaan Shelly Pro 2 -releellä joka kytkee EM3-moduulin tuloja. SG1 on output_0, SG2 on output_1.

    Aktiivinen Smart Grid -tila on luettavissa myös Modbusista: rekisteri 30083 (Comfort mode) palauttaa arvon 1=EVU, 4=Normal, 5=Comfort, 6=Boost. Tämä mahdollistaa tilan varmistamisen ohjelman puolelta — ei tarvitse olettaa että rele on mennyt oikeaan asentoon. Tämä readback-mahdollisuus osoittautui myöhemmin arvokkaammaksi kuin kirjoitushetkellä ymmärsin: FMA-kovennuskierroksen jälkeen kaikki tilan palautuspolut varmistavat rekisteristä 30083 että Normal todella tuli voimaan, ja yrittävät uudelleen jos ei tullut. Komento ei ole sama kuin toteuma — tästä lisää sarjan osassa 15.

    EVU — esto joka odottaa

    EVU (SG1=1, SG2=0) on yksinkertaisin tila: se estää normaalisti kompressorin käynnistymisen ja rajoittaa lämmitystoimintaa valmistajan Smart Grid -logiikan mukaisesti. Käytetään kalliina tunteina tai sulakevaaratilanteissa.

    Tärkeä yksityiskohta: EVU ei katkaise käynnissä olevaa kompressoria välittömästi. Kompressorin saa käynnistää vähintään viisi minuuttia ennen pysähtymistä — EVU-signaali odottaa kunnes tämä ehto täyttyy. Tämä suojaa kompressoria lyhyiltä käynnistys-sammutus-sykliltä.

    Viive on luettavissa suoraan Modbusista: rekisteri 30061 (Compressor temporarily blocked, start restriction timer) kertoo onko kompressori estoajassaan. Ohjauslogiikka voi seurata tätä rekisteriä eikä sen tarvitse olettaa milloin esto päättyy.

    Käytännössä EVU:n vaikutus näkyy vasta minuuttien kuluessa, ei sekunteina. Ohjauslogiikka ei saa olettaa välitöntä pysähtymistä. Tällä viiveellä on käytännön seuraus myös kuormanpudotuksessa: kun sulakeraja uhkaa ja Thermia pudotetaan EVU:lla, vaikutus verkkovirtaan näkyy vasta minuuttien päästä — pudotuslogiikan on siis toimittava iteratiivisesti eikä olettaa että yksi komento riittää.

    Boost — trigger and release

    Boost (SG1=1, SG2=1) on tehokkain tila: se käynnistää käyttöveden lämmityksen korkeammilla käynnistys- ja pysäytyslämpötiloilla, ja laukaisee tarvittaessa bakteerineston jos säiliö on liian kylmä.

    Käytännön testaus paljasti fiksuimman tavan käyttää Boostia:

    1. Aseta Boost — SG1=1, SG2=1
    2. Odota kunnes käyttövesiajo on käynnistynyt (running_priority = 3) — pumppu on tarttunut tehtävään
    3. Palauta Normal — SG1=0, SG2=0
    4. Pumppu jatkaa käyttövesiboostin loppuun itse

    Tämä ”trigger and release” -malli on turvallinen koska Boost-tila kytketään pois heti kun tiedetään että pumppu on tarttunut tehtävään. Rele ei jää Boost-tilaan pidemmäksi aikaa kuin välttämätöntä — ja mikä tärkeintä, lämmityksen tahaton boostaus estyy. Käytännön testien perusteella Boost ei välttämättä käynnistä käyttövesiajoa jos käyttövesi on jo riittävän lämmin. Tällöin pumppu ei reagoi Boost-signaaliin ja tila voidaan palauttaa Normaliin ilman vaikutuksia — trigger-and-release toimii myös tässä tilanteessa oikein.

    Comfort wheel — pehmein ohjausmetodi

    Comfort wheel on arkkitehtuurisesti puhtain tapa ohjata maalämpöpumppua. Se ei pakota tilaa päälle tai pois — se muuttaa pumpun käyttäytymistä sen omien rajojen sisällä.

    Tässä on syytä oikaista yleinen väärinkäsitys, johon itsekin aluksi sorruin: Thermian comfort wheel ei ole lämpökäyrän siirto, vaan sisälämpötilan tavoitearvo celsiuksina. Rekisteriin kirjoitetaan esimerkiksi 2200, joka tarkoittaa 22,00 °C:n tavoitetta. Käytännön vaikutus hintaohjauksessa on samankaltainen — tavoitteen laskeminen vähentää lämmitystä, nostaminen lisää sitä — mutta yksikkö ja mekanismi ovat eri asia kuin perinteinen käyräsiirto.

    Vanhassa HA:ssa ohjaus oli toteutettu kolmella tasolla:

    • Solar boost — kun P1-mittari näyttää yli 1500 W vientiä ja huonelämpötila on alle 22,3 °C: tavoitearvoa nostetaan. Aurinko tuottaa ylimääräistä — hyödynnetään se lämmitykseen ennen kuin se menee verkkoon.
    • Normaali — perustila, tavoitearvo asetetun arvon mukaan.
    • Eco — kahdeksan kalleinta tuntia vuorokaudessa: tavoitearvoa lasketaan. Rakennuksen lämpömassa toimii puskurina — sisälämpötila ei laske heti vaikka lämmitystä vähennetään.

    Comfort wheel kirjoitetaan Modbus-rekisteriin 40006 (scale 100). Home Assistantin YAML:ssa tämä on address: 5, input_type: holding — tästä lisää alla osoitteistusta käsittelevässä huomiossa. Muutos vaikuttaa pumpun ohjaukseen välittömästi — ei tarvita releohjausta.

    Nykyisessä EnergyHubissa myös comfort wheel -kirjoitus noudattaa komento ≠ toteuma -periaatetta: Node-RED pitää kirjaa pyydetystä arvosta (pending) ja pitää sitä voimassa olevana (confirmed) vasta kun rekisteristä luettu arvo vastaa pyydettyä. Näin hiljainen kirjoitusvirhe — esimerkiksi Modbus-katkon aikana hukkunut kirjoitus — ei jää huomaamatta.

    Mitä Modbus-kirjoitus ei tee

    Käytännön testaus opetti myös rajoitteen jota ei dokumentaatiosta selvästi löydy.

    Käyttöveden käynnistyslämpötilaa yritettiin muuttaa Modbus-kirjoituksella — tavoitteena käynnistää käyttövesiajo halutuilla hetkillä. Rekisteri kirjoitettiin, arvo muuttui. Mutta mitään ei tapahtunut. Vaikka odotettiin useita tunteja, pumppu ei käynnistänyt käyttövesiajoaan.

    Todennäköinen selitys: pumpun sisäinen logiikka tarvitsee käynnistysehdon täyttymisen — riittävän nopean lämpötilanpudotuksen — ennen kuin se reagoi muuttuneeseen käynnistysarvoon. Pelkkä rekisterin arvon muuttaminen ei riitä.

    Johtopäätös: käytännön kokeissa Boost-rele osoittautui luotettavimmaksi tavaksi käynnistää käyttövesijakso haluttuna ajankohtana. Modbus-kirjoitus sopii comfort wheel -ohjaukseen, mutta ei käyttövesilämmityksen pakottamiseen halutulle hetkelle.

    Viitteeksi: käyttöveden käynnistys- ja pysäytyslämpötilat ovat rekistereissä 40023 (start, scale 100) ja 40024 (stop, scale 100). Arvot voi lukea ja kirjoittaa — mutta pelkkä arvon muuttaminen ei riitä käynnistämään uutta käyttövesijaksoa ilman riittävää lämpötilanpudotusta.

    Legionella — seurataan, ei ohjata

    Thermia Calibra 12 ajaa anti-legionella-syklin automaattisesti noin 14 päivän välein. Syklin aikana käyttövesi lämmitetään korkeaan lämpötilaan. Sykli näkyy selvästi datassa: kompressori pyörii täydellä teholla, käyttöveden lämpötila nousee poikkeuksellisen korkeaksi.

    Tässä järjestelmässä legionellasykli näkyy myös lisälämmittimen aktivoitumisena — mutta vain silloin. Pumppu on sopivasti mitoitettu eikä lisävastuksia tarvita normaaliolosuhteissa edes kovimmilla pakkasilla.

    Thermia Calibra 12:n firmware 17.03.145 toi mukanaan käytännön parannuksen: legionellasyklin kuumennusväli on säädettävissä suoraan pumpun näytöltä. Oletusarvo on 14 päivää, mutta liukusäätimellä voi asettaa pidemmän tai lyhyemmän välin. Samasta valikosta näkyy viimeisin ajo ja seuraavan ajon ajankohta — ja halutessa syklin voi käynnistää manuaalisesti ”Käynnistä bakteerinestotoiminto” -napista.

    EnergyHub seuraa legionellasykliä: Node-REDin State Collector johtaa tilan suoraan prioriteettirekisteristä (running_priority = 7 → m_hp_legionella_active). Alkamis- ja päättymisajat kirjataan, laskuri kasvaa. Näin nähdään jälkikäteen milloin syklejä on ajettu — ja mikä tärkeintä, käynnissä oleva legionella-ajo estää EnergyHubin oman Boost-ohjauksen.

    Mutta ulkoisesta automaatiosta sitä ei voi ohjata. Legionellasuojaus on valmistajan toteuttama turvallisuustoiminto. Välin säätö ja manuaalinen käynnistys tapahtuvat pumpun omasta paneelista — ei Modbusista.

    Huomio Modbus-osoitteistuksesta

    Thermian dokumentaatio käyttää De Facto -osoitteita jotka alkavat luvuista 40001 (holding), 30001 (input) ja 10001 (discrete input). Esimerkiksi comfort wheel on dokumentaatiossa osoitteessa 40006.

    Osa Modbus-ohjelmistoista — mukaan lukien Home Assistantin Modbus-integraatio — käyttää nollapohjaisia osoitteita. Tällöin sama rekisteri on address: 5 YAML-konfiguraatiossa.

    Käytännössä:

    Dokumentaation De FactoHA YAML addressTyyppi
    400065holding
    4002322holding
    3001615input
    10202201discrete input

    Tämä on yksi yleisimmistä Thermia-integraation virhelähteistä. Jos rekisteri ei vastaa odotetusti, ensimmäinen tarkistus on osoitemuoto — ei rekisterin sisältö.

    Miksi ON/OFF-ohjaus on väärä lähestymistapa

    Ensimmäinen ajatus lämpöpumpun optimoinnista on usein koko laitteen sähkönsyötön katkominen kontaktorilla tai älyreleellä. Käytännössä tämä on huono ratkaisu — riippumatta siitä onko kyseessä Thermia, Nibe, Daikin, Mitsubishi vai Vaillant.

    Kompressorin minimikäyntiajat rikkoutuvat pakkosammutuksessa. Sisäinen logiikka menettää tilatietonsa. Sulatus- ja käyttövesisyklit keskeytyvät kesken ajon. Käynnistysmäärät kasvavat ja jokainen käynnistys rasittaa kompressoria. Valmistajan suojauslogiikka — se joka oikeasti suojelee laitetta — ohitetaan kokonaan.

    Thermian EM3- ja Modbus-rajapinnat mahdollistavat huomattavasti elegantimman lähestymistavan. Lämpöpumpulle ei anneta käskyä ”sammu” — sen käyttäytymistä ohjataan sen omien rajojen sisällä. EVU pysäyttää kompressorin valmistajan suunnittelemalla tavalla. Comfort wheel muuttaa sisälämpötilan tavoitearvoa ilman tilakoneen rikkomista. Boost käynnistää käyttövesilämmityksen oikeaan aikaan — ja pumppu hoitaa loput itse.

    Arkkitehtuurinen johtopäätös

    Thermia-ohjauksen kokemus tiivistyy yhteen lauseeseen: maalämpöpumppua ei etäohjata — sen toimintaa ohjataan rajojen sisällä.

    Käytännössä tämä tarkoittaa kolmea toimenpidettä:

    1. EVU kalliina tunteina tai sulakevaarassa — kompressori ei käynnisty, sulakeriski pienenee.
    2. Boost halvalla tai aurinkoylijäämällä — käyttövesi lämmitetään optimaaliseen aikaan trigger-and-release -mallilla.
    3. Comfort wheel hintaohjauksella — sisälämpötilan tavoitetta nostetaan halvalla, lasketaan kalliilla. Pehmeä muutos joka ei stressaa laitetta.

    Mitä ei tehdä: kompressorin pakkokäynnistystä, jatkuvaa ON/OFF-ohjausta tai valmistajan suojausten ohittamista.

    Ja neljäs periaate, joka kirjoitushetkellä oli vasta itämässä mutta on sittemmin osoittautunut koko järjestelmän kulmakiveksi: jokainen tilanvaihto varmistetaan lukemalla toteutunut tila takaisin — releen kytkeminen ei vielä todista että pumppu on halutussa tilassa. Miten tämä periaate syntyi ja mitä sen laiminlyönnistä seurasi, käsitellään osassa 15.

    Tämä on HEOMF-ajattelun ydin lämpöpumpun kohdalla: optimointi ei tarkoita täyttä kontrollia — se tarkoittaa älykkäämpää yhteistyötä laitteen oman logiikan kanssa.

    Tarkemmat ohjeet löytyvät artikkeleista Modbus käytännössä — Home Assistantin integraatio, Thermia Modbus + EVU/Boost-ohjaus ja Käyttöveden älykäs lämmitys aurinkosähköllä.


    Seuraavaksi: Osa 13 — Sungrow + EV-lataus. Miten aurinkoinvertterin ohjaus ja sähköauton lataus toimivat yhdessä ja miten negatiiviset hinnat vaikuttavat kumpaankin.

    Piditkö artikkelista?

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

    Seuraa blogia Blogit.fi:ssä
  • Case: oma talo — Osa 11

    Konfliktit — kun kolme tavoitetta taistelee

    Tämä on yhdestoista osa sarjasta joka dokumentoi EnergyHub-järjestelmän rakentamisen vanhaan rintamamiestaloon. Edellisissä osissa rakennettiin päätöslogiikka, MQTT-viestintä ja aurinkoinvertterin ohjaus. Tässä osassa käsitellään tilannetta joka on arkkitehtuurin todellinen koetinkivi: mitä tapahtuu kun useampi tavoite haluaa samaa asiaa yhtä aikaa — tai estää toisiaan.


    Kello on 14:00. Aurinko tuottaa 11 kW. Sähkön hinta on 1,2 senttiä — halpa. EV on kytkettynä ja haluaa ladata. Lämpöpumppu on juuri käynnistynyt käyttöveden boostin. L1-vaihe on 23,8 ampeeria.

    Kolme asiaa haluaa tapahtua. Yksi pakottaa toisen pois.

    Tämä on konflikti. Ja konflikti ei ole poikkeustilanne — se on normaalitila kaikissa energiajärjestelmissä joissa on useampi ohjattava kuorma.

    Miksi konfliktit ovat vaikeita

    Yksittäinen automaatio ei näe konflikteja. Se näkee vain oman ehtonsa: ”jos hinta alle X, lataa auto.” Se ei tiedä että samaan aikaan lämpöpumppu boostaa, sulake on jo 90 %:lla ja aurinkoinvertteri on rajoitustilassa.

    Kun järjestelmässä on kolme automaatiota jotka kaikki yrittävät ohjata samaa relettä tai samaa kokonaiskulutusta, tuloksena on arvaamaton käyttäytyminen. Automaatio A sallii latauksen. Automaatio B estää sen. Automaatio C sallii sen taas. Rele flippaa minuutin välein — tai pahimmillaan sekunnin välein.

    Tätä kutsutaan control fightiksi. Se ei näy virheenä logeissa. Se näkyy vain käyttäytymisessä — satunnaisena, selittämättömänä toimintana.

    EnergyHubissa control fight on estetty arkkitehtuurisesti: yksi komponentti tekee kaikki päätökset, muut toteuttavat. Node-REDin Priority Resolver on se yksi komponentti. Se ei kilpaile muiden kanssa — ei ole muita. Konfliktia ei ratkaista siellä missä se syntyy, vaan yhdessä keskitettyssä päätöspisteessä.

    Konfliktityypit

    EnergyHubissa on kolme tyypillistä konfliktirakennetta.

    Resurssikonflikti — kaksi kuormaa haluaa samaa rajallista resurssia. Tyypillisin tapaus: EV ja lämpöpumppu haluavat molemmat ladata/boostata samaan aikaan, mutta sulake ei kestä kumpaakin. Toisen pitää väistyä.

    Tavoitekonflikti — kaksi optimointitavoitetta osoittavat eri suuntiin. Tyypillisin tapaus: hinta on halpa joten lämpöpumppu haluaa boostata (kuluta enemmän), mutta samaan aikaan aurinko ylituottaa ja invertteri on rajoitustilassa (tuotetaan vähemmän). Boostaaminen olisi järkevää — mutta rajoitustilassa se merkitsee verkkosähkön ostamista eikä aurinkosähkön käyttöä.

    Aikakonflikti — päätös on oikea nyt mutta väärä viiden minuutin päästä. Tyypillisin tapaus: EV-lataus sallitaan koska hinta on halpa, mutta aurinko on laskemassa ja viidentoista minuutin päästä verkkosähkön hinta nousee huipputunnille.

    Miten Priority Resolver ratkaisee konfliktit

    Priority Resolver ei neuvottele. Se käy tilanteen läpi kiinteässä järjestyksessä ja ensimmäinen osuma ratkaisee.

    Turvallisuus voittaa aina. Jos sulake on uhattuna, kuormat pudotetaan prioriteettijärjestyksessä riippumatta hinnasta, aurinkotilanteesta tai käyttäjän toiveista. EV menee ensin pois koska se on suurin yksittäinen kuorma. Kiuas menee toisena. Lämpöpumppu blokataan vain jos kompressori pyörii yli 60 %:lla — alle sen se saa jatkaa.

    Tämä ei ole optimointipäätös — se on fyysinen rajoite. Sulake ei jousta.

    Moodi määrittää kehyksen. Kun operating mode on peak_protection, hintaoptimointia ei tehdä lainkaan. Kun moodi on vacation, mukavuus väistyy minimikulutuksen tieltä. Moodi ei ole yksityiskohta — se on koko päätöskehys.

    Capability on portti. Ennen kuin yksikään optimointipäätös tehdään, tarkistetaan onko toimenpide ylipäätään sallittu. c_ev_charge_allowed: false sulkee EV-latauksen kokonaan riippumatta muista olosuhteista. Tämä on käyttäjän hallintapiste — ei optimoinnin asia.

    Hinta ja aurinko ovat viimeisenä. Vasta kun turvallisuus on varmistettu, moodi tarkistettu ja capabilityt selvitetty, päästään varsinaiseen optimointiin: onko hinta halpa, onko aurinkoylijäämää, kumpi kuorma hyötyy enemmän.

    Resurssikonflikti käytännössä: EV vs lämpöpumppu

    25.5.2026 klo 14:05 tilanne oli tämä:

    • L1-vaihe: 23,8 A (sulakemaksimi 25 A, varoitusraja 22,5 A)
    • EV latautui täydellä 3,6 kW teholla
    • Lämpöpumppu käynnisti käyttöveden boostiin

    Varoitusraja ylittyi heti kun lämpöpumppu käynnistyi. Safety Guardian havaitsi tilanteen 10 sekunnin sisällä ja laukaisi peak_protection-moodin.

    Priority Resolver sai tiedon moodinvaihdosta. Päätökset:

    • EV: off — suurin yksittäinen kuorma, poistuu ensin
    • HP: normal — kompressori alle 60 %, saa jatkaa

    EV:n sammuminen vapautti noin 16 ampeeria L1-vaiheelta (3,6 kW yksivaiheisena ≈ 16 A). Virta laski 16,2 ampeerin. Kun virta oli pysynyt alle 18,75 ampeerin (75 % sulakerajasta) riittävän kauan, Safety Guardian palautti normal-moodin.

    Koko tapahtuma kesti 15 minuuttia. Käyttäjä ei tehnyt mitään.

    Tavoitekonflikti: aurinko vs halpa hinta

    Tämä on hienosyisempi konflikti jota ei aina huomata.

    Tilanne: spot-hinta on 1 senttiä — halpa. Aurinko tuottaa 10 kW mutta talossa kulutus on vain 2 kW. Ylijäämä on 8 kW joka menee verkkoon. Hinta on positiivinen joten myynti on edelleen tuottoisaa — PV-rajoitusta ei aktivoida.

    Lämpöpumppu haluaa boostata koska hinta on halpa. Järjestelmä sallii sen.

    Mutta boostin käynnistyminen nostaa kulutusta 2 kW:lla. Se pienentää verkkoon menevää ylijäämää — mutta se myös tarkoittaa että aurinko kattaa boostin ilman verkkosähkön ostoa. Tulos: sama sähkölasku, lämpimämpi käyttövesi. Konflikti ratkesi luontevasti.

    Eri tilanne: spot-hinta on -0,5 senttiä — negatiivinen. PV-rajoitus on aktiivinen, invertteri rajoitettu 25 %:iin. Lämpöpumppu haluaa boostata käyttövettä.

    Tässä on tavoitekonfliktin ydin. Jos boosti tehdään nostamatta PV-rajoitusta, invertteri tuottaa edelleen vain 25 % — ja boostin tarvitsema lisäenergia ostetaan verkosta. Maksetaan verkkoon myymisestä ja ostetaan samaan aikaan verkkosähköä. Molemmat suunnat ovat tappiollisia.

    Oikea ratkaisu on säätää PV-rajoitusta boostin tarpeen mukaan. Jos lämpöpumppu tarvitsee lisää tehoa, rajoitusta nostetaan — invertteri saa tuottaa enemmän ja boosti katetaan omalla aurinkosähköllä. Tämä on sama metodi jota käytetään muulloinkin: rajoitusta säädetään dynaamisesti omakäytön perusteella, ei kiinteällä arvolla.

    EnergyHubissa Priority Resolver tekee tämän koordinoinnin: se laskee sekä PV-rajoituksen että lämpöpumpun ohjaustarpeen samassa päätössyklissä, ei erikseen. Boostin ja rajoituksen välinen tasapaino löytyy yhdessä laskennassa.

    Aikakonflikti: oikea päätös väärään aikaan

    Aikakonflikti on vaikein hallita koska se vaatii tulevaisuuden ennustamista.

    Yksinkertainen esimerkki: klo 13:55 hinta on halpa, EV-lataus sallitaan. Klo 14:00 hinta nousee kalliiseen — mutta EV on jo latauksessa eikä järjestelmä reagoi ennen seuraavaa minuuttisykliä.

    EnergyHubissa tähän ei ole täydellistä ratkaisua — ja sen myöntäminen on tärkeä osa realistista arkkitehtuurikuvausta. Minuuttisykli tarkoittaa että järjestelmä voi ”jäädä kiinni” väärään tilaan enintään minuutin ajaksi.

    Pahempi tapaus: EV-lataus aloitetaan koska tuntihinta on halpa, mutta varttihinta — joka on tullut käyttöön 2025 — on jo noussut kalliiseen. Tähän tarvitaan varttitason hintadata — ja logiikka joka osaa toimia sillä. Tuntipohjainen logiikka ei riitä kun markkina- ja mittausjakso siirtyy 15 minuuttiin.

    Toinen pahempi tapaus: aurinkoylijäämä on suuri juuri nyt, mutta pilvi on tulossa. Järjestelmä ei tiedä tätä ellei sääennustedataa integroida.

    Nämä ovat tunnettuja rajoitteita. Ne eivät estä järjestelmää toimimasta — ne rajoittavat sen optimaalisuutta reunatapauksissa.

    Ownership — kuka saa päättää

    Konfliktien hallinta ei ole vain logiikkakysymys. Se on omistajuuskysymys.

    EnergyHubissa omistajuus on eksplisiittinen: Node-RED päättää, HA toteuttaa, kenttälaitteet toimivat. Ketjussa ei ole rinnakkaisia päätöksentekijöitä. Ei ole HA-automaatiota joka ohittaa Node-REDin. Ei ole Node-RED-flow’ta joka kirjoittaa suoraan Modbus-rekisteriin ohi HA:n.

    Tämä on se asia jonka puuttuminen tekee useimmista kotiautomaatiojärjestelmistä hallitsemattomia. Kymmenen automaatiota voivat kaikki olla oikein yksin — mutta yhdessä ne luovat konflikteja joita kukaan ei suunnitellut.

    Shadow/live-mekanismi on toinen omistajuuden ilmentymä. Kun kaksi järjestelmää on käynnissä samanaikaisesti — vanha ja uusi — vain toinen saa ohjata kutakin laitetta. Raja on eksplisiittinen eikä se riipu siitä kumpi ”ehtii ensin.”

    Konfliktit ovat tietoa

    Lopuksi näkökulma joka muuttaa suhtautumisen konflikteihin.

    Konflikti ei ole virhe — se on signaali. Kun Priority Resolver estää EV-latauksen sulakeongelman takia, se kertoo jotain tärkeää: talo kuluttaa liikaa yhdellä vaiheella. Kun lämpöpumpun boosti estetään negatiivisen hinnan aikana, se kertoo että aurinkotuotanto ja kulutus eivät ole tasapainossa.

    EnergyHubin observability-kerros tallentaa nämä signaalit. InfluxDB:stä näkee jälkikäteen kuinka usein Peak Protection laukesi, milloin EV-lataus estettiin ja mistä syystä. Nämä eivät ole häiriöitä jotka pitää piilottaa — ne ovat dataa joka kertoo miten järjestelmää pitää kehittää.

    Konflikti jota ei näe ei katoa. Se vain toimii äänettömästi väärällä tavalla.


    Seuraavaksi: Osa 12 — Thermia Modbus-ohjaus. Miten maalämpöpumpun EVU- ja Boost-signaalit toimivat käytännössä ja miten ne integroidaan EnergyHubiin.

    Piditkö artikkelista?

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

    Seuraa blogia Blogit.fi:ssä