mirror of
https://github.com/home-assistant/core.git
synced 2025-07-23 21:27:38 +00:00
Add esphome native device update entities (#119339)
Co-authored-by: J. Nick Koston <nick@koston.org>
This commit is contained in:
parent
faf2a447a4
commit
f8711dbfbf
@ -38,6 +38,7 @@ from aioesphomeapi import (
|
|||||||
TextInfo,
|
TextInfo,
|
||||||
TextSensorInfo,
|
TextSensorInfo,
|
||||||
TimeInfo,
|
TimeInfo,
|
||||||
|
UpdateInfo,
|
||||||
UserService,
|
UserService,
|
||||||
ValveInfo,
|
ValveInfo,
|
||||||
build_unique_id,
|
build_unique_id,
|
||||||
@ -82,6 +83,7 @@ INFO_TYPE_TO_PLATFORM: dict[type[EntityInfo], Platform] = {
|
|||||||
TextInfo: Platform.TEXT,
|
TextInfo: Platform.TEXT,
|
||||||
TextSensorInfo: Platform.SENSOR,
|
TextSensorInfo: Platform.SENSOR,
|
||||||
TimeInfo: Platform.TIME,
|
TimeInfo: Platform.TIME,
|
||||||
|
UpdateInfo: Platform.UPDATE,
|
||||||
ValveInfo: Platform.VALVE,
|
ValveInfo: Platform.VALVE,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -5,7 +5,12 @@ from __future__ import annotations
|
|||||||
import asyncio
|
import asyncio
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
from aioesphomeapi import DeviceInfo as ESPHomeDeviceInfo, EntityInfo
|
from aioesphomeapi import (
|
||||||
|
DeviceInfo as ESPHomeDeviceInfo,
|
||||||
|
EntityInfo,
|
||||||
|
UpdateInfo,
|
||||||
|
UpdateState,
|
||||||
|
)
|
||||||
|
|
||||||
from homeassistant.components.update import (
|
from homeassistant.components.update import (
|
||||||
UpdateDeviceClass,
|
UpdateDeviceClass,
|
||||||
@ -19,10 +24,17 @@ from homeassistant.helpers import device_registry as dr
|
|||||||
from homeassistant.helpers.device_registry import DeviceInfo
|
from homeassistant.helpers.device_registry import DeviceInfo
|
||||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||||
|
from homeassistant.util.enum import try_parse_enum
|
||||||
|
|
||||||
from .coordinator import ESPHomeDashboardCoordinator
|
from .coordinator import ESPHomeDashboardCoordinator
|
||||||
from .dashboard import async_get_dashboard
|
from .dashboard import async_get_dashboard
|
||||||
from .domain_data import DomainData
|
from .domain_data import DomainData
|
||||||
|
from .entity import (
|
||||||
|
EsphomeEntity,
|
||||||
|
convert_api_error_ha_error,
|
||||||
|
esphome_state_property,
|
||||||
|
platform_async_setup_entry,
|
||||||
|
)
|
||||||
from .entry_data import RuntimeEntryData
|
from .entry_data import RuntimeEntryData
|
||||||
|
|
||||||
KEY_UPDATE_LOCK = "esphome_update_lock"
|
KEY_UPDATE_LOCK = "esphome_update_lock"
|
||||||
@ -36,6 +48,15 @@ async def async_setup_entry(
|
|||||||
async_add_entities: AddEntitiesCallback,
|
async_add_entities: AddEntitiesCallback,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Set up ESPHome update based on a config entry."""
|
"""Set up ESPHome update based on a config entry."""
|
||||||
|
await platform_async_setup_entry(
|
||||||
|
hass,
|
||||||
|
entry,
|
||||||
|
async_add_entities,
|
||||||
|
info_type=UpdateInfo,
|
||||||
|
entity_type=ESPHomeUpdateEntity,
|
||||||
|
state_type=UpdateState,
|
||||||
|
)
|
||||||
|
|
||||||
if (dashboard := async_get_dashboard(hass)) is None:
|
if (dashboard := async_get_dashboard(hass)) is None:
|
||||||
return
|
return
|
||||||
entry_data = DomainData.get(hass).get_entry_data(entry)
|
entry_data = DomainData.get(hass).get_entry_data(entry)
|
||||||
@ -54,7 +75,7 @@ async def async_setup_entry(
|
|||||||
unsub()
|
unsub()
|
||||||
unsubs.clear()
|
unsubs.clear()
|
||||||
|
|
||||||
async_add_entities([ESPHomeUpdateEntity(entry_data, dashboard)])
|
async_add_entities([ESPHomeDashboardUpdateEntity(entry_data, dashboard)])
|
||||||
|
|
||||||
if entry_data.available and dashboard.last_update_success:
|
if entry_data.available and dashboard.last_update_success:
|
||||||
_async_setup_update_entity()
|
_async_setup_update_entity()
|
||||||
@ -66,7 +87,9 @@ async def async_setup_entry(
|
|||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
class ESPHomeUpdateEntity(CoordinatorEntity[ESPHomeDashboardCoordinator], UpdateEntity):
|
class ESPHomeDashboardUpdateEntity(
|
||||||
|
CoordinatorEntity[ESPHomeDashboardCoordinator], UpdateEntity
|
||||||
|
):
|
||||||
"""Defines an ESPHome update entity."""
|
"""Defines an ESPHome update entity."""
|
||||||
|
|
||||||
_attr_has_entity_name = True
|
_attr_has_entity_name = True
|
||||||
@ -179,3 +202,65 @@ class ESPHomeUpdateEntity(CoordinatorEntity[ESPHomeDashboardCoordinator], Update
|
|||||||
)
|
)
|
||||||
finally:
|
finally:
|
||||||
await self.coordinator.async_request_refresh()
|
await self.coordinator.async_request_refresh()
|
||||||
|
|
||||||
|
|
||||||
|
class ESPHomeUpdateEntity(EsphomeEntity[UpdateInfo, UpdateState], UpdateEntity):
|
||||||
|
"""A update implementation for esphome."""
|
||||||
|
|
||||||
|
_attr_supported_features = (
|
||||||
|
UpdateEntityFeature.INSTALL | UpdateEntityFeature.PROGRESS
|
||||||
|
)
|
||||||
|
|
||||||
|
@callback
|
||||||
|
def _on_static_info_update(self, static_info: EntityInfo) -> None:
|
||||||
|
"""Set attrs from static info."""
|
||||||
|
super()._on_static_info_update(static_info)
|
||||||
|
static_info = self._static_info
|
||||||
|
self._attr_device_class = try_parse_enum(
|
||||||
|
UpdateDeviceClass, static_info.device_class
|
||||||
|
)
|
||||||
|
|
||||||
|
@property
|
||||||
|
@esphome_state_property
|
||||||
|
def installed_version(self) -> str | None:
|
||||||
|
"""Return the installed version."""
|
||||||
|
return self._state.current_version
|
||||||
|
|
||||||
|
@property
|
||||||
|
@esphome_state_property
|
||||||
|
def in_progress(self) -> bool | int | None:
|
||||||
|
"""Return if the update is in progress."""
|
||||||
|
if self._state.has_progress:
|
||||||
|
return int(self._state.progress)
|
||||||
|
return self._state.in_progress
|
||||||
|
|
||||||
|
@property
|
||||||
|
@esphome_state_property
|
||||||
|
def latest_version(self) -> str | None:
|
||||||
|
"""Return the latest version."""
|
||||||
|
return self._state.latest_version
|
||||||
|
|
||||||
|
@property
|
||||||
|
@esphome_state_property
|
||||||
|
def release_summary(self) -> str | None:
|
||||||
|
"""Return the release summary."""
|
||||||
|
return self._state.release_summary
|
||||||
|
|
||||||
|
@property
|
||||||
|
@esphome_state_property
|
||||||
|
def release_url(self) -> str | None:
|
||||||
|
"""Return the release URL."""
|
||||||
|
return self._state.release_url
|
||||||
|
|
||||||
|
@property
|
||||||
|
@esphome_state_property
|
||||||
|
def title(self) -> str | None:
|
||||||
|
"""Return the title of the update."""
|
||||||
|
return self._state.title
|
||||||
|
|
||||||
|
@convert_api_error_ha_error
|
||||||
|
async def async_install(
|
||||||
|
self, version: str | None, backup: bool, **kwargs: Any
|
||||||
|
) -> None:
|
||||||
|
"""Update the current value."""
|
||||||
|
self._client.update_command(key=self._key, install=True)
|
||||||
|
@ -3,12 +3,21 @@
|
|||||||
from collections.abc import Awaitable, Callable
|
from collections.abc import Awaitable, Callable
|
||||||
from unittest.mock import Mock, patch
|
from unittest.mock import Mock, patch
|
||||||
|
|
||||||
from aioesphomeapi import APIClient, EntityInfo, EntityState, UserService
|
from aioesphomeapi import (
|
||||||
|
APIClient,
|
||||||
|
EntityInfo,
|
||||||
|
EntityState,
|
||||||
|
UpdateInfo,
|
||||||
|
UpdateState,
|
||||||
|
UserService,
|
||||||
|
)
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from homeassistant.components.esphome.dashboard import async_get_dashboard
|
from homeassistant.components.esphome.dashboard import async_get_dashboard
|
||||||
from homeassistant.components.update import UpdateEntityFeature
|
from homeassistant.components.update import DOMAIN as UPDATE_DOMAIN, UpdateEntityFeature
|
||||||
|
from homeassistant.components.update.const import SERVICE_INSTALL
|
||||||
from homeassistant.const import (
|
from homeassistant.const import (
|
||||||
|
ATTR_ENTITY_ID,
|
||||||
ATTR_SUPPORTED_FEATURES,
|
ATTR_SUPPORTED_FEATURES,
|
||||||
STATE_OFF,
|
STATE_OFF,
|
||||||
STATE_ON,
|
STATE_ON,
|
||||||
@ -83,7 +92,7 @@ async def test_update_entity(
|
|||||||
|
|
||||||
with patch(
|
with patch(
|
||||||
"homeassistant.components.esphome.update.DomainData.get_entry_data",
|
"homeassistant.components.esphome.update.DomainData.get_entry_data",
|
||||||
return_value=Mock(available=True, device_info=mock_device_info),
|
return_value=Mock(available=True, device_info=mock_device_info, info={}),
|
||||||
):
|
):
|
||||||
assert await hass.config_entries.async_setup(mock_config_entry.entry_id)
|
assert await hass.config_entries.async_setup(mock_config_entry.entry_id)
|
||||||
await hass.async_block_till_done()
|
await hass.async_block_till_done()
|
||||||
@ -266,7 +275,7 @@ async def test_update_entity_dashboard_not_available_startup(
|
|||||||
with (
|
with (
|
||||||
patch(
|
patch(
|
||||||
"homeassistant.components.esphome.update.DomainData.get_entry_data",
|
"homeassistant.components.esphome.update.DomainData.get_entry_data",
|
||||||
return_value=Mock(available=True, device_info=mock_device_info),
|
return_value=Mock(available=True, device_info=mock_device_info, info={}),
|
||||||
),
|
),
|
||||||
patch(
|
patch(
|
||||||
"esphome_dashboard_api.ESPHomeDashboardAPI.get_devices",
|
"esphome_dashboard_api.ESPHomeDashboardAPI.get_devices",
|
||||||
@ -358,7 +367,7 @@ async def test_update_entity_not_present_without_dashboard(
|
|||||||
"""Test ESPHome update entity does not get created if there is no dashboard."""
|
"""Test ESPHome update entity does not get created if there is no dashboard."""
|
||||||
with patch(
|
with patch(
|
||||||
"homeassistant.components.esphome.update.DomainData.get_entry_data",
|
"homeassistant.components.esphome.update.DomainData.get_entry_data",
|
||||||
return_value=Mock(available=True, device_info=mock_device_info),
|
return_value=Mock(available=True, device_info=mock_device_info, info={}),
|
||||||
):
|
):
|
||||||
assert await hass.config_entries.async_setup(mock_config_entry.entry_id)
|
assert await hass.config_entries.async_setup(mock_config_entry.entry_id)
|
||||||
await hass.async_block_till_done()
|
await hass.async_block_till_done()
|
||||||
@ -408,3 +417,104 @@ async def test_update_becomes_available_at_runtime(
|
|||||||
# We now know the version so install is enabled
|
# We now know the version so install is enabled
|
||||||
features = state.attributes[ATTR_SUPPORTED_FEATURES]
|
features = state.attributes[ATTR_SUPPORTED_FEATURES]
|
||||||
assert features is UpdateEntityFeature.INSTALL
|
assert features is UpdateEntityFeature.INSTALL
|
||||||
|
|
||||||
|
|
||||||
|
async def test_generic_device_update_entity(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
mock_client: APIClient,
|
||||||
|
mock_generic_device_entry,
|
||||||
|
) -> None:
|
||||||
|
"""Test a generic device update entity."""
|
||||||
|
entity_info = [
|
||||||
|
UpdateInfo(
|
||||||
|
object_id="myupdate",
|
||||||
|
key=1,
|
||||||
|
name="my update",
|
||||||
|
unique_id="my_update",
|
||||||
|
)
|
||||||
|
]
|
||||||
|
states = [
|
||||||
|
UpdateState(
|
||||||
|
key=1,
|
||||||
|
current_version="2024.6.0",
|
||||||
|
latest_version="2024.6.0",
|
||||||
|
title="ESPHome Project",
|
||||||
|
release_summary="This is a release summary",
|
||||||
|
release_url="https://esphome.io/changelog",
|
||||||
|
)
|
||||||
|
]
|
||||||
|
user_service = []
|
||||||
|
await mock_generic_device_entry(
|
||||||
|
mock_client=mock_client,
|
||||||
|
entity_info=entity_info,
|
||||||
|
user_service=user_service,
|
||||||
|
states=states,
|
||||||
|
)
|
||||||
|
state = hass.states.get("update.test_myupdate")
|
||||||
|
assert state is not None
|
||||||
|
assert state.state == STATE_OFF
|
||||||
|
|
||||||
|
|
||||||
|
async def test_generic_device_update_entity_has_update(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
mock_client: APIClient,
|
||||||
|
mock_esphome_device: Callable[
|
||||||
|
[APIClient, list[EntityInfo], list[UserService], list[EntityState]],
|
||||||
|
Awaitable[MockESPHomeDevice],
|
||||||
|
],
|
||||||
|
) -> None:
|
||||||
|
"""Test a generic device update entity with an update."""
|
||||||
|
entity_info = [
|
||||||
|
UpdateInfo(
|
||||||
|
object_id="myupdate",
|
||||||
|
key=1,
|
||||||
|
name="my update",
|
||||||
|
unique_id="my_update",
|
||||||
|
)
|
||||||
|
]
|
||||||
|
states = [
|
||||||
|
UpdateState(
|
||||||
|
key=1,
|
||||||
|
current_version="2024.6.0",
|
||||||
|
latest_version="2024.6.1",
|
||||||
|
title="ESPHome Project",
|
||||||
|
release_summary="This is a release summary",
|
||||||
|
release_url="https://esphome.io/changelog",
|
||||||
|
)
|
||||||
|
]
|
||||||
|
user_service = []
|
||||||
|
mock_device: MockESPHomeDevice = await mock_esphome_device(
|
||||||
|
mock_client=mock_client,
|
||||||
|
entity_info=entity_info,
|
||||||
|
user_service=user_service,
|
||||||
|
states=states,
|
||||||
|
)
|
||||||
|
state = hass.states.get("update.test_myupdate")
|
||||||
|
assert state is not None
|
||||||
|
assert state.state == STATE_ON
|
||||||
|
|
||||||
|
await hass.services.async_call(
|
||||||
|
UPDATE_DOMAIN,
|
||||||
|
SERVICE_INSTALL,
|
||||||
|
{ATTR_ENTITY_ID: "update.test_myupdate"},
|
||||||
|
blocking=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
mock_device.set_state(
|
||||||
|
UpdateState(
|
||||||
|
key=1,
|
||||||
|
in_progress=True,
|
||||||
|
has_progress=True,
|
||||||
|
progress=50,
|
||||||
|
current_version="2024.6.0",
|
||||||
|
latest_version="2024.6.1",
|
||||||
|
title="ESPHome Project",
|
||||||
|
release_summary="This is a release summary",
|
||||||
|
release_url="https://esphome.io/changelog",
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
state = hass.states.get("update.test_myupdate")
|
||||||
|
assert state is not None
|
||||||
|
assert state.state == STATE_ON
|
||||||
|
assert state.attributes["in_progress"] == 50
|
||||||
|
Loading…
x
Reference in New Issue
Block a user