Skip to content

Commit af7807d

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

File tree

7 files changed

+352
-202
lines changed

7 files changed

+352
-202
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: 44 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -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

@@ -89,7 +91,8 @@ new parameter is ``weekdays``, which is a so called constraint.
8991
Constraints can be used to limit the starting days on which the rule is
9092
considered. 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.
@@ -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)