Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add ability to select/change the configured device in ViCare integration #107906

Closed
wants to merge 24 commits into from
Closed
Show file tree
Hide file tree
Changes from 15 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions .coveragerc
Original file line number Diff line number Diff line change
Expand Up @@ -1499,6 +1499,7 @@ omit =
homeassistant/components/vicare/entity.py
homeassistant/components/vicare/number.py
homeassistant/components/vicare/sensor.py
homeassistant/components/vicare/types.py
homeassistant/components/vicare/utils.py
homeassistant/components/vicare/water_heater.py
homeassistant/components/vilfo/__init__.py
Expand Down
84 changes: 47 additions & 37 deletions homeassistant/components/vicare/__init__.py
Original file line number Diff line number Diff line change
@@ -1,54 +1,64 @@
"""The ViCare integration."""
from __future__ import annotations

from collections.abc import Callable, Mapping
from contextlib import suppress
from dataclasses import dataclass
import logging
import os
from typing import Any

from PyViCare.PyViCare import PyViCare
from PyViCare.PyViCareDevice import Device
from PyViCare.PyViCareDeviceConfig import PyViCareDeviceConfig
from PyViCare.PyViCareUtils import (
PyViCareInvalidConfigurationError,
PyViCareInvalidCredentialsError,
)

from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_CLIENT_ID, CONF_PASSWORD, CONF_USERNAME
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryAuthFailed
from homeassistant.helpers.storage import STORAGE_DIR

from .const import (
CONF_ACTIVE_DEVICE,
CONF_HEATING_TYPE,
DEFAULT_SCAN_INTERVAL,
DOMAIN,
HEATING_TYPE_TO_CREATOR_METHOD,
PLATFORMS,
VICARE_API,
VICARE_DEVICE_CONFIG,
VICARE_DEVICE_CONFIG_LIST,
VICARE_TOKEN_FILENAME,
HeatingType,
)
from .utils import get_device_config_list, get_serial

_LOGGER = logging.getLogger(__name__)
_TOKEN_FILENAME = "vicare_token.save"


@dataclass(frozen=True)
class ViCareRequiredKeysMixin:
"""Mixin for required keys."""
def _get_active_device_for_migration(hass: HomeAssistant, entry: ConfigEntry) -> str:
"""Return the serial of the first element of the device config list (migration helper)."""
device_list = get_device_config_list(hass, entry.data)
# Currently we only support a single device
return get_serial(device_list[0])

value_getter: Callable[[Device], Any]

async def async_migrate_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Migrate old entry."""

@dataclass(frozen=True)
class ViCareRequiredKeysMixinWithSet(ViCareRequiredKeysMixin):
"""Mixin for required keys with setter."""
if entry.version == 1:
_LOGGER.debug("Migrating from version %s", entry.version)
serial = await hass.async_add_executor_job(
_get_active_device_for_migration, hass, entry
)
entry.version = 2
hass.config_entries.async_update_entry(
entry,
data={
**entry.data,
CONF_ACTIVE_DEVICE: serial,
},
)
_LOGGER.debug("Migration to version %s successful", entry.version)

value_setter: Callable[[Device], bool]
return True


async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
Expand All @@ -68,31 +78,16 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
return True


def vicare_login(hass: HomeAssistant, entry_data: Mapping[str, Any]) -> PyViCare:
"""Login via PyVicare API."""
vicare_api = PyViCare()
vicare_api.setCacheDuration(DEFAULT_SCAN_INTERVAL)
vicare_api.initWithCredentials(
entry_data[CONF_USERNAME],
entry_data[CONF_PASSWORD],
entry_data[CONF_CLIENT_ID],
hass.config.path(STORAGE_DIR, _TOKEN_FILENAME),
)
return vicare_api
async def async_update_entry(hass: HomeAssistant, entry: ConfigEntry) -> None:
"""Update a given config entry."""
await hass.config_entries.async_reload(entry.entry_id)
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This seems not to work, after options changed the integration needs to be reloaded.



def setup_vicare_api(hass: HomeAssistant, entry: ConfigEntry) -> None:
"""Set up PyVicare API."""
vicare_api = vicare_login(hass, entry.data)

for device in vicare_api.devices:
_LOGGER.info(
"Found device: %s (online: %s)", device.getModel(), str(device.isOnline())
)

device_list = get_device_config_list(hass, entry.data)
# Currently we only support a single device
device_list = vicare_api.devices
device = device_list[0]
device = get_configured_device(device_list, entry)
hass.data[DOMAIN][entry.entry_id][VICARE_DEVICE_CONFIG_LIST] = device_list
hass.data[DOMAIN][entry.entry_id][VICARE_DEVICE_CONFIG] = device
hass.data[DOMAIN][entry.entry_id][VICARE_API] = getattr(
Expand All @@ -109,7 +104,22 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:

with suppress(FileNotFoundError):
await hass.async_add_executor_job(
os.remove, hass.config.path(STORAGE_DIR, _TOKEN_FILENAME)
os.remove, hass.config.path(STORAGE_DIR, VICARE_TOKEN_FILENAME)
)

return unload_ok


def get_configured_device(
devices: list[PyViCareDeviceConfig],
entry: ConfigEntry,
) -> PyViCareDeviceConfig:
"""Return the configured device."""
active_device: str = entry.options.get(
CONF_ACTIVE_DEVICE, entry.data.get(CONF_ACTIVE_DEVICE)
)

for device_config in devices:
if get_serial(device_config) == active_device:
return device_config
return None
2 changes: 1 addition & 1 deletion homeassistant/components/vicare/binary_sensor.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,9 +27,9 @@
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback

from . import ViCareRequiredKeysMixin
from .const import DOMAIN, VICARE_API, VICARE_DEVICE_CONFIG
from .entity import ViCareEntity
from .types import ViCareRequiredKeysMixin
from .utils import get_burners, get_circuits, get_compressors, is_supported

_LOGGER = logging.getLogger(__name__)
Expand Down
2 changes: 1 addition & 1 deletion homeassistant/components/vicare/button.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,9 @@
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback

from . import ViCareRequiredKeysMixinWithSet
from .const import DOMAIN, VICARE_API, VICARE_DEVICE_CONFIG
from .entity import ViCareEntity
from .types import ViCareRequiredKeysMixinWithSet
from .utils import is_supported

_LOGGER = logging.getLogger(__name__)
Expand Down
135 changes: 128 additions & 7 deletions homeassistant/components/vicare/config_flow.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,18 +14,26 @@
from homeassistant import config_entries
from homeassistant.components import dhcp
from homeassistant.const import CONF_CLIENT_ID, CONF_PASSWORD, CONF_USERNAME
from homeassistant.core import HomeAssistant, callback
from homeassistant.data_entry_flow import FlowResult
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.device_registry import format_mac
from homeassistant.helpers.selector import (
SelectOptionDict,
SelectSelector,
SelectSelectorConfig,
SelectSelectorMode,
)

from . import vicare_login
from .const import (
CONF_ACTIVE_DEVICE,
CONF_HEATING_TYPE,
DEFAULT_HEATING_TYPE,
DOMAIN,
VICARE_NAME,
HeatingType,
)
from .utils import get_device_config_list, get_device_serial_model_list, vicare_login

_LOGGER = logging.getLogger(__name__)

Expand All @@ -46,11 +54,22 @@
)


def _get_device_list(
hass: HomeAssistant, entry_data: dict[str, Any]
) -> list[tuple[str, str]]:
# device_list = hass.data[DOMAIN][entry.entry_id].get(
# VICARE_DEVICE_CONFIG_LIST, get_device_config_list(hass, entry.data)
# )
device_list = get_device_config_list(hass, entry_data)
return get_device_serial_model_list(hass, device_list)


class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
"""Handle a config flow for ViCare."""

VERSION = 1
VERSION = 2
entry: config_entries.ConfigEntry | None
available_devices: list[tuple[str, str]] = []

async def async_step_user(
self, user_input: dict[str, Any] | None = None
Expand All @@ -60,23 +79,66 @@
return self.async_abort(reason="single_instance_allowed")

errors: dict[str, str] = {}

if user_input is not None:
try:
await self.hass.async_add_executor_job(
vicare_login, self.hass, user_input
self.available_devices = await self.hass.async_add_executor_job(
_get_device_list,
self.hass,
user_input,
)
except (PyViCareInvalidConfigurationError, PyViCareInvalidCredentialsError):
errors["base"] = "invalid_auth"
else:
return self.async_create_entry(title=VICARE_NAME, data=user_input)

if len(self.available_devices) > 1:
return await self.async_step_select()

Check warning on line 93 in homeassistant/components/vicare/config_flow.py

View check run for this annotation

Codecov / codecov/patch

homeassistant/components/vicare/config_flow.py#L93

Added line #L93 was not covered by tests
if len(self.available_devices) == 1:
return self.async_create_entry(
title=VICARE_NAME,
data={
**user_input,
CONF_ACTIVE_DEVICE: self.available_devices[0][0],
},
)
errors["base"] = "no_devices"

Check warning on line 102 in homeassistant/components/vicare/config_flow.py

View check run for this annotation

Codecov / codecov/patch

homeassistant/components/vicare/config_flow.py#L102

Added line #L102 was not covered by tests
return self.async_show_form(
step_id="user",
data_schema=USER_SCHEMA,
errors=errors,
)

async def async_step_select(
self, user_input: dict[str, Any] | None = None
) -> FlowResult:
"""Select which device to show."""
errors: dict[str, str] = {}
if user_input is not None:
return self.async_create_entry(title="", data=user_input)

Check warning on line 115 in homeassistant/components/vicare/config_flow.py

View check run for this annotation

Codecov / codecov/patch

homeassistant/components/vicare/config_flow.py#L113-L115

Added lines #L113 - L115 were not covered by tests

schema = (

Check warning on line 117 in homeassistant/components/vicare/config_flow.py

View check run for this annotation

Codecov / codecov/patch

homeassistant/components/vicare/config_flow.py#L117

Added line #L117 was not covered by tests
vol.Schema(
{
vol.Required(CONF_ACTIVE_DEVICE): SelectSelector(
SelectSelectorConfig(
options=[
SelectOptionDict(
value=serial, label=f"{serial} ({model})"
)
for serial, model in self.available_devices
],
multiple=False,
mode=SelectSelectorMode.LIST,
),
),
}
),
)

return self.async_show_form(

Check warning on line 136 in homeassistant/components/vicare/config_flow.py

View check run for this annotation

Codecov / codecov/patch

homeassistant/components/vicare/config_flow.py#L136

Added line #L136 was not covered by tests
step_id="select",
data_schema=schema,
errors=errors,
)

async def async_step_reauth(self, entry_data: Mapping[str, Any]) -> FlowResult:
"""Handle re-authentication with ViCare."""
self.entry = self.hass.config_entries.async_get_entry(self.context["entry_id"])
Expand Down Expand Up @@ -127,3 +189,62 @@
return self.async_abort(reason="single_instance_allowed")

return await self.async_step_user()

@staticmethod
@callback
def async_get_options_flow(
config_entry: config_entries.ConfigEntry,
) -> config_entries.OptionsFlow:
"""Create the options flow."""
return OptionsFlowHandler(config_entry)

Check warning on line 199 in homeassistant/components/vicare/config_flow.py

View check run for this annotation

Codecov / codecov/patch

homeassistant/components/vicare/config_flow.py#L199

Added line #L199 was not covered by tests


class OptionsFlowHandler(config_entries.OptionsFlow):
"""Options flow handler."""

available_devices: list[tuple[str, str]] = []

def __init__(self, config_entry: config_entries.ConfigEntry) -> None:
"""Initialize options flow."""
self.entry = config_entry

Check warning on line 209 in homeassistant/components/vicare/config_flow.py

View check run for this annotation

Codecov / codecov/patch

homeassistant/components/vicare/config_flow.py#L209

Added line #L209 was not covered by tests

async def async_step_init(
self, user_input: dict[str, Any] | None = None
) -> FlowResult:
"""Manage the options."""
errors: dict[str, str] = {}
if user_input is not None:
return self.async_create_entry(data=user_input)

Check warning on line 217 in homeassistant/components/vicare/config_flow.py

View check run for this annotation

Codecov / codecov/patch

homeassistant/components/vicare/config_flow.py#L215-L217

Added lines #L215 - L217 were not covered by tests

try:
self.available_devices = await self.hass.async_add_executor_job(

Check warning on line 220 in homeassistant/components/vicare/config_flow.py

View check run for this annotation

Codecov / codecov/patch

homeassistant/components/vicare/config_flow.py#L219-L220

Added lines #L219 - L220 were not covered by tests
_get_device_list,
self.hass,
self.entry.data,
)
except (PyViCareInvalidConfigurationError, PyViCareInvalidCredentialsError):
errors["base"] = "invalid_auth"

Check warning on line 226 in homeassistant/components/vicare/config_flow.py

View check run for this annotation

Codecov / codecov/patch

homeassistant/components/vicare/config_flow.py#L225-L226

Added lines #L225 - L226 were not covered by tests

return self.async_show_form(

Check warning on line 228 in homeassistant/components/vicare/config_flow.py

View check run for this annotation

Codecov / codecov/patch

homeassistant/components/vicare/config_flow.py#L228

Added line #L228 was not covered by tests
step_id="init",
data_schema=self.add_suggested_values_to_schema(
vol.Schema(
{
vol.Required(CONF_ACTIVE_DEVICE): SelectSelector(
SelectSelectorConfig(
options=[
SelectOptionDict(
value=serial, label=f"{serial} ({model})"
)
for serial, model in self.available_devices
],
multiple=False,
mode=SelectSelectorMode.LIST,
),
),
}
),
user_input or dict(self.entry.data),
),
errors=errors,
)
2 changes: 2 additions & 0 deletions homeassistant/components/vicare/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,11 @@
VICARE_DEVICE_CONFIG_LIST = "device_config_list"
VICARE_API = "api"
VICARE_NAME = "ViCare"
VICARE_TOKEN_FILENAME = "vicare_token.save"

CONF_CIRCUIT = "circuit"
CONF_HEATING_TYPE = "heating_type"
CONF_ACTIVE_DEVICE = "active_devices"

DEFAULT_SCAN_INTERVAL = 60

Expand Down
2 changes: 1 addition & 1 deletion homeassistant/components/vicare/number.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,9 +29,9 @@
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback

from . import ViCareRequiredKeysMixin
from .const import DOMAIN, VICARE_API, VICARE_DEVICE_CONFIG
from .entity import ViCareEntity
from .types import ViCareRequiredKeysMixin
from .utils import get_circuits, is_supported

_LOGGER = logging.getLogger(__name__)
Expand Down
2 changes: 1 addition & 1 deletion homeassistant/components/vicare/sensor.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,6 @@
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback

from . import ViCareRequiredKeysMixin
from .const import (
DOMAIN,
VICARE_API,
Expand All @@ -48,6 +47,7 @@
VICARE_UNIT_TO_UNIT_OF_MEASUREMENT,
)
from .entity import ViCareEntity
from .types import ViCareRequiredKeysMixin
from .utils import get_burners, get_circuits, get_compressors, is_supported

_LOGGER = logging.getLogger(__name__)
Expand Down