Correctly support humidification and dehumidification in Nexia Thermostats (#139792)

* Add set_dehumidify_setpoint service. Refactor set_humidify_setpoint.

* Add closest_value function in utils

* Refactor target humidity

* Update tests for util.py

* Refactor target humidity. Update tests.

* Remove duplicate constant

* Add humidify and dehumidfy sensors

* Update sensor names

* Remove clamping and commented code

* Iplement suggestions from review

* Switch order check order

* remove closest_value()

* Update strings for clarity/grammar

* Update strings for grammar/clarity

* tweaks

---------

Co-authored-by: J. Nick Koston <nick@koston.org>
This commit is contained in:
currand 2025-04-02 16:29:40 -04:00 committed by GitHub
parent 2876e5d0cd
commit 691cb378a0
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 111 additions and 11 deletions

View File

@ -53,13 +53,18 @@ PARALLEL_UPDATES = 1 # keep data in sync with only one connection at a time
SERVICE_SET_AIRCLEANER_MODE = "set_aircleaner_mode"
SERVICE_SET_HUMIDIFY_SETPOINT = "set_humidify_setpoint"
SERVICE_SET_DEHUMIDIFY_SETPOINT = "set_dehumidify_setpoint"
SERVICE_SET_HVAC_RUN_MODE = "set_hvac_run_mode"
SET_AIRCLEANER_SCHEMA: VolDictType = {
vol.Required(ATTR_AIRCLEANER_MODE): cv.string,
}
SET_HUMIDITY_SCHEMA: VolDictType = {
SET_HUMIDIFY_SCHEMA: VolDictType = {
vol.Required(ATTR_HUMIDITY): vol.All(vol.Coerce(int), vol.Range(min=10, max=45)),
}
SET_DEHUMIDIFY_SCHEMA: VolDictType = {
vol.Required(ATTR_HUMIDITY): vol.All(vol.Coerce(int), vol.Range(min=35, max=65)),
}
@ -126,9 +131,14 @@ async def async_setup_entry(
platform.async_register_entity_service(
SERVICE_SET_HUMIDIFY_SETPOINT,
SET_HUMIDITY_SCHEMA,
SET_HUMIDIFY_SCHEMA,
f"async_{SERVICE_SET_HUMIDIFY_SETPOINT}",
)
platform.async_register_entity_service(
SERVICE_SET_DEHUMIDIFY_SETPOINT,
SET_DEHUMIDIFY_SCHEMA,
f"async_{SERVICE_SET_DEHUMIDIFY_SETPOINT}",
)
platform.async_register_entity_service(
SERVICE_SET_AIRCLEANER_MODE,
SET_AIRCLEANER_SCHEMA,
@ -224,20 +234,48 @@ class NexiaZone(NexiaThermostatZoneEntity, ClimateEntity):
return self._zone.get_preset()
async def async_set_humidity(self, humidity: int) -> None:
"""Dehumidify target."""
if self._thermostat.has_dehumidify_support():
await self.async_set_dehumidify_setpoint(humidity)
"""Set humidity targets.
HA doesn't support separate humidify and dehumidify targets.
Set the target for the current mode if in [heat, cool]
otherwise set both targets to the clamped values.
"""
zone_current_mode = self._zone.get_current_mode()
if zone_current_mode == OPERATION_MODE_HEAT:
if self._thermostat.has_humidify_support():
await self.async_set_humidify_setpoint(humidity)
elif zone_current_mode == OPERATION_MODE_COOL:
if self._thermostat.has_dehumidify_support():
await self.async_set_dehumidify_setpoint(humidity)
else:
await self.async_set_humidify_setpoint(humidity)
if self._thermostat.has_humidify_support():
await self.async_set_humidify_setpoint(humidity)
if self._thermostat.has_dehumidify_support():
await self.async_set_dehumidify_setpoint(humidity)
self._signal_thermostat_update()
@property
def target_humidity(self):
"""Humidity indoors setpoint."""
def target_humidity(self) -> float | None:
"""Humidity indoors setpoint.
In systems that support both humidification and dehumidification,
two values for target exist. We must choose one to return.
:return: The target humidity setpoint.
"""
# If heat is on, always return humidify value first
if (
self._has_humidify_support
and self._zone.get_current_mode() == OPERATION_MODE_HEAT
):
return percent_conv(self._thermostat.get_humidify_setpoint())
# Fall back to previous behavior of returning dehumidify value then humidify
if self._has_dehumidify_support:
return percent_conv(self._thermostat.get_dehumidify_setpoint())
if self._has_humidify_support:
return percent_conv(self._thermostat.get_humidify_setpoint())
return None
@property

View File

@ -26,6 +26,9 @@
"set_humidify_setpoint": {
"service": "mdi:water-percent"
},
"set_dehumidify_setpoint": {
"service": "mdi:water-percent"
},
"set_hvac_run_mode": {
"service": "mdi:hvac"
}

View File

@ -114,6 +114,35 @@ async def async_setup_entry(
percent_conv,
)
)
# Heating Humidification Setpoint
if thermostat.has_humidify_support():
entities.append(
NexiaThermostatSensor(
coordinator,
thermostat,
"get_humidify_setpoint",
"get_humidify_setpoint",
SensorDeviceClass.HUMIDITY,
PERCENTAGE,
SensorStateClass.MEASUREMENT,
percent_conv,
)
)
# Cooling Dehumidification Setpoint
if thermostat.has_dehumidify_support():
entities.append(
NexiaThermostatSensor(
coordinator,
thermostat,
"get_dehumidify_setpoint",
"get_dehumidify_setpoint",
SensorDeviceClass.HUMIDITY,
PERCENTAGE,
SensorStateClass.MEASUREMENT,
percent_conv,
)
)
# Zone Sensors
for zone_id in thermostat.get_zone_ids():

View File

@ -14,6 +14,20 @@ set_aircleaner_mode:
- "quick"
set_humidify_setpoint:
target:
entity:
integration: nexia
domain: climate
fields:
humidity:
required: true
selector:
number:
min: 10
max: 45
unit_of_measurement: "%"
set_dehumidify_setpoint:
target:
entity:
integration: nexia

View File

@ -53,6 +53,12 @@
},
"zone_setpoint_status": {
"name": "Zone setpoint status"
},
"get_humidify_setpoint": {
"name": "Heating humidify setpoint"
},
"get_dehumidify_setpoint": {
"name": "Cooling dehumidify setpoint"
}
},
"switch": {
@ -76,12 +82,22 @@
}
},
"set_humidify_setpoint": {
"name": "Set humidify set point",
"description": "Sets the target humidity.",
"name": "Set humidify setpoint",
"description": "Sets the target humidity for heating.",
"fields": {
"humidity": {
"name": "Humidity",
"description": "The humidification setpoint."
"description": "The setpoint for humidification when heating."
}
}
},
"set_dehumidify_setpoint": {
"name": "Set dehumidify setpoint",
"description": "Sets the target humidity for cooling.",
"fields": {
"humidity": {
"name": "Humidity",
"description": "The setpoint for dehumidification when cooling."
}
}
},