-
Notifications
You must be signed in to change notification settings - Fork 117
feat: enrich Zigbee sensors with room data from RoomControl #743
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
9b5aefb
403f72f
5efde83
9cb3f79
7fe9234
0554fde
48a37ad
a469ad7
5ccddfd
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,136 @@ | ||
| import logging | ||
| from typing import Any, cast | ||
|
|
||
| from PyViCare.PyViCareDevice import Device | ||
| from PyViCare.PyViCareUtils import PyViCareNotSupportedFeatureError, handleNotSupported, handleAPICommandErrors | ||
|
|
||
| logger = logging.getLogger(__name__) | ||
| logger.addHandler(logging.NullHandler()) | ||
|
|
||
|
|
||
| class RoomControl(Device): | ||
| """Viessmann RoomControl virtual device. | ||
|
|
||
| Aggregates room sensor data and heating programs. | ||
| Used to enrich physical Zigbee devices with room data. | ||
| """ | ||
|
|
||
| @handleNotSupported | ||
| def getAvailableRooms(self) -> list[str]: | ||
| return cast(list[str], self.service.getProperty("rooms")["properties"]["enabled"]["value"]) | ||
|
|
||
| def getRoomActorIds(self, room_id: str) -> list[str]: | ||
| """Return list of actor device IDs for a room.""" | ||
| try: | ||
| actors = self.service.getProperty(f"rooms.{room_id}")["properties"]["actors"]["value"] | ||
| return [str(a["deviceId"]) for a in actors] | ||
| except (PyViCareNotSupportedFeatureError, KeyError): | ||
| return [] | ||
|
|
||
| def getRoomName(self, room_id: str) -> str | None: | ||
| try: | ||
| return str(self.service.getProperty(f"rooms.{room_id}")["properties"]["name"]["value"]) | ||
| except (PyViCareNotSupportedFeatureError, KeyError): | ||
| return None | ||
|
|
||
| def getRoomType(self, room_id: str) -> str | None: | ||
| try: | ||
| return str(self.service.getProperty(f"rooms.{room_id}")["properties"]["type"]["value"]) | ||
| except (PyViCareNotSupportedFeatureError, KeyError): | ||
| return None | ||
|
|
||
| # --- Sensors --- | ||
|
|
||
| def getRoomTemperature(self, room_id: str) -> float: | ||
| return float(self.service.getProperty(f"rooms.{room_id}.sensors.temperature")["properties"]["value"]["value"]) | ||
|
|
||
| def getRoomHumidity(self, room_id: str) -> float: | ||
| return float(self.service.getProperty(f"rooms.{room_id}.sensors.humidity")["properties"]["value"]["value"]) | ||
|
|
||
| def getRoomCO2(self, room_id: str) -> int: | ||
| return int(self.service.getProperty(f"rooms.{room_id}.sensors.co2")["properties"]["value"]["value"]) | ||
|
|
||
| def getRoomCondensationRisk(self, room_id: str) -> bool: | ||
| return bool(self.service.getProperty(f"rooms.{room_id}.condensationRisk")["properties"]["value"]["value"]) | ||
|
|
||
| # --- Operating state --- | ||
|
|
||
| def getRoomOperatingStateLevel(self, room_id: str) -> str: | ||
| return str(self.service.getProperty(f"rooms.{room_id}.operating.state")["properties"]["level"]["value"]) | ||
|
|
||
| def getRoomOperatingStateDemand(self, room_id: str) -> str: | ||
| return str(self.service.getProperty(f"rooms.{room_id}.operating.state")["properties"]["demand"]["value"]) | ||
|
|
||
| def getRoomOperatingStateReason(self, room_id: str) -> str: | ||
| return str(self.service.getProperty(f"rooms.{room_id}.operating.state")["properties"]["reason"]["value"]) | ||
|
|
||
| # --- Heating programs --- | ||
|
|
||
| def getRoomNormalHeatingTemperature(self, room_id: str) -> float: | ||
| return float(self.service.getProperty(f"rooms.{room_id}.operating.programs.normalHeating")["properties"]["temperature"]["value"]) | ||
|
|
||
| @handleAPICommandErrors | ||
| def setRoomNormalHeatingTemperature(self, room_id: str, temperature: float) -> None: | ||
| self.service.setProperty(f"rooms.{room_id}.operating.programs.normalHeating", "setTemperature", | ||
| {"targetTemperature": temperature}) | ||
|
|
||
| def getRoomReducedHeatingTemperature(self, room_id: str) -> float: | ||
| return float(self.service.getProperty(f"rooms.{room_id}.operating.programs.reducedHeating")["properties"]["temperature"]["value"]) | ||
|
|
||
| @handleAPICommandErrors | ||
| def setRoomReducedHeatingTemperature(self, room_id: str, temperature: float) -> None: | ||
| self.service.setProperty(f"rooms.{room_id}.operating.programs.reducedHeating", "setTemperature", | ||
| {"targetTemperature": temperature}) | ||
|
|
||
| def getRoomComfortHeatingTemperature(self, room_id: str) -> float: | ||
| return float(self.service.getProperty(f"rooms.{room_id}.operating.programs.comfortHeating")["properties"]["temperature"]["value"]) | ||
|
|
||
| @handleAPICommandErrors | ||
| def setRoomComfortHeatingTemperature(self, room_id: str, temperature: float) -> None: | ||
| self.service.setProperty(f"rooms.{room_id}.operating.programs.comfortHeating", "setTemperature", | ||
| {"targetTemperature": temperature}) | ||
|
|
||
| # --- Schedule --- | ||
|
|
||
| def getRoomSchedule(self, room_id: str) -> dict[str, Any]: | ||
| props = self.service.getProperty(f"rooms.{room_id}.schedule")["properties"] | ||
| return { | ||
| "active": props["active"]["value"], | ||
| "mon": props["entries"]["value"]["mon"], | ||
| "tue": props["entries"]["value"]["tue"], | ||
| "wed": props["entries"]["value"]["wed"], | ||
| "thu": props["entries"]["value"]["thu"], | ||
| "fri": props["entries"]["value"]["fri"], | ||
| "sat": props["entries"]["value"]["sat"], | ||
| "sun": props["entries"]["value"]["sun"], | ||
| } | ||
|
|
||
| # --- Quick modes --- | ||
|
|
||
| def getRoomManualTillNextScheduleActive(self, room_id: str) -> bool: | ||
| return bool(self.service.getProperty( | ||
| f"rooms.{room_id}.quickmodes.manualTillNextSchedule")["properties"]["active"]["value"]) | ||
|
|
||
| @handleAPICommandErrors | ||
| def activateRoomManualTillNextSchedule(self, room_id: str, temperature: float) -> None: | ||
| self.service.setProperty(f"rooms.{room_id}.quickmodes.manualTillNextSchedule", "activate", | ||
| {"temperature": temperature}) | ||
|
|
||
| @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 |
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The room thermostat can be enhanced the same way no?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes, any device that shows up as an actor in a RoomControl room could be enriched the same way. Right now we only do it for RoomSensor, but extending to RadiatorActuator or FloorHeating would be straightforward -- same
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ok, then let's go with this first. |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,17 +1,132 @@ | ||
| from __future__ import annotations | ||
|
|
||
| from typing import Any, TYPE_CHECKING | ||
|
|
||
| from PyViCare.PyViCareDevice import ZigbeeBatteryDevice | ||
| from PyViCare.PyViCareUtils import handleNotSupported | ||
| from PyViCare.PyViCareUtils import handleNotSupported, handleAPICommandErrors | ||
|
|
||
| if TYPE_CHECKING: | ||
| from PyViCare.PyViCareRoomControl import RoomControl | ||
|
|
||
|
|
||
| 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 getSerial(self): | ||
| return self.getProperty("device.sensors.temperature")["deviceId"] | ||
| 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 getTemperature(self): | ||
| return self.getProperty("device.sensors.temperature")["properties"]["value"]["value"] | ||
| 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 getHumidity(self): | ||
| return self.getProperty("device.sensors.humidity")["properties"]["value"]["value"] | ||
| def getSchedule(self) -> dict[str, Any]: | ||
| rc, rid = self._getRoomContext() | ||
| return rc.getRoomSchedule(rid) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Will this class only represent the virtual device that handles the room thermostat mapping or also in-room control real devices?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Only the virtual device (role
type:virtual;smartRoomControl). Physical in-room controllers like Vitotrol 300E are separate zigbee devices with their own device type and class.