"""Voluptuous schemas for the KNX integration.""" from __future__ import annotations from abc import ABC from collections import OrderedDict from datetime import timedelta from typing import ClassVar, Final import voluptuous as vol from xknx.devices.climate import FanSpeedMode, SetpointShiftMode from xknx.dpt import DPTBase, DPTNumeric from xknx.dpt.dpt_20 import HVACControllerMode, HVACOperationMode from xknx.exceptions import ConversionError, CouldNotParseTelegram from homeassistant.components.binary_sensor import ( DEVICE_CLASSES_SCHEMA as BINARY_SENSOR_DEVICE_CLASSES_SCHEMA, ) from homeassistant.components.climate import FAN_OFF, HVACMode from homeassistant.components.cover import ( DEVICE_CLASSES_SCHEMA as COVER_DEVICE_CLASSES_SCHEMA, ) from homeassistant.components.number import ( DEVICE_CLASSES_SCHEMA as NUMBER_DEVICE_CLASSES_SCHEMA, NumberMode, ) from homeassistant.components.sensor import ( CONF_STATE_CLASS as CONF_SENSOR_STATE_CLASS, DEVICE_CLASSES_SCHEMA as SENSOR_DEVICE_CLASSES_SCHEMA, STATE_CLASSES_SCHEMA, ) from homeassistant.components.switch import ( DEVICE_CLASSES_SCHEMA as SWITCH_DEVICE_CLASSES_SCHEMA, ) from homeassistant.components.text import TextMode from homeassistant.const import ( CONF_DEVICE_CLASS, CONF_ENTITY_CATEGORY, CONF_ENTITY_ID, CONF_EVENT, CONF_MODE, CONF_NAME, CONF_PAYLOAD, CONF_TYPE, CONF_UNIT_OF_MEASUREMENT, CONF_VALUE_TEMPLATE, Platform, ) from homeassistant.helpers import config_validation as cv from homeassistant.helpers.entity import ENTITY_CATEGORIES_SCHEMA from .const import ( CONF_CONTEXT_TIMEOUT, CONF_IGNORE_INTERNAL_STATE, CONF_INVERT, CONF_KNX_EXPOSE, CONF_PAYLOAD_LENGTH, CONF_RESET_AFTER, CONF_RESPOND_TO_READ, CONF_STATE_ADDRESS, CONF_SYNC_STATE, KNX_ADDRESS, ClimateConf, ColorTempModes, CoverConf, FanConf, FanZeroMode, NumberConf, SceneConf, ) from .dpt import get_supported_dpts from .validation import ( backwards_compatible_xknx_climate_enum_member, dpt_base_type_validator, ga_list_validator, ga_validator, numeric_type_validator, sensor_type_validator, string_type_validator, sync_state_validator, validate_number_attributes, validate_sensor_attributes, ) ################## # KNX SUB VALIDATORS ################## def _number_limit_sub_validator(config: dict) -> dict: """Validate min, max, and step values for a number entity.""" transcoder = DPTNumeric.parse_transcoder(config[CONF_TYPE]) assert transcoder is not None # already checked by numeric_type_validator return validate_number_attributes(transcoder, config) def _max_payload_value(payload_length: int) -> int: if payload_length == 0: return 0x3F return int(256**payload_length) - 1 def button_payload_sub_validator(entity_config: OrderedDict) -> OrderedDict: """Validate a button entity payload configuration.""" if _type := entity_config.get(CONF_TYPE): _payload = entity_config[ButtonSchema.CONF_VALUE] if (transcoder := DPTBase.parse_transcoder(_type)) is None: raise vol.Invalid(f"'type: {_type}' is not a valid sensor type.") entity_config[CONF_PAYLOAD_LENGTH] = transcoder.payload_length try: _dpt_payload = transcoder.to_knx(_payload) _raw_payload = transcoder.validate_payload(_dpt_payload) except (ConversionError, CouldNotParseTelegram) as ex: raise vol.Invalid( f"'payload: {_payload}' not valid for 'type: {_type}'" ) from ex entity_config[CONF_PAYLOAD] = int.from_bytes(_raw_payload, byteorder="big") return entity_config _payload = entity_config[CONF_PAYLOAD] _payload_length = entity_config[CONF_PAYLOAD_LENGTH] if _payload > (max_payload := _max_payload_value(_payload_length)): raise vol.Invalid( f"'payload: {_payload}' exceeds possible maximum for " f"payload_length {_payload_length}: {max_payload}" ) return entity_config def select_options_sub_validator(entity_config: OrderedDict) -> OrderedDict: """Validate a select entity options configuration.""" options_seen = set() payloads_seen = set() payload_length = entity_config[CONF_PAYLOAD_LENGTH] for opt in entity_config[SelectSchema.CONF_OPTIONS]: option = opt[SelectSchema.CONF_OPTION] payload = opt[CONF_PAYLOAD] if payload > (max_payload := _max_payload_value(payload_length)): raise vol.Invalid( f"'payload: {payload}' for 'option: {option}' exceeds possible" f" maximum of 'payload_length: {payload_length}': {max_payload}" ) if option in options_seen: raise vol.Invalid(f"duplicate item for 'option' not allowed: {option}") options_seen.add(option) if payload in payloads_seen: raise vol.Invalid(f"duplicate item for 'payload' not allowed: {payload}") payloads_seen.add(payload) return entity_config def _sensor_attribute_sub_validator(config: dict) -> dict: """Validate that state_class is compatible with device_class and unit_of_measurement.""" transcoder: type[DPTBase] = DPTBase.parse_transcoder(config[CONF_TYPE]) # type: ignore[assignment] # already checked in sensor_type_validator dpt_metadata = get_supported_dpts()[transcoder.dpt_number_str()] return validate_sensor_attributes(dpt_metadata, config) ######### # EVENT ######### class EventSchema: """Voluptuous schema for KNX events.""" KNX_EVENT_FILTER_SCHEMA = vol.Schema( { vol.Required(KNX_ADDRESS): vol.All(cv.ensure_list, [cv.string]), vol.Optional(CONF_TYPE): dpt_base_type_validator, } ) SCHEMA = { vol.Optional(CONF_EVENT, default=[]): vol.All( cv.ensure_list, [KNX_EVENT_FILTER_SCHEMA] ) } ############# # PLATFORMS ############# class KNXPlatformSchema(ABC): """Voluptuous schema for KNX platform entity configuration.""" PLATFORM: ClassVar[Platform | str] ENTITY_SCHEMA: ClassVar[vol.Schema | vol.All | vol.Any] @classmethod def platform_node(cls) -> dict[vol.Optional, vol.All]: """Return a schema node for the platform.""" return { vol.Optional(str(cls.PLATFORM)): vol.All( cv.ensure_list, [cls.ENTITY_SCHEMA] ) } COMMON_ENTITY_SCHEMA = vol.Schema( { vol.Optional(CONF_NAME, default=""): cv.string, vol.Optional(CONF_ENTITY_CATEGORY): ENTITY_CATEGORIES_SCHEMA, } ) class BinarySensorSchema(KNXPlatformSchema): """Voluptuous schema for KNX binary sensors.""" PLATFORM = Platform.BINARY_SENSOR ENTITY_SCHEMA = vol.All( COMMON_ENTITY_SCHEMA.extend( { vol.Optional(CONF_SYNC_STATE, default=True): sync_state_validator, vol.Optional(CONF_IGNORE_INTERNAL_STATE, default=False): cv.boolean, vol.Optional(CONF_INVERT, default=False): cv.boolean, vol.Required(CONF_STATE_ADDRESS): ga_list_validator, vol.Optional(CONF_CONTEXT_TIMEOUT): vol.All( vol.Coerce(float), vol.Range(min=0, max=10) ), vol.Optional(CONF_DEVICE_CLASS): BINARY_SENSOR_DEVICE_CLASSES_SCHEMA, vol.Optional(CONF_RESET_AFTER): cv.positive_float, } ), ) class ButtonSchema(KNXPlatformSchema): """Voluptuous schema for KNX buttons.""" PLATFORM = Platform.BUTTON CONF_VALUE = "value" payload_or_value_msg = f"Please use only one of `{CONF_PAYLOAD}` or `{CONF_VALUE}`" length_or_type_msg = ( f"Please use only one of `{CONF_PAYLOAD_LENGTH}` or `{CONF_TYPE}`" ) ENTITY_SCHEMA = vol.All( COMMON_ENTITY_SCHEMA.extend( { vol.Required(KNX_ADDRESS): ga_validator, vol.Exclusive( CONF_PAYLOAD, "payload_or_value", msg=payload_or_value_msg ): object, vol.Exclusive( CONF_VALUE, "payload_or_value", msg=payload_or_value_msg ): object, vol.Exclusive( CONF_PAYLOAD_LENGTH, "length_or_type", msg=length_or_type_msg ): object, vol.Exclusive( CONF_TYPE, "length_or_type", msg=length_or_type_msg ): object, } ), vol.Any( vol.Schema( # encoded value { vol.Required(CONF_VALUE): vol.Any(int, float, str), vol.Required(CONF_TYPE): sensor_type_validator, }, extra=vol.ALLOW_EXTRA, ), vol.Schema( # raw payload - default is DPT 1 style True { vol.Optional(CONF_PAYLOAD, default=1): cv.positive_int, vol.Optional(CONF_PAYLOAD_LENGTH, default=0): vol.All( vol.Coerce(int), vol.Range(min=0, max=14) ), vol.Optional(CONF_VALUE): None, vol.Optional(CONF_TYPE): None, }, extra=vol.ALLOW_EXTRA, ), ), # calculate raw CONF_PAYLOAD and CONF_PAYLOAD_LENGTH # from CONF_VALUE and CONF_TYPE if given and check payload size button_payload_sub_validator, ) class ClimateSchema(KNXPlatformSchema): """Voluptuous schema for KNX climate devices.""" PLATFORM = Platform.CLIMATE CONF_ACTIVE_STATE_ADDRESS = "active_state_address" CONF_SETPOINT_SHIFT_ADDRESS = "setpoint_shift_address" CONF_SETPOINT_SHIFT_STATE_ADDRESS = "setpoint_shift_state_address" CONF_SETPOINT_SHIFT_MODE = "setpoint_shift_mode" CONF_TEMPERATURE_ADDRESS = "temperature_address" CONF_TARGET_TEMPERATURE_ADDRESS = "target_temperature_address" CONF_TARGET_TEMPERATURE_STATE_ADDRESS = "target_temperature_state_address" CONF_OPERATION_MODE_ADDRESS = "operation_mode_address" CONF_OPERATION_MODE_STATE_ADDRESS = "operation_mode_state_address" CONF_CONTROLLER_STATUS_ADDRESS = "controller_status_address" CONF_CONTROLLER_STATUS_STATE_ADDRESS = "controller_status_state_address" CONF_CONTROLLER_MODE_ADDRESS = "controller_mode_address" CONF_CONTROLLER_MODE_STATE_ADDRESS = "controller_mode_state_address" CONF_COMMAND_VALUE_STATE_ADDRESS = "command_value_state_address" CONF_HEAT_COOL_ADDRESS = "heat_cool_address" CONF_HEAT_COOL_STATE_ADDRESS = "heat_cool_state_address" CONF_OPERATION_MODE_FROST_PROTECTION_ADDRESS = ( "operation_mode_frost_protection_address" ) CONF_OPERATION_MODE_NIGHT_ADDRESS = "operation_mode_night_address" CONF_OPERATION_MODE_COMFORT_ADDRESS = "operation_mode_comfort_address" CONF_OPERATION_MODE_STANDBY_ADDRESS = "operation_mode_standby_address" CONF_ON_OFF_ADDRESS = "on_off_address" CONF_ON_OFF_STATE_ADDRESS = "on_off_state_address" CONF_FAN_SPEED_ADDRESS = "fan_speed_address" CONF_FAN_SPEED_STATE_ADDRESS = "fan_speed_state_address" CONF_HUMIDITY_STATE_ADDRESS = "humidity_state_address" CONF_SWING_ADDRESS = "swing_address" CONF_SWING_STATE_ADDRESS = "swing_state_address" CONF_SWING_HORIZONTAL_ADDRESS = "swing_horizontal_address" CONF_SWING_HORIZONTAL_STATE_ADDRESS = "swing_horizontal_state_address" DEFAULT_SETPOINT_SHIFT_MODE = "DPT6010" DEFAULT_SETPOINT_SHIFT_MAX = 6 DEFAULT_SETPOINT_SHIFT_MIN = -6 DEFAULT_TEMPERATURE_STEP = 0.1 DEFAULT_ON_OFF_INVERT = False DEFAULT_FAN_SPEED_MODE = "percent" ENTITY_SCHEMA = vol.All( COMMON_ENTITY_SCHEMA.extend( { vol.Optional( ClimateConf.SETPOINT_SHIFT_MAX, default=DEFAULT_SETPOINT_SHIFT_MAX ): vol.All(int, vol.Range(min=0, max=32)), vol.Optional( ClimateConf.SETPOINT_SHIFT_MIN, default=DEFAULT_SETPOINT_SHIFT_MIN ): vol.All(int, vol.Range(min=-32, max=0)), vol.Optional( ClimateConf.TEMPERATURE_STEP, default=DEFAULT_TEMPERATURE_STEP ): vol.All(float, vol.Range(min=0, max=2)), vol.Required(CONF_TEMPERATURE_ADDRESS): ga_list_validator, vol.Required(CONF_TARGET_TEMPERATURE_STATE_ADDRESS): ga_list_validator, vol.Optional(CONF_TARGET_TEMPERATURE_ADDRESS): ga_list_validator, vol.Inclusive( CONF_SETPOINT_SHIFT_ADDRESS, "setpoint_shift", msg=( "'setpoint_shift_address' and 'setpoint_shift_state_address' " "are required for setpoint_shift configuration" ), ): ga_list_validator, vol.Inclusive( CONF_SETPOINT_SHIFT_STATE_ADDRESS, "setpoint_shift", msg=( "'setpoint_shift_address' and 'setpoint_shift_state_address' " "are required for setpoint_shift configuration" ), ): ga_list_validator, vol.Optional(CONF_SETPOINT_SHIFT_MODE): vol.Maybe( vol.All(vol.Upper, cv.enum(SetpointShiftMode)) ), vol.Optional(CONF_ACTIVE_STATE_ADDRESS): ga_list_validator, vol.Optional(CONF_COMMAND_VALUE_STATE_ADDRESS): ga_list_validator, vol.Optional(CONF_OPERATION_MODE_ADDRESS): ga_list_validator, vol.Optional(CONF_OPERATION_MODE_STATE_ADDRESS): ga_list_validator, vol.Optional(CONF_CONTROLLER_STATUS_ADDRESS): ga_list_validator, vol.Optional(CONF_CONTROLLER_STATUS_STATE_ADDRESS): ga_list_validator, vol.Optional(CONF_CONTROLLER_MODE_ADDRESS): ga_list_validator, vol.Optional(CONF_CONTROLLER_MODE_STATE_ADDRESS): ga_list_validator, vol.Optional(CONF_HEAT_COOL_ADDRESS): ga_list_validator, vol.Optional(CONF_HEAT_COOL_STATE_ADDRESS): ga_list_validator, vol.Optional( CONF_OPERATION_MODE_FROST_PROTECTION_ADDRESS ): ga_list_validator, vol.Optional(CONF_OPERATION_MODE_NIGHT_ADDRESS): ga_list_validator, vol.Optional(CONF_OPERATION_MODE_COMFORT_ADDRESS): ga_list_validator, vol.Optional(CONF_OPERATION_MODE_STANDBY_ADDRESS): ga_list_validator, vol.Optional(CONF_ON_OFF_ADDRESS): ga_list_validator, vol.Optional(CONF_ON_OFF_STATE_ADDRESS): ga_list_validator, vol.Optional( ClimateConf.ON_OFF_INVERT, default=DEFAULT_ON_OFF_INVERT ): cv.boolean, vol.Optional(ClimateConf.OPERATION_MODES): vol.All( cv.ensure_list, [backwards_compatible_xknx_climate_enum_member(HVACOperationMode)], ), vol.Optional(ClimateConf.CONTROLLER_MODES): vol.All( cv.ensure_list, [backwards_compatible_xknx_climate_enum_member(HVACControllerMode)], ), vol.Optional( ClimateConf.DEFAULT_CONTROLLER_MODE, default=HVACMode.HEAT ): vol.Coerce(HVACMode), vol.Optional(ClimateConf.MIN_TEMP): vol.Coerce(float), vol.Optional(ClimateConf.MAX_TEMP): vol.Coerce(float), vol.Optional(CONF_FAN_SPEED_ADDRESS): ga_list_validator, vol.Optional(CONF_FAN_SPEED_STATE_ADDRESS): ga_list_validator, vol.Optional(ClimateConf.FAN_MAX_STEP, default=3): cv.byte, vol.Optional( ClimateConf.FAN_SPEED_MODE, default=DEFAULT_FAN_SPEED_MODE ): vol.All(vol.Upper, cv.enum(FanSpeedMode)), vol.Optional(ClimateConf.FAN_ZERO_MODE, default=FAN_OFF): vol.Coerce( FanZeroMode ), vol.Optional(CONF_SWING_ADDRESS): ga_list_validator, vol.Optional(CONF_SWING_STATE_ADDRESS): ga_list_validator, vol.Optional(CONF_SWING_HORIZONTAL_ADDRESS): ga_list_validator, vol.Optional(CONF_SWING_HORIZONTAL_STATE_ADDRESS): ga_list_validator, vol.Optional(CONF_HUMIDITY_STATE_ADDRESS): ga_list_validator, } ), ) class CoverSchema(KNXPlatformSchema): """Voluptuous schema for KNX covers.""" PLATFORM = Platform.COVER CONF_MOVE_LONG_ADDRESS = "move_long_address" CONF_MOVE_SHORT_ADDRESS = "move_short_address" CONF_STOP_ADDRESS = "stop_address" CONF_POSITION_ADDRESS = "position_address" CONF_POSITION_STATE_ADDRESS = "position_state_address" CONF_ANGLE_ADDRESS = "angle_address" CONF_ANGLE_STATE_ADDRESS = "angle_state_address" DEFAULT_TRAVEL_TIME = 25 ENTITY_SCHEMA = vol.All( COMMON_ENTITY_SCHEMA.extend( { vol.Optional(CONF_MOVE_LONG_ADDRESS): ga_list_validator, vol.Optional(CONF_MOVE_SHORT_ADDRESS): ga_list_validator, vol.Optional(CONF_STOP_ADDRESS): ga_list_validator, vol.Optional(CONF_POSITION_ADDRESS): ga_list_validator, vol.Optional(CONF_POSITION_STATE_ADDRESS): ga_list_validator, vol.Optional(CONF_ANGLE_ADDRESS): ga_list_validator, vol.Optional(CONF_ANGLE_STATE_ADDRESS): ga_list_validator, vol.Optional( CoverConf.TRAVELLING_TIME_DOWN, default=DEFAULT_TRAVEL_TIME ): cv.positive_float, vol.Optional( CoverConf.TRAVELLING_TIME_UP, default=DEFAULT_TRAVEL_TIME ): cv.positive_float, vol.Optional(CoverConf.INVERT_UPDOWN, default=False): cv.boolean, vol.Optional(CoverConf.INVERT_POSITION, default=False): cv.boolean, vol.Optional(CoverConf.INVERT_ANGLE, default=False): cv.boolean, vol.Optional(CONF_DEVICE_CLASS): COVER_DEVICE_CLASSES_SCHEMA, } ), vol.Any( vol.Schema( {vol.Required(CONF_MOVE_LONG_ADDRESS): object}, extra=vol.ALLOW_EXTRA, ), vol.Schema( {vol.Required(CONF_POSITION_ADDRESS): object}, extra=vol.ALLOW_EXTRA, ), msg=( f"At least one of '{CONF_MOVE_LONG_ADDRESS}' or" f" '{CONF_POSITION_ADDRESS}' is required." ), ), ) class DateSchema(KNXPlatformSchema): """Voluptuous schema for KNX date.""" PLATFORM = Platform.DATE ENTITY_SCHEMA = COMMON_ENTITY_SCHEMA.extend( { vol.Optional(CONF_RESPOND_TO_READ, default=False): cv.boolean, vol.Optional(CONF_SYNC_STATE, default=True): sync_state_validator, vol.Required(KNX_ADDRESS): ga_list_validator, vol.Optional(CONF_STATE_ADDRESS): ga_list_validator, } ) class DateTimeSchema(KNXPlatformSchema): """Voluptuous schema for KNX date.""" PLATFORM = Platform.DATETIME ENTITY_SCHEMA = COMMON_ENTITY_SCHEMA.extend( { vol.Optional(CONF_RESPOND_TO_READ, default=False): cv.boolean, vol.Optional(CONF_SYNC_STATE, default=True): sync_state_validator, vol.Required(KNX_ADDRESS): ga_list_validator, vol.Optional(CONF_STATE_ADDRESS): ga_list_validator, } ) class ExposeSchema(KNXPlatformSchema): """Voluptuous schema for KNX exposures.""" PLATFORM = CONF_KNX_EXPOSE CONF_KNX_EXPOSE_TYPE = CONF_TYPE CONF_KNX_EXPOSE_ATTRIBUTE = "attribute" CONF_KNX_EXPOSE_BINARY = "binary" CONF_KNX_EXPOSE_COOLDOWN = "cooldown" CONF_KNX_EXPOSE_PERIODIC_SEND = "periodic_send" CONF_KNX_EXPOSE_DEFAULT = "default" CONF_TIME = "time" CONF_DATE = "date" CONF_DATETIME = "datetime" EXPOSE_TIME_TYPES: Final = [CONF_TIME, CONF_DATE, CONF_DATETIME] EXPOSE_TIME_SCHEMA = vol.Schema( { vol.Required(CONF_KNX_EXPOSE_TYPE): vol.All( cv.string, str.lower, vol.In(EXPOSE_TIME_TYPES) ), vol.Required(KNX_ADDRESS): ga_validator, } ) EXPOSE_SENSOR_SCHEMA = vol.Schema( { vol.Optional( CONF_KNX_EXPOSE_COOLDOWN, default=timedelta(0) ): cv.positive_time_period, vol.Optional( CONF_KNX_EXPOSE_PERIODIC_SEND, default=timedelta(0) ): cv.positive_time_period, vol.Optional(CONF_RESPOND_TO_READ, default=True): cv.boolean, vol.Required(CONF_KNX_EXPOSE_TYPE): vol.Any( CONF_KNX_EXPOSE_BINARY, sensor_type_validator ), vol.Required(KNX_ADDRESS): ga_validator, vol.Required(CONF_ENTITY_ID): cv.entity_id, vol.Optional(CONF_KNX_EXPOSE_ATTRIBUTE): cv.string, vol.Optional(CONF_KNX_EXPOSE_DEFAULT): cv.match_all, vol.Optional(CONF_VALUE_TEMPLATE): cv.template, } ) ENTITY_SCHEMA = vol.Any(EXPOSE_SENSOR_SCHEMA, EXPOSE_TIME_SCHEMA) class FanSchema(KNXPlatformSchema): """Voluptuous schema for KNX fans.""" PLATFORM = Platform.FAN CONF_STATE_ADDRESS = CONF_STATE_ADDRESS CONF_OSCILLATION_ADDRESS = "oscillation_address" CONF_OSCILLATION_STATE_ADDRESS = "oscillation_state_address" CONF_SWITCH_ADDRESS = "switch_address" CONF_SWITCH_STATE_ADDRESS = "switch_state_address" ENTITY_SCHEMA = vol.All( COMMON_ENTITY_SCHEMA.extend( { vol.Optional(KNX_ADDRESS): ga_list_validator, vol.Optional(CONF_STATE_ADDRESS): ga_list_validator, vol.Optional(CONF_SWITCH_ADDRESS): ga_list_validator, vol.Optional(CONF_SWITCH_STATE_ADDRESS): ga_list_validator, vol.Optional(CONF_OSCILLATION_ADDRESS): ga_list_validator, vol.Optional(CONF_OSCILLATION_STATE_ADDRESS): ga_list_validator, vol.Optional(FanConf.MAX_STEP): cv.byte, vol.Optional(CONF_SYNC_STATE, default=True): sync_state_validator, } ), vol.Any( vol.Schema( {vol.Required(KNX_ADDRESS): object}, extra=vol.ALLOW_EXTRA, ), vol.Schema( {vol.Required(CONF_SWITCH_ADDRESS): object}, extra=vol.ALLOW_EXTRA, ), msg=( f"At least one of '{KNX_ADDRESS}' or" f" '{CONF_SWITCH_ADDRESS}' is required." ), ), ) class LightSchema(KNXPlatformSchema): """Voluptuous schema for KNX lights.""" PLATFORM = Platform.LIGHT CONF_STATE_ADDRESS = CONF_STATE_ADDRESS CONF_BRIGHTNESS_ADDRESS = "brightness_address" CONF_BRIGHTNESS_STATE_ADDRESS = "brightness_state_address" CONF_COLOR_ADDRESS = "color_address" CONF_COLOR_STATE_ADDRESS = "color_state_address" CONF_COLOR_TEMP_ADDRESS = "color_temperature_address" CONF_COLOR_TEMP_STATE_ADDRESS = "color_temperature_state_address" CONF_COLOR_TEMP_MODE = "color_temperature_mode" CONF_HUE_ADDRESS = "hue_address" CONF_HUE_STATE_ADDRESS = "hue_state_address" CONF_RGBW_ADDRESS = "rgbw_address" CONF_RGBW_STATE_ADDRESS = "rgbw_state_address" CONF_SATURATION_ADDRESS = "saturation_address" CONF_SATURATION_STATE_ADDRESS = "saturation_state_address" CONF_XYY_ADDRESS = "xyy_address" CONF_XYY_STATE_ADDRESS = "xyy_state_address" CONF_MIN_KELVIN = "min_kelvin" CONF_MAX_KELVIN = "max_kelvin" DEFAULT_COLOR_TEMP_MODE = "absolute" DEFAULT_MIN_KELVIN = 2700 # 370 mireds DEFAULT_MAX_KELVIN = 6000 # 166 mireds CONF_INDIVIDUAL_COLORS = "individual_colors" CONF_RED = "red" CONF_GREEN = "green" CONF_BLUE = "blue" CONF_WHITE = "white" _hs_color_inclusion_msg = ( "'hue_address', 'saturation_address' and 'brightness_address'" " are required for hs_color configuration" ) HS_COLOR_SCHEMA = { vol.Optional(CONF_HUE_ADDRESS): ga_list_validator, vol.Optional(CONF_HUE_STATE_ADDRESS): ga_list_validator, vol.Optional(CONF_SATURATION_ADDRESS): ga_list_validator, vol.Optional(CONF_SATURATION_STATE_ADDRESS): ga_list_validator, } INDIVIDUAL_COLOR_SCHEMA = vol.Schema( { vol.Optional(KNX_ADDRESS): ga_list_validator, vol.Optional(CONF_STATE_ADDRESS): ga_list_validator, vol.Required(CONF_BRIGHTNESS_ADDRESS): ga_list_validator, vol.Optional(CONF_BRIGHTNESS_STATE_ADDRESS): ga_list_validator, } ) ENTITY_SCHEMA = vol.All( COMMON_ENTITY_SCHEMA.extend( { vol.Optional(KNX_ADDRESS): ga_list_validator, vol.Optional(CONF_STATE_ADDRESS): ga_list_validator, vol.Optional(CONF_BRIGHTNESS_ADDRESS): ga_list_validator, vol.Optional(CONF_BRIGHTNESS_STATE_ADDRESS): ga_list_validator, vol.Exclusive(CONF_INDIVIDUAL_COLORS, "color"): { vol.Inclusive( CONF_RED, "individual_colors", msg=( "'red', 'green' and 'blue' are required for individual" " colors configuration" ), ): INDIVIDUAL_COLOR_SCHEMA, vol.Inclusive( CONF_GREEN, "individual_colors", msg=( "'red', 'green' and 'blue' are required for individual" " colors configuration" ), ): INDIVIDUAL_COLOR_SCHEMA, vol.Inclusive( CONF_BLUE, "individual_colors", msg=( "'red', 'green' and 'blue' are required for individual" " colors configuration" ), ): INDIVIDUAL_COLOR_SCHEMA, vol.Optional(CONF_WHITE): INDIVIDUAL_COLOR_SCHEMA, }, vol.Exclusive(CONF_COLOR_ADDRESS, "color"): ga_list_validator, vol.Optional(CONF_COLOR_STATE_ADDRESS): ga_list_validator, vol.Optional(CONF_COLOR_TEMP_ADDRESS): ga_list_validator, vol.Optional(CONF_COLOR_TEMP_STATE_ADDRESS): ga_list_validator, vol.Optional( CONF_COLOR_TEMP_MODE, default=DEFAULT_COLOR_TEMP_MODE ): vol.All(vol.Upper, cv.enum(ColorTempModes)), **HS_COLOR_SCHEMA, vol.Exclusive(CONF_RGBW_ADDRESS, "color"): ga_list_validator, vol.Optional(CONF_RGBW_STATE_ADDRESS): ga_list_validator, vol.Exclusive(CONF_XYY_ADDRESS, "color"): ga_list_validator, vol.Optional(CONF_XYY_STATE_ADDRESS): ga_list_validator, vol.Optional(CONF_MIN_KELVIN, default=DEFAULT_MIN_KELVIN): vol.All( vol.Coerce(int), vol.Range(min=1) ), vol.Optional(CONF_MAX_KELVIN, default=DEFAULT_MAX_KELVIN): vol.All( vol.Coerce(int), vol.Range(min=1) ), } ), vol.Any( vol.Schema( {vol.Required(KNX_ADDRESS): object}, extra=vol.ALLOW_EXTRA, ), vol.Schema( # brightness addresses are required in INDIVIDUAL_COLOR_SCHEMA {vol.Required(CONF_INDIVIDUAL_COLORS): object}, extra=vol.ALLOW_EXTRA, ), msg="either 'address' or 'individual_colors' is required", ), vol.Any( vol.Schema( # 'brightness' is non-optional for hs-color { vol.Inclusive( CONF_BRIGHTNESS_ADDRESS, "hs_color", msg=_hs_color_inclusion_msg ): object, vol.Inclusive( CONF_HUE_ADDRESS, "hs_color", msg=_hs_color_inclusion_msg ): object, vol.Inclusive( CONF_SATURATION_ADDRESS, "hs_color", msg=_hs_color_inclusion_msg ): object, }, extra=vol.ALLOW_EXTRA, ), vol.Schema( # hs-colors not used { vol.Optional(CONF_HUE_ADDRESS): None, vol.Optional(CONF_SATURATION_ADDRESS): None, }, extra=vol.ALLOW_EXTRA, ), msg=_hs_color_inclusion_msg, ), ) class NotifySchema(KNXPlatformSchema): """Voluptuous schema for KNX notifications.""" PLATFORM = Platform.NOTIFY ENTITY_SCHEMA = COMMON_ENTITY_SCHEMA.extend( { vol.Optional(CONF_TYPE, default="latin_1"): string_type_validator, vol.Required(KNX_ADDRESS): ga_validator, } ) class NumberSchema(KNXPlatformSchema): """Voluptuous schema for KNX numbers.""" PLATFORM = Platform.NUMBER ENTITY_SCHEMA = vol.All( COMMON_ENTITY_SCHEMA.extend( { vol.Optional(CONF_RESPOND_TO_READ, default=False): cv.boolean, vol.Optional(CONF_MODE, default=NumberMode.AUTO): vol.Coerce( NumberMode ), vol.Required(CONF_TYPE): numeric_type_validator, vol.Required(KNX_ADDRESS): ga_list_validator, vol.Optional(CONF_STATE_ADDRESS): ga_list_validator, vol.Optional(NumberConf.MAX): vol.Coerce(float), vol.Optional(NumberConf.MIN): vol.Coerce(float), vol.Optional(NumberConf.STEP): cv.positive_float, vol.Optional(CONF_DEVICE_CLASS): NUMBER_DEVICE_CLASSES_SCHEMA, vol.Optional(CONF_UNIT_OF_MEASUREMENT): cv.string, } ), _number_limit_sub_validator, ) class SceneSchema(KNXPlatformSchema): """Voluptuous schema for KNX scenes.""" PLATFORM = Platform.SCENE CONF_SCENE_NUMBER = "scene_number" ENTITY_SCHEMA = COMMON_ENTITY_SCHEMA.extend( { vol.Required(KNX_ADDRESS): ga_list_validator, vol.Required(SceneConf.SCENE_NUMBER): vol.All( vol.Coerce(int), vol.Range(min=1, max=64) ), } ) class SelectSchema(KNXPlatformSchema): """Voluptuous schema for KNX selects.""" PLATFORM = Platform.SELECT CONF_OPTION = "option" CONF_OPTIONS = "options" ENTITY_SCHEMA = vol.All( COMMON_ENTITY_SCHEMA.extend( { vol.Optional(CONF_SYNC_STATE, default=True): sync_state_validator, vol.Optional(CONF_RESPOND_TO_READ, default=False): cv.boolean, vol.Required(CONF_PAYLOAD_LENGTH): vol.All( vol.Coerce(int), vol.Range(min=0, max=14) ), vol.Required(CONF_OPTIONS): [ { vol.Required(CONF_OPTION): vol.Coerce(str), vol.Required(CONF_PAYLOAD): cv.positive_int, } ], vol.Required(KNX_ADDRESS): ga_list_validator, vol.Optional(CONF_STATE_ADDRESS): ga_list_validator, } ), select_options_sub_validator, ) class SensorSchema(KNXPlatformSchema): """Voluptuous schema for KNX sensors.""" PLATFORM = Platform.SENSOR CONF_ALWAYS_CALLBACK = "always_callback" CONF_STATE_ADDRESS = CONF_STATE_ADDRESS CONF_SYNC_STATE = CONF_SYNC_STATE ENTITY_SCHEMA = vol.All( COMMON_ENTITY_SCHEMA.extend( { vol.Optional(CONF_SYNC_STATE, default=True): sync_state_validator, vol.Optional(CONF_ALWAYS_CALLBACK, default=False): cv.boolean, vol.Optional(CONF_SENSOR_STATE_CLASS): STATE_CLASSES_SCHEMA, vol.Required(CONF_TYPE): sensor_type_validator, vol.Required(CONF_STATE_ADDRESS): ga_list_validator, vol.Optional(CONF_DEVICE_CLASS): SENSOR_DEVICE_CLASSES_SCHEMA, vol.Optional(CONF_UNIT_OF_MEASUREMENT): cv.string, } ), _sensor_attribute_sub_validator, ) class SwitchSchema(KNXPlatformSchema): """Voluptuous schema for KNX switches.""" PLATFORM = Platform.SWITCH CONF_INVERT = CONF_INVERT CONF_STATE_ADDRESS = CONF_STATE_ADDRESS ENTITY_SCHEMA = COMMON_ENTITY_SCHEMA.extend( { vol.Optional(CONF_INVERT, default=False): cv.boolean, vol.Optional(CONF_RESPOND_TO_READ, default=False): cv.boolean, vol.Required(KNX_ADDRESS): ga_list_validator, vol.Optional(CONF_STATE_ADDRESS): ga_list_validator, vol.Optional(CONF_DEVICE_CLASS): SWITCH_DEVICE_CLASSES_SCHEMA, } ) class TextSchema(KNXPlatformSchema): """Voluptuous schema for KNX text.""" PLATFORM = Platform.TEXT ENTITY_SCHEMA = COMMON_ENTITY_SCHEMA.extend( { vol.Optional(CONF_RESPOND_TO_READ, default=False): cv.boolean, vol.Optional(CONF_TYPE, default="latin_1"): string_type_validator, vol.Optional(CONF_MODE, default=TextMode.TEXT): vol.Coerce(TextMode), vol.Required(KNX_ADDRESS): ga_list_validator, vol.Optional(CONF_STATE_ADDRESS): ga_list_validator, } ) class TimeSchema(KNXPlatformSchema): """Voluptuous schema for KNX time.""" PLATFORM = Platform.TIME ENTITY_SCHEMA = COMMON_ENTITY_SCHEMA.extend( { vol.Optional(CONF_RESPOND_TO_READ, default=False): cv.boolean, vol.Optional(CONF_SYNC_STATE, default=True): sync_state_validator, vol.Required(KNX_ADDRESS): ga_list_validator, vol.Optional(CONF_STATE_ADDRESS): ga_list_validator, } ) class WeatherSchema(KNXPlatformSchema): """Voluptuous schema for KNX weather station.""" PLATFORM = Platform.WEATHER CONF_SYNC_STATE = CONF_SYNC_STATE CONF_KNX_TEMPERATURE_ADDRESS = "address_temperature" CONF_KNX_BRIGHTNESS_SOUTH_ADDRESS = "address_brightness_south" CONF_KNX_BRIGHTNESS_EAST_ADDRESS = "address_brightness_east" CONF_KNX_BRIGHTNESS_WEST_ADDRESS = "address_brightness_west" CONF_KNX_BRIGHTNESS_NORTH_ADDRESS = "address_brightness_north" CONF_KNX_WIND_SPEED_ADDRESS = "address_wind_speed" CONF_KNX_WIND_BEARING_ADDRESS = "address_wind_bearing" CONF_KNX_RAIN_ALARM_ADDRESS = "address_rain_alarm" CONF_KNX_FROST_ALARM_ADDRESS = "address_frost_alarm" CONF_KNX_WIND_ALARM_ADDRESS = "address_wind_alarm" CONF_KNX_DAY_NIGHT_ADDRESS = "address_day_night" CONF_KNX_AIR_PRESSURE_ADDRESS = "address_air_pressure" CONF_KNX_HUMIDITY_ADDRESS = "address_humidity" ENTITY_SCHEMA = COMMON_ENTITY_SCHEMA.extend( { vol.Optional(CONF_SYNC_STATE, default=True): sync_state_validator, vol.Required(CONF_KNX_TEMPERATURE_ADDRESS): ga_list_validator, vol.Optional(CONF_KNX_BRIGHTNESS_SOUTH_ADDRESS): ga_list_validator, vol.Optional(CONF_KNX_BRIGHTNESS_EAST_ADDRESS): ga_list_validator, vol.Optional(CONF_KNX_BRIGHTNESS_WEST_ADDRESS): ga_list_validator, vol.Optional(CONF_KNX_BRIGHTNESS_NORTH_ADDRESS): ga_list_validator, vol.Optional(CONF_KNX_WIND_SPEED_ADDRESS): ga_list_validator, vol.Optional(CONF_KNX_WIND_BEARING_ADDRESS): ga_list_validator, vol.Optional(CONF_KNX_RAIN_ALARM_ADDRESS): ga_list_validator, vol.Optional(CONF_KNX_FROST_ALARM_ADDRESS): ga_list_validator, vol.Optional(CONF_KNX_WIND_ALARM_ADDRESS): ga_list_validator, vol.Optional(CONF_KNX_DAY_NIGHT_ADDRESS): ga_list_validator, vol.Optional(CONF_KNX_AIR_PRESSURE_ADDRESS): ga_list_validator, vol.Optional(CONF_KNX_HUMIDITY_ADDRESS): ga_list_validator, } )