3
3
import logging
4
4
import pandas as pd
5
5
import time
6
- from typing import Tuple
6
+ from typing import Dict , Tuple
7
7
import uuid
8
8
from zoneinfo import ZoneInfo
9
9
10
10
from flask import Flask , render_template , request , redirect , url_for , session , send_from_directory , send_file
11
11
12
12
import folium
13
+ import folium .plugins
14
+
13
15
import paho .mqtt .client as mqtt
14
16
import os
15
17
from datetime import timedelta , datetime , timezone
@@ -78,22 +80,36 @@ def parse_location(loc_str: str) -> Tuple[bool, Location | None]:
78
80
return True , location
79
81
80
82
81
- def time_since (date : datetime ) -> str :
83
+ _warning_seconds = 3600 * 6 ### How many seconds ago a device was seen to show a warning.
84
+ _error_seconds = 3600 * 12 ### How many seconds ago a device was seen to show an error.
85
+ _seconds_per_day = 3600 * 24
86
+
87
+ def time_since (date : datetime ) -> Dict [str , int | str ]:
82
88
now = datetime .now (timezone .utc )
83
89
date_utc = date .astimezone (timezone .utc )
84
90
delta = now - date_utc
85
91
days = delta .days
86
92
hours = int (delta .seconds / 3600 )
87
93
minutes = int ((delta .seconds % 3600 ) / 60 )
88
94
seconds = int (delta .seconds % 60 )
95
+
96
+ ret_val = {
97
+ 'days' : days ,
98
+ 'hours' : hours ,
99
+ 'minutes' : minutes ,
100
+ 'delta_seconds' : delta .seconds + (delta .days * _seconds_per_day )
101
+ }
102
+
89
103
if days > 0 :
90
- return f'{ days } days ago'
104
+ ret_val [ 'desc' ] = f'{ days } days ago'
91
105
elif hours > 0 :
92
- return f'{ hours } hours ago'
106
+ ret_val [ 'desc' ] = f'{ hours } hours ago'
93
107
elif minutes > 0 :
94
- return f'{ minutes } minutes ago'
108
+ ret_val ['desc' ] = f'{ minutes } minutes ago'
109
+ else :
110
+ ret_val ['desc' ] = f'{ seconds } seconds ago'
95
111
96
- return f' { seconds } seconds ago'
112
+ return ret_val
97
113
98
114
99
115
#-------------
@@ -474,17 +490,46 @@ def logical_device_form(uid):
474
490
@app .route ('/map' , methods = ['GET' ])
475
491
def show_map ():
476
492
try :
477
- center_map = folium .Map (location = [- 32.2400951991083 , 148.6324743348766 ], title = 'PhysicalDeviceMap' , zoom_start = 10 )
478
- # folium.Marker([-31.956194913619864, 115.85911692112582], popup="<i>Mt. Hood Meadows</i>", tooltip='click me').add_to(center_map)
493
+ # Map limits cover NSW.
494
+ center_map = folium .Map (
495
+ location = [- 32.42 , 147.5 ],
496
+ min_lat = - 36.8 , max_lat = - 29.6 , min_lon = 141.7 , max_lon = 152.9 ,
497
+ max_bounds = True ,
498
+ title = 'IoTa Logical Devices' ,
499
+ zoom_start = 7 )
500
+
501
+ live_nodes = folium .FeatureGroup (name = 'Live' , show = False )
502
+ late_nodes = folium .FeatureGroup (name = 'Late' )
503
+ dead_nodes = folium .FeatureGroup (name = 'Missing' )
504
+
505
+ live_markers = []
506
+ late_markers = []
507
+ dead_markers = []
508
+
479
509
data : List [LogicalDevice ] = get_logical_devices (session .get ('token' ), include_properties = True )
480
510
for dev in data :
481
511
if dev .location is not None and dev .location .lat is not None and dev .location .long is not None :
482
- color = 'blue'
512
+ color = 'green'
513
+ icon_name = 'circle'
514
+ marker_list = live_markers
483
515
516
+ last_seen = None
484
517
if dev .last_seen is None :
485
518
last_seen_desc = 'Never'
519
+ icon_name = 'circle-question'
520
+ color = 'red'
521
+ marker_list = dead_markers
486
522
else :
487
- last_seen_desc = time_since (dev .last_seen )
523
+ last_seen = time_since (dev .last_seen )
524
+ last_seen_desc = last_seen ['desc' ]
525
+ if last_seen ['delta_seconds' ] > _error_seconds :
526
+ color = 'red'
527
+ icon_name = 'circle-xmark'
528
+ marker_list = dead_markers
529
+ elif last_seen ['delta_seconds' ] > _warning_seconds :
530
+ color = 'orange'
531
+ icon_name = 'circle-exclamation'
532
+ marker_list = late_markers
488
533
489
534
popup_str = f'<span style="white-space: nowrap;">Device: { dev .uid } / { dev .name } <br>Last seen: { last_seen_desc } '
490
535
@@ -497,14 +542,41 @@ def show_map():
497
542
498
543
popup_str = popup_str + '</span>'
499
544
500
- folium .Marker ([dev .location .lat , dev .location .long ],
545
+ marker = folium .Marker ([dev .location .lat , dev .location .long ],
501
546
popup = popup_str ,
502
- icon = folium .Icon (color = color , icon = 'cloud' ),
503
- tooltip = dev .name ).add_to (center_map )
547
+ icon = folium .Icon (color = color , icon = icon_name , prefix = 'fa' ),
548
+ tooltip = f'{ dev .name } , last seen { last_seen_desc } ' )
549
+
550
+ marker_list .append (marker )
551
+
552
+ # This was an attempt to set the draw order of the markers. It did not work
553
+ # but the code has been kept in case having this structure is useful or a
554
+ # way to make it work is found.
555
+ for marker in live_markers :
556
+ live_nodes .add_child (marker )
557
+
558
+ for marker in late_markers :
559
+ late_nodes .add_child (marker )
560
+
561
+ for marker in dead_markers :
562
+ dead_nodes .add_child (marker )
563
+
564
+ center_map .add_child (live_nodes )
565
+ center_map .add_child (late_nodes )
566
+ center_map .add_child (dead_nodes )
567
+
568
+ # It seems to be important to add the LayerControl down here. Doing it before
569
+ # the FeatureGroups are defined doesn't work.
570
+ folium .LayerControl (collapsed = False ).add_to (center_map )
571
+ folium .plugins .Fullscreen (
572
+ position = "topleft" ,
573
+ title = "Full screen" ,
574
+ title_cancel = "Exit full screen" ,
575
+ force_separate_button = True ,
576
+ ).add_to (center_map )
577
+
578
+ return center_map .get_root ().render ()
504
579
505
- return center_map ._repr_html_ ()
506
- # center_map
507
- # return render_template('map.html')
508
580
except requests .exceptions .HTTPError as e :
509
581
return render_template ('error_page.html' , reason = e ), e .response .status_code
510
582
0 commit comments