13import esphome.codegen
as cg
14import esphome.config_validation
as cv
15from esphome.components
import binary_sensor, button, cover, select, sensor, switch, text_sensor
16from esphome.const
import (
18 CONF_DISABLED_BY_DEFAULT,
22 DEVICE_CLASS_MOISTURE,
23 ENTITY_CATEGORY_CONFIG,
24 ENTITY_CATEGORY_DIAGNOSTIC,
25 STATE_CLASS_MEASUREMENT,
28from esphome.core
import ID
30from .
import home_io_control_ns
31from .hub_names
import CONF_INVERT_POSITION
32from .hub_validators
import (
33 DEFAULT_COVER_DEVICE_CLASS,
34 FEATURE_EXTENDED_POSITION_FRAME,
35 NAMED_POSITION_FEATURES,
38 validate_battery_level_poll_interval,
39 validate_rain_sensor_poll_interval,
40 validate_status_refresh_interval,
41 validate_sun_level_poll_interval,
43from .platform_common
import (
44 companion_entity_name,
46 create_companion_sensors,
48 inherit_esphome_device,
49 inject_companion_sensor_ids,
50 platform_schema_extension,
52 validate_device_model,
53 warn_if_experimental_kind,
57 CONF_HOME_IO_CONTROL_ID,
61DEPENDENCIES = [
"home_io_control"]
64CONF_SILENT_SWITCH_ID =
"_silent_switch_id"
65CONF_TRAVEL_SPEED =
"travel_speed"
66CONF_TRAVEL_SPEED_SELECT_ID =
"_travel_speed_select_id"
67CONF_OPTIMISTIC_STATE =
"optimistic_state"
68CONF_RAIN_SENSOR_POLL_INTERVAL =
"rain_sensor_poll_interval"
69CONF_RAIN_SENSOR_ID =
"_rain_sensor_id"
70CONF_SUN_LEVEL_POLL_INTERVAL =
"sun_level_poll_interval"
71CONF_SUN_LEVEL_SENSOR_ID =
"_sun_level_sensor_id"
72CONF_BATTERY_LEVEL_POLL_INTERVAL =
"battery_level_poll_interval"
73CONF_BATTERY_LEVEL_SENSOR_ID =
"_battery_level_sensor_id"
74CONF_STATUS_REFRESH_INTERVAL =
"status_refresh_interval"
75CONF_BATTERY =
"battery"
76CONF_BATTERY_LOW_SENSOR_ID =
"_battery_low_sensor_id"
77CONF_BATTERY_SENSOR_ID =
"_battery_sensor_id"
80CONF_FAVORITE_BUTTON_ID =
"_favorite_button_id"
81CONF_VENT_BUTTON_ID =
"_vent_button_id"
82CONF_PARTIAL_BUTTON_ID =
"_partial_button_id"
83CONF_PEDESTRIAN_BUTTON_ID =
"_pedestrian_button_id"
85IOHomeCover = home_io_control_ns.class_(
"IOHomeCover", cover.Cover, cg.Component)
86IOHomeCoverSilentSwitch = home_io_control_ns.class_(
87 "IOHomeCoverSilentSwitch", switch.Switch, cg.Component
89IOHomeCoverTravelSpeedSelect = home_io_control_ns.class_(
90 "IOHomeCoverTravelSpeedSelect", select.Select, cg.Component
92TravelSpeed = home_io_control_ns.enum(
"TravelSpeed", is_class=
True)
97 "default": TravelSpeed.MOTOR_DEFAULT,
98 "silent": TravelSpeed.SILENT,
99 "fast": TravelSpeed.FAST,
101TRAVEL_SPEED_EXCLUSIVE_MESSAGE = (
102 "silent: and travel_speed: set the same thing; keep one "
103 "(silent: true is travel_speed: silent, silent: false is travel_speed: fast)"
105IOHomeRainBinarySensor = home_io_control_ns.class_(
106 "IOHomeRainBinarySensor", binary_sensor.BinarySensor, cg.Component
108IOHomeSunLevelSensor = home_io_control_ns.class_(
109 "IOHomeSunLevelSensor", sensor.Sensor, cg.Component
111IOHomeBatteryLevelSensor = home_io_control_ns.class_(
112 "IOHomeBatteryLevelSensor", sensor.Sensor, cg.Component
114IOHomeBatteryLowBinarySensor = home_io_control_ns.class_(
115 "IOHomeBatteryLowBinarySensor", binary_sensor.BinarySensor, cg.Component
117IOHomeBatteryTextSensor = home_io_control_ns.class_(
118 "IOHomeBatteryTextSensor", text_sensor.TextSensor, cg.Component
123IOHomeCoverCommandButton = home_io_control_ns.class_(
124 "IOHomeCoverCommandButton", button.Button, cg.Component
126CoverCommand = home_io_control_ns.enum(
"CoverCommand", is_class=
True)
132NAMED_POSITION_BUTTONS = {
133 "FAVORITE": (CONF_FAVORITE_BUTTON_ID,
"favorite_button",
"Favorite Position",
False),
134 "VENT": (CONF_VENT_BUTTON_ID,
"vent_button",
"Ventilation Position",
False),
138 "PEDESTRIAN": (CONF_PEDESTRIAN_BUTTON_ID,
"pedestrian_button",
"Pedestrian Position",
True),
139 "PARTIAL": (CONF_PARTIAL_BUTTON_ID,
"partial_button",
"Partial Position",
True),
144 """The capability word of the declared io_device_type / io_subtype, or 0 when no type is declared."""
145 if CONF_DEVICE_TYPE
not in config:
147 return device_features(config[CONF_DEVICE_TYPE], config.get(CONF_SUBTYPE, 0))
151 """Give a cover that declares io_device_type and sets no `device_class:` the kind's Home
152 Assistant class; an explicit value, including "", is kept."""
153 if CONF_DEVICE_CLASS
not in config
and CONF_DEVICE_TYPE
in config:
154 default = DEFAULT_COVER_DEVICE_CLASS.get(device_type_name(config[CONF_DEVICE_TYPE]))
155 if default
is not None:
156 config[CONF_DEVICE_CLASS] = default
161 """Reject `silent:` or `travel_speed:` on a declared kind that takes no travel-speed parameter."""
162 for key
in (CONF_SILENT, CONF_TRAVEL_SPEED):
167 required=FEATURE_EXTENDED_POSITION_FRAME,
168 reason=f
"this device takes no travel-speed parameter; remove {key}:",
174 """Derive the silent-operation switch name from the parent cover name."""
175 return companion_entity_name(config,
"Silent Operation")
179 """Derive the travel speed select name from the parent cover name."""
180 return companion_entity_name(config,
"Travel Speed")
184 """Derive the rain binary sensor name from the parent cover name."""
185 return companion_entity_name(config,
"Rain sensor")
189 """Derive the sun level sensor name from the parent cover name."""
190 return companion_entity_name(config,
"Sun level")
194 """Derive the battery level (percent) sensor name from the parent cover name."""
195 return companion_entity_name(config,
"Battery level")
199 """Derive the battery-low binary sensor name from the parent cover name."""
200 return companion_entity_name(config,
"Battery Low")
204 """Derive the battery-level text sensor name from the parent cover name."""
205 return companion_entity_name(config,
"Battery")
209 """Declare cover companion entity IDs during schema validation for StaticVector sizing.
211 The named-position buttons are cover-only and gated on the declared kind's capability bits, so
212 their injection stays here. The always-present companion sensor IDs are delegated to the
213 shared helper. See platform_common.companion_id_base() for the StaticVector rationale.
215 base = companion_id_base(config, CONF_ID)
219 for key, (config_key, suffix, _name, _disabled)
in NAMED_POSITION_BUTTONS.items():
220 if features & NAMED_POSITION_FEATURES[key]:
221 config[config_key] = ID(
224 type=IOHomeCoverCommandButton,
230 if CONF_SILENT
in config:
231 config[CONF_SILENT_SWITCH_ID] = ID(
232 f
"{base}_silent_switch",
234 type=IOHomeCoverSilentSwitch,
239 if CONF_TRAVEL_SPEED
in config:
240 config[CONF_TRAVEL_SPEED_SELECT_ID] = ID(
241 f
"{base}_travel_speed_select",
243 type=IOHomeCoverTravelSpeedSelect,
248 if CONF_RAIN_SENSOR_POLL_INTERVAL
in config:
249 config[CONF_RAIN_SENSOR_ID] = ID(
250 f
"{base}_rain_sensor",
252 type=IOHomeRainBinarySensor,
257 if CONF_SUN_LEVEL_POLL_INTERVAL
in config:
258 config[CONF_SUN_LEVEL_SENSOR_ID] = ID(
259 f
"{base}_sun_level_sensor",
261 type=IOHomeSunLevelSensor,
263 if CONF_BATTERY_LEVEL_POLL_INTERVAL
in config:
264 config[CONF_BATTERY_LEVEL_SENSOR_ID] = ID(
265 f
"{base}_battery_level_sensor",
267 type=IOHomeBatteryLevelSensor,
272 if config.get(CONF_BATTERY,
False):
273 config[CONF_BATTERY_LOW_SENSOR_ID] = ID(
274 f
"{base}_battery_low_sensor",
276 type=IOHomeBatteryLowBinarySensor,
278 config[CONF_BATTERY_SENSOR_ID] = ID(
279 f
"{base}_battery_sensor",
281 type=IOHomeBatteryTextSensor,
285 return inject_companion_sensor_ids(config, CONF_ID)
289 """What a cover with `silent:` writes instead, and what changes in Home Assistant."""
290 speed =
"silent" if config[CONF_SILENT]
else "fast"
292 f
"Replace 'silent: {str(config[CONF_SILENT]).lower()}' with 'travel_speed: {speed}'. The "
293 f
"'{silent_switch_name(config)}' switch is then replaced by a '{travel_speed_select_name(config)}' "
294 "select (default, silent, fast): update any automation or dashboard that uses the switch."
298CONFIG_SCHEMA = cv.All(
299 cover.cover_schema(IOHomeCover)
300 .extend(platform_schema_extension())
301 .extend({cv.Optional(CONF_INVERT_POSITION): cv.boolean})
306 CONF_SILENT,
"travel_speed", msg=TRAVEL_SPEED_EXCLUSIVE_MESSAGE
309 CONF_TRAVEL_SPEED,
"travel_speed", msg=TRAVEL_SPEED_EXCLUSIVE_MESSAGE
310 ): cv.enum(TRAVEL_SPEEDS, lower=
True),
316 CONF_RAIN_SENSOR_POLL_INTERVAL
317 ): validate_rain_sensor_poll_interval
322 cv.Optional(CONF_SUN_LEVEL_POLL_INTERVAL): validate_sun_level_poll_interval,
324 CONF_BATTERY_LEVEL_POLL_INTERVAL
325 ): validate_battery_level_poll_interval,
326 cv.Optional(CONF_STATUS_REFRESH_INTERVAL): validate_status_refresh_interval,
329 .extend({cv.Optional(CONF_BATTERY): cv.boolean})
330 .extend({cv.Optional(CONF_OPTIMISTIC_STATE, default=
True): cv.boolean})
331 .extend(cv.COMPONENT_SCHEMA),
332 validate_device_model,
333 _default_device_class,
334 _require_travel_speed_kind,
335 deprecated_key(CONF_SILENT, _silent_replacement),
336 warn_if_experimental_kind,
337 _inject_companion_ids,
342 var = await cover.new_cover(config)
343 await cg.register_component(var, config)
345 parent = await cg.get_variable(config[CONF_HOME_IO_CONTROL_ID])
346 await wire_device_binding(var, parent, config)
348 if CONF_INVERT_POSITION
in config:
349 cg.add(var.set_invert_position(config[CONF_INVERT_POSITION]))
351 cg.add(var.set_optimistic_state(config[CONF_OPTIMISTIC_STATE]))
352 if CONF_SILENT
in config:
353 speed = TravelSpeed.SILENT
if config[CONF_SILENT]
else TravelSpeed.FAST
354 cg.add(var.set_travel_speed(speed))
355 if CONF_TRAVEL_SPEED
in config:
356 cg.add(var.set_travel_speed(config[CONF_TRAVEL_SPEED]))
358 if CONF_SILENT_SWITCH_ID
in config:
364 silent_config = switch.switch_schema(
365 IOHomeCoverSilentSwitch,
366 default_restore_mode=
"DISABLED",
367 entity_category=ENTITY_CATEGORY_CONFIG,
368 ).extend(cv.COMPONENT_SCHEMA)(
369 inherit_esphome_device(
371 CONF_ID: config[CONF_SILENT_SWITCH_ID],
377 silent_switch = await switch.new_switch(silent_config)
378 await cg.register_component(silent_switch, silent_config)
379 cg.add(silent_switch.set_parent(parent))
380 cg.add(silent_switch.set_device_id(config[CONF_IO_DEVICE_ID]))
382 if CONF_TRAVEL_SPEED_SELECT_ID
in config:
385 speed_config = select.select_schema(
386 IOHomeCoverTravelSpeedSelect,
387 entity_category=ENTITY_CATEGORY_CONFIG,
388 ).extend(cv.COMPONENT_SCHEMA)(
389 inherit_esphome_device(
391 CONF_ID: config[CONF_TRAVEL_SPEED_SELECT_ID],
397 speed_select = await select.new_select(
398 speed_config, options=list(TRAVEL_SPEEDS)
400 await cg.register_component(speed_select, speed_config)
401 cg.add(speed_select.set_parent(parent))
402 cg.add(speed_select.set_device_id(config[CONF_IO_DEVICE_ID]))
404 if CONF_RAIN_SENSOR_ID
in config:
406 var.set_rain_sensor_poll_interval(
407 config[CONF_RAIN_SENSOR_POLL_INTERVAL].total_milliseconds
410 rain_config = binary_sensor.binary_sensor_schema(
411 IOHomeRainBinarySensor, device_class=DEVICE_CLASS_MOISTURE
412 ).extend(cv.COMPONENT_SCHEMA)(
413 inherit_esphome_device(
415 CONF_ID: config[CONF_RAIN_SENSOR_ID],
421 rain_sensor = await binary_sensor.new_binary_sensor(rain_config)
422 await cg.register_component(rain_sensor, rain_config)
423 cg.add(rain_sensor.set_parent(parent))
424 cg.add(rain_sensor.set_device_id(config[CONF_IO_DEVICE_ID]))
426 if CONF_SUN_LEVEL_SENSOR_ID
in config:
428 var.set_sun_level_poll_interval(
429 config[CONF_SUN_LEVEL_POLL_INTERVAL].total_milliseconds
436 IOHomeSunLevelSensor,
437 CONF_SUN_LEVEL_SENSOR_ID,
439 icon=
"mdi:white-balance-sunny",
442 if CONF_BATTERY_LEVEL_SENSOR_ID
in config:
444 var.set_battery_level_poll_interval(
445 config[CONF_BATTERY_LEVEL_POLL_INTERVAL].total_milliseconds
451 IOHomeBatteryLevelSensor,
452 CONF_BATTERY_LEVEL_SENSOR_ID,
454 unit_of_measurement=UNIT_PERCENT,
455 device_class=DEVICE_CLASS_BATTERY,
458 if CONF_STATUS_REFRESH_INTERVAL
in config:
460 var.set_status_refresh_interval(
461 config[CONF_STATUS_REFRESH_INTERVAL].total_milliseconds
465 if CONF_BATTERY_LOW_SENSOR_ID
in config:
466 cg.add(var.set_battery_entities(
True))
469 for key, (config_key, _suffix, name, disabled)
in NAMED_POSITION_BUTTONS.items():
470 if config_key
not in config:
472 button_config = inherit_esphome_device(
474 CONF_ID: config[config_key],
475 CONF_NAME: companion_entity_name(config, name),
476 CONF_DISABLED_BY_DEFAULT: disabled,
480 position_button = await button.new_button(button_config)
481 await cg.register_component(position_button, button_config)
482 cg.add(position_button.set_parent(parent))
483 cg.add(position_button.set_device_id(config[CONF_IO_DEVICE_ID]))
484 cg.add(position_button.set_command(getattr(CoverCommand, key)))
486 await create_companion_sensors(config, parent)
490 """Create one polled reading sensor (sun level, battery level) bound to the cover's device."""
491 sensor_config = sensor.sensor_schema(
494 state_class=STATE_CLASS_MEASUREMENT,
496 ).extend(cv.COMPONENT_SCHEMA)(
497 inherit_esphome_device(
499 CONF_ID: config[id_key],
505 reading_sensor = await sensor.new_sensor(sensor_config)
506 await cg.register_component(reading_sensor, sensor_config)
507 cg.add(reading_sensor.set_parent(parent))
508 cg.add(reading_sensor.set_device_id(config[CONF_IO_DEVICE_ID]))
512 """Create the two `battery: true` companions.
514 Battery Low is the enabled one: a `device_class: battery` binary sensor is Home Assistant's own
515 low-battery signal. Battery (full/normal/low) is diagnostic and disabled by default; ESPHome
516 text sensors carry no battery device class, so nothing generic reads it.
518 low_config = binary_sensor.binary_sensor_schema(
519 IOHomeBatteryLowBinarySensor,
520 device_class=DEVICE_CLASS_BATTERY,
521 entity_category=ENTITY_CATEGORY_DIAGNOSTIC,
522 ).extend(cv.COMPONENT_SCHEMA)(
523 inherit_esphome_device(
525 CONF_ID: config[CONF_BATTERY_LOW_SENSOR_ID],
531 low_sensor = await binary_sensor.new_binary_sensor(low_config)
532 await cg.register_component(low_sensor, low_config)
533 cg.add(low_sensor.set_parent(parent))
534 cg.add(low_sensor.set_device_id(config[CONF_IO_DEVICE_ID]))
536 level_config = text_sensor.text_sensor_schema(
537 IOHomeBatteryTextSensor,
538 entity_category=ENTITY_CATEGORY_DIAGNOSTIC,
540 ).extend(cv.COMPONENT_SCHEMA)(
541 inherit_esphome_device(
543 CONF_ID: config[CONF_BATTERY_SENSOR_ID],
545 CONF_DISABLED_BY_DEFAULT:
True,
550 level_sensor = await text_sensor.new_text_sensor(level_config)
551 await cg.register_component(level_sensor, level_config)
552 cg.add(level_sensor.set_parent(parent))
553 cg.add(level_sensor.set_device_id(config[CONF_IO_DEVICE_ID]))
battery_sensor_name(config)
battery_level_sensor_name(config)
_silent_replacement(config)
battery_low_sensor_name(config)
_create_battery_sensors(config, parent)
_default_device_class(config)
_create_reading_sensor(config, parent, cls, id_key, name, **schema_kwargs)
silent_switch_name(config)
_declared_features(config)
travel_speed_select_name(config)
_require_travel_speed_kind(config)
sun_level_sensor_name(config)
_inject_companion_ids(config)