python-bsblan 6.1.4__py3-none-any.whl → 6.1.6__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
bsblan/_hot_water.py ADDED
@@ -0,0 +1,249 @@
1
+ """Hot water domain logic for BSB-LAN."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import TYPE_CHECKING, Any, TypeVar
6
+
7
+ from .constants import ErrorMsg, HotWaterParams
8
+ from .exceptions import BSBLANError
9
+ from .models import (
10
+ HotWaterConfig,
11
+ HotWaterSchedule,
12
+ HotWaterState,
13
+ )
14
+
15
+ if TYPE_CHECKING:
16
+ from collections.abc import Awaitable, Callable
17
+
18
+ from .models import SetHotWaterParam
19
+
20
+ # TypeVar for hot water data models
21
+ HotWaterDataT = TypeVar(
22
+ "HotWaterDataT", HotWaterState, HotWaterConfig, HotWaterSchedule
23
+ )
24
+
25
+
26
+ class HotWaterManager:
27
+ """Read and write hot water state, configuration, and schedules."""
28
+
29
+ def __init__( # noqa: PLR0913 # pylint: disable=too-many-arguments
30
+ self,
31
+ *,
32
+ ensure_group_validated: Callable[
33
+ [str, set[str], list[str] | None], Awaitable[None]
34
+ ],
35
+ get_param_cache: Callable[[], dict[str, str]],
36
+ apply_include_filter: Callable[
37
+ [dict[str, str], list[str] | None], dict[str, str]
38
+ ],
39
+ request_named_params: Callable[[dict[str, str]], Awaitable[dict[str, Any]]],
40
+ validate_single_parameter: Callable[..., None],
41
+ set_payload: Callable[[str, str], dict[str, Any]],
42
+ set_device_state: Callable[[dict[str, Any]], Awaitable[None]],
43
+ ) -> None:
44
+ """Initialize the hot water manager.
45
+
46
+ Args:
47
+ ensure_group_validated: Validates a hot water param group lazily.
48
+ get_param_cache: Returns the validated hot water param cache.
49
+ apply_include_filter: Filters params by requested names.
50
+ request_named_params: Requests params and maps them to names.
51
+ validate_single_parameter: Ensures only one parameter is set.
52
+ set_payload: Builds a set-parameter payload.
53
+ set_device_state: Sends a set-parameter request to the device.
54
+
55
+ """
56
+ self._ensure_group_validated = ensure_group_validated
57
+ self._get_param_cache = get_param_cache
58
+ self._apply_include_filter = apply_include_filter
59
+ self._request_named_params = request_named_params
60
+ self._validate_single_parameter = validate_single_parameter
61
+ self._set_payload = set_payload
62
+ self._set_device_state = set_device_state
63
+
64
+ async def fetch_data(
65
+ self,
66
+ param_filter: set[str],
67
+ model_class: type[HotWaterDataT],
68
+ error_msg: str,
69
+ group_name: str,
70
+ include: list[str] | None = None,
71
+ ) -> HotWaterDataT:
72
+ """Fetch hot water data for a specific parameter set.
73
+
74
+ This is a generic helper method that fetches hot water parameters
75
+ based on the provided filter and returns the appropriate model.
76
+ It uses granular lazy loading to validate only the specific param group.
77
+
78
+ Args:
79
+ param_filter: Set of parameter IDs to fetch.
80
+ model_class: The dataclass type to deserialize the response into.
81
+ error_msg: Error message if no parameters are available.
82
+ group_name: Name of the param group for lazy validation tracking.
83
+ include: Optional list of parameter names to fetch. If None,
84
+ fetches all parameters in the group.
85
+
86
+ Returns:
87
+ The populated model instance.
88
+
89
+ Raises:
90
+ BSBLANError: If no parameters are available for the filter.
91
+
92
+ """
93
+ # Granular lazy load: validate only this param group on first access
94
+ # Pass include filter so we only validate requested params
95
+ await self._ensure_group_validated(group_name, param_filter, include)
96
+
97
+ # Use cached validated params
98
+ filtered_params = {
99
+ param_id: param_name
100
+ for param_id, param_name in self._get_param_cache().items()
101
+ if param_id in param_filter
102
+ }
103
+
104
+ # Apply include filter if specified
105
+ filtered_params = self._apply_include_filter(filtered_params, include)
106
+
107
+ if not filtered_params:
108
+ raise BSBLANError(error_msg)
109
+
110
+ data = await self._request_named_params(filtered_params)
111
+ return model_class.model_validate(data)
112
+
113
+ async def state(self, include: list[str] | None = None) -> HotWaterState:
114
+ """Get essential hot water state for frequent polling.
115
+
116
+ Args:
117
+ include: Optional list of parameter names to fetch.
118
+
119
+ Returns:
120
+ HotWaterState: Essential hot water state information.
121
+
122
+ """
123
+ return await self.fetch_data(
124
+ param_filter=HotWaterParams.ESSENTIAL,
125
+ model_class=HotWaterState,
126
+ error_msg="No essential hot water parameters available",
127
+ group_name="essential",
128
+ include=include,
129
+ )
130
+
131
+ async def config(self, include: list[str] | None = None) -> HotWaterConfig:
132
+ """Get hot water configuration and advanced settings.
133
+
134
+ Args:
135
+ include: Optional list of parameter names to fetch.
136
+
137
+ Returns:
138
+ HotWaterConfig: Hot water configuration information.
139
+
140
+ """
141
+ return await self.fetch_data(
142
+ param_filter=HotWaterParams.CONFIG,
143
+ model_class=HotWaterConfig,
144
+ error_msg="No hot water configuration parameters available",
145
+ group_name="config",
146
+ include=include,
147
+ )
148
+
149
+ async def schedule(self, include: list[str] | None = None) -> HotWaterSchedule:
150
+ """Get hot water time program schedules.
151
+
152
+ Args:
153
+ include: Optional list of parameter names to fetch.
154
+
155
+ Returns:
156
+ HotWaterSchedule: Hot water schedule information.
157
+
158
+ """
159
+ return await self.fetch_data(
160
+ param_filter=HotWaterParams.SCHEDULE,
161
+ model_class=HotWaterSchedule,
162
+ error_msg="No hot water schedule parameters available",
163
+ group_name="schedule",
164
+ include=include,
165
+ )
166
+
167
+ async def set_hot_water(self, params: SetHotWaterParam) -> None:
168
+ """Change the state of the hot water system through BSB-Lan.
169
+
170
+ Only one parameter should be set at a time (BSB-LAN API limitation).
171
+
172
+ Args:
173
+ params: SetHotWaterParam object containing the parameter to set.
174
+
175
+ Raises:
176
+ BSBLANError: If multiple parameters are set or no parameter is set.
177
+
178
+ """
179
+ # Validate only one parameter is being set
180
+ time_program_params: list[str] = []
181
+ if params.dhw_time_programs:
182
+ programs = params.dhw_time_programs
183
+ time_program_params.extend(
184
+ prog
185
+ for prog in [
186
+ programs.monday,
187
+ programs.tuesday,
188
+ programs.wednesday,
189
+ programs.thursday,
190
+ programs.friday,
191
+ programs.saturday,
192
+ programs.sunday,
193
+ programs.standard_values,
194
+ ]
195
+ if prog
196
+ )
197
+
198
+ self._validate_single_parameter(
199
+ params.nominal_setpoint,
200
+ params.reduced_setpoint,
201
+ params.nominal_setpoint_max,
202
+ params.operating_mode,
203
+ params.eco_mode_selection,
204
+ params.dhw_charging_priority,
205
+ params.legionella_function_setpoint,
206
+ params.legionella_function_periodicity,
207
+ params.legionella_function_day,
208
+ params.legionella_function_time,
209
+ params.legionella_function_dwelling_time,
210
+ params.operating_mode_changeover,
211
+ *time_program_params,
212
+ error_msg=ErrorMsg.MULTI_PARAMETER,
213
+ )
214
+
215
+ state = self.prepare_state(params)
216
+ await self._set_device_state(state)
217
+
218
+ def prepare_state(self, params: SetHotWaterParam) -> dict[str, Any]:
219
+ """Prepare the hot water state for setting.
220
+
221
+ Args:
222
+ params: SetHotWaterParam object containing the parameter to set.
223
+
224
+ Returns:
225
+ dict[str, Any]: The prepared state for the hot water.
226
+
227
+ Raises:
228
+ BSBLANError: If no state is provided.
229
+
230
+ """
231
+ state: dict[str, Any] = {}
232
+
233
+ # Process all mapped parameters using constants
234
+ for param_id, attr_name in HotWaterParams.SETTABLE.items():
235
+ value = getattr(params, attr_name)
236
+ if value is not None:
237
+ state.update(self._set_payload(param_id, str(value)))
238
+
239
+ # Process time programs if provided using constants
240
+ if params.dhw_time_programs:
241
+ for param_id, attr_name in HotWaterParams.TIME_PROGRAMS.items():
242
+ value = getattr(params.dhw_time_programs, attr_name)
243
+ if value is not None:
244
+ state.update(self._set_payload(param_id, value))
245
+
246
+ if not state:
247
+ raise BSBLANError(ErrorMsg.NO_STATE)
248
+
249
+ return state
bsblan/_parameters.py ADDED
@@ -0,0 +1,155 @@
1
+ """Low-level parameter lookup and read helpers for BSB-LAN."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import TYPE_CHECKING, Any, cast
6
+
7
+ from .constants import ErrorMsg
8
+ from .exceptions import BSBLANError
9
+ from .models import EntityInfo
10
+
11
+ if TYPE_CHECKING:
12
+ from collections.abc import Awaitable, Callable
13
+
14
+ from .constants import APIConfig
15
+
16
+
17
+ class ParameterReader:
18
+ """Resolve and read BSB-LAN parameters by ID or name."""
19
+
20
+ def __init__(
21
+ self,
22
+ *,
23
+ request: Callable[..., Awaitable[dict[str, Any]]],
24
+ get_api_data: Callable[[], APIConfig | None],
25
+ ) -> None:
26
+ """Initialize the parameter reader.
27
+
28
+ Args:
29
+ request: Callable performing a client request.
30
+ get_api_data: Callable returning the current API config data.
31
+
32
+ """
33
+ self._request = request
34
+ self._get_api_data = get_api_data
35
+
36
+ async def read_parameters(
37
+ self,
38
+ parameter_ids: list[str],
39
+ ) -> dict[str, EntityInfo]:
40
+ """Read specific parameters by their BSB-LAN parameter IDs.
41
+
42
+ Args:
43
+ parameter_ids: List of BSB-LAN parameter IDs to fetch.
44
+
45
+ Returns:
46
+ dict[str, EntityInfo]: Dictionary mapping parameter IDs to
47
+ EntityInfo objects.
48
+
49
+ Raises:
50
+ BSBLANError: If no parameter IDs are provided or request fails.
51
+
52
+ """
53
+ if not parameter_ids:
54
+ raise BSBLANError(ErrorMsg.NO_PARAMETER_IDS)
55
+
56
+ # Request the parameters from the device
57
+ params_string = ",".join(parameter_ids)
58
+ response_data = await self._request(params={"Parameter": params_string})
59
+
60
+ # Convert response to EntityInfo objects
61
+ result: dict[str, EntityInfo] = {}
62
+ for param_id in parameter_ids:
63
+ if param_id in response_data:
64
+ param_data = response_data[param_id]
65
+ if param_data and isinstance(param_data, dict):
66
+ result[param_id] = EntityInfo.model_validate(param_data)
67
+
68
+ return result
69
+
70
+ def get_parameter_id(self, parameter_name: str) -> str | None:
71
+ """Look up the parameter ID for a given parameter name.
72
+
73
+ Args:
74
+ parameter_name: The parameter name (e.g., "current_temperature").
75
+
76
+ Returns:
77
+ str | None: The parameter ID if found, None otherwise.
78
+
79
+ """
80
+ api_data = self._get_api_data()
81
+ if not api_data:
82
+ return None
83
+
84
+ # Search through all sections for the parameter name
85
+ for section_params in api_data.values():
86
+ section_dict = cast("dict[str, str]", section_params)
87
+ for param_id, param_name in section_dict.items():
88
+ if param_name == parameter_name:
89
+ return param_id
90
+
91
+ return None
92
+
93
+ def get_parameter_ids(self, parameter_names: list[str]) -> dict[str, str]:
94
+ """Look up parameter IDs for multiple parameter names.
95
+
96
+ Args:
97
+ parameter_names: List of parameter names to look up.
98
+
99
+ Returns:
100
+ dict[str, str]: Dictionary mapping parameter names to their IDs.
101
+ Only includes parameters that were found.
102
+
103
+ """
104
+ result: dict[str, str] = {}
105
+ for name in parameter_names:
106
+ param_id = self.get_parameter_id(name)
107
+ if param_id is not None:
108
+ result[name] = param_id
109
+ return result
110
+
111
+ async def read_parameters_by_name(
112
+ self,
113
+ parameter_names: list[str],
114
+ ) -> dict[str, EntityInfo]:
115
+ """Read specific parameters by their names.
116
+
117
+ Args:
118
+ parameter_names: List of parameter names to fetch.
119
+
120
+ Returns:
121
+ dict[str, EntityInfo]: Dictionary mapping parameter names to
122
+ EntityInfo objects. Only includes parameters that were found
123
+ and had valid data.
124
+
125
+ Raises:
126
+ BSBLANError: If no parameter names are provided, no IDs could be
127
+ resolved, or the client is not initialized.
128
+
129
+ """
130
+ if not parameter_names:
131
+ raise BSBLANError(ErrorMsg.NO_PARAMETER_NAMES)
132
+
133
+ if not self._get_api_data():
134
+ raise BSBLANError(ErrorMsg.API_DATA_NOT_INITIALIZED)
135
+
136
+ # Resolve names to IDs
137
+ name_to_id = self.get_parameter_ids(parameter_names)
138
+
139
+ if not name_to_id:
140
+ unknown_params = ", ".join(parameter_names)
141
+ msg = f"{ErrorMsg.PARAMETER_NAMES_NOT_RESOLVED}: {unknown_params}"
142
+ raise BSBLANError(msg)
143
+
144
+ # Fetch parameters by ID
145
+ param_ids = list(name_to_id.values())
146
+ id_results = await self.read_parameters(param_ids)
147
+
148
+ # Convert back to name-keyed dictionary
149
+ # id_to_name maps param_id -> param_name for requested params only
150
+ id_to_name = {v: k for k, v in name_to_id.items()}
151
+ return {
152
+ id_to_name[param_id]: entity_info
153
+ for param_id, entity_info in id_results.items()
154
+ if param_id in id_to_name
155
+ }
bsblan/_schedules.py ADDED
@@ -0,0 +1,148 @@
1
+ """Heating and hot water schedule domain logic for BSB-LAN."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import TYPE_CHECKING, Any
6
+
7
+ from .constants import ErrorMsg, HeatingScheduleParams, HotWaterParams
8
+ from .exceptions import BSBLANError
9
+ from .models import HeatingTimeSwitchPrograms
10
+
11
+ if TYPE_CHECKING:
12
+ from collections.abc import Awaitable, Callable
13
+
14
+ from .models import DaySchedule, DHWSchedule, HeatingSchedule
15
+
16
+
17
+ class ScheduleManager:
18
+ """Read and write heating and hot water time switch programs."""
19
+
20
+ def __init__( # noqa: PLR0913 # pylint: disable=too-many-arguments
21
+ self,
22
+ *,
23
+ request: Callable[..., Awaitable[dict[str, Any]]],
24
+ extract_params_summary: Callable[[dict[Any, Any]], dict[Any, Any]],
25
+ apply_include_filter: Callable[
26
+ [dict[str, str], list[str] | None], dict[str, str]
27
+ ],
28
+ validate_circuit: Callable[[int], None],
29
+ set_payload: Callable[[str, str], dict[str, Any]],
30
+ set_device_state: Callable[[dict[str, Any]], Awaitable[None]],
31
+ ) -> None:
32
+ """Initialize the schedule manager.
33
+
34
+ Args:
35
+ request: Callable performing a client request.
36
+ extract_params_summary: Builds the request parameter summary.
37
+ apply_include_filter: Filters params by requested names.
38
+ validate_circuit: Validates a heating circuit number.
39
+ set_payload: Builds a set-parameter payload.
40
+ set_device_state: Sends a set-parameter request to the device.
41
+
42
+ """
43
+ self._request = request
44
+ self._extract_params_summary = extract_params_summary
45
+ self._apply_include_filter = apply_include_filter
46
+ self._validate_circuit = validate_circuit
47
+ self._set_payload = set_payload
48
+ self._set_device_state = set_device_state
49
+
50
+ async def heating_schedule(
51
+ self,
52
+ include: list[str] | None = None,
53
+ circuit: int = 1,
54
+ ) -> HeatingTimeSwitchPrograms:
55
+ """Get heating time switch programs for a specific circuit.
56
+
57
+ Args:
58
+ include: Optional list of day names to fetch.
59
+ circuit: The heating circuit number (1 or 2). Defaults to 1.
60
+
61
+ Returns:
62
+ HeatingTimeSwitchPrograms: Heating schedule information.
63
+
64
+ Raises:
65
+ BSBLANError: If no schedule parameters are available.
66
+
67
+ """
68
+ self._validate_circuit(circuit)
69
+ time_program_params = HeatingScheduleParams.TIME_PROGRAMS[circuit]
70
+
71
+ filtered_params = self._apply_include_filter(time_program_params, include)
72
+
73
+ params = self._extract_params_summary(filtered_params)
74
+ data = await self._request(params={"Parameter": params["string_par"]})
75
+ mapped_data = {
76
+ name: data[param_id]
77
+ for param_id, name in filtered_params.items()
78
+ if param_id in data
79
+ }
80
+
81
+ if not mapped_data:
82
+ raise BSBLANError(ErrorMsg.NO_HEATING_SCHEDULE_PARAMS)
83
+
84
+ return HeatingTimeSwitchPrograms.model_validate(mapped_data)
85
+
86
+ async def set_heating_schedule(
87
+ self,
88
+ schedule: HeatingSchedule,
89
+ circuit: int = 1,
90
+ ) -> None:
91
+ """Set heating time switch programs for a specific circuit.
92
+
93
+ Args:
94
+ schedule: HeatingSchedule object containing the weekly schedule.
95
+ circuit: The heating circuit number (1 or 2). Defaults to 1.
96
+
97
+ Raises:
98
+ BSBLANError: If no schedule is provided.
99
+
100
+ """
101
+ self._validate_circuit(circuit)
102
+ await self._write_day_schedules(
103
+ schedule,
104
+ HeatingScheduleParams.TIME_PROGRAMS[circuit],
105
+ )
106
+
107
+ async def set_hot_water_schedule(self, schedule: DHWSchedule) -> None:
108
+ """Set hot water time program schedules.
109
+
110
+ Args:
111
+ schedule: DHWSchedule object containing the weekly schedule.
112
+
113
+ Raises:
114
+ BSBLANError: If no schedule is provided.
115
+
116
+ """
117
+ await self._write_day_schedules(schedule, HotWaterParams.TIME_PROGRAMS)
118
+
119
+ async def _write_day_schedules(
120
+ self,
121
+ schedule: HeatingSchedule | DHWSchedule,
122
+ time_programs: dict[str, str],
123
+ ) -> None:
124
+ """Write populated day schedules using one request per day.
125
+
126
+ Args:
127
+ schedule: Weekly schedule object with per-day DaySchedule attrs.
128
+ time_programs: Mapping of parameter IDs to day names.
129
+
130
+ Raises:
131
+ BSBLANError: If no schedule is provided.
132
+
133
+ """
134
+ if not schedule.has_any_schedule():
135
+ raise BSBLANError(ErrorMsg.NO_SCHEDULE)
136
+
137
+ # Invert the param mapping to get day_name -> param_id
138
+ # Exclude standard_values as it's not a day of the week
139
+ day_param_map = {
140
+ v: k for k, v in time_programs.items() if v != "standard_values"
141
+ }
142
+
143
+ for day_name, param_id in day_param_map.items():
144
+ day_schedule: DaySchedule | None = getattr(schedule, day_name)
145
+ if day_schedule is not None:
146
+ await self._set_device_state(
147
+ self._set_payload(param_id, day_schedule.to_bsblan_format()),
148
+ )
bsblan/_thermostat.py ADDED
@@ -0,0 +1,137 @@
1
+ """Thermostat write preparation for BSB-LAN."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import TYPE_CHECKING, Any
6
+
7
+ from .constants import CircuitConfig, Validation
8
+ from .exceptions import BSBLANInvalidParameterError
9
+
10
+ if TYPE_CHECKING:
11
+ from collections.abc import Callable
12
+
13
+ from ._temperature import TemperatureManager
14
+
15
+
16
+ class ThermostatWriter:
17
+ """Prepare and validate thermostat write payloads."""
18
+
19
+ def __init__(
20
+ self,
21
+ *,
22
+ uses_pps_bus: Callable[[], bool],
23
+ temperature: TemperatureManager,
24
+ set_payload: Callable[[str, str], dict[str, Any]],
25
+ ) -> None:
26
+ """Initialize the thermostat writer.
27
+
28
+ Args:
29
+ uses_pps_bus: Returns whether the device uses the PPS bus.
30
+ temperature: Temperature manager for setpoint validation.
31
+ set_payload: Builds a set-parameter payload.
32
+
33
+ """
34
+ self._uses_pps_bus = uses_pps_bus
35
+ self._temperature = temperature
36
+ self._set_payload = set_payload
37
+
38
+ async def prepare_state( # pylint: disable=too-many-arguments
39
+ self,
40
+ target_temperature: str | None,
41
+ hvac_mode: int | None,
42
+ circuit: int = 1,
43
+ target_temperature_high: str | float | None = None,
44
+ *,
45
+ cooling_operating_mode: int | None = None,
46
+ ) -> dict[str, Any]:
47
+ """Prepare the thermostat state for setting.
48
+
49
+ Args:
50
+ target_temperature (str | None): The target temperature to set.
51
+ hvac_mode (int | None): The HVAC mode to set as raw integer.
52
+ circuit: The heating circuit number (1 or 2).
53
+ target_temperature_high: The cooling comfort setpoint to set.
54
+ cooling_operating_mode: The cooling circuit operating mode to set
55
+ as raw integer.
56
+
57
+ Returns:
58
+ dict[str, Any]: The prepared state for the thermostat.
59
+
60
+ """
61
+ param_ids = self.thermostat_params(circuit)
62
+ state: dict[str, Any] = {}
63
+ if target_temperature is not None:
64
+ await self._temperature.validate_target_temperature(
65
+ target_temperature,
66
+ circuit,
67
+ )
68
+ state.update(
69
+ self._set_payload(param_ids["target_temperature"], target_temperature),
70
+ )
71
+ if target_temperature_high is not None:
72
+ param_id = param_ids.get("target_temperature_high")
73
+ if param_id is None:
74
+ parameter_name = "target_temperature_high"
75
+ raise BSBLANInvalidParameterError(parameter_name)
76
+ await self._temperature.validate_target_temperature_high(
77
+ target_temperature_high,
78
+ circuit,
79
+ )
80
+ state.update(
81
+ self._set_payload(param_id, str(target_temperature_high)),
82
+ )
83
+ if hvac_mode is not None:
84
+ self.validate_hvac_mode(hvac_mode)
85
+ hvac_value = str(hvac_mode)
86
+ if self._uses_pps_bus():
87
+ hvac_value = Validation.PPS_HVAC_MODE_TO_BSBLAN[hvac_mode]
88
+ state.update(
89
+ self._set_payload(param_ids["hvac_mode"], hvac_value),
90
+ )
91
+ if cooling_operating_mode is not None:
92
+ param_id = param_ids.get("cooling_operating_mode")
93
+ if param_id is None:
94
+ parameter_name = "cooling_operating_mode"
95
+ raise BSBLANInvalidParameterError(parameter_name)
96
+ self.validate_cooling_operating_mode(cooling_operating_mode)
97
+ state.update(
98
+ self._set_payload(param_id, str(cooling_operating_mode)),
99
+ )
100
+ return state
101
+
102
+ def thermostat_params(self, circuit: int) -> dict[str, str]:
103
+ """Return thermostat write parameters for the active bus type."""
104
+ if self._uses_pps_bus():
105
+ return {"target_temperature": "15004", "hvac_mode": "15000"}
106
+ return CircuitConfig.THERMOSTAT_PARAMS[circuit]
107
+
108
+ def validate_hvac_mode(self, hvac_mode: int) -> None:
109
+ """Validate the HVAC mode.
110
+
111
+ Args:
112
+ hvac_mode (int): The HVAC mode to validate. BSB/LPB accepts 0-3;
113
+ PPS accepts 0, 1, and 3.
114
+
115
+ Raises:
116
+ BSBLANInvalidParameterError: If the HVAC mode is invalid.
117
+
118
+ """
119
+ valid_modes = (
120
+ Validation.PPS_HVAC_MODES if self._uses_pps_bus() else Validation.HVAC_MODES
121
+ )
122
+ if hvac_mode not in valid_modes:
123
+ raise BSBLANInvalidParameterError(str(hvac_mode))
124
+
125
+ def validate_cooling_operating_mode(self, cooling_operating_mode: int) -> None:
126
+ """Validate the cooling circuit operating mode.
127
+
128
+ Args:
129
+ cooling_operating_mode (int): The mode to validate. Valid values
130
+ are 0=Protection, 1=Automatic, 2=Reduced, 3=Comfort.
131
+
132
+ Raises:
133
+ BSBLANInvalidParameterError: If the mode is invalid.
134
+
135
+ """
136
+ if cooling_operating_mode not in Validation.COOLING_OPERATING_MODES:
137
+ raise BSBLANInvalidParameterError(str(cooling_operating_mode))