Files
homeassistant/custom_components/yandex_smart_home/config_schema.py
Victor Alexandrovich Tsyrenschikov 373ed28445 python
2026-03-30 20:25:42 +05:00

469 lines
17 KiB
Python

"""Helpers for config validation using voluptuous."""
from contextlib import suppress
import logging
from typing import Any
from homeassistant.components.event import EventDeviceClass
from homeassistant.components.sensor.const import SensorDeviceClass
from homeassistant.const import CONF_DEVICE_CLASS, CONF_NAME, CONF_ROOM, CONF_STATE_TEMPLATE, CONF_TYPE
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.entityfilter import BASE_FILTER_SCHEMA
from homeassistant.helpers.typing import ConfigType
from homeassistant.util.color import RGBColor
import voluptuous as vol
from .color import ColorName, rgb_to_int
from .const import (
CONF_BACKLIGHT_ENTITY_ID,
CONF_BETA,
CONF_CLOUD_STREAM,
CONF_COLOR_PROFILE,
CONF_ENTITY_CONFIG,
CONF_ENTITY_CUSTOM_CAPABILITY_STATE_ATTRIBUTE,
CONF_ENTITY_CUSTOM_CAPABILITY_STATE_ENTITY_ID,
CONF_ENTITY_CUSTOM_MODE_SET_MODE,
CONF_ENTITY_CUSTOM_MODES,
CONF_ENTITY_CUSTOM_RANGE_DECREASE_VALUE,
CONF_ENTITY_CUSTOM_RANGE_INCREASE_VALUE,
CONF_ENTITY_CUSTOM_RANGE_SET_VALUE,
CONF_ENTITY_CUSTOM_RANGES,
CONF_ENTITY_CUSTOM_TOGGLE_TURN_OFF,
CONF_ENTITY_CUSTOM_TOGGLE_TURN_ON,
CONF_ENTITY_CUSTOM_TOGGLES,
CONF_ENTITY_EVENT_MAP,
CONF_ENTITY_MODE_MAP,
CONF_ENTITY_PROPERTIES,
CONF_ENTITY_PROPERTY_ATTRIBUTE,
CONF_ENTITY_PROPERTY_ENTITY,
CONF_ENTITY_PROPERTY_TARGET_UNIT_OF_MEASUREMENT,
CONF_ENTITY_PROPERTY_TYPE,
CONF_ENTITY_PROPERTY_UNIT_OF_MEASUREMENT,
CONF_ENTITY_PROPERTY_VALUE_TEMPLATE,
CONF_ENTITY_RANGE,
CONF_ENTITY_RANGE_MAX,
CONF_ENTITY_RANGE_MIN,
CONF_ENTITY_RANGE_PRECISION,
CONF_ERROR_CODE_TEMPLATE,
CONF_FEATURES,
CONF_FILTER,
CONF_NOTIFIER,
CONF_NOTIFIER_OAUTH_TOKEN,
CONF_NOTIFIER_SKILL_ID,
CONF_NOTIFIER_USER_ID,
CONF_PRESSURE_UNIT,
CONF_SETTINGS,
CONF_SLOW,
CONF_STATE_UNKNOWN,
CONF_SUPPORT_SET_CHANNEL,
CONF_TURN_OFF,
CONF_TURN_ON,
MediaPlayerFeature,
PropertyInstanceType,
)
from .schema import (
ColorScene,
ColorSettingCapabilityInstance,
DeviceType,
EventPropertyInstance,
FloatPropertyInstance,
ModeCapabilityInstance,
ModeCapabilityMode,
RangeCapabilityInstance,
ToggleCapabilityInstance,
)
from .schema.property_event import get_supported_events_for_instance
from .unit_conversion import UnitOfPressure, UnitOfTemperature
_LOGGER = logging.getLogger(__name__)
def property_type(value: str) -> str:
if value.startswith(f"{PropertyInstanceType.EVENT}."):
instance = value.split(".", 1)[1]
try:
EventPropertyInstance(instance)
return value
except ValueError:
raise vol.Invalid(
f"Event property type '{instance}' is not supported, "
f"see valid event types at https://docs.yaha-cloud.ru/v1.0.x/devices/sensor/event/#type"
)
if value.startswith(f"{PropertyInstanceType.FLOAT}."):
instance = value.split(".", 1)[1]
try:
FloatPropertyInstance(instance)
return value
except ValueError:
raise vol.Invalid(
f"Float property type '{instance}' is not supported, "
f"see valid float types at https://docs.yaha-cloud.ru/v1.0.x/devices/sensor/float/#type"
)
for enum in [FloatPropertyInstance, EventPropertyInstance]:
with suppress(ValueError):
return enum(value).value
device_class_to_float_instance = {
SensorDeviceClass.ATMOSPHERIC_PRESSURE.value: FloatPropertyInstance.PRESSURE,
SensorDeviceClass.CO2.value: FloatPropertyInstance.CO2_LEVEL,
SensorDeviceClass.CURRENT.value: FloatPropertyInstance.AMPERAGE,
SensorDeviceClass.ILLUMINANCE.value: FloatPropertyInstance.ILLUMINATION,
SensorDeviceClass.PM1.value: FloatPropertyInstance.PM1_DENSITY,
SensorDeviceClass.PM10.value: FloatPropertyInstance.PM10_DENSITY,
SensorDeviceClass.PM25.value: FloatPropertyInstance.PM2_5_DENSITY,
SensorDeviceClass.VOLATILE_ORGANIC_COMPOUNDS.value: FloatPropertyInstance.TVOC,
}
with suppress(KeyError):
instance = device_class_to_float_instance[value]
return f"{PropertyInstanceType.FLOAT}.{instance}"
raise vol.Invalid(
f"Property type '{value}' is not supported, "
f"see valid types at https://docs.yaha-cloud.ru/v1.0.x/devices/sensor/event/#type and "
f"https://docs.yaha-cloud.ru/v1.0.x/devices/sensor/float/#type"
)
def property_attributes(value: ConfigType) -> ConfigType:
"""Validate keys for property."""
entity = value.get(CONF_ENTITY_PROPERTY_ENTITY)
attribute = value.get(CONF_ENTITY_PROPERTY_ATTRIBUTE)
value_template = value.get(CONF_ENTITY_PROPERTY_VALUE_TEMPLATE)
if value_template and (entity or attribute):
raise vol.Invalid("entity/attribute and value_template are mutually exclusive")
property_type_value = value.get(CONF_ENTITY_PROPERTY_TYPE)
target_unit_of_measurement = value.get(CONF_ENTITY_PROPERTY_TARGET_UNIT_OF_MEASUREMENT)
if target_unit_of_measurement:
try:
if property_type_value in [
FloatPropertyInstance.TEMPERATURE,
f"{PropertyInstanceType.FLOAT}.{FloatPropertyInstance.TEMPERATURE}",
]:
assert UnitOfTemperature(target_unit_of_measurement).as_property_unit
elif property_type_value in [
FloatPropertyInstance.PRESSURE,
f"{PropertyInstanceType.FLOAT}.{FloatPropertyInstance.PRESSURE}",
]:
assert UnitOfPressure(target_unit_of_measurement).as_property_unit
else:
raise ValueError
except ValueError:
raise vol.Invalid(
f"Target unit of measurement '{target_unit_of_measurement}' is not supported "
f"for {property_type_value} property, see valid values "
f"at https://docs.yaha-cloud.ru/v1.0.x/devices/sensor/float/#property-target-unit-of-measurement"
)
return value
def mode_instance(value: str) -> str:
if value == ColorSettingCapabilityInstance.SCENE:
return value
try:
ModeCapabilityInstance(value)
except ValueError:
_LOGGER.error(
f"Mode instance '{value}' is not supported, "
f"see valid modes at https://docs.yaha-cloud.ru/v1.0.x/advanced/capabilities/mode/#instance"
)
raise vol.Invalid(f"Mode instance '{value}' is not supported")
return value
def mode(value: str) -> str:
for enum in [ModeCapabilityMode, ColorScene]:
try:
enum(value)
return value
except ValueError:
pass
_LOGGER.error(
f"Mode '{value}' is not supported, "
f"see valid modes at https://yandex.ru/dev/dialogs/smart-home/doc/concepts/mode-instance-modes.html and "
f"https://docs.yaha-cloud.ru/v1.0.x/devices/light/#scene-list"
)
raise vol.Invalid(f"Mode '{value}' is not supported")
def event_instance(value: str) -> str:
try:
EventPropertyInstance(value)
except ValueError:
_LOGGER.error(
f"Event instance '{value}' is not supported, "
f"see valid event types at https://docs.yaha-cloud.ru/v1.0.x/devices/sensor/event/#event-types"
)
raise vol.Invalid(f"Event instance '{value}' is not supported")
return value
def event_map(value: dict[str, dict[str, list[str]]]) -> dict[str, dict[str, list[str]]]:
for instance, mapped_events in value.items():
supported_events = get_supported_events_for_instance(EventPropertyInstance(instance))
for event in mapped_events:
if event not in supported_events:
_LOGGER.error(
f"Event '{event}' is not supported for '{instance}' event instance, "
f"see valid event types at https://docs.yaha-cloud.ru/v1.0.x/devices/sensor/event/#event-types"
)
raise vol.Invalid(f"Event '{event}' is not supported for '{instance}' event instance")
return value
def toggle_instance(value: str) -> str:
try:
ToggleCapabilityInstance(value)
except ValueError:
_LOGGER.error(
f"Toggle instance '{value}' is not supported, "
f"see valid values at https://docs.yaha-cloud.ru/v1.0.x/advanced/capabilities/toggle/#instance"
)
raise vol.Invalid(f"Toggle instance '{value}' is not supported")
return value
def range_instance(value: str) -> str:
try:
RangeCapabilityInstance(value)
except ValueError:
_LOGGER.error(
f"Range instance '{value}' is not supported, "
f"see valid values at https://docs.yaha-cloud.ru/v1.0.x/advanced/capabilities/range/#instance"
)
raise vol.Invalid(f"Range instance '{value}' is not supported")
return value
def entity_features(value: list[str]) -> list[str]:
for feature in value:
try:
MediaPlayerFeature(feature)
except ValueError:
raise vol.Invalid(f"Feature {feature} is not supported")
return value
def device_type(value: str) -> str:
if value in ("devices.types.fan", "fan"):
_LOGGER.warning(
f"Device type '{value}' is deprecated, use 'devices.types.ventilation.fan' or 'ventilation.fan' instead"
)
value = "devices.types.ventilation.fan"
try:
return str(DeviceType(value))
except ValueError:
try:
return DeviceType(f"devices.types.{value}")
except ValueError:
pass
_LOGGER.error(
f"Device type '{value}' is not supported, "
f"see valid device types at https://yandex.ru/dev/dialogs/smart-home/doc/concepts/device-types.html"
)
raise vol.Invalid(f"Device type '{value}' is not supported")
def color_name(value: str) -> str:
try:
ColorName(value)
except ValueError:
_LOGGER.error(
f"Color name '{value}' is not supported, "
f"see valid values at https://docs.yaha-cloud.ru/v1.0.x/devices/light/#color-profile-config"
)
raise vol.Invalid(f"Color name '{value}' is not supported")
return value
def color_value(value: list[Any] | int) -> int:
if isinstance(value, (int, str)):
return int(value)
if isinstance(value, list) and len(value) == 3:
return rgb_to_int(RGBColor(*[int(v) for v in value]))
raise vol.Invalid(f"Invalid value: {value}")
def custom_capability_state(value: ConfigType) -> ConfigType:
"""Validate keys for custom capability."""
state_entity_id = value.get(CONF_ENTITY_CUSTOM_CAPABILITY_STATE_ENTITY_ID)
state_attribute = value.get(CONF_ENTITY_CUSTOM_CAPABILITY_STATE_ATTRIBUTE)
state_template = value.get(CONF_STATE_TEMPLATE)
if state_template and (state_entity_id or state_attribute):
raise vol.Invalid("state_entity_id/state_attribute and state_template are mutually exclusive")
return value
ENTITY_PROPERTY_SCHEMA = vol.All(
cv.has_at_least_one_key(
CONF_ENTITY_PROPERTY_ENTITY,
CONF_ENTITY_PROPERTY_ATTRIBUTE,
CONF_ENTITY_PROPERTY_VALUE_TEMPLATE,
),
vol.All(
{
vol.Required(CONF_ENTITY_PROPERTY_TYPE): vol.Schema(vol.All(str, property_type)),
vol.Optional(CONF_ENTITY_PROPERTY_UNIT_OF_MEASUREMENT): cv.string,
vol.Optional(CONF_ENTITY_PROPERTY_TARGET_UNIT_OF_MEASUREMENT): cv.string,
vol.Optional(CONF_ENTITY_PROPERTY_ENTITY): cv.entity_id,
vol.Optional(CONF_ENTITY_PROPERTY_ATTRIBUTE): cv.string,
vol.Optional(CONF_ENTITY_PROPERTY_VALUE_TEMPLATE): cv.template,
},
property_attributes,
),
)
ENTITY_MODE_MAP_SCHEMA = vol.Schema(
{vol.All(cv.string, mode_instance): vol.Schema({vol.All(cv.string, mode): vol.All(cv.ensure_list, [cv.string])})}
)
ENTITY_EVENT_MAP_SCHEMA = vol.Schema(
{vol.All(cv.string, event_instance): vol.Schema({cv.string: vol.All(cv.ensure_list, [cv.string])})}
)
ENTITY_RANGE_SCHEMA = vol.Schema(
{
vol.Optional(CONF_ENTITY_RANGE_MAX): vol.All(vol.Coerce(float), vol.Range(min=-100.0, max=1000.0)),
vol.Optional(CONF_ENTITY_RANGE_MIN): vol.All(vol.Coerce(float), vol.Range(min=-100.0, max=1000.0)),
vol.Optional(CONF_ENTITY_RANGE_PRECISION): vol.All(vol.Coerce(float), vol.Range(min=-100.0, max=1000.0)),
},
)
ENTITY_CUSTOM_MODE_SCHEMA = vol.Schema(
{
vol.All(cv.string, mode_instance): vol.Any(
vol.All(
{
vol.Optional(CONF_ENTITY_CUSTOM_MODE_SET_MODE): cv.SERVICE_SCHEMA,
vol.Optional(CONF_ENTITY_CUSTOM_CAPABILITY_STATE_ENTITY_ID): cv.entity_id,
vol.Optional(CONF_ENTITY_CUSTOM_CAPABILITY_STATE_ATTRIBUTE): cv.string,
vol.Optional(CONF_STATE_TEMPLATE): cv.template,
},
custom_capability_state,
),
cv.boolean,
)
}
)
ENTITY_CUSTOM_RANGE_SCHEMA = vol.Schema(
{
vol.All(cv.string, range_instance): vol.Any(
vol.All(
{
vol.Optional(CONF_ENTITY_CUSTOM_RANGE_SET_VALUE): vol.Any(cv.SERVICE_SCHEMA),
vol.Optional(CONF_ENTITY_CUSTOM_RANGE_INCREASE_VALUE): vol.Any(cv.SERVICE_SCHEMA),
vol.Optional(CONF_ENTITY_CUSTOM_RANGE_DECREASE_VALUE): vol.Any(cv.SERVICE_SCHEMA),
vol.Optional(CONF_ENTITY_RANGE): ENTITY_RANGE_SCHEMA,
vol.Optional(CONF_ENTITY_CUSTOM_CAPABILITY_STATE_ENTITY_ID): cv.entity_id,
vol.Optional(CONF_ENTITY_CUSTOM_CAPABILITY_STATE_ATTRIBUTE): cv.string,
vol.Optional(CONF_STATE_TEMPLATE): cv.template,
},
custom_capability_state,
),
cv.boolean,
)
}
)
ENTITY_CUSTOM_TOGGLE_SCHEMA = vol.Schema(
{
vol.All(cv.string, toggle_instance): vol.Any(
vol.All(
{
vol.Optional(CONF_ENTITY_CUSTOM_TOGGLE_TURN_ON): cv.SERVICE_SCHEMA,
vol.Optional(CONF_ENTITY_CUSTOM_TOGGLE_TURN_OFF): cv.SERVICE_SCHEMA,
vol.Optional(CONF_ENTITY_CUSTOM_CAPABILITY_STATE_ENTITY_ID): cv.entity_id,
vol.Optional(CONF_ENTITY_CUSTOM_CAPABILITY_STATE_ATTRIBUTE): cv.string,
vol.Optional(CONF_STATE_TEMPLATE): cv.template,
},
custom_capability_state,
),
cv.boolean,
)
}
)
ENTITY_SCHEMA = vol.All(
vol.Schema(
{
vol.Optional(CONF_NAME): cv.string,
vol.Optional(CONF_ROOM): cv.string,
vol.Optional(CONF_TYPE): vol.All(cv.string, device_type),
vol.Optional(CONF_STATE_TEMPLATE): cv.template,
vol.Optional(CONF_TURN_ON): vol.Any(cv.SERVICE_SCHEMA, cv.boolean),
vol.Optional(CONF_TURN_OFF): vol.Any(cv.SERVICE_SCHEMA, cv.boolean),
vol.Optional(CONF_DEVICE_CLASS): vol.In(EventDeviceClass.BUTTON),
vol.Optional(CONF_FEATURES): vol.All(cv.ensure_list, entity_features),
vol.Optional(CONF_ENTITY_PROPERTIES): [ENTITY_PROPERTY_SCHEMA],
vol.Optional(CONF_SUPPORT_SET_CHANNEL): cv.boolean,
vol.Optional(CONF_STATE_UNKNOWN): cv.boolean,
vol.Optional(CONF_SLOW): cv.boolean,
vol.Optional(CONF_BACKLIGHT_ENTITY_ID): cv.entity_id,
vol.Optional(CONF_COLOR_PROFILE): cv.string,
vol.Optional(CONF_ERROR_CODE_TEMPLATE): cv.template,
vol.Optional(CONF_ENTITY_RANGE): ENTITY_RANGE_SCHEMA,
vol.Optional(CONF_ENTITY_MODE_MAP): ENTITY_MODE_MAP_SCHEMA,
vol.Optional(CONF_ENTITY_EVENT_MAP): vol.All(ENTITY_EVENT_MAP_SCHEMA, event_map),
vol.Optional(CONF_ENTITY_CUSTOM_MODES): ENTITY_CUSTOM_MODE_SCHEMA,
vol.Optional(CONF_ENTITY_CUSTOM_TOGGLES): ENTITY_CUSTOM_TOGGLE_SCHEMA,
vol.Optional(CONF_ENTITY_CUSTOM_RANGES): ENTITY_CUSTOM_RANGE_SCHEMA,
}
)
)
NOTIFIER_SCHEMA = vol.Schema(
{
vol.Required(CONF_NOTIFIER_OAUTH_TOKEN): cv.string,
vol.Required(CONF_NOTIFIER_SKILL_ID): cv.string,
vol.Required(CONF_NOTIFIER_USER_ID): cv.string,
},
)
SETTINGS_SCHEMA = vol.All(
cv.deprecated(CONF_PRESSURE_UNIT),
{
vol.Optional(CONF_PRESSURE_UNIT): cv.string,
vol.Optional(CONF_BETA): cv.boolean,
vol.Optional(CONF_CLOUD_STREAM): cv.boolean,
},
)
YANDEX_SMART_HOME_SCHEMA = vol.All(
vol.Schema(
{
vol.Optional(CONF_NOTIFIER): vol.All(cv.ensure_list, [NOTIFIER_SCHEMA]),
vol.Optional(CONF_SETTINGS): vol.All(lambda value: value or {}, SETTINGS_SCHEMA),
vol.Optional(CONF_FILTER): BASE_FILTER_SCHEMA,
vol.Optional(CONF_ENTITY_CONFIG): vol.All(lambda value: value or {}, {cv.entity_id: ENTITY_SCHEMA}),
vol.Optional(CONF_COLOR_PROFILE): vol.Schema({cv.string: {vol.All(color_name): vol.All(color_value)}}),
},
)
)