Compare commits
2
Commits
ed49dbf429
..
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
96310b1f9e | ||
|
|
f44d74e93b |
No files matched your search
@@ -264,6 +264,8 @@ class Engine:
|
||||
self.last_seen_at = None
|
||||
self.stale_notified = False
|
||||
self.telemetry_writes = 0
|
||||
self.ac_watchdog_since = None
|
||||
self.ac_watchdog_tripped = False
|
||||
|
||||
def evaluate(self, variables, now):
|
||||
for state in self.states:
|
||||
@@ -474,6 +476,9 @@ class Engine:
|
||||
floor = self.profile.battery_floor
|
||||
if floor is not None and floor.enabled:
|
||||
names.add(floor.field)
|
||||
watchdog = self.profile.ac_input_watchdog
|
||||
if watchdog is not None and watchdog.enabled:
|
||||
names.add(watchdog.field)
|
||||
return names
|
||||
|
||||
def summarize(self, variables, now):
|
||||
@@ -674,6 +679,90 @@ class Engine:
|
||||
force=True,
|
||||
)
|
||||
|
||||
async def check_ac_watchdog(self, variables, now):
|
||||
watchdog = self.profile.ac_input_watchdog
|
||||
if watchdog is None or not watchdog.enabled:
|
||||
return
|
||||
|
||||
if not self.last_commanded:
|
||||
if self.ac_watchdog_tripped:
|
||||
self.ac_watchdog_tripped = False
|
||||
self.report("AC input watchdog cleared, target is OFF", force=True)
|
||||
await self.notify_ac_watchdog(variables, recovered=True)
|
||||
self.ac_watchdog_since = None
|
||||
return
|
||||
|
||||
value = variables.get(watchdog.field)
|
||||
if not isinstance(value, (int, float)) or isinstance(value, bool):
|
||||
return
|
||||
|
||||
if value >= watchdog.min_watts:
|
||||
if self.ac_watchdog_tripped:
|
||||
self.ac_watchdog_tripped = False
|
||||
self.report(
|
||||
f"AC input watchdog cleared, {watchdog.field}={value:g}W",
|
||||
force=True,
|
||||
)
|
||||
await self.notify_ac_watchdog(variables, recovered=True)
|
||||
self.ac_watchdog_since = None
|
||||
return
|
||||
|
||||
if self.ac_watchdog_since is None:
|
||||
self.ac_watchdog_since = now
|
||||
|
||||
held = now - self.ac_watchdog_since
|
||||
if held < (watchdog.dwell or 0):
|
||||
return
|
||||
|
||||
if self.ac_watchdog_tripped:
|
||||
return
|
||||
|
||||
self.ac_watchdog_tripped = True
|
||||
self.report(
|
||||
f"AC INPUT WATCHDOG: {watchdog.field}={value:g}W has stayed under "
|
||||
f"{watchdog.min_watts:g}W while commanded ON for "
|
||||
f"{format_duration(held)}",
|
||||
force=True,
|
||||
)
|
||||
await self.notify_ac_watchdog(variables, duration_seconds=held)
|
||||
|
||||
async def notify_ac_watchdog(self, variables, duration_seconds=None, recovered=False):
|
||||
watchdog = self.profile.ac_input_watchdog
|
||||
if watchdog is None:
|
||||
return
|
||||
|
||||
if recovered and not watchdog.notify_recovered:
|
||||
return
|
||||
|
||||
if not self.notifier.available():
|
||||
if not recovered:
|
||||
self.report(
|
||||
"AC input watchdog tripped but no notification channel is "
|
||||
"enabled",
|
||||
force=True,
|
||||
)
|
||||
return
|
||||
|
||||
settings = self.profile.notifications
|
||||
context = self.context(self.last_commanded, None, variables, "ac_watchdog")
|
||||
context["field"] = watchdog.field
|
||||
context["min_watts"] = f"{watchdog.min_watts:g}"
|
||||
context["duration"] = (
|
||||
format_duration(duration_seconds) if duration_seconds else ""
|
||||
)
|
||||
|
||||
template = watchdog.recovered_template if recovered else watchdog.notify_template
|
||||
body = render(template, context)
|
||||
title = render(settings.title or "{profile}", context)
|
||||
|
||||
await self.notifier.send(
|
||||
title,
|
||||
body,
|
||||
priority=None if recovered else "urgent",
|
||||
key="ac_watchdog_recovered" if recovered else "ac_watchdog",
|
||||
force=True,
|
||||
)
|
||||
|
||||
def record_event(self, desired, reason, rule, variables, cause="rule"):
|
||||
source_identity = self.anker_profile.get("identity", {})
|
||||
target_identity = self.target.profile.get("identity", {})
|
||||
@@ -1028,6 +1117,8 @@ class Engine:
|
||||
self.report("telemetry complete, resuming evaluation", force=True)
|
||||
self.incomplete_reported = False
|
||||
|
||||
await self.check_ac_watchdog(variables, now)
|
||||
|
||||
if await self.check_floor(session, variables, now):
|
||||
self.heartbeat(variables, now)
|
||||
return
|
||||
@@ -1063,6 +1154,12 @@ class Engine:
|
||||
else:
|
||||
self.report("battery floor: NONE SET", force=True)
|
||||
|
||||
if self.profile.ac_input_watchdog and self.profile.ac_input_watchdog.enabled:
|
||||
self.report(
|
||||
f"AC input watchdog: {self.profile.ac_input_watchdog.describe()}",
|
||||
force=True,
|
||||
)
|
||||
|
||||
if self.profile.monitor_fields:
|
||||
self.report(
|
||||
"also logging (not used by any rule): "
|
||||
|
||||
+52
-1
@@ -181,6 +181,42 @@ class BatteryFloor:
|
||||
)
|
||||
|
||||
|
||||
class AcInputWatchdog:
|
||||
def __init__(self, raw):
|
||||
if not isinstance(raw, dict):
|
||||
raise ProfileError("safety.ac_input_watchdog must be a mapping")
|
||||
|
||||
self.enabled = bool(raw.get("enabled", False))
|
||||
self.field = str(raw.get("field") or "ac_input_power")
|
||||
|
||||
try:
|
||||
self.min_watts = float(raw.get("min_watts", 50))
|
||||
except (TypeError, ValueError):
|
||||
raise ProfileError(
|
||||
"safety.ac_input_watchdog.min_watts must be a number"
|
||||
) from None
|
||||
|
||||
self.dwell = parse_duration(
|
||||
raw.get("for", 300), "safety.ac_input_watchdog.for"
|
||||
)
|
||||
self.notify_recovered = bool(raw.get("notify_recovered", True))
|
||||
self.notify_template = raw.get("notify_template") or (
|
||||
"{clock} {source_name}: {target_name} has been commanded ON but "
|
||||
"{field} has stayed under {min_watts}W for {duration}. Check that "
|
||||
"grid power is actually reaching the plug."
|
||||
)
|
||||
self.recovered_template = raw.get("recovered_template") or (
|
||||
"{clock} {source_name}: {field} is back above {min_watts}W. "
|
||||
"{target_name} is charging normally again."
|
||||
)
|
||||
|
||||
def describe(self):
|
||||
return (
|
||||
f"{self.field} < {self.min_watts:g}W while ON for "
|
||||
f"{format_duration(self.dwell)}"
|
||||
)
|
||||
|
||||
|
||||
class NotificationSettings:
|
||||
def __init__(self, raw):
|
||||
if not isinstance(raw, dict):
|
||||
@@ -381,6 +417,9 @@ class PowerProfile:
|
||||
floor = safety.get("battery_floor")
|
||||
self.battery_floor = BatteryFloor(floor) if floor else None
|
||||
|
||||
watchdog = safety.get("ac_input_watchdog")
|
||||
self.ac_input_watchdog = AcInputWatchdog(watchdog) if watchdog else None
|
||||
|
||||
limits = raw.get("limits") or {}
|
||||
self.min_gap = parse_duration(
|
||||
limits.get("min_seconds_between_actions", DEFAULT_MIN_GAP),
|
||||
@@ -506,6 +545,14 @@ def validate(profile):
|
||||
"field on this device"
|
||||
)
|
||||
|
||||
if profile.ac_input_watchdog is not None:
|
||||
if profile.ac_input_watchdog.field not in available:
|
||||
problems.append(
|
||||
f"safety.ac_input_watchdog.field "
|
||||
f"{profile.ac_input_watchdog.field!r} is not a field on this "
|
||||
"device"
|
||||
)
|
||||
|
||||
on_rules = [r for r in profile.active_rules() if r.desired_state() is True]
|
||||
off_rules = [r for r in profile.active_rules() if r.desired_state() is False]
|
||||
if not on_rules:
|
||||
@@ -551,6 +598,9 @@ def validate(profile):
|
||||
profile.notifications.switch_failed_template,
|
||||
profile.notifications.switch_unconfirmed_template,
|
||||
]
|
||||
if profile.ac_input_watchdog is not None:
|
||||
templates_to_check.append(profile.ac_input_watchdog.notify_template)
|
||||
templates_to_check.append(profile.ac_input_watchdog.recovered_template)
|
||||
for rule in profile.active_rules():
|
||||
if rule.notify_template:
|
||||
templates_to_check.append(rule.notify_template)
|
||||
@@ -559,7 +609,8 @@ def validate(profile):
|
||||
"profile", "rule", "action", "action_word", "source_name", "source_model",
|
||||
"source_serial", "target_name", "target_model", "target_host",
|
||||
"target_channel", "condition", "time", "clock", "reason", "event",
|
||||
"last_seen", "outage", "attempted", "actual",
|
||||
"last_seen", "outage", "attempted", "actual", "field", "min_watts",
|
||||
"duration",
|
||||
}
|
||||
for template in templates_to_check:
|
||||
for field in notify_module.template_fields(template):
|
||||
|
||||
Reference in new issue
Block a user