Skip to content

Refactor HgiGateway status to use async state model (#537)#541

Open
PWhite-Eng wants to merge 5 commits intoramses-rf:masterfrom
PWhite-Eng:refactor/537-async-gateway-status
Open

Refactor HgiGateway status to use async state model (#537)#541
PWhite-Eng wants to merge 5 commits intoramses-rf:masterfrom
PWhite-Eng:refactor/537-async-gateway-status

Conversation

@PWhite-Eng
Copy link
Copy Markdown
Contributor

The Problem:

Currently, determining if the Gateway is active relies on synchronous engine querying which bypasses the standard asynchronous state model of the architecture. This technical debt makes state tracking inconsistent and harder to maintain for Home Assistant integrations (Issue #537).

Consequences:

If left unaddressed, the library retains architectural fragmentation by mixing synchronous attribute polling with async state evaluations. This limits the ability to safely track and expose the Gateway's live connection status to downstream consumers (like ramses_cc), potentially leading to stale UI representations or blocking calls.

The Fix:

We encapsulated the gateway state logic within the HgiGateway class itself by adding an asynchronous is_active() method. We also introduced a formal configurable timeout to define exactly when a gateway is considered "inactive".

Technical Implementation:

  • const.py: Added GATEWAY_MESSAGE_TIMEOUT = timedelta(minutes=5) to define a standard heartbeat threshold.
  • base.py: Added async def is_active(self) -> bool: to the HgiGateway class. It fetches self._gwy._engine._this_msg, strictly checks the datetime (dtm), and calculates if the time delta against now(UTC) is within the timeout window. Handles both naive and timezone-aware datetimes.
  • test_base.py: Combined scattered base tests into a unified test file. Wrote explicit tests verifying behavior for missing messages, active messages, expired messages, naive datetimes, and mock component behaviors (DeviceBase, BatteryState). Mypy strict compliance was enforced using getattr() fallbacks and class comparisons to prevent static type narrowing errors.

Note: This PR introduces a breaking change to how gateway status is queried and MUST be merged at the exact same time as the matching PR for ramses_cc.

Testing Performed:

  • Executed the full suite of pytest tests (805 passed, 0 failures).
  • Achieved 100% test coverage on the newly implemented gateway status logic.
  • Executed mypy --strict with zero issues found across 131 source files.
  • Addressed boundary cases around simulated "Faked" devices, battery mixins, and strict type checking edge-cases.

Risks of NOT Implementing:

Failing to adopt this change leaves the gateway state tracking coupled to internal engine attributes, making future architectural shifts difficult and maintaining legacy synchronous blocks in an asynchronous application.

Risks of Implementing:

Consumers of the ramses_rf library (specifically ramses_cc) that currently inspect the gateway engine properties directly or rely on synchronous evaluation will break.

Mitigation Steps:

We have ensured complete backward compatibility for standard devices while explicitly isolating the breaking changes to HgiGateway.is_active(). A corresponding branch and PR for ramses_cc has been developed in tandem to utilize resolve_async_attr to handle this new implementation gracefully.

AI Assistance Disclosure:

This contribution was developed with the assistance of Google Gemini 3.1 Pro for code generation and documentation. No Agentic AI systems were employed; all logic and implementations were reviewed, verified, and manually committed by the author.

…ses-rf#537)

- Added `GATEWAY_MESSAGE_TIMEOUT` constant (5 minutes) in `const.py`.
- Refactored `HgiGateway.is_active` in `base.py` into an async method to
  evaluate gateway availability based on `_engine._this_msg.dtm`.
- Consolidated and expanded tests in `test_base.py` covering DeviceBase
  heartbeats, BatteryState mock behavior, and HgiGateway message timeouts.
- Resolved strict mypy static analysis issues using robust type assertions.

Fixes Issue ramses-rf#537.
@PWhite-Eng
Copy link
Copy Markdown
Contributor Author

@silverailscolo, this PR is stacked ontop of #539, please merge that PR before merging this PR. Please note there is a matching PR for ramses_cc to be released shortly

@silverailscolo silverailscolo mentioned this pull request Mar 29, 2026
2 tasks

import logging
from collections.abc import Callable
from datetime import timedelta
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Also here: import timedelta as td

Copy link
Copy Markdown
Collaborator

@silverailscolo silverailscolo left a comment

Choose a reason for hiding this comment

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

Fine to use this. Might store the gwy latest_dtm in a variety of, but this also handles all Entities.
Please update datetime import style including in tests


import logging
from collections.abc import Callable
from datetime import timedelta
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

as td

"""Return the timeout before the device is considered unavailable.

:return: The timeout duration before going unavailable.
:rtype: timedelta
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

td?

# lf._msgs_ot_ctl_polled = {}

@property
def heartbeat_timeout(self) -> timedelta:
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

-> td

"""Return the timeout before the device is considered unavailable.

:return: The timeout duration.
:rtype: timedelta
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

'td'

_STATE_ATTR = SZ_HEAT_DEMAND

@property
def heartbeat_timeout(self) -> timedelta:
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

-> td

"""Return the timeout before the device is considered unavailable.

:return: The timeout duration.
:rtype: timedelta
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

td

"""Return the timeout before the device is considered unavailable.

:return: The timeout duration.
:rtype: timedelta
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

td

which all reside in ramses_rf/device/base.py.
"""

from datetime import UTC, datetime, timedelta
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

as dt, td

@silverailscolo silverailscolo added this to the 0.55.7 milestone Mar 29, 2026
@github-actions
Copy link
Copy Markdown

Code Coverage

Package Line Rate Health
ramses_cli 90%
ramses_rf 80%
ramses_rf.device 72%
ramses_rf.system 75%
ramses_tx 86%
ramses_tx.protocol 81%
ramses_tx.transport 72%
test_ha_mqtt 95%
tests 93%
tests_cli 99%
tests_rf 92%
tests_rf.device 98%
tests_rf.virtual_rf 79%
tests_tx 99%
Summary 85% (14251 / 16787)

Minimum allowed line rate is 80%

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants