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
13 changes: 6 additions & 7 deletions custom_components/compit/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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__)

Expand Down Expand Up @@ -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)
Expand Down
22 changes: 11 additions & 11 deletions custom_components/compit/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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:
Expand All @@ -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:
Expand All @@ -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:
Expand All @@ -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
Expand Down
22 changes: 15 additions & 7 deletions custom_components/compit/coordinator.py
Original file line number Diff line number Diff line change
Expand Up @@ -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] = {}
Expand All @@ -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]:
Expand Down
165 changes: 94 additions & 71 deletions custom_components/compit/switch.py
Original file line number Diff line number Diff line change
@@ -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):
Expand All @@ -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(
Expand All @@ -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):
Expand Down Expand Up @@ -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)
Loading