hassl 0.3.1__py3-none-any.whl → 0.4.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- hassl/__init__.py +1 -1
- hassl/ast/nodes.py +16 -1
- hassl/cli.py +54 -188
- hassl/codegen/package.py +23 -4
- hassl/codegen/rules_min.py +13 -19
- hassl/parser/hassl.lark +68 -30
- hassl/parser/transform.py +252 -345
- hassl/semantics/analyzer.py +235 -12
- {hassl-0.3.1.dist-info → hassl-0.4.0.dist-info}/METADATA +6 -6
- hassl-0.4.0.dist-info/RECORD +23 -0
- {hassl-0.3.1.dist-info → hassl-0.4.0.dist-info}/WHEEL +1 -1
- hassl-0.3.1.dist-info/RECORD +0 -23
- {hassl-0.3.1.dist-info → hassl-0.4.0.dist-info}/entry_points.txt +0 -0
- {hassl-0.3.1.dist-info → hassl-0.4.0.dist-info}/licenses/LICENSE +0 -0
- {hassl-0.3.1.dist-info → hassl-0.4.0.dist-info}/top_level.txt +0 -0
hassl/semantics/analyzer.py
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
|
-
from dataclasses import dataclass, field
|
|
1
|
+
from dataclasses import dataclass, field, is_dataclass, asdict
|
|
2
2
|
from typing import Dict, List, Any, Optional, Tuple
|
|
3
|
+
import copy
|
|
3
4
|
from ..ast.nodes import (
|
|
4
5
|
Program, Alias, Sync, Rule, Schedule,
|
|
5
6
|
HolidaySet, ScheduleWindow, PeriodSelector,
|
|
7
|
+
TemplateDecl, UseTemplate,
|
|
6
8
|
)
|
|
7
9
|
from .domains import DOMAIN_PROPS, domain_of
|
|
8
10
|
|
|
@@ -60,6 +62,20 @@ class IRProgram:
|
|
|
60
62
|
"schedules_windows": self.schedules_windows or {}, # NEW
|
|
61
63
|
}
|
|
62
64
|
|
|
65
|
+
def _resolve_module_id(raw_mod: str) -> str:
|
|
66
|
+
"""
|
|
67
|
+
Map a raw import like 'hall.aliases' to the actual package id present in GLOBAL_EXPORTS,
|
|
68
|
+
e.g. 'home.hall.aliases'. If multiple candidates match, keep raw (fail gently).
|
|
69
|
+
"""
|
|
70
|
+
if 'GLOBAL_EXPORTS' not in globals() or not raw_mod:
|
|
71
|
+
return raw_mod
|
|
72
|
+
# exact hit?
|
|
73
|
+
if any(pkg == raw_mod for (pkg, _k, _n) in globals()['GLOBAL_EXPORTS']):
|
|
74
|
+
return raw_mod
|
|
75
|
+
# suffix match (common when files declare 'home.hall.aliases' but source wrote 'hall.aliases')
|
|
76
|
+
candidates = {pkg for (pkg, _k, _n) in globals()['GLOBAL_EXPORTS'] if pkg.endswith("." + raw_mod)}
|
|
77
|
+
return next(iter(candidates)) if len(candidates) == 1 else raw_mod
|
|
78
|
+
|
|
63
79
|
def _resolve_alias(e: str, amap: Dict[str,str]) -> str:
|
|
64
80
|
if "." not in e and e in amap: return amap[e]
|
|
65
81
|
return e
|
|
@@ -114,8 +130,148 @@ def analyze(prog: Program) -> IRProgram:
|
|
|
114
130
|
local_schedule_windows: Dict[str, List[ScheduleWindow]] = {}
|
|
115
131
|
local_holidays: Dict[str, HolidaySet] = {}
|
|
116
132
|
|
|
117
|
-
#
|
|
118
|
-
|
|
133
|
+
# 0) Preprocess: collect templates and expand 'use template' into concrete nodes
|
|
134
|
+
templates_by_kind: Dict[str, Dict[str, TemplateDecl]] = {"rule": {}, "sync": {}, "schedule": {}}
|
|
135
|
+
|
|
136
|
+
# Collect modules this program imports (treat bare "import X" as glob, same as you already do)
|
|
137
|
+
imported_modules = set()
|
|
138
|
+
for imp in getattr(prog, "imports", []) or []:
|
|
139
|
+
if not isinstance(imp, dict) or imp.get("type") != "import":
|
|
140
|
+
continue
|
|
141
|
+
raw_mod = imp.get("module", "")
|
|
142
|
+
mod = _resolve_module_id(raw_mod)
|
|
143
|
+
|
|
144
|
+
if not mod:
|
|
145
|
+
continue
|
|
146
|
+
# normalize kind the same way you do later
|
|
147
|
+
kind = imp.get("kind")
|
|
148
|
+
if kind not in ("glob", "list", "alias"):
|
|
149
|
+
if imp.get("items"): kind = "list"
|
|
150
|
+
elif imp.get("as"): kind = "alias"
|
|
151
|
+
else: kind = "glob"
|
|
152
|
+
|
|
153
|
+
if kind in ("glob", "list", "alias"):
|
|
154
|
+
imported_modules.add(mod)
|
|
155
|
+
|
|
156
|
+
# Helper: build arg map (params -> values) using defaults
|
|
157
|
+
def _bind_args(t: TemplateDecl, call_args: List[Any]) -> Dict[str, Any]:
|
|
158
|
+
params = list(t.params or [])
|
|
159
|
+
# normalize params: [{"name":..., "default":...}, ...]
|
|
160
|
+
pnames = [p.get("name") for p in params]
|
|
161
|
+
defaults = {p.get("name"): p.get("default") for p in params}
|
|
162
|
+
bound: Dict[str, Any] = dict(defaults)
|
|
163
|
+
# split named vs positional args from transformer
|
|
164
|
+
pos: List[Any] = []
|
|
165
|
+
named: Dict[str, Any] = {}
|
|
166
|
+
for a in call_args or []:
|
|
167
|
+
if isinstance(a, dict) and "name" in a:
|
|
168
|
+
named[str(a["name"])] = a.get("value")
|
|
169
|
+
else:
|
|
170
|
+
pos.append(a)
|
|
171
|
+
# apply positional
|
|
172
|
+
for i, v in enumerate(pos):
|
|
173
|
+
if i < len(pnames):
|
|
174
|
+
bound[pnames[i]] = v
|
|
175
|
+
# apply named (wins over positional/default)
|
|
176
|
+
for k, v in named.items():
|
|
177
|
+
if k in pnames:
|
|
178
|
+
bound[k] = v
|
|
179
|
+
return bound
|
|
180
|
+
|
|
181
|
+
# Deep substitute param identifiers appearing as bare strings in nested dict/list trees
|
|
182
|
+
def _deep_subst(obj: Any, subst: Dict[str, Any]) -> Any:
|
|
183
|
+
# strings: replace only if exactly matches a parameter name
|
|
184
|
+
if isinstance(obj, str):
|
|
185
|
+
return str(subst.get(obj, obj))
|
|
186
|
+
# dicts/lists: walk recursively
|
|
187
|
+
if isinstance(obj, dict):
|
|
188
|
+
return {k: _deep_subst(v, subst) for k, v in obj.items()}
|
|
189
|
+
if isinstance(obj, list):
|
|
190
|
+
return [_deep_subst(x, subst) for x in obj]
|
|
191
|
+
# leave everything else as-is (numbers, bools, None)
|
|
192
|
+
return obj
|
|
193
|
+
|
|
194
|
+
# Expand a single UseTemplate into a concrete node (Rule/Sync/Schedule)
|
|
195
|
+
def _instantiate(use: UseTemplate) -> Optional[Any]:
|
|
196
|
+
# Find matching template by name across kinds (prefer rule->sync->schedule)
|
|
197
|
+
t = None
|
|
198
|
+
for kind in ("rule", "sync", "schedule"):
|
|
199
|
+
t = templates_by_kind.get(kind, {}).get(use.name)
|
|
200
|
+
if t:
|
|
201
|
+
break
|
|
202
|
+
if not t:
|
|
203
|
+
return None
|
|
204
|
+
argmap = _bind_args(t, list(getattr(use, "args", []) or []))
|
|
205
|
+
|
|
206
|
+
if t.body is None:
|
|
207
|
+
# Provide minimal empty bodies so deep_subst & constructors don’t break
|
|
208
|
+
if t.kind == "rule":
|
|
209
|
+
t.body = Rule(name="", clauses=[])
|
|
210
|
+
elif t.kind == "sync":
|
|
211
|
+
t.body = Sync(kind="onoff", members=[], name="", invert=[])
|
|
212
|
+
elif t.kind == "schedule":
|
|
213
|
+
t.body = Schedule(name="", clauses=[], windows=[], private=False)
|
|
214
|
+
original = copy.deepcopy(t.body)
|
|
215
|
+
# Plainify dataclasses/objects so deep_subst can walk them
|
|
216
|
+
if is_dataclass(original):
|
|
217
|
+
plain = asdict(original)
|
|
218
|
+
elif hasattr(original, "__dict__"):
|
|
219
|
+
# shallow mapping of fields; they will typically be lists/dicts we can walk
|
|
220
|
+
plain = copy.deepcopy(vars(original))
|
|
221
|
+
else:
|
|
222
|
+
plain = original
|
|
223
|
+
subbed = _deep_subst(plain, argmap)
|
|
224
|
+
|
|
225
|
+
# Rename resulting node if caller provided "as <name>"
|
|
226
|
+
# Rename resulting node if caller provided "as <name>" or passed name= param
|
|
227
|
+
new_name = getattr(use, "as_name", None) or str(argmap.get("name") or t.name)
|
|
228
|
+
|
|
229
|
+
# Construct concrete AST node of same kind
|
|
230
|
+
if isinstance(t.body, Rule) or (getattr(t.body, "__class__", None).__name__ == "Rule"):
|
|
231
|
+
# subbed is a dict after plainify/subst
|
|
232
|
+
return Rule(name=new_name, clauses=subbed.get("clauses", getattr(original, "clauses", [])))
|
|
233
|
+
if isinstance(t.body, Sync) or (getattr(t.body, "__class__", None).__name__ == "Sync"):
|
|
234
|
+
return Sync(kind=subbed.get("kind", getattr(original, "kind", "onoff")),
|
|
235
|
+
members=subbed.get("members", getattr(original, "members", [])),
|
|
236
|
+
name=new_name,
|
|
237
|
+
invert=subbed.get("invert", getattr(original, "invert", [])))
|
|
238
|
+
if isinstance(t.body, Schedule) or (getattr(t.body, "__class__", None).__name__ == "Schedule"):
|
|
239
|
+
return Schedule(name=new_name,
|
|
240
|
+
clauses=subbed.get("clauses", getattr(original, "clauses", [])),
|
|
241
|
+
windows=subbed.get("windows", getattr(original, "windows", [])),
|
|
242
|
+
private=subbed.get("private", getattr(original, "private", False)))
|
|
243
|
+
return None
|
|
244
|
+
|
|
245
|
+
# Scan once to collect templates
|
|
246
|
+
for s in getattr(prog, "statements", []) or []:
|
|
247
|
+
if isinstance(s, TemplateDecl):
|
|
248
|
+
kind = (s.kind or "rule").lower()
|
|
249
|
+
if kind in templates_by_kind:
|
|
250
|
+
templates_by_kind[kind][s.name] = s
|
|
251
|
+
|
|
252
|
+
if 'GLOBAL_EXPORTS' in globals():
|
|
253
|
+
for (pkg, kind, name), node in globals()['GLOBAL_EXPORTS'].items():
|
|
254
|
+
if kind != "template":
|
|
255
|
+
continue
|
|
256
|
+
# bring templates from any imported module
|
|
257
|
+
if pkg in imported_modules and isinstance(node, TemplateDecl):
|
|
258
|
+
tkind = (node.kind or "rule").lower()
|
|
259
|
+
if tkind in templates_by_kind and name not in templates_by_kind[tkind]:
|
|
260
|
+
templates_by_kind[tkind][name] = node
|
|
261
|
+
|
|
262
|
+
# Build a new statement list with uses expanded
|
|
263
|
+
expanded_statements: List[Any] = []
|
|
264
|
+
for s in getattr(prog, "statements", []) or []:
|
|
265
|
+
if isinstance(s, UseTemplate):
|
|
266
|
+
inst = _instantiate(s)
|
|
267
|
+
if inst is not None:
|
|
268
|
+
expanded_statements.append(inst)
|
|
269
|
+
# do not append the UseTemplate node itself
|
|
270
|
+
else:
|
|
271
|
+
expanded_statements.append(s)
|
|
272
|
+
|
|
273
|
+
# 1) Collect local declarations (aliases & schedules) from expanded statements
|
|
274
|
+
for s in expanded_statements:
|
|
119
275
|
if isinstance(s, Alias):
|
|
120
276
|
local_aliases[s.name] = s.entity
|
|
121
277
|
is_private = getattr(s, "private", False)
|
|
@@ -173,7 +329,9 @@ def analyze(prog: Program) -> IRProgram:
|
|
|
173
329
|
if not isinstance(imp, dict) or imp.get("type") != "import":
|
|
174
330
|
# transformer may also append sentinels to statements; ignore here
|
|
175
331
|
continue
|
|
176
|
-
|
|
332
|
+
raw_mod = imp.get("module", "")
|
|
333
|
+
mod = _resolve_module_id(raw_mod)
|
|
334
|
+
|
|
177
335
|
kind = imp.get("kind")
|
|
178
336
|
# Be generous: if transformer emitted "none" or omitted kind, infer it.
|
|
179
337
|
if kind not in ("glob", "list", "alias"):
|
|
@@ -224,7 +382,7 @@ def analyze(prog: Program) -> IRProgram:
|
|
|
224
382
|
|
|
225
383
|
# --- Syncs ---
|
|
226
384
|
syncs: List[IRSync] = []
|
|
227
|
-
for s in
|
|
385
|
+
for s in expanded_statements:
|
|
228
386
|
if isinstance(s, Sync):
|
|
229
387
|
mem = [_resolve_alias(m,amap) for m in s.members]
|
|
230
388
|
inv = [_resolve_alias(m,amap) for m in s.invert]
|
|
@@ -239,6 +397,33 @@ def analyze(prog: Program) -> IRProgram:
|
|
|
239
397
|
|
|
240
398
|
# NEW: collect structured windows (serialize to plain dicts)
|
|
241
399
|
sched_windows: Dict[str, List[dict]] = {}
|
|
400
|
+
|
|
401
|
+
# --- helpers: normalization for day selector & holiday mode ---
|
|
402
|
+
def _norm_day_selector(ds: Optional[str]) -> str:
|
|
403
|
+
s = (ds or "").strip().lower()
|
|
404
|
+
if s in ("weekdays", "weekday", "wd", "mon-fri", "monfri"):
|
|
405
|
+
return "weekdays"
|
|
406
|
+
if s in ("weekends", "weekend", "we", "sat-sun", "satsun"):
|
|
407
|
+
return "weekends"
|
|
408
|
+
return "daily"
|
|
409
|
+
|
|
410
|
+
def _norm_holiday_mode(mode: Optional[str]) -> Optional[str]:
|
|
411
|
+
"""
|
|
412
|
+
Normalize holiday text to {'only','except',None}.
|
|
413
|
+
Accepts variants like:
|
|
414
|
+
'holiday', 'only holiday', 'holiday only' -> 'only'
|
|
415
|
+
'except holiday', 'exclude holiday', 'unless holiday', 'not holiday' -> 'except'
|
|
416
|
+
"""
|
|
417
|
+
if mode is None:
|
|
418
|
+
return None
|
|
419
|
+
m = str(mode).strip().lower().replace("_", " ").replace("-", " ")
|
|
420
|
+
# look for negation/exclusion first
|
|
421
|
+
if any(tok in m for tok in ("except", "exclude", "unless", "not")):
|
|
422
|
+
return "except"
|
|
423
|
+
if "holiday" in m or "only" in m:
|
|
424
|
+
return "only"
|
|
425
|
+
return None
|
|
426
|
+
|
|
242
427
|
for nm, wins in local_schedule_windows.items():
|
|
243
428
|
out: List[dict] = []
|
|
244
429
|
for w in wins:
|
|
@@ -249,14 +434,23 @@ def analyze(prog: Program) -> IRProgram:
|
|
|
249
434
|
if getattr(w, "period", None):
|
|
250
435
|
p = w.period # PeriodSelector
|
|
251
436
|
period = {"kind": p.kind, "data": dict(p.data)}
|
|
437
|
+
|
|
438
|
+
# --- normalize selectors & holiday mode ---
|
|
439
|
+
day_sel = _norm_day_selector(getattr(w, "day_selector", None))
|
|
440
|
+
href = getattr(w, "holiday_ref", None)
|
|
441
|
+
hmode = _norm_holiday_mode(getattr(w, "holiday_mode", None))
|
|
442
|
+
# Heuristic default: if a weekdays/weekends selector references a holiday
|
|
443
|
+
# set and no mode provided, treat as "except" (workday semantics).
|
|
444
|
+
if href and hmode is None and day_sel in ("weekdays", "weekends"):
|
|
445
|
+
hmode = "except"
|
|
252
446
|
out.append({
|
|
253
447
|
"start": w.start,
|
|
254
448
|
"end": w.end,
|
|
255
|
-
"day_selector":
|
|
449
|
+
"day_selector": day_sel,
|
|
256
450
|
"period": period,
|
|
257
|
-
"holiday_ref":
|
|
258
|
-
"holiday_mode":
|
|
259
|
-
|
|
451
|
+
"holiday_ref": href,
|
|
452
|
+
"holiday_mode": hmode,
|
|
453
|
+
})
|
|
260
454
|
if out:
|
|
261
455
|
sched_windows[nm] = out
|
|
262
456
|
|
|
@@ -338,8 +532,22 @@ def analyze(prog: Program) -> IRProgram:
|
|
|
338
532
|
if ent:
|
|
339
533
|
return ent
|
|
340
534
|
return obj
|
|
535
|
+
|
|
536
|
+
# Resolve only qualified aliases (ns.alias) and leave unqualified aliases intact.
|
|
537
|
+
# This keeps existing IR expectations while making qualified references usable.
|
|
538
|
+
def _walk_qualified_only(obj: Any) -> Any:
|
|
539
|
+
if isinstance(obj, dict):
|
|
540
|
+
return {k: _walk_qualified_only(v) for k, v in obj.items()}
|
|
541
|
+
if isinstance(obj, list):
|
|
542
|
+
return [_walk_qualified_only(x) for x in obj]
|
|
543
|
+
if isinstance(obj, str):
|
|
544
|
+
if "." in obj:
|
|
545
|
+
ent = _resolve_qualified_alias(obj)
|
|
546
|
+
if ent:
|
|
547
|
+
return ent
|
|
548
|
+
return obj
|
|
341
549
|
|
|
342
|
-
for s in
|
|
550
|
+
for s in expanded_statements:
|
|
343
551
|
if isinstance(s, Rule):
|
|
344
552
|
clauses: List[dict] = []
|
|
345
553
|
schedule_uses: List[str] = []
|
|
@@ -351,8 +559,9 @@ def analyze(prog: Program) -> IRProgram:
|
|
|
351
559
|
# IfClause-like items have .condition/.actions
|
|
352
560
|
if hasattr(c, "condition") and hasattr(c, "actions"):
|
|
353
561
|
# Keep alias identifiers intact for tests & codegen (resolve later)
|
|
354
|
-
|
|
355
|
-
|
|
562
|
+
# But resolve qualified aliases (ns.alias) so codegen gets real entities
|
|
563
|
+
cond = _walk_qualified_only(c.condition)
|
|
564
|
+
acts = _walk_qualified_only(c.actions)
|
|
356
565
|
clauses.append({"condition": cond, "actions": acts})
|
|
357
566
|
elif isinstance(c, dict) and c.get("type") == "schedule_use":
|
|
358
567
|
# {"type":"schedule_use","names":[...]}
|
|
@@ -371,6 +580,10 @@ def analyze(prog: Program) -> IRProgram:
|
|
|
371
580
|
for sc in c.get("clauses") or []:
|
|
372
581
|
if isinstance(sc, dict):
|
|
373
582
|
schedules_inline.append(sc)
|
|
583
|
+
elif isinstance(c, dict) and "condition" in c and "actions" in c:
|
|
584
|
+
cond = _walk_alias_with_qualified(c["condition"])
|
|
585
|
+
acts = _walk_alias_with_qualified(c["actions"])
|
|
586
|
+
clauses.append({"condition": cond, "actions": acts})
|
|
374
587
|
else:
|
|
375
588
|
# ignore unknown fragments
|
|
376
589
|
pass
|
|
@@ -386,6 +599,16 @@ def analyze(prog: Program) -> IRProgram:
|
|
|
386
599
|
# -------- NEW: validate schedule windows --------
|
|
387
600
|
allowed_days = {"weekdays", "weekends", "daily"}
|
|
388
601
|
for sched_name, wins in sched_windows.items():
|
|
602
|
+
# Normalize holiday modes defensively:
|
|
603
|
+
# If a window references a holiday set and also specifies a day bucket,
|
|
604
|
+
# it should be "except" (non-holiday behavior). We keep pure holiday-only
|
|
605
|
+
# windows (`day_selector == "daily"`) as "only".
|
|
606
|
+
for w in wins:
|
|
607
|
+
ds = (w.get("day_selector") or "daily").lower()
|
|
608
|
+
href = w.get("holiday_ref")
|
|
609
|
+
hmode = w.get("holiday_mode")
|
|
610
|
+
if href and ds in ("weekdays", "weekends") and (hmode is None or hmode == "only"):
|
|
611
|
+
w["holiday_mode"] = "except"
|
|
389
612
|
for w in wins:
|
|
390
613
|
ds = w.get("day_selector")
|
|
391
614
|
if ds not in allowed_days:
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: hassl
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.4.0
|
|
4
4
|
Summary: HASSL: Home Assistant Simple Scripting Language
|
|
5
5
|
Home-page: https://github.com/adanowitz/hassl
|
|
6
6
|
Author: adanowitz
|
|
@@ -17,7 +17,7 @@ Dynamic: license-file
|
|
|
17
17
|
|
|
18
18
|
> **Home Assistant Simple Scripting Language**
|
|
19
19
|
|
|
20
|
-

|
|
21
21
|
|
|
22
22
|
HASSL is a human-friendly domain-specific language (DSL) for building **loop-safe**, **deterministic**, and **composable** automations for [Home Assistant](https://www.home-assistant.io/).
|
|
23
23
|
|
|
@@ -30,7 +30,7 @@ It compiles lightweight `.hassl` scripts into fully functional YAML packages tha
|
|
|
30
30
|
- **Readable DSL** → write logic like natural language (`if motion && lux < 50 then light = on`)
|
|
31
31
|
- **Sync devices** → keep switches, dimmers, and fans perfectly in sync
|
|
32
32
|
- **Schedules** → declare time-based gates (`enable from 08:00 until 19:00`)
|
|
33
|
-
- **Weekday/weekend/holiday schedules** → full support for Home Assistant’s **Workday integration** (v0.
|
|
33
|
+
- **Weekday/weekend/holiday schedules** → full support for Home Assistant’s **Workday integration** (v0.4.0)
|
|
34
34
|
- **Loop-safe** → context ID tracking prevents feedback loops
|
|
35
35
|
- **Per-rule enable gates** → `disable rule` or `enable rule` dynamically
|
|
36
36
|
- **Inline waits** → `wait (!motion for 10m)` works like native HA triggers
|
|
@@ -148,7 +148,7 @@ Each `.hassl` file compiles into an isolated package — no naming collisions, n
|
|
|
148
148
|
| `scripts_<pkg>.yaml` | Writer scripts with context stamping |
|
|
149
149
|
| `sync_<pkg>_*.yaml` | Sync automations for each property |
|
|
150
150
|
| `rules_bundled_<pkg>.yaml` | Rule logic automations + schedules |
|
|
151
|
-
| `schedules_<pkg>.yaml` | Time/sun-based schedule sensors (v0.
|
|
151
|
+
| `schedules_<pkg>.yaml` | Time/sun-based schedule sensors (v0.4.0) |
|
|
152
152
|
|
|
153
153
|
---
|
|
154
154
|
|
|
@@ -181,7 +181,7 @@ All schedules are restart-safe:
|
|
|
181
181
|
|
|
182
182
|
---
|
|
183
183
|
|
|
184
|
-
## 🗓️ Holiday & Workday Integration (v0.
|
|
184
|
+
## 🗓️ Holiday & Workday Integration (v0.4.0)
|
|
185
185
|
|
|
186
186
|
HASSL now supports `holidays <id>:` schedules tied to Home Assistant’s **Workday** integration.
|
|
187
187
|
|
|
@@ -238,7 +238,7 @@ Once created, HASSL automatically references them in generated automations.
|
|
|
238
238
|
|
|
239
239
|
## ⚗️ Experimental: Date & Month Range Schedules
|
|
240
240
|
|
|
241
|
-
HASSL v0.
|
|
241
|
+
HASSL v0.4.0 includes early support for:
|
|
242
242
|
|
|
243
243
|
```hassl
|
|
244
244
|
on months Jun–Aug 07:00–22:00;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
hassl/__init__.py,sha256=42STGor_9nKYXumfeV5tiyD_M8VdcddX7CEexmibPBk,22
|
|
2
|
+
hassl/cli.py,sha256=8y15glAYjHsJChaHefwqvoC5sz-l-pt9E9mWtzrt-Ik,9890
|
|
3
|
+
hassl/ast/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
4
|
+
hassl/ast/nodes.py,sha256=Z6Ac5RrDEv6KNAAxdKQ3wrnnoHqbgP6-SDvda9Z8CNk,3254
|
|
5
|
+
hassl/codegen/__init__.py,sha256=NgEw86oHlsk7cQrHz8Ttrtp68Cm7WKceTWRr02SDfjo,854
|
|
6
|
+
hassl/codegen/generate.py,sha256=JmVBz8xhuPHosch0lhh8xU6tmdCsAl-qVWoY7hQxjow,206
|
|
7
|
+
hassl/codegen/init.py,sha256=kMfi_Us_7c_T35OK_jo8eqb79lxNV-dToO9-iAb5fHI,55
|
|
8
|
+
hassl/codegen/package.py,sha256=wruN26-wo64RBb94SxyEhG_4NIc6zBFcvOhGpuO_CIA,43289
|
|
9
|
+
hassl/codegen/rules_min.py,sha256=3hiL0dUvlU9uIf1kc_BYcyTsaWGUBS787WkaIh1vZwk,28909
|
|
10
|
+
hassl/codegen/yaml_emit.py,sha256=VTNnR_uvSSqsL7kX5NyXuPUZh5FK36a_sUFsRyrQOS8,2207
|
|
11
|
+
hassl/parser/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
12
|
+
hassl/parser/hassl.lark,sha256=aBlZ609bZ-Wnrs5EsFukDqAIo5xLNQ6p2ruS5qmxtI8,6978
|
|
13
|
+
hassl/parser/loader.py,sha256=xwhdoMkmXskanY8IhqIOfkcOkkn33goDnsbjzS66FL8,249
|
|
14
|
+
hassl/parser/transform.py,sha256=xVz1fKjfxHbiTeUFuZJbVwctGWwd4cdvAPhfKgslwbs,25328
|
|
15
|
+
hassl/semantics/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
16
|
+
hassl/semantics/analyzer.py,sha256=layYd1x_UajNPZN9Up_fp2dfApnBzCyeoG351nutpjU,28641
|
|
17
|
+
hassl/semantics/domains.py,sha256=-OuhA4RkOLVaLnBxhKfJFjiQMYBSTryX9KjHit_8ezI,308
|
|
18
|
+
hassl-0.4.0.dist-info/licenses/LICENSE,sha256=hPNSrn93Btl9sU4BkQlrIK1FpuFPvaemdW6-Scz-Xeo,1072
|
|
19
|
+
hassl-0.4.0.dist-info/METADATA,sha256=Noo52fAx3gT0U5s0hrNMeia8BLnLgXnXsuSkhKpyZZE,9006
|
|
20
|
+
hassl-0.4.0.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
|
|
21
|
+
hassl-0.4.0.dist-info/entry_points.txt,sha256=qA8jYPZlESNL6V4fJCTPOBXFeEBtjLGGsftXsSo82so,42
|
|
22
|
+
hassl-0.4.0.dist-info/top_level.txt,sha256=BPuiULeikxMI3jDVLmv3_n7yjSD9Qrq58bp9af_HixI,6
|
|
23
|
+
hassl-0.4.0.dist-info/RECORD,,
|
hassl-0.3.1.dist-info/RECORD
DELETED
|
@@ -1,23 +0,0 @@
|
|
|
1
|
-
hassl/__init__.py,sha256=r4xAFihOf72W9TD-lpMi6ntWSTKTP2SlzKP1ytkjRbI,22
|
|
2
|
-
hassl/cli.py,sha256=TSUvkoAg7ck1vlDUhC2sv_1NSetksdVGuOv-P9wrKxA,15762
|
|
3
|
-
hassl/ast/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
4
|
-
hassl/ast/nodes.py,sha256=U_UTmWS87laFuFX39mmzkk43JnGx7NEyNbkSomfTzSM,2704
|
|
5
|
-
hassl/codegen/__init__.py,sha256=NgEw86oHlsk7cQrHz8Ttrtp68Cm7WKceTWRr02SDfjo,854
|
|
6
|
-
hassl/codegen/generate.py,sha256=JmVBz8xhuPHosch0lhh8xU6tmdCsAl-qVWoY7hQxjow,206
|
|
7
|
-
hassl/codegen/init.py,sha256=kMfi_Us_7c_T35OK_jo8eqb79lxNV-dToO9-iAb5fHI,55
|
|
8
|
-
hassl/codegen/package.py,sha256=sGG40gonOoK3QJphO62Bju6WqFqfTT3XNx37QPHlb2w,42457
|
|
9
|
-
hassl/codegen/rules_min.py,sha256=LbLrpJ_2TV_FJlHB17TcuBaqfbLl97VUKzLck3s9KXo,29557
|
|
10
|
-
hassl/codegen/yaml_emit.py,sha256=VTNnR_uvSSqsL7kX5NyXuPUZh5FK36a_sUFsRyrQOS8,2207
|
|
11
|
-
hassl/parser/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
12
|
-
hassl/parser/hassl.lark,sha256=-At0mQQiUXxQU2aB1pxrk1WfQdQiUJeH7Ev8ada7kLg,5637
|
|
13
|
-
hassl/parser/loader.py,sha256=xwhdoMkmXskanY8IhqIOfkcOkkn33goDnsbjzS66FL8,249
|
|
14
|
-
hassl/parser/transform.py,sha256=UXiUQBT3oMkFVJwFWA8X3L_Ds7PzZle7ZMBVVNSmp7Y,26332
|
|
15
|
-
hassl/semantics/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
16
|
-
hassl/semantics/analyzer.py,sha256=M8pQTFRYSpAyBo-ctIRuT1DBb_WZsGs7hjT4-pHPLjA,18066
|
|
17
|
-
hassl/semantics/domains.py,sha256=-OuhA4RkOLVaLnBxhKfJFjiQMYBSTryX9KjHit_8ezI,308
|
|
18
|
-
hassl-0.3.1.dist-info/licenses/LICENSE,sha256=hPNSrn93Btl9sU4BkQlrIK1FpuFPvaemdW6-Scz-Xeo,1072
|
|
19
|
-
hassl-0.3.1.dist-info/METADATA,sha256=TYCCs2uU-7mRXyvq2_pkMLbVMQY_VTqW5Oi5i0msaJg,9006
|
|
20
|
-
hassl-0.3.1.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
21
|
-
hassl-0.3.1.dist-info/entry_points.txt,sha256=qA8jYPZlESNL6V4fJCTPOBXFeEBtjLGGsftXsSo82so,42
|
|
22
|
-
hassl-0.3.1.dist-info/top_level.txt,sha256=BPuiULeikxMI3jDVLmv3_n7yjSD9Qrq58bp9af_HixI,6
|
|
23
|
-
hassl-0.3.1.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|