Home Assistant Unofficial Reference 2024.12.1
climate.py
Go to the documentation of this file.
1 """Support for mill wifi-enabled home heaters."""
2 
3 from typing import Any
4 
5 import mill
6 from mill_local import OperationMode
7 import voluptuous as vol
8 
10  ClimateEntity,
11  ClimateEntityFeature,
12  HVACAction,
13  HVACMode,
14 )
15 from homeassistant.config_entries import ConfigEntry
16 from homeassistant.const import (
17  ATTR_TEMPERATURE,
18  CONF_IP_ADDRESS,
19  CONF_USERNAME,
20  PRECISION_TENTHS,
21  UnitOfTemperature,
22 )
23 from homeassistant.core import HomeAssistant, ServiceCall, callback
24 from homeassistant.helpers import config_validation as cv
25 from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC, DeviceInfo
26 from homeassistant.helpers.entity_platform import AddEntitiesCallback
27 from homeassistant.helpers.update_coordinator import CoordinatorEntity
28 
29 from .const import (
30  ATTR_AWAY_TEMP,
31  ATTR_COMFORT_TEMP,
32  ATTR_ROOM_NAME,
33  ATTR_SLEEP_TEMP,
34  CLOUD,
35  CONNECTION_TYPE,
36  DOMAIN,
37  LOCAL,
38  MANUFACTURER,
39  MAX_TEMP,
40  MIN_TEMP,
41  SERVICE_SET_ROOM_TEMP,
42 )
43 from .coordinator import MillDataUpdateCoordinator
44 
45 SET_ROOM_TEMP_SCHEMA = vol.Schema(
46  {
47  vol.Required(ATTR_ROOM_NAME): cv.string,
48  vol.Optional(ATTR_AWAY_TEMP): cv.positive_int,
49  vol.Optional(ATTR_COMFORT_TEMP): cv.positive_int,
50  vol.Optional(ATTR_SLEEP_TEMP): cv.positive_int,
51  }
52 )
53 
54 
56  hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback
57 ) -> None:
58  """Set up the Mill climate."""
59  if entry.data.get(CONNECTION_TYPE) == LOCAL:
60  mill_data_coordinator = hass.data[DOMAIN][LOCAL][entry.data[CONF_IP_ADDRESS]]
61  async_add_entities([LocalMillHeater(mill_data_coordinator)])
62  return
63 
64  mill_data_coordinator = hass.data[DOMAIN][CLOUD][entry.data[CONF_USERNAME]]
65 
66  entities = [
67  MillHeater(mill_data_coordinator, mill_device)
68  for mill_device in mill_data_coordinator.data.values()
69  if isinstance(mill_device, mill.Heater)
70  ]
71  async_add_entities(entities)
72 
73  async def set_room_temp(service: ServiceCall) -> None:
74  """Set room temp."""
75  room_name = service.data.get(ATTR_ROOM_NAME)
76  sleep_temp = service.data.get(ATTR_SLEEP_TEMP)
77  comfort_temp = service.data.get(ATTR_COMFORT_TEMP)
78  away_temp = service.data.get(ATTR_AWAY_TEMP)
79  await mill_data_coordinator.mill_data_connection.set_room_temperatures_by_name(
80  room_name, sleep_temp, comfort_temp, away_temp
81  )
82 
83  hass.services.async_register(
84  DOMAIN, SERVICE_SET_ROOM_TEMP, set_room_temp, schema=SET_ROOM_TEMP_SCHEMA
85  )
86 
87 
88 class MillHeater(CoordinatorEntity[MillDataUpdateCoordinator], ClimateEntity):
89  """Representation of a Mill Thermostat device."""
90 
91  _attr_has_entity_name = True
92  _attr_hvac_modes = [HVACMode.HEAT, HVACMode.OFF]
93  _attr_max_temp = MAX_TEMP
94  _attr_min_temp = MIN_TEMP
95  _attr_name = None
96  _attr_supported_features = (
97  ClimateEntityFeature.TARGET_TEMPERATURE
98  | ClimateEntityFeature.TURN_OFF
99  | ClimateEntityFeature.TURN_ON
100  )
101  _attr_target_temperature_step = PRECISION_TENTHS
102  _attr_temperature_unit = UnitOfTemperature.CELSIUS
103  _enable_turn_on_off_backwards_compatibility = False
104 
105  def __init__(
106  self, coordinator: MillDataUpdateCoordinator, heater: mill.Heater
107  ) -> None:
108  """Initialize the thermostat."""
109 
110  super().__init__(coordinator)
111 
112  self._available_available = False
113 
114  self._id_id = heater.device_id
115  self._attr_unique_id_attr_unique_id = heater.device_id
116  self._attr_device_info_attr_device_info = DeviceInfo(
117  identifiers={(DOMAIN, heater.device_id)},
118  manufacturer=MANUFACTURER,
119  model=heater.model,
120  name=heater.name,
121  )
122 
123  self._update_attr_update_attr(heater)
124 
125  async def async_set_temperature(self, **kwargs: Any) -> None:
126  """Set new target temperature."""
127  if (temperature := kwargs.get(ATTR_TEMPERATURE)) is None:
128  return
129  await self.coordinator.mill_data_connection.set_heater_temp(
130  self._id_id, float(temperature)
131  )
132  await self.coordinator.async_request_refresh()
133 
134  async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None:
135  """Set new target hvac mode."""
136  if hvac_mode == HVACMode.HEAT:
137  await self.coordinator.mill_data_connection.heater_control(
138  self._id_id, power_status=True
139  )
140  await self.coordinator.async_request_refresh()
141  elif hvac_mode == HVACMode.OFF:
142  await self.coordinator.mill_data_connection.heater_control(
143  self._id_id, power_status=False
144  )
145  await self.coordinator.async_request_refresh()
146 
147  @property
148  def available(self) -> bool:
149  """Return True if entity is available."""
150  return super().available and self._available_available
151 
152  @callback
153  def _handle_coordinator_update(self) -> None:
154  """Handle updated data from the coordinator."""
155  self._update_attr_update_attr(self.coordinator.data[self._id_id])
156  self.async_write_ha_stateasync_write_ha_state()
157 
158  @callback
159  def _update_attr(self, heater):
160  self._available_available = heater.available
161  self._attr_extra_state_attributes_attr_extra_state_attributes = {
162  "open_window": heater.open_window,
163  "controlled_by_tibber": heater.tibber_control,
164  }
165  if heater.room_name:
166  self._attr_extra_state_attributes_attr_extra_state_attributes["room"] = heater.room_name
167  self._attr_extra_state_attributes_attr_extra_state_attributes["avg_room_temp"] = heater.room_avg_temp
168  else:
169  self._attr_extra_state_attributes_attr_extra_state_attributes["room"] = "Independent device"
170  self._attr_target_temperature_attr_target_temperature = heater.set_temp
171  self._attr_current_temperature_attr_current_temperature = heater.current_temp
172  if heater.is_heating:
173  self._attr_hvac_action_attr_hvac_action = HVACAction.HEATING
174  else:
175  self._attr_hvac_action_attr_hvac_action = HVACAction.IDLE
176  if heater.power_status:
177  self._attr_hvac_mode_attr_hvac_mode = HVACMode.HEAT
178  else:
179  self._attr_hvac_mode_attr_hvac_mode = HVACMode.OFF
180 
181 
182 class LocalMillHeater(CoordinatorEntity[MillDataUpdateCoordinator], ClimateEntity):
183  """Representation of a Mill Thermostat device."""
184 
185  _attr_has_entity_name = True
186  _attr_hvac_modes = [HVACMode.HEAT, HVACMode.OFF]
187  _attr_max_temp = MAX_TEMP
188  _attr_min_temp = MIN_TEMP
189  _attr_name = None
190  _attr_supported_features = (
191  ClimateEntityFeature.TARGET_TEMPERATURE
192  | ClimateEntityFeature.TURN_OFF
193  | ClimateEntityFeature.TURN_ON
194  )
195  _attr_target_temperature_step = PRECISION_TENTHS
196  _attr_temperature_unit = UnitOfTemperature.CELSIUS
197  _enable_turn_on_off_backwards_compatibility = False
198 
199  def __init__(self, coordinator: MillDataUpdateCoordinator) -> None:
200  """Initialize the thermostat."""
201  super().__init__(coordinator)
202  if mac := coordinator.mill_data_connection.mac_address:
203  self._attr_unique_id_attr_unique_id = mac
204  self._attr_device_info_attr_device_info = DeviceInfo(
205  connections={(CONNECTION_NETWORK_MAC, mac)},
206  configuration_url=self.coordinator.mill_data_connection.url,
207  manufacturer=MANUFACTURER,
208  model="Generation 3",
209  name=coordinator.mill_data_connection.name,
210  sw_version=coordinator.mill_data_connection.version,
211  )
212 
213  self._update_attr_update_attr()
214 
215  async def async_set_temperature(self, **kwargs: Any) -> None:
216  """Set new target temperature."""
217  if (temperature := kwargs.get(ATTR_TEMPERATURE)) is None:
218  return
219  await self.coordinator.mill_data_connection.set_target_temperature(
220  float(temperature)
221  )
222  await self.coordinator.async_request_refresh()
223 
224  async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None:
225  """Set new target hvac mode."""
226  if hvac_mode == HVACMode.HEAT:
227  await self.coordinator.mill_data_connection.set_operation_mode_control_individually()
228  await self.coordinator.async_request_refresh()
229  elif hvac_mode == HVACMode.OFF:
230  await self.coordinator.mill_data_connection.set_operation_mode_off()
231  await self.coordinator.async_request_refresh()
232 
233  @callback
234  def _handle_coordinator_update(self) -> None:
235  """Handle updated data from the coordinator."""
236  self._update_attr_update_attr()
237  self.async_write_ha_stateasync_write_ha_state()
238 
239  @callback
240  def _update_attr(self) -> None:
241  data = self.coordinator.data
242  self._attr_target_temperature_attr_target_temperature = data["set_temperature"]
243  self._attr_current_temperature_attr_current_temperature = data["ambient_temperature"]
244 
245  if data["operation_mode"] == OperationMode.OFF.value:
246  self._attr_hvac_mode_attr_hvac_mode = HVACMode.OFF
247  self._attr_hvac_action_attr_hvac_action = HVACAction.OFF
248  else:
249  self._attr_hvac_mode_attr_hvac_mode = HVACMode.HEAT
250  if data["current_power"] > 0:
251  self._attr_hvac_action_attr_hvac_action = HVACAction.HEATING
252  else:
253  self._attr_hvac_action_attr_hvac_action = HVACAction.IDLE
None __init__(self, MillDataUpdateCoordinator coordinator)
Definition: climate.py:199
None async_set_hvac_mode(self, HVACMode hvac_mode)
Definition: climate.py:224
None async_set_temperature(self, **Any kwargs)
Definition: climate.py:125
None __init__(self, MillDataUpdateCoordinator coordinator, mill.Heater heater)
Definition: climate.py:107
None async_set_hvac_mode(self, HVACMode hvac_mode)
Definition: climate.py:134
None async_setup_entry(HomeAssistant hass, ConfigEntry entry, AddEntitiesCallback async_add_entities)
Definition: climate.py:57