PyViCare 2.59.0__tar.gz → 2.60.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.59.0 → pyvicare-2.60.0}/PKG-INFO +1 -1
- {pyvicare-2.59.0 → pyvicare-2.60.0}/PyViCare/PyViCare.py +42 -7
- {pyvicare-2.59.0 → pyvicare-2.60.0}/PyViCare/PyViCareAbstractOAuthManager.py +34 -3
- {pyvicare-2.59.0 → pyvicare-2.60.0}/PyViCare/PyViCareDeviceConfig.py +35 -3
- pyvicare-2.60.0/PyViCare/PyViCareRoomControl.py +136 -0
- pyvicare-2.60.0/PyViCare/PyViCareRoomSensor.py +132 -0
- {pyvicare-2.59.0 → pyvicare-2.60.0}/pyproject.toml +1 -1
- pyvicare-2.59.0/PyViCare/PyViCareRoomSensor.py +0 -17
- {pyvicare-2.59.0 → pyvicare-2.60.0}/LICENSE +0 -0
- {pyvicare-2.59.0 → pyvicare-2.60.0}/PyViCare/Feature.py +0 -0
- {pyvicare-2.59.0 → pyvicare-2.60.0}/PyViCare/PyViCareBrowserOAuthManager.py +0 -0
- {pyvicare-2.59.0 → pyvicare-2.60.0}/PyViCare/PyViCareCachedService.py +0 -0
- {pyvicare-2.59.0 → pyvicare-2.60.0}/PyViCare/PyViCareDevice.py +0 -0
- {pyvicare-2.59.0 → pyvicare-2.60.0}/PyViCare/PyViCareElectricalEnergySystem.py +0 -0
- {pyvicare-2.59.0 → pyvicare-2.60.0}/PyViCare/PyViCareFloorHeating.py +0 -0
- {pyvicare-2.59.0 → pyvicare-2.60.0}/PyViCare/PyViCareFuelCell.py +0 -0
- {pyvicare-2.59.0 → pyvicare-2.60.0}/PyViCare/PyViCareGateway.py +0 -0
- {pyvicare-2.59.0 → pyvicare-2.60.0}/PyViCare/PyViCareGazBoiler.py +0 -0
- {pyvicare-2.59.0 → pyvicare-2.60.0}/PyViCare/PyViCareHeatCurveCalculation.py +0 -0
- {pyvicare-2.59.0 → pyvicare-2.60.0}/PyViCare/PyViCareHeatPump.py +0 -0
- {pyvicare-2.59.0 → pyvicare-2.60.0}/PyViCare/PyViCareHeatingDevice.py +0 -0
- {pyvicare-2.59.0 → pyvicare-2.60.0}/PyViCare/PyViCareHybrid.py +0 -0
- {pyvicare-2.59.0 → pyvicare-2.60.0}/PyViCare/PyViCareOAuthManager.py +0 -0
- {pyvicare-2.59.0 → pyvicare-2.60.0}/PyViCare/PyViCareOilBoiler.py +0 -0
- {pyvicare-2.59.0 → pyvicare-2.60.0}/PyViCare/PyViCarePelletsBoiler.py +0 -0
- {pyvicare-2.59.0 → pyvicare-2.60.0}/PyViCare/PyViCareRadiatorActuator.py +0 -0
- {pyvicare-2.59.0 → pyvicare-2.60.0}/PyViCare/PyViCareRepeater.py +0 -0
- {pyvicare-2.59.0 → pyvicare-2.60.0}/PyViCare/PyViCareService.py +0 -0
- {pyvicare-2.59.0 → pyvicare-2.60.0}/PyViCare/PyViCareUtils.py +0 -0
- {pyvicare-2.59.0 → pyvicare-2.60.0}/PyViCare/PyViCareVentilationDevice.py +0 -0
- {pyvicare-2.59.0 → pyvicare-2.60.0}/PyViCare/__init__.py +0 -0
- {pyvicare-2.59.0 → pyvicare-2.60.0}/README.md +0 -0
|
@@ -6,8 +6,9 @@ from PyViCare.PyViCareBrowserOAuthManager import ViCareBrowserOAuthManager
|
|
|
6
6
|
from PyViCare.PyViCareCachedService import ViCareCachedService
|
|
7
7
|
from PyViCare.PyViCareDeviceConfig import PyViCareDeviceConfig
|
|
8
8
|
from PyViCare.PyViCareOAuthManager import ViCareOAuthManager
|
|
9
|
+
from PyViCare.PyViCareRoomControl import RoomControl
|
|
9
10
|
from PyViCare.PyViCareService import ViCareDeviceAccessor, ViCareService
|
|
10
|
-
from PyViCare.PyViCareUtils import PyViCareInvalidDataError
|
|
11
|
+
from PyViCare.PyViCareUtils import PyViCareInvalidDataError, PyViCareNotSupportedFeatureError
|
|
11
12
|
|
|
12
13
|
logger = logging.getLogger(__name__)
|
|
13
14
|
logger.addHandler(logging.NullHandler())
|
|
@@ -46,23 +47,57 @@ class PyViCare:
|
|
|
46
47
|
|
|
47
48
|
data = installations['data']
|
|
48
49
|
self.installations = Wrap(data)
|
|
49
|
-
self.
|
|
50
|
+
self.all_devices = list(self.__extract_all_devices())
|
|
51
|
+
self.devices = [d for d in self.all_devices
|
|
52
|
+
if d.device_type in self.SUPPORTED_DEVICE_TYPES]
|
|
53
|
+
self.__enrichZigbeeDevices()
|
|
50
54
|
|
|
51
|
-
|
|
55
|
+
SUPPORTED_DEVICE_TYPES = [
|
|
56
|
+
"heating", "zigbee", "vitoconnect", "electricityStorage",
|
|
57
|
+
"tcu", "ventilation", "roomControl",
|
|
58
|
+
]
|
|
59
|
+
|
|
60
|
+
def __extract_all_devices(self):
|
|
52
61
|
for installation in self.installations:
|
|
53
62
|
for gateway in installation.gateways:
|
|
54
63
|
for device in gateway.devices:
|
|
55
|
-
if device.deviceType not in ["heating", "zigbee", "vitoconnect", "electricityStorage", "tcu", "ventilation"]:
|
|
56
|
-
continue # we are only interested in heating, photovoltaic, electricityStorage, and ventilation devices
|
|
57
|
-
|
|
58
64
|
accessor = ViCareDeviceAccessor(
|
|
59
65
|
installation.id, gateway.serial, device.id)
|
|
60
66
|
service = self.__buildService(accessor, device.roles)
|
|
61
67
|
|
|
62
|
-
logger.info("Device found: %s", device.modelId)
|
|
68
|
+
logger.info("Device found: %s (type=%s)", device.modelId, device.deviceType)
|
|
63
69
|
|
|
64
70
|
yield PyViCareDeviceConfig(service, device.id, device.modelId, device.status, device.deviceType, device.roles)
|
|
65
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 (KeyError, IndexError, PyViCareNotSupportedFeatureError):
|
|
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())
|
|
100
|
+
|
|
66
101
|
|
|
67
102
|
class DictWrap(object):
|
|
68
103
|
def __init__(self, d):
|
|
@@ -44,7 +44,9 @@ class AbstractViCareOAuthManager:
|
|
|
44
44
|
def get(self, url: str) -> Any:
|
|
45
45
|
try:
|
|
46
46
|
logger.debug(self.__oauth)
|
|
47
|
-
|
|
47
|
+
raw_response = self.__oauth.get(f"{API_BASE_URL}{url}", timeout=31)
|
|
48
|
+
self.__raise_on_non_json_error(raw_response)
|
|
49
|
+
response = raw_response.json()
|
|
48
50
|
logger.debug("Response to get request: %s", response)
|
|
49
51
|
self.__handle_expired_token(response)
|
|
50
52
|
self.__handle_rate_limit(response)
|
|
@@ -58,6 +60,21 @@ class AbstractViCareOAuthManager:
|
|
|
58
60
|
except InvalidTokenError:
|
|
59
61
|
self.renewToken()
|
|
60
62
|
return self.get(url)
|
|
63
|
+
except OSError as e:
|
|
64
|
+
raise PyViCareInternalServerError(
|
|
65
|
+
{"statusCode": 0,
|
|
66
|
+
"message": str(e),
|
|
67
|
+
"viErrorId": "n/a"}) from e
|
|
68
|
+
|
|
69
|
+
def __raise_on_non_json_error(self, response):
|
|
70
|
+
"""Guard against non-JSON error responses (e.g. 502 HTML pages from API gateway)."""
|
|
71
|
+
if response.status_code >= 500:
|
|
72
|
+
content_type = response.headers.get('content-type', '')
|
|
73
|
+
if 'application/json' not in content_type:
|
|
74
|
+
raise PyViCareInternalServerError(
|
|
75
|
+
{"statusCode": response.status_code,
|
|
76
|
+
"message": f"Non-JSON {response.status_code} response",
|
|
77
|
+
"viErrorId": "n/a"})
|
|
61
78
|
|
|
62
79
|
def __handle_expired_token(self, response):
|
|
63
80
|
if ("error" in response and response["error"] == "EXPIRED TOKEN"):
|
|
@@ -82,6 +99,13 @@ class AbstractViCareOAuthManager:
|
|
|
82
99
|
if ("statusCode" in response and response["statusCode"] >= 500):
|
|
83
100
|
raise PyViCareInternalServerError(response)
|
|
84
101
|
|
|
102
|
+
extended = response.get("extendedPayload", {})
|
|
103
|
+
if isinstance(extended, dict) and extended.get("code") in ("500", "502", "503"):
|
|
104
|
+
raise PyViCareInternalServerError(
|
|
105
|
+
{"statusCode": int(extended["code"]),
|
|
106
|
+
"message": extended.get("reason", ""),
|
|
107
|
+
"viErrorId": response.get("viErrorId", "n/a")})
|
|
108
|
+
|
|
85
109
|
def __handle_command_error(self, response):
|
|
86
110
|
if not Feature.raise_exception_on_command_failure:
|
|
87
111
|
return
|
|
@@ -106,8 +130,10 @@ class AbstractViCareOAuthManager:
|
|
|
106
130
|
headers = {"Content-Type": "application/json",
|
|
107
131
|
"Accept": "application/vnd.siren+json"}
|
|
108
132
|
try:
|
|
109
|
-
|
|
110
|
-
f"{API_BASE_URL}{url}", data, headers=headers)
|
|
133
|
+
raw_response = self.__oauth.post(
|
|
134
|
+
f"{API_BASE_URL}{url}", data, headers=headers)
|
|
135
|
+
self.__raise_on_non_json_error(raw_response)
|
|
136
|
+
response = raw_response.json()
|
|
111
137
|
self.__handle_expired_token(response)
|
|
112
138
|
self.__handle_rate_limit(response)
|
|
113
139
|
self.__handle_command_error(response)
|
|
@@ -118,3 +144,8 @@ class AbstractViCareOAuthManager:
|
|
|
118
144
|
except InvalidTokenError:
|
|
119
145
|
self.renewToken()
|
|
120
146
|
return self.post(url, data)
|
|
147
|
+
except OSError as e:
|
|
148
|
+
raise PyViCareInternalServerError(
|
|
149
|
+
{"statusCode": 0,
|
|
150
|
+
"message": str(e),
|
|
151
|
+
"viErrorId": "n/a"}) from e
|
|
@@ -4,6 +4,7 @@ import re
|
|
|
4
4
|
|
|
5
5
|
from PyViCare.PyViCareFloorHeating import FloorHeating, FloorHeatingChannel
|
|
6
6
|
from PyViCare.PyViCareFuelCell import FuelCell
|
|
7
|
+
from PyViCare.PyViCareRoomControl import RoomControl
|
|
7
8
|
from PyViCare.PyViCareGazBoiler import GazBoiler
|
|
8
9
|
from PyViCare.PyViCareHeatingDevice import HeatingDevice
|
|
9
10
|
from PyViCare.PyViCareHeatPump import HeatPump
|
|
@@ -22,7 +23,7 @@ logger.addHandler(logging.NullHandler())
|
|
|
22
23
|
|
|
23
24
|
|
|
24
25
|
class PyViCareDeviceConfig:
|
|
25
|
-
# pylint: disable=too-many-arguments,too-many-positional-arguments
|
|
26
|
+
# pylint: disable=too-many-arguments,too-many-positional-arguments,too-many-instance-attributes
|
|
26
27
|
def __init__(self, service, device_id, device_model, status, device_type=None, roles=None):
|
|
27
28
|
self.service = service
|
|
28
29
|
self.device_id = device_id
|
|
@@ -30,6 +31,8 @@ class PyViCareDeviceConfig:
|
|
|
30
31
|
self.status = status
|
|
31
32
|
self.device_type = device_type
|
|
32
33
|
self.roles = roles if roles is not None else []
|
|
34
|
+
self._room_control = None
|
|
35
|
+
self._room_id = None
|
|
33
36
|
|
|
34
37
|
def asGeneric(self):
|
|
35
38
|
return HeatingDevice(self.service)
|
|
@@ -62,7 +65,18 @@ class PyViCareDeviceConfig:
|
|
|
62
65
|
return FloorHeatingChannel(self.service)
|
|
63
66
|
|
|
64
67
|
def asRoomSensor(self):
|
|
65
|
-
|
|
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
|
|
72
|
+
|
|
73
|
+
def asRoomControl(self):
|
|
74
|
+
return RoomControl(self.service)
|
|
75
|
+
|
|
76
|
+
def setRoomControlEnrichment(self, room_control, room_id):
|
|
77
|
+
"""Store RoomControl enrichment data to apply when creating a RoomSensor."""
|
|
78
|
+
self._room_control = room_control
|
|
79
|
+
self._room_id = room_id
|
|
66
80
|
|
|
67
81
|
def asRepeater(self):
|
|
68
82
|
return Repeater(self.service)
|
|
@@ -113,6 +127,7 @@ class PyViCareDeviceConfig:
|
|
|
113
127
|
(self.asRadiatorActuator, r"E3_RadiatorActuator", ["type:radiator"]),
|
|
114
128
|
(self.asFloorHeating, r"Smart_zigbee_fht_main|E3_FloorHeatingCircuitDistributorBox", ["type:fhtMain"]),
|
|
115
129
|
(self.asFloorHeatingChannel, r"Smart_zigbee_fht_channel", ["type:fhtChannel"]),
|
|
130
|
+
(self.asRoomControl, r"E3_RoomControl|Smart_RoomControl", ["type:virtual;smartRoomControl"]),
|
|
116
131
|
(self.asRoomSensor, r"E3_RoomSensor", ["type:climateSensor"]),
|
|
117
132
|
(self.asRepeater, r"E3_Repeater", ["type:repeater"]),
|
|
118
133
|
(self.asGateway, r"E3_TCU41_x04", ["type:gateway;TCU100"]),
|
|
@@ -125,11 +140,28 @@ class PyViCareDeviceConfig:
|
|
|
125
140
|
for (creator_method, type_name, roles) in device_types:
|
|
126
141
|
if re.search(type_name, self.device_model) or self.service.hasRoles(roles):
|
|
127
142
|
logger.info("detected %s %s", self.device_model, creator_method.__name__)
|
|
128
|
-
|
|
143
|
+
device = creator_method()
|
|
144
|
+
if isinstance(device, (GazBoiler, HeatPump)) and not isinstance(device, Hybrid):
|
|
145
|
+
if self._isHybridByFeatures():
|
|
146
|
+
logger.info("upgrading %s to Hybrid based on API features", self.device_model)
|
|
147
|
+
return self.asHybridDevice()
|
|
148
|
+
return device
|
|
129
149
|
|
|
130
150
|
logger.info("Could not auto detect %s. Use generic device.", self.device_model)
|
|
131
151
|
return self.asGeneric()
|
|
132
152
|
|
|
153
|
+
def _isHybridByFeatures(self):
|
|
154
|
+
"""Check API features to detect hybrid devices (both burners and compressors)."""
|
|
155
|
+
try:
|
|
156
|
+
features = self.service.fetch_all_features()
|
|
157
|
+
feature_names = [f["feature"] for f in features.get("data", [])]
|
|
158
|
+
has_burners = any(f.startswith("heating.burners") for f in feature_names)
|
|
159
|
+
has_compressors = any(f.startswith("heating.compressors") for f in feature_names)
|
|
160
|
+
return has_burners and has_compressors
|
|
161
|
+
except (KeyError, TypeError, AttributeError, OSError):
|
|
162
|
+
logger.debug("Could not fetch features for hybrid detection of %s", self.device_model)
|
|
163
|
+
return False
|
|
164
|
+
|
|
133
165
|
def get_raw_json(self):
|
|
134
166
|
return self.service.fetch_all_features()
|
|
135
167
|
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
from typing import Any, cast
|
|
3
|
+
|
|
4
|
+
from PyViCare.PyViCareDevice import Device
|
|
5
|
+
from PyViCare.PyViCareUtils import PyViCareNotSupportedFeatureError, handleNotSupported, handleAPICommandErrors
|
|
6
|
+
|
|
7
|
+
logger = logging.getLogger(__name__)
|
|
8
|
+
logger.addHandler(logging.NullHandler())
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class RoomControl(Device):
|
|
12
|
+
"""Viessmann RoomControl virtual device.
|
|
13
|
+
|
|
14
|
+
Aggregates room sensor data and heating programs.
|
|
15
|
+
Used to enrich physical Zigbee devices with room data.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
@handleNotSupported
|
|
19
|
+
def getAvailableRooms(self) -> list[str]:
|
|
20
|
+
return cast(list[str], self.service.getProperty("rooms")["properties"]["enabled"]["value"])
|
|
21
|
+
|
|
22
|
+
def getRoomActorIds(self, room_id: str) -> list[str]:
|
|
23
|
+
"""Return list of actor device IDs for a room."""
|
|
24
|
+
try:
|
|
25
|
+
actors = self.service.getProperty(f"rooms.{room_id}")["properties"]["actors"]["value"]
|
|
26
|
+
return [str(a["deviceId"]) for a in actors]
|
|
27
|
+
except (PyViCareNotSupportedFeatureError, KeyError):
|
|
28
|
+
return []
|
|
29
|
+
|
|
30
|
+
def getRoomName(self, room_id: str) -> str | None:
|
|
31
|
+
try:
|
|
32
|
+
return str(self.service.getProperty(f"rooms.{room_id}")["properties"]["name"]["value"])
|
|
33
|
+
except (PyViCareNotSupportedFeatureError, KeyError):
|
|
34
|
+
return None
|
|
35
|
+
|
|
36
|
+
def getRoomType(self, room_id: str) -> str | None:
|
|
37
|
+
try:
|
|
38
|
+
return str(self.service.getProperty(f"rooms.{room_id}")["properties"]["type"]["value"])
|
|
39
|
+
except (PyViCareNotSupportedFeatureError, KeyError):
|
|
40
|
+
return None
|
|
41
|
+
|
|
42
|
+
# --- Sensors ---
|
|
43
|
+
|
|
44
|
+
def getRoomTemperature(self, room_id: str) -> float:
|
|
45
|
+
return float(self.service.getProperty(f"rooms.{room_id}.sensors.temperature")["properties"]["value"]["value"])
|
|
46
|
+
|
|
47
|
+
def getRoomHumidity(self, room_id: str) -> float:
|
|
48
|
+
return float(self.service.getProperty(f"rooms.{room_id}.sensors.humidity")["properties"]["value"]["value"])
|
|
49
|
+
|
|
50
|
+
def getRoomCO2(self, room_id: str) -> int:
|
|
51
|
+
return int(self.service.getProperty(f"rooms.{room_id}.sensors.co2")["properties"]["value"]["value"])
|
|
52
|
+
|
|
53
|
+
def getRoomCondensationRisk(self, room_id: str) -> bool:
|
|
54
|
+
return bool(self.service.getProperty(f"rooms.{room_id}.condensationRisk")["properties"]["value"]["value"])
|
|
55
|
+
|
|
56
|
+
# --- Operating state ---
|
|
57
|
+
|
|
58
|
+
def getRoomOperatingStateLevel(self, room_id: str) -> str:
|
|
59
|
+
return str(self.service.getProperty(f"rooms.{room_id}.operating.state")["properties"]["level"]["value"])
|
|
60
|
+
|
|
61
|
+
def getRoomOperatingStateDemand(self, room_id: str) -> str:
|
|
62
|
+
return str(self.service.getProperty(f"rooms.{room_id}.operating.state")["properties"]["demand"]["value"])
|
|
63
|
+
|
|
64
|
+
def getRoomOperatingStateReason(self, room_id: str) -> str:
|
|
65
|
+
return str(self.service.getProperty(f"rooms.{room_id}.operating.state")["properties"]["reason"]["value"])
|
|
66
|
+
|
|
67
|
+
# --- Heating programs ---
|
|
68
|
+
|
|
69
|
+
def getRoomNormalHeatingTemperature(self, room_id: str) -> float:
|
|
70
|
+
return float(self.service.getProperty(f"rooms.{room_id}.operating.programs.normalHeating")["properties"]["temperature"]["value"])
|
|
71
|
+
|
|
72
|
+
@handleAPICommandErrors
|
|
73
|
+
def setRoomNormalHeatingTemperature(self, room_id: str, temperature: float) -> None:
|
|
74
|
+
self.service.setProperty(f"rooms.{room_id}.operating.programs.normalHeating", "setTemperature",
|
|
75
|
+
{"targetTemperature": temperature})
|
|
76
|
+
|
|
77
|
+
def getRoomReducedHeatingTemperature(self, room_id: str) -> float:
|
|
78
|
+
return float(self.service.getProperty(f"rooms.{room_id}.operating.programs.reducedHeating")["properties"]["temperature"]["value"])
|
|
79
|
+
|
|
80
|
+
@handleAPICommandErrors
|
|
81
|
+
def setRoomReducedHeatingTemperature(self, room_id: str, temperature: float) -> None:
|
|
82
|
+
self.service.setProperty(f"rooms.{room_id}.operating.programs.reducedHeating", "setTemperature",
|
|
83
|
+
{"targetTemperature": temperature})
|
|
84
|
+
|
|
85
|
+
def getRoomComfortHeatingTemperature(self, room_id: str) -> float:
|
|
86
|
+
return float(self.service.getProperty(f"rooms.{room_id}.operating.programs.comfortHeating")["properties"]["temperature"]["value"])
|
|
87
|
+
|
|
88
|
+
@handleAPICommandErrors
|
|
89
|
+
def setRoomComfortHeatingTemperature(self, room_id: str, temperature: float) -> None:
|
|
90
|
+
self.service.setProperty(f"rooms.{room_id}.operating.programs.comfortHeating", "setTemperature",
|
|
91
|
+
{"targetTemperature": temperature})
|
|
92
|
+
|
|
93
|
+
# --- Schedule ---
|
|
94
|
+
|
|
95
|
+
def getRoomSchedule(self, room_id: str) -> dict[str, Any]:
|
|
96
|
+
props = self.service.getProperty(f"rooms.{room_id}.schedule")["properties"]
|
|
97
|
+
return {
|
|
98
|
+
"active": props["active"]["value"],
|
|
99
|
+
"mon": props["entries"]["value"]["mon"],
|
|
100
|
+
"tue": props["entries"]["value"]["tue"],
|
|
101
|
+
"wed": props["entries"]["value"]["wed"],
|
|
102
|
+
"thu": props["entries"]["value"]["thu"],
|
|
103
|
+
"fri": props["entries"]["value"]["fri"],
|
|
104
|
+
"sat": props["entries"]["value"]["sat"],
|
|
105
|
+
"sun": props["entries"]["value"]["sun"],
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
# --- Quick modes ---
|
|
109
|
+
|
|
110
|
+
def getRoomManualTillNextScheduleActive(self, room_id: str) -> bool:
|
|
111
|
+
return bool(self.service.getProperty(
|
|
112
|
+
f"rooms.{room_id}.quickmodes.manualTillNextSchedule")["properties"]["active"]["value"])
|
|
113
|
+
|
|
114
|
+
@handleAPICommandErrors
|
|
115
|
+
def activateRoomManualTillNextSchedule(self, room_id: str, temperature: float) -> None:
|
|
116
|
+
self.service.setProperty(f"rooms.{room_id}.quickmodes.manualTillNextSchedule", "activate",
|
|
117
|
+
{"temperature": temperature})
|
|
118
|
+
|
|
119
|
+
@handleAPICommandErrors
|
|
120
|
+
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
|
|
@@ -0,0 +1,132 @@
|
|
|
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)
|
|
@@ -1,17 +0,0 @@
|
|
|
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):
|
|
9
|
-
return self.getProperty("device.sensors.temperature")["deviceId"]
|
|
10
|
-
|
|
11
|
-
@handleNotSupported
|
|
12
|
-
def getTemperature(self):
|
|
13
|
-
return self.getProperty("device.sensors.temperature")["properties"]["value"]["value"]
|
|
14
|
-
|
|
15
|
-
@handleNotSupported
|
|
16
|
-
def getHumidity(self):
|
|
17
|
-
return self.getProperty("device.sensors.humidity")["properties"]["value"]["value"]
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|