The 24-hour running total stays; the switch to the feeder's own portions_per_day in 1.3.1 is reverted. 24-hour times, the door-only line and no "..." stay. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
122 lines
5.9 KiB
Markdown
122 lines
5.9 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 % · 5 🐱 | 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 |
|
|
| 🐾 | feed bella | last fed 07:11 |
|
|
|
|
The 🌡 row: temperature (the sensor reports Celsius; the card shows Fahrenheit) · humidity ·
|
|
**🐱 = how many times the pet was fed in the last 24 hours** — a running count the bot keeps,
|
|
not the feeder's own counter (that one resets at midnight). 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` · `feed bella` · `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.*
|