
Tämä liite kokoaa Osa 15:n syvätekniset yksityiskohdat: vikatilojen tarkat kuvaukset, korjausten koodit, commit-tunnisteet ja todennustavat. Itse artikkeli kertoo tarinan ja periaatteet; tämä liite on tarkoitettu lukijalle, joka haluaa nähdä, mitä koodissa todella tapahtui, tai joka tekee vastaavaa työtä omaan järjestelmäänsä.
Järjestelmäarkkitehtuuri lyhyesti: Home Assistant (integraatiokerros), Node-RED (optimointimoottori), MQTT/Mosquitto (viestiväylä), InfluxDB (historia), Grafana (visualisointi). Node-RED tekee päätökset; Home Assistant toteuttaa ne porttina laitteille. Viisikerroksinen datamalli: fyysiset laitteet → raakasensorit → energyhub_*-abstraktioentiteetit → Node-RED-optimoija → toimilaitteet.
FMA:n rakenne
17 vikatilaa (F1–F17), seitsemän erää. Kolme perusvikaluokkaa:
- Stale retained -viesti (zombie-latch)
- Hiljainen epäonnistuminen (silent failure)
- Kahden omistajan ristiriita (F7)
Toteutuksen kuri: jokainen korjaus oma commit, oma testi, kaatuva muutosskripti (sys.exit(1) jos kohdetekstiä ei löydy), ChatGPT-katselmointi. Toteutusloki: Era4_toteutus.md.
Vikaluokka 2 — hiljainen epäonnistuminen, esimerkkikorjaukset
F4 — cheap_slots-tyyppisuojaus
Alkuperäinen laskenta käytti || 36 -suojaa, joka suojaa nollalta mutta ei negatiiviselta eikä NaN-arvolta. Tyhjä hintalista johti tilaan sorted[0] = undefined → threshold = undefined → prices[x] <= undefined aina false → EV ei lataudu halvalla, hiljaa.
Korjaus erottaa kolme tilannetta — virheellinen arvo, tyhjä lista, validi laskenta:
javascript
const rawCheapSlots = Number(global.get('m_cheap_slots'));
const cheapSlotsN = (Number.isFinite(rawCheapSlots) && rawCheapSlots > 0)
? Math.floor(rawCheapSlots) : 36;
const sortedPrices = [...pricesNow].sort((a, b) => a - b);
s.cheapThreshold = sortedPrices.length > 0
? sortedPrices[Math.max(0, Math.min(cheapSlotsN - 1, sortedPrices.length - 1))]
: (global.get('m_cheap_threshold') ?? null);
Lisäksi isCheapNow / isCheapNext tarkistavat cheapThreshold !== null, jottei null johda virheelliseen vertailuun. Commit 82ffffb.
Tärkeä yksityiskohta defaulteissa: käytä ?? (nullish coalescing), älä koskaan ||, parametrien oletusarvoissa. || kohtelee nollaa epätotena, mikä on väärin numeerisille parametreille, joiden legitiimi arvo voi olla 0.
F6 — päivätilan nollaus päivämääräpohjaisesti
Ajastettu nollaus klo 00:05 jäi väliin, jos Node-RED oli alhaalla tasan silloin. dhw_heated_today ja floor_heated_today jäivät edellisen päivän true-arvoon → ei lämmitystä seuraavana päivänä, hiljaa.
Korjaus: päivämääräpohjainen nollaus joka tarkistetaan jokaisella resolver-syklillä, sijoitettuna Priority Resolverin alkuun ennen emergency-haaraa. Kriittinen yksityiskohta: jos nollaus olisi emergency-haaran jälkeen, return msg emergency-tilassa estäisi päivänvaihdon nollauksen. Nollaus on pelkkää tilanylläpitoa (ei fyysistä ohjausta), joten se on turvallista tehdä ennen emergency-tarkistusta.
javascript
const today = s.dateFi || '';
const lastDate = global.get('eh_daily_reset_date') || '';
if (today && today !== lastDate) {
global.set('dhw_heated_today', false);
global.set('dhw_failed_today', false);
global.set('dhw_cancelled_today', false);
// ... floor_heat_state, floor_heated_today, m_comfort_wheel_last_cmd
global.set('eh_daily_reset_date', today);
}
s.dateFi lasketaan Suomen ajassa Intl.DateTimeFormat-pohjaisesti. Yhteinen eh_daily_reset_date nollaa sekä DHW:n että floorin. Selviää restartista, koska tarkistus on joka syklissä eikä kerran vuorokaudessa. Commit cb155ec.
F10 — DHW retry-suoja ja keskeytyksen erottelu
Boost-timeoutin laukaistessa asetettiin vain dhw_boost_active = false, mutta trigger-ehdot (aika, lämpötila, vienti) jäivät voimaan → boost käynnistyi heti uudelleen. Korjaus erottaa kolme lopputulosta:
- Timeout →
dhw_failed_today = true(estää uuden yrityksen samana päivänä) - Ulkoinen keskeytys (capability poistui / legionella alkoi) →
dhw_cancelled_today = true - Trigger-suojat:
!dhwFailedToday && !dhwCancelledToday && !hpLegionellaActive
Keskeytystarkistus on ongoing-boost-haaran ensimmäinen ehto, joten boost ei jää päälle vaikka tilanne muuttuu kesken jakson. Commit a5bb42b.
F8 — comfort wheel readback-vahvistus
m_comfort_wheel_last_cmd päivitettiin lähetyshetkellä, ei vahvistushetkellä. Jos Thermia ei ottanut komentoa vastaan (Modbus-virhe), last_cmd sanoi silti ”lähetetty” → ei uudelleenlähetystä → comfort wheel jäi väärään arvoon, hiljaa.
Korjaus on pending/confirmed-tilakone:
target= Priority Resolverin haluama arvopending= lähetetty, ei vielä vahvistettu (arvo + aikaleima)readback= Thermian todellinen Modbus-arvo (m_hp_comfort_wheel_readback)last_confirmed= viimeksi vahvistettu (readback vastasi, deadband 0,5 °C)- retry 5 min jos pending ei vahvistu
Nimi last_cmd → last_confirmed, koska vanha nimi houkutteli sekoittamaan lähetetyn ja vahvistetun. Commit 97612c8.
Vikaluokka 3 — kahden omistajan ristiriita (F7) ja sen kerrokset
Tämä on artikkelin ”sama virhe kolmessa kerroksessa”. Kronologia:
Kerros 1: moodin kaksoisomistus — commit cc4cd49
sys_operating_mode kirjoitettiin kahdesta lähteestä nodessa ”Päivitä flow-konteksti”: energyhub/system/mode (oma topic) ja energyhub/system/capabilities.payload.mode (koontiviestin mode-kenttä). Kolme lukijaa luki samaa muuttujaa. Ristiriidassa viimeksi saapunut viesti voitti → epädeterministinen moodi.
Korjaus: system/mode on moodin ainoa omistaja. Perustelu: oma dedikoitu topic, julkaistaan retain: true, broker palauttaa retained-arvon käynnistyksessä → ei tarvita capabilities-fallbackia. Poistettiin rivi global.set('sys_operating_mode', payload.mode) capabilities-haarasta.
Kerros 1.5: HA-puolen kanoninen julkaisija — commit ad76dd8
F7:n puuttuva puolisko. HA julkaisi system/mode-topicin vain emergency-recoveryssa (arvolla normal). Normaali operaattorin moodinvaihto meni vain capabilities-viestiin, jota Node-RED ei enää lue moodille → moodi olisi jäänyt jumiin. Korjaus: uusi automaatio energyhub_publish_canonical_mode julkaisee energyhub/system/mode (retained) aina kun input_select.sys_operating_mode muuttuu.
Kerros 2: Node-RED-lukijan zombie — commit 610db7e
Sama F7-kuvio turvakatkaisussa. Node ”Päivitä flow-konteksti” luki capabilities-haarassa:
javascript
global.set('sys_safety_trip', payload.sys_safety_trip);
Kun vanha retained capabilities kantoi sys_safety_trip: true, se palautti turvalukon globaaliin vaikka oma topic (energyhub/system/safety_trip) oli nollattu → zombie-latch, EV+EVU jäivät estoon. Korjaus: poistettiin rivi. sys_safety_trip omistaa vain oma topic.
Kerros 3: capabilities-payloadin julkaisu — commit 16ec467
Vaikka lukija oli korjattu, HA julkaisi yhä sys_safety_trip-kentän capabilities-payloadissa (40_optimization.yaml). Kenttä oli enää raportointia — kukaan ei ajanut sitä mooditilaan — mutta se rikkoi periaatetta ja jätti zombie-riskin tulevaisuuteen. Poistettiin kaksi riviä capabilities-automaatiosta: payload-kenttä ja vastaava trigger-rivi. Automaatiot mqtt_safety_trip_sync ja mode_emergency_recovery säilyivät koskemattomina — ne ovat oikea MQTT→HA-omistusketju.
Live-todennus: reload (Developer Tools → YAML → Reload Automations), c_pv_curtail_allowed flipattu pakottamaan tuore julkaisu, mosquitto_sub vahvisti ettei tuore capabilities-viesti enää sisällä kenttää.
Turvakatkaisun kolme juurisyytä — commit 610db7e
Artikkelin huipentuma yksityiskohtina. Yksi tapahtuma (sauna + EV → ylivirta → trip), kolme estävää vikaa palautumiselle:
- Dead-code deadlock Sulakevalvonnassa:
if (safetyTripActive) returnteki auto-reset-haaran saavuttamattomaksi. Vapautuslogiikka oli ehdon jälkeen → ei koskaan ajettu niin kauan kuin trip oli päällä. - Zombie-latch capabilities-MQTT:stä (kerros 2 yllä): vanha retained
sys_safety_trip: truepalautti lukon manuaalisen resetin jälkeen. - Ennenaikainen vapautus: auto-reset lähetti
ev_allowed: truevälittömästi, mikä riski laukaista uuden tripin jos sauna oli yhä päällä.
Kaikki korjattu kaatuvilla Python-skripteillä (sys.exit(1)-turvatarkistukset) ja read-back-verifioinnilla. Manuaalinen reset-prosessi ilman SSH:ta dokumentoitu: MQTT energyhub/system/safety_trip → false (retained) + energyhub/command/mode → normal; HA Developer Tools input_boolean/input_select -flippaukseen.
EVU emergency zombie -juurisyy (sama tapahtumaperhe)
28 A:n L1-piikki → vanha HA sys_fuse_guard_trip -automaatio (automations.yaml) triggeröi input_boolean.sys_safety_trip → emergency mode retained MQTT:nä → Priority Resolver pakotti hpMode: block → SG1-rele jäi EVU-asentoon → zombie säilyi tripin selvittyä. Fix: vanha fuse guard -automaatio poistettiin, zombie tyhjennettiin mosquitto_pub:lla. Node-RED Safety Guardian -kynnykset vahvistettu: warn 30 A / reduce 36 A / trip 40 A.
Huomio releen semantiikasta: c_hp_evu_allowed capabilitiesissa on lupaflagi, ei releen ohjaussignaali. Todellinen HP-moodi tulee non-retained-topicista energyhub/command/hp/mode Node-REDin Command Layeriltä. EVU-jumi oli emergency-zombien oire, ei itsenäinen bugi.
Telemetrian validointi — commit 61fd3b4
Periaate ”rikkinäinen/puuttuva arvo ei saa naamioitua validiksi”. Pahin yksittäinen kohta oli const runningPriority = payload.running_priority ?? 0 — puuttuva arvo pakotettiin nollaan, jolloin === 3 (DHW) ja === 7 (legionella) menivät epätodeksi ja Thermian tila katosi hiljaa. 0 ei ole edes dokumentoitu Thermian tila.
Apufunktiot: ehNum (Number.isFinite-validointi), ehSetIfValid (aseta vain jos kelvollinen), ehBadCount (harvennettu diagnostiikka). Domain-kohtaiset säännöt: kelvoton power_w hylkää koko grid-päivityksen ja säilyttää edellisen (sulakevalvonta ja EV eivät saa väärää nollaa); tyhjä hintalista ei yliaja edellistä hyvää listaa; running_priority säilyttää edellisen tilan eikä pakota nollaan. Lisätty aikaleimat stale-telemetrian havaitsemiseen.
Komento vs. toteuma — F5 ja F17
Koko FMA:n läpileikkaava periaate: päätös perustuu mitattuun/vahvistettuun toteumaan, ei oletettuun komentoon.
F5a/F5b (commitit 40d1af8, bc68944): EV-päättely toteumaan. ev_relay_on = desired (komento), m_ev_relay_on = actual (mitattu Shellyltä). ev_on_since/ev_off_since perustuvat toteuman muutoshetkeen, jotta minRuntimeMs ja viiveet perustuvat todelliseen latausaikaan, ei komennon kaikuun. Boolean-normalisointi ehAsBool (hyväksyy true/’on’/’true’/1).
F17 (commitit 9225e79, 6587e85): päätös-vs-toteuma-havaintokerros. Yhteinen actuals-objekti Priority Resolverissa. EV-status: idle / charging / relay_on_no_power / decided_no_relay. DHW-status käyttää yhdistelmäehtoa, ei pelkkää immersionia (katselmoijan tärkein tarkennus):
running_priority === 3 AND (shelly_total_w > 800 OR compressor_pct > 5 OR immersion_active)
Debounce 120 s: poikkeama vasta kun boost on ollut aktiivinen yli käynnistysviiveen. actuals julkaistaan observability/decisions-kanavaan (MQTT + InfluxDB). Vain havaintokerros — ei muuta päätöksiä, ei lähetä komentoja. F17 vaihe 2 (aktiiviset reaktiot poikkeamiin) odottaa tuotantodataa.
Erä A — rajapintakorjaukset (staattisen katselmoinnin löydöt)
Katselmoijan staattinen arkkitehtuurikatselmus nosti esiin rajapintatason poikkeamia, jotka eivät olleet enää päätöslogiikassa vaan HA↔Node-RED-rajapinnassa ja aikalaskennassa.
Aikavyöhykebugit (A2, A3): kontti pyörii UTC:ssä, mutta Nordpool-hinnat ja Open-Meteo-ennuste ovat Suomen ajassa. getHours()*4 antoi UTC-tunnin → hinnat luettiin ~3 h väärästä slotista → väärät latauspäätökset. Korjaus: ehHelsinkiSlot()-apufunktio (Intl-pohjainen). Aikavyöhykebugit piiloutuvat useaan kohtaan; sama perhe löytyi neljästä eri paikasta.
Null-suoja väärällä kerroksella (A4): export_w:n null-suoja 40_optimization.yaml:ssa oli tehoton, koska lähdesensori (20_measurements.yaml) käytti sisäisesti float(0)-fallbackia → ei koskaan palauttanut unavailable-tilaa ylemmälle kerrokselle. Periaate: null-suoja pitää lisätä siihen kerrokseen, missä unavailable ensin syntyy tai katoaa, ei mihin tahansa kohtaan ketjussa.
Orpo entity_registry -merkintä (A5): sensor.m_hp_shelly_total_power_w oli rekisteröity core.entity_registry:ssä (platform: template) mutta toteutusta ei löytynyt mistään YAML-tiedostosta eikä .storage-helperista → pysyvästi unavailable, ei virhettä lokissa, täysin hiljainen vika. Tämä naamioitui aluksi ”InfluxDB-kirjoitus puuttuu” -ongelmaksi. Korjaus: lisättiin puuttuva template-sensori. Oma vikaluokkansa: rekisteri sanoo template, mutta toteutus puuttuu kokonaan.
Toteutustavasta opittua
- Kaatuva skripti: kun
flows.json-muokkaus tehdään merkkijonokorvauksella, skriptin pitääsys.exit(1)jos kohdetekstiä ei löydy. Muuten voi luulla tehneensä muutoksen, vaikka flow jäi ennalleen. - Erilliset commitit semantiikan muutoksille: käyttäytymistä muuttava korjaus pidetään erillään puhtaasta bugikorjauksesta.
git add -Aon vaarallinen: aina eksplisiittinengit add <tiedosto>. Yhdessä tapauksessa-Aveti takaisin 22 aiemmin poistettua.bak-tiedostoa, koska.gitignoreei kattanut kaikkia nimimuotoja.- Tarkista git log ennen kuin oletat työn olevan tekemättä: osa korjauksista oli jo tehty aiemmassa commitissa mutta jäänyt kirjaamatta lokiin → päällekkäistä analyysiä.
git log -S "<hakusana>"(pickaxe) on nopea tarkistus. - Context-tiedoston tallennusviive: Node-REDin
global.jsonpäivittyy ~30 s viiveellä; heti restartin jälkeen luettu arvo voi olla vanhentunut. MQTT-state-kanava on luotettavampi live-mittari. - Commit-viestit: vältä
!-merkkejä (bashin history-expansion).
FMA-tilanne
Kaikki 17 failure modea (F1–F17) käsitelty seitsemässä erässä, plus Erä A:n rajapintakorjaukset (A1–A6). F17 vaihe 2 (aktiiviset reaktiot poikkeamiin) ja capabilities-payloadin viimeinen siivous (16ec467) viimeisteltiin myöhemmin. Toteutusloki kaikkine commit-tunnisteineen: Era4_toteutus.md.