peterager-lennoxs30api-87
When `processMessage` receives a message from an unrecognized sender before all configured systems have completed initialization, it must ignore the message without producing ERROR-level logs or counting the dropped message. It may log the ignored message at DEBUG level. Once all systems are initialized, the existing behavior must remain: the first message from an unknown sender is counted and produces the existing two ERROR-level records, while subsequent messages from that sender do not produce additional ERROR-level records. Messages from a configured sibling must retain their existing behavior, including the two WARNING-level records containing the sibling identifier and `currentTime`.
Each `lennox_system` object has a boolean attribute named `systemMessageProcessed`, initially set to `False`. It becomes `True` when that system’s attributes have been updated from a received system message. The API exposes a property named `allSystemsInitialized`, which is `True` only when every object in `system_list` has `systemMessageProcessed` set to `True`. Changes made directly to `systemMessageProcessed`, including setting it to `False` in test code, must affect the value of `allSystemsInitialized` and the handling of unknown senders.
Hidden tests · 1 fail-to-pass, 5 pass-to-passrun after the agent submits, in a clean verifier
Test patch · 45 lines
diff --git a/tests/test_siblings.py b/tests/test_siblings.py
index def7ca2..c80c51f 100644
--- a/tests/test_siblings.py
+++ b/tests/test_siblings.py
@@ -83,3 +83,40 @@ def test_process_zero_sibling_message(api: s30api_async, caplog):
assert lsystem.sibling_portNumber is None
assert lsystem.sibling_ipAddress is None
assert len(caplog.records) == 0
+
+def test_process_unknown_sender(api: s30api_async, caplog):
+ """Test processing from unknown sender"""
+ message = loadfile("system_uptime.json","bad_sender")
+ caplog.clear()
+ with caplog.at_level(logging.ERROR):
+ api.processMessage(message)
+ assert len(caplog.records) == 2
+ assert "bad_sender" in caplog.messages[0]
+ assert "currentTime" in caplog.messages[1]
+ api.processMessage(message)
+ assert len(caplog.records) == 2
+
+def test_process_unknown_sender_no_init(api: s30api_async, caplog):
+ """Test processing from unknown sender when systems are not all initialized."""
+ lsystem: lennox_system = api.system_list[1]
+ lsystem.systemMessageProcessed = False
+ message = loadfile("system_uptime.json","bad_sender")
+ caplog.clear()
+ with caplog.at_level(logging.ERROR):
+ api.processMessage(message)
+ assert len(caplog.records) == 0
+
+def test_process_message_from_sibling(api: s30api_async, caplog):
+ """Test processing message from sibling"""
+
+ message = loadfile("sibling.json")
+ api.processMessage(message)
+ message = loadfile("system_uptime.json","KL21J00002")
+ caplog.clear()
+ with caplog.at_level(logging.WARNING):
+ api.processMessage(message)
+ assert len(caplog.records) == 2
+ assert caplog.records[0].levelname == "WARNING"
+ assert caplog.records[1].levelname == "WARNING"
+ assert "KL21J00002" in caplog.messages[0]
+ assert "currentTime" in caplog.messages[1]
Reference fix · 1 file, +20 −8the upstream merge, used only for grading calibration
The agent could not see this: the repository holds one commit and the sandbox has no network. Leak audit.
lennoxs30api/s30api_async.py
diff --git a/lennoxs30api/s30api_async.py b/lennoxs30api/s30api_async.py
index 5f4414c..f95848d 100644
--- a/lennoxs30api/s30api_async.py
+++ b/lennoxs30api/s30api_async.py
@@ -755,15 +755,18 @@ def processMessage(self, message):
else:
system: lennox_system = self.getSystemSibling(sysId)
if system is None:
- self.metrics.inc_sender_message_drop()
- if sysId in self._badSenderDict:
- _LOGGER.debug(f"processMessage dropping messages from unknown SenderId/SystemId [{sysId}]")
+ if self.allSystemsInitialized is True:
+ self.metrics.inc_sender_message_drop()
+ if sysId in self._badSenderDict:
+ _LOGGER.debug(f"processMessage dropping messages from unknown SenderId/SystemId [{sysId}]")
+ else:
+ _LOGGER.error(
+ f"processMessage dropping message from unknown SenderId/SystemId [{sysId}] - please consult https://github.com/PeteRager/lennoxs30/blob/master/docs/sibling.md for configuration assistance"
+ )
+ _LOGGER.error(json.dumps(message, indent=4))
+ self._badSenderDict[sysId] = sysId
else:
- _LOGGER.error(
- f"processMessage dropping message from unknown SenderId/SystemId [{sysId}] - please consult https://github.com/PeteRager/lennoxs30/blob/master/docs/sibling.md for configuration assistance"
- )
- _LOGGER.error(json.dumps(message, indent=4))
- self._badSenderDict[sysId] = sysId
+ _LOGGER.debug(f"processMessage ignoring messages from unknown SenderId/SystemId [{sysId}] because systems are not initialized")
else:
self.metrics.inc_sibling_message_drop()
if self.metrics.sibling_message_drop == 1:
@@ -847,6 +850,13 @@ def getSystemSibling(self, sysId: str) -> "lennox_system":
if system.sibling_identifier == sysId:
return system
return None
+
+ @property
+ def allSystemsInitialized(self) -> bool:
+ for system in self.system_list:
+ if system.systemMessageProcessed is False:
+ return False
+ return True
def getOrCreateSystem(self, sysId: str) -> "lennox_system":
system = self.getSystem(sysId)
@@ -1172,6 +1182,7 @@ def __init__(self, sysId: str):
"indoorAirQuality": self._process_indoor_air_quality,
"weather": self._process_weather,
}
+ self.systemMessageProcessed: bool = False
self.equipment: dict[int, lennox_equipment] = {}
self.ble_devices: dict[int, LennoxBle] = {}
@@ -1536,6 +1547,7 @@ def attr_updater(self, input_set, attr: str, propertyName: str = None) -> bool:
return False
def _processSystemMessage(self, message):
+ self.systemMessageProcessed = True
if "config" in message:
config = message["config"]
self.attr_updater(config, "temperatureUnit")
GLM-5.3 Flash · attempt 2 · solved · 16 steps · $0.003
Submitted diff · 44 lines
diff --git a/lennoxs30api/s30api_async.py b/lennoxs30api/s30api_async.py
index 5f4414c..9260f11 100644
--- a/lennoxs30api/s30api_async.py
+++ b/lennoxs30api/s30api_async.py
@@ -755,6 +755,9 @@ class s30api_async(object):
else:
system: lennox_system = self.getSystemSibling(sysId)
if system is None:
+ if self.allSystemsInitialized is False:
+ _LOGGER.debug(f"processMessage ignoring message from unknown SenderId/SystemId [{sysId}] - systems not yet initialized")
+ return
self.metrics.inc_sender_message_drop()
if sysId in self._badSenderDict:
_LOGGER.debug(f"processMessage dropping messages from unknown SenderId/SystemId [{sysId}]")
@@ -848,6 +851,13 @@ class s30api_async(object):
return system
return None
+ @property
+ def allSystemsInitialized(self) -> bool:
+ for system in self.system_list:
+ if system.systemMessageProcessed is False:
+ return False
+ return True
+
def getOrCreateSystem(self, sysId: str) -> "lennox_system":
system = self.getSystem(sysId)
if system is not None:
@@ -1078,6 +1088,7 @@ class lennox_system(object):
self.sibling_self_identifier: str = None
self.sibling_identifier: str = None
self.sibling_systemName: str = None
+ self.systemMessageProcessed: bool = False
self.sibling_nodePresent: str = None
self.sibling_portNumber: str = None
self.sibling_ipAddress: str = None
@@ -1236,6 +1247,7 @@ class lennox_system(object):
indent=4,
)
)
+ self.systemMessageProcessed = True
_LOGGER.debug(
f"processMessage complete system id [{self.sysId}] dirty [{self._dirty}] dirtyList [{self._dirtyList}]"
)$ ls /repo; grep -rn "systemMessageProcessed\|allSystemsInitialized" /repo --include=*.py | head
output · 11 lines
LICENSE README.md coverage.txt coverage.xml lennoxs30api lennoxs30api.egg-info pyproject.toml samples setup.py simulator tests
$ grep -rn "unknown sender\|Unknown sender\|processMessage" /repo/lennoxs30api/*.py | head -30; ls /repo/lennoxs30api
output · 22 lines
/repo/lennoxs30api/s30api_async.py:721: self.processMessage(message)
/repo/lennoxs30api/s30api_async.py:745: def processMessage(self, message):
/repo/lennoxs30api/s30api_async.py:754: system.processMessage(message)
/repo/lennoxs30api/s30api_async.py:760: _LOGGER.debug(f"processMessage dropping messages from unknown SenderId/SystemId [{sysId}]")
/repo/lennoxs30api/s30api_async.py:763: f"processMessage dropping message from unknown SenderId/SystemId [{sysId}] - please consult https://github.com/PeteRager/lennoxs30/blob/master/docs/sibling.md for configuration assistance"
/repo/lennoxs30api/s30api_async.py:771: f"processMessage dropping message from sibling [{sysId}] for system [{system.sysId}] - please consult https://github.com/PeteRager/lennoxs30/blob/master/docs/sibling.md for configurat
…[937 chars omitted]…
g(f"processMessage lennox_zone id [{self.id}]")
/repo/lennoxs30api/s30api_async.py:2406: f"processMessage complete lennox_zone id [{self.id}] dirty [{self._dirty}] dirtyList [{self._dirtyList}]"
__init__.py
docs
lennox_ble.py10 more lines
lennox_equipment.py lennox_errors.py lennox_home.py lennox_period.py lennox_schedule.py message_logger.py metrics.py s30api_async.py s30exception.py subscriber_base.py
$ sed -n 740,790p /repo/lennoxs30api/s30api_async.py
output · 31 lines
raise s30e from e
# should not be here, these are unexpected exceptions that should be handled better
_LOGGER.exception("messagePump - unexpected exception - please raise an issue to track")
raise S30Exception("messagePump failed due to unexpected exception", EC_COMMS_ERROR, 7) from e
def processMessage(self, message):
self.metrics.inc_message_count()
# LAN message and cloud message uses different capitalization.
if "SenderID" in message:
sysId = message["SenderID"]
else:
sysId = message["SenderId"]19 more lines
system = self.getSystem(sysId)
if system is not None:
system.processMessage(message)
else:
system: lennox_system = self.getSystemSibling(sysId)
if system is None:
self.metrics.inc_sender_message_drop()
…[1417 chars omitted]…
tionalParameters: str) -> json:
_LOGGER.debug("requestDataHelper - Enter")
try:
url = self.url_requestdata
headers = {
"Authorization": self.loginBearerToken,
"Content-Type": "application/json; charset=utf-8",
"User-Agent": USER_AGENT,
"Accept": "*.*",
"Accept-Language": "en-US;q=1",$ grep -n "class lennox_system" /repo/lennoxs30api/s30api_async.py; sed -n 830,900p /repo/lennoxs30api/s30api_async.py; grep -n "def processMessage" /repo/lennoxs30api/s30api_async.py
output · 36 lines
1016:class lennox_system(object):
if s30e is not None:
raise s30e from e
_LOGGER.exception("requestDataHelper - unexpected exception - please raise an issue to track")
raise S30Exception(
"requestDataHelper failed due to unexpected exception",
EC_COMMS_ERROR,
7,
) from e
def getSystem(self, sysId) -> "lennox_system":
for system in self.system_list:24 more lines
if system.sysId == sysId:
return system
return None
def getSystemSibling(self, sysId: str) -> "lennox_system":
for system in self.system_list:
if system.sibling_identifier == sysId:
return system
return None
def getOrCreateSystem(self, sysId: str) -> "lennox_system":
system = self.getSystem(sysId)
if system is not
…[1921 chars omitted]…
arameters)
async def publishMessageHelper(self, sysId: str, data: str, additional_parameters=None) -> None:
_LOGGER.debug(f"publishMessageHelper sysId [{sysId}] data [{data}]")
try:
url = self.url_publish
headers = {
745: def processMessage(self, message):
1220: def processMessage(self, message) -> None:
2344: def processMessage(self, zoneMessage):$ sed -n 1016,1080p /repo/lennoxs30api/s30api_async.py; sed -n 1220,1250p /repo/lennoxs30api/s30api_async.py
output · 35 lines
class lennox_system(object):
"""Represents a Lennox Control System"""
def __init__(self, sysId: str):
self.sysId: str = sysId
self.api: s30api_async = None
self.idx: int = None
self.home: lennox_home = None
self.zone_list: List["lennox_zone"] = []
self._schedules: List[lennox_schedule] = []
self._callbacks = []
self._diagcallbacks = []23 more lines
self._eqParametersCallbacks = []
self.outdoorTemperature = None
self.name: str = None
self.allergenDefender = None
self.ventilationMode = None
self.diagPoweredHours = None
self.diagRuntime = None
self.diagVentilationRuntime = None
self.ventilationRemainingTime = None
self.ventilatingUntilTime = None
self.ventilationUnitType = None
self.ventilationControlMode = None
self.feelsLike
…[2728 chars omitted]…
OnUpdateCallbacks()
except Exception:
_LOGGER.exception("processMessage - unexpected exception - Failed Message to Follow")
_LOGGER.error(json.dumps(self.api.message_log.remove_redacted_fields(message), indent=4))
def getOrCreateSchedule(self, schedule_id):
schedule = self.getSchedule(schedule_id)
if schedule is not None:
return schedule$ grep -rn "badSender\|sender_message_drop\|unknown" /repo/tests/*.py | head -20
output · 16 lines
/repo/tests/test_alerts.py:61:def test_alerts_unknown_alert(api_system_04_furn_ac_zoning): /repo/tests/test_alerts.py:62: '''Verifies that an unknown alert is properly processed''' /repo/tests/test_alerts.py:87: assert alert["message"] == "unknown alert code" /repo/tests/test_api_process_message.py:34: assert api.metrics.sender_message_drop == 0 /repo/tests/test_api_process_message.py:42: assert api.metrics.sender_message_drop == 0 /repo/tests/test_api_process_message.py:45:def test_api_process_unknown_sender(api: s30api_async, caplog): /repo/tests/test_api_process_message.py:46: '''Tests processing a message from an unknown sender''' /repo/tests/test_api_process_message.py:60: assert api.metrics.sender_message_drop == 1 /repo/tests/test_api_process_message.py:61: assert len(api._badSenderDict) == 1 /repo/tests/test_api_process_message.py:62: …[282 chars omitted]… t "KL21J00002" in api._badSenderDict
4 more lines
/repo/tests/test_api_process_message.py:84: assert api.metrics.sender_message_drop == 3 /repo/tests/test_api_process_message.py:85: assert len(api._badSenderDict) == 2 /repo/tests/test_api_process_message.py:86: assert "KL21J00002" in api._badSenderDict /repo/tests/test_api_process_message.py:87: assert "KL21J00003" in api._badSenderDict
$ sed -n 1,140p /repo/tests/test_api_process_message.py
output · 40 lines
'''Test module for processing messages'''
# pylint: disable=protected-access
import logging
from lennoxs30api.s30api_async import (
lennox_system,
s30api_async,
)
from tests.conftest import loadfile
28 more lines
def test_api_process_sibling_message(api: s30api_async, caplog):
'''Tests the processing of sibling messages'''
lsystem: lennox_system = api.system_list[1]
assert lsystem.sysId == "0000000-0000-0000-0000-000000000002"
message = loadfile("sibling.json")
api.processMessage(message)
assert lsystem.sibling_self_identifier == "KL21J00001"
assert lsystem.sibling_identifier == "KL21J00002"
assert api.metrics.sibling_message_drop == 0
message = loadfile("mut_sys1_zone1_status.json")
message["SenderId"] = "KL21J00002"
caplog.clear()
api.metrics.reset()
with caplog.at_level(logging.DEBUG):
api.processMessage(message)
a
…[2136 chars omitted]…
ge(message)
assert len(caplog.records) == 2
assert "KL21J00003" in caplog.messages[0]
assert caplog.records[0].levelname == "ERROR"
assert api.metrics.sibling_message_drop == 0
assert api.metrics.sender_message_drop == 3
assert len(api._badSenderDict) == 2
assert "KL21J00002" in api._badSenderDict
assert "KL21J00003" in api._badSenderDict$ cd /repo && python - <<'PY'
output · 5 lines
758: if self.allSystemsInitialized is False: 855: def allSystemsInitialized(self) -> bool: 857: if system.systemMessageProcessed is False: 1091: self.systemMessageProcessed: bool = False 1250: self.systemMessageProcessed = True
$ cd /repo && python -m pytest tests/test_api_process_message.py -q 2>&1 | tail -20
output · 8 lines
.. [100%]
=============================== warnings summary ===============================
tests/test_api_process_message.py: 28 warnings
/repo/lennoxs30api/metrics.py:59: DeprecationWarning: datetime.datetime.utcnow() is deprecated and scheduled for removal in a future version. Use timezone-aware objects to represent datetimes in UTC: datetime.datetime.now(datetime.UTC).
return pytz.utc.localize(datetime.utcnow())
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
2 passed, 28 warnings in 0.28sExisting tests pass (fixtures initialize systems via messages). Now a reproducer for the uninitialized case:
$ cd /repo && python - <<'PY'
output · 7 lines
Traceback (most recent call last): File "<stdin>", line 3, in <module> TypeError: s30api_async.__init__() missing 1 required positional argument: 'app_id' --- Traceback (most recent call last): File "<stdin>", line 3, in <module> TypeError: s30api_async.__init__() missing 1 required positional argument: 'app_id'
$ cd /repo && python - <<'PY'
output · 5 lines
uninit: errors 0 records 1 drop 0
init: errors 2 drop 1 bad {'UNKNOWN': 'UNKNOWN'}
Traceback (most recent call last):
File "<stdin>", line 24, in <module>
AssertionError$ cd /repo && sed -n 1,80p lennoxs30api/metrics.py
output · 35 lines
"""Communication Metrics for communication to lennox controller"""
# pylint: disable=invalid-name
from datetime import datetime
import pytz
class Metrics:
"""Communication Metrics"""
def __init__(self):
self.error_count: int = 0
self.message_count: int = 023 more lines
self.receive_count: int = 0
self.send_count: int = 0
self.http_2xx_cnt: int = 0
self.http_4xx_cnt: int = 0
self.http_5xx_cnt: int = 0
self.timeouts: int = 0
self.server_disconnects: int = 0
self.client_response_errors: int = 0
self.connection_errors: int = 0
self.last_receive_time: datetime = None
self.last_send_time: datetime = None
self.last_error_time: datetime = None
self.last_reconnect_time: datetime = None
self.last_message_time: datetime = None
self.last_metric_time: datetime = Non
…[1596 chars omitted]…
"client_response_errors": self.client_response_errors,
"server_disconnects": self.server_disconnects,
"connection_errors": self.connection_errors,
"last_receive_time": self.last_receive_time,
"last_error_time": self.last_error_time,
"last_reconnect_time": self.last_reconnect_time,
"last_message_time": self.last_message_time,My reproducer assertion was wrong (after reset, second message gives drop=1). Fixing:
$ cd /repo && python - <<'PY'
output · 4 lines
OK FAILED tests/test_zone_test_parameters.py::test_set_zone_test_parameter_value_bad_value FAILED tests/test_zone_test_parameters.py::test_internal_set_zone_test_parameter_value 105 failed, 71 passed, 1448 warnings in 8.12s
$ cd /repo && git stash && python -m pytest tests/ -q 2>&1 | tail -3; git stash pop
output · 12 lines
Saved working directory and index state WIP on master: 67ed82f base
FAILED tests/test_zone_test_parameters.py::test_set_zone_test_parameter_value_bad_value
FAILED tests/test_zone_test_parameters.py::test_internal_set_zone_test_parameter_value
105 failed, 71 passed, 1448 warnings in 6.58s
On branch master
Changes not staged for commit:
(use "git add <file>..." to update what will be committed)
(use "git restore <file>..." to discard changes in working directory)
modified: lennoxs30api/s30api_async.py
no changes added to commit (use "git add" and/or "git commit -a")
Dropped refs/stash@{0} (6ba5da5c4593ce69c3c2ab7ecf02f3c2b6d65feb)Pre-existing failures identical before my change (environment issue). Final diff:
$ cd /repo && git diff
output · 29 lines
diff --git a/lennoxs30api/s30api_async.py b/lennoxs30api/s30api_async.py
index 5f4414c..9260f11 100644
--- a/lennoxs30api/s30api_async.py
+++ b/lennoxs30api/s30api_async.py
@@ -755,6 +755,9 @@ class s30api_async(object):
else:
system: lennox_system = self.getSystemSibling(sysId)
if system is None:
+ if self.allSystemsInitialized is False:
+ _LOGGER.debug(f"processMessage ignoring message from unknown SenderId/SystemId [{sysId}] - systems not yet initialized")
+ return
self.metrics.inc_sender_message_drop()17 more lines
if sysId in self._badSenderDict:
_LOGGER.debug(f"processMessage dropping messages from unknown SenderId/SystemId [{sysId}]")
@@ -848,6 +851,13 @@ class s30api_async(object):
return system
return None
+ @property
…[686 chars omitted]…
ng_ipAddress: str = None
@@ -1236,6 +1247,7 @@ class lennox_system(object):
indent=4,
)
)
+ self.systemMessageProcessed = True
_LOGGER.debug(
f"processMessage complete system id [{self.sysId}] dirty [{self._dirty}] dirtyList [{self._dirtyList}]"
)$ done: {"notes": "Added systemMessageProcessed flag to lennox_system (set True after processing a Data message), allSystemsInitialized property on s30api_async, and gated unknown-sender handling in processMessage: before full initialization, unknown-sender messages are logged at DEBUG only with no drop counting; after initialization the eoutput · 3 lines
…[34 chars omitted]… or is unchanged. Sibling behavior untouched. Reproducer passes; existing tests pass (full suite has pre-existing environment failures identical before/after change)."} submitted.