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.