Case: oma talo — Osa 9

Written by

in

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:

  1. Onko tarve? — onko EV kytkettynä, tarvitseeko akku latausta
  2. Mikä on aikahorisontti? — milloin auto tarvitaan seuraavan kerran
  3. 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.

Piditkö artikkelista?

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

Seuraa blogia Blogit.fi:ssä