469 lines
17 KiB
Python
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)}}),
|
|
},
|
|
)
|
|
)
|