Home Assistant Unofficial Reference 2024.12.1
water_heater.py
Go to the documentation of this file.
1 """Support for MQTT water heater devices."""
2 
3 from __future__ import annotations
4 
5 import logging
6 from typing import TYPE_CHECKING, Any
7 
8 import voluptuous as vol
9 
10 from homeassistant.components import water_heater
12  ATTR_OPERATION_MODE,
13  DEFAULT_MIN_TEMP,
14  STATE_ECO,
15  STATE_ELECTRIC,
16  STATE_GAS,
17  STATE_HEAT_PUMP,
18  STATE_HIGH_DEMAND,
19  STATE_PERFORMANCE,
20  WaterHeaterEntity,
21  WaterHeaterEntityFeature,
22 )
23 from homeassistant.config_entries import ConfigEntry
24 from homeassistant.const import (
25  CONF_NAME,
26  CONF_OPTIMISTIC,
27  CONF_PAYLOAD_OFF,
28  CONF_PAYLOAD_ON,
29  CONF_TEMPERATURE_UNIT,
30  CONF_VALUE_TEMPLATE,
31  PRECISION_HALVES,
32  PRECISION_TENTHS,
33  PRECISION_WHOLE,
34  STATE_OFF,
35  UnitOfTemperature,
36 )
37 from homeassistant.core import HomeAssistant, callback
39 from homeassistant.helpers.entity_platform import AddEntitiesCallback
40 from homeassistant.helpers.template import Template
41 from homeassistant.helpers.typing import ConfigType, VolSchemaType
42 from homeassistant.util.unit_conversion import TemperatureConverter
43 
44 from .climate import MqttTemperatureControlEntity
45 from .config import DEFAULT_RETAIN, MQTT_BASE_SCHEMA
46 from .const import (
47  CONF_CURRENT_TEMP_TEMPLATE,
48  CONF_CURRENT_TEMP_TOPIC,
49  CONF_MODE_COMMAND_TEMPLATE,
50  CONF_MODE_COMMAND_TOPIC,
51  CONF_MODE_LIST,
52  CONF_MODE_STATE_TEMPLATE,
53  CONF_MODE_STATE_TOPIC,
54  CONF_POWER_COMMAND_TEMPLATE,
55  CONF_POWER_COMMAND_TOPIC,
56  CONF_PRECISION,
57  CONF_RETAIN,
58  CONF_TEMP_COMMAND_TEMPLATE,
59  CONF_TEMP_COMMAND_TOPIC,
60  CONF_TEMP_INITIAL,
61  CONF_TEMP_MAX,
62  CONF_TEMP_MIN,
63  CONF_TEMP_STATE_TEMPLATE,
64  CONF_TEMP_STATE_TOPIC,
65  DEFAULT_OPTIMISTIC,
66  PAYLOAD_NONE,
67 )
68 from .entity import async_setup_entity_entry_helper
69 from .models import MqttCommandTemplate, MqttValueTemplate, ReceiveMessage
70 from .schemas import MQTT_ENTITY_COMMON_SCHEMA
71 from .util import valid_publish_topic, valid_subscribe_topic
72 
73 _LOGGER = logging.getLogger(__name__)
74 
75 PARALLEL_UPDATES = 0
76 
77 DEFAULT_NAME = "MQTT Water Heater"
78 
79 MQTT_WATER_HEATER_ATTRIBUTES_BLOCKED = frozenset(
80  {
81  water_heater.ATTR_CURRENT_TEMPERATURE,
82  water_heater.ATTR_MAX_TEMP,
83  water_heater.ATTR_MIN_TEMP,
84  water_heater.ATTR_TEMPERATURE,
85  water_heater.ATTR_OPERATION_LIST,
86  water_heater.ATTR_OPERATION_MODE,
87  }
88 )
89 
90 VALUE_TEMPLATE_KEYS = (
91  CONF_CURRENT_TEMP_TEMPLATE,
92  CONF_MODE_STATE_TEMPLATE,
93  CONF_TEMP_STATE_TEMPLATE,
94 )
95 
96 COMMAND_TEMPLATE_KEYS = {
97  CONF_MODE_COMMAND_TEMPLATE,
98  CONF_TEMP_COMMAND_TEMPLATE,
99  CONF_POWER_COMMAND_TEMPLATE,
100 }
101 
102 
103 TOPIC_KEYS = (
104  CONF_CURRENT_TEMP_TOPIC,
105  CONF_MODE_COMMAND_TOPIC,
106  CONF_MODE_STATE_TOPIC,
107  CONF_POWER_COMMAND_TOPIC,
108  CONF_TEMP_COMMAND_TOPIC,
109  CONF_TEMP_STATE_TOPIC,
110 )
111 
112 
113 _PLATFORM_SCHEMA_BASE = MQTT_BASE_SCHEMA.extend(
114  {
115  vol.Optional(CONF_CURRENT_TEMP_TEMPLATE): cv.template,
116  vol.Optional(CONF_CURRENT_TEMP_TOPIC): valid_subscribe_topic,
117  vol.Optional(CONF_MODE_COMMAND_TEMPLATE): cv.template,
118  vol.Optional(CONF_MODE_COMMAND_TOPIC): valid_publish_topic,
119  vol.Optional(
120  CONF_MODE_LIST,
121  default=[
122  STATE_ECO,
123  STATE_ELECTRIC,
124  STATE_GAS,
125  STATE_HEAT_PUMP,
126  STATE_HIGH_DEMAND,
127  STATE_PERFORMANCE,
128  STATE_OFF,
129  ],
130  ): cv.ensure_list,
131  vol.Optional(CONF_MODE_STATE_TEMPLATE): cv.template,
132  vol.Optional(CONF_MODE_STATE_TOPIC): valid_subscribe_topic,
133  vol.Optional(CONF_NAME): vol.Any(cv.string, None),
134  vol.Optional(CONF_OPTIMISTIC, default=DEFAULT_OPTIMISTIC): cv.boolean,
135  vol.Optional(CONF_PAYLOAD_ON, default="ON"): cv.string,
136  vol.Optional(CONF_PAYLOAD_OFF, default="OFF"): cv.string,
137  vol.Optional(CONF_POWER_COMMAND_TOPIC): valid_publish_topic,
138  vol.Optional(CONF_POWER_COMMAND_TEMPLATE): cv.template,
139  vol.Optional(CONF_PRECISION): vol.In(
140  [PRECISION_TENTHS, PRECISION_HALVES, PRECISION_WHOLE]
141  ),
142  vol.Optional(CONF_RETAIN, default=DEFAULT_RETAIN): cv.boolean,
143  vol.Optional(CONF_TEMP_INITIAL): cv.positive_int,
144  vol.Optional(CONF_TEMP_MIN): vol.Coerce(float),
145  vol.Optional(CONF_TEMP_MAX): vol.Coerce(float),
146  vol.Optional(CONF_TEMP_COMMAND_TEMPLATE): cv.template,
147  vol.Optional(CONF_TEMP_COMMAND_TOPIC): valid_publish_topic,
148  vol.Optional(CONF_TEMP_STATE_TEMPLATE): cv.template,
149  vol.Optional(CONF_TEMP_STATE_TOPIC): valid_subscribe_topic,
150  vol.Optional(CONF_TEMPERATURE_UNIT): cv.temperature_unit,
151  vol.Optional(CONF_VALUE_TEMPLATE): cv.template,
152  }
153 ).extend(MQTT_ENTITY_COMMON_SCHEMA.schema)
154 
155 PLATFORM_SCHEMA_MODERN = vol.All(
156  _PLATFORM_SCHEMA_BASE,
157 )
158 
159 _DISCOVERY_SCHEMA_BASE = _PLATFORM_SCHEMA_BASE.extend({}, extra=vol.REMOVE_EXTRA)
160 
161 DISCOVERY_SCHEMA = vol.All(
162  _DISCOVERY_SCHEMA_BASE,
163 )
164 
165 
167  hass: HomeAssistant,
168  config_entry: ConfigEntry,
169  async_add_entities: AddEntitiesCallback,
170 ) -> None:
171  """Set up MQTT water heater device through YAML and through MQTT discovery."""
173  hass,
174  config_entry,
175  MqttWaterHeater,
176  water_heater.DOMAIN,
177  async_add_entities,
178  DISCOVERY_SCHEMA,
179  PLATFORM_SCHEMA_MODERN,
180  )
181 
182 
184  """Representation of an MQTT water heater device."""
185 
186  _default_name = DEFAULT_NAME
187  _entity_id_format = water_heater.ENTITY_ID_FORMAT
188  _attributes_extra_blocked = MQTT_WATER_HEATER_ATTRIBUTES_BLOCKED
189  _attr_target_temperature_low: float | None = None
190  _attr_target_temperature_high: float | None = None
191 
192  @staticmethod
193  def config_schema() -> VolSchemaType:
194  """Return the config schema."""
195  return DISCOVERY_SCHEMA
196 
197  def _setup_from_config(self, config: ConfigType) -> None:
198  """(Re)Setup the entity."""
199  self._attr_operation_list_attr_operation_list = config[CONF_MODE_LIST]
200  self._attr_temperature_unit_attr_temperature_unit = config.get(
201  CONF_TEMPERATURE_UNIT, self.hasshasshass.config.units.temperature_unit
202  )
203  if (min_temp := config.get(CONF_TEMP_MIN)) is not None:
204  self._attr_min_temp_attr_min_temp = min_temp
205  if (max_temp := config.get(CONF_TEMP_MAX)) is not None:
206  self._attr_max_temp_attr_max_temp = max_temp
207  if (precision := config.get(CONF_PRECISION)) is not None:
208  self._attr_precision_attr_precision = precision
209 
210  self._topic_topic = {key: config.get(key) for key in TOPIC_KEYS}
211 
212  self._optimistic_optimistic = config[CONF_OPTIMISTIC]
213 
214  # Set init temp, if it is missing convert the default to the temperature units
215  init_temp: float = config.get(
216  CONF_TEMP_INITIAL,
217  TemperatureConverter.convert(
218  DEFAULT_MIN_TEMP,
219  UnitOfTemperature.FAHRENHEIT,
220  self.temperature_unittemperature_unit,
221  ),
222  )
223  if self._topic_topic[CONF_TEMP_STATE_TOPIC] is None or self._optimistic_optimistic:
224  self._attr_target_temperature_attr_target_temperature = init_temp
225  if self._topic_topic[CONF_MODE_STATE_TOPIC] is None or self._optimistic_optimistic:
226  self._attr_current_operation_attr_current_operation = STATE_OFF
227 
228  value_templates: dict[str, Template | None] = {
229  key: config.get(CONF_VALUE_TEMPLATE) for key in VALUE_TEMPLATE_KEYS
230  }
231  value_templates.update(
232  {key: config[key] for key in VALUE_TEMPLATE_KEYS & config.keys()}
233  )
234  self._value_templates_value_templates = {
235  key: MqttValueTemplate(
236  template, entity=self
237  ).async_render_with_possible_json_value
238  for key, template in value_templates.items()
239  }
240 
241  self._command_templates_command_templates = {
242  key: MqttCommandTemplate(config.get(key), entity=self).async_render
243  for key in COMMAND_TEMPLATE_KEYS
244  }
245 
246  support = WaterHeaterEntityFeature(0)
247  if (self._topic_topic[CONF_TEMP_STATE_TOPIC] is not None) or (
248  self._topic_topic[CONF_TEMP_COMMAND_TOPIC] is not None
249  ):
250  support |= WaterHeaterEntityFeature.TARGET_TEMPERATURE
251 
252  if (self._topic_topic[CONF_MODE_STATE_TOPIC] is not None) or (
253  self._topic_topic[CONF_MODE_COMMAND_TOPIC] is not None
254  ):
255  support |= WaterHeaterEntityFeature.OPERATION_MODE
256 
257  if self._topic_topic[CONF_POWER_COMMAND_TOPIC] is not None:
258  support |= WaterHeaterEntityFeature.ON_OFF
259 
260  self._attr_supported_features_attr_supported_features = support
261 
262  @callback
263  def _handle_current_mode_received(self, msg: ReceiveMessage) -> None:
264  """Handle receiving operation mode via MQTT."""
265 
266  payload = self.render_templaterender_template(msg, CONF_MODE_STATE_TEMPLATE)
267 
268  if not payload.strip(): # No output from template, ignore
269  _LOGGER.debug(
270  "Ignoring empty payload '%s' for current operation "
271  "after rendering for topic %s",
272  payload,
273  msg.topic,
274  )
275  return
276 
277  if payload == PAYLOAD_NONE:
278  self._attr_current_operation_attr_current_operation = None
279  elif payload not in self._config_config[CONF_MODE_LIST]:
280  _LOGGER.warning("Invalid %s mode: %s", CONF_MODE_LIST, payload)
281  else:
282  if TYPE_CHECKING:
283  assert isinstance(payload, str)
284  self._attr_current_operation_attr_current_operation = payload
285 
286  @callback
287  def _prepare_subscribe_topics(self) -> None:
288  """(Re)Subscribe to topics."""
289  # add subscriptions for WaterHeaterEntity
290  self.add_subscriptionadd_subscription(
291  CONF_MODE_STATE_TOPIC,
292  self._handle_current_mode_received_handle_current_mode_received,
293  {"_attr_current_operation"},
294  )
295  # add subscriptions for MqttTemperatureControlEntity
296  self.prepare_subscribe_topicsprepare_subscribe_topics()
297 
298  async def async_set_temperature(self, **kwargs: Any) -> None:
299  """Set new target temperature."""
300  operation_mode: str | None
301  if (operation_mode := kwargs.get(ATTR_OPERATION_MODE)) is not None:
302  await self.async_set_operation_modeasync_set_operation_modeasync_set_operation_mode(operation_mode)
303  await super().async_set_temperature(**kwargs)
304 
305  async def async_set_operation_mode(self, operation_mode: str) -> None:
306  """Set new operation mode."""
307  payload = self._command_templates_command_templates[CONF_MODE_COMMAND_TEMPLATE](operation_mode)
308  await self._publish_publish(CONF_MODE_COMMAND_TOPIC, payload)
309 
310  if self._optimistic_optimistic or self._topic_topic[CONF_MODE_STATE_TOPIC] is None:
311  self._attr_current_operation_attr_current_operation = operation_mode
312  self.async_write_ha_stateasync_write_ha_state()
313 
314  async def async_turn_on(self, **kwargs: Any) -> None:
315  """Turn the entity on."""
316  if CONF_POWER_COMMAND_TOPIC in self._config_config:
317  mqtt_payload = self._command_templates_command_templates[CONF_POWER_COMMAND_TEMPLATE](
318  self._config_config[CONF_PAYLOAD_ON]
319  )
320  await self._publish_publish(CONF_POWER_COMMAND_TOPIC, mqtt_payload)
321 
322  async def async_turn_off(self, **kwargs: Any) -> None:
323  """Turn the entity off."""
324  if CONF_POWER_COMMAND_TOPIC in self._config_config:
325  mqtt_payload = self._command_templates_command_templates[CONF_POWER_COMMAND_TEMPLATE](
326  self._config_config[CONF_PAYLOAD_OFF]
327  )
328  await self._publish_publish(CONF_POWER_COMMAND_TOPIC, mqtt_payload)
None _publish(self, str topic, PublishPayloadType payload)
Definition: climate.py:457
ReceivePayloadType render_template(self, ReceiveMessage msg, str template_name)
Definition: climate.py:386
bool add_subscription(self, str state_topic_config_key, Callable[[ReceiveMessage], None] msg_callback, set[str]|None tracked_attributes, bool disable_encoding=False)
Definition: entity.py:1484
None _handle_current_mode_received(self, ReceiveMessage msg)
None async_set_operation_mode(self, str operation_mode)
Definition: __init__.py:332
None async_setup_entity_entry_helper(HomeAssistant hass, ConfigEntry entry, type[MqttEntity]|None entity_class, str domain, AddEntitiesCallback async_add_entities, VolSchemaType discovery_schema, VolSchemaType platform_schema_modern, dict[str, type[MqttEntity]]|None schema_class_mapping=None)
Definition: entity.py:245
None async_setup_entry(HomeAssistant hass, ConfigEntry config_entry, AddEntitiesCallback async_add_entities)