Add support for unreachable (unplugged) Zigbee devices in Philips Hue emulation and Alexa

This commit is contained in:
Stephan Hadinger 2020-03-26 19:34:59 +01:00
parent 7d33480b7b
commit d4a9ed41c9
6 changed files with 102 additions and 57 deletions

View File

@ -6,6 +6,7 @@
- Change GPIO initialization solving possible Relay toggle on (OTA) restart
- Fix Zigbee sending wrong Sat value with Hue emulation
- Add command ``ZbRestore`` to restore device configuration dumped with ``ZbStatus 2``
- Add support for unreachable (unplugged) Zigbee devices in Philips Hue emulation and Alexa
## Released

View File

@ -46,7 +46,7 @@ typedef struct Z_Device {
uint8_t seqNumber;
// Light information for Hue integration integration, last known values
int8_t bulbtype; // number of channel for the bulb: 0-5, or 0xFF if no Hue integration
uint8_t power; // power state (boolean)
uint8_t power; // power state (boolean), MSB (0x80) stands for reachable
uint8_t colormode; // 0x00: Hue/Sat, 0x01: XY, 0x02: CT
uint8_t dimmer; // last Dimmer value: 0-254
uint8_t sat; // last Sat: 0..254
@ -66,12 +66,15 @@ typedef enum Z_Def_Category {
Z_CAT_NONE = 0, // no category, it will happen anyways
Z_CAT_READ_ATTR, // Attribute reporting, either READ_ATTRIBUTE or REPORT_ATTRIBUTE, we coalesce all attributes reported if we can
Z_CAT_VIRTUAL_ATTR, // Creation of a virtual attribute, typically after a time-out. Ex: Aqara presence sensor
Z_CAT_REACHABILITY, // timer set to measure reachability of device, i.e. if we don't get an answer after 1s, it is marked as unreachable (for Alexa)
Z_CAT_READ_0006, // Read 0x0006 cluster
Z_CAT_READ_0008, // Read 0x0008 cluster
Z_CAT_READ_0102, // Read 0x0300 cluster
Z_CAT_READ_0300, // Read 0x0300 cluster
} Z_Def_Category;
const uint32_t Z_CAT_REACHABILITY_TIMEOUT = 1000; // 1000 ms or 1s
typedef struct Z_Deferred {
// below are per device timers, used for example to query the new state of the device
uint32_t timer; // millis() when to fire the timer, 0 if no timer
@ -124,6 +127,7 @@ public:
void setFriendlyName(uint16_t shortaddr, const char * str);
const char * getFriendlyName(uint16_t shortaddr) const;
const char * getModelId(uint16_t shortaddr) const;
void setReachable(uint16_t shortaddr, bool reachable);
// get next sequence number for (increment at each all)
uint8_t getNextSeqNumber(uint16_t shortaddr);
@ -137,15 +141,17 @@ public:
void setHueBulbtype(uint16_t shortaddr, int8_t bulbtype);
int8_t getHueBulbtype(uint16_t shortaddr) const ;
void updateHueState(uint16_t shortaddr,
const uint8_t *power, const uint8_t *colormode,
const bool *power, const uint8_t *colormode,
const uint8_t *dimmer, const uint8_t *sat,
const uint16_t *ct, const uint16_t *hue,
const uint16_t *x, const uint16_t *y);
const uint16_t *x, const uint16_t *y,
const bool *reachable);
bool getHueState(uint16_t shortaddr,
uint8_t *power, uint8_t *colormode,
bool *power, uint8_t *colormode,
uint8_t *dimmer, uint8_t *sat,
uint16_t *ct, uint16_t *hue,
uint16_t *x, uint16_t *y) const ;
uint16_t *x, uint16_t *y,
bool *reachable) const ;
// Timers
void resetTimersForDevice(uint16_t shortaddr, uint16_t groupaddr, uint8_t category);
@ -262,7 +268,7 @@ Z_Device & Z_Devices::createDeviceEntry(uint16_t shortaddr, uint64_t longaddr) {
0, // seqNumber
// Hue support
-1, // no Hue support
0, // power
0x80, // power off + reachable
0, // colormode
0, // dimmer
0, // sat
@ -583,6 +589,12 @@ const char * Z_Devices::getModelId(uint16_t shortaddr) const {
return nullptr;
}
void Z_Devices::setReachable(uint16_t shortaddr, bool reachable) {
Z_Device & device = getShortAddr(shortaddr);
if (&device == nullptr) { return; } // don't crash if not found
bitWrite(device.power, 7, reachable);
}
// get the next sequance number for the device, or use the global seq number if device is unknown
uint8_t Z_Devices::getNextSeqNumber(uint16_t shortaddr) {
int32_t short_found = findShortAddr(shortaddr);
@ -616,12 +628,13 @@ int8_t Z_Devices::getHueBulbtype(uint16_t shortaddr) const {
// Hue support
void Z_Devices::updateHueState(uint16_t shortaddr,
const uint8_t *power, const uint8_t *colormode,
const bool *power, const uint8_t *colormode,
const uint8_t *dimmer, const uint8_t *sat,
const uint16_t *ct, const uint16_t *hue,
const uint16_t *x, const uint16_t *y) {
const uint16_t *x, const uint16_t *y,
const bool *reachable) {
Z_Device &device = getShortAddr(shortaddr);
if (power) { device.power = *power; }
if (power) { bitWrite(device.power, 0, *power); }
if (colormode){ device.colormode = *colormode; }
if (dimmer) { device.dimmer = *dimmer; }
if (sat) { device.sat = *sat; }
@ -629,18 +642,20 @@ void Z_Devices::updateHueState(uint16_t shortaddr,
if (hue) { device.hue = *hue; }
if (x) { device.x = *x; }
if (y) { device.y = *y; }
if (reachable){ bitWrite(device.power, 7, *reachable); }
}
// return true if ok
bool Z_Devices::getHueState(uint16_t shortaddr,
uint8_t *power, uint8_t *colormode,
bool *power, uint8_t *colormode,
uint8_t *dimmer, uint8_t *sat,
uint16_t *ct, uint16_t *hue,
uint16_t *x, uint16_t *y) const {
uint16_t *x, uint16_t *y,
bool *reachable) const {
int32_t found = findShortAddr(shortaddr);
if (found >= 0) {
const Z_Device &device = *(_devices[found]);
if (power) { *power = device.power; }
if (power) { *power = bitRead(device.power, 0); }
if (colormode){ *colormode = device.colormode; }
if (dimmer) { *dimmer = device.dimmer; }
if (sat) { *sat = device.sat; }
@ -648,6 +663,7 @@ bool Z_Devices::getHueState(uint16_t shortaddr,
if (hue) { *hue = device.hue; }
if (x) { *x = device.x; }
if (y) { *y = device.y; }
if (reachable){ *reachable = bitRead(device.power, 7); }
return true;
} else {
return false;
@ -951,7 +967,8 @@ String Z_Devices::dumpLightState(uint16_t shortaddr) const {
dev[F(D_JSON_ZIGBEE_LIGHT)] = device.bulbtype; // sign extend, 0xFF changed as -1
if (0 <= device.bulbtype) {
// bulbtype is defined
dev[F("Power")] = device.power;
dev[F("Power")] = bitRead(device.power, 0);
dev[F("Reachable")] = bitRead(device.power, 7);
if (1 <= device.bulbtype) {
dev[F("Dimmer")] = device.dimmer;
}

View File

@ -24,13 +24,19 @@
// idx: index in the list of zigbee_devices
void HueLightStatus1Zigbee(uint16_t shortaddr, uint8_t local_light_subtype, String *response) {
uint8_t power, colormode, bri, sat;
static const char HUE_LIGHTS_STATUS_JSON1_SUFFIX_ZIGBEE[] PROGMEM =
"%s\"alert\":\"none\","
"\"effect\":\"none\","
"\"reachable\":%s}";
bool power, reachable;
uint8_t colormode, bri, sat;
uint16_t ct, hue;
uint16_t x, y;
String light_status = "";
uint32_t echo_gen = findEchoGeneration(); // 1 for 1st gen =+ Echo Dot 2nd gen, 2 for 2nd gen and above
zigbee_devices.getHueState(shortaddr, &power, &colormode, &bri, &sat, &ct, &hue, &x, &y);
zigbee_devices.getHueState(shortaddr, &power, &colormode, &bri, &sat, &ct, &hue, &x, &y, &reachable);
if (bri > 254) bri = 254; // Philips Hue bri is between 1 and 254
if (bri < 1) bri = 1;
@ -40,7 +46,7 @@ void HueLightStatus1Zigbee(uint16_t shortaddr, uint8_t local_light_subtype, Stri
const size_t buf_size = 256;
char * buf = (char*) malloc(buf_size); // temp buffer for strings, avoid stack
snprintf_P(buf, buf_size, PSTR("{\"on\":%s,"), (power & 1) ? "true" : "false");
snprintf_P(buf, buf_size, PSTR("{\"on\":%s,"), power ? "true" : "false");
// Brightness for all devices with PWM
if ((1 == echo_gen) || (LST_SINGLE <= local_light_subtype)) { // force dimmer for 1st gen Echo
snprintf_P(buf, buf_size, PSTR("%s\"bri\":%d,"), buf, bri);
@ -61,7 +67,7 @@ void HueLightStatus1Zigbee(uint16_t shortaddr, uint8_t local_light_subtype, Stri
if (LST_COLDWARM == local_light_subtype || LST_RGBW <= local_light_subtype) { // white temp
snprintf_P(buf, buf_size, PSTR("%s\"ct\":%d,"), buf, ct > 0 ? ct : 284);
}
snprintf_P(buf, buf_size, HUE_LIGHTS_STATUS_JSON1_SUFFIX, buf);
snprintf_P(buf, buf_size, HUE_LIGHTS_STATUS_JSON1_SUFFIX_ZIGBEE, buf, reachable ? "true" : "false");
*response += buf;
free(buf);
@ -123,9 +129,9 @@ void ZigbeeHueGroups(String * lights) {
// Send commands
// Power On/Off
void ZigbeeHuePower(uint16_t shortaddr, uint8_t power) {
zigbeeZCLSendStr(shortaddr, 0, 0, true, 0x0006, power, "");
zigbee_devices.updateHueState(shortaddr, &power, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr);
void ZigbeeHuePower(uint16_t shortaddr, bool power) {
zigbeeZCLSendStr(shortaddr, 0, 0, true, 0x0006, power ? 1 : 0, "");
zigbee_devices.updateHueState(shortaddr, &power, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr);
}
// Dimmer
@ -134,7 +140,7 @@ void ZigbeeHueDimmer(uint16_t shortaddr, uint8_t dimmer) {
char param[8];
snprintf_P(param, sizeof(param), PSTR("%02X0A00"), dimmer);
zigbeeZCLSendStr(shortaddr, 0, 0, true, 0x0008, 0x04, param);
zigbee_devices.updateHueState(shortaddr, nullptr, nullptr, &dimmer, nullptr, nullptr, nullptr, nullptr, nullptr);
zigbee_devices.updateHueState(shortaddr, nullptr, nullptr, &dimmer, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr);
}
// CT
@ -145,7 +151,7 @@ void ZigbeeHueCT(uint16_t shortaddr, uint16_t ct) {
snprintf_P(param, sizeof(param), PSTR("%02X%02X0A00"), ct & 0xFF, ct >> 8);
uint8_t colormode = 2; // "ct"
zigbeeZCLSendStr(shortaddr, 0, 0, true, 0x0300, 0x0A, param);
zigbee_devices.updateHueState(shortaddr, nullptr, &colormode, nullptr, nullptr, &ct, nullptr, nullptr, nullptr);
zigbee_devices.updateHueState(shortaddr, nullptr, &colormode, nullptr, nullptr, &ct, nullptr, nullptr, nullptr, nullptr);
}
// XY
@ -156,7 +162,7 @@ void ZigbeeHueXY(uint16_t shortaddr, uint16_t x, uint16_t y) {
snprintf_P(param, sizeof(param), PSTR("%02X%02X%02X%02X0A00"), x & 0xFF, x >> 8, y & 0xFF, y >> 8);
uint8_t colormode = 1; // "xy"
zigbeeZCLSendStr(shortaddr, 0, 0, true, 0x0300, 0x07, param);
zigbee_devices.updateHueState(shortaddr, nullptr, &colormode, nullptr, nullptr, nullptr, nullptr, &x, &y);
zigbee_devices.updateHueState(shortaddr, nullptr, &colormode, nullptr, nullptr, nullptr, nullptr, &x, &y, nullptr);
}
// HueSat
@ -167,7 +173,7 @@ void ZigbeeHueHS(uint16_t shortaddr, uint16_t hue, uint8_t sat) {
snprintf_P(param, sizeof(param), PSTR("%02X%02X0000"), hue8, sat);
uint8_t colormode = 0; // "hs"
zigbeeZCLSendStr(shortaddr, 0, 0, true, 0x0300, 0x06, param);
zigbee_devices.updateHueState(shortaddr, nullptr, &colormode, nullptr, &sat, nullptr, &hue, nullptr, nullptr);
zigbee_devices.updateHueState(shortaddr, nullptr, &colormode, nullptr, &sat, nullptr, &hue, nullptr, nullptr, nullptr);
}
void ZigbeeHandleHue(uint16_t shortaddr, uint32_t device_id, String &response) {

View File

@ -1215,38 +1215,38 @@ void ZCLFrame::postProcessAttributes(uint16_t shortaddr, JsonObject& json) {
// see if we need to update the Hue bulb status
if ((cluster == 0x0006) && ((attribute == 0x0000) || (attribute == 0x8000))) {
uint8_t power = value;
bool power = value;
zigbee_devices.updateHueState(shortaddr, &power, nullptr, nullptr, nullptr,
nullptr, nullptr, nullptr, nullptr);
nullptr, nullptr, nullptr, nullptr, nullptr);
} else if ((cluster == 0x0008) && (attribute == 0x0000)) {
uint8_t dimmer = value;
zigbee_devices.updateHueState(shortaddr, nullptr, nullptr, &dimmer, nullptr,
nullptr, nullptr, nullptr, nullptr);
nullptr, nullptr, nullptr, nullptr, nullptr);
} else if ((cluster == 0x0300) && (attribute == 0x0000)) {
uint16_t hue8 = value;
uint16_t hue = changeUIntScale(hue8, 0, 254, 0, 360); // change range from 0..254 to 0..360
zigbee_devices.updateHueState(shortaddr, nullptr, nullptr, nullptr, nullptr,
nullptr, &hue, nullptr, nullptr);
nullptr, &hue, nullptr, nullptr, nullptr);
} else if ((cluster == 0x0300) && (attribute == 0x0001)) {
uint8_t sat = value;
zigbee_devices.updateHueState(shortaddr, nullptr, nullptr, nullptr, &sat,
nullptr, nullptr, nullptr, nullptr);
nullptr, nullptr, nullptr, nullptr, nullptr);
} else if ((cluster == 0x0300) && (attribute == 0x0003)) {
uint16_t x = value;
zigbee_devices.updateHueState(shortaddr, nullptr, nullptr, nullptr, nullptr,
nullptr, nullptr, &x, nullptr);
nullptr, nullptr, &x, nullptr, nullptr);
} else if ((cluster == 0x0300) && (attribute == 0x0004)) {
uint16_t y = value;
zigbee_devices.updateHueState(shortaddr, nullptr, nullptr, nullptr, nullptr,
nullptr, nullptr, nullptr, &y);
nullptr, nullptr, nullptr, &y, nullptr), nullptr;
} else if ((cluster == 0x0300) && (attribute == 0x0007)) {
uint16_t ct = value;
zigbee_devices.updateHueState(shortaddr, nullptr, nullptr, nullptr, nullptr,
&ct, nullptr, nullptr, nullptr);
&ct, nullptr, nullptr, nullptr, nullptr);
} else if ((cluster == 0x0300) && (attribute == 0x0008)) {
uint8_t colormode = value;
zigbee_devices.updateHueState(shortaddr, nullptr, &colormode, nullptr, nullptr,
nullptr, nullptr, nullptr, nullptr);
nullptr, nullptr, nullptr, nullptr, nullptr);
}
// Iterate on filter

View File

@ -173,6 +173,14 @@ int32_t Z_ReadAttrCallback(uint16_t shortaddr, uint16_t groupaddr, uint16_t clus
}
}
// This callback is registered after a an attribute read command was made to a light, and fires if we don't get any response after 1000 ms
int32_t Z_Unreachable(uint16_t shortaddr, uint16_t groupaddr, uint16_t cluster, uint8_t endpoint, uint32_t value) {
if (shortaddr) {
zigbee_devices.setReachable(shortaddr, false); // mark device as reachable
}
}
// set a timer to read back the value in the future
void zigbeeSetCommandTimer(uint16_t shortaddr, uint16_t groupaddr, uint16_t cluster, uint8_t endpoint) {
uint32_t wait_ms = 0;
@ -192,6 +200,9 @@ void zigbeeSetCommandTimer(uint16_t shortaddr, uint16_t groupaddr, uint16_t clus
}
if (wait_ms) {
zigbee_devices.setTimer(shortaddr, groupaddr, wait_ms, cluster, endpoint, Z_CAT_NONE, 0 /* value */, &Z_ReadAttrCallback);
if (shortaddr) { // reachability test is not possible for group addresses, since we don't know the list of devices in the group
zigbee_devices.setTimer(shortaddr, groupaddr, wait_ms + Z_CAT_REACHABILITY_TIMEOUT, cluster, endpoint, Z_CAT_REACHABILITY, 0 /* value */, &Z_Unreachable);
}
}
}
@ -300,6 +311,10 @@ void sendHueUpdate(uint16_t shortaddr, uint16_t groupaddr, uint16_t cluster, uin
}
if ((endpoint) || (groupaddr)) { // send only if we know the endpoint
zigbee_devices.setTimer(shortaddr, groupaddr, wait_ms, cluster, endpoint, z_cat, 0 /* value */, &Z_ReadAttrCallback);
if (shortaddr) { // reachability test is not possible for group addresses, since we don't know the list of devices in the group
zigbee_devices.setTimer(shortaddr, groupaddr, wait_ms + Z_CAT_REACHABILITY_TIMEOUT, cluster, endpoint, Z_CAT_REACHABILITY, 0 /* value */, &Z_Unreachable);
}
}
}
}

View File

@ -308,6 +308,7 @@ int32_t Z_DataConfirm(int32_t res, const class SBuffer &buf) {
//
// Handle Receive End Device Announce incoming message
// This message is also received when a previously paired device is powered up
// Send back Active Ep Req message
//
int32_t Z_ReceiveEndDeviceAnnonce(int32_t res, const class SBuffer &buf) {
@ -328,6 +329,9 @@ int32_t Z_ReceiveEndDeviceAnnonce(int32_t res, const class SBuffer &buf) {
(capabilities & 0x08) ? "true" : "false",
(capabilities & 0x40) ? "true" : "false"
);
// query the state of the bulb (for Alexa)
uint32_t wait_ms = 2000; // wait for 2s
Z_Query_Bulb(nwkAddr, wait_ms);
MqttPublishPrefixTopic_P(RESULT_OR_TELE, PSTR(D_JSON_ZIGBEEZCL_RECEIVED));
XdrvRulesProcess();
@ -513,6 +517,10 @@ int32_t Z_ReceiveAfIncomingMessage(int32_t res, const class SBuffer &buf) {
// Add linkquality
json[F(D_CMND_ZIGBEE_LINKQUALITY)] = linkquality;
// since we just receveived data from the device, it is reachable
zigbee_devices.resetTimersForDevice(srcaddr, 0 /* groupaddr */, Z_CAT_REACHABILITY); // remove any reachability timer already there
zigbee_devices.setReachable(srcaddr, true); // mark device as reachable
if (defer_attributes) {
// Prepare for publish
if (zigbee_devices.jsonIsConflict(srcaddr, json)) {
@ -591,38 +599,36 @@ int32_t Z_Load_Devices(uint8_t value) {
return 0; // continue
}
//
// Query the state of a bulb (light) if its type allows it
//
void Z_Query_Bulb(uint16_t shortaddr, uint32_t &wait_ms) {
const uint32_t inter_message_ms = 100; // wait 100ms between messages
if (0 <= zigbee_devices.getHueBulbtype(shortaddr)) {
uint8_t endpoint = zigbee_devices.findFirstEndpoint(shortaddr);
if (endpoint) { // send only if we know the endpoint
zigbee_devices.setTimer(shortaddr, 0 /* groupaddr */, wait_ms, 0x0006, endpoint, Z_CAT_NONE, 0 /* value */, &Z_ReadAttrCallback);
wait_ms += inter_message_ms;
zigbee_devices.setTimer(shortaddr, 0 /* groupaddr */, wait_ms, 0x0008, endpoint, Z_CAT_NONE, 0 /* value */, &Z_ReadAttrCallback);
wait_ms += inter_message_ms;
zigbee_devices.setTimer(shortaddr, 0 /* groupaddr */, wait_ms, 0x0300, endpoint, Z_CAT_NONE, 0 /* value */, &Z_ReadAttrCallback);
wait_ms += inter_message_ms;
zigbee_devices.setTimer(shortaddr, 0, wait_ms + Z_CAT_REACHABILITY_TIMEOUT, 0, endpoint, Z_CAT_REACHABILITY, 0 /* value */, &Z_Unreachable);
}
}
}
//
// Send messages to query the state of each Hue emulated light
//
int32_t Z_Query_Bulbs(uint8_t value) {
// Scan all devices and send deferred requests to know the state of bulbs
uint32_t wait_ms = 1000; // start with 1.0 s delay
const uint32_t inter_message_ms = 100; // wait 100ms between messages
for (uint32_t i = 0; i < zigbee_devices.devicesSize(); i++) {
const Z_Device &device = zigbee_devices.devicesAt(i);
if (0 <= device.bulbtype) {
uint16_t cluster;
uint8_t endpoint = zigbee_devices.findFirstEndpoint(device.shortaddr);
cluster = 0x0006;
if (endpoint) { // send only if we know the endpoint
zigbee_devices.setTimer(device.shortaddr, 0 /* groupaddr */, wait_ms, cluster, endpoint, Z_CAT_NONE, 0 /* value */, &Z_ReadAttrCallback);
wait_ms += inter_message_ms;
}
cluster = 0x0008;
if (endpoint) { // send only if we know the endpoint
zigbee_devices.setTimer(device.shortaddr, 0 /* groupaddr */, wait_ms, cluster, endpoint, Z_CAT_NONE, 0 /* value */, &Z_ReadAttrCallback);
wait_ms += inter_message_ms;
}
cluster = 0x0300;
if (endpoint) { // send only if we know the endpoint
zigbee_devices.setTimer(device.shortaddr, 0 /* groupaddr */, wait_ms, cluster, endpoint, Z_CAT_NONE, 0 /* value */, &Z_ReadAttrCallback);
wait_ms += inter_message_ms;
}
}
Z_Query_Bulb(device.shortaddr, wait_ms);
}
return 0; // continue
}