Skip to content

Commit 69ad24e

Browse files
committed
No longer using deveui from TTN devices for Ubidots device label, webapp map now colour codes icons based upon time last seen.
1 parent 35c1c38 commit 69ad24e

File tree

4 files changed

+90
-28
lines changed

4 files changed

+90
-28
lines changed

src/python/delivery/UbidotsWriter.py

+2-8
Original file line numberDiff line numberDiff line change
@@ -107,21 +107,15 @@ def on_message(self, pd: PhysicalDevice, ld: LogicalDevice, msg: dict[Any], retr
107107
new_device = True
108108
ld.properties['ubidots'] = {}
109109
# TODO: Remove the device source specific code here and always use a random
110-
# UUID for the Ubidots label. This cannot be done until the current TTN ubifunction
111-
# is switched off because the broker must be able to determine the same device label
112-
# used by the ubifunction when it creates Ubidots devices.
113-
if pd.source_name == BrokerConstants.TTN:
114-
ld.properties['ubidots']['label'] = pd.source_ids['dev_eui']
115-
lu.cid_logger.info(f'Using physical device eui for label: {ld.properties["ubidots"]["label"]}', extra=msg)
116-
elif pd.source_name == BrokerConstants.GREENBRAIN:
110+
# UUID for the Ubidots label.
111+
if pd.source_name == BrokerConstants.GREENBRAIN:
117112
lu.cid_logger.info('Using system-station-sensor-group ids as label', extra=msg)
118113
system_id = pd.source_ids['system_id']
119114
station_id = pd.source_ids['station_id']
120115
sensor_group_id = pd.source_ids['sensor_group_id']
121116
ubi_label = f'{system_id}-{station_id}-{sensor_group_id}'
122117
ld.properties['ubidots']['label'] = ubi_label
123118
else:
124-
lu.cid_logger.info('Using a UUID as Ubidots label', extra=msg)
125119
ld.properties['ubidots']['label'] = uuid.uuid4()
126120

127121
#

src/www/Dockerfile

-4
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,10 @@
1-
# syntax=docker/dockerfile:1
2-
31
FROM python:3.10
42

53
WORKDIR /app
64

75
COPY src/www/requirements.txt requirements.txt
86
RUN pip install -r requirements.txt
97

10-
COPY . .
11-
128
EXPOSE 5000
139

1410
CMD [ "python", "./app/main.py"]

src/www/app/main.py

+88-16
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,15 @@
33
import logging
44
import pandas as pd
55
import time
6-
from typing import Tuple
6+
from typing import Dict, Tuple
77
import uuid
88
from zoneinfo import ZoneInfo
99

1010
from flask import Flask, render_template, request, redirect, url_for, session, send_from_directory, send_file
1111

1212
import folium
13+
import folium.plugins
14+
1315
import paho.mqtt.client as mqtt
1416
import os
1517
from datetime import timedelta, datetime, timezone
@@ -78,22 +80,36 @@ def parse_location(loc_str: str) -> Tuple[bool, Location | None]:
7880
return True, location
7981

8082

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]:
8288
now = datetime.now(timezone.utc)
8389
date_utc = date.astimezone(timezone.utc)
8490
delta = now - date_utc
8591
days = delta.days
8692
hours = int(delta.seconds / 3600)
8793
minutes = int((delta.seconds % 3600) / 60)
8894
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+
89103
if days > 0:
90-
return f'{days} days ago'
104+
ret_val['desc'] = f'{days} days ago'
91105
elif hours > 0:
92-
return f'{hours} hours ago'
106+
ret_val['desc'] = f'{hours} hours ago'
93107
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'
95111

96-
return f'{seconds} seconds ago'
112+
return ret_val
97113

98114

99115
#-------------
@@ -474,17 +490,46 @@ def logical_device_form(uid):
474490
@app.route('/map', methods=['GET'])
475491
def show_map():
476492
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+
479509
data: List[LogicalDevice] = get_logical_devices(session.get('token'), include_properties=True)
480510
for dev in data:
481511
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
483515

516+
last_seen = None
484517
if dev.last_seen is None:
485518
last_seen_desc = 'Never'
519+
icon_name = 'circle-question'
520+
color = 'red'
521+
marker_list = dead_markers
486522
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
488533

489534
popup_str = f'<span style="white-space: nowrap;">Device: {dev.uid} / {dev.name}<br>Last seen: {last_seen_desc}'
490535

@@ -497,14 +542,41 @@ def show_map():
497542

498543
popup_str = popup_str + '</span>'
499544

500-
folium.Marker([dev.location.lat, dev.location.long],
545+
marker = folium.Marker([dev.location.lat, dev.location.long],
501546
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()
504579

505-
return center_map._repr_html_()
506-
# center_map
507-
# return render_template('map.html')
508580
except requests.exceptions.HTTPError as e:
509581
return render_template('error_page.html', reason=e), e.response.status_code
510582

src/www/requirements.txt

-12 Bytes
Binary file not shown.

0 commit comments

Comments
 (0)