Skip to content

Commit acf6193

Browse files
committed
refactor: extract API version resolution into VersionResolver
Move the version-to-API-config mapping policy out of the BSBLAN client into a dedicated _version.py collaborator. _set_api_version now delegates to VersionResolver.resolve_config_version, and the pure mapping logic (previously _resolve_api_version) lives in the module-level _map_reported_version helper. Behavior-preserving: the JSON-API version remains preferred over firmware, the same version floors and v3 thresholds apply, and the public api_version / json_api_version properties are unchanged. Existing tests pass unchanged.
1 parent 7466fe6 commit acf6193

2 files changed

Lines changed: 132 additions & 56 deletions

File tree

src/bsblan/_version.py

Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
"""API version resolution for the BSBLAN client.
2+
3+
Owns the policy that maps a device's reported version signals to a supported
4+
API configuration version. The BSB-LAN JSON-API version (from ``/JV``) is the
5+
documented, firmware-independent compatibility signal and is preferred when
6+
available; the adapter firmware version (from ``/JI``) is used as a fallback
7+
for very old firmware that does not expose ``/JV``.
8+
"""
9+
10+
from __future__ import annotations
11+
12+
from packaging import version as pkg_version
13+
from packaging.version import InvalidVersion
14+
15+
from .constants import (
16+
BASIC_API_VERSION,
17+
MIN_SUPPORTED_FIRMWARE,
18+
MIN_SUPPORTED_JSON_API,
19+
SUPPORTED_API_VERSION,
20+
V3_FIRMWARE_MINIMUM,
21+
V3_JSON_API_MINIMUM,
22+
ErrorMsg,
23+
)
24+
from .exceptions import BSBLANError, BSBLANVersionError
25+
26+
27+
def _map_reported_version(reported: str, *, minimum: str, v3_minimum: str) -> str:
28+
"""Map a reported version string to a supported API config version.
29+
30+
Args:
31+
reported: The version string reported by the device.
32+
minimum: The lowest supported version; anything below is rejected.
33+
v3_minimum: The threshold at or above which the full "v3" config is
34+
used; below it the basic "v2" config is used.
35+
36+
Returns:
37+
``"v2"`` for the basic single-circuit config or ``"v3"`` for the full
38+
config.
39+
40+
Raises:
41+
BSBLANVersionError: If the reported version cannot be parsed or is
42+
below ``minimum``.
43+
44+
"""
45+
try:
46+
parsed = pkg_version.parse(reported)
47+
except InvalidVersion as exc:
48+
raise BSBLANVersionError(ErrorMsg.VERSION, version=reported) from exc
49+
if parsed < pkg_version.parse(minimum):
50+
raise BSBLANVersionError(ErrorMsg.VERSION, version=reported)
51+
if parsed < pkg_version.parse(v3_minimum):
52+
# Legacy / basic capability: single-circuit support only.
53+
return BASIC_API_VERSION
54+
return SUPPORTED_API_VERSION
55+
56+
57+
class VersionResolver:
58+
"""Resolve the API configuration version from reported device versions.
59+
60+
The resolver is configured with the version floors and ``v3`` thresholds
61+
for both the JSON-API and firmware signals. Defaults use the library's
62+
named constants; custom thresholds can be supplied for testing.
63+
"""
64+
65+
def __init__(
66+
self,
67+
*,
68+
firmware_minimum: str = MIN_SUPPORTED_FIRMWARE,
69+
firmware_v3_minimum: str = V3_FIRMWARE_MINIMUM,
70+
json_api_minimum: str = MIN_SUPPORTED_JSON_API,
71+
json_api_v3_minimum: str = V3_JSON_API_MINIMUM,
72+
) -> None:
73+
"""Initialize the resolver with version policy thresholds.
74+
75+
Args:
76+
firmware_minimum: Lowest supported adapter firmware version.
77+
firmware_v3_minimum: Firmware version at/above which "v3" is used.
78+
json_api_minimum: Lowest supported JSON-API version.
79+
json_api_v3_minimum: JSON-API version at/above which "v3" is used.
80+
81+
"""
82+
self._firmware_minimum = firmware_minimum
83+
self._firmware_v3_minimum = firmware_v3_minimum
84+
self._json_api_minimum = json_api_minimum
85+
self._json_api_v3_minimum = json_api_v3_minimum
86+
87+
def resolve_config_version(
88+
self,
89+
*,
90+
json_api_version: str | None,
91+
firmware_version: str | None,
92+
) -> str:
93+
"""Resolve the API config version from the available version signals.
94+
95+
The JSON-API version is preferred when present; otherwise the firmware
96+
version is used as a fallback.
97+
98+
Args:
99+
json_api_version: The JSON-API version reported by ``/JV``, or None.
100+
firmware_version: The adapter firmware version from ``/JI``, or None.
101+
102+
Returns:
103+
``"v2"`` for the basic single-circuit config or ``"v3"`` for the
104+
full config.
105+
106+
Raises:
107+
BSBLANError: If neither the JSON-API version nor the firmware
108+
version is available.
109+
BSBLANVersionError: If the reported version is not supported.
110+
111+
"""
112+
if json_api_version is not None:
113+
return _map_reported_version(
114+
json_api_version,
115+
minimum=self._json_api_minimum,
116+
v3_minimum=self._json_api_v3_minimum,
117+
)
118+
119+
if not firmware_version:
120+
raise BSBLANError(ErrorMsg.FIRMWARE_VERSION)
121+
122+
return _map_reported_version(
123+
firmware_version,
124+
minimum=self._firmware_minimum,
125+
v3_minimum=self._firmware_v3_minimum,
126+
)

src/bsblan/bsblan.py

Lines changed: 6 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -12,22 +12,17 @@
1212

1313
import aiohttp
1414
from aiohttp.hdrs import METH_POST
15-
from packaging import version as pkg_version
16-
from packaging.version import InvalidVersion
1715

1816
from ._transport import BSBLANTransport
17+
from ._version import VersionResolver
1918
from .constants import (
2019
API_V2,
2120
API_V3,
2221
BASIC_API_VERSION,
23-
MIN_SUPPORTED_FIRMWARE,
24-
MIN_SUPPORTED_JSON_API,
2522
PPS_HEATING_PARAMS,
2623
PPS_STATIC_VALUES_PARAMS,
2724
SUPPORTED_API_VERSION,
2825
SUPPORTED_API_VERSIONS,
29-
V3_FIRMWARE_MINIMUM,
30-
V3_JSON_API_MINIMUM,
3126
APIConfig,
3227
CircuitConfig,
3328
ErrorMsg,
@@ -127,6 +122,7 @@ class BSBLAN:
127122
_section_locks: dict[str, asyncio.Lock] = field(default_factory=dict)
128123
_hot_water_group_locks: dict[str, asyncio.Lock] = field(default_factory=dict)
129124
_transport: BSBLANTransport = field(init=False)
125+
_version_resolver: VersionResolver = field(init=False)
130126

131127
def __post_init__(self) -> None:
132128
"""Wire up internal collaborators after dataclass construction."""
@@ -135,6 +131,7 @@ def __post_init__(self) -> None:
135131
lambda: self.session,
136132
lambda: self._firmware_version,
137133
)
134+
self._version_resolver = VersionResolver()
138135

139136
async def __aenter__(self) -> Self:
140137
"""Enter the context manager.
@@ -666,58 +663,11 @@ def _set_api_version(self) -> None:
666663
BSBLANVersionError: If the reported version is not supported.
667664
668665
"""
669-
if self._json_api_version is not None:
670-
self._api_version = self._resolve_api_version(
671-
self._json_api_version,
672-
minimum=MIN_SUPPORTED_JSON_API,
673-
v3_minimum=V3_JSON_API_MINIMUM,
674-
)
675-
return
676-
677-
if not self._firmware_version:
678-
raise BSBLANError(ErrorMsg.FIRMWARE_VERSION)
679-
680-
self._api_version = self._resolve_api_version(
681-
self._firmware_version,
682-
minimum=MIN_SUPPORTED_FIRMWARE,
683-
v3_minimum=V3_FIRMWARE_MINIMUM,
666+
self._api_version = self._version_resolver.resolve_config_version(
667+
json_api_version=self._json_api_version,
668+
firmware_version=self._firmware_version,
684669
)
685670

686-
def _resolve_api_version(
687-
self,
688-
reported: str,
689-
*,
690-
minimum: str,
691-
v3_minimum: str,
692-
) -> str:
693-
"""Map a reported version string to a supported API config version.
694-
695-
Args:
696-
reported: The version string reported by the device.
697-
minimum: The lowest supported version; anything below is rejected.
698-
v3_minimum: The threshold at or above which the full "v3" config is
699-
used; below it the basic "v2" config is used.
700-
701-
Returns:
702-
``"v2"`` for the basic single-circuit config or ``"v3"`` for the
703-
full config.
704-
705-
Raises:
706-
BSBLANVersionError: If the reported version cannot be parsed or is
707-
below ``minimum``.
708-
709-
"""
710-
try:
711-
parsed = pkg_version.parse(reported)
712-
except InvalidVersion as exc:
713-
raise BSBLANVersionError(ErrorMsg.VERSION, version=reported) from exc
714-
if parsed < pkg_version.parse(minimum):
715-
raise BSBLANVersionError(ErrorMsg.VERSION, version=reported)
716-
if parsed < pkg_version.parse(v3_minimum):
717-
# Legacy / basic capability: single-circuit support only.
718-
return BASIC_API_VERSION
719-
return SUPPORTED_API_VERSION
720-
721671
async def _fetch_temperature_range(
722672
self,
723673
circuit: int,

0 commit comments

Comments
 (0)