diff --git a/.pylintrc b/.pylintrc index 1026a9b..ef4856c 100644 --- a/.pylintrc +++ b/.pylintrc @@ -172,7 +172,7 @@ ignored-parents= max-args=8 # Maximum number of attributes for a class (see R0902). -max-attributes=10 +max-attributes=13 # Maximum number of boolean expressions in an if statement (see R0916). max-bool-expr=5 diff --git a/custom_components/compit/api.py b/custom_components/compit/api.py index ca5c491..8ffbe64 100644 --- a/custom_components/compit/api.py +++ b/custom_components/compit/api.py @@ -1,12 +1,14 @@ -import logging import asyncio +import logging from typing import Any -from .types.DeviceState import DeviceState -from .types.SystemInfo import SystemInfo -from .const import API_URL + import aiohttp import async_timeout +from .const import API_URL +from .types.DeviceState import DeviceState +from .types.SystemInfo import SystemInfo + TIMEOUT = 10 _LOGGER: logging.Logger = logging.getLogger(__package__) HEADERS = {"Content-type": "application/json; charset=UTF-8"} @@ -20,6 +22,18 @@ def __init__(self, email, password, session: aiohttp.ClientSession): self._api_wrapper = ApiWrapper(session) async def authenticate(self): + """ + Handles user authentication asynchronously by interacting with an API endpoint. + + Raises: + Exception: Captures and logs any exception encountered during the + authentication process. + + Returns: + SystemInfo | bool: Returns a SystemInfo object created from the successful + response data, or False in case of an error or failure during the + authentication process. + """ try: response = await self._api_wrapper.post( f"{API_URL}/authorize", @@ -55,6 +69,22 @@ async def authenticate(self): return False async def get_gates(self): + """ + Retrieves the gates information asynchronously. + + This method interacts with an external API to fetch the information about + available gates. In case of an error during the fetching process, it logs + the exception and returns False. + + Raises: + Exception: General exception raised during API communication or data + transformation. + + Returns: + SystemInfo: A SystemInfo object populated with data fetched from the + API if successful. + bool: Returns False when an error occurs. + """ try: response = await self._api_wrapper.get(f"{API_URL}/gates", {}, self.token) @@ -64,6 +94,27 @@ async def get_gates(self): return False async def get_state(self, device_id: int): + """ + Fetches the state of a device using its unique device identifier asynchronously. + + This method interacts with an external API to retrieve the current state of + the specified device. It uses an internal API wrapper to perform the HTTP GET + request to the desired endpoint and processes the response to return a + DeviceState object representing the device's state. In case of an error, it + logs the exception and returns False. + + Args: + device_id (int): The unique identifier of the device for which the state + is to be fetched. + + Returns: + DeviceState | bool: A DeviceState object parsed from the API response if + successful, otherwise False. + + Raises: + Any exception occurring during the API call or response processing will + be logged and returned as False without being re-raised. + """ try: response = await self._api_wrapper.get( f"{API_URL}/devices/{device_id}/state", {}, self.token @@ -76,12 +127,28 @@ async def get_state(self, device_id: int): return False async def update_device_parameter( - self, device_id: int, parameter: str, value: str | int + self, device_id: int, parameter: str, value: str | int ): - try: - print(f"Set {parameter} to {value} for device {device_id}") - _LOGGER.info(f"Set {parameter} to {value} for device {device_id}") + """ + Updates a device parameter by sending a request to the device API. + This method allows updating the configuration parameter of a specific device + with the given value. It logs the request and processes the response from the + API upon completion. If the operation fails, it logs the error and returns + False. + + Parameters: + device_id (int): The unique identifier of the device. + parameter (str): The name of the parameter to be updated. + value (str | int): The new value to set for the specified parameter. + + Returns: + bool: Returns a boolean indicating the success of the operation. If the + operation is successful, it returns the result of the API response, else + returns False. + """ + try: + _LOGGER.info("Set %s to %s for device %s", parameter, value, device_id) data = {"values": [{"code": parameter, "value": value}]} response = await self._api_wrapper.put( @@ -94,8 +161,27 @@ async def update_device_parameter( return False async def get_result( - self, response: aiohttp.ClientResponse, ignore_response_code: bool = False + self, response: aiohttp.ClientResponse, ignore_response_code: bool = False ) -> Any: + """ + Asynchronously retrieves and processes the JSON response from an aiohttp.ClientResponse + object. Allows for optional ignoring of response status codes. + + Parameters: + response: aiohttp.ClientResponse + The HTTP response object received from the aiohttp request. + ignore_response_code: bool, optional + A boolean indicating whether to ignore the response status code. Defaults to False. + + Returns: + Any + The JSON-decoded response content. + + Raises: + Exception + If the response status code is not successful (non-2xx) and ignore_response_code + is False. + """ if response.ok or ignore_response_code: return await response.json() @@ -109,18 +195,24 @@ def __init__(self, session: aiohttp.ClientSession): self._session = session async def get( - self, url: str, headers: dict = {}, auth: Any = None + self, url: str, headers=None, auth: Any = None ) -> aiohttp.ClientResponse: """Run http GET method""" + if headers is None: + headers = {} if auth: headers["Authorization"] = auth return await self.api_wrapper("get", url, headers=headers, auth=None) async def post( - self, url: str, data: dict = {}, headers: dict = {}, auth: Any = None + self, url: str, data=None, headers=None, auth: Any = None ) -> aiohttp.ClientResponse: """Run http POST method""" + if headers is None: + headers = {} + if data is None: + data = {} if auth: headers["Authorization"] = auth @@ -129,39 +221,53 @@ async def post( ) async def put( - self, url: str, data: dict = {}, headers: dict = {}, auth: Any = None + self, url: str, data=None, headers=None, auth: Any = None ) -> aiohttp.ClientResponse: """Run http PUT method""" + if headers is None: + headers = {} + if data is None: + data = {} if auth: headers["Authorization"] = auth return await self.api_wrapper("put", url, data=data, headers=headers, auth=None) async def api_wrapper( - self, - method: str, - url: str, - data: dict = {}, - headers: dict = {}, - auth: Any = None, + self, + method: str, + url: str, + data: dict = None, + headers: dict = None, + auth: Any = None, ) -> Any: """Get information from the API.""" + # Use None as default and create a new dict if needed + if data is None: + data = {} + if headers is None: + headers = {} + try: async with async_timeout.timeout(TIMEOUT): - if method == "get": + if method.lower() == "get": response = await self._session.get(url, headers=headers, auth=auth) return response - - elif method == "post": + elif method.lower() == "post": response = await self._session.post( - url, headers=headers, data=data, auth=auth + url, + headers=headers, + json=data, + auth=auth, # Use JSON for consistency ) return response - elif method == "put": + elif method.lower() == "put": response = await self._session.put( url, headers=headers, json=data, auth=auth ) return response + else: + raise ValueError(f"Unsupported HTTP method: {method}") except asyncio.TimeoutError as exception: _LOGGER.error( @@ -169,3 +275,11 @@ async def api_wrapper( url, exception, ) + raise # Re-raise the exception instead of returning None + except Exception as exception: + _LOGGER.error( + "Error fetching information from %s - %s", + url, + exception, + ) + raise # Re-raise the exception diff --git a/custom_components/compit/climate.py b/custom_components/compit/climate.py index dfbc0e6..aceb614 100644 --- a/custom_components/compit/climate.py +++ b/custom_components/compit/climate.py @@ -1,20 +1,38 @@ import logging from typing import List -from homeassistant.helpers.update_coordinator import CoordinatorEntity + from homeassistant.components.climate import ClimateEntity +from homeassistant.components.climate.const import ClimateEntityFeature, HVACMode +from homeassistant.const import UnitOfTemperature from homeassistant.core import HomeAssistant +from homeassistant.helpers.update_coordinator import CoordinatorEntity +from .const import DOMAIN, MANURFACER_NAME +from .coordinator import CompitDataUpdateCoordinator from .types.DeviceDefinitions import Parameter from .types.SystemInfo import Device -from .coordinator import CompitDataUpdateCoordinator -from homeassistant.components.climate.const import ClimateEntityFeature, HVACMode -from homeassistant.const import ATTR_TEMPERATURE, UnitOfTemperature -from .const import DOMAIN, MANURFACER_NAME _LOGGER: logging.Logger = logging.getLogger(__package__) async def async_setup_entry(hass: HomeAssistant, entry, async_add_devices): + """ + Sets up the asynchronous platform entry for CompitClimate devices. + + This function initializes and adds CompitClimate devices to the platform by iterating + over the devices provided by the coordinator. It filters and maps the necessary device + data and definitions required for the platform. + + Parameters: + hass (HomeAssistant): The HomeAssistant instance. + entry: The configuration entry to set up. + async_add_devices (Callable[[List[CompitClimate]], Awaitable[None]]): Asynchronous + function to add the CompitClimate devices. + + Raises: + Exception: Raised if any internal error occurs during the setup. + + """ coordinator: CompitDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] async_add_devices( @@ -53,6 +71,9 @@ def __init__( device_name: str, ): super().__init__(coordinator) + self._hvac_mode = None + self._fan_mode = None + self._preset_mode = None self.coordinator = coordinator self.unique_id = f"{device.label}_climate" self.label = f"{device.label} climate" @@ -69,6 +90,16 @@ def __init__( self.set_initial_values() def set_initial_values(self): + """ + Sets the initial values for thermostat mode, fan mode, and HVAC (Heating, Ventilation, and Air Conditioning) + mode based on the current parameters provided by the coordinator. It retrieves each relevant parameter + and determines the appropriate current mode, such as thermostat preset mode, fan operating mode, + and HVAC operation. + + Raises: + KeyError: If a required key is missing in the state or its parameters. + + """ preset_mode = self.coordinator.data[self.device.id].state.get_parameter_value( "__trybpracytermostatu" ) diff --git a/custom_components/compit/config_flow.py b/custom_components/compit/config_flow.py index f1b7b02..410a57c 100644 --- a/custom_components/compit/config_flow.py +++ b/custom_components/compit/config_flow.py @@ -1,16 +1,41 @@ +import voluptuous as vol from homeassistant import config_entries -from .const import DOMAIN -from .api import CompitAPI from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers.aiohttp_client import async_create_clientsession -import voluptuous as vol +from .api import CompitAPI +from .const import DOMAIN -class CompitConfigFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): +class CompitConfigFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): VERSION = 1 + def __init__(self): + self.data_schema = None + async def async_step_user(self, user_input=None): + """ + Handles the user step of the configuration flow for setting up the Compit integration. + + This asynchronous method manages the user input form for authentication. It validates + the credentials provided by the user by communicating with the Compit API and ensures + that the entered data is correct. If the authentication is successful, an entry is + created for the integration; otherwise, the appropriate error message is displayed + to the user. + + Parameters: + user_input: The dictionary containing the user-provided input. Expected fields are + 'email' and 'password'. If None, the method will return the initial user input + form. + + Returns: + Returns either the configuration entry created upon successful authentication or + a form displaying errors if the authentication fails. + + Raises: + KeyError: Raised if user_input lacks required keys ('email' or 'password'), + although validation prevents this in practice. + """ errors = {} if user_input is not None: @@ -36,6 +61,20 @@ async def async_step_user(self, user_input=None): class CompitOptionsFlowHandler(config_entries.OptionsFlow): + """ + Handles the option flow for the Compit integration. + + This class defines how the user can modify configuration options in the entry + through the Home Assistant UI. It provides steps for user interaction and processes + the provided input to update the configuration. + + Attributes: + config_entry: The configuration entry for the integration. + + Methods: + async_step_init: Handles the initial step of the options flow, which allows the + user to either provide input to modify options or display a form for input. + """ def __init__(self, config_entry): self.config_entry = config_entry diff --git a/custom_components/compit/coordinator.py b/custom_components/compit/coordinator.py index ae42ed3..f23d545 100644 --- a/custom_components/compit/coordinator.py +++ b/custom_components/compit/coordinator.py @@ -1,14 +1,15 @@ -from typing import Any, List -from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed -from homeassistant.core import Config, HomeAssistant import logging from datetime import timedelta +from typing import Any, List, Dict, Tuple, Optional + +from homeassistant.core import HomeAssistant +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed -from .types.SystemInfo import Gate -from .types.DeviceDefinitions import DeviceDefinitions -from .types.DeviceState import DeviceInstance from .api import CompitAPI from .const import DOMAIN +from .types.DeviceDefinitions import DeviceDefinitions, Device +from .types.DeviceState import DeviceInstance +from .types.SystemInfo import Gate SCAN_INTERVAL = timedelta(minutes=1) _LOGGER: logging.Logger = logging.getLogger(__package__) @@ -18,45 +19,73 @@ class CompitDataUpdateCoordinator(DataUpdateCoordinator[dict[Any, DeviceInstance """Class to manage fetching data from the API.""" def __init__( - self, - hass: HomeAssistant, - gates: List[Gate], - api: CompitAPI, - device_definitions: DeviceDefinitions, + self, + hass: HomeAssistant, + gates: List[Gate], + api: CompitAPI, + device_definitions: DeviceDefinitions, ) -> None: """Initialize.""" self.devices: dict[Any, DeviceInstance] = {} self.api = api - self.platforms = [] self.gates = gates self.device_definitions = device_definitions - + self.platforms = [] # Initialize platforms list + # Build an index for fast device definition lookup: key = (class, code/type) + self._definitions_by_key: Dict[Tuple[int, int], Device] = ( + self._build_definitions_index(device_definitions) + ) super().__init__(hass, _LOGGER, name=DOMAIN, update_interval=SCAN_INTERVAL) + @staticmethod + def _build_definitions_index( + definitions: DeviceDefinitions, + ) -> Dict[Tuple[int, int], Device]: + """Create an index for device definitions keyed by (class, code).""" + index: Dict[Tuple[int, int], Device] = {} + for d in definitions.devices: + index[(d._class, d.code)] = d + return index + + def _find_definition(self, class_id: int, type_code: int) -> Optional[Device]: + """Find a device definition by class and type/code.""" + return self._definitions_by_key.get((class_id, type_code)) + + def _get_or_create_device_instance(self, device) -> DeviceInstance: + """Return an existing DeviceInstance or create one using its definition.""" + dev_id = device.id + instance = self.devices.get(dev_id) + if instance is not None: + return instance + + definition = self._find_definition(device.class_, device.type) + if definition is None: + raise UpdateFailed( + f"Missing device definition for device id={dev_id}, class={device.class_}, type={device.type}" + ) + + instance = DeviceInstance(definition) + self.devices[dev_id] = instance + return instance + async def _async_update_data(self) -> dict[Any, DeviceInstance]: """Update data via library.""" try: for gate in self.gates: - print(f"Bramka: {gate.label}, Kod: {gate.code}") + _LOGGER.info("Bramka: %s, Kod: %s", gate.label, gate.code) for device in gate.devices: - if device.id not in self.devices: - self.devices[device.id] = DeviceInstance( - next( - filter( - lambda item: item._class == device.class_ - and item.code == device.type, - self.device_definitions.devices, - ), - None, - ) - ) - - print( - f" Urządzenie: {device.label}, ID: {device.id}, Klasa: {device.class_}, Typ: {device.type}" + instance = self._get_or_create_device_instance(device) + + _LOGGER.info( + " Urządzenie: %s, ID: %s, Klasa: %s, Typ: %s", + device.label, + device.id, + device.class_, + device.type, ) state = await self.api.get_state(device.id) - self.devices[device.id].state = state + instance.state = state return self.devices except Exception as exception: - raise UpdateFailed() from exception + raise UpdateFailed(f"Update failed: {exception}") from exception diff --git a/custom_components/compit/number.py b/custom_components/compit/number.py index f1523f6..4d03c12 100644 --- a/custom_components/compit/number.py +++ b/custom_components/compit/number.py @@ -1,25 +1,35 @@ import logging -from homeassistant.const import Platform -from homeassistant.helpers.update_coordinator import CoordinatorEntity + from homeassistant.components.number import NumberEntity +from homeassistant.const import Platform from homeassistant.core import HomeAssistant +from homeassistant.helpers.update_coordinator import CoordinatorEntity +from .const import DOMAIN, MANURFACER_NAME +from .coordinator import CompitDataUpdateCoordinator from .sensor_matcher import SensorMatcher - from .types.DeviceDefinitions import Parameter -from .types.DeviceState import DeviceInstance, DeviceState -from .types.SystemInfo import Device, Gate -from .coordinator import CompitDataUpdateCoordinator - -from .const import DOMAIN, MANURFACER_NAME +from .types.SystemInfo import Device _LOGGER: logging.Logger = logging.getLogger(__package__) async def async_setup_entry(hass: HomeAssistant, entry, async_add_devices): - + """ + Set up number entities for the Compit integration from a configuration entry. + + This function is responsible for initializing and adding number entities to + Home Assistant based on the provided configuration entry and the data retrieved + from the `CompitDataUpdateCoordinator`. It dynamically determines which devices + and parameters should be represented as number entities by matching their platform. + Only valid devices and parameters that match `Platform.NUMBER` are added. + + Args: + hass (HomeAssistant): The Home Assistant instance. + entry: Configuration entry for the integration. + async_add_devices: A callback function to add device entities to Home Assistant. + """ coordinator: CompitDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] - coordinator.device_definitions.devices async_add_devices( [ CompitNumber(coordinator, device, parameter, device_definition.name) diff --git a/custom_components/compit/select.py b/custom_components/compit/select.py index e52afa1..e301e92 100644 --- a/custom_components/compit/select.py +++ b/custom_components/compit/select.py @@ -1,22 +1,40 @@ import logging -from homeassistant.const import Platform -from homeassistant.helpers.update_coordinator import CoordinatorEntity + from homeassistant.components.select import SelectEntity +from homeassistant.const import Platform from homeassistant.core import HomeAssistant -from .sensor_matcher import SensorMatcher -from .types.DeviceDefinitions import Parameter -from .types.DeviceState import DeviceInstance, DeviceState -from .types.SystemInfo import Device, Gate -from .coordinator import CompitDataUpdateCoordinator +from homeassistant.helpers.update_coordinator import CoordinatorEntity from .const import DOMAIN, MANURFACER_NAME +from .coordinator import CompitDataUpdateCoordinator +from .sensor_matcher import SensorMatcher +from .types.DeviceDefinitions import Parameter +from .types.SystemInfo import Device _LOGGER: logging.Logger = logging.getLogger(__package__) async def async_setup_entry(hass: HomeAssistant, entry, async_add_devices): + """ + Sets up `CompitSelect` entities based on configuration entry. + + This function initializes and adds the `CompitSelect` devices to the home + assistant platform. It processes the coordinator's gates, devices, device + definitions, and their parameters to determine which devices should be created + as selection entities based on the platform determined by the `SensorMatcher`. + + Parameters: + entry + The configuration entry used for setting up the component in Home Assistant. + hass : HomeAssistant + The Home Assistant instance that loads the component. + async_add_devices + A callable to register new devices/entities with Home Assistant. + + Raises: + No specific exceptions are documented for this function. + """ coordinator: CompitDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] - coordinator.device_definitions.devices async_add_devices( [ CompitSelect(coordinator, device, parameter, device_definition.name) @@ -99,8 +117,7 @@ def state(self): ) if parameter is not None: return parameter.description - else: - return self._value.description + return self._value.description return None diff --git a/custom_components/compit/sensor.py b/custom_components/compit/sensor.py index bb9b116..322e866 100644 --- a/custom_components/compit/sensor.py +++ b/custom_components/compit/sensor.py @@ -1,18 +1,30 @@ -from homeassistant.const import Platform from homeassistant.components.sensor import SensorEntity -from homeassistant.helpers.update_coordinator import CoordinatorEntity +from homeassistant.const import Platform from homeassistant.core import HomeAssistant +from homeassistant.helpers.update_coordinator import CoordinatorEntity + +from .const import DOMAIN, MANURFACER_NAME +from .coordinator import CompitDataUpdateCoordinator from .sensor_matcher import SensorMatcher from .types.DeviceDefinitions import Parameter from .types.SystemInfo import Device -from .coordinator import CompitDataUpdateCoordinator - -from .const import DOMAIN, MANURFACER_NAME async def async_setup_entry(hass: HomeAssistant, entry, async_add_devices): + """ + Sets up a sensor platform for a given configuration entry. + + This function initializes and adds sensor devices based on the configuration + entry and the corresponding device data. It retrieves device definitions, + matches devices and their parameters to the sensor platform, and dynamically + creates sensors. + + Args: + hass (HomeAssistant): The Home Assistant instance. + entry: The configuration entry providing details for setup. + async_add_devices: Function to add discovered devices asynchronously. + """ coordinator: CompitDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] - coordinator.device_definitions.devices async_add_devices( [ CompitSensor(coordinator, device, parameter, device_definition.name) diff --git a/custom_components/compit/sensor_matcher.py b/custom_components/compit/sensor_matcher.py index 5c70948..313887d 100644 --- a/custom_components/compit/sensor_matcher.py +++ b/custom_components/compit/sensor_matcher.py @@ -1,12 +1,31 @@ from homeassistant.const import Platform -from .types.DeviceState import Param from .types.DeviceDefinitions import Parameter +from .types.DeviceState import Param class SensorMatcher: @staticmethod def get_platform(paramater: Parameter, value: Param) -> Platform | None: + """ + Determines the platform type for a given parameter and its associated value. + + This method inspects the properties of the provided parameter and its value + to identify the appropriate platform type, such as sensor, number, or selection. + If no valid platform type is derived or the conditions for determination + are unmet, it returns None. + + Args: + paramater (Parameter): The parameter object containing information + regarding the type, range, and details. + value (Param): The value associated with the parameter which includes + its visibility status. + + Returns: + Platform | None: A platform description based on the provided parameter + and value, or None if the conditions are not sufficient to determine + a platform. + """ if value is None or value.hidden: return None if paramater.readWrite == "R": diff --git a/custom_components/compit/switch.py b/custom_components/compit/switch.py index 181cd02..463c2b1 100644 --- a/custom_components/compit/switch.py +++ b/custom_components/compit/switch.py @@ -1,22 +1,35 @@ import logging -from homeassistant.const import Platform -from homeassistant.helpers.update_coordinator import CoordinatorEntity + from homeassistant.components.switch import SwitchEntity +from homeassistant.const import Platform from homeassistant.core import HomeAssistant -from .sensor_matcher import SensorMatcher -from .types.DeviceDefinitions import Parameter -from .types.DeviceState import DeviceInstance, DeviceState -from .types.SystemInfo import Device, Gate -from .coordinator import CompitDataUpdateCoordinator +from homeassistant.helpers.update_coordinator import CoordinatorEntity from .const import DOMAIN +from .coordinator import CompitDataUpdateCoordinator +from .sensor_matcher import SensorMatcher +from .types.DeviceDefinitions import Parameter +from .types.SystemInfo import Device _LOGGER: logging.Logger = logging.getLogger(__package__) async def async_setup_entry(hass: HomeAssistant, entry, async_add_devices): + """ + Sets up the switch platform for a specific entry in Home Assistant. + + This function initializes and adds switch devices dynamically based on the + provided entry, using the data from the specified coordinator object. The + devices are filtered according to their type, platform compatibility, and available + parameters. + + Args: + hass (HomeAssistant): The Home Assistant core object. + entry: The configuration entry for the integration. + async_add_devices: Callback function to add devices to Home Assistant. + + """ coordinator: CompitDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] - coordinator.device_definitions.devices async_add_devices( [ CompitSwitch(coordinator, device, parameter, device_definition.name) @@ -53,26 +66,50 @@ def __init__( ): super().__init__(coordinator) self.coordinator = coordinator - self.unique_id = f"select_{device.label}{parameter.parameter_code}" + # Use a "switch_" prefix for clarity + self.unique_id = f"switch_{device.label}{parameter.parameter_code}" self.label = f"{device.label} {parameter.label}" self.parameter = parameter self.device = device self.device_name = device_name - value = self.coordinator.data[self.device.id].state.get_parameter_value( - self.parameter + + # Initialize boolean state + self._is_on: bool = False + + # Safely read current value from coordinator + data_entry = ( + self.coordinator.data.get(self.device.id) + if hasattr(self.coordinator, "data") + else None + ) + state_obj = ( + getattr(data_entry, "state", None) if data_entry is not None else None ) - self._value = 0 - if value is not None: - parameter = next( - ( - detail - for detail in self.parameter.details - if detail.param == value.value_code - ), - None, - ) - if parameter is not None: - self._value = parameter + + # If a state is already a boolean, use it directly + if isinstance(state_obj, bool): + self._is_on = state_obj + # If a state has get_parameter_value, resolve the parameter + elif hasattr(state_obj, "get_parameter_value"): + value = state_obj.get_parameter_value(self.parameter) + if value is not None: + # Prefer numeric/boolean value when present + raw_val = getattr(value, "value", None) + if raw_val is not None: + # Coerce to boolean + try: + self._is_on = bool(int(raw_val)) # handles "0"/"1"/0/1 + except Exception: + self._is_on = bool(raw_val) + else: + # Fall back to matching by value_code against parameter details + vcode = getattr(value, "value_code", None) + details = self.parameter.details or [] + matched = next( + (d for d in details if getattr(d, "param", None) == vcode), None + ) + if matched is not None and hasattr(matched, "state"): + self._is_on = bool(getattr(matched, "state")) @property def device_info(self): @@ -90,20 +127,18 @@ def name(self): @property def is_on(self): - return self._value + return self._is_on @property def extra_state_attributes(self): - items = [] - - items.append( + items = [ { "device": self.device.label, "device_id": self.device.id, "device_class": self.device.class_, "device_type": self.device.type, } - ) + ] return { "details": items, @@ -111,28 +146,24 @@ def extra_state_attributes(self): async def async_turn_on(self, **kwargs): try: - if ( - await self.coordinator.api.update_device_parameter( - self.device.id, self.parameter.parameter_code, 1 - ) - != False - ): + ok = await self.coordinator.api.update_device_parameter( + self.device.id, self.parameter.parameter_code, 1 + ) + if not ok: await self.coordinator.async_request_refresh() - self._value = 1 + self._is_on = True self.async_write_ha_state() except Exception as e: _LOGGER.error(e) async def async_turn_off(self, **kwargs): try: - if ( - await self.coordinator.api.update_device_parameter( - self.device.id, self.parameter.parameter_code, 0 - ) - != False - ): + ok = await self.coordinator.api.update_device_parameter( + self.device.id, self.parameter.parameter_code, 0 + ) + if not ok: await self.coordinator.async_request_refresh() - self._value = 0 + self._is_on = False self.async_write_ha_state() except Exception as e: _LOGGER.error(e) diff --git a/custom_components/compit/types/DeviceDefinitions.py b/custom_components/compit/types/DeviceDefinitions.py index af1a47e..11ad0c9 100644 --- a/custom_components/compit/types/DeviceDefinitions.py +++ b/custom_components/compit/types/DeviceDefinitions.py @@ -10,14 +10,14 @@ def __init__(self, State: int, Description: str, Param: str): class Parameter: def __init__( - self, - ParameterCode: str, - Label: str, - ReadWrite: str = "R", - Details: Optional[List[ParameterDetails]] = None, - MinValue: Optional[float] = None, - MaxValue: Optional[float] = None, - Unit: Optional[str] = None, + self, + ParameterCode: str, + Label: str, + ReadWrite: str = "R", + Details: Optional[List[ParameterDetails]] = None, + MinValue: Optional[float] = None, + MaxValue: Optional[float] = None, + Unit: Optional[str] = None, ): self.parameter_code = ParameterCode self.label = Label @@ -34,12 +34,12 @@ def __init__( class Device: def __init__( - self, - name: str, - parameters: List[Parameter], - code: int, - _class: int, - id: Optional[int], + self, + name: str, + parameters: List[Parameter], + code: int, + _class: int, + id: Optional[int], ): self.name = name self.parameters = parameters