Remove pre-production extensions
This commit is contained in:
parent
0b891392b1
commit
bd0d7d0ac3
Binary file not shown.
@ -1,9 +0,0 @@
|
|||||||
# 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
|
|
||||||
tasmota.add_extension(devices_online)
|
|
||||||
end
|
|
||||||
|
|
||||||
# to remove:
|
|
||||||
# tasmota.unload_extension('Devices Online')
|
|
||||||
@ -1,359 +0,0 @@
|
|||||||
###################################################################################
|
|
||||||
# Display in Main GUI Devices Online based on MQTT Tasmota Discovery Config and STATE reports
|
|
||||||
#
|
|
||||||
# Copyright (C) 2025 Stephan Hadinger & Theo Arends
|
|
||||||
#
|
|
||||||
# Enable either
|
|
||||||
# line_option = 1 : Scroll 'line_cnt' lines
|
|
||||||
# or
|
|
||||||
# line_option = 2 : Show devices updating within 'line_teleperiod'
|
|
||||||
#
|
|
||||||
# rm Devices_Online.tapp; zip -j -0 Devices_Online.tapp Devices_Online/*
|
|
||||||
###################################################################################
|
|
||||||
|
|
||||||
import mqtt
|
|
||||||
import json
|
|
||||||
import string
|
|
||||||
import webserver
|
|
||||||
import persist
|
|
||||||
|
|
||||||
class devices_online
|
|
||||||
# static var line_option = 1 # Scroll line_cnt lines
|
|
||||||
static var line_option = 2 # Show devices updating within line_teleperiod
|
|
||||||
|
|
||||||
static var line_cnt = 10 # Option 1 number of lines to show
|
|
||||||
static var line_teleperiod = 600 # Option 2 number of teleperiod seconds for devices to be shown as online
|
|
||||||
static var line_highlight = 10 # Highlight latest change duration in seconds
|
|
||||||
static var line_highlight_color = "yellow" # Latest change highlight HTML color like "#FFFF00" or "yellow"
|
|
||||||
static var line_lowuptime_color = "lime" # Low uptime highlight HTML color like "#00FF00" or "lime"
|
|
||||||
|
|
||||||
var mqtt_tele # MQTT tele STATE subscribe format
|
|
||||||
var bool_devicename # Show device name
|
|
||||||
var bool_version # Show version
|
|
||||||
var bool_ipaddress # Show IP address
|
|
||||||
var sort_direction # Sort direction
|
|
||||||
var sort_column # Sort column
|
|
||||||
var sort_last_column # Sort last column
|
|
||||||
var list_buffer # Buffer storing lines
|
|
||||||
var list_config # Buffer storing retained config
|
|
||||||
|
|
||||||
#################################################################################
|
|
||||||
# init
|
|
||||||
#
|
|
||||||
# install the extension and allocate all resources
|
|
||||||
#################################################################################
|
|
||||||
def init()
|
|
||||||
self.bool_devicename = persist.std_devicename # Show device name
|
|
||||||
self.bool_version = persist.std_version # Show version
|
|
||||||
self.bool_ipaddress = persist.std_ipaddress # Show IP address
|
|
||||||
|
|
||||||
self.sort_direction = persist.std_direction # Sort direction (0) Up or (1) Down
|
|
||||||
if !self.sort_direction
|
|
||||||
self.sort_direction = 0 # Default Up
|
|
||||||
end
|
|
||||||
self.sort_column = persist.std_column # Sort column
|
|
||||||
if !self.sort_column
|
|
||||||
self.sort_column = 0 # Default Hostname
|
|
||||||
end
|
|
||||||
self.sort_last_column = self.sort_column # Sort last column to detect direction toggle
|
|
||||||
|
|
||||||
self.list_buffer = [] # Init line buffer list
|
|
||||||
self.list_config = [] # Init retained config buffer list
|
|
||||||
|
|
||||||
# var full_topic = tasmota.cmd("FullTopic", true)['FullTopic'] # "%prefix%/%topic%/"
|
|
||||||
var prefix_tele = tasmota.cmd("Prefix", true)['Prefix3'] # tele = Prefix3 used by STATE message
|
|
||||||
self.mqtt_tele = format("%s/#", prefix_tele)
|
|
||||||
mqtt.subscribe(self.mqtt_tele, /topic, idx, data, databytes -> self.handle_state_data(topic, idx, data, databytes))
|
|
||||||
mqtt.subscribe("tasmota/discovery/+/config", /topic, idx, data, databytes -> self.handle_discovery_data(topic, idx, data, databytes))
|
|
||||||
|
|
||||||
tasmota.add_driver(self)
|
|
||||||
end
|
|
||||||
|
|
||||||
#################################################################################
|
|
||||||
# unload
|
|
||||||
#
|
|
||||||
# Uninstall the extension and deallocate all resources
|
|
||||||
#################################################################################
|
|
||||||
def unload()
|
|
||||||
mqtt.unsubscribe("tasmota/discovery/+/config")
|
|
||||||
mqtt.unsubscribe(self.mqtt_tele)
|
|
||||||
tasmota.remove_driver(self)
|
|
||||||
end
|
|
||||||
|
|
||||||
#################################################################################
|
|
||||||
# handle_discovery_data(discovery_topic, idx, data, databytes)
|
|
||||||
#
|
|
||||||
# Handle MQTT Tasmota Discovery Config data
|
|
||||||
#################################################################################
|
|
||||||
def handle_discovery_data(discovery_topic, idx, data, databytes)
|
|
||||||
var config = json.load(data)
|
|
||||||
if config
|
|
||||||
# tasmota/discovery/142B2F9FAF38/config = {"ip":"192.168.2.208","dn":"AtomLite2","fn":["Tasmota",null,null,null,null,null,null,null],"hn":"atomlite2","mac":"142B2F9FAF38","md":"M5Stack Atom Lite","ty":0,"if":0,"cam":0,"ofln":"Offline","onln":"Online","state":["OFF","ON","TOGGLE","HOLD"],"sw":"15.0.1.4","t":"atomlite2","ft":"%prefix%/%topic%/","tp":["cmnd","stat","tele"],"rl":[2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],"swc":[-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1],"swn":[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],"btn":[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],"so":{"4":0,"11":0,"13":0,"17":0,"20":0,"30":0,"68":0,"73":0,"82":0,"114":0,"117":0},"lk":1,"lt_st":3,"bat":0,"dslp":0,"sho":[],"sht":[],"ver":1} (retained)
|
|
||||||
var topic = config['t']
|
|
||||||
var hostname = config['hn']
|
|
||||||
var ipaddress = config['ip']
|
|
||||||
var devicename = config['dn']
|
|
||||||
var version = config['sw']
|
|
||||||
var line = format("%s\001%s\001%s\001%s\001%s", topic, hostname, ipaddress, devicename, version)
|
|
||||||
# tasmota.log(format("STD: 111 Size %03d, Topic '%s', Line '%s'", self.list_config.size(), topic, line), 3)
|
|
||||||
if self.list_config.size()
|
|
||||||
var list_index = 0
|
|
||||||
var list_size = size(self.list_config)
|
|
||||||
var topic_delim = format("%s\001", topic) # Add find delimiter
|
|
||||||
while list_index < list_size # Use while loop as counter is decremented
|
|
||||||
if 0 == string.find(self.list_config[list_index], topic_delim)
|
|
||||||
self.list_config.remove(list_index) # Remove current config
|
|
||||||
list_size -= 1 # Continue for duplicates
|
|
||||||
end
|
|
||||||
list_index += 1
|
|
||||||
end
|
|
||||||
end
|
|
||||||
self.list_config.push(line) # Add (re-discovered) config as last entry
|
|
||||||
# tasmota.log(format("STD: 222 Size %03d, Topic '%s', Line '%s'", self.list_config.size(), topic, line), 3)
|
|
||||||
end
|
|
||||||
return true # return true to stop propagation as a Tasmota cmd
|
|
||||||
end
|
|
||||||
|
|
||||||
#################################################################################
|
|
||||||
# handle_state_data(tele_topic, idx, data, databytes)
|
|
||||||
#
|
|
||||||
# Handle MQTT STATE data
|
|
||||||
#################################################################################
|
|
||||||
def handle_state_data(tele_topic, idx, data, databytes)
|
|
||||||
var subtopic = string.split(tele_topic, "/")
|
|
||||||
if subtopic[-1] == "STATE" # tele/atomlite2/STATE
|
|
||||||
var topic = subtopic[1] # Assume default Fulltopic (%prefix%/%topic%/) = tele/atomlite2/STATE = atomlite2
|
|
||||||
|
|
||||||
var topic_index = -1
|
|
||||||
for i: self.list_config.keys()
|
|
||||||
if 0 == string.find(self.list_config[i], topic)
|
|
||||||
topic_index = i
|
|
||||||
break
|
|
||||||
end
|
|
||||||
end
|
|
||||||
# tasmota.log(format("STD: Topic '%s', Index %d, Size %d, Line '%s'", topic, topic_index, self.list_config.size(), self.list_config[topic_index]), 3)
|
|
||||||
if topic_index == -1 return true end # return true to stop propagation as a Tasmota cmd
|
|
||||||
|
|
||||||
var state = json.load(data) # Assume topic is in retained discovery list
|
|
||||||
if state # Valid JSON state message
|
|
||||||
var config_splits = string.split(self.list_config[topic_index], "\001")
|
|
||||||
var hostname = config_splits[1]
|
|
||||||
var ipaddress = config_splits[2]
|
|
||||||
var devicename = config_splits[3]
|
|
||||||
var version = config_splits[4]
|
|
||||||
|
|
||||||
# tele/atomlite2/STATE = {"Time":"2025-09-24T14:13:00","Uptime":"0T00:15:09","UptimeSec":909,"Heap":142,"SleepMode":"Dynamic","Sleep":50,"LoadAvg":19,"MqttCount":1,"Berry":{"HeapUsed":12,"Objects":167},"POWER":"OFF","Dimmer":10,"Color":"1A0000","HSBColor":"0,100,10","Channel":[10,0,0],"Scheme":0,"Width":1,"Fade":"OFF","Speed":1,"LedTable":"ON","Wifi":{"AP":1,"SSId":"indebuurt_IoT","BSSId":"18:E8:29:CA:17:C1","Channel":11,"Mode":"HT40","RSSI":100,"Signal":-28,"LinkCount":1,"Downtime":"0T00:00:04"},"Hostname":"atomlite2","IPAddress":"192.168.2.208"}
|
|
||||||
var uptime = state['Uptime'] # 0T00:15:09
|
|
||||||
if state.find('Hostname')
|
|
||||||
hostname = state['Hostname'] # atomlite2
|
|
||||||
ipaddress = state['IPAddress'] # 192.168.2.208
|
|
||||||
end
|
|
||||||
var last_seen = tasmota.rtc('local')
|
|
||||||
var line = format("%s\001%s\001%s\001%d\001%s\001%s", hostname, ipaddress, uptime, last_seen, devicename, version)
|
|
||||||
|
|
||||||
if self.list_buffer.size()
|
|
||||||
var list_index = 0
|
|
||||||
var list_size = size(self.list_buffer)
|
|
||||||
var hostname_delim = format("%s\001", hostname) # Add find delimiter
|
|
||||||
while list_index < list_size # Use while loop as counter is decremented
|
|
||||||
if 0 == string.find(self.list_buffer[list_index], hostname_delim)
|
|
||||||
self.list_buffer.remove(list_index) # Remove current state
|
|
||||||
list_size -= 1 # Continue for duplicates
|
|
||||||
end
|
|
||||||
list_index += 1
|
|
||||||
end
|
|
||||||
end
|
|
||||||
self.list_buffer.push(line) # Add state as last entry
|
|
||||||
|
|
||||||
end
|
|
||||||
end
|
|
||||||
return true # return true to stop propagation as a Tasmota cmd
|
|
||||||
end
|
|
||||||
|
|
||||||
#################################################################################
|
|
||||||
# sort_col(l, col, dir)
|
|
||||||
#
|
|
||||||
# Shell sort list of online devices based on user selected column and direction
|
|
||||||
#################################################################################
|
|
||||||
def sort_col(l, col, dir) # Sort list based on col and Hostname (is first entry in line)
|
|
||||||
# For 50 records takes 6ms (primary key) or 25ms(ESP32S3&240MHz) / 50ms(ESP32@160MHz) (primary and secondary key)
|
|
||||||
var cmp = /a,b -> a < b # Sort up
|
|
||||||
if dir
|
|
||||||
cmp = /a,b -> a > b # Sort down
|
|
||||||
end
|
|
||||||
if col # col is new primary key (not Hostname)
|
|
||||||
for i:l.keys()
|
|
||||||
var splits = string.split(l[i], "\001")
|
|
||||||
l[i] = splits[col] + "\002" + l[i] # Add primary key to secondary key as "col" + Hostname
|
|
||||||
end
|
|
||||||
end
|
|
||||||
for i:1..size(l)-1
|
|
||||||
var k = l[i]
|
|
||||||
var j = i
|
|
||||||
while (j > 0) && !cmp(l[j-1], k)
|
|
||||||
l[j] = l[j-1]
|
|
||||||
j -= 1
|
|
||||||
end
|
|
||||||
l[j] = k
|
|
||||||
end
|
|
||||||
if col
|
|
||||||
for i:l.keys()
|
|
||||||
var splits = string.split(l[i], "\002") # Remove primary key
|
|
||||||
l[i] = splits[1]
|
|
||||||
end
|
|
||||||
end
|
|
||||||
return l
|
|
||||||
end
|
|
||||||
|
|
||||||
#################################################################################
|
|
||||||
# persist_save
|
|
||||||
#
|
|
||||||
# Save user data to be used on restart
|
|
||||||
#################################################################################
|
|
||||||
def persist_save()
|
|
||||||
persist.std_devicename = self.bool_devicename
|
|
||||||
persist.std_version = self.bool_version
|
|
||||||
persist.std_ipaddress = self.bool_ipaddress
|
|
||||||
persist.std_column = self.sort_column
|
|
||||||
persist.std_direction = self.sort_direction
|
|
||||||
persist.save()
|
|
||||||
# tasmota.log("STD: Persist saved", 3)
|
|
||||||
end
|
|
||||||
|
|
||||||
#################################################################################
|
|
||||||
# web_sensor
|
|
||||||
#
|
|
||||||
# Display Devices Online in user selected sorted columns
|
|
||||||
#################################################################################
|
|
||||||
def web_sensor()
|
|
||||||
if webserver.has_arg("sd_dn")
|
|
||||||
# Toggle display Device Name
|
|
||||||
if self.bool_devicename self.bool_devicename = false else self.bool_devicename = true end
|
|
||||||
self.persist_save()
|
|
||||||
elif webserver.has_arg("sd_sw")
|
|
||||||
# Toggle display software version
|
|
||||||
if self.bool_version self.bool_version = false else self.bool_version = true end
|
|
||||||
self.persist_save()
|
|
||||||
elif webserver.has_arg("sd_ip")
|
|
||||||
# Toggle display IP address
|
|
||||||
if self.bool_ipaddress self.bool_ipaddress = false else self.bool_ipaddress = true end
|
|
||||||
self.persist_save()
|
|
||||||
elif webserver.has_arg("sd_sort")
|
|
||||||
# Toggle sort column
|
|
||||||
self.sort_column = int(webserver.arg("sd_sort"))
|
|
||||||
if self.sort_last_column == self.sort_column
|
|
||||||
self.sort_direction ^= 1
|
|
||||||
end
|
|
||||||
self.sort_last_column = self.sort_column
|
|
||||||
self.persist_save()
|
|
||||||
end
|
|
||||||
|
|
||||||
if self.list_buffer.size()
|
|
||||||
var now = tasmota.rtc('local')
|
|
||||||
var time_window = now - self.line_teleperiod
|
|
||||||
var list_index = 0
|
|
||||||
var list_size = size(self.list_buffer)
|
|
||||||
while list_index < list_size
|
|
||||||
var splits = string.split(self.list_buffer[list_index], "\001")
|
|
||||||
var last_seen = int(splits[3])
|
|
||||||
if time_window > last_seen # Remove offline devices
|
|
||||||
self.list_buffer.remove(list_index)
|
|
||||||
list_size -= 1
|
|
||||||
end
|
|
||||||
list_index += 1
|
|
||||||
end
|
|
||||||
if !list_size return end # If list became empty bail out
|
|
||||||
|
|
||||||
var msg = "</table><table style='width:100%;font-size:80%'>" # Terminate two column table and open new table
|
|
||||||
msg += "<tr>"
|
|
||||||
|
|
||||||
list_index = 0
|
|
||||||
if 1 == self.line_option
|
|
||||||
list_index = list_size - self.line_cnt # Offset in list using self.line_cnt
|
|
||||||
if list_index < 0 list_index = 0 end
|
|
||||||
|
|
||||||
if self.bool_devicename
|
|
||||||
msg += "<th>Device Name </th>"
|
|
||||||
end
|
|
||||||
if self.bool_version
|
|
||||||
msg += "<th>Version </th>"
|
|
||||||
end
|
|
||||||
msg += "<th>Hostname </th>"
|
|
||||||
if self.bool_ipaddress
|
|
||||||
msg += "<th>IP Address </th>"
|
|
||||||
end
|
|
||||||
msg += "<th align='right'>Uptime </th>"
|
|
||||||
else
|
|
||||||
self.sort_col(self.list_buffer, self.sort_column, self.sort_direction) # Sort list by column
|
|
||||||
|
|
||||||
var icon_direction = self.sort_direction ? "▼" : "▲"
|
|
||||||
if self.bool_devicename
|
|
||||||
msg += format("<th><a href='#p' onclick='la(\"&sd_sort=4\");'>Device Name</a>%s </th>", self.sort_column == 4 ? icon_direction : "")
|
|
||||||
end
|
|
||||||
if self.bool_version
|
|
||||||
msg += format("<th><a href='#p' onclick='la(\"&sd_sort=5\");'>Version</a>%s </th>", self.sort_column == 5 ? icon_direction : "")
|
|
||||||
end
|
|
||||||
msg += format("<th><a href='#p' onclick='la(\"&sd_sort=0\");'>Hostname</a>%s </th>", self.sort_column == 0 ? icon_direction : "")
|
|
||||||
if self.bool_ipaddress
|
|
||||||
msg += format("<th><a href='#p' onclick='la(\"&sd_sort=1\");'>IP Address</a>%s </th>", self.sort_column == 1 ? icon_direction : "")
|
|
||||||
end
|
|
||||||
msg += format("<th align='right'><a href='#p' onclick='la(\"&sd_sort=2\");'>Uptime</a>%s </th>", self.sort_column == 2 ? icon_direction : "")
|
|
||||||
end
|
|
||||||
|
|
||||||
msg += "</tr>"
|
|
||||||
|
|
||||||
while list_index < list_size
|
|
||||||
var splits = string.split(self.list_buffer[list_index], "\001")
|
|
||||||
var hostname = splits[0]
|
|
||||||
var ipaddress = splits[1]
|
|
||||||
var uptime = splits[2]
|
|
||||||
var last_seen = int(splits[3])
|
|
||||||
var devicename = splits[4]
|
|
||||||
var version = splits[5]
|
|
||||||
|
|
||||||
msg += "<tr>"
|
|
||||||
if self.bool_devicename
|
|
||||||
msg += format("<td>%s </td>", devicename)
|
|
||||||
end
|
|
||||||
if self.bool_version
|
|
||||||
msg += format("<td>%s </td>", version)
|
|
||||||
end
|
|
||||||
msg += format("<td><a target=_blank href='http://%s.'>%s </a></td>", hostname, hostname)
|
|
||||||
if self.bool_ipaddress
|
|
||||||
msg += format("<td><a target=_blank href='http://%s'>%s </a></td>", ipaddress, ipaddress)
|
|
||||||
end
|
|
||||||
|
|
||||||
var uptime_str = string.replace(uptime, "T", ":") # 11T21:50:34 -> 11:21:50:34
|
|
||||||
var uptime_splits = string.split(uptime_str, ":")
|
|
||||||
var uptime_sec = (int(uptime_splits[0]) * 86400) + # 11 * 86400
|
|
||||||
(int(uptime_splits[1]) * 3600) + # 21 * 3600
|
|
||||||
(int(uptime_splits[2]) * 60) + # 50 * 60
|
|
||||||
int(uptime_splits[3]) # 34
|
|
||||||
if last_seen >= (now - self.line_highlight) # Highlight changes within latest seconds
|
|
||||||
msg += format("<td align='right' style='color:%s'>%s</td>", self.line_highlight_color, uptime)
|
|
||||||
elif uptime_sec < self.line_teleperiod # Highlight changes just after restart
|
|
||||||
msg += format("<td align='right' style='color:%s'>%s</td>", self.line_lowuptime_color, uptime)
|
|
||||||
else
|
|
||||||
msg += format("<td align='right'>%s</td>", uptime)
|
|
||||||
end
|
|
||||||
|
|
||||||
msg += "</tr>"
|
|
||||||
list_index += 1
|
|
||||||
end
|
|
||||||
msg += "</table>{t}" # Terminate three/four/five column table and open new table: <table style='width:100%'>
|
|
||||||
msg += format("{s}Devices online{m}%d{e}", list_size) # <tr><th>Devices online</th><td style='width:20px;white-space:nowrap'>%d</td></tr>
|
|
||||||
|
|
||||||
msg += "</table><p></p>{t}" # Terminate two column table and open new table: <table style='width:100%'>
|
|
||||||
msg += "<td style=\"width:33%\"><button onclick='la(\"&sd_dn=1\");'>Name</button></td>"
|
|
||||||
msg += "<td style=\"width:33%\"><button onclick='la(\"&sd_sw=1\");'>Version</button></td>"
|
|
||||||
msg += "<td style=\"width:33%\"><button onclick='la(\"&sd_ip=1\");'>Address</button></td>"
|
|
||||||
msg += "</table>{t}" # Terminate two column table and open new table: <table style='width:100%'>
|
|
||||||
|
|
||||||
tasmota.web_send(msg) # Do not use tasmota.web_send_decimal() which will replace IPAddress dots
|
|
||||||
tasmota.web_send_decimal("") # Force horizontal line
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
end
|
|
||||||
|
|
||||||
return devices_online()
|
|
||||||
@ -1,8 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "Devices Online",
|
|
||||||
"version": "0x190A0200",
|
|
||||||
"description": "Display devices online",
|
|
||||||
"author": "Theo Arends",
|
|
||||||
"min_tasmota": "0x0E060001",
|
|
||||||
"features": ""
|
|
||||||
}
|
|
||||||
Binary file not shown.
@ -1,129 +0,0 @@
|
|||||||
# 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-<node>
|
|
||||||
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 += "<tr class='htr'><td colspan='4'>┆" # |
|
|
||||||
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}" # = </td></tr>
|
|
||||||
end
|
|
||||||
return msg
|
|
||||||
end #add_web_sensor()
|
|
||||||
end #class
|
|
||||||
|
|
||||||
global.LwDeco = LwDecoDrgD20
|
|
||||||
@ -1,94 +0,0 @@
|
|||||||
# 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-<node>
|
|
||||||
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 += "<tr class='htr'><td colspan='4'>┆" # |
|
|
||||||
msg += string.format(" ⭳️ %.0fmm", distance) # ⭳
|
|
||||||
msg += "{e}" # = </td></tr>
|
|
||||||
end
|
|
||||||
return msg
|
|
||||||
end #add_web_sensor()
|
|
||||||
end #class
|
|
||||||
|
|
||||||
global.LwDeco = LwDecoDDS75LB
|
|
||||||
@ -1,103 +0,0 @@
|
|||||||
# 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-<node>
|
|
||||||
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 += "<tr class='htr'><td colspan='4'>┆" # |
|
|
||||||
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}" # = </td></tr>
|
|
||||||
end
|
|
||||||
return msg
|
|
||||||
end #add_web_sensor()
|
|
||||||
end #class
|
|
||||||
|
|
||||||
global.LwDeco = LwDecoDW10
|
|
||||||
@ -1,88 +0,0 @@
|
|||||||
# 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-<node>
|
|
||||||
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 += "<tr class='htr'><td colspan='4'>┆" # |
|
|
||||||
msg += string.format(" %s %s", (door_open) ? "🔓" : "🔒", # Open or Closed lock - Door
|
|
||||||
fmt.dhm(door_open_last_seen))
|
|
||||||
msg += "{e}" # = </td></tr>
|
|
||||||
end
|
|
||||||
return msg
|
|
||||||
end #add_web_sensor()
|
|
||||||
end #class
|
|
||||||
|
|
||||||
global.LwDeco = LwDecoLDS02
|
|
||||||
@ -1,125 +0,0 @@
|
|||||||
# 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-<node>
|
|
||||||
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 += "<tr class='htr'><td colspan='4'>┆" # |
|
|
||||||
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}" # = </td></tr>
|
|
||||||
end
|
|
||||||
return msg
|
|
||||||
end #add_web_sensor()
|
|
||||||
end #class
|
|
||||||
|
|
||||||
global.LwDeco = LwDecoLHT52
|
|
||||||
@ -1,202 +0,0 @@
|
|||||||
# 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-<node>
|
|
||||||
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 += "<tr class=\"htr\"><td colspan=\"4\">┆" # |
|
|
||||||
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}" # = </td></tr>
|
|
||||||
end
|
|
||||||
return msg
|
|
||||||
end #add_web_sensor()
|
|
||||||
end # class
|
|
||||||
|
|
||||||
global.LwDeco = LwDecoLHT65
|
|
||||||
@ -1,114 +0,0 @@
|
|||||||
# 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-<node>
|
|
||||||
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 += "<tr class='htr'><td colspan='4'>┆" # |
|
|
||||||
msg += string.format(" ⭳️ %.1fcm", Water_deep_cm) # тн│
|
|
||||||
msg += "{e}" # = </td></tr>
|
|
||||||
end
|
|
||||||
return msg
|
|
||||||
end #add_web_sensor()
|
|
||||||
end #class
|
|
||||||
|
|
||||||
LwDeco = LwDecoPSLI5
|
|
||||||
@ -1,149 +0,0 @@
|
|||||||
# 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-<node>
|
|
||||||
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 += "<tr class='htr'><td colspan='4'>┆" # |
|
|
||||||
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}" # = </td></tr>
|
|
||||||
end
|
|
||||||
return msg
|
|
||||||
end #add_web_sensor()
|
|
||||||
end #class
|
|
||||||
|
|
||||||
global.LwDeco = LwDecoSE01L
|
|
||||||
@ -1,119 +0,0 @@
|
|||||||
# 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-<node>
|
|
||||||
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 += "<tr class='htr'><td colspan='4'>┆" # |
|
|
||||||
|
|
||||||
msg += string.format(" ️ %s", workingMode)
|
|
||||||
|
|
||||||
msg += "{e}" # = </td></tr>
|
|
||||||
end
|
|
||||||
return msg
|
|
||||||
end #add_web_sensor()
|
|
||||||
end #class
|
|
||||||
|
|
||||||
global.LwDeco = LwDecoDrgSN50v3L
|
|
||||||
@ -1,118 +0,0 @@
|
|||||||
# 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-<node>
|
|
||||||
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
|
|
||||||
@ -1,118 +0,0 @@
|
|||||||
# 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-<node>
|
|
||||||
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
|
|
||||||
@ -1,369 +0,0 @@
|
|||||||
# 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-<node>
|
|
||||||
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
|
|
||||||
@ -1,9 +0,0 @@
|
|||||||
# 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')
|
|
||||||
@ -1,590 +0,0 @@
|
|||||||
###################################################################################
|
|
||||||
# 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("<p></p><form id=ac action='lrw' style='display: block;' method='get'><button>LoRaWAN</button></form>")
|
|
||||||
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(
|
|
||||||
"<style>"
|
|
||||||
".tl{float:left;border-radius:0;border:1px solid var(--c_frm);padding:1px;width:12.5%;}"
|
|
||||||
".tl:hover{background:var(--c_frm);}"
|
|
||||||
".inactive{background:var(--c_tab);color:var(--c_tabtxt);font-weight:normal;}"
|
|
||||||
".active{background:var(--c_frm);color:var(--c_txt);font-weight:bold;}"
|
|
||||||
"</style>"
|
|
||||||
"<script>"
|
|
||||||
"function selNode(n){"
|
|
||||||
"var i;"
|
|
||||||
"var e=document.getElementById('n'+n);"
|
|
||||||
"var o=document.getElementsByClassName('tl active');"
|
|
||||||
"if(o.length){"
|
|
||||||
"for(i=0;i<o.length;i++){"
|
|
||||||
"o[i].classList.add('inactive');"
|
|
||||||
"o[i].classList.remove('active');"
|
|
||||||
"}"
|
|
||||||
"}"
|
|
||||||
"e.classList.add('active');"
|
|
||||||
"for(i=1;i<=" + str(maxnode) + ";i++){"
|
|
||||||
"document.getElementById('nd'+i).style.display=(i==n)?'block':'none';"
|
|
||||||
"}"
|
|
||||||
"}"
|
|
||||||
"window.onload = function(){selNode(" + str(inode) + ");};"
|
|
||||||
"</script>")
|
|
||||||
|
|
||||||
webserver.content_send(
|
|
||||||
format("<fieldset>"
|
|
||||||
"<legend><b> LoRaWan End Device </b></legend>"
|
|
||||||
"<br><div>")) #- Add space and indent to align form tabs -#
|
|
||||||
for node:1 .. maxnode
|
|
||||||
webserver.content_send(format("<button type='button' onclick='selNode(%i)' id='n%i' class='tl inactive'>%i</button>", node, node, node))
|
|
||||||
end
|
|
||||||
webserver.content_send("</div><br><br><br><br>") #- 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("<div id='nd%i' style='display:none'>"
|
|
||||||
"<form action='' method='post'>"
|
|
||||||
"<p><label><input id='ce' name='ce' type='checkbox'%s><b>Enabled</b></label></p>"
|
|
||||||
"<p><b>Application Key</b>"
|
|
||||||
"<input title='%s' pattern='[A-Fa-f0-9]{32}' id='ak' minlength='32' maxlength='32' required='' placeholder='%s' value='%s' name='ak' style='font-size:smaller'>"
|
|
||||||
"</p>"
|
|
||||||
"<p></p>"
|
|
||||||
"<p><b>Device Name</b>"
|
|
||||||
"<input id='an' placeholder='%s' value='%s' name='an'>"
|
|
||||||
"</p>"
|
|
||||||
"<p></p>"
|
|
||||||
"<p><b>Decoder File</b>"
|
|
||||||
"<input title='%s' id='dc' placeholder='%s' value='%s' name='dc'>"
|
|
||||||
"</p>"
|
|
||||||
"<br>"
|
|
||||||
"<button name='save' class='button bgrn'>Save</button>"
|
|
||||||
"<input type='hidden' name='node' value='%i'>"
|
|
||||||
"</form>"
|
|
||||||
"</div>", node, enabled, hintAK, hintAK, appKey, hintAN, name, hintDecoder, hintDecoder, decoder, node))
|
|
||||||
end
|
|
||||||
|
|
||||||
webserver.content_send("</fieldset>")
|
|
||||||
|
|
||||||
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 .. "<tr class='htr'><td colspan='4'>┆"
|
|
||||||
return self
|
|
||||||
end
|
|
||||||
|
|
||||||
def end_line()
|
|
||||||
self.msg_buffer .. "{e}"
|
|
||||||
return self
|
|
||||||
end
|
|
||||||
|
|
||||||
def next_line()
|
|
||||||
self.msg_buffer .. "{e}<tr class='htr'><td colspan='4'>┆"
|
|
||||||
return self
|
|
||||||
end
|
|
||||||
|
|
||||||
def begin_tooltip(ttip)
|
|
||||||
self.msg_buffer .. format(" <div title='%s' class='si'>", ttip)
|
|
||||||
return self
|
|
||||||
end
|
|
||||||
|
|
||||||
def end_tooltip()
|
|
||||||
self.msg_buffer .. "</div>"
|
|
||||||
return self
|
|
||||||
end
|
|
||||||
|
|
||||||
def add_link(title, url, target)
|
|
||||||
if !target target = "_blank" end
|
|
||||||
self.msg_buffer .. format(" <a target=%s href='%s'>%s</a>", 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("<tr class='ltd htr'><td><b title='%s'>%s</b></td>", 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("<td><i class=\"bt\" title=\"%.3fV (%s)\" style=\"--bl:%dpx;color:var(--c_txt)\"></i></td>",
|
|
||||||
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("<td><i class=\"bt\" title=\"%d%% (%s)\" style=\"--bl:%dpx;color:var(--c_txt)\"></i></td>",
|
|
||||||
pbatt, self.dhm(battery_last_seen), batt_percent)
|
|
||||||
else
|
|
||||||
msg += "<td> </td>"
|
|
||||||
end
|
|
||||||
|
|
||||||
if rssi < 1000
|
|
||||||
if rssi < -132 rssi = -132 end
|
|
||||||
var num_bars = 4 - ((rssi * -1) / 33)
|
|
||||||
msg += format("<td><div title='RSSI %i' class='si'>", rssi)
|
|
||||||
for j:0..3
|
|
||||||
msg += format("<i class='b%d%s'></i>", j, (num_bars < j) ? " o30" : "") # Bars
|
|
||||||
end
|
|
||||||
msg += "</div></td>" # Close RSSI
|
|
||||||
else
|
|
||||||
msg += "<td> </td>"
|
|
||||||
end
|
|
||||||
|
|
||||||
msg += format("<td style='color:var(--c_txt)'>🕗%s</td></tr>", 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("</table>" # Terminate current two column table and open new table
|
|
||||||
"<style>"
|
|
||||||
# Table CSS
|
|
||||||
".ltd td:not(:first-child){width:20px;font-size:70%%}"
|
|
||||||
".ltd td:last-child{width:45px}"
|
|
||||||
".ltd .bt{margin-right:10px;}" # Margin right should be half of the not-first width
|
|
||||||
".htr{line-height:20px}"
|
|
||||||
# Signal Strength Indicator
|
|
||||||
".si{display:inline-flex;align-items:flex-end;height:15px;padding:0}"
|
|
||||||
".si i{width:3px;margin-right:1px;border-radius:3px;background-color:var(--c_txt)}" # WebColor(COL_TEXT)
|
|
||||||
".si .b0{height:25%%}.si .b1{height:50%%}.si .b2{height:75%%}.si .b3{height:100%%}.o30{opacity:.3}"
|
|
||||||
"</style>"
|
|
||||||
"{t}%s</table>{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
|
|
||||||
###################################################################################
|
|
||||||
@ -1,8 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "LoRaWan Decoders",
|
|
||||||
"version": "0x190A0200",
|
|
||||||
"description": "Decode LoRaWan devices",
|
|
||||||
"author": "Theo Arends",
|
|
||||||
"min_tasmota": "0x0E060001",
|
|
||||||
"features": ""
|
|
||||||
}
|
|
||||||
@ -1,242 +0,0 @@
|
|||||||
# 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-<node>
|
|
||||||
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
|
|
||||||
Loading…
Reference in New Issue
Block a user