Home IO Control
ESPHome add-on for IO-Homecontrol devices
Loading...
Searching...
No Matches
cover.py
Go to the documentation of this file.
1## @file
2## @brief ESPHome cover platform schema and code generation.
3## @ingroup hioc_codegen
4##
5## Bridges the YAML ``cover:`` platform declaration to the runtime IOHomeCover entity.
6## Shared device-binding logic lives in platform_common.py; cover-specific extras — the
7## ``invert_position`` option, the favorite/ventilation companion buttons, the opt-in
8## ``rain_sensor_poll_interval`` key with its rain binary sensor and the opt-in ``battery`` key with
9## its two battery sensors — stay here.
10
11import esphome.codegen as cg
12import esphome.config_validation as cv
13from esphome.components import binary_sensor, button, cover, switch, text_sensor
14from esphome.const import (
15 CONF_DISABLED_BY_DEFAULT,
16 CONF_ID,
17 CONF_NAME,
18 DEVICE_CLASS_BATTERY,
19 DEVICE_CLASS_MOISTURE,
20 ENTITY_CATEGORY_CONFIG,
21 ENTITY_CATEGORY_DIAGNOSTIC,
22)
23from esphome.core import ID
24
25from . import home_io_control_ns
26from .hub_validators import validate_rain_sensor_poll_interval
27from .platform_common import (
28 companion_entity_name,
29 companion_id_base,
30 create_companion_sensors,
31 inherit_esphome_device,
32 inject_companion_sensor_ids,
33 platform_schema_extension,
34 wire_device_binding,
35 CONF_IO_DEVICE_ID,
36 CONF_DEVICE_TYPE,
37 CONF_HOME_IO_CONTROL_ID,
38)
39
40DEPENDENCIES = ["home_io_control"]
41
42CONF_INVERT_POSITION = "invert_position"
43CONF_SILENT = "silent"
44CONF_SILENT_SWITCH_ID = "_silent_switch_id"
45CONF_OPTIMISTIC_STATE = "optimistic_state"
46CONF_RAIN_SENSOR_POLL_INTERVAL = "rain_sensor_poll_interval"
47CONF_RAIN_SENSOR_ID = "_rain_sensor_id"
48CONF_BATTERY = "battery"
49CONF_BATTERY_LOW_SENSOR_ID = "_battery_low_sensor_id"
50CONF_BATTERY_SENSOR_ID = "_battery_sensor_id"
51
52# Internal config keys for the cover-only companion button IDs (injected by post-validator).
53CONF_FAVORITE_BUTTON_ID = "_favorite_button_id"
54CONF_VENT_BUTTON_ID = "_vent_button_id"
55
56IOHomeCover = home_io_control_ns.class_("IOHomeCover", cover.Cover, cg.Component)
57IOHomeCoverSilentSwitch = home_io_control_ns.class_(
58 "IOHomeCoverSilentSwitch", switch.Switch, cg.Component
59)
60IOHomeRainBinarySensor = home_io_control_ns.class_(
61 "IOHomeRainBinarySensor", binary_sensor.BinarySensor, cg.Component
62)
63IOHomeBatteryLowBinarySensor = home_io_control_ns.class_(
64 "IOHomeBatteryLowBinarySensor", binary_sensor.BinarySensor, cg.Component
65)
66IOHomeBatteryTextSensor = home_io_control_ns.class_(
67 "IOHomeBatteryTextSensor", text_sensor.TextSensor, cg.Component
68)
69# One C++ class backs both cover command companions; codegen sets which command each press
70# sends via set_command() (mirrors OneWayButtonAction in hub_names.py). A device-bound `button:`
71# entry is deliberately never the source — see the IOHomeOneWayCommandButton comment there.
72IOHomeCoverCommandButton = home_io_control_ns.class_(
73 "IOHomeCoverCommandButton", button.Button, cg.Component
74)
75CoverCommand = home_io_control_ns.enum("CoverCommand", is_class=True)
76
77# Device types that support 0-100% position control (maps to DeviceCapabilityClass::COVER in C++).
78# Used to decide whether a favorite-position button companion should be generated.
79POSITION_CONTROL_DEVICE_TYPES = {
80 0x01, # interior_venetian_blind
81 0x02, # roller_shutter
82 0x03, # vertical_exterior_awning
83 0x04, # window_opener
84 0x05, # garage_opener
85 0x07, # gate_opener
86 0x08, # rolling_door_opener
87 0x0A, # vertical_interior_blind
88 0x0D, # dual_roller_shutter
89 0x10, # horizontal_awning
90 0x11, # exterior_venetian_blind
91 0x12, # louvre_blind
92 0x13, # curtain_track
93 0x18, # swinging_shutter
94 0x1B, # sliding_window
95 0x1D, # bioclimatic_pergola
96}
97
98
100 """Check if the given device type value supports 0-100% position control."""
101 return value in POSITION_CONTROL_DEVICE_TYPES
102
103
104# Device types that support the ventilation position command.
105# These are window-type actuators that can move to a predefined vent opening.
106VENT_DEVICE_TYPES = {
107 0x04, # window_opener
108 0x14, # ventilation_point
109}
110
111
113 """Check if the given device type value supports the ventilation command."""
114 return value in VENT_DEVICE_TYPES
115
116
118 """Derive the favorite-position button name from the parent cover name."""
119 return companion_entity_name(config, "Favorite Position")
120
121
123 """Derive the ventilation-position button name from the parent cover name."""
124 return companion_entity_name(config, "Ventilation Position")
125
126
128 """Derive the silent-operation switch name from the parent cover name."""
129 return companion_entity_name(config, "Silent Operation")
130
131
133 """Derive the rain binary sensor name from the parent cover name."""
134 return companion_entity_name(config, "Rain sensor")
135
136
138 """Derive the battery-low binary sensor name from the parent cover name."""
139 return companion_entity_name(config, "Battery Low")
140
141
143 """Derive the battery-level text sensor name from the parent cover name."""
144 return companion_entity_name(config, "Battery")
145
146
148 """Declare cover companion entity IDs during schema validation for StaticVector sizing.
149
150 The favorite and ventilation buttons are cover-only and gated on device capability, so
151 their injection stays here. The always-present companion sensor IDs are delegated to the
152 shared helper. See platform_common.companion_id_base() for the StaticVector rationale.
153 """
154 base = companion_id_base(config, CONF_ID)
155
156 # Favorite-position button — only for position-capable device types.
157 if CONF_DEVICE_TYPE in config and device_supports_position_control(
158 config[CONF_DEVICE_TYPE]
159 ):
160 config[CONF_FAVORITE_BUTTON_ID] = ID(
161 f"{base}_favorite_button",
162 is_declaration=True,
163 type=IOHomeCoverCommandButton,
164 )
165
166 # Ventilation-position button — only for window-type device types.
167 if CONF_DEVICE_TYPE in config and device_supports_vent(config[CONF_DEVICE_TYPE]):
168 config[CONF_VENT_BUTTON_ID] = ID(
169 f"{base}_vent_button",
170 is_declaration=True,
171 type=IOHomeCoverCommandButton,
172 )
173
174 # Silent-operation toggle — only when the cover declares `silent:` at all. Declaring the key
175 # is what opts a cover into runtime control of its travel profile; a config that never mentions
176 # it gains no entity, and the YAML value is simply the boot state.
177 if CONF_SILENT in config:
178 config[CONF_SILENT_SWITCH_ID] = ID(
179 f"{base}_silent_switch",
180 is_declaration=True,
181 type=IOHomeCoverSilentSwitch,
182 )
183
184 # Rain binary sensor — only when the cover declares `rain_sensor_poll_interval:`. The key is
185 # the opt-in to the periodic limitation read, so a config without it gains no entity.
186 if CONF_RAIN_SENSOR_POLL_INTERVAL in config:
187 config[CONF_RAIN_SENSOR_ID] = ID(
188 f"{base}_rain_sensor",
189 is_declaration=True,
190 type=IOHomeRainBinarySensor,
191 )
192
193 # Battery sensors — only with `battery: true`. The level is decoded from status replies the hub
194 # already receives, so the key adds entities but no radio traffic.
195 if config.get(CONF_BATTERY, False):
196 config[CONF_BATTERY_LOW_SENSOR_ID] = ID(
197 f"{base}_battery_low_sensor",
198 is_declaration=True,
199 type=IOHomeBatteryLowBinarySensor,
200 )
201 config[CONF_BATTERY_SENSOR_ID] = ID(
202 f"{base}_battery_sensor",
203 is_declaration=True,
204 type=IOHomeBatteryTextSensor,
205 )
206
207 # Companion diagnostic sensors — always generated (shared with other platforms).
208 return inject_companion_sensor_ids(config, CONF_ID)
209
210
211CONFIG_SCHEMA = cv.All(
212 cover.cover_schema(IOHomeCover)
213 .extend(platform_schema_extension())
214 .extend({cv.Optional(CONF_INVERT_POSITION): cv.boolean})
215 .extend({cv.Optional(CONF_SILENT): cv.boolean})
216 .extend(
217 {
218 cv.Optional(
219 CONF_RAIN_SENSOR_POLL_INTERVAL
220 ): validate_rain_sensor_poll_interval
221 }
222 )
223 .extend({cv.Optional(CONF_BATTERY): cv.boolean})
224 .extend({cv.Optional(CONF_OPTIMISTIC_STATE, default=True): cv.boolean})
225 .extend(cv.COMPONENT_SCHEMA),
226 _inject_companion_ids,
227)
228
229
230async def to_code(config):
231 var = await cover.new_cover(config)
232 await cg.register_component(var, config)
233
234 parent = await cg.get_variable(config[CONF_HOME_IO_CONTROL_ID])
235 await wire_device_binding(var, parent, config)
236
237 if CONF_INVERT_POSITION in config:
238 cg.add(var.set_invert_position(config[CONF_INVERT_POSITION]))
239
240 cg.add(var.set_optimistic_state(config[CONF_OPTIMISTIC_STATE]))
241 if CONF_SILENT in config:
242 cg.add(var.set_silent(config[CONF_SILENT]))
243
244 if CONF_SILENT_SWITCH_ID in config:
245 # Built through switch_schema()+COMPONENT_SCHEMA so it carries the entity/component
246 # defaults register_switch() requires, matching _create_accept_foreign_pairing_switch().
247 # Restore mode DISABLED on purpose: the YAML `silent:` value is the boot state, and
248 # setup() publishes it. Any restoring mode would either fight that or drive write_state()
249 # before the device is even registered.
250 silent_config = switch.switch_schema(
251 IOHomeCoverSilentSwitch,
252 default_restore_mode="DISABLED",
253 entity_category=ENTITY_CATEGORY_CONFIG,
254 ).extend(cv.COMPONENT_SCHEMA)(
255 inherit_esphome_device(
256 {
257 CONF_ID: config[CONF_SILENT_SWITCH_ID],
258 CONF_NAME: silent_switch_name(config),
259 },
260 config,
261 )
262 )
263 silent_switch = await switch.new_switch(silent_config)
264 await cg.register_component(silent_switch, silent_config)
265 cg.add(silent_switch.set_parent(parent))
266 cg.add(silent_switch.set_device_id(config[CONF_IO_DEVICE_ID]))
267
268 if CONF_RAIN_SENSOR_ID in config:
269 cg.add(
270 var.set_rain_sensor_poll_interval(
271 config[CONF_RAIN_SENSOR_POLL_INTERVAL].total_milliseconds
272 )
273 )
274 rain_config = binary_sensor.binary_sensor_schema(
275 IOHomeRainBinarySensor, device_class=DEVICE_CLASS_MOISTURE
276 ).extend(cv.COMPONENT_SCHEMA)(
277 inherit_esphome_device(
278 {
279 CONF_ID: config[CONF_RAIN_SENSOR_ID],
280 CONF_NAME: rain_sensor_name(config),
281 },
282 config,
283 )
284 )
285 rain_sensor = await binary_sensor.new_binary_sensor(rain_config)
286 await cg.register_component(rain_sensor, rain_config)
287 cg.add(rain_sensor.set_parent(parent))
288 cg.add(rain_sensor.set_device_id(config[CONF_IO_DEVICE_ID]))
289
290 if CONF_BATTERY_LOW_SENSOR_ID in config:
291 cg.add(var.set_battery_entities(True))
292 await _create_battery_sensors(config, parent)
293
294 if CONF_FAVORITE_BUTTON_ID in config:
295 favorite_config = inherit_esphome_device(
296 {
297 CONF_ID: config[CONF_FAVORITE_BUTTON_ID],
298 CONF_NAME: favorite_button_name(config),
299 CONF_DISABLED_BY_DEFAULT: False,
300 },
301 config,
302 )
303 favorite = await button.new_button(favorite_config)
304 await cg.register_component(favorite, favorite_config)
305 cg.add(favorite.set_parent(parent))
306 cg.add(favorite.set_device_id(config[CONF_IO_DEVICE_ID]))
307 cg.add(favorite.set_command(CoverCommand.FAVORITE))
308
309 if CONF_VENT_BUTTON_ID in config:
310 vent_config = inherit_esphome_device(
311 {
312 CONF_ID: config[CONF_VENT_BUTTON_ID],
313 CONF_NAME: vent_button_name(config),
314 CONF_DISABLED_BY_DEFAULT: False,
315 },
316 config,
317 )
318 vent = await button.new_button(vent_config)
319 await cg.register_component(vent, vent_config)
320 cg.add(vent.set_parent(parent))
321 cg.add(vent.set_device_id(config[CONF_IO_DEVICE_ID]))
322 cg.add(vent.set_command(CoverCommand.VENT))
323
324 await create_companion_sensors(config, parent)
325
326
327async def _create_battery_sensors(config, parent):
328 """Create the two `battery: true` companions.
329
330 Battery Low is the enabled one: a `device_class: battery` binary sensor is Home Assistant's own
331 low-battery signal. Battery (full/normal/low) is diagnostic and disabled by default; ESPHome
332 text sensors carry no battery device class, so nothing generic reads it.
333 """
334 low_config = binary_sensor.binary_sensor_schema(
335 IOHomeBatteryLowBinarySensor,
336 device_class=DEVICE_CLASS_BATTERY,
337 entity_category=ENTITY_CATEGORY_DIAGNOSTIC,
338 ).extend(cv.COMPONENT_SCHEMA)(
339 inherit_esphome_device(
340 {
341 CONF_ID: config[CONF_BATTERY_LOW_SENSOR_ID],
342 CONF_NAME: battery_low_sensor_name(config),
343 },
344 config,
345 )
346 )
347 low_sensor = await binary_sensor.new_binary_sensor(low_config)
348 await cg.register_component(low_sensor, low_config)
349 cg.add(low_sensor.set_parent(parent))
350 cg.add(low_sensor.set_device_id(config[CONF_IO_DEVICE_ID]))
351
352 level_config = text_sensor.text_sensor_schema(
353 IOHomeBatteryTextSensor,
354 entity_category=ENTITY_CATEGORY_DIAGNOSTIC,
355 icon="mdi:battery",
356 ).extend(cv.COMPONENT_SCHEMA)(
357 inherit_esphome_device(
358 {
359 CONF_ID: config[CONF_BATTERY_SENSOR_ID],
360 CONF_NAME: battery_sensor_name(config),
361 CONF_DISABLED_BY_DEFAULT: True,
362 },
363 config,
364 )
365 )
366 level_sensor = await text_sensor.new_text_sensor(level_config)
367 await cg.register_component(level_sensor, level_config)
368 cg.add(level_sensor.set_parent(parent))
369 cg.add(level_sensor.set_device_id(config[CONF_IO_DEVICE_ID]))
battery_sensor_name(config)
Definition cover.py:142
device_supports_position_control(value)
Definition cover.py:99
device_supports_vent(value)
Definition cover.py:112
rain_sensor_name(config)
Definition cover.py:132
vent_button_name(config)
Definition cover.py:122
battery_low_sensor_name(config)
Definition cover.py:137
_create_battery_sensors(config, parent)
Definition cover.py:327
silent_switch_name(config)
Definition cover.py:127
favorite_button_name(config)
Definition cover.py:117
_inject_companion_ids(config)
Definition cover.py:147