LoRaWAN Sensor standard formatter (#23761)

* LoRaWAN decoder standard formatter implemented

* New icons added

* Milesight WS202/WS301/WS522 standard formatter implementation

* Milesight WS202/WS301 bug fix

* little code optimization

* bug fix

* formatter sample usage

* Removed Icons handling from standard formatted, adjust standard sensor format, added links to standard formatter

* altitude formatter, add_link formatter, SendDownLink  function

* WS522 missing attribute, Downlink refactoring, UI Standardization
This commit is contained in:
Fabrizio Amodio 2025-08-07 15:25:34 +02:00 committed by GitHub
parent 324ac9b158
commit efb5ab1ef8
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 211 additions and 94 deletions

View File

@ -7,6 +7,88 @@ var LwDeco
import mqtt
import string
class LwSensorFormatter_cls
var Msg
static Formatter = {
"string": { "u": nil, "f": " %s", "i": nil },
"volt": { "u": "V", "f": " %.1f", "i": "⚡" }, # High Voltage ⚡
"milliamp": { "u": "mA", "f": " %.0f", "i": "🔌" }, # Electric Plug 🔌
"power_factor%": { "u": "%", "f": " %.0f", "i": "📊" }, # Bar Chart 📊
"power": { "u": "W", "f": " %.0f", "i": "💡" }, # Light Bulb 💡
"energy": { "u": "Wh", "f": " %.0f", "i": "🧮" }, # Abacus 🧮
"altitude": { "u": "mt", "f": " %d", "i": "⛰" }, # Moutain ⛰
"empty": { "u": nil, "f": nil, "i": nil }
}
def init()
self.Msg = ""
end
def start_line()
self.Msg += format("<tr class='htr'><td colspan='4'>&#9478;" ) # | <sensor1><sensor2>...
return self
end
def end_line()
self.Msg += "{e}" # End of line
return self
end
def next_line()
return self.end_line().start_line() # End of current line and start new line
end
def begin_tooltip(ttip)
self.Msg += format("&nbsp;<div title='%s' class='si'>", ttip)
return self
end
def end_tooltip()
self.Msg += "</div>"
return self
end
def add_link(title, url, target)
self.Msg += format( " <a target=%s href='%s'>%s</a>", ( target ? target : "_blank" ), 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 += format(" %s", alt_icon ) # Use alternative icon
elif fmt && fmt.find("i") && fmt["i"]
self.Msg += format(" %s", fmt["i"] ) # Use icon from formatter
end
if fmt && fmt.find("f") && fmt["f"]
self.Msg += format(fmt["f"], value)
else
self.Msg += str(value) # Default to string representation
end
if fmt && fmt.find("u") && fmt["u"]
self.Msg += format("%s", fmt["u"]) # Append unit if defined
end
if tooltip
return self.end_tooltip()
end
return self
end
def get_msg()
return self.Msg
end
end
class lwdecode_cls
var LwDecoders
var topic
@ -26,6 +108,17 @@ class lwdecode_cls
tasmota.add_rule("LwReceived", /value, trigger, payload -> self.LwDecode(payload))
end
def SendDownlink(nodes, cmd, idx, payload, ok_result)
if nodes.find(idx)
var sendcmd = 'LoRaWanSend'
var output = tasmota.cmd( f'{sendcmd}{idx} {payload}', true)
if output.find(sendcmd) == 'Done'
return tasmota.resp_cmnd(f'{{"{cmd}{idx}":"{ok_result}"}}')
end
return output
end
end
def LwDecode(data)
import json
@ -85,6 +178,10 @@ class lwdecode_cls
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 color_text = f'{tasmota.webcolor(0 #-COL_TEXT-#)}' # '#eaeaea'
var msg = "<tr class='ltd htr'>" # ==== Start first table row

View File

@ -94,20 +94,20 @@ class LwDecoWS202
msg += lwdecode.header(name, name_tooltip, battery + 100000, battery_last_seen, rssi, last_seen)
# Sensors
var pir = sensor[6]
var pir_last_seen = sensor[7]
var light = sensor[8]
var light_last_seen = sensor[9]
var pir = lwdecode.dhm(sensor[7])
var pir_ls = lwdecode.dhm_tt(sensor[7])
var pir_alt = (sensor[6] == true ? "&#x1F6AB;" : "&#x1F193;") # No Entry 🚫 / Free 🆓
msg += "<tr class='htr'><td colspan='4'>&#9478;" # |
var light = lwdecode.dhm(sensor[9])
var light_ls = lwdecode.dhm_tt(sensor[9])
var light_alt = (sensor[8] == 0) ? "&#x1F315;" : "&#x1F31E;" # Moon 🌕 / Sun 🌞
msg += string.format(" %s %s", pir == true ? "&#x1F6AB" : "&#x1F193", # PIR Free or Busy
lwdecode.dhm(pir_last_seen))
msg += string.format(" %s %s", light == 0 ? "&#x1F315" : "&#x1F31E", # Light
lwdecode.dhm(light_last_seen))
msg += "{e}" # = </td></tr>
var fmt = LwSensorFormatter_cls()
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()

View File

@ -94,19 +94,20 @@ class LwDecoWS301
msg += lwdecode.header(name, name_tooltip, battery + 100000, battery_last_seen, rssi, last_seen)
# Sensors
var door_open = sensor[6]
var door_open_last_seen = sensor[7]
var installed = sensor[8]
var installed_last_seen = sensor[9]
var dopen = lwdecode.dhm(sensor[7])
var dopen_tt = nil
var dopen_alt = (sensor[6] == true) ? "&#x1F513;" : "&#x1F512;" # Open Lock 🔓 / Lock 🔒
msg += "<tr class='htr'><td colspan='4'>&#9478;" # |
msg += string.format(" %s %s", (door_open == true) ? "&#x1F513" : "&#x1F512", # Open or Closed lock - Door
lwdecode.dhm(door_open_last_seen))
var inst = lwdecode.dhm(sensor[9])
var inst_tt = nil
var inst_alt = (sensor[8] == true) ? "&#x2705;" : "&#x274C;" # Heavy Check Mark ✅ / Cross Mark ❌
msg += string.format(" %s %s", (installed == true) ? "&#x2705" : "&#x274C", # Installed
lwdecode.dhm(installed_last_seen))
msg += "{e}" # = </td></tr>
var fmt = LwSensorFormatter_cls()
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()

View File

@ -119,6 +119,38 @@ class LwDecoWS522
data.insert("Button_State", button_state ? "Open" : "Close" )
i += 1
# FE02(ReportInterval) 3C00=>06
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
# FF0b(PowerOn) Deviceison
elif channel_id == 0xFF && channel_type == 0x0B
i += 1
# FF16(DeviceSN) 16digits
elif channel_id == 0xFF && channel_type == 0x16
i += 8
# FF0f(DeviceType) 00:ClassA,01:ClassB,02:ClassC
elif channel_id == 0xFF && channel_type == 0x0F
i += 1
else
log( string.format("WS522: something missing? id={%s} type={%s}", channel_id, channel_type), 1)
@ -130,56 +162,40 @@ class LwDecoWS522
if valid_values
if !command_init
tasmota.add_cmd( "LoraWS522Power",
tasmota.add_cmd( "LwWS522Power",
def (cmd, idx, payload)
if global.ws522Nodes.find(idx)
if payload == "1" || string.toupper(payload) == "ON"
tasmota.cmd(string.format("LoraWanSend%d 080100FF",idx))
return lwdecode.SendDownlink(global.ws522Nodes, cmd, idx, '080100FF', 'ON')
elif payload == "0" || string.toupper(payload) == "OFF"
tasmota.cmd(string.format("LoraWanSend%d 080000FF",idx))
else
# nothing else
end
end
end
)
tasmota.add_cmd( "LoraWS522Period",
def (cmd, idx, payload)
if global.ws522Nodes.find(idx)
if number(payload) > 30
tasmota.cmd( string.format("LoraWanSend%d FF02%s", idx, uint16le(number(payload))) )
end
end
end
)
tasmota.add_cmd( "LoraWS522Reboot",
def (cmd, idx, payload)
if global.ws522Nodes.find(idx)
tasmota.cmd( string.format("LoraWanSend%d FF10FF", idx) )
return lwdecode.SendDownlink(global.ws522Nodes, cmd, idx, '080000FF', 'OFF')
end
end
)
tasmota.add_cmd( "LoraWS522ResetPowerUsage",
tasmota.add_cmd( "LwWS522Period",
def (cmd, idx, payload)
if global.ws522Nodes.find(idx)
tasmota.cmd( string.format("LoraWanSend%d FF27FF", idx) )
end
return lwdecode.SendDownlink(global.ws522Nodes, cmd, idx, format('FF02%s',uint16le(number(payload))), number(payload))
end
)
tasmota.add_cmd( "LoraWS522PowerLock",
tasmota.add_cmd( "LwWS522Reboot",
def (cmd, idx, payload)
return lwdecode.SendDownlink(global.ws522Nodes, cmd, idx, 'FF10FF', 'Done')
end
)
tasmota.add_cmd( "LwWS522ResetPowerUsage",
def (cmd, idx, payload)
return lwdecode.SendDownlink(global.ws522Nodes, cmd, idx, 'FF27FF', 'Done')
end
)
tasmota.add_cmd( "LwWS522PowerLock",
def (cmd, idx, payload)
if global.ws522Nodes.find(idx)
if payload == "1" || string.toupper(payload) == "ON"
tasmota.cmd(string.format("LoraWanSend%d FF250080",idx))
return lwdecode.SendDownlink(global.ws522Nodes, cmd, idx, 'FF250080', 'ON')
elif payload == "0" || string.toupper(payload) == "OFF"
tasmota.cmd(string.format("LoraWanSend%d FF250000",idx))
else
# nothing else
end
return lwdecode.SendDownlink(global.ws522Nodes, cmd, idx, 'FF250080', 'OFF')
end
end
)
@ -233,37 +249,40 @@ class LwDecoWS522
msg += lwdecode.header(name, name_tooltip, 1000, last_seen, rssi, last_seen)
# IEC Power Symbols
# Power &#x23FB; ⏻
# Toggle Power &#x23FC; ⏼
# Power On &#x23FD;
# Power Off &#x2B58; ⭘
# Sleep Mode &#x23FE; ⏾
# Sensors
var voltage = sensor[4]
var active_power = sensor[5]
var power_factor = sensor[6]
var energy_sum = sensor[7]
var current = sensor[8]
var button_state = sensor[9] ? "&#x23FD;" : "&#x2B58;"
var voltage_ls = sensor[10]
var active_power_ls = sensor[11]
var power_factor_ls = sensor[12]
var energy_sum_ls = sensor[13]
var current_ls = sensor[14]
var button_state_ls = sensor[15]
var voltage_tt = lwdecode.dhm(sensor[10])
msg += "<tr class='htr'><td colspan='4'>&#9478;" # |
msg += string.format(" %s %.1fV", "&#x26A1;", voltage) # High Voltage Icon
msg += string.format(" %s %dmA", "&#x1F50C;", current) # Electric Plug Icon
msg += string.format(" %s %d%%", "&#x1F4CA;", power_factor) # Bar Chart Icon
msg += string.format(" %s %dw", "&#x1F4A1;", active_power) # Light Bulb Icon
msg += "{e}<tr class='htr'><td colspan='4'>&#9478;" # |
msg += string.format(" %s", button_state) # Button Sate ON | OFF icon
msg += string.format(" %s %s", "&#x23F1;", lwdecode.dhm(button_state_ls)) # Stopwatch icon
msg += string.format(" %s %dWh", "&#x1F9EE;", energy_sum) # Abacus Icon
msg += "{e}" # = </td></tr>
var active_power = sensor[5]
var active_power_tt = lwdecode.dhm(sensor[11])
var power_factor = sensor[6]
var power_factor_tt = lwdecode.dhm(sensor[12])
var current = sensor[8]
var current_tt = lwdecode.dhm(sensor[14])
var button_state = lwdecode.dhm(sensor[15])
var button_state_tt = lwdecode.dhm(sensor[15])
var button_state_icon = (sensor[9] ? " &#x1F7E2; " : " &#x26AB; ") # Large Green Circle 🟢 | Medium Black Circle ⚫
var energy_sum = sensor[7]
var energy_sum_tt = lwdecode.dhm(sensor[13] )
var fmt = LwSensorFormatter_cls()
# 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()