Skip to content

Commit c07d4f4

Browse files
committed
feat!: prune RoomControl to IoT-scope methods, rebuild fixture
The previous fixture and the matching RoomControl methods were sourced from an Internal-scope dump. The features they relied on (rooms parent, room metadata, schedules, operating programs, quickmodes) are not returned by the public IoT scope used by all developer-portal client_ids, so those methods silently fail for end users. This commit cuts the loss to what actually works on IoT: - New tests/response/RoomControl.json: 2 rooms, 8 features (sensors.temperature, sensors.humidity, co2, condensationRisk per room) sourced from a real IoT-scope dump. - RoomControl reduced to four methods: getRoomTemperature, getRoomHumidity, getRoomCO2, getRoomCondensationRisk. - Bug fix: getRoomCO2 now reads rooms.{id}.co2 with property concentration. Previously it read rooms.{id}.sensors.co2.value, which the IoT scope does not return. - Tests adjusted to the new fixture and surviving method set. BREAKING CHANGE: Removed from RoomControl: getAvailableRooms, getRoomActorIds, getRoomName, getRoomType, getRoomOperatingState{Level,Demand,Reason}, getRoom{Normal,Reduced,Comfort}HeatingTemperature plus setters, getRoomSchedule, getRoomManualTillNextScheduleActive plus activate/deactivateRoomManualTillNextSchedule. These all relied on features the IoT scope does not return.
1 parent 60880e1 commit c07d4f4

3 files changed

Lines changed: 176 additions & 149 deletions

File tree

PyViCare/PyViCareRoomControl.py

Lines changed: 4 additions & 102 deletions
Original file line numberDiff line numberDiff line change
@@ -1,120 +1,22 @@
1-
import logging
2-
from typing import Any, cast
3-
41
from PyViCare.PyViCareDevice import Device
5-
from PyViCare.PyViCareUtils import PyViCareNotSupportedFeatureError, handleNotSupported, handleAPICommandErrors
6-
7-
logger = logging.getLogger(__name__)
8-
logger.addHandler(logging.NullHandler())
92

103

114
class RoomControl(Device):
125
"""Viessmann RoomControl virtual device.
136
14-
Aggregates room sensor data and heating programs per room.
7+
Exposes per-room sensor readings. The IoT scope only returns sensor
8+
data; room metadata, schedules and operating programs are not
9+
available to public client_ids.
1510
"""
1611

17-
@handleNotSupported
18-
def getAvailableRooms(self) -> list[str]:
19-
return cast(list[str], self.service.getProperty("rooms")["properties"]["enabled"]["value"])
20-
21-
def getRoomActorIds(self, room_id: str) -> list[str]:
22-
"""Return list of actor device IDs for a room."""
23-
try:
24-
actors = self.service.getProperty(f"rooms.{room_id}")["properties"]["actors"]["value"]
25-
return [str(a["deviceId"]) for a in actors]
26-
except (PyViCareNotSupportedFeatureError, KeyError):
27-
return []
28-
29-
def getRoomName(self, room_id: str) -> str | None:
30-
try:
31-
return str(self.service.getProperty(f"rooms.{room_id}")["properties"]["name"]["value"])
32-
except (PyViCareNotSupportedFeatureError, KeyError):
33-
return None
34-
35-
def getRoomType(self, room_id: str) -> str | None:
36-
try:
37-
return str(self.service.getProperty(f"rooms.{room_id}")["properties"]["type"]["value"])
38-
except (PyViCareNotSupportedFeatureError, KeyError):
39-
return None
40-
41-
# --- Sensors ---
42-
4312
def getRoomTemperature(self, room_id: str) -> float:
4413
return float(self.service.getProperty(f"rooms.{room_id}.sensors.temperature")["properties"]["value"]["value"])
4514

4615
def getRoomHumidity(self, room_id: str) -> float:
4716
return float(self.service.getProperty(f"rooms.{room_id}.sensors.humidity")["properties"]["value"]["value"])
4817

4918
def getRoomCO2(self, room_id: str) -> int:
50-
return int(self.service.getProperty(f"rooms.{room_id}.sensors.co2")["properties"]["value"]["value"])
19+
return int(self.service.getProperty(f"rooms.{room_id}.co2")["properties"]["concentration"]["value"])
5120

5221
def getRoomCondensationRisk(self, room_id: str) -> bool:
5322
return bool(self.service.getProperty(f"rooms.{room_id}.condensationRisk")["properties"]["value"]["value"])
54-
55-
# --- Operating state ---
56-
57-
def getRoomOperatingStateLevel(self, room_id: str) -> str:
58-
return str(self.service.getProperty(f"rooms.{room_id}.operating.state")["properties"]["level"]["value"])
59-
60-
def getRoomOperatingStateDemand(self, room_id: str) -> str:
61-
return str(self.service.getProperty(f"rooms.{room_id}.operating.state")["properties"]["demand"]["value"])
62-
63-
def getRoomOperatingStateReason(self, room_id: str) -> str:
64-
return str(self.service.getProperty(f"rooms.{room_id}.operating.state")["properties"]["reason"]["value"])
65-
66-
# --- Heating programs ---
67-
68-
def getRoomNormalHeatingTemperature(self, room_id: str) -> float:
69-
return float(self.service.getProperty(f"rooms.{room_id}.operating.programs.normalHeating")["properties"]["temperature"]["value"])
70-
71-
@handleAPICommandErrors
72-
def setRoomNormalHeatingTemperature(self, room_id: str, temperature: float) -> None:
73-
self.service.setProperty(f"rooms.{room_id}.operating.programs.normalHeating", "setTemperature",
74-
{"targetTemperature": temperature})
75-
76-
def getRoomReducedHeatingTemperature(self, room_id: str) -> float:
77-
return float(self.service.getProperty(f"rooms.{room_id}.operating.programs.reducedHeating")["properties"]["temperature"]["value"])
78-
79-
@handleAPICommandErrors
80-
def setRoomReducedHeatingTemperature(self, room_id: str, temperature: float) -> None:
81-
self.service.setProperty(f"rooms.{room_id}.operating.programs.reducedHeating", "setTemperature",
82-
{"targetTemperature": temperature})
83-
84-
def getRoomComfortHeatingTemperature(self, room_id: str) -> float:
85-
return float(self.service.getProperty(f"rooms.{room_id}.operating.programs.comfortHeating")["properties"]["temperature"]["value"])
86-
87-
@handleAPICommandErrors
88-
def setRoomComfortHeatingTemperature(self, room_id: str, temperature: float) -> None:
89-
self.service.setProperty(f"rooms.{room_id}.operating.programs.comfortHeating", "setTemperature",
90-
{"targetTemperature": temperature})
91-
92-
# --- Schedule ---
93-
94-
def getRoomSchedule(self, room_id: str) -> dict[str, Any]:
95-
props = self.service.getProperty(f"rooms.{room_id}.schedule")["properties"]
96-
return {
97-
"active": props["active"]["value"],
98-
"mon": props["entries"]["value"]["mon"],
99-
"tue": props["entries"]["value"]["tue"],
100-
"wed": props["entries"]["value"]["wed"],
101-
"thu": props["entries"]["value"]["thu"],
102-
"fri": props["entries"]["value"]["fri"],
103-
"sat": props["entries"]["value"]["sat"],
104-
"sun": props["entries"]["value"]["sun"],
105-
}
106-
107-
# --- Quick modes ---
108-
109-
def getRoomManualTillNextScheduleActive(self, room_id: str) -> bool:
110-
return bool(self.service.getProperty(
111-
f"rooms.{room_id}.quickmodes.manualTillNextSchedule")["properties"]["active"]["value"])
112-
113-
@handleAPICommandErrors
114-
def activateRoomManualTillNextSchedule(self, room_id: str, temperature: float) -> None:
115-
self.service.setProperty(f"rooms.{room_id}.quickmodes.manualTillNextSchedule", "activate",
116-
{"temperature": temperature})
117-
118-
@handleAPICommandErrors
119-
def deactivateRoomManualTillNextSchedule(self, room_id: str) -> None:
120-
self.service.setProperty(f"rooms.{room_id}.quickmodes.manualTillNextSchedule", "deactivate", {})

tests/response/RoomControl.json

Lines changed: 162 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,162 @@
1+
{
2+
"data": [
3+
{
4+
"apiVersion": 1,
5+
"commands": {},
6+
"deviceId": "RoomControl-1",
7+
"feature": "rooms.0.sensors.temperature",
8+
"gatewayId": "################",
9+
"isEnabled": true,
10+
"isReady": true,
11+
"properties": {
12+
"status": {
13+
"type": "string",
14+
"value": "connected"
15+
},
16+
"value": {
17+
"type": "number",
18+
"unit": "celsius",
19+
"value": 23.4
20+
}
21+
},
22+
"timestamp": "2026-05-07T00:00:00.000Z",
23+
"uri": "https://api.viessmann-climatesolutions.com/iot/v2/features/installations/0/gateways/################/devices/RoomControl-1/features/rooms.0.sensors.temperature"
24+
},
25+
{
26+
"apiVersion": 1,
27+
"commands": {},
28+
"deviceId": "RoomControl-1",
29+
"feature": "rooms.0.sensors.humidity",
30+
"gatewayId": "################",
31+
"isEnabled": true,
32+
"isReady": true,
33+
"properties": {
34+
"status": {
35+
"type": "string",
36+
"value": "connected"
37+
},
38+
"value": {
39+
"type": "number",
40+
"unit": "percent",
41+
"value": 49
42+
}
43+
},
44+
"timestamp": "2026-05-07T00:00:00.000Z",
45+
"uri": "https://api.viessmann-climatesolutions.com/iot/v2/features/installations/0/gateways/################/devices/RoomControl-1/features/rooms.0.sensors.humidity"
46+
},
47+
{
48+
"apiVersion": 1,
49+
"commands": {},
50+
"deviceId": "RoomControl-1",
51+
"feature": "rooms.0.co2",
52+
"gatewayId": "################",
53+
"isEnabled": true,
54+
"isReady": true,
55+
"properties": {
56+
"concentration": {
57+
"type": "number",
58+
"unit": "partsPerMillion",
59+
"value": 1000
60+
}
61+
},
62+
"timestamp": "2026-05-07T00:00:00.000Z",
63+
"uri": "https://api.viessmann-climatesolutions.com/iot/v2/features/installations/0/gateways/################/devices/RoomControl-1/features/rooms.0.co2"
64+
},
65+
{
66+
"apiVersion": 1,
67+
"commands": {},
68+
"deviceId": "RoomControl-1",
69+
"feature": "rooms.0.condensationRisk",
70+
"gatewayId": "################",
71+
"isEnabled": true,
72+
"isReady": true,
73+
"properties": {
74+
"value": {
75+
"type": "boolean",
76+
"value": false
77+
}
78+
},
79+
"timestamp": "2026-05-07T00:00:00.000Z",
80+
"uri": "https://api.viessmann-climatesolutions.com/iot/v2/features/installations/0/gateways/################/devices/RoomControl-1/features/rooms.0.condensationRisk"
81+
},
82+
{
83+
"apiVersion": 1,
84+
"commands": {},
85+
"deviceId": "RoomControl-1",
86+
"feature": "rooms.1.sensors.temperature",
87+
"gatewayId": "################",
88+
"isEnabled": true,
89+
"isReady": true,
90+
"properties": {
91+
"status": {
92+
"type": "string",
93+
"value": "connected"
94+
},
95+
"value": {
96+
"type": "number",
97+
"unit": "celsius",
98+
"value": 22.8
99+
}
100+
},
101+
"timestamp": "2026-05-07T00:00:00.000Z",
102+
"uri": "https://api.viessmann-climatesolutions.com/iot/v2/features/installations/0/gateways/################/devices/RoomControl-1/features/rooms.1.sensors.temperature"
103+
},
104+
{
105+
"apiVersion": 1,
106+
"commands": {},
107+
"deviceId": "RoomControl-1",
108+
"feature": "rooms.1.sensors.humidity",
109+
"gatewayId": "################",
110+
"isEnabled": true,
111+
"isReady": true,
112+
"properties": {
113+
"status": {
114+
"type": "string",
115+
"value": "connected"
116+
},
117+
"value": {
118+
"type": "number",
119+
"unit": "percent",
120+
"value": 56
121+
}
122+
},
123+
"timestamp": "2026-05-07T00:00:00.000Z",
124+
"uri": "https://api.viessmann-climatesolutions.com/iot/v2/features/installations/0/gateways/################/devices/RoomControl-1/features/rooms.1.sensors.humidity"
125+
},
126+
{
127+
"apiVersion": 1,
128+
"commands": {},
129+
"deviceId": "RoomControl-1",
130+
"feature": "rooms.1.co2",
131+
"gatewayId": "################",
132+
"isEnabled": true,
133+
"isReady": true,
134+
"properties": {
135+
"concentration": {
136+
"type": "number",
137+
"unit": "partsPerMillion",
138+
"value": 850
139+
}
140+
},
141+
"timestamp": "2026-05-07T00:00:00.000Z",
142+
"uri": "https://api.viessmann-climatesolutions.com/iot/v2/features/installations/0/gateways/################/devices/RoomControl-1/features/rooms.1.co2"
143+
},
144+
{
145+
"apiVersion": 1,
146+
"commands": {},
147+
"deviceId": "RoomControl-1",
148+
"feature": "rooms.1.condensationRisk",
149+
"gatewayId": "################",
150+
"isEnabled": true,
151+
"isReady": true,
152+
"properties": {
153+
"value": {
154+
"type": "boolean",
155+
"value": false
156+
}
157+
},
158+
"timestamp": "2026-05-07T00:00:00.000Z",
159+
"uri": "https://api.viessmann-climatesolutions.com/iot/v2/features/installations/0/gateways/################/devices/RoomControl-1/features/rooms.1.condensationRisk"
160+
}
161+
]
162+
}

tests/test_RoomControl.py

Lines changed: 10 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -9,55 +9,18 @@ def setUp(self):
99
self.service = ViCareServiceMock('response/RoomControl.json')
1010
self.device = RoomControl(self.service)
1111

12-
def test_getAvailableRooms(self):
13-
rooms = self.device.getAvailableRooms()
14-
self.assertIn("0", rooms)
15-
self.assertEqual(len(rooms), 10)
16-
17-
def test_getRoomActorIds(self):
18-
actors = self.device.getRoomActorIds("0")
19-
self.assertIsInstance(actors, list)
20-
self.assertTrue(len(actors) > 0)
21-
22-
def test_getRoomName(self):
23-
self.assertEqual(self.device.getRoomName("0"), "Bedroom")
24-
self.assertEqual(self.device.getRoomName("4"), "Living Room")
25-
26-
def test_getRoomType(self):
27-
self.assertEqual(self.device.getRoomType("0"), "bedroom")
28-
self.assertEqual(self.device.getRoomType("4"), "livingroom")
29-
3012
def test_getRoomTemperature(self):
31-
self.assertAlmostEqual(self.device.getRoomTemperature("0"), 20.7)
13+
self.assertAlmostEqual(self.device.getRoomTemperature("0"), 23.4)
14+
self.assertAlmostEqual(self.device.getRoomTemperature("1"), 22.8)
3215

3316
def test_getRoomHumidity(self):
34-
self.assertEqual(self.device.getRoomHumidity("0"), 53)
35-
36-
def test_getRoomCondensationRisk(self):
37-
result = self.device.getRoomCondensationRisk("0")
38-
self.assertIsNotNone(result)
17+
self.assertEqual(self.device.getRoomHumidity("0"), 49)
18+
self.assertEqual(self.device.getRoomHumidity("1"), 56)
3919

40-
def test_getRoomOperatingStateLevel(self):
41-
result = self.device.getRoomOperatingStateLevel("0")
42-
self.assertIsNotNone(result)
20+
def test_getRoomCO2(self):
21+
self.assertEqual(self.device.getRoomCO2("0"), 1000)
22+
self.assertEqual(self.device.getRoomCO2("1"), 850)
4323

44-
def test_getRoomNormalHeatingTemperature(self):
45-
temp = self.device.getRoomNormalHeatingTemperature("0")
46-
self.assertIsInstance(temp, (int, float))
47-
48-
def test_getRoomReducedHeatingTemperature(self):
49-
temp = self.device.getRoomReducedHeatingTemperature("0")
50-
self.assertIsInstance(temp, (int, float))
51-
52-
def test_getRoomComfortHeatingTemperature(self):
53-
temp = self.device.getRoomComfortHeatingTemperature("0")
54-
self.assertIsInstance(temp, (int, float))
55-
56-
def test_getRoomSchedule(self):
57-
schedule = self.device.getRoomSchedule("0")
58-
self.assertIn("active", schedule)
59-
self.assertIn("mon", schedule)
60-
61-
def test_getRoomManualTillNextScheduleActive(self):
62-
result = self.device.getRoomManualTillNextScheduleActive("0")
63-
self.assertIsInstance(result, bool)
24+
def test_getRoomCondensationRisk(self):
25+
self.assertFalse(self.device.getRoomCondensationRisk("0"))
26+
self.assertFalse(self.device.getRoomCondensationRisk("1"))

0 commit comments

Comments
 (0)