Commit Graph

6 Commits (8086a9647b4e99aba0ecda08c3f5ac1aab45321a)

Author SHA1 Message Date
HomeOS Bootstrap a4489ac7d3 feat(addons): Ring backend bridge — ring-mqtt ingest, cloud devices, badge
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>
2026-07-18 01:58:40 +02:00
HomeOS Bootstrap af951d9f91 Charts: Chart.js MQTT + host traffic on the Host page
- 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>
2026-07-18 00:40:32 +02:00
HomeOS Bootstrap d950abe375 Fix R15: make the last_seen update atomic (no read-then-write race)
Two concurrent workers could both read the old last_seen_at, pass the in-memory
comparison and save in reverse order, rewinding presence. The guard now lives in
the WHERE clause of a single conditional UPDATE, so last_seen_at only ever
advances even under concurrent ingestion.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-17 23:11:08 +02:00
HomeOS Bootstrap 23ef993cb2 Fix R15: per-device MQTT ACLs, monotonic presence, real-device online semantics
- ACL: drop the shared `shelly` account + `+` wildcard (allowed cross-device
  spoofing/control). Devices now authenticate with per-device credentials
  (username = topic prefix, provisioned at onboarding) bound to their own prefix
  via `pattern %u`. gen-passwd.sh creates only laravel + sidecar.
- last_seen_at is set from the message receive time (observed_at), monotonically,
  so a delayed/retried/stale ingest job can't mark a device online incorrectly.
- Device::isOnline: a real device with no last_seen is OFFLINE (never connected);
  only demo devices are assumed reachable. Added `demo` flag + presence tests.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-17 23:08:29 +02:00
HomeOS Bootstrap 66f0d7b067 Fix R15: reject out-of-order MQTT state updates (concurrency)
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>
2026-07-17 23:01:32 +02:00
HomeOS Bootstrap ea9e35883b Phase 3: MQTT ingest — Mosquitto bus, Shelly driver, live device state
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>
2026-07-17 22:51:14 +02:00