Skip to content

Commit

Permalink
Add toggle service to climate (home-assistant#100418)
Browse files Browse the repository at this point in the history
* 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>
  • Loading branch information
arturpragacz and gjohansson-ST authored Feb 16, 2024
1 parent cb77659 commit 3392660
Show file tree
Hide file tree
Showing 5 changed files with 135 additions and 13 deletions.
31 changes: 30 additions & 1 deletion homeassistant/components/climate/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
ATTR_TEMPERATURE,
PRECISION_TENTHS,
PRECISION_WHOLE,
SERVICE_TOGGLE,
SERVICE_TURN_OFF,
SERVICE_TURN_ON,
STATE_OFF,
Expand Down Expand Up @@ -166,6 +167,12 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
"async_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(
SERVICE_SET_HVAC_MODE,
{vol.Required(ATTR_HVAC_MODE): vol.Coerce(HVACMode)},
Expand Down Expand Up @@ -756,7 +763,9 @@ async def async_turn_on(self) -> None:
if mode not in self.hvac_modes:
continue
await self.async_set_hvac_mode(mode)
break
return

raise NotImplementedError

def turn_off(self) -> None:
"""Turn the entity off."""
Expand All @@ -772,6 +781,26 @@ async def async_turn_off(self) -> None:
# Fake turn off
if HVACMode.OFF in self.hvac_modes:
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
def supported_features(self) -> ClimateEntityFeature:
Expand Down
8 changes: 8 additions & 0 deletions homeassistant/components/climate/services.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -148,3 +148,11 @@ turn_off:
domain: climate
supported_features:
- climate.ClimateEntityFeature.TURN_OFF

toggle:
target:
entity:
domain: climate
supported_features:
- climate.ClimateEntityFeature.TURN_OFF
- climate.ClimateEntityFeature.TURN_ON
4 changes: 4 additions & 0 deletions homeassistant/components/climate/strings.json
Original file line number Diff line number Diff line change
Expand Up @@ -219,6 +219,10 @@
"turn_off": {
"name": "[%key:common::action::turn_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": {
Expand Down
99 changes: 90 additions & 9 deletions tests/components/climate/test_init.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@

from enum import Enum
from types import ModuleType
from unittest.mock import MagicMock, patch
from unittest.mock import MagicMock, Mock, patch

import pytest
import voluptuous as vol
Expand Down Expand Up @@ -110,12 +110,6 @@ def hvac_modes(self) -> list[HVACMode]:
"""
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:
"""Set preset mode."""
self._attr_preset_mode = preset_mode
Expand All @@ -129,9 +123,19 @@ def set_swing_mode(self, swing_mode: str) -> None:
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:
"""Test if async turn_on calls sync turn_on."""
climate = MockClimateEntity()
climate = MockClimateEntityTestMethods()
climate.hass = hass

climate.turn_on = MagicMock()
Expand All @@ -142,7 +146,7 @@ async def test_sync_turn_on(hass: HomeAssistant) -> None:

async def test_sync_turn_off(hass: HomeAssistant) -> None:
"""Test if async turn_off calls sync turn_off."""
climate = MockClimateEntity()
climate = MockClimateEntityTestMethods()
climate.hass = hass

climate.turn_off = MagicMock()
Expand Down Expand Up @@ -684,3 +688,80 @@ async def async_setup_entry_climate_platform(
" implements HVACMode(s): off, heat and therefore implicitly supports the off, heat methods"
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
6 changes: 3 additions & 3 deletions tests/components/mqtt/test_climate.py
Original file line number Diff line number Diff line change
Expand Up @@ -504,10 +504,10 @@ async def test_turn_on_and_off_without_power_command(
assert state.state == "cool"
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:
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"
mqtt_mock.async_publish.assert_has_calls([call("mode-topic", "off", 0, False)])
else:
Expand Down

0 comments on commit 3392660

Please sign in to comment.