Skip to content

Commit 56fc26b

Browse files
committed
Some progress
1 parent 9e26331 commit 56fc26b

File tree

4 files changed

+97
-46
lines changed

4 files changed

+97
-46
lines changed

plugwise/__init__.py

Lines changed: 29 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,8 @@
4141
from munch import Munch
4242
from packaging.version import Version, parse
4343

44+
from .model import GatewayData, PlugwiseData
45+
4446

4547
class Smile(SmileComm):
4648
"""The main Plugwise Smile API class."""
@@ -74,18 +76,8 @@ def __init__(
7476
self._smile_api: SmileAPI | SmileLegacyAPI
7577
self._stretch_v2 = False
7678
self._target_smile: str = NONE
77-
self.smile: Munch = Munch()
78-
self.smile.anna_p1 = False
79-
self.smile.hostname = NONE
80-
self.smile.hw_version = None
81-
self.smile.legacy = False
82-
self.smile.mac_address = None
83-
self.smile.model = NONE
84-
self.smile.model_id = None
85-
self.smile.name = NONE
86-
self.smile.type = NONE
87-
self.smile.version = Version("0.0.0")
88-
self.smile.zigbee_mac_address = None
79+
self.data: PlugwiseData
80+
self.smile: GatewayData
8981

9082
@property
9183
def cooling_present(self) -> bool:
@@ -161,6 +153,7 @@ async def connect(self) -> Version:
161153
self._opentherm_device,
162154
self._request,
163155
self._schedule_old_states,
156+
self.data,
164157
self.smile,
165158
)
166159
if not self.smile.legacy
@@ -172,14 +165,15 @@ async def connect(self) -> Version:
172165
self._request,
173166
self._stretch_v2,
174167
self._target_smile,
168+
self.data,
175169
self.smile,
176170
)
177171
)
178172

179173
# Update all endpoints on first connect
180174
await self._smile_api.full_xml_update()
181175

182-
return cast(Version, self.smile.version)
176+
return self.smile.firmware_version
183177

184178
async def _smile_detect(
185179
self, result: etree.Element, dsmrmain: etree.Element
@@ -189,29 +183,32 @@ async def _smile_detect(
189183
Detect which type of Plugwise Gateway is being connected.
190184
"""
191185
model: str = "Unknown"
192-
if (gateway := result.find("./gateway")) is not None:
193-
self.smile.version = parse(gateway.find("firmware_version").text)
194-
self.smile.hw_version = gateway.find("hardware_version").text
195-
self.smile.hostname = gateway.find("hostname").text
196-
self.smile.mac_address = gateway.find("mac_address").text
197-
if (vendor_model := gateway.find("vendor_model")) is None:
186+
if self.data.gateway is not None:
187+
if gateway.vendor_model is None:
198188
return # pragma: no cover
199189

200-
model = vendor_model.text
201-
elec_measurement = gateway.find(
202-
"gateway_environment/electricity_consumption_tariff_structure"
203-
)
190+
self.smile.version = self.data.gateway.firmware_version
191+
self.smile.hw_version = self.data.gateway.firmware_version
192+
self.smile.hostname = self.data.gateway.hostname
193+
self.smile.mac_address = self.data.gateway.mac_address
194+
195+
print(f"HOI11 {self.data.gateway.environment}")
204196
if (
205-
elec_measurement is not None
197+
"electricity_consumption_tariff_structure"
198+
in self.data.gateway.environment
206199
and elec_measurement.text
207-
and model == "smile_thermo"
200+
and self.smile.vendor_model == "smile_thermo"
208201
):
209202
self.smile.anna_p1 = True
210203
else:
211-
model = await self._smile_detect_legacy(result, dsmrmain, model)
204+
# TODO
205+
self.smile.vendor_model = await self._smile_detect_legacy(
206+
result, dsmrmain, model
207+
)
212208

213-
if model == "Unknown" or self.smile.version == Version(
214-
"0.0.0"
209+
if (
210+
self.smile.vendor_model == "Unknown"
211+
or self.smile.firmware_version == Version("0.0.0")
215212
): # pragma: no cover
216213
# Corner case check
217214
LOGGER.error(
@@ -220,8 +217,8 @@ async def _smile_detect(
220217
)
221218
raise UnsupportedDeviceError
222219

223-
version_major = str(self.smile.version.major)
224-
self._target_smile = f"{model}_v{version_major}"
220+
version_major = str(self.smile.firmware_version.major)
221+
self._target_smile = f"{self.data.gateway.model}_v{version_major}"
225222
LOGGER.debug("Plugwise identified as %s", self._target_smile)
226223
if self._target_smile not in SMILES:
227224
LOGGER.error(
@@ -242,7 +239,8 @@ async def _smile_detect(
242239
raise UnsupportedDeviceError # pragma: no cover
243240

244241
self.smile.model = "Gateway"
245-
self.smile.model_id = model
242+
self.smile.model_id = self.data.gateway.model
243+
# TODO gateway name+type?
246244
self.smile.name = SMILES[self._target_smile].smile_name
247245
self.smile.type = SMILES[self._target_smile].smile_type
248246
if self.smile.name == "Smile Anna" and self.smile.anna_p1:

plugwise/helper.py

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -415,9 +415,8 @@ def _collect_appliance_data(
415415
measurements: dict[str, DATA | UOM],
416416
) -> etree.Element | None:
417417
"""Collect initial appliance data."""
418-
if (
419-
appliance := self._domain_objects.find(f'./appliance[@id="{entity_id}"]')
420-
) is not None:
418+
if (appliance := self._domain_objects.get_appliance(entity_id)) is not None:
419+
print(f"HOI9 {appliance}")
421420
self._appliance_measurements(appliance, data, measurements)
422421
self._get_lock_state(appliance, data)
423422

@@ -451,14 +450,24 @@ def _power_data_from_location(self) -> GwEntityData:
451450

452451
def _appliance_measurements(
453452
self,
454-
appliance: etree.Element,
453+
appliance: Appliance,
455454
data: GwEntityData,
456455
measurements: dict[str, DATA | UOM],
457456
) -> None:
458457
"""Helper-function for _get_measurement_data() - collect appliance measurement data."""
459458
for measurement, attrs in measurements.items():
460-
p_locator = f'.//logs/point_log[type="{measurement}"]/period/measurement'
461-
if (appl_p_loc := appliance.find(p_locator)) is not None:
459+
print(f"HOI10 {appliance}")
460+
print(f"HOI10 {appliance.logs}")
461+
if "point_log" not in appliance.logs:
462+
continue
463+
464+
print(f"HOI10 {appliance.logs.point_log}")
465+
466+
if (
467+
measurement := next(
468+
(m for m in appliance.logs if m.type == "measurement"), None
469+
)
470+
) is not None:
462471
if skip_obsolete_measurements(appliance, measurement):
463472
continue
464473

plugwise/model.py

Lines changed: 44 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
from enum import Enum
44
from typing import Any
55

6+
from packaging.version import Version
67
from pydantic import BaseModel, ConfigDict, Field
78

89

@@ -178,6 +179,7 @@ class ApplianceType(str, Enum):
178179
CD = "computer_desktop"
179180
HC = "heater_central"
180181
HT = "hometheater"
182+
STRETCH = "stretch"
181183
THERMO_RV = "thermostatic_radiator_valve"
182184
VA = "valve_actuator"
183185
WHV = "water_heater_vessel"
@@ -305,8 +307,28 @@ class DomainObjects(PWBase):
305307
rule: list[dict] = []
306308
template: list[dict] = []
307309

310+
# Runtime-only cache
311+
_appliance_index: dict[str, Appliance] = {}
312+
_location_index: dict[str, Location] = {}
308313

309-
class Root(PWBase):
314+
def model_post_init(self, __context):
315+
"""Build index for referencing by ID.
316+
317+
Runs after validation.
318+
"""
319+
self._appliance_index = {a.id: a for a in self.appliance}
320+
self._location_index = {a.id: a for a in self.location}
321+
322+
def get_appliance(self, id: str) -> Appliance | None:
323+
"""Get Appliance by ID."""
324+
return self._appliance_index.get(id)
325+
326+
def get_location(self, id: str) -> Location | None:
327+
"""Get Location by ID."""
328+
return self._location_index.get(id)
329+
330+
331+
class PlugwiseData(PWBase):
310332
"""Main XML definition."""
311333

312334
domain_objects: DomainObjects
@@ -344,3 +366,24 @@ class Switch(BaseModel):
344366
func_type: SwitchFunctionType = SwitchFunctionType.TOGGLE
345367
act_type: SwitchActuatorType = SwitchActuatorType.CE
346368
func: SwitchFunctionType = SwitchFunctionType.NONE
369+
370+
371+
class GatewayData(BaseModel):
372+
"""Base Smile/gateway/hub model."""
373+
374+
anna_p1: bool = False
375+
hostname: str
376+
firmware_version: str | None = None
377+
hardware_version: str | None = None
378+
legacy: bool = False
379+
mac_address: str | None = None
380+
model: str | None = None
381+
model_id: str | None = None
382+
name: str | None = None
383+
type: ApplianceType | None = None
384+
version: str = "0.0.0"
385+
zigbee_mac_address: str | None = None
386+
387+
def model_post_init(self, __context):
388+
"""Init arbitrary types."""
389+
self.version = Version(self.version)

plugwise/smile.py

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@
3838
from munch import Munch
3939
import xmltodict
4040

41-
from .model import Appliance, Root, Switch
41+
from .model import Appliance, PlugwiseData, Switch
4242

4343

4444
def model_to_switch_items(model: str, state: str, switch: Switch) -> tuple[str, Switch]:
@@ -77,6 +77,7 @@ def __init__(
7777
_opentherm_device: bool,
7878
_request: Callable[..., Awaitable[Any]],
7979
_schedule_old_states: dict[str, dict[str, str]],
80+
data: PlugwiseData,
8081
smile: Munch,
8182
) -> None:
8283
"""Set the constructor for this class."""
@@ -112,16 +113,16 @@ def parse_xml(self, xml: str) -> dict:
112113
appliance = Appliance.model_validate(appliance_in)
113114
print(f"HOI4a2 {appliance}")
114115

115-
return Root.model_validate(xml_dict)
116+
return PlugwiseData.model_validate(xml_dict)
116117

117118
async def full_xml_update(self) -> None:
118119
"""Perform a first fetch of the Plugwise server XML data."""
119-
self._domain_objects = await self._request(DOMAIN_OBJECTS, new=True)
120-
root = self.parse_xml(self._domain_objects)
121-
self._domain_objects = root.domain_objects
122-
print(f"HOI3a {self._domain_objects}")
123-
print(f"HOI3b {self._domain_objects.notification}")
124-
if self._domain_objects.notification is not None:
120+
domain_objects = await self._request(DOMAIN_OBJECTS, new=True)
121+
root = self.parse_xml(domain_objects)
122+
self.data = root.domain_objects
123+
print(f"HOI3a {self.data}")
124+
print(f"HOI3b {self.data.notification}")
125+
if self.data.notification is not None:
125126
self._get_plugwise_notifications()
126127

127128
def get_all_gateway_entities(self) -> None:

0 commit comments

Comments
 (0)