Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
7b7e635
Add manual-to-auto hvac_mode testcase
bouwew May 22, 2026
74fbe0b
Update typing of from_dict() function
bouwew May 23, 2026
e06f1f4
Revert changes
bouwew May 23, 2026
7befc1a
Improve/simplify restore_state related code
bouwew May 23, 2026
6c80a3d
Ruff fixes
bouwew May 23, 2026
3384dce
Use specific selfs for restore_state atttributes
bouwew May 23, 2026
0c7d200
Update self._last_active_schedule when turning a schedule off
bouwew May 23, 2026
4d61362
Rework set_hvac_mode()
bouwew May 24, 2026
ee286af
Update CHANGELOG
bouwew May 24, 2026
c18fea0
Set to v0.64.2a0 test-version
bouwew May 24, 2026
18ac12f
Correct to a2
bouwew May 24, 2026
98ed6dc
Fix _regulation_mode_for_hvac() as suggested
bouwew May 24, 2026
e6e9364
Ruff fixes
bouwew May 24, 2026
d5423ab
Reduce complexity
bouwew May 24, 2026
146a0bf
Rename requirements_test_all.txt to requirements_test.txt
CoMPaTech May 24, 2026
e468113
Update CACHE_VERSION to 4 in core_next.yml
CoMPaTech May 24, 2026
7daa269
Update CACHE_VERSION to 4 in test workflow
CoMPaTech May 24, 2026
4a75cc8
Remove raise, extend docstring instead
bouwew May 24, 2026
40e7142
Add new test fixture
bouwew May 24, 2026
88e6624
Add test coverage
bouwew May 24, 2026
d9fcc42
Correct fixture contents
bouwew May 24, 2026
dce133b
Add more test coverage
bouwew May 24, 2026
46da180
Sorting
bouwew May 24, 2026
4c466d9
Set to v0.64.2 release-version
bouwew May 24, 2026
2d55cdb
CHANGELOG - link to Core Issue
bouwew May 24, 2026
4d54ca0
Implement suggested improvement
bouwew May 24, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/core_next.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ name: Validate plugwise-beta against HA-core dev

env:
# Uses a different key/restore key than test.yml
CACHE_VERSION: 3
CACHE_VERSION: 4
DEFAULT_PYTHON: "3.14"
VENV: venv

Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
name: Test PR against HA-core

env:
CACHE_VERSION: 3
CACHE_VERSION: 4
DEFAULT_PYTHON: "3.14"
VENV: venv

Expand Down
3 changes: 2 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,9 @@

Versions from 0.40 and up

## Ongoing
## v0.64.2

- Fix for Core Issue [#171955](https://github.com/home-assistant/core/issues/171955) via PR [#1073](https://github.com/plugwise/plugwise-beta/pull/1073)
- Update snapshots to the new format via PR [#1070](https://github.com/plugwise/plugwise-beta/pull/1070)
- Add local beta brand icons via PR [#1050](https://github.com/plugwise/plugwise-beta/pull/1050)
- Lining up strings with Core Plugwise, update translations via PR [#1048](https://github.com/plugwise/plugwise-beta/pull/1048)
Expand Down
140 changes: 58 additions & 82 deletions custom_components/plugwise/climate.py
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The multiple if statements should be cleaned up, but lets leave that for another time, just to mark for future optimisation

Original file line number Diff line number Diff line change
Expand Up @@ -102,14 +102,6 @@ def as_dict(self) -> dict[str, Any]:
"""Return a dict representation of the text data."""
return asdict(self)

@classmethod
def from_dict(cls, restored: dict[str, Any]) -> PlugwiseClimateExtraStoredData:
"""Initialize a stored data object from a dict."""
return cls(
last_active_schedule=restored.get("last_active_schedule"),
previous_action_mode=restored.get("previous_action_mode"),
)


class PlugwiseClimateEntity(PlugwiseEntity, ClimateEntity, RestoreEntity):
"""Representation of a Plugwise thermostat."""
Expand All @@ -120,22 +112,6 @@ class PlugwiseClimateEntity(PlugwiseEntity, ClimateEntity, RestoreEntity):
_attr_translation_key = DOMAIN
_enable_turn_on_off_backwards_compatibility = False

_last_active_schedule: str | None = None
_previous_action_mode: str | None = HVACAction.HEATING.value

async def async_added_to_hass(self) -> None:
"""Run when entity about to be added."""
await super().async_added_to_hass()

if extra_data := await self.async_get_last_extra_data():
plugwise_extra_data = PlugwiseClimateExtraStoredData.from_dict(
extra_data.as_dict()
)
self._last_active_schedule = plugwise_extra_data.last_active_schedule
self._previous_action_mode = (
plugwise_extra_data.previous_action_mode or HVACAction.HEATING.value
)

def __init__(
self,
coordinator: PlugwiseDataUpdateCoordinator,
Expand All @@ -147,9 +123,11 @@ def __init__(
self._api = coordinator.api
gateway_id: str = self._api.gateway_id
self._gateway_data = coordinator.data[gateway_id]
self._last_active_schedule: str | None = None
self._location = device_id
if (location := self.device.get(LOCATION)) is not None:
self._location = location
self._previous_action_mode = HVACAction.HEATING.value

self._attr_max_temp = min(self.device.get(THERMOSTAT, {}).get(UPPER_BOUND, 35.0), 35.0)
self._attr_min_temp = self.device.get(THERMOSTAT, {}).get(LOWER_BOUND, 0.0)
Expand All @@ -176,19 +154,32 @@ def __init__(
self._attr_supported_features |= ClimateEntityFeature.PRESET_MODE
self._attr_preset_modes = presets

@property
def current_temperature(self) -> float | None:
"""Return the current temperature."""
return self.device.get(SENSORS, {}).get(ATTR_TEMPERATURE)
async def async_added_to_hass(self) -> None:
"""Run when entity about to be added."""

extra_data = await self.async_get_last_extra_data()
if extra_data is not None:
data = extra_data.as_dict()
self._last_active_schedule = data.get("last_active_schedule")
self._previous_action_mode = (
data.get("previous_action_mode") or HVACAction.HEATING.value
)

await super().async_added_to_hass()

@property
def extra_restore_state_data(self) -> PlugwiseClimateExtraStoredData:
"""Return text specific state data to be restored."""
return PlugwiseClimateExtraStoredData(
last_active_schedule=self._last_active_schedule,
previous_action_mode=self._previous_action_mode,
self._last_active_schedule,
self._previous_action_mode,
)

@property
def current_temperature(self) -> float | None:
"""Return the current temperature."""
return self.device.get(SENSORS, {}).get(ATTR_TEMPERATURE)

@property
def target_temperature(self) -> float | None:
"""Return the temperature we try to reach.
Expand Down Expand Up @@ -293,80 +284,65 @@ async def async_set_temperature(self, **kwargs: Any) -> None:

await self._api.set_temperature(self._location, data)

def _regulation_mode_for_hvac(self, hvac_mode: HVACMode) -> str | None:
"""Return the API regulation value for a manual HVAC mode, or None."""
def _regulation_mode_for_hvac(self, hvac_mode: HVACMode) -> str:
"""Return the API regulation value for a manual HVAC mode.

The function inputs are limited to the HVACModes HEAT and COOL.
"""
if hvac_mode == HVACMode.HEAT:
return HVACAction.HEATING.value
mode = HVACAction.HEATING.value
if hvac_mode == HVACMode.COOL:
return HVACAction.COOLING.value
return None
mode = HVACAction.COOLING.value
return mode
Comment thread
bouwew marked this conversation as resolved.

@plugwise_command
async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None:
"""Set the HVAC mode (off, heat, cool, heat_cool, or auto/schedule)."""

# Early exit if no mode change
if hvac_mode == self.hvac_mode:
return

current_schedule = self.device.get("select_schedule")
# OFF: single API call
# Adam only: set to HVACMode.OFF
if hvac_mode == HVACMode.OFF:
await self._api.set_regulation_mode(hvac_mode.value)
return

# Manual mode (heat/cool/heat_cool) without a schedule: set regulation only
if (
current_schedule is None
and hvac_mode != HVACMode.AUTO
and (
regulation := self._regulation_mode_for_hvac(hvac_mode)
or self._previous_action_mode
)
):
current_schedule = self.device.get("select_schedule")
schedule_is_active = current_schedule not in (None, "off")
# Adam only: transition from HVACMode.OFF
if self.hvac_mode == HVACMode.OFF:
if hvac_mode == HVACMode.AUTO:
if not schedule_is_active and self._last_active_schedule is None:
raise HomeAssistantError(
translation_domain=DOMAIN,
translation_key=ERROR_NO_SCHEDULE,
)
await self._api.set_schedule_state(self._location, STATE_ON, self._last_active_schedule)
Comment thread
bouwew marked this conversation as resolved.
await self._api.set_regulation_mode(self._previous_action_mode)
return

# Transition to manual mode
if schedule_is_active:
await self._api.set_schedule_state(self._location, STATE_OFF, current_schedule)
self._last_active_schedule = current_schedule
regulation = self._regulation_mode_for_hvac(hvac_mode)
await self._api.set_regulation_mode(regulation)
return

# Manual mode: ensure regulation and turn off schedule when needed
if hvac_mode in (HVACMode.HEAT, HVACMode.COOL, HVACMode.HEAT_COOL):
await self._set_manual_hvac_mode(hvac_mode, current_schedule)
# Common - transition from auto = schedule off
if self.hvac_mode == HVACMode.AUTO:
await self._api.set_schedule_state(self._location, STATE_OFF, current_schedule)
self._last_active_schedule = current_schedule
Comment thread
bouwew marked this conversation as resolved.
return

# AUTO: restore schedule and regulation
desired_schedule = current_schedule
if desired_schedule and desired_schedule != "off":
self._last_active_schedule = desired_schedule
elif desired_schedule == "off":
desired_schedule = self._last_active_schedule

if not desired_schedule:
# Common - transition to auto = schedule on
if self._last_active_schedule is None:
raise HomeAssistantError(
translation_domain=DOMAIN,
translation_key=ERROR_NO_SCHEDULE,
)

await self._set_auto_hvac_mode(desired_schedule)

async def _set_manual_hvac_mode(self, mode: HVACMode, schedule: str | None) -> None:
"""Execute relevant api-functions based on the requested manual and present mode."""
if (
regulation := self._regulation_mode_for_hvac(mode) or (
self._previous_action_mode
if self.hvac_mode in (HVACMode.HEAT_COOL, HVACMode.OFF)
else None
)
):
await self._api.set_regulation_mode(regulation)

if (
self.hvac_mode == HVACMode.OFF and schedule not in (None, "off")
) or (self.hvac_mode == HVACMode.AUTO and schedule is not None):
await self._api.set_schedule_state(self._location, STATE_OFF, schedule)

async def _set_auto_hvac_mode(self, schedule: str) -> None:
"""Execute relevant api-functions based on the requested auto and present mode."""
if self._previous_action_mode:
if self.hvac_mode == HVACMode.OFF:
await self._api.set_regulation_mode(self._previous_action_mode)
await self._api.set_schedule_state(self._location, STATE_ON, schedule)
await self._api.set_schedule_state(self._location, STATE_ON, self._last_active_schedule)
Comment thread
bouwew marked this conversation as resolved.

Comment thread
bouwew marked this conversation as resolved.
@plugwise_command
async def async_set_preset_mode(self, preset_mode: str) -> None:
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[project]
name = "plugwise-beta"
version = "0.64.1"
version = "0.64.2"
description = "Plugwise beta custom-component"
readme = "README.md"
requires-python = ">=3.14"
Expand Down
4 changes: 2 additions & 2 deletions scripts/core-testing.sh
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,7 @@ mkdir -p "${coredir}"

if [ -z "${GITHUB_ACTIONS}" ] || [ "$1" == "core_prep" ] ; then
# If only dir exists, but not cloned yet
if [ ! -f "${coredir}/requirements_test_all.txt" ]; then
if [ ! -f "${coredir}/requirements_test.txt" ]; then
if [ -d "${manualdir}" ]; then
echo ""
echo -e "${CINFO} ** Reusing copy, rebasing and copy to HA core**${CNORM}"
Expand All @@ -143,7 +143,7 @@ if [ -z "${GITHUB_ACTIONS}" ] || [ "$1" == "core_prep" ] ; then
git clone https://github.com/home-assistant/core.git "${coredir}"
cp -a "${coredir}." "${manualdir}"
fi
if [ ! -f "${coredir}/requirements_test_all.txt" ]; then
if [ ! -f "${coredir}/requirements_test.txt" ]; then
echo ""
echo -e "${CFAIL}Cloning failed .. make sure ${coredir} exists and is an empty directory${CNORM}"
echo ""
Expand Down
Loading
Loading