Debounce MQTT disconnects, undo precautionary safe_state, make stale notifications informative
This commit is contained in:
1 parent
9cc47d9a8f
commit
aeabae212a
2 files changed
+74
-6
No files matched your search
+62
-6
@@ -121,6 +121,9 @@ class Engine:
|
|||||||
self.disconnected_since = None
|
self.disconnected_since = None
|
||||||
self.stale_action_state = None
|
self.stale_action_state = None
|
||||||
self.pre_stale_state = None
|
self.pre_stale_state = None
|
||||||
|
self.last_variables = {}
|
||||||
|
self.last_seen_at = None
|
||||||
|
self.stale_notified = False
|
||||||
|
|
||||||
def evaluate(self, variables, now):
|
def evaluate(self, variables, now):
|
||||||
for state in self.states:
|
for state in self.states:
|
||||||
@@ -213,13 +216,32 @@ class Engine:
|
|||||||
)
|
)
|
||||||
return context
|
return context
|
||||||
|
|
||||||
async def notify(self, desired, rule, variables, event="action"):
|
async def notify(self, desired, rule, variables, event="action", extra=None):
|
||||||
settings = self.profile.notifications
|
settings = self.profile.notifications
|
||||||
if not settings.wants(event):
|
subscription = "stale" if event == "recovered" else event
|
||||||
|
if not settings.wants(subscription):
|
||||||
return
|
return
|
||||||
if event == "action" and not settings.rule_enabled(rule):
|
if event == "action" and not settings.rule_enabled(rule):
|
||||||
return
|
return
|
||||||
|
|
||||||
|
if event in ("stale", "recovered"):
|
||||||
|
merged = dict(self.last_variables)
|
||||||
|
merged.update(variables or {})
|
||||||
|
context = self.context(desired, rule, merged, event)
|
||||||
|
context["last_seen"] = self.last_seen_at or "unknown"
|
||||||
|
context.update(extra or {})
|
||||||
|
template = (
|
||||||
|
settings.stale_template
|
||||||
|
if event == "stale"
|
||||||
|
else settings.recovered_template
|
||||||
|
)
|
||||||
|
body = render(template, context)
|
||||||
|
title = render(settings.title, context)
|
||||||
|
await self.notifier.send(
|
||||||
|
title, body, priority=settings.priority, key=event, force=True
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
context = self.context(desired, rule, variables, event)
|
context = self.context(desired, rule, variables, event)
|
||||||
body = render(settings.template_for(rule), context)
|
body = render(settings.template_for(rule), context)
|
||||||
title = render(settings.title, context)
|
title = render(settings.title, context)
|
||||||
@@ -473,9 +495,6 @@ class Engine:
|
|||||||
force=True,
|
force=True,
|
||||||
)
|
)
|
||||||
self.stale_reported = True
|
self.stale_reported = True
|
||||||
await self.notify(
|
|
||||||
None, None, {"reason": "mqtt disconnected"}, event="stale"
|
|
||||||
)
|
|
||||||
|
|
||||||
if self.profile.on_stale == "stop":
|
if self.profile.on_stale == "stop":
|
||||||
raise RuntimeError("stopping: MQTT session disconnected")
|
raise RuntimeError("stopping: MQTT session disconnected")
|
||||||
@@ -486,6 +505,21 @@ class Engine:
|
|||||||
await self.apply(
|
await self.apply(
|
||||||
session, self.profile.safe_state, "mqtt disconnected safe state", now
|
session, self.profile.safe_state, "mqtt disconnected safe state", now
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if not self.stale_notified:
|
||||||
|
self.stale_notified = True
|
||||||
|
await self.notify(
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
{},
|
||||||
|
event="stale",
|
||||||
|
extra={
|
||||||
|
"reason": "connection to Anker lost",
|
||||||
|
"action": self._word(self.profile.safe_state)
|
||||||
|
if self.profile.on_stale == "safe_state"
|
||||||
|
else "left as it was",
|
||||||
|
},
|
||||||
|
)
|
||||||
return
|
return
|
||||||
|
|
||||||
if self.disconnected_since is not None:
|
if self.disconnected_since is not None:
|
||||||
@@ -494,8 +528,15 @@ class Engine:
|
|||||||
f"{format_duration(now - self.disconnected_since)}",
|
f"{format_duration(now - self.disconnected_since)}",
|
||||||
force=True,
|
force=True,
|
||||||
)
|
)
|
||||||
|
outage = format_duration(now - self.disconnected_since)
|
||||||
self.disconnected_since = None
|
self.disconnected_since = None
|
||||||
await self.undo_stale_action(session, now)
|
await self.undo_stale_action(session, now)
|
||||||
|
if self.stale_notified:
|
||||||
|
self.stale_notified = False
|
||||||
|
self.stale_reported = False
|
||||||
|
await self.notify(
|
||||||
|
None, None, {}, event="recovered", extra={"outage": outage}
|
||||||
|
)
|
||||||
|
|
||||||
if age is not None and self.profile.stale_after and age > self.profile.stale_after:
|
if age is not None and self.profile.stale_after and age > self.profile.stale_after:
|
||||||
if not self.stale_reported:
|
if not self.stale_reported:
|
||||||
@@ -506,7 +547,20 @@ class Engine:
|
|||||||
)
|
)
|
||||||
self.stale_reported = True
|
self.stale_reported = True
|
||||||
|
|
||||||
await self.notify(None, None, {"reason": "telemetry stale"}, event="stale")
|
if not self.stale_notified:
|
||||||
|
self.stale_notified = True
|
||||||
|
await self.notify(
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
{},
|
||||||
|
event="stale",
|
||||||
|
extra={
|
||||||
|
"reason": f"no message for {format_duration(age)}",
|
||||||
|
"action": self._word(self.profile.safe_state)
|
||||||
|
if self.profile.on_stale == "safe_state"
|
||||||
|
else "left as it was",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
if self.profile.on_stale == "stop":
|
if self.profile.on_stale == "stop":
|
||||||
raise RuntimeError("stopping: telemetry went stale")
|
raise RuntimeError("stopping: telemetry went stale")
|
||||||
@@ -525,6 +579,8 @@ class Engine:
|
|||||||
await self.undo_stale_action(session, now)
|
await self.undo_stale_action(session, now)
|
||||||
|
|
||||||
variables = derived_values(self.anker_profile, status)
|
variables = derived_values(self.anker_profile, status)
|
||||||
|
self.last_variables = dict(variables)
|
||||||
|
self.last_seen_at = stamp()
|
||||||
|
|
||||||
missing = sorted(
|
missing = sorted(
|
||||||
name
|
name
|
||||||
|
|||||||
@@ -194,6 +194,18 @@ class NotificationSettings:
|
|||||||
self.channels = list(channels or [])
|
self.channels = list(channels or [])
|
||||||
|
|
||||||
self.template = str(raw.get("template") or DEFAULT_NOTIFY_TEMPLATE)
|
self.template = str(raw.get("template") or DEFAULT_NOTIFY_TEMPLATE)
|
||||||
|
self.stale_template = str(
|
||||||
|
raw.get("stale_template")
|
||||||
|
or (
|
||||||
|
"{source_name}: telemetry lost ({reason}). Last seen battery "
|
||||||
|
"{battery_soc}% at {last_seen}. {target_name} was turned {action} "
|
||||||
|
"as a precaution."
|
||||||
|
)
|
||||||
|
)
|
||||||
|
self.recovered_template = str(
|
||||||
|
raw.get("recovered_template")
|
||||||
|
or "{source_name}: telemetry is back after {outage}. Rules resumed."
|
||||||
|
)
|
||||||
self.title = str(raw.get("title") or "{profile}")
|
self.title = str(raw.get("title") or "{profile}")
|
||||||
self.throttle = parse_duration(
|
self.throttle = parse_duration(
|
||||||
raw.get("throttle", DEFAULT_NOTIFY_THROTTLE), "notifications.throttle"
|
raw.get("throttle", DEFAULT_NOTIFY_THROTTLE), "notifications.throttle"
|
||||||
|
|||||||
Reference in new issue
Block a user