core/homeassistant/components/rflink/light.py

283 lines
9.8 KiB
Python

"""Support for Rflink lights."""
from __future__ import annotations
import logging
import re
from typing import Any
import voluptuous as vol
from homeassistant.components.light import (
ATTR_BRIGHTNESS,
PLATFORM_SCHEMA as LIGHT_PLATFORM_SCHEMA,
ColorMode,
LightEntity,
)
from homeassistant.const import CONF_DEVICES, CONF_NAME, CONF_TYPE
from homeassistant.core import HomeAssistant
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
from .const import (
CONF_ALIASES,
CONF_AUTOMATIC_ADD,
CONF_DEVICE_DEFAULTS,
CONF_FIRE_EVENT,
CONF_GROUP,
CONF_GROUP_ALIASES,
CONF_NOGROUP_ALIASES,
CONF_SIGNAL_REPETITIONS,
DATA_DEVICE_REGISTER,
DEVICE_DEFAULTS_SCHEMA,
EVENT_KEY_COMMAND,
EVENT_KEY_ID,
)
from .entity import SwitchableRflinkDevice
from .utils import brightness_to_rflink, rflink_to_brightness
_LOGGER = logging.getLogger(__name__)
PARALLEL_UPDATES = 0
TYPE_DIMMABLE = "dimmable"
TYPE_SWITCHABLE = "switchable"
TYPE_HYBRID = "hybrid"
TYPE_TOGGLE = "toggle"
PLATFORM_SCHEMA = LIGHT_PLATFORM_SCHEMA.extend(
{
vol.Optional(
CONF_DEVICE_DEFAULTS, default=DEVICE_DEFAULTS_SCHEMA({})
): DEVICE_DEFAULTS_SCHEMA,
vol.Optional(CONF_AUTOMATIC_ADD, default=True): cv.boolean,
vol.Optional(CONF_DEVICES, default={}): {
cv.string: vol.Schema(
{
vol.Optional(CONF_NAME): cv.string,
vol.Optional(CONF_TYPE): vol.Any(
TYPE_DIMMABLE, TYPE_SWITCHABLE, TYPE_HYBRID, TYPE_TOGGLE
),
vol.Optional(CONF_ALIASES, default=[]): vol.All(
cv.ensure_list, [cv.string]
),
vol.Optional(CONF_GROUP_ALIASES, default=[]): vol.All(
cv.ensure_list, [cv.string]
),
vol.Optional(CONF_NOGROUP_ALIASES, default=[]): vol.All(
cv.ensure_list, [cv.string]
),
vol.Optional(CONF_FIRE_EVENT): cv.boolean,
vol.Optional(CONF_SIGNAL_REPETITIONS): vol.Coerce(int),
vol.Optional(CONF_GROUP, default=True): cv.boolean,
}
)
},
},
extra=vol.ALLOW_EXTRA,
)
def entity_type_for_device_id(device_id):
"""Return entity class for protocol of a given device_id.
Async friendly.
"""
entity_type_mapping = {
# KlikAanKlikUit support both dimmers and on/off switches on the same
# protocol
"newkaku": TYPE_HYBRID
}
protocol = device_id.split("_")[0]
return entity_type_mapping.get(protocol)
def entity_class_for_type(entity_type):
"""Translate entity type to entity class.
Async friendly.
"""
entity_device_mapping = {
# sends only 'dim' commands not compatible with on/off switches
TYPE_DIMMABLE: DimmableRflinkLight,
# sends only 'on/off' commands not advices with dimmers and signal
# repetition
TYPE_SWITCHABLE: RflinkLight,
# sends 'dim' and 'on' command to support both dimmers and on/off
# switches. Not compatible with signal repetition.
TYPE_HYBRID: HybridRflinkLight,
# sends only 'on' commands for switches which turn on and off
# using the same 'on' command for both.
TYPE_TOGGLE: ToggleRflinkLight,
}
return entity_device_mapping.get(entity_type, RflinkLight)
def devices_from_config(domain_config):
"""Parse configuration and add Rflink light devices."""
devices = []
for device_id, config in domain_config[CONF_DEVICES].items():
# Determine which kind of entity to create
if CONF_TYPE in config:
# Remove type from config to not pass it as and argument to entity
# instantiation
entity_type = config.pop(CONF_TYPE)
else:
entity_type = entity_type_for_device_id(device_id)
entity_class = entity_class_for_type(entity_type)
device_config = dict(domain_config[CONF_DEVICE_DEFAULTS], **config)
is_hybrid = entity_class is HybridRflinkLight
# Make user aware this can cause problems
repetitions_enabled = device_config[CONF_SIGNAL_REPETITIONS] != 1
if is_hybrid and repetitions_enabled:
_LOGGER.warning(
(
"Hybrid type for %s not compatible with signal "
"repetitions. Please set 'dimmable' or 'switchable' "
"type explicitly in configuration"
),
device_id,
)
device = entity_class(device_id, **device_config)
devices.append(device)
return devices
async def async_setup_platform(
hass: HomeAssistant,
config: ConfigType,
async_add_entities: AddEntitiesCallback,
discovery_info: DiscoveryInfoType | None = None,
) -> None:
"""Set up the Rflink light platform."""
async_add_entities(devices_from_config(config))
async def add_new_device(event):
"""Check if device is known, otherwise add to list of known devices."""
device_id = event[EVENT_KEY_ID]
entity_type = entity_type_for_device_id(event[EVENT_KEY_ID])
entity_class = entity_class_for_type(entity_type)
device_config = config[CONF_DEVICE_DEFAULTS]
device = entity_class(device_id, initial_event=event, **device_config)
async_add_entities([device])
if config[CONF_AUTOMATIC_ADD]:
hass.data[DATA_DEVICE_REGISTER][EVENT_KEY_COMMAND] = add_new_device
class RflinkLight(SwitchableRflinkDevice, LightEntity):
"""Representation of a Rflink light."""
_attr_color_mode = ColorMode.ONOFF
_attr_supported_color_modes = {ColorMode.ONOFF}
class DimmableRflinkLight(SwitchableRflinkDevice, LightEntity):
"""Rflink light device that support dimming."""
_attr_color_mode = ColorMode.BRIGHTNESS
_attr_supported_color_modes = {ColorMode.BRIGHTNESS}
_brightness = 255
async def async_added_to_hass(self) -> None:
"""Restore RFLink light brightness attribute."""
await super().async_added_to_hass()
old_state = await self.async_get_last_state()
if (
old_state is not None
and old_state.attributes.get(ATTR_BRIGHTNESS) is not None
):
# restore also brightness in dimmables devices
self._brightness = int(old_state.attributes[ATTR_BRIGHTNESS])
async def async_turn_on(self, **kwargs: Any) -> None:
"""Turn the device on."""
if ATTR_BRIGHTNESS in kwargs:
# rflink only support 16 brightness levels
self._brightness = rflink_to_brightness(
brightness_to_rflink(kwargs[ATTR_BRIGHTNESS])
)
# Turn on light at the requested dim level
await self._async_handle_command("dim", self._brightness)
def _handle_event(self, event):
"""Adjust state if Rflink picks up a remote command for this device."""
self.cancel_queued_send_commands()
command = event["command"]
if command in ["on", "allon"]:
self._state = True
elif command in ["off", "alloff"]:
self._state = False
# dimmable device accept 'set_level=(0-15)' commands
elif re.search("^set_level=(0?[0-9]|1[0-5])$", command, re.IGNORECASE):
self._brightness = rflink_to_brightness(int(command.split("=")[1]))
self._state = True
@property
def brightness(self):
"""Return the brightness of this light between 0..255."""
return self._brightness
class HybridRflinkLight(DimmableRflinkLight):
"""Rflink light device that sends out both dim and on/off commands.
Used for protocols which support lights that are not exclusively on/off
style. For example KlikAanKlikUit supports both on/off and dimmable light
switches using the same protocol. This type allows unconfigured
KlikAanKlikUit devices to support dimming without breaking support for
on/off switches.
This type is not compatible with signal repetitions as the 'dim' and 'on'
command are send sequential and multiple 'on' commands to a dimmable
device can cause the dimmer to switch into a pulsating brightness mode.
Which results in a nice house disco :)
"""
async def async_turn_on(self, **kwargs: Any) -> None:
"""Turn the device on and set dim level."""
await super().async_turn_on(**kwargs)
# if the receiving device does not support dimlevel this
# will ensure it is turned on when full brightness is set
if self.brightness == 255:
await self._async_handle_command("turn_on")
class ToggleRflinkLight(RflinkLight):
"""Rflink light device which sends out only 'on' commands.
Some switches like for example Livolo light switches use the
same 'on' command to switch on and switch off the lights.
If the light is on and 'on' gets sent, the light will turn off
and if the light is off and 'on' gets sent, the light will turn on.
"""
def _handle_event(self, event):
"""Adjust state if Rflink picks up a remote command for this device."""
self.cancel_queued_send_commands()
if event["command"] == "on":
# if the state is unknown or false, it gets set as true
# if the state is true, it gets set as false
self._state = self._state in [None, False]
async def async_turn_on(self, **kwargs: Any) -> None:
"""Turn the device on."""
await self._async_handle_command("toggle")
async def async_turn_off(self, **kwargs: Any) -> None:
"""Turn the device off."""
await self._async_handle_command("toggle")