
Tämä ohje on Case oma talo artikkelisarjan Osan 3 tekninen vastinpari. Siinä missä Osa 3 selittää miksi Home Assistant toimii integraatiokerroksena, tässä ohjeessa käydään läpi miten se toteutetaan käytännössä — pakettipohja, nimeämiskäytäntö, kanoniset mittaukset, capability-flagit ja MQTT-rajapinta Node-REDiin.
Ohje olettaa että HA Core on asennettuna Docker-kontissa. Asennusohjeet löytyvät ohjeesta Energiahubin perusalusta.
Pakettipohjainen konfiguraatio
Tavallisessa HA-asennuksessa kaikki kertyy configuration.yaml-tiedostoon tai sen include-viittauksiin. EnergyHubissa käytetään pakettipohjaista rakennetta jossa jokainen toiminnallinen kokonaisuus on omassa tiedostossaan.
configuration.yaml sisältää vain minimaaliset asetukset ja viittauksen pakettikansioon:
yaml
homeassistant:
packages: !include_dir_named packages
default_config:
logger:
default: warning
!include_dir_named packages lataa kaikki YAML-tiedostot packages/-kansiosta ja yhdistää ne. Jokainen paketti voi sisältää mitä tahansa HA:n domain-lohkoja — input_number, template, automation ja niin edelleen — ilman erillisiä include-viittauksia.
Paketit on nimetty numerojärjestyksessä loogisten vastuurajojen mukaan:
packages/
00_core.yaml # järjestelmäparametrit, globaalit boolean-arvot
00_core_modes.yaml # operating modes, capability-flagit
10_integrations.yaml # Modbus, Nordpool, P1-mittari
20_measurements.yaml # kanoniset m_-sensorit
30_load_ev.yaml # EV-latauksen tilakone
31_load_heatpump.yaml # lämpöpumpun EVU/Boost-ohjaus
40_optimization.yaml # MQTT-rajapinta Node-REDiin
50_reporting.yaml # energiamittarit
Kun jotain muuttuu, tiedetään heti missä tiedostossa muutos tehdään.
Nimeämiskäytäntö
Johdonmukainen nimeäminen on edellytys sille että järjestelmä pysyy ymmärrettävänä. EnergyHubissa kaikilla entiteeteillä on etuliite joka kertoo sen roolin:
| Etuliite | Tarkoitus | Esimerkki |
|---|---|---|
m_ | mittaus (measurement) | sensor.m_grid_power_w |
p_ | parametri | input_number.p_fuse_limit_a |
c_ | capability tai ohjaussignaali | input_boolean.c_ev_charge_allowed |
d_ | diagnostiikka | input_text.d_ev_last_reason |
sys_ | järjestelmätila | input_boolean.sys_safety_trip |
c_-etuliite kattaa tässä toteutuksessa sekä capabilityt (c_ev_charge_allowed — saako laite toimia) että ohjaussignaalit (c_hp_mode — missä tilassa laite on). Erottelu näkyy nimestä: _allowed-päätteiset ovat käyttöoikeuksia, muut ovat tilaohjausta. Arkkitehtuurisesti puhtaammassa toteutuksessa eksplisiittiset komennot saisivat oman cmd_-etuliitteensä — tämä on kehityssuunta jos järjestelmä kasvaa.
Yksikkö merkitään nimeen aina kun se on epäselvä: _w watteille, _a ampeereille, _c celsiusasteille, _kwh kilowattitunneille, _eur_kwh euroille per kilowattitunti.
Signaalin etumerkki: m_grid_power_w > 0 tarkoittaa ostoa verkosta, < 0 myyntiä verkkoon. Sama konventio kaikkialla — ei erikoistapauksia.
Järjestelmäparametrit — 00_core.yaml
Kaikki säädettävät kynnysarvot ovat input_number-entiteetteinä 00_core.yaml:ssa. Ne eivät ole hardkoodattuja lukuja automaatioissa tai Node-RED-funktioissa.
yaml
input_number:
p_fuse_limit_a:
name: "Pääsulakkeraja A"
min: 10
max: 35
step: 1
initial: 25
unit_of_measurement: "A"
mode: slider
p_negative_price:
name: "Negatiivinen hinta"
min: -0.15
max: 0.00
step: 0.005
initial: -0.02
unit_of_measurement: "EUR/kWh"
mode: box
p_cheap_price:
name: "Halpa hinta"
min: -0.10
max: 0.10
step: 0.005
initial: 0.04
unit_of_measurement: "EUR/kWh"
mode: box
p_solar_excess_w:
name: "Aurinkoylijäämä W"
min: 500
max: 5000
step: 100
initial: 2000
unit_of_measurement: "W"
mode: box
input_boolean:
sys_safety_trip:
name: "Turvakatkaisu"
sys_optimization_enabled:
name: "Optimointi päällä"
Node-RED lukee nämä arvot MQTT-telemetrian kautta. Parametrin muuttaminen HA:n käyttöliittymässä vaikuttaa välittömästi Node-RED:n päätöksentekoon ilman että koodiin tarvitsee koskea.
Operating modes ja capability-flagit — 00_core_modes.yaml
yaml
input_select:
sys_operating_mode:
name: "EnergyHub moodi"
icon: mdi:state-machine
options:
- normal
- solar_maximize
- cheap_energy
- peak_protection
- vacation
- guest
- manual_override
- fallback
- emergency
initial: normal
c_hp_mode:
name: "HP ohjausmoodi"
options:
- normal
- boost
- block
initial: normal
input_boolean:
c_ev_charge_allowed:
name: "EV lataus sallittu"
icon: mdi:ev-station
c_hp_evu_allowed:
name: "HP EVU sallittu"
icon: mdi:heat-pump
c_hp_boost_allowed:
name: "HP Boost sallittu"
icon: mdi:thermometer-high
c_pv_curtail_allowed:
name: "PV rajoitus sallittu"
icon: mdi:solar-power
c_sauna_start_allowed:
name: "Kiuas sallittu"
icon: mdi:sauna
Capability-flagit ovat käyttöoikeuksia, eivät komentoja. c_ev_charge_allowed: false ei sammuta latausta — se kertoo Node-REDille että lataus ei ole sallittua tässä tilanteessa. Node-RED tekee varsinaisen päätöksen. Tämä erottelu on tärkeä: capability kertoo voiko järjestelmä tehdä jotain, komento kertoo tekeekö se sen.
Kanoniset mittaukset — 20_measurements.yaml
Kaikki raakadata normalisoidaan template-sensoreiksi ennen kuin muut järjestelmän osat käyttävät niitä. Raakadata ei ole sama asia kuin luotettava järjestelmätason mittaus.
yaml
template:
- sensor:
- name: "m_grid_power_w"
unique_id: m_grid_power_w
unit_of_measurement: "W"
device_class: power
state_class: measurement
state: "{{ states('sensor.p1_meter_teho') | float(0) | round(0) }}"
- name: "m_grid_export_w"
unique_id: m_grid_export_w
unit_of_measurement: "W"
device_class: power
state_class: measurement
state: >
{% set p = states('sensor.p1_meter_teho') | float(0) %}
{{ [(-p), 0] | max | round(0) }}
- name: "m_spot_price_eur_kwh"
unique_id: m_spot_price_eur_kwh
unit_of_measurement: "EUR/kWh"
state: >
{{ state_attr('sensor.nordpool_kwh_fi_eur_3_00_0', 'current_price')
| float(0) | round(4) }}
- name: "m_buy_price_eur_kwh"
unique_id: m_buy_price_eur_kwh
unit_of_measurement: "EUR/kWh"
state: >
{% set spot = state_attr('sensor.nordpool_kwh_fi_eur_3_00_0',
'current_price') | float(0) %}
{% set marginaali = states('input_number.p_electricity_margin')
| float(0.004) %}
{% set siirto = states('input_number.p_electricity_transfer')
| float(0.048) %}
{{ ((spot * 1.255) + marginaali + siirto) | round(4) }}
- name: "m_pv_power_w"
unique_id: m_pv_power_w
unit_of_measurement: "W"
device_class: power
state_class: measurement
state: >
{{ [states('sensor.sungrow_active_power_raw') | float(0), 0]
| max | round(0) }}
m_buy_price_eur_kwh laskee todellisen ostohinnan: spot × 1,255 (ALV) + siirtomaksu + marginaali. Node-RED käyttää tätä arvoa päätöksenteossa — ei raakaa spot-hintaa.
Huomio float(0)-käytöstä: Kun sensori on tilassa unknown tai unavailable, float(0) muuttaa arvon hiljaisesti nollaksi. Nolla ei aina tarkoita nollaa — joskus se tarkoittaa että data puuttuu. Kriittisille arvoille kuten verkkoteholle kannattaa lisätä diagnostiikkasensori joka havaitsee unavailable-tilan erikseen.
Jos Nordpool-integraation yksikkö vaihtuu tai P1-mittarin API muuttuu, korjaus tehdään yhteen paikkaan. Node-RED ei tiedä eikä sen tarvitse tietää mistä luku tulee.
MQTT-rajapinta Node-REDiin — 40_optimization.yaml
HA julkaisee telemetrian MQTT:hen kahdella eri syklillä: yleinen telemetria minuutin välein ja vaihevirrat 10 sekunnin välein Safety Guardiania varten. Node-RED kuuntelee ja päivittää flow-kontekstin.
yaml
automation:
- alias: "Telemetria: verkko ja tuotanto"
id: telemetry_grid_pv
mode: restart
trigger:
- platform: time_pattern
minutes: "/1"
action:
- service: mqtt.publish
data:
topic: "energyhub/telemetry/grid"
retain: true
payload: >
{
"power_w": {{ states('sensor.m_grid_power_w') | float(0) | round(0) }},
"export_w": {{ states('sensor.m_grid_export_w') | float(0) | round(0) }},
"l1_a": {{ states('sensor.m_phase_l1_a') | float(0) | round(1) }},
"l2_a": {{ states('sensor.m_phase_l2_a') | float(0) | round(1) }},
"l3_a": {{ states('sensor.m_phase_l3_a') | float(0) | round(1) }},
"ts": "{{ now().isoformat() }}"
}
- alias: "Telemetria: vaihevirrat nopea"
id: telemetry_phases_fast
mode: restart
trigger:
- platform: time_pattern
seconds: "/10"
action:
- service: mqtt.publish
data:
topic: "energyhub/telemetry/phases"
retain: false
payload: >
{
"l1_a": {{ states('sensor.m_phase_l1_a') | float(0) | round(1) }},
"l2_a": {{ states('sensor.m_phase_l2_a') | float(0) | round(1) }},
"l3_a": {{ states('sensor.m_phase_l3_a') | float(0) | round(1) }},
"max_a": {{ [states('sensor.m_phase_l1_a') | float(0) | abs, states('sensor.m_phase_l2_a') | float(0) | abs, states('sensor.m_phase_l3_a') | float(0) | abs] | max | round(1) }},
"ts": "{{ now().isoformat() }}"
}
- alias: "Telemetria: capabilities ja moodi"
id: telemetry_capabilities
mode: restart
trigger:
- platform: state
entity_id:
- input_select.sys_operating_mode
- input_boolean.c_ev_charge_allowed
- input_boolean.c_hp_evu_allowed
- input_boolean.c_hp_boost_allowed
- input_boolean.c_pv_curtail_allowed
- input_boolean.sys_safety_trip
action:
- service: mqtt.publish
data:
topic: "energyhub/system/capabilities"
retain: true
payload: >
{
"mode": "{{ states('input_select.sys_operating_mode') }}",
"c_ev_charge_allowed": {{ (states('input_boolean.c_ev_charge_allowed') == 'on') | lower }},
"c_hp_evu_allowed": {{ (states('input_boolean.c_hp_evu_allowed') == 'on') | lower }},
"c_hp_boost_allowed": {{ (states('input_boolean.c_hp_boost_allowed') == 'on') | lower }},
"c_pv_curtail_allowed": {{ (states('input_boolean.c_pv_curtail_allowed') == 'on') | lower }},
"sys_safety_trip": {{ (states('input_boolean.sys_safety_trip') == 'on') | lower }},
"ts": "{{ now().isoformat() }}"
}
# Komentojen vastaanotto Node-REDiltä
- alias: "MQTT: EV latauksen sallinta"
id: mqtt_cmd_ev_allowed
mode: queued
trigger:
- platform: mqtt
topic: "energyhub/command/ev/allowed"
action:
- service: >
input_boolean.turn_{{ 'on' if trigger.payload == 'true' else 'off' }}
target:
entity_id: input_boolean.c_ev_charge_allowed
- alias: "MQTT: HP moodi"
id: mqtt_cmd_hp_mode
mode: queued
trigger:
- platform: mqtt
topic: "energyhub/command/hp/mode"
condition:
- condition: template
value_template: >
{{ trigger.payload in ['normal', 'boost', 'block'] }}
action:
- service: input_select.select_option
target:
entity_id: input_select.c_hp_mode
data:
option: "{{ trigger.payload }}"
- alias: "MQTT: moodinvaihtopyynto"
id: mqtt_cmd_mode_change
mode: queued
trigger:
- platform: mqtt
topic: "energyhub/command/mode"
condition:
- condition: template
value_template: >
{{ trigger.payload in ['normal', 'solar_maximize', 'cheap_energy',
'peak_protection', 'vacation', 'guest', 'manual_override', 'fallback'] }}
action:
- service: input_select.select_option
target:
entity_id: input_select.sys_operating_mode
data:
option: "{{ trigger.payload }}"
Tärkeä yksityiskohta boolean-arvoissa: HA:n Jinja2-templatessa == 'on' tuottaa Python-tyyppisen True/False — ei JSON-standardin true/false. Node-RED ei osaa parsia Python-muotoa. Lisää | lower aina boolean-arvojen perään MQTT-payloadeissa.
MQTT-topicrakenne
energyhub/telemetry/grid HA → Node-RED verkkoteho, vaihevirrat (1 min)
energyhub/telemetry/pv HA → Node-RED aurinkotuotanto (1 min)
energyhub/telemetry/prices HA → Node-RED spot-hinta, ostohinta (1 min)
energyhub/telemetry/heatpump HA → Node-RED lämpöpumpun tiladata (1 min)
energyhub/telemetry/phases HA → Node-RED vaihevirrat (10 s)
energyhub/system/capabilities HA → Node-RED flagit ja moodi (muutoksesta)
energyhub/command/ev/allowed Node-RED → HA true/false
energyhub/command/hp/mode Node-RED → HA normal/boost/block
energyhub/command/pv/curtail_pct Node-RED → HA 0–110
energyhub/command/mode Node-RED → HA moodinvaihto
energyhub/system/safety_trip Guardian → kaikki true/false
energyhub/observability/decisions Node-RED → * päätösloki (ei ohjausta)
energyhub/observability/decisions on erityistapaus — se ei ole ohjausta varten vaan analyysiä ja auditointia varten. Node-RED julkaisee sinne jokaisen päätöksen perusteluineen. Tämä mahdollistaa järjestelmän käyttäytymisen analysoinnin jälkikäteen — miksi EV ei latautunut tiistaina klo 14? Vastaus löytyy observability-topicista.
Retain-arvot: telemetria ja capabilities käyttävät retain: true jotta Node-RED saa viimeisimmän arvon heti käynnistyksen jälkeen. Komennot ja observability käyttävät retain: false.
Ownership — yksi ohjaaja kerrallaan
Yksi EnergyHubin keskeisimmistä periaatteista on omistajuus: yhdellä ohjauskohteella saa olla vain yksi aktiivinen ohjaaja kerrallaan.
Tämä on koko control-fight-ongelman ydin. Jos sekä vanha HA että uusi EnergyHub yrittävät ohjata samaa relettä, tuloksena on arvaamaton käyttäytyminen — kumpikin kumoaa toisen päätöksen omalla syklillään. Tämä ei näy virheenä, vaan oireilee satunnaisena ja vaikeasti debuggattavana toimintahäiriönä.
EnergyHubissa omistajuus on eksplisiittistä: shadow/live-mekanismi takaa että siirtymäaikana vain yksi järjestelmä ohjaa kutakin laitetta.
Shadow/live-ohjaus — turvallinen siirtymä
Shadow/live-mekanismi mahdollistaa turvallisen siirtymän vanhasta ohjausjärjestelmästä uuteen, ja samalla rinnakkaisvalidoinnin — voidaan seurata mitä EnergyHub tekisi ennen kuin se oikeasti tekee sen.
yaml
input_boolean:
energyhub_ev_live_control:
name: "EnergyHub EV live-ohjaus"
icon: mdi:car-electric
energyhub_hp_live_control:
name: "EnergyHub HP live-ohjaus"
icon: mdi:heat-pump
Kun energyhub_ev_live_control on off (shadow-mode), EnergyHub kirjaa päätökset logbookiin mutta ei koske releen tilaan. Tämä mahdollistaa päätösten vertailun vanhaan logiikkaan ennen tuotantokäyttöä — nähdään suoraan milloin järjestelmät olisivat eri mieltä ja miksi.
Kun energyhub_ev_live_control on on (live-mode), EnergyHub ottaa ohjauksen. Aktivointi on yksi toggle — tehdään vasta kun vanha ohjausjärjestelmä on sammutettu.
Automaatio shadow-modessa:
yaml
- alias: "EV: shadow/live ohjaus"
id: ev_shadow_live
mode: queued
trigger:
- platform: state
entity_id: input_boolean.c_ev_charge_allowed
action:
- choose:
- conditions:
- condition: state
entity_id: input_boolean.energyhub_ev_live_control
state: "on"
- condition: state
entity_id: input_boolean.c_ev_charge_allowed
state: "on"
sequence:
- action: switch.turn_on
target:
entity_id: switch.shellypro1_ec62608be038
- conditions:
- condition: state
entity_id: input_boolean.energyhub_ev_live_control
state: "off"
- condition: state
entity_id: input_boolean.c_ev_charge_allowed
state: "on"
sequence:
- action: logbook.log
data:
name: "EnergyHub EV (shadow)"
message: "Shadow: EH haluaisi SALLIA latauksen"
Validointi ja käyttöönotto
Aina kun konfiguraatioon tehdään muutoksia, validoidaan ennen käynnistystä:
bash
docker exec homeassistant python -m homeassistant \
--script check_config --config /config 2>&1 | tail -5
Puhdas validointi näyttää vain Testing configuration at /config ilman virheitä. Sen jälkeen:
bash
docker restart homeassistant
MQTT-yhteyden toimivuus ja telemetrian kulku voidaan tarkistaa:
bash
mosquitto_sub -h localhost -t "energyhub/#" -v
Capabilities-viestin pitäisi tulla muutaman minuutin kuluessa käynnistyksen jälkeen — tai välittömästi jos jokin capability-flag muuttuu.
Yleisimmät ongelmat
Duplikaatti-ID:t automaatioissa — sama id-kenttä esiintyy kahdessa paikassa, esimerkiksi sekä automations.yaml:ssa että paketissa. HA antaa varoituksen käynnistyksen yhteydessä ja jättää toisen huomiotta. Tarkista docker logs homeassistant käynnistyksen jälkeen.
Python True/False JSON:ssa — MQTT-payload sisältää True isolla alkukirjaimella. Node-RED ei osaa parsia tätä. Ratkaisu: lisää | lower kaikkien boolean-lausekkeiden perään templateissa.
float(0) piilottaa puuttuvan datan — unknown tai unavailable muuttuu hiljaisesti nollaksi. Nolla ei aina tarkoita nollaa — joskus se tarkoittaa että data puuttuu. Kriittisille arvoille lisää diagnostiikkasensori joka havaitsee unavailable-tilan erikseen.
Control fight — kaksi järjestelmää ohjaa samaa laitetta samanaikaisesti. Oireilee satunnaisena käyttäytymisena ilman selkeää virhettä. Ratkaisu: shadow/live-mekanismi ja eksplisiittinen omistajuus jokaiselle ohjauskohteelle.