Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
d8f0896
Start adding firmware updating
bouwew Mar 11, 2026
b880fa1
Collect firmware-list at init
bouwew Mar 12, 2026
6754db6
More debug
bouwew Mar 12, 2026
85e560f
Combine functions
bouwew Mar 12, 2026
faf3376
Clean up
bouwew Mar 12, 2026
ea8b871
Continue adding firmware update function
bouwew Mar 12, 2026
6f328b7
Implement remaining functions
bouwew Mar 12, 2026
e6d9953
Fixes
bouwew Mar 12, 2026
0948b02
Add testcase for firmware updating
bouwew Mar 13, 2026
20bf5cd
Move mutable coordinator states to self
bouwew Mar 13, 2026
42d5552
Implement firmware-list improvement suggested by AI
bouwew Mar 13, 2026
0e11e51
Fix logger
bouwew Mar 13, 2026
5d3e80c
Implement AI optimizations
bouwew Mar 14, 2026
bcb981b
Improve function-names, reorder, private firmware_list
bouwew Mar 14, 2026
05bc711
Pop from _firmware_list when a device has been removed
bouwew Mar 14, 2026
a94e56f
Potential fix for pull request finding
bouwew Mar 14, 2026
4b2302e
Potential fix for pull request finding
bouwew Mar 14, 2026
d1f7244
Potential fix for pull request finding
bouwew Mar 14, 2026
3689e7b
Improve as suggested by Copilot
bouwew Mar 14, 2026
ef9db47
Improve testcode
bouwew Mar 14, 2026
080d347
Update Docstring
bouwew Mar 14, 2026
c8864bd
Clean up
bouwew Mar 14, 2026
4914230
Fix typo
bouwew Mar 14, 2026
6dc0277
Add pragma-no-covers
bouwew Mar 14, 2026
3ab8d3f
Fix indentation
bouwew Mar 14, 2026
37cab60
Improve docstring
bouwew Mar 14, 2026
9e7803e
Small improvement
bouwew Mar 14, 2026
43d3ad5
Replace left-over identifier[1]
bouwew Mar 14, 2026
2014a01
Update CHANGELOG
bouwew Mar 15, 2026
4a271e8
Don't populate _firmware_list twice
bouwew Mar 15, 2026
951923f
Potential fix for pull request finding
CoMPaTech Mar 15, 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
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@

Versions from 0.40 and up

## Ongoing

- New feature: show updated device firmware shortly after being updated via PR [#1039](https://github.com/plugwise/plugwise-beta/pull/1039)

## v0.63.1

- Implement Core PR [#163713](https://github.com/home-assistant/core/pull/163713) via PR [#1024](https://github.com/plugwise/plugwise-beta/pull/1024)
Expand Down
60 changes: 44 additions & 16 deletions custom_components/plugwise/coordinator.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
DEFAULT_UPDATE_INTERVAL,
DEV_CLASS,
DOMAIN,
FIRMWARE,
LOGGER,
P1_UPDATE_INTERVAL,
SWITCH_GROUPS,
Expand All @@ -44,11 +45,6 @@
class PlugwiseDataUpdateCoordinator(DataUpdateCoordinator[dict[str, GwEntityData]]):
"""Class to manage fetching Plugwise data from single endpoint."""

_connected: bool = False
_current_devices: set[str]
_stored_devices: set[str]
new_devices: set[str]

config_entry: PlugwiseConfigEntry

def __init__(
Expand Down Expand Up @@ -83,9 +79,11 @@ def __init__(
username=self.config_entry.data[CONF_USERNAME],
websession=async_get_clientsession(hass, verify_ssl=False),
)
self._current_devices = set()
self._stored_devices = set()
self.new_devices = set()
self._connected: bool = False
self._current_devices: set[str] = set()
self._firmware_list: dict[str, str | None] = {}
self._stored_devices: set[str] = set()
self.new_devices: set[str] = set()

async def _connect(self) -> None:
"""Connect to the Plugwise Smile.
Expand Down Expand Up @@ -154,11 +152,12 @@ async def _async_update_data(self) -> dict[str, GwEntityData]:
translation_key="unsupported_firmware",
) from err

await self._async_add_remove_devices(data)
LOGGER.debug("%s data: %s", self.api.smile.name, data)
await self._add_remove_devices(data)
await self._update_device_firmware(data)
return data

async def _async_add_remove_devices(self, data: dict[str, GwEntityData]) -> None:
async def _add_remove_devices(self, data: dict[str, GwEntityData]) -> None:
"""Add new Plugwise devices, remove non-existing devices."""
# Block switch-groups, use HA group helper instead to create switch-groups
for device_id, device in data.copy().items():
Expand All @@ -170,18 +169,20 @@ async def _async_add_remove_devices(self, data: dict[str, GwEntityData]) -> None
# this is required for the initialization of the available platform entities.
set_of_data = set(data)
self.new_devices = set_of_data - self._current_devices
for device_id in self.new_devices:
self._firmware_list.setdefault(device_id, data[device_id].get(FIRMWARE))

current_devices = self._stored_devices if not self._current_devices else self._current_devices
self._current_devices = set_of_data
if (current_devices - set_of_data): # device(s) to remove
await self._async_remove_devices(data)
if removed_devices := (current_devices - set_of_data): # device(s) to remove
await self._remove_devices(removed_devices)

async def _async_remove_devices(self, data: dict[str, GwEntityData]) -> None:
async def _remove_devices(self, removed_devices: set[str]) -> None:
"""Clean registries when removed devices found."""
device_reg = dr.async_get(self.hass)
device_list = dr.async_entries_for_config_entry(
device_reg, self.config_entry.entry_id
)
Comment thread
bouwew marked this conversation as resolved.

# First find the Plugwise via_device
gateway_device = device_reg.async_get_device({(DOMAIN, self.api.gateway_id)})
if gateway_device is None:
Expand All @@ -191,17 +192,44 @@ async def _async_remove_devices(self, data: dict[str, GwEntityData]) -> None:
# Then remove the connected orphaned device(s)
for device_entry in device_list:
for identifier in device_entry.identifiers:
device_id = identifier[1]
if (
identifier[0] == DOMAIN
and device_entry.via_device_id == via_device_id
and identifier[1] not in data
and device_id in removed_devices
):
device_reg.async_update_device(
device_entry.id, remove_config_entry_id=self.config_entry.entry_id
)
self._firmware_list.pop(device_id, None)
LOGGER.debug(
"Removed %s device/zone %s %s from device_registry",
DOMAIN,
device_entry.model,
identifier[1],
device_id,
)
break

async def _update_device_firmware(self, data: dict[str, GwEntityData]) -> None:
"""Detect firmware changes and update the device registry."""
for device_id, device in data.items():
if device_id not in self._firmware_list:
continue # pragma: no cover
if (new_firmware := device.get(FIRMWARE)) != self._firmware_list[device_id]:
await self._update_firmware_in_dr(device_id, new_firmware)
self._firmware_list[device_id] = new_firmware

async def _update_firmware_in_dr(self, device_id: str, firmware: str | None) -> None:
"""Update device sw_version in device_registry."""
device_reg = dr.async_get(self.hass)
device_entry = device_reg.async_get_device({(DOMAIN, device_id)})
if device_entry is None:
return # pragma: no cover

device_reg.async_update_device(device_entry.id, sw_version=firmware)
LOGGER.debug(
"Updated device firmware for %s %s %s",
DOMAIN,
device_entry.model,
device_id,
)
32 changes: 32 additions & 0 deletions tests/components/plugwise/test_init.py
Original file line number Diff line number Diff line change
Expand Up @@ -378,6 +378,38 @@ async def test_delete_removed_device(
assert "14df5c4dc8cb4ba69f9d1ac0eaf7c5c6" not in item_list


@pytest.mark.parametrize("chosen_env", ["m_adam_heating"], indirect=True)
@pytest.mark.parametrize("cooling_present", [False], indirect=True)
async def test_update_device_firmware(
hass: HomeAssistant,
mock_config_entry: MockConfigEntry,
mock_smile_adam_heat_cool: MagicMock,
device_registry: dr.DeviceRegistry,
init_integration: MockConfigEntry,
freezer: FrozenDateTimeFactory,
) -> None:
"""Test device firmware update via coordinator."""
data = mock_smile_adam_heat_cool.async_update.return_value

device_entry = device_registry.async_get_device(
identifiers={(DOMAIN, "da224107914542988a88561b4452b0f6")}
)
assert device_entry is not None
assert str(device_entry.sw_version) == "3.9.0"

data["da224107914542988a88561b4452b0f6"]["firmware"] = "3.10.13"
with patch(HA_PLUGWISE_SMILE_ASYNC_UPDATE, return_value=data):
freezer.tick(timedelta(minutes=1))
async_fire_time_changed(hass)
await hass.async_block_till_done()

device_entry = device_registry.async_get_device(
identifiers={(DOMAIN, "da224107914542988a88561b4452b0f6")}
)
assert device_entry is not None
assert str(device_entry.sw_version) == "3.10.13"


@pytest.mark.parametrize("chosen_env", ["m_adam_heating"], indirect=True)
@pytest.mark.parametrize("cooling_present", [False], indirect=True)
async def test_update_interval_adam(
Expand Down
Loading