1 """Support for Honeywell (US) Total Connect Comfort climate systems."""
3 from __future__
import annotations
8 from aiohttp
import ClientConnectionError
9 from aiosomecomfort
import (
12 ConnectionError
as AscConnectionError,
17 from aiosomecomfort.device
import Device
as SomeComfortDevice
20 ATTR_TARGET_TEMP_HIGH,
43 from .
import HoneywellData
46 CONF_COOL_AWAY_TEMPERATURE,
47 CONF_HEAT_AWAY_TEMPERATURE,
52 MODE_PERMANENT_HOLD = 2
53 MODE_TEMPORARY_HOLD = 1
54 MODE_HOLD = {MODE_PERMANENT_HOLD, MODE_TEMPORARY_HOLD}
56 ATTR_FAN_ACTION =
"fan_action"
58 ATTR_PERMANENT_HOLD =
"permanent_hold"
62 HEATING_MODES = {
"heat",
"emheat",
"auto"}
63 COOLING_MODES = {
"cool",
"auto"}
65 HVAC_MODE_TO_HW_MODE = {
66 "SwitchOffAllowed": {HVACMode.OFF:
"off"},
67 "SwitchAutoAllowed": {HVACMode.HEAT_COOL:
"auto"},
68 "SwitchCoolAllowed": {HVACMode.COOL:
"cool"},
69 "SwitchHeatAllowed": {HVACMode.HEAT:
"heat"},
71 HW_MODE_TO_HVAC_MODE = {
73 "emheat": HVACMode.HEAT,
74 "heat": HVACMode.HEAT,
75 "cool": HVACMode.COOL,
76 "auto": HVACMode.HEAT_COOL,
78 HW_MODE_TO_HA_HVAC_ACTION = {
79 "off": HVACAction.IDLE,
80 "fan": HVACAction.FAN,
81 "heat": HVACAction.HEATING,
82 "cool": HVACAction.COOLING,
85 "fanModeOnAllowed": {FAN_ON:
"on"},
86 "fanModeAutoAllowed": {FAN_AUTO:
"auto"},
87 "fanModeCirculateAllowed": {FAN_DIFFUSE:
"circulate"},
92 "circulate": FAN_DIFFUSE,
93 "follow schedule": FAN_AUTO,
96 SCAN_INTERVAL = datetime.timedelta(seconds=30)
100 hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback
102 """Set up the Honeywell thermostat."""
103 cool_away_temp = entry.options.get(CONF_COOL_AWAY_TEMPERATURE)
104 heat_away_temp = entry.options.get(CONF_HEAT_AWAY_TEMPERATURE)
106 data: HoneywellData = hass.data[DOMAIN][entry.entry_id]
111 for device
in data.devices.values()
118 hass: HomeAssistant, devices: dict[str, SomeComfortDevice]
120 """Migrate entities to string."""
121 entity_registry = er.async_get(hass)
122 for device
in devices.values():
123 entity_id = entity_registry.async_get_entity_id(
124 "climate", DOMAIN, device.deviceid
126 if entity_id
is not None:
127 entity_registry.async_update_entity(
128 entity_id, new_unique_id=
str(device.deviceid)
134 config_entry: ConfigEntry,
135 devices: dict[str, SomeComfortDevice],
137 """Remove stale devices from device registry."""
138 device_registry = dr.async_get(hass)
139 device_entries = dr.async_entries_for_config_entry(
140 device_registry, config_entry.entry_id
142 all_device_ids = {device.deviceid
for device
in devices.values()}
144 for device_entry
in device_entries:
145 device_id: str |
None =
None
147 for identifier
in device_entry.identifiers:
148 if identifier[0] == DOMAIN:
149 device_id = identifier[1]
152 if device_id
is None or device_id
not in all_device_ids:
156 device_registry.async_update_device(
157 device_entry.id, remove_config_entry_id=config_entry.entry_id
162 """Representation of a Honeywell US Thermostat."""
164 _attr_has_entity_name =
True
166 _attr_translation_key =
"honeywell"
167 _enable_turn_on_off_backwards_compatibility =
False
172 device: SomeComfortDevice,
173 cool_away_temp: int |
None,
174 heat_away_temp: int |
None,
176 """Initialize the thermostat."""
188 identifiers={(DOMAIN, device.deviceid)},
190 manufacturer=
"Honeywell",
195 if device.temperature_unit ==
"C":
203 for key1, value1
in HVAC_MODE_TO_HW_MODE.items()
204 if device.raw_ui_data[key1]
205 for key2, value2
in value1.items()
210 ClimateEntityFeature.PRESET_MODE
211 | ClimateEntityFeature.TARGET_TEMPERATURE
212 | ClimateEntityFeature.TARGET_TEMPERATURE_RANGE
216 ClimateEntityFeature.TURN_OFF | ClimateEntityFeature.TURN_ON
219 if device._data.get(
"canControlHumidification"):
222 if not device._data.get(
"hasFan"):
228 for key1, value1
in FAN_MODE_TO_HW.items()
229 if device.raw_fan_data[key1]
230 for key2, value2
in value1.items()
239 """Return the device specific state attributes."""
240 data: dict[str, Any] = {}
241 data[ATTR_FAN_ACTION] =
"running" if self.
_device_device.fan_running
else "idle"
243 if self.
_device_device.raw_dr_data:
244 data[
"dr_phase"] = self.
_device_device.raw_dr_data.get(
"Phase")
249 """Return the minimum temperature."""
251 return self.
_device_device.raw_ui_data[
"CoolLowerSetptLimit"]
253 return self.
_device_device.raw_ui_data[
"HeatLowerSetptLimit"]
257 self.
_device_device.raw_ui_data[
"CoolLowerSetptLimit"],
258 self.
_device_device.raw_ui_data[
"HeatLowerSetptLimit"],
261 return TemperatureConverter.convert(
262 DEFAULT_MIN_TEMP, UnitOfTemperature.CELSIUS, self.
temperature_unittemperature_unit
267 """Return the maximum temperature."""
269 return self.
_device_device.raw_ui_data[
"CoolUpperSetptLimit"]
271 return self.
_device_device.raw_ui_data[
"HeatUpperSetptLimit"]
275 self.
_device_device.raw_ui_data[
"CoolUpperSetptLimit"],
276 self.
_device_device.raw_ui_data[
"HeatUpperSetptLimit"],
279 return TemperatureConverter.convert(
280 DEFAULT_MAX_TEMP, UnitOfTemperature.CELSIUS, self.
temperature_unittemperature_unit
285 """Return the current humidity."""
286 return self.
_device_device.current_humidity
290 """Return hvac operation ie. heat, cool mode."""
291 return HW_MODE_TO_HVAC_MODE.get(self.
_device_device.system_mode)
295 """Return the current running hvac operation if supported."""
298 return HW_MODE_TO_HA_HVAC_ACTION.get(self.
_device_device.equipment_output_status)
302 """Return the current temperature."""
303 return self.
_device_device.current_temperature
307 """Return the temperature we try to reach."""
309 return self.
_device_device.setpoint_cool
311 return self.
_device_device.setpoint_heat
316 """Return the highbound target temperature we try to reach."""
318 return self.
_device_device.setpoint_cool
323 """Return the lowbound target temperature we try to reach."""
325 return self.
_device_device.setpoint_heat
330 """Return the current preset mode, e.g., home, away, temp."""
338 self.
_away_away =
False
344 """Return the fan setting."""
345 return HW_FAN_MODE_TO_HA.get(self.
_device_device.fan_mode)
348 heat_status = self.
_device_device.raw_ui_data.get(
"StatusHeat", 0)
349 cool_status = self.
_device_device.raw_ui_data.get(
"StatusCool", 0)
350 return heat_status
in MODE_HOLD
or cool_status
in MODE_HOLD
353 heat_status = self.
_device_device.raw_ui_data.get(
"StatusHeat", 0)
354 cool_status = self.
_device_device.raw_ui_data.get(
"StatusCool", 0)
355 return MODE_PERMANENT_HOLD
in (heat_status, cool_status)
358 """Set new target temperature."""
359 if (temperature := kwargs.get(ATTR_TEMPERATURE))
is None:
363 mode = self.
_device_device.system_mode
365 if self.
_device_device.hold_heat
is False and self.
_device_device.hold_cool
is False:
367 hour_heat, minute_heat = divmod(
368 self.
_device_device.raw_ui_data[
"HeatNextPeriod"] * 15, 60
370 hour_cool, minute_cool = divmod(
371 self.
_device_device.raw_ui_data[
"CoolNextPeriod"] * 15, 60
374 if mode
in COOLING_MODES:
375 await self.
_device_device.set_hold_cool(
376 datetime.time(hour_cool, minute_cool), temperature
378 if mode
in HEATING_MODES:
379 await self.
_device_device.set_hold_heat(
380 datetime.time(hour_heat, minute_heat), temperature
386 await self.
_device_device.set_setpoint_cool(temperature)
387 if mode
in [
"heat",
"emheat"]:
388 await self.
_device_device.set_setpoint_heat(temperature)
390 except (AscConnectionError, UnexpectedResponse)
as err:
392 translation_domain=DOMAIN,
393 translation_key=
"temp_failed",
396 except SomeComfortError
as err:
397 _LOGGER.error(
"Invalid temperature %.1f: %s", temperature, err)
399 translation_domain=DOMAIN,
400 translation_key=
"temp_failed_value",
401 translation_placeholders={
"temperature": temperature},
405 """Set new target temperature."""
406 if {HVACMode.COOL, HVACMode.HEAT} & set(self.
_hvac_mode_map_hvac_mode_map):
409 if temperature := kwargs.get(ATTR_TARGET_TEMP_HIGH):
410 await self.
_device_device.set_setpoint_cool(temperature)
411 if temperature := kwargs.get(ATTR_TARGET_TEMP_LOW):
412 await self.
_device_device.set_setpoint_heat(temperature)
414 except (AscConnectionError, UnexpectedResponse)
as err:
416 translation_domain=DOMAIN,
417 translation_key=
"temp_failed",
420 except SomeComfortError
as err:
421 _LOGGER.error(
"Invalid temperature %.1f: %s", temperature, err)
423 translation_domain=DOMAIN,
424 translation_key=
"temp_failed_value",
425 translation_placeholders={
"temperature":
str(temperature)},
429 """Set new target fan mode."""
433 except SomeComfortError
as err:
435 translation_domain=DOMAIN,
436 translation_key=
"fan_mode_failed",
440 """Set new target hvac mode."""
444 except SomeComfortError
as err:
446 translation_domain=DOMAIN,
447 translation_key=
"sys_mode_failed",
453 Somecomfort does have a proprietary away mode, but it doesn't really
454 work the way it should. For example: If you set a temperature manually
455 it doesn't get overwritten when away mode is switched on.
457 self.
_away_away =
True
459 mode = self.
_device_device.system_mode
463 if mode
in COOLING_MODES:
465 if mode
in HEATING_MODES:
468 except (AscConnectionError, UnexpectedResponse)
as err:
470 translation_domain=DOMAIN,
471 translation_key=
"away_mode_failed",
474 except SomeComfortError
as err:
476 "Temperature out of range. Mode: %s, Heat Temperature: %.1f, Cool Temperature: %.1f",
482 translation_domain=DOMAIN,
483 translation_key=
"temp_failed_range",
484 translation_placeholders={
492 """Turn permanent hold on."""
494 mode = self.
_device_device.system_mode
496 if mode
in HW_MODE_TO_HVAC_MODE:
499 if mode
in COOLING_MODES:
500 await self.
_device_device.set_hold_cool(
True)
501 if mode
in HEATING_MODES:
502 await self.
_device_device.set_hold_heat(
True)
504 except SomeComfortError
as err:
505 _LOGGER.error(
"Couldn't set permanent hold")
507 translation_domain=DOMAIN,
508 translation_key=
"set_hold_failed",
511 _LOGGER.error(
"Invalid system mode returned: %s", mode)
513 translation_domain=DOMAIN,
514 translation_key=
"set_mode_failed",
515 translation_placeholders={
"mode": mode},
519 """Turn away/hold off."""
520 self.
_away_away =
False
523 await self.
_device_device.set_hold_cool(
False)
524 await self.
_device_device.set_hold_heat(
False)
526 except SomeComfortError
as err:
527 _LOGGER.error(
"Can not stop hold mode")
529 translation_domain=DOMAIN,
530 translation_key=
"stop_hold_failed",
534 """Set new preset mode."""
535 if preset_mode == PRESET_AWAY:
537 elif preset_mode == PRESET_HOLD:
538 self.
_away_away =
False
544 """Get the latest state from the service."""
546 async
def _login() -> None:
548 await self.
_data_data.client.login()
549 await self.
_device_device.refresh()
556 ClientConnectionError,
566 await self.
_device_device.refresh()
568 except UnauthorizedError:
575 ClientConnectionError,
581 except UnexpectedResponse:
None set_fan_mode(self, str fan_mode)
str temperature_unit(self)
HVACMode|None hvac_mode(self)
list[HVACMode] hvac_modes(self)
None _turn_away_mode_off(self)
str|None preset_mode(self)
HVACMode|None hvac_mode(self)
None __init__(self, HoneywellData data, SomeComfortDevice device, int|None cool_away_temp, int|None heat_away_temp)
_attr_translation_placeholders
bool _is_permanent_hold(self)
int|None current_humidity(self)
float|None target_temperature(self)
None _set_temperature(self, **kwargs)
float|None current_temperature(self)
None async_set_hvac_mode(self, HVACMode hvac_mode)
None _turn_away_mode_on(self)
float|None target_temperature_low(self)
dict[str, Any] extra_state_attributes(self)
float|None target_temperature_high(self)
None async_set_fan_mode(self, str fan_mode)
HVACAction|None hvac_action(self)
None async_set_temperature(self, **Any kwargs)
None async_set_preset_mode(self, str preset_mode)
None _turn_hold_mode_on(self)
None async_setup_entry(HomeAssistant hass, ConfigEntry entry, AddEntitiesCallback async_add_entities)
None remove_stale_devices(HomeAssistant hass, ConfigEntry config_entry, dict[str, SomeComfortDevice] devices)
None _async_migrate_unique_id(HomeAssistant hass, dict[str, SomeComfortDevice] devices)
None _login(ConnectionManager connection_manager, str url, str username, str password)