Merge pull request #9220 from DigitalAlchemist/zigbee_last_seen

Add last seen time to zigbee devices
This commit is contained in:
Theo Arends 2020-09-18 09:14:04 +02:00 committed by GitHub
commit a6683900dd
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 113 additions and 36 deletions

View File

@ -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;
}

View File

@ -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);

View File

@ -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("</table>{t}")); // Terminate current two column table and open new table
WSContentSend_P(PSTR("<style>.bx{height:14px;width:14px;display:inline-block;border:1px solid currentColor;background-color:var(--cl,#fff)}</style>"));
WSContentSend_P(PSTR(
"<style>"
// Table CSS
".ztd td:not(:first-child){width:20px;font-size:70%%}"
".ztd td:last-child{width:45px}"
".ztd .bt{margin-right:10px;}" // Margin right should be half of the not-first width
// Lighting
".bx{height:14px;width:14px;display:inline-block;border:1px solid currentColor;background-color:var(--cl,#fff)}"
// Signal Strength Indicator
".ssi{display:inline-flex;align-items:flex-end;height:15px;padding:0}"
".ssi i{width:3px;margin-right:1px;border-radius:3px;background-color:#eee}"
".ssi .b0{height:25%%}.ssi .b1{height:50%%}.ssi .b2{height:75%%}.ssi .b3{height:100%%}.o30{opacity:.3}"
"</style>"
));
// 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("&nbsp;"));
if (device.validBatteryPercent()) {
snprintf_P(sbatt, sizeof(sbatt), PSTR("<i class=\"bt\" title=\"%d%%\" style=\"--bl:%dpx\"></i>"), device.batterypercent, changeUIntScale(device.batterypercent, 0, 100, 0, 14));
}
if (!i) { // First row needs style info
WSContentSend_PD(PSTR("<tr><td><b>%s</b></td><td style='width:%dpx'>%s</td><td style='width:%dpx'>" 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("<tr><td><b>%s</b></td><td>%s</td><td>" 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("&nbsp;"));
if (device.validBatteryPercent()) {
snprintf_P(sbatt, sizeof(sbatt),
PSTR("<i class=\"bt\" title=\"%d%%\" style=\"--bl:%dpx\"></i>"),
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(
"<tr class='ztd'>"
"<td><b>%s</b></td>" // name
"<td>%s</td>" // sbatt (Battery Indicator)
"<td><div title='" D_LQI " %s' class='ssi'>" // slqi
), name, sbatt, slqi);
if(device.validLqi()) {
for(uint32_t j = 0; j < 4; ++j) {
WSContentSend_PD(PSTR("<i class='b%d%s'></i>"), j, (num_bars < j) ? PSTR(" o30") : PSTR(""));
}
}
char dhm[16]; // len("&#x1F557;" + "49710d" + '\0') == 16
snprintf_P(dhm, sizeof(dhm), PSTR("&nbsp;"));
if(device.validLastSeen()){
snprintf_P(dhm, sizeof(dhm), PSTR("&#x1F557;"));
convert_seconds_to_dhm(now - device.last_seen, &dhm[9], 7);
}
WSContentSend_PD(PSTR(
"</div></td>" // Close LQI
"<td>%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("<tr><td colspan=\"3\">&#9478;"));
WSContentSend_P(PSTR("<tr><td colspan=\"4\">&#9478;"));
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(" &#x26C5; %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("<tr><td colspan=\"3\">&#9478; %s"), device.getPower() ? PSTR(D_ON) : PSTR(D_OFF));
WSContentSend_P(PSTR("<tr><td colspan=\"4\">&#9478; %s"), device.getPower() ? PSTR(D_ON) : PSTR(D_OFF));
if (device.validDimmer() && (channels >= 1)) {
WSContentSend_P(PSTR(" &#128261; %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}"));
}
}