User config flow and custom panel for Dynalite integration (#77181)
This commit is contained in:
parent
6250b0a230
commit
dd7db85529
10 changed files with 591 additions and 163 deletions
|
@ -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
|
||||
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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"]
|
||||
}
|
||||
|
|
117
homeassistant/components/dynalite/panel.py
Normal file
117
homeassistant/components/dynalite/panel.py
Normal 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,
|
||||
)
|
155
homeassistant/components/dynalite/schema.py
Normal file
155
homeassistant/components/dynalite/schema.py
Normal 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,
|
||||
}
|
||||
)
|
24
homeassistant/components/dynalite/strings.json
Normal file
24
homeassistant/components/dynalite/strings.json
Normal 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."
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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"
|
||||
|
|
141
tests/components/dynalite/test_panel.py
Normal file
141
tests/components/dynalite/test_panel.py
Normal 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}
|
Loading…
Add table
Reference in a new issue