Home IO Control
ESPHome add-on for IO-Homecontrol devices
Loading...
Searching...
No Matches
water_heater.py
Go to the documentation of this file.
1## @file
2## @brief ESPHome water_heater platform schema and code generation.
3## @ingroup hioc_codegen
4##
5## Binds an ESPHome water_heater entity to a domestic hot water heater (io_device_type: domestic_hot_water),
6## with four opt-in companions: energy (kWh, for the energy dashboard), heat pump running, electric backup
7## running, and a boost switch. Boost and away travel in the heater's operating-mode word and are
8## experimental: not confirmed on hardware yet. The heater is polled (default every five minutes);
9## `status_poll_interval` sets the cadence, with the same one-minute floor as climate. Shared device-binding
10## logic lives in platform_common.py; only the water heater's entity and companions are built here.
11##
12## The kind is required: the codegen checks it carries the WATER_HEATER capability, so a config can never
13## send the water heater's register frames to another device.
14
15import functools
16
17import esphome.codegen as cg
18import esphome.config_validation as cv
19from esphome.components import binary_sensor, sensor, switch, water_heater
20from esphome.const import (
21 CONF_DEVICE_ID,
22 CONF_ENERGY,
23 CONF_ID,
24 DEVICE_CLASS_ENERGY,
25 DEVICE_CLASS_RUNNING,
26 STATE_CLASS_TOTAL_INCREASING,
27 UNIT_KILOWATT_HOURS,
28)
29
30from . import home_io_control_ns
31from .hub_validators import FEATURE_WATER_HEATER
32from .platform_common import (
33 create_companion_sensors,
34 inherit_esphome_device,
35 inject_companion_sensor_ids,
36 platform_schema_extension,
37 require_feature,
38 validate_device_model,
39 validate_register_poll_interval,
40 warn_if_experimental_kind,
41 wire_device_binding,
42 CONF_DEVICE_TYPE,
43 CONF_HOME_IO_CONTROL_ID,
44 CONF_IO_DEVICE_ID,
45)
46
47DEPENDENCIES = ["home_io_control"]
48
49IOHomeWaterHeater = home_io_control_ns.class_("IOHomeWaterHeater", water_heater.WaterHeater, cg.Component)
50IOHomeDhwEnergySensor = home_io_control_ns.class_("IOHomeDhwEnergySensor", sensor.Sensor, cg.Component)
51IOHomeDhwBinarySensor = home_io_control_ns.class_(
52 "IOHomeDhwBinarySensor", binary_sensor.BinarySensor, cg.Component
53)
54IOHomeDhwBoostSwitch = home_io_control_ns.class_("IOHomeDhwBoostSwitch", switch.Switch, cg.Component)
55DhwReading = IOHomeDhwBinarySensor.enum("Reading", is_class=True)
56
57CONF_HEAT_PUMP_RUNNING = "heat_pump_running"
58CONF_ELECTRIC_BACKUP_RUNNING = "electric_backup_running"
59CONF_BOOST = "boost"
60
61# (YAML key, IOHomeDhwBinarySensor::Reading) of each opt-in binary companion.
62_BINARY_COMPANIONS = (
63 (CONF_HEAT_PUMP_RUNNING, DhwReading.HEAT_PUMP),
64 (CONF_ELECTRIC_BACKUP_RUNNING, DhwReading.ELECTRIC_BACKUP),
65)
66
67
69 """Require io_device_type, and a kind that is read and commanded as a water heater.
70
71 The water heater's frames (family-0x00 register reads, the mode write, a target EXECUTE) go only to a
72 device whose kind carries the WATER_HEATER capability, so the kind must be known at codegen time.
73 """
74 if CONF_DEVICE_TYPE not in config:
75 raise cv.Invalid(
76 "io_device_type is required on the water_heater platform: set io_device_type: domestic_hot_water",
77 path=[CONF_DEVICE_TYPE],
78 )
79 require_feature(
80 config,
81 "the water_heater platform",
82 True,
83 required=FEATURE_WATER_HEATER,
84 reason="it drives a domestic hot water heater (io_device_type: domestic_hot_water)",
85 )
86 return config
87
88
90 """Declare the auto-generated companion IDs during validation (ESPHome 2026.x StaticVector sizing).
91
92 The opt-in companions (sensors and the boost switch) are schemas of their own, whose IDs are declared by
93 their GenerateID.
94 """
95 return inject_companion_sensor_ids(config, CONF_ID)
96
97
98CONFIG_SCHEMA = cv.All(
99 water_heater.water_heater_schema(IOHomeWaterHeater)
100 .extend(platform_schema_extension())
101 .extend(
102 {
103 cv.Optional(CONF_ENERGY): sensor.sensor_schema(
104 IOHomeDhwEnergySensor,
105 unit_of_measurement=UNIT_KILOWATT_HOURS,
106 accuracy_decimals=3,
107 device_class=DEVICE_CLASS_ENERGY,
108 state_class=STATE_CLASS_TOTAL_INCREASING,
109 ).extend(cv.COMPONENT_SCHEMA),
110 cv.Optional(CONF_HEAT_PUMP_RUNNING): binary_sensor.binary_sensor_schema(
111 IOHomeDhwBinarySensor, device_class=DEVICE_CLASS_RUNNING
112 ).extend(cv.COMPONENT_SCHEMA),
113 cv.Optional(CONF_ELECTRIC_BACKUP_RUNNING): binary_sensor.binary_sensor_schema(
114 IOHomeDhwBinarySensor, device_class=DEVICE_CLASS_RUNNING
115 ).extend(cv.COMPONENT_SCHEMA),
116 # Restore mode DISABLED: the switch shows what the heater reports and never writes at boot, so a
117 # reboot cannot end a running boost.
118 cv.Optional(CONF_BOOST): switch.switch_schema(
119 IOHomeDhwBoostSwitch, default_restore_mode="DISABLED"
120 ).extend(cv.COMPONENT_SCHEMA),
121 }
122 )
123 .extend(cv.COMPONENT_SCHEMA),
124 _validate_water_heater_kind,
125 functools.partial(validate_register_poll_interval, platform="water_heater"),
126 validate_device_model,
127 warn_if_experimental_kind,
128 _inject_companion_ids,
129)
130
131
132def _companion_config(companion, config):
133 """A companion's config, grouped under the entity's ESPHome sub-device unless it names its own."""
134 if CONF_DEVICE_ID not in companion:
135 inherit_esphome_device(companion, config)
136 return companion
137
138
139async def to_code(config):
140 var = cg.new_Pvariable(config[CONF_ID])
141 await cg.register_component(var, config)
142 await water_heater.register_water_heater(var, config)
143
144 parent = await cg.get_variable(config[CONF_HOME_IO_CONTROL_ID])
145 await wire_device_binding(var, parent, config)
146 await create_companion_sensors(config, parent)
147
148 if CONF_ENERGY in config:
149 energy_config = _companion_config(config[CONF_ENERGY], config)
150 energy = await sensor.new_sensor(energy_config)
151 await cg.register_component(energy, energy_config)
152 cg.add(energy.set_parent(parent))
153 cg.add(energy.set_device_id(config[CONF_IO_DEVICE_ID]))
154
155 for key, reading in _BINARY_COMPANIONS:
156 if key not in config:
157 continue
158 companion_config = _companion_config(config[key], config)
159 companion = await binary_sensor.new_binary_sensor(companion_config)
160 await cg.register_component(companion, companion_config)
161 cg.add(companion.set_parent(parent))
162 cg.add(companion.set_device_id(config[CONF_IO_DEVICE_ID]))
163 cg.add(companion.set_reading(reading))
164
165 if CONF_BOOST in config:
166 boost_config = _companion_config(config[CONF_BOOST], config)
167 boost = await switch.new_switch(boost_config)
168 await cg.register_component(boost, boost_config)
169 cg.add(boost.set_parent(parent))
170 cg.add(boost.set_device_id(config[CONF_IO_DEVICE_ID]))
_companion_config(companion, config)