Tekijä: Juha Mujunen

  • Case: oma talo — Osa 26: Home Assistant -migraatio ei ollut varmuuskopion palautus vaan arkeologinen tutkimus

    Vanha Home Assistant oli pyörinyt vuosia Raspberry Pi:llä. Uusi Home Assistant toimii nyt osana EnergyHubia Lenovo Tinyssä Docker-kontissa, jossa Node-RED ja muut komponentit hoitavat varsinaisen energiaoptimoinnin. Kun rakensin uuden järjestelmän valmiiksi, edessä oli kysymys, joka on tutumpi kuin haluaisin myöntää: mitä tehdä vanhalle koneelle, jossa pyöri vielä kourallinen toimivia automaatioita?

    Helppo vastaus olisi ollut palauttaa vanhan HA:n varmuuskopio uudelle palvelimelle ja jatkaa siitä. Home Assistant tukee varmuuskopion palauttamista myös migraatiossa, ja tavallisessa siirrossa se voi olla aivan oikea tapa. Tässä tapauksessa sitä en kuitenkaan halunnut tehdä, koska vanha järjestelmä sisälsi myös ohjauksia ja teknistä velkaa, joita uuteen ei haluttu tuoda. Sen sijaan kävin vanhan konfiguraation läpi toiminto kerrallaan ja siirsin vain sen, mikä oli yhä tarpeen. Vanhaa Home Assistantia käytettiin lähteenä ja dokumentaationa, ei uuden järjestelmän mallina. Home Assistantin virallinen varmuuskopio- ja palautusohje kuvaa varsinaisen backup-migraation.

    Tässä artikkelissa kerron, miksi tällainen valikoiva migraatio oli tässä järjestelmässä järkevä, mitä tämäntyyppisessä siirrossa pitää huomioida ja mikä oli oma prosessini pääpiirteittäin. Lopuksi kerron, mitä vanhalle Raspberry Pi:lle tapahtuu — se ei jää eläkkeelle vaan saa uuden ja mielenkiintoisen tehtävän.

    Miksi vanhaa ei tässä tapauksessa kannattanut kloonata

    Vanha HA sisälsi kolmenlaista tavaraa sekaisin: yhä hyödyllisiä laiteintegraatioita, EnergyHubin jo korvaamia vanhoja ohjauksia sekä observability-toimintoja, jotka kannattaa rakentaa uudelleen nykyisen arkkitehtuurin päälle. Nämä kolme luokkaa vaativat kolme eri kohtelua, eikä varmuuskopion palautus osaa erottaa niitä.

    Tärkein syy olla kloonaamatta on kuitenkin ohjausvastuu. Uusi EnergyHub sisältää jo omat, kovaksi ajetut ohjauspolut sähköautolle, lämpöpumpulle, käyttövedelle ja invertterin tehonrajoitukselle. Jos vanha ohjauslogiikka tuotaisiin rinnalle, syntyisi kaksi kirjoittajaa samaan laitteeseen. Pahimmillaan tämä rikkoisi koko vastuunjakomallin, jossa jokaisella laitteella on täsmälleen yksi taho, joka päättää sen tilan. Kilpailevat ohjauspolut ovat sellainen vikaluokka, jota ei haluta rakentaa vahingossa sisään migraation nimissä.

    Toinen syy on tekninen velka. Vuosien aikana vanhaan HA:han oli kertynyt päällekkäisiä automaatioita, samoilla tai lähes samoilla ID:illä olevia toimintoja, useita versioita samoista template-sensoreista, irrallisia Python-scriptejä ja include-tiedostoja, joita ei välttämättä enää käytetty. Koko /config-hakemiston kopioiminen olisi tuonut kaiken tämän mukanaan uuteen, puhtaaseen järjestelmään.

    Mitä siirrettiin: laiteintegraatiot

    Ensimmäinen luokka on helpoin: laitteet, jotka toimivat edelleen ja joita uusi järjestelmä ei korvaa. Näitä oli käytännössä kaksi.

    Rappukäytävän WLED-valo kolmella Zigbee-painikkeella siirrettiin omaksi packageksi. Kaikilla kolmella painikkeella oli vanhassa järjestelmässä sama toiminta, joten niitä ei tarvinnut erotella fyysisen sijainnin mukaan: lyhyt painallus antaa kirkkaamman valon hetkeksi, pitkä ja kaksoispainallus ajavat valoefektit, ja päivä- ja yötila säätävät kirkkauden kellonajan mukaan. Pakettipohjainen Home Assistant -rakenne on kuvattu tarkemmin ohjeessa HA:n valmistelu integraatiokerrokseksi.

    Toinen WLED-laite on EnergyHubin fyysinen tilapaneeli — 22-segmenttinen palkki, joka näyttää lämpöpumpun, sähköauton, aurinkotuotannon, sähkön hinnan, talon kulutuksen, verkon tuonnin ja viennin sekä kiukaan ja saunan tilan yhdellä silmäyksellä. Olennaista oli, että uudessa toteutuksessa paneeli lukee mahdollisimman pitkälle EnergyHubin canonical-signaaleja eikä ylläpidä omaa rinnakkaista mittauslogiikkaansa. Kun samat tiedot ovat jo olemassa järjestelmän omina totuuslähteinä, paneelin ei pidä laskea niitä uudestaan omalla tavallaan.

    Konkreettinen löytö: WLED:n master ja segmentti 0

    Paras esimerkki siitä, miksi vanhaa logiikkaa ei pidä siirtää sokkona, tuli tilapaneelin kanssa.

    Home Assistantin WLED-integraatiossa entiteettien nimet eivät kertoneet niiden todellista merkitystä. Laitteella oli erikseen master-entiteetti ja segmentti 0, mutta nimistä sitä ei suoraan nähnyt. Aluksi tulkitsin segmentti 0:aa masteriksi. Seurauksena automaatio sytytti jatkuvasti segmentti 0:n, mikä näkyi paneelissa ylimääräisinä valkoisina LED-valoina.

    Merkitys varmistui vasta entiteettien unique_id-tunnisteista, joissa saman laitteen master ja segmentit erottuvat toisistaan tunnisteen loppuosan perusteella. Kun mapping korjattiin — master toimii masterina, segmentti 0 pidetään pois päältä ja segmentit 1–22 muodostavat varsinaisen näytön — paneeli alkoi toimia oikein.

    Tämä on koko migraation ydinopetus tiivistettynä yhteen bugiin: HA:n näyttämä nimi ei aina yksin kerro entiteetin todellista merkitystä, ja toiminnallinen testaus oli tärkeämpää kuin se, että YAML meni config-checkistä läpi. Konfiguraatio oli koko ajan validia. Se vain ohjasi väärää asiaa.

    Mitä ei siirretty: EnergyHubin jo korvaamat ohjaukset

    Toinen luokka on vanhat ohjaukset, jotka uusi järjestelmä tekee jo paremmin. Nämä jätettiin tietoisesti siirtämättä.

    Vanhassa HA:ssa oli runsaasti sähköauton logiikkaa: aurinkolatausta, halpojen tuntien latausta, yölatausta, releohjausta, pääsulakesuojaa, watchdogeja ja manuaaliohituksia. Uusi EnergyHub sisältää jo nämä omina, testattuina kerroksinaan. Sama koskee lämpöpumpun vanhaa hintasääntöpohjaista comfort wheel -optimointia, käyttöveden aurinko- ja hintaperusteista boostia sekä invertterin oman tehonrajoituslogiikan. Kaikissa näissä pätee sama periaate: vanhan automaation kopiointi loisi toisen kirjoittajan samaan laitteeseen, ja yksinkertaisen vanhan säännön palauttaminen kehittyneemmän tilalle olisi askel taaksepäin.

    Tähän luokkaan kuuluvat myös viihde- ja kokeilutoiminnot — mediasoitinohjaukset, juhlapyhäautomaatiot ja erilaiset valoefektit, joita ei enää käytetä. Jos niitä joskus tarvitaan, ne rakennetaan puhtaasti uuden HA:n nykyisillä integraatioilla. Tavoitteena ei ole säilyttää historiallista konfiguraatiota vain siksi, että se on joskus tehty.

    Mitä rakennetaan uudelleen: observability

    Kolmas luokka on kiinnostavin. Vanhassa HA:ssa oli hyödyllisiä seuranta- ja diagnostiikkatoimintoja, joita ei kannata kopioida sellaisenaan mutta jotka kannattaa rakentaa uudelleen nykyisen datan päälle. Näiden yhteinen piirre on, että ne ovat read-only: ne eivät ohjaa mitään, joten ne eivät voi joutua ristiriitaan EnergyHubin ohjauspolkujen kanssa.

    Kärkiaihe on aurinkotuotannon rahallinen hyöty. Vanha HA laski jo yksinkertaista eurolaskentaa — paljonko tuotettiin, paljonko vietiin verkkoon, paljonko käytettiin itse ja mikä oli hinta kulloinkin. Uudessa EnergyHubissa on parempi mittausdata, InfluxDB ja varttihinnoittelu, joten sama asia kannattaa rakentaa kunnolliseksi aikajaksokohtaiseksi historiatoiminnoksi eikä kääntää vanhaa versiota.

    Muut uudelleenrakennettavat ovat lämpöpumpun huolto- ja diagnostiikkatietoja: legionella-ajojen historia, kompressorin ja lisälämmittimen käyttötunnit, lämmitys- ja liuospiirin ΔT:t sekä demand- ja prioriteettidiagnostiikka. Näistä tulee oma read-only maintenance/diagnostics-kerros. Sungrown MPPT- ja DC-diagnostiikka voidaan rakentaa kevyesti template-sensoreina, koska raakadata on jo olemassa eikä uusia Modbus-lukuja tarvita.

    Tässä luokassa piili myös hyödyllinen varoitus. Vanha ”COP”-sensori oli nimetty harhaanjohtavasti — se laski todellisuudessa kahden lämpötilaeron suhdetta, ei lämpöpumpun hyötysuhdetta lainkaan. Juuri tällaista sensoria ei pidä siirtää nimen perusteella. Jos COP-laskenta rakennetaan, sen on perustuttava tuotettuun lämpötehoon suhteessa todelliseen sähkötehoon. Vanhan järjestelmän läpikäynti on siis myös tilaisuus korjata vanhat virheet sen sijaan, että ne monistettaisiin eteenpäin.

    Mitä tämäntyyppisessä siirrossa pitää huomioida

    Kokemuksesta tiivistettynä muutama asia toistuu jokaisessa vastaavassa migraatiossa.

    Siirrä toiminto kerrallaan, älä koko konfiguraatiota. Jokainen siirretty toiminto on oma pieni, testattava ja palautettava kokonaisuutensa. LED-migraatio esimerkiksi vietiin Gitiin omana rajattuna committinaan, joka sisälsi vain kaksi package-tiedostoa. Muut samaan aikaan keskeneräiset EnergyHub-muutokset jätettiin koskematta. Näin jokaisen muutoksen vaikutus on erikseen todennettavissa ja tarvittaessa peruttavissa.

    Testaa fyysisesti, älä vain konfiguraation validoinnilla. Config-check ei löydä väärää entiteettimappausta. Segmentti 0 -bugi paljastui vasta katsomalla, mitä paneeli oikeasti näytti. Toiminnallinen testi migraation jälkeen on ainoa tapa varmistua, että siirretty toiminto tekee sitä mitä pitääkin.

    Sammuta vanha järjestelmä testin ajaksi. Vanha Raspberry Pi -HA sammutettiin kokonaan, jotta se ei voinut enää kirjoittaa samoihin WLED-laitteisiin. Tämä oli olennaista: kahden Home Assistantin rinnakkainen ohjaus olisi tehnyt vikojen lähteen tunnistamisesta hyvin vaikeaa. Kun vanha HA oli poissa, uusi oli ainoa ohjaaja ja segmentti 0:n ongelma voitiin jäljittää yksiselitteisesti uuden järjestelmän automaatioon. Sama periaate koskee mitä tahansa laitetta, jota kaksi järjestelmää voisi ohjata yhtä aikaa.

    Käsittele vanha konfiguraatio arkistona, joka voi sisältää salaisuuksia. Vanhojen konfiguraatiotiedostojen seassa on tyypillisesti tunnistetietoja selväkielisenä — vanhoja API-avaimia, käyttäjätunnuksia ja tokeneita jääneistä integraatioista. Näitä ei pidä koskaan siirtää uuteen järjestelmään eikä julkaista blogin liitteenä tai Git-repositorioon. Vanha vientipaketti kannattaa säilyttää salaisuuksia sisältävänä arkistona ja kohdella sitä sen mukaisesti.

    Erottele ohjaus ja seuranta. Se, mikä ohjaa laitetta, on siirrossa vaarallista, koska se voi joutua ristiriitaan uuden järjestelmän kanssa. Se, mikä vain lukee ja näyttää, on turvallista. Kun toiminnot lajitellaan tämän mukaan, iso osa päätöksistä tekee itsensä: ohjaukset korvataan tai jätetään, seuranta rakennetaan uudelleen.

    Vanha Raspberry Pi saa uuden tehtävän

    Alkuperäinen syy koko urakkaan oli, että vanhassa HA:ssa oli enää vähän toimintoja käytössä — ja sen Raspberry Pi haluttiin vapauttaa uuteen käyttöön. Kun tarpeelliset toiminnot on siirretty ja loput jätetty tietoisesti taakse, kone vapautuu tehtävään, johon se sopii itse asiassa erinomaisesti: ulkopuoliseksi järjestelmävahdiksi.

    Ajatus on yksinkertainen ja se korjaa yhden EnergyHubin rakenteellisen heikkouden. Tällä hetkellä järjestelmän oma valvontakerros — watchdogit, System Health -näkymä ja muut terveystarkastukset — pyörii samalla Lenovo Tinyllä kuin valvottava järjestelmä itse. Jos kone, sen verkko tai Docker-ympäristö kaatuu, myös valvonta kaatuu sen mukana. Silloin hiljaisuus voi tarkoittaa joko sitä, että kaikki on kunnossa, tai sitä, ettei kukaan ole enää kertomassa ettei ole. Sisäinen valvonta ei voi luotettavasti valvoa omaa alustaansa.

    Erillinen Raspberry Pi ratkaisee tämän, koska se on riippumaton solmu. Se voidaan rakentaa tarkkailemaan EnergyHub-palvelinta ulkopuolelta: vastaako se, päivittyvätkö sen julkaisemat signaalit, pyörivätkö valvottavat palvelut ja onko sen kellonaika kunnossa. Koska vahti on erillisellä raudalla, erillisellä virransyötöllä ja mahdollisesti erillisellä verkkoyhteydellä, se pystyy hälyttämään juuri siinä tilanteessa, jossa itse pääjärjestelmä on pudonnut kokonaan pois — eli juuri silloin kun hälytystä eniten tarvitaan.

    Tämä on myös hyvä esimerkki siitä, ettei ”eläkkeelle jäävä” laite ole automaattisesti hyödytön. Raspberry Pi:tä ei enää ollut järkevää käyttää koko EnergyHubin alustana, mutta juuri sen keveys ja erillisyys tekevät siitä hyvän riippumattoman vahdin. Ulkopuolisen järjestelmävahdin rakentaminen on oma projektinsa, ja palaan siihen myöhemmin omassa artikkelissaan.

    Yhteenveto

    Home Assistant -migraatio ei ollut varmuuskopion palautus vaan järjestelmän arkeologinen tutkimus. Vanhasta HA:sta erotettiin kolme asiaa: edelleen hyödylliset laiteintegraatiot, jotka siirrettiin; EnergyHubin jo korvaamat vanhat ohjaukset, jotka jätettiin tietoisesti taakse; ja observability-toiminnot, jotka kannattaa rakentaa uudelleen nykyisen arkkitehtuurin päälle.

    Tähän mennessä valmiina ovat rappukäytävän WLED-kokonaisuus painikkeineen ja EnergyHubin 22-segmenttinen tilapaneeli. Migraatio vietiin Gitiin rajattuna muutoksena, ja vanha HA voidaan pitää sammutettuna näiden toimintojen osalta. Jatkokehitykseen jäivät lämpöpumpun huolto- ja diagnostiikkakerros, aurinkotuotannon €-hyödyn uudelleentoteutus Influx-pohjaisesti, Sungrown MPPT-diagnostiikka ja talokuorman suodatetut UI-signaalit. Niiden toteutus kuuluu sarjan myöhempiin vaiheisiin eikä tämän migraation onnistumisen ehdoksi.

    Isoin oppi on kuitenkin metodinen. Vanha järjestelmä kannattaa kohdata tutkittavana kohteena, ei mallina, jota jäljitellään. Silloin migraatiosta tulee tilaisuus siivota, korjata ja ymmärtää — ei vain tapa raahata vuosien tekninen velka mukanaan uuteen ympäristöön.


    Seuraavassa osassa siirryn yksittäisten toimintojen migraatiosta koko alustan kestävyyteen. Uusi palvelin hoitaa nyt ohjauksen ja valvonnan, mutta se pyörii itse verkkosähkön varassa — ja seuraava osa kertoo, mitä tapahtui kun kytkin sen UPS:n taakse. Matkalla selviää, ettei UPS ole pelkkä akku vaan ohjattava laite, johon ei kannata luottaa sokeasti, ja miksi en vielä antanut sen sammuttaa palvelinta automaattisesti.

  • Case: oma talo — Osa 25: Sama kysymys, eri taso: paljonko rahaa liikkui juuri siinä vartissa

    Edellisessä osassa rakensin yhden rivin, joka kertoo totuuden siitä, mitä lämpöpumpun ohjauksessa oikeasti tapahtui: yhteen näytteeseen ankkuroitu, jälkikäteen auditoitava rivi, jossa komento, lupa ja fyysinen reletila ovat oikein ajallisesti kohdistettuina. Lopetin sen osan lupaukseen, että seuraava askel ei ole uutta ohjauslogiikkaa vaan datan laadun todistaminen — ja että vasta sen jälkeen voi rakentaa ylemmän kerroksen.

    Tämä osa on eräänlainen sisarkappale sille. Se ei jatka lämmitysdatan tarinaa eteenpäin — se kysyy saman kysymyksen kokonaan toiselta puolelta taloa. Lämpöpumppupuolella kysymys oli ”mitä ohjauksessa todella tapahtui silloin”. Nyt kysymys on: mitä energiaa ja rahaa oikeasti liikkui juuri siinä vartissa.

    Ja kävi ilmi, että se on täsmälleen sama ongelma. Eri suureet, eri lähteet, mutta sama tapa, jolla data valehtelee onnistuneelta näyttäessään — ja sama kuri, jolla se pakotetaan kertomaan totuus.

    Miksi vanha aurinkolaskuri ei riittänyt

    Talossa on jo pitkään ollut yksinkertainen laskuri, joka kertoo aurinkotuotannon hyödyn: paljonko tuotannosta käytettiin itse, paljonko myytiin verkkoon, mitä se euroissa tarkoittaa. Se on ollut hyödyllinen, mutta se laskee tuntitasolla ja käyttää kovakoodattuja hinnan osia, ja se vastaa vain yhteen kysymykseen: mikä oli aurinkopaneelien hyöty.

    Halusin jotain isompaa ja rehellisempää. En vain kolmea aurinkohyötysensoria, vaan kokonaisen energiatalouden kirjanpidon, jossa jokainen vartti on oma auditoitava rivinsä ja jossa lukee rinnakkain: paljonko talo kulutti, paljonko aurinkoa tuotettiin, paljonko siitä käytettiin itse, paljonko ostettiin, paljonko myytiin, mikä oli vartin todellinen ostohinta, mikä myynnin arvo — ja ratkaisevasti, mikä osa laskennasta on täydellinen ja mikä jää tietoisesti puutteelliseksi.

    Se viimeinen on koko idean ydin. Vanha laskuri antaa aina jonkin luvun. Uusi saa sanoa ”en tiedä” — ja sen pitää sanoa se ääneen sen sijaan, että täyttäisi aukon nollalla tai edellisellä arvolla.

    Tässä on sama sähkön varttihinnoittelun logiikka kuin ohjauspuolellakin: kallis vartti ja halpa vartti eivät saa sekoittua tunnin keskiarvoon. Jos haluaa ymmärtää, mitä aurinkovoimala oikeasti säästi, sen on tapahduttava vartin tarkkuudella, samassa rytmissä kuin muukin järjestelmän ohjaus.

    Ensin matematiikka, sitten vasta mitään muuta

    Rakensin tämän samalla vaiheistuksella kuin lämmityksen auditrivin, ja järjestys oli tarkoituksellinen. Ensin puhdas laskentalogiikka ilman mitään yhteyttä ulkomaailmaan. Sitten lähdeadapterit, jotka kääntävät järjestelmän raakadatan laskennan ymmärtämään muotoon. Sitten puhdas laskentamoottori. Vasta sitten tuotannon lukukomponentti, joka oikeasti hakee dataa. Ja vasta aivan viimeisenä se, joka liittää kaiken tuotannon Node-REDiin.

    Kanonista kirjoittajaa — sitä, joka joskus tallentaa nämä vartit pysyvästi järjestelmän viralliseksi totuudeksi — ei ole vielä olemassa. Se on tarkoituksellista. Sitä ei rakenneta ennen kuin tämä koko ketju on todistettu toimivaksi oikealla datalla.

    Miksi näin verkkaisesti? Koska laskennan matematiikka on juuri se kohta, jonka on oltava horjumaton ennen kuin sen päälle rakentaa mitään. Vartin energiatase ilman akkua on yksinkertainen: kulutus on tuotanto miinus vienti plus tuonti. Oma käyttö on se osa tuotannosta, joka ei mennyt verkkoon. Mutta yksinkertaisen kaavan ympärille tarvitaan joukko sääntöjä, jotka eivät ole yhtään neuvoteltavissa: puuttuva arvo ei ole nolla, puuttuva lähde tekee koko vartista epätäydellisen, nollalla ei jaeta, ja — tämä on tärkeä — negatiivinen myyntihinta on sallittu. Kun sähköstä maksetaan verkkoon vietäessä, vienti maksaa rahaa eikä tuota sitä, ja laskennan on kestettävä se ilman että se pitää sitä virheenä.

    Nämä säännöt kirjoitettiin ensin, testattiin kymmenillä keksityillä rajatapauksilla, ja lukittiin. Vasta sitten annettiin oikean datan tulla lähelle.

    Arvonlisävero lisätään täsmälleen kerran

    Yksi hintalaskennan periaatteista ansaitsee tulla sanotuksi erikseen, koska se on juuri sellainen kohta, jossa hiljainen tuplaus olisi helppo. Ostohinta rakentuu spot-hinnasta, johon lisätään arvonlisävero, marginaali, siirto ja verot. Nordpool antaa spotin arvonlisäverottomana. Arvonlisäkerroin on lisättävä täsmälleen kerran — ei kertaakaan, ei kahdesti.

    Tämä kuulostaa itsestäänselvältä, mutta järjestelmässä on useita paikkoja, joissa hinta esiintyy eri muodoissa, ja joissakin niistä arvonlisä on jo mukana. Jos laskenta ottaisi hinnan väärästä lähteestä, se joko unohtaisi veron tai lisäisi sen kahdesti — ja kumpikin näyttäisi täysin uskottavalta luvulta. Ratkaisu oli tehdä hinnan alkuperästä nimenomainen osa dataa: laskenta ei ota ”jotain hintaa”, vaan se ottaa nimenomaisesti arvonlisäverottoman spotin ja lisää veron itse, yhden kerran, dokumentoidusti. Ja se kieltäytyy hyväksymästä vanhoja ”valmiiksi paketoituja” hintasensoreita kanonisena lähteenä, koska niiden alkuperä on hämärä.

    Hankalin osa ei ollut laskenta vaan se, milloin dataa saa

    Tähän asti tarina kuulostaa siistiltä. Mutta kuten aina, oikea data opetti jotain, mitä en osannut suunnitteluvaiheessa arvata — ja tällä kertaa opetus muutti koko arkkitehtuurin ajoituksen.

    Alkuperäinen ajatus oli yksinkertainen: kun vartti sulkeutuu, laske se. Jos laskenta epäonnistuu, yritä hetken päästä uudelleen. Se on intuitiivinen malli, ja se on väärä.

    Kun tutkin seitsemän vuorokauden todellista sähkömittarin dataa, paljastui jotain, mitä en ollut odottanut. Sähkömittari ei raportoi arvojaan säännöllisin väliajoin vaan silloin, kun ne muuttuvat — ja verkosta ostetun ja verkkoon myydyn energian laskurit voivat olla pitkiä aikoja hiljaa. Jotta vartin rajan energiamäärän voi laskea, tarvitaan mittarilukema molemmilta puolilta vartin rajaa. Ja jos mittari on ollut hiljaa rajan yli, sitä lukemaa ei yksinkertaisesti ole vielä olemassa sillä hetkellä, kun vartti sulkeutuu.

    Kuinka pitkään se voi olla hiljaa? Datassa mediaani siihen, että vartti oli täydellisesti laskettavissa molempiin suuntiin, oli noin kaksi ja puoli tuntia vartin sulkeutumisesta. Yhdeksänkymmenennessä persentiilissä yli kahdeksan tuntia. Ja pahimmillaan yli yksitoista tuntia. Vasta kahdentoista tunnin kohdalla sata prosenttia vartteista oli laskettavissa.

    Tämä on juuri sitä lajia löytöä, joka on koko sarjan toistuva teema. Mikään ei ollut rikki. Data oli oikein. Laskenta oli oikein. Mutta laskenta oli väärässä maailmassa ajassa: se yritti laskea vartin, jonka totuutta ei vielä ollut olemassa. ”Yritä heti uudelleen” olisi tuottanut pitkän jonon epätäydellisiä vartteja, jotka olisivat täydentyneet vasta tunteja myöhemmin — ja välissä syntynyt kohina olisi näyttänyt vialta, vaikka se oli vain kärsimättömyyttä.

    Ratkaisu oli lakata kysymästä liian aikaisin

    Kun mittasin, miten myöhään data oikeasti saapuu, arkkitehtuuri muuttui itsestään. Sen sijaan että vartti laskettaisiin heti sen sulkeuduttua, se lasketaan vasta kahdentoista tunnin kuluttua — silloin valtaosa dataa on jo saapunut. Ja sama vartti lasketaan uudelleen kahdenkymmenenneljän tunnin kohdalla erillisenä varmennuksena, jolloin sata prosenttia datasta on varmasti paikalla.

    Tärkeää on, että tämä toinen ajokerta ei ole ”korjaus”. Se laskee täsmälleen saman vartin — saman aikaidentiteetin — uudelleen, ei uutta väliä. Jos jokin oli kahdentoista tunnin kohdalla vielä epätäydellistä, kahdenkymmenenneljän tunnin ajo joko täydentää sen tai vahvistaa, että se jää pysyvästi epätäydelliseksi. Puuttuva pysyy puuttuvana, epätäydellinen pysyy epätäydellisenä. Mikään ei täytä aukkoa arvauksella, eikä mikään kanna vanhaa mittarilukemaa keinotekoisesti eteenpäin.

    Tämä on sama opetus kuin lämmityspuolella, vain toisin päin. Siellä opetus oli, ettei riviä saa rakentaa ”viimeisimmästä tunnetusta arvosta”, koska se sekoittaa eri hetkiä. Täällä opetus on, ettei varttia saa laskea liian aikaisin, koska silloin ”viimeisin tunnettu arvo” ei vielä ole se, mikä siitä lopulta tulee. Molemmissa juuri aika on se, joka valehtelee, jos sitä ei kohtele mittauksen osana.

    Hintakin osoittautui monimutkaisemmaksi kuin luku

    Ennen kuin lukitsin hinnan, tutkin kuudenkymmenen vuorokauden hintadataa. Odotin yksinkertaista aikasarjaa. Sain sen sijaan pienen kokoelman ristiriitoja: joukossa oli vartteja, joille löytyi kaksi eri hintaa, ja osalle näistä kaksi eri arvoa. Kaikki ristiriidat osuivat samaan kohtaan — paikalliseen keskiyöhön, vuorokauden vaihteeseen.

    Tämä olisi voinut jäädä huomaamatta ja myrkyttää hiljaa hintalaskennan juuri vuorokauden rajalla. Ratkaisu oli olla luottamatta yhteen hintalähteeseen sokeasti. Järjestelmässä on kaksi tapaa tietää vartin hinta: hetkellinen hintahavainto ja päivän hinta-aikataulu, jonka Nordpool julkaisee etukäteen. Lukittu sääntö vaatii, että molemmat ovat olemassa ja että hetkellinen havainto vastaa aikataulun arvoa. Jos hetkellisiä havaintoja on ristiriitaisia, aikataulu ratkaisee, kumpi on oikea — ja ristiriita jää näkyviin todistusaineistoksi. Jos vain toinen on olemassa, vartti jää epätäydelliseksi. Hintaa ei koskaan kanneta edelliseltä vartilta eteenpäin.

    Tämä on sama periaate kuin readbackin todentaminen ohjauspuolella: yksi lähde ei riitä todistamaan totuutta, kun toinenkin on saatavilla.

    Kirjoitusoikeus ei ole taaskaan lukuoikeus

    Kun rakensin tuotannon lukukomponentin, tuli vastaan vanha tuttu. Järjestelmässä oli ennestään tietokantatunnus, jonka luulin voivani käyttää lukemiseen. Se osoittautui käytännössä pelkäksi kirjoitustunnukseksi: sillä pystyi kirjoittamaan, mutta kysely samalla tunnuksella palautti ”ei löydy”. Sama opetus kuin läpi koko sarjan — onnistunut kirjoitus ei ole sama asia kuin luettavissa oleva data, ja niitä ei saa todistaa samalla oikeudella.

    Ratkaisu oli minimioikeusperiaatteen mukainen: en laajentanut vanhaa tunnusta, vaan loin erillisen, pelkän lukuoikeuden tietokannan siihen yhteen säiliöön, josta laskenta tarvitsee dataa. Se todennettiin molempiin suuntiin: kysely toimii, mutta kirjoitusyritys sillä tunnuksella torjutaan. Nyt lukukomponentti ei rakenteellisesti pysty kirjoittamaan mitään, vaikka joku yrittäisi käskeä sitä siihen.

    Havaitseva, ei ohjaava — ja se on lukittu monessa kohdassa

    Koko tämä kerros on tarkoituksella pelkkä tarkkailija. Se lukee dataa, laskee vartin, ja julkaisee tuloksen kahteen tarkkailukanavaan. Se ei kirjoita järjestelmän viralliseen totuuteen, ei muuta olemassa olevia vartteja, ei koske sähköauton tai lämpöpumpun ohjaukseen, ei releisiin, ei tariffeihin.

    En luottanut siihen, että ”havaitseva” olisi turvassa vain siksi, että niin oli tarkoitus. Se on lukittu useassa kohdassa peräkkäin. Laskentamoottorin tulos kantaa aina kahta lippua, jotka sanovat ”vain havaitseva” ja ”kanoninen kirjoittaja pois päältä” — eikä mikään syöte voi kääntää niitä. Se komponentti, joka päättää julkaistaanko vartti, tarkistaa nuo liput ja kieltäytyy julkaisemasta mitään, jos ne eivät ole kohdallaan. Sallittuja ulostuloja on täsmälleen kaksi, molemmat tarkkailukanavia. Ja lukuoikeus tietokantaan on fyysisesti pelkkä lukuoikeus. Mikä tahansa näistä yksinään riittäisi; yhdessä ne tekevät ohjaavasta toiminnasta rakenteellisesti mahdotonta tässä vaiheessa.

    Miten tämä vietiin tuotantoon — todistaen, ei kokeillen

    Kuten lämmityspuolellakin, suurin osa työstä ei ollut koodin kirjoittamista vaan sen todistamista, ettei koodi tee mitään muuta kuin mitä sen piti. Muutos tuotannon ohjelmavuohon rakennettiin niin, että kone tuotti sen automaattisesti annetusta lähtötilasta, ja riippumaton tarkistus todensi, että lopputulos oli tavulleen sama kuin katselmoitu — ja että se koski täsmälleen kahdeksaa uutta palasta eikä muuttanut yhtäkään vanhaa.

    Ennen kuin sitä vietiin liveen, se ajettiin nimenomaan tuotannon oman ohjelmistoversion päällä — koska edellinen osa opetti kalliisti, että ”testit menivät läpi” ja ”toimii tuotannossa” ovat kaksi eri väitettä. Ja itse käyttöönotto tehtiin niin, että vanha tila varmuuskopioitiin ensin, muutos vietiin sisään atomisesti, ja järjestelmä olisi palautunut itsestään edelliseen tilaan, jos mikä tahansa tarkistus olisi pettänyt. Käyttöönoton jälkeen todennettiin, että tuotannossa on täsmälleen odotettu tila, ei mitään enempää.

    Missä mennään nyt — ja mikä on rehellisesti vielä auki

    Sanon tämän yhtä suoraan kuin edellisenkin osan lopussa, koska muuten lupaisin enemmän kuin on totta.

    Se, mikä nyt on tuotannossa, on havaitseva talouskerros. Se on livenä, mutta se on tuoretta, ja se on tarkoituksella hiljainen aivan lähipäiviin asti — ensimmäinen sallittu vartti alkaa vasta erikseen asetetusta rajapäivästä, jotta laskenta ei koskaan väitä laskeneensa dataa ajalta, jolta sillä ei ole täyttä kuvaa. Ensimmäinen oikea live-vartti, sen kahdentoista tunnin tulos ja sen kahdenkymmenenneljän tunnin varmennus ovat vasta edessä. Ja jos ensimmäinen tulos on epätäydellinen, se ei ole automaattisesti vika — sähkömittarin datan hidas saapuminen todistettiin nimenomaan siksi, että epätäydellisyys osataan lukea odotettuna eikä virheenä.

    Vasta kun havaitsevasta kerroksesta on kertynyt riittävästi live-dataa, seuraavat askeleet ovat erillisiä päätöksiä: uuden laskennan vertaaminen vanhaan rinnakkain usean päivän ajalta, ja vasta sen jälkeen — jos vertailu kestää tarkastelun — se kanoninen kirjoittaja, joka tekee näistä varteista järjestelmän virallisen totuuden. Sitä ei rakenneta ennen kuin havaitseva vaihe on hyväksytty. Ja kun se joskus rakennetaan, sen on perittävä täsmälleen samat periaatteet: yksi kanoninen aika, tarkka lähde, näkyvä alkuperä, puuttuva ei ole nolla, kirjoituksen onnistuminen todennetaan lukemalla, eikä historian merkitys saa muuttua jälkikäteen.

    Sama laki, kaksi eri kysymystä

    Kun katson lämmityksen auditriviä ja tätä talouskerrosta rinnakkain, ne näyttävät samalta ongelmalta eri tasolla. Toinen kysyy, mitä ohjauksessa todella tapahtui juuri sillä hetkellä. Toinen kysyy, mitä energiaa ja rahaa oikeasti liikkui juuri siinä vartissa. Molemmissa vastaus rakentuu samoista laeista: yksi kanoninen aika, tarkka lähde, näkyvä alkuperä, ja ennen kaikkea se, että järjestelmä saa — ja sen täytyy — sanoa ”en tiedä” silloin kun se ei tiedä, sen sijaan että keksisi uskottavan luvun.

    Se on hitaampaa kuin ”laske ja katso, näyttääkö oikealta”. Mutta järjestelmässä, jonka datan varaan on tarkoitus joskus rakentaa malli, hitaus oikeassa kohdassa on halvempaa kuin nopeus. Ja tässä tapauksessa hitaus ei ollut edes valinta vaan löydös: oikea data kertoi, ettei totuutta yksinkertaisesti ole olemassa vielä silloin, kun sitä ensin haluaisi kysyä. Suurin muutos tässä koko työssä ei ollut rivi koodia. Se oli sen myöntäminen, milloin on liian aikaista kysyä.


    Seuraavaksi vanhan Home Assistantin siirto uuteen järjestelmään — mutta ei varmuuskopion palautuksena vaan arkeologisena tutkimuksena. Vanha konfiguraatio käytiin läpi toiminto kerrallaan ja siirrettiin vain se, mikä oli yhä tarpeen: laiteintegraatiot siirrettiin, EnergyHubin jo korvaamat ohjaukset jätettiin tietoisesti taakse, ja seurantatoiminnot rakennetaan uudelleen nykyisen arkkitehtuurin päälle. Mukana muun muassa se, miksi vanhaa ohjauslogiikkaa ei pidä kloonata sokkona — ja mikä uusi tehtävä vanhalle Raspberry Pi:lle lopulta löytyi.

  • Case: oma talo — Osa 24:Yksi rivi, joka kertoo totuuden: EnergyHub rakentaa jäljitettävän ohjaushistorian

    Osassa 22 lupasin, että tästä tulee oma jatko-osansa: miten yhdestä lämpöpumpun näytteestä rakennetaan jälkikäteen auditoitava rivi, jossa komento, lupa ja fyysinen reletila ovat oikein ajallisesti kohdistettuja. Se lupaus on nyt lunastettu. Tämä osa kertoo siitä työstä — ja siitä, että se osoittautui hankalammaksi kuin uskoin, ei siksi että logiikka olisi monimutkaista, vaan siksi että data valehtelee monella tavalla onnistuneelta näyttäessään.

    Sanon heti alkuun kaksi asiaa suoraan, koska muuten tämä artikkeli lupaisi enemmän kuin on totta. Ensinnäkin: se mikä nyt on valmis, on raaka- ja auditkerros, ei vielä laatusuodatettu tilakerros. Osassa 22 kuvasin kaksikerrosmallin — raaka säilyttää haamut, tila suodattaa ne pois — ja tämä työ rakensi nimenomaan sen alemman kerroksen, todistusaineiston. Se ylempi kerros, joka luokittelee havainnot käyttökelpoisiksi ja erottaa lämmitys-, käyttövesi- ja jälkilämpöjaksot toisistaan, on yhä edessä. Toiseksi: tämä rivi on juuri saatu tuotantoon ja live-todennettua, mutta se on tuoretta. Vakautus- ja hyväksyntävaihe on vielä käynnissä. En kirjoita tässä valmiista, vuosia hiotusta ominaisuudesta vaan työstä, joka juuri ylitti maaliviivan.

    Kirjoitan silti nyt, koska tämän työn opetukset ovat kiinnostavampia kuin sen lopputila — ja koska yksi niistä opetuksista on paras esimerkki koko sarjassa siitä, miten uskottava data voi olla täysin väärä.

    Miksi tarvitaan erillinen rivi siitä, mitä oikeasti tapahtui

    Palataan siihen, mikä ongelma tässä ylipäätään ratkaistaan. EnergyHub tekee jatkuvasti ohjauspäätöksiä: lämpöpumppu saa boostata, lämmitys estetään kalliin hinnan ajaksi, mukavuustavoitetta lasketaan. Jokainen päätös kulkee monen portin läpi — prioriteettilogiikka päättää halutun tilan, lupakerros tarkistaa saako sen tehdä, ja lopulta fyysinen rele joko kytkee tai ei.

    Ongelma on, että näiden välissä voi mennä pieleen mitä tahansa, eikä siitä jää jälkeä. Final gate saattoi komentaa ”estä”, mutta lupa oli vanhentunut. Rele sai käskyn, mutta raportoi tilansa sekunteja myöhemmin. Jos haluaa myöhemmin ymmärtää, miksi talo käyttäytyi tiettynä pakkasaamuna niin kuin käyttäytyi, tarvitaan yksi rivi jokaisesta päätöshetkestä, jossa lukee rinnakkain: mitä komennettiin, mitä lupa salli, ja mitä releet fyysisesti tekivät — kaikki sidottuna samaan, oikeaan hetkeen.

    Tämä ei ole mukava-jos-on-aikaa -ominaisuus. Se on ehto sille, että myöhempään ennustepohjaiseen lämmitykseen voi ylipäätään luottaa. Malli, joka katsoo säätä ja hintoja ja päättää lämmitysstrategian, on täsmälleen niin hyvä kuin se historia, jonka varaan se rakennetaan. Ja jos se historia on rakennettu arvauksista siitä, mitä luultavasti tapahtui, malli oppii talven väärin eikä koskaan kerro tehneensä niin.

    Rivi ankkuroidaan yhteen näytteeseen, ei ”viimeisimpään tunnettuun arvoon”

    Ensimmäinen ja tärkein arkkitehtoninen päätös oli, mihin rivi ajallisesti kiinnitetään. Helppo ja väärä tapa olisi rakentaa rivi säännöllisin väliajoin ja täyttää se kunkin lähteen viimeisimmällä tunnetulla arvolla — lämpöpumpun tila tästä, releen tila tuosta, lupa tuolta, kaikki ”sen hetken parhaan tiedon mukaan”.

    Se olisi väärin, koska eri lähteet päivittyvät eri tahtiin, ja ”viimeisin tunnettu arvo” tarkoittaa käytännössä, että rivi sekoittaa eri hetkien tietoja yhteen ja väittää niiden kuuluvan yhteen. Se on sama virhe, joka opittiin kalliisti jo aiemmin: päätös vanhalla datalla on väärä päätös, vaikka data olisi joskus ollut oikea.

    Ratkaisu oli tehdä lämpöpumpun näytteestä ankkuri. Jokainen rivi syntyy täsmälleen yhdestä lämpöpumpun mittausviestistä, ja sen viestin oma aikaleima on rivin kanoninen hetki. Kaikki muu — releiden tila, lupa, final gaten päätös — haetaan suhteessa siihen hetkeen, ei ”nyt”-hetkeen. Jos ankkurinäytettä ei ole, riviä ei synny lainkaan; hylätty näyte menee omaan kanavaansa näkyvänä hylkäyksenä, ei tyhjänä rivinä.

    Tämä kuulostaa itsestäänselvältä kirjoitettuna, mutta se kääntää koko rakennuslogiikan päälaelleen. Rivi ei kysy ”mitä tiedän nyt”, vaan ”mikä oli totta silloin kun tämä näyte otettiin”. Ja koska mikään lähde ei ole täydellinen, jokaisella liitoksella on oma statuksensa: löytyikö kelvollinen releen tila 120 sekunnin sisällä ankkurihetkestä, oliko final gaten snapshot tuore vai vanhentunut, vai puuttuiko se kokonaan. Rivi ei koskaan teeskentele täydellisyyttä, jota sillä ei ole.

    Sama sana kolmessa eri merkityksessä

    Yksi työn hankalimmista kohdista ei ollut lainkaan tekninen vaan käsitteellinen: sana ”comfort” tarkoitti koodissa kolmea täysin eri asiaa, ja ne oli erotettava, ettei väärää poisteta.

    Ensimmäinen oli lämpöpumpun tilakomento nimeltä comfort — arvo, joka oli aikoinaan lisätty komentojen listaan mutta jota mikään ei enää oikeasti tuottanut. Prioriteettilogiikka antoi vain arvoja normaali, estä ja boost — ei koskaan comfort. Se oli siis kuollut arvo: listalla, mutta ei käytössä. Toinen oli fyysinen releiden tila, jossa tietty SG-releiden yhdistelmä tarkoittaa comfort-moodia — täysin elävä ja tarpeellinen, osa sitä mitä releet oikeasti raportoivat. Kolmas oli mukavuuspyörän lämpötilatavoite, käyttäjän Voimapirtillä asettama luku, jolla on oma täysin erillinen elinkaarensa.

    Kolme eri asiaa, jotka vain sattuvat jakamaan saman sanan. Ensimmäinen piti siivota pois — mutta vain se, ja niin että kaksi muuta jäävät koskematta. Tämä on juuri sellainen kohta, jossa hätäinen ”poista kaikki comfort-viittaukset” olisi rikkonut fyysisen tilan lukemisen tai mukavuuspyörän logiikan. Erottelu piti vahvistaa suoraan elävästä koodista molempiin suuntiin ennen kuin yhtäkään riviä muutettiin: että poistettava arvo on aidosti kuollut eikä mikään nojaa siihen, ja että kaksi säilytettävää ovat aidosti eri asioita.

    Kun kuollut arvo lopulta poistettiin, muutos oli kaksi riviä. Mutta se, että se oli turvallista tehdä, vaati enemmän varmistustyötä kuin itse muutos — ja se on tämän koko projektin toistuva teema. Pieni muutos on halpa kirjoittaa ja kallis todistaa oikeaksi.

    Hankalin bugi: data oli oikein, mutta se tuli väärästä maailmasta

    Tähän mennessä sarja on kerännyt kokoelman bugeja, jotka näyttivät onnistumiselta mutta eivät olleet: tyhjä rivi, joka näytti kelvolliselta, vanha arvo, joka näytti tuoreelta, rikkinäinen anturi, joka näytti uskottavalta. Tämän työn paras löytö kuuluu samaan sukuun, mutta se tulee kulmasta, jota en osannut odottaa.

    Kun havaintorivin kokoava logiikka vietiin ensimmäistä kertaa tuotantoon, se alkoi hylätä täysin kelvollisia lämpöpumpun näytteitä virheellä, joka käännettynä tarkoitti ”ankkuri ei ole objekti”. Se oli hämmentävää, koska ankkuri oli objekti. Siinä oli kaikki oikeat kentät, oikeat arvot, oikea aikaleima. Jos sen tulosti, se näytti täydelliseltä. Silti sitä kokoava tarkistus kieltäytyi hyväksymästä sitä.

    Syy osoittautui hienovaraiseksi tavalla, joka on helppo ohittaa. Node-RED ajaa jokaisen funktiosolmun omassa eristetyssä ajoympäristössään — käytännössä omassa pienessä maailmassaan. Kun yksi solmu luo tavallisen objektin ja lähettää sen toiselle solmulle, objektissa on kaikki odotetut ominaisuudet, mutta sen ”syntymämerkki” — se sisäinen tieto, mistä maailmasta se on kotoisin — on peräisin lähettävästä solmusta. Vastaanottava solmu käytti tarkistusta, joka vaati, että objektin syntymämerkki on täsmälleen sen oman maailman merkki. Eri maailmasta tullut objekti, vaikka rakenteeltaan identtinen, ei läpäissyt tuota identiteettitarkistusta.

    Tämä on käsitteellisesti sama asia kuin passi, joka on kaikin puolin oikein täytetty mutta myönnetty maassa, jota tarkastaja ei tunnusta. Mikään tiedoissa ei ole väärin. Silti se hylätään, koska tunnistus perustuu alkuperään, ei sisältöön.

    Sama raja koski kaikkia kolmea historialähdettä, joita rivi tarvitsi — releiden tilan ja final gaten historian, jotka noudetaan muistista ja jotka niin ikään on tuotettu muissa solmuissa. Pelkän ankkurin korjaaminen olisi jättänyt saman ansan piiloon kahteen muuhun kohtaan.

    Korjaus oli tarkoituksella pieni ja rajattu. Juuri ennen kuin nämä kolme lähdettä annetaan kokoavalle logiikalle, ne rakennetaan uudelleen vastaanottavan solmun omassa maailmassa — kierrätetään tekstimuodon kautta ja takaisin, jolloin lopputuloksella on oikea syntymämerkki. Data ei muutu lainkaan; vain sen alkuperä normalisoituu. Alkuperäinen tiukka tarkistus jätettiin ennalleen, koska se on hyvästä syystä tiukka. Silta rakennettiin sen ympärille, ei sitä purkamalla.

    Se, mikä tekee tästä opettavaisen, ei ole yksityiskohta itsessään vaan sen laji. Tavallinen testi ei ollut löytänyt tätä, koska testeissä objektit syntyivät ja tarkistettiin samassa maailmassa — sama raja, joka tuotantoympäristössä oli olemassa, puuttui testipenkistä. Vika näkyi vasta oikeassa ajoympäristössä. Ja se on nöyryyttävä muistutus siitä, että ”testit menevät läpi” ja ”toimii tuotannossa” ovat kaksi eri väitettä, aivan kuten ”kirjoitus onnistui” ja ”data on luettavissa” ovat kaksi eri väitettä.

    Kirjoitettu piste ei saa muuttua jälkikäteen

    Havaintorivin toinen puoli on sen tallennus tietokantaan, ja siinä oli oma vaikea vaatimuksensa. Kun rivi on kerran vahvistettu tallennetuksi, se ei saa koskaan muuttua — ei silloinkaan, kun sama lämpöpumpun näyte jostain syystä saapuu toiseen kertaan.

    Miksi näyte saapuisi kahdesti? Koska tiedonsiirto ei ole täydellistä. Viesti voi toistua. Verkkoyhteys voi katketa juuri sillä hetkellä, kun tietokanta on jo hyväksynyt kirjoituksen mutta vastaus ei ehtinyt takaisin. Silloin lähettävä pää luulee, että kirjoitus epäonnistui, ja yrittää uudelleen. Jos uusi yritys rakentaa rivin uudestaan sen hetken tiedoista, se voi saada eri sisällön kuin alkuperäinen — sama aikaleima, eri arvot. Tietokantaan jäisi kaksi eri versiota samasta hetkestä, ja historia olisi pilalla juuri siltä osin, jonka piti olla luotettavin.

    Ratkaisu nojaa periaatteeseen, jonka voisi tiivistää: kun rivi on kerran rakennettu, se jäädytetään tavutasolla. Sen valmis, tietokantaan lähetettävä muoto talletetaan kestävästi ennen kuin sitä yritetään lähettää, ja jokainen uudelleenyritys lähettää täsmälleen ne samat jäädytetyt tavut — ei koskaan rakenna riviä uudelleen tuoreesta tilasta. Vasta kun tietokanta kuittaa kirjoituksen onnistuneeksi, rivi merkitään vahvistetuksi. Jos sama näyte saapuu vahvistuksen jälkeen uudelleen, se tunnistetaan duplikaatiksi ja pudotetaan ilman uutta kirjoitusta.

    Tämä todennettiin livenä tavalla, josta olen tyytyväinen. Otettiin oikea, jo vahvistettu näyte ja syötettiin sama hetki tarkoituksella uudelleen. Järjestelmä luokitteli sen duplikaatiksi, tallennettu rivi pysyi tavulleen samana, tietokannan piste ei muuttunut, eikä uutta kirjoitusta käynnistetty. Sivutuotteena paljastui vielä se, että yhdestä toistetusta viestistä syntyi kaksi duplikaatti-tunnistusta — mikä ei ollut ongelma vaan päinvastoin todiste siitä, että suoja kestää myös tiedonsiirron oman uusintatoimituksen.

    Mitä bugit opettivat tällä kierroksella

    Kuten aiemmissakin osissa, teoria on siistiä mutta käytäntö opetti. Muutama vika kannattaa kertoa, koska ne näyttävät saman teeman eri puolilta: data voi valehdella monella tavalla näyttäessään onnistuneelta.

    Kirjoitusoikeus ei ole lukuoikeus. Tietokantaan kirjoittava komponentti oli tietoturvasyistä rajattu pelkkään kirjoitukseen — sama minimioikeusperiaate, joka on kulkenut sarjassa mukana. Kirjoitus palautti siististi ”onnistui”, mutta kun tallennus yritettiin varmistaa lukemalla se takaisin samalla oikeudella, vastaus oli ”ei löydy”. Data oli tallessa; lukuoikeutta ei ollut. Opetus on täsmälleen sama kuin läpi koko sarjan: onnistunut kirjoitus on todennettava lukemalla, ja mielellään eri oikeuksin kuin kirjoitettiin.

    Aikaleiman tarkkuus voi ylittää lukujen rajat. Tietokannalle tarjottu erittäin tarkka aikaleima oli suurempi kuin se kokonaisluku, jonka käytetty ohjelmointiympäristö pystyy turvallisesti esittämään — jolloin arvo olisi voinut hiljaa vääristyä. Sama bugi löytyi myöhemmin toisesta, jo aiemmin tuotannossa olleesta kirjoittajasta. Aikaleima palautettiin karkeampaan mutta turvalliseen tarkkuuteen, ja sen turvallisuus tarkistetaan erikseen ennen jokaista kirjoitusta.

    Muisti vuotaa hiljaa, jos sitä ei rajata. Duplikaattien tunnistamiseksi pidettiin muistissa karttaa jo vahvistetuista näytteistä. Kartta kasvoi rajatta, koska mikään ei siivonnut sitä. Korjaus oli antaa merkinnöille elinaika ja määrälle katto. Ilman tätä järjestelmä olisi hidastunut ja lopulta kaatunut viikkojen mittaan — hitaasti, ilman selvää syytä.

    Yksikään näistä ei kaada mitään heti. Ne ovat juuri sitä hiljaista lajia, joka myrkyttää aineiston vähitellen. Ja aineisto on se, jonka varaan koko tuleva malli on tarkoitus rakentaa.

    Miten tämä rakennettiin: pieninä, todistettavina paloina

    Yksi asia tässä työssä ansaitsee tulla sanotuksi menetelmänä, ei vain lopputuloksena. Koko ketju rakennettiin viitenä erillisenä, järjestyksessä katselmoituna vaiheena — kuollut komentoarvo pois, näytteen ankkuriadapteri, relehistorian liitos, final gaten historian liitos, ja lopuksi varsinainen rivin kokoaja ja kestävä tallennus. Yksikään vaihe ei mennyt tuotantoon ennen kuin se oli itsenäisesti katselmoitu ja sen testit ajettu oikeaa lähtötilaa vastaan.

    Katselmointi tehtiin tavalla, joka on itsessään opettavainen: jokainen ehdotettu muutos toimitettiin pakettina, joka sisälsi tarkat tarkistussummat lähtötilasta ja lopputilasta, ja katselmoija todensi riippumattomasti, että muutos koski täsmälleen niitä solmuja, joita sen piti koskea — ei yhtään enempää. Useammin kuin kerran tämä kuri esti oikean virheen: kokonaisen tiedoston tuominen varmuuskopiosta olisi hiljaa palauttanut myöhemmät, jo tuotantoon tehdyt muutokset. Se estettiin sillä, että muutokset tehtiin kirurgisesti elävään tilaan, ei tuomalla kokonaista tiedostoa jostain aiemmasta hetkestä.

    Tämä on hitaampaa kuin ”muokkaa ja katso toimiiko”. Mutta järjestelmässä, joka ohjaa oikean talon lämmitystä ja jonka datan varaan aiotaan rakentaa malli, hitaus oikeassa kohdassa on halvempaa kuin nopeus. Suurin osa tämän kierroksen todellisesta työstä ei ollut koodin kirjoittamista vaan sen todistamista, ettei koodi tee mitään muuta kuin mitä sen piti.

    Mitä vielä puuttuu

    Rehellisyyden nimissä iso osa on yhä edessä, ja se kannattaa sanoa selvästi.

    Se mikä nyt on valmis, on auditoitava havaintorivi ja sen kestävä tallennus — raaka- ja auditkerros. Se on juuri saatu tuotantoon ja live-todennettua, mutta se on tuoretta. Seuraava askel ei ole uutta ohjauslogiikkaa vaan datan laadun todistaminen: järjestelmän annetaan kerätä havaintoja rauhassa, ja sen jälkeen erillinen, vain-luku-analyysi käy läpi vuorokauden verran dataa ja tarkistaa, että jokaisella lämpöpumpun näytteellä on täsmälleen yksi rivi, että statukset ja tuoreudet jakautuvat järkevästi, ja että tallennus pysyy terveenä normaalikäytössä. Ensimmäiset uudelleenkäynnistyksen jälkeiset epätäydelliset rivit ovat odotettuja — kiinnostavaa on, häviävätkö ne, kun historia on ehtinyt täyttyä.

    Tämän jälkeenkin edessä on vielä se ylempi kerros, joka tästä osasta puuttuu kokonaan: tilamittaukset, jotka luokittelevat raakadatan käyttökelpoiseksi tai ei — mukaan lukien se aidosti vaikea kysymys, miten lämmitys-, käyttövesi- ja jälkilämpöjaksot erotetaan toisistaan niin, ettei käyttöveden tuottama korkea menovesi sekoitu lämmitysdataan. Ja lopuksi aukkoanalyysi ja visualisointi, jotka tekevät hiljaisista data-aukoista näkyviä. Vasta kun tämä koko ketju on todennettu ensimmäisen oikean lämmitystalven yli, EnergyHub voi alkaa rakentaa ennustavaa mallia sen päälle.

    Tämä on myös luonteva piste, jossa jokin muuttuu luonteeltaan. Tähän asti control_raw on ollut rakennettava ominaisuus. Kun se on kerätty ja sen laatu todistettu, se lakkaa olemasta ominaisuus ja muuttuu joksikin muuksi: tutkimusdatan lähteeksi, jonka varaan seuraava, varovainen askel — ennustepohjaisen lämmityksen suunnittelija, joka ensin vain ehdottaa eikä vielä ohjaa — voidaan rakentaa. Mutta se on jo toisen osan aihe.

    Ja koska ajoitus ei ole meidän käsissämme vaan sään, tässä on aito kilpajuoksu. Lämmitysdataa ei voi kerätä jälkikäteen. Jos tallennus ei ole valmis ja todennettu ennen ensimmäisiä pysyviä pakkasia, se talvi menee ohi keräämättä. Kirjoitushetkellä seitsemän vuorokauden keskilämpötila on yhä reilusti lämmityskynnyksen yläpuolella — mutta se ikkuna sulkeutuu viikoissa, ei kuukausissa. Siksi tämä rivi rakennettiin nyt, eikä sitten kun olisi ollut mukavampi aika.


    Seuraavaksi kysytään sama kysymys talon toiselta puolelta: mitä energiaa ja rahaa oikeasti liikkui juuri siinä vartissa. Se osoittautui täsmälleen samaksi ongelmaksi kuin lämmityksen auditrivi — eri suureet ja lähteet, mutta sama tapa, jolla data valehtelee onnistuneelta näyttäessään. Suurin muutos koko työssä ei lopulta ollut rivi koodia vaan sen myöntäminen, milloin on liian aikaista kysyä: oikea sähkömittaridata kertoi, ettei vartin totuutta usein ole vielä olemassa silloin, kun sitä ensin haluaisi laskea.

  • Case: oma talo — Osa 23: Varmuuskopio ei ole varmuuskopio ennen palautustestiä

    Koko tämä sarja on nojannut yhteen oletukseen, jota ei ollut kertaakaan koeteltu: että jos EnergyHub hajoaa, sen saa palautettua. Varmuuskopioita on otettu joka yö, ne on kopioitu pilveen, ja niiden koko ja tarkistussumma on tarkistettu. Silti mikään näistä ei todista sitä ainoaa asiaa, joka lopulta merkitsee — että tyhjästä koneesta saa rakennettua toimivan järjestelmän. Tämä osa kertoo siitä illasta, jona se oletus vihdoin testattiin: puhdas Debian tyhjälle levylle, yksi palautuspaketti, ja kysymys, johon ei ollut aiempaa vastausta.

    Lyhyt vastaus: se toimi. Pitkä vastaus on kiinnostavampi, koska matkalla löytyi kaksi vakavaa mutta rajattua vikaa palautusautomaatiossa — ja koska palautuksen jälkeen syntyi jotain, joka on yhtä paljon turvallisuusongelma kuin varajärjestelmä.

    Miksi tämä piti tehdä ollenkaan

    Aiemmassa osassa datalaadusta kirjoitin, että hiljainen data-aukko on vaarallisempi kuin näkyvä virhe. Varmuuskopioilla on tismalleen sama ongelma. Backup, joka ei koskaan palaudu, näyttää joka päivä onnistuneelta: arkisto syntyy, tarkistussumma täsmää, pilvikopio menee läpi. Kaikki vihreää. Ja silti se voi olla täysin hyödytön — puuttuva salaisuustiedosto, väärä hakemistorakenne, tai vain se, ettei kukaan ole koskaan kokeillut. Ainoa tapa tietää on rakentaa järjestelmä uudelleen tyhjästä ja katsoa, käynnistyykö se.

    Julkaistuissa osissa on aiemmin kuvattu varmuuskopiointi rclonella ja pilvitallennukseen. Se kuvasi todellisuutta silloin. Myöhemmässä auditoinnissa selvisi, että pelkkä yöarkisto ei yksin riitä puhtaan koneen palauttamiseen — se on palautuksen tärkein dataosa, mutta sen ympärille tarvitaan vielä erikseen salaisuudet, projektin juuri, tarkat Docker-imaget ja itse palautusautomaatio. Näistä koottiin erillinen, täydellinen palautuspaketti, ja juuri se laitettiin tässä kokeessa tulikokeeseen.

    Koeasetelma

    Palautuskoneeksi otettiin vanha läppäri, jossa on 500 gigatavun mekaaninen kiintolevy — tarkoituksella vaatimaton, koska varakoneen ei kuulu olla parempi kuin mitä hätätilanteessa oikeasti on käsillä. Sille asennettiin puhdas Debian tyhjälle levylle, ilman työpöytäympäristöä, pelkkä SSH ja perustyökalut. Palautuspaketti — kooltaan noin puolitoista gigatavua — siirrettiin tuotantokoneelta USB-tikulla, ja sen eheys tarkistettiin joka siirtovaiheessa: Windowsin puolella, USB-tikulta ja vielä kiintolevylle purettuna. Sama periaate kuin telemetriatyössä: tarkistussumma varmistetaan joka rajan yli, ei vain kerran.

    Aikajana oli tiukka ja siksi vakuuttava. Tuore varmuuskopio käynnistyi illalla, palautuspaketti valmistui reilussa minuutissa, ja itse palautus tyhjälle koneelle vei kaikkineen noin kymmenen minuuttia. Sen jälkeen kaikki viisi palvelua — tietokanta, viestivälittäjä, visualisointi, Home Assistant ja Node-RED — olivat pystyssä. Ratkaisevin yksittäinen mittari oli tietokannan historiadata: palautuiko sinne oikeasti se lämpöpumpun raakatelemetria, jota edellinen osa käsitteli, vai pelkkä tyhjä tietokantarakenne. Se palautui, aikaleimoineen ja mittausarvoineen. Kone, jota ei tuntia aiemmin ollut olemassa, tunsi talon lämmityshistorian.

    Palautuksen tärkein testi tehtiin vasta uudelleenkäynnistyksessä

    Kun palvelut olivat pystyssä ja status vihreä, olisi ollut houkuttelevaa julistaa koe onnistuneeksi. Mutta järjestelmä, joka toimii kerran käsin käynnistettynä, ei vielä ole palautunut — se on vasta koottu. Oikea kysymys on, selviääkö se uudelleenkäynnistyksestä ilman käsiä.

    Kone käynnistettiin uudelleen. Wi-Fi liittyi itsestään takaisin, SSH palasi, Docker käynnisti kaikki viisi konttia ilman yhtäkään käsikomentoa, ja noin kahdessa minuutissa kaikki HTTP-palvelut vastasivat. Node-RED saavutti terveen tilan, ja paikallinen viestintätesti meni läpi. Vasta tämä teki kokeesta uskottavan. Sama periaate on kulkenut koko tämän sarjan läpi lämpöpumpun ja sähköauton final gate -siirroissa: mikään ei ole valmista ennen kuin se on todennettu restartin yli. Palautuskin todistetaan vasta silloin, kun kone osaa nousta itse.

    Kaksi vikaa, jotka eivät estäneet palautusta mutta paljastivat automaation heikkoudet

    Itse data ja palvelut palautuivat, mutta palautusautomaatiosta löytyi kaksi vakavaa toteutusvirhettä. Kumpikin on juuri sitä luokkaa, joka ei näy ennen kuin joku oikeasti ajaa palautuksen läpi — mikä on koko harjoituksen paras perustelu.

    Ensimmäinen koski hakemisto-oikeuksia. Salaisuudet oli pakattu arkistoon tavalla, joka palautettaessa muutti kotihakemiston oikeudet niin, ettei koneelle enää päässyt kirjautumaan normaalisti — komentotulkki ja pääkäyttäjäoikeuksien nosto alkoivat kumpikin vastata ”käyttö estetty”. Palautunut järjestelmä oli teknisesti pystyssä mutta käytännössä lukossa. Korjaus tehtiin koneen paikalliselta konsolilta palauttamalla hakemisto-oikeudet oikeiksi. Juurisyy on tuttu koko sarjasta: arkisto oli tallentanut mukaansa myös ylätason hakemistojen oikeudet, ja purku juureen levitti ne koko polkuun. Korjauslistan ykkösasia on pakata salaisuudet niin, että ne purkautuvat vain omaan hakemistoonsa eivätkä kajoa juuripolun oikeuksiin.

    Toinen vika oli petollisempi, koska se valehteli väärään suuntaan. Palautuksen automaattinen verifiointi ilmoitti, ettei tietokannan historiadataa löytynyt — vaikka data oli todellisuudessa täysin tallessa. Vika oli itse tarkistuskyselyssä: se yhdisti eri tietotyyppisiä kenttiä tavalla, joka tuotti virheen tietokannan sisällön sijaan. Toisin sanoen verifiointi hylkäsi terveen palautuksen. Tämä on vaarallisempi kuin miltä kuulostaa. Väärä epäonnistuminen palautustilanteessa voi saada hätääntyneen ylläpitäjän hylkäämään täysin toimivan palautuksen ja aloittamaan alusta — juuri silloin kun aikaa ja hermoja on vähiten. Korjaus vaihtaa tarkistuksen kyselyyn, joka hakee yhden tunnetun mittauksen ilman tyyppien yhdistämistä, ja erottaa selkeästi kaksi eri tilaa: ”data palautui” ja ”verifiointi epäonnistui”. Nämä eivät saa näyttää samalta.

    Kolmas, lievempi havainto oli, että automaattinen verifiointi ei malttanut odottaa. Se tarkisti Home Assistantin vain viisitoista sekuntia käynnistyksen jälkeen, sai vastaukseksi tyhjän — ja noin kahden minuutin kuluttua palvelu vastasi normaalisti. Kiinteä odotusaika oli yksinkertaisesti liian lyhyt. Korjaus on ehtopohjainen odotus, joka kysyy toistuvasti viiden minuutin ajan sen sijaan että luottaisi yhteen kiinteään sekuntimäärään. Sama opetus kuin Voimapirtin puolella: älä oleta valmistumista, todenna se.

    Palautettu kone on ladattu ase

    Tässä on koko kokeen tärkein oivallus, ja se on turvallisuuskysymys ennen kuin se on varajärjestelmäkysymys. Palautettu läppäri ei ole viaton varakone, jonka voi huoletta kytkeä nurkkaan odottamaan. Se sisältää tuotannon asetukset, laiteosoitteet ja ohjauslogiikan — ja jos se kytketään talon verkkoon, se voi tavoittaa oikeat laitteet välittömästi.

    Docker käynnistää kaikki kontit automaattisesti bootissa. Palautetut viestit ja tilat voivat aktivoida päätöksenteon heti. Ja jos sekä alkuperäinen tuotantokone että palautettu kone olisivat yhtä aikaa verkossa, syntyisi tilanne, jota kannattaa pysähtyä miettimään: kaksi täysin toimivaa EnergyHubia, jotka molemmat luulevat omistavansa samat releet, samat MQTT-aiheet ja samat laitteet. Ne voisivat lähettää ristiriitaisia komentoja samalle sähköauton latausreleelle tai samalle lämpöpumpulle. Automaatiossa tätä kutsutaan split-brain-tilanteeksi, ja se on pahempi kuin ei varakonetta lainkaan — koska kaksi ohjainta, jotka kamppailevat samasta laitteesta, tuottaa arvaamatonta käytöstä juuri siihen fyysiseen järjestelmään, jonka piti olla hallinnassa.

    Siksi palautunut kone on tässä vaiheessa nimenomaan kylmä varakone: sammutettuna, verkosta irti, odottamassa. Se on todistetusti toimiva, mutta sitä ei saa vain liittää verkkoon. Turvallinen käyttöönotto vaatii hallitun vaihtomenettelyn, jonka ehdoton ensimmäinen askel on varmistaa, että vanha kone on varmasti pois käytöstä ennen kuin uusi päästetään verkkoon. Yksi ohjain kerrallaan, ei koskaan kahta.

    Palautuksessa itsessään sama periaate toteutettiin verkkoeristyksellä: koko koe ajettiin erillisessä verkossa, ei talon tuotantoverkossa, juuri siksi että palautetut asetukset voisivat muuten tavoittaa oikeat laitteet heti kun verkko sen sallii. Palautusta ei ajeta tuotanto-osoitteistossa. Se on yksi niistä säännöistä, jotka tuntuvat ylivarovaisilta kunnes tajuaa, että palautettu järjestelmä ei tiedä olevansa koe — se luulee olevansa tuotanto ja käyttäytyy sen mukaan.

    Mitä koe ei vielä todistanut

    Rehellisyyden nimissä on sanottava selkeästi, mitä tämä koe ei osoittanut. Oikeiden laitteiden — sähköauton, lämpöpumpun, aurinkoinvertterin, energiamittarien — yhteyksiä ei testattu, koska koe ajettiin tarkoituksella eristetyssä verkossa ilman pääsyä niihin. Hallittua tuotantoon vaihtoa vanhalta koneelta uudelle ei tehty; se on edelleen suunnitelma, ei todennettu menettely. Etähallintaidentiteettiä ja pilvivarmennuksen asetuksia ei aktivoitu. Ja mikä tärkeintä: käytetty palautuspaketti oli yhden illan jäädytetty tilannekuva. Se ei päivity myöhemmillä öillä automaattisesti. Jos tuotantokone rikkoutuisi kuukausien päästä, tämän paketin data olisi vanhaa, ellei siihen palautettaisi tuoreinta yövarmistusta erikseen.

    Tämä johtaa suoraan siihen, mitä seuraavaksi on parannettava.

    Kolme tasoa ja se, mikä yöarkistosta puuttuu

    Koe jäsensi varmistuksen kolmeen tasoon, jotka palvelevat eri tarkoitusta. Yöarkisto on nopea ja pieni, kelpaa datan päivittäiseen turvaamiseen ja pilveen vientiin. Täysi palautuspaketti — salaisuuksineen, projektin juurineen ja Docker-imageineen — tehdään harvemmin, mutta vain se mahdollistaa palautuksen puhtaalle koneelle. Levykuva on valinnainen kolmas taso saman koneen nopeaan palautukseen, muttei korvaa kahta ensimmäistä. Vasta nämä yhdessä muodostavat kokonaisuuden, jossa yhden tason puute ei kaada koko palautettavuutta.

    Selvin parannuskohde on, että salaisuudet — ne tiedostot, joita ilman mikään ei käynnisty — eivät saa elää vain tuotantokoneella tai satunnaisessa täydessä paketissa. Niille tarvitaan oma, salattu varmistuksensa, joka viedään turvaan erikseen. Tähän liittyy kokeen kiusallisin yksityiskohta: palautuspaketti sisältää selväkielisiä tunnuksia ja salasanoja, mikä tekee sekä USB-tikusta että sen Windows-kopiosta arkaluontoista aineistoa. Prosessin on jatkossa salattava paketti ja säilytettävä purkuavain erillään — salaamatonta pakettia ei jätetä lojumaan tavalliselle työpöytäkoneelle.

    Yövarmistuksesta pitää myös tulla itseään valvova. Sen kuuluu epäonnistua äänekkäästi, jos arkistoa ei synny, tarkistussumma ei täsmää, Node-REDin vuot eivät ole kelvollista JSONia, Home Assistantin oleelliset hakemistot puuttuvat, tietokannan varmistus on tyhjä, arkiston koko poikkeaa rajusti totutusta, pilvikopio ei vastaa paikallista tiedostoa, tai edellisestä onnistuneesta pilvivarmistuksesta on kulunut liian kauan. Jokainen näistä on hiljainen vika, joka ilman omaa hälytystään huomataan vasta silloin kun palautusta oikeasti tarvitaan — eli pahimmalla mahdollisella hetkellä.

    Opit

    Varmuuskopio ei ole varmuuskopio ennen palautustestiä. Onnistuneelta näyttävä backup voi olla täysin hyödytön, eikä sitä voi tietää katsomatta. Ainoa todiste palautettavuudesta on rakentaa järjestelmä tyhjästä ja katsoa nouseeko se — mieluiten vielä uudelleenkäynnistyksen yli.

    Väärä epäonnistuminen on palautuksessa vaarallisempi kuin väärä onnistuminen. Verifiointi, joka hylkää terveen palautuksen, voi saada hylkäämään toimivan järjestelmän juuri silloin kun paniikki on suurimmillaan. ”Data palautui” ja ”verifiointi epäonnistui” on pidettävä ehdottomasti erillään.

    Palautettu kone on tuotantokone, joka ei tiedä olevansa koe. Se sisältää oikeat asetukset ja tavoittaa oikeat laitteet heti kun verkko sallii. Kaksi yhtä aikaa toimivaa ohjainta on pahempi kuin ei varakonetta — split-brain samasta releestä on juuri se arvaamattomuus, jota koko järjestelmä pyrkii välttämään. Yksi ohjain kerrallaan.

    Salaisuudet ovat erillinen ongelma datasta. Tokeneita ja salasanoja ei voi käsitellä samalla huolettomuudella kuin mittausdataa. Ne vaativat oman salatun varmistuksensa ja avaimen, joka säilytetään erillään — muuten koko palautuspaketti on vuotoriski.

    Palautettavuus on ominaisuus, joka pitää harjoitella, ei kertaluontoinen tarkistus. Pieni verifiointi joka yö, täysi paketti jokaisen merkittävän muutoksen jälkeen, ja päästä päähän -palautus erilliselle koneelle säännöllisin väliajoin. Muuten palautusohje vanhenee hiljaa samalla tavalla kuin konfiguraatio osassa 18.

    Mitä vielä puuttuu

    Palautusautomaatiosta korjataan seuraavaksi ne kaksi vikaa, jotka koe paljasti: salaisuuksien purku niin ettei se riko hakemisto-oikeuksia, ja verifiointi joka ei hylkää tervettä dataa. Sen jälkeen rakennetaan uusi paketti ja ajetaan toinen puhdas palautus varmistukseksi. Varakoneelle suunnitellaan pysyvä lukko, joka estää Dockeria käynnistymästä automaattisesti ja torjuu tuotantoverkon ennen erillistä, tietoista vaihtokomentoa — niin että koneen voi käynnistää turvallisesti tarkastusta varten ilman split-brain-riskiä. Docker-imaget kiinnitetään tarkkaan versioon, ettei myöhempi päivitys vaihda niitä huomaamatta. Ja salaisuuksien salattu offsite-varmistus sekä hallitun tuotantoon vaihdon runbook — sellainen, joka on saatavilla myös ilman internetiä — ovat molemmat vielä tekemättä.

    Varakone on nyt olemassa ja todistetusti toimiva. Mutta valmiudella on oma määritelmänsä, joka täyttyy vasta kun tuorein testattu paketti on käsillä, salaisuudet ovat turvassa erikseen, kone on lukossa tai sammuksissa, ja vaihdon ohje on luettavissa silloinkin kun kaikki muu on poikki. Siihen asti tämä on toimiva koe eikä valmis järjestelmä — mikä on täsmälleen se rehellinen ero, jota koko tämä sarja on yrittänyt pitää näkyvissä.

    Palautuskoe vastasi kysymykseen, joka oli roikkunut vastaamattomana koko rakennusprojektin ajan: saako tämän palautettua? Saa. Mutta vastaus tuli varauksin, ja juuri ne varaukset ovat se työ, joka nyt on edessä.


    Seuraavaksi rakennetaan yhdestä lämpöpumpun näytteestä jälkikäteen auditoitava rivi, jossa komento, lupa ja fyysinen reletila ovat oikein ajallisesti kohdistettuja. Se osoittautui hankalammaksi kuin uskoin — ei siksi että logiikka olisi monimutkaista, vaan siksi että data valehtelee monella tavalla onnistuneelta näyttäessään. Mukana muun muassa objekti, jossa on kaikki oikeat kentät ja oikea aikaleima mutta joka silti hylätään, koska se tulee ”väärästä maailmasta”. Ja koska lämmitysdataa ei voi kerätä jälkikäteen, tämä on aito, sään sanelema kilpajuoksu.

  • Case: oma talo — Osa 22: Voiko dataan luottaa? EnergyHub alkaa rakentaa aineistoa, ei vain ohjata

    Koko tämä sarja on rakentanut järjestelmää, joka tekee päätöksiä. Se päättää milloin autoa ladataan, milloin käyttövettä lämmitetään, milloin lämpöpumppu saa boostata. Ja jokainen niistä päätöksistä on täsmälleen niin hyvä kuin data, jonka varaan se rakennetaan. Seuraava iso kehitysaskel — ennustepohjainen lämmitys, joka katsoo säätä, hintoja ja talon lämpömassaa — ei ole rakennettavissa ennen kuin sen alla on aineistoa, johon voi luottaa. Tämä osa kertoo siitä työstä, jolla raakatelemetriasta tehdään mallikelpoista dataa. Ja se on osoittautunut vaikeammaksi kuin itse ohjaus.

    Sanon heti aluksi, että tämä osa on toisenlainen kuin edelliset. Aiemmat kertoivat valmiista, tuotannossa todennetuista ominaisuuksista. Tämä kertoo työstä, joka on parhaillaan käynnissä: osa siitä on jo asennettuna ja todennettuna, osa odottaa vielä commitointia ja 24 tunnin vakausikkunaa, osa on vasta suunniteltu. Kirjoitan tämän nyt, koska työn opetukset ovat kiinnostavampia kuin sen lopputila — ja koska yksi näistä opetuksista näkyi jo osassa 20, kun Voimapirtin Lämpö-sivun huoneanturit näyttivät ”Ei tietoa”. Ne olivat hiljaa juuri tämän työn takia.

    Miksi juuri nyt: talvidata ei odota

    Ajoitus ei ole sattumaa. Lämmitysmallia varten tarvitaan talvidataa — sitä, miten talo käyttäytyy kun lämpöpumppu oikeasti lämmittää. Kesällä maalämpö tekee vain käyttöveden ja kylpyhuoneen lattialämmön, tunti pari vuorokaudessa. Lämmityskausi alkaa vasta kun ulkolämpötila painuu pysyvästi kynnyksen alle, ja kirjoitushetkellä seitsemän vuorokauden keskiarvo on 19,4 °C, kun kynnys on 12 °C. Siitä kertyy karkeasti kahdeksasta kymmeneen viikkoa aikaa saada tallennus kuntoon.

    Lämmitysdataa ei voi kerätä jälkikäteen. Jos tallennus ei ole valmis ja todennettu ennen ensimmäisiä pakkasjaksoja, se talvi menee ohi. Malli, joka olisi voinut oppia tästä talvesta, joutuisi odottamaan seuraavaa. Tämä on koko työn kiireen todellinen syy: kyse ei ole siitä, että data pitäisi saada nopeasti, vaan siitä, että sitä ei saa lainkaan, jos ikkuna menee kiinni tyhjänä.

    Kaksi kerrosta: raaka säilyttää haamut, tila suodattaa ne pois

    Ensimmäinen tärkeä ratkaisu oli jakaa data kahteen kerrokseen, joilla on eri tehtävä ja eri sääntö.

    Raakakerros säilyttää kaiken, myös virheelliseltä näyttävän. Se on auditointia varten. Jos anturi julkaisee epäilyttävän arvon, raakakerros tallentaa sen sellaisenaan, koska myöhemmin voi olla tarpeen katsoa, mitä oikeasti tuli linjaa pitkin. Raakakerrosta ei saa siivota, korjata eikä täydentää — se on todistusaineisto, ei tulkinta.

    Tilakerros on laatusuodatettu, ja se on se, jonka varaan malli rakennetaan. Siinä jokainen havainto on luokiteltu käyttökelpoiseksi tai ei, ja käyttökelvoton havainto ei valu malliin mukana. Sama fyysinen mittaus on siis tietokannassa kahdesti eri roolissa: kerran haamuineen todistusaineistona, kerran puhdistettuna päätöksentekoa varten.

    Ero kuulostaa akateemiselta, mutta se ratkaisee käytännön ongelman. Ilman raakakerrosta jokainen laatusääntö olisi peruuttamaton: kun havainto on kerran hylätty, sitä ei enää saa takaisin tarkistettavaksi. Ilman tilakerrosta taas malli joutuisi luottamaan kaikkeen, myös rikkinäiseen. Kaksi kerrosta antaa molemmat: näkyvyyden menneeseen ja luotettavuuden tulevaan.

    Hiljainen data-aukko on vaarallisempi kuin näkyvä virhe

    Tämän työn opettavaisin yksittäinen löytö oli rikkinäinen ulkolämpötila-anturi. Talon Netatmo-ulkomoduuli on ollut pitkään viallinen niin, että sovellus näyttää siitä lukeman, mutta lukema ei ole totta. Se ei näy tyhjänä, ei virheenä, ei vanhentuneena — se näyttää täysin uskottavalta arvolta, jonka ikä on vain 21 sekuntia. Juuri sellaiselta, johon malli mielellään luottaisi.

    Näkyvä virhe on turvallisempi kuin uskottava valhe. Tyhjän ruudun huomaa. Vanhentuneen aikaleiman huomaa. Mutta tuoreelta näyttävä väärä arvo menee suoraan läpi, ja päätös rakentuu sen varaan ilman että kukaan huomaa mitään. Malli, joka saisi tämän anturin arvon, oppisi talvesta väärän tarinan eikä koskaan kertoisi tehneensä niin.

    Ratkaisu oli erottaa kytkeytyneisyys lukemasta. Jokaisella anturilla on erillinen tieto siitä, onko yhteys oikeasti pystyssä, ja connected=0 pakottaa laadun tilaan unavailable riippumatta siitä, miltä itse lukema ja sen ikä näyttävät. Rikkinäinen ulkoanturi merkitään käyttökelvottomaksi, vaikka se julkaisisi kuinka uskottavaa lukemaa. Ainoa oikeasti toimiva ulkolämpötilan lähde on tällä hetkellä lämpöpumppu itse, ja tilakerros nojaa siihen.

    Puuttuva, nolla ja tuntematon ovat kolme eri asiaa

    Sama periaate, joka kulki Voimapirtin läpi osassa 20viiva ei ole nolla — pätee tallennukseen vielä tiukemmin. Kun anturi ei kerro arvoaan, se tieto tallennetaan tyhjänä, null, ei koskaan nollana. Nolla on mittaustulos. Tyhjä on mittauksen puuttuminen. Jos nämä sekoittuvat, malli oppii, että ulkona oli nolla astetta silloin kun anturi oli oikeasti mykkä, ja se on suoraan väärää oppia.

    Kolmas tila on tuntematon: arvo, jota ei syystä tai toisesta voi luokitella luotettavaksi. Se ei ole sama kuin puuttuva eikä sama kuin nolla. Näiden kolmen erottaminen läpi koko ketjun — anturilta MQTT:n kautta tietokantaan — on työn tylsin ja tärkein osa. Suurin osa datalaadun bugeista, jotka tässä työssä löytyi, oli juuri näiden kolmen sekoittumista jossain kohtaa putkea.

    Ikä on osa mittausta

    Osassa 18 opittiin kalliisti, että päätös vanhalla datalla on väärä päätös, vaikka data olisi joskus ollut oikea. Sama pätee tallennukseen. Jokaisella kentällä on oma tuoreutensa, ja se tuoreus tallennetaan mittauksen mukana, ei erikseen arvattavaksi.

    Eri suureilla on eri tahti. Lämpöpumpun prioriteettitieto ja kompressorin nopeus päivittyvät kolmenkymmenen sekunnin välein, menoveden lämpötila ja mukavuuspyörän lukema minuutin välein, lisävastuksen tila kymmenen sekunnin välein, käyttöveden asetusarvot vain viiden minuutin välein. Jos kaikkia kohdeltaisiin samalla tuoreusrajalla, joko tuoreet arvot hylättäisiin turhaan tai vanhat hyväksyttäisiin liian pitkään. Siksi lämpöpumpun kentät on ryhmitelty tuoreuden mukaan omiin failsafe-rajoihinsa, karkeasti 90 ja 180 sekuntia sen mukaan kuinka nopeasti suure oikeasti muuttuu.

    Tämä on sama oivallus kuin readback-työssä osassa 18, mutta nyt data-auditoinnin tasolla: aikaleima ei ole metatietoa mittauksen vieressä, se on osa itse mittausta. Arvo ilman ikäänsä ei ole havainto, se on huhu.

    Sama periaate kantaa jo ohjauspuolellekin. Home Assistantiin rakennettiin oma canonical-julkaisija fyysisille SG-releille, ja se erottaa toisistaan sen, milloin releen tila todella havaittiin ja milloin tilannekuva julkaistiin, eikä heartbeat saa vanhentunutta havaintoa näyttämään uudelta. Se, milloin rele oikeasti raportoi tilansa, ja se, milloin julkaisija todistaa olevansa elossa, ovat kaksi eri aikaa, eikä jälkimmäinen saa teeskennellä edellistä. Ikä on siis osa mittausta yhtä lailla silloin, kun kysytään mitä rele teki, kuin silloin, kun kysytään kuinka lämmin ulkona oli.

    Mitä työ opetti bugien kautta

    Teoria on siistiä, mutta tallennus opetti käytännössä. Muutama todellinen vika kannattaa kertoa, koska ne näyttävät, miten monella tavalla data voi valehdella onnistuneelta näyttäessään.

    Kirjoitus onnistui ei ole sama kuin data on luettavissa. Node-REDin kirjoitustoken oli tietoturvasyistä rajattu pelkkään kirjoitukseen — sama A6-vaiheen minimioikeusperiaate, joka on kulkenut sarjassa mukana. Kirjoitus palautti siististi koodin 204, ”onnistui”, mutta verifiointi, joka luki dataa takaisin samalla tokenilla, sai 404, ”ei löydy”. Data oli tallessa; lukuoikeutta ei ollut. Opetus on täsmälleen sama ”komento ≠ toteuma” -periaate, joka on kulkenut koko sarjan läpi: onnistunut kirjoitus on todennettava lukemalla, eri oikeuksilla kuin kirjoitettiin.

    Rivi voi syntyä ilman että siinä on mitään. Eräs varhainen versio kirjoittajasta tuotti tietokantaan rivin myös silloin, kun anturi puuttui kokonaan viestistä — pakolliset kentät oli kovakoodattu, joten rivi näytti kelvolliselta vaikka se oli tyhjä kuori. Korjaus oli tehdä kytkeytyneisyydestä pakollinen kenttä: näin puuttuva anturi hylkää koko erän, mutta aidosti käyttökelvoton havainto (rikkinäinen ulkoanturi, connected=0, lukema tyhjä) tallentuu silti — juuri niin kuin kaksikerrosmalli vaatii.

    Aikaleiman tarkkuus voi ylittää lukujen rajat. Tietokantaan tarjottu nanosekuntitarkka aikaleima oli suurempi kuin se kokonaisluku, jonka JavaScript pystyy turvallisesti esittämään. Sama bugi löytyi myöhemmin toisesta, jo tuotannossa olleesta kirjoittajasta. Aikaleima korjattiin millisekuntitarkkuuteen ja sen turvallisuus tarkistetaan erikseen ennen kirjoitusta.

    Muisti vuotaa hiljaa, jos sitä ei rajata. Kirjoittaja piti muistissa karttaa vahvistetuista näytteistä duplikaattien torjumiseksi. Kartta kasvoi rajatta, koska mikään ei siivonnut sitä. Korjaus oli antaa merkinnöille elinaika ja katto: vanhat poistetaan, ja määrälle on yläraja. Ilman tätä järjestelmä olisi hidastunut ja lopulta kaatunut viikkojen mittaan — hitaasti, ilman selvää syytä.

    Nämä eivät ole näyttäviä vikoja. Ne ovat juuri sitä hiljaista lajia, joka ei kaada mitään heti mutta myrkyttää aineiston vähitellen. Ja aineisto on se, jonka varaan koko tuleva malli aiotaan rakentaa.

    Mitä vielä puuttuu

    Tämä on alustava kuvaus, ja rehellisyyden nimissä iso osa on vielä edessä. Pysyväistallennuksen ensimmäinen kerros odottaa 24 tunnin vakausikkunan läpäisyä ja commitointia. Lämpöpumpun raakadatan pysyväistallennus on jo viety tuotantoon ja sen minuutin kadenssi todennettu, mutta se on vasta ensimmäinen kokonainen kerros. Ohjauspolun raakadata — mitä final gate komensi ja mitä releet oikeasti tekivät — on lohkattu omaksi myöhemmäksi kierroksekseen, koska se vaatii uusia välimuistipolkuja jotka pitää ensin rakentaa ja todentaa. Sen ensimmäiset palaset ovat jo paikoillaan: komentopolun vanha comfort-arvo on poistettu kuolleena arvona ja lämpöpumpun tarkkaan näyteaikaan sidottu shadow-triggeri rakennettu. Mutta koko ketju — Node-REDin rajattu historia, final gaten liitos oikeaan hetkeen ja pysyvä tallennus — on vielä kesken.

    Siitä — miten yhdestä lämpöpumpun näytteestä rakennetaan jälkikäteen auditoitava rivi, jossa komento, lupa ja fyysinen reletila ovat oikein ajallisesti kohdistettuja — tulee oma jatko-osansa myöhemmin. Sitä ennen tulevat varsinaiset tilamittaukset, jotka luokittelevat raakadatan käyttökelpoiseksi tai ei — mukaan lukien vaikea kysymys siitä, miten lämmitys-, käyttövesi- ja jälkilämpöjaksot erotetaan toisistaan niin, ettei käyttöveden tuottama korkea menovesi sekoitu lämmitysdataan. Ja lopuksi aukkoanalyysi ja visualisointi, jotka tekevät hiljaisista data-aukoista näkyviä. Vasta kun tämä koko ketju on todennettu talven yli, EnergyHub voi alkaa rakentaa mallia sen päälle.

    Sen jälkeen tulevat varsinaiset tilamittaukset, jotka luokittelevat raakadatan käyttökelpoiseksi tai ei — mukaan lukien vaikea kysymys siitä, miten lämmitys-, käyttövesi- ja jälkilämpöjaksot erotetaan toisistaan niin, ettei käyttöveden tuottama korkea menovesi sekoitu lämmitysdataan. Ja lopuksi aukkoanalyysi ja visualisointi, jotka tekevät hiljaisista data-aukoista näkyviä. Vasta kun tämä koko ketju on todennettu talven yli, EnergyHub voi alkaa rakentaa mallia sen päälle.


    Seuraavassa osassa palataan konkretiaan aivan toisesta suunnasta. Koko tämä sarja on nojannut yhteen oletukseen, jota ei ole vielä koeteltu: että järjestelmän saa palautettua, jos se hajoaa. Varmuuskopioita otetaan, mutta varmuuskopio ei ole varmuuskopio ennen kuin se on kertaalleen palautettu tyhjälle koneelle ja todistettu toimivaksi. Siitä, mitä palautustestissä oikeasti selviää — ja mikä jää palautumatta — kertoo osa 23.

  • Case: oma talo — Osa 21: Auto täyteen aamukuudeksi: miksi määräaikalataus ei ole ajastin

    Voimapirtissä on kaksi yksinkertaista painiketta: 80 % aamuksi ja 100 % aamuksi. Painalluksen jälkeen nappi lukittuu aktiiviseksi ja näkymä kertoo, että lataus on tilattu. Ja silti tabletti ei kytke yhtään latausrelettä. Se ei ajasta mitään. Se luo määräaikaan sidotun lataustavoitteen, jota EnergyHub arvioi uudelleen kerta toisensa jälkeen — järjestelmän muun automaation ja turvarajojen rinnalla.

    Tämä ero ajastimen ja tavoitteen välillä on koko osan ydin. Ajastin kytkee releen kello 02.00. Tavoite sanoo ”auto riittävän täyteen kello kuudeksi, mahdollisimman halvalla” ja jättää järjestelmän ratkaistavaksi, milloin ja miten se tapahtuu. Ja kun alkaa purkaa sitä, mitä ”riittävän täyteen” oikeasti tarkoittaa, törmää artikkelin toiseen kantavaan väitteeseen:

    80 prosenttia on laskettava energiamäärä. 100 prosenttia on auton ilmoittama valmistumisehto.

    Ensimmäinen voidaan arvioida melko luotettavasti akun koon, nykyisen varaustason ja lataustehon perusteella. Jälkimmäinen riippuu lisäksi auton omasta akunhallinnasta, lataustehon hidastumisesta loppua kohden, SOC-telemetrian päivitysväleistä ja siitä, milloin auto itse katsoo latauksen päättyneeksi. Nämä eivät ole sama luokka ongelmaa, vaikka käyttöliittymässä ne näyttävät kahdelta vierekkäiseltä napilta.

    Mistä tämä jatkaa

    Osa 13 käsitteli sähköauton perusoptimointia ja jätti määräaikalatauksen nimenomaan tulevaksi työksi. Jo silloin tunnistettiin lähtökohdat: auto on Nissan Leaf, käytettävä akkukapasiteetti on noin 39 kWh, kotilataus on yksivaiheinen noin 3,6 kW / 16 A, SOC saadaan Nissan-integraatiosta, Shelly Pro 3EM mittaa todellisen lataustehon ja Shelly Pro 1 ohjaa fyysistä latausrelettä. Ja mikä tärkeintä: Nissanin oma charging-tieto ei yksin riitä todistamaan, että lataus todella kuluttaa sähköä. Releen tila, mitattu teho ja auton SOC ovat kolme eri asiaa.

    Osa 19 kuvasi käsiohjauksen perusarkkitehtuurin: tabletti ei ohjaa relettä suoraan, vaan lähettää validoidun pyynnön EnergyHubille. Osa 21 yhdistää nämä kaksi lankaa — miten käyttäjän määräaikapyyntö syntyy, miten siitä lasketaan energiantarve ja latausikkuna, miten suunnitelma muutetaan turvalliseksi fyysiseksi ohjaukseksi, miten toteutunut lataus todetaan, ja miksi sadan prosentin tavoite osoittautui erikoistapaukseksi.

    Mitä napin painaminen todella tekee

    Painike ei lähetä komentoa Shellylle. Se kutsuu EnergyHubin HTTP-taustapalvelua reitillä POST /energyhub/api/ev/charge_by, ja taustapalvelu muodostaa manuaalipyynnön:

    { "schema": "eh.manual.v1", "domain": "ev", "intent": "charge_by", "payload": { "target_soc_pct": 80, "deadline": "seuraava klo 06.00 Europe/Helsinki-ajassa" } }

    Pyynnölle luodaan yksilöllinen request_id, eikä selain saa itse määrätä pyynnön elinkaarta tai kirjoittaa suoraan komentotopiceihin. Määräaika lasketaan palvelimella: painike muodostaa määräajaksi seuraavan aamun kello 06.00 Europe/Helsinki-aikavyöhykkeessä. Tabletin kelloon ei tässäkään luoteta.

    Ohjausketju on tuttu edellisistä osista, mutta nyt sen läpi kulkee tavoite eikä kytkin:

    Voimapirtti → HTTP-taustapalvelu → energyhub/manual/request → B3 manual gate validator → EV Manual Request Manager → retained energyhub/manual/intent/ev → Priority Resolver → EV lease → final_gate → energyhub/command/ev/allowed → Home Assistant → Shelly Pro 1 → rele-, teho- ja SOC-readback

    Tärkein arkkitehtoninen kohta on, että manuaalipyyntö tarkoittaa tavoitetta, ei suoraa laitekomentoa. Aktiivinen charge_by saa ohittaa normaalin tilanteen, jossa hinta- tai aurinkologiikka ei muuten juuri sillä hetkellä lataisi autoa. Mutta se ei ohita pääsulake- ja vaihekuormarajoja, vanhentunutta vaihetelemetriaa, järjestelmän safety trip -tilaa, laitteen capability-estoa, final gate -lukkoja, auton poissaoloa eikä fyysisen toteuman tarkistusta. Siksi ”80 % aamuksi” on turvarajojen alainen best-effort-tavoite, ei ehdoton lupaus.

    Kahdeksankymmentä prosenttia kilowattitunneiksi

    Tässä on artikkelin tärkein konkreettinen laskelma, ja se on lohdullisen suoraviivainen. Puuttuva energia saadaan käytettävästä akkukapasiteetista ja SOC-erosta:

    puuttuva energia = 39 kWh × (tavoite-SOC − nykyinen SOC) / 100

    Esimerkiksi 70 prosentista 80 prosenttiin se on 39 kWh × (80 − 70) / 100 = 3,9 kWh. Yksi 15 minuutin latausjakso tuottaa nimellisesti 3,6 kW × 0,25 h = 0,9 kWh verkosta, ja 90 prosentin hyötysuhteella siitä päätyy akkuun 0,81 kWh vartilta. 3,9 kWh vaatii siten laskennallisesti noin viisi varttia, ja planneri lisää yhden puskurivartin — yhteensä kuusi varttia eli 90 minuuttia latausaikaa.

    Nämä eivät ole paperilaskelmia vaan tuotantokoodista erotetun plannerin testituloksia. Planneri käyttää 15 minuutin jaksoja ja seuraavia oletuksia, jotka on hyvä kirjata näkyviin:

    p_ev_battery_usable_kwh = 39 kWh p_ev_charge_power_kw = 3,6 kW p_ev_charge_efficiency = 0,90 p_ev_charge_by_buffer_slots = 1 vartti p_ev_soc_stale_s = 600 s p_ev_full_soc_complete_pct = 98 %

    Halvin ikkuna ei ole sama kuin viimeinen turvallinen aloitus

    Planneri ei vain poimi irrallisia halpoja vartteja. Se laskee, paljonko latausaikaa tarvitaan, kuinka monta varttia ennen määräaikaa on jäljellä, milloin latauksen on viimeistään alettava, ja löytyykö käytettävissä olevista hinnoista ylipäätään toteuttamiskelpoinen suunnitelma. Kaksi aikaleimaa kannattaa pitää erillään, koska ne tarkoittavat eri asiaa:

    Tavoite: 80 % Nykyinen SOC: 70 % Tarvittava lataus: 90 min Suunniteltu alku: 02.00 Suunniteltu loppu: 03.30 Aloitettava viimeistään: 04.30 Määräaika: 06.00

    Suunniteltu alku perustuu halvimpaan löydettyyn latausikkunaan. ”Aloitettava viimeistään” — latest start — on turvallinen takaraja, jonka jälkeen hintojen odottaminen alkaisi vaarantaa itse tavoitteen. Niin kauan kuin latest start ei ole lähellä, planneri saa odottaa halvempaa hetkeä. Kun se lähestyy, odottaminen loppuu ja tavoite menee hinnan edelle.

    Suunnitelma elää olosuhteiden mukana

    Yhden onnistuneen polun näyttäminen antaisi väärän kuvan. Määräaikasuunnitelma on tilakone, ja sen mielenkiintoisimmat tilat ovat ne, joissa asiat eivät mene suoraviivaisesti.

    Tavoite jo saavutettu. Jos auto on 80 prosentissa ja tavoite on 80, tarvittava energia on nolla ja tarvittavat vartit nolla. Järjestelmä ei käynnistä latausta pelkästään siksi, että nappia painettiin.

    Suunnitelma odottaa halvinta ikkunaa. Auto on kotona, SOC on tuore, hintatiedot riittävät ja aikaa on — mutta halvin ikkuna on vasta myöhemmin, joten rele pysyy toistaiseksi pois päältä. Tämä on normaali ”kaikki hyvin, odotetaan” -tila.

    Suunnitelma odottaa hintoja. Määräaika ja energiantarve tunnetaan, mutta huomisen varttihintoja ei vielä ole julkaistu määräaikaan asti. Tässä on tärkeä täsmennys: järjestelmä ei voi valita ”huomisen halvimpia vartteja”, ennen kuin ne hinnat ovat oikeasti saatavilla. Testissä puuttuvat hinnat eivät johtaneet siihen, että auto olisi varmuuden vuoksi käynnistetty heti — planneri jäi odottamaan hintoja niin kauan kuin latest start ei ollut lähellä. Puuttuville hinnoille ei keksitä arvoja.

    Auto ei ole kotona. Planneri voi laskea energiantarpeen ja suunnitelman, mutta ei saa käynnistää fyysistä latausta: blocked_by = ev_not_home. Auton palatessa suunnitelma arvioidaan uudelleen.

    Opportunistinen lataus. Normaali EnergyHub-automaatio voi haluta ladata jo ennen suunniteltua ikkunaa — hinta on jo edullinen, aurinkoa on tarjolla, tai lataus on jo käynnissä eikä sitä kannata katkaista juuri ennen seuraavaa halpaa varttia. Aktiivinen charge_by ei lukitse autoa odottamaan yhtä ennalta määrättyä alkuhetkeä. Se sallii järkevän normaalin latauksen edetä kohti samaa tavoitetta.

    Aikaa ei ole riittävästi. Tämä on turvallisen käyttöliittymän kannalta tärkein tila. Jos SOC on 0 %, tavoite 100 % ja aikaa on jäljellä kaksi varttia, suunnitelma ei ole toteuttamiskelpoinen — 39 kWh vaatisi 750 minuuttia. Silloin järjestelmä ei väitä tavoitteen olevan saavutettavissa. Se aloittaa latauksen heti ja tekee sen minkä ehtii, tilassa best_effort_time_short. Mahdotonta tavoitetta ei muuteta onnistuneeksi suunnitelmaksi vain siksi, että käyttäjä painoi nappia.

    Vanhentunut SOC ei ole sama kuin oikea SOC

    Nissanin SOC ei päivity jatkuvana reaaliaikamittauksena, joten planneri tarkistaa tiedon iän. Nykyinen raja on 600 sekuntia. Jos viimeisin lukema on sitä vanhempi, järjestelmä ei käytä esimerkiksi vanhaa 70 prosentin lukemaa varmana lähtökohtana, vaan pudottaa suunnittelu-SOC:n konservatiivisesti nollaan:

    current_soc_pct = 70 planning_soc_pct = 0 soc_fresh = false soc_estimate = conservative_zero

    Tällöin 80 prosentin tavoitteeseen varataan 31,2 kWh, 40 varttia, 600 minuuttia. Se on erittäin varovainen ratkaisu: se vähentää alilatauksen riskiä mutta voi johtaa tarpeettoman pitkään suunnitelmaan. Ja tässä on hyvä esimerkki siitä, miksi käyttöliittymän on näytettävä muutakin kuin prosenttiluku. ”Varaustaso 70 %, päivitetty 4 min sitten” on aivan eri tieto kuin ”Varaustaso 70 %, tiedon ikä 45 min — suunnitelma tehty varovaisella oletuksella”. Sama luku, eri luotettavuus.

    Kaksi eri tapaa käyttää varttihintoja

    Tässä kohtaa on syytä erottaa kaksi läheistä mutta eri asiaa. Aktiivisen charge_by-pyynnön planneri palvelee käyttäjän nimenomaista tavoitetta: ”haluan auton 80 tai 100 prosenttiin kello kuudeksi”. Se laskee tarvittavat vartit ja valitsee määräajan sisältä edullisen toteutuksen.

    Sen rinnalle on rakennettu erillinen aamukuuden hintaennuste, joka voi ilman aktiivista pyyntöä arvioida, mikä SOC olisi aamukuudelta pelkällä normaalilla hintaohjauksella, montako latausvarttia olisi käytössä, ja tarvittaisiinko erillistä 80 tai 100 prosentin tilausta ollenkaan. Sen skeemassa on kentät kuten forecast_at_06_pct, delivered_kwh, cheap_slots_count, price_window_complete ja confidence. Jos hintajakso ei ulotu kokonaan seuraavaan aamukuuteen, ennuste palauttaa rehellisesti available = false, reason = price_window_incomplete — se ei keksi puuttuville hinnoille arvoja. Tässä taustaennusteessa aurinkolataus on tarkoituksella erotettu vahvistetuista hintavarteista: illalla tehtävässä arviossa huomisen aurinko on epävarmempi kuin julkaistu varttihinta.

    Ratkaisevaa on, että ennuste on havainnointia, ei fyysisen ohjauksen omistaja. Se kertoo mitä todennäköisesti tapahtuu; se ei yksin käynnistä relettä.

    Kaksi korjausta, jotka kuulostavat pieniltä mutta eivät ole

    Toteutuksesta poistettiin vanhentunut kiinteä ”halpa hinta” -kynnys (commit 18993d0). Järjestelmään ei haluttu kahta rinnakkaista totuutta: vanhaa kiinteää hintarajaa ja varttisuunnitelmasta johdettua dynaamista logiikkaa. Tämä on sama sarjan läpi kulkeva opetus kuin osan 18 legacy-ohjauspolku — vaarallista ei ole vain algoritmin vaikeus, vaan se, jos vanhaa ja uutta päätöslogiikkaa jää elämään rinnakkain.

    Toinen korjaus koski seuraavan halvan vartin ennakointia (commit 4eb9943). Alkuperäinen logiikka saattoi tulkita ”onko seuraava vartti halpa” liian laajasti ja käynnistää latauksen yhden vartin etuajassa. Korjaus rajasi merkityksen: jos lataus on jo käynnissä ja seuraava vartti on halpa, sitä voidaan jatkaa ilman turhaa katkaisua — mutta jos lataus ei ole käynnissä, seuraavan halvan vartin tieto ei yksin käynnistä sitä etuajassa. Ero on hiuksenhieno ihmiselle mutta ratkaiseva releelle: ”älä katkaise juuri ennen halpaa varttia” ja ”käynnistä ennen halpaa varttia” kuulostavat melkein samalta, mutta ovat fyysiselle releelle täysin eri komentoja.

    Kahdeksankymppinen valmistui — kahdeksassakymmenessä

    80 prosentin määräaikapolku on toteutettu ja se on saavuttanut terminal-tilan puhtaasti:

    target_soc_pct = 80 completion_soc_pct = 80 observed_soc_pct = 80 state = completed completed_at = 20.7.2026 klo 18.23.25

    Järjestelmä ei vain lakannut pyytämästä latausta. Manual Request Manager kirjoitti retained-intentin valmistuneeksi ja säilytti alkuperäisen request ID:n, tavoitteen, toteutuneen SOC:n, valmistumisajan ja edellisen intentin. Näin käyttöliittymä osaa erottaa toisistaan ”ei aktiivista pyyntöä” ja ”80 % -pyyntö valmistui onnistuneesti” — kaksi tilaa, jotka ilman tätä näyttäisivät samalta tyhjältä.

    Sadan prosentin ensimmäinen yön yli -testi

    Sitten se mielenkiintoisempi tavoite. Ensimmäisessä dokumentoidussa 100 prosentin testissä 20.7. noin klo 21.04 lähtötilanne oli 84 %, tavoite 100 %, completion-raja 98 %, määräaika seuraavana aamuna kuudelta. Planneri laski yhdeksän varttia, 135 minuuttia, suunnitellun alun klo 03.30, latest start klo 03.45 — suunnitelma oli toteuttamiskelpoinen. (Järjestelmän JSONissa aikaleimat olivat UTC-muodossa, joten 00:30Z tarkoitti heinäkuussa Suomen aikaa klo 03.30.)

    Mutta auto ei odottanut kello 03.30 asti. Normaali automaatio löysi jo keskiyöllä edullisen lataustilanteen, ja aktiivinen määräaikapyyntö siirtyi tilaan charge_by_opportunistic_normal_charge. Lataus jatkui noin 3,7 kW teholla. Kello 01.30 paikallista aikaa SOC päivittyi 98 prosenttiin, planneri palautti charge_by_target_met_soc_98_target_100, ja Manual Request Manager merkitsi 100 prosentin pyynnön valmistuneeksi. Heti tämän jälkeen normaali hintalogiikka salli latauksen uudelleen, koska sähkö oli yhä halpaa, ja auto jatkoi oman akunhallintansa ohjaamana täyteen. Aamulla se oli sadassa prosentissa.

    Testi todisti, että koko ketju toimii: pyyntö säilyi aktiivisena, hinnat ja määräaika muodostivat toteuttamiskelpoisen suunnitelman, final gate välitti latausluvan, rele ja todellinen teho vastasivat päätöstä, SOC saavutti 98 prosentin valmistumisrajan, ja auto saavutti myöhemmin todellisen 100 prosentin varaustason.

    Mutta se paljasti myös semanttisen ongelman, ja tämä on artikkelin tekninen käännekohta: charge_by ilmoitti sadan prosentin tehtävän valmistuneeksi jo 98 prosentissa. Varsinainen sataan asti jatkaminen tapahtui tällä kertaa normaalin halvan hinnan automaation ansiosta. Jos normaali automaatio ei olisi juuri silloin jatkanut latausta, ”100 % aamuksi” -pyyntö olisi voinut päättyä järjestelmän mielestä onnistuneena, vaikka auto olisi jäänyt 98 prosenttiin.

    Toinen testi: 48 prosentista aamuksi

    Jotta ensimmäinen ei jäisi sattumaksi, toinen yön yli -testi alkoi paljon matalammalta. Nykyinen SOC 48 %, tavoite 100 %, energiantarve noin 20,28 kWh, tarvittavat vartit 27, tarvittava aika 405 minuuttia, suunniteltu alku noin klo 23.15, loppu klo 06.00, price plan -keskihinta noin 0,0192 €/kWh, feasible = true. Planneri odotti ensin suunniteltua ikkunaa ja siirtyi sitten lataukseen.

    Retained-intent kirjasi:

    state = completed target_soc_pct = 100 completion_soc_pct = 98 observed_soc_pct = 98 completed_at = 22.7.2026 klo 03.47.27

    Aamun myöhempi tilanne oli SOC 100 %, relay_actual = true, power_w = 0, status = relay_on_no_power. Auto oli täynnä, mutta rele jäi sallituksi eikä auto enää ottanut tehoa. Tämä ei ollut päätöksen ja fyysisen toteuman ristiriita vaan normaali täyden auton tila. Toinenkin testi siis vahvisti käytännön toiminnan — ja toisti täsmälleen saman kysymyksen: aktiivinen sadan prosentin intentti valmistui 98 prosentissa.

    Miksi sata prosenttia on eri asia kuin kahdeksankymmentä

    Kahdeksaankymmeneen asti lataus on lähellä lineaarista. Tavallisella AC-latauksella Leaf ottaa suurimman osan matkasta lähes vakiotehon, jolloin SOC-muutoksen voi arvioida melko hyvin kaavalla energia jaettuna tehokkaalla latausteholla. Loppupäässä auton oma akunhallinta ottaa ohjat, ja silloin lineaarinen oletus pettää. Aiemmassa mittauksessa 29.5.2026 tämä näkyi selvästi:

    SOC Mitattu teho 85–95 % noin 3,71–3,78 kW 98 % noin 2,87 kW 100 % noin 0,63 kW 100 % jälkeen pieni teho jatkui vielä pitkään

    Teho alkoi laskea noin 97–98 prosentin kohdalla, loppuvaihe saattoi kestää 60–75 minuuttia, ja auton SOC saattoi näyttää 100 prosenttia ennen kuin latausvirta loppui kokonaan — akunhallinta viimeisteli latausta vielä lukeman saavuttamisen jälkeen. Tästä seuraa kolme eri ”valmis”-määritelmää, jotka eivät toteudu samalla hetkellä: SOC näyttää 100 %, SOC on vähintään valmistumisrajan (esim. 98 %), tai auto ei enää ota merkittävää tehoa.

    Completion-rajaksi valittiin 98 prosenttia juuri siksi, ettei aktiivinen intentti jää roikkumaan viivästyvää Nissan API -päivitystä, hidasta tasapainotusta tai pientä loppuvaiheen tehoa odottamaan. Ratkaisu estää zombie-intentin — mutta samalla se tekee käyttäjälle näkyvästä ”100 %” -lupauksesta epätarkan. Tässä on rehellisyyden paikka: nykyinen tuotantoplanneri ei mallinna koko taper-käyrää. Sen todennettu logiikka nojaa 39 kWh kapasiteettiin, 3,6 kW tehoon, 90 prosentin hyötysuhteeseen, yhteen puskurivarttiin ja sadan prosentin pyynnön 98 prosentin completion-rajaan. Mitattu loppupään hidastuminen selittää, miksi sadalle prosentille tarvitaan oma valmistumispolitiikka — mutta itse valmistuminen ratkaistaan tuolla completion-rajalla, ei tarkalla käyrämallilla.

    Milloin auto oikeasti lataa

    Järjestelmä ei päättele latausta pelkästä releestä. Rele pois ja teho 0 W on relay_off — lataus ei ole fyysisesti sallittu. Rele päällä ja teho noin 3,6–3,8 kW on charging — auto ottaa oikeasti tehoa. Mutta rele päällä ja teho 0 W on oma tilansa, relay_on_no_power, joka voi tarkoittaa montaa asiaa: auto on jo täynnä, autoa ei ole kytketty, auton oma ajastus estää latauksen, auto ei juuri nyt pyydä virtaa, käynnistymisessä on viive, tai fyysisessä polussa on ongelma. Tätä tilaa ei saa automaattisesti nimetä viaksi. Sen merkitys riippuu SOC:sta, kotonaolosta, telemetrian iästä ja siitä, onko latauksen juuri pitänyt käynnistyä. Sadan prosentin testin aamuna SOC 100 %, rele päällä, teho 0 W oli täysin normaali täyden auton tila.

    Planneri päättää, milloin latausta tarvitaan. Final gate päättää, saako päätös muuttua fyysiseksi komennoksi. Priority Resolver ei kirjoita suoraan Shellylle, vaan sen päätös muutetaan lyhytikäiseksi EV leaseksi, jonka final gate tarkistaa — onko lease voimassa, haluaako se latauksen päälle, onko turvalukkoja, onko ohjauspolku fyysinen. Vasta tästä muodostuu energyhub/command/ev/allowed, ja vasta sitten Home Assistant toteuttaa releohjauksen.

    Kun pääsulake voittaa täydellisen hintasuunnitelman

    Leaf lataa yhdeltä vaiheelta noin 16 ampeerilla, ja talon pääsulakkeet ovat 3 × 25 A. Juuri L1-vaiheen muu kuorma ratkaisee, voidaanko lataus käynnistää. Kun rele on pois päältä, start admission laskee mitatun nykyisen vaihevirran plus Leafin odotetun 16 A kuorman, ja jos ennuste ylittää rajan, käynnistys estetään tai siirtyy. Tämä voi tuottaa tilanteen, jossa hinnan perusteella valittu vartti olisi optimaalinen ja aikaa olisi teoriassa riittävästi, mutta pääsulakevahti estää latauksen — ja plannerin suunnitelma eroaa toteumasta turvallisuussyystä. Silloin käyttäjälle ei riitä ”Lataus suunniteltu 02.00–04.30”, vaan tarvittaessa on kerrottava ”Lataus viivästyy: vaihekuorma estää käynnistyksen, tavoite on edelleen mahdollinen” — tai pahemmassa tapauksessa ”Tavoitetta ei ehkä saavuteta: turvallisuusraja on estänyt latausta 75 min, paras mahdollinen lataus jatkuu”.

    Restart-kestävä tavoite ei saa olla ikuinen tavoite

    charge_by tallennetaan retained manual intentinä, joten Node-REDin tai palvelimen uudelleenkäynnistys ei unohda, että auto piti ladata aamuksi. Sama retained-tila on kuitenkin riski, jonka tämä sarja on nähnyt aiemminkin: ilman määräaikaa tai terminal-tilaa vanha latauspyyntö voisi nousta restartin jälkeen zombina takaisin eloon. Siksi intentillä on yksilöllinen request ID, deadline, aktiivinen tila, completed-tila, cleared-tila, havaittu valmistumis-SOC, valmistumisaika ja edellinen intentti. Periaate tiivistyy yhteen lauseeseen: restart-kestävä tavoite ei saa olla ikuinen tavoite.

    Mitä Voimapirtin pitäisi luvata

    Kaikki edellä johtaa yhteen käyttöliittymäkysymykseen: mitä paneeli saa sanoa? Ero on kolmijakoinen — tavoite, ennuste ja vahvistettu toteuma ovat kolme eri asiaa, eikä niitä saa esittää yhtenä.

    Aktiivisen latauspyynnön ennusteessa on kentät kuten forecast_at_deadline_pct, estimated_finish_at, planned_start_at, confidence, delivered_kwh ja slots. Mukana on myös kaksi kenttää, joiden ero on olennainen: assumption_connected = true ja connected_verified = false. Järjestelmä voi suunnitella sillä oletuksella, että auto on kytketty, mutta sillä ei ole vielä kaikissa tilanteissa varmaa erillistä tietoa kaapelin kytkennästä. Siksi käyttöliittymä ei saa sanoa ”Auto on varmasti valmis klo 06.00” vain siksi, että laskennallinen suunnitelma näyttää hyvältä. Rehellisempiä muotoiluja ovat ”80 % arvioitu klo 06.00 — auton oletetaan olevan kytketty” tai ”Tavoite saavutetaan nykyisellä suunnitelmalla, mikäli auto pysyy kytkettynä eikä turvaraja keskeytä latausta”.

    Demossa sadan prosentin tilauksen teksti on ollut ”Vähintään 100 % määräaikaan mennessä”, ja se ei ole hyvä ilmaisu. Yli sataa prosenttia ei ole, järjestelmän oma completion-raja on 98 prosenttia, ja kyse on tavoitteesta tai ennusteesta, ei absoluuttisesta takuusta. Parempi olisi ”100 % tilattu aamuksi — tavoite klo 06.00, suunnitelma valmis”, tai kun luottamus on heikompi, ”100 % tavoite aamuksi — arvio 98–100 %, loppulatauksen kesto vaihtelee”. Käyttöliittymän eri tilat — odottaa hintoja, best-effort, auto ei kotona, pääsulake estää, loppuvaihe käynnissä, auto täynnä — ansaitsevat kukin oman ihmiskielisen tekstinsä sen sijaan, että ne kaikki näyttäisivät ”lataus tilattu”.

    Opit

    Määräaikalataus ei ole ajastin, vaan tavoite turvarajojen alla. Nappi ei kytke relettä kello 02.00. Se luo tavoitteen, jonka järjestelmä arvioi uudelleen joka kierroksella muun automaation ja turvarajojen rinnalla — ja jonka turvaraja saa perustellusti estää.

    80 % on laskettava, 100 % on neuvoteltava. Kahdeksankymmenen prosentin energiantarpeen saa akun koosta ja SOC-erosta melko luotettavasti. Sadan prosentin kohdalla auton oma akunhallinta, taper-käyrä ja telemetrian viive tekevät ”valmiista” määrittelykysymyksen, ei laskutoimituksen.

    Valmistumisraja on kompromissi, ei totuus. 98 prosentin completion-raja estää zombie-intentin mutta tekee sadan prosentin lupauksesta epätarkan. Molemmat testit päätyivät aamulla oikeaan sataan prosenttiin, mutta molemmissa intentti merkittiin valmiiksi jo 98:ssa — ja toisella kertaa se olisi voinut jäädä siihen, ellei normaali halvan hinnan automaatio olisi jatkanut.

    Tavoite, ennuste ja toteuma ovat kolme eri asiaa. Sama ”komento ≠ toteuma” -periaate, joka on kulkenut tämän sarjan läpi releistä lähtien, laajenee tässä kolmeksi: mitä käyttäjä pyysi, mitä järjestelmä arvioi tapahtuvan, ja mitä oikeasti mitattiin. Käyttöliittymä saa näyttää kaikki kolme — mutta ei sekoittaa niitä yhdeksi lupaukseksi.

    Rele päällä ei tarkoita, että auto lataa. relay_on_no_power on normaali tila täydelle autolle. Todellinen teho on aina tarkistettava, eikä yksikään tila saa muuttua ”viaksi” ilman, että SOC, kotonaolo ja telemetrian ikä on otettu huomioon.

    Mitä vielä avoinna

    Rehellisyyden nimissä osa isoimmista kysymyksistä on tarkoituksella jätetty auki. Miten sadan prosentin valmistuminen pitäisi lopulta määritellä? Todennäköisesti yhdistelmänä: pyyntö on valmis, kun tuore SOC on 100 %, tai kun SOC on vähintään 98 % ja auto ottaa alle 200 W yhtäjaksoisesti määritellyn ajan ja varattu loppulatauspuskuri on kulunut. Tähän liittyy kysymys, pitäisikö sadan prosentin tavoitteen pysyä aktiivisena 98 prosentin jälkeen omana finishing-tilanaan, jolloin Voimapirtti näyttäisi ”98 % · loppulataus käynnissä” sen sijaan että hyppäisi suoraan valmiiseen.

    Lisäksi auki ovat toteutuneen energian perusteella tarkentuva ennuste (Shellyn mittaama latausenergia täydentämässä viivästyvää Nissan-SOC:ta, ei korvaamassa sitä terminal-päätöksessä), kytkentätiedon varsinainen vahvistus, sekä DST-, restart- ja turvalukko-soak-testit. Aamukuusi muodostetaan jo oikein Europe/Helsinki-aikavyöhykkeessä eikä ”nyt plus tuntimäärä” -laskuna — kesä- ja talviaikaan siirtyvässä yössä vuorokaudessa on eri määrä vartteja — ja Voimapirtin fixtureissä on jo 100 vartin DST-päivä, mutta pitkäkestoinen määräaikapolun DST-hyväksyntätesti on vielä ajettava.

    Määräaikalatauksen vaikein osa ei lopulta ollut halvimpien varttien järjestäminen. Vaikeinta oli päättää, mitä ”valmis” tarkoittaa järjestelmässä, jossa käyttäjä pyytää 100 prosenttia, auton API ilmoittaa 98, latausteho alkaa laskea — ja akku on silti aamulla oikeasti täynnä.


    Seuraavassa osassa siirrytään ohjaamisesta mittaamiseen. Koko tämä sarja on rakentanut järjestelmää, joka tekee päätöksiä — mutta yksikään päätös ei ole parempi kuin data, jonka varaan se rakennetaan. Siitä, miten raakatelemetriasta tehdään luotettavaa aineistoa, miten lämmitys- ja käyttövesijaksot erotetaan toisistaan ja miksi hiljainen data-aukko on vaarallisempi kuin näkyvä virhe, kertoo osa 22.

  • 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.

  • 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.

  • 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.

  • 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.