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.
Files changed (36) hide show
  1. {pyvicare-2.60.2 → pyvicare-2.61.0}/PKG-INFO +1 -1
  2. {pyvicare-2.60.2 → pyvicare-2.61.0}/PyViCare/PyViCare.py +26 -36
  3. pyvicare-2.61.0/PyViCare/PyViCareCachedService.py +23 -0
  4. pyvicare-2.61.0/PyViCare/PyViCareCachedServiceBase.py +82 -0
  5. pyvicare-2.61.0/PyViCare/PyViCareCachedServiceViaGateway.py +34 -0
  6. {pyvicare-2.60.2 → pyvicare-2.61.0}/PyViCare/PyViCareDevice.py +8 -6
  7. {pyvicare-2.60.2 → pyvicare-2.61.0}/PyViCare/PyViCareDeviceConfig.py +50 -35
  8. {pyvicare-2.60.2 → pyvicare-2.61.0}/PyViCare/PyViCareFuelCell.py +1 -1
  9. {pyvicare-2.60.2 → pyvicare-2.61.0}/PyViCare/PyViCareGazBoiler.py +1 -5
  10. {pyvicare-2.60.2 → pyvicare-2.61.0}/PyViCare/PyViCareHeatingDevice.py +19 -4
  11. {pyvicare-2.60.2 → pyvicare-2.61.0}/PyViCare/PyViCareOilBoiler.py +1 -1
  12. {pyvicare-2.60.2 → pyvicare-2.61.0}/PyViCare/PyViCarePelletsBoiler.py +1 -1
  13. {pyvicare-2.60.2 → pyvicare-2.61.0}/PyViCare/PyViCareRoomControl.py +22 -38
  14. pyvicare-2.61.0/PyViCare/PyViCareRoomSensor.py +17 -0
  15. {pyvicare-2.60.2 → pyvicare-2.61.0}/PyViCare/PyViCareService.py +26 -20
  16. pyvicare-2.61.0/PyViCare/PyViCareServiceViaGateway.py +38 -0
  17. {pyvicare-2.60.2 → pyvicare-2.61.0}/PyViCare/PyViCareVentilationDevice.py +29 -0
  18. {pyvicare-2.60.2 → pyvicare-2.61.0}/pyproject.toml +7 -7
  19. pyvicare-2.60.2/PyViCare/PyViCareCachedService.py +0 -72
  20. pyvicare-2.60.2/PyViCare/PyViCareRoomSensor.py +0 -132
  21. {pyvicare-2.60.2 → pyvicare-2.61.0}/LICENSE +0 -0
  22. {pyvicare-2.60.2 → pyvicare-2.61.0}/PyViCare/Feature.py +0 -0
  23. {pyvicare-2.60.2 → pyvicare-2.61.0}/PyViCare/PyViCareAbstractOAuthManager.py +0 -0
  24. {pyvicare-2.60.2 → pyvicare-2.61.0}/PyViCare/PyViCareBrowserOAuthManager.py +0 -0
  25. {pyvicare-2.60.2 → pyvicare-2.61.0}/PyViCare/PyViCareElectricalEnergySystem.py +0 -0
  26. {pyvicare-2.60.2 → pyvicare-2.61.0}/PyViCare/PyViCareFloorHeating.py +0 -0
  27. {pyvicare-2.60.2 → pyvicare-2.61.0}/PyViCare/PyViCareGateway.py +0 -0
  28. {pyvicare-2.60.2 → pyvicare-2.61.0}/PyViCare/PyViCareHeatCurveCalculation.py +0 -0
  29. {pyvicare-2.60.2 → pyvicare-2.61.0}/PyViCare/PyViCareHeatPump.py +0 -0
  30. {pyvicare-2.60.2 → pyvicare-2.61.0}/PyViCare/PyViCareHybrid.py +0 -0
  31. {pyvicare-2.60.2 → pyvicare-2.61.0}/PyViCare/PyViCareOAuthManager.py +0 -0
  32. {pyvicare-2.60.2 → pyvicare-2.61.0}/PyViCare/PyViCareRadiatorActuator.py +0 -0
  33. {pyvicare-2.60.2 → pyvicare-2.61.0}/PyViCare/PyViCareRepeater.py +0 -0
  34. {pyvicare-2.60.2 → pyvicare-2.61.0}/PyViCare/PyViCareUtils.py +0 -0
  35. {pyvicare-2.60.2 → pyvicare-2.61.0}/PyViCare/__init__.py +0 -0
  36. {pyvicare-2.60.2 → pyvicare-2.61.0}/README.md +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: PyViCare
3
- Version: 2.60.2
3
+ Version: 2.61.0
4
4
  Summary: Library to communicate with the Viessmann ViCare API.
5
5
  License: Apache-2.0
6
6
  License-File: LICENSE
@@ -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, accessor, roles):
50
+ def __buildService(self, roles):
37
51
  if self.cacheDuration > 0:
38
- return ViCareCachedService(self.oauth_manager, accessor, roles, self.cacheDuration)
39
- return ViCareService(self.oauth_manager, accessor, roles)
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(accessor, device.roles)
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(service, device.id, device.modelId, device.status, device.deviceType, device.roles)
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 self.service.hasRoles(["type:legacy"])
34
+ return hasRoles(["type:legacy"], self.roles)
33
35
 
34
36
  def isE3Device(self) -> bool:
35
- return self.service.hasRoles(["type:E3"])
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, service, device_id, device_model, status, device_type=None, roles=None):
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
- sensor = RoomSensor(self.service)
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.service.accessor
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.service._isGateway() # pylint: disable=protected-access
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.service.hasRoles(roles):
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": raw_data['data']
194
+ "data": data
180
195
  }
181
196
 
182
197
  if flat:
@@ -17,7 +17,7 @@ class FuelCell(HeatingDevice):
17
17
 
18
18
  @handleNotSupported
19
19
  def getAvailableBurners(self):
20
- return get_available_burners(self.service)
20
+ return get_available_burners(self)
21
21
 
22
22
  @handleNotSupported
23
23
  def getReturnTemperature(self):
@@ -17,7 +17,7 @@ class GazBoiler(HeatingDevice):
17
17
 
18
18
  @handleNotSupported
19
19
  def getAvailableBurners(self):
20
- return get_available_burners(self.service)
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(service):
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 service.getProperty(f"heating.burners.{burner}") is not None:
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 self.service.hasRoles(["type:heatpump", "type:E3"]):
52
+ if hasRoles(["type:heatpump", "type:E3"], self.roles):
52
53
  return heat_curve_formular_variant1
53
- if self.service.hasRoles(["type:heatpump"]) and len(self.getAvailableCircuits()) == 1:
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]:
@@ -17,7 +17,7 @@ class OilBoiler(HeatingDevice):
17
17
 
18
18
  @handleNotSupported
19
19
  def getAvailableBurners(self):
20
- return get_available_burners(self.service)
20
+ return get_available_burners(self)
21
21
 
22
22
  @handleNotSupported
23
23
  def getBoilerTemperature(self):
@@ -16,7 +16,7 @@ class PelletsBoiler(HeatingDevice):
16
16
 
17
17
  @handleNotSupported
18
18
  def getAvailableBurners(self):
19
- return get_available_burners(self.service)
19
+ return get_available_burners(self)
20
20
 
21
21
  @handleNotSupported
22
22
  def getBoilerTemperature(self):
@@ -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.service.getProperty("rooms")["properties"]["enabled"]["value"])
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.service.getProperty(f"rooms.{room_id}")["properties"]["actors"]["value"]
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.service.getProperty(f"rooms.{room_id}")["properties"]["name"]["value"])
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.service.getProperty(f"rooms.{room_id}")["properties"]["type"]["value"])
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.service.getProperty(f"rooms.{room_id}.sensors.temperature")["properties"]["value"]["value"])
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.service.getProperty(f"rooms.{room_id}.sensors.humidity")["properties"]["value"]["value"])
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.service.getProperty(f"rooms.{room_id}.sensors.co2")["properties"]["value"]["value"])
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.service.getProperty(f"rooms.{room_id}.condensationRisk")["properties"]["value"]["value"])
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.service.getProperty(f"rooms.{room_id}.operating.state")["properties"]["level"]["value"])
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.service.getProperty(f"rooms.{room_id}.operating.state")["properties"]["demand"]["value"])
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.service.getProperty(f"rooms.{room_id}.operating.state")["properties"]["reason"]["value"])
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.service.getProperty(f"rooms.{room_id}.operating.programs.normalHeating")["properties"]["temperature"]["value"])
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.service.setProperty(f"rooms.{room_id}.operating.programs.normalHeating", "setTemperature",
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.service.getProperty(f"rooms.{room_id}.operating.programs.reducedHeating")["properties"]["temperature"]["value"])
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.service.setProperty(f"rooms.{room_id}.operating.programs.reducedHeating", "setTemperature",
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.service.getProperty(f"rooms.{room_id}.operating.programs.comfortHeating")["properties"]["temperature"]["value"])
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.service.setProperty(f"rooms.{room_id}.operating.programs.comfortHeating", "setTemperature",
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.service.getProperty(f"rooms.{room_id}.schedule")["properties"]
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.service.getProperty(
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.service.setProperty(f"rooms.{room_id}.quickmodes.manualTillNextSchedule", "activate",
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.service.setProperty(f"rooms.{room_id}.quickmodes.manualTillNextSchedule", "deactivate", {})
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, accessor: ViCareDeviceAccessor, roles: List[str]) -> None:
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._isGateway():
44
- return f'/features/installations/{self.accessor.id}/gateways/{self.accessor.serial}/features/{property_name}'
45
- return f'/features/installations/{self.accessor.id}/gateways/{self.accessor.serial}/devices/{self.accessor.device_id}/features/{property_name}'
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 _isGateway(self) -> bool:
51
- return self.hasRoles(["type:gateway;VitoconnectOpto1"]) or self.hasRoles(["type:gateway;VitoconnectOpto2/OT2"]) or self.hasRoles(["type:gateway;TCU100"]) or self.hasRoles(["type:gateway;TCU200"]) or self.hasRoles(["type:gateway;TCU300"])
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/{self.accessor.id}/gateways/{self.accessor.serial}/devices/{self.accessor.device_id}/features/'
62
- if self._isGateway():
63
- url = f'/features/installations/{self.accessor.id}/gateways/{self.accessor.serial}/features/'
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/{self.accessor.id}/gateways/{self.accessor.serial}/reboot'
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.60.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 = "^2.3.0"
52
- mypy = "^1.11.2"
53
- pylint = "^4.0.0"
54
- pytest = "^9.0.0"
55
- pytest-cov = "^7.0.0"
56
- ruff = "^0.15.0"
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