Skip to content

Commit a54fbad

Browse files
author
Robert Schindler
committed
[schedy] Simplified internal handling of IncludeSchedule
1 parent 5402076 commit a54fbad

6 files changed

Lines changed: 53 additions & 145 deletions

File tree

docs/apps/schedy/CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.
1919
* Infinite retrying of value sending to an actor is no longer supported, meaning
2020
`send_retries: -1` is now a configuration error. Use a reasonably high value
2121
instead if you really need excessive retrying.
22+
* Simplified internal handling of `IncludeSchedule()`. If this causes problems with
23+
existing configurations, please file an issue.
2224

2325
### Deprecated
2426

docs/apps/schedy/schedules/expressions/examples.rst

Lines changed: 0 additions & 85 deletions
Original file line numberDiff line numberDiff line change
@@ -224,91 +224,6 @@ weather sensors.
224224
readable as they grow.
225225

226226

227-
.. _schedy/schedules/expressions/examples/include-schedule/cycles:
228-
229-
Cycles
230-
~~~~~~
231-
232-
.. include:: /advanced-topic.rst.inc
233-
234-
Schedy prevents you from creating cycles when using ``IncludeSchedule()``,
235-
that would lead to infinite recursion.
236-
237-
Take this example. It's quite useless, but simple enough for demonstration
238-
purposes.
239-
240-
::
241-
242-
schedule_snippets:
243-
snippet:
244-
- x: "IncludeSchedule(schedule_snippets['snippet'])"
245-
name: snippet rule 1
246-
247-
schedule_prepend:
248-
- v: 21
249-
rules:
250-
- x: "IncludeSchedule(schedule_snippets['snippet'])"
251-
252-
The ``IncludeSchedule()`` returned by ``snippet rule 1`` is not
253-
considered, because doing so would lead to an infinite recursion. It is
254-
treated as if it was ``Inherit()``, what then causes value lookup to be
255-
continued at the next parent, resulting in ``21`` .
256-
257-
Here's another, more useful example, inspired by my own configuration,
258-
which makes the benefits of this behaviour more obvious.
259-
260-
::
261-
262-
schedule_snippets:
263-
somebody_home:
264-
- x: "Inherit() if is_on('input_boolean.awake') else Next()"
265-
name: always heat when awake switch is on
266-
- weekdays: 1-5
267-
name: normal working days
268-
rules:
269-
- { start: "07:00", end: "09:00" }
270-
- { start: "16:00", end: "22:00" }
271-
272-
schedule_prepend:
273-
- v: 15
274-
275-
watched_entities:
276-
- "input_boolean.awake"
277-
278-
rooms:
279-
living:
280-
schedule:
281-
- v: 22
282-
rules:
283-
- x: "IncludeSchedule(schedule_snippets['somebody_home'])"
284-
285-
office:
286-
schedule:
287-
- v: 20
288-
rules:
289-
- x: "IncludeSchedule(schedule_snippets['somebody_home'])"
290-
name: include somebody_home snippet
291-
rules:
292-
- { start: "06:00", end: "18:00", name: "stop at 6.00 pm" }
293-
294-
I'm not going to describe what every single rule does. If you want to
295-
see the actual evaluation flow, put it into your configuration and turn
296-
on the debug logging.
297-
298-
What I do want to highlight is that each room can have it's own heating
299-
temperature, without having to define the times redundantly. I can even
300-
limit the ``office`` to heat no longer than until ``18:00``, and that's
301-
where the magic kicks in. The ``stop at 6.00 pm`` rule inherits its result
302-
from its parent rule (``include somebody_home snippet``), which causes the
303-
``somebody_home`` snippet to be inserted and evaluated. Now, we assume
304-
that ``always heat when awake switch is on`` returns ``Inherit()``,
305-
which again causes its parent's value to be used. The nearest parent
306-
with a value again is ``include somebody_home snippet`` - and there we
307-
would get an infinite recursion. But Schedy is smart enough to notice that
308-
we're already inside the ``somebody_home`` snippet and just takes the next
309-
parent's value, which is ``20`` and exactly what we want. Problem solved.
310-
311-
312227
What to Use ``Abort()`` for
313228
---------------------------
314229

docs/apps/schedy/schedules/expressions/index.rst

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,6 @@ make their later evaluation really performant.
2020
helpers/index
2121
postprocessors
2222
result-markers
23-
sub-schedules
2423

2524

2625
Security Considerations

docs/apps/schedy/schedules/expressions/sub-schedules.rst

Lines changed: 0 additions & 38 deletions
This file was deleted.

docs/apps/schedy/schedules/expressions/writing-expressions.rst

Lines changed: 36 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -86,12 +86,46 @@ processed.
8686
* ``Break(levels=1)``, which causes lookup of one (or multiple nested)
8787
sub-schedule(s) to be aborted immediately. The evaluation will continue
8888
after the sub-schedule(s).
89-
* ``IncludeSchedule(schedule)``, which dynamically inserts the given
90-
schedule object as a sub-schedule after the current rule.
89+
* ``IncludeSchedule(schedule)``, which dynamically inserts a sub-schedule rule with
90+
the given ``Schedule`` object after the current rule. Especially, ``Break()`` and
91+
``Inherit()`` in included schedules do behave as if the included schedule was a
92+
regular sub-schedule.
9193
* ``Inherit()``, which causes the value or expression of the nearest
9294
ancestor rule to be used as result for the current rule. See the next
9395
section for a more detailed explanation.
9496
* ``Next()``, which causes the rule to be treated as if it didn't exist
9597
at all. If one exists, the next rule is evaluated in this case.
9698

9799
For all of these types, :doc:`usage examples <examples>` are provided.
100+
101+
102+
Expressions and Sub-Schedules
103+
-----------------------------
104+
105+
In general, there is no difference between using plain values and advanced
106+
expressions in both rules with a sub-schedule attached to them (so-called
107+
sub-schedule rules) and the rules contained in these sub-schedules. But
108+
with expressions, you gain a lot more flexibility.
109+
110+
As you know from :ref:`schedy/schedules/basics/rules-with-sub-schedules`,
111+
rules of sub-schedules inherit their ``v`` parameter from the nearest
112+
ancestor rule having it defined, should they miss an own one. Basically,
113+
this is true for the ``x`` parameter as well.
114+
115+
With an expression as the ``x`` value of the rule inside a sub-schedule,
116+
you get the flexibility to conditionally overwrite the ancestor rule's
117+
value or expression. Should an expression return ``Inherit()``, the next
118+
ancestor rule's value or expression is used. When compared to static
119+
values, returning ``Inherit()`` is the equivalent of omitting the ``v``
120+
parameter completely, but with the benefit of deciding dynamically about
121+
whether to omit it or not.
122+
123+
The whole process can be described as follows. To find the result for
124+
a particular rule inside a sub-schedule, the ``v``/``x`` parameters of
125+
the rule and it's ancestor rules are evaluated from inside to outside
126+
(from right to left when looking at the indentation of the YAML syntax)
127+
until one results in something different to ``Inherit()``.
128+
129+
``Inherit()`` even works accross the boundaries of a schedule snippet, because Schedy
130+
internally converts a rule which returned ``IncludeSchedule()`` into a sub-schedule
131+
rule, with the included schedule attached to it.

hass_apps/schedy/schedule.py

Lines changed: 15 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -261,10 +261,8 @@ def extend(self, rules: T.Iterable[Rule]) -> None:
261261

262262
def includes_schedule(self, schedule: "Schedule") -> bool:
263263
"""Checks whether the given schedule is included in this path."""
264-
265264
if schedule is self.root_schedule:
266265
return True
267-
268266
for rule in self.rules:
269267
if isinstance(rule, SubScheduleRule) and rule.sub_schedule is schedule:
270268
return True
@@ -474,6 +472,9 @@ def log(msg: str, path: RulePath, *args: T.Any, **kwargs: T.Any) -> None:
474472
result = room.eval_expr(rule.expr, expr_env)
475473
expr_cache[rule.expr] = result
476474
log("=> {}".format(repr(result)), path, level="DEBUG")
475+
# Unwrap a result with markers
476+
if isinstance(result, expression.types.Mark):
477+
result = result.unwrap(markers)
477478
else:
478479
log(
479480
"=> {} [cache-hit]".format(repr(result)),
@@ -490,29 +491,23 @@ def log(msg: str, path: RulePath, *args: T.Any, **kwargs: T.Any) -> None:
490491
result = rule.value
491492
log("=> {}".format(repr(result)), path, level="DEBUG")
492493

493-
# Unwrap a result with markers
494-
if isinstance(result, expression.types.Mark):
495-
result = result.unwrap(markers)
496-
497494
if isinstance(
498495
result, expression.types.IncludeSchedule
499496
) and path.includes_schedule(result.schedule):
500-
# Prevent reusing IncludeSchedule results that would
501-
# lead to a cycle. This happens when a rule of an
502-
# included schedule returns Inherit() and the search
503-
# then reaches the IncludeSchedule within the parent.
504-
log(
505-
"== skipping in favour of the parent to prevent " "a cycle",
506-
path,
507-
level="DEBUG",
497+
room.log(
498+
"IncludeSchedule() created a cycle, which would lead to "
499+
"infinite recursion in rule {}.".format(path),
500+
level="ERROR",
508501
)
509502
result = None
510-
elif result is None or isinstance(result, expression.types.Inherit):
511-
log("== skipping in favour of the parent", path, level="DEBUG")
512-
result = None
513-
else:
514503
break
515504

505+
if result is None or isinstance(result, expression.types.Inherit):
506+
log("== skipping in favour of parent", path, level="DEBUG")
507+
result = None
508+
continue
509+
break
510+
516511
if result is None:
517512
room.log(
518513
"No expression/value definition found, skipping {}.".format(path),
@@ -535,7 +530,8 @@ def log(msg: str, path: RulePath, *args: T.Any, **kwargs: T.Any) -> None:
535530
):
536531
del paths[path_idx]
537532
elif isinstance(result, expression.types.IncludeSchedule):
538-
# Replace the current rule with a dynamic SubScheduleRule
533+
# Replace current rule with temporary SubScheduleRule to enable
534+
# proper handling of Inherit() and Break()
539535
_path = path.copy()
540536
_path.pop()
541537
_path.append(SubScheduleRule(result.schedule))

0 commit comments

Comments
 (0)