From 2eb56b77a2db0e9462394196c45b373742043561 Mon Sep 17 00:00:00 2001 From: Theo Arends <11044339+arendst@users.noreply.github.com> Date: Thu, 29 May 2025 17:31:21 +0200 Subject: [PATCH] Change LoRaWan GUI sensor representation --- tasmota/berry/lorawan/decoders/LwDecode.be | 82 ++++++++++++++++++- .../lorawan/decoders/vendors/dragino/LDS02.be | 34 ++++++-- .../lorawan/decoders/vendors/dragino/LHT52.be | 58 +++++++++---- .../lorawan/decoders/vendors/dragino/LHT65.be | 77 +++++++++++------ .../lorawan/decoders/vendors/merryiot/DW10.be | 42 +++++++--- .../xdrv_01_9_webserver.ino | 9 +- 6 files changed, 233 insertions(+), 69 deletions(-) diff --git a/tasmota/berry/lorawan/decoders/LwDecode.be b/tasmota/berry/lorawan/decoders/LwDecode.be index 923067744..fa7c8216a 100644 --- a/tasmota/berry/lorawan/decoders/LwDecode.be +++ b/tasmota/berry/lorawan/decoders/LwDecode.be @@ -5,6 +5,7 @@ var LwRegions = ["EU868", "US915", "IN865","AU915","KZ865","RU864","AS923", "AS9 var LwDeco import mqtt +import string class lwdecode_cls var thisDevice @@ -27,6 +28,7 @@ class lwdecode_cls var deviceData = data['LwReceived'] var deviceName = deviceData.keys()() var Node = deviceData[deviceName]['Node'] + var RSSI = deviceData[deviceName]['RSSI'] var Payload = deviceData[deviceName]['Payload'] var FPort = deviceData[deviceName]['FPort'] var decoder = deviceData[deviceName].find('Decoder') @@ -44,7 +46,7 @@ class lwdecode_cls if Payload.size() && self.LwDecoders.find(decoder) var topic = "tele/" + self.thisDevice + "/SENSOR" - var decoded = self.LwDecoders[decoder].decodeUplink(Node, FPort, Payload) + var decoded = self.LwDecoders[decoder].decodeUplink(Node, RSSI, FPort, Payload) var mqttData = {"LwDecoded":{deviceName:decoded}} mqtt.publish(topic, json.dump(mqttData)) end @@ -52,6 +54,61 @@ class lwdecode_cls return true #processed end + def dhm(last_time) + var since = tasmota.rtc('local') - last_time + var unit = "d" + if since > (24 * 3600) + since /= (24 * 3600) + if since > 99 + since = 99 + end + elif since > 3600 + since /= 3600 + unit = "h" + else + since /= 60 + unit = "m" + end + return string.format("%02d%s", since, unit) + end + + def header(name, name_tooltip, battery, battery_last_seen, rssi, last_seen) + var color_text = f'{tasmota.webcolor(0 #-COL_TEXT-#)}' # '#eaeaea' + var msg = string.format("") # ==== Start first table row + msg += string.format("%s", name_tooltip, name) + if (battery < 1000) + # Battery low <= 2.5V (0%), high >= 3.1V (100%) + var batt_percent = (battery * 1000) - 2500 + batt_percent /= 6 # 3.1V - 2.5V = 0.6V = 100% + if batt_percent < 0 + batt_percent = 0 + end + if batt_percent > 100 + batt_percent = 100 + end + batt_percent /= 7.14 # 1..14px showing battery load + msg += string.format("", + battery, self.dhm(battery_last_seen), batt_percent, color_text) + else + msg += " " + end + if rssi < 1000 + var num_bars = 4 - ((rssi * -1) / 33) + msg += string.format("
",rssi) + for j:0..3 + msg += string.format("", + j, (num_bars < j) ? " o30" : "") # Bars + end + msg += string.format("
") # Close RSSI + else + msg += " " + end + msg += string.format("🕗%s", # Clock + color_text, self.dhm(last_seen)) + msg += "" # ==== End first table row + return msg + end #sensor() + #------------------------------------------------------------ Display sensor value in the web UI and react to button Called every WebRefresh time @@ -61,16 +118,35 @@ class lwdecode_cls var msg = "" for decoder: self.LwDecoders - msg = msg + decoder.add_web_sensor() + msg += decoder.add_web_sensor() end if msg - tasmota.web_send_decimal(msg) + var color_text = f'{tasmota.webcolor(0 #-COL_TEXT-#)}' # '#eaeaea' + var full_msg = string.format("".. # Terminate current two column table and open new table + "".. + "{t}", # Open new table + color_text) + full_msg += msg + full_msg += "{t}" # Close table and open new table + + tasmota.web_send_decimal(full_msg) end end end lwdecode = lwdecode_cls() +tasmota.cmd('LoraOption3 off') # Disable embedded decoding tasmota.cmd('SetOption100 off') # Keep LwReceived in JSON message tasmota.cmd('SetOption118 off') # Keep SENSOR as subtopic name tasmota.cmd('SetOption119 off') # Keep device address in JSON message diff --git a/tasmota/berry/lorawan/decoders/vendors/dragino/LDS02.be b/tasmota/berry/lorawan/decoders/vendors/dragino/LDS02.be index df2b7fed9..6957b1c0f 100644 --- a/tasmota/berry/lorawan/decoders/vendors/dragino/LDS02.be +++ b/tasmota/berry/lorawan/decoders/vendors/dragino/LDS02.be @@ -9,22 +9,28 @@ import string global.lds02Nodes = {} class LwDecoLDS02 - static def decodeUplink(Node, FPort, Bytes) + static def decodeUplink(Node, RSSI, FPort, Bytes) var data = {"Device":"Dragino LDS02"} data.insert("Node", Node) var valid_values = false + var last_seen + var battery_last_seen + var battery + var rssi = RSSI var door_open ## SENSOR DATA ## if 10 == FPort && Bytes.size() == 10 + last_seen = tasmota.rtc('local') door_open = ( Bytes[0] & 0x80 ) ? 1 : 0 data.insert("DoorOpen", ( door_open ) ? true : false) - data.insert("Battery_mV", ( Bytes[1] | (Bytes[0] << 8) & 0x3FFF )) + data.insert("BattV", ( Bytes[1] | (Bytes[0] << 8) & 0x3FFF ) / 1000.0) + battery_last_seen = tasmota.rtc('local') + battery = ( Bytes[1] | (Bytes[0] << 8) & 0x3FFF ) / 1000.0 data.insert("DoorOpenEvents", Bytes[5] | (Bytes[4] << 8) | (Bytes[3] << 16 )) data.insert("DoorOpenLastDuration_mins", Bytes[8] | (Bytes[7] << 8) | (Bytes[6] << 16)) data.insert("Alarm", (Bytes[9] & 0x01 ) ? true : false) valid_values = true - else # Ignore other Fports end #Fport @@ -33,7 +39,7 @@ class LwDecoLDS02 if global.lds02Nodes.find(Node) global.lds02Nodes.remove(Node) end - global.lds02Nodes.insert(Node, [Node, door_open]) + global.lds02Nodes.insert(Node, [Node, last_seen, battery_last_seen, battery, RSSI, door_open]) end return data @@ -42,12 +48,24 @@ class LwDecoLDS02 static def add_web_sensor() var msg = "" for sensor: global.lds02Nodes - msg += string.format("{s}LDS02_%i Door{m}%s{e}", - sensor[0], (sensor[1]) ? "Open" : "Closed") - end + # Sensor[0] [1] [2] [3] [4] [5] + # [Node, last_seen, battery_last_seen, battery, RSSI, door_open] + var name = string.format("LDS02-%i", sensor[0]) + var name_tooltip = "Dragino LDS02" + var battery = sensor[3] + var battery_last_seen = sensor[2] + var rssi = sensor[4] + var last_seen = sensor[1] + msg += lwdecode.header(name, name_tooltip, battery, battery_last_seen, rssi, last_seen) + + # Sensors + msg += "┆" # | + msg += string.format(" %s", (sensor[5]) ? "🔓" : "🔒") # Open or Closed lock - Door + msg += "{e}" # = + end return msg - end + end #add_web_sensor() end #class LwDeco = LwDecoLDS02 diff --git a/tasmota/berry/lorawan/decoders/vendors/dragino/LHT52.be b/tasmota/berry/lorawan/decoders/vendors/dragino/LHT52.be index f5d6bf425..b10546015 100644 --- a/tasmota/berry/lorawan/decoders/vendors/dragino/LHT52.be +++ b/tasmota/berry/lorawan/decoders/vendors/dragino/LHT52.be @@ -9,19 +9,33 @@ import string global.lht52Nodes = {} class LwDecoLHT52 - static def decodeUplink(Node, FPort, Bytes) + static def decodeUplink(Node, RSSI, FPort, Bytes) var data = {"Device":"Dragino LHT52"} data.insert("Node", Node) var valid_values = false + var last_seen + var battery_last_seen + var battery = 1000 + var rssi = RSSI var temp_int var humidity var temp_ext = 1000 + if global.lht52Nodes.find(Node) + last_seen = global.lht52Nodes.item(Node)[1] + battery_last_seen = global.lht52Nodes.item(Node)[2] + battery = global.lht52Nodes.item(Node)[3] + rssi = global.lht52Nodes.item(Node)[4] + temp_int = global.lht52Nodes.item(Node)[5] + humidity = global.lht52Nodes.item(Node)[6] + temp_ext = global.lht52Nodes.item(Node)[7] + end ## SENSOR DATA ## if 2 == FPort && Bytes.size() == 11 + last_seen = tasmota.rtc('local') + var TempC - - TempC = Bytes[0] << 8 | Bytes[1] + TempC = Bytes[0] << 8 | Bytes[1] if Bytes[0] > 0x7F TempC -= 0x10000 end @@ -55,8 +69,10 @@ class LwDecoLHT52 data.insert("Firmware_Version", f'v{Bytes[1]:%u}.{Bytes[2]>>4:%u}.{Bytes[2]&0xF:%u}') data.insert("Freq_Band",LwRegions[Bytes[3]-1]) data.insert("Sub_Band",Bytes[4]) - data.insert("Bat_mV",(Bytes[5] << 8) | Bytes[6]) - + data.insert("BattV",((Bytes[5] << 8) | Bytes[6]) / 1000) + battery_last_seen = tasmota.rtc('local') + battery = ((Bytes[5] << 8) | Bytes[6]) / 1000 + valid_values = true else # Ignore other Fports end #Fport @@ -65,7 +81,7 @@ class LwDecoLHT52 if global.lht52Nodes.find(Node) global.lht52Nodes.remove(Node) end - global.lht52Nodes.insert(Node, [Node, temp_int, humidity, temp_ext]) + global.lht52Nodes.insert(Node, [Node, last_seen, battery_last_seen, battery, RSSI, temp_int, humidity, temp_ext]) end return data @@ -74,18 +90,28 @@ class LwDecoLHT52 static def add_web_sensor() var msg = "" for sensor: global.lht52Nodes - msg += string.format("{s}LHT52_%i Temperature{m}%.1f °C{e}".. - "{s}LHT52_%i Humidity{m}%.1f %%{e}", - sensor[0], sensor[1], - sensor[0], sensor[2]) - if sensor[3] < 1000 - msg += string.format("{s}LHT52_%i Temperature ext.{m}%.1f °C{e}", - sensor[0], sensor[3]) - end - end + # Sensor[0] [1] [2] [3] [4] [5] [6] [7] + # [Node, last_seen, battery_last_seen, battery, RSSI, temp_int, humidity, temp_ext] + var name = string.format("LHT52-%i", sensor[0]) + var name_tooltip = "Dragino LHT52" + var battery = sensor[3] + var battery_last_seen = sensor[2] + var rssi = sensor[4] + var last_seen = sensor[1] + msg += lwdecode.header(name, name_tooltip, battery, battery_last_seen, rssi, last_seen) + + # Sensors + msg += "┆" # | + msg += string.format(" ☀️ %.1f°C", sensor[5]) # Sunshine - Temperature internal + msg += string.format(" 💧 %.1f%%", sensor[6]) # Raindrop - Humidity + if sensor[7] < 1000 + msg += string.format(" ☀️ ext %.1f°C", sensor[7]) # Sunshine - Temperature external + end + msg += "{e}" # = + end return msg - end + end #add_web_sensor() end #class LwDeco = LwDecoLHT52 diff --git a/tasmota/berry/lorawan/decoders/vendors/dragino/LHT65.be b/tasmota/berry/lorawan/decoders/vendors/dragino/LHT65.be index 605697721..65c2f465f 100644 --- a/tasmota/berry/lorawan/decoders/vendors/dragino/LHT65.be +++ b/tasmota/berry/lorawan/decoders/vendors/dragino/LHT65.be @@ -9,7 +9,7 @@ var LHT65_BatteryStatus = ["Very low <= 2.5V","Low <=2.55V","OK","Good >= 2.65V" global.lht65Nodes = {} class LwDecoLHT65 - static def decodeUplink(Node, FPort, Bytes) + static def decodeUplink(Node, RSSI, FPort, Bytes) var data = {"Device":"Dragino LHT65"} data.insert("Node", Node) data.insert("poll_message_status",(Bytes[6] & 0x40) >> 6) @@ -18,21 +18,30 @@ class LwDecoLHT65 var NoConnect = (Bytes[6] & 0x80) >> 7 var valid_values = false + var last_seen + var battery_last_seen + var battery = 1000 + var rssi = RSSI var temp_int = 1000 var humidity var temp_ext = 1000 var door_open = 1000 if global.lht65Nodes.find(Node) - temp_int = global.lht65Nodes.item(Node)[1] - humidity = global.lht65Nodes.item(Node)[2] - temp_ext = global.lht65Nodes.item(Node)[3] - door_open = global.lht65Nodes.item(Node)[4] + last_seen = global.lht65Nodes.item(Node)[1] + battery_last_seen = global.lht65Nodes.item(Node)[2] + battery = global.lht65Nodes.item(Node)[3] + RSSI = global.lht65Nodes.item(Node)[4] + temp_int = global.lht65Nodes.item(Node)[5] + humidity = global.lht65Nodes.item(Node)[6] + temp_ext = global.lht65Nodes.item(Node)[7] + door_open = global.lht65Nodes.item(Node)[8] end ## SENSOR DATA ## if 2 == FPort && Bytes.size() == 11 var TempC if Ext == 9 #Sensor E3, Temperature Sensor, Datalog Mod + last_seen = tasmota.rtc('local') TempC = ((Bytes[0] << 8) | Bytes[1]) if 0x7FFF == TempC data.insert("Ext_SensorConnected", false) @@ -47,11 +56,15 @@ class LwDecoLHT65 end data.insert("Bat_status", LHT65_BatteryStatus[Bytes[4] >> 6]) else - data.insert("BatV",(((Bytes[0] << 8) | Bytes[1]) & 0x3fff) / 1000.0) + data.insert("BattV",(((Bytes[0] << 8) | Bytes[1]) & 0x3fff) / 1000.0) + battery_last_seen = tasmota.rtc('local') + battery = (((Bytes[0] << 8) | Bytes[1]) & 0x3fff) / 1000.0 data.insert("Bat_status", LHT65_BatteryStatus[Bytes[0] >> 6]) + valid_values = true end if Ext != 0x0F + last_seen = tasmota.rtc('local') TempC = ((Bytes[2] << 8) | Bytes[3]) if Bytes[2]>0x7F TempC -= 0x10000 @@ -70,6 +83,7 @@ class LwDecoLHT65 if 0 == Ext data.insert("Ext_sensor", 'No external sensor') elif 1==Ext + last_seen = tasmota.rtc('local') data.insert("Ext_sensor",'Temperature Sensor') TempC = ((Bytes[7] << 8) | Bytes[8]) if 0x7FFF == TempC @@ -84,6 +98,7 @@ class LwDecoLHT65 valid_values = true end elif 4 == Ext + last_seen = tasmota.rtc('local') data.insert("Work_mode", 'Interrupt Sensor send') door_open = ( Bytes[7] ) ? 0 : 1 # DS sensor data.insert("Exti_pin_level", Bytes[7] ? 'High' : 'Low') @@ -117,8 +132,10 @@ class LwDecoLHT65 data.insert("Firmware_Version", f'v{Bytes[1]:%u}.{Bytes[2]>>4:%u}.{Bytes[2]&0xF:%u}') data.insert("Freq_Band",LwRegions[Bytes[3]-1]) data.insert("Sub_Band",Bytes[4]) - data.insert("Bat_mV",(Bytes[5] << 8) | Bytes[6]) - + data.insert("BattV",((Bytes[5] << 8) | Bytes[6]) / 1000.0) + battery_last_seen = tasmota.rtc('local') + battery = ((Bytes[5] << 8) | Bytes[6]) / 1000.0 + valid_values = true else # Ignore other Fports end #Fport @@ -127,34 +144,42 @@ class LwDecoLHT65 if global.lht65Nodes.find(Node) global.lht65Nodes.remove(Node) end - global.lht65Nodes.insert(Node, [Node, temp_int, humidity, temp_ext, door_open]) + global.lht65Nodes.insert(Node, [Node, last_seen, battery_last_seen, battery, rssi, temp_int, humidity, temp_ext, door_open]) end - return data end # decodeUplink() static def add_web_sensor() var msg = "" for sensor: global.lht65Nodes - if sensor[1] < 1000 - msg += string.format("{s}LHT65_%i Temperature{m}%.1f °C{e}".. - "{s}LHT65_%i Humidity{m}%.1f %%{e}", - sensor[0], sensor[1], - sensor[0], sensor[2]) - end - if sensor[3] < 1000 - msg += string.format("{s}LHT65_%i Temperature ext.{m}%.1f °C{e}", - sensor[0], sensor[3]) - end - if sensor[4] < 1000 - msg += string.format("{s}LHT65_%i Door{m}%s{e}", - sensor[0], (sensor[4]) ? "Open" : "Closed") - end - end + # Sensor[0] [1] [2] [3] [4] [5] [6] [7] [8] + # [Node, last_seen, battery_last_seen, battery, RSSI, temp_int, humidity, temp_ext, door_open] + var name = string.format("LHT65-%i", sensor[0]) + var name_tooltip = "Dragino LHT65" + var battery = sensor[3] + var battery_last_seen = sensor[2] + var rssi = sensor[4] + var last_seen = sensor[1] + msg += lwdecode.header(name, name_tooltip, battery, battery_last_seen, rssi, last_seen) + + # Sensors + msg += "┆" # | + if sensor[5] < 1000 + msg += string.format(" ☀️ %.1f°C", sensor[5]) # Sunshine - Temperature + msg += string.format(" 💧 %.1f%%", sensor[6]) # Raindrop - Humidity + end + if sensor[7] < 1000 + msg += string.format(" ☀️ ext %.1f°C", sensor[7]) # Sunshine - Temperature external + end + if sensor[8] < 1000 + msg += string.format(" %s", (sensor[8]) ? "🔓" : "🔒") # Open or Closed lock - Door + end + msg += "{e}" # = + end return msg - end + end #add_web_sensor() end # class LwDeco = LwDecoLHT65 \ No newline at end of file diff --git a/tasmota/berry/lorawan/decoders/vendors/merryiot/DW10.be b/tasmota/berry/lorawan/decoders/vendors/merryiot/DW10.be index a529fc21e..5a46de53b 100644 --- a/tasmota/berry/lorawan/decoders/vendors/merryiot/DW10.be +++ b/tasmota/berry/lorawan/decoders/vendors/merryiot/DW10.be @@ -9,25 +9,32 @@ import string global.dw10Nodes = {} class LwDecoDW10 - static def decodeUplink(Node, FPort, Bytes) + static def decodeUplink(Node, RSSI, FPort, Bytes) var data = {"Device":"MerryIoT DW10"} data.insert("Node", Node) var valid_values = false + var last_seen + var battery_last_seen + var battery + var rssi = RSSI var door_open var button_pressed var temperature var humidity ## SENSOR DATA ## if 120 == FPort && Bytes.size() == 9 + last_seen = tasmota.rtc('local') door_open = ( Bytes[0] & 0x01 ) ? 1 : 0 data.insert("DoorOpen", ( door_open ) ? true : false ) button_pressed = ( Bytes[0] & 0x02 ) ? 1 : 0 data.insert("ButtonPress", ( button_pressed ) ? true : false ) data.insert("TamperDetect", ( Bytes[0] & 0x04 ) ? true : false ) data.insert("TiltDetect", ( Bytes[0] & 0x08 ) ? true : false ) - data.insert("Battery_mV", ( 21 + Bytes[1] ) * 100 ) - data.insert("Temperature_C", Bytes[2]) + data.insert("BattV", (( 21 + Bytes[1] ) * 100) / 1000.0 ) + battery_last_seen = tasmota.rtc('local') + battery = (( 21 + Bytes[1] ) * 100) / 1000.0 + data.insert("TemperatureC", Bytes[2]) temperature = Bytes[2] data.insert("Humidity", Bytes[3]) humidity = Bytes[3] @@ -43,7 +50,7 @@ class LwDecoDW10 if global.dw10Nodes.find(Node) global.dw10Nodes.remove(Node) end - global.dw10Nodes.insert(Node, [Node, door_open, button_pressed, temperature, humidity]) + global.dw10Nodes.insert(Node, [Node, last_seen, battery_last_seen, battery, RSSI, door_open, button_pressed, temperature, humidity]) end return data @@ -52,17 +59,26 @@ class LwDecoDW10 static def add_web_sensor() var msg = "" for sensor: global.dw10Nodes - msg += string.format("{s}DW10_%i Door{m}%s{e}".. - "{s}DW10_%i Button{m}%s{e}".. - "{s}DW10_%i Temperature{m}%.1f °C{e}".. - "{s}DW10_%i Humidity{m}%.1f %%{e}", - sensor[0], (sensor[1]) ? "Open" : "Closed", - sensor[0], (sensor[2]) ? "Pressed" : "Released", - sensor[0], sensor[3], - sensor[0], sensor[4]) + # Sensor[0] [1] [2] [3] [4] [5] [6] [7] [8] + # [Node, last_seen, battery_last_seen, battery, RSSI, door_open, button_pressed, temperature, humidity] + + var name = string.format("DW10-%i", sensor[0]) + var name_tooltip = "MerryIoT DW10" + var battery = sensor[3] + var battery_last_seen = sensor[2] + var rssi = sensor[4] + var last_seen = sensor[1] + msg += lwdecode.header(name, name_tooltip, battery, battery_last_seen, rssi, last_seen) + + # Sensors + msg += "┆" # | + msg += string.format(" ☀️ %.1f°C", sensor[7]) # Sunshine - Temperature + msg += string.format(" 💧 %.1f%%", sensor[8]) # Raindrop - Humidity + msg += string.format(" %s", (sensor[5]) ? "🔓" : "🔒") # Open or Closed lock - Door + msg += "{e}" # = end return msg - end + end #add_web_sensor() end #class LwDeco = LwDecoDW10 diff --git a/tasmota/tasmota_xdrv_driver/xdrv_01_9_webserver.ino b/tasmota/tasmota_xdrv_driver/xdrv_01_9_webserver.ino index f0e595aac..0d1b1ac4c 100644 --- a/tasmota/tasmota_xdrv_driver/xdrv_01_9_webserver.ino +++ b/tasmota/tasmota_xdrv_driver/xdrv_01_9_webserver.ino @@ -231,8 +231,8 @@ const char HTTP_SCRIPT_INFO_END[] PROGMEM = #include "./html_uncompressed/HTTP_HEAD_STYLE_WIFI.h" #endif -#ifdef USE_ZIGBEE -// Styles used for Zigbee Web UI +#if defined(USE_ZIGBEE) || defined(USE_LORAWAN_BRIDGE) +// Styles used for Zigbee and LoRaWan Web UI // Battery icon from https://css.gg/battery // #ifdef USE_UNISHOX_COMPRESSION @@ -997,7 +997,10 @@ void WSContentSendStyle_P(const char* formatP, ...) { #ifdef USE_WEB_STATUS_LINE_WIFI WSContentSend_P(HTTP_HEAD_STYLE_WIFI, WebColor(COL_FORM), WebColor(COL_TITLE)); #endif -#ifdef USE_ZIGBEE + +USE_LORAWAN_BRIDGE + +#if defined(USE_ZIGBEE) || defined(USE_LORAWAN_BRIDGE) WSContentSend_P(HTTP_HEAD_STYLE_ZIGBEE); #endif // USE_ZIGBEE if (formatP != nullptr) {