Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
52 commits
Select commit Hold shift + click to select a range
6cc350d
Climate: add _last_active_schedule parameter and use RestoreEntity
bouwew Oct 19, 2025
c9fe3ca
Fix logic
bouwew Oct 19, 2025
1281247
Fix test-assert
bouwew Oct 19, 2025
ce35ef2
Fix another test-assert
bouwew Oct 19, 2025
77b4e01
Clear multple spaces
bouwew Oct 19, 2025
33e847b
Improve guarding
bouwew Oct 19, 2025
0d1130e
Fix logic
bouwew Oct 19, 2025
f7ae643
Move as suggested
bouwew Oct 19, 2025
5c72828
Again fix logic
bouwew Oct 19, 2025
1585605
Follow CRAI suggestion
bouwew Oct 19, 2025
ad51a6d
Try restoring state
bouwew Oct 19, 2025
81a6888
Add _previous_action_mode to restore
bouwew Oct 20, 2025
527311f
Try fixing adam3 test-fail
bouwew Oct 20, 2025
211d563
Fix order, add more debugging
bouwew Oct 21, 2025
befd01e
Updates
bouwew Oct 21, 2025
8bab227
Code and test updates
bouwew Oct 21, 2025
d345155
Improve logic for determining the required schedule argument
bouwew Oct 21, 2025
549f8d4
Update testcase as suggested
bouwew Oct 21, 2025
a6bb3a8
More code updates
bouwew Oct 21, 2025
2ee0364
Try
bouwew Oct 21, 2025
402f82c
Complete logic
bouwew Oct 22, 2025
a3e85eb
Ruffed
bouwew Oct 22, 2025
ff057e3
Reruffed
bouwew Oct 22, 2025
3e822c6
Improve logic as suggested
bouwew Oct 22, 2025
b454309
Link to plugwise v1.8.3a0
bouwew Oct 22, 2025
72eb579
Improve as suggested
bouwew Oct 22, 2025
c0be15f
Partly revert
bouwew Oct 22, 2025
2943bd4
Try fixing
bouwew Oct 22, 2025
9d217f8
Try fixing 2
bouwew Oct 22, 2025
bc7847f
Improve
bouwew Oct 22, 2025
dddcb74
Fix previous_action_mode typing and use
bouwew Oct 22, 2025
72b0670
Test-cover case with no schedule name available for restore
bouwew Oct 22, 2025
39c4585
Add coverage for using previous_action_mode
bouwew Oct 22, 2025
562a0d1
Try
bouwew Oct 23, 2025
97c66d4
Link to plugwise v1.8.3
bouwew Oct 23, 2025
8b5af85
More type improvements
bouwew Oct 23, 2025
43e326f
Revert back to working tests
bouwew Oct 23, 2025
b66df0f
Use a string
bouwew Oct 23, 2025
dbbda42
Improve comments, constant the long HAError message
bouwew Oct 23, 2025
6b64ccc
More comment improvements, add missing .value's
bouwew Oct 23, 2025
19ceb93
Break out _homekit_translate() function
bouwew Oct 23, 2025
23fa5b3
Extend broken-out function
bouwew Oct 23, 2025
fda8977
Add missing typing
bouwew Oct 23, 2025
f9917df
Add pragma-no-cover
bouwew Oct 23, 2025
086032e
Test-climate: import STATE_OFF/_ON and implement
bouwew Oct 23, 2025
279c673
Add one more .value
bouwew Oct 23, 2025
a7bdc77
Add missing comma
bouwew Oct 23, 2025
7066dd6
Update-change restore testcase
bouwew Oct 23, 2025
f234b8c
Add pragma-no-cover
bouwew Oct 23, 2025
456329e
Implement suggestion
bouwew Oct 23, 2025
b15dce2
Update CHANGELOG
bouwew Oct 23, 2025
25347ea
Set to v0.59.0 release-version
bouwew Oct 23, 2025
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
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.59.0

- New Feature: use RestoreState in climate to save schedule en regulation status, also via plugwise [v1.8.3](https://github.com/plugwise/python-plugwise/releases/tag/v1.8.3)
- More Emma-related updates via plugwise [v1.8.2](https://github.com/plugwise/python-plugwise/releases/tag/v1.8.2)

## v0.58.1
Expand Down
2 changes: 1 addition & 1 deletion custom_components/plugwise/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -146,7 +146,7 @@ async def async_migrate_entry(hass: HomeAssistant, entry: PlugwiseConfigEntry) -
"""Migrate back to v1.1 config entry."""
if entry.version > 1:
# This means the user has downgraded from a future version
return False
return False #pragma: no cover

if entry.version == 1 and entry.minor_version == 2:
new_data = {**entry.data}
Expand Down
118 changes: 88 additions & 30 deletions custom_components/plugwise/climate.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

from __future__ import annotations

from dataclasses import dataclass
from typing import Any

from homeassistant.components.climate import (
Expand All @@ -23,7 +24,9 @@
UnitOfTemperature,
)
from homeassistant.core import HomeAssistant, callback
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from homeassistant.helpers.restore_state import ExtraStoredData, RestoreEntity

from .const import (
ACTIVE_PRESET,
Expand Down Expand Up @@ -51,6 +54,7 @@
from .entity import PlugwiseEntity
from .util import plugwise_command

ERROR_NO_SCHEDULE = "Failed setting HVACMode, set a schedule first"
PARALLEL_UPDATES = 0


Expand Down Expand Up @@ -97,7 +101,30 @@ def _add_entities() -> None:
entry.async_on_unload(coordinator.async_add_listener(_add_entities))


class PlugwiseClimateEntity(PlugwiseEntity, ClimateEntity):
@dataclass
class PlugwiseClimateExtraStoredData(ExtraStoredData):
"""Object to hold extra stored data."""

last_active_schedule: str | None
previous_action_mode: str | None

def as_dict(self) -> dict[str, Any]:
"""Return a dict representation of the text data."""
return {
"last_active_schedule": self.last_active_schedule,
"previous_action_mode": self.previous_action_mode,
}

@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."""

_attr_has_entity_name = True
Expand All @@ -106,9 +133,21 @@ class PlugwiseClimateEntity(PlugwiseEntity, ClimateEntity):
_attr_translation_key = DOMAIN
_enable_turn_on_off_backwards_compatibility = False

_previous_mode: str = HVACAction.HEATING # Upstream
_last_active_schedule: str | None = None
_previous_action_mode: str | None = HVACAction.HEATING.value # Upstream
_homekit_mode: HVACMode | None = None # pw-beta homekit emulation + intentional unsort

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

def __init__(
self,
coordinator: PlugwiseDataUpdateCoordinator,
Expand All @@ -121,7 +160,6 @@ def __init__(
gateway_id: str = coordinator.api.gateway_id
self._gateway_data = coordinator.data[gateway_id]
self._homekit_enabled = homekit_enabled # pw-beta homekit emulation

self._location = device_id
if (location := self.device.get(LOCATION)) is not None:
self._location = location
Expand Down Expand Up @@ -151,25 +189,19 @@ def __init__(
self._attr_supported_features |= ClimateEntityFeature.PRESET_MODE
self._attr_preset_modes = presets

def _previous_action_mode(self, coordinator: PlugwiseDataUpdateCoordinator) -> None:
"""Return the previous action-mode when the regulation-mode is not heating or cooling.

Helper for set_hvac_mode().
"""
# When no cooling available, _previous_mode is always heating
if (
REGULATION_MODES in self._gateway_data
and HVACAction.COOLING in self._gateway_data[REGULATION_MODES]
):
mode = self._gateway_data[SELECT_REGULATION_MODE]
if mode in (HVACAction.COOLING, HVACAction.HEATING):
self._previous_mode = mode

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

@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,
)

@property
def target_temperature(self) -> float | None:
"""Return the temperature we try to reach.
Expand Down Expand Up @@ -203,13 +235,14 @@ def hvac_mode(self) -> HVACMode:
return HVACMode.HEAT # pragma: no cover
try:
hvac = HVACMode(mode)
except ValueError:
except ValueError: # pragma: no cover
return HVACMode.HEAT # pragma: no cover
if hvac not in self.hvac_modes:
return HVACMode.HEAT # pragma: no cover
# pw-beta homekit emulation
if self._homekit_enabled and self._homekit_mode == HVACMode.OFF:
return HVACMode.OFF # pragma: no cover

return hvac

@property
Expand All @@ -228,9 +261,9 @@ def hvac_modes(self) -> list[HVACMode]:
if self.coordinator.api.cooling_present:
if REGULATION_MODES in self._gateway_data:
selected = self._gateway_data.get(SELECT_REGULATION_MODE)
if selected == HVACAction.COOLING:
if selected == HVACAction.COOLING.value:
hvac_modes.append(HVACMode.COOL)
if selected == HVACAction.HEATING:
if selected == HVACAction.HEATING.value:
hvac_modes.append(HVACMode.HEAT)
else:
hvac_modes.append(HVACMode.HEAT_COOL)
Expand All @@ -242,8 +275,15 @@ def hvac_modes(self) -> list[HVACMode]:
@property
def hvac_action(self) -> HVACAction: # pw-beta add to Core
"""Return the current running hvac operation if supported."""
# Keep track of the previous action-mode
self._previous_action_mode(self.coordinator)
# Keep track of the previous hvac_action mode.
# When no cooling available, _previous_action_mode is always heating
if (
REGULATION_MODES in self._gateway_data
and HVACAction.COOLING.value in self._gateway_data[REGULATION_MODES]
):
mode = self._gateway_data[SELECT_REGULATION_MODE]
if mode in (HVACAction.COOLING.value, HVACAction.HEATING.value):
self._previous_action_mode = mode

if (action := self.device.get(CONTROL_STATE)) is not None:
return HVACAction(action)
Expand Down Expand Up @@ -280,20 +320,38 @@ async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None:
return

if hvac_mode != HVACMode.OFF:
current = self.device.get("select_schedule")
desired = current

# Capture the last valid schedule
if desired and desired != "off":
self._last_active_schedule = desired
elif desired == "off":
desired = self._last_active_schedule

# Enabling HVACMode.AUTO requires a previously set schedule for saving and restoring
if hvac_mode == HVACMode.AUTO and not desired:
raise HomeAssistantError(ERROR_NO_SCHEDULE)

await self.coordinator.api.set_schedule_state(
self._location,
STATE_ON if hvac_mode == HVACMode.AUTO else STATE_OFF,
desired,
)

await self._homekit_translate_or_not(hvac_mode) # pw-beta

async def _homekit_translate_or_not(self, mode: HVACMode) -> None:
"""Mimic HomeKit by setting a suitable preset, when homekit mode is enabled."""
if (
not self._homekit_enabled
): # pw-beta: feature request - mimic HomeKit behavior
if hvac_mode == HVACMode.OFF:
await self.coordinator.api.set_regulation_mode(hvac_mode)
elif self.hvac_mode == HVACMode.OFF:
await self.coordinator.api.set_regulation_mode(self._previous_mode)
else:
self._homekit_mode = hvac_mode # pragma: no cover
not self._homekit_enabled # pw-beta
):
if mode == HVACMode.OFF:
await self.coordinator.api.set_regulation_mode(mode.value)
elif self.hvac_mode == HVACMode.OFF and self._previous_action_mode:
await self.coordinator.api.set_regulation_mode(self._previous_action_mode)
else: # pw-beta
self._homekit_mode = mode # pragma: no cover
if self._homekit_mode == HVACMode.OFF: # pragma: no cover
await self.async_set_preset_mode(PRESET_AWAY) # pragma: no cover
if (
Expand Down
4 changes: 2 additions & 2 deletions custom_components/plugwise/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
"integration_type": "hub",
"iot_class": "local_polling",
"loggers": ["plugwise"],
"requirements": ["plugwise==1.8.2"],
"version": "0.58.1",
"requirements": ["plugwise==1.8.3"],
"version": "0.59.0",
"zeroconf": ["_plugwise._tcp.local."]
}
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.58.2"
version = "0.59.0"
description = "Plugwise beta custom-component"
readme = "README.md"
requires-python = ">=3.13"
Expand Down
Loading
Loading