From 7f8341e03a564fee8562576849f2a22c5e13060f Mon Sep 17 00:00:00 2001 From: mkmer Date: Sat, 6 Apr 2024 11:22:56 -0400 Subject: [PATCH] Deprecate aux heat from Honeywell (#114110) * Remove aux heat * Add switch entity for emheat * Optimized async_setup_entry * Fix errors in comments * Fix new ruff failuer * Use constant for EM * Protect EM mode - must be in heat to turn on/off * Restore aux_heat * Add repair issue * Add missing place holder to issue * Better placeholder "option" --- .../components/honeywell/__init__.py | 2 +- homeassistant/components/honeywell/climate.py | 25 ++++- .../components/honeywell/strings.json | 26 +++++ homeassistant/components/honeywell/switch.py | 97 +++++++++++++++++++ .../components/honeywell/test_diagnostics.py | 2 +- tests/components/honeywell/test_init.py | 16 +-- tests/components/honeywell/test_switch.py | 87 +++++++++++++++++ 7 files changed, 244 insertions(+), 11 deletions(-) create mode 100644 homeassistant/components/honeywell/switch.py create mode 100644 tests/components/honeywell/test_switch.py diff --git a/homeassistant/components/honeywell/__init__.py b/homeassistant/components/honeywell/__init__.py index c1c46e2b7af..8349c383e9f 100644 --- a/homeassistant/components/honeywell/__init__.py +++ b/homeassistant/components/honeywell/__init__.py @@ -21,7 +21,7 @@ from .const import ( ) UPDATE_LOOP_SLEEP_TIME = 5 -PLATFORMS = [Platform.CLIMATE, Platform.SENSOR] +PLATFORMS = [Platform.CLIMATE, Platform.SENSOR, Platform.SWITCH] MIGRATE_OPTIONS_KEYS = {CONF_COOL_AWAY_TEMPERATURE, CONF_HEAT_AWAY_TEMPERATURE} diff --git a/homeassistant/components/honeywell/climate.py b/homeassistant/components/honeywell/climate.py index 5ac5e8a2472..bd32ee0a23d 100644 --- a/homeassistant/components/honeywell/climate.py +++ b/homeassistant/components/honeywell/climate.py @@ -35,7 +35,7 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.const import ATTR_TEMPERATURE, UnitOfTemperature from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError -from homeassistant.helpers import device_registry as dr +from homeassistant.helpers import device_registry as dr, issue_registry as ir from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -169,6 +169,7 @@ class HoneywellUSThermostat(ClimateEntity): manufacturer="Honeywell", ) + self._attr_translation_placeholders = {"name": device.name} self._attr_temperature_unit = UnitOfTemperature.FAHRENHEIT if device.temperature_unit == "C": self._attr_temperature_unit = UnitOfTemperature.CELSIUS @@ -480,6 +481,16 @@ class HoneywellUSThermostat(ClimateEntity): async def async_turn_aux_heat_on(self) -> None: """Turn auxiliary heater on.""" + ir.async_create_issue( + self.hass, + DOMAIN, + "service_deprecation", + breaks_in_ha_version="2024.10.0", + is_fixable=True, + is_persistent=True, + severity=ir.IssueSeverity.WARNING, + translation_key="service_deprecation", + ) try: await self._device.set_system_mode("emheat") except SomeComfortError as err: @@ -489,6 +500,18 @@ class HoneywellUSThermostat(ClimateEntity): async def async_turn_aux_heat_off(self) -> None: """Turn auxiliary heater off.""" + + ir.async_create_issue( + self.hass, + DOMAIN, + "service_deprecation", + breaks_in_ha_version="2024.10.0", + is_fixable=True, + is_persistent=True, + severity=ir.IssueSeverity.WARNING, + translation_key="service_deprecation", + ) + try: if HVACMode.HEAT in self.hvac_modes: await self.async_set_hvac_mode(HVACMode.HEAT) diff --git a/homeassistant/components/honeywell/strings.json b/homeassistant/components/honeywell/strings.json index 6f855828e01..7506a7fda7c 100644 --- a/homeassistant/components/honeywell/strings.json +++ b/homeassistant/components/honeywell/strings.json @@ -41,6 +41,11 @@ "name": "Outdoor humidity" } }, + "switch": { + "emergency_heat": { + "name": "Emergency heat" + } + }, "climate": { "honeywell": { "state_attributes": { @@ -54,5 +59,26 @@ } } } + }, + "exceptions": { + "switch_failed_off": { + "message": "Honeywell could turn off emergency heat mode." + }, + "switch_failed_on": { + "message": "Honeywell could not set system mode to emergency heat mode." + } + }, + "issues": { + "service_deprecation": { + "title": "Honeywell aux heat is being removed", + "fix_flow": { + "step": { + "confirm": { + "title": "[%key:component::honeywell::issues::service_deprecation::title%]", + "description": "Use `switch.{name}_emergency_heat` instead to change mode.\n\nPlease adjust your automations and scripts and select **submit** to fix this issue." + } + } + } + } } } diff --git a/homeassistant/components/honeywell/switch.py b/homeassistant/components/honeywell/switch.py new file mode 100644 index 00000000000..4aebde76727 --- /dev/null +++ b/homeassistant/components/honeywell/switch.py @@ -0,0 +1,97 @@ +"""Support for Honeywell switches.""" + +from __future__ import annotations + +from typing import Any + +from aiosomecomfort import SomeComfortError +from aiosomecomfort.device import Device as SomeComfortDevice + +from homeassistant.components.switch import ( + SwitchDeviceClass, + SwitchEntity, + SwitchEntityDescription, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import HomeAssistantError +from homeassistant.helpers.device_registry import DeviceInfo +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from . import HoneywellData +from .const import DOMAIN + +EMERGENCY_HEAT_KEY = "emergency_heat" + +SWITCH_TYPES: tuple[SwitchEntityDescription, ...] = ( + SwitchEntityDescription( + key=EMERGENCY_HEAT_KEY, + translation_key=EMERGENCY_HEAT_KEY, + device_class=SwitchDeviceClass.SWITCH, + ), +) + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up the Honeywell switches.""" + data: HoneywellData = hass.data[DOMAIN][config_entry.entry_id] + async_add_entities( + HoneywellSwitch(hass, config_entry, device, description) + for device in data.devices.values() + if device.raw_ui_data.get("SwitchEmergencyHeatAllowed") + for description in SWITCH_TYPES + ) + + +class HoneywellSwitch(SwitchEntity): + """Representation of a honeywell switch.""" + + _attr_has_entity_name = True + + def __init__( + self, + hass: HomeAssistant, + config_entry: ConfigEntry, + device: SomeComfortDevice, + description: SwitchEntityDescription, + ) -> None: + """Initialize the switch.""" + self._data = hass.data[DOMAIN][config_entry.entry_id] + self._device = device + self.entity_description = description + self._attr_unique_id = f"{device.deviceid}_{description.key}" + self._attr_device_info = DeviceInfo( + identifiers={(DOMAIN, device.deviceid)}, + name=device.name, + manufacturer="Honeywell", + ) + + async def async_turn_on(self, **kwargs: Any) -> None: + """Turn the switch on if heat mode is enabled.""" + if self._device.system_mode == "heat": + try: + await self._device.set_system_mode("emheat") + except SomeComfortError as err: + raise HomeAssistantError( + translation_domain=DOMAIN, translation_key="switch_failed_on" + ) from err + + async def async_turn_off(self, **kwargs: Any) -> None: + """Turn the switch off if on.""" + if self.is_on: + try: + await self._device.set_system_mode("off") + + except SomeComfortError as err: + raise HomeAssistantError( + translation_domain=DOMAIN, translation_key="switch_failed_off" + ) from err + + @property + def is_on(self) -> bool: + """Return true if Emergency heat is enabled.""" + return self._device.system_mode == "emheat" diff --git a/tests/components/honeywell/test_diagnostics.py b/tests/components/honeywell/test_diagnostics.py index b180bf0e5bc..06c41d3d055 100644 --- a/tests/components/honeywell/test_diagnostics.py +++ b/tests/components/honeywell/test_diagnostics.py @@ -29,7 +29,7 @@ async def test_entry_diagnostics( await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() assert config_entry.state is ConfigEntryState.LOADED - assert hass.states.async_entity_ids_count() == 6 + assert hass.states.async_entity_ids_count() == 8 result = await get_diagnostics_for_config_entry(hass, hass_client, config_entry) diff --git a/tests/components/honeywell/test_init.py b/tests/components/honeywell/test_init.py index d27428fcf65..a77c0aaed7e 100644 --- a/tests/components/honeywell/test_init.py +++ b/tests/components/honeywell/test_init.py @@ -30,7 +30,7 @@ async def test_setup_entry(hass: HomeAssistant, config_entry: MockConfigEntry) - await hass.async_block_till_done() assert config_entry.state is ConfigEntryState.LOADED assert ( - hass.states.async_entity_ids_count() == 3 + hass.states.async_entity_ids_count() == 4 ) # 1 climate entity; 2 sensor entities @@ -63,8 +63,8 @@ async def test_setup_multiple_thermostats( await hass.async_block_till_done() assert config_entry.state is ConfigEntryState.LOADED assert ( - hass.states.async_entity_ids_count() == 6 - ) # 2 climate entities; 4 sensor entities + hass.states.async_entity_ids_count() == 8 + ) # 2 climate entities; 4 sensor entities; 2 switch entities async def test_setup_multiple_thermostats_with_same_deviceid( @@ -84,8 +84,8 @@ async def test_setup_multiple_thermostats_with_same_deviceid( await hass.async_block_till_done() assert config_entry.state is ConfigEntryState.LOADED assert ( - hass.states.async_entity_ids_count() == 3 - ) # 1 climate entity; 2 sensor entities + hass.states.async_entity_ids_count() == 4 + ) # 1 climate entity; 2 sensor entities; 1 switch enitiy assert "Platform honeywell does not generate unique IDs" not in caplog.text @@ -171,7 +171,7 @@ async def test_remove_stale_device( await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() assert config_entry.state is ConfigEntryState.LOADED - assert hass.states.async_entity_ids_count() == 6 + assert hass.states.async_entity_ids_count() == 8 device_entries = dr.async_entries_for_config_entry( device_registry, config_entry.entry_id @@ -209,8 +209,8 @@ async def test_remove_stale_device( assert config_entry.state is ConfigEntryState.LOADED assert ( - hass.states.async_entity_ids_count() == 3 - ) # 1 climate entities; 2 sensor entities + hass.states.async_entity_ids_count() == 4 + ) # 1 climate entities; 2 sensor entities; 1 switch entity device_entries = dr.async_entries_for_config_entry( device_registry, config_entry.entry_id diff --git a/tests/components/honeywell/test_switch.py b/tests/components/honeywell/test_switch.py new file mode 100644 index 00000000000..73052871ef1 --- /dev/null +++ b/tests/components/honeywell/test_switch.py @@ -0,0 +1,87 @@ +"""Tests for Honeywell switch component.""" + +from unittest.mock import MagicMock + +from aiosomecomfort.exceptions import SomeComfortError +import pytest + +from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN +from homeassistant.const import ATTR_ENTITY_ID, SERVICE_TURN_OFF, SERVICE_TURN_ON +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import HomeAssistantError + +from . import init_integration + +from tests.common import MockConfigEntry + + +async def test_emheat_switch( + hass: HomeAssistant, + config_entry: MockConfigEntry, + device: MagicMock, +) -> None: + """Test emergency heat switch.""" + + await init_integration(hass, config_entry) + entity_id = f"switch.{device.name}_emergency_heat" + await hass.services.async_call( + SWITCH_DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: entity_id}, + blocking=True, + ) + device.set_system_mode.assert_not_called() + + device.set_system_mode.reset_mock() + + await hass.services.async_call( + SWITCH_DOMAIN, + SERVICE_TURN_OFF, + {ATTR_ENTITY_ID: entity_id}, + blocking=True, + ) + device.set_system_mode.assert_not_called() + + device.system_mode = "heat" + + await hass.services.async_call( + SWITCH_DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: entity_id}, + blocking=True, + ) + device.set_system_mode.assert_called_once_with("emheat") + + device.set_system_mode.reset_mock() + device.system_mode = "emheat" + await hass.services.async_call( + SWITCH_DOMAIN, + SERVICE_TURN_OFF, + {ATTR_ENTITY_ID: entity_id}, + blocking=True, + ) + device.set_system_mode.assert_called_once_with("off") + + device.set_system_mode.reset_mock() + device.system_mode = "heat" + device.set_system_mode.side_effect = SomeComfortError + with pytest.raises(HomeAssistantError): + await hass.services.async_call( + SWITCH_DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: entity_id}, + blocking=True, + ) + device.set_system_mode.assert_called_once_with("emheat") + + device.set_system_mode.reset_mock() + device.system_mode = "emheat" + device.set_system_mode.side_effect = SomeComfortError + with pytest.raises(HomeAssistantError): + await hass.services.async_call( + SWITCH_DOMAIN, + SERVICE_TURN_OFF, + {ATTR_ENTITY_ID: entity_id}, + blocking=True, + ) + device.set_system_mode.assert_called_once_with("off")