Add an actions box showing why each switch changed

This commit is contained in:
Sterling Archer
2026-08-12 10:53:34 -07:00
parent a28db4d1c9
commit 6c43256088
3 changed files with 338 additions and 8 deletions
+79 -4
View File
@@ -8,12 +8,15 @@ from datetime import datetime
import aiohttp
from . import paths
from .profiles import load_yaml
from .profiles import load_yaml, slugify
from .notify import Notifier, render
from .rules import derived_values, format_duration, validate
from .shelly import ShellyTarget
EVENT_HISTORY = 200
async def interruptible_sleep(seconds):
remaining = float(seconds or 0)
while remaining > 0:
@@ -150,7 +153,15 @@ class Engine:
return None
async def apply(
self, session, desired, reason, now, rule=None, variables=None, force=False
self,
session,
desired,
reason,
now,
rule=None,
variables=None,
force=False,
cause="rule",
):
if self.last_commanded is desired:
return False
@@ -176,6 +187,7 @@ class Engine:
self.recent_actions.append(now)
self.report(f"turned {self._word(desired)} - {reason}")
self.save_state(desired, reason)
self.record_event(desired, reason, rule, variables, cause=cause)
await self.notify(desired, rule, variables, event="action")
return True
@@ -341,6 +353,7 @@ class Engine:
"restored after telemetry recovered",
now,
force=True,
cause="recovered",
)
async def check_floor(self, session, variables, now):
@@ -377,6 +390,7 @@ class Engine:
now,
variables=variables,
force=True,
cause="floor",
)
return True
@@ -404,6 +418,7 @@ class Engine:
now,
variables=variables,
force=True,
cause="floor",
)
if floor.notify:
@@ -445,6 +460,58 @@ class Engine:
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", {})
detail = {}
for key in (
"battery_soc",
"pv_total",
"pv_surplus",
"output_power_total",
"ac_input_power",
):
value = (variables or {}).get(key)
if isinstance(value, (int, float)) and not isinstance(value, bool):
detail[key] = round(value)
event = {
"epoch": time.time(),
"when": stamp(),
"state": bool(desired),
"cause": cause,
"profile": self.profile.name,
"rule": rule.name if rule else None,
"condition": rule.when_source if rule else None,
"reason": reason,
"target": target_identity.get("name")
or target_identity.get("model")
or self.target.host,
"target_channel": self.target.channel,
"source": source_identity.get("name")
or source_identity.get("model")
or source_identity.get("serial"),
"source_serial": source_identity.get("serial"),
"values": detail,
}
path = paths.STATE_DIR / f"events-{slugify(self.profile.name)}.json"
try:
paths.STATE_DIR.mkdir(parents=True, exist_ok=True)
history = []
if path.exists():
try:
history = json.loads(path.read_text(encoding="utf-8"))
except Exception:
history = []
if not isinstance(history, list):
history = []
history.append(event)
path.write_text(json.dumps(history[-EVENT_HISTORY:]), encoding="utf-8")
except Exception:
pass
def publish_live(self, variables, age):
identity = self.anker_profile.get("identity", {})
serial = identity.get("serial") or "unknown"
@@ -531,7 +598,11 @@ class Engine:
self.pre_stale_state = self.last_commanded
self.stale_action_state = self.profile.safe_state
await self.apply(
session, self.profile.safe_state, "mqtt disconnected safe state", now
session,
self.profile.safe_state,
"connection to Anker lost",
now,
cause="stale",
)
if not self.stale_notified:
@@ -597,7 +668,11 @@ class Engine:
self.pre_stale_state = self.last_commanded
self.stale_action_state = self.profile.safe_state
await self.apply(
session, self.profile.safe_state, "stale telemetry safe state", now
session,
self.profile.safe_state,
"telemetry went stale",
now,
cause="stale",
)
return