HA:n valmistelu integraatiokerrokseksi

Written by

in

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:

EtuliiteTarkoitusEsimerkki
m_mittaus (measurement)sensor.m_grid_power_w
p_parametriinput_number.p_fuse_limit_a
c_capability tai ohjaussignaaliinput_boolean.c_ev_charge_allowed
d_diagnostiikkainput_text.d_ev_last_reason
sys_järjestelmätilainput_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 datanunknown 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.

Piditkö artikkelista?

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

Seuraa blogia Blogit.fi:ssä