mirror of
https://github.com/home-assistant/core.git
synced 2025-07-14 08:47:10 +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),
|
0x04: (HVAC_MODE_OFF, HVAC_MODE_HEAT_COOL, HVAC_MODE_COOL, HVAC_MODE_HEAT),
|
||||||
# cooling and heating 4-pipes
|
# cooling and heating 4-pipes
|
||||||
0x05: (HVAC_MODE_OFF, HVAC_MODE_HEAT_COOL, HVAC_MODE_COOL, HVAC_MODE_HEAT),
|
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
|
self._thrm.pi_heating_demand is None
|
||||||
and self._thrm.pi_cooling_demand is None
|
and self._thrm.pi_cooling_demand is None
|
||||||
):
|
):
|
||||||
running_state = self._thrm.running_state
|
return self._rm_rs_action
|
||||||
if running_state is None:
|
return self._pi_demand_action
|
||||||
return None
|
|
||||||
if running_state & (RunningState.HEAT | RunningState.HEAT_STAGE_2):
|
@property
|
||||||
return CURRENT_HVAC_HEAT
|
def _rm_rs_action(self) -> Optional[str]:
|
||||||
if running_state & (RunningState.COOL | RunningState.COOL_STAGE_2):
|
"""Return the current HVAC action based on running mode and running state."""
|
||||||
return CURRENT_HVAC_COOL
|
|
||||||
if running_state & (
|
running_mode = self._thrm.running_mode
|
||||||
RunningState.FAN | RunningState.FAN_STAGE_2 | RunningState.FAN_STAGE_3
|
if running_mode == SystemMode.HEAT:
|
||||||
):
|
return CURRENT_HVAC_HEAT
|
||||||
return CURRENT_HVAC_FAN
|
if running_mode == SystemMode.COOL:
|
||||||
else:
|
return CURRENT_HVAC_COOL
|
||||||
heating_demand = self._thrm.pi_heating_demand
|
|
||||||
if heating_demand is not None and heating_demand > 0:
|
running_state = self._thrm.running_state
|
||||||
return CURRENT_HVAC_HEAT
|
if running_state and running_state & (
|
||||||
cooling_demand = self._thrm.pi_cooling_demand
|
RunningState.FAN | RunningState.FAN_STAGE_2 | RunningState.FAN_STAGE_3
|
||||||
if cooling_demand is not None and cooling_demand > 0:
|
):
|
||||||
return CURRENT_HVAC_COOL
|
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:
|
if self.hvac_mode != HVAC_MODE_OFF:
|
||||||
return CURRENT_HVAC_IDLE
|
return CURRENT_HVAC_IDLE
|
||||||
return CURRENT_HVAC_OFF
|
return CURRENT_HVAC_OFF
|
||||||
@ -389,14 +406,11 @@ class Thermostat(ZhaEntity, ClimateEntity):
|
|||||||
if occupancy is True:
|
if occupancy is True:
|
||||||
self._preset = PRESET_NONE
|
self._preset = PRESET_NONE
|
||||||
|
|
||||||
|
self.debug("Attribute '%s' = %s update", record.attr_name, record.value)
|
||||||
self.async_write_ha_state()
|
self.async_write_ha_state()
|
||||||
|
|
||||||
async def async_set_fan_mode(self, fan_mode: str) -> None:
|
async def async_set_fan_mode(self, fan_mode: str) -> None:
|
||||||
"""Set fan mode."""
|
"""Set fan mode."""
|
||||||
if self.fan_modes is None:
|
|
||||||
self.warning("Fan is not supported")
|
|
||||||
return
|
|
||||||
|
|
||||||
if fan_mode not in self.fan_modes:
|
if fan_mode not in self.fan_modes:
|
||||||
self.warning("Unsupported '%s' fan mode", fan_mode)
|
self.warning("Unsupported '%s' fan mode", fan_mode)
|
||||||
return
|
return
|
||||||
@ -540,3 +554,32 @@ class SinopeTechnologiesThermostat(Thermostat):
|
|||||||
|
|
||||||
self.debug("set occupancy to %s. Status: %s", 0 if is_away else 1, res)
|
self.debug("set occupancy to %s. Status: %s", 0 if is_away else 1, res)
|
||||||
return 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,
|
"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 = {
|
ZCL_ATTR_PLUG = {
|
||||||
"abs_min_heat_setpoint_limit": 800,
|
"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):
|
async def device_climate_sinope(device_climate_mock):
|
||||||
"""Sinope thermostat."""
|
"""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():
|
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
|
thrm_cluster = device_climate.device.endpoints[1].thermostat
|
||||||
entity_id = await find_entity_id(DOMAIN, device_climate, hass)
|
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)
|
state = hass.states.get(entity_id)
|
||||||
assert ATTR_HVAC_ACTION not in state.attributes
|
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)
|
entity_id = await find_entity_id(DOMAIN, device_climate, hass)
|
||||||
|
|
||||||
state = hass.states.get(entity_id)
|
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})
|
await send_attributes_report(hass, thrm_cluster, {0x0007: 10})
|
||||||
state = hass.states.get(entity_id)
|
state = hass.states.get(entity_id)
|
||||||
@ -381,7 +449,7 @@ async def test_target_temperature(
|
|||||||
"unoccupied_heating_setpoint": 1600,
|
"unoccupied_heating_setpoint": 1600,
|
||||||
"unoccupied_cooling_setpoint": 2700,
|
"unoccupied_cooling_setpoint": 2700,
|
||||||
},
|
},
|
||||||
manuf=SINOPE,
|
manuf=MANUF_SINOPE,
|
||||||
)
|
)
|
||||||
entity_id = await find_entity_id(DOMAIN, device_climate, hass)
|
entity_id = await find_entity_id(DOMAIN, device_climate, hass)
|
||||||
if preset:
|
if preset:
|
||||||
@ -417,7 +485,7 @@ async def test_target_temperature_high(
|
|||||||
"system_mode": Thermostat.SystemMode.Auto,
|
"system_mode": Thermostat.SystemMode.Auto,
|
||||||
"unoccupied_cooling_setpoint": unoccupied,
|
"unoccupied_cooling_setpoint": unoccupied,
|
||||||
},
|
},
|
||||||
manuf=SINOPE,
|
manuf=MANUF_SINOPE,
|
||||||
)
|
)
|
||||||
entity_id = await find_entity_id(DOMAIN, device_climate, hass)
|
entity_id = await find_entity_id(DOMAIN, device_climate, hass)
|
||||||
if preset:
|
if preset:
|
||||||
@ -453,7 +521,7 @@ async def test_target_temperature_low(
|
|||||||
"system_mode": Thermostat.SystemMode.Auto,
|
"system_mode": Thermostat.SystemMode.Auto,
|
||||||
"unoccupied_heating_setpoint": unoccupied,
|
"unoccupied_heating_setpoint": unoccupied,
|
||||||
},
|
},
|
||||||
manuf=SINOPE,
|
manuf=MANUF_SINOPE,
|
||||||
)
|
)
|
||||||
entity_id = await find_entity_id(DOMAIN, device_climate, hass)
|
entity_id = await find_entity_id(DOMAIN, device_climate, hass)
|
||||||
if preset:
|
if preset:
|
||||||
@ -665,7 +733,7 @@ async def test_set_temperature_heat_cool(hass, device_climate_mock):
|
|||||||
"unoccupied_heating_setpoint": 1600,
|
"unoccupied_heating_setpoint": 1600,
|
||||||
"unoccupied_cooling_setpoint": 2700,
|
"unoccupied_cooling_setpoint": 2700,
|
||||||
},
|
},
|
||||||
manuf=SINOPE,
|
manuf=MANUF_SINOPE,
|
||||||
)
|
)
|
||||||
entity_id = await find_entity_id(DOMAIN, device_climate, hass)
|
entity_id = await find_entity_id(DOMAIN, device_climate, hass)
|
||||||
thrm_cluster = device_climate.device.endpoints[1].thermostat
|
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_heating_setpoint": 1600,
|
||||||
"unoccupied_cooling_setpoint": 2700,
|
"unoccupied_cooling_setpoint": 2700,
|
||||||
},
|
},
|
||||||
manuf=SINOPE,
|
manuf=MANUF_SINOPE,
|
||||||
)
|
)
|
||||||
entity_id = await find_entity_id(DOMAIN, device_climate, hass)
|
entity_id = await find_entity_id(DOMAIN, device_climate, hass)
|
||||||
thrm_cluster = device_climate.device.endpoints[1].thermostat
|
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_cooling_setpoint": 1600,
|
||||||
"unoccupied_heating_setpoint": 2700,
|
"unoccupied_heating_setpoint": 2700,
|
||||||
},
|
},
|
||||||
manuf=SINOPE,
|
manuf=MANUF_SINOPE,
|
||||||
)
|
)
|
||||||
entity_id = await find_entity_id(DOMAIN, device_climate, hass)
|
entity_id = await find_entity_id(DOMAIN, device_climate, hass)
|
||||||
thrm_cluster = device_climate.device.endpoints[1].thermostat
|
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_cooling_setpoint": 1600,
|
||||||
"unoccupied_heating_setpoint": 2700,
|
"unoccupied_heating_setpoint": 2700,
|
||||||
},
|
},
|
||||||
manuf=SINOPE,
|
manuf=MANUF_SINOPE,
|
||||||
)
|
)
|
||||||
entity_id = await find_entity_id(DOMAIN, device_climate, hass)
|
entity_id = await find_entity_id(DOMAIN, device_climate, hass)
|
||||||
thrm_cluster = device_climate.device.endpoints[1].thermostat
|
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
|
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):
|
async def test_set_fan_mode_not_supported(hass, device_climate_fan):
|
||||||
"""Test fan setting unsupported mode."""
|
"""Test fan setting unsupported mode."""
|
||||||
|
|
||||||
|
@ -3349,7 +3349,7 @@ DEVICES = [
|
|||||||
},
|
},
|
||||||
("climate", "00:11:22:33:44:55:66:77-1"): {
|
("climate", "00:11:22:33:44:55:66:77-1"): {
|
||||||
"channels": ["thermostat", "fan"],
|
"channels": ["thermostat", "fan"],
|
||||||
"entity_class": "Thermostat",
|
"entity_class": "ZenWithinThermostat",
|
||||||
"entity_id": "climate.zen_within_zen_01_77665544_fan_thermostat",
|
"entity_id": "climate.zen_within_zen_01_77665544_fan_thermostat",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
Loading…
x
Reference in New Issue
Block a user