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()