Add Tasmota firmware update availability support#166910
Conversation
|
Hey there @emontnemery, mind taking a look at this pull request as it has been labeled with an integration ( Code owner commandsCode owners of
|
43ec52b to
5e7e299
Compare
There was a problem hiding this comment.
Pull request overview
Add firmware update availability reporting to the Tasmota integration by checking the latest GitHub release and exposing it via the update platform.
Changes:
- Add a new Tasmota
updateplatform entity intended to compare devicesw_versionvs latest GitHub release. - Introduce a
DataUpdateCoordinatorthat fetches the latest Tasmota GitHub release daily. - Add
aiogithubapias an integration requirement and wirePlatform.UPDATEinto Tasmota platforms.
Reviewed changes
Copilot reviewed 7 out of 7 changed files in this pull request and generated 7 comments.
Show a summary per file
| File | Description |
|---|---|
| requirements_test_all.txt | Notes that aiogithubapi is also used by homeassistant.components.tasmota. |
| requirements_all.txt | Notes that aiogithubapi is also used by homeassistant.components.tasmota. |
| homeassistant/components/tasmota/update.py | Adds the Tasmota firmware UpdateEntity and setup logic. |
| homeassistant/components/tasmota/manifest.json | Adds aiogithubapi dependency for Tasmota. |
| homeassistant/components/tasmota/coordinator.py | Adds coordinator to fetch the latest Tasmota GitHub release. |
| homeassistant/components/tasmota/const.py | Adds Platform.UPDATE to the forwarded platforms list. |
| coordinator = TasmotaLatestReleaseUpdateCoordinator(hass, config_entry) | ||
| await coordinator.async_config_entry_first_refresh() | ||
|
|
There was a problem hiding this comment.
Avoid failing Tasmota config entry setup when the GitHub check is temporarily unavailable by not using async_config_entry_first_refresh() here (it raises ConfigEntryNotReady on failure); instead do a non-blocking async_refresh() (or catch ConfigEntryNotReady) and let the entity report available=False until data is fetched.
| 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) |
There was a problem hiding this comment.
Ensure update entities are created for devices discovered after startup by subscribing to device registry updates (or another discovery signal) instead of only snapshotting devices once during setup.
| self.coordinator = coordinator | ||
| self.device_entry = device_entry | ||
| self._attr_unique_id = f"{device_entry.id}_update" | ||
|
|
There was a problem hiding this comment.
Make the entity react to coordinator updates by registering a listener (or inheriting from CoordinatorEntity) so the state changes when the latest release data refreshes.
| 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] |
There was a problem hiding this comment.
Don't hold on to a frozen DeviceEntry snapshot for installed_version; fetch the current device entry from the device registry by id (or subscribe to device-registry updates) so installed_version reflects updated sw_version values.
| 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] | |
| self._device_id = device_entry.id | |
| self._attr_unique_id = f"{device_entry.id}_update" | |
| @property | |
| def installed_version(self) -> str | None: | |
| """Return the installed version.""" | |
| device_registry = dr.async_get(self.hass) | |
| if not (device := device_registry.async_get(self._device_id)): | |
| return None | |
| return device.sw_version # type:ignore[union-attr] |
| 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 |
There was a problem hiding this comment.
Handle coordinator.data being unavailable by returning None for latest_version/release metadata and gating available on coordinator.last_update_success; otherwise attribute access like self.coordinator.data.tag_name will raise when the GitHub fetch fails.
| 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 | |
| def available(self) -> bool: | |
| """Return if entity is available.""" | |
| return bool(self.coordinator.last_update_success and self.coordinator.data) | |
| @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 | None: | |
| """Return the latest version.""" | |
| if not self.coordinator.data: | |
| return None | |
| return self.coordinator.data.tag_name.removeprefix("v") | |
| @property | |
| def release_url(self) -> str | None: | |
| """Return the release URL.""" | |
| if not self.coordinator.data: | |
| return None | |
| return self.coordinator.data.html_url | |
| @property | |
| def release_summary(self) -> str | None: | |
| """Return the release summary.""" | |
| if not self.coordinator.data: | |
| return None | |
| return self.coordinator.data.name | |
| def release_notes(self) -> str | None: | |
| """Return the release notes.""" | |
| if not self.coordinator.data or not self.coordinator.data.body: | |
| return None | |
| return re.sub( | |
| r"^<picture>.*?</picture>", | |
| "", | |
| self.coordinator.data.body, | |
| flags=re.DOTALL, |
| @property | ||
| def installed_version(self) -> str | None: | ||
| """Return the installed version.""" | ||
| return self.device_entry.sw_version # type:ignore[union-attr] |
There was a problem hiding this comment.
Remove the # type:ignore[union-attr] (and fix the type: ignore syntax if you keep one); DeviceEntry.sw_version already exists and is str | None, which matches this property’s return type.
| return self.device_entry.sw_version # type:ignore[union-attr] | |
| return self.device_entry.sw_version |
| 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) |
There was a problem hiding this comment.
Add test coverage for the new update platform (entity creation, availability when GitHub fetch fails, and version comparison) since the integration already has extensive tests under tests/components/tasmota/.
There was a problem hiding this comment.
This is very much broken/WIP, but already here for communicating some intent.
|
@emontnemery here's the current state of the work on this. I may have a bit of time to work more on this this week, and practically none at least two weeks after that. Do feel free to push here whatever you feel appropriate, if you have time and interest! |
Proposed change
Support displaying availability of Tasmota firmware updates. Use latest GitHub release as the latest ref.
Reopen of #159789
Type of change
Additional information
Checklist
ruff format homeassistant tests)If user exposed functionality or configuration variables are added/changed:
If the code communicates with devices, web services, or third-party tools:
Updated and included derived files by running:
python3 -m script.hassfest.requirements_all.txt.Updated by running
python3 -m script.gen_requirements_all.To help with the load of incoming pull requests: