Skip to content

Commit 6e34793

Browse files
committed
More progress
1 parent 56fc26b commit 6e34793

File tree

6 files changed

+104
-79
lines changed

6 files changed

+104
-79
lines changed

plugwise/__init__.py

Lines changed: 54 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,7 @@ def __init__(
7777
self._stretch_v2 = False
7878
self._target_smile: str = NONE
7979
self.data: PlugwiseData
80-
self.smile: GatewayData
80+
self.smile = GatewayData(hostname="smile")
8181

8282
@property
8383
def cooling_present(self) -> bool:
@@ -109,23 +109,19 @@ def reboot(self) -> bool:
109109

110110
async def connect(self) -> Version:
111111
"""Connect to the Plugwise Gateway and determine its name, type, version, and other data."""
112-
result = await self._request(DOMAIN_OBJECTS)
113-
# Work-around for Stretch fw 2.7.18
114-
if not (vendor_names := result.findall("./module/vendor_name")):
115-
result = await self._request(MODULES)
116-
vendor_names = result.findall("./module/vendor_name")
117-
118-
names: list[str] = []
119-
for name in vendor_names:
120-
names.append(name.text)
112+
await self._request(DOMAIN_OBJECTS, new=True)
121113

122-
vendor_models = result.findall("./module/vendor_model")
123-
models: list[str] = []
124-
for model in vendor_models:
125-
models.append(model.text)
126-
127-
dsmrmain = result.find("./module/protocols/dsmrmain")
128-
if "Plugwise" not in names and dsmrmain is None: # pragma: no cover
114+
# Work-around for Stretch fw 2.7.18
115+
dsmrmain: bool = False
116+
vendor_names: list = []
117+
vendor_models: list = []
118+
for module in self.data.module:
119+
vendor_names.append(module.vendor_name)
120+
vendor_models.append(module.vendor_model)
121+
if "dsmrmain" in module.protocols:
122+
dsmrmain = True
123+
124+
if "Plugwise" not in vendor_names and dsmrmain is None: # pragma: no cover
129125
LOGGER.error(
130126
"Connected but expected text not returned, we got %s. Please create"
131127
" an issue on http://github.com/plugwise/python-plugwise",
@@ -134,14 +130,14 @@ async def connect(self) -> Version:
134130
raise ResponseError
135131

136132
# Check if Anna is connected to an Adam
137-
if "159.2" in models:
133+
if "159.2" in vendor_models:
138134
LOGGER.error(
139135
"Your Anna is connected to an Adam, make sure to only add the Adam as integration."
140136
)
141137
raise InvalidSetupError
142138

143139
# Determine smile specifics
144-
await self._smile_detect(result, dsmrmain)
140+
await self._smile_detect()
145141

146142
self._smile_api = (
147143
SmileAPI(
@@ -153,8 +149,8 @@ async def connect(self) -> Version:
153149
self._opentherm_device,
154150
self._request,
155151
self._schedule_old_states,
156-
self.data,
157152
self.smile,
153+
self.data,
158154
)
159155
if not self.smile.legacy
160156
else SmileLegacyAPI(
@@ -165,8 +161,8 @@ async def connect(self) -> Version:
165161
self._request,
166162
self._stretch_v2,
167163
self._target_smile,
168-
self.data,
169164
self.smile,
165+
self.data,
170166
)
171167
)
172168

@@ -175,28 +171,36 @@ async def connect(self) -> Version:
175171

176172
return self.smile.firmware_version
177173

178-
async def _smile_detect(
179-
self, result: etree.Element, dsmrmain: etree.Element
180-
) -> None:
174+
async def _smile_detect(self) -> None:
181175
"""Helper-function for connect().
182176
183177
Detect which type of Plugwise Gateway is being connected.
184178
"""
179+
print(f"HOI14 {self}")
180+
print(f"HOI14 {self.smile}")
185181
model: str = "Unknown"
186182
if self.data.gateway is not None:
187-
if gateway.vendor_model is None:
183+
if self.data.gateway.vendor_model is None:
188184
return # pragma: no cover
189185

190-
self.smile.version = self.data.gateway.firmware_version
191-
self.smile.hw_version = self.data.gateway.firmware_version
186+
self.smile.firmware_version = self.data.gateway.firmware_version
187+
self.smile.hardware_version = self.data.gateway.hardware_version
192188
self.smile.hostname = self.data.gateway.hostname
193189
self.smile.mac_address = self.data.gateway.mac_address
194190

195-
print(f"HOI11 {self.data.gateway.environment}")
191+
print(f"HOI11a {self.data.gateway}")
192+
print(f"HOI11b {self.data.gateway.gateway_environment}")
196193
if (
197194
"electricity_consumption_tariff_structure"
198-
in self.data.gateway.environment
199-
and elec_measurement.text
195+
in self.data.gateway.gateway_environment
196+
):
197+
print(
198+
f"HOI11c {self.data.gateway.gateway_environment.electricity_consumption_tariff_structure}"
199+
)
200+
if (
201+
"electricity_consumption_tariff_structure"
202+
in self.data.gateway.gateway_environment
203+
and self.data.gateway.gateway_environment.electricity_consumption_tariff_structure
200204
and self.smile.vendor_model == "smile_thermo"
201205
):
202206
self.smile.anna_p1 = True
@@ -207,7 +211,7 @@ async def _smile_detect(
207211
)
208212

209213
if (
210-
self.smile.vendor_model == "Unknown"
214+
self.data.gateway.vendor_model == "Unknown"
211215
or self.smile.firmware_version == Version("0.0.0")
212216
): # pragma: no cover
213217
# Corner case check
@@ -217,8 +221,8 @@ async def _smile_detect(
217221
)
218222
raise UnsupportedDeviceError
219223

220-
version_major = str(self.smile.firmware_version.major)
221-
self._target_smile = f"{self.data.gateway.model}_v{version_major}"
224+
version_major = Version(self.smile.firmware_version).major
225+
self._target_smile = f"{self.data.gateway.vendor_model}_v{version_major}"
222226
LOGGER.debug("Plugwise identified as %s", self._target_smile)
223227
if self._target_smile not in SMILES:
224228
LOGGER.error(
@@ -239,8 +243,7 @@ async def _smile_detect(
239243
raise UnsupportedDeviceError # pragma: no cover
240244

241245
self.smile.model = "Gateway"
242-
self.smile.model_id = self.data.gateway.model
243-
# TODO gateway name+type?
246+
self.smile.model_id = self.data.gateway.vendor_model
244247
self.smile.name = SMILES[self._target_smile].smile_name
245248
self.smile.type = SMILES[self._target_smile].smile_type
246249
if self.smile.name == "Smile Anna" and self.smile.anna_p1:
@@ -249,9 +252,9 @@ async def _smile_detect(
249252
if self.smile.type == "stretch":
250253
self._stretch_v2 = int(version_major) == 2
251254

252-
self._process_for_thermostat(result)
255+
self._process_for_thermostat()
253256

254-
def _process_for_thermostat(self, result: etree.Element) -> None:
257+
def _process_for_thermostat(self) -> None:
255258
"""Extra processing for thermostats."""
256259
if self.smile.type != "thermostat":
257260
return
@@ -260,18 +263,22 @@ def _process_for_thermostat(self, result: etree.Element) -> None:
260263
# For Adam, Anna, determine the system capabilities:
261264
# Find the connected heating/cooling device (heater_central),
262265
# e.g. heat-pump or gas-fired heater
263-
onoff_boiler = result.find("./module/protocols/onoff_boiler")
264-
open_therm_boiler = result.find("./module/protocols/open_therm_boiler")
265-
self._on_off_device = onoff_boiler is not None
266-
self._opentherm_device = open_therm_boiler is not None
266+
self._on_off_device: bool = (
267+
True
268+
if "protocols" in self.data.module
269+
and "on_off_boiler" in self.data.module.protocols
270+
else False
271+
)
272+
self._opentherm_device: bool = (
273+
True
274+
if "protocols" in self.data.module
275+
and "open_therm_boiler" in self.data.module.protocols
276+
else False
277+
)
267278

268279
# Determine the presence of special features
269-
locator_1 = "./gateway/features/cooling"
270-
locator_2 = "./gateway/features/elga_support"
271-
if result.find(locator_1) is not None:
272-
self._cooling_present = True
273-
if result.find(locator_2) is not None:
274-
self._elga = True
280+
self._cooling_present = "cooling" in self.data.gateway.features
281+
self._elga = "elga_support" in self.data.gateway.features
275282

276283
async def _smile_detect_legacy(
277284
self, result: etree.Element, dsmrmain: etree.Element, model: str

plugwise/helper.py

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -104,7 +104,7 @@ def _get_appliances(self) -> None:
104104
self._count = 0
105105
self._get_locations()
106106

107-
for appliance in self._domain_objects.appliance:
107+
for appliance in self.data.appliance:
108108
appl = Munch()
109109
appl.available = None
110110
appl.entity_id = appliance.id
@@ -200,7 +200,8 @@ def _get_locations(self) -> None:
200200
"""Collect all locations."""
201201
counter = 0
202202
loc = Munch()
203-
locations = self._domain_objects.location
203+
print(f"HOI15 {self.data.location}")
204+
locations = self.data.location
204205
if not locations:
205206
raise KeyError("No location data present!")
206207

@@ -219,7 +220,7 @@ def _get_locations(self) -> None:
219220
counter += 1
220221
self._home_loc_id = loc.loc_id
221222
self._home_location = next(
222-
(l for l in self._domain_objects.location if l.id == loc.loc_id),
223+
(l for l in self.data.location if l.id == loc.loc_id),
223224
None,
224225
)
225226

@@ -307,7 +308,7 @@ def _get_appl_actuator_modes(
307308
def _get_appliances_with_offset_functionality(self) -> list[str]:
308309
"""Helper-function collecting all appliance that have offset_functionality."""
309310
therm_list = []
310-
for appl in self._domain_objects.appliance:
311+
for appl in self.data.appliance:
311312
af = appl.actuator_functionalities
312313
if not af or not isinstance(af, OffsetFunctionality):
313314
continue

plugwise/model.py

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -230,14 +230,22 @@ class Module(WithID):
230230

231231

232232
# Gateway
233+
class GatewayEnvironment(WithID):
234+
"""Minimal Gateway Environment."""
235+
236+
postal_code: str | None = None
237+
electricity_consumption_tariff_structure: str | None = None
238+
electricity_production_tariff_structure: str | None = None
239+
240+
233241
class Gateway(Module):
234242
"""Plugwise Gateway."""
235243

236244
last_reset_date: str | list[str] | None = None
237245
last_boot_date: str | list[str] | None = None
238246

239247
project: dict[str, Any] | None = None
240-
gateway_environment: dict[str, Any] | None = None
248+
gateway_environment: GatewayEnvironment | None = None
241249
features: dict[str, Any] | None = None
242250

243251

plugwise/smile.py

Lines changed: 8 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@
77

88
from collections.abc import Awaitable, Callable
99
import datetime as dt
10-
import json
1110
from typing import Any, cast
1211

1312
from plugwise.constants import (
@@ -36,9 +35,8 @@
3635

3736
# Dict as class
3837
from munch import Munch
39-
import xmltodict
4038

41-
from .model import Appliance, PlugwiseData, Switch
39+
from .model import PlugwiseData, Switch
4240

4341

4442
def model_to_switch_items(model: str, state: str, switch: Switch) -> tuple[str, Switch]:
@@ -77,8 +75,8 @@ def __init__(
7775
_opentherm_device: bool,
7876
_request: Callable[..., Awaitable[Any]],
7977
_schedule_old_states: dict[str, dict[str, str]],
80-
data: PlugwiseData,
8178
smile: Munch,
79+
data: PlugwiseData,
8280
) -> None:
8381
"""Set the constructor for this class."""
8482
super().__init__()
@@ -92,37 +90,21 @@ def __init__(
9290
self._schedule_old_states = _schedule_old_states
9391
self.smile = smile
9492
self.therms_with_offset_func: list[str] = []
93+
self.data = data
94+
95+
print(f"HOI16 {self.data.location}")
9596

9697
@property
9798
def cooling_present(self) -> bool:
9899
"""Return the cooling capability."""
99100
return self._cooling_present
100101

101-
def parse_xml(self, xml: str) -> dict:
102-
# Safely parse XML
103-
element = etree.fromstring(xml)
104-
xml_dict = xmltodict.parse(etree.tostring(element))
105-
print(f"HOI1 {xml_dict.keys()}")
106-
print(
107-
f"HOI2 {json.dumps(xmltodict.parse(xml, process_namespaces=True), indent=2)}"
108-
)
109-
appliance_in = xml_dict["domain_objects"]["appliance"][0]
110-
print(f"HOI4a1 {json.dumps(appliance_in, indent=2)}")
111-
appliance_in = xml_dict["domain_objects"]["appliance"][5]
112-
print(f"HOI4a1 {json.dumps(appliance_in, indent=2)}")
113-
appliance = Appliance.model_validate(appliance_in)
114-
print(f"HOI4a2 {appliance}")
115-
116-
return PlugwiseData.model_validate(xml_dict)
117-
118102
async def full_xml_update(self) -> None:
119103
"""Perform a first fetch of the Plugwise server XML data."""
120-
domain_objects = await self._request(DOMAIN_OBJECTS, new=True)
121-
root = self.parse_xml(domain_objects)
122-
self.data = root.domain_objects
104+
self.data = await self._request(DOMAIN_OBJECTS, new=True)
123105
print(f"HOI3a {self.data}")
124-
print(f"HOI3b {self.data.notification}")
125-
if self.data.notification is not None:
106+
if "notification" in self.data and self.data.notification is not None:
107+
print(f"HOI3b {self.data.notification}")
126108
self._get_plugwise_notifications()
127109

128110
def get_all_gateway_entities(self) -> None:

plugwise/smilecomm.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@
55

66
from __future__ import annotations
77

8+
import json # Debugging
9+
810
from plugwise.constants import LOGGER
911
from plugwise.exceptions import (
1012
ConnectionFailedError,
@@ -17,6 +19,9 @@
1719
# This way of importing aiohttp is because of patch/mocking in testing (aiohttp timeouts)
1820
from aiohttp import BasicAuth, ClientError, ClientResponse, ClientSession, ClientTimeout
1921
from defusedxml import ElementTree as etree
22+
import xmltodict
23+
24+
from .model import Appliance, PlugwiseData
2025

2126

2227
class SmileComm:
@@ -45,6 +50,23 @@ def __init__(
4550
self._auth = BasicAuth(username, password=password)
4651
self._endpoint = f"http://{host}:{str(port)}" # Sensitive
4752

53+
def _parse_xml(self, xml: str) -> dict:
54+
"""Map XML to Pydantic class."""
55+
element = etree.fromstring(xml)
56+
xml_dict = xmltodict.parse(etree.tostring(element))
57+
print(f"HOI1 {xml_dict.keys()}")
58+
print(
59+
f"HOI2 {json.dumps(xmltodict.parse(xml, process_namespaces=True), indent=2)}"
60+
)
61+
appliance_in = xml_dict["domain_objects"]["appliance"][0]
62+
print(f"HOI4a1 {json.dumps(appliance_in, indent=2)}")
63+
appliance_in = xml_dict["domain_objects"]["appliance"][5]
64+
print(f"HOI4a1 {json.dumps(appliance_in, indent=2)}")
65+
appliance = Appliance.model_validate(appliance_in)
66+
print(f"HOI4a2 {appliance}")
67+
68+
return PlugwiseData.model_validate(xml_dict)
69+
4870
async def _request(
4971
self,
5072
command: str,
@@ -148,6 +170,9 @@ async def _request_validate(
148170
raise InvalidXMLError from exc
149171

150172
if new:
173+
domain_objects = result
174+
root = self._parse_xml(domain_objects)
175+
self.data = root.domain_objects
151176
return result
152177
return xml
153178

0 commit comments

Comments
 (0)