From 5bd9414012d547c63f7e939468aaa4dd34ebf08c Mon Sep 17 00:00:00 2001 From: Fabrizio Amodio <32312585+ZioFabry@users.noreply.github.com> Date: Mon, 4 Aug 2025 11:43:34 +0200 Subject: [PATCH] Milesight WS522 LoraWan Decoder (Portable Socket) (#23750) * Milesight WS522 LoraWan Decoder (Portable Socket) * WS522 Bug fix * WS522 Code Refactoring, UI Improvment, Downlink Management --- .../decoders/vendors/milesight/WS522.be | 272 ++++++++++++++++++ 1 file changed, 272 insertions(+) create mode 100644 tasmota/berry/lorawan/decoders/vendors/milesight/WS522.be diff --git a/tasmota/berry/lorawan/decoders/vendors/milesight/WS522.be b/tasmota/berry/lorawan/decoders/vendors/milesight/WS522.be new file mode 100644 index 000000000..2b77190cb --- /dev/null +++ b/tasmota/berry/lorawan/decoders/vendors/milesight/WS522.be @@ -0,0 +1,272 @@ +# LoRaWAN Decoder file for Milesight WS522 +# +# References +# WS522 User Manual: https://resource.milesight.com/milesight/iot/document/ws52x-user-guide-en.pdf +# TTN Device Repository: https://github.com/TheThingsNetwork/lorawan-devices/blob/master/vendor/milesight-iot/ws52x.js + +import string + +global.ws522Nodes = {} + +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 + +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 + + 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 + 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 + 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) ) + end + 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( "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 + 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 msg = "" + for sensor: global.ws522Nodes + var name = sensor[0] + + # If LoRaWanName contains WS522 use WS522- + if string.find(name, "WS522") > -1 + name = string.format("WS522-%i", sensor[1]) + end + + var name_tooltip = "Milesight WS522" + + var last_seen = sensor[2] + var rssi = sensor[3] + + msg += 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] + + 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}" # = + end + return msg + end #add_web_sensor() +end #class + +LwDeco = LwDecoWS522