diff --git a/tasmota/berry/extensions/Devices_Online.tapp b/tasmota/berry/extensions/Devices_Online.tapp index 93468ce38..b67ddcb5c 100644 Binary files a/tasmota/berry/extensions/Devices_Online.tapp and b/tasmota/berry/extensions/Devices_Online.tapp differ diff --git a/tasmota/berry/extensions/Devices_Online/autoexec.be b/tasmota/berry/extensions/Devices_Online/autoexec.be index 29c20df0a..47034daa8 100644 --- a/tasmota/berry/extensions/Devices_Online/autoexec.be +++ b/tasmota/berry/extensions/Devices_Online/autoexec.be @@ -1,4 +1,4 @@ -# rm Devices_Online.tapp; zip -j -0 Devices_Online.tapp Devices_Online/autoexec.be Devices_Online/devices_online.be Devices_Online/manifest.json +# rm Devices_Online.tapp; zip -j -0 Devices_Online.tapp Devices_Online/* do # embed in `do` so we don't add anything to global namespace import introspect var devices_online = introspect.module('devices_online', true) # load module but don't cache diff --git a/tasmota/berry/extensions/Devices_Online/devices_online.be b/tasmota/berry/extensions/Devices_Online/devices_online.be index 0feba4b9f..1791e33f0 100644 --- a/tasmota/berry/extensions/Devices_Online/devices_online.be +++ b/tasmota/berry/extensions/Devices_Online/devices_online.be @@ -8,7 +8,7 @@ # or # line_option = 2 : Show devices updating within 'line_teleperiod' # -# rm Devices_Online.tapp; zip -j -0 Devices_Online.tapp Devices_Online/autoexec.be Devices_Online/devices_online.be Devices_Online/manifest.json +# rm Devices_Online.tapp; zip -j -0 Devices_Online.tapp Devices_Online/* ################################################################################### import mqtt diff --git a/tasmota/berry/extensions/Devices_Online/manifest.json b/tasmota/berry/extensions/Devices_Online/manifest.json index 91b25c37b..d94838dba 100644 --- a/tasmota/berry/extensions/Devices_Online/manifest.json +++ b/tasmota/berry/extensions/Devices_Online/manifest.json @@ -1,6 +1,6 @@ { "name": "Devices Online", - "version": "0x01010100", + "version": "0x190A0200", "description": "Display devices online", "author": "Theo Arends", "min_tasmota": "0x0E060001", diff --git a/tasmota/berry/extensions/LoRaWan_Decoders.tapp b/tasmota/berry/extensions/LoRaWan_Decoders.tapp new file mode 100644 index 000000000..6f7df1c52 Binary files /dev/null and b/tasmota/berry/extensions/LoRaWan_Decoders.tapp differ diff --git a/tasmota/berry/extensions/LoRaWan_Decoders/D20.be b/tasmota/berry/extensions/LoRaWan_Decoders/D20.be new file mode 100644 index 000000000..9f7815e4d --- /dev/null +++ b/tasmota/berry/extensions/LoRaWan_Decoders/D20.be @@ -0,0 +1,129 @@ +# LoRaWAN Decoder file for Dragino D20/D22/D23 (1,2,3 temp sensor models) +# +# References +# User Manual: https://wiki.dragino.com/xwiki/bin/view/Main/User%20Manual%20for%20LoRaWAN%20End%20Nodes/D20-LBD22-LBD23-LB_LoRaWAN_Temperature_Sensor_User_Manual/ +# TTN Device Repository: https://github.com/TheThingsNetwork/lorawan-devices/blob/master/vendor/dragino/d2x-lb.js + +import string + +if !global.DrgD20Nodes # data survive to decoder reload + global.DrgD20Nodes = {} +end + +class LwDecoDrgD20 + static def decodeUplink(Name, Node, RSSI, FPort, Bytes) + var data = {"Device":"Dragino D20"} + + var valid_values = false + var last_seen = 1451602800 + var battery_last_seen = 1451602800 + var battery = 1000 + var rssi = RSSI + var tempC1 = 1000 + var tempC2 = 1000 + var tempC3 = 1000 + + if global.DrgD20Nodes.find(Node) + last_seen = global.DrgD20Nodes.item(Node)[2] + battery_last_seen = global.DrgD20Nodes.item(Node)[3] + battery = global.DrgD20Nodes.item(Node)[4] + rssi = global.DrgD20Nodes.item(Node)[5] + tempC1 = global.DrgD20Nodes.item(Node)[6] + tempC2 = global.DrgD20Nodes.item(Node)[7] + tempC3 = global.DrgD20Nodes.item(Node)[8] + end + + ## SENSOR DATA ## + if 2 == FPort && Bytes.size() == 11 + last_seen = tasmota.rtc('local') + var mode=(Bytes[6] & 0x7C)>>2 + + if 3==mode + battery = (Bytes[0]<<8 | Bytes[1])/1000 + data.insert("BattV", battery) + battery_last_seen = tasmota.rtc('local') + + # 0x07FF = 2047 = no temp sensor + + tempC1 = Bytes[2] << 8 | Bytes[3] + if Bytes[2]>0x7F tempC1-=0x10000 end + tempC1 /= 10.0 + data.insert("TempC1", tempC1) + + tempC2 = Bytes[7] << 8 | Bytes[8] + if Bytes[7]>0x7F tempC2-=0x10000 end + tempC2 /= 10.0 + data.insert("TempC2", tempC2) + + tempC3 = Bytes[9] << 8 | Bytes[10] + if Bytes[9]>0x7F tempC3-=0x10000 end + tempC3 /= 10.0 + data.insert("TempC3", tempC3) + + end + + valid_values = true + + ## STATUS DATA ## + elif 5 == FPort && Bytes.size() == 7 + data.insert("Sensor_Model",Bytes[0]) + 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("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 + + if valid_values + if global.DrgD20Nodes.find(Node) + global.DrgD20Nodes.remove(Node) + end + # sensor[0] [1] [2] [3] [4] [5] [6] [7] [8] + global.DrgD20Nodes.insert(Node, [Name, Node, last_seen, battery_last_seen, battery, RSSI, tempC1, tempC2, tempC3]) + end + + return data + end #decodeUplink() + + static def add_web_sensor() + var fmt = global.LwSensorFormatter_cls() + var msg = "" + for sensor: global.DrgD20Nodes + var name = sensor[0] + if string.find(name, "D20") > -1 # If LoRaWanName contains D20 use D20- + name = string.format("D20-%i", sensor[1]) + end + var name_tooltip = "Dragino D20" + var last_seen = sensor[2] + var battery_last_seen = sensor[3] + var battery = sensor[4] + var rssi = sensor[5] + msg += fmt.header(name, name_tooltip, battery, battery_last_seen, rssi, last_seen) + + # Sensors + var tempC1 = sensor[6] + msg += "┆" # | + if tempC1 < 1000 + msg += string.format(" ☀️ %.1f°C", tempC1) # Sunshine - Temperature + end + + var tempC2 = sensor[7] + if tempC2 < 1000 + msg += string.format(" ☀️ %.1f°C", tempC2) + end + + var tempC3 = sensor[8] + if tempC3 < 1000 + msg += string.format(" ☀️ %.1f°C", tempC3) + end + msg += "{e}" # = + end + return msg + end #add_web_sensor() +end #class + +global.LwDeco = LwDecoDrgD20 diff --git a/tasmota/berry/extensions/LoRaWan_Decoders/DDS75L.be b/tasmota/berry/extensions/LoRaWan_Decoders/DDS75L.be new file mode 100644 index 000000000..3f4b64681 --- /dev/null +++ b/tasmota/berry/extensions/LoRaWan_Decoders/DDS75L.be @@ -0,0 +1,94 @@ +# LoRaWAN Decoder file for Dragino DDS75-LB/LS +# +# References +# User Manual: https://wiki.dragino.com/xwiki/bin/view/Main/User%20Manual%20for%20LoRaWAN%20End%20Nodes/DDS75-LB_LoRaWAN_Distance_Detection_Sensor_User_Manual/ +# TTN Device Repository: https://github.com/TheThingsNetwork/lorawan-devices/blob/master/vendor/dragino/dds75-lb.js + +import string + +if !global.dds75lbNodes # data survive to decoder reload + global.dds75lbNodes = {} +end + +class LwDecoDDS75LB + static def decodeUplink(Name, Node, RSSI, FPort, Bytes) + var data = {"Device":"Dragino DDS75-LB/LS"} + + var valid_values = false + var last_seen = 1451602800 + var battery_last_seen = 1451602800 + var battery = 1000 + var rssi = RSSI + var distance = 0 + + if global.dds75lbNodes.find(Node) + last_seen = global.dds75lbNodes.item(Node)[2] + battery_last_seen = global.dds75lbNodes.item(Node)[3] + battery = global.dds75lbNodes.item(Node)[4] + rssi = global.dds75lbNodes.item(Node)[5] + distance = global.dds75lbNodes.item(Node)[6] + end + + ## SENSOR DATA ## + if 2 == FPort && 8 == Bytes.size() && 0 == ( Bytes[0] & 0x10 ) + last_seen = tasmota.rtc('local') + + battery_last_seen = tasmota.rtc('local') + battery = ((Bytes[0] << 8) | Bytes[1]) / 1000.0 + data.insert("BattV",battery) + + distance=Bytes[2]<<8 | Bytes[3] + data.insert("Distance",distance) + + valid_values = true + + ## STATUS DATA ## + elif 5 == FPort && 7 == Bytes.size() + data.insert("Sensor_Model",Bytes[0]) + 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]) + battery_last_seen = tasmota.rtc('local') + battery = ((Bytes[5] << 8) | Bytes[6]) / 1000.0 + valid_values = true + else + # Ignore other Fports + end #Fport + + if valid_values + if global.dds75lbNodes.find(Node) + global.dds75lbNodes.remove(Node) + end + # sensor[0] [1] [2] [3] [4] [5] [6] + global.dds75lbNodes.insert(Node, [Name, Node, last_seen, battery_last_seen, battery, RSSI, distance]) + end + + return data + end #decodeUplink() + + static def add_web_sensor() + var fmt = global.LwSensorFormatter_cls() + var msg = "" + for sensor: global.dds75lbNodes + var name = sensor[0] + if string.find(name, "DDS75-L") > -1 # If LoRaWanName contains DDS75-L use DDS75-L- + name = string.format("DDS75-L-%i", sensor[1]) + end + var name_tooltip = "Dragino DDS75-L" + var last_seen = sensor[2] + var battery_last_seen = sensor[3] + var battery = sensor[4] + var rssi = sensor[5] + msg += fmt.header(name, name_tooltip, battery, battery_last_seen, rssi, last_seen) + + # Sensors + var distance = sensor[6] + msg += "┆" # | + msg += string.format(" ⭳️ %.0fmm", distance) # ⭳ + msg += "{e}" # = + end + return msg + end #add_web_sensor() +end #class + +global.LwDeco = LwDecoDDS75LB diff --git a/tasmota/berry/extensions/LoRaWan_Decoders/DW10.be b/tasmota/berry/extensions/LoRaWan_Decoders/DW10.be new file mode 100644 index 000000000..3a49c272d --- /dev/null +++ b/tasmota/berry/extensions/LoRaWan_Decoders/DW10.be @@ -0,0 +1,103 @@ +# LoRaWAN Decoder file for MerryIoT DW10 Open/Close +# +# References +# DW10 Product information: https://www.browan.com/products-detail/OpenClose-Sensor-EBL-LoRaWAN/ +# Browan JS Decoder (TTN): https://www.browan.com/member/login/?refererUrl=https%3A%2F%2Fwww.browan.com%2Fproducts-detail%2FOpenClose-Sensor-EBL-LoRaWAN%2F + +import string + +if !global.dw10Nodes # data survive to decoder reload + global.dw10Nodes = {} +end + +class LwDecoDW10 + static def decodeUplink(Name, Node, RSSI, FPort, Bytes) + var data = {"Device":"MerryIoT DW10"} + + var valid_values = false + var last_seen = 1451602800 + var battery_last_seen = 1451602800 + var battery + var rssi = RSSI + var door_open + var door_open_last_seen = 1451602800 + var button_pressed + var temperature + var humidity + if global.dw10Nodes.find(Node) + door_open = global.dw10Nodes.item(Node)[6] + door_open_last_seen = global.dw10Nodes.item(Node)[7] + end + ## SENSOR DATA ## + if 120 == FPort && Bytes.size() == 9 + last_seen = tasmota.rtc('local') + + var last_door_open = door_open + door_open = ( Bytes[0] & 0x01 ) ? 1 : 0 + data.insert("DoorOpen", ( door_open ) ? true : false ) + if last_door_open != door_open + door_open_last_seen = tasmota.rtc('local') + end + + 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("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] + data.insert("DoorOpenLastDuration_mins", Bytes[4] | (Bytes[5] << 8)) + data.insert("DoorOpenEvents", Bytes[6] | (Bytes[7] << 8) | (Bytes[8] << 16 )) + valid_values = true + else + # Ignore other Fports + end #Fport + + if valid_values + if global.dw10Nodes.find(Node) + global.dw10Nodes.remove(Node) + end + # sensor[0] [1] [2] [3] [4] [5] [6] [7] [8] [9] [10] + global.dw10Nodes.insert(Node, [Name, Node, last_seen, battery_last_seen, battery, RSSI, door_open, door_open_last_seen, button_pressed, temperature, humidity]) + end + + return data + end #decodeUplink() + + static def add_web_sensor() + var fmt = global.LwSensorFormatter_cls() + var msg = "" + for sensor: global.dw10Nodes + var name = sensor[0] + if string.find(name, "DW10") > -1 # If LoRaWaName contains DW10 use DW10- + name = string.format("DW10-%i", sensor[1]) + end + var name_tooltip = "MerryIoT DW10" + var last_seen = sensor[2] + var battery_last_seen = sensor[3] + var battery = sensor[4] + var rssi = sensor[5] + msg += fmt.header(name, name_tooltip, battery, battery_last_seen, rssi, last_seen) + + # Sensors + var door_open = sensor[6] + var door_open_last_seen = sensor[7] + var button_pressed = sensor[8] + var temperature = sensor[9] + var humidity = sensor[10] + msg += "┆" # | + msg += string.format(" ☀️ %.1f°C", temperature) # Sunshine - Temperature + msg += string.format(" 💧 %.1f%%", humidity) # Raindrop - Humidity + msg += string.format(" %s %s", (door_open) ? "🔓" : "🔒", # Open or Closed lock - Door + fmt.dhm(door_open_last_seen)) + msg += "{e}" # = + end + return msg + end #add_web_sensor() +end #class + +global.LwDeco = LwDecoDW10 diff --git a/tasmota/berry/extensions/LoRaWan_Decoders/LDS02.be b/tasmota/berry/extensions/LoRaWan_Decoders/LDS02.be new file mode 100644 index 000000000..225cbf7e6 --- /dev/null +++ b/tasmota/berry/extensions/LoRaWan_Decoders/LDS02.be @@ -0,0 +1,88 @@ +# LoRaWAN Decoder file for Dragino LDS02 +# +# References +# LHT52 User Manual: https://wiki.dragino.com/xwiki/bin/view/Main/User%20Manual%20for%20LoRaWAN%20End%20Nodes/LDS02%20-%20LoRaWAN%20Door%20Sensor%20User%20Manual/ +# TTN Device Repository: https://github.com/TheThingsNetwork/lorawan-devices/blob/master/vendor/dragino/lds02.js + +import string + +if !global.lds02Nodes # data survive to decoder reload + global.lds02Nodes = {} +end + +class LwDecoLDS02 + static def decodeUplink(Name, Node, RSSI, FPort, Bytes) + var data = {"Device":"Dragino LDS02"} + + var valid_values = false + var last_seen = 1451602800 + var battery_last_seen = 1451602800 + var battery + var rssi = RSSI + var door_open + var door_open_last_seen = 1451602800 + if global.lds02Nodes.find(Node) + door_open = global.lds02Nodes.item(Node)[6] + door_open_last_seen = global.lds02Nodes.item(Node)[7] + end + ## SENSOR DATA ## + if 10 == FPort && Bytes.size() == 10 + last_seen = tasmota.rtc('local') + + var last_door_open = door_open + door_open = ( Bytes[0] & 0x80 ) ? 1 : 0 + data.insert("DoorOpen", ( door_open ) ? true : false) + if last_door_open != door_open + door_open_last_seen = tasmota.rtc('local') + end + + 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 + + if valid_values + if global.lds02Nodes.find(Node) + global.lds02Nodes.remove(Node) + end + # sensor[0] [1] [2] [3] [4] [5] [6] [7] + global.lds02Nodes.insert(Node, [Name, Node, last_seen, battery_last_seen, battery, RSSI, door_open, door_open_last_seen]) + end + + return data + end #decodeUplink() + + static def add_web_sensor() + var fmt = global.LwSensorFormatter_cls() + var msg = "" + for sensor: global.lds02Nodes + var name = sensor[0] + if string.find(name, "LDS02") > -1 # If LoRaWanName contains LDS02 use LDS02- + name = string.format("LDS02-%i", sensor[1]) + end + var name_tooltip = "Dragino LDS02" + var last_seen = sensor[2] + var battery_last_seen = sensor[3] + var battery = sensor[4] + var rssi = sensor[5] + msg += fmt.header(name, name_tooltip, battery, battery_last_seen, rssi, last_seen) + + # Sensors + var door_open = sensor[6] + var door_open_last_seen = sensor[7] + msg += "┆" # | + msg += string.format(" %s %s", (door_open) ? "🔓" : "🔒", # Open or Closed lock - Door + fmt.dhm(door_open_last_seen)) + msg += "{e}" # = + end + return msg + end #add_web_sensor() +end #class + +global.LwDeco = LwDecoLDS02 diff --git a/tasmota/berry/extensions/LoRaWan_Decoders/LHT52.be b/tasmota/berry/extensions/LoRaWan_Decoders/LHT52.be new file mode 100644 index 000000000..f9acffd6c --- /dev/null +++ b/tasmota/berry/extensions/LoRaWan_Decoders/LHT52.be @@ -0,0 +1,125 @@ +# LoRaWAN Decoder file for Dragino LHT52 +# +# References +# User Manual: https://wiki.dragino.com/xwiki/bin/view/Main/User%20Manual%20for%20LoRaWAN%20End%20Nodes/LHT52%20-%20LoRaWAN%20Temperature%20%26%20Humidity%20Sensor%20User%20Manual/ +# TTN Device Repository: https://github.com/TheThingsNetwork/lorawan-devices/blob/master/vendor/dragino/lht52.js + +import string + +if !global.lht52Nodes # data survive to decoder reload + global.lht52Nodes = {} +end + +class LwDecoLHT52 + static def decodeUplink(Name, Node, RSSI, FPort, Bytes) + var data = {"Device":"Dragino LHT52"} + + var valid_values = false + var last_seen = 1451602800 + var battery_last_seen = 1451602800 + var battery = 1000 + var rssi = RSSI + var temp_int = 1000 + var humidity + var temp_ext = 1000 + if global.lht52Nodes.find(Node) + last_seen = global.lht52Nodes.item(Node)[2] + battery_last_seen = global.lht52Nodes.item(Node)[3] + battery = global.lht52Nodes.item(Node)[4] + rssi = global.lht52Nodes.item(Node)[5] + temp_int = global.lht52Nodes.item(Node)[6] + humidity = global.lht52Nodes.item(Node)[7] + temp_ext = global.lht52Nodes.item(Node)[8] + end + ## SENSOR DATA ## + if 2 == FPort && Bytes.size() == 11 + last_seen = tasmota.rtc('local') + + var TempC + TempC = Bytes[0] << 8 | Bytes[1] + if Bytes[0] > 0x7F + TempC -= 0x10000 + end + TempC /= 100.0 + data.insert("TempC_Internal", TempC) + temp_int = TempC + + TempC = Bytes[4] << 8 | Bytes[5] + if 0x7FFF == TempC + data.insert("Ext_SensorConnected", false) + else + data.insert("Ext_SensorConnected", true) + if Bytes[4] > 0x7F + TempC -= 0x10000 + end + TempC /= 100.0 + data.insert("TempC_External", TempC) + temp_ext = TempC + end + + data.insert("Hum_Internal", ((Bytes[2] << 8) | Bytes[3]) / 10.0) + humidity = ((Bytes[2] << 8) | Bytes[3]) / 10.0 + data.insert("Ext_SensorType", Bytes[6]) + var epoch = (Bytes[7] << 24) | (Bytes[8] << 16) | (Bytes[9] << 8) | Bytes[10] + data.insert("Systimestamp",tasmota.time_str(epoch)) + valid_values = true + + ## STATUS DATA ## + elif 5 == FPort && Bytes.size() == 7 + data.insert("Sensor_Model",Bytes[0]) + 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("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 + + if valid_values + if global.lht52Nodes.find(Node) + global.lht52Nodes.remove(Node) + end + # sensor[0] [1] [2] [3] [4] [5] [6] [7] [8] + global.lht52Nodes.insert(Node, [Name, Node, last_seen, battery_last_seen, battery, RSSI, temp_int, humidity, temp_ext]) + end + + return data + end #decodeUplink() + + static def add_web_sensor() + var fmt = global.LwSensorFormatter_cls() + var msg = "" + for sensor: global.lht52Nodes + var name = sensor[0] + if string.find(name, "LHT52") > -1 # If LoRaWanName contains LHT52 use LHT52- + name = string.format("LHT52-%i", sensor[1]) + end + var name_tooltip = "Dragino LHT52" + var last_seen = sensor[2] + var battery_last_seen = sensor[3] + var battery = sensor[4] + var rssi = sensor[5] + msg += fmt.header(name, name_tooltip, battery, battery_last_seen, rssi, last_seen) + + # Sensors + var temp_int = sensor[6] + var humidity = sensor[7] + var temp_ext = sensor[8] + msg += "┆" # | + if temp_int < 1000 + msg += string.format(" ☀️ %.1f°C", temp_int) # Sunshine - Temperature internal + msg += string.format(" 💧 %.1f%%", humidity) # Raindrop - Humidity + end + if temp_ext < 1000 + msg += string.format(" ☀️ ext %.1f°C", temp_ext) # Sunshine - Temperature external + end + msg += "{e}" # = + end + return msg + end #add_web_sensor() +end #class + +global.LwDeco = LwDecoLHT52 diff --git a/tasmota/berry/extensions/LoRaWan_Decoders/LHT65.be b/tasmota/berry/extensions/LoRaWan_Decoders/LHT65.be new file mode 100644 index 000000000..35411e328 --- /dev/null +++ b/tasmota/berry/extensions/LoRaWan_Decoders/LHT65.be @@ -0,0 +1,202 @@ +# LoRaWAN Decoder file for Dragino LHT65 +# +# References +# User Manual: https://www.dragino.com/downloads/downloads/LHT65/UserManual/LHT65_Temperature_Humidity_Sensor_UserManual_v1.8.5.pdf +# TTN Device Repository: https://github.com/TheThingsNetwork/lorawan-devices/blob/master/vendor/dragino/lht65.js + +import string +var LHT65_BatteryStatus = ["Very low <= 2.5V","Low <=2.55V","OK","Good >= 2.65V"] + +if !global.lht65Nodes # data survive to decoder reload + global.lht65Nodes = {} +end + +class LwDecoLHT65 + static def decodeUplink(Name, Node, RSSI, FPort, Bytes) + var data = {"Device":"Dragino LHT65"} + + var valid_values = false + var last_seen = 1451602800 + var battery_last_seen = 1451602800 + var battery = 1000 + var rssi = RSSI + var temp_int = 1000 + var humidity + var temp_ext = 1000 + var door_open = 1000 + var door_open_last_seen = 1451602800 + if global.lht65Nodes.find(Node) + last_seen = global.lht65Nodes.item(Node)[2] + battery_last_seen = global.lht65Nodes.item(Node)[3] + battery = global.lht65Nodes.item(Node)[4] + RSSI = global.lht65Nodes.item(Node)[5] + temp_int = global.lht65Nodes.item(Node)[6] + humidity = global.lht65Nodes.item(Node)[7] + temp_ext = global.lht65Nodes.item(Node)[8] + door_open = global.lht65Nodes.item(Node)[9] + door_open_last_seen = global.lht65Nodes.item(Node)[10] + end + + var Ext = Bytes[6] & 0x0F #External sensor type + var NoConnect = (Bytes[6] & 0x80) >> 7 + + ## SENSOR DATA ## + data.insert("poll_message_status",(Bytes[6] & 0x40) >> 6) + + 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) + else + data.insert("Ext_SensorConnected", true) + if Bytes[0]>0x7F + TempC -= 0x10000 + end + temp_ext = TempC / 100.0 + data.insert("TempC_External", temp_ext) + valid_values = true + end + data.insert("Bat_status", LHT65_BatteryStatus[Bytes[4] >> 6]) + else + 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 + end + temp_int = TempC / 100.0 + data.insert("TempC_Internal", temp_int) + humidity = (((Bytes[4] << 8) | Bytes[5]) / 10.0) + data.insert("Hum_Internal" , humidity) + valid_values = true + end + + if NoConnect + data.insert('No_connect','No connection to external sensor') + end + + 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 + data.insert("Ext_SensorConnected", false) + else + data.insert("Ext_SensorConnected", true) + if Bytes[7]>0x7F + TempC -= 0x10000 + end + temp_ext = TempC / 100.0 + data.insert("TempC_External", temp_ext) + 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') + data.insert("Exti_status", Bytes[8] ? 'True' : 'False') + if Bytes[8] + door_open_last_seen = tasmota.rtc('local') + end + valid_values = true + elif 5 == Ext + data.insert("Work_mode", 'Illumination Sensor') + data.insert("ILL_lx", (Bytes[7] << 8) | Bytes[8]) + elif 6 == Ext + data.insert("Work_mode", 'ADC Sensor') + data.insert("ADC_V", ((Bytes[7] << 8) | Bytes[8]) / 1000.0) + elif 7 == Ext + data.insert("Work_mode", 'Interrupt Sensor count') + data.insert("Exit_count", (Bytes[7] << 8) | Bytes[8]) + elif 8 == Ext + data.insert("Work_mode", 'Interrupt Sensor count') + data.insert("Exit_count", (Bytes[7] << 24) | (Bytes[8] << 16) | (Bytes[9] << 8) | Bytes[10]) + elif 9 == Ext + data.insert("Work_mode", 'DS18B20 & timestamp') + var epoch = (Bytes[7] << 24) | (Bytes[8] << 16) | (Bytes[9] << 8) | Bytes[10] + data.insert("Systimestamp",tasmota.time_str(epoch)) + elif 15 == Ext + data.insert("Work_mode",'DS18B20ID') + data.insert("ID",f"{Bytes[2]:%02X}" + f"{Bytes[3]:%02X}" + f"{Bytes[4]:%02X}" + f"{Bytes[5]:%02X}" + f"{Bytes[6]:%02X}" + f"{Bytes[8]:%02X}" + f"{Bytes[9]:%02X}" + f"{Bytes[10]:%02X}" ) + else + data.insert("Ext_sensor", 'Unknown') + end + + elif 5 == FPort && Bytes.size() == 7 + data.insert("Sensor_Model",Bytes[0]) + 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("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 + + if valid_values + if global.lht65Nodes.find(Node) + global.lht65Nodes.remove(Node) + end + # sensor[0] [1] [2] [3] [4] [5] [6] [7] [8] [9] [10] + global.lht65Nodes.insert(Node, [Name, Node, last_seen, battery_last_seen, battery, rssi, temp_int, humidity, temp_ext, door_open, door_open_last_seen]) + end + + return data + end # decodeUplink() + + static def add_web_sensor() + var fmt = global.LwSensorFormatter_cls() + var msg = "" + for sensor: global.lht65Nodes + var name = sensor[0] + if string.find(name, "LHT65") > -1 # If LoRaWanName contains LHT65 use LHT65- + name = string.format("LHT65-%i", sensor[1]) + end + var name_tooltip = "Dragino LHT65" + var last_seen = sensor[2] + var battery_last_seen = sensor[3] + var battery = sensor[4] + var rssi = sensor[5] + msg += fmt.header(name, name_tooltip, battery, battery_last_seen, rssi, last_seen) + + # Sensors + var temp_int = sensor[6] + var humidity = sensor[7] + var temp_ext = sensor[8] + var door_open = sensor[9] + var door_open_last_seen = sensor[10] + msg += "┆" # | + if temp_int < 1000 + msg += string.format(" ☀️ %.1f°C", temp_int) # Sunshine - Temperature + msg += string.format(" 💧 %.1f%%", humidity) # Raindrop - Humidity + end + if temp_ext < 1000 + msg += string.format(" ☀️ ext %.1f°C", temp_ext) # Sunshine - Temperature external + end + if door_open < 1000 + msg += string.format(" %s %s", (door_open) ? "🔓" : "🔒", # Open or Closed lock - Door + fmt.dhm(door_open_last_seen)) + end + msg += "{e}" # = + end + return msg + end #add_web_sensor() +end # class + +global.LwDeco = LwDecoLHT65 \ No newline at end of file diff --git a/tasmota/berry/extensions/LoRaWan_Decoders/PS-L-I5.be b/tasmota/berry/extensions/LoRaWan_Decoders/PS-L-I5.be new file mode 100644 index 000000000..33d94f752 --- /dev/null +++ b/tasmota/berry/extensions/LoRaWan_Decoders/PS-L-I5.be @@ -0,0 +1,114 @@ +# LoRaWAN Decoder file for Dragino PS-LB/LS - LoRaWAN Air Water Pressure Sensor +# Model: Immersion type, 0-5m Range (PS-Lx-I5) +# +# References +# User Manual: https://wiki.dragino.com/xwiki/bin/view/Main/User%20Manual%20for%20LoRaWAN%20End%20Nodes/PS-LB%20--%20LoRaWAN%20Pressure%20Sensor/ +# Dragino Repository: https://github.com/dragino/dragino-end-node-decoder/blob/main/PS-LB/PS%20LB%20Chirpstack%20V4%20decoder.txt + +import string + +if !global.psli5Nodes # data survive to decoder reload + global.psli5Nodes = {} +end + +class LwDecoPSLI5 + static def decodeUplink(Name, Node, RSSI, FPort, Bytes) + var data = {"Device":"Dragino PS-LB/LS-I5"} + + var valid_values = false + var last_seen = 1451602800 + var battery_last_seen = 1451602800 + var battery = 1000 + var rssi = RSSI + var Water_deep_cm = 0 + + var Probe_mod + var IDC_input_mA + var modelRangeCm = 500 # 4mA=0cm, 20mA=500cm + + if global.psli5Nodes.find(Node) + last_seen = global.psli5Nodes.item(Node)[2] + battery_last_seen = global.psli5Nodes.item(Node)[3] + battery = global.psli5Nodes.item(Node)[4] + rssi = global.psli5Nodes.item(Node)[5] + Water_deep_cm = global.psli5Nodes.item(Node)[6] + + end + + ## SENSOR DATA ## + if 2 == FPort && 9 == Bytes.size() + ## eg 0e46 0000 197f 0000 00 + ## BATV ProbeModel mA Volt Int + last_seen = tasmota.rtc('local') + + battery_last_seen = tasmota.rtc('local') + battery = ((Bytes[0] << 8) | Bytes[1]) / 1000.0 + data.insert("BattV",battery) + + Probe_mod = Bytes[2] + IDC_input_mA = (Bytes[4]<<8 | Bytes[5])/1000.0 + + if Probe_mod == 0x00 # Immersion Sensor + if IDC_input_mA <= 4.0 + Water_deep_cm = 0 + else + Water_deep_cm = (IDC_input_mA - 4.0) * modelRangeCm / 16.0 + end + end # Probe_mod + + data.insert("WaterDepth_cm" ,Water_deep_cm) + data.insert("IDC_ma" ,IDC_input_mA) + data.insert("ModelRange_cm" ,modelRangeCm) + + valid_values = true + + ## STATUS DATA ## + elif 5 == FPort && 7 == Bytes.size() + data.insert("Sensor_Model",Bytes[0]) + 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]) + battery_last_seen = tasmota.rtc('local') + battery = ((Bytes[5] << 8) | Bytes[6]) / 1000.0 + valid_values = true + else + # Ignore other Fports + end #Fport + + if valid_values + if global.psli5Nodes.find(Node) + global.psli5Nodes.remove(Node) + end + # sensor[0] [1] [2] [3] [4] [5] [6] + global.psli5Nodes.insert(Node, [Name, Node, last_seen, battery_last_seen, battery, RSSI, Water_deep_cm]) + end + + return data + end #decodeUplink() + + static def add_web_sensor() + var fmt = global.LwSensorFormatter_cls() + var msg = "" + for sensor: global.psli5Nodes + var name = sensor[0] + if string.find(name, "PS-L-I5") > -1 # If LoRaWanName contains PS-L-I5 use PS-L-I5- + name = string.format("PS-L-I5-%i", sensor[1]) + end + var name_tooltip = "Dragino PS-L-I5" + var last_seen = sensor[2] + var battery_last_seen = sensor[3] + var battery = sensor[4] + var rssi = sensor[5] + msg += fmt.header(name, name_tooltip, battery, battery_last_seen, rssi, last_seen) + + # Sensors + var Water_deep_cm = sensor[6] + msg += "┆" # | + msg += string.format(" ⭳️ %.1fcm", Water_deep_cm) # тн│ + msg += "{e}" # = + end + return msg + end #add_web_sensor() +end #class + +LwDeco = LwDecoPSLI5 diff --git a/tasmota/berry/extensions/LoRaWan_Decoders/SE01-L.be b/tasmota/berry/extensions/LoRaWan_Decoders/SE01-L.be new file mode 100644 index 000000000..f67cada31 --- /dev/null +++ b/tasmota/berry/extensions/LoRaWan_Decoders/SE01-L.be @@ -0,0 +1,149 @@ +# LoRaWAN Decoder file for Dragino SE01-LB/LS Soil Sensor +# URL: https://www.dragino.com/products/agriculture-weather-station/item/277-se01-lb.html +# File Name: SE01-L.be +# +# References +# User Manual: https://wiki.dragino.com/xwiki/bin/view/Main/User%20Manual%20for%20LoRaWAN%20End%20Nodes/SE01-LB_LoRaWAN_Soil%20Moisture%26EC_Sensor_User_Manual/ +# TTN Device Repository:https://github.com/TheThingsNetwork/lorawan-devices/blob/master/vendor/dragino/lse01-121.js + +import string + +if !global.se01LNodes # data survive to decoder reload + global.se01LNodes = {} +end + +class LwDecoSE01L + static def decodeUplink(Name, Node, RSSI, FPort, Bytes) + var data = {"Device":"Dragino SE01-LB/LS"} + + var valid_values = false + var last_seen = 1451602800 + var battery_last_seen = 1451602800 + var battery = 1000 + var rssi = RSSI + + var temp = 1000 + var conductivity=0 + var moisture=0 + var dielectric=0 + var mod + var i_flag # 0: Normal uplink packet, 1: Interrupt Uplink Packet. + var s_flag # 0: No sensor was identified, 1: The sensor has been identified + + if global.se01LNodes.find(Node) + last_seen = global.se01LNodes.item(Node)[2] + battery_last_seen = global.se01LNodes.item(Node)[3] + battery = global.se01LNodes.item(Node)[4] + rssi = global.se01LNodes.item(Node)[5] + temp = global.se01LNodes.item(Node)[6] + conductivity = global.se01LNodes.item(Node)[7] + moisture = global.se01LNodes.item(Node)[8] + dielectric = global.se01LNodes.item(Node)[9] + end + + ## SENSOR DATA ## + #e.g. 0f5a 0ccc 0000 079d 0000 10 + # Battery ExtTemp Moisture Temp EC Mode,Flags + if 2 == FPort && ( Bytes.size() == 11 || Bytes.size() == 15) + last_seen = tasmota.rtc('local') + + battery_last_seen = tasmota.rtc('local') + battery=((Bytes[0]<<8 | Bytes[1]) & 0x3FFF)/1000.0 ##Battery,units:V + s_flag = (Bytes[10] >> 4) & 0x01 + i_flag = Bytes[10] & 0x0f + mod=(Bytes[10]>>7)&0x01 + + if 0==mod #Default mode + moisture=((Bytes[4]<<8 | Bytes[5])/100.0) ##moisture,units:% + conductivity=Bytes[8]<<8 | Bytes[9] + var value=Bytes[6]<<8 | Bytes[7] + if((value & 0x8000)>>15 == 0) + temp=(value/100.0) + else + temp=((value-0xFFFF)/100.0) + end + data.insert("Mode", "Default") + data.insert("Temp", temp) + + else #Raw Data mode + conductivity=Bytes[4]<<8 | Bytes[5] + moisture = Bytes[6]<<8 | Bytes[7] + dielectric = ((Bytes[8]<<8 | Bytes[9])/10.0) + data.insert("Mode", "Raw") + data.insert("DielectricConstant", dielectric) + end + + data.insert("BattV",battery) + data.insert("Moisture", moisture) + data.insert("Conductivity", conductivity) + data.insert("i_flag", i_flag) + data.insert("s_flag", s_flag) + + valid_values = true + + ## STATUS DATA ## + elif 5 == FPort && Bytes.size() == 7 + data.insert("Sensor_Model",Bytes[0]) + 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("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 + + if valid_values + if global.se01LNodes.find(Node) + global.se01LNodes.remove(Node) + end + # sensor[0] [1] [2] [3] [4] [5] [6] [7] [8] [9] [10] + global.se01LNodes.insert(Node, [Name, Node, last_seen, battery_last_seen, battery, RSSI, temp, conductivity, moisture, dielectric, mod]) + end + + return data + end #decodeUplink() + + static def add_web_sensor() + var fmt = global.LwSensorFormatter_cls() + var msg = "" + for sensor: global.se01LNodes + var name = sensor[0] + if string.find(name, "SE01-L") > -1 # If LoRaWanName contains SE01-L use SE01-L- + name = string.format("SE01-L-%i", sensor[1]) + end + var name_tooltip = "Dragino SE01-L" + var last_seen = sensor[2] + var battery_last_seen = sensor[3] + var battery = sensor[4] + var rssi = sensor[5] + msg += fmt.header(name, name_tooltip, battery, battery_last_seen, rssi, last_seen) + + # Sensors + var temp = sensor[6] + var conductivity = sensor[7] + var moisture = sensor[8] + var dielectric = sensor[9] + var mod = sensor[10] + + msg += "┆" # | + if mod + msg += string.format(" κ %.1f", dielectric ) # Kappa - dielectric + msg += string.format(" 💧️ %u", moisture) # Raindrop - moisture + msg += string.format(" σ %u", conductivity) # Sigma - conductivity + msg += " (raw)" + else + msg += string.format(" ☀️ %.1f°C", temp) # Sunshine/Color - Temperature + msg += string.format(" 💧️ %.1f%%", moisture) # Raindrop/Color - moisture + msg += string.format(" σ %uuS/cm", conductivity) # Sigma - conductivity + end + + msg += "{e}" # = + end + return msg + end #add_web_sensor() +end #class + +global.LwDeco = LwDecoSE01L diff --git a/tasmota/berry/extensions/LoRaWan_Decoders/SN50v3L.be b/tasmota/berry/extensions/LoRaWan_Decoders/SN50v3L.be new file mode 100644 index 000000000..536bc9e23 --- /dev/null +++ b/tasmota/berry/extensions/LoRaWan_Decoders/SN50v3L.be @@ -0,0 +1,119 @@ +# LoRaWAN Decoder file for Dragino SN50v3-LB/LS +# +# References +# User Manual: https://wiki.dragino.com/xwiki/bin/view/Main/User%20Manual%20for%20LoRaWAN%20End%20Nodes/SN50v3-LB/ +# Codec Repository: https://github.com/dragino/dragino-end-node-decoder/tree/main/SN50_v3-LB + +import string + +if !global.DrgSN50v3LNodes # data survive to decoder reload + global.DrgSN50v3LNodes = {} +end + +class LwDecoDrgSN50v3L + static def decodeUplink(Name, Node, RSSI, FPort, Bytes) + var data = {"Device":"Dragino SN50v3-L"} + + var valid_values = false + var last_seen = 1451602800 + var battery_last_seen = 1451602800 + var battery = 1000 + var rssi = RSSI + var WorkingMode ='' + var WorkingModes = ['IIC','Distance', '3ADC+IIC', '3DS18B20','Weight','1Count','3Interrupt','3ADC+1DS18B20','3DS18B20+2Count','PWM','TMP117','Count+SHT31'] + + if global.DrgSN50v3LNodes.find(Node) + last_seen = global.DrgSN50v3LNodes.item(Node)[2] + battery_last_seen = global.DrgSN50v3LNodes.item(Node)[3] + battery = global.DrgSN50v3LNodes.item(Node)[4] + rssi = global.DrgSN50v3LNodes.item(Node)[5] + end + + ## SENSOR DATA ## + if 2==FPort && Bytes.size()>10 #Variable length, depending on mode, but always 11 bytes or more + valid_values = true + last_seen = tasmota.rtc('local') + + var mode=(Bytes[6] & 0x7C)>>2 + if (mode+1) > size(WorkingModes) mode = 0 end + WorkingMode = WorkingModes[mode] + data.insert("WorkingMode", WorkingMode) #mode in data = 0..11. Mode in documentation = 1..12 + + battery = (Bytes[0]<<8 | Bytes[1])/1000.0 + data.insert("BattV", battery) + battery_last_seen = tasmota.rtc('local') + + ### TBA - handle all of the many cases + if 0==mode # Mode 1 (default) + if((Bytes[2]!=0x7f)||(Bytes[3]!=0xFF)) data.insert('TempC1',(Bytes[2]<<8 | Bytes[3])/10.0) end + data.insert('Digital_IStatus', (Bytes[6]&0x02)? 'High':'Low') + data.insert('ADC1_V',(Bytes[4]<<8 | Bytes[5])/1000.0) + + data.insert('EXTI_Trigger',(Bytes[6] & 0x01)? 'TRUE':'FALSE') + data.insert('Door_status' ,(Bytes[6] & 0x80)? 'CLOSE':'OPEN') + + if((Bytes[9]<<8 | Bytes[10])==0) + data.insert('Illum',(Bytes[7]<<8 | Bytes[8])) + else + var noshowTemp = ((Bytes[7]==0x7f)&&(Bytes[8]==0xff))||((Bytes[7]==0xff)&&(Bytes[8]==0xff)) + if !noshowTemp data.insert('TempC_SHT',((Bytes[7]<<24>>16 | Bytes[8])/10.0)) end + end + + if((Bytes[9]!=0xff)||(Bytes[10]!=0xff)) data.insert('Hum_SHT',(((Bytes[9]<<8 | Bytes[10])/10.0))) end + + end #mode + + ## STATUS DATA ## + elif 5 == FPort && Bytes.size() == 7 + data.insert("Sensor_Model",Bytes[0]) + 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("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 + + if valid_values + if global.DrgSN50v3LNodes.find(Node) + global.DrgSN50v3LNodes.remove(Node) + end + # sensor[0] [1] [2] [3] [4] [5] [6] + global.DrgSN50v3LNodes.insert(Node, [Name, Node, last_seen, battery_last_seen, battery, RSSI, WorkingMode]) + end + + return data + end #decodeUplink() + + static def add_web_sensor() + var fmt = global.LwSensorFormatter_cls() + var msg = "" + for sensor: global.DrgSN50v3LNodes + var name = sensor[0] + if string.find(name, "SN50v3-L") > -1 # If LoRaWanName contains SN50v3 use SN50v3- + name = string.format("SN50v3-L-%i", sensor[1]) + end + var name_tooltip = "Dragino SN50v3-L" + var last_seen = sensor[2] + var battery_last_seen = sensor[3] + var battery = sensor[4] + var rssi = sensor[5] + var workingMode = sensor[6] + + msg += fmt.header(name, name_tooltip, battery, battery_last_seen, rssi, last_seen) + + # Sensors + msg += "┆" # | + + msg += string.format(" ️ %s", workingMode) + + msg += "{e}" # = + end + return msg + end #add_web_sensor() +end #class + +global.LwDeco = LwDecoDrgSN50v3L diff --git a/tasmota/berry/extensions/LoRaWan_Decoders/WS202.be b/tasmota/berry/extensions/LoRaWan_Decoders/WS202.be new file mode 100644 index 000000000..0a74af8c2 --- /dev/null +++ b/tasmota/berry/extensions/LoRaWan_Decoders/WS202.be @@ -0,0 +1,118 @@ +# LoRaWAN Decoder file for Milesight WS202 +# +# References +# WS202 User Manual: https://resource.milesight.com/milesight/iot/document/ws202-user-guide-en.pdf +# TTN Device Repository: https://github.com/TheThingsNetwork/lorawan-devices/blob/master/vendor/milesight-iot/ws202.js + +import string + +if !global.ws202Nodes # data survive to decoder reload + global.ws202Nodes = {} +end + +class LwDecoWS202 + static def decodeUplink(Name, Node, RSSI, FPort, Bytes) + var data = {"Device":"Milesight WS202"} + + var valid_values = false + + var rssi = RSSI + var last_seen = 1451602800 + var battery = 0 + var battery_last_seen = 1451602800 + var pir = 0 # 0=Normal 1=Trigger + var pir_last_seen = 1451602800 + var light = 0 # 0=Dark >0=light + var light_last_seen = 1451602800 + + if global.ws202Nodes.find(Node) + battery = global.ws202Nodes.item(Node)[4] + battery_last_seen = global.ws202Nodes.item(Node)[5] + pir = global.ws202Nodes.item(Node)[6] + pir_last_seen = global.ws202Nodes.item(Node)[7] + light = global.ws202Nodes.item(Node)[8] + light_last_seen = global.ws202Nodes.item(Node)[9] + end + + var i = 0 + while i < (Bytes.size()-1) + last_seen = tasmota.rtc('local') + + var channel_id = Bytes[i] + i += 1 + var channel_type = Bytes[i] + i += 1 + + if channel_id == 0x01 && channel_type == 0x75 + battery_last_seen = tasmota.rtc('local') + battery = Bytes[i] + i += 1 + valid_values = true + + elif channel_id == 0x03 && channel_type == 0x00 + pir_last_seen = tasmota.rtc('local') + pir = Bytes[i] == 0 ? false : true + data.insert("PIR", pir) + i += 1 + valid_values = true + + elif channel_id == 0x04 && channel_type == 0x00 + light_last_seen = tasmota.rtc('local') + light = Bytes[i] + data.insert("Light", light) + i += 1 + valid_values = true + + else + # Ignore other + valid_values = false + i = Bytes.size() + end + end + + if valid_values + if global.ws202Nodes.find(Node) + global.ws202Nodes.remove(Node) + end + # sensor[0] [1] [2] [3] [4] [5] [6] [7] [8] [9] + global.ws202Nodes.insert(Node, [Name, Node, last_seen, battery_last_seen, battery, RSSI, pir, pir_last_seen, light, light_last_seen]) + end + + return data + end #decodeUplink() + + static def add_web_sensor() + var fmt = global.LwSensorFormatter_cls() + var msg = "" + for sensor: global.ws202Nodes + var name = sensor[0] + if string.find(name, "WS202") > -1 # If LoRaWanName contains WS202 use WS202- + name = string.format("WS202-%i", sensor[1]) + end + var name_tooltip = "Milesight WS202" + var last_seen = sensor[2] + var battery_last_seen = sensor[3] + var battery = sensor[4] + var rssi = sensor[5] + msg += fmt.header(name, name_tooltip, battery + 100000, battery_last_seen, rssi, last_seen) + + # Sensors + var pir = fmt.dhm(sensor[7]) + var pir_ls = fmt.dhm_tt(sensor[7]) + var pir_alt = (sensor[6] == true ? "🚫" : "🆓") # No Entry 🚫 / Free 🆓 + + var light = fmt.dhm(sensor[9]) + var light_ls = fmt.dhm_tt(sensor[9]) + var light_alt = (sensor[8] == 0) ? "🌕" : "🌞" # Moon 🌕 / Sun 🌞 + + msg += fmt.start_line() + .add_sensor( "string", pir, pir_ls, pir_alt ) + .add_sensor( "string", light, light_ls, light_alt ) + .end_line() + .get_msg() + end + return msg + end #add_web_sensor() +end #class + +global.LwDeco = LwDecoWS202 diff --git a/tasmota/berry/extensions/LoRaWan_Decoders/WS301.be b/tasmota/berry/extensions/LoRaWan_Decoders/WS301.be new file mode 100644 index 000000000..502473d04 --- /dev/null +++ b/tasmota/berry/extensions/LoRaWan_Decoders/WS301.be @@ -0,0 +1,118 @@ +# LoRaWAN Decoder file for Milesight WS301 +# +# References +# WS301 User Manual: https://resource.milesight.com/milesight/iot/document/ws301-user-guide-en.pdf +# TTN Device Repository: https://github.com/TheThingsNetwork/lorawan-devices/blob/master/vendor/milesight-iot/ws301.js + +import string + +if !global.ws301Nodes # data survive to decoder reload + global.ws301Nodes = {} +end + +class LwDecoWS301 + static def decodeUplink(Name, Node, RSSI, FPort, Bytes) + var data = {"Device":"Milesight WS301"} + + var valid_values = false + + var rssi = RSSI + var last_seen = 1451602800 + var battery = 0 + var battery_last_seen = 1451602800 + var door_open = false + var door_open_last_seen = 1451602800 + var installed = false + var installed_last_seen = 1451602800 + + if global.ws301Nodes.find(Node) + battery = global.ws301Nodes.item(Node)[4] + battery_last_seen = global.ws301Nodes.item(Node)[5] + door_open = global.ws301Nodes.item(Node)[6] + door_open_last_seen = global.ws301Nodes.item(Node)[7] + installed = global.ws301Nodes.item(Node)[8] + installed_last_seen = global.ws301Nodes.item(Node)[9] + end + + var i = 0 + while i < (Bytes.size()-1) + last_seen = tasmota.rtc('local') + + var channel_id = Bytes[i] + i += 1 + var channel_type = Bytes[i] + i += 1 + + if channel_id == 0x01 && channel_type == 0x75 + battery_last_seen = tasmota.rtc('local') + battery = Bytes[i] + i += 1 + valid_values = true + + elif channel_id == 0x03 && channel_type == 0x00 + door_open_last_seen = tasmota.rtc('local') + door_open = Bytes[i] == 0 ? false : true + data.insert("DoorOpen", ( door_open ) ? true : false) + i += 1 + valid_values = true + + elif channel_id == 0x04 && channel_type == 0x00 + installed_last_seen = tasmota.rtc('local') + installed = Bytes[i] == 0 ? true : false + data.insert("Installed", ( installed ) ? true : false) + i += 1 + valid_values = true + + else + # Ignore other + valid_values = false + i = Bytes.size() + end + end + + if valid_values + if global.ws301Nodes.find(Node) + global.ws301Nodes.remove(Node) + end + # sensor[0] [1] [2] [3] [4] [5] [6] [7] [8] [9] + global.ws301Nodes.insert(Node, [Name, Node, last_seen, battery_last_seen, battery, RSSI, door_open, door_open_last_seen, installed, installed_last_seen]) + end + + return data + end #decodeUplink() + + static def add_web_sensor() + var fmt = global.LwSensorFormatter_cls() + var msg = "" + for sensor: global.ws301Nodes + var name = sensor[0] + if string.find(name, "WS301") > -1 # If LoRaWanName contains WS301 use WS301- + name = string.format("WS301-%i", sensor[1]) + end + var name_tooltip = "Milesight WS301" + var last_seen = sensor[2] + var battery_last_seen = sensor[3] + var battery = sensor[4] + var rssi = sensor[5] + msg += fmt.header(name, name_tooltip, battery + 100000, battery_last_seen, rssi, last_seen) + + # Sensors + var dopen = fmt.dhm(sensor[7]) + var dopen_tt = nil + var dopen_alt = (sensor[6] == true) ? "🔓" : "🔒" # Open Lock 🔓 / Lock 🔒 + + var inst = fmt.dhm(sensor[9]) + var inst_tt = nil + var inst_alt = (sensor[8] == true) ? "✅" : "❌" # Heavy Check Mark ✅ / Cross Mark ❌ + + msg += fmt.start_line() + .add_sensor( "string", dopen, dopen_tt, dopen_alt ) + .add_sensor( "string", inst, inst_tt, inst_alt ) + .end_line() + .get_msg() + end + return msg + end #add_web_sensor() +end #class + +global.LwDeco = LwDecoWS301 diff --git a/tasmota/berry/extensions/LoRaWan_Decoders/WS522.be b/tasmota/berry/extensions/LoRaWan_Decoders/WS522.be new file mode 100644 index 000000000..68658a8a2 --- /dev/null +++ b/tasmota/berry/extensions/LoRaWan_Decoders/WS522.be @@ -0,0 +1,369 @@ +# LoRaWAN Decoder file for Milesight WS522 +# +# References +# WS522 User Manual: https://resource.milesight.com/milesight/iot/document/ws52x-user-guide-en.pdf +# Device Decoder: https://github.com/Milesight-IoT/SensorDecoders/blob/main/WS_Series/WS52x/WS52x_Decoder.js + +import string + +if !global.ws522Nodes # data survive to decoder reload + global.ws522Nodes = {} +end + +class LwDecoWS522 + static def decodeUplink(Name, Node, RSSI, FPort, Bytes) + var data = {"Device":"Milesight WS522"} + + var valid_values = false + + var rssi = RSSI + + var last_seen = 1451602800 + + var voltage = 0.0 # 0.1 Volt + var active_power = 0 # Watt + var power_factor = 0 # % + var energy_sum = 0 # kW + var current = 0 + var button_state = false # false=close true=open + + var voltage_ls = 1451602800 + var active_power_ls = 1451602800 + var power_factor_ls = 1451602800 + var energy_sum_ls = 1451602800 + var current_ls = 1451602800 + var button_state_ls = 1451602800 + var command_init = false + + if global.ws522Nodes.find(Node) + voltage = global.ws522Nodes.item(Node)[4] + active_power = global.ws522Nodes.item(Node)[5] + power_factor = global.ws522Nodes.item(Node)[6] + energy_sum = global.ws522Nodes.item(Node)[7] + current = global.ws522Nodes.item(Node)[8] + button_state = global.ws522Nodes.item(Node)[9] + + voltage_ls = global.ws522Nodes.item(Node)[10] + active_power_ls = global.ws522Nodes.item(Node)[11] + power_factor_ls = global.ws522Nodes.item(Node)[12] + energy_sum_ls = global.ws522Nodes.item(Node)[13] + current_ls = global.ws522Nodes.item(Node)[14] + button_state_ls = global.ws522Nodes.item(Node)[15] + + command_init = global.ws522Nodes.item(Node)[16] + end + + var i = 0 + while i < (Bytes.size()-1) + last_seen = tasmota.rtc('local') + valid_values = true + + var channel_id = Bytes[i] + i += 1 + var channel_type = Bytes[i] + i += 1 + + # VOLTAGE + if channel_id == 0x03 && channel_type == 0x74 + voltage_ls = tasmota.rtc('local') + voltage = ((Bytes[i+1] << 8) | Bytes[i]) / 10.0 + data.insert("Voltage", voltage) + i += 2 + + # ACTIVE POWER + elif channel_id == 0x04 && channel_type == 0x80 + active_power_ls = tasmota.rtc('local') + active_power = (Bytes[i+3] << 24) | (Bytes[i+2] << 16) | (Bytes[i+1] << 8) | Bytes[i] + data.insert("Active_Power", active_power) + i += 4 + + # POWER FACTOR + elif channel_id == 0x05 && channel_type == 0x81 + power_factor_ls = tasmota.rtc('local') + power_factor = Bytes[i] + data.insert("Power_Factor", power_factor) + i += 1 + + # ENERGY SUM + elif channel_id == 0x06 && channel_type == 0x83 + energy_sum_ls = tasmota.rtc('local') + energy_sum = (Bytes[i+3] << 24) | (Bytes[i+2] << 16) | (Bytes[i+1] << 8) | Bytes[i] + data.insert("Energy_Sum", energy_sum) + i += 4 + + # CURRENT + elif channel_id == 0x07 && channel_type == 0xc9 + current_ls = tasmota.rtc('local') + current = (Bytes[i+1] << 8) | Bytes[i] + data.insert("Current", current) + i += 2 + + # STATE + elif channel_id == 0x08 && channel_type == 0x70 + button_state_ls = tasmota.rtc('local') + button_state = Bytes[i] == 1 ? true : false + data.insert("Button_State", button_state ? "Open" : "Close" ) + i += 1 + + # FE03(ReportInterval) 3C00=>60 5802=>600 + elif channel_id == 0xFE && channel_type == 0x02 + data.insert("Period", ((Bytes[i+1] << 8) | Bytes[i]) ) + i += 2 + + # FF01(ProtocolVersion) 01=>V1 + elif channel_id == 0xFF && channel_type == 0x01 + data.insert("Protocol Version", Bytes[i] ) + i += 1 + + # FF09(HardwareVersion) 0140=>V1.4 + elif channel_id == 0xFF && channel_type == 0x09 + data.insert("Hardware Version", format("v%02x.%02x", Bytes[i], Bytes[i+1]) ) + i += 2 + + # FF0a(SoftwareVersion) 0114=>V1.14 + elif channel_id == 0xFF && channel_type == 0x0A + data.insert("Software Version", format("v%02x.%02x", Bytes[i], Bytes[i+1]) ) + i += 2 + + elif channel_id == 0xFF && channel_type == 0x0B i += 1 # FF0b(PowerOn) Deviceison + elif channel_id == 0xFF && channel_type == 0x16 i += 8 # FF16(DeviceSN) 16digits + elif channel_id == 0xFF && channel_type == 0x0F i += 1 # FF0f(DeviceType) 00:ClassA,01:ClassB,02:ClassC + elif channel_id == 0xFF && channel_type == 0xFF i += 2 # TSL VERSION + elif channel_id == 0xFF && channel_type == 0xFE i += 1 # RESET EVENT + + elif channel_id == 0xFE && channel_type == 0x03 i += 2 # id=0xFE yy Downlink Reporting Event + elif channel_id == 0xFE && channel_type == 0x10 i += 1 + elif channel_id == 0xFE && channel_type == 0x22 i += 4 + elif channel_id == 0xFE && channel_type == 0x23 i += 2 + elif channel_id == 0xFE && channel_type == 0x24 i += 2 + elif channel_id == 0xFE && channel_type == 0x25 i += 2 + elif channel_id == 0xFE && channel_type == 0x26 i += 1 + elif channel_id == 0xFE && channel_type == 0x27 i += 1 + elif channel_id == 0xFE && channel_type == 0x28 i += 1 + elif channel_id == 0xFE && channel_type == 0x2F i += 1 + elif channel_id == 0xFE && channel_type == 0x30 i += 2 + + else + log( string.format("WS522: something missing? id={%s} type={%s}", channel_id, channel_type), 1) + + # Ignore other + valid_values = false + i = Bytes.size() + end + end + + if valid_values + if !command_init + # + # Downlink Commands + # ================= =============================== + # ✅ 08 00 00 FF Close + # ✅ 08 01 00 FF Open + # ✅ FF 03 ss ss SetReportingInterval (2 bytes, seconds) + # ✅ FF 10 FF Reboot + # ✅ FF 22 00 ss ss aa AddDelayTask (ss=delay seconds, aa=action 10=close/11=open) + # ✅ FF 23 00 FF DeleteDelayTask + # ❓ FF 24 xx yy OvercurrentAlarm (xx: 00=off/01=on, yy=threshold) + # ✅ FF 25 00 xx ButtonLock (xx: 00=off/80=on) + # ✅ FF 26 yy PowerConsumption (yy: 00=off/01=on) + # ✅ FF 27 FF ResetPowerConsumption + # ✅ FF 28 FF EnquireElectricalStatus + # ✅ FF 2F xx LEDMode (xx: 00=off/01=on) + # ❓ FF 30 xx yy OvercurrentProtection (XX: 00=off/01=on, YY=threshold) + # + # ✅ = Verified ❓= Not verified yet ❌=Issue, under investigation + # + var lwdecode = global.LwTools_cls() + var pfx = 'LwWS522' + + tasmota.remove_cmd( pfx + 'Power' ) + tasmota.add_cmd( pfx + 'Power', + def (cmd, idx, payload) + return lwdecode.SendDownlinkMap(global.ws522Nodes, cmd, idx, payload, { '1|ON': ['080100FF', 'ON'], '0|OFF': ['080000FF', 'OFF'] }) + end + ) + + tasmota.remove_cmd( pfx + 'Period' ) + tasmota.add_cmd( pfx + 'Period', + def (cmd, idx, payload) + return lwdecode.SendDownlink(global.ws522Nodes, cmd, idx, format('FF03%s',lwdecode.uint16le(number(payload))), number(payload)) + end + ) + + tasmota.remove_cmd( pfx + 'Reboot' ) + tasmota.add_cmd( pfx + 'Reboot', + def (cmd, idx, payload) + return lwdecode.SendDownlink(global.ws522Nodes, cmd, idx, 'FF10FF', 'Done') + end + ) + + tasmota.remove_cmd( pfx + 'ResetPowerUsage' ) + tasmota.add_cmd( pfx + 'ResetPowerUsage', + def (cmd, idx, payload) + return lwdecode.SendDownlink(global.ws522Nodes, cmd, idx, 'FF27FF', 'Done') + end + ) + + tasmota.remove_cmd( pfx + 'PowerLock' ) + tasmota.add_cmd( pfx + 'PowerLock', + def (cmd, idx, payload) + return lwdecode.SendDownlinkMap(global.ws522Nodes, cmd, idx, payload, { '1|ON': ['FF250080', 'ON'], '0|OFF': ['FF250000', 'OFF'] }) + end + ) + + tasmota.remove_cmd( pfx + 'DelayTask' ) + tasmota.add_cmd( pfx + 'DelayTask', + def (cmd, idx, payload) + var parts = string.split(payload,',') + if parts.size() != 2 + return tasmota.resp_cmnd_str("Usage: delay_seconds,action (action: 0=close, 1=open)") + end + var delay = number(parts[0]) + var action = number(parts[1]) == 1 ? '11' : '10' + return lwdecode.SendDownlink(global.ws522Nodes, cmd, idx, format('FF2200%s%s',lwdecode.uint16le(delay),action), payload) + end + ) + + tasmota.remove_cmd( pfx + 'DelTask' ) + tasmota.add_cmd( pfx + 'DelTask', + def (cmd, idx, payload) + return lwdecode.SendDownlink(global.ws522Nodes, cmd, idx, 'FF2300FF', 'Done') + end + ) + + tasmota.remove_cmd( pfx + 'OcAlarm' ) + tasmota.add_cmd( pfx + 'OcAlarm', + def (cmd, idx, payload) + var parts = string.split(payload,',') + if parts.size() != 2 + return tasmota.resp_cmnd_str("Usage: enable,threshold (enable: 0/1, threshold: 0-255)") + end + var enable = number(parts[0]) ? '01' : '00' + var threshold = format('%02X', number(parts[1])) + return lwdecode.SendDownlink(global.ws522Nodes, cmd, idx, format('FF24%s%s',enable,threshold), payload) + end + ) + + tasmota.remove_cmd( pfx + 'PwrUsage' ) + tasmota.add_cmd( pfx + 'PwrUsage', + def (cmd, idx, payload) + return lwdecode.SendDownlinkMap(global.ws522Nodes, cmd, idx, payload, { '1|ON': ['FF2601FF', 'ON'], '0|OFF': ['FF2600FF', 'OFF'] }) + end + ) + + tasmota.remove_cmd( pfx + 'Status' ) + tasmota.add_cmd( pfx + 'Status', + def (cmd, idx, payload) + return lwdecode.SendDownlink(global.ws522Nodes, cmd, idx, 'FF28FF', 'Done') + end + ) + + tasmota.remove_cmd( pfx + 'LED' ) + tasmota.add_cmd( pfx + 'LED', + def (cmd, idx, payload) + return lwdecode.SendDownlinkMap(global.ws522Nodes, cmd, idx, payload, { '1|ON': ['FF2F01', 'ON'], '0|OFF': ['FF2F00', 'OFF'] }) + end + ) + + tasmota.remove_cmd( pfx + 'OcProt' ) + tasmota.add_cmd( pfx + 'OcProt', + def (cmd, idx, payload) + var parts = string.split(payload,',') + if parts.size() != 2 + return tasmota.resp_cmnd_str("Usage: enable,threshold (enable: 0/1, threshold: 0-255)") + end + var enable = number(parts[0]) ? '01' : '00' + var threshold = format('%02X', number(parts[1])) + return lwdecode.SendDownlink(global.ws522Nodes, cmd, idx, format('FF30%s%s',enable,threshold), payload) + end + ) + + command_init = true + end + + if global.ws522Nodes.find(Node) + global.ws522Nodes.remove(Node) + end + + global.ws522Nodes.insert(Node, + [ # sensor + Name, # [0] + Node, # [1] + last_seen, # [2] + rssi, # [3] + voltage, # [4] + active_power, # [5] + power_factor, # [6] + energy_sum, # [7] + current, # [8] + button_state, # [9] + voltage_ls, # [10] + active_power_ls, # [11] + power_factor_ls, # [12] + energy_sum_ls, # [13] + current_ls, # [14] + button_state_ls, # [15] + command_init # [16] + ] + ) + end + + return data + end #decodeUplink() + + static def add_web_sensor() + var fmt = global.LwSensorFormatter_cls() + var msg = "" + for sensor: global.ws522Nodes + var name = sensor[0] + + # If LoRaWanName contains WS522 use WS522- + if string.find(name, "WS522") > -1 + name = string.format("WS522-%i", sensor[1]) + end + + var name_tooltip = "Milesight WS522" + + var last_seen = sensor[2] + var rssi = sensor[3] + + msg += fmt.header(name, name_tooltip, 1000, last_seen, rssi, last_seen) + + # Sensors + var voltage = sensor[4] + var voltage_tt = fmt.dhm(sensor[10]) + + var active_power = sensor[5] + var active_power_tt = fmt.dhm(sensor[11]) + + var power_factor = sensor[6] + var power_factor_tt = fmt.dhm(sensor[12]) + + var current = sensor[8] + var current_tt = fmt.dhm(sensor[14]) + + var button_state = fmt.dhm(sensor[15]) + var button_state_tt = fmt.dhm(sensor[15]) + var button_state_icon = (sensor[9] ? " 🟢 " : " ⚫ ") # Large Green Circle 🟢 | Medium Black Circle ⚫ + + var energy_sum = sensor[7] + var energy_sum_tt = fmt.dhm(sensor[13] ) + + # Formatter Value Tooltip alternative icon + # ================ ============ ================== ================ + msg += fmt.start_line() + .add_sensor("volt", voltage, voltage_tt ) + .add_sensor("milliamp", current, current_tt ) + .add_sensor("power_factor%", power_factor, power_factor_tt ) + .add_sensor("power", active_power, active_power_tt ) + .next_line() + .add_sensor("string", button_state, button_state_tt, button_state_icon ) + .add_sensor("energy", energy_sum, energy_sum_tt ) + .end_line() + .get_msg() + end + return msg + end #add_web_sensor() +end #class + +global.LwDeco = LwDecoWS522 diff --git a/tasmota/berry/extensions/LoRaWan_Decoders/autoexec.be b/tasmota/berry/extensions/LoRaWan_Decoders/autoexec.be new file mode 100644 index 000000000..202cc2ace --- /dev/null +++ b/tasmota/berry/extensions/LoRaWan_Decoders/autoexec.be @@ -0,0 +1,9 @@ +# rm LoRaWan_Decoders.tapp; zip -j -0 LoRaWan_Decoders.tapp LoRaWan_Decoders/* +do # embed in `do` so we don't add anything to global namespace + import introspect + var lorawan_decoders = introspect.module('lorawan_decoders', true) # load module but don't cache + tasmota.add_extension(lorawan_decoders) +end + +# to remove: +# tasmota.unload_extension('LoRaWan Decoders') diff --git a/tasmota/berry/extensions/LoRaWan_Decoders/lorawan_decoders.be b/tasmota/berry/extensions/LoRaWan_Decoders/lorawan_decoders.be new file mode 100644 index 000000000..e6275bb29 --- /dev/null +++ b/tasmota/berry/extensions/LoRaWan_Decoders/lorawan_decoders.be @@ -0,0 +1,590 @@ +################################################################################### +# Decode LoRaWan devices +# +# Copyright (C) 2025 Stephan Hadinger & Theo Arends +# +# Decoder files are modeled on the *.js files found here: +# https://github.com/TheThingsNetwork/lorawan-devices/tree/master/vendor +# +# rm LoRaWan_Decoders.tapp; zip -j -0 LoRaWan_Decoders.tapp LoRaWan_Decoders/* +################################################################################### + +import mqtt +import string + +################################################################################### +# Display Configuration LoRaWan GUI +#---------------------------------------------------------------------------------# +import webserver + +class lorawan_settings + var max_node_cached + + ################################################################################# + # init + # + # install the extension and allocate all resources + ################################################################################# + def init() + self.max_node_cached = nil + + tasmota.add_driver(self) + if tasmota.is_network_up() + self.web_add_handler() # if init is called after the network is up, `web_add_handler` event is not fired + end + end + + def close() + webserver.remove_route("/lrw") + tasmota.remove_driver(self) + end + + def web_add_config_button() + webserver.content_send("

") + end + + def _get_max_nodes() + if !self.max_node_cached + var enables = string.split(tasmota.cmd('LoRaWanNode', true).find('LoRaWanNode'), ',') + self.max_node_cached = enables.size() + end + return self.max_node_cached + end + + #- this method displays the web page -# + def pageLoRaWAN() + if !webserver.check_privileged_access() return nil end + + var inode = 1 + var cmdArg + if webserver.has_arg('save') + inode = int(webserver.arg('node')) + tasmota.cmd(format('LoRaWanAppKey%i %s', inode, webserver.arg('ak')), true) + cmdArg = webserver.arg('dc') + if !cmdArg cmdArg = '"' end + tasmota.cmd(format('LoRaWanDecoder%i %s', inode, cmdArg), true) + cmdArg = webserver.arg('an') + if !cmdArg cmdArg = '"' end + tasmota.cmd(format('LoRaWanName%i %s', inode, cmdArg), true) + cmdArg = webserver.arg('ce') + if !cmdArg + cmdArg = '0' + else + cmdArg = '1' + end + tasmota.cmd(format('LoRaWanNode%i %s', inode, cmdArg), true) + end + + var appKey, decoder, name, enabled + var hintAK = '32 character Application Key' + var hintDecoder = 'Decoder file, ending in .be' + var hintAN = 'Device name for MQTT messages' + var arg = 'LoRaWanNode' + var enables = string.split(tasmota.cmd(arg, true).find(arg), ',') # [1,!2,!3,!4,5,6] + var maxnode = enables.size() + + webserver.content_start("LoRaWAN") #- title of the web page -# + webserver.content_send_style() #- send standard Tasmota styles -# + + webserver.content_send( + "" + "") + + webserver.content_send( + format("
" + " LoRaWan End Device " + "
")) #- Add space and indent to align form tabs -# + for node:1 .. maxnode + webserver.content_send(format("", node, node, node)) + end + webserver.content_send("




") #- Terminate indent and add space -# + + for node:1 .. maxnode + enabled = "" + if enables[node-1][0] != '!' + enabled = ' checked' + end + arg = format('LoRaWanAppKey%i', node) + appKey = tasmota.cmd(arg, true).find(arg) + arg = format('LoRaWanName%i', node) + name = tasmota.cmd(arg, true).find(arg) + arg = format('LoRaWanDecoder%i', node) + decoder = tasmota.cmd(arg, true).find(arg) + + webserver.content_send( + format("", node, enabled, hintAK, hintAK, appKey, hintAN, name, hintDecoder, hintDecoder, decoder, node)) + end + + webserver.content_send("
") + + webserver.content_button(webserver.BUTTON_CONFIGURATION) #- button back to conf page -# + webserver.content_stop() #- end of web page -# + end + + #- this is called at Tasmota start-up, as soon as Wifi/Eth is up and web server running -# + def web_add_handler() + #- we need to register a closure, not just a function, that captures the current instance -# + webserver.on("/lrw", / -> self.pageLoRaWAN()) + end +end +#---------------------------------------------------------------------------------# +# Display Configuration LoRaWan GUI +################################################################################### + + +################################################################################### +# global LwSensorFormatter_cls +#---------------------------------------------------------------------------------# +class LwSensorFormatter_cls + static var Formatter = { + "string": { "u": nil, "f": " %s", "i": nil }, + "volt": { "u": "V", "f": " %.1f", "i": "⚡" }, + "milliamp": { "u": "mA", "f": " %.0f", "i": "🔌" }, + "power_factor%": { "u": "%", "f": " %.0f", "i": "📊" }, + "power": { "u": "W", "f": " %.0f", "i": "💡" }, + "energy": { "u": "Wh", "f": " %.0f", "i": "🧮" }, + "altitude": { "u": "mt", "f": " %d", "i": "⛰" }, + "empty": { "u": nil, "f": nil, "i": nil } + } + + var msg_buffer + + def init() + self.msg_buffer = bytes(512) + self.msg_buffer.clear() + tasmota.add_driver(self) + end + + def close() + tasmota.remove_driver(self) + end + + def start_line() + self.msg_buffer .. "┆" + return self + end + + def end_line() + self.msg_buffer .. "{e}" + return self + end + + def next_line() + self.msg_buffer .. "{e}┆" + return self + end + + def begin_tooltip(ttip) + self.msg_buffer .. format(" 
", ttip) + return self + end + + def end_tooltip() + self.msg_buffer .. "
" + return self + end + + def add_link(title, url, target) + if !target target = "_blank" end + self.msg_buffer .. format(" %s", target, url, title) + return self + end + + def add_sensor(formatter, value, tooltip, alt_icon) + + if tooltip self.begin_tooltip(tooltip) end + + var fmt = self.Formatter.find(formatter) + + if alt_icon + self.msg_buffer .. format(" %s", alt_icon) + elif fmt && fmt.find("i") && fmt["i"] + self.msg_buffer .. format(" %s", fmt["i"]) + end + + if fmt && fmt.find("f") && fmt["f"] + self.msg_buffer .. format(fmt["f"], value) + else + self.msg_buffer .. str(value) + end + + if fmt && fmt.find("u") && fmt["u"] + self.msg_buffer .. format("%s", fmt["u"]) + end + + if tooltip self.end_tooltip() end + return self + end + + def get_msg() + return self.msg_buffer.asstring() + end + + def dhm(last_time) + var since = tasmota.rtc('local') - last_time + var unit = "d" + if since > 86400 + since /= 86400 + if since > 99 since = 99 end + elif since > 3600 + since /= 3600 + unit = "h" + else + since /= 60 + unit = "m" + end + return format("%02d%s", since, unit) + end + + def dhm_tt(last_time) + return format("Received %s ago", self.dhm(last_time)) + end + + def header(name, name_tooltip, battery, battery_last_seen, rssi, last_seen) + var msg = 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 > 98 batt_percent = 98 end # 98% / 14px = 7 + batt_percent /= 7 # 1..14px showing battery load + msg += format("", + battery, self.dhm(battery_last_seen), batt_percent) + elif battery >= 100000 && battery <= 100100 # battery already expressed in % + var pbatt = battery - 100000 + var batt_percent = pbatt + if batt_percent > 98 batt_percent = 98 end # 98% / 14px = 7 + batt_percent /= 7 # 1..14px showing battery load + msg += format("", + pbatt, self.dhm(battery_last_seen), batt_percent) + else + msg += " " + end + + if rssi < 1000 + if rssi < -132 rssi = -132 end + var num_bars = 4 - ((rssi * -1) / 33) + msg += format("
", rssi) + for j:0..3 + msg += format("", j, (num_bars < j) ? " o30" : "") # Bars + end + msg += "
" # Close RSSI + else + msg += " " + end + + msg += format("🕗%s", self.dhm(last_seen)) + + return msg + end +end + +global.LwSensorFormatter_cls = LwSensorFormatter_cls +#---------------------------------------------------------------------------------# +# global LwSensorFormatter_cls +################################################################################### + + +################################################################################### +# global LwTools_cls +#---------------------------------------------------------------------------------# +class LwTools_cls + def init() + tasmota.add_driver(self) + end + + def close() + tasmota.remove_driver(self) + end + + def uint16le(value) + return string.format( "%02x%02x", + value & 0xFF, + (value >> 8) & 0xFF + ) + end + + def uint32le(value) + return string.format( "%02x%02x%02x%02x", + value & 0xFF, + (value >> 8) & 0xFF, + (value >> 16) & 0xFF, + (value >> 24) & 0xFF + ) + end + + def SendDownlink(nodes, cmd, idx, payload, ok_result) + if !nodes.find(idx) return nil end + + var _send = 'LoRaWanSend' + var _cmdSend = _send + str(idx) + ' ' + payload + var _out = tasmota.cmd(_cmdSend, true) + + return tasmota.resp_cmnd( + format('{"%s%i":"%s","%s":"%s","Payload":"%s"}', + cmd, + idx, + ok_result, + _send, + _out[_send], + payload + ) + ) + end + + def SendDownlinkMap(nodes, cmd, idx, payload, choice_map) + var key = string.toupper(str(payload)) + for choice_key : choice_map.keys() + if string.find(choice_key, key) >= 0 && (choice_key == key || string.find(choice_key, '|' + key + '|') >= 0 || string.find(choice_key, key + '|') == 0 || string.find(choice_key, '|' + key) == size(choice_key) - size(key) - 1) + var choice = choice_map[choice_key] + return self.SendDownlink(nodes, cmd, idx, choice[0], choice[1]) + end + end + return tasmota.resp_cmnd_error() + end +end + +global.LwTools_cls = LwTools_cls +#---------------------------------------------------------------------------------# +# global LwTools_cls +################################################################################### + + +################################################################################### +# extension lorawan_decoders +#---------------------------------------------------------------------------------# +global.LwRegions = ["EU868","US915","IN865","AU915","KZ865","RU864","AS923","AS923-1","AS923-2","AS923-3"] +global.LwDeco = nil + +class lorawan_decoders + var lw_decoders + var topic_cached + var last_payload_hash + var web_msg_cache + var cache_timeout + var lw_settings + + ################################################################################# + # init + # + # install the extension and allocate all resources + ################################################################################# + def init() + self.lw_decoders = {} + self.last_payload_hash = 0 + self.web_msg_cache = "" + self.cache_timeout = 0 + + self._cache_topic() + + tasmota.add_driver(self) + + tasmota.add_rule("LwReceived", /value, trigger, payload -> self.lw_decode(payload)) + + tasmota.cmd('LoraOption3 off', true) # Disable embedded decoding + tasmota.cmd('SetOption100 off', true) # Keep LwReceived in JSON message + tasmota.cmd('SetOption118 off', true) # Keep SENSOR as subtopic name + tasmota.cmd('SetOption119 off', true) # Keep device address in JSON message +# tasmota.cmd('SetOption147 on', true) # Hide LwReceived MQTT message but keep rule processing + tasmota.cmd('LoRaWanBridge on', true) + + self.lw_settings = lorawan_settings() + end + + ################################################################################# + # unload + # + # Uninstall the extension and deallocate all resources + ################################################################################# + def unload() + tasmota.remove_rule("LwReceived") + tasmota.remove_driver(self.lw_settings) + tasmota.remove_driver(self) + global.undef("LwSensorFormatter_cls") + global.undef("LwTools_cls") + global.undef("LwRegions") + global.undef("LwDeco") + end + + def _cache_topic() + var full_topic = tasmota.cmd('_FullTopic',true)['FullTopic'] + var topic = tasmota.cmd('_Status',true)['Status']['Topic'] + var prefix = tasmota.cmd('_Prefix',true)['Prefix3'] + + self.topic_cached = string.replace(string.replace(full_topic, '%topic%', topic), '%prefix%', prefix) + 'SENSOR' + end + + def _calculate_payload_hash(payload) + var hash = 0 + for i:0..payload.size()-1 + hash = (hash * 31 + payload[i]) & 0xFFFFFFFF + end + return hash + end + + def lw_decode(data) + import json + + var device_data = data['LwReceived'] + var device_name = device_data.keys()() + var device_info = device_data[device_name] + + var decoder = device_info.find('Decoder') + if !decoder return true end + + var payload = device_info['Payload'] + if !payload || payload.size() == 0 return true end + + if !self.lw_decoders.find(decoder) + try + global.LwDeco = nil +# load(decoder) + load(".extensions/LoRaWan_Decoders.tapp#" + decoder) + if global.LwDeco + self.lw_decoders[decoder] = global.LwDeco + else + log("LwD: Unable to load decoder",1) + return true + end + except .. as e, m + log(format("LwD: Decoder load error: %s", m),1) + return true + end + end + + var hashCheck + # check if the decoder driver have the hashCheck properties + try + hashCheck = self.lw_decoders[decoder].hashCheck + except .. as e, m + hashCheck = true + end + + if hashCheck + var current_hash = self._calculate_payload_hash(payload) + if current_hash == self.last_payload_hash return true end + self.last_payload_hash = current_hash + end + + try + var decoded = self.lw_decoders[decoder].decodeUplink( + device_info['Name'], + device_info['Node'], + device_info['RSSI'], + device_info['FPort'], + payload + ) + + decoded['Node'] = device_info['Node'] + decoded['RSSI'] = device_info['RSSI'] + + var mqtt_data + if tasmota.get_option(83) == 0 # SetOption83 - Remove LwDecoded form JSON message (1) + mqtt_data = {"LwDecoded": {device_name: decoded}} + else + mqtt_data = {device_name: decoded} + end + + var topic + if tasmota.get_option(89) == 1 # SetOption89 - Distinct MQTT topics per device (1) + topic = format("%s/%s%s", self.topic_cached, device_info['DevEUIh'], device_info['DevEUIl']) + else + topic = self.topic_cached + end + + mqtt.publish(topic, json.dump(mqtt_data)) + tasmota.global.restart_flag = 0 # Signal LwDecoded successful (default state) + + except .. as e, m + log(format("LwD: Decode error for %s: %s", device_name, m),1) + end + + return true + end + + #------------------------------------------------------------ + Display sensor value in the web UI and react to button + Called every WebRefresh time + ------------------------------------------------------------# + def web_sensor() + var current_time = tasmota.millis() + + if current_time < self.cache_timeout + tasmota.web_send_decimal(self.web_msg_cache) + return + end + + var msg = "" + for decoder: self.lw_decoders + msg += decoder.add_web_sensor() + end + + if msg + var full_msg = format("" # Terminate current two column table and open new table + "" + "{t}%s{t}", + msg) + + self.web_msg_cache = full_msg + self.cache_timeout = current_time + 5000 + tasmota.web_send_decimal(full_msg) + end + end +end + +return lorawan_decoders() +#---------------------------------------------------------------------------------# +# extension lorawan_decoders +################################################################################### diff --git a/tasmota/berry/extensions/LoRaWan_Decoders/manifest.json b/tasmota/berry/extensions/LoRaWan_Decoders/manifest.json new file mode 100644 index 000000000..422e2e1e5 --- /dev/null +++ b/tasmota/berry/extensions/LoRaWan_Decoders/manifest.json @@ -0,0 +1,8 @@ +{ + "name": "LoRaWan Decoders", + "version": "0x190A0200", + "description": "Decode LoRaWan devices", + "author": "Theo Arends", + "min_tasmota": "0x0E060001", + "features": "" +} diff --git a/tasmota/berry/extensions/LoRaWan_Decoders/walker.be b/tasmota/berry/extensions/LoRaWan_Decoders/walker.be new file mode 100644 index 000000000..db23325ce --- /dev/null +++ b/tasmota/berry/extensions/LoRaWan_Decoders/walker.be @@ -0,0 +1,242 @@ +# LoRaWAN Decoder file for Glamos Walker +# +# References https://glamos.eu/product/walker/ + +import string + +if !global.walkerNodes + global.walkerNodes = {} +end + +class LwDecoWALKER + static var hashCheck = false + + static def decodeUplink(Name, Node, RSSI, FPort, Bytes) + var data = {"Device":"Glamos WALKER"} + + var valid_values = false + + var rssi = RSSI + + var last_seen = 1451602800 + + var latitude = 0.0 # 3 bytes + var longitude = 0.0 # 3 bytes + var altitude = 0.0 # 2 bytes + var antenna = 0 # 1 byte (manually set by user on the device) + var position = 0 # 1 byte (manually set by user on the device) + var counter = 0 + var command_init = false + var track_enabled = false + var history = [] + + if global.walkerNodes.find(Node) + latitude = global.walkerNodes.item(Node)[4] + longitude = global.walkerNodes.item(Node)[5] + altitude = number( global.walkerNodes.item(Node)[6] ) + antenna = number( global.walkerNodes.item(Node)[7] ) + position = number( global.walkerNodes.item(Node)[8] ) + counter = number( global.walkerNodes.item(Node)[9] ) + command_init = global.walkerNodes.item(Node)[10] + track_enabled = global.walkerNodes.item(Node)[11] + history = global.walkerNodes.item(Node)[12] + end + + try + if Bytes.size() == 10 + valid_values = true + + last_seen = tasmota.rtc('local') + + latitude = (((Bytes[0] << 16) | (Bytes[1] << 8) | (Bytes[2] << 0)) / 16777215.0) * 180.0 - 90.0 + longitude = (((Bytes[3] << 16) | (Bytes[4] << 8) | (Bytes[5] << 0)) / 16777215.0) * 360.0 - 180.0 + altitude = (Bytes[6] << 8) | (Bytes[7] << 0) + antenna = Bytes[8] + position = Bytes[9] + counter += 1 + + data.insert("Latitude", latitude) + data.insert("Longitude", longitude) + data.insert("Altitude", altitude) + data.insert("Antenna", antenna) + data.insert("Position", position) + data.insert("Counter", counter) + data.insert("Tracking", track_enabled) + data.insert("History Size", history.size()) + end + except .. as e, m + print(e .. ': ' .. m) + end + + if valid_values + if !command_init + var pfx = 'Walker' + + tasmota.remove_cmd(pfx + 'ReloadCmd') + tasmota.add_cmd( pfx + 'ReloadCmd', + def (cmd, idx, payload) + if global.walkerNodes.find(idx) + global.walkerNodes.item(idx)[10] = false # reload command after a 'LwReload' + return tasmota.resp_cmnd_done() + end + end + ) + + tasmota.remove_cmd(pfx + 'ResetCounter') + tasmota.add_cmd( pfx + 'ResetCounter', + def (cmd, idx, payload) + if global.walkerNodes.find(idx) + global.walkerNodes.item(idx)[9] = 0 + return tasmota.resp_cmnd_done() + end + end + ) + + tasmota.remove_cmd(pfx + 'EnableTrack') + tasmota.add_cmd( pfx + 'EnableTrack', + def (cmd, idx, payload) + if global.walkerNodes.find(idx) + global.walkerNodes.item(idx)[11] = true + return tasmota.resp_cmnd_done() + end + end + ) + + tasmota.remove_cmd(pfx + 'DisableTrack') + tasmota.add_cmd( pfx + 'DisableTrack', + def (cmd, idx, payload) + if global.walkerNodes.find(idx) + global.walkerNodes.item(idx)[11] = false + return tasmota.resp_cmnd_done() + end + end + ) + + tasmota.remove_cmd(pfx + 'ClearHistory') + tasmota.add_cmd( pfx + 'ClearHistory', + def (cmd, idx, payload) + if global.walkerNodes.find(idx) + global.walkerNodes.item(idx)[12].clear() + return tasmota.resp_cmnd_done() + end + end + ) + + tasmota.remove_cmd(pfx + 'GetHistory') + tasmota.add_cmd( pfx + 'GetHistory', + def (cmd, idx, payload) + try + if global.walkerNodes.find(idx) + import json + return tasmota.resp_cmnd( '{"' .. pfx .. 'History":' .. json.dump( global.walkerNodes.item(idx)[12]) .. '}' ) + end + except .. as e, m + print(e .. ': ' .. m) + end + end + ) + + command_init = true + end + + try + if track_enabled + if history.size() > 50 # may be dynamic? + history.remove(0) + end + + history.push( [ + last_seen + ,latitude + ,longitude + ,altitude + ,antenna + ,position + ,counter + ]) + end + except .. as e, m + print(e .. ': ' .. m) + end + + if global.walkerNodes.find(Node) + global.walkerNodes.remove(Node) + end + + global.walkerNodes.insert(Node, + [ # sensor + Name # [0] + ,Node # [1] + ,last_seen # [2] + ,rssi # [3] + ,latitude # [4] + ,longitude # [5] + ,altitude # [6] + ,antenna # [7] + ,position # [8] + ,counter # [9] + ,command_init # [10] + ,track_enabled # [11] + ,history # [12] + ] + ) + end + + return data + end #decodeUplink() + + static def add_web_sensor() + var fmt = global.LwSensorFormatter_cls() + var msg = "" + try + for sensor: global.walkerNodes + var name = sensor[0] + + # If LoRaWanName contains WALKER use WALKER- + if string.find(name, "WALKER") > -1 + name = string.format("WALKER-%i", sensor[1]) + end + + var name_tooltip = "Glamos Walker" + + var last_seen = sensor[2] + var rssi = sensor[3] + + msg += fmt.header(name, name_tooltip, 1000, last_seen, rssi, last_seen) + + # Sensors + var latitude = sensor[4] + var longitude = sensor[5] + var altitude = sensor[6] + var antenna = sensor[7] + var position = sensor[8] + var counter = sensor[9] + var track_enabled = sensor[11] + var history = sensor[12] + + var latlon = format("🎯 %.4f, %.4f", latitude, longitude) + var map_link = format("https://www.google.com/maps/place/%.6f,%.6f",latitude,longitude) + + var te_value = (track_enabled ? str(history.size()) : "") + var te_tt = "Tracking " .. (track_enabled ? "On" : "Off") + var te_icon = (track_enabled ? "🔴" : "⚪") + + msg += fmt.start_line() + .add_link( latlon, map_link ) # Direct History 🎯 + .next_line() + .add_sensor("altitude", altitude, "Altitude" ) # Mountain ⛰ + .add_sensor("string", format("%d", antenna), "Antenna", "📡" ) # Satellite Antenna 📡 + .add_sensor("string", format("%d", position), "Position", "📍" ) # Round Pushpin 📍 + .add_sensor("string", format("%d", counter), "Counter", "⏱" ) # Chronometer ⏱️ + .add_sensor("string", te_value, te_tt, te_icon) # Track ON/OFF 🔴 ⚪ + .end_line() + .get_msg() + end + except .. as e, m + print(e .. ': ' .. m) + end + return msg + end #add_web_sensor() +end #class + +global.LwDeco = LwDecoWALKER