@@ -27,7 +27,9 @@ class devices_online
2727 static var line_highlight_color = "yellow" # Latest change highlight HTML color like "#FFFF00" or "yellow"
2828 static var line_lowuptime_color = "lime" # Low uptime highlight HTML color like "#00FF00" or "lime"
2929
30- var mqtt_tele # MQTT tele STATE subscribe format
30+ var mqtt_state # MQTT tele STATE subscribe format
31+ var mqtt_topic_idx # Index of %topic% within full topic
32+ var mqtt_step # MQTT message state
3133 var bool_devicename # Show device name
3234 var bool_version # Show version
3335 var bool_ipaddress # Show IP address
@@ -60,13 +62,38 @@ class devices_online
6062 self .list_buffer = [] # Init line buffer list
6163 self .list_config = [] # Init retained config buffer list
6264
63- # var full_topic = tasmota.cmd("FullTopic", true)['FullTopic'] # "%prefix%/%topic%/"
64- var prefix_tele = tasmota.cmd ( "Prefix" , true )[ 'Prefix3' ] # tele = Prefix3 used by STATE message
65- self .mqtt_tele = format ( "%s/#" , prefix_tele)
66- mqtt.subscribe ( self .mqtt_tele , / topic, idx, data, databytes -> self .handle_state_data ( topic, idx, data, databytes))
67- mqtt.subscribe ( "tasmota/discovery/+/config" , / topic, idx, data, databytes -> self .handle_discovery_data ( topic, idx, data, databytes))
65+ var parts = string.split ( tasmota.cmd ( '_FullTopic' , true )[ 'FullTopic' ] , '/' )
66+ var prefix3 = tasmota.cmd ( "Prefix" , true )[ 'Prefix3' ] # tele = Prefix3 used by STATE message
67+ self .mqtt_topic_idx = - 1
68+ for ix : 0 . .size ( parts)- 1
69+ var level = parts[ ix]
70+ if level == '%prefix%'
71+ parts[ ix] = prefix3
72+ elif level == '%topic%'
73+ parts[ ix] = '+'
74+ self .mqtt_topic_idx = ix
75+ elif level == ''
76+ parts[ ix] = 'STATE'
77+ else
78+ parts[ ix] = '+'
79+ end
80+ end
81+ self .mqtt_state = parts.concat ( '/' ) # default = tele/+/STATE
82+
83+ if self .mqtt_topic_idx == - 1
84+ log ( "DVO: ERROR No %topic% in FullTopic defined" , 1 )
85+ return
86+ end
6887
6988 tasmota.add_driver ( self )
89+
90+ mqtt.subscribe ( self .mqtt_state , / topic, idx, data, databytes -> self .handle_state_data ( topic, idx, data, databytes))
91+ mqtt.subscribe ( "tasmota/discovery/+/config" , / topic, idx, data, databytes -> self .handle_discovery_data ( topic, idx, data, databytes))
92+
93+ self .mqtt_step = 0
94+ if ! mqtt.connected ()
95+ log ( "DVO: Need MQTT connected" , 1 )
96+ end
7097 end
7198
7299 #################################################################################
@@ -76,7 +103,7 @@ class devices_online
76103 #################################################################################
77104 def unload ()
78105 mqtt.unsubscribe ( "tasmota/discovery/+/config" )
79- mqtt.unsubscribe ( self .mqtt_tele )
106+ mqtt.unsubscribe ( self .mqtt_state )
80107 tasmota.remove_driver ( self )
81108 end
82109
@@ -86,6 +113,11 @@ class devices_online
86113 # Handle MQTT Tasmota Discovery Config data
87114 #################################################################################
88115 def handle_discovery_data ( discovery_topic, idx, data, databytes)
116+ if self .mqtt_step == 0
117+ log ( "DVO: Discovery started..." , 3 )
118+ self .mqtt_step = 1
119+ end
120+ # log(f"DVO: Discovery topic '{discovery_topic}'", 4)
89121 var config = json.load ( data)
90122 if config
91123 # 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)
@@ -94,22 +126,19 @@ class devices_online
94126 var ipaddress = config[ 'ip' ]
95127 var devicename = config[ 'dn' ]
96128 var version = config[ 'sw' ]
97- var line = format ( "%s\001%s\001%s\001%s\001%s" , topic, hostname, ipaddress, devicename, version)
98- # tasmota.log(format("STD: 111 Size %03d, Topic '%s', Line '%s'", self.list_config.size(), topic, line), 3)
129+ var line = [ topic, hostname, ipaddress, devicename, version]
99130 if self .list_config.size ()
100131 var list_index = 0
101132 var list_size = size ( self .list_config )
102- var topic_delim = format ( "%s\001" , topic) # Add find delimiter
103133 while list_index < list_size # Use while loop as counter is decremented
104- if 0 == string .find ( self .list_config [ list_index] , topic_delim )
134+ if self .list_config [ list_index][ 0 ] == topic
105135 self .list_config.remove ( list_index) # Remove current config
106136 list_size -= 1 # Continue for duplicates
107137 end
108138 list_index += 1
109139 end
110140 end
111141 self .list_config.push ( line) # Add (re-discovered) config as last entry
112- # tasmota.log(format("STD: 222 Size %03d, Topic '%s', Line '%s'", self.list_config.size(), topic, line), 3)
113142 end
114143 return true # return true to stop propagation as a Tasmota cmd
115144 end
@@ -120,43 +149,55 @@ class devices_online
120149 # Handle MQTT STATE data
121150 #################################################################################
122151 def handle_state_data ( tele_topic, idx, data, databytes)
152+ if self .mqtt_step == 1
153+ log ( "DVO: Discovery complete" , 3 )
154+ self .mqtt_step = 2
155+ end
156+ # log(f"DVO: STATE topic '{tele_topic}'", 4)
123157 var subtopic = string.split ( tele_topic, "/" )
124- if subtopic[- 1 ] == "STATE" # tele/atomlite2/STATE
125- var topic = subtopic[ 1 ] # Assume default Fulltopic (%prefix%/%topic%/) = tele/atomlite2/STATE = atomlite2
126-
158+ if subtopic[- 1 ] == "STATE" # we are only serving topic ending in STATE
159+ var topic = subtopic[ self .mqtt_topic_idx ]
127160 var topic_index = - 1
128161 for i: self .list_config.keys ()
129- if 0 == string .find ( self .list_config [ i] , topic)
162+ if self .list_config [ i][ 0 ] == topic
130163 topic_index = i
131164 break
132165 end
133166 end
134- # tasmota. log(format("STD : Topic '%s', Index %d, Size %d, Line '%s'", topic, topic_index, self.list_config.size(), self.list_config[topic_index]), 3)
167+ # log(format("DVO : Topic '%s', Index %d, Size %d, Line '%s'", topic, topic_index, self.list_config.size(), self.list_config[topic_index]), 3)
135168 if topic_index == - 1 return true end # return true to stop propagation as a Tasmota cmd
136169
137170 var state = json.load ( data) # Assume topic is in retained discovery list
138171 if state # Valid JSON state message
139- var config_splits = string.split ( self .list_config [ topic_index] , "\001" )
140- var hostname = config_splits[ 1 ]
141- var ipaddress = config_splits[ 2 ]
142- var devicename = config_splits[ 3 ]
143- var version = config_splits[ 4 ]
172+ var hostname = self .list_config [ topic_index][ 1 ]
173+ var ipaddress = self .list_config [ topic_index][ 2 ]
174+ var devicename = self .list_config [ topic_index][ 3 ]
175+ var version = self .list_config [ topic_index][ 4 ]
176+ var version_splits = string.split ( version, "." )
177+ var version_int = 0
178+ var multiplier = 0x1000000
179+ for split : version_splits
180+ version_int += int ( split) * multiplier
181+ if multiplier
182+ multiplier /= 0x100
183+ end
184+ end
185+ var version_num = format ( "%011i" , version_int) # 00235143427 - Convert to string to enable multicolumn sort
144186
145187 # 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"}
146188 var uptime = state[ 'Uptime' ] # 0T00:15:09
189+ var uptime_sec = format ( "%011i" , state[ 'UptimeSec' ]) # 00000000909 - Convert to string to enable multicolumn sort
147190 if state.find ( 'Hostname' )
148191 hostname = state[ 'Hostname' ] # atomlite2
149192 ipaddress = state[ 'IPAddress' ] # 192.168.2.208
150193 end
151194 var last_seen = tasmota.rtc ( 'local' )
152- var line = format ( "%s\001%s\001%s\001%d\001%s\001%s" , hostname, ipaddress, uptime, last_seen, devicename, version)
153-
195+ var line = [ hostname, ipaddress, uptime, uptime_sec, last_seen, devicename, version, version_num]
154196 if self .list_buffer.size ()
155197 var list_index = 0
156198 var list_size = size ( self .list_buffer )
157- var hostname_delim = format ( "%s\001" , hostname) # Add find delimiter
158199 while list_index < list_size # Use while loop as counter is decremented
159- if 0 == string .find ( self .list_buffer [ list_index] , hostname_delim )
200+ if self .list_buffer [ list_index ][ 0 ] == hostname || self .list_buffer [ list_index][ 1 ] == ipaddress
160201 self .list_buffer.remove ( list_index) # Remove current state
161202 list_size -= 1 # Continue for duplicates
162203 end
@@ -175,34 +216,35 @@ class devices_online
175216 #
176217 # Shell sort list of online devices based on user selected column and direction
177218 #################################################################################
178- def sort_col ( l, col, dir) # Sort list based on col and Hostname (is first entry in line)
179- # For 50 records takes 6ms (primary key) or 25ms(ESP32S3&240MHz) / 50ms(ESP32@160MHz) (primary and secondary key)
219+ def sort_col ( l, col, dir)
180220 var cmp = / a,b -> a < b # Sort up
181221 if dir
182222 cmp = / a,b -> a > b # Sort down
183223 end
184- if col # col is new primary key (not Hostname)
185- for i: l.keys ()
186- var splits = string.split ( l[ i] , "\001" )
187- l[ i] = splits[ col] + "\002" + l[ i] # Add primary key to secondary key as "col" + Hostname
188- end
189- end
190- for i: 1 . .size ( l)- 1
191- var k = l[ i]
192- var j = i
193- while ( j > 0 ) && ! cmp ( l[ j- 1 ] , k)
194- l[ j] = l[ j- 1 ]
195- j -= 1
224+
225+ if col == 0 # Sort hostname as primary key
226+ for i: 1 . .size ( l)- 1 # Sort string
227+ var k = l[ i]
228+ var ks = k[ col]
229+ var j = i
230+ while ( j > 0 ) && ! cmp ( l[ j- 1 ][ col] , ks)
231+ l[ j] = l[ j- 1 ]
232+ j -= 1
233+ end
234+ l[ j] = k
196235 end
197- l[ j] = k
198- end
199- if col
200- for i: l.keys ()
201- var splits = string.split ( l[ i] , "\002" ) # Remove primary key
202- l[ i] = splits[ 1 ]
236+ else # Sort any other string using primary and secondary key
237+ for i: 1 . .size ( l)- 1
238+ var k = l[ i]
239+ var ks = k[ col] + k[ 0 ] # Primary search key and Secondary unique search key (hostname)
240+ var j = i
241+ while ( j > 0 ) && ! cmp ( l[ j- 1 ][ col] + l[ j- 1 ][ 0 ] , ks)
242+ l[ j] = l[ j- 1 ]
243+ j -= 1
244+ end
245+ l[ j] = k
203246 end
204247 end
205- return l
206248 end
207249
208250 #################################################################################
@@ -217,7 +259,7 @@ class devices_online
217259 persist.std_column = self .sort_column
218260 persist.std_direction = self .sort_direction
219261 persist.save ()
220- # tasmota. log("STD : Persist saved", 3)
262+ # log("DVO : Persist saved", 3)
221263 end
222264
223265 #################################################################################
@@ -254,9 +296,8 @@ class devices_online
254296 var list_index = 0
255297 var list_size = size ( self .list_buffer )
256298 while list_index < list_size
257- var splits = string.split ( self .list_buffer [ list_index] , "\001" )
258- var last_seen = int ( splits[ 3 ])
259- if time_window > last_seen # Remove offline devices
299+ var last_seen = self .list_buffer [ list_index][ 4 ]
300+ if time_window > int ( last_seen) # Remove offline devices
260301 self .list_buffer.remove ( list_index)
261302 list_size -= 1
262303 end
@@ -284,32 +325,34 @@ class devices_online
284325 end
285326 msg += "<th align='right'>Uptime </th>"
286327 else
328+ # var start = tasmota.millis()
287329 self .sort_col ( self .list_buffer , self .sort_column , self .sort_direction ) # Sort list by column
288-
330+ # var stop = tasmota.millis()
331+ # log(format("DVO: Sort time %d ms", stop - start), 3)
289332 var icon_direction = self .sort_direction ? "▼" : "▲"
290333 if self .bool_devicename
291- msg += format ( "<th><a href='#p' onclick='la(\"&sd_sort=4 \");'>Device Name</a>%s </th>" , self .sort_column == 4 ? icon_direction : "" )
334+ msg += format ( "<th><a href='#p' onclick='la(\"&sd_sort=5 \");'>Device Name</a>%s </th>" , self .sort_column == 5 ? icon_direction : "" )
292335 end
293336 if self .bool_version
294- msg += format ( "<th><a href='#p' onclick='la(\"&sd_sort=5 \");'>Version</a>%s </th>" , self .sort_column == 5 ? icon_direction : "" )
337+ msg += format ( "<th><a href='#p' onclick='la(\"&sd_sort=7 \");'>Version</a>%s </th>" , self .sort_column == 7 ? icon_direction : "" )
295338 end
296339 msg += format ( "<th><a href='#p' onclick='la(\"&sd_sort=0\");'>Hostname</a>%s </th>" , self .sort_column == 0 ? icon_direction : "" )
297340 if self .bool_ipaddress
298341 msg += format ( "<th><a href='#p' onclick='la(\"&sd_sort=1\");'>IP Address</a>%s </th>" , self .sort_column == 1 ? icon_direction : "" )
299342 end
300- msg += format ( "<th align='right'><a href='#p' onclick='la(\"&sd_sort=2 \");'>Uptime</a>%s </th>" , self .sort_column == 2 ? icon_direction : "" )
343+ msg += format ( "<th align='right'><a href='#p' onclick='la(\"&sd_sort=3 \");'>Uptime</a>%s </th>" , self .sort_column == 3 ? icon_direction : "" )
301344 end
302345
303346 msg += "</tr>"
304347
305348 while list_index < list_size
306- var splits = string .split ( self .list_buffer [ list_index] , "\001" )
307- var hostname = splits [ 0 ]
308- var ipaddress = splits [ 1 ]
309- var uptime = splits [ 2 ]
310- var last_seen = int ( splits [ 3 ])
311- var devicename = splits [ 4 ]
312- var version = splits [ 5 ]
349+ var hostname = self .list_buffer [ list_index][ 0 ]
350+ var ipaddress = self .list_buffer [ list_index ][ 1 ]
351+ var uptime = self .list_buffer [ list_index ][ 2 ]
352+ var uptime_sec = self .list_buffer [ list_index ][ 3 ]
353+ var last_seen = self .list_buffer [ list_index ][ 4 ]
354+ var devicename = self .list_buffer [ list_index ][ 5 ]
355+ var version = self .list_buffer [ list_index ][ 6 ]
313356
314357 msg += "<tr>"
315358 if self .bool_devicename
@@ -323,15 +366,9 @@ class devices_online
323366 msg += format ( "<td><a target=_blank href='http://%s'>%s </a></td>" , ipaddress, ipaddress)
324367 end
325368
326- var uptime_str = string.replace ( uptime, "T" , ":" ) # 11T21:50:34 -> 11:21:50:34
327- var uptime_splits = string.split ( uptime_str, ":" )
328- var uptime_sec = ( int ( uptime_splits[ 0 ]) * 86400 ) + # 11 * 86400
329- ( int ( uptime_splits[ 1 ]) * 3600 ) + # 21 * 3600
330- ( int ( uptime_splits[ 2 ]) * 60 ) + # 50 * 60
331- int ( uptime_splits[ 3 ]) # 34
332- if last_seen >= ( now - self .line_highlight ) # Highlight changes within latest seconds
369+ if int ( last_seen) >= ( now - self .line_highlight ) # Highlight changes within latest seconds
333370 msg += format ( "<td align='right' style='color:%s'>%s</td>" , self .line_highlight_color , uptime)
334- elif uptime_sec < self .line_teleperiod # Highlight changes just after restart
371+ elif int ( uptime_sec) < self .line_teleperiod # Highlight changes just after restart
335372 msg += format ( "<td align='right' style='color:%s'>%s</td>" , self .line_lowuptime_color , uptime)
336373 else
337374 msg += format ( "<td align='right'>%s</td>" , uptime)
0 commit comments