From b67af176c20f4f01aa2176c9837e45ca6aee6390 Mon Sep 17 00:00:00 2001 From: maczet Date: Mon, 29 Sep 2025 12:27:21 +0200 Subject: [PATCH] fixed to last HA --- custom_components/compit/__init__.py | 13 +- custom_components/compit/api.py | 22 +-- custom_components/compit/coordinator.py | 22 ++- custom_components/compit/switch.py | 165 ++++++++++-------- .../compit/types/DeviceDefinitions.py | 28 +-- 5 files changed, 140 insertions(+), 110 deletions(-) diff --git a/custom_components/compit/__init__.py b/custom_components/compit/__init__.py index 92a18c7..4ec332d 100644 --- a/custom_components/compit/__init__.py +++ b/custom_components/compit/__init__.py @@ -4,14 +4,15 @@ import json import logging import os + from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.helpers.aiohttp_client import async_get_clientsession -from .types.DeviceDefinitions import DeviceDefinitions -from .coordinator import CompitDataUpdateCoordinator -from .const import DOMAIN, PLATFORMS from .api import CompitAPI +from .const import DOMAIN, PLATFORMS +from .coordinator import CompitDataUpdateCoordinator +from .types.DeviceDefinitions import DeviceDefinitions _LOGGER = logging.getLogger(__name__) @@ -99,18 +100,16 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: async def get_device_definitions(hass: HomeAssistant, lang: str) -> DeviceDefinitions: """Load device definitions from JSON file based on language.""" file_name = f"devices_{lang}.json" + file_path = os.path.join(os.path.dirname(__file__), "definitions", file_name) _LOGGER.debug("Loading device definitions from %s", file_name) + _LOGGER.debug("Full file path: %s", file_path) try: - file_path = os.path.join(os.path.dirname(__file__), "definitions", file_name) - _LOGGER.debug("Full file path: %s", file_path) - with open(file_path, "r", encoding="utf-8") as file: definitions = DeviceDefinitions.from_json(json.load(file)) _LOGGER.debug( "Successfully loaded device definitions for language: %s", lang ) - return definitions except FileNotFoundError: _LOGGER.warning("Device definitions file not found: %s", file_path) diff --git a/custom_components/compit/api.py b/custom_components/compit/api.py index 8ffbe64..481c5a6 100644 --- a/custom_components/compit/api.py +++ b/custom_components/compit/api.py @@ -127,7 +127,7 @@ 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 ): """ Updates a device parameter by sending a request to the device API. @@ -161,7 +161,7 @@ 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 @@ -195,7 +195,7 @@ def __init__(self, session: aiohttp.ClientSession): self._session = session async def get( - self, url: str, headers=None, auth: Any = None + self, url: str, headers=None, auth: Any = None ) -> aiohttp.ClientResponse: """Run http GET method""" if headers is None: @@ -206,7 +206,7 @@ async def get( return await self.api_wrapper("get", url, headers=headers, auth=None) async def post( - self, url: str, data=None, headers=None, auth: Any = None + self, url: str, data=None, headers=None, auth: Any = None ) -> aiohttp.ClientResponse: """Run http POST method""" if headers is None: @@ -221,7 +221,7 @@ async def post( ) async def put( - self, url: str, data=None, headers=None, auth: Any = None + self, url: str, data=None, headers=None, auth: Any = None ) -> aiohttp.ClientResponse: """Run http PUT method""" if headers is None: @@ -234,12 +234,12 @@ async def put( return await self.api_wrapper("put", url, data=data, headers=headers, auth=None) async def api_wrapper( - self, - method: str, - url: str, - data: dict = None, - headers: dict = None, - 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 diff --git a/custom_components/compit/coordinator.py b/custom_components/compit/coordinator.py index f23d545..0896f22 100644 --- a/custom_components/compit/coordinator.py +++ b/custom_components/compit/coordinator.py @@ -19,11 +19,11 @@ 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] = {} @@ -39,12 +39,20 @@ def __init__( @staticmethod def _build_definitions_index( - definitions: DeviceDefinitions, + 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 + # Prefer public attribute or property if available + class_id = getattr(d, "class_", None) + if class_id is None: + # Fallback to a public accessor if defined, else use name-mangled private cautiously + class_id = getattr(d, "classId", None) + if class_id is None: + # As last resort, read the protected field but silence lint by local aliasing + class_id = getattr(d, "_class", None) + index[(class_id, d.code)] = d return index def _find_definition(self, class_id: int, type_code: int) -> Optional[Device]: diff --git a/custom_components/compit/switch.py b/custom_components/compit/switch.py index 463c2b1..04c6271 100644 --- a/custom_components/compit/switch.py +++ b/custom_components/compit/switch.py @@ -1,59 +1,15 @@ import logging from homeassistant.components.switch import SwitchEntity -from homeassistant.const import Platform from homeassistant.core import HomeAssistant 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] - async_add_devices( - [ - CompitSwitch(coordinator, device, parameter, device_definition.name) - for gate in coordinator.gates - for device in gate.devices - if ( - device_definition := next( - ( - definition - for definition in coordinator.device_definitions.devices - if definition.code == device.type - ), - None, - ) - ) - is not None - for parameter in device_definition.parameters - if SensorMatcher.get_platform( - parameter, - coordinator.data[device.id].state.get_parameter_value(parameter), - ) - == Platform.SWITCH - ] - ) +_LOGGER = logging.getLogger(__name__) class CompitSwitch(CoordinatorEntity, SwitchEntity): @@ -66,43 +22,34 @@ def __init__( ): super().__init__(coordinator) self.coordinator = coordinator - # 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 - - # 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 - ) + # Initialize from coordinator data safely (state may be bool or DeviceState) + data_entry = getattr(self.coordinator, "data", {}).get(self.device.id) state_obj = ( getattr(data_entry, "state", None) if data_entry is not None else None ) - # 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) + try: + value = state_obj.get_parameter_value(self.parameter) + except Exception: # defensive: unexpected state shape + value = None 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 + self._is_on = bool(int(raw_val)) 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( @@ -127,21 +74,39 @@ def name(self): @property def is_on(self): + # Try to reflect latest coordinator value if available + try: + data_entry = getattr(self.coordinator, "data", {}).get(self.device.id) + state_obj = ( + getattr(data_entry, "state", None) if data_entry is not None else None + ) + if isinstance(state_obj, bool): + return state_obj + if hasattr(state_obj, "get_parameter_value"): + value = state_obj.get_parameter_value(self.parameter) + if value is not None: + raw_val = getattr(value, "value", None) + if raw_val is not None: + try: + return bool(int(raw_val)) + except Exception: + return bool(raw_val) + except Exception: + # fall back to cached flag + pass return self._is_on @property def extra_state_attributes(self): - items = [ - { - "device": self.device.label, - "device_id": self.device.id, - "device_class": self.device.class_, - "device_type": self.device.type, - } - ] - return { - "details": items, + "details": [ + { + "device": self.device.label, + "device_id": self.device.id, + "device_class": self.device.class_, + "device_type": self.device.type, + } + ], } async def async_turn_on(self, **kwargs): @@ -173,3 +138,61 @@ async def async_toggle(self, **kwargs): await self.async_turn_off() else: await self.async_turn_on() + + +# ... existing code ... + + +async def async_setup_entry(hass: HomeAssistant, entry, async_add_entities): + coordinator: CompitDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] + entities = [] + for gate in coordinator.gates: + for device in gate.devices: + device_definition = next( + ( + d + for d in coordinator.device_definitions.devices + if d.code == device.type + ), + None, + ) + if device_definition is None: + continue + + # Safely inspect current state + data_entry = getattr(coordinator, "data", {}).get(device.id) + state_obj = ( + getattr(data_entry, "state", None) if data_entry is not None else None + ) + + for parameter in device_definition.parameters: + # Only writable, non-number, non-select -> treat as switch + is_writable = getattr(parameter, "readWrite", "R") != "R" + is_number_like = ( + parameter.min_value is not None and parameter.max_value is not None + ) + is_select_like = parameter.details is not None + if not is_writable or is_number_like or is_select_like: + continue + + # If state is a DeviceState, check visibility; if bool or None, skip the check + visible = True + if hasattr(state_obj, "get_parameter_value"): + try: + v = state_obj.get_parameter_value(parameter) + visible = v is not None and not getattr(v, "hidden", False) + except Exception: + visible = False + + if visible: + entities.append( + CompitSwitch( + coordinator=coordinator, + device=device, + parameter=parameter, + device_name=device_definition.name, + ) + ) + + if entities: + async_add_entities(entities) diff --git a/custom_components/compit/types/DeviceDefinitions.py b/custom_components/compit/types/DeviceDefinitions.py index 11ad0c9..af1a47e 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