Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 9 additions & 2 deletions .github/skills/code-standards/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,11 +42,18 @@ Nested subprocess calls need their own timeout—outer timeout won't save you.
## Time Utilities

```python
from utils.datetime_utils import timeNowDB
from utils.datetime_utils import timeNowUTC

timestamp = timeNowDB()
timestamp = timeNowUTC()
```

This is the ONLY function that calls datetime.datetime.now() in the entire codebase.

⚠️ CRITICAL: ALL database timestamps MUST be stored in UTC
This is the SINGLE SOURCE OF TRUTH for current time in NetAlertX
Use timeNowUTC() for DB writes (returns UTC string by default)
Use timeNowUTC(as_string=False) for datetime operations (scheduling, comparisons, logging)

## String Sanitization

Use sanitizers from `server/helper.py` before storing user input.
Expand Down
6 changes: 3 additions & 3 deletions .github/workflows/run-all-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ on:
type: boolean
default: false
run_backend:
description: '📂 backend/ (SQL Builder & Security)'
description: '📂 backend/ & db/ (SQL Builder, Security & Migration)'
type: boolean
default: false
run_docker_env:
Expand Down Expand Up @@ -43,9 +43,9 @@ jobs:
run: |
PATHS=""
# Folder Mapping with 'test/' prefix
if [ "${{ github.event.inputs.scan }}" == "true" ]; then PATHS="$PATHS test/scan/"; fi
if [ "${{ github.event.inputs.run_scan }}" == "true" ]; then PATHS="$PATHS test/scan/"; fi
if [ "${{ github.event.inputs.run_api }}" == "true" ]; then PATHS="$PATHS test/api_endpoints/ test/server/"; fi
if [ "${{ github.event.inputs.run_backend }}" == "true" ]; then PATHS="$PATHS test/backend/"; fi
if [ "${{ github.event.inputs.run_backend }}" == "true" ]; then PATHS="$PATHS test/backend/ test/db/"; fi
if [ "${{ github.event.inputs.run_docker_env }}" == "true" ]; then PATHS="$PATHS test/docker_tests/"; fi
if [ "${{ github.event.inputs.run_ui }}" == "true" ]; then PATHS="$PATHS test/ui/"; fi

Expand Down
32 changes: 23 additions & 9 deletions front/js/common.js
Original file line number Diff line number Diff line change
Expand Up @@ -447,21 +447,35 @@ function localizeTimestamp(input) {
return formatSafe(input, tz);

function formatSafe(str, tz) {
const date = new Date(str);
// CHECK: Does the input string have timezone information?
// - Ends with Z: "2026-02-11T11:37:02Z"
// - Has GMT±offset: "Wed Feb 11 2026 12:34:12 GMT+1100 (...)"
// - Has offset at end: "2026-02-11 11:37:02+11:00"
// - Has timezone name in parentheses: "(Australian Eastern Daylight Time)"
const hasOffset = /Z$/i.test(str.trim()) ||
/GMT[+-]\d{2,4}/.test(str) ||
/[+-]\d{2}:?\d{2}$/.test(str.trim()) ||
/\([^)]+\)$/.test(str.trim());

// ⚠️ CRITICAL: All DB timestamps are stored in UTC without timezone markers.
// If no offset is present, we must explicitly mark it as UTC by appending 'Z'
// so JavaScript doesn't interpret it as local browser time.
let isoStr = str.trim();
if (!hasOffset) {
// Ensure proper ISO format before appending Z
// Replace space with 'T' if needed: "2026-02-11 11:37:02" → "2026-02-11T11:37:02Z"
isoStr = isoStr.trim().replace(/^(\d{4}-\d{2}-\d{2}) (\d{2}:\d{2}:\d{2})$/, '$1T$2') + 'Z';
}
Comment on lines +455 to +468
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Appending Z to non-ISO strings (e.g., RFC2822) may break parsing.

When case 5 (RFC2822, line 425) matches a string like "25 Aug 2025 13:45:22" without an offset, hasOffset is false and Z gets appended, producing "25 Aug 2025 13:45:22Z". This is not a valid format for new Date() in all browsers and will likely yield NaN/"Failed conversion".

Consider limiting the Z-append to ISO-shaped strings only:

Proposed fix
     let isoStr = str.trim();
     if (!hasOffset) {
-      // Ensure proper ISO format before appending Z
-      // Replace space with 'T' if needed: "2026-02-11 11:37:02" → "2026-02-11T11:37:02Z"
-      isoStr = isoStr.trim().replace(/^(\d{4}-\d{2}-\d{2}) (\d{2}:\d{2}:\d{2})$/, '$1T$2') + 'Z';
+      // Only force UTC for ISO-shaped strings (DB timestamps)
+      if (/^\d{4}-\d{2}-\d{2}[T ]\d{2}:\d{2}(:\d{2})?$/.test(isoStr)) {
+        isoStr = isoStr.replace(' ', 'T') + 'Z';
+      }
     }
🤖 Prompt for AI Agents
In `@front/js/common.js` around lines 455 - 468, The current logic unconditionally
appends 'Z' to any timestamp lacking an offset (using hasOffset and isoStr),
which breaks non-ISO inputs like RFC2822; change the behavior so you only append
'Z' when the input is ISO-shaped: first test str against an ISO pattern (e.g.,
/^\d{4}-\d{2}-\d{2}(?:[ T]\d{2}:\d{2}:\d{2}(?:\.\d+)?)?$/) and only then perform
the space→'T' replacement and append 'Z' to isoStr; leave other formats (e.g.,
RFC2822) untouched so they are parsed as-is by new Date().


const date = new Date(isoStr);
if (!isFinite(date)) {
console.error(`ERROR: Couldn't parse date: '${str}' with TIMEZONE ${tz}`);
return 'Failed conversion';
}

// CHECK: Does the input string have an offset (e.g., +11:00 or Z)?
// If it does, and we apply a 'tz' again, we double-shift.
const hasOffset = /[Z|[+-]\d{2}:?\d{2}]$/.test(str.trim());

return new Intl.DateTimeFormat(LOCALE, {
// If it has an offset, we display it as-is (UTC mode in Intl
// effectively means "don't add more hours").
// If no offset, apply your variable 'tz'.
timeZone: hasOffset ? 'UTC' : tz,
// Convert from UTC to user's configured timezone
timeZone: tz,
year: 'numeric', month: '2-digit', day: '2-digit',
hour: '2-digit', minute: '2-digit', second: '2-digit',
hour12: false
Expand Down
4 changes: 2 additions & 2 deletions front/plugins/_publisher_apprise/apprise.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@

import conf # noqa: E402 [flake8 lint suppression]
from const import confFileName, logPath # noqa: E402 [flake8 lint suppression]
from utils.datetime_utils import timeNowDB # noqa: E402 [flake8 lint suppression]
from utils.datetime_utils import timeNowUTC # noqa: E402 [flake8 lint suppression]
from plugin_helper import Plugin_Objects # noqa: E402 [flake8 lint suppression]
from logger import mylog, Logger # noqa: E402 [flake8 lint suppression]
from helper import get_setting_value # noqa: E402 [flake8 lint suppression]
Expand Down Expand Up @@ -60,7 +60,7 @@ def main():
# Log result
plugin_objects.add_object(
primaryId = pluginName,
secondaryId = timeNowDB(),
secondaryId = timeNowUTC(),
watched1 = notification["GUID"],
watched2 = result,
watched3 = 'null',
Expand Down
4 changes: 2 additions & 2 deletions front/plugins/_publisher_email/email_smtp.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
import conf # noqa: E402 [flake8 lint suppression]
from const import confFileName, logPath # noqa: E402 [flake8 lint suppression]
from plugin_helper import Plugin_Objects # noqa: E402 [flake8 lint suppression]
from utils.datetime_utils import timeNowDB # noqa: E402 [flake8 lint suppression]
from utils.datetime_utils import timeNowUTC # noqa: E402 [flake8 lint suppression]
from logger import mylog, Logger # noqa: E402 [flake8 lint suppression]
from helper import get_setting_value, hide_email # noqa: E402 [flake8 lint suppression]
from models.notification_instance import NotificationInstance # noqa: E402 [flake8 lint suppression]
Expand Down Expand Up @@ -80,7 +80,7 @@ def main():
# Log result
plugin_objects.add_object(
primaryId = pluginName,
secondaryId = timeNowDB(),
secondaryId = timeNowUTC(),
watched1 = notification["GUID"],
watched2 = result,
watched3 = 'null',
Expand Down
6 changes: 3 additions & 3 deletions front/plugins/_publisher_mqtt/mqtt.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@
from helper import get_setting_value, bytes_to_string, \
sanitize_string, normalize_string # noqa: E402 [flake8 lint suppression]
from database import DB, get_device_stats # noqa: E402 [flake8 lint suppression]
from utils.datetime_utils import timeNowDB # noqa: E402 [flake8 lint suppression]
from utils.datetime_utils import timeNowUTC # noqa: E402 [flake8 lint suppression]
from models.notification_instance import NotificationInstance # noqa: E402 [flake8 lint suppression]

# Make sure the TIMEZONE for logging is correct
Expand Down Expand Up @@ -583,7 +583,7 @@ def publish_notifications(db, mqtt_client):

# Optional: attach meta info
payload["_meta"] = {
"published_at": timeNowDB(),
"published_at": timeNowUTC(),
"source": "NetAlertX",
"notification_GUID": notification["GUID"]
}
Expand Down Expand Up @@ -631,7 +631,7 @@ def prepTimeStamp(datetime_str):
except ValueError:
mylog('verbose', [f"[{pluginName}] Timestamp conversion failed of string '{datetime_str}'"])
# Use the current time if the input format is invalid
parsed_datetime = datetime.now(conf.tz)
parsed_datetime = timeNowUTC(as_string=False)

# Convert to the required format with 'T' between date and time and ensure the timezone is included
return parsed_datetime.isoformat() # This will include the timezone offset
Expand Down
4 changes: 2 additions & 2 deletions front/plugins/_publisher_ntfy/ntfy.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
import conf # noqa: E402 [flake8 lint suppression]
from const import confFileName, logPath # noqa: E402 [flake8 lint suppression]
from plugin_helper import Plugin_Objects, handleEmpty # noqa: E402 [flake8 lint suppression]
from utils.datetime_utils import timeNowDB # noqa: E402 [flake8 lint suppression]
from utils.datetime_utils import timeNowUTC # noqa: E402 [flake8 lint suppression]
from logger import mylog, Logger # noqa: E402 [flake8 lint suppression]
from helper import get_setting_value # noqa: E402 [flake8 lint suppression]
from models.notification_instance import NotificationInstance # noqa: E402 [flake8 lint suppression]
Expand Down Expand Up @@ -63,7 +63,7 @@ def main():
# Log result
plugin_objects.add_object(
primaryId = pluginName,
secondaryId = timeNowDB(),
secondaryId = timeNowUTC(),
watched1 = notification["GUID"],
watched2 = handleEmpty(response_text),
watched3 = response_status_code,
Expand Down
4 changes: 2 additions & 2 deletions front/plugins/_publisher_pushover/pushover.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
from plugin_helper import Plugin_Objects, handleEmpty # noqa: E402 [flake8 lint suppression]
from logger import mylog, Logger # noqa: E402 [flake8 lint suppression]
from helper import get_setting_value, hide_string # noqa: E402 [flake8 lint suppression]
from utils.datetime_utils import timeNowDB # noqa: E402 [flake8 lint suppression]
from utils.datetime_utils import timeNowUTC # noqa: E402 [flake8 lint suppression]
from models.notification_instance import NotificationInstance # noqa: E402 [flake8 lint suppression]
from database import DB # noqa: E402 [flake8 lint suppression]

Expand Down Expand Up @@ -60,7 +60,7 @@ def main():
# Log result
plugin_objects.add_object(
primaryId=pluginName,
secondaryId=timeNowDB(),
secondaryId=timeNowUTC(),
watched1=notification["GUID"],
watched2=handleEmpty(response_text),
watched3=response_status_code,
Expand Down
4 changes: 2 additions & 2 deletions front/plugins/_publisher_pushsafer/pushsafer.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
from plugin_helper import Plugin_Objects, handleEmpty # noqa: E402 [flake8 lint suppression]
from logger import mylog, Logger # noqa: E402 [flake8 lint suppression]
from helper import get_setting_value, hide_string # noqa: E402 [flake8 lint suppression]
from utils.datetime_utils import timeNowDB # noqa: E402 [flake8 lint suppression]
from utils.datetime_utils import timeNowUTC # noqa: E402 [flake8 lint suppression]
from models.notification_instance import NotificationInstance # noqa: E402 [flake8 lint suppression]
from database import DB # noqa: E402 [flake8 lint suppression]
from pytz import timezone # noqa: E402 [flake8 lint suppression]
Expand Down Expand Up @@ -61,7 +61,7 @@ def main():
# Log result
plugin_objects.add_object(
primaryId = pluginName,
secondaryId = timeNowDB(),
secondaryId = timeNowUTC(),
watched1 = notification["GUID"],
watched2 = handleEmpty(response_text),
watched3 = response_status_code,
Expand Down
4 changes: 2 additions & 2 deletions front/plugins/_publisher_telegram/tg.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
import conf # noqa: E402 [flake8 lint suppression]
from const import confFileName, logPath # noqa: E402 [flake8 lint suppression]
from plugin_helper import Plugin_Objects # noqa: E402 [flake8 lint suppression]
from utils.datetime_utils import timeNowDB # noqa: E402 [flake8 lint suppression]
from utils.datetime_utils import timeNowUTC # noqa: E402 [flake8 lint suppression]
from logger import mylog, Logger # noqa: E402 [flake8 lint suppression]
from helper import get_setting_value # noqa: E402 [flake8 lint suppression]
from models.notification_instance import NotificationInstance # noqa: E402 [flake8 lint suppression]
Expand Down Expand Up @@ -60,7 +60,7 @@ def main():
# Log result
plugin_objects.add_object(
primaryId=pluginName,
secondaryId=timeNowDB(),
secondaryId=timeNowUTC(),
watched1=notification["GUID"],
watched2=result,
watched3='null',
Expand Down
4 changes: 2 additions & 2 deletions front/plugins/_publisher_webhook/webhook.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
import conf # noqa: E402 [flake8 lint suppression]
from const import logPath, confFileName # noqa: E402 [flake8 lint suppression]
from plugin_helper import Plugin_Objects, handleEmpty # noqa: E402 [flake8 lint suppression]
from utils.datetime_utils import timeNowDB # noqa: E402 [flake8 lint suppression]
from utils.datetime_utils import timeNowUTC # noqa: E402 [flake8 lint suppression]
from logger import mylog, Logger # noqa: E402 [flake8 lint suppression]
from helper import get_setting_value, write_file # noqa: E402 [flake8 lint suppression]
from models.notification_instance import NotificationInstance # noqa: E402 [flake8 lint suppression]
Expand Down Expand Up @@ -69,7 +69,7 @@ def main():
# Log result
plugin_objects.add_object(
primaryId = pluginName,
secondaryId = timeNowDB(),
secondaryId = timeNowUTC(),
watched1 = notification["GUID"],
watched2 = handleEmpty(response_stdout),
watched3 = handleEmpty(response_stderr),
Expand Down
4 changes: 2 additions & 2 deletions front/plugins/csv_backup/script.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
import argparse
import sys
import csv
from datetime import datetime

# Register NetAlertX directories
INSTALL_PATH = os.getenv('NETALERTX_APP', '/app')
Expand All @@ -13,6 +12,7 @@
from logger import mylog, Logger # noqa: E402 [flake8 lint suppression]
from helper import get_setting_value # noqa: E402 [flake8 lint suppression]
from const import logPath # noqa: E402 [flake8 lint suppression]
from utils.datetime_utils import timeNowUTC # noqa: E402 [flake8 lint suppression]
import conf # noqa: E402 [flake8 lint suppression]
from pytz import timezone # noqa: E402 [flake8 lint suppression]
from database import get_temp_db_connection # noqa: E402 [flake8 lint suppression]
Expand Down Expand Up @@ -60,7 +60,7 @@ def main():
if overwrite:
filename = 'devices.csv'
else:
timestamp = datetime.now().strftime('%Y%m%d%H%M%S')
timestamp = timeNowUTC(as_string=False).strftime('%Y%m%d%H%M%S')
filename = f'devices_{timestamp}.csv'

fullPath = os.path.join(values.location.split('=')[1], filename)
Expand Down
4 changes: 2 additions & 2 deletions front/plugins/freebox/freebox.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
from const import logPath # noqa: E402 [flake8 lint suppression]
from helper import get_setting_value # noqa: E402 [flake8 lint suppression]
import conf # noqa: E402 [flake8 lint suppression]
from utils.datetime_utils import timeNowDB, DATETIME_PATTERN # noqa: E402 [flake8 lint suppression]
from utils.datetime_utils import timeNowUTC, DATETIME_PATTERN # noqa: E402 [flake8 lint suppression]

# Make sure the TIMEZONE for logging is correct
conf.tz = timezone(get_setting_value("TIMEZONE"))
Expand Down Expand Up @@ -151,7 +151,7 @@ def main():
watched1=freebox["name"],
watched2=freebox["operator"],
watched3="Gateway",
watched4=timeNowDB(),
watched4=timeNowUTC(),
extra="",
foreignKey=freebox["mac"],
)
Expand Down
4 changes: 2 additions & 2 deletions front/plugins/internet_ip/script.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
sys.path.extend([f"{INSTALL_PATH}/front/plugins", f"{INSTALL_PATH}/server"])

from plugin_helper import Plugin_Objects # noqa: E402 [flake8 lint suppression]
from utils.datetime_utils import timeNowDB # noqa: E402 [flake8 lint suppression]
from utils.datetime_utils import timeNowUTC # noqa: E402 [flake8 lint suppression]
from logger import mylog, Logger, append_line_to_file # noqa: E402 [flake8 lint suppression]
from helper import check_IP_format, get_setting_value # noqa: E402 [flake8 lint suppression]
from const import logPath # noqa: E402 [flake8 lint suppression]
Expand Down Expand Up @@ -74,7 +74,7 @@ def main():
mylog('verbose', [f'[{pluginName}] Curl Fallback (new_internet_IP|cmd_output): {new_internet_IP} | {cmd_output}'])

# logging
append_line_to_file(logPath + '/IP_changes.log', '[' + str(timeNowDB()) + ']\t' + new_internet_IP + '\n')
append_line_to_file(logPath + '/IP_changes.log', '[' + str(timeNowUTC()) + ']\t' + new_internet_IP + '\n')

plugin_objects = Plugin_Objects(RESULT_FILE)

Expand Down
4 changes: 2 additions & 2 deletions front/plugins/internet_speedtest/script.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
sys.path.extend([f"{INSTALL_PATH}/front/plugins", f"{INSTALL_PATH}/server"])

from plugin_helper import Plugin_Objects # noqa: E402 [flake8 lint suppression]
from utils.datetime_utils import timeNowDB # noqa: E402 [flake8 lint suppression]
from utils.datetime_utils import timeNowUTC # noqa: E402 [flake8 lint suppression]
from logger import mylog, Logger # noqa: E402 [flake8 lint suppression]
from helper import get_setting_value # noqa: E402 [flake8 lint suppression]
import conf # noqa: E402 [flake8 lint suppression]
Expand All @@ -37,7 +37,7 @@ def main():
speedtest_result = run_speedtest()
plugin_objects.add_object(
primaryId = 'Speedtest',
secondaryId = timeNowDB(),
secondaryId = timeNowUTC(),
watched1 = speedtest_result['download_speed'],
watched2 = speedtest_result['upload_speed'],
watched3 = speedtest_result['full_json'],
Expand Down
4 changes: 2 additions & 2 deletions front/plugins/ipneigh/ipneigh.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
import os
import sys
import subprocess
from datetime import datetime
from pytz import timezone
from functools import reduce

Expand All @@ -13,6 +12,7 @@

from plugin_helper import Plugin_Objects # noqa: E402 [flake8 lint suppression]
from logger import mylog, Logger # noqa: E402 [flake8 lint suppression]
from utils.datetime_utils import timeNowUTC # noqa: E402 [flake8 lint suppression]
from const import logPath # noqa: E402 [flake8 lint suppression]
from helper import get_setting_value # noqa: E402 [flake8 lint suppression]
import conf # noqa: E402 [flake8 lint suppression]
Expand Down Expand Up @@ -95,7 +95,7 @@ def parse_neighbors(raw_neighbors: list[str]):
neighbor = {}
neighbor['ip'] = fields[0]
neighbor['mac'] = fields[2]
neighbor['last_seen'] = datetime.now()
neighbor['last_seen'] = timeNowUTC()

# Unknown data
neighbor['hostname'] = '(unknown)'
Expand Down
4 changes: 2 additions & 2 deletions front/plugins/nmap_scan/script.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@

from plugin_helper import Plugin_Objects # noqa: E402 [flake8 lint suppression]
from logger import mylog, Logger, append_line_to_file # noqa: E402 [flake8 lint suppression]
from utils.datetime_utils import timeNowDB # noqa: E402 [flake8 lint suppression]
from utils.datetime_utils import timeNowUTC # noqa: E402 [flake8 lint suppression]
from helper import get_setting_value # noqa: E402 [flake8 lint suppression]
from const import logPath # noqa: E402 [flake8 lint suppression]
import conf # noqa: E402 [flake8 lint suppression]
Expand Down Expand Up @@ -213,7 +213,7 @@ def performNmapScan(deviceIPs, deviceMACs, timeoutSec, args):
elif 'PORT' in line and 'STATE' in line and 'SERVICE' in line:
startCollecting = False # end reached
elif startCollecting and len(line.split()) == 3:
newEntriesTmp.append(nmap_entry(ip, deviceMACs[devIndex], timeNowDB(), line.split()[0], line.split()[1], line.split()[2]))
newEntriesTmp.append(nmap_entry(ip, deviceMACs[devIndex], timeNowUTC(), line.split()[0], line.split()[1], line.split()[2]))
newPortsPerDevice += 1
elif 'Nmap done' in line:
duration = line.split('scanned in ')[1]
Expand Down
4 changes: 2 additions & 2 deletions front/plugins/pihole_api_scan/pihole_api_scan.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@

import os
import sys
import datetime
import requests
import json
from requests.packages.urllib3.exceptions import InsecureRequestWarning
Expand All @@ -18,6 +17,7 @@
pluginName = 'PIHOLEAPI'

from plugin_helper import Plugin_Objects, is_mac # noqa: E402 [flake8 lint suppression]
from utils.datetime_utils import timeNowUTC # noqa: E402 [flake8 lint suppression]
from logger import mylog, Logger # noqa: E402 [flake8 lint suppression]
from helper import get_setting_value # noqa: E402 [flake8 lint suppression]
from const import logPath # noqa: E402 [flake8 lint suppression]
Expand Down Expand Up @@ -201,7 +201,7 @@ def gather_device_entries():
"""
entries = []
devices = get_pihole_network_devices()
now_ts = int(datetime.datetime.now().timestamp())
now_ts = int(timeNowUTC(as_string=False).timestamp())

for device in devices:
hwaddr = device.get('hwaddr')
Expand Down
Loading
Loading