17import esphome.codegen
as cg
18import esphome.config_validation
as cv
19from esphome.const
import CONF_CS_PIN, CONF_DEVICE_ID
21from .hub_names
import (
28 MIN_RAIN_SENSOR_POLL_INTERVAL_MS,
29 MIN_STATUS_POLL_INTERVAL_MS,
33_LOGGER = logging.getLogger(__name__)
50SPI_RADIO_TYPES = frozenset({
"sx1276",
"sx1262",
"lr1121"})
57TCXO_VOLTAGE_OPTIONS = {
77FemProfile = home_io_control_ns.enum(
"FemProfile", is_class=
True)
79 "none": FemProfile.NONE,
80 "gc1109": FemProfile.GC1109,
81 "kct8103l": FemProfile.KCT8103L,
82 "xy16p35": FemProfile.XY16P35,
94 "gc1109": (CONF_VFEM_PIN, CONF_FEM_EN_PIN, CONF_FEM_PA_PIN),
95 "kct8103l": (CONF_VFEM_PIN, CONF_FEM_EN_PIN, CONF_FEM_PA_PIN),
96 "xy16p35": (CONF_VFEM_PIN, CONF_FEM_PA_PIN),
106FEM_TX_POWER_MAX_QUIET = {
114 """Require the SPI device keys for a radio on the SPI bus. The schema takes them as optional
115 (``spi.spi_device_schema(False, ...)``) so that the requirement follows ``radio_type`` here,
116 in one place, rather than being hard-wired into the schema for every radio."""
117 radio_type = config[CONF_RADIO_TYPE]
118 if radio_type
in SPI_RADIO_TYPES
and CONF_CS_PIN
not in config:
120 f
"radio_type: {radio_type} is an SPI radio and needs cs_pin: (its chip-select GPIO); "
121 "a board package in config/boards/ sets it for you",
128 """Cross-key validation for `fem:` (ADR 0035): which front-end part's control behaviour the
129 SX1262 driver applies to fem_pa_pin (and, for the two parts that have one, fem_en_pin), never
130 which GPIO numbers to use for them -- those are always the board package's job, the same as
131 every other radio pin. Requires radio_type: sx1262 (the raw FEM pins are silently ignored on
132 every other chip -- select_and_construct_radio_() only ever hands them to RadioSX1262 -- so a
133 mismatched radio_type would configure hardware the driver never drives), and that every pin
134 this profile's behaviour needs is actually present, naming exactly which is missing rather
135 than leaving the driver to silently no-op on a null pin. Also warns when tx_power looks likely
136 to push this FEM's antenna-port power over a typical 868 MHz SRD limit -- radio_sx1262.cpp's
137 own init() logs the actual per-part estimate this warning can only gesture at without knowing
138 the front-end part's exact gain table here too.
140 profile = config[CONF_FEM]
141 if profile ==
"none":
144 if config[CONF_RADIO_TYPE] !=
"sx1262":
146 f
"fem: {profile} requires radio_type: sx1262 -- the front-end module pins are only "
147 "wired to the SX1262 driver"
150 missing = [key
for key
in FEM_REQUIRED_PINS[profile]
if key
not in config]
153 f
"fem: {profile} requires {', '.join(missing)} to be set -- see "
154 "docs/hardware.md#front-end-module-fem-support for what your board needs"
157 if config[CONF_TX_POWER] > FEM_TX_POWER_MAX_QUIET[profile]:
159 "fem: %s together with tx_power: %d -- a front-end module turns tx_power into a "
160 "much larger antenna-port power than the bare SX1262 would radiate, and at this "
161 "setting the estimate is likely over a typical 868 MHz SRD ERP limit (+14 dBm). The "
162 "driver logs its own per-profile estimate (and its uncertainty) at boot -- check it, "
163 "and prefer tx_power: %d or lower (what this profile's own board package ships) "
164 "until you have measured the actual radiated power for your board",
166 config[CONF_TX_POWER],
167 FEM_TX_POWER_MAX_QUIET[profile],
176DEVICE_TYPE_MAX = 0x3FF
178DEVICE_TYPE_OPTIONS = {
180 "interior_venetian_blind": 0x01,
181 "roller_shutter": 0x02,
182 "vertical_exterior_awning": 0x03,
183 "window_opener": 0x04,
184 "garage_opener": 0x05,
187 "rolling_door_opener": 0x08,
189 "vertical_interior_blind": 0x0A,
190 "dual_roller_shutter": 0x0D,
191 "heating_temperature_interface": 0x0E,
192 "on_off_switch": 0x0F,
193 "horizontal_awning": 0x10,
194 "exterior_venetian_blind": 0x11,
195 "louvre_blind": 0x12,
196 "curtain_track": 0x13,
197 "exterior_heating": 0x15,
199 "intrusion_alarm": 0x17,
200 "swinging_shutter": 0x18,
201 "sliding_window": 0x1B,
202 "bioclimatic_pergola": 0x1D,
203 "electrical_heater": 0x34,
210HEATING_WINDOW_PROFILES = {
211 "electrical_heater": 1,
216 """True when the heating profile of (io_device_type value, io_subtype) has the window companions."""
218 DEVICE_TYPE_OPTIONS[name] == device_type
and profile_subtype == subtype
219 for name, profile_subtype
in HEATING_WINDOW_PROFILES.items()
225RENAMED_DEVICE_TYPES = {
227 "device type 'screen' no longer exists: type 11 is a secure configuration device, not an "
228 "actuator. Use the type Scan Paired Devices reports for your device, usually "
229 "'vertical_exterior_awning' or 'vertical_interior_blind'"
231 "blind":
"device type 'blind' was renamed: use 'vertical_interior_blind' instead",
232 "venetian_blind":
"device type 'venetian_blind' was renamed: use 'interior_venetian_blind' instead",
233 "awning":
"device type 'awning' was renamed: use 'vertical_exterior_awning' instead",
234 "dual_shutter":
"device type 'dual_shutter' was renamed: use 'dual_roller_shutter' instead",
235 "external_venetian_blind": (
236 "device type 'external_venetian_blind' was renamed: use 'exterior_venetian_blind' instead"
246MANUFACTURER_OPTIONS = {
253 "window_master": 0x07,
258 "atlantic_group": 0x0C,
264ONEWAY_WIRE_PROFILE_MANUFACTURERS = {
265 MANUFACTURER_OPTIONS[
"somfy"],
266 MANUFACTURER_OPTIONS[
"velux"],
271 """Resolve a lowercase, stripped token (a name from `options`, or a raw int/hex string) to
272 an integer 0-`max_value`.
274 Shared "named value, else raw integer" acceptance rule: validate_device_type()/
275 validate_linked_remote_entry() (DEVICE_TYPE_OPTIONS) and validate_manufacturer()
276 (MANUFACTURER_OPTIONS) are the same shape of small, protocol-defined enum with an escape
277 hatch for values this project hasn't named yet, so the lookup lives here once.
278 @raises ValueError if token is neither a known name nor a parseable integer.
279 @raises cv.Invalid if token parses as an integer but is out of range.
282 return options[token]
283 return cv.int_range(min=0, max=max_value)(int(token, 0))
287 """Resolve a lowercase, stripped device-type token (name or raw int/hex string) to 0-1023.
289 Single source of truth for the "named value from DEVICE_TYPE_OPTIONS, else raw integer"
290 acceptance rule shared by validate_device_type() (io_device_type) and
291 validate_linked_remote_entry() (the class:<device_type> linked-remotes form) — both accept
292 the exact same set of device-type spellings, so the lookup lives here once.
293 @raises ValueError if token is neither a known name nor a parseable integer.
294 @raises cv.Invalid if token is a renamed device-type name (naming the new one), or parses as an
295 integer but is out of range 0-1023.
297 if token
in RENAMED_DEVICE_TYPES:
298 raise cv.Invalid(RENAMED_DEVICE_TYPES[token])
303 """Validate io_device_type as a named string or integer 0-1023."""
304 if isinstance(value, int):
305 return cv.int_range(min=0, max=DEVICE_TYPE_MAX)(value)
307 if isinstance(value, str):
308 normalized = cv.string_strict(value).strip().lower()
311 except ValueError
as err:
313 "Device type must be a known name or an integer in the range 0..1023 (for example 0x11)"
317 "Device type must be a known name or an integer in the range 0..1023"
322 """Validate manufacturer as a named string (MANUFACTURER_OPTIONS) or integer 0-255.
324 Mirrors validate_device_type() exactly (same "name, else raw integer" shape via
325 _resolve_named_or_raw_token()) — a manufacturer ID is the same kind of small,
326 protocol-defined enum, it just has no linked_remotes-style second caller.
328 if isinstance(value, int):
329 return cv.int_range(min=0, max=0xFF)(value)
331 if isinstance(value, str):
332 normalized = cv.string_strict(value).strip().lower()
335 except ValueError
as err:
337 "manufacturer must be a known name or an integer in the range 0..255 (for example 0x02)"
340 raise cv.Invalid(
"manufacturer must be a known name or an integer in the range 0..255")
344 """Generate a C++ static_cast expression for a validated device type."""
345 return cg.RawExpression(
346 f
"static_cast<esphome::home_io_control::DeviceType>(0x{value:02X})"
351 """Validate node_id as exactly 6 hex characters (3 bytes)."""
352 value = cv.string_strict(value).upper()
354 raise cv.Invalid(
"Node ID must be exactly 6 hex characters (3 bytes)")
358 raise cv.Invalid(
"Node ID must be valid hexadecimal")
363 """Validate system_key as exactly 32 hex characters (16 bytes)."""
364 value = cv.string_strict(value).upper()
366 raise cv.Invalid(
"System key must be exactly 32 hex characters (16 bytes)")
370 raise cv.Invalid(
"System key must be valid hexadecimal")
375 """Validate io_device_id as exactly 6 hex characters (3 bytes)."""
376 value = cv.string_strict(value).upper()
378 raise cv.Invalid(
"Device ID must be exactly 6 hex characters (3 bytes)")
382 raise cv.Invalid(
"Device ID must be valid hexadecimal")
387 """Propagate the parent entity's ESPHome sub-device (YAML `device_id:`) onto a hand-built
388 companion config dict, so the companion entity groups under the same HA device as its parent.
390 Lives here rather than in platform_common.py: it is needed by button.py's pairing-result
391 sensor, which is not a device-bound platform and would otherwise have to import the whole
392 platform-schema module for a four-line helper. platform_common.py re-exports it so cover.py's
393 existing import keeps working.
395 Companion entities (diagnostic sensors, cover favorite/vent buttons, ...) are built from
396 dicts fed straight to e.g. new_text_sensor()/new_button() rather than through the platform's
397 own cv.Schema(), so they never go through ENTITY_BASE_SCHEMA and never pick up `device_id:`
398 on their own. esphome.core.entity_helpers.setup_entity() reads it with
399 `config.get(CONF_DEVICE_ID)`, a truthiness check, so an explicit `None` and an absent key
400 behave identically; omitted here rather than set to None just to keep the dict shape
401 identical to a companion with no sub-device at all.
403 Deliberately not called anywhere for the hub-level dynamic entities (1W identity buttons/
404 sensors, the arming switches, LR1121 firmware controls, tuning numbers/selects): none of their
405 parent configs carry a `device_id:` schema slot, since those entities aren't attached to a
406 single cover/light/switch/lock to inherit one from. `device_id:` grouping is scoped to the
407 four device-bound platforms; hub-level entities always live on ESPHome's main device.
409 if (esphome_device_id := parent_config.get(CONF_DEVICE_ID))
is not None:
410 companion_config[CONF_DEVICE_ID] = esphome_device_id
411 return companion_config
415 """Validate a linked_remotes entry: either a device ID or 'class:<device_type>'.
417 The class form matches how 1W remotes address a typed broadcast (e.g. "all awnings")
418 rather than a single node, so one entry can cover many same-type devices without
419 enumerating each one. Shares _resolve_device_type_token() with validate_device_type()
420 so a type without a named YAML alias yet (e.g. discovered via pairing) can still be
421 class-linked. Normalized to 'class:0x<HH>' (uppercase hex) so wire_device_binding() can
422 parse the type directly without a second DEVICE_TYPE_OPTIONS lookup; bare device IDs are
423 validated exactly as before and behave identically.
425 if isinstance(value, str)
and value.lower().startswith(
"class:"):
426 type_token = value.split(
":", 1)[1].strip().lower()
429 except ValueError
as err:
431 f
"Unknown device class '{type_token}' in linked_remotes; expected one of: "
432 +
", ".join(sorted(DEVICE_TYPE_OPTIONS))
433 +
", or a raw integer such as 0x14"
435 return f
"class:0x{type_value:02X}"
440 """Parse a time period and reject anything shorter than minimum_ms, naming the key."""
441 value = cv.positive_time_period_milliseconds(value)
442 if value.total_milliseconds < minimum_ms:
443 raise cv.Invalid(f
"{key} must be at least {minimum_ms}ms")
448 """Validate status_poll_interval is at least MIN_STATUS_POLL_INTERVAL_MS."""
450 value, MIN_STATUS_POLL_INTERVAL_MS,
"status_poll_interval"
455 """Validate rain_sensor_poll_interval is at least MIN_RAIN_SENSOR_POLL_INTERVAL_MS."""
457 value, MIN_RAIN_SENSOR_POLL_INTERVAL_MS,
"rain_sensor_poll_interval"
_validate_min_interval(value, minimum_ms, key)
validate_linked_remote_entry(value)
heating_profile_has_window(device_type, subtype)
validate_system_key(value)
validate_status_poll_interval(value)
validate_rain_sensor_poll_interval(value)
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_expression(value)
inherit_esphome_device(companion_config, parent_config)