/* * zigbee_client — room temperatures from the Zigbee sensors. * * Named for its role rather than its transport. The PRD assumed HTTP polling * of the Zigbee hub, but zigbee2mqtt has no REST endpoint that returns live * sensor state — its frontend streams state over a WebSocket, and /api/devices * returns definitions, not readings. So this subscribes to the MQTT broker * that z2m publishes to instead. * * That turns out to be the better shape for this application anyway: * * - z2m publishes sensor topics *retained*, so every room's last known * reading arrives within milliseconds of connecting, including after a * reboot. An HTTP poller would have had the same property; a WebSocket * client would not, and could have waited an hour for a battery sensor to * report in. * - It bypasses z2m's frontend entirely. Readings keep flowing if the * frontend is down, and there is no HTTP shim to become a new single point * of failure. * - Updates are pushed, so readings are as fresh as the sensors allow rather * than as fresh as a poll interval. * * A failed connection never clears previously good readings — it only lets * them age, per PRD 4.1. Staleness is the control loop's business, not this * module's. */ #pragma once #include #include #include "app_config.h" #include "esp_err.h" /* Room count, derived from APP_ROOM_TABLE in app_config.h. */ #define ZB_ROOM_COUNT_HELPER(name, topic) +1 #define ROOM_COUNT (0 APP_ROOM_TABLE(ZB_ROOM_COUNT_HELPER)) typedef struct { bool valid; /* a reading has been received since boot */ float temperature_c; int64_t received_us; /* local receipt time, esp_timer clock */ bool has_battery; int battery_percent; /* * z2m's own last_seen, when the sensor reports it, as seconds before * receipt. Sensor-side age is more honest than our receipt time for a * retained message replayed on connect: a retained reading can be hours * old and still arrive "just now". */ bool has_sensor_age; uint32_t sensor_age_at_receipt_s; } room_reading_t; esp_err_t zigbee_client_init(void); int zigbee_client_room_count(void); const char *zigbee_client_room_name(int idx); /* Copy out a room's latest reading. False if idx is out of range. */ bool zigbee_client_get_reading(int idx, room_reading_t *out); /* * Age of a room's reading in seconds, UINT32_MAX if never received. * * Includes the sensor-side age at receipt when z2m provided last_seen, so a * retained message replayed on connect is correctly reported as old rather * than as brand new. */ uint32_t zigbee_client_reading_age_s(int idx); /* True while the MQTT session is up. Purely informational — the control loop * reasons about reading age, not about connection state. */ bool zigbee_client_is_connected(void); /* Lowest battery percentage across rooms that report one, or -1. */ int zigbee_client_lowest_battery(void);