mirror of
https://github.com/home-assistant/core.git
synced 2025-07-23 13:17:32 +00:00
Add zwave_js options flow to reconfigure server (#51840)
This commit is contained in:
parent
fbdd6a9d95
commit
0e5040d917
@ -12,6 +12,7 @@ from homeassistant.components.hassio import (
|
|||||||
async_get_addon_discovery_info,
|
async_get_addon_discovery_info,
|
||||||
async_get_addon_info,
|
async_get_addon_info,
|
||||||
async_install_addon,
|
async_install_addon,
|
||||||
|
async_restart_addon,
|
||||||
async_set_addon_options,
|
async_set_addon_options,
|
||||||
async_start_addon,
|
async_start_addon,
|
||||||
async_stop_addon,
|
async_stop_addon,
|
||||||
@ -89,6 +90,7 @@ class AddonManager:
|
|||||||
"""Set up the add-on manager."""
|
"""Set up the add-on manager."""
|
||||||
self._hass = hass
|
self._hass = hass
|
||||||
self._install_task: asyncio.Task | None = None
|
self._install_task: asyncio.Task | None = None
|
||||||
|
self._restart_task: asyncio.Task | None = None
|
||||||
self._start_task: asyncio.Task | None = None
|
self._start_task: asyncio.Task | None = None
|
||||||
self._update_task: asyncio.Task | None = None
|
self._update_task: asyncio.Task | None = None
|
||||||
|
|
||||||
@ -222,6 +224,11 @@ class AddonManager:
|
|||||||
"""Start the Z-Wave JS add-on."""
|
"""Start the Z-Wave JS add-on."""
|
||||||
await async_start_addon(self._hass, ADDON_SLUG)
|
await async_start_addon(self._hass, ADDON_SLUG)
|
||||||
|
|
||||||
|
@api_error("Failed to restart the Z-Wave JS add-on")
|
||||||
|
async def async_restart_addon(self) -> None:
|
||||||
|
"""Restart the Z-Wave JS add-on."""
|
||||||
|
await async_restart_addon(self._hass, ADDON_SLUG)
|
||||||
|
|
||||||
@callback
|
@callback
|
||||||
def async_schedule_start_addon(self, catch_error: bool = False) -> asyncio.Task:
|
def async_schedule_start_addon(self, catch_error: bool = False) -> asyncio.Task:
|
||||||
"""Schedule a task that starts the Z-Wave JS add-on.
|
"""Schedule a task that starts the Z-Wave JS add-on.
|
||||||
@ -235,6 +242,19 @@ class AddonManager:
|
|||||||
)
|
)
|
||||||
return self._start_task
|
return self._start_task
|
||||||
|
|
||||||
|
@callback
|
||||||
|
def async_schedule_restart_addon(self, catch_error: bool = False) -> asyncio.Task:
|
||||||
|
"""Schedule a task that restarts the Z-Wave JS add-on.
|
||||||
|
|
||||||
|
Only schedule a new restart task if the there's no running task.
|
||||||
|
"""
|
||||||
|
if not self._restart_task or self._restart_task.done():
|
||||||
|
LOGGER.info("Restarting Z-Wave JS add-on")
|
||||||
|
self._restart_task = self._async_schedule_addon_operation(
|
||||||
|
self.async_restart_addon, catch_error=catch_error
|
||||||
|
)
|
||||||
|
return self._restart_task
|
||||||
|
|
||||||
@api_error("Failed to stop the Z-Wave JS add-on")
|
@api_error("Failed to stop the Z-Wave JS add-on")
|
||||||
async def async_stop_addon(self) -> None:
|
async def async_stop_addon(self) -> None:
|
||||||
"""Stop the Z-Wave JS add-on."""
|
"""Stop the Z-Wave JS add-on."""
|
||||||
|
@ -23,9 +23,12 @@ from homeassistant.data_entry_flow import (
|
|||||||
)
|
)
|
||||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||||
|
|
||||||
|
from . import disconnect_client
|
||||||
from .addon import AddonError, AddonInfo, AddonManager, AddonState, get_addon_manager
|
from .addon import AddonError, AddonInfo, AddonManager, AddonState, get_addon_manager
|
||||||
from .const import (
|
from .const import (
|
||||||
CONF_ADDON_DEVICE,
|
CONF_ADDON_DEVICE,
|
||||||
|
CONF_ADDON_EMULATE_HARDWARE,
|
||||||
|
CONF_ADDON_LOG_LEVEL,
|
||||||
CONF_ADDON_NETWORK_KEY,
|
CONF_ADDON_NETWORK_KEY,
|
||||||
CONF_INTEGRATION_CREATED_ADDON,
|
CONF_INTEGRATION_CREATED_ADDON,
|
||||||
CONF_NETWORK_KEY,
|
CONF_NETWORK_KEY,
|
||||||
@ -41,8 +44,25 @@ TITLE = "Z-Wave JS"
|
|||||||
|
|
||||||
ADDON_SETUP_TIMEOUT = 5
|
ADDON_SETUP_TIMEOUT = 5
|
||||||
ADDON_SETUP_TIMEOUT_ROUNDS = 4
|
ADDON_SETUP_TIMEOUT_ROUNDS = 4
|
||||||
|
CONF_EMULATE_HARDWARE = "emulate_hardware"
|
||||||
|
CONF_LOG_LEVEL = "log_level"
|
||||||
SERVER_VERSION_TIMEOUT = 10
|
SERVER_VERSION_TIMEOUT = 10
|
||||||
|
|
||||||
|
ADDON_LOG_LEVELS = {
|
||||||
|
"error": "Error",
|
||||||
|
"warn": "Warn",
|
||||||
|
"info": "Info",
|
||||||
|
"verbose": "Verbose",
|
||||||
|
"debug": "Debug",
|
||||||
|
"silly": "Silly",
|
||||||
|
}
|
||||||
|
ADDON_USER_INPUT_MAP = {
|
||||||
|
CONF_ADDON_DEVICE: CONF_USB_PATH,
|
||||||
|
CONF_ADDON_NETWORK_KEY: CONF_NETWORK_KEY,
|
||||||
|
CONF_ADDON_LOG_LEVEL: CONF_LOG_LEVEL,
|
||||||
|
CONF_ADDON_EMULATE_HARDWARE: CONF_EMULATE_HARDWARE,
|
||||||
|
}
|
||||||
|
|
||||||
ON_SUPERVISOR_SCHEMA = vol.Schema({vol.Optional(CONF_USE_ADDON, default=True): bool})
|
ON_SUPERVISOR_SCHEMA = vol.Schema({vol.Optional(CONF_USE_ADDON, default=True): bool})
|
||||||
|
|
||||||
|
|
||||||
@ -52,6 +72,12 @@ def get_manual_schema(user_input: dict[str, Any]) -> vol.Schema:
|
|||||||
return vol.Schema({vol.Required(CONF_URL, default=default_url): str})
|
return vol.Schema({vol.Required(CONF_URL, default=default_url): str})
|
||||||
|
|
||||||
|
|
||||||
|
def get_on_supervisor_schema(user_input: dict[str, Any]) -> vol.Schema:
|
||||||
|
"""Return a schema for the on Supervisor step."""
|
||||||
|
default_use_addon = user_input[CONF_USE_ADDON]
|
||||||
|
return vol.Schema({vol.Optional(CONF_USE_ADDON, default=default_use_addon): bool})
|
||||||
|
|
||||||
|
|
||||||
async def validate_input(hass: HomeAssistant, user_input: dict) -> VersionInfo:
|
async def validate_input(hass: HomeAssistant, user_input: dict) -> VersionInfo:
|
||||||
"""Validate if the user input allows us to connect."""
|
"""Validate if the user input allows us to connect."""
|
||||||
ws_address = user_input[CONF_URL]
|
ws_address = user_input[CONF_URL]
|
||||||
@ -89,6 +115,7 @@ class BaseZwaveJSFlow(FlowHandler):
|
|||||||
self.network_key: str | None = None
|
self.network_key: str | None = None
|
||||||
self.usb_path: str | None = None
|
self.usb_path: str | None = None
|
||||||
self.ws_address: str | None = None
|
self.ws_address: str | None = None
|
||||||
|
self.restart_addon: bool = False
|
||||||
# If we install the add-on we should uninstall it on entry remove.
|
# If we install the add-on we should uninstall it on entry remove.
|
||||||
self.integration_created_addon = False
|
self.integration_created_addon = False
|
||||||
self.install_task: asyncio.Task | None = None
|
self.install_task: asyncio.Task | None = None
|
||||||
@ -159,7 +186,10 @@ class BaseZwaveJSFlow(FlowHandler):
|
|||||||
addon_manager: AddonManager = get_addon_manager(self.hass)
|
addon_manager: AddonManager = get_addon_manager(self.hass)
|
||||||
self.version_info = None
|
self.version_info = None
|
||||||
try:
|
try:
|
||||||
await addon_manager.async_schedule_start_addon()
|
if self.restart_addon:
|
||||||
|
await addon_manager.async_schedule_restart_addon()
|
||||||
|
else:
|
||||||
|
await addon_manager.async_schedule_start_addon()
|
||||||
# Sleep some seconds to let the add-on start properly before connecting.
|
# Sleep some seconds to let the add-on start properly before connecting.
|
||||||
for _ in range(ADDON_SETUP_TIMEOUT_ROUNDS):
|
for _ in range(ADDON_SETUP_TIMEOUT_ROUNDS):
|
||||||
await asyncio.sleep(ADDON_SETUP_TIMEOUT)
|
await asyncio.sleep(ADDON_SETUP_TIMEOUT)
|
||||||
@ -262,6 +292,14 @@ class ConfigFlow(BaseZwaveJSFlow, config_entries.ConfigFlow, domain=DOMAIN):
|
|||||||
"""Return the correct flow manager."""
|
"""Return the correct flow manager."""
|
||||||
return self.hass.config_entries.flow
|
return self.hass.config_entries.flow
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
@callback
|
||||||
|
def async_get_options_flow(
|
||||||
|
config_entry: config_entries.ConfigEntry,
|
||||||
|
) -> OptionsFlowHandler:
|
||||||
|
"""Return the options flow."""
|
||||||
|
return OptionsFlowHandler(config_entry)
|
||||||
|
|
||||||
async def async_step_user(
|
async def async_step_user(
|
||||||
self, user_input: dict[str, Any] | None = None
|
self, user_input: dict[str, Any] | None = None
|
||||||
) -> FlowResult:
|
) -> FlowResult:
|
||||||
@ -445,6 +483,235 @@ class ConfigFlow(BaseZwaveJSFlow, config_entries.ConfigFlow, domain=DOMAIN):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class OptionsFlowHandler(BaseZwaveJSFlow, config_entries.OptionsFlow):
|
||||||
|
"""Handle an options flow for Z-Wave JS."""
|
||||||
|
|
||||||
|
def __init__(self, config_entry: config_entries.ConfigEntry) -> None:
|
||||||
|
"""Set up the options flow."""
|
||||||
|
super().__init__()
|
||||||
|
self.config_entry = config_entry
|
||||||
|
self.original_addon_config: dict[str, Any] | None = None
|
||||||
|
self.revert_reason: str | None = None
|
||||||
|
|
||||||
|
@property
|
||||||
|
def flow_manager(self) -> config_entries.OptionsFlowManager:
|
||||||
|
"""Return the correct flow manager."""
|
||||||
|
return self.hass.config_entries.options
|
||||||
|
|
||||||
|
@callback
|
||||||
|
def _async_update_entry(self, data: dict[str, Any]) -> None:
|
||||||
|
"""Update the config entry with new data."""
|
||||||
|
self.hass.config_entries.async_update_entry(self.config_entry, data=data)
|
||||||
|
|
||||||
|
async def async_step_init(
|
||||||
|
self, user_input: dict[str, Any] | None = None
|
||||||
|
) -> FlowResult:
|
||||||
|
"""Manage the options."""
|
||||||
|
if is_hassio(self.hass):
|
||||||
|
return await self.async_step_on_supervisor()
|
||||||
|
|
||||||
|
return await self.async_step_manual()
|
||||||
|
|
||||||
|
async def async_step_manual(
|
||||||
|
self, user_input: dict[str, Any] | None = None
|
||||||
|
) -> FlowResult:
|
||||||
|
"""Handle a manual configuration."""
|
||||||
|
if user_input is None:
|
||||||
|
return self.async_show_form(
|
||||||
|
step_id="manual",
|
||||||
|
data_schema=get_manual_schema(
|
||||||
|
{CONF_URL: self.config_entry.data[CONF_URL]}
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
errors = {}
|
||||||
|
|
||||||
|
try:
|
||||||
|
version_info = await validate_input(self.hass, user_input)
|
||||||
|
except InvalidInput as err:
|
||||||
|
errors["base"] = err.error
|
||||||
|
except Exception: # pylint: disable=broad-except
|
||||||
|
_LOGGER.exception("Unexpected exception")
|
||||||
|
errors["base"] = "unknown"
|
||||||
|
else:
|
||||||
|
if self.config_entry.unique_id != version_info.home_id:
|
||||||
|
return self.async_abort(reason="different_device")
|
||||||
|
|
||||||
|
# Make sure we disable any add-on handling
|
||||||
|
# if the controller is reconfigured in a manual step.
|
||||||
|
self._async_update_entry(
|
||||||
|
{
|
||||||
|
**self.config_entry.data,
|
||||||
|
**user_input,
|
||||||
|
CONF_USE_ADDON: False,
|
||||||
|
CONF_INTEGRATION_CREATED_ADDON: False,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
self.hass.async_create_task(
|
||||||
|
self.hass.config_entries.async_reload(self.config_entry.entry_id)
|
||||||
|
)
|
||||||
|
return self.async_create_entry(title=TITLE, data={})
|
||||||
|
|
||||||
|
return self.async_show_form(
|
||||||
|
step_id="manual", data_schema=get_manual_schema(user_input), errors=errors
|
||||||
|
)
|
||||||
|
|
||||||
|
async def async_step_on_supervisor(
|
||||||
|
self, user_input: dict[str, Any] | None = None
|
||||||
|
) -> FlowResult:
|
||||||
|
"""Handle logic when on Supervisor host."""
|
||||||
|
if user_input is None:
|
||||||
|
return self.async_show_form(
|
||||||
|
step_id="on_supervisor",
|
||||||
|
data_schema=get_on_supervisor_schema(
|
||||||
|
{CONF_USE_ADDON: self.config_entry.data.get(CONF_USE_ADDON, True)}
|
||||||
|
),
|
||||||
|
)
|
||||||
|
if not user_input[CONF_USE_ADDON]:
|
||||||
|
return await self.async_step_manual()
|
||||||
|
|
||||||
|
addon_info = await self._async_get_addon_info()
|
||||||
|
|
||||||
|
if addon_info.state == AddonState.NOT_INSTALLED:
|
||||||
|
return await self.async_step_install_addon()
|
||||||
|
|
||||||
|
return await self.async_step_configure_addon()
|
||||||
|
|
||||||
|
async def async_step_configure_addon(
|
||||||
|
self, user_input: dict[str, Any] | None = None
|
||||||
|
) -> FlowResult:
|
||||||
|
"""Ask for config for Z-Wave JS add-on."""
|
||||||
|
addon_info = await self._async_get_addon_info()
|
||||||
|
addon_config = addon_info.options
|
||||||
|
|
||||||
|
if user_input is not None:
|
||||||
|
self.network_key = user_input[CONF_NETWORK_KEY]
|
||||||
|
self.usb_path = user_input[CONF_USB_PATH]
|
||||||
|
|
||||||
|
new_addon_config = {
|
||||||
|
**addon_config,
|
||||||
|
CONF_ADDON_DEVICE: self.usb_path,
|
||||||
|
CONF_ADDON_NETWORK_KEY: self.network_key,
|
||||||
|
CONF_ADDON_LOG_LEVEL: user_input[CONF_LOG_LEVEL],
|
||||||
|
CONF_ADDON_EMULATE_HARDWARE: user_input[CONF_EMULATE_HARDWARE],
|
||||||
|
}
|
||||||
|
|
||||||
|
if new_addon_config != addon_config:
|
||||||
|
if addon_info.state == AddonState.RUNNING:
|
||||||
|
self.restart_addon = True
|
||||||
|
# Copy the add-on config to keep the objects separate.
|
||||||
|
self.original_addon_config = dict(addon_config)
|
||||||
|
await self._async_set_addon_config(new_addon_config)
|
||||||
|
|
||||||
|
if addon_info.state == AddonState.RUNNING and not self.restart_addon:
|
||||||
|
return await self.async_step_finish_addon_setup()
|
||||||
|
|
||||||
|
if (
|
||||||
|
self.config_entry.data.get(CONF_USE_ADDON)
|
||||||
|
and self.config_entry.state == config_entries.ConfigEntryState.LOADED
|
||||||
|
):
|
||||||
|
# Disconnect integration before restarting add-on.
|
||||||
|
await disconnect_client(self.hass, self.config_entry)
|
||||||
|
|
||||||
|
return await self.async_step_start_addon()
|
||||||
|
|
||||||
|
usb_path = addon_config.get(CONF_ADDON_DEVICE, self.usb_path or "")
|
||||||
|
network_key = addon_config.get(CONF_ADDON_NETWORK_KEY, self.network_key or "")
|
||||||
|
log_level = addon_config.get(CONF_ADDON_LOG_LEVEL, "info")
|
||||||
|
emulate_hardware = addon_config.get(CONF_ADDON_EMULATE_HARDWARE, False)
|
||||||
|
|
||||||
|
data_schema = vol.Schema(
|
||||||
|
{
|
||||||
|
vol.Required(CONF_USB_PATH, default=usb_path): str,
|
||||||
|
vol.Optional(CONF_NETWORK_KEY, default=network_key): str,
|
||||||
|
vol.Optional(CONF_LOG_LEVEL, default=log_level): vol.In(
|
||||||
|
ADDON_LOG_LEVELS
|
||||||
|
),
|
||||||
|
vol.Optional(CONF_EMULATE_HARDWARE, default=emulate_hardware): bool,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
return self.async_show_form(step_id="configure_addon", data_schema=data_schema)
|
||||||
|
|
||||||
|
async def async_step_start_failed(
|
||||||
|
self, user_input: dict[str, Any] | None = None
|
||||||
|
) -> FlowResult:
|
||||||
|
"""Add-on start failed."""
|
||||||
|
return await self.async_revert_addon_config(reason="addon_start_failed")
|
||||||
|
|
||||||
|
async def async_step_finish_addon_setup(
|
||||||
|
self, user_input: dict[str, Any] | None = None
|
||||||
|
) -> FlowResult:
|
||||||
|
"""Prepare info needed to complete the config entry update.
|
||||||
|
|
||||||
|
Get add-on discovery info and server version info.
|
||||||
|
Check for same unique id and abort if not the same unique id.
|
||||||
|
"""
|
||||||
|
if self.revert_reason:
|
||||||
|
self.original_addon_config = None
|
||||||
|
reason = self.revert_reason
|
||||||
|
self.revert_reason = None
|
||||||
|
return await self.async_revert_addon_config(reason=reason)
|
||||||
|
|
||||||
|
if not self.ws_address:
|
||||||
|
discovery_info = await self._async_get_addon_discovery_info()
|
||||||
|
self.ws_address = f"ws://{discovery_info['host']}:{discovery_info['port']}"
|
||||||
|
|
||||||
|
if not self.version_info:
|
||||||
|
try:
|
||||||
|
self.version_info = await async_get_version_info(
|
||||||
|
self.hass, self.ws_address
|
||||||
|
)
|
||||||
|
except CannotConnect:
|
||||||
|
return await self.async_revert_addon_config(reason="cannot_connect")
|
||||||
|
|
||||||
|
if self.config_entry.unique_id != self.version_info.home_id:
|
||||||
|
return await self.async_revert_addon_config(reason="different_device")
|
||||||
|
|
||||||
|
self._async_update_entry(
|
||||||
|
{
|
||||||
|
**self.config_entry.data,
|
||||||
|
CONF_URL: self.ws_address,
|
||||||
|
CONF_USB_PATH: self.usb_path,
|
||||||
|
CONF_NETWORK_KEY: self.network_key,
|
||||||
|
CONF_USE_ADDON: True,
|
||||||
|
CONF_INTEGRATION_CREATED_ADDON: self.integration_created_addon,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
# Always reload entry since we may have disconnected the client.
|
||||||
|
self.hass.async_create_task(
|
||||||
|
self.hass.config_entries.async_reload(self.config_entry.entry_id)
|
||||||
|
)
|
||||||
|
return self.async_create_entry(title=TITLE, data={})
|
||||||
|
|
||||||
|
async def async_revert_addon_config(self, reason: str) -> FlowResult:
|
||||||
|
"""Abort the options flow.
|
||||||
|
|
||||||
|
If the add-on options have been changed, revert those and restart add-on.
|
||||||
|
"""
|
||||||
|
# If reverting the add-on options failed, abort immediately.
|
||||||
|
if self.revert_reason:
|
||||||
|
_LOGGER.error(
|
||||||
|
"Failed to revert add-on options before aborting flow, reason: %s",
|
||||||
|
reason,
|
||||||
|
)
|
||||||
|
|
||||||
|
if self.revert_reason or not self.original_addon_config:
|
||||||
|
self.hass.async_create_task(
|
||||||
|
self.hass.config_entries.async_reload(self.config_entry.entry_id)
|
||||||
|
)
|
||||||
|
return self.async_abort(reason=reason)
|
||||||
|
|
||||||
|
self.revert_reason = reason
|
||||||
|
addon_config_input = {
|
||||||
|
ADDON_USER_INPUT_MAP[addon_key]: addon_val
|
||||||
|
for addon_key, addon_val in self.original_addon_config.items()
|
||||||
|
}
|
||||||
|
_LOGGER.debug("Reverting add-on options, reason: %s", reason)
|
||||||
|
return await self.async_step_configure_addon(addon_config_input)
|
||||||
|
|
||||||
|
|
||||||
class CannotConnect(exceptions.HomeAssistantError):
|
class CannotConnect(exceptions.HomeAssistantError):
|
||||||
"""Indicate connection error."""
|
"""Indicate connection error."""
|
||||||
|
|
||||||
|
@ -2,6 +2,8 @@
|
|||||||
import logging
|
import logging
|
||||||
|
|
||||||
CONF_ADDON_DEVICE = "device"
|
CONF_ADDON_DEVICE = "device"
|
||||||
|
CONF_ADDON_EMULATE_HARDWARE = "emulate_hardware"
|
||||||
|
CONF_ADDON_LOG_LEVEL = "log_level"
|
||||||
CONF_ADDON_NETWORK_KEY = "network_key"
|
CONF_ADDON_NETWORK_KEY = "network_key"
|
||||||
CONF_INTEGRATION_CREATED_ADDON = "integration_created_addon"
|
CONF_INTEGRATION_CREATED_ADDON = "integration_created_addon"
|
||||||
CONF_NETWORK_KEY = "network_key"
|
CONF_NETWORK_KEY = "network_key"
|
||||||
|
@ -47,5 +47,51 @@
|
|||||||
"install_addon": "Please wait while the Z-Wave JS add-on installation finishes. This can take several minutes.",
|
"install_addon": "Please wait while the Z-Wave JS add-on installation finishes. This can take several minutes.",
|
||||||
"start_addon": "Please wait while the Z-Wave JS add-on start completes. This may take some seconds."
|
"start_addon": "Please wait while the Z-Wave JS add-on start completes. This may take some seconds."
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"options": {
|
||||||
|
"step": {
|
||||||
|
"manual": {
|
||||||
|
"data": {
|
||||||
|
"url": "[%key:common::config_flow::data::url%]"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"on_supervisor": {
|
||||||
|
"title": "Select connection method",
|
||||||
|
"description": "Do you want to use the Z-Wave JS Supervisor add-on?",
|
||||||
|
"data": { "use_addon": "Use the Z-Wave JS Supervisor add-on" }
|
||||||
|
},
|
||||||
|
"install_addon": {
|
||||||
|
"title": "The Z-Wave JS add-on installation has started"
|
||||||
|
},
|
||||||
|
"configure_addon": {
|
||||||
|
"title": "Enter the Z-Wave JS add-on configuration",
|
||||||
|
"data": {
|
||||||
|
"usb_path": "[%key:common::config_flow::data::usb_path%]",
|
||||||
|
"network_key": "Network Key",
|
||||||
|
"log_level": "Log level",
|
||||||
|
"emulate_hardware": "Emulate Hardware"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"start_addon": { "title": "The Z-Wave JS add-on is starting." }
|
||||||
|
},
|
||||||
|
"error": {
|
||||||
|
"invalid_ws_url": "Invalid websocket URL",
|
||||||
|
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
|
||||||
|
"unknown": "[%key:common::config_flow::error::unknown%]"
|
||||||
|
},
|
||||||
|
"abort": {
|
||||||
|
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]",
|
||||||
|
"addon_info_failed": "Failed to get Z-Wave JS add-on info.",
|
||||||
|
"addon_install_failed": "Failed to install the Z-Wave JS add-on.",
|
||||||
|
"addon_set_config_failed": "Failed to set Z-Wave JS configuration.",
|
||||||
|
"addon_start_failed": "Failed to start the Z-Wave JS add-on.",
|
||||||
|
"addon_get_discovery_info_failed": "Failed to get Z-Wave JS add-on discovery info.",
|
||||||
|
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
|
||||||
|
"different_device": "The connected USB device is not the same as previously configured for this config entry. Please instead create a new config entry for the new device."
|
||||||
|
},
|
||||||
|
"progress": {
|
||||||
|
"install_addon": "Please wait while the Z-Wave JS add-on installation finishes. This can take several minutes.",
|
||||||
|
"start_addon": "Please wait while the Z-Wave JS add-on start completes. This may take some seconds."
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -51,5 +51,55 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"options": {
|
||||||
|
"abort": {
|
||||||
|
"addon_get_discovery_info_failed": "Failed to get Z-Wave JS add-on discovery info.",
|
||||||
|
"addon_info_failed": "Failed to get Z-Wave JS add-on info.",
|
||||||
|
"addon_install_failed": "Failed to install the Z-Wave JS add-on.",
|
||||||
|
"addon_set_config_failed": "Failed to set Z-Wave JS configuration.",
|
||||||
|
"addon_start_failed": "Failed to start the Z-Wave JS add-on.",
|
||||||
|
"already_configured": "Device is already configured",
|
||||||
|
"cannot_connect": "Failed to connect",
|
||||||
|
"different_device": "The connected USB device is not the same as previously configured for this config entry. Please instead create a new config entry for the new device."
|
||||||
|
},
|
||||||
|
"error": {
|
||||||
|
"cannot_connect": "Failed to connect",
|
||||||
|
"invalid_ws_url": "Invalid websocket URL",
|
||||||
|
"unknown": "Unexpected error"
|
||||||
|
},
|
||||||
|
"progress": {
|
||||||
|
"install_addon": "Please wait while the Z-Wave JS add-on installation finishes. This can take several minutes.",
|
||||||
|
"start_addon": "Please wait while the Z-Wave JS add-on start completes. This may take some seconds."
|
||||||
|
},
|
||||||
|
"step": {
|
||||||
|
"configure_addon": {
|
||||||
|
"data": {
|
||||||
|
"emulate_hardware": "Emulate Hardware",
|
||||||
|
"log_level": "Log level",
|
||||||
|
"network_key": "Network Key",
|
||||||
|
"usb_path": "USB Device Path"
|
||||||
|
},
|
||||||
|
"title": "Enter the Z-Wave JS add-on configuration"
|
||||||
|
},
|
||||||
|
"install_addon": {
|
||||||
|
"title": "The Z-Wave JS add-on installation has started"
|
||||||
|
},
|
||||||
|
"manual": {
|
||||||
|
"data": {
|
||||||
|
"url": "URL"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"on_supervisor": {
|
||||||
|
"data": {
|
||||||
|
"use_addon": "Use the Z-Wave JS Supervisor add-on"
|
||||||
|
},
|
||||||
|
"description": "Do you want to use the Z-Wave JS Supervisor add-on?",
|
||||||
|
"title": "Select connection method"
|
||||||
|
},
|
||||||
|
"start_addon": {
|
||||||
|
"title": "The Z-Wave JS add-on is starting."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"title": "Z-Wave JS"
|
"title": "Z-Wave JS"
|
||||||
}
|
}
|
@ -60,9 +60,14 @@ def mock_addon_options(addon_info):
|
|||||||
|
|
||||||
|
|
||||||
@pytest.fixture(name="set_addon_options_side_effect")
|
@pytest.fixture(name="set_addon_options_side_effect")
|
||||||
def set_addon_options_side_effect_fixture():
|
def set_addon_options_side_effect_fixture(addon_options):
|
||||||
"""Return the set add-on options side effect."""
|
"""Return the set add-on options side effect."""
|
||||||
return None
|
|
||||||
|
async def set_addon_options(hass, slug, options):
|
||||||
|
"""Mock set add-on options."""
|
||||||
|
addon_options.update(options["options"])
|
||||||
|
|
||||||
|
return set_addon_options
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture(name="set_addon_options")
|
@pytest.fixture(name="set_addon_options")
|
||||||
@ -75,11 +80,24 @@ def mock_set_addon_options(set_addon_options_side_effect):
|
|||||||
yield set_options
|
yield set_options
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(name="install_addon_side_effect")
|
||||||
|
def install_addon_side_effect_fixture(addon_info):
|
||||||
|
"""Return the install add-on side effect."""
|
||||||
|
|
||||||
|
async def install_addon(hass, slug):
|
||||||
|
"""Mock install add-on."""
|
||||||
|
addon_info.return_value["state"] = "stopped"
|
||||||
|
addon_info.return_value["version"] = "1.0"
|
||||||
|
|
||||||
|
return install_addon
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture(name="install_addon")
|
@pytest.fixture(name="install_addon")
|
||||||
def mock_install_addon():
|
def mock_install_addon(install_addon_side_effect):
|
||||||
"""Mock install add-on."""
|
"""Mock install add-on."""
|
||||||
with patch(
|
with patch(
|
||||||
"homeassistant.components.zwave_js.addon.async_install_addon"
|
"homeassistant.components.zwave_js.addon.async_install_addon",
|
||||||
|
side_effect=install_addon_side_effect,
|
||||||
) as install_addon:
|
) as install_addon:
|
||||||
yield install_addon
|
yield install_addon
|
||||||
|
|
||||||
@ -94,9 +112,14 @@ def mock_update_addon():
|
|||||||
|
|
||||||
|
|
||||||
@pytest.fixture(name="start_addon_side_effect")
|
@pytest.fixture(name="start_addon_side_effect")
|
||||||
def start_addon_side_effect_fixture():
|
def start_addon_side_effect_fixture(addon_info):
|
||||||
"""Return the set add-on options side effect."""
|
"""Return the start add-on options side effect."""
|
||||||
return None
|
|
||||||
|
async def start_addon(hass, slug):
|
||||||
|
"""Mock start add-on."""
|
||||||
|
addon_info.return_value["state"] = "started"
|
||||||
|
|
||||||
|
return start_addon
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture(name="start_addon")
|
@pytest.fixture(name="start_addon")
|
||||||
@ -118,6 +141,22 @@ def stop_addon_fixture():
|
|||||||
yield stop_addon
|
yield stop_addon
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(name="restart_addon_side_effect")
|
||||||
|
def restart_addon_side_effect_fixture():
|
||||||
|
"""Return the restart add-on options side effect."""
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(name="restart_addon")
|
||||||
|
def mock_restart_addon(restart_addon_side_effect):
|
||||||
|
"""Mock restart add-on."""
|
||||||
|
with patch(
|
||||||
|
"homeassistant.components.zwave_js.addon.async_restart_addon",
|
||||||
|
side_effect=restart_addon_side_effect,
|
||||||
|
) as restart_addon:
|
||||||
|
yield restart_addon
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture(name="uninstall_addon")
|
@pytest.fixture(name="uninstall_addon")
|
||||||
def uninstall_addon_fixture():
|
def uninstall_addon_fixture():
|
||||||
"""Mock uninstall add-on."""
|
"""Mock uninstall add-on."""
|
||||||
|
@ -2,6 +2,7 @@
|
|||||||
import asyncio
|
import asyncio
|
||||||
from unittest.mock import DEFAULT, call, patch
|
from unittest.mock import DEFAULT, call, patch
|
||||||
|
|
||||||
|
import aiohttp
|
||||||
import pytest
|
import pytest
|
||||||
from zwave_js_server.version import VersionInfo
|
from zwave_js_server.version import VersionInfo
|
||||||
|
|
||||||
@ -19,6 +20,21 @@ ADDON_DISCOVERY_INFO = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(name="persistent_notification", autouse=True)
|
||||||
|
async def setup_persistent_notification(hass):
|
||||||
|
"""Set up persistent notification integration."""
|
||||||
|
await setup.async_setup_component(hass, "persistent_notification", {})
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(name="setup_entry")
|
||||||
|
def setup_entry_fixture():
|
||||||
|
"""Mock entry setup."""
|
||||||
|
with patch(
|
||||||
|
"homeassistant.components.zwave_js.async_setup_entry", return_value=True
|
||||||
|
) as mock_setup_entry:
|
||||||
|
yield mock_setup_entry
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture(name="supervisor")
|
@pytest.fixture(name="supervisor")
|
||||||
def mock_supervisor_fixture():
|
def mock_supervisor_fixture():
|
||||||
"""Mock Supervisor."""
|
"""Mock Supervisor."""
|
||||||
@ -134,6 +150,19 @@ async def slow_server_version(*args):
|
|||||||
await asyncio.sleep(0.1)
|
await asyncio.sleep(0.1)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"flow, flow_params",
|
||||||
|
[
|
||||||
|
(
|
||||||
|
"flow",
|
||||||
|
lambda entry: {
|
||||||
|
"handler": DOMAIN,
|
||||||
|
"context": {"source": config_entries.SOURCE_USER},
|
||||||
|
},
|
||||||
|
),
|
||||||
|
("options", lambda entry: {"handler": entry.entry_id}),
|
||||||
|
],
|
||||||
|
)
|
||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
"url, server_version_side_effect, server_version_timeout, error",
|
"url, server_version_side_effect, server_version_timeout, error",
|
||||||
[
|
[
|
||||||
@ -157,20 +186,15 @@ async def slow_server_version(*args):
|
|||||||
),
|
),
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
async def test_manual_errors(
|
async def test_manual_errors(hass, integration, url, error, flow, flow_params):
|
||||||
hass,
|
|
||||||
url,
|
|
||||||
error,
|
|
||||||
):
|
|
||||||
"""Test all errors with a manual set up."""
|
"""Test all errors with a manual set up."""
|
||||||
result = await hass.config_entries.flow.async_init(
|
entry = integration
|
||||||
DOMAIN, context={"source": config_entries.SOURCE_USER}
|
result = await getattr(hass.config_entries, flow).async_init(**flow_params(entry))
|
||||||
)
|
|
||||||
|
|
||||||
assert result["type"] == "form"
|
assert result["type"] == "form"
|
||||||
assert result["step_id"] == "manual"
|
assert result["step_id"] == "manual"
|
||||||
|
|
||||||
result = await hass.config_entries.flow.async_configure(
|
result = await getattr(hass.config_entries, flow).async_configure(
|
||||||
result["flow_id"],
|
result["flow_id"],
|
||||||
{
|
{
|
||||||
"url": url,
|
"url": url,
|
||||||
@ -1059,3 +1083,714 @@ async def test_install_addon_failure(hass, supervisor, addon_installed, install_
|
|||||||
|
|
||||||
assert result["type"] == "abort"
|
assert result["type"] == "abort"
|
||||||
assert result["reason"] == "addon_install_failed"
|
assert result["reason"] == "addon_install_failed"
|
||||||
|
|
||||||
|
|
||||||
|
async def test_options_manual(hass, client, integration):
|
||||||
|
"""Test manual settings in options flow."""
|
||||||
|
entry = integration
|
||||||
|
entry.unique_id = 1234
|
||||||
|
|
||||||
|
assert client.connect.call_count == 1
|
||||||
|
assert client.disconnect.call_count == 0
|
||||||
|
|
||||||
|
result = await hass.config_entries.options.async_init(entry.entry_id)
|
||||||
|
|
||||||
|
assert result["type"] == "form"
|
||||||
|
assert result["step_id"] == "manual"
|
||||||
|
|
||||||
|
result = await hass.config_entries.options.async_configure(
|
||||||
|
result["flow_id"], {"url": "ws://1.1.1.1:3001"}
|
||||||
|
)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
assert result["type"] == "create_entry"
|
||||||
|
assert entry.data["url"] == "ws://1.1.1.1:3001"
|
||||||
|
assert entry.data["use_addon"] is False
|
||||||
|
assert entry.data["integration_created_addon"] is False
|
||||||
|
assert client.connect.call_count == 2
|
||||||
|
assert client.disconnect.call_count == 1
|
||||||
|
|
||||||
|
|
||||||
|
async def test_options_manual_different_device(hass, integration):
|
||||||
|
"""Test options flow manual step connecting to different device."""
|
||||||
|
entry = integration
|
||||||
|
entry.unique_id = 5678
|
||||||
|
|
||||||
|
result = await hass.config_entries.options.async_init(entry.entry_id)
|
||||||
|
|
||||||
|
assert result["type"] == "form"
|
||||||
|
assert result["step_id"] == "manual"
|
||||||
|
|
||||||
|
result = await hass.config_entries.options.async_configure(
|
||||||
|
result["flow_id"], {"url": "ws://1.1.1.1:3001"}
|
||||||
|
)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
assert result["type"] == "abort"
|
||||||
|
assert result["reason"] == "different_device"
|
||||||
|
|
||||||
|
|
||||||
|
async def test_options_not_addon(hass, client, supervisor, integration):
|
||||||
|
"""Test options flow and opting out of add-on on Supervisor."""
|
||||||
|
entry = integration
|
||||||
|
entry.unique_id = 1234
|
||||||
|
|
||||||
|
assert client.connect.call_count == 1
|
||||||
|
assert client.disconnect.call_count == 0
|
||||||
|
|
||||||
|
result = await hass.config_entries.options.async_init(entry.entry_id)
|
||||||
|
|
||||||
|
assert result["type"] == "form"
|
||||||
|
assert result["step_id"] == "on_supervisor"
|
||||||
|
|
||||||
|
result = await hass.config_entries.options.async_configure(
|
||||||
|
result["flow_id"], {"use_addon": False}
|
||||||
|
)
|
||||||
|
|
||||||
|
assert result["type"] == "form"
|
||||||
|
assert result["step_id"] == "manual"
|
||||||
|
|
||||||
|
result = await hass.config_entries.options.async_configure(
|
||||||
|
result["flow_id"],
|
||||||
|
{
|
||||||
|
"url": "ws://localhost:3000",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
assert result["type"] == "create_entry"
|
||||||
|
assert entry.data["url"] == "ws://localhost:3000"
|
||||||
|
assert entry.data["use_addon"] is False
|
||||||
|
assert entry.data["integration_created_addon"] is False
|
||||||
|
assert client.connect.call_count == 2
|
||||||
|
assert client.disconnect.call_count == 1
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"discovery_info, entry_data, old_addon_options, new_addon_options, disconnect_calls",
|
||||||
|
[
|
||||||
|
(
|
||||||
|
{"config": ADDON_DISCOVERY_INFO},
|
||||||
|
{},
|
||||||
|
{"device": "/test", "network_key": "abc123"},
|
||||||
|
{
|
||||||
|
"usb_path": "/new",
|
||||||
|
"network_key": "new123",
|
||||||
|
"log_level": "info",
|
||||||
|
"emulate_hardware": False,
|
||||||
|
},
|
||||||
|
0,
|
||||||
|
),
|
||||||
|
(
|
||||||
|
{"config": ADDON_DISCOVERY_INFO},
|
||||||
|
{"use_addon": True},
|
||||||
|
{"device": "/test", "network_key": "abc123"},
|
||||||
|
{
|
||||||
|
"usb_path": "/new",
|
||||||
|
"network_key": "new123",
|
||||||
|
"log_level": "info",
|
||||||
|
"emulate_hardware": False,
|
||||||
|
},
|
||||||
|
1,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
async def test_options_addon_running(
|
||||||
|
hass,
|
||||||
|
client,
|
||||||
|
supervisor,
|
||||||
|
integration,
|
||||||
|
addon_running,
|
||||||
|
addon_options,
|
||||||
|
set_addon_options,
|
||||||
|
restart_addon,
|
||||||
|
get_addon_discovery_info,
|
||||||
|
discovery_info,
|
||||||
|
entry_data,
|
||||||
|
old_addon_options,
|
||||||
|
new_addon_options,
|
||||||
|
disconnect_calls,
|
||||||
|
):
|
||||||
|
"""Test options flow and add-on already running on Supervisor."""
|
||||||
|
addon_options.update(old_addon_options)
|
||||||
|
entry = integration
|
||||||
|
entry.unique_id = 1234
|
||||||
|
data = {**entry.data, **entry_data}
|
||||||
|
hass.config_entries.async_update_entry(entry, data=data)
|
||||||
|
|
||||||
|
assert entry.data["url"] == "ws://test.org"
|
||||||
|
|
||||||
|
assert client.connect.call_count == 1
|
||||||
|
assert client.disconnect.call_count == 0
|
||||||
|
|
||||||
|
result = await hass.config_entries.options.async_init(entry.entry_id)
|
||||||
|
|
||||||
|
assert result["type"] == "form"
|
||||||
|
assert result["step_id"] == "on_supervisor"
|
||||||
|
|
||||||
|
result = await hass.config_entries.options.async_configure(
|
||||||
|
result["flow_id"], {"use_addon": True}
|
||||||
|
)
|
||||||
|
|
||||||
|
assert result["type"] == "form"
|
||||||
|
assert result["step_id"] == "configure_addon"
|
||||||
|
|
||||||
|
result = await hass.config_entries.options.async_configure(
|
||||||
|
result["flow_id"],
|
||||||
|
new_addon_options,
|
||||||
|
)
|
||||||
|
|
||||||
|
new_addon_options["device"] = new_addon_options.pop("usb_path")
|
||||||
|
assert set_addon_options.call_args == call(
|
||||||
|
hass,
|
||||||
|
"core_zwave_js",
|
||||||
|
{"options": new_addon_options},
|
||||||
|
)
|
||||||
|
assert client.disconnect.call_count == disconnect_calls
|
||||||
|
|
||||||
|
assert result["type"] == "progress"
|
||||||
|
assert result["step_id"] == "start_addon"
|
||||||
|
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
result = await hass.config_entries.options.async_configure(result["flow_id"])
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
assert restart_addon.call_args == call(hass, "core_zwave_js")
|
||||||
|
|
||||||
|
assert result["type"] == "create_entry"
|
||||||
|
assert entry.data["url"] == "ws://host1:3001"
|
||||||
|
assert entry.data["usb_path"] == new_addon_options["device"]
|
||||||
|
assert entry.data["network_key"] == new_addon_options["network_key"]
|
||||||
|
assert entry.data["use_addon"] is True
|
||||||
|
assert entry.data["integration_created_addon"] is False
|
||||||
|
assert client.connect.call_count == 2
|
||||||
|
assert client.disconnect.call_count == 1
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"discovery_info, entry_data, old_addon_options, new_addon_options",
|
||||||
|
[
|
||||||
|
(
|
||||||
|
{"config": ADDON_DISCOVERY_INFO},
|
||||||
|
{},
|
||||||
|
{
|
||||||
|
"device": "/test",
|
||||||
|
"network_key": "abc123",
|
||||||
|
"log_level": "info",
|
||||||
|
"emulate_hardware": False,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"usb_path": "/test",
|
||||||
|
"network_key": "abc123",
|
||||||
|
"log_level": "info",
|
||||||
|
"emulate_hardware": False,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
async def test_options_addon_running_no_changes(
|
||||||
|
hass,
|
||||||
|
client,
|
||||||
|
supervisor,
|
||||||
|
integration,
|
||||||
|
addon_running,
|
||||||
|
addon_options,
|
||||||
|
set_addon_options,
|
||||||
|
restart_addon,
|
||||||
|
get_addon_discovery_info,
|
||||||
|
discovery_info,
|
||||||
|
entry_data,
|
||||||
|
old_addon_options,
|
||||||
|
new_addon_options,
|
||||||
|
):
|
||||||
|
"""Test options flow without changes, and add-on already running on Supervisor."""
|
||||||
|
addon_options.update(old_addon_options)
|
||||||
|
entry = integration
|
||||||
|
entry.unique_id = 1234
|
||||||
|
data = {**entry.data, **entry_data}
|
||||||
|
hass.config_entries.async_update_entry(entry, data=data)
|
||||||
|
|
||||||
|
assert entry.data["url"] == "ws://test.org"
|
||||||
|
|
||||||
|
assert client.connect.call_count == 1
|
||||||
|
assert client.disconnect.call_count == 0
|
||||||
|
|
||||||
|
result = await hass.config_entries.options.async_init(entry.entry_id)
|
||||||
|
|
||||||
|
assert result["type"] == "form"
|
||||||
|
assert result["step_id"] == "on_supervisor"
|
||||||
|
|
||||||
|
result = await hass.config_entries.options.async_configure(
|
||||||
|
result["flow_id"], {"use_addon": True}
|
||||||
|
)
|
||||||
|
|
||||||
|
assert result["type"] == "form"
|
||||||
|
assert result["step_id"] == "configure_addon"
|
||||||
|
|
||||||
|
result = await hass.config_entries.options.async_configure(
|
||||||
|
result["flow_id"],
|
||||||
|
new_addon_options,
|
||||||
|
)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
new_addon_options["device"] = new_addon_options.pop("usb_path")
|
||||||
|
assert set_addon_options.call_count == 0
|
||||||
|
assert restart_addon.call_count == 0
|
||||||
|
|
||||||
|
assert result["type"] == "create_entry"
|
||||||
|
assert entry.data["url"] == "ws://host1:3001"
|
||||||
|
assert entry.data["usb_path"] == new_addon_options["device"]
|
||||||
|
assert entry.data["network_key"] == new_addon_options["network_key"]
|
||||||
|
assert entry.data["use_addon"] is True
|
||||||
|
assert entry.data["integration_created_addon"] is False
|
||||||
|
assert client.connect.call_count == 2
|
||||||
|
assert client.disconnect.call_count == 1
|
||||||
|
|
||||||
|
|
||||||
|
async def different_device_server_version(*args):
|
||||||
|
"""Return server version for a device with different home id."""
|
||||||
|
return VersionInfo(
|
||||||
|
driver_version="mock-driver-version",
|
||||||
|
server_version="mock-server-version",
|
||||||
|
home_id=5678,
|
||||||
|
min_schema_version=0,
|
||||||
|
max_schema_version=1,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"discovery_info, entry_data, old_addon_options, new_addon_options, disconnect_calls, server_version_side_effect",
|
||||||
|
[
|
||||||
|
(
|
||||||
|
{"config": ADDON_DISCOVERY_INFO},
|
||||||
|
{},
|
||||||
|
{
|
||||||
|
"device": "/test",
|
||||||
|
"network_key": "abc123",
|
||||||
|
"log_level": "info",
|
||||||
|
"emulate_hardware": False,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"usb_path": "/new",
|
||||||
|
"network_key": "new123",
|
||||||
|
"log_level": "info",
|
||||||
|
"emulate_hardware": False,
|
||||||
|
},
|
||||||
|
0,
|
||||||
|
different_device_server_version,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
async def test_options_different_device(
|
||||||
|
hass,
|
||||||
|
client,
|
||||||
|
supervisor,
|
||||||
|
integration,
|
||||||
|
addon_running,
|
||||||
|
addon_options,
|
||||||
|
set_addon_options,
|
||||||
|
restart_addon,
|
||||||
|
get_addon_discovery_info,
|
||||||
|
discovery_info,
|
||||||
|
entry_data,
|
||||||
|
old_addon_options,
|
||||||
|
new_addon_options,
|
||||||
|
disconnect_calls,
|
||||||
|
server_version_side_effect,
|
||||||
|
):
|
||||||
|
"""Test options flow and configuring a different device."""
|
||||||
|
addon_options.update(old_addon_options)
|
||||||
|
entry = integration
|
||||||
|
entry.unique_id = 1234
|
||||||
|
data = {**entry.data, **entry_data}
|
||||||
|
hass.config_entries.async_update_entry(entry, data=data)
|
||||||
|
|
||||||
|
assert entry.data["url"] == "ws://test.org"
|
||||||
|
|
||||||
|
assert client.connect.call_count == 1
|
||||||
|
assert client.disconnect.call_count == 0
|
||||||
|
|
||||||
|
result = await hass.config_entries.options.async_init(entry.entry_id)
|
||||||
|
|
||||||
|
assert result["type"] == "form"
|
||||||
|
assert result["step_id"] == "on_supervisor"
|
||||||
|
|
||||||
|
result = await hass.config_entries.options.async_configure(
|
||||||
|
result["flow_id"], {"use_addon": True}
|
||||||
|
)
|
||||||
|
|
||||||
|
assert result["type"] == "form"
|
||||||
|
assert result["step_id"] == "configure_addon"
|
||||||
|
|
||||||
|
result = await hass.config_entries.options.async_configure(
|
||||||
|
result["flow_id"],
|
||||||
|
new_addon_options,
|
||||||
|
)
|
||||||
|
|
||||||
|
assert set_addon_options.call_count == 1
|
||||||
|
new_addon_options["device"] = new_addon_options.pop("usb_path")
|
||||||
|
assert set_addon_options.call_args == call(
|
||||||
|
hass,
|
||||||
|
"core_zwave_js",
|
||||||
|
{"options": new_addon_options},
|
||||||
|
)
|
||||||
|
assert client.disconnect.call_count == disconnect_calls
|
||||||
|
assert result["type"] == "progress"
|
||||||
|
assert result["step_id"] == "start_addon"
|
||||||
|
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
assert restart_addon.call_count == 1
|
||||||
|
assert restart_addon.call_args == call(hass, "core_zwave_js")
|
||||||
|
|
||||||
|
result = await hass.config_entries.options.async_configure(result["flow_id"])
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
assert set_addon_options.call_count == 2
|
||||||
|
assert set_addon_options.call_args == call(
|
||||||
|
hass,
|
||||||
|
"core_zwave_js",
|
||||||
|
{"options": old_addon_options},
|
||||||
|
)
|
||||||
|
assert result["type"] == "progress"
|
||||||
|
assert result["step_id"] == "start_addon"
|
||||||
|
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
assert restart_addon.call_count == 2
|
||||||
|
assert restart_addon.call_args == call(hass, "core_zwave_js")
|
||||||
|
|
||||||
|
result = await hass.config_entries.options.async_configure(result["flow_id"])
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
assert result["type"] == "abort"
|
||||||
|
assert result["reason"] == "different_device"
|
||||||
|
assert entry.data == data
|
||||||
|
assert client.connect.call_count == 2
|
||||||
|
assert client.disconnect.call_count == 1
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"discovery_info, entry_data, old_addon_options, new_addon_options, disconnect_calls, restart_addon_side_effect",
|
||||||
|
[
|
||||||
|
(
|
||||||
|
{"config": ADDON_DISCOVERY_INFO},
|
||||||
|
{},
|
||||||
|
{
|
||||||
|
"device": "/test",
|
||||||
|
"network_key": "abc123",
|
||||||
|
"log_level": "info",
|
||||||
|
"emulate_hardware": False,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"usb_path": "/new",
|
||||||
|
"network_key": "new123",
|
||||||
|
"log_level": "info",
|
||||||
|
"emulate_hardware": False,
|
||||||
|
},
|
||||||
|
0,
|
||||||
|
[HassioAPIError(), None],
|
||||||
|
),
|
||||||
|
(
|
||||||
|
{"config": ADDON_DISCOVERY_INFO},
|
||||||
|
{},
|
||||||
|
{
|
||||||
|
"device": "/test",
|
||||||
|
"network_key": "abc123",
|
||||||
|
"log_level": "info",
|
||||||
|
"emulate_hardware": False,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"usb_path": "/new",
|
||||||
|
"network_key": "new123",
|
||||||
|
"log_level": "info",
|
||||||
|
"emulate_hardware": False,
|
||||||
|
},
|
||||||
|
0,
|
||||||
|
[
|
||||||
|
HassioAPIError(),
|
||||||
|
HassioAPIError(),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
async def test_options_addon_restart_failed(
|
||||||
|
hass,
|
||||||
|
client,
|
||||||
|
supervisor,
|
||||||
|
integration,
|
||||||
|
addon_running,
|
||||||
|
addon_options,
|
||||||
|
set_addon_options,
|
||||||
|
restart_addon,
|
||||||
|
get_addon_discovery_info,
|
||||||
|
discovery_info,
|
||||||
|
entry_data,
|
||||||
|
old_addon_options,
|
||||||
|
new_addon_options,
|
||||||
|
disconnect_calls,
|
||||||
|
restart_addon_side_effect,
|
||||||
|
):
|
||||||
|
"""Test options flow and add-on restart failure."""
|
||||||
|
addon_options.update(old_addon_options)
|
||||||
|
entry = integration
|
||||||
|
entry.unique_id = 1234
|
||||||
|
data = {**entry.data, **entry_data}
|
||||||
|
hass.config_entries.async_update_entry(entry, data=data)
|
||||||
|
|
||||||
|
assert entry.data["url"] == "ws://test.org"
|
||||||
|
|
||||||
|
assert client.connect.call_count == 1
|
||||||
|
assert client.disconnect.call_count == 0
|
||||||
|
|
||||||
|
result = await hass.config_entries.options.async_init(entry.entry_id)
|
||||||
|
|
||||||
|
assert result["type"] == "form"
|
||||||
|
assert result["step_id"] == "on_supervisor"
|
||||||
|
|
||||||
|
result = await hass.config_entries.options.async_configure(
|
||||||
|
result["flow_id"], {"use_addon": True}
|
||||||
|
)
|
||||||
|
|
||||||
|
assert result["type"] == "form"
|
||||||
|
assert result["step_id"] == "configure_addon"
|
||||||
|
|
||||||
|
result = await hass.config_entries.options.async_configure(
|
||||||
|
result["flow_id"],
|
||||||
|
new_addon_options,
|
||||||
|
)
|
||||||
|
|
||||||
|
assert set_addon_options.call_count == 1
|
||||||
|
new_addon_options["device"] = new_addon_options.pop("usb_path")
|
||||||
|
assert set_addon_options.call_args == call(
|
||||||
|
hass,
|
||||||
|
"core_zwave_js",
|
||||||
|
{"options": new_addon_options},
|
||||||
|
)
|
||||||
|
assert client.disconnect.call_count == disconnect_calls
|
||||||
|
assert result["type"] == "progress"
|
||||||
|
assert result["step_id"] == "start_addon"
|
||||||
|
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
assert restart_addon.call_count == 1
|
||||||
|
assert restart_addon.call_args == call(hass, "core_zwave_js")
|
||||||
|
|
||||||
|
result = await hass.config_entries.options.async_configure(result["flow_id"])
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
assert set_addon_options.call_count == 2
|
||||||
|
assert set_addon_options.call_args == call(
|
||||||
|
hass,
|
||||||
|
"core_zwave_js",
|
||||||
|
{"options": old_addon_options},
|
||||||
|
)
|
||||||
|
assert result["type"] == "progress"
|
||||||
|
assert result["step_id"] == "start_addon"
|
||||||
|
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
assert restart_addon.call_count == 2
|
||||||
|
assert restart_addon.call_args == call(hass, "core_zwave_js")
|
||||||
|
|
||||||
|
result = await hass.config_entries.options.async_configure(result["flow_id"])
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
assert result["type"] == "abort"
|
||||||
|
assert result["reason"] == "addon_start_failed"
|
||||||
|
assert entry.data == data
|
||||||
|
assert client.connect.call_count == 2
|
||||||
|
assert client.disconnect.call_count == 1
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"discovery_info, entry_data, old_addon_options, new_addon_options, disconnect_calls, server_version_side_effect",
|
||||||
|
[
|
||||||
|
(
|
||||||
|
{"config": ADDON_DISCOVERY_INFO},
|
||||||
|
{},
|
||||||
|
{
|
||||||
|
"device": "/test",
|
||||||
|
"network_key": "abc123",
|
||||||
|
"log_level": "info",
|
||||||
|
"emulate_hardware": False,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"usb_path": "/test",
|
||||||
|
"network_key": "abc123",
|
||||||
|
"log_level": "info",
|
||||||
|
"emulate_hardware": False,
|
||||||
|
},
|
||||||
|
0,
|
||||||
|
aiohttp.ClientError("Boom"),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
async def test_options_addon_running_server_info_failure(
|
||||||
|
hass,
|
||||||
|
client,
|
||||||
|
supervisor,
|
||||||
|
integration,
|
||||||
|
addon_running,
|
||||||
|
addon_options,
|
||||||
|
set_addon_options,
|
||||||
|
restart_addon,
|
||||||
|
get_addon_discovery_info,
|
||||||
|
discovery_info,
|
||||||
|
entry_data,
|
||||||
|
old_addon_options,
|
||||||
|
new_addon_options,
|
||||||
|
disconnect_calls,
|
||||||
|
server_version_side_effect,
|
||||||
|
):
|
||||||
|
"""Test options flow and add-on already running with server info failure."""
|
||||||
|
addon_options.update(old_addon_options)
|
||||||
|
entry = integration
|
||||||
|
entry.unique_id = 1234
|
||||||
|
data = {**entry.data, **entry_data}
|
||||||
|
hass.config_entries.async_update_entry(entry, data=data)
|
||||||
|
|
||||||
|
assert entry.data["url"] == "ws://test.org"
|
||||||
|
|
||||||
|
assert client.connect.call_count == 1
|
||||||
|
assert client.disconnect.call_count == 0
|
||||||
|
|
||||||
|
result = await hass.config_entries.options.async_init(entry.entry_id)
|
||||||
|
|
||||||
|
assert result["type"] == "form"
|
||||||
|
assert result["step_id"] == "on_supervisor"
|
||||||
|
|
||||||
|
result = await hass.config_entries.options.async_configure(
|
||||||
|
result["flow_id"], {"use_addon": True}
|
||||||
|
)
|
||||||
|
|
||||||
|
assert result["type"] == "form"
|
||||||
|
assert result["step_id"] == "configure_addon"
|
||||||
|
|
||||||
|
result = await hass.config_entries.options.async_configure(
|
||||||
|
result["flow_id"],
|
||||||
|
new_addon_options,
|
||||||
|
)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
assert result["type"] == "abort"
|
||||||
|
assert result["reason"] == "cannot_connect"
|
||||||
|
assert entry.data == data
|
||||||
|
assert client.connect.call_count == 2
|
||||||
|
assert client.disconnect.call_count == 1
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"discovery_info, entry_data, old_addon_options, new_addon_options, disconnect_calls",
|
||||||
|
[
|
||||||
|
(
|
||||||
|
{"config": ADDON_DISCOVERY_INFO},
|
||||||
|
{},
|
||||||
|
{"device": "/test", "network_key": "abc123"},
|
||||||
|
{
|
||||||
|
"usb_path": "/new",
|
||||||
|
"network_key": "new123",
|
||||||
|
"log_level": "info",
|
||||||
|
"emulate_hardware": False,
|
||||||
|
},
|
||||||
|
0,
|
||||||
|
),
|
||||||
|
(
|
||||||
|
{"config": ADDON_DISCOVERY_INFO},
|
||||||
|
{"use_addon": True},
|
||||||
|
{"device": "/test", "network_key": "abc123"},
|
||||||
|
{
|
||||||
|
"usb_path": "/new",
|
||||||
|
"network_key": "new123",
|
||||||
|
"log_level": "info",
|
||||||
|
"emulate_hardware": False,
|
||||||
|
},
|
||||||
|
1,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
async def test_options_addon_not_installed(
|
||||||
|
hass,
|
||||||
|
client,
|
||||||
|
supervisor,
|
||||||
|
addon_installed,
|
||||||
|
install_addon,
|
||||||
|
integration,
|
||||||
|
addon_options,
|
||||||
|
set_addon_options,
|
||||||
|
start_addon,
|
||||||
|
get_addon_discovery_info,
|
||||||
|
discovery_info,
|
||||||
|
entry_data,
|
||||||
|
old_addon_options,
|
||||||
|
new_addon_options,
|
||||||
|
disconnect_calls,
|
||||||
|
):
|
||||||
|
"""Test options flow and add-on not installed on Supervisor."""
|
||||||
|
addon_installed.return_value["version"] = None
|
||||||
|
addon_options.update(old_addon_options)
|
||||||
|
entry = integration
|
||||||
|
entry.unique_id = 1234
|
||||||
|
data = {**entry.data, **entry_data}
|
||||||
|
hass.config_entries.async_update_entry(entry, data=data)
|
||||||
|
|
||||||
|
assert entry.data["url"] == "ws://test.org"
|
||||||
|
|
||||||
|
assert client.connect.call_count == 1
|
||||||
|
assert client.disconnect.call_count == 0
|
||||||
|
|
||||||
|
result = await hass.config_entries.options.async_init(entry.entry_id)
|
||||||
|
|
||||||
|
assert result["type"] == "form"
|
||||||
|
assert result["step_id"] == "on_supervisor"
|
||||||
|
|
||||||
|
result = await hass.config_entries.options.async_configure(
|
||||||
|
result["flow_id"], {"use_addon": True}
|
||||||
|
)
|
||||||
|
|
||||||
|
assert result["type"] == "progress"
|
||||||
|
assert result["step_id"] == "install_addon"
|
||||||
|
|
||||||
|
# Make sure the flow continues when the progress task is done.
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
result = await hass.config_entries.options.async_configure(result["flow_id"])
|
||||||
|
|
||||||
|
assert install_addon.call_args == call(hass, "core_zwave_js")
|
||||||
|
|
||||||
|
assert result["type"] == "form"
|
||||||
|
assert result["step_id"] == "configure_addon"
|
||||||
|
|
||||||
|
result = await hass.config_entries.options.async_configure(
|
||||||
|
result["flow_id"],
|
||||||
|
new_addon_options,
|
||||||
|
)
|
||||||
|
|
||||||
|
new_addon_options["device"] = new_addon_options.pop("usb_path")
|
||||||
|
assert set_addon_options.call_args == call(
|
||||||
|
hass,
|
||||||
|
"core_zwave_js",
|
||||||
|
{"options": new_addon_options},
|
||||||
|
)
|
||||||
|
assert client.disconnect.call_count == disconnect_calls
|
||||||
|
|
||||||
|
assert result["type"] == "progress"
|
||||||
|
assert result["step_id"] == "start_addon"
|
||||||
|
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
assert start_addon.call_count == 1
|
||||||
|
assert start_addon.call_args == call(hass, "core_zwave_js")
|
||||||
|
|
||||||
|
result = await hass.config_entries.options.async_configure(result["flow_id"])
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
assert result["type"] == "create_entry"
|
||||||
|
assert entry.data["url"] == "ws://host1:3001"
|
||||||
|
assert entry.data["usb_path"] == new_addon_options["device"]
|
||||||
|
assert entry.data["network_key"] == new_addon_options["network_key"]
|
||||||
|
assert entry.data["use_addon"] is True
|
||||||
|
assert entry.data["integration_created_addon"] is True
|
||||||
|
assert client.connect.call_count == 2
|
||||||
|
assert client.disconnect.call_count == 1
|
||||||
|
Loading…
x
Reference in New Issue
Block a user