Generic Thermostat with scheduling

Hi Guys

Thought this might help someone wanting to control the generic_thermostat component with a schedule

We converted to all electric storage heaters and 6.29kWh worth of solar panels with 10kWh battery storage but the heating controls were very basic i.e either on or off based on a preset time and preset high/low temperature over a 7 day period which was not very freindly to the pocket or usable

So, I have spent a fair bit of time getting my all electric heating into a very usable, configrabe state over the winter months by changing the heater controlls to Sonoff TH16’s (x 13 and tamotarised). I now have a good WAF rating on the heating setup as she can either control it via voice or the iOS app

So, Here goes. This is my setup (as of writing) which is running on a raspberrypi 3B+

Worth noting I split all my components into seperate config files using the “!include” option after each component making it easier to mange/update one component and not have a long configuration.yaml file

EDIT: Updated to Appdaemon V4 Add-on due to the following errors in Appdaemon

[hassio.api.proxy] Client error on WebSocket API Cannot connect to host 172.30.32.1:8123 ssl:False [Connection refused].

Which in turn was causing the following errors in the appdaemon log and hass was disconnecting and retry which was cuseing schedy to throw a wobbly

WARNING AppDaemon: HASS: Disconnected from Home Assistant, retrying in 5 seconds

Homeassistant setup:

  • Home Assistant release 0.105.1
  • MQTT Server & Web client (Community Hass.io Add-ons)
  • Appdaemon 3 v5.0.1 (Community Hass.io Add-ons) - replaced with Appdaemon 4 v0.1.2

Heater control devices:

  • Sonoff’s are contolled/updated via MQTT and create the required temperature/humidity sensors and switch in Homeassistant per room

The following is added into my configuration.yaml file:

climate: !include climate.yaml
input_select: !include input_select.yaml

Then, Create a climate.yaml file in my /config folder with the following:

- platform: generic_thermostat
  name: Entrance Heater
  heater: switch.entranceheater
  target_sensor: sensor.entranceheater_am2301_temperature
  min_temp: 15.0
  max_temp: 23.0
  ac_mode: False
  target_temp: 17.0
  cold_tolerance: 0.0
  hot_tolerance: 0.0
  initial_hvac_mode: "off"
  away_temp: 16
  
- platform: generic_thermostat
  name: Bedroom Heater
  heater: switch.bedroomheater
  target_sensor: sensor.bedroomheater_am2301_temperature
  min_temp: 15.0
  max_temp: 23.0
  ac_mode: False
  target_temp: 17.0
  cold_tolerance: 0.0
  hot_tolerance: 0.0
  initial_hvac_mode: "off"
  away_temp: 16

Just duplicate as need for more heaters

Then, Create a input_select.yaml file in my /config folder with the following:

heating_mode:
  name: Heating Status
  options:
    - Home
    - Away

EDIT: Updated appdaemon to v4 from v3 - config added/updated
Appdaemon Setup:
Install and setup Appdaemon as per add-on instructions. Here is my Appdaemon Config for the add-on:

Appdaemon 3 Add-on config

disable_auto_token: false
system_packages: []
python_packages:
  - hass-apps
init_commands: []

Appdaemon 4 Add-on config

system_packages: []
python_packages:
  - hass-apps
init_commands: []

EDIT: added appdaemon.yaml

Here is my appdaemon.yaml file
Appdaemon 3

log:
  logfile: STDOUT
  errorfile: STDERR
appdaemon:
  threads: 10
  app_dir: /config/appdaemon/apps
  plugins:
    HASS:
      type: hass
      ha_url: http://hassio/homeassistant
      token: GENERATE_LONG_LIVED_TOKEN_FOR_HASSAPPS_IN_HOMEASSISTANT

Appdaemon 4

secrets: /config/secrets.yaml
appdaemon:
  latitude: xx.xxxxxx
  longitude: -x.xxxxxx
  elevation: 2
  time_zone: Europe/London
  plugins:
    HASS:
      type: hass
http:
  url: http://127.0.0.1:5050
hadashboard:
admin:
api:

Hass-Apps Schedy Setup:
install and setup the basic hass-apps Schedy addin (https://hass-apps.readthedocs.io/en/stable/apps/schedy)
In the appdaemon folder update the hass-apps file created during setup or create a new one. Mine is called schedy_heating.yaml located in the “/config/appdaemon/apps” folder

schedy_heating:  # This is our app instance name.
  module: hass_apps_loader
  class: SchedyApp
  actor_type: thermostat
  expression_environment: |
    def heating_mode():
        return state("input_select.heating_mode")
  watched_entities:
  - input_select.heating_mode

  schedule_append:
  - v: 17

  rooms:
    entrance:
      rescheduling_delay: 120
      actors:
        climate.entrance_heater:
      schedule:
      - v: 17
        rules:
        - weekdays: 6-7
          rules:
          - rules:
            - x: "Next() if heating_mode() == 'Home' else Break()"
            - { v: 20.5, start: "09:00", end: "20:00" }
          - rules:
            - x: "Next() if heating_mode() == 'Away' else Break()"
            - { v: 16 }
          - rules:
            - x: "Next() if heating_mode() != 'Home' else Break()"
    bedroom:
      rescheduling_delay: 120
      actors:
        climate.bedroom_heater:
      schedule:
      - v: 17
        rules:
        - weekdays: 6-7
          rules:
          - rules:
            - x: "Next() if heating_mode() == 'Home' else Break()"
            - { v: 20.5, start: "19:00", end: "22:00" }
          - rules:
            - x: "Next() if heating_mode() == 'Away' else Break()"
            - { v: 16 }
          - rules:
            - x: "Next() if heating_mode() != 'Home' else Break()"

Check you config and reboot

You should now have a climate.xxxxx entity you can add to your lovelace ui.

As I use the cloud: component, it adds the climate.xxx device to Alexa (in my case) which adds voice control of each heater which are configured in alexa on a per heater to seperate rooms which have an echo dot per room thus allowing you to say “Alexa, Turn up the heating by 1 degree” etc in each room.

Schedy will also be monitoring each climate.xxx heater and depending on the “resceduling_delay” will reset to the normal sceduled temp if controlled via the UI or iOS app

Hope this helps anyone looking to do similar

I will post a link to my full config in due time on guthub
Matthew

2 Likes

You, my friend, have already won ! :rofl:

2 Likes

you rock…
but maybe explain the Schedy Setup a little clearer. I really thought at first there was another app to install into HassIO. but all you had to do is put the hass-apps package in the app appdaemon config when you installed appdaemon. then do all the steps you showed.
I was still not sure so I just said screw it follow Matthew’s steps and try.
Thanks again for posting this. its very clear for us newbies using the standard hassIO

*my appdaemon.yaml for a fresh install of hassio the other gave me errors *```

---
secrets: /config/secrets.yaml
appdaemon:
  latitude: 37.314417
  longitude: -119.658383
  elevation: 2800
  time_zone: America/Los_Angeles
  plugins:
    HASS:
      type: hass
http:
  url: http://192.168.1.5:5050
admin:
api:
hadashboard:

This maybe the difference between V 3 and V4

Have not plucked up the courage to upgrade yet

I took the plunge and upgraded to Appdaemon 4. Config below (and added to the above)

appdaemon.yaml:

secrets: /config/secrets.yaml
appdaemon:
  latitude: 52.902630
  longitude: -0.091980
  elevation: 2
  time_zone: Europe/London
  plugins:
    HASS:
      type: hass
http:
  url: http://127.0.0.1:5050
hadashboard:
admin:
api:

appdaemon add-on config

system_packages: []
python_packages:
  - hass-apps
init_commands: []

Another approach:

hi there,
just got one question.
I use AVM301 thermostats which are added as climate. in HA.
I also use Schedy and want to manage the temperature from my xiaomi temp. sensors.

My config to create a new “heater” is

#generic climate
  - platform: generic_thermostat
    name: climate_bad
    heater: switch.heater_bad
    target_sensor: sensor.temp_bad_temperature
    precision: 0.5

and I change the climate avm301 entity to a switch with:

#Template HVAC
  - platform: template
    switches:
      heater_bad:
        value_template: "{{ is_state('climate.avm301_dg_bad', 'heat') }}"
        turn_on:
          service: climate.set_hvac_mode
          data:
            entity_id: climate.avm301_dg_bad
            hvac_mode: heat
        turn_off:
          service: climate.set_hvac_mode
          data:
            entity_id: climate.avm301_dg_bad
            hvac_mode: off

but it didnt work…
If I change the temperature at the heater directly, Schedy gets no input and cannot change the temp after a given time…
Also the generic thermostat entity didnt show the correct temperatur.
And if I change the value from the generic thermostat (eg. the heater shows 20° and I put it to 22°) the avm301 dont change the value too…

Do you know a way to solve this issue?

@Timsche2210

This is my generic cliamte


- platform: generic_thermostat
  name: Bedroom2 Heater
  heater: switch.bedroom2_heater_switch
  target_sensor: sensor.bedroom2_sensor_temperature
  min_temp: 15.0
  max_temp: 23.0
  ac_mode: false
  cold_tolerance: 0.1
  hot_tolerance: 0.1
  initial_hvac_mode: "heat"
  away_temp: 16
  precision: 0.5

The target_sensor can be anydevice that is supply the room temp, in my case a Xiomi Aqara so as long as you are seeing the room temp, you will see it on the termostat card

Also in you schedy do you have the climate device as the actor as its named in the climate config?

    bedroom2:
      rescheduling_delay: 60
      actors:
        climate.bedroom2_heater:
      schedule:
      - weekdays: 1-5
        rules:
        - x: "IncludeSchedule(schedule_snippets['mjptraining']) if heating_mjptraining() == 'on' else Next()"
        - x: "IncludeSchedule(schedule_snippets['hobbying']) if heating_hobbying() == 'on' else Next()"
        - v: 17
      - weekdays: 6-7
        rules:
        - x: "IncludeSchedule(schedule_snippets['hobbying']) if heating_hobbying() == 'on' else Next()"
        - v: 17

Some changes to my setup

  • Now using “IncludSnippets” as it has reduced my config a bit
  • Added logic to switch heaters on/off at varying times so I dont overload my incoming supply and trip the house
  • Day or Night HA autiomation for heating zones/rooms at various times
  • Activity based room heating - Bedtime/Training/Hobbying/Weekend work etc
  • Monitoring Octopus Agile and turn on/off heating based on plunge or peak/high pricing
  • Location based room heating - Home/Away

I would say the biggest change was the snippets inclusion as it reduce my schedule massively and the active heating so my electric storage heaters dont keep blasting away all the time but do what they were designed to now

tock_heating:  # This is our app instance name.
  module: hass_apps_loader
  class: SchedyApp
  actor_type: thermostat
  expression_environment: |
    def heating_mode():
        return state("input_select.heating_mode")
    def heating_season():
        return state("input_select.heating_season")
    def front_door():
        return state("binary_sensor.frontdoor_contact")
    def back_door():
        return state("binary_sensor.backdoor_contact")
    def sliding_door():
        return state("binary_sensor.slidingdoor_contact")
    def octopus_price():
        return state("input_select.octopus_price")
    def octopus_tariff():
        return state("input_select.octopus_tariff")
    def power_saving():
        return state("input_boolean.power_saving")
    def heating_bedtime():
        return state("input_boolean.heating_bedtime")
    def heating_mjptraining():
        return state("input_boolean.heating_mjptraining")
    def heating_hobbying():
        return state("input_boolean.heating_hobbying")
    def heating_weekendworking():
        return state("input_boolean.heating_weekendworking")
    def bsp_location():
        return state("input_select.bsp_status")
    def mjp_location():
        return state("input_select.mjp_status")
    def jmp_location():
        return state("input_select.jmp_status")
    def mandj_location():
        return state("input_select.mandj_status")
    def heating_active_upstairs():
        return state("input_boolean.heating_active_upstairs")
    def heating_active_downstairs():
        return state("input_boolean.heating_active_downstairs")

  watched_entities:
  - input_select.heating_mode
  - input_select.heating_season
  - input_boolean.heating_active_upstairs
  - input_boolean.heating_active_downstairs
  - input_boolean.heating_bedtime
  - input_boolean.heating_mjptraining
  - input_boolean.heating_hobbying
  - input_boolean.heating_weekendworking
  - input_select.octopus_price
  - input_select.octopus_tariff
  - input_boolean.power_saving
  - binary_sensor.frontdoor_contact
  - binary_sensor.backdoor_contact
  - binary_sensor.slidingdoor_contact
  - input_select.mjp_status
  - input_select.jmp_status
  - input_select.bsp_status
  - input_select.mandj_status

  schedule_snippets:
    bedrooms:
    - { v: 17, start: "00:00", end: "20:00" }
    - { v: 20, start: "20:00", end: "22:00" }
    - { v: 17, start: "22:00", end: "00:00" }
    bedroom1:
    - { v: 18, start: "00:00", end: "07:00" }
    - { v: 17, start: "07:00", end: "20:00" }
    - { v: 20, start: "20:00", end: "22:00" }
    - { v: 18, start: "22:00", end: "00:00" }
    bedroom1mjpaway:
    - { v: 18, start: "00:00", end: "07:00" }
    - { v: 17, start: "07:00", end: "19:00" }
    - { v: 20, start: "19:00", end: "22:00" }
    - { v: 18, start: "22:00", end: "00:00" }
    home:
    - { v: 17, start: "00:00", end: "08:00" }
    - { v: 18, start: "08:00", end: "20:00" }
    - { v: 17, start: "20:00", end: "00:00" }
    holiday:
    - { v: 17, start: "00:00", end: "00:00" }
    default:
    - { v: 17, start: "00:00", end: "00:00" }
    octopusagile:
    - { v: 17, start: "16:00", end: "19:00" }
    officehours:
    - { v: 17, start: "00:00", end: "07:00" }
    - { v: 21, start: "07:00", end: "17:00" }
    - { v: 17, start: "17:00", end: "00:00" }
    weekendworking:
    - { v: 17, start: "00:00", end: "07:00" }
    - { v: 21, start: "07:00", end: "17:00" }
    - { v: 17, start: "17:00", end: "00:00" }
    hobbying:
    - { v: 17, start: "00:00", end: "07:00" }
    - { v: 21, start: "07:00", end: "17:00" }
    - { v: 17, start: "17:00", end: "00:00" }
    mjptraining:
    - { v: 17, start: "00:00", end: "07:00" }
    - { v: 21, start: "07:00", end: "17:00" }
    - { v: 17, start: "17:00", end: "00:00" }
    loungeweekday:
    - { v: 17, start: "00:00", end: "07:00" }
#    - { v: 18, start: "07:00", end: "16:00" } #OctopusGo
#    - { v: 21, start: "16:00", end: "22:00" } #OctopusGo
    - { v: 18, start: "07:00", end: "14:00" } #OctopusAgile
    - { v: 21, start: "14:00", end: "22:00" } #OctopusAgile
    - { v: 17, start: "22:00", end: "00:00" }
    loungeweekend:
    - { v: 17, start: "00:00", end: "09:00" }
    - { v: 21, start: "09:00", end: "21:00" }
    - { v: 17, start: "21:00", end: "00:00" }
    loungemjpaway:
    - { v: 17, start: "00:00", end: "16:00" }
    - { v: 21, start: "16:00", end: "19:00" }
    - { v: 17, start: "19:00", end: "00:00" }

  schedule_prepend:
  - x: "16 if octopus_price() == 'Peak' else Next()"
  - x: "16 if power_saving() == 'On' else Next()"
  - x: "16 if heating_mode() == 'Away' else Next()"
  - x: "18 if heating_season() == 'Summer' else Next()"
  - x: "16 if heating_mode() == 'Holiday' else Next()"

  schedule_apppend:
  - x: "IncludeSchedule(schedule_snippets['octopusagile']) if octopus_tariff() == 'Agile'"

  rooms:
    entrance:
      rescheduling_delay: 60
      actors:
        climate.entrance_heater:
      schedule:
      - weekdays: 1-7
        rules:
        - x: "23 if octopus_price() == 'Plunge' else Next()"
        - x: "17 if heating_bedtime() == 'on' else Next()"
        - x: "10 if is_on('binary_sensor.frontdoor_contact') else Next()"
        - x: "10 if is_on('binary_sensor.backdoor_contact') else Next()"
        - x: "10 if is_on('binary_sensor.slidingdoor_contact') else Next()"
        - x: "17 if heating_active_downstairs() == 'off' else Next()"
        - x: "IncludeSchedule(schedule_snippets['default']) if mandj_location() == 'Away' else Next()"
        - x: "IncludeSchedule(schedule_snippets['home']) if heating_mode() == 'Home' else Next()"
        - v: 17

    downstairsufh:
      rescheduling_delay: 60
      actors:
        climate.entrance_ufh:
        climate.passage_ufh:
        climate.diningroom_ufh:
      schedule:
      - weekdays: 1-7
        rules:
        - x: "23 if octopus_price() == 'Plunge' else Next()"
        - x: "17 if heating_bedtime() == 'on' else Next()"
        - x: "10 if is_on('binary_sensor.frontdoor_contact') else Next()"
        - x: "10 if is_on('binary_sensor.backdoor_contact') else Next()"
        - x: "10 if is_on('binary_sensor.slidingdoor_contact') else Next()"
        - x: "17 if heating_active_downstairs() == 'off' else Next()"
        - x: "IncludeSchedule(schedule_snippets['default']) if mandj_location() == 'Away' else Next()"
        - x: "IncludeSchedule(schedule_snippets['home']) if heating_mode() == 'Home' else Next()"
        - v: 17

    diningroom:
      rescheduling_delay: 60
      actors:
        climate.diningroom_heater:
      schedule:
      - weekdays: 1-7
        rules:
        - x: "23 if octopus_price() == 'Plunge' else Next()"
        - x: "17 if heating_bedtime() == 'on' else Next()"
        - x: "10 if is_on('binary_sensor.frontdoor_contact') else Next()"
        - x: "10 if is_on('binary_sensor.backdoor_contact') else Next()"
        - x: "10 if is_on('binary_sensor.slidingdoor_contact') else Next()"
        - x: "17 if heating_active_downstairs() == 'off' else Next()"
        - x: "IncludeSchedule(schedule_snippets['default']) if mandj_location() == 'Away' else Next()"
        - x: "IncludeSchedule(schedule_snippets['home']) if heating_mode() == 'Home' else Next()"
        - v: 17

    kitchen:
      rescheduling_delay: 60
      actors:
        climate.kitchen_heater:
      schedule:
      - weekdays: 1-7
        rules:
        - x: "23 if octopus_price() == 'Plunge' else Next()"
        - x: "17 if heating_bedtime() == 'on' else Next()"
        - x: "10 if is_on('binary_sensor.frontdoor_contact') else Next()"
        - x: "10 if is_on('binary_sensor.backdoor_contact') else Next()"
        - x: "10 if is_on('binary_sensor.slidingdoor_contact') else Next()"
        - x: "17 if heating_active_downstairs() == 'off' else Next()"
        - x: "IncludeSchedule(schedule_snippets['default']) if mandj_location() == 'Away' else Next()"
        - x: "IncludeSchedule(schedule_snippets['home']) if heating_mode() == 'Home' else Next()"
        - v: 17

    lounge:
      rescheduling_delay: 60
      actors:
        climate.lounge_front_heater:
        climate.lounge_side_heater:
      schedule:
      - weekdays: 1-5
        rules:
        - x: "23 if octopus_price() == 'Plunge' else Next()"
        - x: "17 if heating_bedtime() == 'on' else Next()"
        - x: "17 if heating_active_downstairs() == 'off' else Next()"
        - x: "IncludeSchedule(schedule_snippets['default']) if mandj_location() == 'Away' else Next()"
        - x: "IncludeSchedule(schedule_snippets['loungemjpaway']) if mjp_location() == 'Away' else Next()"
        - x: "IncludeSchedule(schedule_snippets['loungeweekend']) if heating_mode() == 'OffWork' else Next()"
        - x: "IncludeSchedule(schedule_snippets['loungeweekday']) if heating_mode() == 'Home' else Next()"
        - v: 17
      - weekdays: 6-7
        rules:
        - x: "23 if octopus_price() == 'Plunge' else Next()"
        - x: "17 if heating_bedtime() == 'on' else Next()"
        - x: "17 if heating_active_downstairs() == 'off' else Next()"
        - x: "IncludeSchedule(schedule_snippets['default']) if mandj_location() == 'Away' else Next()"
        - x: "IncludeSchedule(schedule_snippets['loungemjpaway']) if mjp_location() == 'Away' else Next()"
        - x: "IncludeSchedule(schedule_snippets['loungeweekend']) if heating_mode() == 'Home' else Next()"
        - v: 17

    office:
      rescheduling_delay: 60
      actors:
        climate.office_side_heater:
        climate.office_front_heater:
      schedule:
      - weekdays: 1-5
        rules:
        - x: "23 if octopus_price() == 'Plunge' else Next()"
        - x: "17 if heating_active_downstairs() == 'off' else Next()"
        - x: "IncludeSchedule(schedule_snippets['default']) if mandj_location() == 'Away' else Next()"
        - x: "IncludeSchedule(schedule_snippets['officehours']) if mjp_location() == 'Away' else Next()"
        - x: "IncludeSchedule(schedule_snippets['default']) if heating_mode() == 'OffWork' else Next()"
        - x: "IncludeSchedule(schedule_snippets['officehours']) if heating_mode() == 'Home' else Next()"
        - v: 17
      - weekdays: 6-7
        rules:
        - x: "23 if octopus_price() == 'Plunge' else Next()"
        - x: "17 if heating_active_downstairs() == 'off' else Next()"
        - x: "IncludeSchedule(schedule_snippets['weekendworking']) if heating_weekendworking() == 'on' else Next()"
        - v: 17

    masterbathroom:
      rescheduling_delay: 60
      actors:
        climate.master_bathroom_heater:
      schedule:
      - weekdays: 1-7
        rules:
        - x: "23 if octopus_price() == 'Plunge' else Next()"
        - x: "17 if heating_active_upstairs() == 'off' else Next()"
        - v: 17

    guestbathroom:
      rescheduling_delay: 60
      actors:
        climate.guest_bathroom_heater:
      schedule:
      - weekdays: 1-7
        rules:
        - x: "23 if octopus_price() == 'Plunge' else Next()"
        - x: "17 if heating_active_upstairs() == 'off' else Next()"
        - v: 17

    bedroom1:
      rescheduling_delay: 60
      actors:
        climate.bedroom1_heater:
      schedule:
      - weekdays: 1-7
        rules:
        - x: "20 if heating_bedtime() == 'on' else Next()"
        - x: "17 if heating_active_upstairs() == 'off' else Next()"
        - x: "IncludeSchedule(schedule_snippets['default']) if mandj_location() == 'Away' else Next()"
        - x: "IncludeSchedule(schedule_snippets['bedroom1mjpaway']) if mjp_location() == 'Away' else Next()"
        - x: "IncludeSchedule(schedule_snippets['bedroom1']) if heating_mode() == 'Home' else Next()"
        - v: 17

    bedroom2:
      rescheduling_delay: 60
      actors:
        climate.bedroom2_heater:
      schedule:
      - weekdays: 1-5
        rules:
#        - x: "20 if heating_bedtime() == 'on' else Next()" # With guests
        - x: "17 if heating_active_upstairs() == 'off' else Next()"
#        - x: "21 if heating_mjptraining() == 'on' else Next()"
        - x: "IncludeSchedule(schedule_snippets['mjptraining']) if heating_mjptraining() == 'on' else Next()"
        - x: "IncludeSchedule(schedule_snippets['hobbying']) if heating_hobbying() == 'on' else Next()"
#        - x: "IncludeSchedule(schedule_snippets['bedrooms'])" # With guests
        - v: 17
      - weekdays: 6-7
        rules:
#        - x: "20 if heating_bedtime() == 'on' else Next()" # With guests
#        - x: "17 if heating_active_upstairs() == 'off' else Next()"
#        - x: "21 if heating_mjptraining() == 'on' else Next()"
        - x: "IncludeSchedule(schedule_snippets['hobbying']) if heating_hobbying() == 'on' else Next()"
#        - x: "IncludeSchedule(schedule_snippets['bedrooms'])" # With guests
        - v: 17

    bedroom3:
      rescheduling_delay: 60
      actors:
        climate.bedroom3_heater:
      schedule:
      - weekdays: 1-7
        rules:
        - x: "17 if bsp_location() == 'Away' else Next()"
        - x: "20 if heating_bedtime() == 'on' else Next()"
        - x: "17 if heating_active_upstairs() == 'off' else Next()"
        - x: "IncludeSchedule(schedule_snippets['bedrooms']) if heating_mode() == 'Home' else Next()"
        - v: 17

    bedroom4:
      rescheduling_delay: 60
      actors:
        climate.bedroom1_heater:
      schedule:
      - weekdays: 1-7
        rules:
#        - x: "20 if heating_bedtime() == 'on' else Next()"
        - x: "17 if heating_active_upstairs() == 'off' else Next()"
#        - x: "IncludeSchedule(schedule_snippets['bedrooms'])"
        - v: 17

A new alternative to scheduling is by calendar. See my Heating X Blueprint.