Skip to content

Commit

Permalink
User config flow and custom panel for Dynalite integration (#77181)
Browse files Browse the repository at this point in the history
  • Loading branch information
ziv1234 committed May 10, 2023
1 parent 6250b0a commit dd7db85
Show file tree
Hide file tree
Showing 10 changed files with 589 additions and 161 deletions.
159 changes: 11 additions & 148 deletions homeassistant/components/dynalite/__init__.py
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -17,165 +14,29 @@
# 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,
SERVICE_REQUEST_AREA_PRESET,
SERVICE_REQUEST_CHANNEL_LEVEL,
)
from .convert_config import convert_config
from .panel import async_register_dynalite_frontend
from .schema import BRIDGE_SCHEMA


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(
CONFIG_SCHEMA = vol.Schema(
vol.All(
cv.deprecated(DOMAIN),
{
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),
DOMAIN: vol.Schema(
{vol.Optional(CONF_BRIDGES): vol.All(cv.ensure_list, [BRIDGE_SCHEMA])}
),
},
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,
}
)

CONFIG_SCHEMA = vol.Schema(
{
DOMAIN: vol.Schema(
{vol.Optional(CONF_BRIDGES): vol.All(cv.ensure_list, [BRIDGE_SCHEMA])}
)
},
),
extra=vol.ALLOW_EXTRA,
)

Expand Down Expand Up @@ -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


Expand Down
57 changes: 49 additions & 8 deletions homeassistant/components/dynalite/config_flow.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,16 @@

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


Expand All @@ -23,8 +27,20 @@ def __init__(self) -> None:

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(
Expand All @@ -33,9 +49,34 @@ async def async_step_import(self, import_info: dict[str, Any]) -> FlowResult:
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)
4 changes: 3 additions & 1 deletion homeassistant/components/dynalite/manifest.json
Original file line number Diff line number Diff line change
@@ -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"]
}

0 comments on commit dd7db85

Please sign in to comment.