PyViCare 2.56.0__tar.gz → 2.57.0__tar.gz
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.
- {pyvicare-2.56.0 → pyvicare-2.57.0}/PKG-INFO +1 -1
- {pyvicare-2.56.0 → pyvicare-2.57.0}/PyViCare/PyViCare.py +1 -1
- {pyvicare-2.56.0 → pyvicare-2.57.0}/PyViCare/PyViCareAbstractOAuthManager.py +6 -0
- {pyvicare-2.56.0 → pyvicare-2.57.0}/PyViCare/PyViCareCachedService.py +13 -3
- {pyvicare-2.56.0 → pyvicare-2.57.0}/PyViCare/PyViCareDeviceConfig.py +29 -4
- {pyvicare-2.56.0 → pyvicare-2.57.0}/PyViCare/PyViCareHeatPump.py +36 -1
- {pyvicare-2.56.0 → pyvicare-2.57.0}/PyViCare/PyViCareHeatingDevice.py +10 -0
- {pyvicare-2.56.0 → pyvicare-2.57.0}/PyViCare/PyViCareUtils.py +12 -0
- {pyvicare-2.56.0 → pyvicare-2.57.0}/PyViCare/PyViCareVentilationDevice.py +16 -0
- {pyvicare-2.56.0 → pyvicare-2.57.0}/pyproject.toml +3 -2
- {pyvicare-2.56.0 → pyvicare-2.57.0}/LICENSE +0 -0
- {pyvicare-2.56.0 → pyvicare-2.57.0}/PyViCare/Feature.py +0 -0
- {pyvicare-2.56.0 → pyvicare-2.57.0}/PyViCare/PyViCareBrowserOAuthManager.py +0 -0
- {pyvicare-2.56.0 → pyvicare-2.57.0}/PyViCare/PyViCareDevice.py +0 -0
- {pyvicare-2.56.0 → pyvicare-2.57.0}/PyViCare/PyViCareElectricalEnergySystem.py +0 -0
- {pyvicare-2.56.0 → pyvicare-2.57.0}/PyViCare/PyViCareFloorHeating.py +0 -0
- {pyvicare-2.56.0 → pyvicare-2.57.0}/PyViCare/PyViCareFuelCell.py +0 -0
- {pyvicare-2.56.0 → pyvicare-2.57.0}/PyViCare/PyViCareGateway.py +0 -0
- {pyvicare-2.56.0 → pyvicare-2.57.0}/PyViCare/PyViCareGazBoiler.py +0 -0
- {pyvicare-2.56.0 → pyvicare-2.57.0}/PyViCare/PyViCareHeatCurveCalculation.py +0 -0
- {pyvicare-2.56.0 → pyvicare-2.57.0}/PyViCare/PyViCareHybrid.py +0 -0
- {pyvicare-2.56.0 → pyvicare-2.57.0}/PyViCare/PyViCareOAuthManager.py +0 -0
- {pyvicare-2.56.0 → pyvicare-2.57.0}/PyViCare/PyViCareOilBoiler.py +0 -0
- {pyvicare-2.56.0 → pyvicare-2.57.0}/PyViCare/PyViCarePelletsBoiler.py +0 -0
- {pyvicare-2.56.0 → pyvicare-2.57.0}/PyViCare/PyViCareRadiatorActuator.py +0 -0
- {pyvicare-2.56.0 → pyvicare-2.57.0}/PyViCare/PyViCareRepeater.py +0 -0
- {pyvicare-2.56.0 → pyvicare-2.57.0}/PyViCare/PyViCareRoomSensor.py +0 -0
- {pyvicare-2.56.0 → pyvicare-2.57.0}/PyViCare/PyViCareService.py +0 -0
- {pyvicare-2.56.0 → pyvicare-2.57.0}/PyViCare/__init__.py +0 -0
- {pyvicare-2.56.0 → pyvicare-2.57.0}/README.md +0 -0
|
@@ -61,7 +61,7 @@ class PyViCare:
|
|
|
61
61
|
|
|
62
62
|
logger.info("Device found: %s", device.modelId)
|
|
63
63
|
|
|
64
|
-
yield PyViCareDeviceConfig(service, device.id, device.modelId, device.status)
|
|
64
|
+
yield PyViCareDeviceConfig(service, device.id, device.modelId, device.status, device.deviceType, device.roles)
|
|
65
65
|
|
|
66
66
|
|
|
67
67
|
class DictWrap(object):
|
|
@@ -7,6 +7,7 @@ from authlib.integrations.requests_client import OAuth2Session
|
|
|
7
7
|
|
|
8
8
|
from PyViCare import Feature
|
|
9
9
|
from PyViCare.PyViCareUtils import (PyViCareCommandError,
|
|
10
|
+
PyViCareDeviceCommunicationError,
|
|
10
11
|
PyViCareInternalServerError,
|
|
11
12
|
PyViCareRateLimitError)
|
|
12
13
|
|
|
@@ -39,6 +40,7 @@ class AbstractViCareOAuthManager:
|
|
|
39
40
|
logger.debug("Response to get request: %s", response)
|
|
40
41
|
self.__handle_expired_token(response)
|
|
41
42
|
self.__handle_rate_limit(response)
|
|
43
|
+
self.__handle_device_communication_error(response)
|
|
42
44
|
self.__handle_server_error(response)
|
|
43
45
|
return response
|
|
44
46
|
except TokenExpiredError:
|
|
@@ -59,6 +61,10 @@ class AbstractViCareOAuthManager:
|
|
|
59
61
|
if ("statusCode" in response and response["statusCode"] == 429):
|
|
60
62
|
raise PyViCareRateLimitError(response)
|
|
61
63
|
|
|
64
|
+
def __handle_device_communication_error(self, response):
|
|
65
|
+
if ("errorType" in response and response["errorType"] == "DEVICE_COMMUNICATION_ERROR"):
|
|
66
|
+
raise PyViCareDeviceCommunicationError(response)
|
|
67
|
+
|
|
62
68
|
def __handle_server_error(self, response):
|
|
63
69
|
if ("statusCode" in response and response["statusCode"] >= 500):
|
|
64
70
|
raise PyViCareInternalServerError(response)
|
|
@@ -5,7 +5,10 @@ from typing import Any, List
|
|
|
5
5
|
from PyViCare.PyViCareAbstractOAuthManager import AbstractViCareOAuthManager
|
|
6
6
|
from PyViCare.PyViCareService import (ViCareDeviceAccessor, ViCareService,
|
|
7
7
|
readFeature)
|
|
8
|
-
from PyViCare.PyViCareUtils import
|
|
8
|
+
from PyViCare.PyViCareUtils import (PyViCareDeviceCommunicationError,
|
|
9
|
+
PyViCareInvalidDataError,
|
|
10
|
+
PyViCareInternalServerError,
|
|
11
|
+
ViCareTimer)
|
|
9
12
|
|
|
10
13
|
logger = logging.getLogger('ViCare')
|
|
11
14
|
logger.addHandler(logging.NullHandler())
|
|
@@ -33,13 +36,20 @@ class ViCareCachedService(ViCareService):
|
|
|
33
36
|
def __get_or_update_cache(self):
|
|
34
37
|
with self.__lock:
|
|
35
38
|
if self.is_cache_invalid():
|
|
36
|
-
# we always
|
|
39
|
+
# we always set the cache time before we fetch the data
|
|
37
40
|
# to avoid consuming all the api calls if the api is down
|
|
38
41
|
# see https://github.com/home-assistant/core/issues/67052
|
|
39
42
|
# we simply return the old cache in this case
|
|
40
43
|
self.__cacheTime = ViCareTimer().now()
|
|
41
44
|
|
|
42
|
-
|
|
45
|
+
try:
|
|
46
|
+
data = self.fetch_all_features()
|
|
47
|
+
except (PyViCareDeviceCommunicationError, PyViCareInternalServerError) as e:
|
|
48
|
+
if self.__cache is not None:
|
|
49
|
+
logger.warning("API error, returning stale cache: %s", e)
|
|
50
|
+
return self.__cache
|
|
51
|
+
raise
|
|
52
|
+
|
|
43
53
|
if "data" not in data:
|
|
44
54
|
logger.error("Missing 'data' property when fetching data.")
|
|
45
55
|
raise PyViCareInvalidDataError(data)
|
|
@@ -22,11 +22,14 @@ logger.addHandler(logging.NullHandler())
|
|
|
22
22
|
|
|
23
23
|
|
|
24
24
|
class PyViCareDeviceConfig:
|
|
25
|
-
|
|
25
|
+
# pylint: disable=too-many-arguments,too-many-positional-arguments
|
|
26
|
+
def __init__(self, service, device_id, device_model, status, device_type=None, roles=None):
|
|
26
27
|
self.service = service
|
|
27
28
|
self.device_id = device_id
|
|
28
29
|
self.device_model = device_model
|
|
29
30
|
self.status = status
|
|
31
|
+
self.device_type = device_type
|
|
32
|
+
self.roles = roles if roles is not None else []
|
|
30
33
|
|
|
31
34
|
def asGeneric(self):
|
|
32
35
|
return HeatingDevice(self.service)
|
|
@@ -85,6 +88,15 @@ class PyViCareDeviceConfig:
|
|
|
85
88
|
def isOnline(self):
|
|
86
89
|
return self.status == "Online"
|
|
87
90
|
|
|
91
|
+
def getDeviceType(self):
|
|
92
|
+
return self.device_type
|
|
93
|
+
|
|
94
|
+
def getRoles(self):
|
|
95
|
+
return self.roles
|
|
96
|
+
|
|
97
|
+
def isGateway(self):
|
|
98
|
+
return self.service._isGateway() # pylint: disable=protected-access
|
|
99
|
+
|
|
88
100
|
# see: https://vitodata300.viessmann.com/vd300/ApplicationHelp/VD300/1031_de_DE/Ger%C3%A4teliste.html
|
|
89
101
|
def asAutoDetectDevice(self):
|
|
90
102
|
device_types = [
|
|
@@ -122,12 +134,25 @@ class PyViCareDeviceConfig:
|
|
|
122
134
|
return self.service.fetch_all_features()
|
|
123
135
|
|
|
124
136
|
def dump_secure(self, flat=False):
|
|
137
|
+
raw_data = self.get_raw_json()
|
|
138
|
+
device_info = {
|
|
139
|
+
"id": self.device_id,
|
|
140
|
+
"modelId": self.device_model,
|
|
141
|
+
"type": self.device_type,
|
|
142
|
+
"status": self.status,
|
|
143
|
+
"roles": self.roles
|
|
144
|
+
}
|
|
145
|
+
output = {
|
|
146
|
+
"device": device_info,
|
|
147
|
+
"data": raw_data['data']
|
|
148
|
+
}
|
|
149
|
+
|
|
125
150
|
if flat:
|
|
126
|
-
inner = ',\n'.join([json.dumps(x, sort_keys=True) for x in
|
|
127
|
-
outer = json.dumps({'data': ['placeholder']}, indent=0)
|
|
151
|
+
inner = ',\n'.join([json.dumps(x, sort_keys=True) for x in output['data']])
|
|
152
|
+
outer = json.dumps({'device': output['device'], 'data': ['placeholder']}, indent=0, sort_keys=True)
|
|
128
153
|
dumpJSON = outer.replace('"placeholder"', inner)
|
|
129
154
|
else:
|
|
130
|
-
dumpJSON = json.dumps(
|
|
155
|
+
dumpJSON = json.dumps(output, indent=4, sort_keys=True)
|
|
131
156
|
|
|
132
157
|
def repl(m):
|
|
133
158
|
return m.group(1) + ('#' * len(m.group(2))) + m.group(3)
|
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
|
+
from contextlib import suppress
|
|
2
3
|
from typing import Any, List
|
|
3
4
|
from deprecated import deprecated
|
|
4
5
|
|
|
5
6
|
from PyViCare.PyViCareHeatingDevice import HeatingDevice, HeatingDeviceWithComponent
|
|
6
|
-
from PyViCare.PyViCareUtils import
|
|
7
|
+
from PyViCare.PyViCareUtils import (PyViCareNotSupportedFeatureError,
|
|
8
|
+
handleAPICommandErrors, handleNotSupported)
|
|
7
9
|
from PyViCare.PyViCareVentilationDevice import VentilationDevice
|
|
8
10
|
|
|
9
11
|
|
|
@@ -320,6 +322,39 @@ class HeatPump(HeatingDevice, VentilationDevice):
|
|
|
320
322
|
def getHeatingRodPowerConsumptionTotalThisYear(self) -> float:
|
|
321
323
|
return float(self.getProperty("heating.heatingRod.power.consumption.total")["properties"]["year"]["value"][0])
|
|
322
324
|
|
|
325
|
+
# Cooling circuits
|
|
326
|
+
@property
|
|
327
|
+
def coolingCircuits(self) -> List[CoolingCircuit]:
|
|
328
|
+
return [self.getCoolingCircuit(x) for x in self.getAvailableCoolingCircuits()]
|
|
329
|
+
|
|
330
|
+
def getCoolingCircuit(self, circuit) -> CoolingCircuit:
|
|
331
|
+
return CoolingCircuit(self, circuit)
|
|
332
|
+
|
|
333
|
+
def getAvailableCoolingCircuits(self):
|
|
334
|
+
"""Detect available cooling circuits (0, 1, 2, etc.)."""
|
|
335
|
+
available = []
|
|
336
|
+
for circuit in ['0', '1', '2', '3']:
|
|
337
|
+
with suppress(KeyError, PyViCareNotSupportedFeatureError):
|
|
338
|
+
if self.getProperty(f"heating.coolingCircuits.{circuit}.type") is not None:
|
|
339
|
+
available.append(circuit)
|
|
340
|
+
return available
|
|
341
|
+
|
|
342
|
+
|
|
343
|
+
class CoolingCircuit(HeatingDeviceWithComponent):
|
|
344
|
+
"""Cooling circuit component for heat pumps with cooling capability."""
|
|
345
|
+
|
|
346
|
+
@property
|
|
347
|
+
def circuit(self) -> str:
|
|
348
|
+
return self.component
|
|
349
|
+
|
|
350
|
+
@handleNotSupported
|
|
351
|
+
def getType(self) -> str:
|
|
352
|
+
return str(self.getProperty(f"heating.coolingCircuits.{self.circuit}.type")["properties"]["value"]["value"])
|
|
353
|
+
|
|
354
|
+
@handleNotSupported
|
|
355
|
+
def getReverseActive(self) -> bool:
|
|
356
|
+
return bool(self.getProperty(f"heating.coolingCircuits.{self.circuit}.reverse")["properties"]["active"]["value"])
|
|
357
|
+
|
|
323
358
|
|
|
324
359
|
class Compressor(HeatingDeviceWithComponent):
|
|
325
360
|
|
|
@@ -198,6 +198,16 @@ class HeatingDevice(Device):
|
|
|
198
198
|
return self.setProperty("heating.dhw.temperature.temp2", "setTargetTemperature",
|
|
199
199
|
{"temperature": int(temperature)})
|
|
200
200
|
|
|
201
|
+
@handleNotSupported
|
|
202
|
+
def getDomesticHotWaterOperatingModes(self):
|
|
203
|
+
return self.getProperty("heating.dhw.operating.modes.active")[
|
|
204
|
+
"commands"]["setMode"]["params"]["mode"]["constraints"]["enum"]
|
|
205
|
+
|
|
206
|
+
@handleNotSupported
|
|
207
|
+
def getDomesticHotWaterActiveOperatingMode(self):
|
|
208
|
+
return self.getProperty("heating.dhw.operating.modes.active")[
|
|
209
|
+
"properties"]["value"]["value"]
|
|
210
|
+
|
|
201
211
|
@handleAPICommandErrors
|
|
202
212
|
def setDomesticHotWaterOperatingMode(self, mode):
|
|
203
213
|
return self.setProperty("heating.dhw.operating.modes.active", "setMode",
|
|
@@ -105,6 +105,18 @@ class PyViCareInvalidDataError(Exception):
|
|
|
105
105
|
pass
|
|
106
106
|
|
|
107
107
|
|
|
108
|
+
class PyViCareDeviceCommunicationError(Exception):
|
|
109
|
+
def __init__(self, response):
|
|
110
|
+
extended_payload = response.get("extendedPayload", {})
|
|
111
|
+
reason = extended_payload.get("reason", "Unknown")
|
|
112
|
+
|
|
113
|
+
msg = f'Device communication error. Reason: {reason}'
|
|
114
|
+
|
|
115
|
+
super().__init__(self, msg)
|
|
116
|
+
self.message = msg
|
|
117
|
+
self.reason = reason
|
|
118
|
+
|
|
119
|
+
|
|
108
120
|
class PyViCareRateLimitError(Exception):
|
|
109
121
|
|
|
110
122
|
def __init__(self, response):
|
|
@@ -289,3 +289,19 @@ class VentilationDevice(Device):
|
|
|
289
289
|
@handleNotSupported
|
|
290
290
|
def getExhaustVolumeFlow(self) -> int:
|
|
291
291
|
return int(self.getProperty("ventilation.volumeFlow.current.output")["properties"]["value"]["value"])
|
|
292
|
+
|
|
293
|
+
@handleNotSupported
|
|
294
|
+
def getConfiguredLevelOneVolumeFlow(self) -> int:
|
|
295
|
+
return int(self.getProperty("ventilation.levels.levelOne")["properties"]["volumeFlow"]["value"])
|
|
296
|
+
|
|
297
|
+
@handleNotSupported
|
|
298
|
+
def getConfiguredLevelTwoVolumeFlow(self) -> int:
|
|
299
|
+
return int(self.getProperty("ventilation.levels.levelTwo")["properties"]["volumeFlow"]["value"])
|
|
300
|
+
|
|
301
|
+
@handleNotSupported
|
|
302
|
+
def getConfiguredLevelThreeVolumeFlow(self) -> int:
|
|
303
|
+
return int(self.getProperty("ventilation.levels.levelThree")["properties"]["volumeFlow"]["value"])
|
|
304
|
+
|
|
305
|
+
@handleNotSupported
|
|
306
|
+
def getConfiguredLevelFourVolumeFlow(self) -> int:
|
|
307
|
+
return int(self.getProperty("ventilation.levels.levelFour")["properties"]["volumeFlow"]["value"])
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[tool.poetry]
|
|
2
2
|
name = "PyViCare"
|
|
3
|
-
version = "2.
|
|
3
|
+
version = "2.57.0"
|
|
4
4
|
description = "Library to communicate with the Viessmann ViCare API."
|
|
5
5
|
authors = [
|
|
6
6
|
"Simon Gillet <mail+pyvicare@gillet.ninja>"
|
|
@@ -26,6 +26,7 @@ classifiers = [
|
|
|
26
26
|
"Programming Language :: Python :: 3.10",
|
|
27
27
|
"Programming Language :: Python :: 3.11",
|
|
28
28
|
"Programming Language :: Python :: 3.12",
|
|
29
|
+
"Programming Language :: Python :: 3.13",
|
|
29
30
|
"Programming Language :: Python :: 3",
|
|
30
31
|
"License :: OSI Approved :: Apache Software License",
|
|
31
32
|
"Topic :: Software Development :: Libraries :: Python Modules",
|
|
@@ -51,7 +52,7 @@ mypy = "^1.11.2"
|
|
|
51
52
|
pylint = "^4.0.0"
|
|
52
53
|
pytest = "^9.0.0"
|
|
53
54
|
pytest-cov = "^7.0.0"
|
|
54
|
-
ruff = "^0.
|
|
55
|
+
ruff = "^0.15.0"
|
|
55
56
|
types-deprecated = "^1.2.15.20241117"
|
|
56
57
|
types-requests = ">=2.31"
|
|
57
58
|
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|