Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
80 changes: 31 additions & 49 deletions src/bsblan/bsblan.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,42 +18,24 @@
from yarl import URL

from .constants import (
API_DATA_NOT_INITIALIZED_ERROR_MSG,
API_VALIDATOR_NOT_INITIALIZED_ERROR_MSG,
API_VERSION_ERROR_MSG,
API_VERSIONS,
CIRCUIT_HEATING_SECTIONS,
CIRCUIT_PROBE_PARAMS,
CIRCUIT_STATIC_SECTIONS,
CIRCUIT_STATUS_PARAMS,
CIRCUIT_THERMOSTAT_PARAMS,
DHW_TIME_PROGRAM_PARAMS,
EMPTY_INCLUDE_LIST_ERROR_MSG,
EMPTY_SECTION_PARAMS_ERROR_MSG,
FIRMWARE_VERSION_ERROR_MSG,
HOT_WATER_CONFIG_PARAMS,
HOT_WATER_ESSENTIAL_PARAMS,
HOT_WATER_SCHEDULE_PARAMS,
INACTIVE_CIRCUIT_MARKER,
INVALID_CIRCUIT_ERROR_MSG,
INVALID_INCLUDE_PARAMS_ERROR_MSG,
INVALID_RESPONSE_ERROR_MSG,
MAX_VALID_YEAR,
MIN_VALID_YEAR,
MULTI_PARAMETER_ERROR_MSG,
NO_PARAMETER_IDS_ERROR_MSG,
NO_PARAMETER_NAMES_ERROR_MSG,
NO_SCHEDULE_ERROR_MSG,
NO_STATE_ERROR_MSG,
PARAMETER_NAMES_NOT_RESOLVED_ERROR_MSG,
SECTION_NOT_FOUND_ERROR_MSG,
SESSION_NOT_INITIALIZED_ERROR_MSG,
SETTABLE_HOT_WATER_PARAMS,
TEMPERATURE_RANGE_ERROR_MSG,
VALID_CIRCUITS,
VALID_HVAC_MODES,
VERSION_ERROR_MSG,
APIConfig,
ErrorMsg,
)
from .exceptions import (
BSBLANAuthError,
Expand Down Expand Up @@ -258,7 +240,7 @@ async def _setup_api_validator(self) -> None:
section validation until the data is needed (lazy loading).
"""
if self._api_version is None:
raise BSBLANError(API_VERSION_ERROR_MSG)
raise BSBLANError(ErrorMsg.API_VERSION)

# Initialize API data if not already done
if self._api_data is None:
Expand All @@ -285,7 +267,7 @@ async def _ensure_section_validated(

"""
if not self._api_validator:
raise BSBLANError(API_VALIDATOR_NOT_INITIALIZED_ERROR_MSG)
raise BSBLANError(ErrorMsg.API_VALIDATOR_NOT_INITIALIZED)

# Fast path: skip if already validated (no lock needed)
if self._api_validator.is_section_validated(section):
Expand Down Expand Up @@ -335,10 +317,10 @@ async def _ensure_hot_water_group_validated(
return

if not self._api_validator:
raise BSBLANError(API_VALIDATOR_NOT_INITIALIZED_ERROR_MSG)
raise BSBLANError(ErrorMsg.API_VALIDATOR_NOT_INITIALIZED)

if not self._api_data:
raise BSBLANError(API_DATA_NOT_INITIALIZED_ERROR_MSG)
raise BSBLANError(ErrorMsg.API_DATA_NOT_INITIALIZED)

# Get or create lock for this group
if group_name not in self._hot_water_group_locks:
Expand Down Expand Up @@ -424,7 +406,7 @@ async def _initialize_api_validator(self) -> None:
This method is kept for backwards compatibility.
"""
if self._api_version is None:
raise BSBLANError(API_VERSION_ERROR_MSG)
raise BSBLANError(ErrorMsg.API_VERSION)

# Initialize API data if not already done
if self._api_data is None:
Expand Down Expand Up @@ -468,10 +450,10 @@ async def _validate_api_section(

"""
if not self._api_validator:
raise BSBLANError(API_VALIDATOR_NOT_INITIALIZED_ERROR_MSG)
raise BSBLANError(ErrorMsg.API_VALIDATOR_NOT_INITIALIZED)

if not self._api_data:
raise BSBLANError(API_DATA_NOT_INITIALIZED_ERROR_MSG)
raise BSBLANError(ErrorMsg.API_DATA_NOT_INITIALIZED)

# Assign to local variable after asserting it's not None
api_validator = self._api_validator
Expand All @@ -483,7 +465,7 @@ async def _validate_api_section(
try:
section_data = self._api_data[section]
except KeyError as err:
msg = SECTION_NOT_FOUND_ERROR_MSG.format(section)
msg = ErrorMsg.SECTION_NOT_FOUND.format(section)
raise BSBLANError(msg) from err

# Filter to only included params if specified
Expand Down Expand Up @@ -592,7 +574,7 @@ def _set_api_version(self) -> None:

"""
if not self._firmware_version:
raise BSBLANError(FIRMWARE_VERSION_ERROR_MSG)
raise BSBLANError(ErrorMsg.FIRMWARE_VERSION)

version = pkg_version.parse(self._firmware_version)
if version < pkg_version.parse("1.2.0"):
Expand All @@ -603,7 +585,7 @@ def _set_api_version(self) -> None:
elif version >= pkg_version.parse("3.0.0"):
self._api_version = "v3"
else:
raise BSBLANVersionError(VERSION_ERROR_MSG)
raise BSBLANVersionError(ErrorMsg.VERSION)

async def _fetch_temperature_range(
self,
Expand Down Expand Up @@ -693,7 +675,7 @@ def _validate_circuit(self, circuit: int) -> None:

"""
if circuit not in VALID_CIRCUITS:
msg = INVALID_CIRCUIT_ERROR_MSG.format(circuit)
msg = ErrorMsg.INVALID_CIRCUIT.format(circuit)
raise BSBLANInvalidParameterError(msg)

@property
Expand Down Expand Up @@ -724,7 +706,7 @@ async def _initialize_api_data(self) -> APIConfig:
self._api_data = self._copy_api_config()
logger.debug("API data initialized for version: %s", self._api_version)
if self._api_data is None:
raise BSBLANError(API_DATA_NOT_INITIALIZED_ERROR_MSG)
raise BSBLANError(ErrorMsg.API_DATA_NOT_INITIALIZED)
return self._api_data

def _copy_api_config(self) -> APIConfig:
Expand All @@ -738,7 +720,7 @@ def _copy_api_config(self) -> APIConfig:

"""
if self._api_version is None:
raise BSBLANError(API_VERSION_ERROR_MSG)
raise BSBLANError(ErrorMsg.API_VERSION)
source_config: APIConfig = API_VERSIONS[self._api_version]
return cast(
"APIConfig",
Expand Down Expand Up @@ -814,7 +796,7 @@ async def _request_with_retry(

"""
if self.session is None:
raise BSBLANError(SESSION_NOT_INITIALIZED_ERROR_MSG)
raise BSBLANError(ErrorMsg.SESSION_NOT_INITIALIZED)
url = self._build_url(base_path)
auth = self._get_auth()
headers = self._get_headers()
Expand All @@ -838,7 +820,7 @@ async def _request_with_retry(
raise
except (ValueError, UnicodeDecodeError) as e:
# Handle JSON decode errors and other parsing issues
msg = INVALID_RESPONSE_ERROR_MSG.format(e)
msg = ErrorMsg.INVALID_RESPONSE.format(e)
raise BSBLANError(msg) from e

def _process_response(
Expand Down Expand Up @@ -976,20 +958,20 @@ async def _fetch_section_data(

# Guard: if validation removed all params, the section is not available
if not section_params:
msg = EMPTY_SECTION_PARAMS_ERROR_MSG.format(section)
msg = ErrorMsg.EMPTY_SECTION_PARAMS.format(section)
raise BSBLANError(msg)

# Filter parameters if include list is specified
if include is not None:
if not include:
raise BSBLANError(EMPTY_INCLUDE_LIST_ERROR_MSG)
raise BSBLANError(ErrorMsg.EMPTY_INCLUDE_LIST)
section_params = {
param_id: name
for param_id, name in section_params.items()
if name in include
}
if not section_params:
raise BSBLANError(INVALID_INCLUDE_PARAMS_ERROR_MSG)
raise BSBLANError(ErrorMsg.INVALID_INCLUDE_PARAMS)

params = await self._extract_params_summary(section_params)
data = await self._request(params={"Parameter": params["string_par"]})
Expand Down Expand Up @@ -1171,7 +1153,7 @@ async def thermostat(
self._validate_single_parameter(
target_temperature,
hvac_mode,
error_msg=MULTI_PARAMETER_ERROR_MSG,
error_msg=ErrorMsg.MULTI_PARAMETER,
)

state = await self._prepare_thermostat_state(
Expand Down Expand Up @@ -1247,7 +1229,7 @@ async def _validate_target_temperature(
await self._initialize_temperature_range(circuit)

if self._min_temp is None or self._max_temp is None:
raise BSBLANError(TEMPERATURE_RANGE_ERROR_MSG)
raise BSBLANError(ErrorMsg.TEMPERATURE_RANGE)

min_temp = self._min_temp
max_temp = self._max_temp
Expand All @@ -1261,7 +1243,7 @@ async def _validate_target_temperature(
max_temp = temp_range.get("max")

if min_temp is None or max_temp is None:
raise BSBLANError(TEMPERATURE_RANGE_ERROR_MSG)
raise BSBLANError(ErrorMsg.TEMPERATURE_RANGE)

try:
temp = float(target_temperature)
Expand Down Expand Up @@ -1353,14 +1335,14 @@ async def _fetch_hot_water_data(
# Apply include filter if specified
if include is not None:
if not include:
raise BSBLANError(EMPTY_INCLUDE_LIST_ERROR_MSG)
raise BSBLANError(ErrorMsg.EMPTY_INCLUDE_LIST)
filtered_params = {
param_id: name
for param_id, name in filtered_params.items()
if name in include
}
if not filtered_params:
raise BSBLANError(INVALID_INCLUDE_PARAMS_ERROR_MSG)
raise BSBLANError(ErrorMsg.INVALID_INCLUDE_PARAMS)

if not filtered_params:
raise BSBLANError(error_msg)
Expand Down Expand Up @@ -1529,7 +1511,7 @@ async def set_hot_water(self, params: SetHotWaterParam) -> None:
params.legionella_function_dwelling_time,
params.operating_mode_changeover,
*time_program_params,
error_msg=MULTI_PARAMETER_ERROR_MSG,
error_msg=ErrorMsg.MULTI_PARAMETER,
)

state = self._prepare_hot_water_state(params)
Expand Down Expand Up @@ -1561,7 +1543,7 @@ async def set_hot_water_schedule(self, schedule: DHWSchedule) -> None:

"""
if not schedule.has_any_schedule():
raise BSBLANError(NO_SCHEDULE_ERROR_MSG)
raise BSBLANError(ErrorMsg.NO_SCHEDULE)

# Invert DHW_TIME_PROGRAM_PARAMS to get day_name -> param_id mapping
# Exclude standard_values as it's not a day of the week
Expand Down Expand Up @@ -1611,7 +1593,7 @@ def _prepare_hot_water_state(
state.update({"Parameter": param_id, "Value": value, "Type": "1"})

if not state:
raise BSBLANError(NO_STATE_ERROR_MSG)
raise BSBLANError(ErrorMsg.NO_STATE)
return state

# -------------------------------------------------------------------------
Expand Down Expand Up @@ -1646,7 +1628,7 @@ async def read_parameters(

"""
if not parameter_ids:
raise BSBLANError(NO_PARAMETER_IDS_ERROR_MSG)
raise BSBLANError(ErrorMsg.NO_PARAMETER_IDS)

# Request the parameters from the device
params_string = ",".join(parameter_ids)
Expand Down Expand Up @@ -1744,17 +1726,17 @@ async def read_parameters_by_name(

"""
if not parameter_names:
raise BSBLANError(NO_PARAMETER_NAMES_ERROR_MSG)
raise BSBLANError(ErrorMsg.NO_PARAMETER_NAMES)

if not self._api_data:
raise BSBLANError(API_DATA_NOT_INITIALIZED_ERROR_MSG)
raise BSBLANError(ErrorMsg.API_DATA_NOT_INITIALIZED)

# Resolve names to IDs
name_to_id = self.get_parameter_ids(parameter_names)

if not name_to_id:
unknown_params = ", ".join(parameter_names)
msg = f"{PARAMETER_NAMES_NOT_RESOLVED_ERROR_MSG}: {unknown_params}"
msg = f"{ErrorMsg.PARAMETER_NAMES_NOT_RESOLVED}: {unknown_params}"
raise BSBLANError(msg)

# Fetch parameters by ID
Expand Down
62 changes: 30 additions & 32 deletions src/bsblan/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -463,43 +463,41 @@ def get_hvac_action_category(status_code: int) -> HVACActionCategory:


# Error Messages
NO_STATE_ERROR_MSG: Final[str] = "No state provided."
NO_SCHEDULE_ERROR_MSG: Final[str] = "No schedule provided."
VERSION_ERROR_MSG: Final[str] = "Version not supported"
FIRMWARE_VERSION_ERROR_MSG: Final[str] = "Firmware version not available"
TEMPERATURE_RANGE_ERROR_MSG: Final[str] = "Temperature range not initialized"
API_VERSION_ERROR_MSG: Final[str] = "API version not set"
MULTI_PARAMETER_ERROR_MSG: Final[str] = "Only one parameter can be set at a time"
SESSION_NOT_INITIALIZED_ERROR_MSG: Final[str] = "Session not initialized"
API_DATA_NOT_INITIALIZED_ERROR_MSG: Final[str] = "API data not initialized"
API_VALIDATOR_NOT_INITIALIZED_ERROR_MSG: Final[str] = "API validator not initialized"
SECTION_NOT_FOUND_ERROR_MSG: Final[str] = "Section '{}' not found in API data"
INVALID_CIRCUIT_ERROR_MSG: Final[str] = "Invalid circuit number: {}. Must be 1 or 2."
INVALID_RESPONSE_ERROR_MSG: Final[str] = (
"Invalid response format from BSB-LAN device: {}"
)
EMPTY_SECTION_PARAMS_ERROR_MSG: Final[str] = (
"No valid parameters found for section '{}'. "
"The device may not support this circuit or section."
)
class ErrorMsg:
"""Error message constants for BSBLAN."""

NO_STATE = "No state provided."
NO_SCHEDULE = "No schedule provided."
VERSION = "Version not supported"
FIRMWARE_VERSION = "Firmware version not available"
TEMPERATURE_RANGE = "Temperature range not initialized"
API_VERSION = "API version not set"
MULTI_PARAMETER = "Only one parameter can be set at a time"
SESSION_NOT_INITIALIZED = "Session not initialized"
Comment on lines 465 to +476
Copy link

Copilot AI Apr 10, 2026

Choose a reason for hiding this comment

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

Replacing the module-level *_ERROR_MSG constants with ErrorMsg is a breaking API change for any downstream code importing those names from bsblan.constants. Consider keeping backward-compatible aliases (e.g., NO_STATE_ERROR_MSG = ErrorMsg.NO_STATE, etc.) and deprecating them over at least one release cycle to avoid unexpected runtime ImportErrors.

Copilot uses AI. Check for mistakes.
API_DATA_NOT_INITIALIZED = "API data not initialized"
API_VALIDATOR_NOT_INITIALIZED = "API validator not initialized"
SECTION_NOT_FOUND = "Section '{}' not found in API data"
INVALID_CIRCUIT = "Invalid circuit number: {}. Must be 1 or 2."
INVALID_RESPONSE = "Invalid response format from BSB-LAN device: {}"
EMPTY_SECTION_PARAMS = (
"No valid parameters found for section '{}'. "
"The device may not support this circuit or section."
)
NO_PARAMETER_IDS = "No parameter IDs provided"
NO_PARAMETER_NAMES = "No parameter names provided"
PARAMETER_NAMES_NOT_RESOLVED = "Could not resolve any parameter names"
INVALID_INCLUDE_PARAMS = (
"None of the requested parameters are valid for this section"
)
EMPTY_INCLUDE_LIST = (
"Empty include list provided. Use None to fetch all parameters."
)


# Time validation constants
MIN_VALID_YEAR: Final[int] = 1900 # Reasonable minimum year for BSB-LAN devices
MAX_VALID_YEAR: Final[int] = 2100 # Reasonable maximum year for BSB-LAN devices

# Error messages for low-level parameter access
NO_PARAMETER_IDS_ERROR_MSG: Final[str] = "No parameter IDs provided"
NO_PARAMETER_NAMES_ERROR_MSG: Final[str] = "No parameter names provided"
PARAMETER_NAMES_NOT_RESOLVED_ERROR_MSG: Final[str] = (
"Could not resolve any parameter names"
)
INVALID_INCLUDE_PARAMS_ERROR_MSG: Final[str] = (
"None of the requested parameters are valid for this section"
)
EMPTY_INCLUDE_LIST_ERROR_MSG: Final[str] = (
"Empty include list provided. Use None to fetch all parameters."
)

# Handle both ASCII and Unicode degree symbols
TEMPERATURE_UNITS = {"°C", "°F", "&#176;C", "&#176;F", "&deg;C", "&deg;F"}

Expand Down
4 changes: 2 additions & 2 deletions tests/test_api_initialization.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@
from bsblan import BSBLAN
from bsblan.bsblan import BSBLANConfig
from bsblan.constants import (
API_VERSION_ERROR_MSG,
API_VERSIONS,
ErrorMsg,
)
from bsblan.exceptions import BSBLANError

Expand All @@ -23,7 +23,7 @@ async def test_initialize_api_data_no_api_version() -> None:
bsblan = BSBLAN(config)

# API version is None by default
with pytest.raises(BSBLANError, match=API_VERSION_ERROR_MSG):
with pytest.raises(BSBLANError, match=ErrorMsg.API_VERSION):
await bsblan._initialize_api_data()


Expand Down
Loading
Loading