18import esphome.codegen
as cg
19import esphome.config_validation
as cv
20from esphome.const
import (
34from .hub_names
import (
41 MIN_BATTERY_LEVEL_POLL_INTERVAL_MS,
42 MIN_RAIN_SENSOR_POLL_INTERVAL_MS,
43 MIN_SUN_LEVEL_POLL_INTERVAL_MS,
44 MIN_STATUS_POLL_INTERVAL_MS,
45 MIN_STATUS_REFRESH_INTERVAL_MS,
49_LOGGER = logging.getLogger(__name__)
66SPI_RADIO_TYPES = frozenset({
"sx1276",
"sx1262",
"lr1121"})
73TCXO_VOLTAGE_OPTIONS = {
93FemProfile = home_io_control_ns.enum(
"FemProfile", is_class=
True)
95 "none": FemProfile.NONE,
96 "gc1109": FemProfile.GC1109,
97 "kct8103l": FemProfile.KCT8103L,
98 "xy16p35": FemProfile.XY16P35,
110 "gc1109": (CONF_VFEM_PIN, CONF_FEM_EN_PIN, CONF_FEM_PA_PIN),
111 "kct8103l": (CONF_VFEM_PIN, CONF_FEM_EN_PIN, CONF_FEM_PA_PIN),
112 "xy16p35": (CONF_VFEM_PIN, CONF_FEM_PA_PIN),
122FEM_TX_POWER_MAX_QUIET = {
130 """Require the SPI device keys for a radio on the SPI bus. The schema takes them as optional
131 (``spi.spi_device_schema(False, ...)``) so that the requirement follows ``radio_type`` here,
132 in one place, rather than being hard-wired into the schema for every radio."""
133 radio_type = config[CONF_RADIO_TYPE]
134 if radio_type
in SPI_RADIO_TYPES
and CONF_CS_PIN
not in config:
136 f
"radio_type: {radio_type} is an SPI radio and needs cs_pin: (its chip-select GPIO); "
137 "a board package in config/boards/ sets it for you",
144 """Cross-key validation for `fem:` (ADR 0035): which front-end part's control behaviour the
145 SX1262 driver applies to fem_pa_pin (and, for the two parts that have one, fem_en_pin), never
146 which GPIO numbers to use for them -- those are always the board package's job, the same as
147 every other radio pin. Requires radio_type: sx1262 (the raw FEM pins are silently ignored on
148 every other chip -- select_and_construct_radio_() only ever hands them to RadioSX1262 -- so a
149 mismatched radio_type would configure hardware the driver never drives), and that every pin
150 this profile's behaviour needs is actually present, naming exactly which is missing rather
151 than leaving the driver to silently no-op on a null pin. Also warns when tx_power looks likely
152 to push this FEM's antenna-port power over a typical 868 MHz SRD limit -- radio_sx1262.cpp's
153 own init() logs the actual per-part estimate this warning can only gesture at without knowing
154 the front-end part's exact gain table here too.
156 profile = config[CONF_FEM]
157 if profile ==
"none":
160 if config[CONF_RADIO_TYPE] !=
"sx1262":
162 f
"fem: {profile} requires radio_type: sx1262 -- the front-end module pins are only "
163 "wired to the SX1262 driver"
166 missing = [key
for key
in FEM_REQUIRED_PINS[profile]
if key
not in config]
169 f
"fem: {profile} requires {', '.join(missing)} to be set -- see "
170 "docs/hardware.md#front-end-module-fem-support for what your board needs"
173 if config[CONF_TX_POWER] > FEM_TX_POWER_MAX_QUIET[profile]:
175 "fem: %s together with tx_power: %d -- a front-end module turns tx_power into a "
176 "much larger antenna-port power than the bare SX1262 would radiate, and at this "
177 "setting the estimate is likely over a typical 868 MHz SRD ERP limit (+14 dBm). The "
178 "driver logs its own per-profile estimate (and its uncertainty) at boot -- check it, "
179 "and prefer tx_power: %d or lower (what this profile's own board package ships) "
180 "until you have measured the actual radiated power for your board",
182 config[CONF_TX_POWER],
183 FEM_TX_POWER_MAX_QUIET[profile],
192DEVICE_TYPE_MAX = 0x3FF
194DEVICE_TYPE_OPTIONS = {
196 "interior_venetian_blind": 0x01,
197 "roller_shutter": 0x02,
198 "vertical_exterior_awning": 0x03,
199 "window_opener": 0x04,
200 "garage_opener": 0x05,
203 "rolling_door_opener": 0x08,
205 "vertical_interior_blind": 0x0A,
206 "dual_roller_shutter": 0x0D,
207 "heating_temperature_interface": 0x0E,
208 "on_off_switch": 0x0F,
209 "horizontal_awning": 0x10,
210 "exterior_venetian_blind": 0x11,
211 "louvre_blind": 0x12,
212 "curtain_track": 0x13,
213 "exterior_heating": 0x15,
215 "intrusion_alarm": 0x17,
216 "swinging_shutter": 0x18,
217 "sliding_window": 0x1B,
218 "bioclimatic_pergola": 0x1D,
219 "domestic_hot_water": 0x33,
220 "electrical_heater": 0x34,
226DEFAULT_COVER_DEVICE_CLASS = {
227 "garage_opener": DEVICE_CLASS_GARAGE,
228 "gate_opener": DEVICE_CLASS_GATE,
229 "rolling_door_opener": DEVICE_CLASS_DOOR,
230 "window_opener": DEVICE_CLASS_WINDOW,
231 "sliding_window": DEVICE_CLASS_WINDOW,
232 "roller_shutter": DEVICE_CLASS_SHUTTER,
233 "dual_roller_shutter": DEVICE_CLASS_SHUTTER,
234 "swinging_shutter": DEVICE_CLASS_SHUTTER,
235 "interior_venetian_blind": DEVICE_CLASS_BLIND,
236 "exterior_venetian_blind": DEVICE_CLASS_BLIND,
237 "louvre_blind": DEVICE_CLASS_BLIND,
238 "vertical_interior_blind": DEVICE_CLASS_SHADE,
239 "curtain_track": DEVICE_CLASS_CURTAIN,
240 "horizontal_awning": DEVICE_CLASS_AWNING,
241 "vertical_exterior_awning": DEVICE_CLASS_AWNING,
251FEATURE_MAIN_PARAMETER_ONLY = 0x0001
252FEATURE_LEVELS = 0x0002
254FEATURE_FAVORITE = 0x0008
256FEATURE_PARTIAL = 0x0020
257FEATURE_PEDESTRIAN = 0x0040
258FEATURE_EXTENDED_POSITION_FRAME = 0x0080
259FEATURE_WINDOW_COMPANIONS = 0x0100
260FEATURE_INVERTED = 0x0200
261FEATURE_WATER_HEATER = 0x0400
262FEATURE_NIGHT_LIGHT = 0x0800
266KIND_EXPERIMENTAL = 0x8000
272DEVICE_MODEL_OPTIONS = {
274 "heater_with_light": 2,
283 "interior_venetian_blind": 0x000E,
284 "roller_shutter": 0x008A,
285 "roller_shutter/1": 0x008A,
286 "roller_shutter/2": 0x008A,
287 "vertical_exterior_awning": 0x008A,
288 "window_opener": 0x009A,
289 "window_opener/1": 0x009A,
290 "garage_opener": 0x0023,
291 "garage_opener/58": 0x0021,
296 "gate_opener": 0x00CA,
297 "gate_opener/58": 0x8049,
298 "rolling_door_opener": 0x008A,
302 "vertical_interior_blind": 0x008A,
303 "dual_roller_shutter": 0x000A,
304 "on_off_switch": 0x0080,
305 "on_off_switch/2": 0x8080,
306 "horizontal_awning": 0x028A,
307 "horizontal_awning/1": 0x028A,
308 "exterior_venetian_blind": 0x008E,
309 "louvre_blind": 0x008E,
310 "curtain_track": 0x008A,
311 "exterior_heating": 0x0003,
312 "exterior_heating/58": 0x0001,
313 "heat_pump/1": 0x8000,
314 "heat_pump/2": 0x8000,
315 "swinging_shutter": 0x008A,
316 "swinging_shutter/1": 0x008A,
317 "swinging_shutter/58": 0x8009,
318 "sliding_window": 0x008A,
319 "sliding_window/1": 0x808A,
320 "bioclimatic_pergola": 0x008A,
321 "domestic_hot_water": 0x8400,
322 "electrical_heater/1": 0x0100,
323 "electrical_heater/1/towel_dryer": 0x0000,
324 "electrical_heater/1/heater_with_light": 0x0900,
329NAMED_POSITION_FEATURES = {
332 "PEDESTRIAN": 0x0040,
337SUBTYPE_MAIN_PARAMETER_ONLY = 58
339_DEVICE_TYPE_NAMES = {value: name
for name, value
in DEVICE_TYPE_OPTIONS.items()}
343 """The io_device_type name of a node type value, or None for one YAML has no name for."""
344 return _DEVICE_TYPE_NAMES.get(device_type)
347_DEVICE_MODEL_NAMES = {value: name
for name, value
in DEVICE_MODEL_OPTIONS.items()}
351 """The io_model names (io_device_type value, io_subtype) offers, sorted; empty for most kinds."""
355 prefix = f
"{name}/{subtype}/"
356 return sorted(key[len(prefix):]
for key
in DEVICE_FEATURES
if key.startswith(prefix))
360 """The capability word of (io_device_type value, io_subtype, io_model value), as the C++
361 device_features() answers.
363 The model row; else the exact row; else, for subtype 58, the main-parameter-only variant of the
364 type's row (of no capabilities when the type has none); else the type's own row. A subtype without
365 a row of its own takes the type row's capabilities but never its KIND_EXPERIMENTAL marker: C++
366 device_is_experimental() reads only the exact row, so the marker names a row, not a family. A node type YAML
367 has no name for (a raw value) has no row here, so it has no capabilities beyond the subtype-58
368 rule. ``model`` is a DEVICE_MODEL_OPTIONS value, 0 for none; codegen passes the config's model
369 through platform_common.config_model().
372 model_name = _DEVICE_MODEL_NAMES.get(model)
373 if name
is not None and model_name
is not None:
374 model_row = DEVICE_FEATURES.get(f
"{name}/{subtype}/{model_name}")
375 if model_row
is not None:
378 exact = DEVICE_FEATURES.get(name
if subtype == 0
else f
"{name}/{subtype}")
379 if exact
is not None:
381 base = (DEVICE_FEATURES.get(name, 0)
if name
is not None else 0) & ~KIND_EXPERIMENTAL
382 if subtype == SUBTYPE_MAIN_PARAMETER_ONLY:
383 cleared = FEATURE_LEVELS | FEATURE_TILT | FEATURE_EXTENDED_POSITION_FRAME
384 return (base & ~cleared) | FEATURE_MAIN_PARAMETER_ONLY
389 """True when no capture or report has confirmed (io_device_type value, io_subtype) on a real device.
391 Reads KIND_EXPERIMENTAL from the same word as the capabilities, so it follows the same fallback as
392 device_features(): only an exact row (or model row) carries the marker, and a subtype with no row of
393 its own never inherits it from its type row.
395 return bool(
device_features(device_type, subtype, model) & KIND_EXPERIMENTAL)
400RENAMED_DEVICE_TYPES = {
402 "device type 'screen' no longer exists: type 11 is a secure configuration device, not an "
403 "actuator. Use the type Scan Paired Devices reports for your device, usually "
404 "'vertical_exterior_awning' or 'vertical_interior_blind'"
406 "blind":
"device type 'blind' was renamed: use 'vertical_interior_blind' instead",
407 "venetian_blind":
"device type 'venetian_blind' was renamed: use 'interior_venetian_blind' instead",
408 "awning":
"device type 'awning' was renamed: use 'vertical_exterior_awning' instead",
409 "dual_shutter":
"device type 'dual_shutter' was renamed: use 'dual_roller_shutter' instead",
410 "external_venetian_blind": (
411 "device type 'external_venetian_blind' was renamed: use 'exterior_venetian_blind' instead"
421MANUFACTURER_OPTIONS = {
428 "window_master": 0x07,
433 "atlantic_group": 0x0C,
439ONEWAY_WIRE_PROFILE_MANUFACTURERS = {
440 MANUFACTURER_OPTIONS[
"somfy"],
441 MANUFACTURER_OPTIONS[
"velux"],
446 """Resolve a lowercase, stripped token (a name from `options`, or a raw int/hex string) to
447 an integer 0-`max_value`.
449 Shared "named value, else raw integer" acceptance rule: validate_device_type()/
450 validate_linked_remote_entry() (DEVICE_TYPE_OPTIONS) and validate_manufacturer()
451 (MANUFACTURER_OPTIONS) are the same shape of small, protocol-defined enum with an escape
452 hatch for values this project hasn't named yet, so the lookup lives here once.
453 @raises ValueError if token is neither a known name nor a parseable integer.
454 @raises cv.Invalid if token parses as an integer but is out of range.
457 return options[token]
458 return cv.int_range(min=0, max=max_value)(int(token, 0))
462 """Resolve a lowercase, stripped device-type token (name or raw int/hex string) to 0-1023.
464 Single source of truth for the "named value from DEVICE_TYPE_OPTIONS, else raw integer"
465 acceptance rule shared by validate_device_type() (io_device_type) and
466 validate_linked_remote_entry() (the class:<device_type> linked-remotes form) — both accept
467 the exact same set of device-type spellings, so the lookup lives here once.
468 @raises ValueError if token is neither a known name nor a parseable integer.
469 @raises cv.Invalid if token is a renamed device-type name (naming the new one), or parses as an
470 integer but is out of range 0-1023.
472 if token
in RENAMED_DEVICE_TYPES:
473 raise cv.Invalid(RENAMED_DEVICE_TYPES[token])
478 """Validate io_device_type as a named string or integer 0-1023."""
479 if isinstance(value, int):
480 return cv.int_range(min=0, max=DEVICE_TYPE_MAX)(value)
482 if isinstance(value, str):
483 normalized = cv.string_strict(value).strip().lower()
486 except ValueError
as err:
488 "Device type must be a known name or an integer in the range 0..1023 (for example 0x11)"
492 "Device type must be a known name or an integer in the range 0..1023"
497 """Validate manufacturer as a named string (MANUFACTURER_OPTIONS) or integer 0-255.
499 Mirrors validate_device_type() exactly (same "name, else raw integer" shape via
500 _resolve_named_or_raw_token()) — a manufacturer ID is the same kind of small,
501 protocol-defined enum, it just has no linked_remotes-style second caller.
503 if isinstance(value, int):
504 return cv.int_range(min=0, max=0xFF)(value)
506 if isinstance(value, str):
507 normalized = cv.string_strict(value).strip().lower()
510 except ValueError
as err:
512 "manufacturer must be a known name or an integer in the range 0..255 (for example 0x02)"
515 raise cv.Invalid(
"manufacturer must be a known name or an integer in the range 0..255")
519 """Generate a C++ static_cast expression for a validated device type."""
520 return cg.RawExpression(
521 f
"static_cast<esphome::home_io_control::DeviceType>(0x{value:02X})"
526 """Generate a C++ static_cast expression for a validated DEVICE_MODEL_OPTIONS value."""
527 return cg.RawExpression(f
"static_cast<esphome::home_io_control::DeviceModel>({value})")
531 """Validate io_model as one of the DEVICE_MODEL_OPTIONS names (lowercase).
533 Whether the declared io_device_type / io_subtype offers the model is
534 platform_common.validate_device_model()'s job: it needs the whole entry.
536 normalized = cv.string_strict(value).strip().lower()
537 if normalized
not in DEVICE_MODEL_OPTIONS:
539 f
"Unknown io_model '{value}'; known models: {', '.join(sorted(DEVICE_MODEL_OPTIONS))}"
545 """Validate node_id as exactly 6 hex characters (3 bytes)."""
546 value = cv.string_strict(value).upper()
548 raise cv.Invalid(
"Node ID must be exactly 6 hex characters (3 bytes)")
552 raise cv.Invalid(
"Node ID must be valid hexadecimal")
557 """Validate system_key as exactly 32 hex characters (16 bytes)."""
558 value = cv.string_strict(value).upper()
560 raise cv.Invalid(
"System key must be exactly 32 hex characters (16 bytes)")
564 raise cv.Invalid(
"System key must be valid hexadecimal")
569 """Validate io_device_id as exactly 6 hex characters (3 bytes)."""
570 value = cv.string_strict(value).upper()
572 raise cv.Invalid(
"Device ID must be exactly 6 hex characters (3 bytes)")
576 raise cv.Invalid(
"Device ID must be valid hexadecimal")
581 """Propagate the parent entity's ESPHome sub-device (YAML `device_id:`) onto a hand-built
582 companion config dict, so the companion entity groups under the same HA device as its parent.
584 Lives here, a leaf module, so any platform module can import it without the whole
585 platform-schema module; platform_common.py re-exports it for cover.py.
587 Companion entities (diagnostic sensors, cover favorite/vent buttons, ...) are built from
588 dicts fed straight to e.g. new_text_sensor()/new_button() rather than through the platform's
589 own cv.Schema(), so they never go through ENTITY_BASE_SCHEMA and never pick up `device_id:`
590 on their own. esphome.core.entity_helpers.setup_entity() reads it with
591 `config.get(CONF_DEVICE_ID)`, a truthiness check, so an explicit `None` and an absent key
592 behave identically; omitted here rather than set to None just to keep the dict shape
593 identical to a companion with no sub-device at all.
595 Deliberately not called anywhere for the hub-level dynamic entities (1W identity buttons/
596 sensors, the arming switches, LR1121 firmware controls, tuning numbers/selects): none of their
597 parent configs carry a `device_id:` schema slot, since those entities aren't attached to a
598 single cover/light/switch/lock to inherit one from. `device_id:` grouping is scoped to the
599 four device-bound platforms; hub-level entities always live on ESPHome's main device.
601 if (esphome_device_id := parent_config.get(CONF_DEVICE_ID))
is not None:
602 companion_config[CONF_DEVICE_ID] = esphome_device_id
603 return companion_config
607 """Validate a linked_remotes entry: either a device ID or 'class:<device_type>'.
609 The class form matches how 1W remotes address a typed broadcast (e.g. "all awnings")
610 rather than a single node, so one entry can cover many same-type devices without
611 enumerating each one. Shares _resolve_device_type_token() with validate_device_type()
612 so a type without a named YAML alias yet (e.g. discovered via pairing) can still be
613 class-linked. Normalized to 'class:0x<HH>' (uppercase hex) so wire_device_binding() can
614 parse the type directly without a second DEVICE_TYPE_OPTIONS lookup; bare device IDs are
615 validated exactly as before and behave identically.
617 if isinstance(value, str)
and value.lower().startswith(
"class:"):
618 type_token = value.split(
":", 1)[1].strip().lower()
621 except ValueError
as err:
623 f
"Unknown device class '{type_token}' in linked_remotes; expected one of: "
624 +
", ".join(sorted(DEVICE_TYPE_OPTIONS))
625 +
", or a raw integer such as 0x14"
627 return f
"class:0x{type_value:02X}"
632 """Parse a time period and reject anything shorter than minimum_ms, naming the key."""
633 value = cv.positive_time_period_milliseconds(value)
634 if value.total_milliseconds < minimum_ms:
635 raise cv.Invalid(f
"{key} must be at least {minimum_ms}ms")
640 """Validate status_poll_interval is at least MIN_STATUS_POLL_INTERVAL_MS."""
642 value, MIN_STATUS_POLL_INTERVAL_MS,
"status_poll_interval"
647 """Validate rain_sensor_poll_interval is at least MIN_RAIN_SENSOR_POLL_INTERVAL_MS."""
649 value, MIN_RAIN_SENSOR_POLL_INTERVAL_MS,
"rain_sensor_poll_interval"
654 """Validate sun_level_poll_interval is at least MIN_SUN_LEVEL_POLL_INTERVAL_MS."""
656 value, MIN_SUN_LEVEL_POLL_INTERVAL_MS,
"sun_level_poll_interval"
661 """Validate battery_level_poll_interval is at least MIN_BATTERY_LEVEL_POLL_INTERVAL_MS."""
663 value, MIN_BATTERY_LEVEL_POLL_INTERVAL_MS,
"battery_level_poll_interval"
668 """Validate status_refresh_interval is at least MIN_STATUS_REFRESH_INTERVAL_MS."""
670 value, MIN_STATUS_REFRESH_INTERVAL_MS,
"status_refresh_interval"
_validate_min_interval(value, minimum_ms, key)
device_features(device_type, subtype=0, model=0)
validate_linked_remote_entry(value)
validate_battery_level_poll_interval(value)
validate_sun_level_poll_interval(value)
validate_system_key(value)
device_models(device_type, subtype=0)
validate_status_poll_interval(value)
device_model_expression(value)
validate_rain_sensor_poll_interval(value)
kind_is_experimental(device_type, subtype=0, model=0)
validate_device_type(value)
validate_radio_transport(config)
_resolve_named_or_raw_token(token, options, max_value=0xFF)
validate_manufacturer(value)
_resolve_device_type_token(token)
validate_device_id(value)
device_type_name(device_type)
device_type_expression(value)
validate_device_model_name(value)
validate_status_refresh_interval(value)
inherit_esphome_device(companion_config, parent_config)