Hintaohjauksen flow Node-REDissä

Tämä on yhdeksäs osa sarjasta joka dokumentoi EnergyHub-järjestelmän rakentamisen vanhaan rintamamiestaloon. Edellisessä osassa käytiin läpi MQTT-viestintäkerros — miten tieto kulkee komponenttien välillä. Tässä osassa katsotaan mitä Node-RED tekee sillä tiedolla: miten hintaohjauksen päätöslogiikka rakentuu käytännössä.
Node-RED on EnergyHubin optimointikerros. Se vastaanottaa kaiken telemetrian, ylläpitää tilannekuvaa ja tekee päätökset. Mutta Node-RED ei ole yksi iso automaatio — se on kolme erillistä flow’ta joilla on omat vastuunsa.
State Collector kerää kaiken datan ja pitää tilannekuvan ajan tasalla. Se ei tee päätöksiä.
Decision Engine ajaa päätöslogiikan minuutin välein. Se kysyy: mitä pitää tehdä, kenen vuoro on, miksi.
Safety Guardian valvoo sulakerajoja 10 sekunnin välein ja reagoi välittömästi. Se ei optimoi — se suojelee.
Tässä osassa keskitytään Decision Engineen — siihen miten hintatieto muuttuu päätöksiksi.
State Collector — tilannekuvan ylläpito
Ennen kuin Decision Engine voi päättää mitään, sen täytyy tietää missä ollaan. State Collector kuuntelee kaikkia energyhub/#-topiceja ja päivittää flow-kontekstin aina kun uusi viesti saapuu.
Global context toimii Node-REDin yhteisenä tilamuistina — kaikki flow’t voivat lukea ja kirjoittaa siihen. Avainten etuliitteet kertovat mistä on kyse: m_ on mitattu arvo (Layer 2 -abstraktio), c_ capability- tai ohjaussignaali, p_ keskitetty parametri, sys_ järjestelmän tila. Se sisältää kaiken tarvittavan:
javascript
global.get('m_grid_power_w') // verkkoteho tällä hetkellä (mitattu)
global.get('m_pv_power_w') // aurinkotuotanto (mitattu)
global.get('m_spot_price_eur_kwh') // spot-hinta
global.get('m_phase_l1_a') // L1-vaihevirta
global.get('m_hp_tap_water_top_c') // käyttöveden lämpötila
global.get('sys_operating_mode') // järjestelmän tila
global.get('c_ev_charge_allowed') // onko EV-lataus sallittu
global.get('p_fuse_limit_a') // sulakearvo (parametri)
State Collector myös laskee johdetut arvot jotka helpottavat päätöksentekoa:
javascript
s.maxPhaseA = Math.max(Math.abs(l1), Math.abs(l2), Math.abs(l3))
s.solarExcessW = Math.max(0, pvPowerW - Math.max(0, gridPowerW))
s.isSolarExcess = s.solarExcessW > solarExcessThresholdW
s.isCheapPrice = s.spotPrice < cheapPriceThreshold
s.isNegativePrice = s.spotPrice < negativePriceThreshold
s.isPeakLoad = s.maxPhaseA > peakLoadThresholdA
s.isTapWaterLow = s.tapWaterC < tapWaterMinC
Nämä boolean-arvot tekevät päätöslogiikasta luettavaa. s.isSolarExcess on helpompi ymmärtää kuin pvPowerW – Math.max(0, gridPowerW) > 2000. Kaikki kynnysarvot — solarExcessThresholdW, cheapPriceThreshold, peakLoadThresholdA ja muut — luetaan keskitetystä EH Parameters -nodesta, ei kovakoodattuna jokaiseen funktioon. Tämä on sarjassa aiemmin esitelty periaate: yksi paikka jossa kaikki säädettävät arvot elävät.
Decision Engine — päätöslogiikka
Decision Engine käynnistyy minuutin välein. Se lukee tilannekuvan, ajaa Priority Resolverin ja julkaisee komennot MQTT:hen.
Rakenne on yksinkertainen:
[Inject: 1 min] → [Lue tila] → [Priority Resolver] → [Julkaise komennot]
↓
[Kirjaa päätökset]
Priority Resolver on koko järjestelmän tärkein funktio. Se käy tilanteen läpi kiinteässä järjestyksessä — ensimmäinen osuma ratkaisee. Järjestys ei ole sattuma: ylimpänä ovat turvallisuus ja erikoistilat, alimpana talous.
Priority Resolver käytännössä
Koodi etenee hierarkkisesti. Turvallisuus ensin, talous viimeisenä. Tasoja on käytännössä useampi kuin tässä näytetään — täysi ketju on emergency → manual_override → fallback → peak protection → vacation → normaali optimointi. Manual_override palauttaa tyhjän päätöksen (ei muutoksia mihinkään) ja fallback ajaa minimiparametreilla, kun tilannekuva on puutteellinen. Keskitytään tässä turvallisuuden ja talouden kannalta oleellisimpiin tasoihin.
Taso 1: Emergency
javascript
if (s.safetyTrip || s.mode === 'emergency') {
return {
ev: false,
hpMode: 'block',
hpImmersion: false, // lisävastus pois
pvCurtailPct: null, // aurinko saa tuottaa vapaasti
saunaAllowed: false, // kiuas estetty
comfortWheel: 17 // lattialämmitys minimiin
};
}
Emergency-tilassa ei optimoida. Kaikki ei-kriittiset kuormat alas — EV, lisävastus, kiuas — ja lattialämmitys minimiin. Aurinko jätetään vapaaksi, koska se vain pienentää verkkokuormaa.
Taso 2: Peak protection
javascript
if (s.mode === 'peak_protection' || s.isPeakLoad) {
decisions.ev = false;
decisions.saunaAllowed = false;
decisions.hpImmersion = false;
decisions.hpMode = s.hpCompressorPct > 60 ? 'block' : 'normal';
decisions.pvCurtailPct = null; // ei rajoiteta — aurinko pienentää verkkokuormaa
decisions.comfortWheel = 17;
return decisions;
}
Sulake uhkaa — pudota kuormat. Huomaa: PV:tä ei rajoiteta koska se pienentää verkkokuormaa, ei kasvata sitä.
Taso 3: Normaali optimointi
Tässä haarassa tehdään varsinainen hintaohjaus. Jokainen kuorma käsitellään erikseen, ja turvallisuustarkistukset tulevat aina ensin.
EV-latauksen päätös alkaa kahdella suojalla ennen hintalogiikkaa:
javascript
if (s.phasesStale) {
decisions.ev = false; // F15: ilman tuoretta vaihevirtatietoa EV voisi
// ylikuormittaa sulakkeen huomaamatta
} else if (s.evManualOverride) {
decisions.ev = true; // manuaaliohitus pakottaa latauksen päälle
} else if (!s.cap.evAllowed) {
decisions.ev = false; // capability estää
} else if (s.mode === 'solar_maximize' && s.isSolarExcess) {
decisions.ev = true; // aurinkoylijäämä — käytä itse
} else if (s.isVeryCheap) {
decisions.ev = true; // alle 1 snt/kWh — lataa aina
} else if (s.isCheapPrice || s.mode === 'cheap_energy') {
decisions.ev = true; // halpa hetki — lataa
} else if (s.mode === 'guest') {
decisions.ev = true; // vierastila — mukavuus prioriteetiksi
} else {
// normaali tilanne — laske onko perusteltua, viiveiden kanssa
decisions.ev = s.isCheapPrice || s.isSolarExcess;
}
Ensimmäinen tarkistus on vaihevirtadatan tuoreus. Jos Safety Guardian ei ole saanut tuoretta vaihevirtatietoa, EV-lataus estetään — ilman tätä lataus voisi ylikuormittaa sulakkeen ilman että järjestelmä huomaa. Tämä sama suoja toistuu vielä komentokerroksessa viimeisenä porttina.
Lämpöpumpun päätös:
javascript
if (s.mode === 'solar_maximize' || s.mode === 'cheap_energy') {
decisions.hpMode = s.cap.hpBoostAllowed ? 'boost' : 'normal';
} else if (s.isNegativePrice && s.cap.hpBoostAllowed) {
decisions.hpMode = 'boost'; // negatiivinen hinta — käytä kaikki itse
} else if (s.spotPrice > evuThreshold && s.cap.hpEvuAllowed) {
decisions.hpMode = 'block'; // kallis — säästä (EVU)
} else {
decisions.hpMode = 'normal';
}
Tässä block ei tarkoita lämpöpumpun täyttä sammutusta vaan Thermian EVU-signaalia (SG1=ON, SG2=OFF), jolla pumppu lykkää lämmitystä kalliin hinnan yli. EVU:lla on noin 5 minuutin reagointiviive — kyse on lykkäyksestä, ei kovasta katkaisusta. Muiden valmistajien pumpuissa (Nibe, Vaillant, Mitsubishi) vastaava toiminto löytyy SG Ready -rajapinnasta hieman eri muodossa. evuThreshold tulee EH Parametersista (oletus 0,15 €/kWh).
PV-rajoituksen päätös:
javascript
if (s.isNegativePrice && s.pvPowerW > pvCurtailMinW) {
const ownLoad = Math.max(300, s.pvPowerW + Math.min(0, s.gridPowerW));
const pct = Math.max(10, Math.min(95,
Math.round((ownLoad / s.pvPowerW) * 100)
));
decisions.pvCurtailPct = pct;
} else {
decisions.pvCurtailPct = null; // ei rajoitusta
}
PV-rajoitus lasketaan dynaamisesti omakäytön perusteella — ei kiinteällä prosentilla. Rajoitusprosentti on invertterin nimellistehosta (tässä 15 kW). Jos talo kuluttaa 3 kW ja aurinko tuottaa 12 kW, omakäyttö on 3 kW — rajoitus asetetaan noin 20 %:iin nimellistehosta (3 kW / 15 kW) jotta vienti minimoidaan mutta oma kulutus katetaan edelleen aurinkosähköllä.
Tarveperustainen ajoitus — ei pelkkiä kynnyksiä
Pelkkä hintakynnys ei riitä älykkääseen ohjaukseen. EV-latauksen tapauksessa järjestelmä ei vain tarkista ”onko hinta alle X” — se arvioi onko lataus ylipäätään tarpeen ja milloin se kannattaa tehdä.
Käytännössä tämä tarkoittaa että Decision Engine arvioi:
- Onko tarve? — onko EV kytkettynä, tarvitseeko akku latausta
- Mikä on aikahorisontti? — milloin auto tarvitaan seuraavan kerran
- Mikä on halvin hetki tässä ikkunassa? — ei välttämättä juuri nyt
Sama periaate lämpöpumpussa: boosti ei laukea vain koska hinta on halpa — se laukea jos käyttövesi on viilenemässä tai huomisen sääennuste lupaa kylmää. Halpa hinta on mahdollisuus, tarve on perustelu.
Tässä on keskeinen ero tavalliseen kynnysohjaukseen: järjestelmä optimoi aikaikkunan sisällä eikä reagoi yksittäiseen hetkeen. Optimointi ei ole vain ON/OFF-päätös — se on ajoitusongelma.
Komennot ulos — ja miksi ne ovat yksinkertaisia
Kun Priority Resolver on tehnyt päätöksensä, komennot julkaistaan MQTT:hen:
javascript
node.send({ topic: 'energyhub/command/ev/allowed', payload: String(d.ev), retain: false });
node.send({ topic: 'energyhub/command/hp/mode', payload: d.hpMode, retain: false });
if (d.pvCurtailPct !== null) {
node.send({ topic: 'energyhub/command/pv/curtail_pct', payload: String(d.pvCurtailPct), retain: false });
}
Komennot ovat tarkoituksella yksinkertaisia, eikä niitä julkaista retain-lipulla — komento on hetkellinen, kuten osassa 8 käytiin läpi. Node-RED ei tiedä miten EV-lataus teknisesti toteutetaan — se tietää vain haluaako se sallia sen vai ei. HA hoitaa loput: Shelly-releen ohjauksen, Modbus-kirjoitukset, automaatioiden laukeamisen.
Tämä on kerrosmallin konkretiaa: optimointikerros päättää, integraatiokerros toteuttaa.
Päätösten kirjaus — observability
Jokainen Decision Enginen päätös kirjataan observability-topiciin:
javascript
node.send({
topic: 'energyhub/observability/decisions',
payload: {
ts: new Date().toISOString(),
mode: s.mode,
grid_w: s.gridPowerW,
pv_w: s.pvPowerW,
spot_eur: s.spotPrice,
max_phase_a: s.maxPhaseA,
decisions: { ev: d.ev, hp_mode: d.hpMode, pv_curtail_pct: d.pvCurtailPct },
reasons: r // miksi kukin päätös tehtiin
},
retain: false
});
reasons-objekti on se mikä tekee tästä oikeasti hyödyllistä. Se ei kirjaa vain mitä tehtiin — se kirjaa miksi.
javascript
reasons.ev = 'PEAK: vaihevirta 23.8A'
reasons.hpMode = 'Negatiivinen hinta: -0.001 EUR/kWh'
reasons.pvCurtail = 'Negatiivinen hinta: rajoitus 24%'
Kun jälkikäteen kysytään miksi EV ei latautunut tiistaina klo 14 — vastaus on tallessa InfluxDB:ssä täsmällisesti kirjattuna.
Mitä Decision Engine ei tee
On yhtä tärkeää ymmärtää mitä Decision Engine ei tee kuin mitä se tekee.
Se ei ohjaa releitä suoraan. Se ei kirjoita Modbus-rekistereihin. Se ei tiedä mikä laite on fyysisesti kytkettynä mihinkin. Kaikki tämä on HA:n vastuulla.
Se ei myöskään säilytä päätöshistoriaa optimointimielessä — jokainen minuuttisykli lukee tilannekuvan uudelleen eikä ”muista” mitä se päätti tunti sitten edukseen tai haitakseen. Mutta täysin tilaton se ei ole, eikä saa olla: Decision Engine ylläpitää eksplisiittistä tilakonetta hystereesiä, viiveitä ja toteumahavaintoja varten. EV-latauksella on minimiajoaika ja päälle/pois-viiveet, jotta lataus ei pätki minuutin välein hinnan heilahdellessa. DHW-boostilla on oma elinkaarensa (käynnistys → vahvistus mitatusta tehosta → palautus). Erillinen havaintokerros vertaa jokaisella syklillä mitä järjestelmä päätti siihen mitä fyysisesti tapahtui. Tämä tila tekee logiikasta vakaata — ilman sitä sama tilannekuva tuottaisi nopeaa edestakaista vaihtelua. Ennakoitavuus ei siis synny tilattomuudesta vaan siitä, että tila on hallittua ja kirjattua: hystereesi, operating modet ja capability-flagien hitaampi muutosrytmi vaimentavat heilahtelun.
Seuraavaksi: Osa 10 — Aurinko ja teho Node-REDissä. Miten PV-tuotanto, aurinkoylijäämä ja tehonrajoitus toimivat yhdessä päätöksenteossa.
Tekninen toteutus: Node-RED-flowt ja Priority Resolver -koodi on kuvattu ohjeessa HA:n valmistelu integraatiokerrokseksi sekä Node-RED flowt — rakenne ja toteutus.