Examples

Considering the State of Entities

Let’s say we use the thermostat actor type and have a switch that should prepare our bathroom for taking a bath. It’s name is switch.take_a_bath. We write the following schedule for the room bathroom.

schedule:
- x: "22 if is_on('switch.take_a_bath') else Next()"
- v: 19

Last step is to tell Schedy to watch for changes of the state of switch.take_a_bath, so that it can re-evaluate the schedule of the bathroom when the switch is toggled. We add the following to the room’s configuration:

watched_entities:
- "switch.take_a_bath"

We’re done! Now, whenever we toggle the take_a_bath switch, the schedule is re-evaluated and our first schedule rule executes. The rule is evaluating our custom expression, checking the state of the take_a_bath switch and, if it’s enabled, causes the temperature to be set to 22 degrees. However, if the switch is off, the rule is ignored completely due to the Next() we return in that case and the second rule is processed, which always evaluates to 19 degrees.

What’s so nice about these ... if ... else ... expressions in Python is that they’re almost always self-explanatory. We’ll use them extensively in the following examples.

Use of Add() and Next()

This is something I once used in my own heating configuration at home:

schedule_prepend:
- x: "Add(-3) if is_on('input_boolean.absent') else Next()"
watched_entities:
- "input_boolean.absent"

What does this? Well, the first thing we see is that the rule is placed inside the schedule_prepend section. That means, it is valid for every room and always the first rule being evaluated.

I’ve defined an input_boolean called absent in Home Assistant. Whenever I leave the house, this gets enabled. If I return, it’s turned off again. In order for Schedy to notice the toggling, I added it to the global watched_entities configuration.

Now let’s get back to the schedule rule. When it evaluates, it checks the state of input_boolean.absent. If the switch is turned on, it evaluates to Add(-3), otherwise to Next().

Add(-3) is a so-called postprocessor. Think of it as a temporary value that is remembered and used later, after a real result was found.

Now, my regular schedule starts being evaluated, which, of course, is different for every room. Rules are evaluated just as normal. If one returns a result, that is used as the temperature and evaluation stops. But wait, there was the Add(-3), wasn’t it? Hence -3 is now added to the final result.

With this minimal configuration effort, I added an useful away-mode which throttles all thermostats in the house as soon as I leave.

Think of a device tracker that is able to report the distance between you and your home. Having such one set up, you could even implement dynamic throttling that slowly decreases as you near with almost zero configuration effort.

Conditional Sub-Schedules Using Break()

When in a sub-schedule, returning Break() from an expression will skip the remaining rules of that sub-schedule and continue evaluation after it. You can use it together with Next() to create a conditional sub-schedule, for instance. Again, we assume to write a schedule for the thermostat actor type.

schedule:
- v: 20
  rules:
  - x: "Next() if is_on('input_boolean.include_sub_schedule') else Break()"
  - { start: "07:00", end: "09:00" }
  - { start: "12:00", end: "22:00" }
  - v: 17
 - v: "OFF"

watched_entities:
- "input_boolean.include_sub_schedule"

The rules 2-4 of the sub-schedule will only be respected when input_boolean.include_sub_schedule is on. Otherwise, evaluation continues with the last rule, setting the value to OFF.

Note

Since schedule_prepend, a room’s individual schedule and schedule_append are just sub-schedules chained internally, returning Break() from a top-level rule of one of these three sections causes evaluation to be continued with the next section.

The actual definition of this result type is Break(levels=1), which means that you may optionally pass a parameter called levels to Break(). This parameter controls how many levels of nested sub-schedules to break out of. The implicit default value 1 will only abort the innermost sub-schedule (the one currently in). However, you may want to directly abort its parent schedule as well by returning Break(2). In the above example, this would actually break the room’s schedule and hence continue evaluating the schedule_append section.

Here’s another example with multiple nested sub-schedules utilizing Break(). It is used by an user of Schedy to turn his bathroom floor heating on at specific times, but only when the outside temperature is 5 degrees or lower. It additionally differenciates between away, holiday and normal modes.

schedule:
- v: "on"
  rules:
  # don't turn on when it's > 5 degrees outside
  - x: "Break() if float(state('sensor.outside_temperature') or 0) > 5 else Next()"

  # don't turn on when in away mode
  - x: "Break() if is_on('input_boolean.away') else Next()"

  # on weekends and during holidays, turn on from 09:00 to 10:30
  - rules:
    - x: "Next() if is_on('input_boolean.holidays') else Break()"
      weekdays: "!6-7"
    - { start: "09:00", end: "10:30" }

  # on normal working days, turn on from 06:30 to 07:00
  - weekdays: 1-5
    rules:
    - { start: "06:30", end: "07:00"}

# at all other times, turn off
- v: 'off'

watched_entities:
- "sensor.outside_temperature"
- "input_boolean.away"
- "input_boolean.holidays"

Including Schedules Dynamically with IncludeSchedule()

The IncludeSchedule() result type for expressions can be used to insert a set of schedule rules right at the position of the current rule. This comes handy when a set of rules needs to be chosen depending on the state of entities or is reused in multiple rooms.

Note

If you just want to prevent yourself from repeating the same static constraints over and over for multiple consecutive rules that are used only once in your configuration, use the sub-schedule feature of the normal rule syntax instead.

You can reference any schedule defined under schedule_snippets in the configuration, hence we create one to play with for our heating setup:

schedule_snippets:
  vacation:
  - { v: 21, start: "08:30", end: "23:00" }
  - { v: 16 }

Now, we include the snippet into a room’s schedule:

schedule:
- x: "IncludeSchedule(schedule_snippets['vacation']) if is_on('input_boolean.vacation') else Next()"
# when not in vacation mode, have the normal per-room schedule
- { v: 21, start: "07:00", end: "21:30", weekdays: 1-5 }
- { v: 21, start: "08:00", end: "23:00", weekdays: 6-7 }
- { v: 16 }

watched_entities:
- "input_boolean.vacation"

It turns out that you could have done the exact same without including a snippet by adding the vacation rules directly to the room’s schedule, but doing it this way makes the configuration more readable, easier to maintain and avoids redundancy in case you want to include the vacation snippet into other rooms as well.

Other use cases for IncludeSchedule are selecting different schedules based on presence (maybe even long holidays vs. short absence) or weather sensors.

Note

Splitting up schedules doesn’t bring any extra power to Schedy’s scheduling capabilities, but it can make configurations much more readable as they grow.

What to Use Abort() for

The Abort return type is most useful for disabling Schedy’s scheduling mechanism depending on the state of entities. You might implement on/off switches for disabling the schedules with it, like so:

schedule_prepend:
- name: global schedule on/off switch
  x: "Abort() if is_off('input_boolean.schedy') else Next()"
- name: per-room schedule on/off switch
  x: "Abort() if is_off('input_boolean.schedy_room_' + room_name) else Next()"

# These should trigger a re-evaluation in every room.
watched_entities:
- "input_boolean.schedy"

# And for these it is sufficient to re-evaluate the corresponding room only.
rooms:
  living:
    watched_entities:
    - "input_boolean.schedy_room_living"
  kitchen:
    watched_entities:
    - "input_boolean.schedy_room_kitchen"

As soon as Abort() is returned, schedule evaluation is aborted and the value stays unchanged.

Using the Generic Postprocess() Postprocessor

The Postprocess() postprocessor lets you alter the result of scheduling in arbitrary ways. It takes a callable which is then called with the result as its argument and should return the eventually altered result.

In this example, we use Postprocess() with lambda closures (in-line functions that generate their return value with only a single expression) to limit the scheduled value to the range from 16 to 22. This could be useful for a temperature, for instance.

- x: "Postprocess(lambda result: max(16, result))"
- x: "Postprocess(lambda result: min(result, 22))"

You could of course have done this with a single postprocessor as well.

- x: "Postprocess(lambda result: max(16, min(result, 22)))"

Instead of lambda closures, normal functions may also be used. Here is an identically behaving, quite verbose implementation.

- x: |
    def limit(r):
        if r < 16:
            return 16
        if r > 22:
            return 22
        return r

    result = Postprocess(limit)

Here’s another one which actually behaves like Add(-3).

- x: "Postprocess(lambda result: result - 3)"

Note

As you know, evaluation stops at the first rule generating a result. Hence you need to ensure the rules returning postprocessors are placed before the rules that generate the results to be postprocessed, not after them.