Skip to content

Commit 71ee3ed

Browse files
committed
Add new exception types for unsupported features and malformed responses
- Introduced BSBLANUnsupportedFeatureError for permanent feature unavailability. - Introduced BSBLANMalformedResponseError for transient JSON parsing issues. - Updated relevant code to raise new exceptions in appropriate scenarios. - Enhanced tests to cover new exception handling.
1 parent 1f0a3d6 commit 71ee3ed

8 files changed

Lines changed: 67 additions & 17 deletions

File tree

src/bsblan/__init__.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@
1212
BSBLANAuthError,
1313
BSBLANConnectionError,
1414
BSBLANError,
15+
BSBLANMalformedResponseError,
16+
BSBLANUnsupportedFeatureError,
1517
BSBLANVersionError,
1618
)
1719
from .models import (
@@ -45,6 +47,8 @@
4547
"BSBLANConfig",
4648
"BSBLANConnectionError",
4749
"BSBLANError",
50+
"BSBLANMalformedResponseError",
51+
"BSBLANUnsupportedFeatureError",
4852
"BSBLANVersionError",
4953
"DHWSchedule",
5054
"DHWTimeSwitchPrograms",

src/bsblan/_hot_water.py

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
from typing import TYPE_CHECKING, Any, TypeVar
66

77
from .constants import ErrorMsg, HotWaterParams
8-
from .exceptions import BSBLANError
8+
from .exceptions import BSBLANError, BSBLANUnsupportedFeatureError
99
from .models import (
1010
HotWaterConfig,
1111
HotWaterSchedule,
@@ -87,7 +87,10 @@ async def fetch_data(
8787
The populated model instance.
8888
8989
Raises:
90-
BSBLANError: If no parameters are available for the filter.
90+
BSBLANUnsupportedFeatureError: If the schedule group exposes no
91+
parameters (a permanent condition).
92+
BSBLANError: If no parameters are available for a non-schedule
93+
group.
9194
9295
"""
9396
# Granular lazy load: validate only this param group on first access
@@ -105,6 +108,8 @@ async def fetch_data(
105108
filtered_params = self._apply_include_filter(filtered_params, include)
106109

107110
if not filtered_params:
111+
if group_name == "schedule":
112+
raise BSBLANUnsupportedFeatureError(error_msg)
108113
raise BSBLANError(error_msg)
109114

110115
data = await self._request_named_params(filtered_params)

src/bsblan/_schedules.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
from typing import TYPE_CHECKING, Any
66

77
from .constants import ErrorMsg, HeatingScheduleParams, HotWaterParams
8-
from .exceptions import BSBLANError
8+
from .exceptions import BSBLANError, BSBLANUnsupportedFeatureError
99
from .models import HeatingTimeSwitchPrograms
1010

1111
if TYPE_CHECKING:
@@ -62,7 +62,8 @@ async def heating_schedule(
6262
HeatingTimeSwitchPrograms: Heating schedule information.
6363
6464
Raises:
65-
BSBLANError: If no schedule parameters are available.
65+
BSBLANUnsupportedFeatureError: If the device exposes no heating
66+
schedule parameters (a permanent condition).
6667
6768
"""
6869
self._validate_circuit(circuit)
@@ -79,7 +80,7 @@ async def heating_schedule(
7980
}
8081

8182
if not mapped_data:
82-
raise BSBLANError(ErrorMsg.NO_HEATING_SCHEDULE_PARAMS)
83+
raise BSBLANUnsupportedFeatureError(ErrorMsg.NO_HEATING_SCHEDULE_PARAMS)
8384

8485
return HeatingTimeSwitchPrograms.model_validate(mapped_data)
8586

src/bsblan/_transport.py

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@
2020
from yarl import URL
2121

2222
from .constants import ErrorMsg
23-
from .exceptions import BSBLANAuthError, BSBLANError
23+
from .exceptions import BSBLANAuthError, BSBLANError, BSBLANMalformedResponseError
2424

2525
if TYPE_CHECKING:
2626
from collections.abc import Callable, Mapping
@@ -110,8 +110,10 @@ async def request_with_retry(
110110
dict[str, Any]: The JSON response from the BSBLAN device.
111111
112112
Raises:
113-
BSBLANError: If the session is missing or the response is invalid.
113+
BSBLANError: If the session is missing.
114114
BSBLANAuthError: If authentication fails (401/403, not retried).
115+
BSBLANMalformedResponseError: If the response body cannot be
116+
decoded or parsed as JSON (a transient condition).
115117
116118
"""
117119
session = self._session_getter()
@@ -141,7 +143,7 @@ async def request_with_retry(
141143
except (ValueError, UnicodeDecodeError) as e:
142144
# Handle JSON decode errors and other parsing issues
143145
msg = ErrorMsg.INVALID_RESPONSE.format(e)
144-
raise BSBLANError(msg) from e
146+
raise BSBLANMalformedResponseError(msg) from e
145147

146148
@staticmethod
147149
async def _read_json(response: aiohttp.ClientResponse) -> dict[str, Any]:

src/bsblan/exceptions.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,3 +77,20 @@ class BSBLANAuthError(BSBLANError):
7777
"""Raised when authentication fails."""
7878

7979
message: str = "Authentication failed. Please check your username and password."
80+
81+
82+
class BSBLANUnsupportedFeatureError(BSBLANError):
83+
"""Raised when the device does not support a requested feature.
84+
85+
This signals a permanent condition: the device does not expose the
86+
requested parameters (for example a heating or hot water schedule), so
87+
retrying the same request will never succeed.
88+
"""
89+
90+
91+
class BSBLANMalformedResponseError(BSBLANError):
92+
"""Raised when a device response cannot be decoded or parsed.
93+
94+
This signals a transient condition: the response body could not be decoded
95+
or parsed as valid JSON, so a retry may succeed.
96+
"""

tests/test_bsblan_edge_cases.py

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,11 @@
99
import pytest
1010

1111
from bsblan import BSBLAN, BSBLANConfig
12-
from bsblan.exceptions import BSBLANConnectionError, BSBLANError
12+
from bsblan.exceptions import (
13+
BSBLANConnectionError,
14+
BSBLANError,
15+
BSBLANMalformedResponseError,
16+
)
1317

1418

1519
@pytest.mark.asyncio
@@ -77,9 +81,14 @@ async def test_value_error_path(monkeypatch: Any) -> None:
7781

7882
monkeypatch.setattr(session, "request", MagicMock(return_value=mock_response))
7983

80-
with pytest.raises(BSBLANError, match="Invalid response format"):
84+
with pytest.raises(
85+
BSBLANMalformedResponseError, match="Invalid response format"
86+
) as exc_info:
8187
await bsblan._request()
8288

89+
# Transient error must remain catchable as the generic BSBLANError.
90+
assert isinstance(exc_info.value, BSBLANError)
91+
8392

8493
def test_bsblan_config_initialization_edge_cases() -> None:
8594
"""Test edge cases in BSBLAN initialization."""

tests/test_heating_schedule.py

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77

88
import pytest
99

10-
from bsblan.exceptions import BSBLANError
10+
from bsblan.exceptions import BSBLANError, BSBLANUnsupportedFeatureError
1111
from bsblan.models import HeatingTimeSwitchPrograms
1212

1313
if TYPE_CHECKING:
@@ -138,8 +138,14 @@ async def test_heating_schedule_invalid_include_raises(mock_bsblan: BSBLAN) -> N
138138

139139
@pytest.mark.asyncio
140140
async def test_heating_schedule_no_params_raises(mock_bsblan: BSBLAN) -> None:
141-
"""Test no schedule params in response raises error."""
141+
"""Test no schedule params in response raises unsupported-feature error."""
142142
mock_bsblan._request = AsyncMock(return_value={}) # type: ignore[method-assign]
143143

144-
with pytest.raises(BSBLANError, match="No heating schedule parameters available"):
144+
with pytest.raises(
145+
BSBLANUnsupportedFeatureError,
146+
match="No heating schedule parameters available",
147+
) as exc_info:
145148
await mock_bsblan.heating_schedule(circuit=1)
149+
150+
# Permanent error must remain catchable as the generic BSBLANError.
151+
assert isinstance(exc_info.value, BSBLANError)

tests/test_hot_water_additional.py

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212

1313
from bsblan import BSBLAN, BSBLANConfig
1414
from bsblan.constants import API_FULL
15-
from bsblan.exceptions import BSBLANError
15+
from bsblan.exceptions import BSBLANError, BSBLANUnsupportedFeatureError
1616
from bsblan.models import HotWaterConfig, HotWaterSchedule
1717
from bsblan.utility import APIValidator
1818
from tests import load_fixture
@@ -253,11 +253,14 @@ async def test_hot_water_schedule_no_params_error(
253253
bsblan._validator._validated_hot_water_groups.add("schedule")
254254

255255
with pytest.raises(
256-
BSBLANError,
256+
BSBLANUnsupportedFeatureError,
257257
match="No hot water schedule parameters available",
258-
):
258+
) as exc_info:
259259
await bsblan.hot_water_schedule()
260260

261+
# Permanent error must remain catchable as the generic BSBLANError.
262+
assert isinstance(exc_info.value, BSBLANError)
263+
261264

262265
@pytest.mark.asyncio
263266
async def test_hot_water_state_no_params_error(
@@ -285,9 +288,12 @@ async def test_hot_water_state_no_params_error(
285288
with pytest.raises(
286289
BSBLANError,
287290
match="No essential hot water parameters available",
288-
):
291+
) as exc_info:
289292
await bsblan.hot_water_state()
290293

294+
# State group must NOT be reclassified as an unsupported-feature error.
295+
assert not isinstance(exc_info.value, BSBLANUnsupportedFeatureError)
296+
291297

292298
@pytest.mark.asyncio
293299
async def test_granular_hot_water_validation(

0 commit comments

Comments
 (0)