From 7a2dd392822e079b78bf315a58f2dfc9ce113d85 Mon Sep 17 00:00:00 2001 From: SukramJ Date: Sun, 13 Oct 2024 13:44:04 +0200 Subject: [PATCH] Add OPTIMUM_START_STOP and TEMPERATURE_OFFSET to climate (#1770) * OPTIMUM_START_STOP and TEMPERATURE_OFFSET to climate master parameters * Add OPTIMUM_START_STOP and TEMPERATURE_OFFSET to climate --- changelog.md | 1 + hahomematic/caches/visibility.py | 63 +++++--------- hahomematic/platforms/custom/climate.py | 96 ++++++++++++++-------- hahomematic/platforms/custom/const.py | 2 + hahomematic/platforms/custom/definition.py | 6 ++ tests/test_central.py | 34 ++++---- tests/test_central_pydevccu.py | 6 +- tests/test_entity.py | 2 +- 8 files changed, 109 insertions(+), 101 deletions(-) diff --git a/changelog.md b/changelog.md index ffdb2c51..f108b92d 100644 --- a/changelog.md +++ b/changelog.md @@ -1,5 +1,6 @@ # Version 2024.10.7 (2024-10-12) +- Add OPTIMUM_START_STOP and TEMPERATURE_OFFSET to climate - Improve profile validation - Use regex to identify schedule profiles diff --git a/hahomematic/caches/visibility.py b/hahomematic/caches/visibility.py index 64042ab6..5a0aa3f9 100644 --- a/hahomematic/caches/visibility.py +++ b/hahomematic/caches/visibility.py @@ -29,35 +29,22 @@ # from being display by default. Usually these enties are used within custom entities, # and not for general display. # {model: (channel_no, parameter)} + +_CLIMATE_MASTER_PARAMETERS: Final[tuple[Parameter, ...]] = ( + Parameter.GLOBAL_BUTTON_LOCK, + Parameter.OPTIMUM_START_STOP, + Parameter.TEMPERATURE_MAXIMUM, + Parameter.TEMPERATURE_MINIMUM, + Parameter.TEMPERATURE_OFFSET, +) _RELEVANT_MASTER_PARAMSETS_BY_DEVICE: Final[ Mapping[str, tuple[tuple[int | None, ...], tuple[Parameter, ...]]] ] = { - "ALPHA-IP-RBG": ( - (0, 1), - ( - Parameter.TEMPERATURE_MAXIMUM, - Parameter.TEMPERATURE_MINIMUM, - Parameter.GLOBAL_BUTTON_LOCK, - ), - ), - "HM-CC-RT-DN": ( - (None, 1), - ( - Parameter.TEMPERATURE_MAXIMUM, - Parameter.TEMPERATURE_MINIMUM, - Parameter.GLOBAL_BUTTON_LOCK, - ), - ), - "HM-CC-VG-1": ((1,), (Parameter.TEMPERATURE_MAXIMUM, Parameter.TEMPERATURE_MINIMUM)), + "ALPHA-IP-RBG": ((0, 1), _CLIMATE_MASTER_PARAMETERS), + "HM-CC-RT-DN": ((None, 1), _CLIMATE_MASTER_PARAMETERS), + "HM-CC-VG-1": ((0, 1), _CLIMATE_MASTER_PARAMETERS), "HM-TC-IT-WM-W-EU": ((None,), (Parameter.GLOBAL_BUTTON_LOCK,)), - "HmIP-BWTH": ( - (0, 1), - ( - Parameter.TEMPERATURE_MAXIMUM, - Parameter.TEMPERATURE_MINIMUM, - Parameter.GLOBAL_BUTTON_LOCK, - ), - ), + "HmIP-BWTH": ((0, 1), _CLIMATE_MASTER_PARAMETERS), "HmIP-DRBLI4": ( (1, 2, 3, 4, 5, 6, 7, 8, 9, 13, 17, 21), (Parameter.CHANNEL_OPERATION_MODE,), @@ -70,33 +57,19 @@ "HmIP-FCI1": ((1,), (Parameter.CHANNEL_OPERATION_MODE,)), "HmIP-FCI6": (tuple(range(1, 7)), (Parameter.CHANNEL_OPERATION_MODE,)), "HmIP-FSI16": ((1,), (Parameter.CHANNEL_OPERATION_MODE,)), - "HmIP-HEATING": ((1,), (Parameter.TEMPERATURE_MAXIMUM, Parameter.TEMPERATURE_MINIMUM)), + "HmIP-HEATING": ((0, 1), _CLIMATE_MASTER_PARAMETERS), "HmIP-MIO16-PCB": ((13, 14, 15, 16), (Parameter.CHANNEL_OPERATION_MODE,)), "HmIP-MOD-RC8": (tuple(range(1, 9)), (Parameter.CHANNEL_OPERATION_MODE,)), "HmIP-RGBW": ((0,), (Parameter.DEVICE_OPERATION_MODE,)), - "HmIP-STH": ((1,), (Parameter.TEMPERATURE_MAXIMUM, Parameter.TEMPERATURE_MINIMUM)), - "HmIP-WTH": ( - (0, 1), - ( - Parameter.TEMPERATURE_MAXIMUM, - Parameter.TEMPERATURE_MINIMUM, - Parameter.GLOBAL_BUTTON_LOCK, - ), - ), - "HmIP-eTRV": ( - (0, 1), - ( - Parameter.TEMPERATURE_MAXIMUM, - Parameter.TEMPERATURE_MINIMUM, - Parameter.GLOBAL_BUTTON_LOCK, - ), - ), + "HmIP-STH": ((1,), _CLIMATE_MASTER_PARAMETERS), + "HmIP-WTH": ((0, 1), _CLIMATE_MASTER_PARAMETERS), + "HmIP-eTRV": ((0, 1), _CLIMATE_MASTER_PARAMETERS), "HmIPW-DRBL4": ((1, 5, 9, 13), (Parameter.CHANNEL_OPERATION_MODE,)), "HmIPW-DRI16": (tuple(range(1, 17)), (Parameter.CHANNEL_OPERATION_MODE,)), "HmIPW-DRI32": (tuple(range(1, 33)), (Parameter.CHANNEL_OPERATION_MODE,)), "HmIPW-FAL": ((0,), (Parameter.GLOBAL_BUTTON_LOCK,)), "HmIPW-FIO6": (tuple(range(1, 7)), (Parameter.CHANNEL_OPERATION_MODE,)), - "HmIPW-STH": ((1,), (Parameter.TEMPERATURE_MAXIMUM, Parameter.TEMPERATURE_MINIMUM)), + "HmIPW-STH": ((0, 1), _CLIMATE_MASTER_PARAMETERS), } # Ignore events for some devices @@ -111,10 +84,12 @@ Parameter.CONFIG_PENDING, Parameter.DIRECTION, Parameter.ERROR, + Parameter.OPTIMUM_START_STOP, Parameter.SECTION, Parameter.STICKY_UN_REACH, Parameter.TEMPERATURE_MAXIMUM, Parameter.TEMPERATURE_MINIMUM, + Parameter.TEMPERATURE_OFFSET, Parameter.UN_REACH, Parameter.UPDATE_PENDING, Parameter.WORKING, diff --git a/hahomematic/platforms/custom/climate.py b/hahomematic/platforms/custom/climate.py index 7542f354..c17029ab 100644 --- a/hahomematic/platforms/custom/climate.py +++ b/hahomematic/platforms/custom/climate.py @@ -177,10 +177,30 @@ def _init_entity_fields(self) -> None: field=Field.TEMPERATURE_MINIMUM, entity_type=HmFloat ) - @config_property - def temperature_unit(self) -> str: - """Return temperature unit.""" - return _TEMP_CELSIUS + @state_property + def current_humidity(self) -> int | None: + """Return the current humidity.""" + return self._e_humidity.value + + @state_property + def current_temperature(self) -> float | None: + """Return current temperature.""" + return self._e_temperature.value + + @state_property + def hvac_action(self) -> HmHvacAction | None: + """Return the hvac action.""" + return None + + @state_property + def hvac_mode(self) -> HmHvacMode: + """Return hvac operation mode.""" + return HmHvacMode.HEAT + + @state_property + def hvac_modes(self) -> tuple[HmHvacMode, ...]: + """Return the available hvac operation modes.""" + return (HmHvacMode.HEAT,) @state_property def min_temp(self) -> float: @@ -202,14 +222,14 @@ def max_temp(self) -> float: return self._e_setpoint.max # type: ignore[no-any-return] @state_property - def current_humidity(self) -> int | None: - """Return the current humidity.""" - return self._e_humidity.value + def preset_mode(self) -> HmPresetMode: + """Return the current preset mode.""" + return HmPresetMode.NONE @state_property - def current_temperature(self) -> float | None: - """Return current temperature.""" - return self._e_temperature.value + def preset_modes(self) -> tuple[HmPresetMode, ...]: + """Return available preset modes.""" + return (HmPresetMode.NONE,) @state_property def target_temperature(self) -> float | None: @@ -221,36 +241,16 @@ def target_temperature_step(self) -> float: """Return the supported step of target temperature.""" return _DEFAULT_TEMPERATURE_STEP - @state_property - def preset_mode(self) -> HmPresetMode: - """Return the current preset mode.""" - return HmPresetMode.NONE - - @state_property - def preset_modes(self) -> tuple[HmPresetMode, ...]: - """Return available preset modes.""" - return (HmPresetMode.NONE,) - - @state_property - def hvac_action(self) -> HmHvacAction | None: - """Return the hvac action.""" - return None - - @state_property - def hvac_mode(self) -> HmHvacMode: - """Return hvac operation mode.""" - return HmHvacMode.HEAT - - @state_property - def hvac_modes(self) -> tuple[HmHvacMode, ...]: - """Return the available hvac operation modes.""" - return (HmHvacMode.HEAT,) - @property def supports_preset(self) -> bool: """Flag if climate supports preset.""" return False + @config_property + def temperature_unit(self) -> str: + """Return temperature unit.""" + return _TEMP_CELSIUS + @property def _min_or_target_temperature(self) -> float: """Return the min or target temperature.""" @@ -590,6 +590,9 @@ def _init_entity_fields(self) -> None: self._e_control_mode: HmSensor[str | None] = self._get_entity( field=Field.CONTROL_MODE, entity_type=HmSensor[str | None] ) + self._e_temperature_offset: HmSelect = self._get_entity( + field=Field.TEMPERATURE_OFFSET, entity_type=HmSelect + ) self._e_valve_state: HmSensor[int | None] = self._get_entity( field=Field.VALVE_STATE, entity_type=HmSensor[int | None] ) @@ -645,6 +648,11 @@ def supports_preset(self) -> bool: """Flag if climate supports preset.""" return True + @state_property + def temperature_offset(self) -> str | None: + """Return the maximum temperature.""" + return self._e_temperature_offset.value + @bind_collector() async def set_hvac_mode( self, hvac_mode: HmHvacMode, collector: CallParameterCollector | None = None @@ -745,16 +753,22 @@ def _init_entity_fields(self) -> None: self._e_heating_mode: HmSelect = self._get_entity( field=Field.HEATING_COOLING, entity_type=HmSelect ) + self._e_level: HmFloat = self._get_entity(field=Field.LEVEL, entity_type=HmFloat) + self._e_optimum_start_stop: HmBinarySensor = self._get_entity( + field=Field.OPTIMUM_START_STOP, entity_type=HmBinarySensor + ) self._e_party_mode: HmBinarySensor = self._get_entity( field=Field.PARTY_MODE, entity_type=HmBinarySensor ) self._e_set_point_mode: HmInteger = self._get_entity( field=Field.SET_POINT_MODE, entity_type=HmInteger ) - self._e_level: HmFloat = self._get_entity(field=Field.LEVEL, entity_type=HmFloat) self._e_state: HmBinarySensor = self._get_entity( field=Field.STATE, entity_type=HmBinarySensor ) + self._e_temperature_offset: HmFloat = self._get_entity( + field=Field.TEMPERATURE_OFFSET, entity_type=HmFloat + ) @property def _is_heating_mode(self) -> bool: @@ -815,11 +829,21 @@ def preset_modes(self) -> tuple[HmPresetMode, ...]: presets.extend(self._profile_names) return tuple(presets) + @property + def optimum_start_stop(self) -> bool | None: + """Return if optimum_start_stop is enabled.""" + return self._e_optimum_start_stop.value + @property def supports_preset(self) -> bool: """Flag if climate supports preset.""" return True + @state_property + def temperature_offset(self) -> float | None: + """Return the maximum temperature.""" + return self._e_temperature_offset.value + @bind_collector() async def set_hvac_mode( self, hvac_mode: HmHvacMode, collector: CallParameterCollector | None = None diff --git a/hahomematic/platforms/custom/const.py b/hahomematic/platforms/custom/const.py index ebefd8f3..25ee34cb 100644 --- a/hahomematic/platforms/custom/const.py +++ b/hahomematic/platforms/custom/const.py @@ -110,6 +110,7 @@ class Field(Enum): OPERATING_VOLTAGE = "operating_voltage" OPTICAL_ALARM_ACTIVE = "optical_alarm_active" OPTICAL_ALARM_SELECTION = "optical_alarm_selection" + OPTIMUM_START_STOP = "optimum_start_stop" PARTY_MODE = "party_mode" POWER = "power" PROGRAM = "program" @@ -134,5 +135,6 @@ class Field(Enum): TEMPERATURE = "temperature" TEMPERATURE_MAXIMUM = "temperature_maximum" TEMPERATURE_MINIMUM = "temperature_minimum" + TEMPERATURE_OFFSET = "temperature_offset" VALVE_STATE = "valve_state" VOLTAGE = "voltage" diff --git a/hahomematic/platforms/custom/definition.py b/hahomematic/platforms/custom/definition.py index 6768c79d..2fff7ff2 100644 --- a/hahomematic/platforms/custom/definition.py +++ b/hahomematic/platforms/custom/definition.py @@ -310,11 +310,13 @@ Field.ACTIVE_PROFILE: Parameter.ACTIVE_PROFILE, Field.BOOST_MODE: Parameter.BOOST_MODE, Field.CONTROL_MODE: Parameter.CONTROL_MODE, + Field.OPTIMUM_START_STOP: Parameter.OPTIMUM_START_STOP, Field.PARTY_MODE: Parameter.PARTY_MODE, Field.SETPOINT: Parameter.SET_POINT_TEMPERATURE, Field.SET_POINT_MODE: Parameter.SET_POINT_MODE, Field.TEMPERATURE_MAXIMUM: Parameter.TEMPERATURE_MAXIMUM, Field.TEMPERATURE_MINIMUM: Parameter.TEMPERATURE_MINIMUM, + Field.TEMPERATURE_OFFSET: Parameter.TEMPERATURE_OFFSET, }, ED.VISIBLE_REPEATABLE_FIELDS: { Field.HEATING_COOLING: Parameter.HEATING_COOLING, @@ -338,11 +340,13 @@ Field.ACTIVE_PROFILE: Parameter.ACTIVE_PROFILE, Field.BOOST_MODE: Parameter.BOOST_MODE, Field.CONTROL_MODE: Parameter.CONTROL_MODE, + Field.OPTIMUM_START_STOP: Parameter.OPTIMUM_START_STOP, Field.PARTY_MODE: Parameter.PARTY_MODE, Field.SETPOINT: Parameter.SET_POINT_TEMPERATURE, Field.SET_POINT_MODE: Parameter.SET_POINT_MODE, Field.TEMPERATURE_MAXIMUM: Parameter.TEMPERATURE_MAXIMUM, Field.TEMPERATURE_MINIMUM: Parameter.TEMPERATURE_MINIMUM, + Field.TEMPERATURE_OFFSET: Parameter.TEMPERATURE_OFFSET, }, ED.VISIBLE_REPEATABLE_FIELDS: { Field.HEATING_COOLING: Parameter.HEATING_COOLING, @@ -478,6 +482,7 @@ Field.SETPOINT: Parameter.SET_TEMPERATURE, Field.TEMPERATURE_MAXIMUM: Parameter.TEMPERATURE_MAXIMUM, Field.TEMPERATURE_MINIMUM: Parameter.TEMPERATURE_MINIMUM, + Field.TEMPERATURE_OFFSET: Parameter.TEMPERATURE_OFFSET, }, ED.VISIBLE_REPEATABLE_FIELDS: { Field.HUMIDITY: Parameter.ACTUAL_HUMIDITY, @@ -502,6 +507,7 @@ Field.SETPOINT: Parameter.SET_TEMPERATURE, Field.TEMPERATURE_MAXIMUM: Parameter.TEMPERATURE_MAXIMUM, Field.TEMPERATURE_MINIMUM: Parameter.TEMPERATURE_MINIMUM, + Field.TEMPERATURE_OFFSET: Parameter.TEMPERATURE_OFFSET, }, ED.VISIBLE_REPEATABLE_FIELDS: { Field.HUMIDITY: Parameter.ACTUAL_HUMIDITY, diff --git a/tests/test_central.py b/tests/test_central.py index 4304d6ed..cac97f2a 100644 --- a/tests/test_central.py +++ b/tests/test_central.py @@ -574,7 +574,7 @@ async def test_add_device( """Test add_device.""" central, _, _ = central_client_factory assert len(central._devices) == 1 - assert len(central.get_entities(exclude_no_create=False)) == 24 + assert len(central.get_entities(exclude_no_create=False)) == 26 assert len(central.device_descriptions._raw_device_descriptions.get(const.INTERFACE_ID)) == 9 assert ( len(central.paramset_descriptions._raw_paramset_descriptions.get(const.INTERFACE_ID)) == 9 @@ -582,7 +582,7 @@ async def test_add_device( dev_desc = helper.load_device_description(central=central, filename="HmIP-BSM.json") await central.add_new_devices(interface_id=const.INTERFACE_ID, device_descriptions=dev_desc) assert len(central._devices) == 2 - assert len(central.get_entities(exclude_no_create=False)) == 55 + assert len(central.get_entities(exclude_no_create=False)) == 57 assert len(central.device_descriptions._raw_device_descriptions.get(const.INTERFACE_ID)) == 20 assert ( len(central.paramset_descriptions._raw_paramset_descriptions.get(const.INTERFACE_ID)) == 20 @@ -611,7 +611,7 @@ async def test_delete_device( """Test device delete_device.""" central, _, _ = central_client_factory assert len(central._devices) == 2 - assert len(central.get_entities(exclude_no_create=False)) == 55 + assert len(central.get_entities(exclude_no_create=False)) == 57 assert len(central.device_descriptions._raw_device_descriptions.get(const.INTERFACE_ID)) == 20 assert ( len(central.paramset_descriptions._raw_paramset_descriptions.get(const.INTERFACE_ID)) == 20 @@ -619,7 +619,7 @@ async def test_delete_device( await central.delete_devices(interface_id=const.INTERFACE_ID, addresses=["VCU2128127"]) assert len(central._devices) == 1 - assert len(central.get_entities(exclude_no_create=False)) == 24 + assert len(central.get_entities(exclude_no_create=False)) == 26 assert len(central.device_descriptions._raw_device_descriptions.get(const.INTERFACE_ID)) == 9 assert ( len(central.paramset_descriptions._raw_paramset_descriptions.get(const.INTERFACE_ID)) == 9 @@ -760,29 +760,29 @@ async def test_central_services( include_internal=DEFAULT_INCLUDE_INTERNAL_SYSVARS ) - assert len(mock_client.method_calls) == 39 + assert len(mock_client.method_calls) == 41 await central.load_and_refresh_entity_data(paramset_key=ParamsetKey.MASTER) - assert len(mock_client.method_calls) == 39 + assert len(mock_client.method_calls) == 41 await central.load_and_refresh_entity_data(paramset_key=ParamsetKey.VALUES) - assert len(mock_client.method_calls) == 57 + assert len(mock_client.method_calls) == 59 await central.get_system_variable(name="SysVar_Name") assert mock_client.method_calls[-1] == call.get_system_variable("SysVar_Name") - assert len(mock_client.method_calls) == 58 + assert len(mock_client.method_calls) == 60 await central.set_system_variable(name="sv_alarm", value=True) assert mock_client.method_calls[-1] == call.set_system_variable(name="sv_alarm", value=True) - assert len(mock_client.method_calls) == 59 + assert len(mock_client.method_calls) == 61 await central.set_system_variable(name="SysVar_Name", value=True) - assert len(mock_client.method_calls) == 59 + assert len(mock_client.method_calls) == 61 await central.set_install_mode(interface_id=const.INTERFACE_ID) assert mock_client.method_calls[-1] == call.set_install_mode( on=True, t=60, mode=1, device_address=None ) - assert len(mock_client.method_calls) == 60 + assert len(mock_client.method_calls) == 62 await central.set_install_mode(interface_id="NOT_A_VALID_INTERFACE_ID") - assert len(mock_client.method_calls) == 60 + assert len(mock_client.method_calls) == 62 await central.get_client(interface_id=const.INTERFACE_ID).set_value( channel_address="123", @@ -796,7 +796,7 @@ async def test_central_services( parameter="LEVEL", value=1.0, ) - assert len(mock_client.method_calls) == 61 + assert len(mock_client.method_calls) == 63 with pytest.raises(HaHomematicException): await central.get_client(interface_id="NOT_A_VALID_INTERFACE_ID").set_value( @@ -805,7 +805,7 @@ async def test_central_services( parameter="LEVEL", value=1.0, ) - assert len(mock_client.method_calls) == 61 + assert len(mock_client.method_calls) == 63 await central.get_client(interface_id=const.INTERFACE_ID).put_paramset( channel_address="123", @@ -815,14 +815,14 @@ async def test_central_services( assert mock_client.method_calls[-1] == call.put_paramset( channel_address="123", paramset_key="VALUES", values={"LEVEL": 1.0} ) - assert len(mock_client.method_calls) == 62 + assert len(mock_client.method_calls) == 64 with pytest.raises(HaHomematicException): await central.get_client(interface_id="NOT_A_VALID_INTERFACE_ID").put_paramset( channel_address="123", paramset_key=ParamsetKey.VALUES, values={"LEVEL": 1.0}, ) - assert len(mock_client.method_calls) == 62 + assert len(mock_client.method_calls) == 64 assert ( central.get_generic_entity( @@ -851,7 +851,7 @@ async def test_central_direct(factory: helper.Factory) -> None: assert central.available is False assert central.system_information.serial == "0815_4711" assert len(central._devices) == 2 - assert len(central.get_entities(exclude_no_create=False)) == 55 + assert len(central.get_entities(exclude_no_create=False)) == 57 finally: await central.stop() diff --git a/tests/test_central_pydevccu.py b/tests/test_central_pydevccu.py index 0b704890..d445fe67 100644 --- a/tests/test_central_pydevccu.py +++ b/tests/test_central_pydevccu.py @@ -29,7 +29,7 @@ async def test_central_mini(central_unit_mini) -> None: assert central_unit_mini.get_client(const.INTERFACE_ID).model == "PyDevCCU" assert central_unit_mini.primary_client.model == "PyDevCCU" assert len(central_unit_mini._devices) == 1 - assert len(central_unit_mini.get_entities(exclude_no_create=False)) == 31 + assert len(central_unit_mini.get_entities(exclude_no_create=False)) == 33 @pytest.mark.asyncio() @@ -115,7 +115,7 @@ async def test_central_full(central_unit_full) -> None: ) as fptr: fptr.write(orjson.dumps(addresses, option=orjson.OPT_INDENT_2 | orjson.OPT_NON_STR_KEYS)) - assert usage_types[EntityUsage.NO_CREATE] == 3117 + assert usage_types[EntityUsage.NO_CREATE] == 3154 assert usage_types[EntityUsage.CE_PRIMARY] == 208 assert usage_types[EntityUsage.ENTITY] == 3638 assert usage_types[EntityUsage.CE_VISIBLE] == 125 @@ -123,7 +123,7 @@ async def test_central_full(central_unit_full) -> None: assert len(ce_channels) == 121 assert len(entity_types) == 6 - assert len(parameters) == 216 + assert len(parameters) == 218 assert len(central_unit_full._devices) == 383 virtual_remotes = ["VCU4264293", "VCU0000057", "VCU0000001"] diff --git a/tests/test_entity.py b/tests/test_entity.py index bb58871b..8b00be08 100644 --- a/tests/test_entity.py +++ b/tests/test_entity.py @@ -228,5 +228,5 @@ async def test_generic_wrapped_entity( def test_custom_required_entities() -> None: """Test required parameters from entity definitions.""" required_parameters = get_required_parameters() - assert len(required_parameters) == 76 + assert len(required_parameters) == 78 assert check_ignore_parameters_is_clean() is True