mirror of
https://github.com/esphome/esphome.git
synced 2026-08-17 10:52:56 +08:00
[core] Classify entity metadata visibility for the visual editor (#17503)
This commit is contained in:
@@ -448,7 +448,9 @@ _BINARY_SENSOR_SCHEMA = (
|
||||
cv.Exclusive(
|
||||
CONF_TRIGGER_ON_INITIAL_STATE, CONF_TRIGGER_ON_INITIAL_STATE
|
||||
): cv.boolean,
|
||||
cv.Optional(CONF_DEVICE_CLASS): validate_device_class,
|
||||
cv.Optional(
|
||||
CONF_DEVICE_CLASS, visibility=cv.Visibility.ADVANCED
|
||||
): validate_device_class,
|
||||
cv.Optional(CONF_FILTERS): validate_filters,
|
||||
cv.Optional(CONF_ON_PRESS): automation.validate_automation({}),
|
||||
cv.Optional(CONF_ON_RELEASE): automation.validate_automation({}),
|
||||
|
||||
@@ -50,7 +50,9 @@ _BUTTON_SCHEMA = (
|
||||
.extend(
|
||||
{
|
||||
cv.OnlyWith(CONF_MQTT_ID, "mqtt"): cv.declare_id(mqtt.MQTTButtonComponent),
|
||||
cv.Optional(CONF_DEVICE_CLASS): validate_device_class,
|
||||
cv.Optional(
|
||||
CONF_DEVICE_CLASS, visibility=cv.Visibility.ADVANCED
|
||||
): validate_device_class,
|
||||
cv.Optional(CONF_ON_PRESS): automation.validate_automation({}),
|
||||
}
|
||||
)
|
||||
|
||||
@@ -131,7 +131,9 @@ _COVER_SCHEMA = (
|
||||
cv.Optional(CONF_MQTT_JSON_STATE_PAYLOAD): cv.All(
|
||||
cv.requires_component("mqtt"), cv.boolean
|
||||
),
|
||||
cv.Optional(CONF_DEVICE_CLASS): cv.one_of(*DEVICE_CLASSES, lower=True),
|
||||
cv.Optional(
|
||||
CONF_DEVICE_CLASS, visibility=cv.Visibility.ADVANCED
|
||||
): cv.one_of(*DEVICE_CLASSES, lower=True),
|
||||
cv.Optional(CONF_POSITION_COMMAND_TOPIC): cv.All(
|
||||
cv.requires_component("mqtt"), cv.subscribe_topic
|
||||
),
|
||||
|
||||
@@ -50,7 +50,9 @@ _EVENT_SCHEMA = (
|
||||
{
|
||||
cv.OnlyWith(CONF_MQTT_ID, "mqtt"): cv.declare_id(mqtt.MQTTEventComponent),
|
||||
cv.GenerateID(): cv.declare_id(Event),
|
||||
cv.Optional(CONF_DEVICE_CLASS): validate_device_class,
|
||||
cv.Optional(
|
||||
CONF_DEVICE_CLASS, visibility=cv.Visibility.ADVANCED
|
||||
): validate_device_class,
|
||||
cv.Optional(CONF_ON_EVENT): automation.validate_automation({}),
|
||||
}
|
||||
)
|
||||
|
||||
@@ -212,9 +212,15 @@ _NUMBER_SCHEMA = (
|
||||
},
|
||||
cv.has_at_least_one_key(CONF_ABOVE, CONF_BELOW),
|
||||
),
|
||||
cv.Optional(CONF_UNIT_OF_MEASUREMENT): validate_unit_of_measurement,
|
||||
cv.Optional(CONF_MODE, default="AUTO"): cv.enum(NUMBER_MODES, upper=True),
|
||||
cv.Optional(CONF_DEVICE_CLASS): validate_device_class,
|
||||
cv.Optional(
|
||||
CONF_UNIT_OF_MEASUREMENT, visibility=cv.Visibility.ADVANCED
|
||||
): validate_unit_of_measurement,
|
||||
cv.Optional(
|
||||
CONF_MODE, default="AUTO", visibility=cv.Visibility.ADVANCED
|
||||
): cv.enum(NUMBER_MODES, upper=True),
|
||||
cv.Optional(
|
||||
CONF_DEVICE_CLASS, visibility=cv.Visibility.ADVANCED
|
||||
): validate_device_class,
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
@@ -321,13 +321,25 @@ _SENSOR_SCHEMA = (
|
||||
{
|
||||
cv.OnlyWith(CONF_MQTT_ID, "mqtt"): cv.declare_id(mqtt.MQTTSensorComponent),
|
||||
cv.GenerateID(): cv.declare_id(Sensor),
|
||||
cv.Optional(CONF_UNIT_OF_MEASUREMENT): validate_unit_of_measurement,
|
||||
cv.Optional(CONF_ACCURACY_DECIMALS): validate_accuracy_decimals,
|
||||
cv.Optional(CONF_DEVICE_CLASS): validate_device_class,
|
||||
cv.Optional(CONF_STATE_CLASS): validate_state_class,
|
||||
cv.Optional(CONF_ENTITY_CATEGORY): sensor_entity_category,
|
||||
cv.Optional(CONF_FORCE_UPDATE, default=False): cv.boolean,
|
||||
cv.Optional(CONF_EXPIRE_AFTER): cv.All(
|
||||
cv.Optional(
|
||||
CONF_UNIT_OF_MEASUREMENT, visibility=cv.Visibility.ADVANCED
|
||||
): validate_unit_of_measurement,
|
||||
cv.Optional(
|
||||
CONF_ACCURACY_DECIMALS, visibility=cv.Visibility.ADVANCED
|
||||
): validate_accuracy_decimals,
|
||||
cv.Optional(
|
||||
CONF_DEVICE_CLASS, visibility=cv.Visibility.ADVANCED
|
||||
): validate_device_class,
|
||||
cv.Optional(
|
||||
CONF_STATE_CLASS, visibility=cv.Visibility.ADVANCED
|
||||
): validate_state_class,
|
||||
cv.Optional(
|
||||
CONF_ENTITY_CATEGORY, visibility=cv.Visibility.ADVANCED
|
||||
): sensor_entity_category,
|
||||
cv.Optional(
|
||||
CONF_FORCE_UPDATE, default=False, visibility=cv.Visibility.ADVANCED
|
||||
): cv.boolean,
|
||||
cv.Optional(CONF_EXPIRE_AFTER, visibility=cv.Visibility.ADVANCED): cv.All(
|
||||
cv.requires_component("mqtt"),
|
||||
cv.Any(None, cv.positive_time_period_milliseconds),
|
||||
),
|
||||
|
||||
@@ -78,7 +78,9 @@ _SWITCH_SCHEMA = (
|
||||
cv.Optional(CONF_ON_STATE): automation.validate_automation({}),
|
||||
cv.Optional(CONF_ON_TURN_ON): automation.validate_automation({}),
|
||||
cv.Optional(CONF_ON_TURN_OFF): automation.validate_automation({}),
|
||||
cv.Optional(CONF_DEVICE_CLASS): validate_device_class,
|
||||
cv.Optional(
|
||||
CONF_DEVICE_CLASS, visibility=cv.Visibility.ADVANCED
|
||||
): validate_device_class,
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
@@ -144,7 +144,9 @@ _TEXT_SENSOR_SCHEMA = (
|
||||
{
|
||||
cv.OnlyWith(CONF_MQTT_ID, "mqtt"): cv.declare_id(mqtt.MQTTTextSensor),
|
||||
cv.GenerateID(): cv.declare_id(TextSensor),
|
||||
cv.Optional(CONF_DEVICE_CLASS): validate_device_class,
|
||||
cv.Optional(
|
||||
CONF_DEVICE_CLASS, visibility=cv.Visibility.ADVANCED
|
||||
): validate_device_class,
|
||||
cv.Optional(CONF_FILTERS): validate_filters,
|
||||
cv.Optional(CONF_ON_VALUE): automation.validate_automation({}),
|
||||
cv.Optional(CONF_ON_RAW_VALUE): automation.validate_automation({}),
|
||||
|
||||
@@ -54,7 +54,9 @@ _UPDATE_SCHEMA = (
|
||||
.extend(
|
||||
{
|
||||
cv.OnlyWith(CONF_MQTT_ID, "mqtt"): cv.declare_id(mqtt.MQTTUpdateComponent),
|
||||
cv.Optional(CONF_DEVICE_CLASS): cv.one_of(*DEVICE_CLASSES, lower=True),
|
||||
cv.Optional(
|
||||
CONF_DEVICE_CLASS, visibility=cv.Visibility.ADVANCED
|
||||
): cv.one_of(*DEVICE_CLASSES, lower=True),
|
||||
cv.Optional(CONF_ON_UPDATE_AVAILABLE): automation.validate_automation(
|
||||
single=True
|
||||
),
|
||||
@@ -136,7 +138,9 @@ async def to_code(config):
|
||||
automation.maybe_simple_id(
|
||||
{
|
||||
cv.GenerateID(): cv.use_id(UpdateEntity),
|
||||
cv.Optional(CONF_FORCE_UPDATE, default=False): cv.templatable(cv.boolean),
|
||||
cv.Optional(
|
||||
CONF_FORCE_UPDATE, default=False, visibility=cv.Visibility.ADVANCED
|
||||
): cv.templatable(cv.boolean),
|
||||
}
|
||||
),
|
||||
synchronous=True,
|
||||
|
||||
@@ -87,7 +87,9 @@ _VALVE_SCHEMA = (
|
||||
{
|
||||
cv.GenerateID(): cv.declare_id(Valve),
|
||||
cv.OnlyWith(CONF_MQTT_ID, "mqtt"): cv.declare_id(mqtt.MQTTValveComponent),
|
||||
cv.Optional(CONF_DEVICE_CLASS): cv.one_of(*DEVICE_CLASSES, lower=True),
|
||||
cv.Optional(
|
||||
CONF_DEVICE_CLASS, visibility=cv.Visibility.ADVANCED
|
||||
): cv.one_of(*DEVICE_CLASSES, lower=True),
|
||||
cv.Optional(CONF_POSITION_COMMAND_TOPIC): cv.All(
|
||||
cv.requires_component("mqtt"), cv.subscribe_topic
|
||||
),
|
||||
|
||||
@@ -172,7 +172,9 @@ sorting_group = {
|
||||
|
||||
WEBSERVER_SORTING_SCHEMA = cv.Schema(
|
||||
{
|
||||
cv.Optional(CONF_WEB_SERVER): cv.Schema(
|
||||
# The per-entity web_server block is cosmetic dashboard ordering —
|
||||
# mark the whole block advanced; the children inherit via the cascade.
|
||||
cv.Optional(CONF_WEB_SERVER, visibility=cv.Visibility.ADVANCED): cv.Schema(
|
||||
{
|
||||
cv.OnlyWith(CONF_WEB_SERVER_ID, "web_server"): cv.use_id(WebServer),
|
||||
cv.Optional(CONF_SORTING_WEIGHT): cv.All(
|
||||
|
||||
@@ -292,10 +292,14 @@ class Visibility(StrEnum):
|
||||
the same way. ESPHome itself ignores the value at runtime;
|
||||
consumers downstream of the schema dump act on it.
|
||||
|
||||
A field with no ``visibility`` set (the default) renders on the
|
||||
editor's main form. The two values below are points along a
|
||||
single axis of "how prominently to surface this":
|
||||
Three points along a single axis of "how prominently to surface
|
||||
this", from least to most hidden:
|
||||
|
||||
- ``UI`` — always render on the editor's main form. Use to
|
||||
promote an ``Optional`` that would otherwise fall through to
|
||||
the advanced disclosure (see the default rule below): the
|
||||
"headline" config a user reaches for first (e.g. a sensor's
|
||||
``name`` or its primary pin/address).
|
||||
- ``ADVANCED`` — render under the editor's "advanced settings"
|
||||
disclosure. Use for fields whose default is right for ~all
|
||||
users (e.g. ``update_interval`` on time platforms — 15 min is
|
||||
@@ -307,25 +311,35 @@ class Visibility(StrEnum):
|
||||
tweaks can break boot). The YAML escape hatch stays
|
||||
available for the rare power-user override.
|
||||
|
||||
The single-axis shape encodes "yaml-only is strictly stronger
|
||||
than advanced" at the type level — there's no way to ask for
|
||||
both at once, and no way to set a contradictory state like
|
||||
"advanced=False, yaml_only=True".
|
||||
Default when unset (``visibility=None``): resolved by the
|
||||
consumer, not encoded on the marker. A schema-aware editor
|
||||
treats an ``Optional`` with no setting as ``ADVANCED`` (most
|
||||
optional knobs have sensible defaults and would clutter the
|
||||
form), and a ``Required`` with no setting as ``UI`` (a required
|
||||
field needs the user's attention). Pass an explicit value to
|
||||
override either default — most commonly ``UI`` to keep a
|
||||
high-value ``Optional`` on the main form.
|
||||
|
||||
The single-axis shape encodes the strictness ladder
|
||||
(``UI`` < ``ADVANCED`` < ``YAML_ONLY``) at the type level —
|
||||
there's no way to set a contradictory state.
|
||||
|
||||
Per-field; the dumper walks recursively into nested schemas
|
||||
and emits each field's setting independently. Cascading
|
||||
semantics — "a stricter parent makes its descendants at-least
|
||||
as strict" — belong on the consumer side: the schema marker
|
||||
is faithfully what the field author wrote, and a consumer that
|
||||
cares about effective visibility walks the parent chain and
|
||||
takes the strictest setting. ``YAML_ONLY`` is strictly stronger
|
||||
than ``ADVANCED``, which is strictly stronger than no setting.
|
||||
Inner fields can declare their own visibility; an inner
|
||||
and emits each field's setting independently, omitting the key
|
||||
when unset so the dump stays compact and the per-field default
|
||||
is the consumer's to apply. Cascading semantics — "a stricter
|
||||
parent makes its descendants at-least as strict" — belong on the
|
||||
consumer side: the schema marker is faithfully what the field
|
||||
author wrote, and a consumer that cares about effective
|
||||
visibility walks the parent chain and takes the strictest
|
||||
setting. Inner fields can declare their own visibility; an inner
|
||||
``YAML_ONLY`` under an ``ADVANCED`` parent stays ``YAML_ONLY``,
|
||||
and the consumer's cascade keeps siblings under the parent at
|
||||
``ADVANCED`` regardless of their own (less-strict) setting.
|
||||
and the consumer's cascade keeps a ``UI`` sibling under an
|
||||
``ADVANCED`` parent at ``ADVANCED`` regardless of its own
|
||||
(less-strict) setting.
|
||||
"""
|
||||
|
||||
UI = "ui"
|
||||
ADVANCED = "advanced"
|
||||
YAML_ONLY = "yaml_only"
|
||||
|
||||
@@ -347,6 +361,9 @@ class Optional(vol.Optional):
|
||||
|
||||
See :class:`Visibility` for the ``visibility`` kwarg — a UI
|
||||
hint for schema-driven editors that doesn't affect validation.
|
||||
Left unset, an ``Optional`` is treated as ``Visibility.ADVANCED``
|
||||
by schema-aware editors; pass ``Visibility.UI`` to keep it on the
|
||||
main form.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
@@ -369,9 +386,11 @@ class Required(vol.Required):
|
||||
|
||||
See :class:`Visibility` for the ``visibility`` kwarg — a UI
|
||||
hint for schema-driven editors that doesn't affect validation.
|
||||
Required fields rarely need it (a required field by definition
|
||||
needs the user's attention) but the kwarg is exposed for
|
||||
symmetry so consumers can apply uniform logic across key markers.
|
||||
Required fields rarely need it: left unset, a ``Required`` is
|
||||
treated as on the main form (``Visibility.UI``) by schema-aware
|
||||
editors, since a required field needs the user's attention. The
|
||||
kwarg is exposed for symmetry so consumers can apply uniform
|
||||
logic across key markers.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
@@ -2274,16 +2293,25 @@ MQTT_COMPONENT_AVAILABILITY_SCHEMA = Schema(
|
||||
}
|
||||
)
|
||||
|
||||
# Per-entity MQTT plumbing — integration metadata, never a primary UI field.
|
||||
MQTT_COMPONENT_SCHEMA = Schema(
|
||||
{
|
||||
Optional(CONF_QOS): All(requires_component("mqtt"), mqtt_qos),
|
||||
Optional(CONF_RETAIN): All(requires_component("mqtt"), boolean),
|
||||
Optional(CONF_DISCOVERY): All(requires_component("mqtt"), boolean),
|
||||
Optional(CONF_SUBSCRIBE_QOS): All(requires_component("mqtt"), mqtt_qos),
|
||||
Optional(CONF_STATE_TOPIC): All(
|
||||
Optional(CONF_QOS, visibility=Visibility.ADVANCED): All(
|
||||
requires_component("mqtt"), mqtt_qos
|
||||
),
|
||||
Optional(CONF_RETAIN, visibility=Visibility.ADVANCED): All(
|
||||
requires_component("mqtt"), boolean
|
||||
),
|
||||
Optional(CONF_DISCOVERY, visibility=Visibility.ADVANCED): All(
|
||||
requires_component("mqtt"), boolean
|
||||
),
|
||||
Optional(CONF_SUBSCRIBE_QOS, visibility=Visibility.ADVANCED): All(
|
||||
requires_component("mqtt"), mqtt_qos
|
||||
),
|
||||
Optional(CONF_STATE_TOPIC, visibility=Visibility.ADVANCED): All(
|
||||
requires_component("mqtt"), templatable(publish_topic)
|
||||
),
|
||||
Optional(CONF_AVAILABILITY): All(
|
||||
Optional(CONF_AVAILABILITY, visibility=Visibility.ADVANCED): All(
|
||||
requires_component("mqtt"), Any(None, MQTT_COMPONENT_AVAILABILITY_SCHEMA)
|
||||
),
|
||||
}
|
||||
@@ -2291,10 +2319,12 @@ MQTT_COMPONENT_SCHEMA = Schema(
|
||||
|
||||
MQTT_COMMAND_COMPONENT_SCHEMA = MQTT_COMPONENT_SCHEMA.extend(
|
||||
{
|
||||
Optional(CONF_COMMAND_TOPIC): All(
|
||||
Optional(CONF_COMMAND_TOPIC, visibility=Visibility.ADVANCED): All(
|
||||
requires_component("mqtt"), templatable(subscribe_topic)
|
||||
),
|
||||
Optional(CONF_COMMAND_RETAIN): All(requires_component("mqtt"), boolean),
|
||||
Optional(CONF_COMMAND_RETAIN, visibility=Visibility.ADVANCED): All(
|
||||
requires_component("mqtt"), boolean
|
||||
),
|
||||
}
|
||||
)
|
||||
|
||||
@@ -2369,12 +2399,16 @@ def string_no_slash(value):
|
||||
|
||||
ENTITY_BASE_SCHEMA = Schema(
|
||||
{
|
||||
Optional(CONF_NAME): _validate_entity_name,
|
||||
Optional(CONF_INTERNAL): boolean,
|
||||
Optional(CONF_DISABLED_BY_DEFAULT, default=False): boolean,
|
||||
Optional(CONF_ICON): icon,
|
||||
Optional(CONF_ENTITY_CATEGORY): entity_category,
|
||||
Optional(CONF_DEVICE_ID): sub_device_id,
|
||||
# The name is every entity's headline field — keep it on the
|
||||
# main form rather than letting it fall through to advanced.
|
||||
Optional(CONF_NAME, visibility=Visibility.UI): _validate_entity_name,
|
||||
Optional(CONF_INTERNAL, visibility=Visibility.ADVANCED): boolean,
|
||||
Optional(
|
||||
CONF_DISABLED_BY_DEFAULT, default=False, visibility=Visibility.ADVANCED
|
||||
): boolean,
|
||||
Optional(CONF_ICON, visibility=Visibility.ADVANCED): icon,
|
||||
Optional(CONF_ENTITY_CATEGORY, visibility=Visibility.ADVANCED): entity_category,
|
||||
Optional(CONF_DEVICE_ID, visibility=Visibility.ADVANCED): sub_device_id,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@@ -1174,9 +1174,10 @@ def test_update_interval__never_passes_through() -> None:
|
||||
def test_optional_default_visibility_is_none() -> None:
|
||||
"""An ``Optional`` with no ``visibility`` kwarg reports ``None``.
|
||||
|
||||
Consumers can read the attribute directly with plain attribute
|
||||
access; absence (``None``) means "render on the editor's main
|
||||
form."
|
||||
The marker stays faithful to what the author wrote: ESPHome does
|
||||
not encode the default on it. Resolving ``None`` to an effective
|
||||
visibility is the consumer's job — a schema-aware editor treats an
|
||||
unset ``Optional`` as ``ADVANCED`` (see :class:`Visibility`).
|
||||
"""
|
||||
o = cv.Optional("foo")
|
||||
assert o.visibility is None
|
||||
@@ -1194,6 +1195,17 @@ def test_optional_visibility_yaml_only() -> None:
|
||||
assert o.visibility is cv.Visibility.YAML_ONLY
|
||||
|
||||
|
||||
def test_optional_visibility_ui() -> None:
|
||||
"""``visibility=Visibility.UI`` is recorded on the marker.
|
||||
|
||||
``UI`` promotes an ``Optional`` onto the editor's main form,
|
||||
overriding the consumer's default of ``ADVANCED`` for unset
|
||||
optionals.
|
||||
"""
|
||||
o = cv.Optional("foo", visibility=cv.Visibility.UI)
|
||||
assert o.visibility is cv.Visibility.UI
|
||||
|
||||
|
||||
def test_visibility_str_values_match_dump_emission() -> None:
|
||||
"""``Visibility`` is a ``StrEnum`` whose values are the literal
|
||||
strings the schema dumper emits.
|
||||
@@ -1203,6 +1215,7 @@ def test_visibility_str_values_match_dump_emission() -> None:
|
||||
field — pinning the on-the-wire spelling here keeps the dump
|
||||
contract stable.
|
||||
"""
|
||||
assert str(cv.Visibility.UI) == "ui"
|
||||
assert str(cv.Visibility.ADVANCED) == "advanced"
|
||||
assert str(cv.Visibility.YAML_ONLY) == "yaml_only"
|
||||
|
||||
@@ -1325,6 +1338,57 @@ def test_visibility_marker_is_per_field_no_mutation() -> None:
|
||||
assert inner_yaml_only.visibility is cv.Visibility.YAML_ONLY
|
||||
|
||||
|
||||
def test_entity_metadata_visibility_hints() -> None:
|
||||
"""Entity and value-describing metadata is classified for visual editors.
|
||||
|
||||
The headline ``name`` stays on the main form (``UI``); descriptive
|
||||
metadata (device_class, unit, …), presentation options, and per-entity
|
||||
integration plumbing (MQTT, web_server ordering) fall to the advanced
|
||||
disclosure (``ADVANCED``).
|
||||
"""
|
||||
advanced = cv.Visibility.ADVANCED
|
||||
|
||||
entity_base = {str(k): k for k in cv.ENTITY_BASE_SCHEMA.schema}
|
||||
assert entity_base["name"].visibility is cv.Visibility.UI
|
||||
for field in (
|
||||
"icon",
|
||||
"internal",
|
||||
"disabled_by_default",
|
||||
"entity_category",
|
||||
"device_id",
|
||||
):
|
||||
assert entity_base[field].visibility is advanced, field
|
||||
|
||||
mqtt = {str(k): k for k in cv.MQTT_COMPONENT_SCHEMA.schema}
|
||||
for field in ("qos", "retain", "discovery", "state_topic", "availability"):
|
||||
assert mqtt[field].visibility is advanced, field
|
||||
|
||||
from esphome.components import binary_sensor, number, sensor
|
||||
from esphome.components.web_server import WEBSERVER_SORTING_SCHEMA
|
||||
|
||||
sensor_markers = {str(k): k for k in sensor.sensor_schema().schema}
|
||||
for field in (
|
||||
"unit_of_measurement",
|
||||
"accuracy_decimals",
|
||||
"device_class",
|
||||
"state_class",
|
||||
"force_update",
|
||||
):
|
||||
assert sensor_markers[field].visibility is advanced, field
|
||||
|
||||
binary = {str(k): k for k in binary_sensor.binary_sensor_schema().schema}
|
||||
assert binary["device_class"].visibility is advanced
|
||||
|
||||
number_markers = {str(k): k for k in number.number_schema(number.Number).schema}
|
||||
assert number_markers["mode"].visibility is advanced
|
||||
assert number_markers["device_class"].visibility is advanced
|
||||
|
||||
# The whole per-entity web_server block is advanced; children inherit
|
||||
# via the consumer cascade, so only the parent key carries the hint.
|
||||
web = {str(k): k for k in WEBSERVER_SORTING_SCHEMA.schema}
|
||||
assert web["web_server"].visibility is advanced
|
||||
|
||||
|
||||
def _wrap_str(value: str) -> ESPHomeDataBase:
|
||||
"""Wrap a raw string as an ESPHomeDataBase, mimicking a YAML-loaded value."""
|
||||
return make_data_base(value)
|
||||
|
||||
Reference in New Issue
Block a user