Skip to content

Commit ff036ef

Browse files
committed
Initial typed xml reading attempt (incomplete)
1 parent 4dc0121 commit ff036ef

File tree

5 files changed

+300
-26
lines changed

5 files changed

+300
-26
lines changed

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
# Changelog
22

3+
## Ongoing
4+
5+
- Attempt to ditch untyped Munch for the existing TypedDicts by leveraging pydantic to type xmltodict XML conversion
6+
37
## v1.11.2
48

59
- Add/update model-data for Jip, Tom and Floor via PR [#842](https://github.com/plugwise/python-plugwise/pull/842)

plugwise/helper.py

Lines changed: 18 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -60,10 +60,8 @@
6060
def extend_plug_device_class(appl: Munch, appliance: etree.Element) -> None:
6161
"""Extend device_class name of Plugs (Plugwise and Aqara) - Pw-Beta Issue #739."""
6262

63-
if (
64-
(search := appliance.find("description")) is not None
65-
and (description := search.text) is not None
66-
and ("ZigBee protocol" in description or "smart plug" in description)
63+
if (description := appliance.description) is not None and (
64+
"ZigBee protocol" in description or "smart plug" in description
6765
):
6866
appl.pwclass = f"{appl.pwclass}_plug"
6967

@@ -114,19 +112,19 @@ def _get_appliances(self) -> None:
114112
self._count = 0
115113
self._get_locations()
116114

117-
for appliance in self._domain_objects.findall("./appliance"):
115+
for appliance in self._domain_objects.appliance:
118116
appl = Munch()
119117
appl.available = None
120-
appl.entity_id = appliance.get("id")
118+
appl.entity_id = appliance.id
121119
appl.firmware = None
122120
appl.hardware = None
123121
appl.location = None
124122
appl.mac = None
125123
appl.model = None
126124
appl.model_id = None
127125
appl.module_id = None
128-
appl.name = appliance.find("name").text
129-
appl.pwclass = appliance.find("type").text
126+
appl.name = appliance.name
127+
appl.pwclass = appliance.type
130128
appl.zigbee_mac = None
131129
appl.vendor_name = None
132130

@@ -138,7 +136,7 @@ def _get_appliances(self) -> None:
138136
):
139137
continue
140138

141-
if (appl_loc := appliance.find("location")) is not None:
139+
if (appl_loc := appliance.location) is not None:
142140
appl.location = appl_loc.get("id")
143141
# Set location to the _home_loc_id when the appliance-location is not found,
144142
# except for thermostat-devices without a location, they are not active
@@ -204,14 +202,14 @@ def _get_locations(self) -> None:
204202
"""Collect all locations."""
205203
counter = 0
206204
loc = Munch()
207-
locations = self._domain_objects.findall("./location")
205+
locations = self._domain_objects.location
208206
if not locations:
209207
raise KeyError("No location data present!")
210208

211209
for location in locations:
212-
loc.loc_id = location.get("id")
213-
loc.name = location.find("name").text
214-
loc._type = location.find("type").text
210+
loc.loc_id = location.id
211+
loc.name = location.name
212+
loc._type = location.type
215213
self._loc_data[loc.loc_id] = {
216214
"name": loc.name,
217215
"primary": [],
@@ -222,8 +220,9 @@ def _get_locations(self) -> None:
222220
if loc._type == "building":
223221
counter += 1
224222
self._home_loc_id = loc.loc_id
225-
self._home_location = self._domain_objects.find(
226-
f"./location[@id='{loc.loc_id}']"
223+
self._home_location = next(
224+
(l for l in self._domain_objects.location if l.id == loc.loc_id),
225+
None,
227226
)
228227

229228
if counter == 0:
@@ -504,11 +503,11 @@ def _get_toggle_state(
504503
def _get_plugwise_notifications(self) -> None:
505504
"""Collect the Plugwise notifications."""
506505
self._notifications = {}
507-
for notification in self._domain_objects.findall("./notification"):
506+
for notification in self._domain_objects.notification:
508507
try:
509-
msg_id = notification.get("id")
510-
msg_type = notification.find("type").text
511-
msg = notification.find("message").text
508+
msg_id = notification.id
509+
msg_type = notification.type
510+
msg = notification.message
512511
self._notifications[msg_id] = {msg_type: msg}
513512
LOGGER.debug("Plugwise notifications: %s", self._notifications)
514513
except AttributeError: # pragma: no cover

plugwise/model.py

Lines changed: 239 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,239 @@
1+
"""Plugwise models."""
2+
3+
from typing import Any
4+
5+
from pydantic import BaseModel, ConfigDict, Field
6+
7+
8+
class PWBase(BaseModel):
9+
"""Base / common Plugwise class."""
10+
11+
# Allow additional struct (ignored)
12+
model_config = ConfigDict(extra="ignore")
13+
14+
15+
class WithID(PWBase):
16+
"""Class for Plugwise ID base XML elements.
17+
18+
Takes id from the xml definition.
19+
"""
20+
21+
id: str = Field(alias="@id")
22+
model_config = ConfigDict(extra="allow")
23+
24+
25+
# Period and measurements
26+
class Measurement(PWBase):
27+
"""Plugwise Measurement."""
28+
29+
log_date: str = Field(alias="@log_date")
30+
value: str = Field(alias="#text")
31+
32+
33+
class Period(PWBase):
34+
"""Plugwise period of time."""
35+
36+
start_date: str = Field(alias="@start_date")
37+
end_date: str = Field(alias="@end_date")
38+
interval: str | None = Field(default=None, alias="@interval")
39+
measurement: Measurement | None = None
40+
41+
42+
# Notification
43+
class Notification(WithID):
44+
"""Plugwise notification.
45+
46+
Our examples only show single optional notification being present
47+
"""
48+
49+
type: str
50+
origin: str | None = None
51+
title: str | None = None
52+
message: str | None = None
53+
54+
created_date: str
55+
modified_date: str | list[str] | None = None
56+
deleted_date: str | None = None
57+
58+
valid_from: str | list[str] | None = None
59+
valid_to: str | list[str] | None = None
60+
read_date: str | list[str] | None = None
61+
62+
63+
# Logging
64+
class BaseLog(WithID):
65+
"""Plugwise mapping for point_log and interval_log constructs."""
66+
67+
type: str
68+
unit: str | None = None
69+
updated_date: str | None = None
70+
last_consecutive_log_date: str | None = None
71+
interval: str | None = None
72+
period: Period | None = None
73+
74+
75+
class PointLog(BaseLog):
76+
"""Plugwise class ofr specific point_logs.
77+
78+
i.e. <relay id="..."/>
79+
"""
80+
81+
relay: WithID | None = None
82+
thermo_meter: WithID | None = None
83+
thermostat: WithID | None = None
84+
battery_meter: WithID | None = None
85+
temperature_offset: WithID | None = None
86+
weather_descriptor: WithID | None = None
87+
irradiance_meter: WithID | None = None
88+
wind_vector: WithID | None = None
89+
hygro_meter: WithID | None = None
90+
91+
92+
class IntervalLog(BaseLog):
93+
"""Plugwise class ofr specific interval_logs."""
94+
95+
electricity_interval_meter: WithID | None = (
96+
None # references only, still to type if we need this
97+
)
98+
99+
100+
# Functionality
101+
class BaseFunctionality(WithID):
102+
"""Plugwise functionality."""
103+
104+
updated_date: str | None = None
105+
106+
107+
class RelayFunctionality(BaseFunctionality):
108+
"""Relay functionality."""
109+
110+
lock: bool | None = None
111+
state: str | None = None
112+
relay: WithID | None = None
113+
114+
115+
class ThermostatFunctionality(BaseFunctionality):
116+
"""Thermostat functionality."""
117+
118+
type: str
119+
lower_bound: float
120+
upper_bound: float
121+
resolution: float
122+
setpoint: float
123+
thermostat: WithID | None = None
124+
125+
126+
class OffsetFunctionality(BaseFunctionality):
127+
"""Offset functionality."""
128+
129+
type: str
130+
offset: float
131+
temperature_offset: WithID | None = None
132+
133+
134+
# Services
135+
class ServiceBase(WithID):
136+
"""Plugwise Services."""
137+
138+
log_type: str | None = Field(default=None, alias="@log_type")
139+
endpoint: str | None = Field(default=None, alias="@endpoint")
140+
functionalities: dict[str, WithID | list[WithID]] | None = (
141+
None # references only, still to type if we need this
142+
)
143+
144+
145+
# Protocols
146+
class Neighbor(PWBase):
147+
"""Neighbor definition."""
148+
149+
mac_address: str = Field(alias="@mac_address")
150+
lqi: int | None = None
151+
depth: int | None = None
152+
relationship: str | None = None
153+
154+
155+
class ZigBeeNode(WithID):
156+
"""ZigBee node definition."""
157+
158+
mac_address: str
159+
type: str
160+
reachable: bool
161+
power_source: str | None = None
162+
battery_type: str | None = None
163+
zig_bee_coordinator: WithID | None = None
164+
neighbors: list[Neighbor]
165+
last_neighbor_table_received: str | None = None
166+
neighbor_table_support: bool | None = None
167+
168+
169+
# Appliance
170+
class Appliance(WithID):
171+
"""Plugwise Appliance."""
172+
173+
name: str
174+
description: str | None = None
175+
type: str
176+
created_date: str
177+
modified_date: str | list[str] | None = None
178+
deleted_date: str | None = None
179+
180+
location: dict[str, Any] | None = None
181+
groups: dict[str, WithID | list[WithID]] | None = None
182+
logs: dict[str, BaseLog | list[BaseLog]] | None = None
183+
actuator_functionalities: (
184+
dict[str, BaseFunctionality | list[BaseFunctionality]] | None
185+
) = None
186+
187+
188+
# Module
189+
class Module(WithID):
190+
"""Plugwise Module."""
191+
192+
vendor_name: str | None = None
193+
vendor_model: str | None = None
194+
hardware_version: str | None = None
195+
firmware_version: str | None = None
196+
created_date: str
197+
modified_date: str | list[str] | None = None
198+
deleted_date: str | None = None
199+
200+
# This is too much :) shorted to Any, but we should still look at this
201+
# services: dict[str, ServiceBase | list[ServiceBase]] | list[dict[str, Any]] | None = None
202+
services: dict[str, Any] | list[Any] | None = None
203+
204+
protocols: dict[str, Any] | None = None # ZigBeeNode, WLAN, LAN
205+
206+
207+
# Location
208+
class Location(WithID):
209+
"""Plugwise Location."""
210+
211+
name: str
212+
description: str | None = None
213+
type: str
214+
created_date: str
215+
modified_date: str | list[str] | None = None
216+
deleted_date: str | None = None
217+
preset: str | None = None
218+
appliances: list[WithID]
219+
logs: dict[str, BaseLog | list[BaseLog]] | list[BaseLog] | None
220+
appliances: dict[str, WithID | list[WithID]] | None = None
221+
actuator_functionalities: dict[str, BaseFunctionality] | None = None
222+
223+
224+
# Root objects
225+
class DomainObjects(PWBase):
226+
"""Plugwise Domain Objects."""
227+
228+
appliance: list[Appliance] = []
229+
module: list[Module] = []
230+
location: list[Location] = []
231+
notification: Notification | list[Notification] | None = None
232+
rule: list[dict] = []
233+
template: list[dict] = []
234+
235+
236+
class Root(PWBase):
237+
"""Main XML definition."""
238+
239+
domain_objects: DomainObjects

0 commit comments

Comments
 (0)