Atmospheric conditions based heating control for Home Assistant with AppDaemon


This post shows how to implement a heating control for a central heating using the powerful home automation framework Home Assistant (HASS). It is implemented using the AppDaemon extension of HASS, which allows to automate your home devices with pure python3 code.

The heating control this python script provides uses the sensor values of an outside and a flow temperature sensor to calculate an atmospheric conditions based heating curve (in German: "witterungsgeführte Heizungsregelung"). You can set a target inside temperature (e.g. 21°C). The central heating needs to be connected to a switch which will be turned on until a upper temperature limit is reached.

-

First of all, you need three entities in
Home Assistant to get this script working:

  • an outside temperature sensor  (in C°)
  • a flow temperature sensor  (in C°)
  • your central heating control contact connected as
    a switch in HASS
    => If you turn the switch on in HASS, the central heating
         needs to turn on

 



In AppDaemons configuration file "appdaemon.cfg" you need to speciy the following values:
[heating]
module = heating
class = HeatingLogic
sensor_temperature_flow_actual = sensor.<flow temperature>
sensor_temperature_outside_actual = sensor.<outside temperature
input_temperature_inside_setpoint = input_slider.<setpoint temperature>
switch_heating = switch.<central heating>

adjustment = 3
gain = 1.2
temperature_flow_max = 70   # °C
temperature_flow_min = 25   # °C
temperature_flow_tolerance_above = 5   # °C
temperature_flow_tolerance_below = 5   # °C
switch_heating_max_on = 150  # Minutes

 

IMAGE flow temperature; mean value temperature is the setpoint value

Put the two mentioned sensors as sensor_temperature_flow_actual and sensor_temperature_outside_actual. The switch to turn on the heating needs to be set as switch_heatinginput_temperature_inside_setpoint is a value (e.g. an input slider) which defines the target  setpoint temperature for inside your house.

 

The follwing settings are needed to calculate the heating curve. The algorithm used for the curve is copied from http://ewsch.de/cms/index.php?id=211
A proper documentation of the values is located on this site. There is also an online calculation tool for the heating curve to test your seetings. I will sum up the parameters functionalities:

  • temperature_flow_max and temperature_flow_min are limits for the flow temperature. The calculated flow setpoint temperature will never be above/below these values (set in °C)
  • temperature_flow_tolerance_above and temperature_flow_tolerance_below are added/removed to/from the flow setpoint temperature. Let's say both values are set to 5 and the calculated setpoint temperature is 50°C. The heating will be turned on until a temperature of 55°C is reached. After the temperature declined again to below 45°C, the heating is turned on again.
  • switch_heating_max_on is a timeout in minutes after the heating will be turned off. This is needed if for example the calculated setpoint flow temperature cannot be reached. The heating will be turned on by the 'normal   process', after the actual flow temperature declined below the temperature_flow_tolerance_below limit.
  • gain defines the gain of the heating curve. This needs to be set based on your heating setup. You can lookup the value in your current heating control.
  • adjustment is used to change the heating curve to your needs.

 

The next step is to create a new AppDaemon module called HeatingLogic.
<AppDaemon root>/apps/heating.py

import appdaemon.appapi as appapi

class HeatingLogic(appapi.AppDaemon):

    def initialize(self):
        self.log("Starting HeatingLogic ...")

        self.gain = float(self.args['gain'])
        self.adjustment = float(self.args['adjustment'])
        self.temperature_flow_min = float(self.args['temperature_flow_min'])
        self.temperature_flow_max = float(self.args['temperature_flow_max'])
        self.temperature_flow_tolerance_above = float(self.args['temperature_flow_tolerance_above'])
        self.temperature_flow_tolerance_below = float(self.args['temperature_flow_tolerance_below'])
        self.switch_heating = self.args['switch_heating']
        self.switch_heating_max_on = float(self.args['switch_heating_max_on'])    # Minutes

        # Get current temperature information
        self.get_sensor_temperatures()
        
        # Register for sensor status updates
        self.listen_state(self.temperature_change, entity=self.args['sensor_temperature_outside_actual'])
        self.listen_state(self.temperature_change, entity=self.args['sensor_temperature_flow_actual'])
        self.listen_state(self.temperature_change, entity=self.args['input_temperature_inside_setpoint'])

        # Set inital power state 
        self.action_heating()

    def get_sensor_temperatures(self, update_entity="", update_value=0.0):
        # Get values from HA
        if not update_entity:
            self.temperature_outside_actual = float(self.get_state(self.args['sensor_temperature_outside_actual']))
            self.temperature_flow_actual = float(self.get_state(self.args['sensor_temperature_flow_actual']))
            self.temperature_inside_setpoint = float(self.get_state(self.args['input_temperature_inside_setpoint']))
        elif update_entity == self.args['sensor_temperature_outside_actual']:
            self.temperature_outside_actual = update_value
        elif update_entity == self.args['sensor_temperature_flow_actual']:
            self.temperature_flow_actual = update_value
        elif update_entity == self.args['input_temperature_inside_setpoint']:
            self.temperature_inside_setpoint = update_value

    def calculate_temperature_flow_setpoint(self):
        """
            Calculate best vorlauftemperatur
        """
        temperature_flow_setpoint = float(min(max(0.55*self.gain*(self.temperature_inside_setpoint**(self.temperature_outside_actual/(320-self.temperature_outside_actual*4)))*((-self.temperature_outside_actual+20)*2)+self.temperature_inside_setpoint+self.adjustment, self.temperature_flow_min), self.temperature_flow_max))
        return temperature_flow_setpoint

    def calculate_heating_action(self):
        """
            Decide wether to turn heating on, off or to leave it in its current state
        """
        temperature_flow_setpoint = self.calculate_temperature_flow_setpoint()
        self.log("T-flow-setpoint: "+str(temperature_flow_setpoint) +" => ")
        if self.temperature_flow_actual < (temperature_flow_setpoint-self.temperature_flow_tolerance_below):
            return "on"
        elif self.temperature_flow_actual >= (temperature_flow_setpoint+self.temperature_flow_tolerance_above): 
            return "off"
        else:
            return ""
    
    def temperature_change(self, entity, attribute, old, new, kwargs):
        """
            Callback function
            called if any temperature changes
        """
        self.get_sensor_temperatures(update_entity=entity, update_value=float(new))
        self.action_heating()

    def action_heating(self):
        """
            Action: Turn heating on or off
        """

        # Decide to turn heating on or off
        calculated_action = self.calculate_heating_action()
        
        # Execute action
        if calculated_action == "on":
            self.action_heating_on({})
        elif calculated_action == "off":
            self.action_heating_off({})
        else:
            self.log("Leaving heating in its current state (T-flow-actual: %f; T-outside-actual: %f; T-inside-setpoint: %f)" % (self.temperature_flow_actual, self.temperature_outside_actual, self.temperature_inside_setpoint))

    def action_heating_on(self, kwargs):
        self.log("Turning heating on (T-flow-actual: %f; T-outside-actual: %f; T-inside-setpoint: %f)" % (self.temperature_flow_actual, self.temperature_outside_actual, self.temperature_inside_setpoint))
        self.call_service("switch/turn_on", entity_id=self.switch_heating)
            
        # Set timer for auto turn off to prevent damage
        self.turn_off_timer = self.run_in(self.action_heating_off, int(60*self.switch_heating_max_on), timer=True)

    def action_heating_off(self, kwargs):
        if 'timer' in kwargs and kwargs['timer']==True:
            self.log("Timeout reached for heating in state 'on'. Going to turn it off =>")
        self.log("Turning heating off (T-flow-actual: %f; T-outside-actual: %f; T-inside-setpoint: %f)" % (self.temperature_flow_actual, self.temperature_outside_actual, self.temperature_inside_setpoint))
        self.call_service("switch/turn_off", entity_id=self.switch_heating)

        # cancel timer for auto turn off
        if hasattr(self, 'turn_off_timer'):
            self.cancel_timer(self.turn_off_timer)
            self.log("Canceled turn_off timer")

this post was added

Jan. 20, 2017, 10:25 p.m.

this post was last modified

Jan. 22, 2017, 1:58 p.m.