Home Assistant Heating Control

This is my setup for controlling my central heating.

I connected a Shelly 1 relay switch to my boiler (a Vaillant), wiring it in to the demand connector.  On this boiler this replaces the jumper on connector 3 & 4.

I designed and 3D printed an enclosure for the Shelly 1 (download here) and stuck it on the side of the boiler.

I use 3 Sonoff thermostats to get regular temperature readings from around the house.  In home assistant these are averaged into a template sensor.

sensors:
  - platform: template
    sensors:
      average_temperature:
        friendly_name: "Average temperature"
        unit_of_measurement: "°C"
        device_class: temperature
        value_template: >
          {% set hall = states('sensor.hall_sonoff_temperature') | float %}
          {% set landing = states('sensor.landing_sonoff_temperature') | float %}
          {% set front_room = states('sensor.front_room_sonoff_temperature') | float %}
          {{ ((hall + landing + front_room) / 3) | round(1) }}

This is then set as the target sensor in a generic thermostat and the Shelly 1 switch set as the heater -

climate:
  - platform: generic_thermostat
    name: Landing
    heater: switch.heating
    target_sensor: sensor.average_temperature
    min_temp: 10
    max_temp: 25
    target_temp: 20
    cold_tolerance: 0.5
    initial_hvac_mode: heat
    hot_tolerance: 0
    min_cycle_duration:
      minutes: 5
    away_temp: 14
    precision: 0.1

There are a number of inputs that determine the schedule and modes -

And all the automations that make it work -

  # Boost switch turned on - set boost mode for 30 minutes
  alias: Heating Boost Toggled On
  description: ''
  trigger:
  - platform: state
    entity_id: input_boolean.heating_boost
    from: 'off'
    to: 'on'
  condition: []
  action:
  - service: input_datetime.set_datetime
    target:
      entity_id: input_datetime.heating_boost_until
    data:
      timestamp: '{{ now().timestamp() + 30*60 }}'
  - service: python_script.heating
  mode: single

  # Boost end time has passed - turn off boost mode
  alias: Heating Boost Expired
  description: ''
  trigger:
  - platform: state
    entity_id: sensor.date_time
  condition:
  - condition: state
    entity_id: input_boolean.heating_boost
    state: 'on'
  - condition: and
    conditions:
    - condition: template
      value_template: '{{ states.input_datetime.heating_boost_until.attributes.timestamp
        < now().timestamp() }}'
  action:
  - service: input_boolean.turn_off
    target:
      entity_id: input_boolean.heating_boost
  mode: single

  # Boost mode turned off - reset target temperature
  alias: Heating Boost Toggled Off
  description: ''
  trigger:
  - platform: state
    entity_id: input_boolean.heating_boost
    from: 'on'
    to: 'off'
  condition: []
  action:
  - service: python_script.heating
  mode: single
  
  # Every minute, reset target temperature
  alias: Heating Ticker
  description: ''
  trigger:
  - platform: state
    entity_id: sensor.time
  condition: []
  action:
  - service: python_script.heating
  mode: single

  # Away mode set - reset target temperature
  alias: Away Changed
  description: ''
  trigger:
  - platform: state
    entity_id: input_boolean.away
  condition: []
  action:
  - service: python_script.heating
  mode: single

Several of these automations call the following python script, which simply updates the climate generic thermostats target temperature -

on_temperature = hass.states.get('input_number.heating_on_temperature').state
off_temperature = hass.states.get('input_number.heating_off_temperature').state
boost_temperature = hass.states.get('input_number.heating_boost_temperature').state

weekday_on_1 = hass.states.get('input_datetime.weekday_on_1').state
weekday_off_1 = hass.states.get('input_datetime.weekday_off_1').state
weekday_on_2 = hass.states.get('input_datetime.weekday_on_2').state
weekday_off_2 = hass.states.get('input_datetime.weekday_off_2').state

weekend_on_1 = hass.states.get('input_datetime.weekend_on_1').state
weekend_off_1 = hass.states.get('input_datetime.weekend_off_1').state
weekend_on_2 = hass.states.get('input_datetime.weekend_on_2').state
weekend_off_2 = hass.states.get('input_datetime.weekend_off_2').state

away = hass.states.get('input_boolean.away').state

last_demand = hass.states.get('input_number.heating_last_demand').state

target_temperature = off_temperature

if away == 'on':
  target_temperature = off_temperature
else:
  boost_active = hass.states.get('input_boolean.heating_boost').state
  if boost_active == 'on':
    target_temperature = boost_temperature
  else:
    now = hass.states.get('sensor.time').state
    if datetime.datetime.now().date().weekday() < 5:   # WEEK DAY
      if (now >= weekday_on_1 and now <= weekday_off_1) or (now >= weekday_on_2 and now <= weekday_off_2):
        target_temperature = on_temperature
    else:
      if (now >= weekend_on_1 and now <= weekend_off_1) or (now >= weekend_on_2 and now <= weekend_off_2):
        target_temperature = on_temperature

if target_temperature != last_demand:
  hass.states.set('input_number.heating_last_demand', target_temperature)
  hass.services.call('climate', 'set_temperature', { 'entity_id': 'climate.landing', 'temperature':  target_temperature })

I have a Raspberry Pi 3 with a Pimoroni Hyperpixel Touch display on it that is setup to run chromium browser in kiosk mode, opening the heating page (screenshot above) of Home Assistant in kiosk mode using the Kiosk Mode custom component.

I'm designing a case for this which I will 3D print and mount on the wall, but first I want use something other than the side facing USB for powering it as it will make the case larger than it needs to be.