bunch of bugs

This commit is contained in:
2026-08-26 18:17:03 +02:00
parent c5b598b522
commit 1e73476830
7 changed files with 77 additions and 9 deletions
+3 -2
View File
@@ -35,8 +35,9 @@ both actions through `on_event: [_notify, _cloud_logger]`. A single action name
such as `on_event: _notify` remains supported.
Arrival detection emits `arrived` and `departed` for individual device IDs. It
also emits `empty` with ID `house` after the last present device departs. Initial
startup state never generates these transition events.
also emits `empty` with ID `house` after the last present device departs, and
`first_arrival` with the arriving device ID when an empty house becomes occupied.
Initial startup state never generates these transition events.
Plugin modules, factories, configuration sections, and event actions are all
declared under `controller`. The controller contains no plugin-specific imports
+2
View File
@@ -1,2 +1,4 @@
{"timestamp":"2026-08-23T06:09:35.779477+00:00","remote_addr":"127.0.0.1","event":{"sender":"_arrival_detection","event":"departed","id":"ignace","text":""}}
{"timestamp":"2026-08-23T06:10:47.665139+00:00","remote_addr":"127.0.0.1","event":{"sender":"_arrival_detection","event":"arrived","id":"ignace","text":""}}
{"timestamp":"2026-08-26T16:16:16.951086+00:00","remote_addr":"127.0.0.1","event":{"sender":"_arrival_detection","event":"departed","id":"ignace","text":"2026-08-26T16:16:16.949395+00:00 _arrival_detection: departed (ignace)","datetime":"2026-08-26T16:16:16.949395+00:00"}}
{"timestamp":"2026-08-26T16:16:17.474743+00:00","remote_addr":"127.0.0.1","event":{"sender":"_arrival_detection","event":"empty","id":"house","text":"2026-08-26T16:16:17.473318+00:00 _arrival_detection: empty (house)","datetime":"2026-08-26T16:16:17.473318+00:00"}}
+13
View File
@@ -137,6 +137,7 @@ class ArrivalMonitor:
]
def _process_states(self, states: dict[str, bool]) -> None:
arrivals: list[dict] = []
for device in self.devices:
present = states[device["mac"]]
LOG.debug(
@@ -154,6 +155,8 @@ class ArrivalMonitor:
}
_record_event(self.config, event, device["mac"])
self.on_event(event)
if transition == "arrived":
arrivals.append(device)
known_states = [detector.state for detector in self.detectors.values()]
if all(state is not None for state in known_states):
@@ -167,6 +170,16 @@ class ArrivalMonitor:
}
_record_event(self.config, event)
self.on_event(event)
elif self._house_occupied is False and occupied:
first = arrivals[0]
event = {
"sender": "_arrival_detection",
"event": "first_arrival",
"id": first["id"],
"text": "",
}
_record_event(self.config, event, first["mac"])
self.on_event(event)
self._house_occupied = occupied
def _run(self) -> None:
+3 -2
View File
@@ -2,7 +2,7 @@
import logging
from datetime import datetime, timezone
from pprint import pprint
import requests
@@ -62,7 +62,7 @@ def on_event(config: dict, message: dict) -> None:
timestamp = datetime.now(timezone.utc).isoformat()
values = {**message, "datetime": timestamp}
messages = config.get("messages", {})
template = messages.get(
template = config.get(
message["event"],
config.get("message", "{datetime} {sender}: {event} ({id})"),
)
@@ -71,6 +71,7 @@ def on_event(config: dict, message: dict) -> None:
"datetime": timestamp,
"text": message.get("text") or str(template).format(**values),
}
pprint(payload)
send_event(
payload,
event_url=str(config.get("event_url", "")),
+13
View File
@@ -1,3 +1,16 @@
2026-08-22T22:27:23+02:00 ARRIVAL name=Ignace mac=ce:68:53:a8:fc:07
2026-08-23T08:09:35+02:00 DEPARTED id=ignace mac=ce:68:53:a8:fc:07
2026-08-23T08:10:47+02:00 ARRIVED id=ignace mac=ce:68:53:a8:fc:07
2026-08-23T15:17:07+02:00 DEPARTED id=ignace mac=ce:68:53:a8:fc:07
2026-08-23T15:23:46+02:00 ARRIVED id=ignace mac=ce:68:53:a8:fc:07
2026-08-26T17:46:51+02:00 DEPARTED id=ignace mac=ce:68:53:a8:fc:07
2026-08-26T17:46:51+02:00 EMPTY id=house
2026-08-26T17:50:34+02:00 DEPARTED id=ignace mac=ce:68:53:a8:fc:07
2026-08-26T17:50:34+02:00 EMPTY id=house
2026-08-26T17:57:07+02:00 DEPARTED id=ignace mac=ce:68:53:a8:fc:07
2026-08-26T17:57:08+02:00 EMPTY id=house
2026-08-26T18:01:41+02:00 DEPARTED id=ignace mac=ce:68:53:a8:fc:07
2026-08-26T18:01:41+02:00 EMPTY id=house
2026-08-26T18:05:14+02:00 ARRIVED id=ignace mac=ce:68:53:a8:fc:07
2026-08-26T18:16:16+02:00 DEPARTED id=ignace mac=ce:68:53:a8:fc:07
2026-08-26T18:16:16+02:00 EMPTY id=house
+6 -4
View File
@@ -30,7 +30,7 @@ arrival_detection:
verify_tls: true
hosts_oid: lanhosts
# Log every router/API call and its result without logging credentials.
debug: false
debug: true
devices:
- id: ignace
@@ -53,15 +53,16 @@ notify:
filter:
# Empty accept lists allow all values. Ignore lists always take precedence.
accept:
events: [empty]
events: [empty, first_arrival]
ids: []
ignore:
events: [arrived, departed]
events: [departed]
ids: []
# Available placeholders: {sender}, {event}, {id}, {text}.
message: "{sender}: {event} ({id})"
messages:
arrived: "{id} arrived home"
first_arrival: "{id} is the first to arrive home"
departed: "{id} left home"
empty: "The house is empty"
# May also be supplied through the NTFY_TOPIC_URL environment variable.
@@ -70,7 +71,8 @@ notify:
cloud_logger:
enabled: true
event_url: http://www.suy.nl/home_logger/event
# event_url: http://www.suy.nl/home_logger/event
event_url: http://127.0.0.1:29651/home_logger/event
timeout: 10
filter:
# Empty accept lists allow all values. Ignore lists always take precedence.
+37 -1
View File
@@ -78,7 +78,43 @@ class ArrivalDetectorTests(unittest.TestCase):
"event": "arrived",
"id": "ignace",
"text": "",
}
},
{
"sender": "_arrival_detection",
"event": "first_arrival",
"id": "ignace",
"text": "",
},
],
)
@patch("lib._arrival_detection._build_router")
def test_first_arrival_fires_only_when_empty_house_becomes_occupied(
self, _build_router
):
events = []
monitor = ArrivalMonitor(
{
"devices": [
{"id": "ignace", "mac": "00:00:00:00:00:01"},
{"id": "janine", "mac": "00:00:00:00:00:02"},
],
"polling": {"absent_after": 1, "present_after": 1},
},
events.append,
)
first = "00:00:00:00:00:01"
second = "00:00:00:00:00:02"
monitor._process_states({first: False, second: False})
monitor._process_states({first: True, second: False})
monitor._process_states({first: True, second: True})
self.assertEqual(
[(event["event"], event["id"]) for event in events],
[
("arrived", "ignace"),
("first_arrival", "ignace"),
("arrived", "janine"),
],
)