Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
110 changes: 87 additions & 23 deletions homeassistant/components/dynalite/__init__.py
Original file line number Diff line number Diff line change
@@ -1,43 +1,55 @@
"""Support for the Dynalite networks."""

import asyncio
from typing import Any, Dict, Union

import voluptuous as vol

from homeassistant import config_entries
from homeassistant.const import CONF_HOST
from homeassistant.components.cover import DEVICE_CLASSES_SCHEMA
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_HOST, CONF_NAME, CONF_PORT, CONF_TYPE
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryNotReady
from homeassistant.helpers import config_validation as cv

# Loading the config flow file will register the flow
from .bridge import DynaliteBridge
from .const import (
ACTIVE_INIT,
ACTIVE_OFF,
ACTIVE_ON,
CONF_ACTIVE,
CONF_ACTIVE_INIT,
CONF_ACTIVE_OFF,
CONF_ACTIVE_ON,
CONF_AREA,
CONF_AUTO_DISCOVER,
CONF_BRIDGES,
CONF_CHANNEL,
CONF_CHANNEL_TYPE,
CONF_CHANNEL_COVER,
CONF_CLOSE_PRESET,
CONF_DEFAULT,
CONF_DEVICE_CLASS,
CONF_DURATION,
CONF_FADE,
CONF_NAME,
CONF_NO_DEFAULT,
CONF_POLLTIMER,
CONF_PORT,
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,
ENTITY_PLATFORMS,
LOGGER,
)


def num_string(value):
def num_string(value: Union[int, str]) -> str:
"""Test if value is a string of digits, aka an integer."""
new_value = str(value)
if new_value.isdigit():
Expand All @@ -49,7 +61,7 @@ def num_string(value):
{
vol.Optional(CONF_NAME): cv.string,
vol.Optional(CONF_FADE): vol.Coerce(float),
vol.Optional(CONF_CHANNEL_TYPE, default=DEFAULT_CHANNEL_TYPE): vol.Any(
vol.Optional(CONF_TYPE, default=DEFAULT_CHANNEL_TYPE): vol.Any(
"light", "switch"
),
}
Expand All @@ -63,15 +75,66 @@ def num_string(value):

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}
)

AREA_DATA_SCHEMA = vol.Schema(
TEMPLATE_TIMECOVER_SCHEMA = vol.Schema(
{
vol.Required(CONF_NAME): cv.string,
vol.Optional(CONF_FADE): vol.Coerce(float),
vol.Optional(CONF_NO_DEFAULT): vol.Coerce(bool),
vol.Optional(CONF_CHANNEL): CHANNEL_SCHEMA,
vol.Optional(CONF_PRESET): PRESET_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})
Comment thread
MartinHjelmare marked this conversation as resolved.


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 template in DEFAULT_TEMPLATES:
for conf in DEFAULT_TEMPLATES[template]:
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): cv.string,
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)})
Expand All @@ -85,13 +148,14 @@ def num_string(value):
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_POLLTIMER, default=1.0): vol.Coerce(float),
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(
CONF_ACTIVE_ON, CONF_ACTIVE_OFF, CONF_ACTIVE_INIT, cv.boolean
ACTIVE_ON, ACTIVE_OFF, ACTIVE_INIT, cv.boolean
),
vol.Optional(CONF_PRESET): PRESET_SCHEMA,
vol.Optional(CONF_TEMPLATE): TEMPLATE_SCHEMA,
}
)

Expand All @@ -105,7 +169,7 @@ def num_string(value):
)


async def async_setup(hass, config):
async def async_setup(hass: HomeAssistant, config: Dict[str, Any]) -> bool:
"""Set up the Dynalite platform."""

conf = config.get(DOMAIN)
Expand Down Expand Up @@ -137,15 +201,15 @@ async def async_setup(hass, config):
return True


async def async_entry_changed(hass, entry):
async def async_entry_changed(hass: HomeAssistant, entry: ConfigEntry) -> None:
"""Reload entry since the data has changed."""
LOGGER.debug("Reconfiguring entry %s", entry.data)
bridge = hass.data[DOMAIN][entry.entry_id]
bridge.reload_config(entry.data)
LOGGER.debug("Reconfiguring entry finished %s", entry.data)


async def async_setup_entry(hass, entry):
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up a bridge from a config entry."""
LOGGER.debug("Setting up entry %s", entry.data)
bridge = DynaliteBridge(hass, entry.data)
Expand All @@ -163,7 +227,7 @@ async def async_setup_entry(hass, entry):
return True


async def async_unload_entry(hass, entry):
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Unload a config entry."""
LOGGER.debug("Unloading entry %s", entry.data)
hass.data[DOMAIN].pop(entry.entry_id)
Expand Down
33 changes: 21 additions & 12 deletions homeassistant/components/dynalite/bridge.py
Original file line number Diff line number Diff line change
@@ -1,17 +1,26 @@
"""Code to handle a Dynalite bridge."""

from typing import TYPE_CHECKING, Any, Callable, Dict, List

from dynalite_devices_lib.dynalite_devices import DynaliteDevices

from homeassistant.core import callback
from homeassistant.const import CONF_HOST
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.dispatcher import async_dispatcher_send

from .const import CONF_ALL, CONF_HOST, ENTITY_PLATFORMS, LOGGER
from .const import CONF_ALL, ENTITY_PLATFORMS, LOGGER
from .convert_config import convert_config

if TYPE_CHECKING: # pragma: no cover
from dynalite_devices_lib.dynalite_devices import ( # pylint: disable=ungrouped-imports
DynaliteBaseDevice,
)


class DynaliteBridge:
"""Manages a single Dynalite bridge."""

def __init__(self, hass, config):
def __init__(self, hass: HomeAssistant, config: Dict[str, Any]) -> None:
"""Initialize the system based on host parameter."""
self.hass = hass
self.area = {}
Expand All @@ -23,20 +32,20 @@ def __init__(self, hass, config):
new_device_func=self.add_devices_when_registered,
update_device_func=self.update_device,
)
self.dynalite_devices.configure(config)
self.dynalite_devices.configure(convert_config(config))

async def async_setup(self):
async def async_setup(self) -> bool:
"""Set up a Dynalite bridge."""
# Configure the dynalite devices
LOGGER.debug("Setting up bridge - host %s", self.host)
return await self.dynalite_devices.async_setup()

def reload_config(self, config):
def reload_config(self, config: Dict[str, Any]) -> None:
"""Reconfigure a bridge when config changes."""
LOGGER.debug("Reloading bridge - host %s, config %s", self.host, config)
self.dynalite_devices.configure(config)
self.dynalite_devices.configure(convert_config(config))

def update_signal(self, device=None):
def update_signal(self, device: "DynaliteBaseDevice" = None) -> str:
"""Create signal to use to trigger entity update."""
if device:
signal = f"dynalite-update-{self.host}-{device.unique_id}"
Expand All @@ -45,26 +54,26 @@ def update_signal(self, device=None):
return signal

@callback
def update_device(self, device):
def update_device(self, device: "DynaliteBaseDevice") -> None:
"""Call when a device or all devices should be updated."""
if device == CONF_ALL:
# This is used to signal connection or disconnection, so all devices may become available or not.
log_string = (
"Connected" if self.dynalite_devices.available else "Disconnected"
"Connected" if self.dynalite_devices.connected else "Disconnected"
)
LOGGER.info("%s to dynalite host", log_string)
async_dispatcher_send(self.hass, self.update_signal())
else:
async_dispatcher_send(self.hass, self.update_signal(device))

@callback
def register_add_devices(self, platform, async_add_devices):
def register_add_devices(self, platform: str, async_add_devices: Callable) -> None:
"""Add an async_add_entities for a category."""
self.async_add_devices[platform] = async_add_devices
if platform in self.waiting_devices:
self.async_add_devices[platform](self.waiting_devices[platform])

def add_devices_when_registered(self, devices):
def add_devices_when_registered(self, devices: List["DynaliteBaseDevice"]) -> None:
"""Add the devices to HA if the add devices callback was registered, otherwise queue until it is."""
for platform in ENTITY_PLATFORMS:
platform_devices = [
Expand Down
6 changes: 4 additions & 2 deletions homeassistant/components/dynalite/config_flow.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
"""Config flow to configure Dynalite hub."""
from typing import Any, Dict

from homeassistant import config_entries
from homeassistant.const import CONF_HOST

Expand All @@ -12,11 +14,11 @@ class DynaliteFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
VERSION = 1
CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_POLL

def __init__(self):
def __init__(self) -> None:
"""Initialize the Dynalite flow."""
self.host = None

async def async_step_import(self, import_info):
async def async_step_import(self, import_info: Dict[str, Any]) -> Any:
"""Import a new bridge as a config entry."""
LOGGER.debug("Starting async_step_import - %s", import_info)
host = import_info[CONF_HOST]
Expand Down
41 changes: 32 additions & 9 deletions homeassistant/components/dynalite/const.py
Original file line number Diff line number Diff line change
@@ -1,31 +1,54 @@
"""Constants for the Dynalite component."""
import logging

from homeassistant.components.cover import DEVICE_CLASS_SHUTTER
from homeassistant.const import CONF_ROOM

LOGGER = logging.getLogger(__package__)
DOMAIN = "dynalite"

ENTITY_PLATFORMS = ["light", "switch"]
ENTITY_PLATFORMS = ["light", "switch", "cover"]


CONF_ACTIVE = "active"
CONF_ACTIVE_INIT = "init"
CONF_ACTIVE_OFF = "off"
CONF_ACTIVE_ON = "on"
ACTIVE_INIT = "init"
ACTIVE_OFF = "off"
ACTIVE_ON = "on"
CONF_ALL = "ALL"
CONF_AREA = "area"
CONF_AUTO_DISCOVER = "autodiscover"
CONF_BRIDGES = "bridges"
CONF_CHANNEL = "channel"
CONF_CHANNEL_TYPE = "type"
CONF_CHANNEL_COVER = "channel_cover"
CONF_CLOSE_PRESET = "close"
CONF_DEFAULT = "default"
CONF_DEVICE_CLASS = "class"
CONF_DURATION = "duration"
CONF_FADE = "fade"
CONF_HOST = "host"
CONF_NAME = "name"
CONF_NO_DEFAULT = "nodefault"
CONF_POLLTIMER = "polltimer"
CONF_PORT = "port"
CONF_OPEN_PRESET = "open"
CONF_POLL_TIMER = "polltimer"
CONF_PRESET = "preset"
CONF_ROOM_OFF = "room_off"
CONF_ROOM_ON = "room_on"
CONF_STOP_PRESET = "stop"
CONF_TEMPLATE = "template"
CONF_TILT_TIME = "tilt"
CONF_TIME_COVER = "time_cover"

DEFAULT_CHANNEL_TYPE = "light"
DEFAULT_COVER_CLASS = DEVICE_CLASS_SHUTTER
DEFAULT_NAME = "dynalite"
DEFAULT_PORT = 12345
DEFAULT_TEMPLATES = {
CONF_ROOM: [CONF_ROOM_ON, CONF_ROOM_OFF],
Comment thread
MartinHjelmare marked this conversation as resolved.
CONF_TIME_COVER: [
CONF_CHANNEL_COVER,
CONF_DEVICE_CLASS,
CONF_OPEN_PRESET,
CONF_CLOSE_PRESET,
CONF_STOP_PRESET,
CONF_DURATION,
CONF_TILT_TIME,
],
}
Loading