Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 0 additions & 31 deletions PyViCare/PyViCare.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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",
Expand All @@ -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):
Expand Down
12 changes: 1 addition & 11 deletions PyViCare/PyViCareDeviceConfig.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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)

Expand Down
18 changes: 1 addition & 17 deletions PyViCare/PyViCareRoomControl.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
117 changes: 1 addition & 116 deletions PyViCare/PyViCareRoomSensor.py
Original file line number Diff line number Diff line change
@@ -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)
78 changes: 0 additions & 78 deletions tests/test_RoomControl.py
Original file line number Diff line number Diff line change
@@ -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


Expand Down Expand Up @@ -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)
Loading