Add preset mode to Comelit climate (#145195)

This commit is contained in:
Simone Chemelli 2025-05-25 00:37:21 +03:00 committed by GitHub
parent ce02a5544d
commit 526a8ee31f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 162 additions and 27 deletions

View File

@ -20,7 +20,12 @@ from homeassistant.core import HomeAssistant, callback
from homeassistant.exceptions import HomeAssistantError from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from .const import DOMAIN from .const import (
DOMAIN,
PRESET_MODE_AUTO,
PRESET_MODE_AUTO_TARGET_TEMP,
PRESET_MODE_MANUAL,
)
from .coordinator import ComelitConfigEntry, ComelitSerialBridge from .coordinator import ComelitConfigEntry, ComelitSerialBridge
from .entity import ComelitBridgeBaseEntity from .entity import ComelitBridgeBaseEntity
from .utils import bridge_api_call from .utils import bridge_api_call
@ -41,11 +46,13 @@ class ClimaComelitMode(StrEnum):
class ClimaComelitCommand(StrEnum): class ClimaComelitCommand(StrEnum):
"""Serial Bridge clima commands.""" """Serial Bridge clima commands."""
AUTO = "auto"
MANUAL = "man"
OFF = "off" OFF = "off"
ON = "on" ON = "on"
MANUAL = "man"
SET = "set" SET = "set"
AUTO = "auto" SNOW = "lower"
SUN = "upper"
class ClimaComelitApiStatus(TypedDict): class ClimaComelitApiStatus(TypedDict):
@ -67,11 +74,15 @@ API_STATUS: dict[str, ClimaComelitApiStatus] = {
), ),
} }
MODE_TO_ACTION: dict[HVACMode, ClimaComelitCommand] = { HVACMODE_TO_ACTION: dict[HVACMode, ClimaComelitCommand] = {
HVACMode.OFF: ClimaComelitCommand.OFF, HVACMode.OFF: ClimaComelitCommand.OFF,
HVACMode.AUTO: ClimaComelitCommand.AUTO, HVACMode.COOL: ClimaComelitCommand.SNOW,
HVACMode.COOL: ClimaComelitCommand.MANUAL, HVACMode.HEAT: ClimaComelitCommand.SUN,
HVACMode.HEAT: ClimaComelitCommand.MANUAL, }
PRESET_MODE_TO_ACTION: dict[str, ClimaComelitCommand] = {
PRESET_MODE_MANUAL: ClimaComelitCommand.MANUAL,
PRESET_MODE_AUTO: ClimaComelitCommand.AUTO,
} }
@ -93,17 +104,20 @@ async def async_setup_entry(
class ComelitClimateEntity(ComelitBridgeBaseEntity, ClimateEntity): class ComelitClimateEntity(ComelitBridgeBaseEntity, ClimateEntity):
"""Climate device.""" """Climate device."""
_attr_hvac_modes = [HVACMode.AUTO, HVACMode.COOL, HVACMode.HEAT, HVACMode.OFF] _attr_hvac_modes = [HVACMode.COOL, HVACMode.HEAT, HVACMode.OFF]
_attr_preset_modes = [PRESET_MODE_AUTO, PRESET_MODE_MANUAL]
_attr_max_temp = 30 _attr_max_temp = 30
_attr_min_temp = 5 _attr_min_temp = 5
_attr_supported_features = ( _attr_supported_features = (
ClimateEntityFeature.TARGET_TEMPERATURE ClimateEntityFeature.TARGET_TEMPERATURE
| ClimateEntityFeature.TURN_OFF | ClimateEntityFeature.TURN_OFF
| ClimateEntityFeature.TURN_ON | ClimateEntityFeature.TURN_ON
| ClimateEntityFeature.PRESET_MODE
) )
_attr_target_temperature_step = PRECISION_TENTHS _attr_target_temperature_step = PRECISION_TENTHS
_attr_temperature_unit = UnitOfTemperature.CELSIUS _attr_temperature_unit = UnitOfTemperature.CELSIUS
_attr_name = None _attr_name = None
_attr_translation_key = "thermostat"
def __init__( def __init__(
self, self,
@ -132,6 +146,8 @@ class ComelitClimateEntity(ComelitBridgeBaseEntity, ClimateEntity):
_mode = values[2] # Values from API: "O", "L", "U" _mode = values[2] # Values from API: "O", "L", "U"
_automatic = values[3] == ClimaComelitMode.AUTO _automatic = values[3] == ClimaComelitMode.AUTO
self._attr_preset_mode = PRESET_MODE_AUTO if _automatic else PRESET_MODE_MANUAL
self._attr_current_temperature = values[0] / 10 self._attr_current_temperature = values[0] / 10
self._attr_hvac_action = None self._attr_hvac_action = None
@ -141,10 +157,6 @@ class ComelitClimateEntity(ComelitBridgeBaseEntity, ClimateEntity):
self._attr_hvac_action = API_STATUS[_mode]["hvac_action"] self._attr_hvac_action = API_STATUS[_mode]["hvac_action"]
self._attr_hvac_mode = None self._attr_hvac_mode = None
if _mode == ClimaComelitMode.OFF:
self._attr_hvac_mode = HVACMode.OFF
if _automatic:
self._attr_hvac_mode = HVACMode.AUTO
if _mode in API_STATUS: if _mode in API_STATUS:
self._attr_hvac_mode = API_STATUS[_mode]["hvac_mode"] self._attr_hvac_mode = API_STATUS[_mode]["hvac_mode"]
@ -160,13 +172,12 @@ class ComelitClimateEntity(ComelitBridgeBaseEntity, ClimateEntity):
async def async_set_temperature(self, **kwargs: Any) -> None: async def async_set_temperature(self, **kwargs: Any) -> None:
"""Set new target temperature.""" """Set new target temperature."""
if ( if (
target_temp := kwargs.get(ATTR_TEMPERATURE) (target_temp := kwargs.get(ATTR_TEMPERATURE)) is None
) is None or self.hvac_mode == HVACMode.OFF: or self.hvac_mode == HVACMode.OFF
or self._attr_preset_mode == PRESET_MODE_AUTO
):
return return
await self.coordinator.api.set_clima_status(
self._device.index, ClimaComelitCommand.MANUAL
)
await self.coordinator.api.set_clima_status( await self.coordinator.api.set_clima_status(
self._device.index, ClimaComelitCommand.SET, target_temp self._device.index, ClimaComelitCommand.SET, target_temp
) )
@ -177,12 +188,28 @@ class ComelitClimateEntity(ComelitBridgeBaseEntity, ClimateEntity):
async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None: async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None:
"""Set hvac mode.""" """Set hvac mode."""
if hvac_mode != HVACMode.OFF: if self._attr_hvac_mode == HVACMode.OFF:
await self.coordinator.api.set_clima_status( await self.coordinator.api.set_clima_status(
self._device.index, ClimaComelitCommand.ON self._device.index, ClimaComelitCommand.ON
) )
await self.coordinator.api.set_clima_status( await self.coordinator.api.set_clima_status(
self._device.index, MODE_TO_ACTION[hvac_mode] self._device.index, HVACMODE_TO_ACTION[hvac_mode]
) )
self._attr_hvac_mode = hvac_mode self._attr_hvac_mode = hvac_mode
self.async_write_ha_state() self.async_write_ha_state()
async def async_set_preset_mode(self, preset_mode: str) -> None:
"""Set new target preset mode."""
if self._attr_hvac_mode == HVACMode.OFF:
return
await self.coordinator.api.set_clima_status(
self._device.index, PRESET_MODE_TO_ACTION[preset_mode]
)
self._attr_preset_mode = preset_mode
if preset_mode == PRESET_MODE_AUTO:
self._attr_target_temperature = PRESET_MODE_AUTO_TARGET_TEMP
self.async_write_ha_state()

View File

@ -11,3 +11,8 @@ DEFAULT_PORT = 80
DEVICE_TYPE_LIST = [BRIDGE, VEDO] DEVICE_TYPE_LIST = [BRIDGE, VEDO]
SCAN_INTERVAL = 5 SCAN_INTERVAL = 5
PRESET_MODE_AUTO = "automatic"
PRESET_MODE_MANUAL = "manual"
PRESET_MODE_AUTO_TARGET_TEMP = 20

View File

@ -4,6 +4,18 @@
"zone_status": { "zone_status": {
"default": "mdi:shield-check" "default": "mdi:shield-check"
} }
},
"climate": {
"thermostat": {
"state_attributes": {
"preset_mode": {
"state": {
"automatic": "mdi:refresh-auto",
"manual": "mdi:alpha-m"
}
}
}
}
} }
} }
} }

View File

@ -74,6 +74,18 @@
"dehumidifier": { "dehumidifier": {
"name": "Dehumidifier" "name": "Dehumidifier"
} }
},
"climate": {
"thermostat": {
"state_attributes": {
"preset_mode": {
"state": {
"automatic": "[%key:common::state::auto%]",
"manual": "[%key:common::state::manual%]"
}
}
}
}
} }
}, },
"exceptions": { "exceptions": {

View File

@ -6,13 +6,16 @@
'area_id': None, 'area_id': None,
'capabilities': dict({ 'capabilities': dict({
'hvac_modes': list([ 'hvac_modes': list([
<HVACMode.AUTO: 'auto'>,
<HVACMode.COOL: 'cool'>, <HVACMode.COOL: 'cool'>,
<HVACMode.HEAT: 'heat'>, <HVACMode.HEAT: 'heat'>,
<HVACMode.OFF: 'off'>, <HVACMode.OFF: 'off'>,
]), ]),
'max_temp': 30, 'max_temp': 30,
'min_temp': 5, 'min_temp': 5,
'preset_modes': list([
'automatic',
'manual',
]),
'target_temp_step': 0.1, 'target_temp_step': 0.1,
}), }),
'config_entry_id': <ANY>, 'config_entry_id': <ANY>,
@ -37,8 +40,8 @@
'original_name': None, 'original_name': None,
'platform': 'comelit', 'platform': 'comelit',
'previous_unique_id': None, 'previous_unique_id': None,
'supported_features': <ClimateEntityFeature: 385>, 'supported_features': <ClimateEntityFeature: 401>,
'translation_key': None, 'translation_key': 'thermostat',
'unique_id': 'serial_bridge_config_entry_id-0', 'unique_id': 'serial_bridge_config_entry_id-0',
'unit_of_measurement': None, 'unit_of_measurement': None,
}) })
@ -50,14 +53,18 @@
'friendly_name': 'Climate0', 'friendly_name': 'Climate0',
'hvac_action': <HVACAction.IDLE: 'idle'>, 'hvac_action': <HVACAction.IDLE: 'idle'>,
'hvac_modes': list([ 'hvac_modes': list([
<HVACMode.AUTO: 'auto'>,
<HVACMode.COOL: 'cool'>, <HVACMode.COOL: 'cool'>,
<HVACMode.HEAT: 'heat'>, <HVACMode.HEAT: 'heat'>,
<HVACMode.OFF: 'off'>, <HVACMode.OFF: 'off'>,
]), ]),
'max_temp': 30, 'max_temp': 30,
'min_temp': 5, 'min_temp': 5,
'supported_features': <ClimateEntityFeature: 385>, 'preset_mode': 'manual',
'preset_modes': list([
'automatic',
'manual',
]),
'supported_features': <ClimateEntityFeature: 401>,
'target_temp_step': 0.1, 'target_temp_step': 0.1,
'temperature': 5.0, 'temperature': 5.0,
}), }),

View File

@ -11,12 +11,19 @@ from syrupy.assertion import SnapshotAssertion
from homeassistant.components.climate import ( from homeassistant.components.climate import (
ATTR_HVAC_MODE, ATTR_HVAC_MODE,
ATTR_PRESET_MODE,
DOMAIN as CLIMATE_DOMAIN, DOMAIN as CLIMATE_DOMAIN,
SERVICE_SET_HVAC_MODE, SERVICE_SET_HVAC_MODE,
SERVICE_SET_PRESET_MODE,
SERVICE_SET_TEMPERATURE, SERVICE_SET_TEMPERATURE,
SERVICE_TURN_OFF,
HVACMode, HVACMode,
) )
from homeassistant.components.comelit.const import SCAN_INTERVAL from homeassistant.components.comelit.const import (
PRESET_MODE_AUTO,
PRESET_MODE_MANUAL,
SCAN_INTERVAL,
)
from homeassistant.const import ATTR_ENTITY_ID, ATTR_TEMPERATURE, Platform from homeassistant.const import ATTR_ENTITY_ID, ATTR_TEMPERATURE, Platform
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.helpers import entity_registry as er from homeassistant.helpers import entity_registry as er
@ -273,10 +280,75 @@ async def test_climate_hvac_mode_when_off(
await hass.services.async_call( await hass.services.async_call(
CLIMATE_DOMAIN, CLIMATE_DOMAIN,
SERVICE_SET_HVAC_MODE, SERVICE_SET_HVAC_MODE,
{ATTR_ENTITY_ID: ENTITY_ID, ATTR_HVAC_MODE: HVACMode.AUTO}, {ATTR_ENTITY_ID: ENTITY_ID, ATTR_HVAC_MODE: HVACMode.COOL},
blocking=True, blocking=True,
) )
mock_serial_bridge.set_clima_status.assert_called() mock_serial_bridge.set_clima_status.assert_called()
assert (state := hass.states.get(ENTITY_ID)) assert (state := hass.states.get(ENTITY_ID))
assert state.state == HVACMode.AUTO assert state.state == HVACMode.COOL
async def test_climate_preset_mode(
hass: HomeAssistant,
mock_serial_bridge: AsyncMock,
mock_serial_bridge_config_entry: MockConfigEntry,
) -> None:
"""Test climate preset mode service."""
await setup_integration(hass, mock_serial_bridge_config_entry)
assert (state := hass.states.get(ENTITY_ID))
assert state.state == HVACMode.HEAT
assert state.attributes[ATTR_TEMPERATURE] == 5.0
assert state.attributes[ATTR_PRESET_MODE] == PRESET_MODE_MANUAL
await hass.services.async_call(
CLIMATE_DOMAIN,
SERVICE_SET_PRESET_MODE,
{ATTR_ENTITY_ID: ENTITY_ID, ATTR_PRESET_MODE: PRESET_MODE_AUTO},
blocking=True,
)
mock_serial_bridge.set_clima_status.assert_called()
assert (state := hass.states.get(ENTITY_ID))
assert state.state == HVACMode.HEAT
assert state.attributes[ATTR_TEMPERATURE] == 20.0
assert state.attributes[ATTR_PRESET_MODE] == PRESET_MODE_AUTO
async def test_climate_preset_mode_when_off(
hass: HomeAssistant,
mock_serial_bridge: AsyncMock,
mock_serial_bridge_config_entry: MockConfigEntry,
) -> None:
"""Test climate preset mode service when off."""
await setup_integration(hass, mock_serial_bridge_config_entry)
assert (state := hass.states.get(ENTITY_ID))
assert state.state == HVACMode.HEAT
assert state.attributes[ATTR_TEMPERATURE] == 5.0
assert state.attributes[ATTR_PRESET_MODE] == PRESET_MODE_MANUAL
await hass.services.async_call(
CLIMATE_DOMAIN,
SERVICE_TURN_OFF,
{ATTR_ENTITY_ID: ENTITY_ID},
blocking=True,
)
mock_serial_bridge.set_clima_status.assert_called()
assert (state := hass.states.get(ENTITY_ID))
assert state.state == HVACMode.OFF
await hass.services.async_call(
CLIMATE_DOMAIN,
SERVICE_SET_PRESET_MODE,
{ATTR_ENTITY_ID: ENTITY_ID, ATTR_PRESET_MODE: PRESET_MODE_AUTO},
blocking=True,
)
mock_serial_bridge.set_clima_status.assert_called()
assert (state := hass.states.get(ENTITY_ID))
assert state.state == HVACMode.OFF