Skip to content

Commit

Permalink
Add firmware update entity to IronOS integration (#123031)
Browse files Browse the repository at this point in the history
  • Loading branch information
tr4nt0r authored Oct 21, 2024
1 parent 1eaaa5c commit 3e8f3cf
Show file tree
Hide file tree
Showing 12 changed files with 315 additions and 21 deletions.
31 changes: 26 additions & 5 deletions homeassistant/components/iron_os/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,23 +2,35 @@

from __future__ import annotations

from dataclasses import dataclass
import logging
from typing import TYPE_CHECKING

from aiogithubapi import GitHubAPI
from pynecil import Pynecil

from homeassistant.components import bluetooth
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_NAME, Platform
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryNotReady
from homeassistant.helpers.aiohttp_client import async_get_clientsession

from .const import DOMAIN
from .coordinator import IronOSCoordinator
from .coordinator import IronOSFirmwareUpdateCoordinator, IronOSLiveDataCoordinator

PLATFORMS: list[Platform] = [Platform.NUMBER, Platform.SENSOR]
PLATFORMS: list[Platform] = [Platform.NUMBER, Platform.SENSOR, Platform.UPDATE]

type IronOSConfigEntry = ConfigEntry[IronOSCoordinator]

@dataclass
class IronOSCoordinators:
"""IronOS data class holding coordinators."""

live_data: IronOSLiveDataCoordinator
firmware: IronOSFirmwareUpdateCoordinator


type IronOSConfigEntry = ConfigEntry[IronOSCoordinators]

_LOGGER = logging.getLogger(__name__)

Expand All @@ -39,10 +51,19 @@ async def async_setup_entry(hass: HomeAssistant, entry: IronOSConfigEntry) -> bo

device = Pynecil(ble_device)

coordinator = IronOSCoordinator(hass, device)
coordinator = IronOSLiveDataCoordinator(hass, device)
await coordinator.async_config_entry_first_refresh()

entry.runtime_data = coordinator
session = async_get_clientsession(hass)
github = GitHubAPI(session=session)

firmware_update_coordinator = IronOSFirmwareUpdateCoordinator(hass, device, github)
await firmware_update_coordinator.async_config_entry_first_refresh()

entry.runtime_data = IronOSCoordinators(
live_data=coordinator,
firmware=firmware_update_coordinator,
)
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)

return True
Expand Down
55 changes: 46 additions & 9 deletions homeassistant/components/iron_os/coordinator.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@

from datetime import timedelta
import logging
from typing import TYPE_CHECKING

from aiogithubapi import GitHubAPI, GitHubException, GitHubReleaseModel
from pynecil import CommunicationError, DeviceInfoResponse, LiveDataResponse, Pynecil

from homeassistant.config_entries import ConfigEntry
Expand All @@ -16,24 +18,43 @@
_LOGGER = logging.getLogger(__name__)

SCAN_INTERVAL = timedelta(seconds=5)
SCAN_INTERVAL_GITHUB = timedelta(hours=3)


class IronOSCoordinator(DataUpdateCoordinator[LiveDataResponse]):
"""IronOS coordinator."""
class IronOSBaseCoordinator[_DataT](DataUpdateCoordinator[_DataT]):
"""IronOS base coordinator."""
device_info: DeviceInfoResponse
config_entry: ConfigEntry
def __init__(self, hass: HomeAssistant, device: Pynecil) -> None:
def __init__(
self,
hass: HomeAssistant,
device: Pynecil,
update_interval: timedelta,
) -> None:
"""Initialize IronOS coordinator."""
super().__init__(
hass,
_LOGGER,
name=DOMAIN,
update_interval=SCAN_INTERVAL,
update_interval=update_interval,
)
self.device = device
async def _async_setup(self) -> None:
"""Set up the coordinator."""
self.device_info = await self.device.get_device_info()
class IronOSLiveDataCoordinator(IronOSBaseCoordinator):
"""IronOS live data coordinator."""
def __init__(self, hass: HomeAssistant, device: Pynecil) -> None:
"""Initialize IronOS coordinator."""
super().__init__(hass, device=device, update_interval=SCAN_INTERVAL)
async def _async_update_data(self) -> LiveDataResponse:
"""Fetch data from Device."""
Expand All @@ -43,11 +64,27 @@ async def _async_update_data(self) -> LiveDataResponse:
except CommunicationError as e:
raise UpdateFailed("Cannot connect to device") from e
async def _async_setup(self) -> None:
"""Set up the coordinator."""
class IronOSFirmwareUpdateCoordinator(IronOSBaseCoordinator):
"""IronOS coordinator for retrieving update information from github."""
def __init__(self, hass: HomeAssistant, device: Pynecil, github: GitHubAPI) -> None:
"""Initialize IronOS coordinator."""
super().__init__(hass, device=device, update_interval=SCAN_INTERVAL_GITHUB)
self.github = github
async def _async_update_data(self) -> GitHubReleaseModel:
"""Fetch data from Github."""

try:
self.device_info = await self.device.get_device_info()
release = await self.github.repos.releases.latest("Ralim/IronOS")

except CommunicationError as e:
raise UpdateFailed("Cannot connect to device") from e
except GitHubException as e:
raise UpdateFailed(
"Failed to retrieve latest release data from Github"
) from e

if TYPE_CHECKING:
assert release.data

return release.data
6 changes: 3 additions & 3 deletions homeassistant/components/iron_os/entity.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,17 +9,17 @@
from homeassistant.helpers.update_coordinator import CoordinatorEntity

from .const import MANUFACTURER, MODEL
from .coordinator import IronOSCoordinator
from .coordinator import IronOSBaseCoordinator


class IronOSBaseEntity(CoordinatorEntity[IronOSCoordinator]):
class IronOSBaseEntity(CoordinatorEntity[IronOSBaseCoordinator]):
"""Base IronOS entity."""

_attr_has_entity_name = True

def __init__(
self,
coordinator: IronOSCoordinator,
coordinator: IronOSBaseCoordinator,
entity_description: EntityDescription,
) -> None:
"""Initialize the sensor."""
Expand Down
4 changes: 2 additions & 2 deletions homeassistant/components/iron_os/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,6 @@
"dependencies": ["bluetooth_adapters"],
"documentation": "https://www.home-assistant.io/integrations/iron_os",
"iot_class": "local_polling",
"loggers": ["pynecil"],
"requirements": ["pynecil==0.2.0"]
"loggers": ["pynecil", "aiogithubapi"],
"requirements": ["pynecil==0.2.0", "aiogithubapi==24.6.0"]
}
2 changes: 1 addition & 1 deletion homeassistant/components/iron_os/number.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ async def async_setup_entry(
async_add_entities: AddEntitiesCallback,
) -> None:
"""Set up number entities from a config entry."""
coordinator = entry.runtime_data
coordinator = entry.runtime_data.live_data

async_add_entities(
IronOSNumberEntity(coordinator, description)
Expand Down
2 changes: 1 addition & 1 deletion homeassistant/components/iron_os/sensor.py
Original file line number Diff line number Diff line change
Expand Up @@ -180,7 +180,7 @@ async def async_setup_entry(
async_add_entities: AddEntitiesCallback,
) -> None:
"""Set up sensors from a config entry."""
coordinator = entry.runtime_data
coordinator = entry.runtime_data.live_data

async_add_entities(
IronOSSensorEntity(coordinator, description)
Expand Down
76 changes: 76 additions & 0 deletions homeassistant/components/iron_os/update.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
"""Update platform for IronOS integration."""

from __future__ import annotations

from homeassistant.components.update import (
UpdateDeviceClass,
UpdateEntity,
UpdateEntityDescription,
UpdateEntityFeature,
)
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback

from . import IronOSConfigEntry
from .coordinator import IronOSBaseCoordinator
from .entity import IronOSBaseEntity

UPDATE_DESCRIPTION = UpdateEntityDescription(
key="firmware",
device_class=UpdateDeviceClass.FIRMWARE,
)


async def async_setup_entry(
hass: HomeAssistant,
entry: IronOSConfigEntry,
async_add_entities: AddEntitiesCallback,
) -> None:
"""Set up IronOS update platform."""

coordinator = entry.runtime_data.firmware

async_add_entities([IronOSUpdate(coordinator, UPDATE_DESCRIPTION)])


class IronOSUpdate(IronOSBaseEntity, UpdateEntity):
"""Representation of an IronOS update entity."""

_attr_supported_features = UpdateEntityFeature.RELEASE_NOTES

def __init__(
self,
coordinator: IronOSBaseCoordinator,
entity_description: UpdateEntityDescription,
) -> None:
"""Initialize the sensor."""
super().__init__(coordinator, entity_description)

@property
def installed_version(self) -> str | None:
"""IronOS version on the device."""

return self.coordinator.device_info.build

@property
def title(self) -> str | None:
"""Title of the IronOS release."""

return f"IronOS {self.coordinator.data.name}"

@property
def release_url(self) -> str | None:
"""URL to the full release notes of the latest IronOS version available."""

return self.coordinator.data.html_url

@property
def latest_version(self) -> str | None:
"""Latest IronOS version available for install."""

return self.coordinator.data.tag_name

async def async_release_notes(self) -> str | None:
"""Return the release notes."""

return self.coordinator.data.body
1 change: 1 addition & 0 deletions requirements_all.txt
Original file line number Diff line number Diff line change
Expand Up @@ -249,6 +249,7 @@ aioflo==2021.11.0
aioftp==0.21.3

# homeassistant.components.github
# homeassistant.components.iron_os
aiogithubapi==24.6.0

# homeassistant.components.guardian
Expand Down
1 change: 1 addition & 0 deletions requirements_test_all.txt
Original file line number Diff line number Diff line change
Expand Up @@ -234,6 +234,7 @@ aioesphomeapi==27.0.0
aioflo==2021.11.0

# homeassistant.components.github
# homeassistant.components.iron_os
aiogithubapi==24.6.0

# homeassistant.components.guardian
Expand Down
23 changes: 23 additions & 0 deletions tests/components/iron_os/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,29 @@ def mock_ble_device() -> Generator[MagicMock]:
yield ble_device


@pytest.fixture(autouse=True)
def mock_githubapi() -> Generator[AsyncMock]:
"""Mock aiogithubapi."""

with patch(
"homeassistant.components.iron_os.GitHubAPI",
autospec=True,
) as mock_client:
client = mock_client.return_value
client.repos.releases.latest = AsyncMock()

client.repos.releases.latest.return_value.data.html_url = (
"https://github.com/Ralim/IronOS/releases/tag/v2.22"
)
client.repos.releases.latest.return_value.data.name = (
"V2.22 | TS101 & S60 Added | PinecilV2 improved"
)
client.repos.releases.latest.return_value.data.tag_name = "v2.22"
client.repos.releases.latest.return_value.data.body = "**RELEASE_NOTES**"

yield client


@pytest.fixture
def mock_pynecil() -> Generator[AsyncMock]:
"""Mock Pynecil library."""
Expand Down
62 changes: 62 additions & 0 deletions tests/components/iron_os/snapshots/test_update.ambr
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
# serializer version: 1
# name: test_update.2
'**RELEASE_NOTES**'
# ---
# name: test_update[update.pinecil_firmware-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': None,
'config_entry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'update',
'entity_category': <EntityCategory.CONFIG: 'config'>,
'entity_id': 'update.pinecil_firmware',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
'original_device_class': <UpdateDeviceClass.FIRMWARE: 'firmware'>,
'original_icon': None,
'original_name': 'Firmware',
'platform': 'iron_os',
'previous_unique_id': None,
'supported_features': <UpdateEntityFeature: 16>,
'translation_key': None,
'unique_id': 'c0:ff:ee:c0:ff:ee_firmware',
'unit_of_measurement': None,
})
# ---
# name: test_update[update.pinecil_firmware-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'auto_update': False,
'device_class': 'firmware',
'entity_picture': 'https://brands.home-assistant.io/_/iron_os/icon.png',
'friendly_name': 'Pinecil Firmware',
'in_progress': False,
'installed_version': 'v2.22',
'latest_version': 'v2.22',
'release_summary': None,
'release_url': 'https://github.com/Ralim/IronOS/releases/tag/v2.22',
'skipped_version': None,
'supported_features': <UpdateEntityFeature: 16>,
'title': 'IronOS V2.22 | TS101 & S60 Added | PinecilV2 improved',
'update_percentage': None,
}),
'context': <ANY>,
'entity_id': 'update.pinecil_firmware',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'off',
})
# ---
Loading

0 comments on commit 3e8f3cf

Please sign in to comment.