mirror of
https://github.com/home-assistant/core.git
synced 2025-07-13 16:27:08 +00:00
Fix ZHA climate hvac_action for Centralite thermostat (#35993)
* Centralite specific control seq of operation * Remove Fan safeguards * Split hvac_action property. * Refactor hvac_action property. Current hvac_action logic is Zen Within thermostat specific and differs a bit from ZCL specs. Implement it as a separate class. * Refactor hvac_action property for default thermostat Follow more closely ZCL specs in parsing hvac state of the thermostat.
This commit is contained in:
parent
3d253fa52a
commit
04cfd36d06
@ -103,6 +103,8 @@ SEQ_OF_OPERATION = {
|
||||
0x04: (HVAC_MODE_OFF, HVAC_MODE_HEAT_COOL, HVAC_MODE_COOL, HVAC_MODE_HEAT),
|
||||
# cooling and heating 4-pipes
|
||||
0x05: (HVAC_MODE_OFF, HVAC_MODE_HEAT_COOL, HVAC_MODE_COOL, HVAC_MODE_HEAT),
|
||||
0x06: (HVAC_MODE_COOL, HVAC_MODE_HEAT, HVAC_MODE_OFF), # centralite specific
|
||||
0x07: (HVAC_MODE_HEAT_COOL, HVAC_MODE_OFF), # centralite specific
|
||||
}
|
||||
|
||||
|
||||
@ -234,24 +236,39 @@ class Thermostat(ZhaEntity, ClimateEntity):
|
||||
self._thrm.pi_heating_demand is None
|
||||
and self._thrm.pi_cooling_demand is None
|
||||
):
|
||||
running_state = self._thrm.running_state
|
||||
if running_state is None:
|
||||
return None
|
||||
if running_state & (RunningState.HEAT | RunningState.HEAT_STAGE_2):
|
||||
return CURRENT_HVAC_HEAT
|
||||
if running_state & (RunningState.COOL | RunningState.COOL_STAGE_2):
|
||||
return CURRENT_HVAC_COOL
|
||||
if running_state & (
|
||||
RunningState.FAN | RunningState.FAN_STAGE_2 | RunningState.FAN_STAGE_3
|
||||
):
|
||||
return CURRENT_HVAC_FAN
|
||||
else:
|
||||
heating_demand = self._thrm.pi_heating_demand
|
||||
if heating_demand is not None and heating_demand > 0:
|
||||
return CURRENT_HVAC_HEAT
|
||||
cooling_demand = self._thrm.pi_cooling_demand
|
||||
if cooling_demand is not None and cooling_demand > 0:
|
||||
return CURRENT_HVAC_COOL
|
||||
return self._rm_rs_action
|
||||
return self._pi_demand_action
|
||||
|
||||
@property
|
||||
def _rm_rs_action(self) -> Optional[str]:
|
||||
"""Return the current HVAC action based on running mode and running state."""
|
||||
|
||||
running_mode = self._thrm.running_mode
|
||||
if running_mode == SystemMode.HEAT:
|
||||
return CURRENT_HVAC_HEAT
|
||||
if running_mode == SystemMode.COOL:
|
||||
return CURRENT_HVAC_COOL
|
||||
|
||||
running_state = self._thrm.running_state
|
||||
if running_state and running_state & (
|
||||
RunningState.FAN | RunningState.FAN_STAGE_2 | RunningState.FAN_STAGE_3
|
||||
):
|
||||
return CURRENT_HVAC_FAN
|
||||
if self.hvac_mode != HVAC_MODE_OFF and running_mode == SystemMode.OFF:
|
||||
return CURRENT_HVAC_IDLE
|
||||
return CURRENT_HVAC_OFF
|
||||
|
||||
@property
|
||||
def _pi_demand_action(self) -> Optional[str]:
|
||||
"""Return the current HVAC action based on pi_demands."""
|
||||
|
||||
heating_demand = self._thrm.pi_heating_demand
|
||||
if heating_demand is not None and heating_demand > 0:
|
||||
return CURRENT_HVAC_HEAT
|
||||
cooling_demand = self._thrm.pi_cooling_demand
|
||||
if cooling_demand is not None and cooling_demand > 0:
|
||||
return CURRENT_HVAC_COOL
|
||||
|
||||
if self.hvac_mode != HVAC_MODE_OFF:
|
||||
return CURRENT_HVAC_IDLE
|
||||
return CURRENT_HVAC_OFF
|
||||
@ -389,14 +406,11 @@ class Thermostat(ZhaEntity, ClimateEntity):
|
||||
if occupancy is True:
|
||||
self._preset = PRESET_NONE
|
||||
|
||||
self.debug("Attribute '%s' = %s update", record.attr_name, record.value)
|
||||
self.async_write_ha_state()
|
||||
|
||||
async def async_set_fan_mode(self, fan_mode: str) -> None:
|
||||
"""Set fan mode."""
|
||||
if self.fan_modes is None:
|
||||
self.warning("Fan is not supported")
|
||||
return
|
||||
|
||||
if fan_mode not in self.fan_modes:
|
||||
self.warning("Unsupported '%s' fan mode", fan_mode)
|
||||
return
|
||||
@ -540,3 +554,32 @@ class SinopeTechnologiesThermostat(Thermostat):
|
||||
|
||||
self.debug("set occupancy to %s. Status: %s", 0 if is_away else 1, res)
|
||||
return res
|
||||
|
||||
|
||||
@STRICT_MATCH(
|
||||
channel_names=CHANNEL_THERMOSTAT,
|
||||
aux_channels=CHANNEL_FAN,
|
||||
manufacturers="Zen Within",
|
||||
)
|
||||
class ZenWithinThermostat(Thermostat):
|
||||
"""Zen Within Thermostat implementation."""
|
||||
|
||||
@property
|
||||
def _rm_rs_action(self) -> Optional[str]:
|
||||
"""Return the current HVAC action based on running mode and running state."""
|
||||
|
||||
running_state = self._thrm.running_state
|
||||
if running_state is None:
|
||||
return None
|
||||
if running_state & (RunningState.HEAT | RunningState.HEAT_STAGE_2):
|
||||
return CURRENT_HVAC_HEAT
|
||||
if running_state & (RunningState.COOL | RunningState.COOL_STAGE_2):
|
||||
return CURRENT_HVAC_COOL
|
||||
if running_state & (
|
||||
RunningState.FAN | RunningState.FAN_STAGE_2 | RunningState.FAN_STAGE_3
|
||||
):
|
||||
return CURRENT_HVAC_FAN
|
||||
|
||||
if self.hvac_mode != HVAC_MODE_OFF:
|
||||
return CURRENT_HVAC_IDLE
|
||||
return CURRENT_HVAC_OFF
|
||||
|
@ -89,7 +89,22 @@ CLIMATE_SINOPE = {
|
||||
"profile_id": 260,
|
||||
},
|
||||
}
|
||||
SINOPE = "Sinope Technologies"
|
||||
|
||||
CLIMATE_ZEN = {
|
||||
1: {
|
||||
"device_type": zigpy.profiles.zha.DeviceType.THERMOSTAT,
|
||||
"in_clusters": [
|
||||
zigpy.zcl.clusters.general.Basic.cluster_id,
|
||||
zigpy.zcl.clusters.general.Identify.cluster_id,
|
||||
zigpy.zcl.clusters.hvac.Fan.cluster_id,
|
||||
zigpy.zcl.clusters.hvac.Thermostat.cluster_id,
|
||||
zigpy.zcl.clusters.hvac.UserInterface.cluster_id,
|
||||
],
|
||||
"out_clusters": [zigpy.zcl.clusters.general.Ota.cluster_id],
|
||||
}
|
||||
}
|
||||
MANUF_SINOPE = "Sinope Technologies"
|
||||
MANUF_ZEN = "Zen Within"
|
||||
|
||||
ZCL_ATTR_PLUG = {
|
||||
"abs_min_heat_setpoint_limit": 800,
|
||||
@ -169,7 +184,14 @@ async def device_climate_fan(device_climate_mock):
|
||||
async def device_climate_sinope(device_climate_mock):
|
||||
"""Sinope thermostat."""
|
||||
|
||||
return await device_climate_mock(CLIMATE_SINOPE, manuf=SINOPE)
|
||||
return await device_climate_mock(CLIMATE_SINOPE, manuf=MANUF_SINOPE)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
async def device_climate_zen(device_climate_mock):
|
||||
"""Zen Within thermostat."""
|
||||
|
||||
return await device_climate_mock(CLIMATE_ZEN, manuf=MANUF_ZEN)
|
||||
|
||||
|
||||
def test_sequence_mappings():
|
||||
@ -201,6 +223,52 @@ async def test_climate_hvac_action_running_state(hass, device_climate):
|
||||
thrm_cluster = device_climate.device.endpoints[1].thermostat
|
||||
entity_id = await find_entity_id(DOMAIN, device_climate, hass)
|
||||
|
||||
state = hass.states.get(entity_id)
|
||||
assert state.attributes[ATTR_HVAC_ACTION] == CURRENT_HVAC_OFF
|
||||
|
||||
await send_attributes_report(
|
||||
hass, thrm_cluster, {0x001E: Thermostat.RunningMode.Off}
|
||||
)
|
||||
state = hass.states.get(entity_id)
|
||||
assert state.attributes[ATTR_HVAC_ACTION] == CURRENT_HVAC_OFF
|
||||
|
||||
await send_attributes_report(
|
||||
hass, thrm_cluster, {0x001C: Thermostat.SystemMode.Auto}
|
||||
)
|
||||
state = hass.states.get(entity_id)
|
||||
assert state.attributes[ATTR_HVAC_ACTION] == CURRENT_HVAC_IDLE
|
||||
|
||||
await send_attributes_report(
|
||||
hass, thrm_cluster, {0x001E: Thermostat.RunningMode.Cool}
|
||||
)
|
||||
state = hass.states.get(entity_id)
|
||||
assert state.attributes[ATTR_HVAC_ACTION] == CURRENT_HVAC_COOL
|
||||
|
||||
await send_attributes_report(
|
||||
hass, thrm_cluster, {0x001E: Thermostat.RunningMode.Heat}
|
||||
)
|
||||
state = hass.states.get(entity_id)
|
||||
assert state.attributes[ATTR_HVAC_ACTION] == CURRENT_HVAC_HEAT
|
||||
|
||||
await send_attributes_report(
|
||||
hass, thrm_cluster, {0x001E: Thermostat.RunningMode.Off}
|
||||
)
|
||||
state = hass.states.get(entity_id)
|
||||
assert state.attributes[ATTR_HVAC_ACTION] == CURRENT_HVAC_IDLE
|
||||
|
||||
await send_attributes_report(
|
||||
hass, thrm_cluster, {0x0029: Thermostat.RunningState.Fan_State_On}
|
||||
)
|
||||
state = hass.states.get(entity_id)
|
||||
assert state.attributes[ATTR_HVAC_ACTION] == CURRENT_HVAC_FAN
|
||||
|
||||
|
||||
async def test_climate_hvac_action_running_state_zen(hass, device_climate_zen):
|
||||
"""Test Zen hvac action via running state."""
|
||||
|
||||
thrm_cluster = device_climate_zen.device.endpoints[1].thermostat
|
||||
entity_id = await find_entity_id(DOMAIN, device_climate_zen, hass)
|
||||
|
||||
state = hass.states.get(entity_id)
|
||||
assert ATTR_HVAC_ACTION not in state.attributes
|
||||
|
||||
@ -266,7 +334,7 @@ async def test_climate_hvac_action_pi_demand(hass, device_climate):
|
||||
entity_id = await find_entity_id(DOMAIN, device_climate, hass)
|
||||
|
||||
state = hass.states.get(entity_id)
|
||||
assert ATTR_HVAC_ACTION not in state.attributes
|
||||
assert state.attributes[ATTR_HVAC_ACTION] == CURRENT_HVAC_OFF
|
||||
|
||||
await send_attributes_report(hass, thrm_cluster, {0x0007: 10})
|
||||
state = hass.states.get(entity_id)
|
||||
@ -381,7 +449,7 @@ async def test_target_temperature(
|
||||
"unoccupied_heating_setpoint": 1600,
|
||||
"unoccupied_cooling_setpoint": 2700,
|
||||
},
|
||||
manuf=SINOPE,
|
||||
manuf=MANUF_SINOPE,
|
||||
)
|
||||
entity_id = await find_entity_id(DOMAIN, device_climate, hass)
|
||||
if preset:
|
||||
@ -417,7 +485,7 @@ async def test_target_temperature_high(
|
||||
"system_mode": Thermostat.SystemMode.Auto,
|
||||
"unoccupied_cooling_setpoint": unoccupied,
|
||||
},
|
||||
manuf=SINOPE,
|
||||
manuf=MANUF_SINOPE,
|
||||
)
|
||||
entity_id = await find_entity_id(DOMAIN, device_climate, hass)
|
||||
if preset:
|
||||
@ -453,7 +521,7 @@ async def test_target_temperature_low(
|
||||
"system_mode": Thermostat.SystemMode.Auto,
|
||||
"unoccupied_heating_setpoint": unoccupied,
|
||||
},
|
||||
manuf=SINOPE,
|
||||
manuf=MANUF_SINOPE,
|
||||
)
|
||||
entity_id = await find_entity_id(DOMAIN, device_climate, hass)
|
||||
if preset:
|
||||
@ -665,7 +733,7 @@ async def test_set_temperature_heat_cool(hass, device_climate_mock):
|
||||
"unoccupied_heating_setpoint": 1600,
|
||||
"unoccupied_cooling_setpoint": 2700,
|
||||
},
|
||||
manuf=SINOPE,
|
||||
manuf=MANUF_SINOPE,
|
||||
)
|
||||
entity_id = await find_entity_id(DOMAIN, device_climate, hass)
|
||||
thrm_cluster = device_climate.device.endpoints[1].thermostat
|
||||
@ -755,7 +823,7 @@ async def test_set_temperature_heat(hass, device_climate_mock):
|
||||
"unoccupied_heating_setpoint": 1600,
|
||||
"unoccupied_cooling_setpoint": 2700,
|
||||
},
|
||||
manuf=SINOPE,
|
||||
manuf=MANUF_SINOPE,
|
||||
)
|
||||
entity_id = await find_entity_id(DOMAIN, device_climate, hass)
|
||||
thrm_cluster = device_climate.device.endpoints[1].thermostat
|
||||
@ -838,7 +906,7 @@ async def test_set_temperature_cool(hass, device_climate_mock):
|
||||
"unoccupied_cooling_setpoint": 1600,
|
||||
"unoccupied_heating_setpoint": 2700,
|
||||
},
|
||||
manuf=SINOPE,
|
||||
manuf=MANUF_SINOPE,
|
||||
)
|
||||
entity_id = await find_entity_id(DOMAIN, device_climate, hass)
|
||||
thrm_cluster = device_climate.device.endpoints[1].thermostat
|
||||
@ -921,7 +989,7 @@ async def test_set_temperature_wrong_mode(hass, device_climate_mock):
|
||||
"unoccupied_cooling_setpoint": 1600,
|
||||
"unoccupied_heating_setpoint": 2700,
|
||||
},
|
||||
manuf=SINOPE,
|
||||
manuf=MANUF_SINOPE,
|
||||
)
|
||||
entity_id = await find_entity_id(DOMAIN, device_climate, hass)
|
||||
thrm_cluster = device_climate.device.endpoints[1].thermostat
|
||||
@ -1000,19 +1068,6 @@ async def test_fan_mode(hass, device_climate_fan):
|
||||
assert state.attributes[ATTR_FAN_MODE] == FAN_ON
|
||||
|
||||
|
||||
async def test_set_fan_mode_no_fan(hass, device_climate):
|
||||
"""Test setting fan mode on fun less climate."""
|
||||
|
||||
entity_id = await find_entity_id(DOMAIN, device_climate, hass)
|
||||
|
||||
await hass.services.async_call(
|
||||
DOMAIN,
|
||||
SERVICE_SET_FAN_MODE,
|
||||
{ATTR_ENTITY_ID: entity_id, ATTR_FAN_MODE: FAN_ON},
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
|
||||
async def test_set_fan_mode_not_supported(hass, device_climate_fan):
|
||||
"""Test fan setting unsupported mode."""
|
||||
|
||||
|
@ -3349,7 +3349,7 @@ DEVICES = [
|
||||
},
|
||||
("climate", "00:11:22:33:44:55:66:77-1"): {
|
||||
"channels": ["thermostat", "fan"],
|
||||
"entity_class": "Thermostat",
|
||||
"entity_class": "ZenWithinThermostat",
|
||||
"entity_id": "climate.zen_within_zen_01_77665544_fan_thermostat",
|
||||
},
|
||||
},
|
||||
|
Loading…
x
Reference in New Issue
Block a user