Files
zigbee_card/README.md
neonoverlordandClaude Opus 5.5 74f7ad805b 1.3.3: 🐱 back to the 24 h running total (feeds_24h)
Brock 2026-10-20: "it was fine on the 24 hour running total" -- the switch to the
feeder's own portions_per_day in 1.3.1 was not approved. 24-hour times, the
door-only line and no "..." stay.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-20 11:48:24 -07:00

5.9 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 % · 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 (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.