PyViCare 2.60.2__tar.gz → 2.62.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.60.2 → pyvicare-2.62.0}/PKG-INFO +2 -2
- {pyvicare-2.60.2 → pyvicare-2.62.0}/PyViCare/PyViCare.py +26 -36
- pyvicare-2.62.0/PyViCare/PyViCareCachedService.py +23 -0
- pyvicare-2.62.0/PyViCare/PyViCareCachedServiceBase.py +87 -0
- pyvicare-2.62.0/PyViCare/PyViCareCachedServiceViaGateway.py +30 -0
- {pyvicare-2.60.2 → pyvicare-2.62.0}/PyViCare/PyViCareDevice.py +8 -6
- {pyvicare-2.60.2 → pyvicare-2.62.0}/PyViCare/PyViCareDeviceConfig.py +55 -35
- {pyvicare-2.60.2 → pyvicare-2.62.0}/PyViCare/PyViCareFuelCell.py +1 -1
- {pyvicare-2.60.2 → pyvicare-2.62.0}/PyViCare/PyViCareGazBoiler.py +1 -5
- {pyvicare-2.60.2 → pyvicare-2.62.0}/PyViCare/PyViCareHeatingDevice.py +19 -4
- {pyvicare-2.60.2 → pyvicare-2.62.0}/PyViCare/PyViCareOilBoiler.py +1 -1
- {pyvicare-2.60.2 → pyvicare-2.62.0}/PyViCare/PyViCarePelletsBoiler.py +1 -1
- {pyvicare-2.60.2 → pyvicare-2.62.0}/PyViCare/PyViCareRoomControl.py +22 -38
- pyvicare-2.62.0/PyViCare/PyViCareRoomSensor.py +17 -0
- {pyvicare-2.60.2 → pyvicare-2.62.0}/PyViCare/PyViCareService.py +26 -20
- pyvicare-2.62.0/PyViCare/PyViCareServiceViaGateway.py +38 -0
- {pyvicare-2.60.2 → pyvicare-2.62.0}/PyViCare/PyViCareVentilationDevice.py +29 -0
- pyvicare-2.62.0/PyViCare/PyViCareWaterTreatment.py +141 -0
- {pyvicare-2.60.2 → pyvicare-2.62.0}/pyproject.toml +8 -8
- pyvicare-2.60.2/PyViCare/PyViCareCachedService.py +0 -72
- pyvicare-2.60.2/PyViCare/PyViCareRoomSensor.py +0 -132
- {pyvicare-2.60.2 → pyvicare-2.62.0}/LICENSE +0 -0
- {pyvicare-2.60.2 → pyvicare-2.62.0}/PyViCare/Feature.py +0 -0
- {pyvicare-2.60.2 → pyvicare-2.62.0}/PyViCare/PyViCareAbstractOAuthManager.py +0 -0
- {pyvicare-2.60.2 → pyvicare-2.62.0}/PyViCare/PyViCareBrowserOAuthManager.py +0 -0
- {pyvicare-2.60.2 → pyvicare-2.62.0}/PyViCare/PyViCareElectricalEnergySystem.py +0 -0
- {pyvicare-2.60.2 → pyvicare-2.62.0}/PyViCare/PyViCareFloorHeating.py +0 -0
- {pyvicare-2.60.2 → pyvicare-2.62.0}/PyViCare/PyViCareGateway.py +0 -0
- {pyvicare-2.60.2 → pyvicare-2.62.0}/PyViCare/PyViCareHeatCurveCalculation.py +0 -0
- {pyvicare-2.60.2 → pyvicare-2.62.0}/PyViCare/PyViCareHeatPump.py +0 -0
- {pyvicare-2.60.2 → pyvicare-2.62.0}/PyViCare/PyViCareHybrid.py +0 -0
- {pyvicare-2.60.2 → pyvicare-2.62.0}/PyViCare/PyViCareOAuthManager.py +0 -0
- {pyvicare-2.60.2 → pyvicare-2.62.0}/PyViCare/PyViCareRadiatorActuator.py +0 -0
- {pyvicare-2.60.2 → pyvicare-2.62.0}/PyViCare/PyViCareRepeater.py +0 -0
- {pyvicare-2.60.2 → pyvicare-2.62.0}/PyViCare/PyViCareUtils.py +0 -0
- {pyvicare-2.60.2 → pyvicare-2.62.0}/PyViCare/__init__.py +0 -0
- {pyvicare-2.60.2 → pyvicare-2.62.0}/README.md +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: PyViCare
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.62.0
|
|
4
4
|
Summary: Library to communicate with the Viessmann ViCare API.
|
|
5
5
|
License: Apache-2.0
|
|
6
6
|
License-File: LICENSE
|
|
@@ -22,7 +22,7 @@ Classifier: Programming Language :: Python :: 3.13
|
|
|
22
22
|
Classifier: Programming Language :: Python :: 3.14
|
|
23
23
|
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
24
24
|
Requires-Dist: authlib (>1.2.0)
|
|
25
|
-
Requires-Dist: deprecated (>=1.
|
|
25
|
+
Requires-Dist: deprecated (>=1.3.0,<2.0.0)
|
|
26
26
|
Requires-Dist: requests (>=2.31.0)
|
|
27
27
|
Project-URL: Bug Tracker, https://github.com/openviess/PyViCare/issues
|
|
28
28
|
Project-URL: Changelog, https://github.com/openviess/PyViCare/releases
|
|
@@ -4,10 +4,11 @@ from datetime import datetime
|
|
|
4
4
|
from PyViCare.PyViCareAbstractOAuthManager import AbstractViCareOAuthManager
|
|
5
5
|
from PyViCare.PyViCareBrowserOAuthManager import ViCareBrowserOAuthManager
|
|
6
6
|
from PyViCare.PyViCareCachedService import ViCareCachedService
|
|
7
|
+
from PyViCare.PyViCareCachedServiceViaGateway import ViCareCachedServiceViaGateway
|
|
7
8
|
from PyViCare.PyViCareDeviceConfig import PyViCareDeviceConfig
|
|
8
9
|
from PyViCare.PyViCareOAuthManager import ViCareOAuthManager
|
|
9
|
-
from PyViCare.PyViCareRoomControl import RoomControl
|
|
10
10
|
from PyViCare.PyViCareService import ViCareDeviceAccessor, ViCareService
|
|
11
|
+
from PyViCare.PyViCareServiceViaGateway import ViCareServiceViaGateway
|
|
11
12
|
from PyViCare.PyViCareUtils import PyViCareInvalidDataError
|
|
12
13
|
|
|
13
14
|
logger = logging.getLogger(__name__)
|
|
@@ -16,12 +17,25 @@ logger.addHandler(logging.NullHandler())
|
|
|
16
17
|
|
|
17
18
|
class PyViCare:
|
|
18
19
|
""""Viessmann ViCare API Python tools"""
|
|
20
|
+
viaGateway = False
|
|
21
|
+
|
|
19
22
|
def __init__(self) -> None:
|
|
20
23
|
self.cacheDuration = 60
|
|
21
24
|
|
|
22
25
|
def setCacheDuration(self, cache_duration):
|
|
23
26
|
self.cacheDuration = int(cache_duration)
|
|
24
27
|
|
|
28
|
+
def loadViaGateway(self, via_gateway: bool = True) -> None:
|
|
29
|
+
"""Opt in to per-gateway bulk fetching.
|
|
30
|
+
|
|
31
|
+
When enabled, ONE service instance per gateway serves all devices on
|
|
32
|
+
that gateway via the bulk endpoint
|
|
33
|
+
`/features/installations/{id}/gateways/{serial}/features?includeDevicesFeatures=true`,
|
|
34
|
+
instead of one HTTP call per device per refresh. Must be called
|
|
35
|
+
before initWith* (services are wired during __loadInstallations).
|
|
36
|
+
"""
|
|
37
|
+
self.viaGateway = via_gateway
|
|
38
|
+
|
|
25
39
|
def initWithCredentials(self, username: str, password: str, client_id: str, token_file: str):
|
|
26
40
|
self.initWithExternalOAuth(ViCareOAuthManager(
|
|
27
41
|
username, password, client_id, token_file))
|
|
@@ -33,10 +47,15 @@ class PyViCare:
|
|
|
33
47
|
def initWithBrowserOAuth(self, client_id: str, token_file: str) -> None:
|
|
34
48
|
self.initWithExternalOAuth(ViCareBrowserOAuthManager(client_id, token_file))
|
|
35
49
|
|
|
36
|
-
def __buildService(self,
|
|
50
|
+
def __buildService(self, roles):
|
|
37
51
|
if self.cacheDuration > 0:
|
|
38
|
-
return ViCareCachedService(self.oauth_manager,
|
|
39
|
-
return ViCareService(self.oauth_manager,
|
|
52
|
+
return ViCareCachedService(self.oauth_manager, roles, self.cacheDuration)
|
|
53
|
+
return ViCareService(self.oauth_manager, roles)
|
|
54
|
+
|
|
55
|
+
def __buildGatewayService(self):
|
|
56
|
+
if self.cacheDuration > 0:
|
|
57
|
+
return ViCareCachedServiceViaGateway(self.oauth_manager, self.cacheDuration)
|
|
58
|
+
return ViCareServiceViaGateway(self.oauth_manager)
|
|
40
59
|
|
|
41
60
|
def __loadInstallations(self):
|
|
42
61
|
installations = self.oauth_manager.get(
|
|
@@ -50,7 +69,6 @@ class PyViCare:
|
|
|
50
69
|
self.all_devices = list(self.__extract_all_devices())
|
|
51
70
|
self.devices = [d for d in self.all_devices
|
|
52
71
|
if d.device_type in self.SUPPORTED_DEVICE_TYPES]
|
|
53
|
-
self.__enrichZigbeeDevices()
|
|
54
72
|
|
|
55
73
|
SUPPORTED_DEVICE_TYPES = [
|
|
56
74
|
"heating", "zigbee", "vitoconnect", "electricityStorage",
|
|
@@ -60,43 +78,15 @@ class PyViCare:
|
|
|
60
78
|
def __extract_all_devices(self):
|
|
61
79
|
for installation in self.installations:
|
|
62
80
|
for gateway in installation.gateways:
|
|
81
|
+
shared_service = self.__buildGatewayService() if self.viaGateway else None
|
|
63
82
|
for device in gateway.devices:
|
|
64
83
|
accessor = ViCareDeviceAccessor(
|
|
65
84
|
installation.id, gateway.serial, device.id)
|
|
66
|
-
service = self.__buildService(
|
|
85
|
+
service = shared_service if shared_service is not None else self.__buildService(device.roles)
|
|
67
86
|
|
|
68
87
|
logger.info("Device found: %s (type=%s)", device.modelId, device.deviceType)
|
|
69
88
|
|
|
70
|
-
yield PyViCareDeviceConfig(
|
|
71
|
-
|
|
72
|
-
def __enrichZigbeeDevices(self):
|
|
73
|
-
"""Enrich Zigbee devices with sensor data from RoomControl.
|
|
74
|
-
|
|
75
|
-
Viessmann moved temperature/humidity data from physical Zigbee
|
|
76
|
-
sensors to the RoomControl virtual device. This reverses that
|
|
77
|
-
mapping by cross-referencing RoomControl actors with Zigbee
|
|
78
|
-
device IDs.
|
|
79
|
-
"""
|
|
80
|
-
devices_by_id = {device_config.device_id: device_config for device_config in self.devices}
|
|
81
|
-
|
|
82
|
-
for device_config in self.devices:
|
|
83
|
-
if device_config.device_type != "roomControl":
|
|
84
|
-
continue
|
|
85
|
-
|
|
86
|
-
room_control = RoomControl(device_config.service)
|
|
87
|
-
try:
|
|
88
|
-
actor_map = room_control.buildActorRoomMap()
|
|
89
|
-
except Exception: # pylint: disable=broad-exception-caught
|
|
90
|
-
logger.debug("Could not build actor map for %s", device_config.getModel(), exc_info=True)
|
|
91
|
-
continue
|
|
92
|
-
|
|
93
|
-
for device_id, room_id in actor_map.items():
|
|
94
|
-
zigbee_config = devices_by_id.get(device_id)
|
|
95
|
-
if zigbee_config is None:
|
|
96
|
-
continue
|
|
97
|
-
zigbee_config.setRoomControlEnrichment(room_control, room_id)
|
|
98
|
-
logger.info("Enriched %s with room %s data from %s",
|
|
99
|
-
zigbee_config.device_id, room_id, device_config.getModel())
|
|
89
|
+
yield PyViCareDeviceConfig(accessor, service, device.modelId, device.status, device.deviceType, device.roles)
|
|
100
90
|
|
|
101
91
|
|
|
102
92
|
class DictWrap(object):
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
from typing import Any, List
|
|
3
|
+
|
|
4
|
+
from PyViCare.PyViCareAbstractOAuthManager import AbstractViCareOAuthManager
|
|
5
|
+
from PyViCare.PyViCareCachedServiceBase import ViCareCachedServiceBase
|
|
6
|
+
from PyViCare.PyViCareService import ViCareDeviceAccessor, ViCareService
|
|
7
|
+
|
|
8
|
+
logger = logging.getLogger(__name__)
|
|
9
|
+
logger.addHandler(logging.NullHandler())
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class ViCareCachedService(ViCareCachedServiceBase, ViCareService):
|
|
13
|
+
|
|
14
|
+
def __init__(self, oauth_manager: AbstractViCareOAuthManager, roles: List[str], cacheDuration: int) -> None:
|
|
15
|
+
ViCareService.__init__(self, oauth_manager, roles)
|
|
16
|
+
self._init_cache(cacheDuration)
|
|
17
|
+
|
|
18
|
+
def _fetch_uncached(self, accessor: ViCareDeviceAccessor) -> Any:
|
|
19
|
+
return ViCareService.fetch_all_features(self, accessor)
|
|
20
|
+
|
|
21
|
+
def _extract_entities(self, data: dict, accessor: ViCareDeviceAccessor) -> list[dict[str, Any]]:
|
|
22
|
+
entities: list[dict[str, Any]] = data["data"]
|
|
23
|
+
return entities
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
import threading
|
|
3
|
+
from datetime import datetime
|
|
4
|
+
from typing import Any, Optional
|
|
5
|
+
|
|
6
|
+
from PyViCare.PyViCareService import (ViCareDeviceAccessor, ViCareService,
|
|
7
|
+
readFeature)
|
|
8
|
+
from PyViCare.PyViCareUtils import (PyViCareDeviceCommunicationError,
|
|
9
|
+
PyViCareInternalServerError,
|
|
10
|
+
PyViCareInvalidDataError,
|
|
11
|
+
PyViCareNotPaidForError,
|
|
12
|
+
PyViCareNotSupportedFeatureError,
|
|
13
|
+
ViCareTimer)
|
|
14
|
+
|
|
15
|
+
logger = logging.getLogger(__name__)
|
|
16
|
+
logger.addHandler(logging.NullHandler())
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class ViCareCachedServiceBase(ViCareService):
|
|
20
|
+
"""Time-based caching shared by the per-device and per-gateway services.
|
|
21
|
+
|
|
22
|
+
Subclasses provide `_fetch_uncached` (the HTTP fetch) and `_extract_entities`
|
|
23
|
+
(pick this device's features out of the cached payload).
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
def _init_cache(self, cacheDuration: int) -> None:
|
|
27
|
+
self._cacheDuration = cacheDuration
|
|
28
|
+
self._cache: Optional[dict] = None
|
|
29
|
+
self._cacheTime: Optional[datetime] = None
|
|
30
|
+
self._cacheLock = threading.Lock()
|
|
31
|
+
|
|
32
|
+
def getProperty(self, accessor: ViCareDeviceAccessor, property_name: str) -> Any:
|
|
33
|
+
data = self._get_or_update_cache(accessor)
|
|
34
|
+
entities = self._extract_entities(data, accessor)
|
|
35
|
+
return readFeature(entities, property_name)
|
|
36
|
+
|
|
37
|
+
def fetch_all_features(self, accessor: ViCareDeviceAccessor) -> Any:
|
|
38
|
+
# cached, so a caller refreshing with fetch_all_features() warms the
|
|
39
|
+
# cache the following getProperty calls read from
|
|
40
|
+
return self._get_or_update_cache(accessor)
|
|
41
|
+
|
|
42
|
+
def setProperty(self, accessor: ViCareDeviceAccessor, property_name: str, action: str, data: Any) -> Any:
|
|
43
|
+
response = super().setProperty(accessor, property_name, action, data)
|
|
44
|
+
self.clear_cache()
|
|
45
|
+
return response
|
|
46
|
+
|
|
47
|
+
def _get_or_update_cache(self, accessor: ViCareDeviceAccessor):
|
|
48
|
+
with self._cacheLock:
|
|
49
|
+
if self.is_cache_invalid():
|
|
50
|
+
# we always set the cache time before we fetch the data
|
|
51
|
+
# to avoid consuming all the api calls if the api is down
|
|
52
|
+
# see https://github.com/home-assistant/core/issues/67052
|
|
53
|
+
# we simply return the old cache in this case
|
|
54
|
+
self._cacheTime = ViCareTimer().now()
|
|
55
|
+
|
|
56
|
+
try:
|
|
57
|
+
data = self._fetch_uncached(accessor)
|
|
58
|
+
except PyViCareNotPaidForError as e:
|
|
59
|
+
logger.error("Viessmann API denied access (PACKAGE_NOT_PAID_FOR). Features unavailable: %s", e)
|
|
60
|
+
if self._cache is not None:
|
|
61
|
+
return self._cache
|
|
62
|
+
raise PyViCareNotSupportedFeatureError("PACKAGE_NOT_PAID_FOR")
|
|
63
|
+
except (PyViCareDeviceCommunicationError, PyViCareInternalServerError) as e:
|
|
64
|
+
if self._cache is not None:
|
|
65
|
+
logger.warning("API error, returning stale cache: %s", e)
|
|
66
|
+
return self._cache
|
|
67
|
+
raise
|
|
68
|
+
|
|
69
|
+
if "data" not in data:
|
|
70
|
+
logger.error("Missing 'data' property when fetching data.")
|
|
71
|
+
raise PyViCareInvalidDataError(data)
|
|
72
|
+
self._cache = data
|
|
73
|
+
return self._cache
|
|
74
|
+
|
|
75
|
+
def is_cache_invalid(self) -> bool:
|
|
76
|
+
return self._cache is None or self._cacheTime is None or (ViCareTimer().now() - self._cacheTime).seconds > self._cacheDuration
|
|
77
|
+
|
|
78
|
+
def clear_cache(self):
|
|
79
|
+
with self._cacheLock:
|
|
80
|
+
self._cache = None
|
|
81
|
+
self._cacheTime = None
|
|
82
|
+
|
|
83
|
+
def _fetch_uncached(self, accessor: ViCareDeviceAccessor) -> Any:
|
|
84
|
+
raise NotImplementedError
|
|
85
|
+
|
|
86
|
+
def _extract_entities(self, data: dict, accessor: ViCareDeviceAccessor) -> list[dict[str, Any]]:
|
|
87
|
+
raise NotImplementedError
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
from typing import Any
|
|
3
|
+
|
|
4
|
+
from PyViCare.PyViCareAbstractOAuthManager import AbstractViCareOAuthManager
|
|
5
|
+
from PyViCare.PyViCareCachedServiceBase import ViCareCachedServiceBase
|
|
6
|
+
from PyViCare.PyViCareService import ViCareDeviceAccessor
|
|
7
|
+
from PyViCare.PyViCareServiceViaGateway import (
|
|
8
|
+
ViCareServiceViaGateway, filter_features_for_device)
|
|
9
|
+
|
|
10
|
+
logger = logging.getLogger(__name__)
|
|
11
|
+
logger.addHandler(logging.NullHandler())
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class ViCareCachedServiceViaGateway(ViCareCachedServiceBase, ViCareServiceViaGateway):
|
|
15
|
+
"""Cached variant of the gateway bulk-fetch service.
|
|
16
|
+
|
|
17
|
+
One instance is shared across all devices on the gateway; every device's
|
|
18
|
+
getProperty reads from the same cached bulk payload. setProperty clears the
|
|
19
|
+
whole cache.
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
def __init__(self, oauth_manager: AbstractViCareOAuthManager, cacheDuration: int) -> None: # pylint: disable=super-init-not-called
|
|
23
|
+
ViCareServiceViaGateway.__init__(self, oauth_manager)
|
|
24
|
+
self._init_cache(cacheDuration)
|
|
25
|
+
|
|
26
|
+
def _fetch_uncached(self, accessor: ViCareDeviceAccessor) -> Any:
|
|
27
|
+
return ViCareServiceViaGateway.fetch_all_features(self, accessor)
|
|
28
|
+
|
|
29
|
+
def _extract_entities(self, data: dict, accessor: ViCareDeviceAccessor) -> list[dict[str, Any]]:
|
|
30
|
+
return filter_features_for_device(data["data"], accessor.device_id)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
from typing import Any
|
|
2
2
|
|
|
3
|
-
from PyViCare.PyViCareService import ViCareService
|
|
3
|
+
from PyViCare.PyViCareService import ViCareDeviceAccessor, ViCareService, hasRoles
|
|
4
4
|
from PyViCare.PyViCareUtils import PyViCareNotSupportedFeatureError, handleAPICommandErrors, handleNotSupported
|
|
5
5
|
|
|
6
6
|
|
|
@@ -11,14 +11,16 @@ class Device:
|
|
|
11
11
|
Note that currently, a new token is generated for each run.
|
|
12
12
|
"""
|
|
13
13
|
|
|
14
|
-
def __init__(self, service: ViCareService) -> None:
|
|
14
|
+
def __init__(self, accessor: ViCareDeviceAccessor, service: ViCareService, roles: list[str] | None = None) -> None:
|
|
15
|
+
self.accessor = accessor
|
|
15
16
|
self.service = service
|
|
17
|
+
self.roles = roles if roles is not None else []
|
|
16
18
|
|
|
17
19
|
def getProperty(self, property_name: str) -> Any:
|
|
18
|
-
return self.service.getProperty(property_name)
|
|
20
|
+
return self.service.getProperty(self.accessor, property_name)
|
|
19
21
|
|
|
20
22
|
def setProperty(self, property_name: str, action: str, data: Any) -> Any:
|
|
21
|
-
return self.service.setProperty(property_name, action, data)
|
|
23
|
+
return self.service.setProperty(self.accessor, property_name, action, data)
|
|
22
24
|
|
|
23
25
|
@handleNotSupported
|
|
24
26
|
def getSerial(self):
|
|
@@ -29,10 +31,10 @@ class Device:
|
|
|
29
31
|
return list[Any](self.getProperty("device.messages.errors.raw")["properties"]["entries"]["value"])
|
|
30
32
|
|
|
31
33
|
def isLegacyDevice(self) -> bool:
|
|
32
|
-
return
|
|
34
|
+
return hasRoles(["type:legacy"], self.roles)
|
|
33
35
|
|
|
34
36
|
def isE3Device(self) -> bool:
|
|
35
|
-
return
|
|
37
|
+
return hasRoles(["type:E3"], self.roles)
|
|
36
38
|
|
|
37
39
|
def isDomesticHotWaterDevice(self):
|
|
38
40
|
return self._isTypeDevice("heating.dhw")
|
|
@@ -16,7 +16,11 @@ from PyViCare.PyViCareRoomSensor import RoomSensor
|
|
|
16
16
|
from PyViCare.PyViCareRepeater import Repeater
|
|
17
17
|
from PyViCare.PyViCareElectricalEnergySystem import ElectricalEnergySystem
|
|
18
18
|
from PyViCare.PyViCareGateway import Gateway
|
|
19
|
+
from PyViCare.PyViCareService import (ViCareDeviceAccessor, ViCareService,
|
|
20
|
+
hasRoles, is_gateway_role)
|
|
21
|
+
from PyViCare.PyViCareUtils import PyViCareNotPaidForError
|
|
19
22
|
from PyViCare.PyViCareVentilationDevice import VentilationDevice
|
|
23
|
+
from PyViCare.PyViCareWaterTreatment import WaterTreatment
|
|
20
24
|
|
|
21
25
|
logger = logging.getLogger(__name__)
|
|
22
26
|
logger.addHandler(logging.NullHandler())
|
|
@@ -24,74 +28,68 @@ logger.addHandler(logging.NullHandler())
|
|
|
24
28
|
|
|
25
29
|
class PyViCareDeviceConfig:
|
|
26
30
|
# pylint: disable=too-many-arguments,too-many-positional-arguments,too-many-instance-attributes
|
|
27
|
-
def __init__(self,
|
|
31
|
+
def __init__(self, accessor: ViCareDeviceAccessor, service: ViCareService, device_model, status, device_type=None, roles=None):
|
|
32
|
+
self.accessor = accessor
|
|
28
33
|
self.service = service
|
|
29
|
-
self.device_id = device_id
|
|
34
|
+
self.device_id = accessor.device_id
|
|
30
35
|
self.device_model = device_model
|
|
31
36
|
self.status = status
|
|
32
37
|
self.device_type = device_type
|
|
33
38
|
self.roles = roles if roles is not None else []
|
|
34
|
-
self._room_control = None
|
|
35
|
-
self._room_id = None
|
|
36
39
|
|
|
37
40
|
def asGeneric(self):
|
|
38
|
-
return HeatingDevice(self.service)
|
|
41
|
+
return HeatingDevice(self.accessor, self.service, self.roles)
|
|
39
42
|
|
|
40
43
|
def asGazBoiler(self):
|
|
41
|
-
return GazBoiler(self.service)
|
|
44
|
+
return GazBoiler(self.accessor, self.service, self.roles)
|
|
42
45
|
|
|
43
46
|
def asFuelCell(self):
|
|
44
|
-
return FuelCell(self.service)
|
|
47
|
+
return FuelCell(self.accessor, self.service, self.roles)
|
|
45
48
|
|
|
46
49
|
def asHeatPump(self):
|
|
47
|
-
return HeatPump(self.service)
|
|
50
|
+
return HeatPump(self.accessor, self.service, self.roles)
|
|
48
51
|
|
|
49
52
|
def asOilBoiler(self):
|
|
50
|
-
return OilBoiler(self.service)
|
|
53
|
+
return OilBoiler(self.accessor, self.service, self.roles)
|
|
51
54
|
|
|
52
55
|
def asPelletsBoiler(self):
|
|
53
|
-
return PelletsBoiler(self.service)
|
|
56
|
+
return PelletsBoiler(self.accessor, self.service, self.roles)
|
|
54
57
|
|
|
55
58
|
def asHybridDevice(self):
|
|
56
|
-
return Hybrid(self.service)
|
|
59
|
+
return Hybrid(self.accessor, self.service, self.roles)
|
|
57
60
|
|
|
58
61
|
def asRadiatorActuator(self):
|
|
59
|
-
return RadiatorActuator(self.service)
|
|
62
|
+
return RadiatorActuator(self.accessor, self.service, self.roles)
|
|
60
63
|
|
|
61
64
|
def asFloorHeating(self):
|
|
62
|
-
return FloorHeating(self.service)
|
|
65
|
+
return FloorHeating(self.accessor, self.service, self.roles)
|
|
63
66
|
|
|
64
67
|
def asFloorHeatingChannel(self):
|
|
65
|
-
return FloorHeatingChannel(self.service)
|
|
68
|
+
return FloorHeatingChannel(self.accessor, self.service, self.roles)
|
|
66
69
|
|
|
67
70
|
def asRoomSensor(self):
|
|
68
|
-
|
|
69
|
-
if self._room_control is not None:
|
|
70
|
-
sensor.setRoomControl(self._room_control, self._room_id)
|
|
71
|
-
return sensor
|
|
71
|
+
return RoomSensor(self.accessor, self.service, self.roles)
|
|
72
72
|
|
|
73
73
|
def asRoomControl(self):
|
|
74
|
-
return RoomControl(self.service)
|
|
75
|
-
|
|
76
|
-
def setRoomControlEnrichment(self, room_control, room_id):
|
|
77
|
-
"""Store RoomControl enrichment data to apply when creating a RoomSensor."""
|
|
78
|
-
self._room_control = room_control
|
|
79
|
-
self._room_id = room_id
|
|
74
|
+
return RoomControl(self.accessor, self.service, self.roles)
|
|
80
75
|
|
|
81
76
|
def asRepeater(self):
|
|
82
|
-
return Repeater(self.service)
|
|
77
|
+
return Repeater(self.accessor, self.service, self.roles)
|
|
83
78
|
|
|
84
79
|
def asElectricalEnergySystem(self):
|
|
85
|
-
return ElectricalEnergySystem(self.service)
|
|
80
|
+
return ElectricalEnergySystem(self.accessor, self.service, self.roles)
|
|
86
81
|
|
|
87
82
|
def asGateway(self):
|
|
88
|
-
return Gateway(self.service)
|
|
83
|
+
return Gateway(self.accessor, self.service, self.roles)
|
|
89
84
|
|
|
90
85
|
def asVentilation(self):
|
|
91
|
-
return VentilationDevice(self.service)
|
|
86
|
+
return VentilationDevice(self.accessor, self.service, self.roles)
|
|
87
|
+
|
|
88
|
+
def asWaterTreatment(self):
|
|
89
|
+
return WaterTreatment(self.accessor, self.service, self.roles)
|
|
92
90
|
|
|
93
91
|
def getConfig(self):
|
|
94
|
-
return self.
|
|
92
|
+
return self.accessor
|
|
95
93
|
|
|
96
94
|
def getId(self):
|
|
97
95
|
return self.device_id
|
|
@@ -108,8 +106,11 @@ class PyViCareDeviceConfig:
|
|
|
108
106
|
def getRoles(self):
|
|
109
107
|
return self.roles
|
|
110
108
|
|
|
109
|
+
def hasRoles(self, requested_roles):
|
|
110
|
+
return hasRoles(requested_roles, self.roles)
|
|
111
|
+
|
|
111
112
|
def isGateway(self):
|
|
112
|
-
return self.
|
|
113
|
+
return is_gateway_role(self.roles)
|
|
113
114
|
|
|
114
115
|
# see: https://vitodata300.viessmann.com/vd300/ApplicationHelp/VD300/1031_de_DE/Ger%C3%A4teliste.html
|
|
115
116
|
def asAutoDetectDevice(self):
|
|
@@ -130,6 +131,7 @@ class PyViCareDeviceConfig:
|
|
|
130
131
|
(self.asRoomControl, r"E3_RoomControl|Smart_RoomControl", ["type:virtual;smartRoomControl"]),
|
|
131
132
|
(self.asRoomSensor, r"E3_RoomSensor", ["type:climateSensor"]),
|
|
132
133
|
(self.asRepeater, r"E3_Repeater", ["type:repeater"]),
|
|
134
|
+
(self.asWaterTreatment, r"VitosetAqua", ["type:waterTreatment"]),
|
|
133
135
|
(self.asGateway, r"E3_TCU41_x04", ["type:gateway;TCU100"]),
|
|
134
136
|
(self.asGateway, r"E3_TCU19_x05", ["type:gateway;TCU200"]),
|
|
135
137
|
(self.asGateway, r"E3_TCU10_x07", ["type:gateway;TCU300"]),
|
|
@@ -138,7 +140,7 @@ class PyViCareDeviceConfig:
|
|
|
138
140
|
]
|
|
139
141
|
|
|
140
142
|
for (creator_method, type_name, roles) in device_types:
|
|
141
|
-
if re.search(type_name, self.device_model) or self.
|
|
143
|
+
if re.search(type_name, self.device_model) or self.hasRoles(roles):
|
|
142
144
|
logger.info("detected %s %s", self.device_model, creator_method.__name__)
|
|
143
145
|
device = creator_method()
|
|
144
146
|
if isinstance(device, (GazBoiler, HeatPump)) and not isinstance(device, Hybrid):
|
|
@@ -153,20 +155,27 @@ class PyViCareDeviceConfig:
|
|
|
153
155
|
def _isHybridByFeatures(self):
|
|
154
156
|
"""Check API features to detect hybrid devices (both burners and compressors)."""
|
|
155
157
|
try:
|
|
156
|
-
features = self.service.fetch_all_features()
|
|
158
|
+
features = self.service.fetch_all_features(self.accessor)
|
|
157
159
|
feature_names = [f["feature"] for f in features.get("data", [])]
|
|
158
160
|
has_burners = any(f.startswith("heating.burners") for f in feature_names)
|
|
159
161
|
has_compressors = any(f.startswith("heating.compressors") for f in feature_names)
|
|
160
162
|
return has_burners and has_compressors
|
|
163
|
+
except PyViCareNotPaidForError:
|
|
164
|
+
# Account lacks the paid feature package, so feature listing is
|
|
165
|
+
# unavailable. Treat as non-hybrid and let downstream feature
|
|
166
|
+
# access fall back to PyViCareNotSupportedFeatureError via the
|
|
167
|
+
# cached service. Without this, auto-detection crashes the
|
|
168
|
+
# integration setup before any device is created.
|
|
169
|
+
logger.debug("PACKAGE_NOT_PAID_FOR while detecting hybrid for %s, treating as non-hybrid", self.device_model)
|
|
170
|
+
return False
|
|
161
171
|
except (KeyError, TypeError, AttributeError, OSError):
|
|
162
172
|
logger.debug("Could not fetch features for hybrid detection of %s", self.device_model)
|
|
163
173
|
return False
|
|
164
174
|
|
|
165
175
|
def get_raw_json(self):
|
|
166
|
-
return self.service.fetch_all_features()
|
|
176
|
+
return self.service.fetch_all_features(self.accessor)
|
|
167
177
|
|
|
168
178
|
def dump_secure(self, flat=False):
|
|
169
|
-
raw_data = self.get_raw_json()
|
|
170
179
|
device_info = {
|
|
171
180
|
"id": self.device_id,
|
|
172
181
|
"modelId": self.device_model,
|
|
@@ -174,9 +183,20 @@ class PyViCareDeviceConfig:
|
|
|
174
183
|
"status": self.status,
|
|
175
184
|
"roles": self.roles
|
|
176
185
|
}
|
|
186
|
+
try:
|
|
187
|
+
raw_data = self.get_raw_json()
|
|
188
|
+
data = raw_data['data']
|
|
189
|
+
except PyViCareNotPaidForError:
|
|
190
|
+
# Feature listing requires a paid package the account does not
|
|
191
|
+
# have. Emit a placeholder so diagnostics still produce a usable
|
|
192
|
+
# dump (device metadata plus an unavailable-reason marker)
|
|
193
|
+
# rather than crashing the diagnostics endpoint.
|
|
194
|
+
logger.debug("PACKAGE_NOT_PAID_FOR while dumping features for %s", self.device_model)
|
|
195
|
+
data = []
|
|
196
|
+
device_info["dataUnavailableReason"] = "PACKAGE_NOT_PAID_FOR"
|
|
177
197
|
output = {
|
|
178
198
|
"device": device_info,
|
|
179
|
-
"data":
|
|
199
|
+
"data": data
|
|
180
200
|
}
|
|
181
201
|
|
|
182
202
|
if flat:
|
|
@@ -17,7 +17,7 @@ class GazBoiler(HeatingDevice):
|
|
|
17
17
|
|
|
18
18
|
@handleNotSupported
|
|
19
19
|
def getAvailableBurners(self):
|
|
20
|
-
return get_available_burners(self
|
|
20
|
+
return get_available_burners(self)
|
|
21
21
|
|
|
22
22
|
@handleNotSupported
|
|
23
23
|
def getGasConsumptionHeatingUnit(self):
|
|
@@ -91,10 +91,6 @@ class GazBoiler(HeatingDevice):
|
|
|
91
91
|
def getGasConsumptionDomesticHotWaterThisYear(self):
|
|
92
92
|
return self.getProperty("heating.gas.consumption.dhw")["properties"]["year"]["value"][0]
|
|
93
93
|
|
|
94
|
-
@handleNotSupported
|
|
95
|
-
def getBoilerTemperature(self):
|
|
96
|
-
return self.getProperty("heating.boiler.sensors.temperature.main")["properties"]["value"]["value"]
|
|
97
|
-
|
|
98
94
|
@handleNotSupported
|
|
99
95
|
def getBoilerTargetTemperature(self):
|
|
100
96
|
return self.getProperty("heating.boiler.temperature")["properties"]["value"]["value"]
|
|
@@ -2,6 +2,7 @@ from contextlib import suppress
|
|
|
2
2
|
from typing import Any, List, Optional
|
|
3
3
|
|
|
4
4
|
from PyViCare.PyViCareDevice import Device
|
|
5
|
+
from PyViCare.PyViCareService import hasRoles
|
|
5
6
|
from PyViCare.PyViCareHeatCurveCalculation import (
|
|
6
7
|
heat_curve_formular_variant1, heat_curve_formular_variant2)
|
|
7
8
|
from PyViCare.PyViCareUtils import (VICARE_DAYS,
|
|
@@ -17,13 +18,13 @@ def all_set(_list: List[Any]) -> bool:
|
|
|
17
18
|
return all(v is not None for v in _list)
|
|
18
19
|
|
|
19
20
|
|
|
20
|
-
def get_available_burners(
|
|
21
|
+
def get_available_burners(device):
|
|
21
22
|
# workaround starting from 25.01.2022
|
|
22
23
|
# see: https://github.com/somm15/PyViCare/issues/243
|
|
23
24
|
available_burners = []
|
|
24
25
|
for burner in ['0', '1', '2', '3', '4', '5']:
|
|
25
26
|
with suppress(PyViCareNotSupportedFeatureError):
|
|
26
|
-
if
|
|
27
|
+
if device.getProperty(f"heating.burners.{burner}") is not None:
|
|
27
28
|
available_burners.append(burner)
|
|
28
29
|
|
|
29
30
|
return available_burners
|
|
@@ -48,9 +49,9 @@ class HeatingDevice(Device):
|
|
|
48
49
|
return HeatingCircuit(self, circuit)
|
|
49
50
|
|
|
50
51
|
def get_heat_curve_formular(self):
|
|
51
|
-
if
|
|
52
|
+
if hasRoles(["type:heatpump", "type:E3"], self.roles):
|
|
52
53
|
return heat_curve_formular_variant1
|
|
53
|
-
if
|
|
54
|
+
if hasRoles(["type:heatpump"], self.roles) and len(self.getAvailableCircuits()) == 1:
|
|
54
55
|
return heat_curve_formular_variant2
|
|
55
56
|
return heat_curve_formular_variant1
|
|
56
57
|
|
|
@@ -391,6 +392,10 @@ class HeatingDevice(Device):
|
|
|
391
392
|
def getBoilerCommonSupplyTemperature(self):
|
|
392
393
|
return self.getProperty("heating.boiler.sensors.temperature.commonSupply")["properties"]["value"]["value"]
|
|
393
394
|
|
|
395
|
+
@handleNotSupported
|
|
396
|
+
def getBoilerTemperature(self):
|
|
397
|
+
return self.getProperty("heating.boiler.sensors.temperature.main")["properties"]["value"]["value"]
|
|
398
|
+
|
|
394
399
|
|
|
395
400
|
class HeatingDeviceWithComponent:
|
|
396
401
|
"""This is the base class for all heating components"""
|
|
@@ -699,6 +704,16 @@ class HeatingCircuit(HeatingDeviceWithComponent):
|
|
|
699
704
|
"sun": properties["entries"]["value"]["sun"]
|
|
700
705
|
}
|
|
701
706
|
|
|
707
|
+
@handleAPICommandErrors
|
|
708
|
+
def setHeatingSchedule(self, schedule: dict) -> None:
|
|
709
|
+
self.device.setProperty(f"heating.circuits.{self.circuit}.heating.schedule",
|
|
710
|
+
"setSchedule", {'newSchedule': schedule})
|
|
711
|
+
|
|
712
|
+
@handleNotSupported
|
|
713
|
+
def getHeatingScheduleModes(self) -> list: # type: ignore[type-arg]
|
|
714
|
+
return list(self.getProperty(f"heating.circuits.{self.circuit}.heating.schedule"
|
|
715
|
+
)["commands"]["setSchedule"]["params"]["newSchedule"]["constraints"]["modes"])
|
|
716
|
+
|
|
702
717
|
# Calculates target supply temperature based on data from Viessmann
|
|
703
718
|
# See: https://www.viessmann-community.com/t5/Gas/Mathematische-Formel-fuer-Vorlauftemperatur-aus-den-vier/m-p/68890#M27556
|
|
704
719
|
def getTargetSupplyTemperature(self) -> Optional[float]:
|