1 """Climate support for Shelly."""
3 from __future__
import annotations
5 from collections.abc
import Mapping
6 from dataclasses
import asdict, dataclass
7 from typing
import Any, cast
9 from aioshelly.block_device
import Block
10 from aioshelly.const
import RPC_GENERATIONS
11 from aioshelly.exceptions
import DeviceConnectionError, InvalidAuthError
14 DOMAIN
as CLIMATE_DOMAIN,
36 NOT_CALIBRATED_ISSUE_ID,
37 RPC_THERMOSTAT_SETTINGS,
38 SHTRV_01_TEMPERATURE_SETTINGS,
40 from .coordinator
import ShellyBlockCoordinator, ShellyConfigEntry, ShellyRpcCoordinator
41 from .entity
import ShellyRpcEntity
43 async_remove_shelly_entity,
46 is_rpc_thermostat_internal_actuator,
52 config_entry: ShellyConfigEntry,
53 async_add_entities: AddEntitiesCallback,
55 """Set up climate device."""
60 coordinator = config_entry.runtime_data.block
62 if coordinator.device.initialized:
66 hass, config_entry, async_add_entities, coordinator
72 async_add_entities: AddEntitiesCallback,
73 coordinator: ShellyBlockCoordinator,
75 """Set up online climate devices."""
77 device_block: Block |
None =
None
78 sensor_block: Block |
None =
None
80 assert coordinator.device.blocks
82 for block
in coordinator.device.blocks:
83 if block.type ==
"device":
85 if hasattr(block,
"targetTemp"):
88 if sensor_block
and device_block:
89 LOGGER.debug(
"Setup online climate device %s", coordinator.name)
98 config_entry: ShellyConfigEntry,
99 async_add_entities: AddEntitiesCallback,
100 coordinator: ShellyBlockCoordinator,
102 """Restore sleeping climate devices."""
104 ent_reg = er.async_get(hass)
105 entries = er.async_entries_for_config_entry(ent_reg, config_entry.entry_id)
107 for entry
in entries:
108 if entry.domain != CLIMATE_DOMAIN:
111 LOGGER.debug(
"Setup sleeping climate device %s", coordinator.name)
112 LOGGER.debug(
"Found entry %s [%s]", entry.original_name, entry.domain)
120 config_entry: ShellyConfigEntry,
121 async_add_entities: AddEntitiesCallback,
123 """Set up entities for RPC device."""
124 coordinator = config_entry.runtime_data.rpc
126 climate_key_ids =
get_rpc_key_ids(coordinator.device.status,
"thermostat")
129 for id_
in climate_key_ids:
130 climate_ids.append(id_)
139 unique_id = f
"{coordinator.mac}-switch:{id_}"
150 """Object to hold extra stored data."""
152 last_target_temp: float |
None =
None
155 """Return a dict representation of the text data."""
160 CoordinatorEntity[ShellyBlockCoordinator], RestoreEntity, ClimateEntity
162 """Representation of a Shelly climate device."""
164 _attr_hvac_modes = [HVACMode.OFF, HVACMode.HEAT]
165 _attr_max_temp = SHTRV_01_TEMPERATURE_SETTINGS[
"max"]
166 _attr_min_temp = SHTRV_01_TEMPERATURE_SETTINGS[
"min"]
167 _attr_supported_features = (
168 ClimateEntityFeature.TARGET_TEMPERATURE
169 | ClimateEntityFeature.PRESET_MODE
170 | ClimateEntityFeature.TURN_OFF
171 | ClimateEntityFeature.TURN_ON
173 _attr_target_temperature_step = SHTRV_01_TEMPERATURE_SETTINGS[
"step"]
174 _attr_temperature_unit = UnitOfTemperature.CELSIUS
175 _enable_turn_on_off_backwards_compatibility =
False
179 coordinator: ShellyBlockCoordinator,
180 sensor_block: Block |
None,
181 device_block: Block |
None,
182 entry: RegistryEntry |
None =
None,
184 """Initialize climate."""
187 self.
blockblock: Block |
None = sensor_block
188 self.control_result: dict[str, Any] |
None =
None
189 self.
device_blockdevice_block: Block |
None = device_block
190 self.
last_statelast_state: State |
None =
None
197 self.
_unique_id_unique_id = f
"{self.coordinator.mac}-{self.block.description}"
198 assert self.
blockblock.channel
201 *coordinator.device.settings[
"thermostats"][
int(self.
blockblock.channel)][
202 "schedule_profile_names"
205 elif entry
is not None:
208 connections={(CONNECTION_NETWORK_MAC, coordinator.mac)},
215 """Return text specific state data to be restored."""
220 """Set unique id of entity."""
225 """Set target temperature."""
226 if self.
blockblock
is not None:
227 return cast(float, self.
blockblock.targetTemp)
231 if self.
hasshass.config.units
is US_CUSTOMARY_SYSTEM
and target_temp:
232 return TemperatureConverter.convert(
233 cast(float, target_temp),
234 UnitOfTemperature.FAHRENHEIT,
235 UnitOfTemperature.CELSIUS,
241 """Return current temperature."""
242 if self.
blockblock
is not None:
243 return cast(float, self.
blockblock.temp)
247 if self.
hasshass.config.units
is US_CUSTOMARY_SYSTEM
and current_temp:
248 return TemperatureConverter.convert(
249 cast(float, current_temp),
250 UnitOfTemperature.FAHRENHEIT,
251 UnitOfTemperature.CELSIUS,
257 """Device availability."""
259 return not cast(bool, self.
device_blockdevice_block.valveError)
260 return super().available
264 """HVAC current mode."""
267 return HVACMode(self.
last_statelast_state.state)
277 """Preset current mode."""
286 """HVAC current action."""
292 return HVACAction.OFF
294 return HVACAction.HEATING
if bool(self.
device_blockdevice_block.status)
else HVACAction.IDLE
298 """Preset available modes."""
302 """Return if valve is off or on."""
309 """Set block state (HTTP request)."""
310 LOGGER.debug(
"Setting state for entity %s, state: %s", self.
namename, kwargs)
312 return await self.coordinator.device.http_request(
313 "get", f
"thermostat/{self._channel}", kwargs
315 except DeviceConnectionError
as err:
316 self.coordinator.last_update_success =
False
318 f
"Setting state for entity {self.name} failed, state: {kwargs}, error:"
321 except InvalidAuthError:
325 """Set new target temperature."""
326 if (current_temp := kwargs.get(ATTR_TEMPERATURE))
is None:
331 if self.
blockblock
is not None and self.
blockblock.channel
is not None:
332 therm = self.coordinator.device.settings[
"thermostats"][
335 LOGGER.debug(
"Themostat settings: %s", therm)
336 if therm.get(
"target_t", {}).
get(
"units",
"C") ==
"F":
337 current_temp = TemperatureConverter.convert(
338 cast(float, current_temp),
339 UnitOfTemperature.CELSIUS,
340 UnitOfTemperature.FAHRENHEIT,
343 await self.
set_state_full_pathset_state_full_path(target_t_enabled=1, target_t=f
"{current_temp}")
347 if hvac_mode == HVACMode.OFF:
351 target_t_enabled=1, target_t=f
"{self._attr_min_temp}"
353 if hvac_mode == HVACMode.HEAT:
359 """Set preset mode."""
363 preset_index = self.
_preset_modes_preset_modes.index(preset_mode)
365 if preset_index == 0:
369 schedule=1, schedule_profile=f
"{preset_index}"
373 """Handle entity which will be added."""
374 LOGGER.info(
"Restoring entity %s", self.
namename)
377 if last_state
is not None:
381 list, self.
last_statelast_state.attributes.get(
"preset_modes")
385 if last_extra_data
is not None:
386 self.
_last_target_temp_last_target_temp = last_extra_data.as_dict()[
"last_target_temp"]
392 """Handle device update."""
393 if not self.coordinator.device.initialized:
397 if self.coordinator.device.status.get(
"calibrated")
is False:
398 ir.async_create_issue(
401 NOT_CALIBRATED_ISSUE_ID.format(unique=self.coordinator.mac),
404 severity=ir.IssueSeverity.ERROR,
405 translation_key=
"device_not_calibrated",
406 translation_placeholders={
407 "device_name": self.coordinator.name,
408 "ip_address": self.coordinator.device.ip_address,
412 ir.async_delete_issue(
415 NOT_CALIBRATED_ISSUE_ID.format(unique=self.coordinator.mac),
418 assert self.coordinator.device.blocks
420 for block
in self.coordinator.device.blocks:
421 if block.type ==
"device":
423 if hasattr(block,
"targetTemp"):
427 LOGGER.debug(
"Entity %s attached to blocks", self.
namename)
429 assert self.
blockblock.channel
434 *self.coordinator.device.settings[
"thermostats"][
436 ][
"schedule_profile_names"],
438 except InvalidAuthError:
439 self.
hasshass.async_create_task(
448 """Entity that controls a thermostat on RPC based Shelly devices."""
450 _attr_max_temp = RPC_THERMOSTAT_SETTINGS[
"max"]
451 _attr_min_temp = RPC_THERMOSTAT_SETTINGS[
"min"]
452 _attr_supported_features = (
453 ClimateEntityFeature.TARGET_TEMPERATURE
454 | ClimateEntityFeature.TURN_OFF
455 | ClimateEntityFeature.TURN_ON
457 _attr_target_temperature_step = RPC_THERMOSTAT_SETTINGS[
"step"]
458 _attr_temperature_unit = UnitOfTemperature.CELSIUS
459 _enable_turn_on_off_backwards_compatibility =
False
461 def __init__(self, coordinator: ShellyRpcCoordinator, id_: int) ->
None:
463 super().
__init__(coordinator, f
"thermostat:{id_}")
474 if (humidity_key := f
"humidity:{id_}")
in self.coordinator.device.status:
479 """Set target temperature."""
480 return cast(float, self.
statusstatus[
"target_C"])
484 """Return current temperature."""
485 return cast(float, self.
statusstatus[
"current_C"])
489 """Return current humidity."""
493 return cast(float, self.coordinator.device.status[self.
_humidity_key_humidity_key][
"rh"])
497 """HVAC current mode."""
498 if not self.
statusstatus[
"enable"]:
501 return HVACMode.COOL
if self.
_thermostat_type_thermostat_type ==
"cooling" else HVACMode.HEAT
505 """HVAC current action."""
506 if not self.
statusstatus[
"output"]:
507 return HVACAction.IDLE
512 else HVACAction.HEATING
516 """Set new target temperature."""
517 if (target_temp := kwargs.get(ATTR_TEMPERATURE))
is None:
521 "Thermostat.SetConfig",
522 {
"config": {
"id": self.
_id_id,
"target_C": target_temp}},
527 mode = hvac_mode
in (HVACMode.COOL, HVACMode.HEAT)
529 "Thermostat.SetConfig", {
"config": {
"id": self.
_id_id,
"enable": mode}}
float|None target_temperature(self)
float|None target_temperature(self)
None _handle_coordinator_update(self)
float|None current_temperature(self)
None async_added_to_hass(self)
HVACAction hvac_action(self)
list[str] preset_modes(self)
str|None preset_mode(self)
None __init__(self, ShellyBlockCoordinator coordinator, Block|None sensor_block, Block|None device_block, RegistryEntry|None entry=None)
None async_set_preset_mode(self, str preset_mode)
ShellyClimateExtraStoredData extra_restore_state_data(self)
None async_set_hvac_mode(self, HVACMode hvac_mode)
Any set_state_full_path(self, **Any kwargs)
None async_set_temperature(self, **Any kwargs)
float|None current_temperature(self)
float|None target_temperature(self)
None async_set_temperature(self, **Any kwargs)
None __init__(self, ShellyRpcCoordinator coordinator, int id_)
HVACAction hvac_action(self)
float|None current_humidity(self)
None async_set_hvac_mode(self, HVACMode hvac_mode)
Any call_rpc(self, str method, Any params)
None async_write_ha_state(self)
str|UndefinedType|None name(self)
State|None async_get_last_state(self)
ExtraStoredData|None async_get_last_extra_data(self)
web.Response get(self, web.Request request, str config_key)
None async_restore_climate_entities(HomeAssistant hass, ShellyConfigEntry config_entry, AddEntitiesCallback async_add_entities, ShellyBlockCoordinator coordinator)
None async_setup_climate_entities(AddEntitiesCallback async_add_entities, ShellyBlockCoordinator coordinator)
None async_setup_entry(HomeAssistant hass, ShellyConfigEntry config_entry, AddEntitiesCallback async_add_entities)
None async_setup_rpc_entry(HomeAssistant hass, ShellyConfigEntry config_entry, AddEntitiesCallback async_add_entities)
None async_shutdown_device_and_start_reauth(self)
bool is_rpc_thermostat_internal_actuator(dict[str, Any] status)
list[int] get_rpc_key_ids(dict[str, Any] keys_dict, str key)
int get_device_entry_gen(ConfigEntry entry)
None async_remove_shelly_entity(HomeAssistant hass, str domain, str unique_id)