1 """Support for Nexia / Trane XL thermostats."""
3 from __future__
import annotations
7 from nexia.const
import (
18 from nexia.thermostat
import NexiaThermostat
19 from nexia.util
import find_humidity_setpoint
20 from nexia.zone
import NexiaThermostatZone
21 import voluptuous
as vol
26 ATTR_TARGET_TEMP_HIGH,
43 ATTR_DEHUMIDIFY_SETPOINT,
44 ATTR_HUMIDIFY_SETPOINT,
48 from .coordinator
import NexiaDataUpdateCoordinator
49 from .entity
import NexiaThermostatZoneEntity
50 from .types
import NexiaConfigEntry
51 from .util
import percent_conv
55 SERVICE_SET_AIRCLEANER_MODE =
"set_aircleaner_mode"
56 SERVICE_SET_HUMIDIFY_SETPOINT =
"set_humidify_setpoint"
57 SERVICE_SET_HVAC_RUN_MODE =
"set_hvac_run_mode"
59 SET_AIRCLEANER_SCHEMA: VolDictType = {
60 vol.Required(ATTR_AIRCLEANER_MODE): cv.string,
63 SET_HUMIDITY_SCHEMA: VolDictType = {
64 vol.Required(ATTR_HUMIDITY): vol.All(vol.Coerce(int), vol.Range(min=35, max=65)),
67 SET_HVAC_RUN_MODE_SCHEMA = vol.All(
68 cv.has_at_least_one_key(ATTR_RUN_MODE, ATTR_HVAC_MODE),
69 cv.make_entity_service_schema(
71 vol.Optional(ATTR_RUN_MODE): vol.In([HOLD_PERMANENT, HOLD_RESUME_SCHEDULE]),
72 vol.Optional(ATTR_HVAC_MODE): vol.In(
73 [HVACMode.HEAT, HVACMode.COOL, HVACMode.AUTO]
88 HA_TO_NEXIA_HVAC_MODE_MAP = {
89 HVACMode.HEAT: OPERATION_MODE_HEAT,
90 HVACMode.COOL: OPERATION_MODE_COOL,
91 HVACMode.HEAT_COOL: OPERATION_MODE_AUTO,
92 HVACMode.AUTO: OPERATION_MODE_AUTO,
93 HVACMode.OFF: OPERATION_MODE_OFF,
95 NEXIA_TO_HA_HVAC_MODE_MAP = {
96 value: key
for key, value
in HA_TO_NEXIA_HVAC_MODE_MAP.items()
108 ClimateEntityFeature.TARGET_TEMPERATURE_RANGE
109 | ClimateEntityFeature.TARGET_TEMPERATURE
110 | ClimateEntityFeature.FAN_MODE
111 | ClimateEntityFeature.PRESET_MODE
112 | ClimateEntityFeature.TURN_OFF
113 | ClimateEntityFeature.TURN_ON
119 config_entry: NexiaConfigEntry,
120 async_add_entities: AddEntitiesCallback,
122 """Set up climate for a Nexia device."""
123 coordinator = config_entry.runtime_data
124 nexia_home = coordinator.nexia_home
126 platform = entity_platform.async_get_current_platform()
128 platform.async_register_entity_service(
129 SERVICE_SET_HUMIDIFY_SETPOINT,
131 f
"async_{SERVICE_SET_HUMIDIFY_SETPOINT}",
133 platform.async_register_entity_service(
134 SERVICE_SET_AIRCLEANER_MODE,
135 SET_AIRCLEANER_SCHEMA,
136 f
"async_{SERVICE_SET_AIRCLEANER_MODE}",
138 platform.async_register_entity_service(
139 SERVICE_SET_HVAC_RUN_MODE,
140 SET_HVAC_RUN_MODE_SCHEMA,
141 f
"async_{SERVICE_SET_HVAC_RUN_MODE}",
144 entities: list[NexiaZone] = []
145 for thermostat_id
in nexia_home.get_thermostat_ids():
146 thermostat: NexiaThermostat = nexia_home.get_thermostat_by_id(thermostat_id)
147 for zone_id
in thermostat.get_zone_ids():
148 zone: NexiaThermostatZone = thermostat.get_zone_by_id(zone_id)
149 entities.append(
NexiaZone(coordinator, zone))
155 """Provides Nexia Climate support."""
158 _enable_turn_on_off_backwards_compatibility =
False
161 self, coordinator: NexiaDataUpdateCoordinator, zone: NexiaThermostatZone
163 """Initialize the thermostat."""
164 super().
__init__(coordinator, zone, zone.zone_id)
166 unit = thermostat.get_unit()
167 min_humidity, max_humidity = thermostat.get_humidity_setpoint_limits()
168 min_setpoint, max_setpoint = thermostat.get_setpoint_limits()
188 UnitOfTemperature.CELSIUS
if unit ==
"C" else UnitOfTemperature.FAHRENHEIT
195 return self.
_thermostat_thermostat.is_blower_active()
199 """Return the current temperature."""
204 """Return the fan setting."""
208 """Set new target fan mode."""
213 """Set the hvac run mode."""
214 if run_mode
is not None:
215 if run_mode == HOLD_PERMANENT:
216 await self.
_zone_zone.set_permanent_hold()
218 await self.
_zone_zone.call_return_to_schedule()
219 if hvac_mode
is not None:
220 await self.
_zone_zone.set_mode(mode=HA_TO_NEXIA_HVAC_MODE_MAP[hvac_mode])
225 """Preset that is active."""
226 return self.
_zone_zone.get_preset()
229 """Dehumidify target."""
230 if self.
_thermostat_thermostat.has_dehumidify_support():
238 """Humidity indoors setpoint."""
247 """Humidity indoors."""
254 """Temperature we try to reach."""
255 current_mode = self.
_zone_zone.get_current_mode()
257 if current_mode == OPERATION_MODE_COOL:
258 return self.
_zone_zone.get_cooling_setpoint()
259 if current_mode == OPERATION_MODE_HEAT:
260 return self.
_zone_zone.get_heating_setpoint()
265 """Highest temperature we are trying to reach."""
266 current_mode = self.
_zone_zone.get_current_mode()
268 if current_mode
in (OPERATION_MODE_COOL, OPERATION_MODE_HEAT):
270 return self.
_zone_zone.get_cooling_setpoint()
274 """Lowest temperature we are trying to reach."""
275 current_mode = self.
_zone_zone.get_current_mode()
277 if current_mode
in (OPERATION_MODE_COOL, OPERATION_MODE_HEAT):
279 return self.
_zone_zone.get_heating_setpoint()
283 """Operation ie. heat, cool, idle."""
284 system_status = self.
_thermostat_thermostat.get_system_status()
285 zone_called = self.
_zone_zone.is_calling()
287 if self.
_zone_zone.get_requested_mode() == OPERATION_MODE_OFF:
288 return HVACAction.OFF
290 return HVACAction.IDLE
291 if system_status == SYSTEM_STATUS_COOL:
292 return HVACAction.COOLING
293 if system_status == SYSTEM_STATUS_HEAT:
294 return HVACAction.HEATING
295 if system_status == SYSTEM_STATUS_IDLE:
296 return HVACAction.IDLE
297 return HVACAction.IDLE
301 """Return current mode, as the user-visible name."""
302 mode = self.
_zone_zone.get_requested_mode()
303 hold = self.
_zone_zone.is_in_permanent_hold()
310 if hold
and mode == OPERATION_MODE_AUTO:
311 return HVACMode.HEAT_COOL
313 return NEXIA_TO_HA_HVAC_MODE_MAP[mode]
316 """Set target temperature."""
317 new_heat_temp = kwargs.get(ATTR_TARGET_TEMP_LOW)
318 new_cool_temp = kwargs.get(ATTR_TARGET_TEMP_HIGH)
319 set_temp = kwargs.get(ATTR_TEMPERATURE)
321 deadband = self.
_thermostat_thermostat.get_deadband()
322 cur_cool_temp = self.
_zone_zone.get_cooling_setpoint()
323 cur_heat_temp = self.
_zone_zone.get_heating_setpoint()
324 (min_temp, max_temp) = self.
_thermostat_thermostat.get_setpoint_limits()
327 if new_heat_temp
and new_heat_temp + deadband > max_temp:
328 new_heat_temp = max_temp - deadband
329 if new_cool_temp
and new_cool_temp - deadband < min_temp:
330 new_cool_temp = min_temp + deadband
335 and new_heat_temp != cur_heat_temp
336 and new_cool_temp - new_heat_temp < deadband
338 new_cool_temp = new_heat_temp + deadband
342 and new_cool_temp != cur_cool_temp
343 and new_cool_temp - new_heat_temp < deadband
345 new_heat_temp = new_cool_temp - deadband
347 await self.
_zone_zone.set_heat_cool_temp(
348 heat_temperature=new_heat_temp,
349 cool_temperature=new_cool_temp,
350 set_temperature=set_temp,
356 """Emergency heat state."""
357 return self.
_thermostat_thermostat.is_emergency_heat_active()
361 """Return the device specific state attributes."""
368 self.
_thermostat_thermostat.get_dehumidify_setpoint()
370 attrs[ATTR_DEHUMIDIFY_SETPOINT] = dehumdify_setpoint
373 attrs[ATTR_HUMIDIFY_SETPOINT] = humdify_setpoint
377 """Set the preset mode."""
378 await self.
_zone_zone.set_preset(preset_mode)
382 """Turn Aux Heat off."""
387 breaks_in_ha_version=
"2025.4.0",
390 translation_key=
"migrate_aux_heat",
391 severity=IssueSeverity.WARNING,
393 await self.
_thermostat_thermostat.set_emergency_heat(
False)
397 """Turn Aux Heat on."""
402 breaks_in_ha_version=
"2025.4.0",
405 translation_key=
"migrate_aux_heat",
406 severity=IssueSeverity.WARNING,
408 await self.
_thermostat_thermostat.set_emergency_heat(
True)
412 """Turn off the zone."""
417 """Turn on the zone."""
422 """Set the system mode (Auto, Heat_Cool, Cool, Heat, etc)."""
423 if hvac_mode == HVACMode.OFF:
424 await self.
_zone_zone.call_permanent_off()
425 elif hvac_mode == HVACMode.AUTO:
426 await self.
_zone_zone.call_return_to_schedule()
427 await self.
_zone_zone.set_mode(mode=OPERATION_MODE_AUTO)
429 await self.
_zone_zone.set_permanent_hold()
430 await self.
_zone_zone.set_mode(mode=HA_TO_NEXIA_HVAC_MODE_MAP[hvac_mode])
435 """Set the aircleaner mode."""
436 await self.
_thermostat_thermostat.set_air_cleaner(aircleaner_mode)
440 """Set the humidify setpoint."""
441 target_humidity = find_humidity_setpoint(humidity / 100.0)
442 if self.
_thermostat_thermostat.get_humidify_setpoint() == target_humidity:
446 await self.
_thermostat_thermostat.set_humidify_setpoint(target_humidity)
450 """Set the dehumidify setpoint."""
451 target_humidity = find_humidity_setpoint(humidity / 100.0)
452 if self.
_thermostat_thermostat.get_dehumidify_setpoint() == target_humidity:
456 await self.
_thermostat_thermostat.set_dehumidify_setpoint(target_humidity)
None set_fan_mode(self, str fan_mode)
None async_set_hvac_mode(self, HVACMode hvac_mode)
None async_turn_aux_heat_off(self)
None async_set_hvac_mode(self, HVACMode hvac_mode)
def target_temperature_low(self)
HVACAction hvac_action(self)
def target_temperature(self)
def async_set_humidify_setpoint(self, humidity)
dict[str, str]|None extra_state_attributes(self)
None async_turn_aux_heat_on(self)
def current_humidity(self)
def async_set_hvac_run_mode(self, run_mode, hvac_mode)
None __init__(self, NexiaDataUpdateCoordinator coordinator, NexiaThermostatZone zone)
def current_temperature(self)
None async_set_humidity(self, int humidity)
None async_turn_off(self)
def target_humidity(self)
_attr_target_temperature_step
None async_set_temperature(self, **Any kwargs)
def async_set_dehumidify_setpoint(self, humidity)
None async_set_fan_mode(self, str fan_mode)
def target_temperature_high(self)
None async_set_preset_mode(self, str preset_mode)
def async_set_aircleaner_mode(self, aircleaner_mode)
None _signal_thermostat_update(self)
None _signal_zone_update(self)
float|None get_temperature(MadVRCoordinator coordinator, str key)
None async_setup_entry(HomeAssistant hass, NexiaConfigEntry config_entry, AddEntitiesCallback async_add_entities)
None async_create_issue(HomeAssistant hass, str entry_id)