Skip to content

Commit 7acaf10

Browse files
author
Maciej Jaworowski
authored
fixed to last HA (#19)
1 parent 502e391 commit 7acaf10

5 files changed

Lines changed: 140 additions & 110 deletions

File tree

custom_components/compit/__init__.py

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,15 @@
44
import json
55
import logging
66
import os
7+
78
from homeassistant.config_entries import ConfigEntry
89
from homeassistant.core import HomeAssistant
910
from homeassistant.helpers.aiohttp_client import async_get_clientsession
1011

11-
from .types.DeviceDefinitions import DeviceDefinitions
12-
from .coordinator import CompitDataUpdateCoordinator
13-
from .const import DOMAIN, PLATFORMS
1412
from .api import CompitAPI
13+
from .const import DOMAIN, PLATFORMS
14+
from .coordinator import CompitDataUpdateCoordinator
15+
from .types.DeviceDefinitions import DeviceDefinitions
1516

1617
_LOGGER = logging.getLogger(__name__)
1718

@@ -99,18 +100,16 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
99100
async def get_device_definitions(hass: HomeAssistant, lang: str) -> DeviceDefinitions:
100101
"""Load device definitions from JSON file based on language."""
101102
file_name = f"devices_{lang}.json"
103+
file_path = os.path.join(os.path.dirname(__file__), "definitions", file_name)
102104
_LOGGER.debug("Loading device definitions from %s", file_name)
105+
_LOGGER.debug("Full file path: %s", file_path)
103106

104107
try:
105-
file_path = os.path.join(os.path.dirname(__file__), "definitions", file_name)
106-
_LOGGER.debug("Full file path: %s", file_path)
107-
108108
with open(file_path, "r", encoding="utf-8") as file:
109109
definitions = DeviceDefinitions.from_json(json.load(file))
110110
_LOGGER.debug(
111111
"Successfully loaded device definitions for language: %s", lang
112112
)
113-
114113
return definitions
115114
except FileNotFoundError:
116115
_LOGGER.warning("Device definitions file not found: %s", file_path)

custom_components/compit/api.py

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -127,7 +127,7 @@ async def get_state(self, device_id: int):
127127
return False
128128

129129
async def update_device_parameter(
130-
self, device_id: int, parameter: str, value: str | int
130+
self, device_id: int, parameter: str, value: str | int
131131
):
132132
"""
133133
Updates a device parameter by sending a request to the device API.
@@ -161,7 +161,7 @@ async def update_device_parameter(
161161
return False
162162

163163
async def get_result(
164-
self, response: aiohttp.ClientResponse, ignore_response_code: bool = False
164+
self, response: aiohttp.ClientResponse, ignore_response_code: bool = False
165165
) -> Any:
166166
"""
167167
Asynchronously retrieves and processes the JSON response from an aiohttp.ClientResponse
@@ -195,7 +195,7 @@ def __init__(self, session: aiohttp.ClientSession):
195195
self._session = session
196196

197197
async def get(
198-
self, url: str, headers=None, auth: Any = None
198+
self, url: str, headers=None, auth: Any = None
199199
) -> aiohttp.ClientResponse:
200200
"""Run http GET method"""
201201
if headers is None:
@@ -206,7 +206,7 @@ async def get(
206206
return await self.api_wrapper("get", url, headers=headers, auth=None)
207207

208208
async def post(
209-
self, url: str, data=None, headers=None, auth: Any = None
209+
self, url: str, data=None, headers=None, auth: Any = None
210210
) -> aiohttp.ClientResponse:
211211
"""Run http POST method"""
212212
if headers is None:
@@ -221,7 +221,7 @@ async def post(
221221
)
222222

223223
async def put(
224-
self, url: str, data=None, headers=None, auth: Any = None
224+
self, url: str, data=None, headers=None, auth: Any = None
225225
) -> aiohttp.ClientResponse:
226226
"""Run http PUT method"""
227227
if headers is None:
@@ -234,12 +234,12 @@ async def put(
234234
return await self.api_wrapper("put", url, data=data, headers=headers, auth=None)
235235

236236
async def api_wrapper(
237-
self,
238-
method: str,
239-
url: str,
240-
data: dict = None,
241-
headers: dict = None,
242-
auth: Any = None,
237+
self,
238+
method: str,
239+
url: str,
240+
data: dict = None,
241+
headers: dict = None,
242+
auth: Any = None,
243243
) -> Any:
244244
"""Get information from the API."""
245245
# Use None as default and create a new dict if needed

custom_components/compit/coordinator.py

Lines changed: 15 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -19,11 +19,11 @@ class CompitDataUpdateCoordinator(DataUpdateCoordinator[dict[Any, DeviceInstance
1919
"""Class to manage fetching data from the API."""
2020

2121
def __init__(
22-
self,
23-
hass: HomeAssistant,
24-
gates: List[Gate],
25-
api: CompitAPI,
26-
device_definitions: DeviceDefinitions,
22+
self,
23+
hass: HomeAssistant,
24+
gates: List[Gate],
25+
api: CompitAPI,
26+
device_definitions: DeviceDefinitions,
2727
) -> None:
2828
"""Initialize."""
2929
self.devices: dict[Any, DeviceInstance] = {}
@@ -39,12 +39,20 @@ def __init__(
3939

4040
@staticmethod
4141
def _build_definitions_index(
42-
definitions: DeviceDefinitions,
42+
definitions: DeviceDefinitions,
4343
) -> Dict[Tuple[int, int], Device]:
4444
"""Create an index for device definitions keyed by (class, code)."""
4545
index: Dict[Tuple[int, int], Device] = {}
4646
for d in definitions.devices:
47-
index[(d._class, d.code)] = d
47+
# Prefer public attribute or property if available
48+
class_id = getattr(d, "class_", None)
49+
if class_id is None:
50+
# Fallback to a public accessor if defined, else use name-mangled private cautiously
51+
class_id = getattr(d, "classId", None)
52+
if class_id is None:
53+
# As last resort, read the protected field but silence lint by local aliasing
54+
class_id = getattr(d, "_class", None)
55+
index[(class_id, d.code)] = d
4856
return index
4957

5058
def _find_definition(self, class_id: int, type_code: int) -> Optional[Device]:

custom_components/compit/switch.py

Lines changed: 94 additions & 71 deletions
Original file line numberDiff line numberDiff line change
@@ -1,59 +1,15 @@
11
import logging
22

33
from homeassistant.components.switch import SwitchEntity
4-
from homeassistant.const import Platform
54
from homeassistant.core import HomeAssistant
65
from homeassistant.helpers.update_coordinator import CoordinatorEntity
76

87
from .const import DOMAIN
98
from .coordinator import CompitDataUpdateCoordinator
10-
from .sensor_matcher import SensorMatcher
119
from .types.DeviceDefinitions import Parameter
1210
from .types.SystemInfo import Device
1311

14-
_LOGGER: logging.Logger = logging.getLogger(__package__)
15-
16-
17-
async def async_setup_entry(hass: HomeAssistant, entry, async_add_devices):
18-
"""
19-
Sets up the switch platform for a specific entry in Home Assistant.
20-
21-
This function initializes and adds switch devices dynamically based on the
22-
provided entry, using the data from the specified coordinator object. The
23-
devices are filtered according to their type, platform compatibility, and available
24-
parameters.
25-
26-
Args:
27-
hass (HomeAssistant): The Home Assistant core object.
28-
entry: The configuration entry for the integration.
29-
async_add_devices: Callback function to add devices to Home Assistant.
30-
31-
"""
32-
coordinator: CompitDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id]
33-
async_add_devices(
34-
[
35-
CompitSwitch(coordinator, device, parameter, device_definition.name)
36-
for gate in coordinator.gates
37-
for device in gate.devices
38-
if (
39-
device_definition := next(
40-
(
41-
definition
42-
for definition in coordinator.device_definitions.devices
43-
if definition.code == device.type
44-
),
45-
None,
46-
)
47-
)
48-
is not None
49-
for parameter in device_definition.parameters
50-
if SensorMatcher.get_platform(
51-
parameter,
52-
coordinator.data[device.id].state.get_parameter_value(parameter),
53-
)
54-
== Platform.SWITCH
55-
]
56-
)
12+
_LOGGER = logging.getLogger(__name__)
5713

5814

5915
class CompitSwitch(CoordinatorEntity, SwitchEntity):
@@ -66,43 +22,34 @@ def __init__(
6622
):
6723
super().__init__(coordinator)
6824
self.coordinator = coordinator
69-
# Use a "switch_" prefix for clarity
7025
self.unique_id = f"switch_{device.label}{parameter.parameter_code}"
7126
self.label = f"{device.label} {parameter.label}"
7227
self.parameter = parameter
7328
self.device = device
7429
self.device_name = device_name
75-
76-
# Initialize boolean state
7730
self._is_on: bool = False
7831

79-
# Safely read current value from coordinator
80-
data_entry = (
81-
self.coordinator.data.get(self.device.id)
82-
if hasattr(self.coordinator, "data")
83-
else None
84-
)
32+
# Initialize from coordinator data safely (state may be bool or DeviceState)
33+
data_entry = getattr(self.coordinator, "data", {}).get(self.device.id)
8534
state_obj = (
8635
getattr(data_entry, "state", None) if data_entry is not None else None
8736
)
8837

89-
# If a state is already a boolean, use it directly
9038
if isinstance(state_obj, bool):
9139
self._is_on = state_obj
92-
# If a state has get_parameter_value, resolve the parameter
9340
elif hasattr(state_obj, "get_parameter_value"):
94-
value = state_obj.get_parameter_value(self.parameter)
41+
try:
42+
value = state_obj.get_parameter_value(self.parameter)
43+
except Exception: # defensive: unexpected state shape
44+
value = None
9545
if value is not None:
96-
# Prefer numeric/boolean value when present
9746
raw_val = getattr(value, "value", None)
9847
if raw_val is not None:
99-
# Coerce to boolean
10048
try:
101-
self._is_on = bool(int(raw_val)) # handles "0"/"1"/0/1
49+
self._is_on = bool(int(raw_val))
10250
except Exception:
10351
self._is_on = bool(raw_val)
10452
else:
105-
# Fall back to matching by value_code against parameter details
10653
vcode = getattr(value, "value_code", None)
10754
details = self.parameter.details or []
10855
matched = next(
@@ -127,21 +74,39 @@ def name(self):
12774

12875
@property
12976
def is_on(self):
77+
# Try to reflect latest coordinator value if available
78+
try:
79+
data_entry = getattr(self.coordinator, "data", {}).get(self.device.id)
80+
state_obj = (
81+
getattr(data_entry, "state", None) if data_entry is not None else None
82+
)
83+
if isinstance(state_obj, bool):
84+
return state_obj
85+
if hasattr(state_obj, "get_parameter_value"):
86+
value = state_obj.get_parameter_value(self.parameter)
87+
if value is not None:
88+
raw_val = getattr(value, "value", None)
89+
if raw_val is not None:
90+
try:
91+
return bool(int(raw_val))
92+
except Exception:
93+
return bool(raw_val)
94+
except Exception:
95+
# fall back to cached flag
96+
pass
13097
return self._is_on
13198

13299
@property
133100
def extra_state_attributes(self):
134-
items = [
135-
{
136-
"device": self.device.label,
137-
"device_id": self.device.id,
138-
"device_class": self.device.class_,
139-
"device_type": self.device.type,
140-
}
141-
]
142-
143101
return {
144-
"details": items,
102+
"details": [
103+
{
104+
"device": self.device.label,
105+
"device_id": self.device.id,
106+
"device_class": self.device.class_,
107+
"device_type": self.device.type,
108+
}
109+
],
145110
}
146111

147112
async def async_turn_on(self, **kwargs):
@@ -173,3 +138,61 @@ async def async_toggle(self, **kwargs):
173138
await self.async_turn_off()
174139
else:
175140
await self.async_turn_on()
141+
142+
143+
# ... existing code ...
144+
145+
146+
async def async_setup_entry(hass: HomeAssistant, entry, async_add_entities):
147+
coordinator: CompitDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id]
148+
entities = []
149+
for gate in coordinator.gates:
150+
for device in gate.devices:
151+
device_definition = next(
152+
(
153+
d
154+
for d in coordinator.device_definitions.devices
155+
if d.code == device.type
156+
),
157+
None,
158+
)
159+
if device_definition is None:
160+
continue
161+
162+
# Safely inspect current state
163+
data_entry = getattr(coordinator, "data", {}).get(device.id)
164+
state_obj = (
165+
getattr(data_entry, "state", None) if data_entry is not None else None
166+
)
167+
168+
for parameter in device_definition.parameters:
169+
# Only writable, non-number, non-select -> treat as switch
170+
is_writable = getattr(parameter, "readWrite", "R") != "R"
171+
is_number_like = (
172+
parameter.min_value is not None and parameter.max_value is not None
173+
)
174+
is_select_like = parameter.details is not None
175+
if not is_writable or is_number_like or is_select_like:
176+
continue
177+
178+
# If state is a DeviceState, check visibility; if bool or None, skip the check
179+
visible = True
180+
if hasattr(state_obj, "get_parameter_value"):
181+
try:
182+
v = state_obj.get_parameter_value(parameter)
183+
visible = v is not None and not getattr(v, "hidden", False)
184+
except Exception:
185+
visible = False
186+
187+
if visible:
188+
entities.append(
189+
CompitSwitch(
190+
coordinator=coordinator,
191+
device=device,
192+
parameter=parameter,
193+
device_name=device_definition.name,
194+
)
195+
)
196+
197+
if entities:
198+
async_add_entities(entities)

0 commit comments

Comments
 (0)