Files

121 lines
6.1 KiB
Markdown

# 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
1. Put this directory in `custom_apps/zigbee_card/` (owned by the web server user) and
`occ app:enable zigbee_card`. There is nothing to build. After a version bump, run
`occ upgrade`. If your PHP runs with `opcache.validate_timestamps=0` (many container
images do), restart PHP after any change to the `lib/` files: it never re-reads them.
2. Point it at the door. Two app values, set by the admin, never in git:
```
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
```
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.
3. 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
```
4. 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](https://code.bawnet.io/bawnet/speedtest) (no build step) plus the
dashboard half of [integration_gotosocial](https://code.bawnet.io/bawnet/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.*