Skip to content

Commit

Permalink
Add climate presets based on WEEK_PROGRAM_POINTER (#2022)
Browse files Browse the repository at this point in the history
  • Loading branch information
SukramJ authored Jan 28, 2025
1 parent 2754b82 commit 5cfa439
Show file tree
Hide file tree
Showing 3 changed files with 229 additions and 6 deletions.
1 change: 1 addition & 0 deletions changelog.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
# Version 2025.1.18 (2025-01-28)

- Add climate presets based on WEEK_PROGRAM_POINTER
- Add WEEK_PROGRAM_POINTER for bidcos climate devices
- Define schedule_channel_address for HM schedule usage
- Fix usage of master dps for bidcos climate devices
Expand Down
59 changes: 53 additions & 6 deletions hahomematic/model/custom/climate.py
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,17 @@ class ClimateProfile(StrEnum):
WEEK_PROGRAM_6 = "week_program_6"


_HM_WEEK_PROFILE_POINTERS_TO_NAMES: Final = {
0: "WEEK PROGRAM 1",
1: "WEEK PROGRAM 2",
2: "WEEK PROGRAM 3",
3: "WEEK PROGRAM 4",
4: "WEEK PROGRAM 5",
5: "WEEK PROGRAM 6",
}
_HM_WEEK_PROFILE_POINTERS_TO_IDX: Final = {v: k for k, v in _HM_WEEK_PROFILE_POINTERS_TO_NAMES.items()}


class ScheduleSlotType(StrEnum):
"""Enum for climate item type."""

Expand Down Expand Up @@ -707,17 +718,17 @@ def profile(self) -> ClimateProfile:
return ClimateProfile.BOOST
if self._dp_control_mode.value == _ModeHm.AWAY:
return ClimateProfile.AWAY
if self.mode == ClimateMode.AUTO:
return self._current_profile_name if self._current_profile_name else ClimateProfile.NONE
return ClimateProfile.NONE

@state_property
def profiles(self) -> tuple[ClimateProfile, ...]:
"""Return available profile."""
return (
ClimateProfile.BOOST,
ClimateProfile.COMFORT,
ClimateProfile.ECO,
ClimateProfile.NONE,
)
control_modes = [ClimateProfile.BOOST, ClimateProfile.COMFORT, ClimateProfile.ECO, ClimateProfile.NONE]
if self.mode == ClimateMode.AUTO:
control_modes.extend(self._profile_names)
return tuple(control_modes)

@property
def supports_profiles(self) -> bool:
Expand Down Expand Up @@ -755,6 +766,14 @@ async def set_profile(self, profile: ClimateProfile, collector: CallParameterCol
await self._dp_comfort_mode.send_value(value=True, collector=collector)
elif profile == ClimateProfile.ECO:
await self._dp_lowering_mode.send_value(value=True, collector=collector)
elif profile in self._profile_names:
if self.mode != ClimateMode.AUTO:
await self.set_mode(mode=ClimateMode.AUTO, collector=collector)
await self._dp_boost_mode.send_value(value=False, collector=collector)
if profile_idx := self._profiles.get(profile):
await self._dp_week_program_pointer.send_value(
value=_HM_WEEK_PROFILE_POINTERS_TO_NAMES[profile_idx], collector=collector
)

@inspector()
async def enable_away_mode_by_calendar(self, start: datetime, end: datetime, away_temperature: float) -> None:
Expand Down Expand Up @@ -786,6 +805,34 @@ async def disable_away_mode(self) -> None:
value=_party_mode_code(start=start, end=end, away_temperature=12.0),
)

@property
def _profile_names(self) -> tuple[ClimateProfile, ...]:
"""Return a collection of profile names."""
return tuple(self._profiles.keys())

@property
def _current_profile_name(self) -> ClimateProfile | None:
"""Return a profile index by name."""
inv_profiles = {v: k for k, v in self._profiles.items()}
if self._dp_week_program_pointer.value is not None:
idx = (
int(self._dp_week_program_pointer.value)
if self._dp_week_program_pointer.value.isnumeric()
else _HM_WEEK_PROFILE_POINTERS_TO_IDX[self._dp_week_program_pointer.value]
)
return inv_profiles.get(idx)
return None

@property
def _profiles(self) -> Mapping[ClimateProfile, int]:
"""Return the profile groups."""
profiles: dict[ClimateProfile, int] = {}
if self._dp_week_program_pointer.min is not None and self._dp_week_program_pointer.max is not None:
for i in range(int(self._dp_week_program_pointer.min) + 1, int(self._dp_week_program_pointer.max) + 2):
profiles[ClimateProfile(f"{PROFILE_PREFIX}{i}")] = i - 1

return profiles


def _party_mode_code(start: datetime, end: datetime, away_temperature: float) -> str:
"""
Expand Down
175 changes: 175 additions & 0 deletions tests/test_climate.py
Original file line number Diff line number Diff line change
Expand Up @@ -305,6 +305,181 @@ async def test_cerfthermostat(
)


@pytest.mark.asyncio
@pytest.mark.parametrize(
(
"address_device_translation",
"do_mock_client",
"add_sysvars",
"add_programs",
"ignore_devices_on_create",
"un_ignore_list",
),
[
(TEST_DEVICES, True, False, False, None, None),
],
)
async def test_cerfthermostat_with_profiles(
central_client_factory: tuple[CentralUnit, Client | Mock, helper.Factory],
) -> None:
"""Test CustomDpRfThermostat."""
central, mock_client, _ = central_client_factory
climate: CustomDpRfThermostat = cast(
CustomDpRfThermostat, helper.get_prepared_custom_data_point(central, "VCU0000341", 2)
)
assert climate.usage == DataPointUsage.CDP_PRIMARY
assert climate.service_method_names == (
"copy_schedule",
"copy_schedule_profile",
"disable_away_mode",
"enable_away_mode_by_calendar",
"enable_away_mode_by_duration",
"get_schedule_profile",
"get_schedule_profile_weekday",
"set_mode",
"set_profile",
"set_schedule_profile",
"set_schedule_profile_weekday",
"set_simple_schedule_profile",
"set_simple_schedule_profile_weekday",
"set_temperature",
)
assert climate.min_temp == 5.0
assert climate.max_temp == 30.5
assert climate.supports_profiles is True
assert climate.target_temperature_step == 0.5
assert climate.profile == ClimateProfile.NONE
assert climate.activity is None
assert climate.current_humidity is None
assert climate.target_temperature is None
await climate.set_temperature(12.0)
assert mock_client.method_calls[-1] == call.set_value(
channel_address="VCU0000341:2",
paramset_key="VALUES",
parameter="SET_TEMPERATURE",
value=12.0,
wait_for_callback=WAIT_FOR_CALLBACK,
)
assert climate.target_temperature == 12.0

assert climate.current_temperature is None
await central.data_point_event(const.INTERFACE_ID, "VCU0000341:2", "ACTUAL_TEMPERATURE", 11.0)
assert climate.current_temperature == 11.0

assert climate.mode == ClimateMode.AUTO
assert climate.modes == (ClimateMode.AUTO, ClimateMode.HEAT, ClimateMode.OFF)
await climate.set_mode(ClimateMode.HEAT)
assert mock_client.method_calls[-1] == call.set_value(
channel_address="VCU0000341:2",
paramset_key="VALUES",
parameter="MANU_MODE",
value=12.0,
wait_for_callback=WAIT_FOR_CALLBACK,
)
await central.data_point_event(const.INTERFACE_ID, "VCU0000341:2", "CONTROL_MODE", _ModeHmIP.MANU.value)
assert climate.mode == ClimateMode.HEAT

await climate.set_mode(ClimateMode.OFF)
assert mock_client.method_calls[-1] == call.put_paramset(
channel_address="VCU0000341:2",
paramset_key="VALUES",
values={"MANU_MODE": 12.0, "SET_TEMPERATURE": 4.5},
wait_for_callback=WAIT_FOR_CALLBACK,
)

assert climate.mode == ClimateMode.OFF

await climate.set_mode(ClimateMode.AUTO)
assert mock_client.method_calls[-1] == call.set_value(
channel_address="VCU0000341:2",
paramset_key="VALUES",
parameter="AUTO_MODE",
value=True,
wait_for_callback=WAIT_FOR_CALLBACK,
)
await central.data_point_event(const.INTERFACE_ID, "VCU0000341:2", "CONTROL_MODE", 0)
await central.data_point_event(const.INTERFACE_ID, "VCU0000341:2", "SET_TEMPERATURE", 24.0)
assert climate.mode == ClimateMode.AUTO

assert climate.profile == ClimateProfile.WEEK_PROGRAM_1
assert climate.profiles == (
ClimateProfile.BOOST,
ClimateProfile.COMFORT,
ClimateProfile.ECO,
ClimateProfile.NONE,
ClimateProfile.WEEK_PROGRAM_1,
ClimateProfile.WEEK_PROGRAM_2,
ClimateProfile.WEEK_PROGRAM_3,
)
await climate.set_profile(ClimateProfile.BOOST)
assert mock_client.method_calls[-1] == call.set_value(
channel_address="VCU0000341:2",
paramset_key="VALUES",
parameter="BOOST_MODE",
value=True,
wait_for_callback=WAIT_FOR_CALLBACK,
)
await central.data_point_event(const.INTERFACE_ID, "VCU0000341:2", "CONTROL_MODE", 3)
assert climate.profile == ClimateProfile.BOOST
await central.data_point_event(const.INTERFACE_ID, "VCU0000341:2", "CONTROL_MODE", 2)
assert climate.profile == ClimateProfile.AWAY
await climate.set_profile(ClimateProfile.COMFORT)
assert mock_client.method_calls[-1] == call.set_value(
channel_address="VCU0000341:2",
paramset_key="VALUES",
parameter="COMFORT_MODE",
value=True,
wait_for_callback=WAIT_FOR_CALLBACK,
)
await climate.set_profile(ClimateProfile.ECO)
assert mock_client.method_calls[-1] == call.set_value(
channel_address="VCU0000341:2",
paramset_key="VALUES",
parameter="LOWERING_MODE",
value=True,
wait_for_callback=WAIT_FOR_CALLBACK,
)

await central.data_point_event(const.INTERFACE_ID, "VCU0000341:2", "CONTROL_MODE", 3)
call_count = len(mock_client.method_calls)
await climate.set_profile(ClimateProfile.BOOST)
assert call_count == len(mock_client.method_calls)

await climate.set_mode(ClimateMode.AUTO)
call_count = len(mock_client.method_calls)
await climate.set_mode(ClimateMode.AUTO)
assert call_count == len(mock_client.method_calls)

with freeze_time("2023-03-03 08:00:00"):
await climate.enable_away_mode_by_duration(hours=100, away_temperature=17.0)
assert mock_client.method_calls[-1] == call.set_value(
channel_address="VCU0000341:2",
paramset_key=ParamsetKey.VALUES,
parameter="PARTY_MODE_SUBMIT",
value="17.0,470,03,03,23,720,07,03,23",
)

with freeze_time("2023-03-03 08:00:00"):
await climate.enable_away_mode_by_calendar(
start=datetime(2000, 12, 1), end=datetime(2024, 12, 1), away_temperature=17.0
)
assert mock_client.method_calls[-1] == call.set_value(
channel_address="VCU0000341:2",
paramset_key=ParamsetKey.VALUES,
parameter="PARTY_MODE_SUBMIT",
value="17.0,0,01,12,00,0,01,12,24",
)

with freeze_time("2023-03-03 08:00:00"):
await climate.disable_away_mode()
assert mock_client.method_calls[-1] == call.set_value(
channel_address="VCU0000341:2",
paramset_key=ParamsetKey.VALUES,
parameter="PARTY_MODE_SUBMIT",
value="12.0,1260,02,03,23,1320,02,03,23",
)


@pytest.mark.asyncio
@pytest.mark.parametrize(
(
Expand Down

0 comments on commit 5cfa439

Please sign in to comment.