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 2: Ring sat on "verbinde…" forever because nothing ever connected — the
ring-mqtt bridge (opt-in container) wasn't running, and HomeOS was collecting
Ring credentials it never used. Per the handoff the BRIDGE owns the Ring login
(email/pw/2FA) in its own web UI; HomeOS must not handle Ring credentials.
- Replaced the credential modal with a RingSetup guide: start the bridge
(`docker compose --profile addons up -d ring-mqtt`), open its web UI (linked
at the auto-detected host:port), log in there; devices then appear.
- Status is now honest — it reflects the bridge's REAL MQTT presence
(IngestRingMessage.markStatus), showing "Nicht verbunden" with next-steps
until the bridge actually reports, instead of a stuck "connecting".
- Removed the RingConnect cred modal + fields; reset any stuck status.
Suite green, 12/12 tabs clean; setup 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>
Issue 5: rooms couldn't be created or deleted.
- "Raum anlegen" button + CreateRoom modal (name + icon picker) on the rooms
index.
- Delete on the room page behind a confirm that states how many devices are in
it; on delete the devices are KEPT but moved to "no room" (room_id null).
3 tests. Suite green, 12/12 tabs clean.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Issue 3: the New-Automation modal was taller than the screen and the whole
dialog scrolled. New <x-modal> component: header + footer are fixed (shrink-0),
only the body scrolls, capped to max-h-85dvh so the page never scrolls. Refactored
create-automation (now a responsive 2-column grid, widened to 3xl), add-device,
ring-connect and add-person onto it — the standard for every modal going forward.
Verified: modal fits a 720px viewport with no page scroll; create flow still works.
12/12 tabs clean, suite green.
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>
Addresses "I have to configure MQTT per device; HA just works":
- Shared device account `shelly` (one credential every Shelly uses) with a
SCOPED ACL (status/events/online/rpc for any prefix; cannot touch homeos/,
ring/ or $SYS). gen-passwd seeds it from MQTT_SHELLY_PASSWORD.
- Auto-onboarding: IngestShellyMessage creates the device on the first
recognizable component (sys/wifi/cloud noise ignored), so pointing a Shelly
at the broker is all it takes — no manual "Zuweisen". Partial unique index on
config->>'mqtt_prefix' + race-safe create (merge-dedup migration).
- Settings → Geräte-MQTT card: server, username, reveal/copy password + steps
(config/homeos.php, MQTT_DEVICE_HOST). Discovery "Zuweisen" now upserts by
prefix (names/rooms an already-onboarded device, no duplicate) and no longer
forces per-device creds.
- Per-device credentials kept as opt-in hardening: a "generate" button on the
device page (pattern %u ACL retained).
Live-verified: publishing as `shelly` to a new prefix auto-creates the device
with switch+power state; a sys topic creates nothing. Suite 46 green, 12/12
tabs clean.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
First half of the Ring integration (handoff §12: ring-mqtt, cloud token auth):
- Addons registry (static catalogue) + Addon model with encrypted config, so
the Ring refresh token / credentials are never stored in plaintext.
- AddonService: install / uninstall (wipes secrets) / saveConnection (blank
password keeps the stored one) / markStatus (from the bridge over MQTT).
- Addons page (new sidebar tab) with per-addon install, Cloud badge, live
status pill, and a Ring connect modal capturing email/password/2FA — we
never call Ring ourselves; creds are handed to the ring-mqtt sidecar.
- Uninstall behind a wire-elements confirm (R5). Fixed the confirm target:
Livewire names App\Livewire\Addons\Index as "addons" (Index suffix dropped),
and only $wire.$dispatch (not Alpine $dispatch) reaches the modal listener.
- DE/EN localization (R16); doorbell/package/cloud icons.
7 AddonTest cases (registry, install, encryption-at-rest, blank-password
keep, uninstall wipe, status guard). Suite 33 green; 12/12 tabs clean; full
install→connect→connecting→uninstall UI flow browser-verified, 0 console errors.
Backend bridge (ring-mqtt container) + Ring MQTT normalizer land next.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
New "Steuerung" page for tablet control:
- Large touch tiles for every light/switch — tap toggles (through
DeviceCommandService), highlighted when on, live via Echo.
- Drag-to-reorder via SortableJS (handle per tile); order persisted to
entities.panel_sort.
- Lights get a colour/brightness modal → Light.Set (brightness + rgb) on the
Shelly, with preset swatches + a custom colour picker. Driver contract gains
setLight(); command service audits it (H1).
Nav check 11/11 tabs clean.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Assigning a Shelly now issues a real broker credential so the physical device can
connect (onboarding was incomplete before):
- MqttCredentialProvisioner writes a mosquitto-compatible PBKDF2-SHA512 ($7$)
password line (in PHP) for username = the device's topic prefix, and touches a
reload trigger. A small wrapper in the mosquitto container (docker/mosquitto/
config/entrypoint.sh) SIGHUPs mosquitto so it re-reads the passwd live — no
restart. Verified: a provisioned device authenticates and publishes to its own
prefix (bound by the pattern %u ACL).
- The credential is shown once on the device page after assignment (enter it into
the Shelly). passwd is app-owned + world-readable so the web request can write it.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- AssignDevice derives the Shelly MQTT prefix from the mDNS service instance name
(the device id) instead of the slugified topic identifier (which carries the
_shelly._tcp suffix); non-Shelly devices get no prefix.
- IngestDiscoveryMessage broadcasts DeviceDiscovered only on first creation, not on
every retained/periodic re-publish.
- The generic Confirm modal can target its event at the opening component (->to),
so a shared event name can't be caught by an unrelated component.
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>
HomeStatus aggregated from Room, so a device with room_id = null (a supported
state, e.g. after picking "no room") vanished from dashboard totals, KPIs and
warnings. Aggregate from all devices instead; the dashboard groups them by room
with a trailing "Ohne Raum" group so nothing disappears. Added a feature test.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Warnings no longer clutter the dashboard: a compact "Warnungen (n)" button in
the topbar opens a themed wire-elements/modal (R5) listing all messages. Dark
modal wrapper overridden; HomeStatus service shared by dashboard + modal.
- Device detail page (/devices/{uuid}, UUID route key): edit name, room and
active state; view info + live capabilities; Neustart (with confirm modal) and
"Update prüfen" as mock commands (Phase 3 routes them through the real driver).
Devices index (/devices) added; "Geräte" nav activated; dashboard device rows
and index link to the detail. Generic Confirm modal + x-detail component.
- Mock devices no longer rot: online is now "active and (no last_seen or seen
<10min)", so the demo stays healthy; the one offline device keeps a stale
timestamp. Full DE/EN i18n for devices + modal copy.
Verified: R12 30/30 in headless Chromium (0 console errors, 0 failed requests);
10 feature tests green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>