Skip to content
Open
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
56 changes: 46 additions & 10 deletions src/saic_ismart_client_ng/api/message/schema.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,18 +32,54 @@ class MessageEntity:

@property
def message_time(self) -> datetime.datetime:
if self.messageTime:
for date_format in MESSAGE_DATE_TIME_FORMATS:
try:
return datetime.datetime.strptime(self.messageTime, date_format)
except ValueError:
pass
LOGGER.error(
"Could not parse messageTime '%s'. This is a bug. Please file a ticket",
self.messageTime,
)
"""The message time, or the current local time if there isn't one.

Falling back to now() means a message with no time looks brand new.
Prefer ``message_time_or_none`` or ``create_time_utc`` when that
matters (e.g. deciding whether a message is old).
"""
parsed = self.message_time_or_none
if parsed is not None:
return parsed
return datetime.datetime.now()

@property
def message_time_or_none(self) -> datetime.datetime | None:
"""``messageTime`` parsed, or None if it is missing or unreadable.

The result is naive: SAIC sends it in the server's local time with no
timezone (the EU servers use CET/CEST), so it can't safely be compared
with UTC. Use ``create_time_utc`` for that.
"""
if not self.messageTime:
return None
for date_format in MESSAGE_DATE_TIME_FORMATS:
try:
return datetime.datetime.strptime(self.messageTime, date_format)
except ValueError:
pass
LOGGER.error(
"Could not parse messageTime '%s'. This is a bug. Please file a ticket",
self.messageTime,
)
return None

@property
def create_time_utc(self) -> datetime.datetime | None:
"""``createTime`` (Unix milliseconds) as a UTC datetime, or None.

None when the message has no createTime (common on EU accounts) or
it can't be read. Unlike ``messageTime`` this has no timezone doubt.
"""
if self.createTime is None:
return None
try:
return datetime.datetime.fromtimestamp(
self.createTime / 1000.0, tz=datetime.UTC
)
except (TypeError, OSError, OverflowError, ValueError):
return None

@property
def read_status(self) -> str:
if self.readStatus is None:
Expand Down
51 changes: 51 additions & 0 deletions tests/test_message_times.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
from __future__ import annotations

import datetime

from saic_ismart_client_ng.api.message.schema import MessageEntity

UTC = datetime.UTC


def test_create_time_utc_from_milliseconds() -> None:
created = datetime.datetime(2026, 9, 25, 17, 57, 38, tzinfo=UTC)
msg = MessageEntity(createTime=int(created.timestamp() * 1000))
assert msg.create_time_utc == created
assert msg.create_time_utc is not None
assert msg.create_time_utc.tzinfo is not None


def test_create_time_utc_missing_is_none() -> None:
assert MessageEntity().create_time_utc is None


def test_create_time_utc_unreadable_is_none() -> None:
assert MessageEntity(createTime="soon").create_time_utc is None # type: ignore[arg-type]
assert MessageEntity(createTime=10**20).create_time_utc is None


def test_message_time_or_none_parses_every_known_format() -> None:
expected = datetime.datetime(2026, 9, 25, 18, 57, 38)
for text in ("2026-09-25 18:57:38", "25-09-2026 18:57:38", "25/09/2026 18:57:38"):
assert MessageEntity(messageTime=text).message_time_or_none == expected


def test_message_time_or_none_is_naive() -> None:
parsed = MessageEntity(messageTime="2026-09-25 18:57:38").message_time_or_none
assert parsed is not None
assert parsed.tzinfo is None


def test_message_time_or_none_missing_or_unreadable_is_none() -> None:
assert MessageEntity().message_time_or_none is None
assert MessageEntity(messageTime="").message_time_or_none is None
assert MessageEntity(messageTime="yesterday").message_time_or_none is None


def test_message_time_still_falls_back_to_now() -> None:
# Unchanged behaviour for existing callers.
before = datetime.datetime.now()
fallback = MessageEntity().message_time
assert before <= fallback <= datetime.datetime.now()
parsed = MessageEntity(messageTime="2026-09-25 18:57:38").message_time
assert parsed == datetime.datetime(2026, 9, 25, 18, 57, 38)
Loading