From d6b35b96fc8e355c3d36e9fafd390a6c350ace6f Mon Sep 17 00:00:00 2001 From: SukramJ Date: Fri, 24 Dec 2021 14:42:32 +0100 Subject: [PATCH] use ACTIVE_PROFILE in climate presets (#90) * Use datetime for last_updated (time_initialized) * Fix example * Sort Fields in EntityDefinition * use ACTIVE_PROFILE in climate presets * Add TEMPERATURE as separate entities for Bidcos thermostats --- changelog.txt | 8 +++ example.py | 3 +- example_multi.py | 3 +- hahomematic/client.py | 25 ++++---- hahomematic/const.py | 1 - hahomematic/decorators.py | 6 +- hahomematic/devices/climate.py | 69 +++++++++++++++++++---- hahomematic/devices/entity_definition.py | 72 +++++++++++++++--------- hahomematic/devices/light.py | 2 - setup.py | 2 +- tests/test_central.py | 2 +- 11 files changed, 132 insertions(+), 61 deletions(-) diff --git a/changelog.txt b/changelog.txt index b9bbb711..9be7b1a5 100644 --- a/changelog.txt +++ b/changelog.txt @@ -1,3 +1,11 @@ +Version 0.4.0 (2021-12-24) +- Use datetime for last_updated (time_initialized) +- Fix example +- Add ACTUAL_TEMPERATURE as separate entity by @towo +- Add HEATING_COOLING to IPThermostat and Group +- Add (*)HUMIDITY and (*)TEMPERATURE as separate entities for Bidcos thermostats +- use ACTIVE_PROFILE in climate presets + Version 0.3.1 (2021-12-23) - Make HmIP-BSM a switch (only dimable devices should be lights) diff --git a/example.py b/example.py index 921a8b5a..127a1841 100644 --- a/example.py +++ b/example.py @@ -29,7 +29,8 @@ def __init__(self): def systemcallback(self, src, *args): self.got_devices = True print("systemcallback: %s" % src) - if src == const.HH_EVENT_NEW_DEVICES: + if src == const.HH_EVENT_NEW_DEVICES and args and args[0] and len(args[0]) > 0: + self.got_devices = True print("Number of new device descriptions: %i" % len(args[0])) return elif src == const.HH_EVENT_DEVICES_CREATED: diff --git a/example_multi.py b/example_multi.py index 3e7acf43..6887b26a 100644 --- a/example_multi.py +++ b/example_multi.py @@ -33,7 +33,8 @@ def __init__(self): def systemcallback(self, src, *args): self.got_devices = True print("systemcallback: %s" % src) - if src == const.HH_EVENT_NEW_DEVICES: + if src == const.HH_EVENT_NEW_DEVICES and args and args[0] and len(args[0]) > 0: + self.got_devices = True print("Number of new device descriptions: %i" % len(args[0])) return elif src == const.HH_EVENT_DEVICES_CREATED: diff --git a/hahomematic/client.py b/hahomematic/client.py index 3063e2a7..22f50a45 100644 --- a/hahomematic/client.py +++ b/hahomematic/client.py @@ -2,8 +2,8 @@ from __future__ import annotations from abc import ABC, abstractmethod +from datetime import datetime, timedelta import logging -import time from typing import Any from hahomematic import config @@ -22,6 +22,7 @@ BACKEND_HOMEGEAR, BACKEND_PYDEVCCU, HM_VIRTUAL_REMOTES, + INIT_DATETIME, PROXY_DE_INIT_FAILED, PROXY_DE_INIT_SKIPPED, PROXY_DE_INIT_SUCCESS, @@ -58,7 +59,7 @@ def __init__(self, client_config: ClientConfig): self._init_url: str = self._client_config.init_url # for all device related interaction self.proxy: XmlRpcProxy = self._client_config.xml_rpc_proxy - self.time_initialized: int = 0 + self.last_updated: datetime = INIT_DATETIME self._json_rpc_session: JsonRpcAioHttpClient = self._central.json_rpc_session self._central.clients[self.interface_id] = self @@ -96,9 +97,9 @@ async def proxy_init(self) -> int: _LOGGER.exception( "proxy_init: Failed to initialize proxy for %s", self.name ) - self.time_initialized = 0 + self.last_updated = INIT_DATETIME return PROXY_INIT_FAILED - self.time_initialized = int(time.time()) + self.last_updated = datetime.now() return PROXY_INIT_SUCCESS async def proxy_de_init(self) -> int: @@ -107,7 +108,7 @@ async def proxy_de_init(self) -> int: """ if self._json_rpc_session.is_activated: await self._json_rpc_session.logout() - if not self.time_initialized: + if self.last_updated == INIT_DATETIME: _LOGGER.debug( "proxy_de_init: Skipping de-init for %s (not initialized)", self.name ) @@ -121,7 +122,7 @@ async def proxy_de_init(self) -> int: ) return PROXY_DE_INIT_FAILED - self.time_initialized = 0 + self.last_updated = INIT_DATETIME return PROXY_DE_INIT_SUCCESS async def proxy_re_init(self) -> int: @@ -149,8 +150,8 @@ async def is_connected(self) -> bool: if not is_connected: return False - diff = int(time.time()) - self.time_initialized - if diff < config.INIT_TIMEOUT: + diff: timedelta = datetime.now() - self.last_updated + if diff.total_seconds() < config.INIT_TIMEOUT: return True return False @@ -403,13 +404,13 @@ async def _check_connection(self) -> bool: try: success = await self.proxy.ping(self.interface_id) if success: - self.time_initialized = int(time.time()) + self.last_updated = datetime.now() return True except NoConnection: _LOGGER.exception("ping: NoConnection") except ProxyException: _LOGGER.exception("ping: ProxyException") - self.time_initialized = 0 + self.last_updated = INIT_DATETIME return False async def set_system_variable(self, name: str, value: Any) -> None: @@ -561,7 +562,7 @@ async def _check_connection(self) -> bool: """Check if proxy is still initialized.""" try: if await self.proxy.clientServerInitialized(self.interface_id): - self.time_initialized = int(time.time()) + self.last_updated = datetime.now() return True except NoConnection: _LOGGER.exception("ping: NoConnection") @@ -570,7 +571,7 @@ async def _check_connection(self) -> bool: _LOGGER.warning( "homegear_check_init: Setting initialized to 0 for %s", self.interface_id ) - self.time_initialized = 0 + self.last_updated = INIT_DATETIME return False async def set_system_variable(self, name: str, value: Any) -> None: diff --git a/hahomematic/const.py b/hahomematic/const.py index 7752ad17..cceb9f35 100644 --- a/hahomematic/const.py +++ b/hahomematic/const.py @@ -116,7 +116,6 @@ "EMERGENCY_OPERATION", "EXTERNAL_CLOCK", "FROST_PROTECTION", - "HEATING_COOLING", "HUMIDITY_LIMITER", "INCLUSION_UNSUPPORTED_DEVICE", "INHIBIT", diff --git a/hahomematic/decorators.py b/hahomematic/decorators.py index ad8df041..2050a566 100644 --- a/hahomematic/decorators.py +++ b/hahomematic/decorators.py @@ -4,9 +4,9 @@ from __future__ import annotations from collections.abc import Callable +from datetime import datetime import functools import logging -import time from typing import Any from hahomematic.data import get_client_by_interface_id @@ -35,7 +35,7 @@ def wrapper_callback_system_event(*args: Any) -> Any: _LOGGER.warning("Failed to reduce args for callback_system_event.") raise Exception("args-exception callback_system_event") from err if client: - client.time_initialized = int(time.time()) + client.last_updated = datetime.now() if client.central.callback_system_event is not None: client.central.callback_system_event(name, *args) return return_value @@ -63,7 +63,7 @@ def wrapper_callback_event(*args: Any) -> Any: _LOGGER.warning("Failed to reduce args for callback_event.") raise Exception("args-exception callback_event") from err if client: - client.time_initialized = int(time.time()) + client.last_updated = datetime.now() if client.central.callback_entity_event is not None: client.central.callback_entity_event(*args) return return_value diff --git a/hahomematic/devices/climate.py b/hahomematic/devices/climate.py index 5e834b7d..aaba4b35 100644 --- a/hahomematic/devices/climate.py +++ b/hahomematic/devices/climate.py @@ -7,10 +7,12 @@ from hahomematic.const import ATTR_HM_MAX, ATTR_HM_MIN, HmPlatform import hahomematic.device as hm_device from hahomematic.devices.entity_definition import ( + FIELD_ACTIVE_PROFILE, FIELD_AUTO_MODE, FIELD_BOOST_MODE, FIELD_COMFORT_MODE, FIELD_CONTROL_MODE, + FIELD_HEATING_COOLING, FIELD_HUMIDITY, FIELD_LOWERING_MODE, FIELD_MANU_MODE, @@ -47,6 +49,9 @@ SUPPORT_TARGET_TEMPERATURE = 1 SUPPORT_PRESET_MODE = 16 +HEATING_PROFILES = {"Profile 1": 1, "Profile 2": 2, "Profile 3": 3} +COOLING_PROFILES = {"Profile 4": 4, "Profile 5": 5, "Profile 6": 6} + class BaseClimateEntity(CustomEntity): """Base HomeMatic climate entity.""" @@ -83,16 +88,16 @@ def _humidity(self) -> int | None: """Return the humidity of the device.""" return self._get_entity_state(FIELD_HUMIDITY) - @property - def _temperature(self) -> float | None: - """Return the temperature of the device.""" - return self._get_entity_state(FIELD_TEMPERATURE) - @property def _setpoint(self) -> float | None: """Return the setpoint of the device.""" return self._get_entity_state(FIELD_SETPOINT) + @property + def _temperature(self) -> float | None: + """Return the temperature of the device.""" + return self._get_entity_state(FIELD_TEMPERATURE) + @property def temperature_unit(self) -> str: """Return temperature unit.""" @@ -267,21 +272,31 @@ class IPThermostat(BaseClimateEntity): """homematic IP thermostat like HmIP-eTRV-B.""" @property - def _set_point_mode(self) -> int | None: - return self._get_entity_state(FIELD_SET_POINT_MODE) + def _active_profile(self) -> int | None: + return self._get_entity_state(FIELD_ACTIVE_PROFILE) + + @property + def _boost_mode(self) -> bool | None: + return self._get_entity_state(FIELD_BOOST_MODE) @property def _control_mode(self) -> int | None: return self._get_entity_state(FIELD_CONTROL_MODE) @property - def _boost_mode(self) -> bool | None: - return self._get_entity_state(FIELD_BOOST_MODE) + def _is_heating(self) -> bool | None: + if heating_cooling := self._get_entity_state(FIELD_HEATING_COOLING): + return str(heating_cooling) == "HEATING" + return True @property def _party_mode(self) -> bool | None: return self._get_entity_state(FIELD_PARTY_MODE) + @property + def _set_point_mode(self) -> int | None: + return self._get_entity_state(FIELD_SET_POINT_MODE) + @property def supported_features(self) -> int: """Return the list of supported features.""" @@ -312,12 +327,15 @@ def preset_mode(self) -> str: # we can't set it from the Home Assistant UI natively. # if self.set_point_mode == HMIP_SET_POINT_MODE_AWAY: # return PRESET_AWAY - return PRESET_NONE + return self._current_profile_name if self._current_profile_name else PRESET_NONE @property def preset_modes(self) -> list[str]: """Return available preset modes.""" - return [PRESET_BOOST, PRESET_NONE] + presets = [PRESET_BOOST, PRESET_NONE] + presets.extend(self._profile_names) + + return presets async def set_hvac_mode(self, hvac_mode: str) -> None: """Set new target hvac mode.""" @@ -339,6 +357,35 @@ async def set_preset_mode(self, preset_mode: str) -> None: if preset_mode == PRESET_NONE: await self._send_value(FIELD_BOOST_MODE, False) + if preset_mode in self._profile_names: + if self.hvac_mode != HVAC_MODE_AUTO: + await self.set_hvac_mode(HVAC_MODE_AUTO) + profile_idx = self._get_profile_idx_by_name(preset_mode) + await self._send_value(FIELD_BOOST_MODE, False) + await self._send_value(FIELD_ACTIVE_PROFILE, profile_idx) + + @property + def _profile_names(self) -> list[str]: + """Return a collection of profile names.""" + return list(self._relevant_profiles.keys()) + + @property + def _current_profile_name(self) -> str | None: + """Return a profile index by name.""" + inv_profiles: dict[int, str] = { + v: k for k, v in self._relevant_profiles.items() + } + return inv_profiles[self._active_profile] if self._active_profile else None + + def _get_profile_idx_by_name(self, profile_name: str) -> int: + """Return a profile index by name.""" + return self._relevant_profiles[profile_name] + + @property + def _relevant_profiles(self) -> dict[str, int]: + """Return the relevant profile groups.""" + return HEATING_PROFILES if self._is_heating else COOLING_PROFILES + def make_simple_thermostat( device: hm_device.HmDevice, address: str, group_base_channels: list[int] diff --git a/hahomematic/devices/entity_definition.py b/hahomematic/devices/entity_definition.py index 781c1198..3ec46eca 100644 --- a/hahomematic/devices/entity_definition.py +++ b/hahomematic/devices/entity_definition.py @@ -42,6 +42,7 @@ FIELD_DUTYCYCLE = "dutycycle" FIELD_ENERGY_COUNTER = "energy_counter" FIELD_FREQUENCY = "frequency" +FIELD_HEATING_COOLING = "heating_cooling" FIELD_HUMIDITY = "humidity" FIELD_LEVEL = "level" FIELD_LEVEL_2 = "level_2" @@ -129,7 +130,6 @@ def __str__(self) -> str: entity_definition: dict[str, dict[int | EntityDefinition, Any]] = { ED_DEFAULT_ENTITIES: { 0: { - FIELD_TEMPERATURE: "ACTUAL_TEMPERATURE", FIELD_DUTY_CYCLE: "DUTY_CYCLE", FIELD_DUTYCYCLE: "DUTYCYCLE", FIELD_LOW_BAT: "LOW_BAT", @@ -138,6 +138,7 @@ def __str__(self) -> str: FIELD_RSSI_DEVICE: "RSSI_DEVICE", FIELD_RSSI_PEER: "RSSI_PEER", FIELD_SABOTAGE: "SABOTAGE", + FIELD_TEMPERATURE: "ACTUAL_TEMPERATURE", } }, ED_DEVICES: { @@ -177,8 +178,8 @@ def __str__(self) -> str: ED_PHY_CHANNEL: [1], ED_VIRT_CHANNEL: [], ED_FIELDS_REP: { - FIELD_DOOR_STATE: "DOOR_STATE", FIELD_DOOR_COMMAND: "DOOR_COMMAND,", + FIELD_DOOR_STATE: "DOOR_STATE", }, ED_FIELDS: {}, }, @@ -214,11 +215,11 @@ def __str__(self) -> str: }, ED_ADDITIONAL_ENTITIES: { 4: { - FIELD_TEMPERATURE: "ACTUAL_TEMPERATURE", FIELD_CURRENT: "CURRENT", FIELD_ENERGY_COUNTER: "ENERGY_COUNTER", FIELD_FREQUENCY: "FREQUENCY", FIELD_POWER: "POWER", + FIELD_TEMPERATURE: "ACTUAL_TEMPERATURE", FIELD_VOLTAGE: "VOLTAGE", }, }, @@ -241,22 +242,23 @@ def __str__(self) -> str: ED_PHY_CHANNEL: [1], ED_VIRT_CHANNEL: [], ED_FIELDS_REP: { + FIELD_ACTIVE_PROFILE: "ACTIVE_PROFILE", + FIELD_AUTO_MODE: "AUTO_MODE", + FIELD_BOOST_MODE: "BOOST_MODE", + FIELD_CONTROL_MODE: "CONTROL_MODE", + FIELD_HEATING_COOLING: "HEATING_COOLING", FIELD_HUMIDITY: "HUMIDITY", - FIELD_TEMPERATURE: "ACTUAL_TEMPERATURE", + FIELD_PARTY_MODE: "PARTY_MODE", FIELD_SETPOINT: "SET_POINT_TEMPERATURE", FIELD_SET_POINT_MODE: "SET_POINT_MODE", - FIELD_CONTROL_MODE: "CONTROL_MODE", - FIELD_BOOST_MODE: "BOOST_MODE", - FIELD_PARTY_MODE: "PARTY_MODE", - FIELD_AUTO_MODE: "AUTO_MODE", + FIELD_TEMPERATURE: "ACTUAL_TEMPERATURE", }, }, ED_ADDITIONAL_ENTITIES: { 1: { - FIELD_ACTIVE_PROFILE: "ACTIVE_PROFILE", FIELD_HUMIDITY: "HUMIDITY", FIELD_LEVEL: "LEVEL", - FIELD_TEMPERATURE: "ACTUAL_TEMPERATURE" + FIELD_TEMPERATURE: "ACTUAL_TEMPERATURE", }, 9: { FIELD_STATE: "STATE", @@ -268,14 +270,16 @@ def __str__(self) -> str: ED_PHY_CHANNEL: [1], ED_VIRT_CHANNEL: [], ED_FIELDS_REP: { + FIELD_ACTIVE_PROFILE: "ACTIVE_PROFILE", + FIELD_AUTO_MODE: "AUTO_MODE", + FIELD_BOOST_MODE: "BOOST_MODE", + FIELD_CONTROL_MODE: "CONTROL_MODE", + FIELD_HEATING_COOLING: "HEATING_COOLING", FIELD_HUMIDITY: "HUMIDITY", - FIELD_TEMPERATURE: "ACTUAL_TEMPERATURE", + FIELD_PARTY_MODE: "PARTY_MODE", FIELD_SETPOINT: "SET_POINT_TEMPERATURE", FIELD_SET_POINT_MODE: "SET_POINT_MODE", - FIELD_CONTROL_MODE: "CONTROL_MODE", - FIELD_BOOST_MODE: "BOOST_MODE", - FIELD_PARTY_MODE: "PARTY_MODE", - FIELD_AUTO_MODE: "AUTO_MODE", + FIELD_TEMPERATURE: "ACTUAL_TEMPERATURE", }, }, ED_INCLUDE_DEFAULT_ENTITIES: False, @@ -308,8 +312,8 @@ def __str__(self) -> str: ED_FIELDS_REP: {}, ED_FIELDS: { 1: { - FIELD_STATE: "STATE", FIELD_OPEN: "OPEN", + FIELD_STATE: "STATE", } }, }, @@ -319,32 +323,38 @@ def __str__(self) -> str: ED_PHY_CHANNEL: [1, 2, 3, 4], ED_VIRT_CHANNEL: [], ED_FIELDS_REP: { - FIELD_HUMIDITY: "ACTUAL_HUMIDITY", - FIELD_TEMPERATURE: "ACTUAL_TEMPERATURE", - FIELD_SETPOINT: "SET_TEMPERATURE", - FIELD_CONTROL_MODE: "CONTROL_MODE", - FIELD_BOOST_MODE: "BOOST_MODE", FIELD_AUTO_MODE: "AUTO_MODE", - FIELD_MANU_MODE: "MANU_MODE", + FIELD_BOOST_MODE: "BOOST_MODE", FIELD_COMFORT_MODE: "COMFORT_MODE", + FIELD_CONTROL_MODE: "CONTROL_MODE", + FIELD_HUMIDITY: "ACTUAL_HUMIDITY", FIELD_LOWERING_MODE: "LOWERING_MODE", + FIELD_MANU_MODE: "MANU_MODE", + FIELD_TEMPERATURE: "ACTUAL_TEMPERATURE", + FIELD_SETPOINT: "SET_TEMPERATURE", }, }, + ED_ADDITIONAL_ENTITIES: { + 1: { + FIELD_HUMIDITY: "ACTUAL_HUMIDITY", + FIELD_TEMPERATURE: "ACTUAL_TEMPERATURE", + } + }, }, EntityDefinition.RF_THERMOSTAT_GROUP: { ED_DEVICE_GROUP: { ED_PHY_CHANNEL: [1, 2, 3, 4], ED_VIRT_CHANNEL: [], ED_FIELDS_REP: { - FIELD_HUMIDITY: "ACTUAL_HUMIDITY", - FIELD_TEMPERATURE: "ACTUAL_TEMPERATURE", - FIELD_SETPOINT: "SET_TEMPERATURE", - FIELD_CONTROL_MODE: "CONTROL_MODE", - FIELD_BOOST_MODE: "BOOST_MODE", FIELD_AUTO_MODE: "AUTO_MODE", - FIELD_MANU_MODE: "MANU_MODE", + FIELD_BOOST_MODE: "BOOST_MODE", FIELD_COMFORT_MODE: "COMFORT_MODE", + FIELD_CONTROL_MODE: "CONTROL_MODE", + FIELD_HUMIDITY: "ACTUAL_HUMIDITY", FIELD_LOWERING_MODE: "LOWERING_MODE", + FIELD_MANU_MODE: "MANU_MODE", + FIELD_TEMPERATURE: "ACTUAL_TEMPERATURE", + FIELD_SETPOINT: "SET_TEMPERATURE", }, }, ED_INCLUDE_DEFAULT_ENTITIES: False, @@ -364,6 +374,12 @@ def __str__(self) -> str: }, }, }, + ED_ADDITIONAL_ENTITIES: { + 1: { + FIELD_HUMIDITY: "HUMIDITY", + FIELD_TEMPERATURE: "TEMPERATURE" + } + }, }, }, } diff --git a/hahomematic/devices/light.py b/hahomematic/devices/light.py index 3fcaa26d..debbca3c 100644 --- a/hahomematic/devices/light.py +++ b/hahomematic/devices/light.py @@ -10,10 +10,8 @@ from hahomematic.devices.entity_definition import ( FIELD_CHANNEL_COLOR, FIELD_CHANNEL_LEVEL, - FIELD_CHANNEL_STATE, FIELD_COLOR, FIELD_LEVEL, - FIELD_STATE, EntityDefinition, make_custom_entity, ) diff --git a/setup.py b/setup.py index 151d4c6a..383d8ba1 100644 --- a/setup.py +++ b/setup.py @@ -15,7 +15,7 @@ def readme(): }, PACKAGE_NAME = "hahomematic" HERE = os.path.abspath(os.path.dirname(__file__)) -VERSION = "0.3.1" +VERSION = "0.4.0" PACKAGES = find_packages(exclude=["tests", "tests.*", "dist", "build"]) diff --git a/tests/test_central.py b/tests/test_central.py index 1a89957e..6367e359 100644 --- a/tests/test_central.py +++ b/tests/test_central.py @@ -18,7 +18,7 @@ async def test_central(central, loop) -> None: assert central.clients["ccu-dev-hm"].model == "PyDevCCU" assert central.get_primary_client().model == "PyDevCCU" assert len(central.hm_devices) == 294 - assert len(central.hm_entities) == 2650 + assert len(central.hm_entities) == 2655 @pytest.mark.asyncio