6.1 KiB
zigbee_card — your house on the Nextcloud dashboard
Six rows, drawn like every other dashboard card's list, nothing else:
| row | title (the action) | the line under it (what is) |
|---|---|---|
| 🌡 | 73 °F · 45 % | door closed yesterday 10:59 (press = refresh) |
| ☀️ / 🌙 | day / night | lights are on night / day |
| 💡 | lights off / on | lights are on / off |
| ❄️ | AC off / on | AC is on / off |
| 🔥 | bathroom heater off / on | bathroom heater is on / off |
| 📺 | display off / on | display is on / off (the display, a Fire TV, over srv's Bluetooth, 2026-10-27; replaced the feeder row — feeding is a DM). What is = what the keyboard last set; the TV reports nothing back |
The 🌡 row: temperature (the sensor reports Celsius; the card shows Fahrenheit) · humidity (the 🐱 feeding count was dropped 2026-10-27 — Bella is on the feeder's schedule). Under it: the door's last change ("door closed 10:59", "door open since 14:02", "yesterday …"), nothing else, so it fits the row. The 🐾 row's line is when the pet was last fed, from any source. Times are 24-hour, like the rest of the node.
A toggle's title is what a press does; the line under it is what is. The card never guesses: every row is drawn from what the devices reported, and redrawn after every press.
It does not talk to zigbee2mqtt. It talks to a command bot that already does — the
same bot that answers one-line commands in a chat DM (lights day, ac off) — through
a small HTTP door the bot opens on a private address. The bot keeps every rule it
already has (scenes stored in the bulbs, a daily cap on the
pet feeder read from the feeder's own counter) and answers with what the devices
actually reported.
No build step, no bundler, no state, no database table, no settings page. Four PHP files, one JavaScript file, one stylesheet.
The path
press
-> js/zigbee_card.js POST /apps/zigbee_card/cmd {line: "lights night"} (Nextcloud's requesttoken: only the dashboard page can post)
-> CardController the line must be one of lib/Buttons.php LINES, else 403
-> the bot's door POST <door_url> {line, who} header X-Zigbee-Key (a private address only this server reaches)
-> the bot its own allow-list, its own rules, mosquitto -> zigbee2mqtt -> the devices -> read back
<- {ok, reply} the first line of the reply shows for a few seconds, then every button redraws from a fresh "state"
The lines the page may send
lights day · lights night · lights on · lights off · ac on · ac off ·
heater on · heater off · display on · display off · state (the read). The list is
lib/Buttons.php; the bot's door carries its own allow-list, so a line has to pass both.
Adding a button is a change in both places, on purpose. Scenes are called by name:
where a scene lives (the bot's config, or scenes stored in the bulbs) is the bot's business.
The door contract (what the bot must implement)
POST <door_url>
X-Zigbee-Key: <shared secret>
Content-Type: application/json
{"line": "lights night", "who": "<nextcloud user id>"}
200 {"ok": true, "reply": "💡 lights night\n…what the devices said…"}
{"line": "state", "who": "<nextcloud user id>"}
200 {"ok": true, "reply": "state", "state": {
"temp_c": 22.6, "humidity": 44.8,
"feeds_24h": 5, "last_fed": "2026-10-20T07:11:26-07:00", (null when unknown)
"door": {"closed": true, "at": "2026-10-19T10:59:28-07:00"}, (at: the last CHANGE, or null)
"lights_on": true, "scene": "night", (scene: the name the lights match, or null)
"plugs": {"ac": "ON", "heater": "OFF"}}}
401 wrong or missing key · 403 line not on the bot's allow-list · 400 bad body
reply is free text; the card prints its first line. who is for the bot's audit log.
Install
- Put this directory in
custom_apps/zigbee_card/(owned by the web server user) andocc app:enable zigbee_card. There is nothing to build. After a version bump, runocc upgrade. If your PHP runs withopcache.validate_timestamps=0(many container images do), restart PHP after any change to thelib/files: it never re-reads them. - Point it at the door. Two app values, set by the admin, never in git:
The door address should be one only the Nextcloud host can reach (a container bridge gateway, a loopback, a wireguard peer). Nextcloud's HTTP client refuses private addresses by default; this app allows them for this one request, because the whole point is that the door is private.
occ config:app:set zigbee_card door_url --value=http://<private address>:<port>/cmd occ config:app:set zigbee_card door_key --value=<the same secret the bot holds> --sensitive - Optional footer link, the other cards' "show more" (none without it):
occ config:app:set zigbee_card link_url --value=https://<your zigbee2mqtt frontend>/ occ config:app:set zigbee_card link_text --value=zigbee2mqtt - Dashboard → Customize → The house.
Any logged-in user may press; restrict it with Nextcloud's app-to-group limits if your users are not all your household.
Files
| file | job |
|---|---|
appinfo/info.xml · appinfo/routes.php |
the app, one route |
lib/AppInfo/Application.php |
registers the widget |
lib/Buttons.php |
the six buttons (initial state for the page) and LINES (allow-list for the controller) |
lib/Dashboard/ZigbeeWidget.php |
the widget: id zigbee_card, title "The house" |
lib/Controller/CardController.php |
POST /cmd → the door |
js/zigbee_card.js · css/zigbee_card.css |
the grid, the header icon |
img/app.svg · img/app-dark.svg |
the icon |
Pattern: speedtest (no build step) plus the
dashboard half of integration_gotosocial
(OCA.Dashboard.register + IWidget), minus Vue.
The most reliable integration is the one with nothing to integrate. This one has six buttons and a door, and the door says no to everything it was not told about.