Multi-agent review of the feedback batch (7 confirmed):
- [P2] Low-battery sweep excluded a genuine 0% reading (`$pct > 0`) — a dead
battery never alerted. Now `< 20` (missing reading still defaults to 100).
- [P2] SMTP encryption rule `required|in:,tls,ssl` rejected the "None" option it
offered → unencrypted relays uncofigurable. Now `in:,tls,ssl`.
- [P2] Avatar preview called temporaryUrl() on any upload → 500 for HEIC/
non-previewable files. Guarded with isPreviewable() + real-time
updatedAvatar() validation for a friendly error.
- [P3] NotificationService::send() now catches transport errors so one SMTP
failure can't abort a whole sweep / automation run.
- [P3] Mailer config is now purged (Mail::purge) so long-running Horizon workers
pick up SMTP setting changes instead of reusing a cached mailer.
- [P3] Alert dedup TTL 1 day → 1 month, so a persistent outage mails once, not
daily.
- User: themed .checkbox (accent, dark-surface, custom check) replaces the
default browser checkbox in SMTP/automation/device modals.
+2 tests (0% battery alerts, no-encryption saves). Suite 84 green, 12/12 clean.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Issue 4: an SMTP add-on for e-mail notifications.
- SmtpSetup modal: server (host/port/encryption/user/pass), from/to, and
individual toggles for the events you want mailed — device offline, low
battery, automation messages. "Send test e-mail" button. Config stored
encrypted on the addon row; a runtime mailer is built from it (no .env edits).
- NotificationService: notify(event, …) sends only if SMTP is set up AND that
event is enabled; send() for the test.
- Triggers: automation "notify" actions e-mail (when enabled);
notifications:sweep (every 5 min) e-mails on NEW device-offline / low-battery
with per-condition dedup + battery hysteresis, so it's one mail per event.
6 tests (gating, per-event toggle, config persist + password-keep, sweep dedup).
Suite 82 green, 12/12 tabs clean; card + modal browser-verified.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Issue 1 — presence was half-built:
- Away detection: the debounce was 8 min (you disconnect, still "home"). Now
3 min and configurable (homeos.presence.away_debounce_minutes /
PRESENCE_AWAY_DEBOUNCE).
- Person management: ManagePerson modal handles BOTH add and edit (name, UniFi
client, avatar). Delete from the person card (confirm). Replaces AddPerson.
- Avatar upload: image stored on the public disk (storage:link), shown on cards
+ the who's-home strip; initials fallback (green when home).
- Who's home: a summary strip ("N Zuhause · M abwesend" with avatars) + clear
per-person home/away, so you can see at a glance who is in.
5 tests (debounce, create+avatar, edit, delete+avatar removal). Suite 78 green,
12/12 tabs clean.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
You were right that HA uses the Shelly LOCAL API, not MQTT. Adds that path
(user chose "both") — control + status over http://<ip>/rpc, no MQTT setup on
the device:
- ShellyRpc (POST /rpc), ShellyHttpDriver (Switch/Light.Set, Reboot).
- ShellyStatusApplier: one shared apply path (normalize + input roles +
monotonic upsert + broadcast) reused by BOTH the MQTT ingest and the HTTP
poll, so transports can't drift. IngestShellyMessage refactored onto it.
- ShellyLocalOnboarder: probe an IP → GetDeviceInfo/GetStatus → create an
http-protocol device with its entities (reuses an MQTT-onboarded row by id,
no duplicate). AssignDevice uses it when a discovered Shelly is reachable;
falls back to MQTT-style if not.
- Manual "Gerät hinzufügen" modal (add by IP). shelly:poll scheduled every 10s
+ a re-poll after each command (PollShellyDevice) for near-live status.
driverFor picks http vs mqtt by protocol.
- Normalizer now drops housekeeping components (sys/wifi/cloud/mqtt/ws/…) so
GetStatus doesn't create junk entities.
6 ShellyHttpTest cases (Http::fake). Suite 68 green, 12/12 clean.
LIVE-VERIFIED against the real Shelly 1 Mini Gen3 at 10.10.30.78: onboarded
over local API (protocol http), entities switch:0 + input:0, kept online by the
10s poll — no MQTT configured on the device.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Second half of the Ring integration (handoff §12):
- RingTopics + RingNormalizer (H3): parse ring/<loc>/<cat>/<id>/<entity>/state,
map ding/motion/contact/lock/battery; bridge status topic drives the addon
connection state. Defensive — unknown topics are ignored, never junk devices.
- IngestRingMessage (H4, single ingest path): auto-creates cloud-flagged Ring
devices on first sight, monotonic race-safe state upsert, broadcasts live.
Gated on the addon being installed (cached) so a still-running bridge can't
recreate devices after uninstall.
- Shared AppliesDeviceState trait: the monotonic upsert now lives once, used by
both Shelly and Ring ingest (MqttTest guards the Shelly path).
- Listener subscribes ring/#. ring-mqtt container (opt-in `addons` compose
profile) as least-privileged `ring` MQTT user (ACL: ring/# only); gen-passwd
seeds the account. "Cloud" badge on Ring devices in list + detail.
11 Ring tests (topic parse, normalizer, bridge status, auto-create, install
gate, unknown-topic guard, out-of-order). Suite 40 green; 12/12 tabs clean.
Live-verified: real MQTT ring/.../ding + info publish → cloud device created
with ding + battery state. (Real Ring OAuth login runs in the ring-mqtt sidecar.)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- metrics table + Metric model; metrics:sample command (scheduled every minute,
pruned to 24h) records MQTT throughput (a Redis counter the ingest jobs bump)
and host CPU load + memory% from /proc.
- Chart.js line-chart Alpine island (themed from CSS tokens, dual axis) on the
Host page shows MQTT/min, CPU load and memory %. wire:ignore so the 10s health
poll doesn't re-init it. Nav check 10/10 clean (0 console errors).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- UnifiClient service (art-of-wifi/unifi-api-client) talks to the local UDM with a
read-only account (self-signed cert, verify_ssl off). Verified live: logged in
and read 32 active clients.
- presence:poll command (scheduled every minute, withoutOverlapping): "home"
immediately on association, "away" only after an 8-min debounce so iPhone WLAN
sleep can't cause false-aways. Broadcasts PresenceChanged on the presence channel.
- Persons page: "Person hinzufügen" modal picks the representing device straight
from the live UniFi client list (falls back to manual MAC entry if UniFi is down);
the sweep then tracks that person's presence. mac + last_seen_home_at on persons.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Python discovery sidecar (zeroconf mDNS over host network) publishes findings
to homeos/discovery/<source>/<id> as the sidecar user. Compose service with
network_mode: host + NET_RAW. Verified live: it found real devices on the LAN
(a printer and Shellys) via mDNS.
- Listener also subscribes homeos/discovery/#; IngestDiscoveryMessage upserts
discovery_findings (preserving assigned/ignored) and broadcasts DeviceDiscovered
on the private discovery channel.
- "Neue Geräte" page lists findings live with Assign (modal → creates a Device
and links the finding) and Ignore/Restore. Per-device broker credentials are
provisioned at onboarding (noted in the assign hint).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
With multiple Horizon workers, ingest jobs for one entity can finish out of
order and overwrite newer state with older. The listener now stamps each message
with a µs receive time (observed_at); IngestShellyMessage applies state only when
the incoming message is newer (race-safe via a conditional update + unique guard),
and broadcasts only when applied. Added a feature test for the ordering guard.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Real bidirectional MQTT so devices are live, not mock (handoff §13.3):
- Mosquitto 2 broker (auth + per-client ACLs for laravel/shelly/sidecar from day
one); passwd generated by docker/mosquitto/gen-passwd.sh (gitignored).
- mqtt-listener daemon: subscribes `+/status/#`, parse + dispatch only (H2),
exponential reconnect backoff, graceful SIGTERM. php-mqtt/laravel-client.
- Ingest path (H4): IngestShellyMessage resolves device by mqtt_prefix, upserts
device_states, refreshes last_seen, broadcasts DeviceStateChanged
(ShouldBroadcastNow) on the private `home` channel.
- Control path (H1): DeviceDriver contract + ShellyMqttDriver (command topic +
Shelly.Reboot RPC) behind DeviceCommandService, which audits every command to
the new `commands` table. Device detail toggles + restart route through it;
flash reflects the real result.
- Live UI: dashboard + device pages listen via Echo (#[On('echo-private:home,
.DeviceStateChanged')]) and re-render instantly.
- Vendor specifics isolated in Support/Mqtt + Support/Drivers (H3).
Verified end-to-end in a real browser: publishing an MQTT status turned a light
"An" on the dashboard in 3.0s with no reload, 0 console errors. R12 30/30;
15 feature tests green (incl. ingest + command audit). README/bootstrap document
the broker passwd step.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>