import logging from datetime import datetime from PyViCare.PyViCareAbstractOAuthManager import AbstractViCareOAuthManager from PyViCare.PyViCareBrowserOAuthManager import ViCareBrowserOAuthManager 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 logger = logging.getLogger(__name__) logger.addHandler(logging.NullHandler()) class PyViCare: """"Viessmann ViCare API Python tools""" def __init__(self) -> None: self.cacheDuration = 60 def setCacheDuration(self, cache_duration): self.cacheDuration = int(cache_duration) def initWithCredentials(self, username: str, password: str, client_id: str, token_file: str): self.initWithExternalOAuth(ViCareOAuthManager( username, password, client_id, token_file)) def initWithExternalOAuth(self, oauth_manager: AbstractViCareOAuthManager) -> None: self.oauth_manager = oauth_manager self.__loadInstallations() def initWithBrowserOAuth(self, client_id: str, token_file: str) -> None: self.initWithExternalOAuth(ViCareBrowserOAuthManager(client_id, token_file)) def __buildService(self, accessor, roles): if self.cacheDuration > 0: return ViCareCachedService(self.oauth_manager, accessor, roles, self.cacheDuration) return ViCareService(self.oauth_manager, accessor, roles) def __loadInstallations(self): installations = self.oauth_manager.get( "/equipment/installations?includeGateways=true") if "data" not in installations: logger.error("Missing 'data' property when fetching installations") raise PyViCareInvalidDataError(installations) data = installations['data'] self.installations = Wrap(data) 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", "tcu", "ventilation", "roomControl", ] def __extract_all_devices(self): for installation in self.installations: for gateway in installation.gateways: for device in gateway.devices: accessor = ViCareDeviceAccessor( installation.id, gateway.serial, device.id) service = self.__buildService(accessor, device.roles) logger.info("Device found: %s (type=%s)", device.modelId, device.deviceType) 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): for k, v in d.items(): setattr(self, k, Wrap(v)) def Wrap(v): if isinstance(v, list): return [Wrap(x) for x in v] if isinstance(v, dict): return DictWrap(v) if isinstance(v, str) and len(v) == 24 and v[23] == 'Z' and v[10] == 'T': return datetime.strptime(v, '%Y-%m-%dT%H:%M:%S.%f%z') return v