User config flow and custom panel for Dynalite integration (#77181)

This commit is contained in:
Ziv 2023-05-11 01:17:23 +03:00 committed by GitHub
parent 6250b0a230
commit dd7db85529
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 591 additions and 163 deletions

View file

@ -1,14 +1,11 @@
"""Support for the Dynalite networks."""
from __future__ import annotations
from typing import Any
import voluptuous as vol
from homeassistant import config_entries
from homeassistant.components.cover import DEVICE_CLASSES_SCHEMA
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_DEFAULT, CONF_HOST, CONF_NAME, CONF_PORT, CONF_TYPE
from homeassistant.const import CONF_HOST
from homeassistant.core import HomeAssistant, ServiceCall
from homeassistant.exceptions import ConfigEntryNotReady
from homeassistant.helpers import config_validation as cv
@ -17,36 +14,10 @@ from homeassistant.helpers.typing import ConfigType
# Loading the config flow file will register the flow
from .bridge import DynaliteBridge
from .const import (
ACTIVE_INIT,
ACTIVE_OFF,
ACTIVE_ON,
ATTR_AREA,
ATTR_CHANNEL,
ATTR_HOST,
CONF_ACTIVE,
CONF_AREA,
CONF_AUTO_DISCOVER,
CONF_BRIDGES,
CONF_CHANNEL,
CONF_CHANNEL_COVER,
CONF_CLOSE_PRESET,
CONF_DEVICE_CLASS,
CONF_DURATION,
CONF_FADE,
CONF_LEVEL,
CONF_NO_DEFAULT,
CONF_OPEN_PRESET,
CONF_POLL_TIMER,
CONF_PRESET,
CONF_ROOM_OFF,
CONF_ROOM_ON,
CONF_STOP_PRESET,
CONF_TEMPLATE,
CONF_TILT_TIME,
DEFAULT_CHANNEL_TYPE,
DEFAULT_NAME,
DEFAULT_PORT,
DEFAULT_TEMPLATES,
DOMAIN,
LOGGER,
PLATFORMS,
@ -54,128 +25,18 @@ from .const import (
SERVICE_REQUEST_CHANNEL_LEVEL,
)
from .convert_config import convert_config
def num_string(value: int | str) -> str:
"""Test if value is a string of digits, aka an integer."""
new_value = str(value)
if new_value.isdigit():
return new_value
raise vol.Invalid("Not a string with numbers")
CHANNEL_DATA_SCHEMA = vol.Schema(
{
vol.Optional(CONF_NAME): cv.string,
vol.Optional(CONF_FADE): vol.Coerce(float),
vol.Optional(CONF_TYPE, default=DEFAULT_CHANNEL_TYPE): vol.Any(
"light", "switch"
),
}
)
CHANNEL_SCHEMA = vol.Schema({num_string: CHANNEL_DATA_SCHEMA})
PRESET_DATA_SCHEMA = vol.Schema(
{
vol.Optional(CONF_NAME): cv.string,
vol.Optional(CONF_FADE): vol.Coerce(float),
vol.Optional(CONF_LEVEL): vol.Coerce(float),
}
)
PRESET_SCHEMA = vol.Schema({num_string: vol.Any(PRESET_DATA_SCHEMA, None)})
TEMPLATE_ROOM_SCHEMA = vol.Schema(
{vol.Optional(CONF_ROOM_ON): num_string, vol.Optional(CONF_ROOM_OFF): num_string}
)
TEMPLATE_TIMECOVER_SCHEMA = vol.Schema(
{
vol.Optional(CONF_CHANNEL_COVER): num_string,
vol.Optional(CONF_DEVICE_CLASS): DEVICE_CLASSES_SCHEMA,
vol.Optional(CONF_OPEN_PRESET): num_string,
vol.Optional(CONF_CLOSE_PRESET): num_string,
vol.Optional(CONF_STOP_PRESET): num_string,
vol.Optional(CONF_DURATION): vol.Coerce(float),
vol.Optional(CONF_TILT_TIME): vol.Coerce(float),
}
)
TEMPLATE_DATA_SCHEMA = vol.Any(TEMPLATE_ROOM_SCHEMA, TEMPLATE_TIMECOVER_SCHEMA)
TEMPLATE_SCHEMA = vol.Schema({str: TEMPLATE_DATA_SCHEMA})
def validate_area(config: dict[str, Any]) -> dict[str, Any]:
"""Validate that template parameters are only used if area is using the relevant template."""
conf_set = set()
for configs in DEFAULT_TEMPLATES.values():
for conf in configs:
conf_set.add(conf)
if config.get(CONF_TEMPLATE):
for conf in DEFAULT_TEMPLATES[config[CONF_TEMPLATE]]:
conf_set.remove(conf)
for conf in conf_set:
if config.get(conf):
raise vol.Invalid(
f"{conf} should not be part of area {config[CONF_NAME]} config"
)
return config
AREA_DATA_SCHEMA = vol.Schema(
vol.All(
{
vol.Required(CONF_NAME): cv.string,
vol.Optional(CONF_TEMPLATE): vol.In(DEFAULT_TEMPLATES),
vol.Optional(CONF_FADE): vol.Coerce(float),
vol.Optional(CONF_NO_DEFAULT): cv.boolean,
vol.Optional(CONF_CHANNEL): CHANNEL_SCHEMA,
vol.Optional(CONF_PRESET): PRESET_SCHEMA,
# the next ones can be part of the templates
vol.Optional(CONF_ROOM_ON): num_string,
vol.Optional(CONF_ROOM_OFF): num_string,
vol.Optional(CONF_CHANNEL_COVER): num_string,
vol.Optional(CONF_DEVICE_CLASS): DEVICE_CLASSES_SCHEMA,
vol.Optional(CONF_OPEN_PRESET): num_string,
vol.Optional(CONF_CLOSE_PRESET): num_string,
vol.Optional(CONF_STOP_PRESET): num_string,
vol.Optional(CONF_DURATION): vol.Coerce(float),
vol.Optional(CONF_TILT_TIME): vol.Coerce(float),
},
validate_area,
)
)
AREA_SCHEMA = vol.Schema({num_string: vol.Any(AREA_DATA_SCHEMA, None)})
PLATFORM_DEFAULTS_SCHEMA = vol.Schema({vol.Optional(CONF_FADE): vol.Coerce(float)})
BRIDGE_SCHEMA = vol.Schema(
{
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
vol.Required(CONF_HOST): cv.string,
vol.Optional(CONF_PORT, default=DEFAULT_PORT): int,
vol.Optional(CONF_AUTO_DISCOVER, default=False): vol.Coerce(bool),
vol.Optional(CONF_POLL_TIMER, default=1.0): vol.Coerce(float),
vol.Optional(CONF_AREA): AREA_SCHEMA,
vol.Optional(CONF_DEFAULT): PLATFORM_DEFAULTS_SCHEMA,
vol.Optional(CONF_ACTIVE, default=False): vol.Any(
ACTIVE_ON, ACTIVE_OFF, ACTIVE_INIT, cv.boolean
),
vol.Optional(CONF_PRESET): PRESET_SCHEMA,
vol.Optional(CONF_TEMPLATE): TEMPLATE_SCHEMA,
}
)
from .panel import async_register_dynalite_frontend
from .schema import BRIDGE_SCHEMA
CONFIG_SCHEMA = vol.Schema(
{
DOMAIN: vol.Schema(
{vol.Optional(CONF_BRIDGES): vol.All(cv.ensure_list, [BRIDGE_SCHEMA])}
)
},
vol.All(
cv.deprecated(DOMAIN),
{
DOMAIN: vol.Schema(
{vol.Optional(CONF_BRIDGES): vol.All(cv.ensure_list, [BRIDGE_SCHEMA])}
),
},
),
extra=vol.ALLOW_EXTRA,
)
@ -277,6 +138,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
await async_register_dynalite_frontend(hass)
return True

View file

@ -3,12 +3,16 @@ from __future__ import annotations
from typing import Any
import voluptuous as vol
from homeassistant import config_entries
from homeassistant.const import CONF_HOST
from homeassistant.const import CONF_HOST, CONF_PORT
from homeassistant.data_entry_flow import FlowResult
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue
from .bridge import DynaliteBridge
from .const import DOMAIN, LOGGER
from .const import DEFAULT_PORT, DOMAIN, LOGGER
from .convert_config import convert_config
@ -23,8 +27,20 @@ class DynaliteFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
async def async_step_import(self, import_info: dict[str, Any]) -> FlowResult:
"""Import a new bridge as a config entry."""
LOGGER.debug("Starting async_step_import - %s", import_info)
LOGGER.debug("Starting async_step_import (deprecated) - %s", import_info)
# Raise an issue that this is deprecated and has been imported
async_create_issue(
self.hass,
DOMAIN,
"deprecated_yaml",
is_fixable=False,
is_persistent=False,
severity=IssueSeverity.WARNING,
translation_key="deprecated_yaml",
)
host = import_info[CONF_HOST]
# Check if host already exists
for entry in self._async_current_entries():
if entry.data[CONF_HOST] == host:
self.hass.config_entries.async_update_entry(
@ -33,9 +49,34 @@ class DynaliteFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
return self.async_abort(reason="already_configured")
# New entry
bridge = DynaliteBridge(self.hass, convert_config(import_info))
return await self._try_create(import_info)
async def async_step_user(
self, user_input: dict[str, Any] | None = None
) -> FlowResult:
"""Step when user initializes a integration."""
if user_input is not None:
return await self._try_create(user_input)
schema = vol.Schema(
{
vol.Required(CONF_HOST): cv.string,
vol.Required(CONF_PORT, default=DEFAULT_PORT): int,
}
)
return self.async_show_form(step_id="user", data_schema=schema)
async def _try_create(self, info: dict[str, Any]) -> FlowResult:
"""Try to connect and if successful, create entry."""
host = info[CONF_HOST]
configured_hosts = [
entry.data[CONF_HOST] for entry in self._async_current_entries()
]
if host in configured_hosts:
return self.async_abort(reason="already_configured")
bridge = DynaliteBridge(self.hass, convert_config(info))
if not await bridge.async_setup():
LOGGER.error("Unable to setup bridge - import info=%s", import_info)
return self.async_abort(reason="no_connection")
LOGGER.debug("Creating entry for the bridge - %s", import_info)
return self.async_create_entry(title=host, data=import_info)
LOGGER.error("Unable to setup bridge - import info=%s", info)
return self.async_abort(reason="cannot_connect")
LOGGER.debug("Creating entry for the bridge - %s", info)
return self.async_create_entry(title=info[CONF_HOST], data=info)

View file

@ -1,10 +1,12 @@
{
"domain": "dynalite",
"name": "Philips Dynalite",
"after_dependencies": ["panel_custom"],
"codeowners": ["@ziv1234"],
"config_flow": true,
"dependencies": ["http", "websocket_api"],
"documentation": "https://www.home-assistant.io/integrations/dynalite",
"iot_class": "local_push",
"loggers": ["dynalite_devices_lib"],
"requirements": ["dynalite_devices==0.1.47"]
"requirements": ["dynalite_devices==0.1.47", "dynalite_panel==0.0.4"]
}

View file

@ -0,0 +1,117 @@
"""Dynalite API interface for the frontend."""
from dynalite_panel import get_build_id, locate_dir
import voluptuous as vol
from homeassistant.components import panel_custom, websocket_api
from homeassistant.components.cover import DEVICE_CLASSES
from homeassistant.const import CONF_DEFAULT, CONF_HOST, CONF_NAME, CONF_PORT
from homeassistant.core import HomeAssistant, callback
from .const import (
CONF_ACTIVE,
CONF_AREA,
CONF_AUTO_DISCOVER,
CONF_PRESET,
CONF_TEMPLATE,
DEFAULT_NAME,
DEFAULT_PORT,
DOMAIN,
LOGGER,
)
from .schema import BRIDGE_SCHEMA
URL_BASE = "/dynalite_static"
RELEVANT_CONFS = [
CONF_NAME,
CONF_HOST,
CONF_PORT,
CONF_AUTO_DISCOVER,
CONF_AREA,
CONF_DEFAULT,
CONF_ACTIVE,
CONF_PRESET,
CONF_TEMPLATE,
]
@websocket_api.websocket_command(
{
vol.Required("type"): "dynalite/get-config",
}
)
@websocket_api.require_admin
@callback
def get_dynalite_config(
hass: HomeAssistant, connection: websocket_api.ActiveConnection, msg: dict
) -> None:
"""Retrieve the Dynalite config for the frontend."""
entries = hass.config_entries.async_entries(DOMAIN)
relevant_config = {
entry.entry_id: {
conf: entry.data[conf] for conf in RELEVANT_CONFS if conf in entry.data
}
for entry in entries
}
dynalite_defaults = {
"DEFAULT_NAME": DEFAULT_NAME,
"DEVICE_CLASSES": DEVICE_CLASSES,
"DEFAULT_PORT": DEFAULT_PORT,
}
connection.send_result(
msg["id"], {"config": relevant_config, "default": dynalite_defaults}
)
@websocket_api.websocket_command(
{
vol.Required("type"): "dynalite/save-config",
vol.Required("entry_id"): str,
vol.Required("config"): BRIDGE_SCHEMA,
}
)
@websocket_api.require_admin
@callback
def save_dynalite_config(
hass: HomeAssistant, connection: websocket_api.ActiveConnection, msg: dict
) -> None:
"""Retrieve the Dynalite config for the frontend."""
entry_id = msg["entry_id"]
entry = hass.config_entries.async_get_entry(entry_id)
if not entry:
LOGGER.error(
"Dynalite - received updated config for invalid entry - %s", entry_id
)
connection.send_result(msg["id"], {"error": True})
return
message_conf = msg["config"]
message_data = {
conf: message_conf[conf] for conf in RELEVANT_CONFS if conf in message_conf
}
LOGGER.info("Updating Dynalite config entry")
hass.config_entries.async_update_entry(entry, data=message_data)
connection.send_result(msg["id"], {})
async def async_register_dynalite_frontend(hass: HomeAssistant):
"""Register the Dynalite frontend configuration panel."""
websocket_api.async_register_command(hass, get_dynalite_config)
websocket_api.async_register_command(hass, save_dynalite_config)
if DOMAIN not in hass.data.get("frontend_panels", {}):
path = locate_dir()
build_id = get_build_id()
hass.http.register_static_path(
URL_BASE, path, cache_headers=(build_id != "dev")
)
await panel_custom.async_register_panel(
hass=hass,
frontend_url_path=DOMAIN,
webcomponent_name="dynalite-panel",
sidebar_title=DOMAIN.capitalize(),
sidebar_icon="mdi:power",
module_url=f"{URL_BASE}/entrypoint-{build_id}.js",
embed_iframe=True,
require_admin=True,
)

View file

@ -0,0 +1,155 @@
"""Schema for config entries."""
from __future__ import annotations
from typing import Any
import voluptuous as vol
from homeassistant.components.cover import DEVICE_CLASSES_SCHEMA
from homeassistant.const import CONF_DEFAULT, CONF_HOST, CONF_NAME, CONF_PORT, CONF_TYPE
from homeassistant.helpers import config_validation as cv
from .const import (
ACTIVE_INIT,
ACTIVE_OFF,
ACTIVE_ON,
CONF_ACTIVE,
CONF_AREA,
CONF_AUTO_DISCOVER,
CONF_CHANNEL,
CONF_CHANNEL_COVER,
CONF_CLOSE_PRESET,
CONF_DEVICE_CLASS,
CONF_DURATION,
CONF_FADE,
CONF_LEVEL,
CONF_NO_DEFAULT,
CONF_OPEN_PRESET,
CONF_POLL_TIMER,
CONF_PRESET,
CONF_ROOM_OFF,
CONF_ROOM_ON,
CONF_STOP_PRESET,
CONF_TEMPLATE,
CONF_TILT_TIME,
DEFAULT_CHANNEL_TYPE,
DEFAULT_NAME,
DEFAULT_PORT,
DEFAULT_TEMPLATES,
)
def num_string(value: str | int) -> str:
"""Test if value is a string of digits, aka an integer."""
new_value = str(value)
if new_value.isdigit():
return new_value
raise vol.Invalid("Not a string with numbers")
CHANNEL_DATA_SCHEMA = vol.Schema(
{
vol.Optional(CONF_NAME): cv.string,
vol.Optional(CONF_FADE): vol.Coerce(float),
vol.Optional(CONF_TYPE, default=DEFAULT_CHANNEL_TYPE): vol.Any(
"light", "switch"
),
}
)
CHANNEL_SCHEMA = vol.Schema({num_string: CHANNEL_DATA_SCHEMA})
PRESET_DATA_SCHEMA = vol.Schema(
{
vol.Optional(CONF_NAME): cv.string,
vol.Optional(CONF_FADE): vol.Coerce(float),
vol.Optional(CONF_LEVEL): vol.Coerce(float),
}
)
PRESET_SCHEMA = vol.Schema({num_string: vol.Any(PRESET_DATA_SCHEMA, None)})
TEMPLATE_ROOM_SCHEMA = vol.Schema(
{vol.Optional(CONF_ROOM_ON): num_string, vol.Optional(CONF_ROOM_OFF): num_string}
)
TEMPLATE_TIMECOVER_SCHEMA = vol.Schema(
{
vol.Optional(CONF_CHANNEL_COVER): num_string,
vol.Optional(CONF_DEVICE_CLASS): DEVICE_CLASSES_SCHEMA,
vol.Optional(CONF_OPEN_PRESET): num_string,
vol.Optional(CONF_CLOSE_PRESET): num_string,
vol.Optional(CONF_STOP_PRESET): num_string,
vol.Optional(CONF_DURATION): vol.Coerce(float),
vol.Optional(CONF_TILT_TIME): vol.Coerce(float),
}
)
TEMPLATE_DATA_SCHEMA = vol.Any(TEMPLATE_ROOM_SCHEMA, TEMPLATE_TIMECOVER_SCHEMA)
TEMPLATE_SCHEMA = vol.Schema({str: TEMPLATE_DATA_SCHEMA})
def validate_area(config: dict[str, Any]) -> dict[str, Any]:
"""Validate that template parameters are only used if area is using the relevant template."""
conf_set = set()
for configs in DEFAULT_TEMPLATES.values():
for conf in configs:
conf_set.add(conf)
if config.get(CONF_TEMPLATE):
for conf in DEFAULT_TEMPLATES[config[CONF_TEMPLATE]]:
conf_set.remove(conf)
for conf in conf_set:
if config.get(conf):
raise vol.Invalid(
f"{conf} should not be part of area {config[CONF_NAME]} config"
)
return config
AREA_DATA_SCHEMA = vol.Schema(
vol.All(
{
vol.Required(CONF_NAME): cv.string,
vol.Optional(CONF_TEMPLATE): vol.In(DEFAULT_TEMPLATES),
vol.Optional(CONF_FADE): vol.Coerce(float),
vol.Optional(CONF_NO_DEFAULT): cv.boolean,
vol.Optional(CONF_CHANNEL): CHANNEL_SCHEMA,
vol.Optional(CONF_PRESET): PRESET_SCHEMA,
# the next ones can be part of the templates
vol.Optional(CONF_ROOM_ON): num_string,
vol.Optional(CONF_ROOM_OFF): num_string,
vol.Optional(CONF_CHANNEL_COVER): num_string,
vol.Optional(CONF_DEVICE_CLASS): DEVICE_CLASSES_SCHEMA,
vol.Optional(CONF_OPEN_PRESET): num_string,
vol.Optional(CONF_CLOSE_PRESET): num_string,
vol.Optional(CONF_STOP_PRESET): num_string,
vol.Optional(CONF_DURATION): vol.Coerce(float),
vol.Optional(CONF_TILT_TIME): vol.Coerce(float),
},
validate_area,
)
)
AREA_SCHEMA = vol.Schema({num_string: vol.Any(AREA_DATA_SCHEMA, None)})
PLATFORM_DEFAULTS_SCHEMA = vol.Schema({vol.Optional(CONF_FADE): vol.Coerce(float)})
BRIDGE_SCHEMA = vol.Schema(
{
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
vol.Required(CONF_HOST): cv.string,
vol.Optional(CONF_PORT, default=DEFAULT_PORT): int,
vol.Optional(CONF_AUTO_DISCOVER, default=False): vol.Coerce(bool),
vol.Optional(CONF_POLL_TIMER, default=1.0): vol.Coerce(float),
vol.Optional(CONF_AREA): AREA_SCHEMA,
vol.Optional(CONF_DEFAULT): PLATFORM_DEFAULTS_SCHEMA,
vol.Optional(CONF_ACTIVE, default=False): vol.Any(
ACTIVE_ON, ACTIVE_OFF, ACTIVE_INIT, cv.boolean
),
vol.Optional(CONF_PRESET): PRESET_SCHEMA,
vol.Optional(CONF_TEMPLATE): TEMPLATE_SCHEMA,
}
)

View file

@ -0,0 +1,24 @@
{
"config": {
"step": {
"user": {
"data": {
"host": "[%key:common::config_flow::data::host%]",
"port": "[%key:common::config_flow::data::port%]"
},
"title": "Configure Dynalite Connection",
"description": "Gateway address to connect to DYNET network"
}
},
"abort": {
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]",
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]"
}
},
"issues": {
"deprecated_yaml": {
"title": "The Dynalite YAML configuration is being removed",
"description": "Configuring Dynalite using YAML is being removed.\n\nYour existing YAML configuration has been imported into the UI automatically.\n\nRemove the Dynalite YAML configuration from your configuration.yaml file and restart Home Assistant to fix this issue."
}
}
}

View file

@ -625,6 +625,9 @@ dweepy==0.3.0
# homeassistant.components.dynalite
dynalite_devices==0.1.47
# homeassistant.components.dynalite
dynalite_panel==0.0.4
# homeassistant.components.rainforest_eagle
eagle100==0.1.1

View file

@ -499,6 +499,9 @@ dwdwfsapi==1.0.6
# homeassistant.components.dynalite
dynalite_devices==0.1.47
# homeassistant.components.dynalite
dynalite_panel==0.0.4
# homeassistant.components.rainforest_eagle
eagle100==0.1.1

View file

@ -5,7 +5,13 @@ import pytest
from homeassistant import config_entries
from homeassistant.components import dynalite
from homeassistant.const import CONF_PORT
from homeassistant.core import HomeAssistant
from homeassistant.helpers.issue_registry import (
IssueSeverity,
async_get as async_get_issue_registry,
)
from homeassistant.setup import async_setup_component
from tests.common import MockConfigEntry
@ -14,14 +20,22 @@ from tests.common import MockConfigEntry
("first_con", "second_con", "exp_type", "exp_result", "exp_reason"),
[
(True, True, "create_entry", config_entries.ConfigEntryState.LOADED, ""),
(False, False, "abort", None, "no_connection"),
(False, False, "abort", None, "cannot_connect"),
(True, False, "create_entry", config_entries.ConfigEntryState.SETUP_RETRY, ""),
],
)
async def test_flow(
hass: HomeAssistant, first_con, second_con, exp_type, exp_result, exp_reason
hass: HomeAssistant,
first_con,
second_con,
exp_type,
exp_result,
exp_reason,
) -> None:
"""Run a flow with or without errors and return result."""
registry = async_get_issue_registry(hass)
issue = registry.async_get_issue(dynalite.DOMAIN, "deprecated_yaml")
assert issue is None
host = "1.2.3.4"
with patch(
"homeassistant.components.dynalite.bridge.DynaliteDevices.async_setup",
@ -38,6 +52,19 @@ async def test_flow(
assert result["result"].state == exp_result
if exp_reason:
assert result["reason"] == exp_reason
issue = registry.async_get_issue(dynalite.DOMAIN, "deprecated_yaml")
assert issue is not None
assert issue.severity == IssueSeverity.WARNING
async def test_deprecated(
hass: HomeAssistant, caplog: pytest.LogCaptureFixture
) -> None:
"""Check that deprecation warning appears in caplog."""
await async_setup_component(
hass, dynalite.DOMAIN, {dynalite.DOMAIN: {dynalite.CONF_HOST: "aaa"}}
)
assert "The 'dynalite' option is deprecated" in caplog.text
async def test_existing(hass: HomeAssistant) -> None:
@ -66,7 +93,7 @@ async def test_existing_update(hass: HomeAssistant) -> None:
port2 = 8888
entry = MockConfigEntry(
domain=dynalite.DOMAIN,
data={dynalite.CONF_HOST: host, dynalite.CONF_PORT: port1},
data={dynalite.CONF_HOST: host, CONF_PORT: port1},
)
entry.add_to_hass(hass)
with patch(
@ -80,7 +107,7 @@ async def test_existing_update(hass: HomeAssistant) -> None:
result = await hass.config_entries.flow.async_init(
dynalite.DOMAIN,
context={"source": config_entries.SOURCE_IMPORT},
data={dynalite.CONF_HOST: host, dynalite.CONF_PORT: port2},
data={dynalite.CONF_HOST: host, CONF_PORT: port2},
)
await hass.async_block_till_done()
assert mock_dyn_dev().configure.call_count == 2
@ -107,3 +134,55 @@ async def test_two_entries(hass: HomeAssistant) -> None:
)
assert result["type"] == "create_entry"
assert result["result"].state == config_entries.ConfigEntryState.LOADED
async def test_setup_user(hass):
"""Test configuration via the user flow."""
host = "3.4.5.6"
port = 1234
result = await hass.config_entries.flow.async_init(
dynalite.DOMAIN, context={"source": config_entries.SOURCE_USER}
)
assert result["type"] == "form"
assert result["step_id"] == "user"
assert result["errors"] is None
with patch(
"homeassistant.components.dynalite.bridge.DynaliteDevices.async_setup",
return_value=True,
):
result = await hass.config_entries.flow.async_configure(
result["flow_id"],
{"host": host, "port": port},
)
assert result["type"] == "create_entry"
assert result["result"].state == config_entries.ConfigEntryState.LOADED
assert result["title"] == host
assert result["data"] == {
"host": host,
"port": port,
}
async def test_setup_user_existing_host(hass):
"""Test that when we setup a host that is defined, we get an error."""
host = "3.4.5.6"
MockConfigEntry(
domain=dynalite.DOMAIN, data={dynalite.CONF_HOST: host}
).add_to_hass(hass)
result = await hass.config_entries.flow.async_init(
dynalite.DOMAIN, context={"source": config_entries.SOURCE_USER}
)
with patch(
"homeassistant.components.dynalite.bridge.DynaliteDevices.async_setup",
return_value=True,
):
result = await hass.config_entries.flow.async_configure(
result["flow_id"],
{"host": host, "port": 1234},
)
assert result["type"] == "abort"
assert result["reason"] == "already_configured"

View file

@ -0,0 +1,141 @@
"""Test websocket commands for the panel."""
from unittest.mock import patch
from homeassistant.components import dynalite
from homeassistant.components.cover import DEVICE_CLASSES
from homeassistant.const import CONF_PORT
from tests.common import MockConfigEntry
async def test_get_config(hass, hass_ws_client):
"""Get the config via websocket."""
host = "1.2.3.4"
port = 765
entry = MockConfigEntry(
domain=dynalite.DOMAIN,
data={dynalite.CONF_HOST: host, CONF_PORT: port},
)
entry.add_to_hass(hass)
with patch(
"homeassistant.components.dynalite.bridge.DynaliteDevices.async_setup",
return_value=True,
):
assert await hass.config_entries.async_setup(entry.entry_id)
await hass.async_block_till_done()
client = await hass_ws_client(hass)
await client.send_json(
{
"id": 24,
"type": "dynalite/get-config",
}
)
msg = await client.receive_json()
assert msg["success"]
result = msg["result"]
entry_id = entry.entry_id
assert result == {
"config": {entry_id: {dynalite.CONF_HOST: host, CONF_PORT: port}},
"default": {
"DEFAULT_NAME": dynalite.const.DEFAULT_NAME,
"DEFAULT_PORT": dynalite.const.DEFAULT_PORT,
"DEVICE_CLASSES": DEVICE_CLASSES,
},
}
async def test_save_config(hass, hass_ws_client):
"""Save the config via websocket."""
host1 = "1.2.3.4"
port1 = 765
host2 = "5.6.7.8"
port2 = 432
host3 = "5.3.2.1"
port3 = 543
entry1 = MockConfigEntry(
domain=dynalite.DOMAIN,
data={dynalite.CONF_HOST: host1, CONF_PORT: port1},
)
entry1.add_to_hass(hass)
with patch(
"homeassistant.components.dynalite.bridge.DynaliteDevices.async_setup",
return_value=True,
):
assert await hass.config_entries.async_setup(entry1.entry_id)
await hass.async_block_till_done()
entry2 = MockConfigEntry(
domain=dynalite.DOMAIN,
data={dynalite.CONF_HOST: host2, CONF_PORT: port2},
)
entry2.add_to_hass(hass)
with patch(
"homeassistant.components.dynalite.bridge.DynaliteDevices.async_setup",
return_value=True,
):
assert await hass.config_entries.async_setup(entry2.entry_id)
await hass.async_block_till_done()
client = await hass_ws_client(hass)
await client.send_json(
{
"id": 24,
"type": "dynalite/save-config",
"entry_id": entry2.entry_id,
"config": {dynalite.CONF_HOST: host3, CONF_PORT: port3},
}
)
msg = await client.receive_json()
assert msg["success"]
assert msg["result"] == {}
existing_entry = hass.config_entries.async_get_entry(entry1.entry_id)
assert existing_entry.data == {dynalite.CONF_HOST: host1, CONF_PORT: port1}
modified_entry = hass.config_entries.async_get_entry(entry2.entry_id)
assert modified_entry.data[dynalite.CONF_HOST] == host3
assert modified_entry.data[CONF_PORT] == port3
async def test_save_config_invalid_entry(hass, hass_ws_client):
"""Try to update nonexistent entry."""
host1 = "1.2.3.4"
port1 = 765
host2 = "5.6.7.8"
port2 = 432
entry = MockConfigEntry(
domain=dynalite.DOMAIN,
data={dynalite.CONF_HOST: host1, CONF_PORT: port1},
)
entry.add_to_hass(hass)
with patch(
"homeassistant.components.dynalite.bridge.DynaliteDevices.async_setup",
return_value=True,
):
assert await hass.config_entries.async_setup(entry.entry_id)
await hass.async_block_till_done()
client = await hass_ws_client(hass)
await client.send_json(
{
"id": 24,
"type": "dynalite/save-config",
"entry_id": "junk",
"config": {dynalite.CONF_HOST: host2, CONF_PORT: port2},
}
)
msg = await client.receive_json()
assert msg["success"]
assert msg["result"] == {"error": True}
existing_entry = hass.config_entries.async_get_entry(entry.entry_id)
assert existing_entry.data == {dynalite.CONF_HOST: host1, CONF_PORT: port1}