PyViCare 2.60.2__tar.gz → 2.61.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.61.0}/PKG-INFO +1 -1
- {pyvicare-2.60.2 → pyvicare-2.61.0}/PyViCare/PyViCare.py +26 -36
- pyvicare-2.61.0/PyViCare/PyViCareCachedService.py +23 -0
- pyvicare-2.61.0/PyViCare/PyViCareCachedServiceBase.py +82 -0
- pyvicare-2.61.0/PyViCare/PyViCareCachedServiceViaGateway.py +34 -0
- {pyvicare-2.60.2 → pyvicare-2.61.0}/PyViCare/PyViCareDevice.py +8 -6
- {pyvicare-2.60.2 → pyvicare-2.61.0}/PyViCare/PyViCareDeviceConfig.py +50 -35
- {pyvicare-2.60.2 → pyvicare-2.61.0}/PyViCare/PyViCareFuelCell.py +1 -1
- {pyvicare-2.60.2 → pyvicare-2.61.0}/PyViCare/PyViCareGazBoiler.py +1 -5
- {pyvicare-2.60.2 → pyvicare-2.61.0}/PyViCare/PyViCareHeatingDevice.py +19 -4
- {pyvicare-2.60.2 → pyvicare-2.61.0}/PyViCare/PyViCareOilBoiler.py +1 -1
- {pyvicare-2.60.2 → pyvicare-2.61.0}/PyViCare/PyViCarePelletsBoiler.py +1 -1
- {pyvicare-2.60.2 → pyvicare-2.61.0}/PyViCare/PyViCareRoomControl.py +22 -38
- pyvicare-2.61.0/PyViCare/PyViCareRoomSensor.py +17 -0
- {pyvicare-2.60.2 → pyvicare-2.61.0}/PyViCare/PyViCareService.py +26 -20
- pyvicare-2.61.0/PyViCare/PyViCareServiceViaGateway.py +38 -0
- {pyvicare-2.60.2 → pyvicare-2.61.0}/PyViCare/PyViCareVentilationDevice.py +29 -0
- {pyvicare-2.60.2 → pyvicare-2.61.0}/pyproject.toml +7 -7
- pyvicare-2.60.2/PyViCare/PyViCareCachedService.py +0 -72
- pyvicare-2.60.2/PyViCare/PyViCareRoomSensor.py +0 -132
- {pyvicare-2.60.2 → pyvicare-2.61.0}/LICENSE +0 -0
- {pyvicare-2.60.2 → pyvicare-2.61.0}/PyViCare/Feature.py +0 -0
- {pyvicare-2.60.2 → pyvicare-2.61.0}/PyViCare/PyViCareAbstractOAuthManager.py +0 -0
- {pyvicare-2.60.2 → pyvicare-2.61.0}/PyViCare/PyViCareBrowserOAuthManager.py +0 -0
- {pyvicare-2.60.2 → pyvicare-2.61.0}/PyViCare/PyViCareElectricalEnergySystem.py +0 -0
- {pyvicare-2.60.2 → pyvicare-2.61.0}/PyViCare/PyViCareFloorHeating.py +0 -0
- {pyvicare-2.60.2 → pyvicare-2.61.0}/PyViCare/PyViCareGateway.py +0 -0
- {pyvicare-2.60.2 → pyvicare-2.61.0}/PyViCare/PyViCareHeatCurveCalculation.py +0 -0
- {pyvicare-2.60.2 → pyvicare-2.61.0}/PyViCare/PyViCareHeatPump.py +0 -0
- {pyvicare-2.60.2 → pyvicare-2.61.0}/PyViCare/PyViCareHybrid.py +0 -0
- {pyvicare-2.60.2 → pyvicare-2.61.0}/PyViCare/PyViCareOAuthManager.py +0 -0
- {pyvicare-2.60.2 → pyvicare-2.61.0}/PyViCare/PyViCareRadiatorActuator.py +0 -0
- {pyvicare-2.60.2 → pyvicare-2.61.0}/PyViCare/PyViCareRepeater.py +0 -0
- {pyvicare-2.60.2 → pyvicare-2.61.0}/PyViCare/PyViCareUtils.py +0 -0
- {pyvicare-2.60.2 → pyvicare-2.61.0}/PyViCare/__init__.py +0 -0
- {pyvicare-2.60.2 → pyvicare-2.61.0}/README.md +0 -0
|
@@ -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,82 @@
|
|
|
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 setProperty(self, accessor: ViCareDeviceAccessor, property_name: str, action: str, data: Any) -> Any:
|
|
38
|
+
response = super().setProperty(accessor, property_name, action, data)
|
|
39
|
+
self.clear_cache()
|
|
40
|
+
return response
|
|
41
|
+
|
|
42
|
+
def _get_or_update_cache(self, accessor: ViCareDeviceAccessor):
|
|
43
|
+
with self._cacheLock:
|
|
44
|
+
if self.is_cache_invalid():
|
|
45
|
+
# we always set the cache time before we fetch the data
|
|
46
|
+
# to avoid consuming all the api calls if the api is down
|
|
47
|
+
# see https://github.com/home-assistant/core/issues/67052
|
|
48
|
+
# we simply return the old cache in this case
|
|
49
|
+
self._cacheTime = ViCareTimer().now()
|
|
50
|
+
|
|
51
|
+
try:
|
|
52
|
+
data = self._fetch_uncached(accessor)
|
|
53
|
+
except PyViCareNotPaidForError as e:
|
|
54
|
+
logger.error("Viessmann API denied access (PACKAGE_NOT_PAID_FOR). Features unavailable: %s", e)
|
|
55
|
+
if self._cache is not None:
|
|
56
|
+
return self._cache
|
|
57
|
+
raise PyViCareNotSupportedFeatureError("PACKAGE_NOT_PAID_FOR")
|
|
58
|
+
except (PyViCareDeviceCommunicationError, PyViCareInternalServerError) as e:
|
|
59
|
+
if self._cache is not None:
|
|
60
|
+
logger.warning("API error, returning stale cache: %s", e)
|
|
61
|
+
return self._cache
|
|
62
|
+
raise
|
|
63
|
+
|
|
64
|
+
if "data" not in data:
|
|
65
|
+
logger.error("Missing 'data' property when fetching data.")
|
|
66
|
+
raise PyViCareInvalidDataError(data)
|
|
67
|
+
self._cache = data
|
|
68
|
+
return self._cache
|
|
69
|
+
|
|
70
|
+
def is_cache_invalid(self) -> bool:
|
|
71
|
+
return self._cache is None or self._cacheTime is None or (ViCareTimer().now() - self._cacheTime).seconds > self._cacheDuration
|
|
72
|
+
|
|
73
|
+
def clear_cache(self):
|
|
74
|
+
with self._cacheLock:
|
|
75
|
+
self._cache = None
|
|
76
|
+
self._cacheTime = None
|
|
77
|
+
|
|
78
|
+
def _fetch_uncached(self, accessor: ViCareDeviceAccessor) -> Any:
|
|
79
|
+
raise NotImplementedError
|
|
80
|
+
|
|
81
|
+
def _extract_entities(self, data: dict, accessor: ViCareDeviceAccessor) -> list[dict[str, Any]]:
|
|
82
|
+
raise NotImplementedError
|
|
@@ -0,0 +1,34 @@
|
|
|
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)
|
|
31
|
+
|
|
32
|
+
def fetch_all_features(self, accessor: ViCareDeviceAccessor) -> Any:
|
|
33
|
+
# cached too, so per-device coordinators share one bulk fetch
|
|
34
|
+
return self._get_or_update_cache(accessor)
|
|
@@ -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,6 +16,9 @@ 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
|
|
20
23
|
|
|
21
24
|
logger = logging.getLogger(__name__)
|
|
@@ -24,74 +27,65 @@ logger.addHandler(logging.NullHandler())
|
|
|
24
27
|
|
|
25
28
|
class PyViCareDeviceConfig:
|
|
26
29
|
# pylint: disable=too-many-arguments,too-many-positional-arguments,too-many-instance-attributes
|
|
27
|
-
def __init__(self,
|
|
30
|
+
def __init__(self, accessor: ViCareDeviceAccessor, service: ViCareService, device_model, status, device_type=None, roles=None):
|
|
31
|
+
self.accessor = accessor
|
|
28
32
|
self.service = service
|
|
29
|
-
self.device_id = device_id
|
|
33
|
+
self.device_id = accessor.device_id
|
|
30
34
|
self.device_model = device_model
|
|
31
35
|
self.status = status
|
|
32
36
|
self.device_type = device_type
|
|
33
37
|
self.roles = roles if roles is not None else []
|
|
34
|
-
self._room_control = None
|
|
35
|
-
self._room_id = None
|
|
36
38
|
|
|
37
39
|
def asGeneric(self):
|
|
38
|
-
return HeatingDevice(self.service)
|
|
40
|
+
return HeatingDevice(self.accessor, self.service, self.roles)
|
|
39
41
|
|
|
40
42
|
def asGazBoiler(self):
|
|
41
|
-
return GazBoiler(self.service)
|
|
43
|
+
return GazBoiler(self.accessor, self.service, self.roles)
|
|
42
44
|
|
|
43
45
|
def asFuelCell(self):
|
|
44
|
-
return FuelCell(self.service)
|
|
46
|
+
return FuelCell(self.accessor, self.service, self.roles)
|
|
45
47
|
|
|
46
48
|
def asHeatPump(self):
|
|
47
|
-
return HeatPump(self.service)
|
|
49
|
+
return HeatPump(self.accessor, self.service, self.roles)
|
|
48
50
|
|
|
49
51
|
def asOilBoiler(self):
|
|
50
|
-
return OilBoiler(self.service)
|
|
52
|
+
return OilBoiler(self.accessor, self.service, self.roles)
|
|
51
53
|
|
|
52
54
|
def asPelletsBoiler(self):
|
|
53
|
-
return PelletsBoiler(self.service)
|
|
55
|
+
return PelletsBoiler(self.accessor, self.service, self.roles)
|
|
54
56
|
|
|
55
57
|
def asHybridDevice(self):
|
|
56
|
-
return Hybrid(self.service)
|
|
58
|
+
return Hybrid(self.accessor, self.service, self.roles)
|
|
57
59
|
|
|
58
60
|
def asRadiatorActuator(self):
|
|
59
|
-
return RadiatorActuator(self.service)
|
|
61
|
+
return RadiatorActuator(self.accessor, self.service, self.roles)
|
|
60
62
|
|
|
61
63
|
def asFloorHeating(self):
|
|
62
|
-
return FloorHeating(self.service)
|
|
64
|
+
return FloorHeating(self.accessor, self.service, self.roles)
|
|
63
65
|
|
|
64
66
|
def asFloorHeatingChannel(self):
|
|
65
|
-
return FloorHeatingChannel(self.service)
|
|
67
|
+
return FloorHeatingChannel(self.accessor, self.service, self.roles)
|
|
66
68
|
|
|
67
69
|
def asRoomSensor(self):
|
|
68
|
-
|
|
69
|
-
if self._room_control is not None:
|
|
70
|
-
sensor.setRoomControl(self._room_control, self._room_id)
|
|
71
|
-
return sensor
|
|
70
|
+
return RoomSensor(self.accessor, self.service, self.roles)
|
|
72
71
|
|
|
73
72
|
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
|
|
73
|
+
return RoomControl(self.accessor, self.service, self.roles)
|
|
80
74
|
|
|
81
75
|
def asRepeater(self):
|
|
82
|
-
return Repeater(self.service)
|
|
76
|
+
return Repeater(self.accessor, self.service, self.roles)
|
|
83
77
|
|
|
84
78
|
def asElectricalEnergySystem(self):
|
|
85
|
-
return ElectricalEnergySystem(self.service)
|
|
79
|
+
return ElectricalEnergySystem(self.accessor, self.service, self.roles)
|
|
86
80
|
|
|
87
81
|
def asGateway(self):
|
|
88
|
-
return Gateway(self.service)
|
|
82
|
+
return Gateway(self.accessor, self.service, self.roles)
|
|
89
83
|
|
|
90
84
|
def asVentilation(self):
|
|
91
|
-
return VentilationDevice(self.service)
|
|
85
|
+
return VentilationDevice(self.accessor, self.service, self.roles)
|
|
92
86
|
|
|
93
87
|
def getConfig(self):
|
|
94
|
-
return self.
|
|
88
|
+
return self.accessor
|
|
95
89
|
|
|
96
90
|
def getId(self):
|
|
97
91
|
return self.device_id
|
|
@@ -108,8 +102,11 @@ class PyViCareDeviceConfig:
|
|
|
108
102
|
def getRoles(self):
|
|
109
103
|
return self.roles
|
|
110
104
|
|
|
105
|
+
def hasRoles(self, requested_roles):
|
|
106
|
+
return hasRoles(requested_roles, self.roles)
|
|
107
|
+
|
|
111
108
|
def isGateway(self):
|
|
112
|
-
return self.
|
|
109
|
+
return is_gateway_role(self.roles)
|
|
113
110
|
|
|
114
111
|
# see: https://vitodata300.viessmann.com/vd300/ApplicationHelp/VD300/1031_de_DE/Ger%C3%A4teliste.html
|
|
115
112
|
def asAutoDetectDevice(self):
|
|
@@ -138,7 +135,7 @@ class PyViCareDeviceConfig:
|
|
|
138
135
|
]
|
|
139
136
|
|
|
140
137
|
for (creator_method, type_name, roles) in device_types:
|
|
141
|
-
if re.search(type_name, self.device_model) or self.
|
|
138
|
+
if re.search(type_name, self.device_model) or self.hasRoles(roles):
|
|
142
139
|
logger.info("detected %s %s", self.device_model, creator_method.__name__)
|
|
143
140
|
device = creator_method()
|
|
144
141
|
if isinstance(device, (GazBoiler, HeatPump)) and not isinstance(device, Hybrid):
|
|
@@ -153,20 +150,27 @@ class PyViCareDeviceConfig:
|
|
|
153
150
|
def _isHybridByFeatures(self):
|
|
154
151
|
"""Check API features to detect hybrid devices (both burners and compressors)."""
|
|
155
152
|
try:
|
|
156
|
-
features = self.service.fetch_all_features()
|
|
153
|
+
features = self.service.fetch_all_features(self.accessor)
|
|
157
154
|
feature_names = [f["feature"] for f in features.get("data", [])]
|
|
158
155
|
has_burners = any(f.startswith("heating.burners") for f in feature_names)
|
|
159
156
|
has_compressors = any(f.startswith("heating.compressors") for f in feature_names)
|
|
160
157
|
return has_burners and has_compressors
|
|
158
|
+
except PyViCareNotPaidForError:
|
|
159
|
+
# Account lacks the paid feature package, so feature listing is
|
|
160
|
+
# unavailable. Treat as non-hybrid and let downstream feature
|
|
161
|
+
# access fall back to PyViCareNotSupportedFeatureError via the
|
|
162
|
+
# cached service. Without this, auto-detection crashes the
|
|
163
|
+
# integration setup before any device is created.
|
|
164
|
+
logger.debug("PACKAGE_NOT_PAID_FOR while detecting hybrid for %s, treating as non-hybrid", self.device_model)
|
|
165
|
+
return False
|
|
161
166
|
except (KeyError, TypeError, AttributeError, OSError):
|
|
162
167
|
logger.debug("Could not fetch features for hybrid detection of %s", self.device_model)
|
|
163
168
|
return False
|
|
164
169
|
|
|
165
170
|
def get_raw_json(self):
|
|
166
|
-
return self.service.fetch_all_features()
|
|
171
|
+
return self.service.fetch_all_features(self.accessor)
|
|
167
172
|
|
|
168
173
|
def dump_secure(self, flat=False):
|
|
169
|
-
raw_data = self.get_raw_json()
|
|
170
174
|
device_info = {
|
|
171
175
|
"id": self.device_id,
|
|
172
176
|
"modelId": self.device_model,
|
|
@@ -174,9 +178,20 @@ class PyViCareDeviceConfig:
|
|
|
174
178
|
"status": self.status,
|
|
175
179
|
"roles": self.roles
|
|
176
180
|
}
|
|
181
|
+
try:
|
|
182
|
+
raw_data = self.get_raw_json()
|
|
183
|
+
data = raw_data['data']
|
|
184
|
+
except PyViCareNotPaidForError:
|
|
185
|
+
# Feature listing requires a paid package the account does not
|
|
186
|
+
# have. Emit a placeholder so diagnostics still produce a usable
|
|
187
|
+
# dump (device metadata plus an unavailable-reason marker)
|
|
188
|
+
# rather than crashing the diagnostics endpoint.
|
|
189
|
+
logger.debug("PACKAGE_NOT_PAID_FOR while dumping features for %s", self.device_model)
|
|
190
|
+
data = []
|
|
191
|
+
device_info["dataUnavailableReason"] = "PACKAGE_NOT_PAID_FOR"
|
|
177
192
|
output = {
|
|
178
193
|
"device": device_info,
|
|
179
|
-
"data":
|
|
194
|
+
"data": data
|
|
180
195
|
}
|
|
181
196
|
|
|
182
197
|
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]:
|
|
@@ -11,89 +11,88 @@ logger.addHandler(logging.NullHandler())
|
|
|
11
11
|
class RoomControl(Device):
|
|
12
12
|
"""Viessmann RoomControl virtual device.
|
|
13
13
|
|
|
14
|
-
Aggregates room sensor data and heating programs.
|
|
15
|
-
Used to enrich physical Zigbee devices with room data.
|
|
14
|
+
Aggregates room sensor data and heating programs per room.
|
|
16
15
|
"""
|
|
17
16
|
|
|
18
17
|
@handleNotSupported
|
|
19
18
|
def getAvailableRooms(self) -> list[str]:
|
|
20
|
-
return cast(list[str], self.
|
|
19
|
+
return cast(list[str], self.getProperty("rooms")["properties"]["enabled"]["value"])
|
|
21
20
|
|
|
22
21
|
def getRoomActorIds(self, room_id: str) -> list[str]:
|
|
23
22
|
"""Return list of actor device IDs for a room."""
|
|
24
23
|
try:
|
|
25
|
-
actors = self.
|
|
24
|
+
actors = self.getProperty(f"rooms.{room_id}")["properties"]["actors"]["value"]
|
|
26
25
|
return [str(a["deviceId"]) for a in actors]
|
|
27
26
|
except (PyViCareNotSupportedFeatureError, KeyError):
|
|
28
27
|
return []
|
|
29
28
|
|
|
30
29
|
def getRoomName(self, room_id: str) -> str | None:
|
|
31
30
|
try:
|
|
32
|
-
return str(self.
|
|
31
|
+
return str(self.getProperty(f"rooms.{room_id}")["properties"]["name"]["value"])
|
|
33
32
|
except (PyViCareNotSupportedFeatureError, KeyError):
|
|
34
33
|
return None
|
|
35
34
|
|
|
36
35
|
def getRoomType(self, room_id: str) -> str | None:
|
|
37
36
|
try:
|
|
38
|
-
return str(self.
|
|
37
|
+
return str(self.getProperty(f"rooms.{room_id}")["properties"]["type"]["value"])
|
|
39
38
|
except (PyViCareNotSupportedFeatureError, KeyError):
|
|
40
39
|
return None
|
|
41
40
|
|
|
42
41
|
# --- Sensors ---
|
|
43
42
|
|
|
44
43
|
def getRoomTemperature(self, room_id: str) -> float:
|
|
45
|
-
return float(self.
|
|
44
|
+
return float(self.getProperty(f"rooms.{room_id}.sensors.temperature")["properties"]["value"]["value"])
|
|
46
45
|
|
|
47
46
|
def getRoomHumidity(self, room_id: str) -> float:
|
|
48
|
-
return float(self.
|
|
47
|
+
return float(self.getProperty(f"rooms.{room_id}.sensors.humidity")["properties"]["value"]["value"])
|
|
49
48
|
|
|
50
49
|
def getRoomCO2(self, room_id: str) -> int:
|
|
51
|
-
return int(self.
|
|
50
|
+
return int(self.getProperty(f"rooms.{room_id}.sensors.co2")["properties"]["value"]["value"])
|
|
52
51
|
|
|
53
52
|
def getRoomCondensationRisk(self, room_id: str) -> bool:
|
|
54
|
-
return bool(self.
|
|
53
|
+
return bool(self.getProperty(f"rooms.{room_id}.condensationRisk")["properties"]["value"]["value"])
|
|
55
54
|
|
|
56
55
|
# --- Operating state ---
|
|
57
56
|
|
|
58
57
|
def getRoomOperatingStateLevel(self, room_id: str) -> str:
|
|
59
|
-
return str(self.
|
|
58
|
+
return str(self.getProperty(f"rooms.{room_id}.operating.state")["properties"]["level"]["value"])
|
|
60
59
|
|
|
61
60
|
def getRoomOperatingStateDemand(self, room_id: str) -> str:
|
|
62
|
-
return str(self.
|
|
61
|
+
return str(self.getProperty(f"rooms.{room_id}.operating.state")["properties"]["demand"]["value"])
|
|
63
62
|
|
|
64
63
|
def getRoomOperatingStateReason(self, room_id: str) -> str:
|
|
65
|
-
return str(self.
|
|
64
|
+
return str(self.getProperty(f"rooms.{room_id}.operating.state")["properties"]["reason"]["value"])
|
|
66
65
|
|
|
67
66
|
# --- Heating programs ---
|
|
68
67
|
|
|
69
68
|
def getRoomNormalHeatingTemperature(self, room_id: str) -> float:
|
|
70
|
-
return float(self.
|
|
69
|
+
return float(self.getProperty(f"rooms.{room_id}.operating.programs.normalHeating")["properties"]["temperature"]["value"])
|
|
71
70
|
|
|
72
71
|
@handleAPICommandErrors
|
|
73
72
|
def setRoomNormalHeatingTemperature(self, room_id: str, temperature: float) -> None:
|
|
74
|
-
self.
|
|
73
|
+
self.setProperty(f"rooms.{room_id}.operating.programs.normalHeating", "setTemperature",
|
|
75
74
|
{"targetTemperature": temperature})
|
|
76
75
|
|
|
77
76
|
def getRoomReducedHeatingTemperature(self, room_id: str) -> float:
|
|
78
|
-
return float(self.
|
|
77
|
+
return float(self.getProperty(f"rooms.{room_id}.operating.programs.reducedHeating")["properties"]["temperature"]["value"])
|
|
79
78
|
|
|
80
79
|
@handleAPICommandErrors
|
|
81
80
|
def setRoomReducedHeatingTemperature(self, room_id: str, temperature: float) -> None:
|
|
82
|
-
self.
|
|
81
|
+
self.setProperty(f"rooms.{room_id}.operating.programs.reducedHeating", "setTemperature",
|
|
83
82
|
{"targetTemperature": temperature})
|
|
84
83
|
|
|
85
84
|
def getRoomComfortHeatingTemperature(self, room_id: str) -> float:
|
|
86
|
-
return float(self.
|
|
85
|
+
return float(self.getProperty(f"rooms.{room_id}.operating.programs.comfortHeating")["properties"]["temperature"]["value"])
|
|
87
86
|
|
|
88
87
|
@handleAPICommandErrors
|
|
89
88
|
def setRoomComfortHeatingTemperature(self, room_id: str, temperature: float) -> None:
|
|
90
|
-
self.
|
|
89
|
+
self.setProperty(f"rooms.{room_id}.operating.programs.comfortHeating", "setTemperature",
|
|
91
90
|
{"targetTemperature": temperature})
|
|
92
91
|
|
|
93
92
|
# --- Schedule ---
|
|
94
93
|
|
|
95
94
|
def getRoomSchedule(self, room_id: str) -> dict[str, Any]:
|
|
96
|
-
props = self.
|
|
95
|
+
props = self.getProperty(f"rooms.{room_id}.schedule")["properties"]
|
|
97
96
|
return {
|
|
98
97
|
"active": props["active"]["value"],
|
|
99
98
|
"mon": props["entries"]["value"]["mon"],
|
|
@@ -108,29 +107,14 @@ class RoomControl(Device):
|
|
|
108
107
|
# --- Quick modes ---
|
|
109
108
|
|
|
110
109
|
def getRoomManualTillNextScheduleActive(self, room_id: str) -> bool:
|
|
111
|
-
return bool(self.
|
|
110
|
+
return bool(self.getProperty(
|
|
112
111
|
f"rooms.{room_id}.quickmodes.manualTillNextSchedule")["properties"]["active"]["value"])
|
|
113
112
|
|
|
114
113
|
@handleAPICommandErrors
|
|
115
114
|
def activateRoomManualTillNextSchedule(self, room_id: str, temperature: float) -> None:
|
|
116
|
-
self.
|
|
115
|
+
self.setProperty(f"rooms.{room_id}.quickmodes.manualTillNextSchedule", "activate",
|
|
117
116
|
{"temperature": temperature})
|
|
118
117
|
|
|
119
118
|
@handleAPICommandErrors
|
|
120
119
|
def deactivateRoomManualTillNextSchedule(self, room_id: str) -> None:
|
|
121
|
-
self.
|
|
122
|
-
|
|
123
|
-
# --- Mapping ---
|
|
124
|
-
|
|
125
|
-
def buildActorRoomMap(self) -> dict[str, str]:
|
|
126
|
-
"""Build a mapping of actor device ID -> room ID."""
|
|
127
|
-
actor_map: dict[str, str] = {}
|
|
128
|
-
try:
|
|
129
|
-
rooms = self.getAvailableRooms()
|
|
130
|
-
except PyViCareNotSupportedFeatureError:
|
|
131
|
-
return actor_map
|
|
132
|
-
|
|
133
|
-
for room_id in rooms:
|
|
134
|
-
for actor_id in self.getRoomActorIds(room_id):
|
|
135
|
-
actor_map[actor_id] = room_id
|
|
136
|
-
return actor_map
|
|
120
|
+
self.setProperty(f"rooms.{room_id}.quickmodes.manualTillNextSchedule", "deactivate", {})
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
from PyViCare.PyViCareDevice import ZigbeeBatteryDevice
|
|
2
|
+
from PyViCare.PyViCareUtils import handleNotSupported
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class RoomSensor(ZigbeeBatteryDevice):
|
|
6
|
+
|
|
7
|
+
@handleNotSupported
|
|
8
|
+
def getSerial(self) -> str:
|
|
9
|
+
return str(self.getProperty("device.sensors.temperature")["deviceId"])
|
|
10
|
+
|
|
11
|
+
@handleNotSupported
|
|
12
|
+
def getTemperature(self) -> float:
|
|
13
|
+
return float(self.getProperty("device.sensors.temperature")["properties"]["value"]["value"])
|
|
14
|
+
|
|
15
|
+
@handleNotSupported
|
|
16
|
+
def getHumidity(self) -> float:
|
|
17
|
+
return float(self.getProperty("device.sensors.humidity")["properties"]["value"]["value"])
|
|
@@ -20,6 +20,17 @@ def readFeature(entities, property_name):
|
|
|
20
20
|
def hasRoles(requested_roles: List[str], existing_roles: List[str]) -> bool:
|
|
21
21
|
return len(requested_roles) > 0 and set(requested_roles).issubset(set(existing_roles))
|
|
22
22
|
|
|
23
|
+
GATEWAY_ROLES = [
|
|
24
|
+
"type:gateway;VitoconnectOpto1",
|
|
25
|
+
"type:gateway;VitoconnectOpto2/OT2",
|
|
26
|
+
"type:gateway;TCU100",
|
|
27
|
+
"type:gateway;TCU200",
|
|
28
|
+
"type:gateway;TCU300",
|
|
29
|
+
]
|
|
30
|
+
|
|
31
|
+
def is_gateway_role(roles: List[str]) -> bool:
|
|
32
|
+
return any(hasRoles([role], roles) for role in GATEWAY_ROLES)
|
|
33
|
+
|
|
23
34
|
def buildSetPropertyUrl(accessor, property_name, action):
|
|
24
35
|
return f'/features/installations/{accessor.id}/gateways/{accessor.serial}/devices/{accessor.device_id}/features/{property_name}/commands/{action}'
|
|
25
36
|
|
|
@@ -30,40 +41,35 @@ class ViCareDeviceAccessor:
|
|
|
30
41
|
self.device_id = device_id
|
|
31
42
|
|
|
32
43
|
class ViCareService:
|
|
33
|
-
def __init__(self, oauth_manager: AbstractViCareOAuthManager,
|
|
44
|
+
def __init__(self, oauth_manager: AbstractViCareOAuthManager, roles: List[str]) -> None:
|
|
34
45
|
self.oauth_manager = oauth_manager
|
|
35
|
-
self.accessor = accessor
|
|
36
46
|
self.roles = roles
|
|
37
47
|
|
|
38
|
-
def getProperty(self, property_name: str) -> Any:
|
|
39
|
-
url = self.buildGetPropertyUrl(property_name)
|
|
48
|
+
def getProperty(self, accessor: ViCareDeviceAccessor, property_name: str) -> Any:
|
|
49
|
+
url = self.buildGetPropertyUrl(accessor, property_name)
|
|
40
50
|
return self.oauth_manager.get(url)
|
|
41
51
|
|
|
42
|
-
def buildGetPropertyUrl(self, property_name):
|
|
43
|
-
if self.
|
|
44
|
-
return f'/features/installations/{
|
|
45
|
-
return f'/features/installations/{
|
|
52
|
+
def buildGetPropertyUrl(self, accessor: ViCareDeviceAccessor, property_name):
|
|
53
|
+
if is_gateway_role(self.roles):
|
|
54
|
+
return f'/features/installations/{accessor.id}/gateways/{accessor.serial}/features/{property_name}'
|
|
55
|
+
return f'/features/installations/{accessor.id}/gateways/{accessor.serial}/devices/{accessor.device_id}/features/{property_name}'
|
|
46
56
|
|
|
47
57
|
def hasRoles(self, requested_roles) -> bool:
|
|
48
58
|
return hasRoles(requested_roles, self.roles)
|
|
49
59
|
|
|
50
|
-
def
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
def setProperty(self, property_name: str, action: str, data: Any) -> Any:
|
|
54
|
-
url = buildSetPropertyUrl(
|
|
55
|
-
self.accessor, property_name, action)
|
|
60
|
+
def setProperty(self, accessor: ViCareDeviceAccessor, property_name: str, action: str, data: Any) -> Any:
|
|
61
|
+
url = buildSetPropertyUrl(accessor, property_name, action)
|
|
56
62
|
|
|
57
63
|
post_data = data if isinstance(data, str) else json.dumps(data)
|
|
58
64
|
return self.oauth_manager.post(url, post_data)
|
|
59
65
|
|
|
60
|
-
def fetch_all_features(self) -> Any:
|
|
61
|
-
url = f'/features/installations/{
|
|
62
|
-
if self.
|
|
63
|
-
url = f'/features/installations/{
|
|
66
|
+
def fetch_all_features(self, accessor: ViCareDeviceAccessor) -> Any:
|
|
67
|
+
url = f'/features/installations/{accessor.id}/gateways/{accessor.serial}/devices/{accessor.device_id}/features/'
|
|
68
|
+
if is_gateway_role(self.roles):
|
|
69
|
+
url = f'/features/installations/{accessor.id}/gateways/{accessor.serial}/features/'
|
|
64
70
|
return self.oauth_manager.get(url)
|
|
65
71
|
|
|
66
|
-
def reboot_gateway(self) -> Any:
|
|
67
|
-
url = f'/equipment/installations/{
|
|
72
|
+
def reboot_gateway(self, accessor: ViCareDeviceAccessor) -> Any:
|
|
73
|
+
url = f'/equipment/installations/{accessor.id}/gateways/{accessor.serial}/reboot'
|
|
68
74
|
data = "{}"
|
|
69
75
|
return self.oauth_manager.post(url, data)
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
from typing import Any
|
|
3
|
+
|
|
4
|
+
from PyViCare.PyViCareAbstractOAuthManager import AbstractViCareOAuthManager
|
|
5
|
+
from PyViCare.PyViCareService import (ViCareDeviceAccessor, ViCareService,
|
|
6
|
+
readFeature)
|
|
7
|
+
|
|
8
|
+
logger = logging.getLogger(__name__)
|
|
9
|
+
logger.addHandler(logging.NullHandler())
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def filter_features_for_device(entities: list[dict[str, Any]], device_id: str) -> list[dict[str, Any]]:
|
|
13
|
+
device_segment = f"/devices/{device_id}/"
|
|
14
|
+
return [e for e in entities if device_segment in e.get("uri", "")]
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class ViCareServiceViaGateway(ViCareService):
|
|
18
|
+
"""Service that reads all device features from the gateway-wide bulk endpoint.
|
|
19
|
+
|
|
20
|
+
One instance can serve multiple devices on the same gateway: the bulk
|
|
21
|
+
response contains features for all devices, and getProperty filters by
|
|
22
|
+
accessor.device_id at read time.
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
def __init__(self, oauth_manager: AbstractViCareOAuthManager) -> None:
|
|
26
|
+
ViCareService.__init__(self, oauth_manager, [])
|
|
27
|
+
|
|
28
|
+
def getProperty(self, accessor: ViCareDeviceAccessor, property_name: str) -> Any:
|
|
29
|
+
data = self.fetch_all_features(accessor)
|
|
30
|
+
entities = filter_features_for_device(data["data"], accessor.device_id)
|
|
31
|
+
return readFeature(entities, property_name)
|
|
32
|
+
|
|
33
|
+
def fetch_all_features(self, accessor: ViCareDeviceAccessor) -> Any:
|
|
34
|
+
url = (
|
|
35
|
+
f'/features/installations/{accessor.id}'
|
|
36
|
+
f'/gateways/{accessor.serial}/features?includeDevicesFeatures=true'
|
|
37
|
+
)
|
|
38
|
+
return self.oauth_manager.get(url)
|
|
@@ -119,6 +119,13 @@ class VentilationDevice(Device):
|
|
|
119
119
|
self.setProperty(f"ventilation.quickmodes.{quickmode}", "deactivate", {})
|
|
120
120
|
|
|
121
121
|
@handleNotSupported
|
|
122
|
+
@deprecated(
|
|
123
|
+
reason=(
|
|
124
|
+
"ventilation.operating.programs.* was deprecated by Viessmann on 2024-09-15. "
|
|
125
|
+
"Use getVentilationModes() instead."
|
|
126
|
+
),
|
|
127
|
+
version="2.61.0",
|
|
128
|
+
)
|
|
122
129
|
def getVentilationPrograms(self):
|
|
123
130
|
available_programs = []
|
|
124
131
|
for program in ['basic', 'intensive', 'reduced', 'standard', 'standby', 'holidayAtHome', 'permanent']:
|
|
@@ -133,6 +140,14 @@ class VentilationDevice(Device):
|
|
|
133
140
|
return self.getVentilationPrograms()
|
|
134
141
|
|
|
135
142
|
@handleNotSupported
|
|
143
|
+
@deprecated(
|
|
144
|
+
reason=(
|
|
145
|
+
"ventilation.operating.programs.* was deprecated by Viessmann on 2024-09-15. "
|
|
146
|
+
"Use getVentilationLevel() to read the current fan level "
|
|
147
|
+
"(from ventilation.operating.state) instead."
|
|
148
|
+
),
|
|
149
|
+
version="2.61.0",
|
|
150
|
+
)
|
|
136
151
|
def getActiveVentilationProgram(self):
|
|
137
152
|
return self.getProperty("ventilation.operating.programs.active")["properties"]["value"]["value"]
|
|
138
153
|
|
|
@@ -141,6 +156,13 @@ class VentilationDevice(Device):
|
|
|
141
156
|
def getActiveProgram(self):
|
|
142
157
|
return self.getActiveVentilationProgram()
|
|
143
158
|
|
|
159
|
+
@deprecated(
|
|
160
|
+
reason=(
|
|
161
|
+
"ventilation.operating.programs.* was deprecated by Viessmann on 2024-09-15. "
|
|
162
|
+
"Use activateVentilationMode() and setVentilationLevel() instead."
|
|
163
|
+
),
|
|
164
|
+
version="2.61.0",
|
|
165
|
+
)
|
|
144
166
|
def activateVentilationProgram(self, program):
|
|
145
167
|
""" Activate a program
|
|
146
168
|
NOTE
|
|
@@ -172,6 +194,13 @@ class VentilationDevice(Device):
|
|
|
172
194
|
"""
|
|
173
195
|
return self.activateVentilationProgram(program)
|
|
174
196
|
|
|
197
|
+
@deprecated(
|
|
198
|
+
reason=(
|
|
199
|
+
"ventilation.operating.programs.* was deprecated by Viessmann on 2024-09-15. "
|
|
200
|
+
"Use activateVentilationMode() with a different mode instead."
|
|
201
|
+
),
|
|
202
|
+
version="2.61.0",
|
|
203
|
+
)
|
|
175
204
|
def deactivateVentilationProgram(self, program):
|
|
176
205
|
""" Deactivate a program
|
|
177
206
|
Parameters
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[tool.poetry]
|
|
2
2
|
name = "PyViCare"
|
|
3
|
-
version = "2.
|
|
3
|
+
version = "2.61.0"
|
|
4
4
|
description = "Library to communicate with the Viessmann ViCare API."
|
|
5
5
|
authors = [
|
|
6
6
|
"Simon Gillet <mail+pyvicare@gillet.ninja>"
|
|
@@ -48,12 +48,12 @@ requests = ">=2.31.0"
|
|
|
48
48
|
deprecated = "^1.2.15"
|
|
49
49
|
|
|
50
50
|
[tool.poetry.group.dev.dependencies]
|
|
51
|
-
codespell = "
|
|
52
|
-
mypy = "
|
|
53
|
-
pylint = "
|
|
54
|
-
pytest = "
|
|
55
|
-
pytest-cov = "
|
|
56
|
-
ruff = "
|
|
51
|
+
codespell = "2.4.2"
|
|
52
|
+
mypy = "2.3.0"
|
|
53
|
+
pylint = "4.0.6"
|
|
54
|
+
pytest = "9.1.1"
|
|
55
|
+
pytest-cov = "7.1.0"
|
|
56
|
+
ruff = "0.15.21"
|
|
57
57
|
types-deprecated = "^1.2.15.20241117"
|
|
58
58
|
types-requests = ">=2.31"
|
|
59
59
|
|
|
@@ -1,72 +0,0 @@
|
|
|
1
|
-
import logging
|
|
2
|
-
import threading
|
|
3
|
-
from typing import Any, List
|
|
4
|
-
|
|
5
|
-
from PyViCare.PyViCareAbstractOAuthManager import AbstractViCareOAuthManager
|
|
6
|
-
from PyViCare.PyViCareService import (ViCareDeviceAccessor, ViCareService,
|
|
7
|
-
readFeature)
|
|
8
|
-
from PyViCare.PyViCareUtils import (PyViCareDeviceCommunicationError,
|
|
9
|
-
PyViCareInvalidDataError,
|
|
10
|
-
PyViCareInternalServerError,
|
|
11
|
-
PyViCareNotPaidForError,
|
|
12
|
-
PyViCareNotSupportedFeatureError,
|
|
13
|
-
ViCareTimer)
|
|
14
|
-
|
|
15
|
-
logger = logging.getLogger(__name__)
|
|
16
|
-
logger.addHandler(logging.NullHandler())
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
class ViCareCachedService(ViCareService):
|
|
20
|
-
|
|
21
|
-
def __init__(self, oauth_manager: AbstractViCareOAuthManager, accessor: ViCareDeviceAccessor, roles: List[str], cacheDuration: int) -> None:
|
|
22
|
-
ViCareService.__init__(self, oauth_manager, accessor, roles)
|
|
23
|
-
self.__cacheDuration = cacheDuration
|
|
24
|
-
self.__cache = None
|
|
25
|
-
self.__cacheTime = None
|
|
26
|
-
self.__lock = threading.Lock()
|
|
27
|
-
|
|
28
|
-
def getProperty(self, property_name: str) -> Any:
|
|
29
|
-
data = self.__get_or_update_cache()
|
|
30
|
-
entities = data["data"]
|
|
31
|
-
return readFeature(entities, property_name)
|
|
32
|
-
|
|
33
|
-
def setProperty(self, property_name, action, data):
|
|
34
|
-
response = super().setProperty(property_name, action, data)
|
|
35
|
-
self.clear_cache()
|
|
36
|
-
return response
|
|
37
|
-
|
|
38
|
-
def __get_or_update_cache(self):
|
|
39
|
-
with self.__lock:
|
|
40
|
-
if self.is_cache_invalid():
|
|
41
|
-
# we always set the cache time before we fetch the data
|
|
42
|
-
# to avoid consuming all the api calls if the api is down
|
|
43
|
-
# see https://github.com/home-assistant/core/issues/67052
|
|
44
|
-
# we simply return the old cache in this case
|
|
45
|
-
self.__cacheTime = ViCareTimer().now()
|
|
46
|
-
|
|
47
|
-
try:
|
|
48
|
-
data = self.fetch_all_features()
|
|
49
|
-
except PyViCareNotPaidForError as e:
|
|
50
|
-
logger.error("Viessmann API denied access (PACKAGE_NOT_PAID_FOR). Features unavailable: %s", e)
|
|
51
|
-
if self.__cache is not None:
|
|
52
|
-
return self.__cache
|
|
53
|
-
raise PyViCareNotSupportedFeatureError("PACKAGE_NOT_PAID_FOR")
|
|
54
|
-
except (PyViCareDeviceCommunicationError, PyViCareInternalServerError) as e:
|
|
55
|
-
if self.__cache is not None:
|
|
56
|
-
logger.warning("API error, returning stale cache: %s", e)
|
|
57
|
-
return self.__cache
|
|
58
|
-
raise
|
|
59
|
-
|
|
60
|
-
if "data" not in data:
|
|
61
|
-
logger.error("Missing 'data' property when fetching data.")
|
|
62
|
-
raise PyViCareInvalidDataError(data)
|
|
63
|
-
self.__cache = data
|
|
64
|
-
return self.__cache
|
|
65
|
-
|
|
66
|
-
def is_cache_invalid(self) -> bool:
|
|
67
|
-
return self.__cache is None or self.__cacheTime is None or (ViCareTimer().now() - self.__cacheTime).seconds > self.__cacheDuration
|
|
68
|
-
|
|
69
|
-
def clear_cache(self):
|
|
70
|
-
with self.__lock:
|
|
71
|
-
self.__cache = None
|
|
72
|
-
self.__cacheTime = None
|
|
@@ -1,132 +0,0 @@
|
|
|
1
|
-
from __future__ import annotations
|
|
2
|
-
|
|
3
|
-
from typing import Any, TYPE_CHECKING
|
|
4
|
-
|
|
5
|
-
from PyViCare.PyViCareDevice import ZigbeeBatteryDevice
|
|
6
|
-
from PyViCare.PyViCareUtils import handleNotSupported, handleAPICommandErrors
|
|
7
|
-
|
|
8
|
-
if TYPE_CHECKING:
|
|
9
|
-
from PyViCare.PyViCareRoomControl import RoomControl
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
class RoomSensor(ZigbeeBatteryDevice):
|
|
13
|
-
|
|
14
|
-
_room_control: RoomControl | None = None
|
|
15
|
-
_room_id: str | None = None
|
|
16
|
-
|
|
17
|
-
def setRoomControl(self, room_control: RoomControl, room_id: str) -> None:
|
|
18
|
-
"""Enrich this sensor with data from a RoomControl device."""
|
|
19
|
-
self._room_control = room_control
|
|
20
|
-
self._room_id = room_id
|
|
21
|
-
|
|
22
|
-
def _getRoomContext(self) -> tuple[RoomControl, str]:
|
|
23
|
-
"""Return (room_control, room_id), raising if not enriched."""
|
|
24
|
-
if self._room_control is None or self._room_id is None:
|
|
25
|
-
raise KeyError("roomControl")
|
|
26
|
-
return self._room_control, self._room_id
|
|
27
|
-
|
|
28
|
-
@handleNotSupported
|
|
29
|
-
def getSerial(self) -> str:
|
|
30
|
-
return str(self.getProperty("device.sensors.temperature")["deviceId"])
|
|
31
|
-
|
|
32
|
-
# --- Sensors (enriched from RoomControl) ---
|
|
33
|
-
|
|
34
|
-
@handleNotSupported
|
|
35
|
-
def getTemperature(self) -> float:
|
|
36
|
-
if self._room_control is not None and self._room_id is not None:
|
|
37
|
-
return self._room_control.getRoomTemperature(self._room_id)
|
|
38
|
-
return float(self.getProperty("device.sensors.temperature")["properties"]["value"]["value"])
|
|
39
|
-
|
|
40
|
-
@handleNotSupported
|
|
41
|
-
def getHumidity(self) -> float:
|
|
42
|
-
if self._room_control is not None and self._room_id is not None:
|
|
43
|
-
return self._room_control.getRoomHumidity(self._room_id)
|
|
44
|
-
return float(self.getProperty("device.sensors.humidity")["properties"]["value"]["value"])
|
|
45
|
-
|
|
46
|
-
@handleNotSupported
|
|
47
|
-
def getCO2(self) -> int:
|
|
48
|
-
rc, rid = self._getRoomContext()
|
|
49
|
-
return rc.getRoomCO2(rid)
|
|
50
|
-
|
|
51
|
-
@handleNotSupported
|
|
52
|
-
def getRoomName(self) -> str | None:
|
|
53
|
-
rc, rid = self._getRoomContext()
|
|
54
|
-
return rc.getRoomName(rid)
|
|
55
|
-
|
|
56
|
-
@handleNotSupported
|
|
57
|
-
def getRoomType(self) -> str | None:
|
|
58
|
-
rc, rid = self._getRoomContext()
|
|
59
|
-
return rc.getRoomType(rid)
|
|
60
|
-
|
|
61
|
-
@handleNotSupported
|
|
62
|
-
def getCondensationRisk(self) -> bool:
|
|
63
|
-
rc, rid = self._getRoomContext()
|
|
64
|
-
return rc.getRoomCondensationRisk(rid)
|
|
65
|
-
|
|
66
|
-
# --- Operating state ---
|
|
67
|
-
|
|
68
|
-
@handleNotSupported
|
|
69
|
-
def getOperatingStateLevel(self) -> str:
|
|
70
|
-
rc, rid = self._getRoomContext()
|
|
71
|
-
return rc.getRoomOperatingStateLevel(rid)
|
|
72
|
-
|
|
73
|
-
@handleNotSupported
|
|
74
|
-
def getOperatingStateDemand(self) -> str:
|
|
75
|
-
rc, rid = self._getRoomContext()
|
|
76
|
-
return rc.getRoomOperatingStateDemand(rid)
|
|
77
|
-
|
|
78
|
-
# --- Heating programs ---
|
|
79
|
-
|
|
80
|
-
@handleNotSupported
|
|
81
|
-
def getNormalHeatingTemperature(self) -> float:
|
|
82
|
-
rc, rid = self._getRoomContext()
|
|
83
|
-
return rc.getRoomNormalHeatingTemperature(rid)
|
|
84
|
-
|
|
85
|
-
@handleAPICommandErrors
|
|
86
|
-
def setNormalHeatingTemperature(self, temperature: float) -> None:
|
|
87
|
-
rc, rid = self._getRoomContext()
|
|
88
|
-
rc.setRoomNormalHeatingTemperature(rid, temperature)
|
|
89
|
-
|
|
90
|
-
@handleNotSupported
|
|
91
|
-
def getReducedHeatingTemperature(self) -> float:
|
|
92
|
-
rc, rid = self._getRoomContext()
|
|
93
|
-
return rc.getRoomReducedHeatingTemperature(rid)
|
|
94
|
-
|
|
95
|
-
@handleAPICommandErrors
|
|
96
|
-
def setReducedHeatingTemperature(self, temperature: float) -> None:
|
|
97
|
-
rc, rid = self._getRoomContext()
|
|
98
|
-
rc.setRoomReducedHeatingTemperature(rid, temperature)
|
|
99
|
-
|
|
100
|
-
@handleNotSupported
|
|
101
|
-
def getComfortHeatingTemperature(self) -> float:
|
|
102
|
-
rc, rid = self._getRoomContext()
|
|
103
|
-
return rc.getRoomComfortHeatingTemperature(rid)
|
|
104
|
-
|
|
105
|
-
@handleAPICommandErrors
|
|
106
|
-
def setComfortHeatingTemperature(self, temperature: float) -> None:
|
|
107
|
-
rc, rid = self._getRoomContext()
|
|
108
|
-
rc.setRoomComfortHeatingTemperature(rid, temperature)
|
|
109
|
-
|
|
110
|
-
# --- Quick modes ---
|
|
111
|
-
|
|
112
|
-
@handleNotSupported
|
|
113
|
-
def getManualTillNextScheduleActive(self) -> bool:
|
|
114
|
-
rc, rid = self._getRoomContext()
|
|
115
|
-
return rc.getRoomManualTillNextScheduleActive(rid)
|
|
116
|
-
|
|
117
|
-
@handleAPICommandErrors
|
|
118
|
-
def activateManualTillNextSchedule(self, temperature: float) -> None:
|
|
119
|
-
rc, rid = self._getRoomContext()
|
|
120
|
-
rc.activateRoomManualTillNextSchedule(rid, temperature)
|
|
121
|
-
|
|
122
|
-
@handleAPICommandErrors
|
|
123
|
-
def deactivateManualTillNextSchedule(self) -> None:
|
|
124
|
-
rc, rid = self._getRoomContext()
|
|
125
|
-
rc.deactivateRoomManualTillNextSchedule(rid)
|
|
126
|
-
|
|
127
|
-
# --- Schedule ---
|
|
128
|
-
|
|
129
|
-
@handleNotSupported
|
|
130
|
-
def getSchedule(self) -> dict[str, Any]:
|
|
131
|
-
rc, rid = self._getRoomContext()
|
|
132
|
-
return rc.getRoomSchedule(rid)
|
|
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
|