Add toggle service to climate (#100418)

* Add toggle service to climate

* Fix mqtt test

* Add comments

* Fix rebase

* Remove not needed properties

* Fix toggle service

* Fix test

* Test

* Mod mqtt test

---------

Co-authored-by: G Johansson <goran.johansson@shiftit.se>
This commit is contained in:
Artur Pragacz 2024-02-16 15:53:48 +01:00 committed by GitHub
parent cb776593cf
commit 3392660537
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 135 additions and 13 deletions

View File

@ -14,6 +14,7 @@ from homeassistant.const import (
ATTR_TEMPERATURE, ATTR_TEMPERATURE,
PRECISION_TENTHS, PRECISION_TENTHS,
PRECISION_WHOLE, PRECISION_WHOLE,
SERVICE_TOGGLE,
SERVICE_TURN_OFF, SERVICE_TURN_OFF,
SERVICE_TURN_ON, SERVICE_TURN_ON,
STATE_OFF, STATE_OFF,
@ -166,6 +167,12 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
"async_turn_off", "async_turn_off",
[ClimateEntityFeature.TURN_OFF], [ClimateEntityFeature.TURN_OFF],
) )
component.async_register_entity_service(
SERVICE_TOGGLE,
{},
"async_toggle",
[ClimateEntityFeature.TURN_OFF, ClimateEntityFeature.TURN_ON],
)
component.async_register_entity_service( component.async_register_entity_service(
SERVICE_SET_HVAC_MODE, SERVICE_SET_HVAC_MODE,
{vol.Required(ATTR_HVAC_MODE): vol.Coerce(HVACMode)}, {vol.Required(ATTR_HVAC_MODE): vol.Coerce(HVACMode)},
@ -756,7 +763,9 @@ class ClimateEntity(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_):
if mode not in self.hvac_modes: if mode not in self.hvac_modes:
continue continue
await self.async_set_hvac_mode(mode) await self.async_set_hvac_mode(mode)
break return
raise NotImplementedError
def turn_off(self) -> None: def turn_off(self) -> None:
"""Turn the entity off.""" """Turn the entity off."""
@ -772,6 +781,26 @@ class ClimateEntity(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_):
# Fake turn off # Fake turn off
if HVACMode.OFF in self.hvac_modes: if HVACMode.OFF in self.hvac_modes:
await self.async_set_hvac_mode(HVACMode.OFF) await self.async_set_hvac_mode(HVACMode.OFF)
return
raise NotImplementedError
def toggle(self) -> None:
"""Toggle the entity."""
raise NotImplementedError
async def async_toggle(self) -> None:
"""Toggle the entity."""
# Forward to self.toggle if it's been overridden.
if type(self).toggle is not ClimateEntity.toggle:
await self.hass.async_add_executor_job(self.toggle)
return
# We assume that since turn_off is supported, HVACMode.OFF is as well.
if self.hvac_mode == HVACMode.OFF:
await self.async_turn_on()
else:
await self.async_turn_off()
@cached_property @cached_property
def supported_features(self) -> ClimateEntityFeature: def supported_features(self) -> ClimateEntityFeature:

View File

@ -148,3 +148,11 @@ turn_off:
domain: climate domain: climate
supported_features: supported_features:
- climate.ClimateEntityFeature.TURN_OFF - climate.ClimateEntityFeature.TURN_OFF
toggle:
target:
entity:
domain: climate
supported_features:
- climate.ClimateEntityFeature.TURN_OFF
- climate.ClimateEntityFeature.TURN_ON

View File

@ -219,6 +219,10 @@
"turn_off": { "turn_off": {
"name": "[%key:common::action::turn_off%]", "name": "[%key:common::action::turn_off%]",
"description": "Turns climate device off." "description": "Turns climate device off."
},
"toggle": {
"name": "[%key:common::action::toggle%]",
"description": "Toggles climate device, from on to off, or off to on."
} }
}, },
"selector": { "selector": {

View File

@ -3,7 +3,7 @@ from __future__ import annotations
from enum import Enum from enum import Enum
from types import ModuleType from types import ModuleType
from unittest.mock import MagicMock, patch from unittest.mock import MagicMock, Mock, patch
import pytest import pytest
import voluptuous as vol import voluptuous as vol
@ -110,12 +110,6 @@ class MockClimateEntity(MockEntity, ClimateEntity):
""" """
return [HVACMode.OFF, HVACMode.HEAT] return [HVACMode.OFF, HVACMode.HEAT]
def turn_on(self) -> None:
"""Turn on."""
def turn_off(self) -> None:
"""Turn off."""
def set_preset_mode(self, preset_mode: str) -> None: def set_preset_mode(self, preset_mode: str) -> None:
"""Set preset mode.""" """Set preset mode."""
self._attr_preset_mode = preset_mode self._attr_preset_mode = preset_mode
@ -129,9 +123,19 @@ class MockClimateEntity(MockEntity, ClimateEntity):
self._attr_swing_mode = swing_mode self._attr_swing_mode = swing_mode
class MockClimateEntityTestMethods(MockClimateEntity):
"""Mock Climate device."""
def turn_on(self) -> None:
"""Turn on."""
def turn_off(self) -> None:
"""Turn off."""
async def test_sync_turn_on(hass: HomeAssistant) -> None: async def test_sync_turn_on(hass: HomeAssistant) -> None:
"""Test if async turn_on calls sync turn_on.""" """Test if async turn_on calls sync turn_on."""
climate = MockClimateEntity() climate = MockClimateEntityTestMethods()
climate.hass = hass climate.hass = hass
climate.turn_on = MagicMock() climate.turn_on = MagicMock()
@ -142,7 +146,7 @@ async def test_sync_turn_on(hass: HomeAssistant) -> None:
async def test_sync_turn_off(hass: HomeAssistant) -> None: async def test_sync_turn_off(hass: HomeAssistant) -> None:
"""Test if async turn_off calls sync turn_off.""" """Test if async turn_off calls sync turn_off."""
climate = MockClimateEntity() climate = MockClimateEntityTestMethods()
climate.hass = hass climate.hass = hass
climate.turn_off = MagicMock() climate.turn_off = MagicMock()
@ -684,3 +688,80 @@ async def test_no_warning_integration_has_migrated(
" implements HVACMode(s): off, heat and therefore implicitly supports the off, heat methods" " implements HVACMode(s): off, heat and therefore implicitly supports the off, heat methods"
not in caplog.text not in caplog.text
) )
async def test_turn_on_off_toggle(hass: HomeAssistant) -> None:
"""Test turn_on/turn_off/toggle methods."""
class MockClimateEntityTest(MockClimateEntity):
"""Mock Climate device."""
_attr_hvac_mode = HVACMode.OFF
@property
def hvac_mode(self) -> HVACMode:
"""Return hvac mode."""
return self._attr_hvac_mode
async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None:
"""Set new target hvac mode."""
self._attr_hvac_mode = hvac_mode
climate = MockClimateEntityTest()
climate.hass = hass
await climate.async_turn_on()
assert climate.hvac_mode == HVACMode.HEAT
await climate.async_turn_off()
assert climate.hvac_mode == HVACMode.OFF
await climate.async_toggle()
assert climate.hvac_mode == HVACMode.HEAT
await climate.async_toggle()
assert climate.hvac_mode == HVACMode.OFF
async def test_sync_toggle(hass: HomeAssistant) -> None:
"""Test if async toggle calls sync toggle."""
class MockClimateEntityTest(MockClimateEntity):
"""Mock Climate device."""
_enable_turn_on_off_backwards_compatibility = False
_attr_supported_features = (
ClimateEntityFeature.TURN_OFF | ClimateEntityFeature.TURN_ON
)
@property
def hvac_mode(self) -> HVACMode:
"""Return hvac operation ie. heat, cool mode.
Need to be one of HVACMode.*.
"""
return HVACMode.HEAT
@property
def hvac_modes(self) -> list[HVACMode]:
"""Return the list of available hvac operation modes.
Need to be a subset of HVAC_MODES.
"""
return [HVACMode.OFF, HVACMode.HEAT]
def turn_on(self) -> None:
"""Turn on."""
def turn_off(self) -> None:
"""Turn off."""
def toggle(self) -> None:
"""Toggle."""
climate = MockClimateEntityTest()
climate.hass = hass
climate.toggle = Mock()
await climate.async_toggle()
assert climate.toggle.called

View File

@ -504,10 +504,10 @@ async def test_turn_on_and_off_without_power_command(
assert state.state == "cool" assert state.state == "cool"
mqtt_mock.async_publish.reset_mock() mqtt_mock.async_publish.reset_mock()
await common.async_turn_off(hass, ENTITY_CLIMATE)
state = hass.states.get(ENTITY_CLIMATE)
assert climate_off is None or state.state == climate_off
if climate_off: if climate_off:
await common.async_turn_off(hass, ENTITY_CLIMATE)
state = hass.states.get(ENTITY_CLIMATE)
assert climate_off is None or state.state == climate_off
assert state.state == "off" assert state.state == "off"
mqtt_mock.async_publish.assert_has_calls([call("mode-topic", "off", 0, False)]) mqtt_mock.async_publish.assert_has_calls([call("mode-topic", "off", 0, False)])
else: else: