From efb5ab1ef859c57de5f51887187bdbaa64b85a56 Mon Sep 17 00:00:00 2001
From: Fabrizio Amodio <32312585+ZioFabry@users.noreply.github.com>
Date: Thu, 7 Aug 2025 15:25:34 +0200
Subject: [PATCH] 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
---
tasmota/berry/lorawan/decoders/LwDecode.be | 97 +++++++++++
.../decoders/vendors/milesight/WS202.be | 26 +--
.../decoders/vendors/milesight/WS301.be | 23 +--
.../decoders/vendors/milesight/WS522.be | 159 ++++++++++--------
4 files changed, 211 insertions(+), 94 deletions(-)
diff --git a/tasmota/berry/lorawan/decoders/LwDecode.be b/tasmota/berry/lorawan/decoders/LwDecode.be
index 85d81100e..758f3637f 100644
--- a/tasmota/berry/lorawan/decoders/LwDecode.be
+++ b/tasmota/berry/lorawan/decoders/LwDecode.be
@@ -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("
| ┆" ) # | ...
+ 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(" ", ttip)
+ return self
+ end
+
+ def end_tooltip()
+ self.Msg += " "
+ return self
+ end
+
+ def add_link(title, url, target)
+ self.Msg += format( " %s", ( 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 = "" # ==== Start first table row
diff --git a/tasmota/berry/lorawan/decoders/vendors/milesight/WS202.be b/tasmota/berry/lorawan/decoders/vendors/milesight/WS202.be
index 9b110a96a..c6366104b 100644
--- a/tasmota/berry/lorawan/decoders/vendors/milesight/WS202.be
+++ b/tasmota/berry/lorawan/decoders/vendors/milesight/WS202.be
@@ -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 ? "🚫" : "🆓") # No Entry 🚫 / Free 🆓
+
+ var light = lwdecode.dhm(sensor[9])
+ var light_ls = lwdecode.dhm_tt(sensor[9])
+ var light_alt = (sensor[8] == 0) ? "🌕" : "🌞" # Moon 🌕 / Sun 🌞
- msg += " | ┆" # |
-
- msg += string.format(" %s %s", pir == true ? "🚫" : "🆓", # PIR Free or Busy
- lwdecode.dhm(pir_last_seen))
-
- msg += string.format(" %s %s", light == 0 ? "🌕" : "🌞", # Light
- lwdecode.dhm(light_last_seen))
-
- msg += "{e}" # = |
+ 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()
diff --git a/tasmota/berry/lorawan/decoders/vendors/milesight/WS301.be b/tasmota/berry/lorawan/decoders/vendors/milesight/WS301.be
index 323b132cd..c2e6dc424 100644
--- a/tasmota/berry/lorawan/decoders/vendors/milesight/WS301.be
+++ b/tasmota/berry/lorawan/decoders/vendors/milesight/WS301.be
@@ -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) ? "🔓" : "🔒" # Open Lock 🔓 / Lock 🔒
- msg += "| ┆" # |
- msg += string.format(" %s %s", (door_open == true) ? "🔓" : "🔒", # 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) ? "✅" : "❌" # Heavy Check Mark ✅ / Cross Mark ❌
- msg += string.format(" %s %s", (installed == true) ? "✅" : "❌", # Installed
- lwdecode.dhm(installed_last_seen))
-
- msg += "{e}" # = |
+ 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()
diff --git a/tasmota/berry/lorawan/decoders/vendors/milesight/WS522.be b/tasmota/berry/lorawan/decoders/vendors/milesight/WS522.be
index 2b77190cb..e063a1efb 100644
--- a/tasmota/berry/lorawan/decoders/vendors/milesight/WS522.be
+++ b/tasmota/berry/lorawan/decoders/vendors/milesight/WS522.be
@@ -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,58 +162,42 @@ class LwDecoWS522
if valid_values
if !command_init
- tasmota.add_cmd( "LoraWS522Power",
- def (cmd, idx, payload)
- if global.ws522Nodes.find(idx)
- if payload == "1" || string.toupper(payload) == "ON"
- tasmota.cmd(string.format("LoraWanSend%d 080100FF",idx))
- elif payload == "0" || string.toupper(payload) == "OFF"
- tasmota.cmd(string.format("LoraWanSend%d 080000FF",idx))
- else
- # nothing else
- end
- end
+ tasmota.add_cmd( "LwWS522Power",
+ def (cmd, idx, payload)
+ if payload == "1" || string.toupper(payload) == "ON"
+ return lwdecode.SendDownlink(global.ws522Nodes, cmd, idx, '080100FF', 'ON')
+ elif payload == "0" || string.toupper(payload) == "OFF"
+ return lwdecode.SendDownlink(global.ws522Nodes, cmd, idx, '080000FF', 'OFF')
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( "LwWS522Period",
+ def (cmd, idx, payload)
+ return lwdecode.SendDownlink(global.ws522Nodes, cmd, idx, format('FF02%s',uint16le(number(payload))), number(payload))
+ end
)
- tasmota.add_cmd( "LoraWS522Reboot",
- def (cmd, idx, payload)
- if global.ws522Nodes.find(idx)
- tasmota.cmd( string.format("LoraWanSend%d FF10FF", idx) )
- end
- end
+ tasmota.add_cmd( "LwWS522Reboot",
+ def (cmd, idx, payload)
+ return lwdecode.SendDownlink(global.ws522Nodes, cmd, idx, 'FF10FF', 'Done')
+ end
)
- tasmota.add_cmd( "LoraWS522ResetPowerUsage",
- def (cmd, idx, payload)
- if global.ws522Nodes.find(idx)
- tasmota.cmd( string.format("LoraWanSend%d FF27FF", idx) )
- end
- end
+ tasmota.add_cmd( "LwWS522ResetPowerUsage",
+ def (cmd, idx, payload)
+ return lwdecode.SendDownlink(global.ws522Nodes, cmd, idx, 'FF27FF', 'Done')
+ end
)
- tasmota.add_cmd( "LoraWS522PowerLock",
- def (cmd, idx, payload)
- if global.ws522Nodes.find(idx)
- if payload == "1" || string.toupper(payload) == "ON"
- tasmota.cmd(string.format("LoraWanSend%d FF250080",idx))
- elif payload == "0" || string.toupper(payload) == "OFF"
- tasmota.cmd(string.format("LoraWanSend%d FF250000",idx))
- else
- # nothing else
- end
- end
+ tasmota.add_cmd( "LwWS522PowerLock",
+ def (cmd, idx, payload)
+ if payload == "1" || string.toupper(payload) == "ON"
+ return lwdecode.SendDownlink(global.ws522Nodes, cmd, idx, 'FF250080', 'ON')
+ elif payload == "0" || string.toupper(payload) == "OFF"
+ return lwdecode.SendDownlink(global.ws522Nodes, cmd, idx, 'FF250080', 'OFF')
end
+ end
)
command_init = true
end
@@ -233,37 +249,40 @@ class LwDecoWS522
msg += lwdecode.header(name, name_tooltip, 1000, last_seen, rssi, last_seen)
- # IEC Power Symbols
- # Power ⏻ ⏻
- # Toggle Power ⏼ ⏼
- # Power On ⏽ ⏽
- # Power Off ⭘ ⭘
- # Sleep Mode ⏾ ⏾
-
# 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] ? "⏽" : "⭘"
- 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 += "| ┆" # |
- msg += string.format(" %s %.1fV", "⚡", voltage) # High Voltage Icon
- msg += string.format(" %s %dmA", "🔌", current) # Electric Plug Icon
- msg += string.format(" %s %d%%", "📊", power_factor) # Bar Chart Icon
- msg += string.format(" %s %dw", "💡", active_power) # Light Bulb Icon
- msg += "{e} | | ┆" # |
- msg += string.format(" %s", button_state) # Button Sate ON | OFF icon
- msg += string.format(" %s %s", "⏱", lwdecode.dhm(button_state_ls)) # Stopwatch icon
- msg += string.format(" %s %dWh", "🧮", energy_sum) # Abacus Icon
- msg += "{e}" # = |
+ 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] ? " 🟢 " : " ⚫ ") # 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()
|