From 74bfad29e19ed48a9a75516cdbc0f8b0c0ece83f Mon Sep 17 00:00:00 2001 From: Justin Monroe Date: Thu, 3 Sep 2020 02:16:13 +0000 Subject: [PATCH] Add last seen time to zigbee devices - Adding last_seen to zigbee devices, and updates to UI to show - Adding signal strength icon for LQI, instead of just the number --- tasmota/xdrv_23_zigbee_2_devices.ino | 12 ++- tasmota/xdrv_23_zigbee_8_parsers.ino | 4 +- tasmota/xdrv_23_zigbee_A_impl.ino | 133 ++++++++++++++++++++------- 3 files changed, 113 insertions(+), 36 deletions(-) diff --git a/tasmota/xdrv_23_zigbee_2_devices.ino b/tasmota/xdrv_23_zigbee_2_devices.ino index dbc459e38..82000b2e1 100644 --- a/tasmota/xdrv_23_zigbee_2_devices.ino +++ b/tasmota/xdrv_23_zigbee_2_devices.ino @@ -75,6 +75,7 @@ public: // powe plug data uint16_t mains_voltage; // AC voltage int16_t mains_power; // Active power + uint32_t last_seen; // Last seen time (epoch) // Constructor with all defaults Z_Device(uint16_t _shortaddr = BAD_SHORTADDR, uint64_t _longaddr = 0x00): @@ -103,7 +104,8 @@ public: pressure(0xFFFF), humidity(0xFF), mains_voltage(0xFFFF), - mains_power(-0x8000) + mains_power(-0x8000), + last_seen(0) { }; inline bool valid(void) const { return BAD_SHORTADDR != shortaddr; } // is the device known, valid and found? @@ -128,6 +130,7 @@ public: inline bool validTemperature(void) const { return -0x8000 != temperature; } inline bool validPressure(void) const { return 0xFFFF != pressure; } inline bool validHumidity(void) const { return 0xFF != humidity; } + inline bool validLastSeen(void) const { return 0x0 != last_seen; } inline bool validMainsVoltage(void) const { return 0xFFFF != mains_voltage; } inline bool validMainsPower(void) const { return -0x8000 != mains_power; } @@ -247,6 +250,7 @@ public: void setReachable(uint16_t shortaddr, bool reachable); void setLQI(uint16_t shortaddr, uint8_t lqi); + void setLastSeenNow(uint16_t shortaddr); // uint8_t getLQI(uint16_t shortaddr) const; void setBatteryPercent(uint16_t shortaddr, uint8_t bp); uint8_t getBatteryPercent(uint16_t shortaddr) const; @@ -630,6 +634,12 @@ void Z_Devices::setLQI(uint16_t shortaddr, uint8_t lqi) { getShortAddr(shortaddr).lqi = lqi; } +void Z_Devices::setLastSeenNow(uint16_t shortaddr) { + if (shortaddr == localShortAddr) { return; } + getShortAddr(shortaddr).last_seen= Rtc.utc_time; +} + + void Z_Devices::setBatteryPercent(uint16_t shortaddr, uint8_t bp) { getShortAddr(shortaddr).batterypercent = bp; } diff --git a/tasmota/xdrv_23_zigbee_8_parsers.ino b/tasmota/xdrv_23_zigbee_8_parsers.ino index c761cabb6..27eb14801 100644 --- a/tasmota/xdrv_23_zigbee_8_parsers.ino +++ b/tasmota/xdrv_23_zigbee_8_parsers.ino @@ -1348,7 +1348,8 @@ void Z_IncomingMessage(class ZCLFrame &zcl_received) { // log the packet details zcl_received.log(); - zigbee_devices.setLQI(srcaddr, linkquality); // EFR32 has a different scale for LQI + zigbee_devices.setLQI(srcaddr, linkquality != 0xFF ? linkquality : 0xFE); // EFR32 has a different scale for LQI + zigbee_devices.setLastSeenNow(srcaddr); char shortaddr[8]; snprintf_P(shortaddr, sizeof(shortaddr), PSTR("0x%04X"), srcaddr); @@ -1492,6 +1493,7 @@ int32_t EZ_IncomingMessage(int32_t res, const class SBuffer &buf) { // ZDO request // Report LQI zigbee_devices.setLQI(srcaddr, linkquality); + zigbee_devices.setLastSeenNow(srcaddr); // Since ZDO messages start with a sequence number, we skip it // but we add the source address in the last 2 bytes SBuffer zdo_buf(buf.get8(20) - 1 + 2); diff --git a/tasmota/xdrv_23_zigbee_A_impl.ino b/tasmota/xdrv_23_zigbee_A_impl.ino index 5ca3b555e..9871f628e 100644 --- a/tasmota/xdrv_23_zigbee_A_impl.ino +++ b/tasmota/xdrv_23_zigbee_A_impl.ino @@ -1350,8 +1350,37 @@ extern "C" { return 1; } } -} + +// Convert seconds to a string representing days, hours or minutes present in the n-value. +// The string will contain the most coarse time only, rounded down (61m == 01h, 01h37m == 01h). +// Inputs: +// - n: uint32_t representing some number of seconds +// - result: a buffer of suitable size (7 bytes would represent the entire solution space +// for UINT32_MAX including the trailing null-byte, or "49710d") +// - result_len: A numeric value representing the total length of the result buffer +// Returns: +// - The number of characters that would have been written were result sufficiently large +// - negatve number on encoding error from snprintf +// + int convert_seconds_to_dhm(uint32_t n, char *result, size_t result_len){ + char fmtstr[] = "%02dmhd"; // Don't want this in progmem, because we mutate it. + uint32_t conversions[3] = {24 * 3600, 3600, 60}; + uint32_t value; + for(int i = 0; i < 3; ++i) { + value = n / conversions[i]; + if(value > 0) { + fmtstr[4] = fmtstr[6-i]; + break; + } + n = n % conversions[i]; + } + + // Null-terminate the string at the last "valid" index, removing any excess zero values. + fmtstr[5] = '\0'; + return snprintf(result, result_len, fmtstr, value); + } +} void ZigbeeShow(bool json) { if (json) { @@ -1362,12 +1391,21 @@ void ZigbeeShow(bool json) if (!zigbee_num) { return; } if (zigbee_num > 255) { zigbee_num = 255; } - // Calculate fixed column width for best visual result (Theos opinion) - const uint8_t px_batt = 30; // Battery icon is 20px, add 10px as separator - const uint8_t px_lqi = (strlen(D_LQI) + 4) * 10; // LQI 254 = 70px - WSContentSend_P(PSTR("{t}")); // Terminate current two column table and open new table - WSContentSend_P(PSTR("")); + WSContentSend_P(PSTR( + "" + )); // sort elements by name, then by id uint8_t sorted_idx[zigbee_num]; @@ -1376,44 +1414,69 @@ void ZigbeeShow(bool json) } qsort(sorted_idx, zigbee_num, sizeof(sorted_idx[0]), device_cmp); + uint32_t now = Rtc.utc_time; + for (uint32_t i = 0; i < zigbee_num; i++) { const Z_Device &device = zigbee_devices.devicesAt(sorted_idx[i]); uint16_t shortaddr = device.shortaddr; - { // exxplicit scope to free up stack allocated strings - char *name = (char*) device.friendlyName; - char sdevice[33]; - if (nullptr == name) { - snprintf_P(sdevice, sizeof(sdevice), PSTR(D_DEVICE " 0x%04X"), shortaddr); - name = sdevice; - } + char *name = (char*) device.friendlyName; - char slqi[8]; - snprintf_P(slqi, sizeof(slqi), PSTR("-")); - if (device.validLqi()) { - snprintf_P(slqi, sizeof(slqi), PSTR("%d"), device.lqi); - } - - char sbatt[64]; - snprintf_P(sbatt, sizeof(sbatt), PSTR(" ")); - if (device.validBatteryPercent()) { - snprintf_P(sbatt, sizeof(sbatt), PSTR(""), device.batterypercent, changeUIntScale(device.batterypercent, 0, 100, 0, 14)); - } - - if (!i) { // First row needs style info - WSContentSend_PD(PSTR("%s%s" D_LQI " %s{e}"), - name, px_batt, sbatt, px_lqi, slqi); - } else { // Following rows don't need style info so reducing ajax package - WSContentSend_PD(PSTR("%s%s" D_LQI " %s{e}"), name, sbatt, slqi); - } + char sdevice[33]; + if (nullptr == name) { + snprintf_P(sdevice, sizeof(sdevice), PSTR(D_DEVICE " 0x%04X"), shortaddr); + name = sdevice; } - // Sensor + char sbatt[64]; + snprintf_P(sbatt, sizeof(sbatt), PSTR(" ")); + if (device.validBatteryPercent()) { + snprintf_P(sbatt, sizeof(sbatt), + PSTR(""), + device.batterypercent, changeUIntScale(device.batterypercent, 0, 100, 0, 14) + ); + } + + uint32_t num_bars = 0; + + char slqi[4]; + slqi[0] = '-'; + slqi[1] = '\0'; + if (device.validLqi()){ + num_bars = changeUIntScale(device.lqi, 0, 254, 0, 4); + snprintf_P(slqi, sizeof(slqi), PSTR("%d"), device.lqi); + } + + WSContentSend_PD(PSTR( + "" + "%s" // name + "%s" // sbatt (Battery Indicator) + "
" // slqi + ), name, sbatt, slqi); + + if(device.validLqi()) { + for(uint32_t j = 0; j < 4; ++j) { + WSContentSend_PD(PSTR(""), j, (num_bars < j) ? PSTR(" o30") : PSTR("")); + } + } + char dhm[16]; // len("🕗" + "49710d" + '\0') == 16 + snprintf_P(dhm, sizeof(dhm), PSTR(" ")); + if(device.validLastSeen()){ + snprintf_P(dhm, sizeof(dhm), PSTR("🕗")); + convert_seconds_to_dhm(now - device.last_seen, &dhm[9], 7); + } + + WSContentSend_PD(PSTR( + "
" // Close LQI + "%s{e}" // dhm (Last Seen) + ), dhm ); + + // Sensors bool temperature_ok = device.validTemperature(); bool humidity_ok = device.validHumidity(); bool pressure_ok = device.validPressure(); if (temperature_ok || humidity_ok || pressure_ok) { - WSContentSend_P(PSTR("┆")); + WSContentSend_P(PSTR("┆")); if (temperature_ok) { char buf[12]; dtostrf(device.temperature / 10.0f, 3, 1, buf); @@ -1425,6 +1488,7 @@ void ZigbeeShow(bool json) if (pressure_ok) { WSContentSend_P(PSTR(" ⛅ %d hPa"), device.pressure); } + WSContentSend_P(PSTR("{e}")); } @@ -1433,7 +1497,7 @@ void ZigbeeShow(bool json) if (power_ok) { uint8_t channels = device.getLightChannels(); if (0xFF == channels) { channels = 5; } // if number of channel is unknown, display all known attributes - WSContentSend_P(PSTR("┆ %s"), device.getPower() ? PSTR(D_ON) : PSTR(D_OFF)); + WSContentSend_P(PSTR("┆ %s"), device.getPower() ? PSTR(D_ON) : PSTR(D_OFF)); if (device.validDimmer() && (channels >= 1)) { WSContentSend_P(PSTR(" 🔅 %d%%"), changeUIntScale(device.dimmer,0,254,0,100)); } @@ -1460,6 +1524,7 @@ void ZigbeeShow(bool json) WSContentSend_P(PSTR(" %dW"), device.mains_power); } } + WSContentSend_P(PSTR("{e}")); } }