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/__init__.py +134 -100
- plugwise/common.py +32 -123
- plugwise/constants.py +6 -16
- plugwise/data.py +60 -37
- plugwise/helper.py +198 -333
- plugwise/legacy/data.py +20 -12
- plugwise/legacy/helper.py +31 -63
- plugwise/legacy/smile.py +64 -60
- plugwise/smile.py +50 -54
- plugwise/smilecomm.py +148 -0
- plugwise/util.py +117 -28
- {plugwise-1.6.3.dist-info → plugwise-1.7.0.dist-info}/METADATA +2 -2
- plugwise-1.7.0.dist-info/RECORD +18 -0
- {plugwise-1.6.3.dist-info → plugwise-1.7.0.dist-info}/WHEEL +1 -1
- plugwise-1.6.3.dist-info/RECORD +0 -17
- {plugwise-1.6.3.dist-info → plugwise-1.7.0.dist-info}/LICENSE +0 -0
- {plugwise-1.6.3.dist-info → plugwise-1.7.0.dist-info}/top_level.txt +0 -0
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
|
-
|
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
|
-
|
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.
|
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
|
100
|
-
self._domain_objects = await self.
|
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
|
-
|
107
|
-
|
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) ->
|
127
|
-
"""Perform an
|
128
|
-
|
129
|
-
|
130
|
-
|
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.
|
137
|
-
heat_cooler = self.gw_entities[self.
|
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"][
|
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
|
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(
|
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 =
|
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
|
-
|
452
|
-
|
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.
|
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"&\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
|
-
|
159
|
-
|
160
|
-
|
161
|
-
|
162
|
-
|
163
|
-
|
164
|
-
|
165
|
-
|
166
|
-
|
167
|
-
|
168
|
-
|
169
|
-
|
170
|
-
|
171
|
-
|
172
|
-
|
173
|
-
|
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")
|
@@ -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,,
|
plugwise-1.6.3.dist-info/RECORD
DELETED
@@ -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,,
|
File without changes
|
File without changes
|