1 """Support for Sensibo wifi-enabled home thermostats."""
3 from __future__
import annotations
5 from bisect
import bisect_left
6 from typing
import TYPE_CHECKING, Any
8 import voluptuous
as vol
30 from .
import SensiboConfigEntry
31 from .const
import DOMAIN
32 from .coordinator
import SensiboDataUpdateCoordinator
33 from .entity
import SensiboDeviceBaseEntity, async_handle_api_call
35 SERVICE_ASSUME_STATE =
"assume_state"
36 SERVICE_ENABLE_TIMER =
"enable_timer"
37 ATTR_MINUTES =
"minutes"
38 SERVICE_ENABLE_PURE_BOOST =
"enable_pure_boost"
39 SERVICE_DISABLE_PURE_BOOST =
"disable_pure_boost"
40 SERVICE_FULL_STATE =
"full_state"
41 SERVICE_ENABLE_CLIMATE_REACT =
"enable_climate_react"
42 ATTR_HIGH_TEMPERATURE_THRESHOLD =
"high_temperature_threshold"
43 ATTR_HIGH_TEMPERATURE_STATE =
"high_temperature_state"
44 ATTR_LOW_TEMPERATURE_THRESHOLD =
"low_temperature_threshold"
45 ATTR_LOW_TEMPERATURE_STATE =
"low_temperature_state"
46 ATTR_SMART_TYPE =
"smart_type"
48 ATTR_AC_INTEGRATION =
"ac_integration"
49 ATTR_GEO_INTEGRATION =
"geo_integration"
50 ATTR_INDOOR_INTEGRATION =
"indoor_integration"
51 ATTR_OUTDOOR_INTEGRATION =
"outdoor_integration"
52 ATTR_SENSITIVITY =
"sensitivity"
53 ATTR_TARGET_TEMPERATURE =
"target_temperature"
54 ATTR_HORIZONTAL_SWING_MODE =
"horizontal_swing_mode"
56 BOOST_INCLUSIVE =
"boost_inclusive"
58 AVAILABLE_FAN_MODES = {
68 AVAILABLE_SWING_MODES = {
86 "fanLevel": ClimateEntityFeature.FAN_MODE,
87 "swing": ClimateEntityFeature.SWING_MODE,
88 "targetTemperature": ClimateEntityFeature.TARGET_TEMPERATURE,
92 "cool": HVACMode.COOL,
93 "heat": HVACMode.HEAT,
94 "fan": HVACMode.FAN_ONLY,
95 "auto": HVACMode.HEAT_COOL,
100 HA_TO_SENSIBO = {value: key
for key, value
in SENSIBO_TO_HA.items()}
103 "targetTemperature":
"target_temp",
104 "fanLevel":
"fan_mode",
107 "swing":
"swing_mode",
112 if target <= valid_targets[0]:
113 return valid_targets[0]
114 if target >= valid_targets[-1]:
115 return valid_targets[-1]
116 return valid_targets[bisect_left(valid_targets, target)]
121 entry: SensiboConfigEntry,
122 async_add_entities: AddEntitiesCallback,
124 """Set up the Sensibo climate entry."""
126 coordinator = entry.runtime_data
130 for device_id, device_data
in coordinator.data.parsed.items()
135 platform = entity_platform.async_get_current_platform()
136 platform.async_register_entity_service(
137 SERVICE_ASSUME_STATE,
139 vol.Required(ATTR_STATE): vol.In([
"on",
"off"]),
141 "async_assume_state",
143 platform.async_register_entity_service(
144 SERVICE_ENABLE_TIMER,
146 vol.Required(ATTR_MINUTES): cv.positive_int,
148 "async_enable_timer",
150 platform.async_register_entity_service(
151 SERVICE_ENABLE_PURE_BOOST,
153 vol.Required(ATTR_AC_INTEGRATION): bool,
154 vol.Required(ATTR_GEO_INTEGRATION): bool,
155 vol.Required(ATTR_INDOOR_INTEGRATION): bool,
156 vol.Required(ATTR_OUTDOOR_INTEGRATION): bool,
157 vol.Required(ATTR_SENSITIVITY): vol.In([
"Normal",
"Sensitive"]),
159 "async_enable_pure_boost",
161 platform.async_register_entity_service(
164 vol.Required(ATTR_MODE): vol.In(
165 [
"cool",
"heat",
"fan",
"auto",
"dry",
"off"]
167 vol.Optional(ATTR_TARGET_TEMPERATURE): int,
168 vol.Optional(ATTR_FAN_MODE): str,
169 vol.Optional(ATTR_SWING_MODE): str,
170 vol.Optional(ATTR_HORIZONTAL_SWING_MODE): str,
171 vol.Optional(ATTR_LIGHT): vol.In([
"on",
"off"]),
173 "async_full_ac_state",
176 platform.async_register_entity_service(
177 SERVICE_ENABLE_CLIMATE_REACT,
179 vol.Required(ATTR_HIGH_TEMPERATURE_THRESHOLD): vol.Coerce(float),
180 vol.Required(ATTR_HIGH_TEMPERATURE_STATE): dict,
181 vol.Required(ATTR_LOW_TEMPERATURE_THRESHOLD): vol.Coerce(float),
182 vol.Required(ATTR_LOW_TEMPERATURE_STATE): dict,
183 vol.Required(ATTR_SMART_TYPE): vol.In(
184 [
"temperature",
"feelsLike",
"humidity"]
187 "async_enable_climate_react",
192 """Representation of a Sensibo device."""
195 _attr_precision = PRECISION_TENTHS
196 _attr_translation_key =
"climate_device"
197 _enable_turn_on_off_backwards_compatibility =
False
200 self, coordinator: SensiboDataUpdateCoordinator, device_id: str
202 """Initiate Sensibo Climate."""
203 super().
__init__(coordinator, device_id)
206 UnitOfTemperature.CELSIUS
208 else UnitOfTemperature.FAHRENHEIT
213 """Get supported features."""
214 features = ClimateEntityFeature.TURN_OFF | ClimateEntityFeature.TURN_ON
215 for key
in self.
device_datadevice_data.full_features:
216 if key
in FIELD_TO_FLAG:
217 features |= FIELD_TO_FLAG[key]
222 """Return the current humidity."""
227 """Return hvac operation."""
229 return SENSIBO_TO_HA[self.
device_datadevice_data.hvac_mode]
234 """Return the list of available hvac operation modes."""
237 hvac_modes = [SENSIBO_TO_HA[mode]
for mode
in self.
device_datadevice_data.hvac_modes]
238 return hvac_modes
if hvac_modes
else [HVACMode.OFF]
242 """Return the current temperature."""
244 return TemperatureConverter.convert(
246 UnitOfTemperature.CELSIUS,
253 """Return temperature unit."""
255 UnitOfTemperature.CELSIUS
257 else UnitOfTemperature.FAHRENHEIT
262 """Return the temperature we try to reach."""
263 target_temp: int |
None = self.
device_datadevice_data.target_temp
268 """Return the supported step of target temperature."""
269 target_temp_step: int = self.
device_datadevice_data.temp_step
270 return target_temp_step
274 """Return the fan setting."""
275 fan_mode: str |
None = self.
device_datadevice_data.fan_mode
280 """Return the list of available fan modes."""
287 """Return the swing setting."""
288 swing_mode: str |
None = self.
device_datadevice_data.swing_mode
293 """Return the list of available swing modes."""
300 """Return the minimum temperature."""
301 min_temp: int = self.
device_datadevice_data.temp_list[0]
306 """Return the maximum temperature."""
307 max_temp: int = self.
device_datadevice_data.temp_list[-1]
312 """Return True if entity is available."""
313 return self.
device_datadevice_data.available
and super().available
316 """Set new target temperature."""
317 if "targetTemperature" not in self.
device_datadevice_data.active_features:
319 translation_domain=DOMAIN,
320 translation_key=
"no_target_temperature_in_features",
323 if (temperature := kwargs.get(ATTR_TEMPERATURE))
is None:
325 translation_domain=DOMAIN,
326 translation_key=
"no_target_temperature",
334 key=AC_STATE_TO_DATA[
"targetTemperature"],
336 name=
"targetTemperature",
341 """Set new target fan mode."""
342 if "fanLevel" not in self.
device_datadevice_data.active_features:
344 translation_domain=DOMAIN,
345 translation_key=
"no_fan_level_in_features",
347 if fan_mode
not in AVAILABLE_FAN_MODES:
349 translation_domain=DOMAIN,
350 translation_key=
"fan_mode_not_supported",
351 translation_placeholders={
"fan_mode": fan_mode},
354 transformation = self.
device_datadevice_data.fan_modes_translated
356 key=AC_STATE_TO_DATA[
"fanLevel"],
360 transformation=transformation,
364 """Set new target operation mode."""
365 if hvac_mode == HVACMode.OFF:
367 key=AC_STATE_TO_DATA[
"on"],
377 key=AC_STATE_TO_DATA[
"on"],
384 key=AC_STATE_TO_DATA[
"mode"],
385 value=HA_TO_SENSIBO[hvac_mode],
391 """Set new target swing operation."""
392 if "swing" not in self.
device_datadevice_data.active_features:
394 translation_domain=DOMAIN,
395 translation_key=
"no_swing_in_features",
397 if swing_mode
not in AVAILABLE_SWING_MODES:
399 translation_domain=DOMAIN,
400 translation_key=
"swing_not_supported",
401 translation_placeholders={
"swing_mode": swing_mode},
404 transformation = self.
device_datadevice_data.swing_modes_translated
406 key=AC_STATE_TO_DATA[
"swing"],
410 transformation=transformation,
414 """Turn Sensibo unit on."""
416 key=AC_STATE_TO_DATA[
"on"],
423 """Turn Sensibo unit on."""
425 key=AC_STATE_TO_DATA[
"on"],
432 """Sync state with api."""
434 key=AC_STATE_TO_DATA[
"on"],
435 value=state != HVACMode.OFF,
443 target_temperature: int |
None =
None,
444 fan_mode: str |
None =
None,
445 swing_mode: str |
None =
None,
446 horizontal_swing_mode: str |
None =
None,
447 light: str |
None =
None,
449 """Set full AC state."""
450 new_ac_state = self.
device_datadevice_data.ac_states
451 new_ac_state.pop(
"timestamp")
452 new_ac_state[
"on"] =
False
454 new_ac_state[
"on"] =
True
455 new_ac_state[
"mode"] = mode
456 if target_temperature:
457 new_ac_state[
"targetTemperature"] = target_temperature
459 new_ac_state[
"fanLevel"] = fan_mode
461 new_ac_state[
"swing"] = swing_mode
462 if horizontal_swing_mode:
463 new_ac_state[
"horizontalSwing"] = horizontal_swing_mode
465 new_ac_state[
"light"] = light
468 key=
"hvac_mode", value=mode, data=new_ac_state
472 """Enable the timer."""
475 "minutesFromNow": minutes,
476 "acState": {**self.
device_datadevice_data.ac_states,
"on": new_state},
486 ac_integration: bool |
None =
None,
487 geo_integration: bool |
None =
None,
488 indoor_integration: bool |
None =
None,
489 outdoor_integration: bool |
None =
None,
490 sensitivity: str |
None =
None,
492 """Enable Pure Boost Configuration."""
494 params: dict[str, str | bool] = {
497 if sensitivity
is not None:
498 params[
"sensitivity"] = sensitivity[0]
499 if indoor_integration
is not None:
500 params[
"measurementsIntegration"] = indoor_integration
501 if ac_integration
is not None:
502 params[
"acIntegration"] = ac_integration
503 if geo_integration
is not None:
504 params[
"geoIntegration"] = geo_integration
505 if outdoor_integration
is not None:
506 params[
"primeIntegration"] = outdoor_integration
509 key=
"pure_boost_enabled",
516 high_temperature_threshold: float,
517 high_temperature_state: dict[str, Any],
518 low_temperature_threshold: float,
519 low_temperature_state: dict[str, Any],
522 """Enable Climate React Configuration."""
523 high_temp = high_temperature_threshold
524 low_temp = low_temperature_threshold
526 if high_temperature_state.get(
"temperatureUnit") ==
"F":
527 high_temp = TemperatureConverter.convert(
528 high_temperature_threshold,
529 UnitOfTemperature.FAHRENHEIT,
530 UnitOfTemperature.CELSIUS,
532 low_temp = TemperatureConverter.convert(
533 low_temperature_threshold,
534 UnitOfTemperature.FAHRENHEIT,
535 UnitOfTemperature.CELSIUS,
538 params: dict[str, str | bool | float | dict] = {
541 "highTemperatureState": high_temperature_state,
542 "highTemperatureThreshold": high_temp,
543 "lowTemperatureState": low_temperature_state,
544 "lowTemperatureThreshold": low_temp,
554 @async_handle_api_call
560 assumed_state: bool =
False,
561 transformation: dict |
None =
None,
563 """Make service call to api."""
565 value = transformation[value]
566 result = await self.
_client_client.async_set_ac_state_property(
573 return bool(result.get(
"result", {}).
get(
"status") ==
"Success")
575 @async_handle_api_call
582 """Make service call to api."""
583 result = await self.
_client_client.async_set_timer(self.
_device_id_device_id, data)
584 return bool(result.get(
"status") ==
"success")
586 @async_handle_api_call
593 """Make service call to api."""
594 result = await self.
_client_client.async_set_pureboost(self.
_device_id_device_id, data)
595 return bool(result.get(
"status") ==
"success")
597 @async_handle_api_call
604 """Make service call to api."""
605 result = await self.
_client_client.async_set_climate_react(self.
_device_id_device_id, data)
606 return bool(result.get(
"status") ==
"success")
608 @async_handle_api_call
615 """Make service call to api."""
616 result = await self.
_client_client.async_set_ac_states(self.
_device_id_device_id, data)
617 return bool(result.get(
"result", {}).
get(
"status") ==
"Success")
str temperature_unit(self)
float|None target_temperature(self)
float|None target_temperature_step(self)
list[str]|None swing_modes(self)
bool api_call_custom_service_full_ac_state(self, str key, Any value, dict data)
None async_full_ac_state(self, str mode, int|None target_temperature=None, str|None fan_mode=None, str|None swing_mode=None, str|None horizontal_swing_mode=None, str|None light=None)
str temperature_unit(self)
bool api_call_custom_service_timer(self, str key, Any value, dict data)
None async_set_temperature(self, **Any kwargs)
bool async_send_api_call(self, str key, Any value, str name, bool assumed_state=False, dict|None transformation=None)
None async_enable_climate_react(self, float high_temperature_threshold, dict[str, Any] high_temperature_state, float low_temperature_threshold, dict[str, Any] low_temperature_state, str smart_type)
None async_enable_pure_boost(self, bool|None ac_integration=None, bool|None geo_integration=None, bool|None indoor_integration=None, bool|None outdoor_integration=None, str|None sensitivity=None)
int|None current_humidity(self)
list[str]|None fan_modes(self)
None async_assume_state(self, str state)
list[HVACMode] hvac_modes(self)
None async_turn_off(self)
None async_enable_timer(self, int minutes)
None async_set_fan_mode(self, str fan_mode)
float|None target_temperature(self)
None async_set_swing_mode(self, str swing_mode)
ClimateEntityFeature get_features(self)
bool api_call_custom_service_climate_react(self, str key, Any value, dict data)
None async_set_hvac_mode(self, HVACMode hvac_mode)
None __init__(self, SensiboDataUpdateCoordinator coordinator, str device_id)
str|None swing_mode(self)
bool api_call_custom_service_pure_boost(self, str key, Any value, dict data)
float|None current_temperature(self)
SensiboDevice device_data(self)
web.Response get(self, web.Request request, str config_key)
None async_setup_entry(HomeAssistant hass, SensiboConfigEntry entry, AddEntitiesCallback async_add_entities)
int _find_valid_target_temp(int target, list[int] valid_targets)