Files
zigbee_card/lib/Controller/CardController.php
neonoverlordandClaude Fable 5.1 2e98a3e7a5 1.0.1: tiles instead of a status dump, the feeder button, the header icon
- The card opens on a few tiles (room temperature + humidity, feeder portions today / cap,
  AC, heater, lights) drawn from a new "state" read -- a small JSON from the bot's door --
  and redraws them after every press and on click. The status button and the text dump
  are gone; a press shows the first line of the bot's reply for a few seconds.
- feed: a button behind a second tap (window.confirm); the bot keeps its daily cap from
  the feeder's own counter.
- The widget's header icon was a CSS class nothing defined, so the dashboard showed none;
  css/zigbee_card.css now defines .icon-zigbee-card from img/app-dark.svg, inverted for
  dark mode like Nextcloud's own.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-19 19:45:12 -07:00

77 lines
3.4 KiB
PHP

<?php
declare(strict_types=1);
namespace OCA\ZigbeeCard\Controller;
use OCA\ZigbeeCard\AppInfo\Application;
use OCA\ZigbeeCard\Buttons;
use OCP\AppFramework\Controller;
use OCP\AppFramework\Http;
use OCP\AppFramework\Http\Attribute\NoAdminRequired;
use OCP\AppFramework\Http\JSONResponse;
use OCP\AppFramework\Services\IAppConfig;
use OCP\Http\Client\IClientService;
use OCP\IRequest;
/**
* POST /apps/zigbee_card/cmd {line: "lights day"}
*
* The path: this controller -> the house bot's door (an HTTP listener on a private
* address that only this server reaches) -> the bot's own command handler -> mosquitto
* -> zigbee2mqtt -> the device -> read back -> the bot's reply text comes back here
* verbatim and the card prints it.
*
* Two app values, set by the admin with occ, 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 secret the bot holds> --sensitive
*
* Any logged-in user may press; the requesttoken check is Nextcloud's default on this
* route, so only the dashboard page can post here. The user id goes to the bot as "who"
* for its audit log.
*/
class CardController extends Controller {
public function __construct(
IRequest $request,
private IAppConfig $appConfig,
private IClientService $clientService,
private ?string $userId,
) {
parent::__construct(Application::APP_ID, $request);
}
#[NoAdminRequired]
public function cmd(string $line = ''): JSONResponse {
$line = strtolower(trim(preg_replace('/\s+/', ' ', $line)));
if (!Buttons::allowed($line)) {
return new JSONResponse(['ok' => false, 'reply' => 'not a card command'], Http::STATUS_FORBIDDEN);
}
$url = $this->appConfig->getAppValueString('door_url', '');
$key = $this->appConfig->getAppValueString('door_key', '');
if ($url === '' || $key === '') {
return new JSONResponse(['ok' => false, 'reply' => 'the card has no door yet (occ config:app:set zigbee_card door_url / door_key)'], Http::STATUS_SERVICE_UNAVAILABLE);
}
try {
$resp = $this->clientService->newClient()->post($url, [
'headers' => ['X-Zigbee-Key' => $key, 'Content-Type' => 'application/json'],
'body' => json_encode(['line' => $line, 'who' => (string)$this->userId]),
// a scene is several publishes and read-backs; give the bot time to answer
'timeout' => 60,
// the door is a private address by design; Nextcloud's client refuses those unless told
'nextcloud' => ['allow_local_address' => true],
]);
$data = json_decode((string)$resp->getBody(), true);
if (!is_array($data) || !isset($data['reply'])) {
return new JSONResponse(['ok' => false, 'reply' => 'the door answered nonsense'], Http::STATUS_BAD_GATEWAY);
}
$out = ['ok' => (bool)($data['ok'] ?? false), 'reply' => (string)$data['reply']];
if (isset($data['state']) && is_array($data['state'])) {
$out['state'] = $data['state']; // the "state" read: the tiles' JSON, passed through as-is
}
return new JSONResponse($out);
} catch (\Throwable $e) {
return new JSONResponse(['ok' => false, 'reply' => 'the door did not answer: ' . $e->getMessage()], Http::STATUS_BAD_GATEWAY);
}
}
}