mirror of
https://github.com/jokob-sk/NetAlertX.git
synced 2026-04-05 01:31:49 -07:00
Merge pull request #1589 from netalertx/next_release
Some checks are pending
Some checks are pending
Next release
This commit is contained in:
@@ -3,7 +3,6 @@
|
|||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
from datetime import datetime
|
|
||||||
import time
|
import time
|
||||||
import re
|
import re
|
||||||
import paho.mqtt.client as mqtt
|
import paho.mqtt.client as mqtt
|
||||||
@@ -26,7 +25,7 @@ from logger import mylog, Logger # noqa: E402 [flake8 lint suppression]
|
|||||||
from helper import get_setting_value, bytes_to_string, \
|
from helper import get_setting_value, bytes_to_string, \
|
||||||
sanitize_string, normalize_string # noqa: E402 [flake8 lint suppression]
|
sanitize_string, normalize_string # noqa: E402 [flake8 lint suppression]
|
||||||
from database import DB, get_device_stats # noqa: E402 [flake8 lint suppression]
|
from database import DB, get_device_stats # noqa: E402 [flake8 lint suppression]
|
||||||
from utils.datetime_utils import timeNowUTC # noqa: E402 [flake8 lint suppression]
|
from utils.datetime_utils import timeNowUTC, format_date_iso # noqa: E402 [flake8 lint suppression]
|
||||||
from models.notification_instance import NotificationInstance # noqa: E402 [flake8 lint suppression]
|
from models.notification_instance import NotificationInstance # noqa: E402 [flake8 lint suppression]
|
||||||
|
|
||||||
# Make sure the TIMEZONE for logging is correct
|
# Make sure the TIMEZONE for logging is correct
|
||||||
@@ -504,8 +503,8 @@ def mqtt_start(db):
|
|||||||
"vendor": sanitize_string(device["devVendor"]),
|
"vendor": sanitize_string(device["devVendor"]),
|
||||||
"mac_address": str(device["devMac"]),
|
"mac_address": str(device["devMac"]),
|
||||||
"model": devDisplayName,
|
"model": devDisplayName,
|
||||||
"last_connection": prepTimeStamp(str(device["devLastConnection"])),
|
"last_connection": format_date_iso(str(device["devLastConnection"])),
|
||||||
"first_connection": prepTimeStamp(str(device["devFirstConnection"])),
|
"first_connection": format_date_iso(str(device["devFirstConnection"])),
|
||||||
"sync_node": device["devSyncHubNode"],
|
"sync_node": device["devSyncHubNode"],
|
||||||
"group": device["devGroup"],
|
"group": device["devGroup"],
|
||||||
"location": device["devLocation"],
|
"location": device["devLocation"],
|
||||||
@@ -617,26 +616,6 @@ def to_binary_sensor(input):
|
|||||||
return "OFF"
|
return "OFF"
|
||||||
|
|
||||||
|
|
||||||
# -------------------------------------
|
|
||||||
# Convert to format that is interpretable by Home Assistant
|
|
||||||
def prepTimeStamp(datetime_str):
|
|
||||||
try:
|
|
||||||
# Attempt to parse the input string to ensure it's a valid datetime
|
|
||||||
parsed_datetime = datetime.fromisoformat(datetime_str)
|
|
||||||
|
|
||||||
# If the parsed datetime is naive (i.e., does not contain timezone info), add UTC timezone
|
|
||||||
if parsed_datetime.tzinfo is None:
|
|
||||||
parsed_datetime = conf.tz.localize(parsed_datetime)
|
|
||||||
|
|
||||||
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 = 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
|
|
||||||
|
|
||||||
|
|
||||||
# -------------INIT---------------------
|
# -------------INIT---------------------
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
sys.exit(main())
|
sys.exit(main())
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ import pytest
|
|||||||
INSTALL_PATH = os.getenv('NETALERTX_APP', '/app')
|
INSTALL_PATH = os.getenv('NETALERTX_APP', '/app')
|
||||||
sys.path.extend([f"{INSTALL_PATH}/front/plugins", f"{INSTALL_PATH}/server"])
|
sys.path.extend([f"{INSTALL_PATH}/front/plugins", f"{INSTALL_PATH}/server"])
|
||||||
|
|
||||||
from utils.datetime_utils import timeNowUTC, DATETIME_PATTERN # noqa: E402
|
from utils.datetime_utils import timeNowUTC, format_date_iso, DATETIME_PATTERN # noqa: E402
|
||||||
|
|
||||||
|
|
||||||
class TestTimeNowUTC:
|
class TestTimeNowUTC:
|
||||||
@@ -104,3 +104,52 @@ class TestTimeNowUTC:
|
|||||||
t2 = datetime.datetime.strptime(t2_str, DATETIME_PATTERN)
|
t2 = datetime.datetime.strptime(t2_str, DATETIME_PATTERN)
|
||||||
|
|
||||||
assert t2 >= t1
|
assert t2 >= t1
|
||||||
|
|
||||||
|
|
||||||
|
class TestFormatDateIso:
|
||||||
|
"""
|
||||||
|
Regression tests for format_date_iso().
|
||||||
|
|
||||||
|
Root cause being guarded: DB timestamps are stored as naive UTC strings
|
||||||
|
(e.g. '2026-04-04 08:54:00'). The old prepTimeStamp() called
|
||||||
|
conf.tz.localize() which LABELS the naive value with the local TZ offset
|
||||||
|
instead of CONVERTING it. This made '08:54 UTC' become '08:54+02:00',
|
||||||
|
telling Home Assistant the event happened at 06:54 UTC — 2 hours too early.
|
||||||
|
|
||||||
|
format_date_iso() correctly replaces(tzinfo=UTC) first, then converts.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def test_naive_utc_string_gets_utc_tzinfo(self):
|
||||||
|
"""A naive DB timestamp must be interpreted as UTC, not local time."""
|
||||||
|
result = format_date_iso("2026-04-04 08:54:00")
|
||||||
|
assert result is not None
|
||||||
|
# Must contain a TZ offset ('+' or 'Z'), not be naive
|
||||||
|
assert "+" in result or result.endswith("Z"), \
|
||||||
|
f"Expected timezone in ISO output, got: {result}"
|
||||||
|
|
||||||
|
def test_naive_utc_string_offset_reflects_utc_source(self):
|
||||||
|
"""
|
||||||
|
The UTC instant must be preserved. Whatever the local offset, the
|
||||||
|
calendar moment encoded in the ISO string must equal 08:54 UTC.
|
||||||
|
"""
|
||||||
|
result = format_date_iso("2026-04-04 08:54:00")
|
||||||
|
parsed = datetime.datetime.fromisoformat(result)
|
||||||
|
# Normalise to UTC for the assertion
|
||||||
|
utc_parsed = parsed.astimezone(datetime.UTC)
|
||||||
|
assert utc_parsed.hour == 8
|
||||||
|
assert utc_parsed.minute == 54
|
||||||
|
|
||||||
|
def test_empty_string_returns_none(self):
|
||||||
|
"""format_date_iso('') must return None, not raise."""
|
||||||
|
assert format_date_iso("") is None
|
||||||
|
|
||||||
|
def test_none_returns_none(self):
|
||||||
|
"""format_date_iso(None) must return None, not raise."""
|
||||||
|
assert format_date_iso(None) is None
|
||||||
|
|
||||||
|
def test_output_is_valid_iso8601(self):
|
||||||
|
"""Output must be parseable by datetime.fromisoformat()."""
|
||||||
|
result = format_date_iso("2026-01-15 12:00:00")
|
||||||
|
assert result is not None
|
||||||
|
# Should not raise
|
||||||
|
datetime.datetime.fromisoformat(result)
|
||||||
|
|||||||
Reference in New Issue
Block a user