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>
|
||
|---|---|---|
| app | ||
| bootstrap | ||
| config | ||
| database | ||
| docker | ||
| lang | ||
| public | ||
| resources | ||
| routes | ||
| storage | ||
| tests | ||
| .editorconfig | ||
| .env.example | ||
| .gitattributes | ||
| .gitignore | ||
| .npmrc | ||
| CLAUDE.md | ||
| README.md | ||
| artisan | ||
| composer.json | ||
| composer.lock | ||
| design-mockup.html | ||
| docker-compose.yml | ||
| handoff.md | ||
| package-lock.json | ||
| package.json | ||
| phpunit.xml | ||
| rules.md | ||
| vite.config.js | ||
README.md
HomeOS
Self-built smart-home control plane — single household, self-hosted, LAN-first. Laravel 13 · Livewire v3 · Tailwind v4 · PostgreSQL 17 + TimescaleDB · Redis/Horizon · Reverb.
Everything runs in Docker; the host needs only Docker (no PHP/Composer/Node).
Authoritative spec: handoff.md · conventions: rules.md · guide: CLAUDE.md.
First-run setup
cp .env.example .env # dev defaults work as-is
docker compose build # build the app image
docker compose run --rm app bash docker/app/bootstrap.sh # deps + key + assets + migrate/seed
bash docker/mosquitto/gen-passwd.sh # broker credentials (fills empty MQTT_*_PASSWORD)
docker compose up -d # start the full stack
docker compose upalone is not the first command —vendor/,node_modules,public/buildand the Mosquitto passwd file are not committed, so the steps above must run first.
Then open http://localhost and sign in with the seeded dev admin:
- admin@homeos.local / homeos-dev ← change this for anything but local dev.
Production
.env.example ships development defaults. For a real deployment you must:
- set
APP_ENV=productionandAPP_DEBUG=false(otherwise exceptions are exposed and the demo household is seeded into your real database); - set strong values for
DB_PASSWORD, theREVERB_APP_*keys andHOMEOS_ADMIN_PASSWORD(seeding aborts if the admin password is unset outside local/testing), and runphp artisan key:generate.
The demo seeder (DemoHomeSeeder) only runs in local/testing.
Everyday commands (in-container, R8)
docker compose exec app php artisan … # artisan
docker compose exec app composer … # composer
docker compose exec app npm run build # rebuild assets (or `npm run dev` for HMR)
docker compose exec app php artisan test # test suite
docker compose logs -f app # logs
Services & ports
| Service | Role | Host port |
|---|---|---|
app |
php-fpm + nginx + supervisor | ${APP_PORT:-80} |
reverb |
websockets (proxied same-origin via nginx /app) |
${REVERB_HOST_PORT:-6001} |
horizon · scheduler |
queue workers · cron | — |
db |
PostgreSQL 17 + TimescaleDB | 127.0.0.1:${DB_HOST_PORT:-5432} |
redis |
cache / queue | — |
All ports and HOST_UID/HOST_GID are env-driven in .env.