plugwise 1.6.3__py3-none-any.whl → 1.7.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
plugwise/smile.py CHANGED
@@ -2,6 +2,7 @@
2
2
 
3
3
  Plugwise backend module for Home Assistant Core.
4
4
  """
5
+
5
6
  from __future__ import annotations
6
7
 
7
8
  from collections.abc import Awaitable, Callable
@@ -12,8 +13,6 @@ from plugwise.constants import (
12
13
  ADAM,
13
14
  ANNA,
14
15
  APPLIANCES,
15
- DEFAULT_PORT,
16
- DEFAULT_USERNAME,
17
16
  DOMAIN_OBJECTS,
18
17
  GATEWAY_REBOOT,
19
18
  LOCATIONS,
@@ -22,15 +21,13 @@ from plugwise.constants import (
22
21
  NOTIFICATIONS,
23
22
  OFF,
24
23
  RULES,
25
- GatewayData,
26
24
  GwEntityData,
27
- PlugwiseData,
25
+ SmileProps,
28
26
  ThermoLoc,
29
27
  )
30
28
  from plugwise.data import SmileData
31
29
  from plugwise.exceptions import ConnectionFailedError, DataMissingError, PlugwiseError
32
30
 
33
- import aiohttp
34
31
  from defusedxml import ElementTree as etree
35
32
 
36
33
  # Dict as class
@@ -45,10 +42,6 @@ class SmileAPI(SmileData):
45
42
 
46
43
  def __init__(
47
44
  self,
48
- host: str,
49
- password: str,
50
- request: Callable[..., Awaitable[Any]],
51
- websession: aiohttp.ClientSession,
52
45
  _cooling_present: bool,
53
46
  _elga: bool,
54
47
  _is_thermostat: bool,
@@ -56,9 +49,9 @@ class SmileAPI(SmileData):
56
49
  _loc_data: dict[str, ThermoLoc],
57
50
  _on_off_device: bool,
58
51
  _opentherm_device: bool,
52
+ _request: Callable[..., Awaitable[Any]],
59
53
  _schedule_old_states: dict[str, dict[str, str]],
60
- gateway_id: str,
61
- smile_fw_version: Version | None,
54
+ _smile_props: SmileProps,
62
55
  smile_hostname: str | None,
63
56
  smile_hw_version: str | None,
64
57
  smile_mac_address: str | None,
@@ -67,23 +60,18 @@ class SmileAPI(SmileData):
67
60
  smile_name: str,
68
61
  smile_type: str,
69
62
  smile_version: Version | None,
70
- port: int = DEFAULT_PORT,
71
- username: str = DEFAULT_USERNAME,
72
63
  ) -> None:
73
64
  """Set the constructor for this class."""
74
- self._cooling_enabled = False
75
65
  self._cooling_present = _cooling_present
76
66
  self._elga = _elga
77
- self._heater_id: str
78
67
  self._is_thermostat = _is_thermostat
79
68
  self._last_active = _last_active
80
69
  self._loc_data = _loc_data
81
70
  self._on_off_device = _on_off_device
82
71
  self._opentherm_device = _opentherm_device
72
+ self._request = _request
83
73
  self._schedule_old_states = _schedule_old_states
84
- self.gateway_id = gateway_id
85
- self.request = request
86
- self.smile_fw_version = smile_fw_version
74
+ self._smile_props = _smile_props
87
75
  self.smile_hostname = smile_hostname
88
76
  self.smile_hw_version = smile_hw_version
89
77
  self.smile_mac_address = smile_mac_address
@@ -92,65 +80,66 @@ class SmileAPI(SmileData):
92
80
  self.smile_name = smile_name
93
81
  self.smile_type = smile_type
94
82
  self.smile_version = smile_version
83
+ self.therms_with_offset_func: list[str] = []
95
84
  SmileData.__init__(self)
96
85
 
97
-
98
86
  async def full_xml_update(self) -> None:
99
- """Perform a first fetch of all XML data, needed for initialization."""
100
- self._domain_objects = await self.request(DOMAIN_OBJECTS)
87
+ """Perform a first fetch of the Plugwise server XML data."""
88
+ self._domain_objects = await self._request(DOMAIN_OBJECTS)
101
89
  self._get_plugwise_notifications()
102
90
 
103
91
  def get_all_gateway_entities(self) -> None:
104
- """Collect the gateway entities from the received raw XML-data.
92
+ """Collect the Plugwise gateway entities and their data and states from the received raw XML-data.
105
93
 
106
- Run this functions once to gather the initial configuration,
107
- then regularly run async_update() to refresh the entity data.
94
+ First, collect all the connected entities and their initial data.
95
+ If a thermostat-gateway, collect a list of thermostats with offset-capability.
96
+ Collect and add switching- and/or pump-group entities.
97
+ Finally, collect the data and states for each entity.
108
98
  """
109
- # Gather all the entities and their initial data
110
99
  self._all_appliances()
111
100
  if self._is_thermostat:
112
- if self.smile(ADAM):
113
- self._scan_thermostats()
114
- # Collect a list of thermostats with offset-capability
115
101
  self.therms_with_offset_func = (
116
102
  self._get_appliances_with_offset_functionality()
117
103
  )
104
+ if self.smile(ADAM):
105
+ self._scan_thermostats()
118
106
 
119
- # Collect and add switching- and/or pump-group devices
120
107
  if group_data := self._get_group_switches():
121
108
  self.gw_entities.update(group_data)
122
109
 
123
- # Collect the remaining data for all entities
124
110
  self._all_entity_data()
125
111
 
126
- async def async_update(self) -> PlugwiseData:
127
- """Perform an incremental update for updating the various device states."""
128
- self.gw_data: GatewayData = {}
129
- self.gw_entities: dict[str, GwEntityData] = {}
130
- self._zones: dict[str, GwEntityData] = {}
112
+ async def async_update(self) -> dict[str, GwEntityData]:
113
+ """Perform an full update: re-collect all gateway entities and their data and states.
114
+
115
+ Any change in the connected entities will be detected immediately.
116
+ """
117
+ self._zones = {}
118
+ self.gw_entities = {}
131
119
  try:
132
120
  await self.full_xml_update()
133
121
  self.get_all_gateway_entities()
134
- # Set self._cooling_enabled -required for set_temperature,
135
- #also, check for a failed data-retrieval
136
- if "heater_id" in self.gw_data:
137
- heat_cooler = self.gw_entities[self.gw_data["heater_id"]]
122
+ # Set self._cooling_enabled - required for set_temperature(),
123
+ # also, check for a failed data-retrieval
124
+ if "heater_id" in self._smile_props:
125
+ heat_cooler = self.gw_entities[self._smile_props["heater_id"]]
138
126
  if (
139
127
  "binary_sensors" in heat_cooler
140
128
  and "cooling_enabled" in heat_cooler["binary_sensors"]
141
129
  ):
142
- self._cooling_enabled = heat_cooler["binary_sensors"]["cooling_enabled"]
130
+ self._cooling_enabled = heat_cooler["binary_sensors"][
131
+ "cooling_enabled"
132
+ ]
133
+ else: # cover failed data-retrieval for P1
134
+ _ = self.gw_entities[self._smile_props["gateway_id"]]["location"]
143
135
  except KeyError as err:
144
- raise DataMissingError("No Plugwise data received") from err
136
+ raise DataMissingError("No Plugwise actual data received") from err
145
137
 
146
- return PlugwiseData(
147
- devices=self.gw_entities,
148
- gateway=self.gw_data,
149
- )
138
+ return self.gw_entities
150
139
 
151
- ########################################################################################################
152
- ### API Set and HA Service-related Functions ###
153
- ########################################################################################################
140
+ ########################################################################################################
141
+ ### API Set and HA Service-related Functions ###
142
+ ########################################################################################################
154
143
 
155
144
  async def delete_notification(self) -> None:
156
145
  """Delete the active Plugwise Notification."""
@@ -222,7 +211,9 @@ class SmileAPI(SmileData):
222
211
 
223
212
  await self.call_request(uri, method="put", data=data)
224
213
 
225
- async def set_select(self, key: str, loc_id: str, option: str, state: str | None) -> None:
214
+ async def set_select(
215
+ self, key: str, loc_id: str, option: str, state: str | None
216
+ ) -> None:
226
217
  """Set a dhw/gateway/regulation mode or the thermostat schedule option."""
227
218
  match key:
228
219
  case "select_dhw_mode":
@@ -254,7 +245,12 @@ class SmileAPI(SmileData):
254
245
  valid = ""
255
246
  if mode == "away":
256
247
  time_1 = self._domain_objects.find("./gateway/time").text
257
- away_time = dt.datetime.fromisoformat(time_1).astimezone(dt.UTC).isoformat(timespec="milliseconds").replace("+00:00", "Z")
248
+ away_time = (
249
+ dt.datetime.fromisoformat(time_1)
250
+ .astimezone(dt.UTC)
251
+ .isoformat(timespec="milliseconds")
252
+ .replace("+00:00", "Z")
253
+ )
258
254
  valid = (
259
255
  f"<valid_from>{away_time}</valid_from><valid_to>{end_time}</valid_to>"
260
256
  )
@@ -263,7 +259,7 @@ class SmileAPI(SmileData):
263
259
  vacation_time = time_2 + "T23:00:00.000Z"
264
260
  valid = f"<valid_from>{vacation_time}</valid_from><valid_to>{end_time}</valid_to>"
265
261
 
266
- uri = f"{APPLIANCES};id={self.gateway_id}/gateway_mode_control"
262
+ uri = f"{APPLIANCES};id={self._smile_props['gateway_id']}/gateway_mode_control"
267
263
  data = f"<gateway_mode_control_functionality><mode>{mode}</mode>{valid}</gateway_mode_control_functionality>"
268
264
 
269
265
  await self.call_request(uri, method="put", data=data)
@@ -448,8 +444,8 @@ class SmileAPI(SmileData):
448
444
 
449
445
  if setpoint is None:
450
446
  raise PlugwiseError(
451
- "Plugwise: failed setting temperature: no valid input provided"
452
- ) # pragma: no cover"
447
+ "Plugwise: failed setting temperature: no valid input provided"
448
+ ) # pragma: no cover"
453
449
 
454
450
  temperature = str(setpoint)
455
451
  uri = self._thermostat_uri(loc_id)
@@ -465,6 +461,6 @@ class SmileAPI(SmileData):
465
461
  method: str = kwargs["method"]
466
462
  data: str | None = kwargs.get("data")
467
463
  try:
468
- await self.request(uri, method=method, data=data)
464
+ await self._request(uri, method=method, data=data)
469
465
  except ConnectionFailedError as exc:
470
466
  raise ConnectionFailedError from exc
plugwise/smilecomm.py ADDED
@@ -0,0 +1,148 @@
1
+ """Use of this source code is governed by the MIT license found in the LICENSE file.
2
+
3
+ Plugwise Smile communication protocol helpers.
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ from plugwise.constants import LOGGER
9
+ from plugwise.exceptions import (
10
+ ConnectionFailedError,
11
+ InvalidAuthentication,
12
+ InvalidXMLError,
13
+ ResponseError,
14
+ )
15
+ from plugwise.util import escape_illegal_xml_characters
16
+
17
+ # This way of importing aiohttp is because of patch/mocking in testing (aiohttp timeouts)
18
+ from aiohttp import BasicAuth, ClientError, ClientResponse, ClientSession, ClientTimeout
19
+ from defusedxml import ElementTree as etree
20
+
21
+
22
+ class SmileComm:
23
+ """The SmileComm class."""
24
+
25
+ def __init__(
26
+ self,
27
+ host: str,
28
+ password: str,
29
+ port: int,
30
+ timeout: int,
31
+ username: str,
32
+ websession: ClientSession | None,
33
+ ) -> None:
34
+ """Set the constructor for this class."""
35
+ if not websession:
36
+ aio_timeout = ClientTimeout(total=timeout)
37
+ self._websession = ClientSession(timeout=aio_timeout)
38
+ else:
39
+ self._websession = websession
40
+
41
+ # Quickfix IPv6 formatting, not covering
42
+ if host.count(":") > 2: # pragma: no cover
43
+ host = f"[{host}]"
44
+
45
+ self._auth = BasicAuth(username, password=password)
46
+ self._endpoint = f"http://{host}:{str(port)}" # Sensitive
47
+
48
+ async def _request(
49
+ self,
50
+ command: str,
51
+ retry: int = 3,
52
+ method: str = "get",
53
+ data: str | None = None,
54
+ ) -> etree:
55
+ """Get/put/delete data from a give URL."""
56
+ resp: ClientResponse
57
+ url = f"{self._endpoint}{command}"
58
+ try:
59
+ match method:
60
+ case "delete":
61
+ resp = await self._websession.delete(url, auth=self._auth)
62
+ case "get":
63
+ # Work-around for Stretchv2, should not hurt the other smiles
64
+ headers = {"Accept-Encoding": "gzip"}
65
+ resp = await self._websession.get(
66
+ url, headers=headers, auth=self._auth
67
+ )
68
+ case "post":
69
+ headers = {"Content-type": "text/xml"}
70
+ resp = await self._websession.post(
71
+ url,
72
+ headers=headers,
73
+ data=data,
74
+ auth=self._auth,
75
+ )
76
+ case "put":
77
+ headers = {"Content-type": "text/xml"}
78
+ resp = await self._websession.put(
79
+ url,
80
+ headers=headers,
81
+ data=data,
82
+ auth=self._auth,
83
+ )
84
+ except (
85
+ ClientError
86
+ ) as exc: # ClientError is an ancestor class of ServerTimeoutError
87
+ if retry < 1:
88
+ LOGGER.warning(
89
+ "Failed sending %s %s to Plugwise Smile, error: %s",
90
+ method,
91
+ command,
92
+ exc,
93
+ )
94
+ raise ConnectionFailedError from exc
95
+ return await self._request(command, retry - 1)
96
+
97
+ if resp.status == 504:
98
+ if retry < 1:
99
+ LOGGER.warning(
100
+ "Failed sending %s %s to Plugwise Smile, error: %s",
101
+ method,
102
+ command,
103
+ "504 Gateway Timeout",
104
+ )
105
+ raise ConnectionFailedError
106
+ return await self._request(command, retry - 1)
107
+
108
+ return await self._request_validate(resp, method)
109
+
110
+ async def _request_validate(self, resp: ClientResponse, method: str) -> etree:
111
+ """Helper-function for _request(): validate the returned data."""
112
+ match resp.status:
113
+ case 200:
114
+ # Cornercases for server not responding with 202
115
+ if method in ("post", "put"):
116
+ return
117
+ case 202:
118
+ # Command accepted gives empty body with status 202
119
+ return
120
+ case 401:
121
+ msg = (
122
+ "Invalid Plugwise login, please retry with the correct credentials."
123
+ )
124
+ LOGGER.error("%s", msg)
125
+ raise InvalidAuthentication
126
+ case 405:
127
+ msg = "405 Method not allowed."
128
+ LOGGER.error("%s", msg)
129
+ raise ConnectionFailedError
130
+
131
+ if not (result := await resp.text()) or (
132
+ "<error>" in result and "Not started" not in result
133
+ ):
134
+ LOGGER.warning("Smile response empty or error in %s", result)
135
+ raise ResponseError
136
+
137
+ try:
138
+ # Encode to ensure utf8 parsing
139
+ xml = etree.XML(escape_illegal_xml_characters(result).encode())
140
+ except etree.ParseError as exc:
141
+ LOGGER.warning("Smile returns invalid XML for %s", self._endpoint)
142
+ raise InvalidXMLError from exc
143
+
144
+ return xml
145
+
146
+ async def close_connection(self) -> None:
147
+ """Close the Plugwise connection."""
148
+ await self._websession.close()
plugwise/util.py CHANGED
@@ -1,4 +1,5 @@
1
1
  """Plugwise protocol helpers."""
2
+
2
3
  from __future__ import annotations
3
4
 
4
5
  import datetime as dt
@@ -19,7 +20,6 @@ from plugwise.constants import (
19
20
  SPECIAL_FORMAT,
20
21
  SPECIALS,
21
22
  SWITCHES,
22
- TEMP_CELSIUS,
23
23
  UOM,
24
24
  BinarySensorType,
25
25
  GwEntityData,
@@ -41,9 +41,7 @@ def check_alternative_location(loc: Munch, legacy: bool) -> Munch:
41
41
  loc.found = False
42
42
  return loc
43
43
 
44
- loc.locator = (
45
- f'./{loc.log_type}[type="{loc.measurement}"]/period/measurement'
46
- )
44
+ loc.locator = f'./{loc.log_type}[type="{loc.measurement}"]/period/measurement'
47
45
  if legacy:
48
46
  loc.locator = (
49
47
  f"./{loc.meas_list[0]}_{loc.log_type}/"
@@ -67,11 +65,11 @@ def in_alternative_location(loc: Munch, legacy: bool) -> bool:
67
65
  """
68
66
  present = "log" in loc.log_type and (
69
67
  "gas" in loc.measurement or "phase" in loc.measurement
70
- )
68
+ )
71
69
  if legacy:
72
70
  present = "meter" in loc.log_type and (
73
71
  "point" in loc.log_type or "gas" in loc.measurement
74
- )
72
+ )
75
73
 
76
74
  return present
77
75
 
@@ -118,6 +116,30 @@ def check_model(name: str | None, vendor_name: str | None) -> str | None:
118
116
  return None
119
117
 
120
118
 
119
+ def collect_power_values(
120
+ data: GwEntityData, loc: Munch, tariff: str, legacy: bool = False
121
+ ) -> None:
122
+ """Something."""
123
+ for loc.peak_select in ("nl_peak", "nl_offpeak"):
124
+ loc.locator = (
125
+ f'./{loc.log_type}[type="{loc.measurement}"]/period/'
126
+ f'measurement[@{tariff}="{loc.peak_select}"]'
127
+ )
128
+ if legacy:
129
+ loc.locator = (
130
+ f"./{loc.meas_list[0]}_{loc.log_type}/measurement"
131
+ f'[@directionality="{loc.meas_list[1]}"][@{tariff}="{loc.peak_select}"]'
132
+ )
133
+
134
+ loc = power_data_peak_value(loc, legacy)
135
+ if not loc.found:
136
+ continue
137
+
138
+ data = power_data_energy_diff(loc.measurement, loc.net_string, loc.f_val, data)
139
+ key = cast(SensorType, loc.key_string)
140
+ data["sensors"][key] = loc.f_val
141
+
142
+
121
143
  def common_match_cases(
122
144
  measurement: str,
123
145
  attrs: DATA | UOM,
@@ -147,6 +169,22 @@ def common_match_cases(
147
169
  data["binary_sensors"]["low_battery"] = False
148
170
 
149
171
 
172
+ def count_data_items(count: int, data: GwEntityData) -> int:
173
+ """When present, count the binary_sensors, sensors and switches dict-items, don't count the dicts.
174
+
175
+ Also, count the remaining single data items, the amount of dicts present have already been pre-subtracted in the previous step.
176
+ """
177
+ if "binary_sensors" in data:
178
+ count += len(data["binary_sensors"]) - 1
179
+ if "sensors" in data:
180
+ count += len(data["sensors"]) - 1
181
+ if "switches" in data:
182
+ count += len(data["switches"]) - 1
183
+
184
+ count += len(data)
185
+ return count
186
+
187
+
150
188
  def escape_illegal_xml_characters(xmldata: str) -> str:
151
189
  """Replace illegal &-characters."""
152
190
  return re.sub(r"&([^a-zA-Z#])", r"&amp;\1", xmldata)
@@ -155,26 +193,22 @@ def escape_illegal_xml_characters(xmldata: str) -> str:
155
193
  def format_measure(measure: str, unit: str) -> float | int:
156
194
  """Format measure to correct type."""
157
195
  result: float | int = 0
158
- try:
159
- result = int(measure)
160
- if unit == TEMP_CELSIUS:
161
- result = float(measure)
162
- except ValueError:
163
- float_measure = float(measure)
164
- if unit == PERCENTAGE and 0 < float_measure <= 1:
165
- return int(float_measure * 100)
166
-
167
- if unit == ENERGY_KILO_WATT_HOUR:
168
- float_measure = float_measure / 1000
169
-
170
- if unit in SPECIAL_FORMAT:
171
- result = float(f"{round(float_measure, 3):.3f}")
172
- elif unit == ELECTRIC_POTENTIAL_VOLT:
173
- result = float(f"{round(float_measure, 1):.1f}")
174
- elif abs(float_measure) < 10:
175
- result = float(f"{round(float_measure, 2):.2f}")
176
- elif abs(float_measure) >= 10:
177
- result = float(f"{round(float_measure, 1):.1f}")
196
+
197
+ float_measure = float(measure)
198
+ if unit == PERCENTAGE and 0 < float_measure <= 1:
199
+ return int(float_measure * 100)
200
+
201
+ if unit == ENERGY_KILO_WATT_HOUR:
202
+ float_measure = float_measure / 1000
203
+
204
+ if unit in SPECIAL_FORMAT:
205
+ result = float(f"{round(float_measure, 3):.3f}")
206
+ elif unit == ELECTRIC_POTENTIAL_VOLT:
207
+ result = float(f"{round(float_measure, 1):.1f}")
208
+ elif abs(float_measure) < 10:
209
+ result = float(f"{round(float_measure, 2):.2f}")
210
+ elif abs(float_measure) >= 10:
211
+ result = float(f"{round(float_measure, 1):.1f}")
178
212
 
179
213
  return result
180
214
 
@@ -189,6 +223,37 @@ def get_vendor_name(module: etree, model_data: ModuleData) -> ModuleData:
189
223
  return model_data
190
224
 
191
225
 
226
+ def power_data_energy_diff(
227
+ measurement: str,
228
+ net_string: SensorType,
229
+ f_val: float | int,
230
+ data: GwEntityData,
231
+ ) -> GwEntityData:
232
+ """Calculate differential energy."""
233
+ if (
234
+ "electricity" in measurement
235
+ and "phase" not in measurement
236
+ and "interval" not in net_string
237
+ ):
238
+ diff = 1
239
+ if "produced" in measurement:
240
+ diff = -1
241
+ if net_string not in data["sensors"]:
242
+ tmp_val: float | int = 0
243
+ else:
244
+ tmp_val = data["sensors"][net_string]
245
+
246
+ if isinstance(f_val, int):
247
+ tmp_val += f_val * diff
248
+ else:
249
+ tmp_val += float(f_val * diff)
250
+ tmp_val = float(f"{round(tmp_val, 3):.3f}")
251
+
252
+ data["sensors"][net_string] = tmp_val
253
+
254
+ return data
255
+
256
+
192
257
  def power_data_local_format(
193
258
  attrs: dict[str, str], key_string: str, val: str
194
259
  ) -> float | int:
@@ -202,6 +267,31 @@ def power_data_local_format(
202
267
  return format_measure(val, attrs_uom)
203
268
 
204
269
 
270
+ def power_data_peak_value(loc: Munch, legacy: bool) -> Munch:
271
+ """Helper-function for _power_data_from_location() and _power_data_from_modules()."""
272
+ loc.found = True
273
+ if loc.logs.find(loc.locator) is None:
274
+ loc = check_alternative_location(loc, legacy)
275
+ if not loc.found:
276
+ return loc
277
+
278
+ if (peak := loc.peak_select.split("_")[1]) == "offpeak":
279
+ peak = "off_peak"
280
+ log_found = loc.log_type.split("_")[0]
281
+ loc.key_string = f"{loc.measurement}_{peak}_{log_found}"
282
+ if "gas" in loc.measurement or loc.log_type == "point_meter":
283
+ loc.key_string = f"{loc.measurement}_{log_found}"
284
+ # Only for P1 Actual -------------------#
285
+ if "phase" in loc.measurement:
286
+ loc.key_string = f"{loc.measurement}"
287
+ # --------------------------------------#
288
+ loc.net_string = f"net_electricity_{log_found}"
289
+ val = loc.logs.find(loc.locator).text
290
+ loc.f_val = power_data_local_format(loc.attrs, loc.key_string, val)
291
+
292
+ return loc
293
+
294
+
205
295
  def remove_empty_platform_dicts(data: GwEntityData) -> None:
206
296
  """Helper-function for removing any empty platform dicts."""
207
297
  if not data["binary_sensors"]:
@@ -222,8 +312,7 @@ def skip_obsolete_measurements(xml: etree, measurement: str) -> bool:
222
312
  locator = f".//logs/point_log[type='{measurement}']/updated_date"
223
313
  if (
224
314
  measurement in OBSOLETE_MEASUREMENTS
225
- and (updated_date_key := xml.find(locator))
226
- is not None
315
+ and (updated_date_key := xml.find(locator)) is not None
227
316
  ):
228
317
  updated_date = updated_date_key.text.split("T")[0]
229
318
  date_1 = dt.datetime.strptime(updated_date, "%Y-%m-%d")
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.1
1
+ Metadata-Version: 2.2
2
2
  Name: plugwise
3
- Version: 1.6.3
3
+ Version: 1.7.0
4
4
  Summary: Plugwise Smile (Adam/Anna/P1) and Stretch module for Python 3.
5
5
  Home-page: https://github.com/plugwise/python-plugwise
6
6
  Author: Plugwise device owners
@@ -0,0 +1,18 @@
1
+ plugwise/__init__.py,sha256=fyot91I4cMkgo_8fL1hycRXwGBDecUq9ltxLAfSY-2U,17762
2
+ plugwise/common.py,sha256=_41FLLjgccaHjaV0Ndn1YEkxjB_qKev1k3ZnhSUzXjc,9305
3
+ plugwise/constants.py,sha256=Vd8tvOHsRtZxtUUFXBXolZj3QiKnxRt0bnwDT9DoEqE,17073
4
+ plugwise/data.py,sha256=ITGnS5RiRbd3O2KbYLc9_5okw81zLl7azXNXtuTwaoY,13022
5
+ plugwise/exceptions.py,sha256=Ce-tO9uNsMB-8FP6VAxBvsHNJ-NIM9F0onUZOdZI4Ys,1110
6
+ plugwise/helper.py,sha256=dsdPpwJHiZ3DQoTc7kWoOiFVPvRda5u1GRJwAEDxuBk,39388
7
+ plugwise/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
8
+ plugwise/smile.py,sha256=I8lssyqmq_PF8ptLxk3daS7lf20kXr5Z3L7SYg3S9j4,18928
9
+ plugwise/smilecomm.py,sha256=aQ2KkebDTou18k-flrVmTnFkgls33Xu8D9ZZQQt2R5k,5178
10
+ plugwise/util.py,sha256=7OPtC4FDbAweAqad6b-6tKtMlSSQd3OU7g5-0lplF34,11002
11
+ plugwise/legacy/data.py,sha256=s2WYjgxwcuAGS8UOxJVf7xLSxS38Zgr8GsjxlxfD98w,3574
12
+ plugwise/legacy/helper.py,sha256=GslPqZL0gAAnqJVy-Ydp_U-eGQ5jYhz9F_Y15BfQgt4,17056
13
+ plugwise/legacy/smile.py,sha256=TxjqRf7KIecECt48iHgSFI7bIOEHNkf4desSBVo1l-M,11666
14
+ plugwise-1.7.0.dist-info/LICENSE,sha256=mL22BjmXtg_wnoDnnaqps5_Bg_VGj_yHueX5lsKwbCc,1144
15
+ plugwise-1.7.0.dist-info/METADATA,sha256=ZWR9Mhkp1CknISTs1adzs9jePeIWqW9M89qMnUyvGI8,9148
16
+ plugwise-1.7.0.dist-info/WHEEL,sha256=In9FTNxeP60KnTkGw7wk6mJPYd_dQSjEZmXdBdMCI-8,91
17
+ plugwise-1.7.0.dist-info/top_level.txt,sha256=MYOmktMFf8ZmX6_OE1y9MoCZFfY-L8DA0F2tA2IvE4s,9
18
+ plugwise-1.7.0.dist-info/RECORD,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: setuptools (75.6.0)
2
+ Generator: setuptools (75.8.0)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5
 
@@ -1,17 +0,0 @@
1
- plugwise/__init__.py,sha256=GzkiJTPI0vFUIjGcIlvL_KS1lIHad_cOIKzCXwY3Eaw,17183
2
- plugwise/common.py,sha256=vyiAbn5SJgcL5A9DYIj2ixHBbPO_6EFa16bK1VJ3In4,13040
3
- plugwise/constants.py,sha256=yTR9uxFyWi0S5-KDtUGbtMI3eb2dGC3ekMxvL8X0qEY,17203
4
- plugwise/data.py,sha256=kFdmCW9UEX7mqdBoGWH6TCiwb7vzdm-es5ZMwJsdBQA,12095
5
- plugwise/exceptions.py,sha256=Ce-tO9uNsMB-8FP6VAxBvsHNJ-NIM9F0onUZOdZI4Ys,1110
6
- plugwise/helper.py,sha256=v0sli2AE1s3ax0aAZoZw3Gmu7N9gImAol7S4l8FcuRY,45790
7
- plugwise/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
8
- plugwise/smile.py,sha256=fyKo60PtOnW8bK3WpIu26R0mnXmHmsdjokTjVNS0j4A,19057
9
- plugwise/util.py,sha256=kt7JNcrTQaLT3eW_fzO4YmGsZzRull2Ge_OmHKl-rHk,8054
10
- plugwise/legacy/data.py,sha256=wHNcRQ_qF4A1rUdxn-1MoW1Z1gUwLqOvYvIkN6tJ_sk,3088
11
- plugwise/legacy/helper.py,sha256=ARIJytJNFiIR5G7Bp75DIULqgt56m0pxUXy6Ze8Te-4,18173
12
- plugwise/legacy/smile.py,sha256=RCQ0kHQwmPjq_G3r6aCe75RIvJt339jilzqEKydNopo,11286
13
- plugwise-1.6.3.dist-info/LICENSE,sha256=mL22BjmXtg_wnoDnnaqps5_Bg_VGj_yHueX5lsKwbCc,1144
14
- plugwise-1.6.3.dist-info/METADATA,sha256=OHR2k69qRTnfT8bl5jFpVRMiDDsnY7VCUgARvRKemCc,9148
15
- plugwise-1.6.3.dist-info/WHEEL,sha256=PZUExdf71Ui_so67QXpySuHtCi3-J3wvF4ORK6k_S8U,91
16
- plugwise-1.6.3.dist-info/top_level.txt,sha256=MYOmktMFf8ZmX6_OE1y9MoCZFfY-L8DA0F2tA2IvE4s,9
17
- plugwise-1.6.3.dist-info/RECORD,,