From 403a421eb85cd1c938ce06262ee887a9c1ee8bec Mon Sep 17 00:00:00 2001 From: Christian Lackas Date: Thu, 7 May 2026 20:41:52 +0200 Subject: [PATCH] feat!: remove non-functional Zigbee room enrichment The RoomControl-driven Zigbee enrichment introduced in #743 does not work in practice: the rooms.{id}.actors mapping that drives buildActorRoomMap() is not returned by the API for end users, so the enrichment is silently no-op and the lib has no reliable path to associate physical Zigbee devices with rooms. Removes the enrichment plumbing and reverts RoomSensor to direct sensor reads (device.sensors.temperature, device.sensors.humidity). RoomControl itself stays for direct per-room access. BREAKING CHANGE: RoomSensor no longer exposes hybrid getters that required RoomControl context (getRoomName, getRoomType, getCondensationRisk, getOperatingState*, getNormal/Reduced/Comfort HeatingTemperature, getManualTillNextSchedule*, getSchedule). Use the corresponding RoomControl methods with a room_id directly. --- PyViCare/PyViCare.py | 31 -------- PyViCare/PyViCareDeviceConfig.py | 12 +--- PyViCare/PyViCareRoomControl.py | 18 +---- PyViCare/PyViCareRoomSensor.py | 117 +------------------------------ tests/test_RoomControl.py | 78 --------------------- 5 files changed, 3 insertions(+), 253 deletions(-) diff --git a/PyViCare/PyViCare.py b/PyViCare/PyViCare.py index b408eea8..64115a7d 100644 --- a/PyViCare/PyViCare.py +++ b/PyViCare/PyViCare.py @@ -6,7 +6,6 @@ from PyViCare.PyViCareCachedService import ViCareCachedService from PyViCare.PyViCareDeviceConfig import PyViCareDeviceConfig from PyViCare.PyViCareOAuthManager import ViCareOAuthManager -from PyViCare.PyViCareRoomControl import RoomControl from PyViCare.PyViCareService import ViCareDeviceAccessor, ViCareService from PyViCare.PyViCareUtils import PyViCareInvalidDataError @@ -50,7 +49,6 @@ def __loadInstallations(self): self.all_devices = list(self.__extract_all_devices()) self.devices = [d for d in self.all_devices if d.device_type in self.SUPPORTED_DEVICE_TYPES] - self.__enrichZigbeeDevices() SUPPORTED_DEVICE_TYPES = [ "heating", "zigbee", "vitoconnect", "electricityStorage", @@ -69,35 +67,6 @@ def __extract_all_devices(self): yield PyViCareDeviceConfig(service, device.id, device.modelId, device.status, device.deviceType, device.roles) - def __enrichZigbeeDevices(self): - """Enrich Zigbee devices with sensor data from RoomControl. - - Viessmann moved temperature/humidity data from physical Zigbee - sensors to the RoomControl virtual device. This reverses that - mapping by cross-referencing RoomControl actors with Zigbee - device IDs. - """ - devices_by_id = {device_config.device_id: device_config for device_config in self.devices} - - for device_config in self.devices: - if device_config.device_type != "roomControl": - continue - - room_control = RoomControl(device_config.service) - try: - actor_map = room_control.buildActorRoomMap() - except Exception: # pylint: disable=broad-exception-caught - logger.debug("Could not build actor map for %s", device_config.getModel(), exc_info=True) - continue - - for device_id, room_id in actor_map.items(): - zigbee_config = devices_by_id.get(device_id) - if zigbee_config is None: - continue - zigbee_config.setRoomControlEnrichment(room_control, room_id) - logger.info("Enriched %s with room %s data from %s", - zigbee_config.device_id, room_id, device_config.getModel()) - class DictWrap(object): def __init__(self, d): diff --git a/PyViCare/PyViCareDeviceConfig.py b/PyViCare/PyViCareDeviceConfig.py index b4a64a24..364193fa 100644 --- a/PyViCare/PyViCareDeviceConfig.py +++ b/PyViCare/PyViCareDeviceConfig.py @@ -31,8 +31,6 @@ def __init__(self, service, device_id, device_model, status, device_type=None, r self.status = status self.device_type = device_type self.roles = roles if roles is not None else [] - self._room_control = None - self._room_id = None def asGeneric(self): return HeatingDevice(self.service) @@ -65,19 +63,11 @@ def asFloorHeatingChannel(self): return FloorHeatingChannel(self.service) def asRoomSensor(self): - sensor = RoomSensor(self.service) - if self._room_control is not None: - sensor.setRoomControl(self._room_control, self._room_id) - return sensor + return RoomSensor(self.service) def asRoomControl(self): return RoomControl(self.service) - def setRoomControlEnrichment(self, room_control, room_id): - """Store RoomControl enrichment data to apply when creating a RoomSensor.""" - self._room_control = room_control - self._room_id = room_id - def asRepeater(self): return Repeater(self.service) diff --git a/PyViCare/PyViCareRoomControl.py b/PyViCare/PyViCareRoomControl.py index 018e358c..2a5127ed 100644 --- a/PyViCare/PyViCareRoomControl.py +++ b/PyViCare/PyViCareRoomControl.py @@ -11,8 +11,7 @@ class RoomControl(Device): """Viessmann RoomControl virtual device. - Aggregates room sensor data and heating programs. - Used to enrich physical Zigbee devices with room data. + Aggregates room sensor data and heating programs per room. """ @handleNotSupported @@ -119,18 +118,3 @@ def activateRoomManualTillNextSchedule(self, room_id: str, temperature: float) - @handleAPICommandErrors def deactivateRoomManualTillNextSchedule(self, room_id: str) -> None: self.service.setProperty(f"rooms.{room_id}.quickmodes.manualTillNextSchedule", "deactivate", {}) - - # --- Mapping --- - - def buildActorRoomMap(self) -> dict[str, str]: - """Build a mapping of actor device ID -> room ID.""" - actor_map: dict[str, str] = {} - try: - rooms = self.getAvailableRooms() - except PyViCareNotSupportedFeatureError: - return actor_map - - for room_id in rooms: - for actor_id in self.getRoomActorIds(room_id): - actor_map[actor_id] = room_id - return actor_map diff --git a/PyViCare/PyViCareRoomSensor.py b/PyViCare/PyViCareRoomSensor.py index aaf0e8cd..4713bc91 100644 --- a/PyViCare/PyViCareRoomSensor.py +++ b/PyViCare/PyViCareRoomSensor.py @@ -1,132 +1,17 @@ -from __future__ import annotations - -from typing import Any, TYPE_CHECKING - from PyViCare.PyViCareDevice import ZigbeeBatteryDevice -from PyViCare.PyViCareUtils import handleNotSupported, handleAPICommandErrors - -if TYPE_CHECKING: - from PyViCare.PyViCareRoomControl import RoomControl +from PyViCare.PyViCareUtils import handleNotSupported class RoomSensor(ZigbeeBatteryDevice): - _room_control: RoomControl | None = None - _room_id: str | None = None - - def setRoomControl(self, room_control: RoomControl, room_id: str) -> None: - """Enrich this sensor with data from a RoomControl device.""" - self._room_control = room_control - self._room_id = room_id - - def _getRoomContext(self) -> tuple[RoomControl, str]: - """Return (room_control, room_id), raising if not enriched.""" - if self._room_control is None or self._room_id is None: - raise KeyError("roomControl") - return self._room_control, self._room_id - @handleNotSupported def getSerial(self) -> str: return str(self.getProperty("device.sensors.temperature")["deviceId"]) - # --- Sensors (enriched from RoomControl) --- - @handleNotSupported def getTemperature(self) -> float: - if self._room_control is not None and self._room_id is not None: - return self._room_control.getRoomTemperature(self._room_id) return float(self.getProperty("device.sensors.temperature")["properties"]["value"]["value"]) @handleNotSupported def getHumidity(self) -> float: - if self._room_control is not None and self._room_id is not None: - return self._room_control.getRoomHumidity(self._room_id) return float(self.getProperty("device.sensors.humidity")["properties"]["value"]["value"]) - - @handleNotSupported - def getCO2(self) -> int: - rc, rid = self._getRoomContext() - return rc.getRoomCO2(rid) - - @handleNotSupported - def getRoomName(self) -> str | None: - rc, rid = self._getRoomContext() - return rc.getRoomName(rid) - - @handleNotSupported - def getRoomType(self) -> str | None: - rc, rid = self._getRoomContext() - return rc.getRoomType(rid) - - @handleNotSupported - def getCondensationRisk(self) -> bool: - rc, rid = self._getRoomContext() - return rc.getRoomCondensationRisk(rid) - - # --- Operating state --- - - @handleNotSupported - def getOperatingStateLevel(self) -> str: - rc, rid = self._getRoomContext() - return rc.getRoomOperatingStateLevel(rid) - - @handleNotSupported - def getOperatingStateDemand(self) -> str: - rc, rid = self._getRoomContext() - return rc.getRoomOperatingStateDemand(rid) - - # --- Heating programs --- - - @handleNotSupported - def getNormalHeatingTemperature(self) -> float: - rc, rid = self._getRoomContext() - return rc.getRoomNormalHeatingTemperature(rid) - - @handleAPICommandErrors - def setNormalHeatingTemperature(self, temperature: float) -> None: - rc, rid = self._getRoomContext() - rc.setRoomNormalHeatingTemperature(rid, temperature) - - @handleNotSupported - def getReducedHeatingTemperature(self) -> float: - rc, rid = self._getRoomContext() - return rc.getRoomReducedHeatingTemperature(rid) - - @handleAPICommandErrors - def setReducedHeatingTemperature(self, temperature: float) -> None: - rc, rid = self._getRoomContext() - rc.setRoomReducedHeatingTemperature(rid, temperature) - - @handleNotSupported - def getComfortHeatingTemperature(self) -> float: - rc, rid = self._getRoomContext() - return rc.getRoomComfortHeatingTemperature(rid) - - @handleAPICommandErrors - def setComfortHeatingTemperature(self, temperature: float) -> None: - rc, rid = self._getRoomContext() - rc.setRoomComfortHeatingTemperature(rid, temperature) - - # --- Quick modes --- - - @handleNotSupported - def getManualTillNextScheduleActive(self) -> bool: - rc, rid = self._getRoomContext() - return rc.getRoomManualTillNextScheduleActive(rid) - - @handleAPICommandErrors - def activateManualTillNextSchedule(self, temperature: float) -> None: - rc, rid = self._getRoomContext() - rc.activateRoomManualTillNextSchedule(rid, temperature) - - @handleAPICommandErrors - def deactivateManualTillNextSchedule(self) -> None: - rc, rid = self._getRoomContext() - rc.deactivateRoomManualTillNextSchedule(rid) - - # --- Schedule --- - - @handleNotSupported - def getSchedule(self) -> dict[str, Any]: - rc, rid = self._getRoomContext() - return rc.getRoomSchedule(rid) diff --git a/tests/test_RoomControl.py b/tests/test_RoomControl.py index 8b431e87..9c5b9611 100644 --- a/tests/test_RoomControl.py +++ b/tests/test_RoomControl.py @@ -1,12 +1,6 @@ import unittest from PyViCare.PyViCareRoomControl import RoomControl -from PyViCare.PyViCareRoomSensor import RoomSensor -from PyViCare.PyViCareUtils import ( - PyViCareCommandError, - PyViCareNotSupportedFeatureError, - isSupported, -) from tests.ViCareServiceMock import ViCareServiceMock @@ -67,75 +61,3 @@ def test_getRoomSchedule(self): def test_getRoomManualTillNextScheduleActive(self): result = self.device.getRoomManualTillNextScheduleActive("0") self.assertIsInstance(result, bool) - - def test_buildActorRoomMap(self): - actor_map = self.device.buildActorRoomMap() - self.assertIsInstance(actor_map, dict) - self.assertTrue(len(actor_map) > 0) - for room_id in actor_map.values(): - self.assertIsInstance(room_id, str) - - -class RoomSensorEnrichmentTest(unittest.TestCase): - def setUp(self): - self.room_control_service = ViCareServiceMock('response/RoomControl.json') - self.room_control = RoomControl(self.room_control_service) - self.sensor_service = ViCareServiceMock('response/RoomControl.json', - rawInput={"data": []}) - self.sensor = RoomSensor(self.sensor_service) - self.sensor.setRoomControl(self.room_control, "0") - - def test_getTemperature(self): - self.assertAlmostEqual(self.sensor.getTemperature(), 20.7) - - def test_getHumidity(self): - self.assertEqual(self.sensor.getHumidity(), 53) - - def test_getRoomName(self): - self.assertEqual(self.sensor.getRoomName(), "Bedroom") - - def test_getRoomType(self): - self.assertEqual(self.sensor.getRoomType(), "bedroom") - - def test_getCondensationRisk(self): - result = self.sensor.getCondensationRisk() - self.assertIsNotNone(result) - - def test_getOperatingStateLevel(self): - result = self.sensor.getOperatingStateLevel() - self.assertIsNotNone(result) - - def test_getNormalHeatingTemperature(self): - temp = self.sensor.getNormalHeatingTemperature() - self.assertIsInstance(temp, (int, float)) - - def test_getReducedHeatingTemperature(self): - temp = self.sensor.getReducedHeatingTemperature() - self.assertIsInstance(temp, (int, float)) - - def test_getComfortHeatingTemperature(self): - temp = self.sensor.getComfortHeatingTemperature() - self.assertIsInstance(temp, (int, float)) - - def test_getSchedule(self): - schedule = self.sensor.getSchedule() - self.assertIn("active", schedule) - self.assertIn("mon", schedule) - - def test_getManualTillNextScheduleActive(self): - result = self.sensor.getManualTillNextScheduleActive() - self.assertIsInstance(result, bool) - - def test_without_enrichment_reports_not_supported(self): - sensor = RoomSensor(self.sensor_service) - self.assertFalse(isSupported(sensor.getRoomName)) - self.assertFalse(isSupported(sensor.getNormalHeatingTemperature)) - - with self.assertRaises(PyViCareNotSupportedFeatureError): - sensor.getRoomName() - - with self.assertRaises(PyViCareNotSupportedFeatureError): - sensor.getNormalHeatingTemperature() - - with self.assertRaises(PyViCareCommandError): - sensor.setNormalHeatingTemperature(20)