Case: oma talo — Osa 18

Written by

in

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

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

Ilta 12.7.: kellontarkka sahaus

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

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

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

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

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

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

Aamu: todisteet talteen ennen kuin mihinkään kosketaan

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

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

Kolme hypoteesia — kaksi väärää

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

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

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

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

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

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

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

Miksi admission luuli relettä sammuneeksi

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

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

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

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

[warn] F17 poikkeama: ev=decided_no_relay

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

Miksi suojat eivät suojanneet

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

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

Koodista löytyi vielä tuttu vikaluokka pahentamassa tilannetta:

javascript

const relayActual = payload.shelly_relay_on || false;

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

Hiljainen ohitus #4: ohjauspolku oli palautunut legacyyn

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

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

Korjausketju: viisi committia, viisi periaatetta

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

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

Jokainen commit vastaa yhtä periaatetta:

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

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

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

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

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

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

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

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

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

Täysi lataussessio: korjausten lopullinen hyväksyntä

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

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

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

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

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

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

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

Opit

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

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

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

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

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


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

Piditkö artikkelista?

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

Seuraa blogia Blogit.fi:ssä