diff --git a/tasmota/berry/extensions/Devices_Online.tapp b/tasmota/berry/extensions/Devices_Online.tapp
new file mode 100644
index 000000000..f1d08f5d8
Binary files /dev/null and b/tasmota/berry/extensions/Devices_Online.tapp differ
diff --git a/tasmota/berry/extensions/Devices_Online/autoexec.be b/tasmota/berry/extensions/Devices_Online/autoexec.be
new file mode 100755
index 000000000..47034daa8
--- /dev/null
+++ b/tasmota/berry/extensions/Devices_Online/autoexec.be
@@ -0,0 +1,9 @@
+# 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')
diff --git a/tasmota/berry/extensions/Devices_Online/devices_online.be b/tasmota/berry/extensions/Devices_Online/devices_online.be
new file mode 100755
index 000000000..fb5b1b707
--- /dev/null
+++ b/tasmota/berry/extensions/Devices_Online/devices_online.be
@@ -0,0 +1,885 @@
+###################################################################################
+# devices_online.be - ESP32 Extension for Tasmota
+#
+# SPDX-FileCopyrightText: 2025 Stephan Hadinger & Theo Arends
+#
+# SPDX-License-Identifier: GPL-3.0-only
+###################################################################################
+# Display in Main GUI Devices Online based on non-invasive MQTT information from
+# retained Tasmota Discovery Config and STATE reports.
+# Enable Tasmota Discovery with command `SetOption19 0`.
+# For realtime power control do execute once command `SetOption59 1` on every
+# online device (ex. `publish cmnd/tasmotas/so59 1`).
+# This will send a STATE report on every POWER change.
+###################################################################################
+# rm Devices_Online.tapp; zip -j -0 Devices_Online.tapp Devices_Online/*
+###################################################################################
+
+import mqtt
+import json
+import string
+import webserver
+import persist
+
+###################################################################################
+# Display Configuration Devices Online GUI
+#---------------------------------------------------------------------------------#
+class dev_online_settings
+ #################################################################################
+ # init
+ #
+ # install the extension and allocate all resources
+ #################################################################################
+ def init()
+ 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("/dvo")
+ tasmota.remove_driver(self)
+ end
+
+ def web_add_config_button()
+ webserver.content_send("
")
+ end
+
+ def page_dev_online()
+ if !webserver.check_privileged_access() return nil end
+
+ if webserver.has_arg('save')
+ persist.dvo_lines = webserver.arg('dvo11')
+ persist.dvo_online_window = webserver.arg('dvo12')
+ persist.dvo_time_highlight = webserver.arg('dvo13')
+ persist.dvo_offline = str(int(webserver.arg('dvo15'))) # Only allow numbers
+ persist.dvo_hostname_suffix = webserver.arg('dvo14')
+ persist.dvo_devicename = (webserver.arg('dvo1')) ? 1 : 0
+ persist.dvo_ipaddress = (webserver.arg('dvo3')) ? 1 : 0
+ persist.dvo_power = (webserver.arg('dvo8')) ? 1 : 0
+ persist.dvo_version = (webserver.arg('dvo2')) ? 1 : 0
+ persist.dvo_heap = (webserver.arg('dvo6')) ? 1 : 0
+ persist.dvo_berryheap = (webserver.arg('dvo4')) ? 1 : 0
+ persist.dvo_berryobject = (webserver.arg('dvo7')) ? 1 : 0
+ persist.dvo_wifirssi = (webserver.arg('dvo5')) ? 1 : 0
+ persist.dvo_wifichnl = (webserver.arg('dvo9')) ? 1 : 0
+ persist.save()
+
+ webserver.redirect("/cn?") # Go back to Configuration menu
+ end
+
+ var dvo_lines = persist.find("dvo_lines", 0)
+ var dvo_online_window = persist.find("dvo_online_window", 600)
+ var dvo_time_highlight = persist.find("dvo_time_highlight", 10)
+ var dvo_offline = persist.find("dvo_offline", 600)
+ var dvo_hostname_suffix = persist.find("dvo_hostname_suffix", "")
+ var dvo_devicename = (persist.find("dvo_devicename", 0)) ? " checked" : ""
+ var dvo_ipaddress = (persist.find("dvo_ipaddress", 0)) ? " checked" : ""
+ var dvo_power = (persist.find("dvo_power", 0)) ? " checked" : ""
+ var dvo_version = (persist.find("dvo_version", 0)) ? " checked" : ""
+ var dvo_heap = (persist.find("dvo_heap", 0)) ? " checked" : ""
+ var dvo_berryheap = (persist.find("dvo_berryheap", 0)) ? " checked" : ""
+ var dvo_berryobject = (persist.find("dvo_berryobject", 0)) ? " checked" : ""
+ var dvo_wifirssi = (persist.find("dvo_wifirssi", 0)) ? " checked" : ""
+ var dvo_wifichnl = (persist.find("dvo_wifichnl", 0)) ? " checked" : ""
+
+ webserver.content_start("DevOnline") #- title of the web page -#
+ webserver.content_send_style() #- send standard Tasmota styles -#
+
+ webserver.content_send(
+ f""
+ )
+
+ webserver.content_button(webserver.BUTTON_CONFIGURATION) #- button back to conf page -#
+ webserver.content_stop() #- end of web page -#
+ end
+
+ def web_add_handler()
+ webserver.on("/dvo", / -> self.page_dev_online())
+ end
+end
+#---------------------------------------------------------------------------------#
+# Display Configuration Devices Online GUI
+###################################################################################
+
+
+###################################################################################
+# extension devices_online
+#---------------------------------------------------------------------------------#
+class devices_online
+ var mqtt_state # MQTT tele STATE subscribe format
+ var mqtt_topic_idx # Index of %topic% within full topic
+ var mqtt_step # MQTT message state
+ var every_second_counter
+ var sort_last_column # Sort last column
+ var list_devices # Buffer storing devices
+ var dvo_settings
+
+ #################################################################################
+ # init
+ #
+ # install the extension and allocate all resources
+ #################################################################################
+ def init()
+ if !persist.has("dvo_column")
+ persist.dvo_lines = 0 # Show growing list of devices
+ persist.dvo_online_window = 600 # Number of teleperiod seconds for devices to be shown as online
+ persist.dvo_time_highlight = 10 # Highlight latest change duration in seconds
+ persist.dvo_offline = 600 # Number of seconds for devices to be shown as offline
+ persist.dvo_hostname_suffix = "" # Optional hostname suffix like "."
+ persist.dvo_devicename = 0 # Show device name
+ persist.dvo_ipaddress = 0 # Show IP address
+ persist.dvo_power = 0 # Show power
+ persist.dvo_version = 0 # Show version
+ persist.dvo_heap = 0 # Show Heap (KB)
+ persist.dvo_berryheap = 0 # Show Berry HeapUsed (KB)
+ persist.dvo_berryobject = 0 # Show Berry Object Count
+ persist.dvo_wifirssi = 0 # Show WiFi RSSI (%)
+ persist.dvo_column = 0 # Sort column, default Hostname
+ persist.dvo_direction = 0 # Sort direction (0) Up or (1) Down, default Up
+ persist.save()
+ end
+ self.sort_last_column = persist.dvo_column # Sort last column to detect direction toggle
+ self.every_second_counter = 0
+
+ self.list_devices = [] # Init device buffer list
+
+ var parts = string.split(tasmota.cmd('_FullTopic', true)['FullTopic'], '/')
+ var prefix3 = tasmota.cmd("Prefix", true)['Prefix3'] # tele = Prefix3 used by STATE message
+ self.mqtt_topic_idx = -1
+ for ix : 0..size(parts)-1
+ var level = parts[ix]
+ if level == '%prefix%'
+ parts[ix] = prefix3
+ elif level == '%topic%'
+ parts[ix] = '+'
+ self.mqtt_topic_idx = ix
+ elif level == ''
+ parts[ix] = 'STATE'
+ else
+ parts[ix] = '+'
+ end
+ end
+ self.mqtt_state = parts.concat('/') # default = tele/+/STATE
+
+ if self.mqtt_topic_idx == -1
+ log("DVO: ERROR No %topic% in FullTopic defined", 1)
+ return
+ end
+
+ tasmota.add_driver(self)
+
+ mqtt.subscribe(self.mqtt_state, /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))
+
+ self.mqtt_step = 0
+ if !mqtt.connected()
+ log("DVO: Need MQTT connected", 1)
+ end
+
+ self.dvo_settings = dev_online_settings()
+ end
+
+ #################################################################################
+ # unload
+ #
+ # Uninstall the extension and deallocate all resources
+ #################################################################################
+ def unload()
+ mqtt.unsubscribe("tasmota/discovery/+/config")
+ mqtt.unsubscribe(self.mqtt_state)
+ self.dvo_settings.close()
+ tasmota.remove_driver(self.dvo_settings)
+ tasmota.remove_driver(self)
+ self.list_devices.clear()
+ end
+
+ #################################################################################
+ # handle_discovery_data(discovery_topic, idx, data, databytes)
+ #
+ # Handle retained MQTT Tasmota Discovery Config data
+ #
+ # A restarted device also provides retained Tasmota Discovery Config data but
+ # that will be received AFTER a restart STATE message therefor we need to handle
+ # discovery data as an update to already received STATE data
+ #################################################################################
+ def handle_discovery_data(discovery_topic, idx, data, databytes)
+ if self.mqtt_step == 0
+ self.mqtt_step = 1
+ log("DVO: Discovery started...", 3)
+# log(format("DVO: Devices size %d, Memory %s", self.list_devices.size(), tasmota.memory()), 3)
+ end
+ 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 version_splits = string.split(version, ".")
+ var version_int = 0
+ var multiplier = 0x1000000
+ for split : version_splits
+ version_int += int(split) * multiplier
+ if multiplier
+ multiplier /= 0x100
+ end
+ end
+ var version_num = format("%011i", version_int) # 00235143427 - Convert to string to enable multicolumn sort
+ var last_seen = str(tasmota.rtc('local'))
+ var uptime = " "
+ var uptime_sec = " "
+ var heap = " "
+ var berryheap = " "
+ var berryobject = " "
+ var wifirssi = " "
+ var wifichnl = " "
+ var power = []
+
+ # 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14
+ var line = [topic, hostname, ipaddress, devicename, version, version_num, last_seen, uptime, uptime_sec, berryheap, wifirssi, heap, berryobject, power, wifichnl]
+ var update = 0;
+ for i: self.list_devices.keys()
+ if self.list_devices[i][0] == topic
+ update = 1
+ if self.list_devices[i][8] != " " # Process posible received STATE data
+ uptime = self.list_devices[i][7]
+ uptime_sec = self.list_devices[i][8]
+ berryheap = self.list_devices[i][9]
+ wifirssi = self.list_devices[i][10]
+ heap = self.list_devices[i][11]
+ berryobject = self.list_devices[i][12]
+ power = self.list_devices[i][13]
+ wifichnl = self.list_devices[i][14]
+ end
+# log(f"DVO: Discovery --- update {self.list_devices[i]}", 3)
+ var update_line = [topic, hostname, ipaddress, devicename, version, version_num, last_seen, uptime, uptime_sec, berryheap, wifirssi, heap, berryobject, power, wifichnl]
+# log(f"DVO: Discovery +++ update {update_line}", 3)
+ self.list_devices[i] = update_line # Update current config
+ break
+ end
+ end
+ if update == 0
+# log(f"DVO: Discovery +++ add {line}", 3)
+ self.list_devices.push(line) # Add (re-discovered) config as last entry
+ end
+ 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)
+ if self.mqtt_step == 1
+ self.mqtt_step = 2
+ log("DVO: Discovery complete", 3)
+ end
+# log(f"DVO: STATE topic '{tele_topic}'", 4)
+ var subtopic = string.split(tele_topic, "/")
+ if subtopic[-1] == "STATE" # we are only serving topic ending in STATE
+ var topic = subtopic[self.mqtt_topic_idx]
+ var state = json.load(data) # Assume topic is in retained discovery list
+ if state # Valid JSON state message
+
+ var hostname = " "
+ var ipaddress = " "
+ var devicename = " "
+ var version = " "
+ var version_num = " "
+ var topic_index = -1
+ for i: self.list_devices.keys()
+ if self.list_devices[i][0] == topic
+ hostname = self.list_devices[i][1]
+ ipaddress = self.list_devices[i][2]
+ devicename = self.list_devices[i][3]
+ version = self.list_devices[i][4]
+ version_num = self.list_devices[i][5]
+ topic_index = i
+ break
+ end
+ end
+
+ # 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"}
+ if state.find('Hostname')
+ # > Tasmota v15.0.1.2
+ hostname = state['Hostname'] # atomlite2
+ ipaddress = state['IPAddress'] # 192.168.2.208
+ end
+
+ var power = [] # [0]
+ var power_state = ""
+ for i: 1..4
+ if i == 1
+ if state.find("POWER")
+ power_state = state["POWER"]
+ end
+ end
+ var keystr = f"POWER{i}"
+ if state.find(keystr)
+ power_state = state[keystr]
+ end
+ if size(power_state)
+ power.push((power_state == "OFF") ? 0 : 1)
+ power_state = ""
+ end
+ end
+
+ var last_seen = str(tasmota.rtc('local'))
+ var uptime = state['Uptime'] # 0T00:15:09
+ var uptime_sec_int
+ if state.find('UptimeSec')
+ uptime_sec_int = state['UptimeSec'] # 909
+ else
+ # < Tasmota v7.0.0.0
+ var uptime_str = string.replace(uptime, "T", ":") # 0T00:15:09 -> 0:00:15:09
+ var uptime_splits = string.split(uptime_str, ":")
+ uptime_sec_int = (int(uptime_splits[0]) * 86400) + # 0 * 86400
+ (int(uptime_splits[1]) * 3600) + # 00 * 3600
+ (int(uptime_splits[2]) * 60) + # 15 * 60
+ int(uptime_splits[3]) # 09
+ end
+ var uptime_sec = format("%011d", uptime_sec_int) # 00000000909 - Convert to string to enable multicolumn sort
+
+ var iheap = 0
+ if state.find('Heap')
+ iheap = state['Heap'] # 142
+ end
+ var heap = format("%05i", iheap) # 00142 - Convert to string to enable multicolumn sort
+
+ var bheap = 0
+ if state.find('Berry') && state['Berry'].find('HeapUsed')
+ bheap = state['Berry']['HeapUsed'] # 12
+ end
+ var berryheap = format("%05i", bheap) # 00012 - Convert to string to enable multicolumn sort
+
+ var bobject = 0
+ if state.find('Berry') && state['Berry'].find('Objects')
+ bobject = state['Berry']['Objects'] # 167
+ end
+ var berryobject = format("%05i", bobject) # 00167 - Convert to string to enable multicolumn sort
+
+ var wrssi = 0
+ if state.find('Wifi') && state['Wifi'].find('RSSI')
+ wrssi = state['Wifi']['RSSI'] # 100
+ end
+ var wifirssi = format("%03i", wrssi) # 100 - Convert to string to enable multicolumn sort
+
+ var wchnl = 0
+ if state.find('Wifi') && state['Wifi'].find('Channel')
+ wchnl = state['Wifi']['Channel'] # 11
+ end
+ var wifichnl = format("%03i", wchnl) # 011 - Convert to string to enable multicolumn sort
+
+ # 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14
+ var line = [topic, hostname, ipaddress, devicename, version, version_num, last_seen, uptime, uptime_sec, berryheap, wifirssi, heap, berryobject, power, wifichnl]
+
+ var dvo_online_window = int(persist.find("dvo_online_window", 600))
+ var dvo_offline = int(persist.find("dvo_offline", 600))
+ var time_window = int(last_seen) - dvo_online_window
+ var update = 0;
+ var list_size = size(self.list_devices)
+ if list_size > 0
+ var list_index = 0
+ while list_index < list_size # Use while loop as counter is decremented
+ var list_last_seen = int(self.list_devices[list_index][6])
+ var list_uptime_sec = int(self.list_devices[list_index][8]) # Is incremental negative if offline
+ var time_window_offline = dvo_offline + list_uptime_sec # Is negative if offline for over dvo_offline seconds
+ var list_heap = int(self.list_devices[list_index][11])
+ if update == 0 &&
+ self.list_devices[list_index][0] == topic
+ update = 1
+# log(f"DVO: State --- update {self.list_devices[list_index]}", 3)
+ self.list_devices[list_index] = line # Update state
+# log(f"DVO: State +++ update {line}", 3)
+ elif self.list_devices[list_index][1] == hostname || # Remove duplicate hostname
+ self.list_devices[list_index][2] == ipaddress || # Remove duplicate IP address
+ (list_heap == 0 && time_window > list_last_seen) || # Remove offlines never seen
+ time_window_offline < 0 # Remove offlines after user visibility
+# log(f"DVO: State --- delete {self.list_devices[list_index]}", 3)
+ self.list_devices.remove(list_index) # Remove duplicates or offline device
+ list_size -= 1
+ list_index -= 1
+ end
+ list_index += 1
+ end
+ end
+ if update == 0
+# log(f"DVO: State +++ add {line}", 3)
+ self.list_devices.push(line) # Add state as last entry
+ end
+# log(format("DVO: Device size %d, Memory %s", self.list_devices.size(), tasmota.memory()), 3)
+ end
+ end
+ return true # return true to stop propagation as a Tasmota cmd
+ end
+
+ #################################################################################
+ # uptime_to_str(uptime_sec_int)
+ #
+ # return uptime string like "-0T01:02:03"
+ #################################################################################
+ def uptime_to_str(uptime_sec_int)
+ var seconds = uptime_sec_int
+ var sign = ""
+ if seconds < 0
+ sign = "-"
+ seconds *= -1
+ end
+ var days = seconds / 86400
+ seconds -= days * 86400
+ var hours = seconds / 3600
+ seconds -= hours * 3600
+ var minutes = seconds / 60
+ seconds -= minutes * 60
+ var uptime = f"{sign}{days}T{hours:02d}:{minutes:02d}:{seconds:02d}"
+ return uptime
+ end
+
+ #################################################################################
+ # every_second()
+ #
+ # Update offline devices
+ #################################################################################
+ def every_second()
+ if self.mqtt_step < 2 return end # Do not do anything until discovery is complete
+
+ self.every_second_counter += 1
+ if self.every_second_counter >= 60
+ self.every_second_counter = 0
+
+ # Update visible offline devices
+ var dvo_offline = int(persist.find("dvo_offline", 600))
+ if (dvo_offline > 0)
+ var dvo_online_window = int(persist.find("dvo_online_window", 600))
+ var now = tasmota.rtc('local')
+ for i: self.list_devices.keys()
+ # 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14
+ # [topic, hostname, ipaddress, devicename, version, version_num, last_seen, uptime, uptime_sec, berryheap, wifirssi, heap, berryobject, power, wifichnl]
+ var uptime = self.list_devices[i][7]
+ if uptime == " " continue end # No STATE info
+
+ var last_seen = self.list_devices[i][6]
+ var uptime_sec = self.list_devices[i][8]
+ var uptime_sec_int = int(uptime_sec)
+ if uptime_sec_int < 0 # Device is offline visible
+ uptime_sec_int -= 60
+ elif int(last_seen) < (now - dvo_online_window)
+ uptime_sec_int = int(last_seen) - now # Negative
+ end
+ if uptime_sec_int < 0
+ self.list_devices[i][8] = format("%011d", uptime_sec_int) # Convert to string to enable multicolumn sort
+ self.list_devices[i][7] = self.uptime_to_str(uptime_sec_int)
+ self.list_devices[i][6] = str(now)
+ end
+ end
+ end
+
+ end
+ 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)
+ var cmp = /a,b -> a < b # Sort up
+ if dir
+ cmp = /a,b -> a > b # Sort down
+ end
+
+ for i:1..size(l)-1
+ var k = l[i]
+ var ks = k[col] # Needle Primary key
+ if col != 1 # Hostname (unique)
+ if col == 14 # Channel
+ ks += k[10] # Needle RSSI
+ end
+ ks += k[1] # Needle Hostname (unique)
+ end
+ var j = i
+ while j > 0
+ var ls = l[j-1][col] # Haystack Primary key
+ if col != 1 # Hostname (unique)
+ if col == 14 # Channel
+ ls += l[j-1][10] # Haystack RSSI
+ end
+ ls += l[j-1][1] # Haystack Hostname (unique)
+ end
+ if cmp(ls, ks)
+ break
+ end
+ l[j] = l[j-1]
+ j -= 1
+ end
+ l[j] = k
+ end
+ end
+
+ #################################################################################
+ # web_sensor
+ #
+ # Display Devices Online in user selected sorted columns
+ #################################################################################
+ def web_sensor()
+ if self.mqtt_step < 2 return end # Do not display until discovery is complete
+
+ if webserver.has_arg("sd_sort")
+ persist.dvo_column = int(webserver.arg("sd_sort")) # Change sort column
+ if self.sort_last_column == persist.dvo_column
+ persist.dvo_direction ^= 1 # Toggle direction
+ end
+ self.sort_last_column = persist.dvo_column
+ persist.save()
+ elif webserver.has_arg("sd_pow")
+ var power_splits = string.split(webserver.arg("sd_pow"), "_")
+ var topic = power_splits[0]
+ var power = int(power_splits[1])
+ var fulltopic = tasmota.cmd('_FullTopic', true)['FullTopic']
+ var prefix = tasmota.cmd("_Prefix", true)['Prefix1'] # cmnd = Prefix1 used by commands
+ fulltopic = string.replace(fulltopic, "%prefix%", prefix)
+ fulltopic = string.replace(fulltopic, "%topic%", topic)
+ if fulltopic[-1] != '/'
+ fulltopic += '/'
+ end
+
+ # The following code could be eliminated when `SetOption59 1` was enabled on all
+ # online devices.
+ var power_state = 0
+ for i: self.list_devices.keys()
+ # Use topic as line index may have changed since GUI sd_pow was initiated
+ if self.list_devices[i][0] == topic
+ # We do not want to subscribe to power response so ...
+ # we need some abracadabra as direct manipulation ends in exception
+ var power_list = self.list_devices[i][13]
+ power_state = power_list[power -1] ^ 1 # Toggle state
+ power_list[power -1] = power_state
+ self.list_devices[i][13] = power_list
+ tasmota.cmd(format("publish %sPOWER%d %d", fulltopic, power, power_state), true)
+ break;
+ end
+ end
+ # it could have been replaced by a simple
+# tasmota.cmd(format("publish %sPOWER%d 2", fulltopic, power), true)
+ end
+
+ if self.list_devices.size()
+ var dvo_online_window = int(persist.find("dvo_online_window", 600))
+ var dvo_offline = int(persist.find("dvo_offline", 600))
+ var dvo_lines = int(persist.find("dvo_lines", 0))
+ var dvo_time_highlight = int(persist.find("dvo_time_highlight", 10))
+ var dvo_hostname_suffix = persist.find("dvo_hostname_suffix", "")
+ var dvo_devicename = persist.find("dvo_devicename", 0)
+ var dvo_ipaddress = persist.find("dvo_ipaddress", 0)
+ var dvo_power = persist.find("dvo_power", 0)
+ var dvo_version = persist.find("dvo_version", 0)
+ var dvo_heap = persist.find("dvo_heap", 0)
+ var dvo_berryheap = persist.find("dvo_berryheap", 0)
+ var dvo_berryobject = persist.find("dvo_berryobject", 0)
+ var dvo_wifirssi = persist.find("dvo_wifirssi", 0)
+ var dvo_wifichnl = persist.find("dvo_wifichnl", 0)
+
+ var msg = "" # Terminate two column table and open new table
+ msg += ""
+ msg += "
" # Open new condensed table
+
+ msg += "
"
+
+ if dvo_lines > 0 # Fixed number of last updated lines
+ self.sort_col(self.list_devices, 6, 1) # Sort list by last_seen and down
+
+ if dvo_devicename
+ msg += "
Device Name
"
+ end
+ msg += "
Hostname
"
+ if dvo_ipaddress
+ msg += "
IP Address
"
+ end
+ if dvo_power
+ msg += "
Power
"
+ end
+ if dvo_version
+ msg += "
Version
"
+ end
+ if dvo_heap
+ msg += "
Heap
"
+ end
+ if dvo_berryheap
+ msg += "
BHeap
"
+ end
+ if dvo_berryobject
+ msg += "
BObject
"
+ end
+ if dvo_wifirssi
+ msg += "
RSSI
"
+ end
+ if dvo_wifichnl
+ msg += "
Ch
"
+ end
+ msg += "
Uptime
"
+ else # All devices sorted by user selected column
+ # 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14
+ var list_dvo = [0, 1, dvo_ipaddress, dvo_devicename, 0, dvo_version, 0, 0, 1, dvo_berryheap, dvo_wifirssi, dvo_heap, dvo_berryobject, 0, dvo_wifichnl]
+ if persist.dvo_column >= list_dvo.size() || list_dvo[persist.dvo_column] == 0
+ persist.dvo_column = 1 # Hostname as default sort column
+ self.sort_last_column = persist.dvo_column
+ persist.save()
+ end
+# log(format("DVO: list_dvo '%s'", list_dvo), 3)
+
+# var start = tasmota.millis()
+ self.sort_col(self.list_devices, persist.dvo_column, persist.dvo_direction) # Sort list by column
+# var stop = tasmota.millis()
+# log(format("DVO: Sort time %d ms", stop - start), 3)
+
+ var icon_direction = persist.dvo_direction ? "▼" : "▲"
+ if dvo_devicename
+ msg += format("