diff --git a/README.md b/README.md index 254f1ba..dc15af4 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/cloud/flask_home_control_log.log b/cloud/flask_home_control_log.log index 5ca9113..96aa7a9 100644 --- a/cloud/flask_home_control_log.log +++ b/cloud/flask_home_control_log.log @@ -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"}} diff --git a/lib/_arrival_detection.py b/lib/_arrival_detection.py index 0e21e6d..8609ed3 100644 --- a/lib/_arrival_detection.py +++ b/lib/_arrival_detection.py @@ -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: diff --git a/lib/_cloud_logger.py b/lib/_cloud_logger.py index 25cd68c..74e8920 100644 --- a/lib/_cloud_logger.py +++ b/lib/_cloud_logger.py @@ -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", "")), diff --git a/log/detect-arrivals.txt b/log/detect-arrivals.txt index c7746e3..81d7afa 100644 --- a/log/detect-arrivals.txt +++ b/log/detect-arrivals.txt @@ -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 diff --git a/service.yaml b/service.yaml index 923790d..ab6191d 100644 --- a/service.yaml +++ b/service.yaml @@ -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. diff --git a/tests/test_service.py b/tests/test_service.py index efb983e..91aff4b 100644 --- a/tests/test_service.py +++ b/tests/test_service.py @@ -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"), ], )