Add config flow to version integration (#54642)

This commit is contained in:
Joakim Sørensen 2021-12-23 21:04:58 +01:00 committed by GitHub
parent 0ec2978698
commit 13e3ca6ab1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 1010 additions and 243 deletions

View File

@ -1 +1,46 @@
"""The version integration."""
"""The Version integration."""
from __future__ import annotations
from pyhaversion import HaVersion
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from .const import (
BOARD_MAP,
CONF_BOARD,
CONF_CHANNEL,
CONF_IMAGE,
CONF_SOURCE,
DOMAIN,
PLATFORMS,
)
from .coordinator import VersionDataUpdateCoordinator
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up the version integration from a config entry."""
coordinator = VersionDataUpdateCoordinator(
hass=hass,
api=HaVersion(
session=async_get_clientsession(hass),
source=entry.data[CONF_SOURCE],
image=entry.data[CONF_IMAGE],
board=BOARD_MAP[entry.data[CONF_BOARD]],
channel=entry.data[CONF_CHANNEL].lower(),
),
)
await coordinator.async_config_entry_first_refresh()
hass.data.setdefault(DOMAIN, {})[entry.entry_id] = coordinator
hass.config_entries.async_setup_platforms(entry, PLATFORMS)
return True
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Unload the config entry."""
if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS):
hass.data[DOMAIN].pop(entry.entry_id)
return unload_ok

View File

@ -0,0 +1,202 @@
"""Config flow for Version integration."""
from __future__ import annotations
from typing import Any
from pyhaversion.consts import HaVersionChannel, HaVersionSource
import voluptuous as vol
from homeassistant import config_entries
from homeassistant.const import CONF_NAME, CONF_SOURCE
from homeassistant.data_entry_flow import FlowResult
from homeassistant.util import slugify
from .const import (
ATTR_VERSION_SOURCE,
CONF_BETA,
CONF_BOARD,
CONF_CHANNEL,
CONF_IMAGE,
CONF_VERSION_SOURCE,
DEFAULT_BOARD,
DEFAULT_CHANNEL,
DEFAULT_CONFIGURATION,
DEFAULT_IMAGE,
DEFAULT_NAME,
DEFAULT_NAME_CURRENT,
DEFAULT_NAME_LATEST,
DEFAULT_SOURCE,
DOMAIN,
POSTFIX_CONTAINER_NAME,
SOURCE_DOKCER,
SOURCE_HASSIO,
STEP_USER,
STEP_VERSION_SOURCE,
VALID_BOARDS,
VALID_CHANNELS,
VALID_CONTAINER_IMAGES,
VALID_IMAGES,
VERSION_SOURCE_DOCKER_HUB,
VERSION_SOURCE_LOCAL,
VERSION_SOURCE_MAP,
VERSION_SOURCE_MAP_INVERTED,
VERSION_SOURCE_VERSIONS,
)
class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
"""Handle a config flow for Version."""
_entry_data: dict[str, Any] = DEFAULT_CONFIGURATION.copy()
VERSION = 1
async def async_step_user(
self,
user_input: dict[str, Any] | None = None,
) -> FlowResult:
"""Handle the initial user step."""
if user_input is None:
self._entry_data = DEFAULT_CONFIGURATION.copy()
return self.async_show_form(
step_id=STEP_USER,
data_schema=vol.Schema(
{
vol.Required(
CONF_VERSION_SOURCE,
default=VERSION_SOURCE_LOCAL,
): vol.In(VERSION_SOURCE_MAP.keys())
}
),
)
user_input[CONF_SOURCE] = VERSION_SOURCE_MAP[user_input[CONF_VERSION_SOURCE]]
self._entry_data.update(user_input)
if not self.show_advanced_options or user_input[CONF_SOURCE] in (
HaVersionSource.LOCAL,
HaVersionSource.HAIO,
):
return self.async_create_entry(
title=self._config_entry_name,
data=self._entry_data,
)
return await self.async_step_version_source()
async def async_step_version_source(
self,
user_input: dict[str, Any] | None = None,
) -> FlowResult:
"""Handle the version_source step."""
if user_input is None:
if self._entry_data[CONF_SOURCE] in (
HaVersionSource.SUPERVISOR,
HaVersionSource.CONTAINER,
):
data_schema = vol.Schema(
{
vol.Required(
CONF_CHANNEL, default=DEFAULT_CHANNEL.title()
): vol.In(VALID_CHANNELS),
}
)
if self._entry_data[CONF_SOURCE] == HaVersionSource.SUPERVISOR:
data_schema = data_schema.extend(
{
vol.Required(CONF_IMAGE, default=DEFAULT_IMAGE): vol.In(
VALID_IMAGES
),
vol.Required(CONF_BOARD, default=DEFAULT_BOARD): vol.In(
VALID_BOARDS
),
}
)
else:
data_schema = data_schema.extend(
{
vol.Required(CONF_IMAGE, default=DEFAULT_IMAGE): vol.In(
VALID_CONTAINER_IMAGES
)
}
)
else:
data_schema = vol.Schema({vol.Required(CONF_BETA, default=False): bool})
return self.async_show_form(
step_id=STEP_VERSION_SOURCE,
data_schema=data_schema,
description_placeholders={
ATTR_VERSION_SOURCE: self._entry_data[CONF_VERSION_SOURCE]
},
)
self._entry_data.update(user_input)
self._entry_data[CONF_CHANNEL] = self._entry_data[CONF_CHANNEL].lower()
return self.async_create_entry(
title=self._config_entry_name, data=self._entry_data
)
async def async_step_import(self, import_config: dict[str, Any]) -> FlowResult:
"""Import a config entry from configuration.yaml."""
self._entry_data = _convert_imported_configuration(import_config)
for entry in self._async_current_entries():
if _fingerprint(entry.data) == _fingerprint(self._entry_data):
return self.async_abort(reason="already_configured")
return self.async_create_entry(
title=self._config_entry_name, data=self._entry_data
)
@property
def _config_entry_name(self) -> str:
"""Return the name of the config entry."""
if self._entry_data[CONF_SOURCE] == HaVersionSource.LOCAL:
return DEFAULT_NAME_CURRENT
name = self._entry_data[CONF_VERSION_SOURCE]
if (channel := self._entry_data[CONF_CHANNEL]) != DEFAULT_CHANNEL:
return f"{name} {channel.title()}"
return name
def _fingerprint(data) -> str:
"""Return a fingerprint of the configuration."""
configuration = {**DEFAULT_CONFIGURATION, **data}
return slugify("_".join(configuration.values()))
def _convert_imported_configuration(config: dict[str, Any]) -> Any:
"""Convert a key from the imported configuration."""
data = DEFAULT_CONFIGURATION.copy()
if config.get(CONF_BETA):
data[CONF_CHANNEL] = HaVersionChannel.BETA
if (source := config.get(CONF_SOURCE)) and source != DEFAULT_SOURCE:
if source == SOURCE_HASSIO:
data[CONF_SOURCE] = HaVersionSource.SUPERVISOR
data[CONF_VERSION_SOURCE] = VERSION_SOURCE_VERSIONS
elif source == SOURCE_DOKCER:
data[CONF_SOURCE] = HaVersionSource.CONTAINER
data[CONF_VERSION_SOURCE] = VERSION_SOURCE_DOCKER_HUB
else:
data[CONF_SOURCE] = source
data[CONF_VERSION_SOURCE] = VERSION_SOURCE_MAP_INVERTED[source]
if (image := config.get(CONF_IMAGE)) and image != DEFAULT_IMAGE:
if data[CONF_SOURCE] == HaVersionSource.CONTAINER:
data[CONF_IMAGE] = f"{config[CONF_IMAGE]}{POSTFIX_CONTAINER_NAME}"
else:
data[CONF_IMAGE] = config[CONF_IMAGE]
if (name := config.get(CONF_NAME)) and name != DEFAULT_NAME:
data[CONF_NAME] = config[CONF_NAME]
else:
if data[CONF_SOURCE] == HaVersionSource.LOCAL:
data[CONF_NAME] = DEFAULT_NAME_CURRENT
else:
data[CONF_NAME] = DEFAULT_NAME_LATEST
return data

View File

@ -0,0 +1,128 @@
"""Constants for the Version integration."""
from __future__ import annotations
from datetime import timedelta
from logging import Logger, getLogger
from typing import Any, Final
from pyhaversion.consts import HaVersionChannel, HaVersionSource
from homeassistant.const import CONF_NAME, Platform
DOMAIN: Final = "version"
LOGGER: Final[Logger] = getLogger(__package__)
PLATFORMS: Final[list[Platform]] = [Platform.SENSOR]
UPDATE_COORDINATOR_UPDATE_INTERVAL: Final[timedelta] = timedelta(minutes=5)
ENTRY_TYPE_SERVICE: Final = "service"
HOME_ASSISTANT: Final = "Home Assistant"
POSTFIX_CONTAINER_NAME: Final = "-homeassistant"
CONF_BETA: Final = "beta"
CONF_BOARD: Final = "board"
CONF_CHANNEL: Final = "channel"
CONF_IMAGE: Final = "image"
CONF_VERSION_SOURCE: Final = "version_source"
CONF_SOURCE: Final = "source"
ATTR_CHANNEL: Final = CONF_CHANNEL
ATTR_VERSION_SOURCE: Final = CONF_VERSION_SOURCE
ATTR_SOURCE: Final = CONF_SOURCE
SOURCE_DOKCER: Final = "docker" # Kept to not break existing configurations
SOURCE_HASSIO: Final = "hassio" # Kept to not break existing configurations
VERSION_SOURCE_DOCKER_HUB: Final = "Docker Hub"
VERSION_SOURCE_HAIO: Final = "Home Assistant Website"
VERSION_SOURCE_LOCAL: Final = "Local installation"
VERSION_SOURCE_PYPI: Final = "Python Package Index (PyPI)"
VERSION_SOURCE_VERSIONS: Final = "Home Assistant Versions"
DEFAULT_BETA: Final = False
DEFAULT_BOARD: Final = "OVA"
DEFAULT_CHANNEL: Final[HaVersionChannel] = HaVersionChannel.STABLE
DEFAULT_IMAGE: Final = "default"
DEFAULT_NAME_CURRENT: Final = "Current Version"
DEFAULT_NAME_LATEST: Final = "Latest Version"
DEFAULT_NAME: Final = ""
DEFAULT_SOURCE: Final[HaVersionSource] = HaVersionSource.LOCAL
DEFAULT_CONFIGURATION: Final[dict[str, Any]] = {
CONF_NAME: DEFAULT_NAME,
CONF_CHANNEL: DEFAULT_CHANNEL,
CONF_IMAGE: DEFAULT_IMAGE,
CONF_BOARD: DEFAULT_BOARD,
CONF_VERSION_SOURCE: VERSION_SOURCE_LOCAL,
CONF_SOURCE: DEFAULT_SOURCE,
}
STEP_VERSION_SOURCE: Final = "version_source"
STEP_USER: Final = "user"
HA_VERSION_SOURCES: Final[list[str]] = [source.value for source in HaVersionSource]
BOARD_MAP: Final[dict[str, str]] = {
"OVA": "ova",
"RaspberryPi": "rpi",
"RaspberryPi Zero-W": "rpi0-w",
"RaspberryPi 2": "rpi2",
"RaspberryPi 3": "rpi3",
"RaspberryPi 3 64bit": "rpi3-64",
"RaspberryPi 4": "rpi4",
"RaspberryPi 4 64bit": "rpi4-64",
"ASUS Tinkerboard": "tinker",
"ODROID C2": "odroid-c2",
"ODROID C4": "odroid-c4",
"ODROID N2": "odroid-n2",
"ODROID XU4": "odroid-xu4",
"Generic x86-64": "generic-x86-64",
"Intel NUC": "intel-nuc",
}
VALID_BOARDS: Final[list[str]] = list(BOARD_MAP)
VERSION_SOURCE_MAP: Final[dict[str, HaVersionSource]] = {
VERSION_SOURCE_LOCAL: HaVersionSource.LOCAL,
VERSION_SOURCE_VERSIONS: HaVersionSource.SUPERVISOR,
VERSION_SOURCE_HAIO: HaVersionSource.HAIO,
VERSION_SOURCE_DOCKER_HUB: HaVersionSource.CONTAINER,
VERSION_SOURCE_PYPI: HaVersionSource.PYPI,
}
VERSION_SOURCE_MAP_INVERTED: Final[dict[HaVersionSource, str]] = {
value: key for key, value in VERSION_SOURCE_MAP.items()
}
VALID_SOURCES: Final[list[str]] = HA_VERSION_SOURCES + [
SOURCE_HASSIO, # Kept to not break existing configurations
SOURCE_DOKCER, # Kept to not break existing configurations
]
VALID_IMAGES: Final = [
"default",
"generic-x86-64",
"intel-nuc",
"odroid-c2",
"odroid-n2",
"odroid-xu",
"qemuarm-64",
"qemuarm",
"qemux86-64",
"qemux86",
"raspberrypi",
"raspberrypi2",
"raspberrypi3-64",
"raspberrypi3",
"raspberrypi4-64",
"raspberrypi4",
"tinker",
]
VALID_CONTAINER_IMAGES: Final[list[str]] = [
f"{image}{POSTFIX_CONTAINER_NAME}" if image != DEFAULT_IMAGE else image
for image in VALID_IMAGES
]
VALID_CHANNELS: Final[list[str]] = [
str(channel.value).title() for channel in HaVersionChannel
]

View File

@ -0,0 +1,54 @@
"""Data update coordinator for Version entities."""
from __future__ import annotations
from typing import Any
from awesomeversion import AwesomeVersion
from pyhaversion import HaVersion
from pyhaversion.exceptions import HaVersionException
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
from .const import DOMAIN, LOGGER, UPDATE_COORDINATOR_UPDATE_INTERVAL
class VersionDataUpdateCoordinator(DataUpdateCoordinator):
"""Data update coordinator for Version entities."""
config_entry: ConfigEntry
def __init__(
self,
hass: HomeAssistant,
api: HaVersion,
) -> None:
"""Initialize the coordinator."""
super().__init__(
hass=hass,
logger=LOGGER,
name=DOMAIN,
update_method=self._async_update_version_data,
update_interval=UPDATE_COORDINATOR_UPDATE_INTERVAL,
)
self._api = api
self._version: AwesomeVersion | None = None
self._version_data: dict[str, Any] | None = None
@property
def version(self) -> str | None:
"""Return the latest version."""
return str(self._version) if self._version else None
@property
def version_data(self) -> dict[str, Any]:
"""Return the version data."""
return self._version_data or {}
async def _async_update_version_data(self) -> None:
"""Update version data."""
try:
self._version, self._version_data = await self._api.get_version()
except HaVersionException as exception:
raise UpdateFailed(exception) from exception

View File

@ -10,5 +10,6 @@
"@ludeeus"
],
"quality_scale": "internal",
"iot_class": "local_push"
"iot_class": "local_push",
"config_flow": true
}

View File

@ -1,156 +1,127 @@
"""Sensor that can display the current Home Assistant versions."""
from datetime import timedelta
import logging
from __future__ import annotations
from typing import Any, Final
from pyhaversion import (
HaVersion,
HaVersionChannel,
HaVersionSource,
exceptions as pyhaversionexceptions,
)
import voluptuous as vol
from voluptuous.schema_builder import Schema
from homeassistant.components.sensor import (
PLATFORM_SCHEMA,
PLATFORM_SCHEMA as SENSOR_PLATFORM_SCHEMA,
SensorEntity,
SensorEntityDescription,
)
from homeassistant.const import CONF_NAME, CONF_SOURCE
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry
from homeassistant.const import CONF_NAME
from homeassistant.core import HomeAssistant
import homeassistant.helpers.config_validation as cv
from homeassistant.util import Throttle
from homeassistant.helpers.device_registry import DeviceEntryType
from homeassistant.helpers.entity import DeviceInfo
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType, StateType
from homeassistant.helpers.update_coordinator import CoordinatorEntity
ALL_IMAGES = [
"default",
"generic-x86-64",
"intel-nuc",
"odroid-c2",
"odroid-n2",
"odroid-xu",
"qemuarm-64",
"qemuarm",
"qemux86-64",
"qemux86",
"raspberrypi",
"raspberrypi2",
"raspberrypi3-64",
"raspberrypi3",
"raspberrypi4-64",
"raspberrypi4",
"tinker",
]
from .const import (
ATTR_SOURCE,
CONF_BETA,
CONF_IMAGE,
CONF_SOURCE,
DEFAULT_BETA,
DEFAULT_IMAGE,
DEFAULT_NAME,
DEFAULT_SOURCE,
DOMAIN,
HOME_ASSISTANT,
LOGGER,
VALID_IMAGES,
VALID_SOURCES,
)
from .coordinator import VersionDataUpdateCoordinator
HA_VERSION_SOURCES = [source.value for source in HaVersionSource]
ALL_SOURCES = HA_VERSION_SOURCES + [
"hassio", # Kept to not break existing configurations
"docker", # Kept to not break existing configurations
]
CONF_BETA = "beta"
CONF_IMAGE = "image"
DEFAULT_IMAGE = "default"
DEFAULT_NAME_LATEST = "Latest Version"
DEFAULT_NAME_LOCAL = "Current Version"
DEFAULT_SOURCE = "local"
TIME_BETWEEN_UPDATES = timedelta(minutes=5)
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
PLATFORM_SCHEMA: Final[Schema] = SENSOR_PLATFORM_SCHEMA.extend(
{
vol.Optional(CONF_BETA, default=False): cv.boolean,
vol.Optional(CONF_IMAGE, default=DEFAULT_IMAGE): vol.In(ALL_IMAGES),
vol.Optional(CONF_NAME, default=""): cv.string,
vol.Optional(CONF_SOURCE, default=DEFAULT_SOURCE): vol.In(ALL_SOURCES),
vol.Optional(CONF_BETA, default=DEFAULT_BETA): cv.boolean,
vol.Optional(CONF_IMAGE, default=DEFAULT_IMAGE): vol.In(VALID_IMAGES),
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
vol.Optional(CONF_SOURCE, default=DEFAULT_SOURCE): vol.In(VALID_SOURCES),
}
)
_LOGGER: logging.Logger = logging.getLogger(__name__)
async def async_setup_platform(hass, config, async_add_entities, discovery_info=None):
"""Set up the Version sensor platform."""
beta = config.get(CONF_BETA)
image = config.get(CONF_IMAGE)
name = config.get(CONF_NAME)
source = config.get(CONF_SOURCE)
channel = HaVersionChannel.BETA if beta else HaVersionChannel.STABLE
session = async_get_clientsession(hass)
if source in HA_VERSION_SOURCES:
source = HaVersionSource(source)
elif source == "hassio":
source = HaVersionSource.SUPERVISOR
elif source == "docker":
source = HaVersionSource.CONTAINER
if (
source == HaVersionSource.CONTAINER
and image is not None
and image != DEFAULT_IMAGE
):
image = f"{image}-homeassistant"
if not (name := config.get(CONF_NAME)):
if source == HaVersionSource.LOCAL:
name = DEFAULT_NAME_LOCAL
else:
name = DEFAULT_NAME_LATEST
async_add_entities(
[
VersionSensor(
VersionData(
HaVersion(
session=session, source=source, image=image, channel=channel
)
),
SensorEntityDescription(key=source, name=name),
)
],
True,
async def async_setup_platform(
hass: HomeAssistant,
config: ConfigType,
async_add_entities: AddEntitiesCallback,
discovery_info: DiscoveryInfoType | None = None,
) -> None:
"""Set up the legacy version sensor platform."""
LOGGER.warning(
"Configuration of the Version platform in YAML is deprecated and will be "
"removed in Home Assistant 2022.4; Your existing configuration "
"has been imported into the UI automatically and can be safely removed "
"from your configuration.yaml file"
)
hass.async_create_task(
hass.config_entries.flow.async_init(
DOMAIN, context={ATTR_SOURCE: SOURCE_IMPORT}, data=config
)
)
class VersionData:
"""Get the latest data and update the states."""
async def async_setup_entry(
hass: HomeAssistant,
entry: ConfigEntry,
async_add_entities: AddEntitiesCallback,
) -> None:
"""Set up version sensors."""
coordinator: VersionDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id]
if (entity_name := entry.data[CONF_NAME]) == DEFAULT_NAME:
entity_name = entry.title
def __init__(self, api: HaVersion) -> None:
"""Initialize the data object."""
self.api = api
version_sensor_entities: list[VersionSensorEntity] = [
VersionSensorEntity(
coordinator=coordinator,
entity_description=SensorEntityDescription(
key=str(entry.data[CONF_SOURCE]),
name=entity_name,
),
)
]
@Throttle(TIME_BETWEEN_UPDATES)
async def async_update(self):
"""Get the latest version information."""
try:
await self.api.get_version()
except pyhaversionexceptions.HaVersionFetchException as exception:
_LOGGER.warning(exception)
except pyhaversionexceptions.HaVersionParseException as exception:
_LOGGER.warning(
"Could not parse data received for %s - %s", self.api.source, exception
)
async_add_entities(version_sensor_entities)
class VersionSensor(SensorEntity):
"""Representation of a Home Assistant version sensor."""
class VersionSensorEntity(CoordinatorEntity, SensorEntity):
"""Version sensor entity class."""
_attr_icon = "mdi:package-up"
_attr_device_info = DeviceInfo(
name=f"{HOME_ASSISTANT} {DOMAIN.title()}",
identifiers={(HOME_ASSISTANT, DOMAIN)},
manufacturer=HOME_ASSISTANT,
entry_type=DeviceEntryType.SERVICE,
)
coordinator: VersionDataUpdateCoordinator
def __init__(
self,
data: VersionData,
description: SensorEntityDescription,
coordinator: VersionDataUpdateCoordinator,
entity_description: SensorEntityDescription,
) -> None:
"""Initialize the Version sensor."""
self.data = data
self.entity_description = description
"""Initialize version sensor entities."""
super().__init__(coordinator)
self.entity_description = entity_description
self._attr_unique_id = (
f"{coordinator.config_entry.unique_id}_{entity_description.key}"
)
async def async_update(self):
"""Get the latest version information."""
await self.data.async_update()
self._attr_native_value = self.data.api.version
self._attr_extra_state_attributes = self.data.api.version_data
@property
def native_value(self) -> StateType:
"""Return the native value of this sensor."""
return self.coordinator.version
@property
def extra_state_attributes(self) -> dict[str, Any]:
"""Return extra state attributes of this sensor."""
return self.coordinator.version_data

View File

@ -0,0 +1,26 @@
{
"config": {
"abort": {
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]"
},
"step": {
"user": {
"title": "Select installation type",
"description": "Select the source you want to track versions from",
"data": {
"version_source": "Version source"
}
},
"version_source": {
"title": "Configure",
"description": "Configure {version_source} version tracking",
"data": {
"beta": "Include beta versions",
"board": "Which board should be tracked",
"channel": "Which channel should be tracked",
"image": "Which image should be tracked"
}
}
}
}
}

View File

@ -0,0 +1,26 @@
{
"config": {
"abort": {
"already_configured": "Device is already configured"
},
"step": {
"user": {
"data": {
"version_source": "Version source"
},
"description": "Select the source you want to track versions from",
"title": "Select installation type"
},
"version_source": {
"data": {
"beta": "Include beta versions",
"board": "Which board should be tracked",
"channel": "Which channel should be tracked",
"image": "Which image should be tracked"
},
"description": "Configure {version_source} version tracking",
"title": "Configure"
}
}
}
}

View File

@ -331,6 +331,7 @@ FLOWS = [
"venstar",
"vera",
"verisure",
"version",
"vesync",
"vicare",
"vilfo",

View File

@ -0,0 +1,73 @@
"""Fixtures for version integration."""
from __future__ import annotations
from typing import Any, Final
from unittest.mock import patch
from homeassistant import config_entries
from homeassistant.components.version.const import (
DEFAULT_CONFIGURATION,
DEFAULT_NAME_CURRENT,
DOMAIN,
UPDATE_COORDINATOR_UPDATE_INTERVAL,
VERSION_SOURCE_LOCAL,
)
from homeassistant.const import CONF_NAME
from homeassistant.core import HomeAssistant
from homeassistant.setup import async_setup_component
from homeassistant.util import dt
from tests.common import MockConfigEntry, async_fire_time_changed
MOCK_VERSION: Final = "1970.1.0"
MOCK_VERSION_DATA: Final = {"source": "local", "channel": "stable"}
MOCK_VERSION_CONFIG_ENTRY_DATA: Final[dict[str, Any]] = {
"domain": DOMAIN,
"title": VERSION_SOURCE_LOCAL,
"data": DEFAULT_CONFIGURATION,
"source": config_entries.SOURCE_USER,
}
TEST_DEFAULT_IMPORT_CONFIG: Final = {
**DEFAULT_CONFIGURATION,
CONF_NAME: DEFAULT_NAME_CURRENT,
}
async def mock_get_version_update(
hass: HomeAssistant,
version: str = MOCK_VERSION,
data: dict[str, Any] = MOCK_VERSION_DATA,
side_effect: Exception = None,
) -> None:
"""Mock getting version."""
with patch(
"pyhaversion.HaVersion.get_version",
return_value=(version, data),
side_effect=side_effect,
):
async_fire_time_changed(hass, dt.utcnow() + UPDATE_COORDINATOR_UPDATE_INTERVAL)
await hass.async_block_till_done()
async def setup_version_integration(hass: HomeAssistant) -> MockConfigEntry:
"""Set up the Version integration."""
await async_setup_component(hass, "persistent_notification", {})
mock_entry = MockConfigEntry(**MOCK_VERSION_CONFIG_ENTRY_DATA)
mock_entry.add_to_hass(hass)
with patch(
"pyhaversion.HaVersion.get_version",
return_value=(MOCK_VERSION, MOCK_VERSION_DATA),
):
assert await hass.config_entries.async_setup(mock_entry.entry_id)
await hass.async_block_till_done()
assert hass.states.get("sensor.local_installation").state == MOCK_VERSION
assert mock_entry.state == config_entries.ConfigEntryState.LOADED
return mock_entry

View File

@ -0,0 +1,236 @@
"""Test the Version config flow."""
from unittest.mock import patch
from pyhaversion.consts import HaVersionChannel, HaVersionSource
from homeassistant import config_entries, setup
from homeassistant.components.version.const import (
CONF_BETA,
CONF_BOARD,
CONF_CHANNEL,
CONF_IMAGE,
CONF_VERSION_SOURCE,
DEFAULT_CONFIGURATION,
DOMAIN,
UPDATE_COORDINATOR_UPDATE_INTERVAL,
VERSION_SOURCE_DOCKER_HUB,
VERSION_SOURCE_PYPI,
VERSION_SOURCE_VERSIONS,
)
from homeassistant.const import CONF_SOURCE
from homeassistant.core import HomeAssistant
from homeassistant.data_entry_flow import (
RESULT_TYPE_ABORT,
RESULT_TYPE_CREATE_ENTRY,
RESULT_TYPE_FORM,
)
from homeassistant.util import dt
from tests.common import async_fire_time_changed
from tests.components.version.common import (
MOCK_VERSION,
MOCK_VERSION_DATA,
setup_version_integration,
)
async def test_reload(hass: HomeAssistant):
"""Test the Version sensor with different sources."""
config_entry = await setup_version_integration(hass)
with patch(
"pyhaversion.HaVersion.get_version",
return_value=(MOCK_VERSION, MOCK_VERSION_DATA),
):
assert await hass.config_entries.async_reload(config_entry.entry_id)
async_fire_time_changed(hass, dt.utcnow() + UPDATE_COORDINATOR_UPDATE_INTERVAL)
await hass.async_block_till_done()
entry = hass.config_entries.async_get_entry(config_entry.entry_id)
assert entry.state == config_entries.ConfigEntryState.LOADED
assert hass.states.get("sensor.local_installation").state == MOCK_VERSION
async def test_basic_form(hass: HomeAssistant) -> None:
"""Test we get the form."""
await setup.async_setup_component(hass, "persistent_notification", {})
result = await hass.config_entries.flow.async_init(
DOMAIN,
context={"source": config_entries.SOURCE_USER, "show_advanced_options": False},
)
assert result["type"] == RESULT_TYPE_FORM
with patch(
"homeassistant.components.version.async_setup_entry",
return_value=True,
) as mock_setup_entry:
result2 = await hass.config_entries.flow.async_configure(
result["flow_id"],
{CONF_VERSION_SOURCE: VERSION_SOURCE_DOCKER_HUB},
)
await hass.async_block_till_done()
assert result2["type"] == RESULT_TYPE_CREATE_ENTRY
assert result2["title"] == VERSION_SOURCE_DOCKER_HUB
assert result2["data"] == {
**DEFAULT_CONFIGURATION,
CONF_SOURCE: HaVersionSource.CONTAINER,
CONF_VERSION_SOURCE: VERSION_SOURCE_DOCKER_HUB,
}
assert len(mock_setup_entry.mock_calls) == 1
async def test_advanced_form_pypi(hass: HomeAssistant) -> None:
"""Show advanced form when pypi is selected."""
await setup.async_setup_component(hass, "persistent_notification", {})
result = await hass.config_entries.flow.async_init(
DOMAIN,
context={"source": config_entries.SOURCE_USER, "show_advanced_options": True},
)
assert result["type"] == RESULT_TYPE_FORM
result = await hass.config_entries.flow.async_configure(
result["flow_id"],
{CONF_VERSION_SOURCE: VERSION_SOURCE_PYPI},
)
await hass.async_block_till_done()
assert result["type"] == RESULT_TYPE_FORM
assert result["step_id"] == "version_source"
result = await hass.config_entries.flow.async_configure(result["flow_id"])
assert result["type"] == RESULT_TYPE_FORM
assert result["step_id"] == "version_source"
with patch(
"homeassistant.components.version.async_setup_entry",
return_value=True,
) as mock_setup_entry:
result = await hass.config_entries.flow.async_configure(
result["flow_id"], {CONF_BETA: True}
)
await hass.async_block_till_done()
assert result["type"] == RESULT_TYPE_CREATE_ENTRY
assert result["title"] == VERSION_SOURCE_PYPI
assert result["data"] == {
**DEFAULT_CONFIGURATION,
CONF_BETA: True,
CONF_SOURCE: HaVersionSource.PYPI,
CONF_VERSION_SOURCE: VERSION_SOURCE_PYPI,
}
assert len(mock_setup_entry.mock_calls) == 1
async def test_advanced_form_container(hass: HomeAssistant) -> None:
"""Show advanced form when container source is selected."""
await setup.async_setup_component(hass, "persistent_notification", {})
result = await hass.config_entries.flow.async_init(
DOMAIN,
context={"source": config_entries.SOURCE_USER, "show_advanced_options": True},
)
assert result["type"] == RESULT_TYPE_FORM
result = await hass.config_entries.flow.async_configure(
result["flow_id"],
{CONF_VERSION_SOURCE: VERSION_SOURCE_DOCKER_HUB},
)
await hass.async_block_till_done()
assert result["type"] == RESULT_TYPE_FORM
assert result["step_id"] == "version_source"
result = await hass.config_entries.flow.async_configure(result["flow_id"])
assert result["type"] == RESULT_TYPE_FORM
assert result["step_id"] == "version_source"
with patch(
"homeassistant.components.version.async_setup_entry",
return_value=True,
) as mock_setup_entry:
result = await hass.config_entries.flow.async_configure(
result["flow_id"], {CONF_IMAGE: "odroid-n2-homeassistant"}
)
await hass.async_block_till_done()
assert result["type"] == RESULT_TYPE_CREATE_ENTRY
assert result["title"] == VERSION_SOURCE_DOCKER_HUB
assert result["data"] == {
**DEFAULT_CONFIGURATION,
CONF_IMAGE: "odroid-n2-homeassistant",
CONF_SOURCE: HaVersionSource.CONTAINER,
CONF_VERSION_SOURCE: VERSION_SOURCE_DOCKER_HUB,
}
assert len(mock_setup_entry.mock_calls) == 1
async def test_advanced_form_supervisor(hass: HomeAssistant) -> None:
"""Show advanced form when docker source is selected."""
await setup.async_setup_component(hass, "persistent_notification", {})
result = await hass.config_entries.flow.async_init(
DOMAIN,
context={"source": config_entries.SOURCE_USER, "show_advanced_options": True},
)
assert result["type"] == RESULT_TYPE_FORM
result = await hass.config_entries.flow.async_configure(
result["flow_id"],
{CONF_VERSION_SOURCE: VERSION_SOURCE_VERSIONS},
)
await hass.async_block_till_done()
assert result["type"] == RESULT_TYPE_FORM
assert result["step_id"] == "version_source"
result = await hass.config_entries.flow.async_configure(result["flow_id"])
assert result["type"] == RESULT_TYPE_FORM
assert result["step_id"] == "version_source"
with patch(
"homeassistant.components.version.async_setup_entry",
return_value=True,
) as mock_setup_entry:
result = await hass.config_entries.flow.async_configure(
result["flow_id"],
{CONF_CHANNEL: "Dev", CONF_IMAGE: "odroid-n2", CONF_BOARD: "ODROID N2"},
)
await hass.async_block_till_done()
assert result["type"] == RESULT_TYPE_CREATE_ENTRY
assert result["title"] == f"{VERSION_SOURCE_VERSIONS} Dev"
assert result["data"] == {
**DEFAULT_CONFIGURATION,
CONF_IMAGE: "odroid-n2",
CONF_BOARD: "ODROID N2",
CONF_CHANNEL: HaVersionChannel.DEV,
CONF_SOURCE: HaVersionSource.SUPERVISOR,
CONF_VERSION_SOURCE: VERSION_SOURCE_VERSIONS,
}
assert len(mock_setup_entry.mock_calls) == 1
async def test_import_existing(hass: HomeAssistant) -> None:
"""Test importing existing configuration."""
with patch(
"homeassistant.components.version.async_setup_entry",
return_value=True,
) as mock_setup_entry:
result = await hass.config_entries.flow.async_init(
DOMAIN,
context={"source": config_entries.SOURCE_IMPORT},
data={},
)
await hass.async_block_till_done()
assert result["type"] == RESULT_TYPE_CREATE_ENTRY
result = await hass.config_entries.flow.async_init(
DOMAIN,
context={"source": config_entries.SOURCE_IMPORT},
data={},
)
await hass.async_block_till_done()
assert result["type"] == RESULT_TYPE_ABORT
assert len(mock_setup_entry.mock_calls) == 1

View File

@ -1,130 +1,134 @@
"""The test for the version sensor platform."""
from datetime import timedelta
from __future__ import annotations
from typing import Any
from unittest.mock import patch
from pyhaversion import HaVersionSource, exceptions as pyhaversionexceptions
from pyhaversion import HaVersionChannel, HaVersionSource
from pyhaversion.exceptions import HaVersionException
import pytest
from homeassistant.components.version.sensor import HA_VERSION_SOURCES
from homeassistant.components.version.const import (
CONF_BETA,
CONF_CHANNEL,
CONF_IMAGE,
CONF_VERSION_SOURCE,
DEFAULT_NAME_LATEST,
DOMAIN,
VERSION_SOURCE_DOCKER_HUB,
VERSION_SOURCE_VERSIONS,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_NAME, CONF_SOURCE
from homeassistant.core import HomeAssistant
from homeassistant.setup import async_setup_component
from homeassistant.util import dt
from tests.common import async_fire_time_changed
from .common import (
MOCK_VERSION,
MOCK_VERSION_DATA,
TEST_DEFAULT_IMPORT_CONFIG,
mock_get_version_update,
setup_version_integration,
)
MOCK_VERSION = "10.0"
async def async_setup_sensor_wrapper(
hass: HomeAssistant, config: dict[str, Any]
) -> ConfigEntry:
"""Set up the Version sensor platform."""
await async_setup_component(hass, "persistent_notification", {})
with patch(
"pyhaversion.HaVersion.get_version",
return_value=(MOCK_VERSION, MOCK_VERSION_DATA),
):
assert await async_setup_component(
hass, "sensor", {"sensor": {"platform": DOMAIN, **config}}
)
await hass.async_block_till_done()
config_entries = hass.config_entries.async_entries(DOMAIN)
print(config_entries)
config_entry = config_entries[-1]
assert config_entry.source == "import"
return config_entry
async def test_version_sensor(hass: HomeAssistant):
"""Test the Version sensor with different sources."""
await setup_version_integration(hass)
state = hass.states.get("sensor.local_installation")
assert state.state == MOCK_VERSION
assert state.attributes["source"] == "local"
assert state.attributes["channel"] == "stable"
async def test_update(hass: HomeAssistant, caplog: pytest.LogCaptureFixture):
"""Test updates."""
await setup_version_integration(hass)
assert hass.states.get("sensor.local_installation").state == MOCK_VERSION
await mock_get_version_update(hass, version="1970.1.1")
assert hass.states.get("sensor.local_installation").state == "1970.1.1"
assert "Error fetching version data" not in caplog.text
await mock_get_version_update(hass, side_effect=HaVersionException)
assert hass.states.get("sensor.local_installation").state == "unavailable"
assert "Error fetching version data" in caplog.text
@pytest.mark.parametrize(
"source,target_source,name",
"yaml,converted",
(
(
("local", HaVersionSource.LOCAL, "current_version"),
("docker", HaVersionSource.CONTAINER, "latest_version"),
("hassio", HaVersionSource.SUPERVISOR, "latest_version"),
)
+ tuple(
(source, HaVersionSource(source), "latest_version")
for source in HA_VERSION_SOURCES
if source != HaVersionSource.LOCAL
)
{},
TEST_DEFAULT_IMPORT_CONFIG,
),
(
{CONF_NAME: "test"},
{**TEST_DEFAULT_IMPORT_CONFIG, CONF_NAME: "test"},
),
(
{CONF_SOURCE: "hassio", CONF_IMAGE: "odroid-n2"},
{
**TEST_DEFAULT_IMPORT_CONFIG,
CONF_NAME: DEFAULT_NAME_LATEST,
CONF_SOURCE: HaVersionSource.SUPERVISOR,
CONF_VERSION_SOURCE: VERSION_SOURCE_VERSIONS,
CONF_IMAGE: "odroid-n2",
},
),
(
{CONF_SOURCE: "docker"},
{
**TEST_DEFAULT_IMPORT_CONFIG,
CONF_NAME: DEFAULT_NAME_LATEST,
CONF_SOURCE: HaVersionSource.CONTAINER,
CONF_VERSION_SOURCE: VERSION_SOURCE_DOCKER_HUB,
},
),
(
{CONF_BETA: True},
{
**TEST_DEFAULT_IMPORT_CONFIG,
CONF_CHANNEL: HaVersionChannel.BETA,
},
),
(
{CONF_SOURCE: "container", CONF_IMAGE: "odroid-n2"},
{
**TEST_DEFAULT_IMPORT_CONFIG,
CONF_NAME: DEFAULT_NAME_LATEST,
CONF_SOURCE: HaVersionSource.CONTAINER,
CONF_VERSION_SOURCE: VERSION_SOURCE_DOCKER_HUB,
CONF_IMAGE: "odroid-n2-homeassistant",
},
),
),
)
async def test_version_source(hass, source, target_source, name):
"""Test the Version sensor with different sources."""
config = {
"sensor": {"platform": "version", "source": source, "image": "qemux86-64"}
}
with patch("homeassistant.components.version.sensor.HaVersion.get_version"), patch(
"homeassistant.components.version.sensor.HaVersion.version", MOCK_VERSION
):
assert await async_setup_component(hass, "sensor", config)
await hass.async_block_till_done()
state = hass.states.get(f"sensor.{name}")
assert state
assert state.attributes["source"] == target_source
assert state.state == MOCK_VERSION
async def test_version_fetch_exception(hass, caplog):
"""Test fetch exception thrown during updates."""
config = {"sensor": {"platform": "version"}}
with patch(
"homeassistant.components.version.sensor.HaVersion.get_version",
side_effect=pyhaversionexceptions.HaVersionFetchException(
"Fetch exception from pyhaversion"
),
):
assert await async_setup_component(hass, "sensor", config)
await hass.async_block_till_done()
assert "Fetch exception from pyhaversion" in caplog.text
async def test_version_parse_exception(hass, caplog):
"""Test parse exception thrown during updates."""
config = {"sensor": {"platform": "version"}}
with patch(
"homeassistant.components.version.sensor.HaVersion.get_version",
side_effect=pyhaversionexceptions.HaVersionParseException,
):
assert await async_setup_component(hass, "sensor", config)
await hass.async_block_till_done()
assert "Could not parse data received for HaVersionSource.LOCAL" in caplog.text
async def test_update(hass):
"""Test updates."""
config = {"sensor": {"platform": "version"}}
with patch("homeassistant.components.version.sensor.HaVersion.get_version"), patch(
"homeassistant.components.version.sensor.HaVersion.version", MOCK_VERSION
):
assert await async_setup_component(hass, "sensor", config)
await hass.async_block_till_done()
state = hass.states.get("sensor.current_version")
assert state
assert state.state == MOCK_VERSION
with patch("homeassistant.components.version.sensor.HaVersion.get_version"), patch(
"homeassistant.components.version.sensor.HaVersion.version", "1234"
):
async_fire_time_changed(hass, dt.utcnow() + timedelta(minutes=5))
await hass.async_block_till_done()
state = hass.states.get("sensor.current_version")
assert state
assert state.state == "1234"
async def test_image_name_container(hass):
"""Test the Version sensor with image name for container."""
config = {
"sensor": {"platform": "version", "source": "docker", "image": "qemux86-64"}
}
with patch("homeassistant.components.version.sensor.HaVersion") as haversion:
assert await async_setup_component(hass, "sensor", config)
await hass.async_block_till_done()
constructor = haversion.call_args[1]
assert constructor["source"] == "container"
assert constructor["image"] == "qemux86-64-homeassistant"
async def test_image_name_supervisor(hass):
"""Test the Version sensor with image name for supervisor."""
config = {
"sensor": {"platform": "version", "source": "hassio", "image": "qemux86-64"}
}
with patch("homeassistant.components.version.sensor.HaVersion") as haversion:
assert await async_setup_component(hass, "sensor", config)
await hass.async_block_till_done()
constructor = haversion.call_args[1]
assert constructor["source"] == "supervisor"
assert constructor["image"] == "qemux86-64"
async def test_config_import(
hass: HomeAssistant, yaml: dict[str, Any], converted: dict[str, Any]
) -> None:
"""Test importing YAML configuration."""
config_entry = await async_setup_sensor_wrapper(hass, yaml)
assert config_entry.data == converted