From ee44e9d4d620e14c295c641515355eb572b6ffa7 Mon Sep 17 00:00:00 2001 From: Josef Zweck <24647999+zweckj@users.noreply.github.com> Date: Wed, 17 Jan 2024 14:49:08 +0100 Subject: [PATCH] Add update platform to La Marzocco (#108235) * add update * requested changes * improve * docstring * docstring --- .../components/lamarzocco/__init__.py | 1 + .../components/lamarzocco/strings.json | 8 ++ homeassistant/components/lamarzocco/update.py | 105 +++++++++++++++++ tests/components/lamarzocco/conftest.py | 3 +- .../lamarzocco/snapshots/test_update.ambr | 111 ++++++++++++++++++ tests/components/lamarzocco/test_update.py | 76 ++++++++++++ 6 files changed, 303 insertions(+), 1 deletion(-) create mode 100644 homeassistant/components/lamarzocco/update.py create mode 100644 tests/components/lamarzocco/snapshots/test_update.ambr create mode 100644 tests/components/lamarzocco/test_update.py diff --git a/homeassistant/components/lamarzocco/__init__.py b/homeassistant/components/lamarzocco/__init__.py index 0ef40a231cc034..ba37a7f90d726d 100644 --- a/homeassistant/components/lamarzocco/__init__.py +++ b/homeassistant/components/lamarzocco/__init__.py @@ -13,6 +13,7 @@ Platform.SELECT, Platform.SENSOR, Platform.SWITCH, + Platform.UPDATE, ] diff --git a/homeassistant/components/lamarzocco/strings.json b/homeassistant/components/lamarzocco/strings.json index 150356d600f83b..fc326b416668e3 100644 --- a/homeassistant/components/lamarzocco/strings.json +++ b/homeassistant/components/lamarzocco/strings.json @@ -104,6 +104,14 @@ "steam_boiler": { "name": "Steam boiler" } + }, + "update": { + "machine_firmware": { + "name": "Machine firmware" + }, + "gateway_firmware": { + "name": "Gateway firmware" + } } } } diff --git a/homeassistant/components/lamarzocco/update.py b/homeassistant/components/lamarzocco/update.py new file mode 100644 index 00000000000000..0e811019cbbb9b --- /dev/null +++ b/homeassistant/components/lamarzocco/update.py @@ -0,0 +1,105 @@ +"""Support for La Marzocco update entities.""" + +from collections.abc import Callable +from dataclasses import dataclass +from typing import Any + +from lmcloud import LMCloud as LaMarzoccoClient +from lmcloud.const import LaMarzoccoUpdateableComponent + +from homeassistant.components.update import ( + UpdateDeviceClass, + UpdateEntity, + UpdateEntityDescription, + UpdateEntityFeature, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import EntityCategory +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import HomeAssistantError +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from .const import DOMAIN +from .entity import LaMarzoccoEntity, LaMarzoccoEntityDescription + + +@dataclass(frozen=True, kw_only=True) +class LaMarzoccoUpdateEntityDescription( + LaMarzoccoEntityDescription, + UpdateEntityDescription, +): + """Description of a La Marzocco update entities.""" + + current_fw_fn: Callable[[LaMarzoccoClient], str] + latest_fw_fn: Callable[[LaMarzoccoClient], str] + component: LaMarzoccoUpdateableComponent + + +ENTITIES: tuple[LaMarzoccoUpdateEntityDescription, ...] = ( + LaMarzoccoUpdateEntityDescription( + key="machine_firmware", + translation_key="machine_firmware", + device_class=UpdateDeviceClass.FIRMWARE, + icon="mdi:cloud-download", + current_fw_fn=lambda lm: lm.firmware_version, + latest_fw_fn=lambda lm: lm.latest_firmware_version, + component=LaMarzoccoUpdateableComponent.MACHINE, + entity_category=EntityCategory.DIAGNOSTIC, + ), + LaMarzoccoUpdateEntityDescription( + key="gateway_firmware", + translation_key="gateway_firmware", + device_class=UpdateDeviceClass.FIRMWARE, + icon="mdi:cloud-download", + current_fw_fn=lambda lm: lm.gateway_version, + latest_fw_fn=lambda lm: lm.latest_gateway_version, + component=LaMarzoccoUpdateableComponent.GATEWAY, + entity_category=EntityCategory.DIAGNOSTIC, + ), +) + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Create update entities.""" + + coordinator = hass.data[DOMAIN][config_entry.entry_id] + async_add_entities( + LaMarzoccoUpdateEntity(coordinator, description) + for description in ENTITIES + if description.supported_fn(coordinator) + ) + + +class LaMarzoccoUpdateEntity(LaMarzoccoEntity, UpdateEntity): + """Entity representing the update state.""" + + entity_description: LaMarzoccoUpdateEntityDescription + _attr_supported_features = UpdateEntityFeature.INSTALL + + @property + def installed_version(self) -> str | None: + """Return the current firmware version.""" + return self.entity_description.current_fw_fn(self.coordinator.lm) + + @property + def latest_version(self) -> str: + """Return the latest firmware version.""" + return self.entity_description.latest_fw_fn(self.coordinator.lm) + + async def async_install( + self, version: str | None, backup: bool, **kwargs: Any + ) -> None: + """Install an update.""" + self._attr_in_progress = True + self.async_write_ha_state() + success = await self.coordinator.lm.update_firmware( + self.entity_description.component + ) + if not success: + raise HomeAssistantError("Update failed") + self._attr_in_progress = False + self.async_write_ha_state() diff --git a/tests/components/lamarzocco/conftest.py b/tests/components/lamarzocco/conftest.py index cc2d121e63290e..7bb7e849ef1767 100644 --- a/tests/components/lamarzocco/conftest.py +++ b/tests/components/lamarzocco/conftest.py @@ -87,9 +87,10 @@ def mock_lamarzocco( lamarzocco.serial_number = serial_number lamarzocco.firmware_version = "1.1" - lamarzocco.latest_firmware_version = "1.1" + lamarzocco.latest_firmware_version = "1.2" lamarzocco.gateway_version = "v2.2-rc0" lamarzocco.latest_gateway_version = "v3.1-rc4" + lamarzocco.update_firmware.return_value = True lamarzocco.current_status = load_json_object_fixture( "current_status.json", DOMAIN diff --git a/tests/components/lamarzocco/snapshots/test_update.ambr b/tests/components/lamarzocco/snapshots/test_update.ambr new file mode 100644 index 00000000000000..29d09278ea28f1 --- /dev/null +++ b/tests/components/lamarzocco/snapshots/test_update.ambr @@ -0,0 +1,111 @@ +# serializer version: 1 +# name: test_update_entites[gateway_firmware-gateway] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'auto_update': False, + 'device_class': 'firmware', + 'entity_picture': 'https://brands.home-assistant.io/_/lamarzocco/icon.png', + 'friendly_name': 'GS01234 Gateway firmware', + 'icon': 'mdi:cloud-download', + 'in_progress': False, + 'installed_version': 'v2.2-rc0', + 'latest_version': 'v3.1-rc4', + 'release_summary': None, + 'release_url': None, + 'skipped_version': None, + 'supported_features': , + 'title': None, + }), + 'context': , + 'entity_id': 'update.gs01234_gateway_firmware', + 'last_changed': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_update_entites[gateway_firmware-gateway].1 + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'update', + 'entity_category': , + 'entity_id': 'update.gs01234_gateway_firmware', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': 'mdi:cloud-download', + 'original_name': 'Gateway firmware', + 'platform': 'lamarzocco', + 'previous_unique_id': None, + 'supported_features': , + 'translation_key': 'gateway_firmware', + 'unique_id': 'GS01234_gateway_firmware', + 'unit_of_measurement': None, + }) +# --- +# name: test_update_entites[machine_firmware-machine] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'auto_update': False, + 'device_class': 'firmware', + 'entity_picture': 'https://brands.home-assistant.io/_/lamarzocco/icon.png', + 'friendly_name': 'GS01234 Machine firmware', + 'icon': 'mdi:cloud-download', + 'in_progress': False, + 'installed_version': '1.1', + 'latest_version': '1.2', + 'release_summary': None, + 'release_url': None, + 'skipped_version': None, + 'supported_features': , + 'title': None, + }), + 'context': , + 'entity_id': 'update.gs01234_machine_firmware', + 'last_changed': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_update_entites[machine_firmware-machine].1 + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'update', + 'entity_category': , + 'entity_id': 'update.gs01234_machine_firmware', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': 'mdi:cloud-download', + 'original_name': 'Machine firmware', + 'platform': 'lamarzocco', + 'previous_unique_id': None, + 'supported_features': , + 'translation_key': 'machine_firmware', + 'unique_id': 'GS01234_machine_firmware', + 'unit_of_measurement': None, + }) +# --- diff --git a/tests/components/lamarzocco/test_update.py b/tests/components/lamarzocco/test_update.py new file mode 100644 index 00000000000000..55c5bb0da3d0a1 --- /dev/null +++ b/tests/components/lamarzocco/test_update.py @@ -0,0 +1,76 @@ +"""Tests for the La Marzocco Update Entities.""" + + +from unittest.mock import MagicMock + +from lmcloud.const import LaMarzoccoUpdateableComponent +import pytest +from syrupy import SnapshotAssertion + +from homeassistant.components.update import DOMAIN as UPDATE_DOMAIN, SERVICE_INSTALL +from homeassistant.const import ATTR_ENTITY_ID +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import HomeAssistantError +from homeassistant.helpers import entity_registry as er + +pytestmark = pytest.mark.usefixtures("init_integration") + + +@pytest.mark.parametrize( + ("entity_name", "component"), + [ + ("machine_firmware", LaMarzoccoUpdateableComponent.MACHINE), + ("gateway_firmware", LaMarzoccoUpdateableComponent.GATEWAY), + ], +) +async def test_update_entites( + hass: HomeAssistant, + mock_lamarzocco: MagicMock, + entity_registry: er.EntityRegistry, + snapshot: SnapshotAssertion, + entity_name: str, + component: LaMarzoccoUpdateableComponent, +) -> None: + """Test the La Marzocco update entities.""" + + serial_number = mock_lamarzocco.serial_number + + state = hass.states.get(f"update.{serial_number}_{entity_name}") + assert state + assert state == snapshot + + entry = entity_registry.async_get(state.entity_id) + assert entry + assert entry == snapshot + + await hass.services.async_call( + UPDATE_DOMAIN, + SERVICE_INSTALL, + { + ATTR_ENTITY_ID: f"update.{serial_number}_{entity_name}", + }, + blocking=True, + ) + + mock_lamarzocco.update_firmware.assert_called_once_with(component) + + +async def test_update_error( + hass: HomeAssistant, + mock_lamarzocco: MagicMock, +) -> None: + """Test error during update.""" + state = hass.states.get(f"update.{mock_lamarzocco.serial_number}_machine_firmware") + assert state + + mock_lamarzocco.update_firmware.return_value = False + + with pytest.raises(HomeAssistantError, match="Update failed"): + await hass.services.async_call( + UPDATE_DOMAIN, + SERVICE_INSTALL, + { + ATTR_ENTITY_ID: f"update.{mock_lamarzocco.serial_number}_machine_firmware", + }, + blocking=True, + )