Summary
With the construction of a new house came the idea of an intelligent energy management system. I had the chance to work with homeassistant in a “pilot” phase when I was still living in an apartment, so I was able to realize the capabilities of this platform on a house-scale environment. My research brought me to the point to not only equip the house with photovoltaic panels (PV) for energy production, but to also use an air-air heat pump together with a battery for energy storage.
My vision at that point was simple: become independent from third party energy providers and maximise the consumption of energy that I produce myself. This vision obviously is not achievable, as grey and cold winter days always require me to pull in electricity from the public grid. However, I now manage to
-
Turn on and off electric consumers, so that I can use most of the energy that I produce with my PV system
-
Minimize the amount of kWh that I inject into the public grid (as there’s no monetary gain for me to do so)
-
Minimize the amount of kWh that I retrieve from the public grid, provided that there’s enough “home solar energy” available
I needed to learn the hard way that none of my technical counterparts (electricians & installer for heating system) was able to connect the dots and to propose technical solutions. So I worked on a way to connect all of my devices and to make them “smart”, in order to achieve the above goals.
Technical Setup and Equipment
The heart of my heating setup is represented by an air-to-air heat pump (Alpha Innotec LWCV 122), combined with an electric heating coil for drinking water. The entire house is equipped with a floor heating system.
The 36 PV panels are installed both on the east (12) and west (24) side of the roof and get inverted by 2 separate Fronius inverters (Fronius Symo 4.5-3-M and Fronius Symo Hybrid 5.0-3-S) whereas the latter is the focal management device for incoming, outgoing and stored energy (BYD B-Box-HV 7.7 with 7.7kW useable storage).
From a homeassistant component perspective, I use darksky for weather forecast and nightly temperature predictions. Besides that, I use the season and sun component.
Configuration of electrical consumers
The electric consumers in my automations are mainly the heat pump, the heating coil for drinking water and a Renault ZOE electric car:
-
The heat pump injects data into homeassistant via rest API and I can artificially inject excess energy by triggering the “photovoltaic” mode in the Luxtronik control unit, which needs to be activated by a certified technician from Alpha Innotec. Once activated, it reacts upon potential free contacts of bridge NTC-24 <-> GND on the mainboard of the heat pump. This bridging is realized through a z-wave device (Fibaro Smart Implant). Once the electric circuit bridged by the Fibaro device, the heat pump sets the heating temperature to the highest value available and activates the heating process. In my case, this “burns” around 2kWh or energy and creates warm water for the floor heating system, which is then stored in a huge 800 liters boiler. Likewise I can transform electric energy into warm water and store it for later usage.
-
The heating coil is an independent device which usually serves as backup in case the heat pump cannot heat the drinking water up to the desired temperature. I turned the logic around and made it the primary heating device as long as there’s excess energy produced by my PV system. A 400 litre boiler gets heated up to 65 degrees and – once hot – provides warm water for 2-3 days. A Sonoff basic device activates the coil’s fuse located in the electric cabinet and then releases 2kW of energy to the heating coil. I was able to reduce the maximum heating capacity to 2kW rather than 4 or 6, via a manual bridging of contacts on the heating coil control unit.
-
As with the heat pump, a z-wave Fibaro Smart Implant acts in order to create a potential free contact in my KEBA P20 charging station which is connected to my electric car. Unfortunately this KEBA charging station cannot be directly connected to a network, so I had to use this workaround. The charging energy can be manually set to values beginning at 10A, so I chose to set it to a static 13 Ampere with 240V, which equals 3.1kWh of maximum energy consumption.
So how does it work?
After some months of experimenting, I decided to put in place a concept which I call “battery debt”. Why a “debt” concept? Because it takes into account the weather forecast and avoids that too much energy gets pushed (and “lost” since it’s only poorly compensated) into the public grid. I can thus optimize my auto-consumption as the battery never gets fully charged during the day but remains a critical “buffer” when switching on and off any consumers. The only component that I do not manage is the battery charge/discharge, so basically the battery sits in the middle of the producers and consumers.
Details on the "battery debt" concept
In my concept, electric consumers gets activated as soon as a specific “battery debt state of charge (SOC)” is reached. I do not wait until the battery charge has reached a SOC of 100%, no, I make a bet on the SOC while taking into account the chance to be able to fully load the battery until sunset, based on weather forecasts. This allows me to trigger consumers pretty early and sometimes at the expense of the (partially loaded) battery. As soon as the “target debt SOC” is higher than the actual SOC, the system cuts out electric consumers, until the battery recovers (loads) again and reaches the target SOC.
It’s important to note that the “target debt SOC” is dynamic and thus starts low the morning and increases towards the evening. The calculation of this value takes into account the length of the day in my timezone and the remaining hours of sunlight that can theoretically shine on my roof. An important additional variable is the precipitations and cloud factor that I retrieve from darksky (cloudy, partial cloudy, sunny, rainy, snowy, etc.). The more “sunny” the weather forecast, the more I’m willing to make a bet regarding the “debt factor”, which can vary between 70% (highest bet) and 10% (lowest bet) off a 100% SOC rate.
[Before applying this concept, I was basing the activation of consumers on “excess energy” threshold which were only attained once the battery was fully loaded. You could use this concept for your installation if you don’t have a battery attached. I can share code with you if that helps.]
Code Review
[Important note: all of the below configurations have been grouped within a package]
The general settings describe things that I need for some basic override functionalities, the definition of the battery debt status, etc.
I don’t want my heat pump (once it runs) to be exposed to arbitrary power cuts (through recurring on/off series), since I read in forums that the compressor should run for a minimum of 30 minutes. The reason is that each start of the compressor shortens the expected lifetime of the heat pump. That’s why I define a timer with the pre-set value of 45 minutes per cycle.
###############################
### General Settings ###
###############################
input_boolean:
energy_flow_auto_control:
name: automatic energy flow control
initial: on
energy_audio_notifications:
name: Notifications for energy events
initial: off
icon: mdi:bell-ring
input_number:
dynamic_battery_debt:
name: dynamically calculated offset value for battery debt
initial: 50
min: 10
max: 100
step: 10
timer:
heat_pump_runtime:
duration: '00:45:00'
The first part of the automation section dynamically calculates the “energy debt” for the battery SOC. Note that this value can change any time, once darksky updates it’s forecasts. The second half manages the timer for the heat pump cycles.
##########################
### -+SECTION+- ###
### Automation ###
##########################
automation:
# calculation of "energy debt" value, based on weather forecast
- alias: '(energy control) dynamic change of energy debt ratio'
initial_state: true
trigger:
- platform: state
entity_id: weather.dark_sky
- platform: state
entity_id: sensor.dark_sky_icon_0
action:
- service: input_number.set_value
data_template:
value: >
{% if states.weather.dark_sky.state == 'cloudy' or states.weather.dark_sky.state == 'rainy' or states.weather.dark_sky.state == 'snowy' %}
10
{% elif states.weather.dark_sky.state == 'partlycloudy' %}
50
{% elif states.weather.dark_sky.state == 'sunny' %}
70
{% else %}
20
{% endif %}
entity_id: input_number.dynamic_battery_debt
# timer for heat pump authorization
- alias: '(energy control) activate heat pump runtime'
initial_state: true
trigger:
- platform: state
entity_id: switch.heat_generation_authorization
to: 'on'
action:
- service: timer.start
entity_id: timer.heat_pump_runtime
- alias: '(energy control) deactivate heat pump runtime'
initial_state: true
trigger:
- platform: event
event_type: timer.finished
event_data:
entity_id: timer.heat_pump_runtime
action:
- service: switch.turn_off
entity_id: switch.heat_generation_authorization
The start section merely turns on consumers at the right point of time, whereas the stop section stops these. Note that I have hard coded the priorities of my favorite 3 consumers (heat pump, hot water boiler, and ev charge) into the code.
##################################
### +Category+ ###
### START & CHANGE ###
##################################
##########################################
### hot water + ev charge + heat pump ###
##########################################
# authorize and activate other consumers as soon as the battery reaches higher levels
- alias: '(energy control) authorize energy consumers'
initial_state: true
trigger:
- platform: state
entity_id: switch.heat_generation_authorization
to: 'off'
- platform: numeric_state
entity_id: sensor.battery_soc
above: 91
- platform: numeric_state
entity_id: sensor.battery_soc
above: 94
- platform: numeric_state
entity_id: sensor.battery_soc
above: 96
- platform: numeric_state
entity_id: sensor.battery_soc
above: 98
- platform: template
value_template: "{{ states.sensor.public_grid_consumption.state | int < -2000 }}" # use case: incoming PV energy is too high for SYMO Hybrid to charge the battery further and energy gets lost to the public grid
- platform: template
value_template: "{{states.sensor.target_debt_soc.state | int < states.sensor.battery_soc.state | int and states.sensor.battery_discharge.state | int < -2500 }}" # will authorize consumers (such as heat pump at first), as soon as target soc level has been reached + PV produces enough power
condition:
- condition: state
entity_id: binary_sensor.target_soc_failure
state: 'off'
- condition: state
entity_id: input_boolean.energy_flow_auto_control
state: 'on'
action:
- service: switch.turn_on
data_template:
entity_id: >
{% if is_state('binary_sensor.heat_gen_auth_requirements_fulfilled', 'on') and states.switch.heat_generation_authorization.state == 'off' %}
switch.heat_generation_authorization
{% elif (states.binary_sensor.heat_gen_auth_requirements_fulfilled.state == 'off' or states.switch.heat_generation_authorization.state == 'on') and states.sensor.energy_flow_prio.state == 'hot_water' and not states.binary_sensor.hot_water_boiler_2kw.state == 'on' and not states.binary_sensor.water_hot.state == 'on' %}
switch.hot_water_template_switch
{% else %}
switch.ev_charging_authorization
{% endif %}
###############################
### STOP ###
###############################
- alias: '(energy control) turn off consumers if missing power'
initial_state: true
trigger:
- platform: state
entity_id: binary_sensor.target_soc_failure
to: 'on'
- platform: state
entity_id: binary_sensor.target_soc_failure
to: 'on'
for:
minutes: 1
- platform: state
entity_id: binary_sensor.target_soc_failure
to: 'on'
for:
minutes: 2
- platform: numeric_state
entity_id: sensor.public_grid_consumption
above: 500
for:
minutes: 3
condition:
- condition: state
entity_id: input_boolean.energy_flow_auto_control
state: "on"
action:
- service: switch.turn_off
data_template:
entity_id: >
{% if states.sensor.energy_flow_prio.state == 'ev_charge' and states.binary_sensor.hot_water_boiler_2kw.state == 'off' %}
switch.ev_charging_authorization
{% elif states.sensor.energy_flow_prio.state == 'ev_charge' and states.binary_sensor.water_hot.state == 'on' %}
switch.hot_water_template_switch
{% elif is_state('binary_sensor.ev_charging_authorized', 'on') %}
switch.ev_charging_authorization
{% elif states.switch.hot_water_template_switch.state == 'on' %}
switch.hot_water_template_switch
{% endif %}
- alias: '(energy control) stop heat pump authorization'
initial_state: false
trigger:
- platform: template
value_template: "{{ states.sensor.remaining_pv_light.state | float < 0.5 }}" # stop the heatpump latest when the sun will go down VERY soon
- platform: numeric_state
entity_id: sensor.luxtronik_id_web_temperatur_trl
above: 40
action:
- service: switch.turn_off
entity_id: switch.heat_generation_authorization
- alias: '(energy control) turn off boiler when water is hot'
initial_state: true
trigger:
- platform: state
entity_id: binary_sensor.water_hot
to: 'on'
action:
- service: switch.turn_off
entity_id: switch.hot_water_template_switch`
The sensors section shows the hard coded priorities for the electrical consumers, followed by some sensors that extract data from the Fronius solar inverters. (Note: there’s now a Fronius component that you could use as an alternative, see https://www.home-assistant.io/integrations/fronius/). The following lines of code (sensors) calculate data for further usage either in the front end or for automations.
Have a look at my other project here to learn about a dynamic allocation of power to consumers, depending on custom set priorities.
##########################
### -+ SECTION +- ###
### Sensors ###
##########################
sensor:
# hard-coded prio for energy consumers
- platform: template
sensors:
energy_flow_prio:
friendly_name: "prio 1 for energy consumption"
value_template: >
{% if states.binary_sensor.water_hot.state == 'off' %}
hot_water
{% elif states.binary_sensor.water_hot.state == 'on' %}
ev_charge
{% endif %}
icon_template: >
{% if states.binary_sensor.water_hot.state == 'off' %}
mdi:water-boiler
{% elif states.binary_sensor.water_hot.state == 'on' %}
mdi:car
{% endif %}
# values from Fronius inverters
- platform: rest
resource: http://192.168.1.31/solar_api/v1/GetInverterRealtimeData.cgi?Scope=System
name: real time AC production est
value_template: "{{value_json.Body.Data.PAC.Values['1'] | int }}"
unit_of_measurement: "W"
scan_interval: 30
- platform: rest
resource: http://192.168.1.32/solar_api/v1/GetPowerFlowRealtimeData.fcgi
name: gross AC production PV ouest
value_template: "{{value_json.Body.Data.Site['P_PV'] | int }}"
unit_of_measurement: "W"
scan_interval: 30
- platform: rest
resource: http://192.168.1.32/solar_api/v1/GetPowerFlowRealtimeData.fcgi
name: net household grid injection
value_template: "{{value_json.Body.Data.Site['P_Load'] | int }}"
unit_of_measurement: "W"
scan_interval: 30
- platform: rest
resource: http://192.168.1.32/solar_api/v1/GetPowerFlowRealtimeData.fcgi
name: public grid consumption
value_template: "{{value_json.Body.Data.Site['P_Grid'] | int }}"
unit_of_measurement: "W"
scan_interval: 30
- platform: rest
resource: http://192.168.1.32/solar_api/v1/GetInverterRealtimeData.cgi?Scope=System
name: net AC production PV ouest
value_template: "{{value_json.Body.Data.PAC.Values['1'] | int }}"
unit_of_measurement: "W"
scan_interval: 30
- platform: rest
resource: http://192.168.1.32/solar_api/v1/GetPowerFlowRealtimeData.fcgi
name: Battery SOC
unit_of_measurement: "%"
value_template: "{{value_json.Body.Data.Inverters['1']['SOC'] | int }}"
scan_interval: 30
- platform: rest
resource: http://192.168.1.32/solar_api/v1/GetPowerFlowRealtimeData.fcgi
name: battery discharge
unit_of_measurement: "W"
value_template: "{{value_json.Body.Data.Site['P_Akku'] | int }}"
scan_interval: 30
# calculated values
- platform: template
sensors:
cumulated_gross_real_time_production: #
friendly_name: "cumulated gross PV real time production"
value_template: "{{ states.sensor.real_time_ac_production_est.state | int + states.sensor.gross_ac_production_pv_ouest.state | int }}"
unit_of_measurement: "W"
- platform: statistics
entity_id: sensor.cumulated_gross_real_time_production
name: cumulated production 5min
max_age:
minutes: 5
precision: 0
- platform: statistics
entity_id: sensor.cumulated_gross_real_time_production
name: cumulated production 30min
max_age:
minutes: 30
precision: 0
# total household power consumption WITHOUT battery charge -> gives consumers priority over battery charge
- platform: template
sensors:
household_power_consumption:
friendly_name: "real time household consumption excl. battery charge"
unit_of_measurement: "W"
value_template: >
{% if states.sensor.net_household_grid_injection.state | int <= 0 %}
{{ states.sensor.net_household_grid_injection.state | int | abs + states.sensor.real_time_ac_production_est.state | int }}
{% elif states.sensor.net_household_grid_injection.state | int > 0 %}
{{ states.sensor.real_time_ac_production_est.state | int - states.sensor.net_household_grid_injection.state | int }}
{%endif%}
- platform: statistics
entity_id: sensor.household_power_consumption
name: total household consumption 5min
max_age:
minutes: 5
precision: 0
# [[[ informational ]]] total household power consumption INCLUDING battery charge -> gives battery charge priority over anything else
- platform: template
sensors:
household_power_consumption_inc_batt:
friendly_name: "real time household consumption incl. battery charge"
unit_of_measurement: "W"
value_template: >
{% if states.sensor.battery_discharge.state | int <= 0 %}
{{ states.sensor.household_power_consumption.state | int + states.sensor.battery_discharge.state | int | abs }}
{% elif states.sensor.battery_discharge.state | int > 0 %}
{{ states.sensor.household_power_consumption.state | int }}
{%endif%}
- platform: statistics
entity_id: sensor.household_power_consumption_inc_batt
name: total household consumption inc batt 5min
max_age:
minutes: 5
precision: 0
# total household excess power EXCLUDING battery charge
- platform: template
sensors:
total_excess_household_power_excl_batt_charge:
friendly_name: "excess household power 5min mean excl batt"
unit_of_measurement: "W"
value_template: "{{ (states.sensor.cumulated_production_5min_mean.state | int - states.sensor.total_household_consumption_5min_mean.state | int) }}"
# total household excess power INCLUDING battery charge
- platform: template
sensors:
total_real_time_excess_household_power:
friendly_name: "excess household power inc batt"
unit_of_measurement: "W"
value_template: "{{ (states.sensor.cumulated_gross_real_time_production.state | int - states.sensor.household_power_consumption_inc_batt.state | int) }}"
- platform: template
sensors:
total_excess_household_power:
friendly_name: "excess household power inc batt (5min mean calc)"
unit_of_measurement: "W"
value_template: "{{ (states.sensor.cumulated_production_5min_mean.state | int - states.sensor.total_household_consumption_inc_batt_5min_mean.state | int) }}"
# calculate remaining_pv_light, dynamic_debt_offset and target_debt_soc
- platform: template
sensors:
remaining_pv_light:
friendly_name: "remaining daylight for PV energy production"
unit_of_measurement: "h"
value_template: >
{% if is_state('sensor.season', 'summer') %}
{{'%.2f' |format (states.sensor.remaining_daylight.state | float -1.5) }}
{% else %}
{{'%.2f' |format (states.sensor.remaining_daylight.state | float -1.15) }}
{% endif %}
- platform: template
sensors:
dynamic_debt_offset:
friendly_name: "debt offset from 100% SOC"
unit_of_measurement: "%"
value_template: "{{ '%.2f' |format (states.input_number.dynamic_battery_debt.state | int * (states.sensor.remaining_pv_light.state | float / (states.sensor.daylength.state | float - 1.5))) }}"
- platform: template
sensors:
target_debt_soc:
friendly_name: "target dynamic SOC including debt"
unit_of_measurement: "%"
value_template: >
{% if states.sensor.dynamic_debt_offset.state | int > 0 %}
{{'%.2f' |format (100-states.sensor.dynamic_debt_offset.state | float) }}
{% else %}
99
{%endif%}
The remaining pv light sensor gives information about the theoretical time which is left in order to produce energy. In my specific situation, the sun eventually gets blocked by either the neighbor’s house or a nearby forest, which I take into account in my template. Based on this value I can then calculate the dynamic offset for the energy debt as well as the target value for the battery over time (target_debt_soc).
- platform: template
sensors:
remaining_daylight:
friendly_name: "Remaining Daylight in hours"
unit_of_measurement: "h"
value_template: "{{ '%.2f' |format ((as_timestamp(states.sun.sun.attributes.next_setting) - as_timestamp(now())) / 3600) }}"
The binary sensors section first calculates whether the requirements for a heat pump authorization are met. It measures the operational state of the heat pump itself, puts it in relation with the remaining light for an operation cycle, then checks the next night’s minimum temperatures and compares the value of the heating water with a pre-set value. Only if this sensor returns “true”, the heat pump would be allowed to get authorized by an automation. The other sensors provide basic binary information about the status of some switches (on or off) and the dynamic SOC value of the battery.
For the calculation of heat pump related sensors (based on the Luxtronik control unit), please see the following thread: Writing a component for Luxtronik Heatpumps
##########################
### - SECTION - ###
### Binary Sensors ###
##########################
binary_sensor:
- platform: template
sensors:
heat_gen_auth_requirements_fulfilled:
friendly_name: "requirements for heat pump authorization are fulfilled"
# value_template: "{{ (is_state('sensor.wp_betriebszustand', 'Heizen') or is_state('sensor.wp_betriebszustand', 'Abtauen')) and states.sensor.remaining_pv_light.state | float >= 1.3 and states.sensor.dark_sky_overnight_low_temperature_0.state | int < 11 and (45 - states.sensor.luxtronik_id_web_mitteltemperatur.state | int >= states.sensor.luxtronik_id_web_temperatur_tvl.state | int) }}"
value_template: "{{ is_state('sensor.wp_betriebszustand', 'Heizen') or states.sensor.remaining_pv_light.state | float >= 1.2 and states.sensor.season.state != 'summer' and (states.sensor.luxtronik_id_web_mitteltemperatur.state | int < 10 or states.sensor.dark_sky_overnight_low_temperature_0.state | int < 15) and (45 - states.sensor.luxtronik_id_web_mitteltemperatur.state | int >= states.sensor.luxtronik_id_web_temperatur_tvl.state | int) }}"
device_class: lock
- platform: template
sensors:
ev_charging_authorized:
friendly_name: "3.1kW EV charge"
value_template: "{{ is_state('switch.ev_charging_authorization', 'on') }}"
device_class: lock
- platform: template
sensors:
heat_generation_authorized:
friendly_name: "2kW heat pump"
value_template: "{{ is_state('switch.heat_generation_authorization', 'on') }}"
device_class: lock
- platform: template
sensors:
target_soc_failure:
friendly_name: "dynamic battery SOC status"
device_class: battery # battery device class: On means low, Off means normal
value_template: "{{ states.sensor.battery_soc.state | int < (100- states.sensor.dynamic_debt_offset.state | int )}}"
- platform: mqtt
name: "hot water boiler 2kw"
state_topic: "stat/sonoff_01/POWER"
payload_on: "ON"
payload_off: "OFF"
device_class: power
Finally, the switches section manages the operation of the Sonoff basic and represents it as a template switch for easier visualisation in the GUI.
##########################
### -+ SECTION +- ###
### Switches ###
##########################
switch: # Sonoff Basic 01 Switch manages heating coil for warm water (drinking water) production
- platform: mqtt
name: "hot water production"
command_topic: "cmnd/sonoff_01/power"
state_topic: "stat/sonoff_01/POWER"
qos: 1
payload_on: "on"
payload_off: "off"
retain: true
- platform: template
switches:
hot_water_template_switch:
friendly_name: "hot water boiler switch"
value_template: "{{ is_state('binary_sensor.hot_water_boiler_2kw', 'on') }}"
turn_on:
service: switch.turn_on
data:
entity_id: switch.hot_water_production
turn_off:
service: switch.turn_off
data:
entity_id: switch.hot_water_production
Conclusion, open points and questions
Obviously, this concept will never be finished. I have however reached a level of satisfaction which gives me confidence into the achievements, even for the coming winter period with shorter an colder days.
It’s clear that there are negative points with regards to a “electricity to heating” concept. The downside of storing electric energy in warm water boilers is that you’ll always encounter a loss through degrading temperatures within the boiler.
One question remains with regards to my installation: Would this concept deteriorate the life cycle of the battery? I’m looking forward to your comments and advice