Compare commits

...

1 Commits

Author SHA1 Message Date
Ville Skyttä
5e7e299876 Add Tasmota firmware update availability support 2026-03-30 23:08:38 +03:00
7 changed files with 186 additions and 1 deletions

View File

@@ -19,6 +19,7 @@ PLATFORMS = [
Platform.LIGHT,
Platform.SENSOR,
Platform.SWITCH,
Platform.UPDATE,
]
TASMOTA_EVENT = "tasmota_event"

View File

@@ -0,0 +1,38 @@
"""Data update coordinators for Tasmota."""
from datetime import timedelta
import logging
from aiogithubapi import GitHubAPI, GitHubRatelimitException, GitHubReleaseModel
from aiogithubapi.client import GitHubConnectionException
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
class TasmotaLatestReleaseUpdateCoordinator(DataUpdateCoordinator[GitHubReleaseModel]):
"""Data update coordinator for Tasmota latest release info."""
def __init__(self, hass: HomeAssistant, config_entry: ConfigEntry) -> None:
"""Initialize the coordinator."""
self.client = GitHubAPI(session=async_get_clientsession(hass))
super().__init__(
hass,
logger=logging.getLogger(__name__),
config_entry=config_entry,
name="Tasmota latest release",
update_interval=timedelta(days=1),
)
async def _async_update_data(self) -> GitHubReleaseModel:
"""Get new data."""
try:
response = await self.client.repos.releases.latest("arendst/Tasmota")
if response.data is None:
raise UpdateFailed("No data received")
except (GitHubConnectionException, GitHubRatelimitException) as ex:
raise UpdateFailed(ex) from ex
else:
return response.data

View File

@@ -8,5 +8,5 @@
"iot_class": "local_push",
"loggers": ["hatasmota"],
"mqtt": ["tasmota/discovery/#"],
"requirements": ["HATasmota==0.10.1"]
"requirements": ["HATasmota==0.10.1", "aiogithubapi==26.0.0"]
}

View File

@@ -0,0 +1,79 @@
"""Update entity for Tasmota."""
import re
from homeassistant.components.update import (
UpdateDeviceClass,
UpdateEntity,
UpdateEntityFeature,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.helpers import device_registry as dr
from homeassistant.helpers.device_registry import DeviceEntry
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from .coordinator import TasmotaLatestReleaseUpdateCoordinator
async def async_setup_entry(
hass: HomeAssistant,
config_entry: ConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up Tasmota update entities."""
coordinator = TasmotaLatestReleaseUpdateCoordinator(hass, config_entry)
await coordinator.async_config_entry_first_refresh()
device_registry = dr.async_get(hass)
devices = device_registry.devices.get_devices_for_config_entry_id(
config_entry.entry_id
)
async_add_entities(TasmotaUpdateEntity(coordinator, device) for device in devices)
class TasmotaUpdateEntity(UpdateEntity):
"""Representation of a Tasmota update entity."""
_attr_device_class = UpdateDeviceClass.FIRMWARE
_attr_name = "Firmware"
_attr_title = "Tasmota firmware"
_attr_supported_features = UpdateEntityFeature.RELEASE_NOTES
def __init__(
self,
coordinator: TasmotaLatestReleaseUpdateCoordinator,
device_entry: DeviceEntry,
) -> None:
"""Initialize the Tasmota update entity."""
self.coordinator = coordinator
self.device_entry = device_entry
self._attr_unique_id = f"{device_entry.id}_update"
@property
def installed_version(self) -> str | None:
"""Return the installed version."""
return self.device_entry.sw_version # type:ignore[union-attr]
@property
def latest_version(self) -> str:
"""Return the latest version."""
return self.coordinator.data.tag_name.removeprefix("v")
@property
def release_url(self) -> str:
"""Return the release URL."""
return self.coordinator.data.html_url
@property
def release_summary(self) -> str:
"""Return the release summary."""
return self.coordinator.data.name
def release_notes(self) -> str | None:
"""Return the release notes."""
if not self.coordinator.data.body:
return None
return re.sub(
r"^<picture>.*?</picture>", "", self.coordinator.data.body, flags=re.DOTALL
)

1
requirements_all.txt generated
View File

@@ -267,6 +267,7 @@ aioftp==0.21.3
aioghost==0.4.0
# homeassistant.components.github
# homeassistant.components.tasmota
aiogithubapi==26.0.0
# homeassistant.components.guardian

View File

@@ -255,6 +255,7 @@ aioflo==2021.11.0
aioghost==0.4.0
# homeassistant.components.github
# homeassistant.components.tasmota
aiogithubapi==26.0.0
# homeassistant.components.guardian

View File

@@ -0,0 +1,65 @@
"""Tests for the Tasmota update platform."""
import copy
import json
from aiogithubapi import GitHubReleaseModel
import pytest
from homeassistant.components.tasmota.const import DEFAULT_PREFIX
from homeassistant.core import HomeAssistant
from homeassistant.helpers import device_registry as dr
from .test_common import DEFAULT_CONFIG
from tests.common import async_fire_mqtt_message
from tests.typing import MqttMockHAClient
@pytest.mark.parametrize(
("candidate_version", "update_available"),
[
("0.0.0", False),
(".".join(str(int(x) + 1) for x in DEFAULT_CONFIG["sw"].split(".")), True),
],
)
async def test_update_state(
hass: HomeAssistant,
mqtt_mock: MqttMockHAClient,
device_registry: dr.DeviceRegistry,
setup_tasmota,
candidate_version: str,
update_available: bool,
) -> None:
"""Test setting up a device."""
config = copy.deepcopy(DEFAULT_CONFIG)
mac = config["mac"]
async_fire_mqtt_message(
hass,
f"{DEFAULT_PREFIX}/{mac}/config",
json.dumps(config),
)
await hass.async_block_till_done()
device_entry = device_registry.async_get_device(
connections={(dr.CONNECTION_NETWORK_MAC, mac)}
)
# TODO mock coordinator.client.repos.releases.latest("arendst/Tasmota") to return this
data = GitHubReleaseModel(
tag_name=f"v{candidate_version}",
name=f"Tasmota v{candidate_version} Foo",
html_url=f"https://github.com/arendst/Tasmota/releases/tag/v{candidate_version}",
body="""\
<picture>
<source media="(prefers-color-scheme: dark)" srcset="./tools/logo/TASMOTA_FullLogo_Vector_White.svg">
<img alt="Logo" src="./tools/logo/TASMOTA_FullLogo_Vector.svg" align="right" height="76">
</picture>
# RELEASE NOTES
... """,
)
# TODO update_available test, device_entry.sw_version has the current version