Skip to content

Commit 2d36bc1

Browse files
author
Robert Schindler
committed
[schedy] Reworked rule selection algorithm
1 parent a435917 commit 2d36bc1

File tree

7 files changed

+367
-217
lines changed

7 files changed

+367
-217
lines changed

docs/apps/schedy/CHANGELOG.md

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,19 +9,25 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.
99
## Unreleased
1010

1111
### Fixed
12-
* Fixed a bug in schedule.next_results() expression helper that caused
13-
some result changes to be skipped.
14-
* Simplified the algorithm that decides whether a rule is active or not
15-
at a given point in time. It now handles rules spanning multiple
16-
days correctly.
12+
* Fixed a bug in schedule.next_results() expression helper that caused some result
13+
changes to be skipped.
14+
* Simplified the algorithm that decides whether a rule is active or not at a given
15+
point in time. It should now handle all rules spanning multiple days correctly.
1716

1817
### Security
1918

2019
### Added
2120

2221
### Changed
22+
* The ``start`` and ``end`` rule parameters now accept day shifts, deprecating the
23+
former ``end_plus_days``.
24+
* Constraints of rules with a sub-schedule attached are now only validated for the
25+
day at which a particular rule starts. Hence rules of such sub-schedules spanning
26+
midnight will now run until they're intended to end.
2327

2428
### Deprecated
29+
* 0.6: The ``end_plus_days`` rule parameter will be removed in favor of the new day
30+
shifts specified with ``start`` and ``end``.
2531

2632
### Removed
2733

docs/apps/schedy/events.rst

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@ control Schedy's behaviour.
6565

6666
All events have an optional ``app_name`` parameter that can be submitted
6767
when you have multiple instances of Schedy running for different purposes
68-
and you want to address exactly one of these instances. It's value has
68+
and you want to address exactly one of these instances. Its value has
6969
to be the name of the app instance as configured in AppDaemon. If you
7070
omit this parameter, all Schedy instances will react to the event. The
7171
app name is the name you start the app's configuration with:

docs/apps/schedy/schedules/basics.rst

Lines changed: 59 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ the 16 degrees-rule and Schedy evaluates rules from top to bottom. From
4949
the value to set. Consequently, you should design your schedules with
5050
the most specific rules at the top and gradually generalize to wider
5151
time frames towards the bottom. Finally, there should be a fallback
52-
rule without time constraints at all to ensure you have no time slot
52+
rule without time restrictions at all to ensure you have no time slot
5353
left without a value defined for.
5454

5555
The ``name`` parameter we specified here is completely optional and
@@ -66,6 +66,8 @@ still can't create different schedules for, for instance, the days of
6666
the week. Let's do this next.
6767

6868

69+
.. _schedy/schedules/basics/constraints:
70+
6971
Constraints
7072
-----------
7173

@@ -83,13 +85,14 @@ Constraints
8385

8486
- v: 15
8587

86-
With your knowledge so far, this should be self-explanatory. The only
87-
new parameter is ``weekdays``, which is a so called constraint.
88+
With your knowledge so far, this should be self-explanatory. The only new parameter is
89+
``weekdays``, which is a so called constraint.
8890

89-
Constraints can be used to limit the starting days on which the rule is
90-
considered. There are a number of these constraints, namely:
91+
Constraints can be used to limit the days on which the rule should start to be
92+
active. There are a number of these constraints, namely:
9193

92-
* ``years``: limit the years (e.g. ``years: 2016-2018``
94+
* ``years``: limit the years (e.g. ``years: 2016-2018``); only years from 1970 to
95+
2099 are supported
9396
* ``months``: limit based on months of the year (e.g.
9497
``months: 1-3, 10-12`` for Jan, Feb, Mar, Oct, Nov and Dec)
9598
* ``days``: limit based on days of the month (e.g.
@@ -111,11 +114,12 @@ considered. There are a number of these constraints, namely:
111114
provided, the nearest prior valid date (namely 2018-02-28 in this
112115
case) is assumed.
113116

114-
All constraints you define need to be fulfilled for the rule to match.
117+
A date needs to fulfill all constraints you defined for a rule to be considered
118+
active at that specific date.
115119

116-
The format used to specify values for the first five types of constraints
117-
is similar to that of crontab files. We call it range specification,
118-
and only integers are supported, no decimal values.
120+
The format used to specify values for the first five types of constraints is similar
121+
to that of crontab files. We call it range specification, and only integers are
122+
supported, no decimal values.
119123

120124
* ``x``: the single number ``x``
121125
* ``x-y`` where ``x < y``: range of numbers from ``x`` to ``y``,
@@ -129,15 +133,14 @@ and only integers are supported, no decimal values.
129133
* ... and so on
130134
* Any spaces are ignored.
131135

132-
If an exclamation mark (``!``) is prepended to the range specification,
133-
it's values are inverted. For instance, the constraint ``weekdays:
134-
"!4-5,7"`` expands to ``weekdays: 1,2,3,6`` and ``months: "!3"`` is
135-
equivalent to ``months: 1-2,4-12``.
136+
If an exclamation mark (``!``) is prepended to the range specification, its values are
137+
inverted. For instance, the constraint ``weekdays: "!4-5,7"`` expands to ``weekdays:
138+
1,2,3,6`` and ``months: "!3"`` is equivalent to ``months: 1-2,4-12``.
136139

137140
.. note::
138141

139-
The ``!`` sign has a special meaning in YAML, hence inverted
140-
specifications have to be enclosed in quotes.
142+
The ``!`` sign has a special meaning in YAML, hence inverted specifications have
143+
to be enclosed in quotes.
141144

142145

143146
Rules Spanning Multiple Days
@@ -157,36 +160,56 @@ If you omit the ``start`` parameter, Schedy assumes that you mean midnight
157160
a rule that ends the same moment it starts at wouldn't make sense. We
158161
expect it to count for the whole day instead.
159162

160-
In order to express what we actually want, there's another parameter named
161-
``end_plus_days`` to tell Schedy how many midnights there are between
162-
the start and end time. As we didn't specify this parameter explicitly,
163-
it's value is determined by Schedy. If the end time of the rule is prior
164-
or equal to its start time, ``end_plus_days`` is assumed to be
165-
``1``, otherwise ``0``.
166-
167-
.. note::
168-
169-
The value of ``end_plus_days`` can't be negative, meaning you can't
170-
span a rule backwards in time. Only positive integers and ``0``
171-
are allowed.
163+
In order to express what we actually want, we'd have to set ``end`` to ``"00:00+1d"``,
164+
which tells Schedy that there is one midnight between the start and end times. For
165+
convenience, Schedy automatically assumes one midnight between start and end when
166+
you don't specify a number of days explicitly and the start time is prior or equal
167+
to the end time, as in our case.
172168

173169
.. note::
174170

175-
You don't need to care about setting ``end_plus_days`` yourself,
176-
unless one of your rules should span more than 24 hours, requiring
177-
``end_plus_days: 2`` or greater.
171+
You don't need to care about setting ``+?d`` yourself unless one of your rules
172+
should span more than 24 hours, requiring ``+1d`` or greater.
178173

179174
Having written out what Schedy assumes automatically would result in
180175
the following rule, which behaves exactly identical to what we begun with.
181176

182177
::
183178

184-
- { v: 16, start: "0:00", end: "0:00", end_plus_days: 1 }
179+
- { v: 16, start: "0:00", end: "0:00+1d" }
180+
181+
.. note::
182+
183+
The rule has been rewritten to take just a single line. This is no
184+
special feature of Schedy, it's rather normal YAML. But writing rules
185+
this way is often more readable, especially if you need to create
186+
multiple similar ones which, for instance, only differ in weekdays,
187+
time or value.
188+
189+
Let's get back to :ref:`schedy/schedules/basics/constraints` briefly. We know that
190+
constraints limit the days on which a rule starts to be active. This explanation is
191+
not correct in all cases, as you'll see now.
192+
193+
There are some days, such as the last day of a month, which can't be expressed
194+
using constraints explicitly. To allow targeting such days anyway, the ``start``
195+
parameter of a rule accepts a day shifting suffix as well. Your constraints are
196+
checked for some date, but the rule starts being active some days earlier or later,
197+
relative to the matching date.
198+
199+
Even though you can't specify the last day of a month, you can well specify the
200+
1st. This rule is active on the last day of February from 6.00 pm to 10.00 pm,
201+
no matter if in a leap year or not::
202+
203+
- { v: 22, start: "18:00-1d", end: "22:00", days: 1, months: 3 }
204+
205+
This one even runs until March 1st, 10.00 pm::
206+
207+
- { v: 22, start: "18:00-1d", end: "22:00+1d", days: 1, months: 3 }
185208

186-
Note how the rule has been rewritten to take just a single line. This is
187-
no special feature of Schedy, it's rather normal YAML. But writing rules
188-
this way is often more readable, especially if you need to create multiple
189-
similar ones which, for instance, only differ in weekdays, time or value.
209+
As you noted, the day shift of ``start`` can be negative as well, but not that of
210+
``end``, meaning your rules can't span backwards in time. This design decision was
211+
made in order to keep rules readable and the evaluation algorithm simple. It neither
212+
has a technical reason nor does it reduce the expressiveness of rules.
190213

191214

192215
.. _schedy/schedules/basics/rules-with-sub-schedules:

hass_apps/schedy/config.py

Lines changed: 32 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -33,9 +33,10 @@ def build_schedule_rule(rule: dict) -> schedule.Rule:
3333
)
3434

3535
kwargs = {
36-
"start_time": rule["start"],
37-
"end_time": rule["end"],
38-
"end_plus_days": rule["end_plus_days"],
36+
"start_time": rule["start"][0],
37+
"start_plus_days": rule["start"][1],
38+
"end_time": rule["end"][0],
39+
"end_plus_days": rule["end"][1],
3940
"constraints": constraints,
4041
"expr": expr,
4142
"expr_raw": expr_raw,
@@ -151,20 +152,33 @@ def config_post_hook(cfg: dict) -> dict:
151152

152153
return cfg
153154

155+
154156
def schedule_rule_pre_hook(rule: dict) -> dict:
155-
"""Copy value for the value key over from alternative names."""
157+
"""Copy value for the expression and value keys over from alternative names."""
156158

157159
rule = rule.copy()
158160
util.normalize_dict_key(rule, "expression", "x")
159161
util.normalize_dict_key(rule, "value", "v")
162+
# Merge the legacy end_plus_days field into end
163+
end = rule.get("end")
164+
end_plus_days = rule.pop("end_plus_days", None)
165+
if isinstance(end_plus_days, int):
166+
if end is None:
167+
end = ""
168+
if isinstance(end, str) and "+" not in end and "-" not in end:
169+
if end_plus_days < 0:
170+
end += "{}".format(end_plus_days)
171+
else:
172+
end += "-{}".format(end_plus_days)
173+
rule["end"] = end
160174
return rule
161175

162176
def validate_rule_paths(sched: schedule.Schedule) -> schedule.Schedule:
163177
"""A validator to be run after schedule creation to ensure
164178
each path contains at least one rule with an expression or value.
165179
A ValueError is raised when this check fails."""
166180

167-
for path in sched.unfold():
181+
for path in sched.unfolded:
168182
if path.is_final and not list(path.rules_with_expr_or_value):
169183
raise ValueError(
170184
"No expression or value specified along the path {}."
@@ -196,6 +210,10 @@ def build_range_spec_validator(min_value: int, max_value: int) -> vol.Schema:
196210
vol.Match(util.TIME_REGEXP),
197211
util.parse_time_string,
198212
)
213+
TIME_PLUS_DAYS_VALIDATOR = vol.All(
214+
vol.Match(util.TIME_PLUS_DAYS_REGEXP),
215+
util.parse_time_plus_days_string,
216+
)
199217

200218
# This schema does no real validation and default value insertion,
201219
# it just ensures a dictionary containing string keys and dictionary
@@ -265,10 +283,15 @@ def parse_watched_entity_str(value: str) -> T.Dict[str, T.Any]:
265283
"expression": str,
266284
"value": object,
267285
vol.Optional("name", default=None): vol.Any(str, None),
268-
vol.Optional("start", default=None): vol.Any(TIME_VALIDATOR, None),
269-
vol.Optional("end", default=None): vol.Any(TIME_VALIDATOR, None),
270-
vol.Optional("end_plus_days", default=None):
271-
vol.Any(vol.All(int, vol.Range(min=0)), None),
286+
vol.Optional("start", default=(None, None)):
287+
vol.Any((None, None), TIME_PLUS_DAYS_VALIDATOR),
288+
vol.Optional("end", default=(None, None)): vol.Any(
289+
(None, None),
290+
vol.All(
291+
TIME_PLUS_DAYS_VALIDATOR,
292+
(object, vol.Any(None, vol.Range(min=0))),
293+
),
294+
),
272295
vol.Optional("years"): build_range_spec_validator(1970, 2099),
273296
vol.Optional("months"): build_range_spec_validator(1, 12),
274297
vol.Optional("days"): build_range_spec_validator(1, 31),

0 commit comments

Comments
 (0)