diff --git a/custom_components/lennoxs30/const.py b/custom_components/lennoxs30/const.py index 84e442f..506876b 100644 --- a/custom_components/lennoxs30/const.py +++ b/custom_components/lennoxs30/const.py @@ -45,6 +45,7 @@ UNIQUE_ID_SUFFIX_RESET_SMART_HUB: Final = "_RESET_SMART_HUB" UNIQUE_ID_SUFFIX_BLE: Final = "_BLE" UNIQUE_ID_SUFFIX_BLE_COMMSTATUS: Final = "_BLE_COMMSTATUS" +UNIQUE_ID_SUFFIX_VENTILATION_SELECT: Final = "_VENT_SELECT" VENTILATION_EQUIPMENT_ID = -900 diff --git a/custom_components/lennoxs30/number.py b/custom_components/lennoxs30/number.py index 40d83c1..982238e 100644 --- a/custom_components/lennoxs30/number.py +++ b/custom_components/lennoxs30/number.py @@ -368,7 +368,7 @@ class TimedVentilationNumber(S30BaseEntityMixin, NumberEntity): def __init__(self, hass: HomeAssistant, manager: Manager, system: lennox_system): super().__init__(manager, system) self._hass = hass - self._myname = self._system.name + "_timed_ventilation" + self._myname = self._system.name + "_ventilate_now" _LOGGER.debug("Create TimedVentilationNumber myname [%s]", self._myname) async def async_added_to_hass(self) -> None: diff --git a/custom_components/lennoxs30/select.py b/custom_components/lennoxs30/select.py index c3a8cb4..b5c29b7 100644 --- a/custom_components/lennoxs30/select.py +++ b/custom_components/lennoxs30/select.py @@ -23,6 +23,10 @@ LENNOX_DEHUMIDIFICATION_MODE_HIGH, LENNOX_DEHUMIDIFICATION_MODE_MEDIUM, LENNOX_DEHUMIDIFICATION_MODE_AUTO, + LENNOX_VENTILATION_MODES, + LENNOX_VENTILATION_MODE_INSTALLER, + LENNOX_VENTILATION_MODE_ON, + LENNOX_VENTILATION_MODE_OFF, lennox_system, lennox_zone, ) @@ -39,7 +43,12 @@ ) from .base_entity import S30BaseEntityMixin -from .const import LOG_INFO_SELECT_ASYNC_SELECT_OPTION, MANAGER, UNIQUE_ID_SUFFIX_EQ_PARAM_SELECT +from .const import ( + LOG_INFO_SELECT_ASYNC_SELECT_OPTION, + MANAGER, + UNIQUE_ID_SUFFIX_EQ_PARAM_SELECT, + UNIQUE_ID_SUFFIX_VENTILATION_SELECT, +) from . import DOMAIN, Manager @@ -61,6 +70,11 @@ async def async_setup_entry( _LOGGER.debug("Create DehumidificationModeSelect system [%s]", system.sysId) sel = DehumidificationModeSelect(hass, manager, system) select_list.append(sel) + + if system.supports_ventilation(): + _LOGGER.info("Create S30 ventilation select system [%s]", system.sysId) + select_list.append(VentilationModeSelect(hass, manager, system)) + for zone in system.zone_list: if zone.is_zone_active(): if zone.dehumidificationOption or zone.humidificationOption: @@ -362,3 +376,86 @@ def entity_category(self): @property def extra_state_attributes(self) -> dict[str, Any]: return helper_get_parameter_extra_attributes(self.equipment, self.parameter) + + +class VentilationModeSelect(S30BaseEntityMixin, SelectEntity): + """Set the ventilation mode""" + + def __init__( + self, + hass: HomeAssistant, + manager: Manager, + system: lennox_system, + ): + super().__init__(manager, system) + self.hass: HomeAssistant = hass + self._myname = self._system.name + "_ventilation_mode" + _LOGGER.debug("Create VentilationModeSelect myname [%s]", self._myname) + + async def async_added_to_hass(self) -> None: + """Run when entity about to be added to hass.""" + _LOGGER.debug("async_added_to_hass VentilationModeSelect myname [%s]", self._myname) + self._system.registerOnUpdateCallback( + self.system_update_callback, + [ + "ventilationMode", + "ventilationControlMode", + ], + ) + await super().async_added_to_hass() + + def system_update_callback(self): + """Callback for system updates""" + if _LOGGER.isEnabledFor(logging.DEBUG): + _LOGGER.debug( + "system_update_callback VentilationModeSelect myname [%s] system ventilation mode [%s]", + self._myname, + self._system.ventilationMode, + ) + self.schedule_update_ha_state() + + @property + def unique_id(self) -> str: + # HA fails with dashes in IDs + return self._system.unique_id + UNIQUE_ID_SUFFIX_VENTILATION_SELECT + + @property + def name(self): + return self._myname + + @property + def current_option(self) -> str: + return self._system.ventilationMode + + @property + def options(self) -> list: + return LENNOX_VENTILATION_MODES + + async def async_select_option(self, option: str) -> None: + _LOGGER.info(LOG_INFO_SELECT_ASYNC_SELECT_OPTION, self.__class__.__name__, self._myname, option) + try: + if option == LENNOX_VENTILATION_MODE_ON: + await self._system.ventilation_on() + elif option == LENNOX_VENTILATION_MODE_OFF: + await self._system.ventilation_off() + elif option == LENNOX_VENTILATION_MODE_INSTALLER: + await self._system.ventilation_installer() + else: + raise HomeAssistantError(f"select_option [{self._myname}] invalid mode [{option}]") + except Exception as ex: + raise HomeAssistantError(f"select_option unexpected exception, [{self._myname}] exception [{ex}]") from ex + + @property + def device_info(self) -> DeviceInfo: + """Return device info.""" + result = { + "identifiers": {(DOMAIN, self._system.unique_id)}, + } + return result + + @property + def extra_state_attributes(self): + """Return the state attributes.""" + attrs: dict[str, Any] = {} + attrs["installer_settings"] = self._system.ventilationControlMode + return attrs diff --git a/tests/test_number_timed_ventilation.py b/tests/test_number_timed_ventilation.py index 6e425ff..3c740bf 100644 --- a/tests/test_number_timed_ventilation.py +++ b/tests/test_number_timed_ventilation.py @@ -40,7 +40,7 @@ async def test_timed_ventilation_time_unique_id(hass, manager: Manager): async def test_timed_ventilation_time_name(hass, manager: Manager): system: lennox_system = manager.api.system_list[0] c = TimedVentilationNumber(hass, manager, system) - assert c.name == system.name + "_timed_ventilation" + assert c.name == system.name + "_ventilate_now" @pytest.mark.asyncio diff --git a/tests/test_select_setup.py b/tests/test_select_setup.py index ea91d25..414b6a1 100644 --- a/tests/test_select_setup.py +++ b/tests/test_select_setup.py @@ -1,27 +1,26 @@ -from pickle import FALSE -from lennoxs30api.s30api_async import ( - LENNOX_VENTILATION_DAMPER, - lennox_system, -) -from custom_components.lennoxs30 import ( - Manager, -) +"""Test setup of select entities""" +# pylint: disable=line-too-long +# pylint: disable=protected-access + +from unittest.mock import Mock import pytest -from custom_components.lennoxs30.const import MANAGER +from lennoxs30api.s30api_async import LENNOX_VENTILATION_2_SPEED_HRV, lennox_system +from custom_components.lennoxs30 import Manager + +from custom_components.lennoxs30.const import MANAGER from custom_components.lennoxs30.select import ( DehumidificationModeSelect, HumidityModeSelect, EquipmentParameterSelect, + VentilationModeSelect, async_setup_entry, ) -from unittest.mock import Mock - - @pytest.mark.asyncio -async def test_async_number_setup_entry(hass, manager: Manager, caplog): +async def test_async_number_setup_entry(hass, manager: Manager): + """Test the select setup""" system: lennox_system = manager.api.system_list[0] entry = manager.config_entry hass.data["lennoxs30"] = {} @@ -93,3 +92,17 @@ async def test_async_number_setup_entry(hass, manager: Manager, caplog): assert len(sensor_list) == 15 for i in range(0, 15): assert isinstance(sensor_list[i], EquipmentParameterSelect) + + # VenitlatioNModeSelect should be created + system.dehumidifierType = None + for zone in system.zone_list: + zone.dehumidificationOption = False + zone.humidificationOption = False + manager.create_equipment_parameters = False + system.ventilationUnitType = LENNOX_VENTILATION_2_SPEED_HRV + async_add_entities = Mock() + await async_setup_entry(hass, entry, async_add_entities) + assert async_add_entities.call_count == 1 + sensor_list = async_add_entities.call_args[0][0] + assert len(sensor_list) == 1 + assert isinstance(sensor_list[0], VentilationModeSelect) diff --git a/tests/test_select_ventilation_mode.py b/tests/test_select_ventilation_mode.py new file mode 100644 index 0000000..4ef507d --- /dev/null +++ b/tests/test_select_ventilation_mode.py @@ -0,0 +1,173 @@ +# pylint: disable=too-many-lines +# pylint: disable=missing-module-docstring +# pylint: disable=missing-function-docstring +# pylint: disable=invalid-name +# pylint: disable=protected-access +# pylint: disable=line-too-long + +import logging +from unittest.mock import patch +import pytest + +from homeassistant.exceptions import HomeAssistantError + +from lennoxs30api.s30api_async import ( + LENNOX_VENTILATION_MODE_OFF, + LENNOX_VENTILATION_MODE_ON, + LENNOX_VENTILATION_MODE_INSTALLER, + LENNOX_VENTILATION_CONTROL_MODE_ASHRAE, + lennox_system, +) + + +from custom_components.lennoxs30 import Manager +from custom_components.lennoxs30.select import VentilationModeSelect +from custom_components.lennoxs30.const import LENNOX_DOMAIN + +from tests.conftest import ( + conf_test_exception_handling, + conftest_base_entity_availability, + conf_test_select_info_async_select_option, +) + + +@pytest.mark.asyncio +async def test_select_ventilation_mode_unique_id(hass, manager: Manager): + system: lennox_system = manager.api.system_list[0] + c = VentilationModeSelect(hass, manager, system) + assert c.unique_id == system.unique_id + "_VENT_SELECT" + + +@pytest.mark.asyncio +async def test_select_ventilation_mode_name(hass, manager: Manager): + system: lennox_system = manager.api.system_list[0] + c = VentilationModeSelect(hass, manager, system) + assert c.name == system.name + "_ventilation_mode" + + +@pytest.mark.asyncio +async def test_select_ventilation_mode_current_option(hass, manager_mz: Manager): + manager = manager_mz + system: lennox_system = manager.api.system_list[0] + c = VentilationModeSelect(hass, manager, system) + + system.ventilationMode = LENNOX_VENTILATION_MODE_INSTALLER + assert c.current_option == LENNOX_VENTILATION_MODE_INSTALLER + assert c.available is True + arr = c.extra_state_attributes + assert len(arr) == 1 + assert arr["installer_settings"] == system.ventilationControlMode + + system.ventilationMode = LENNOX_VENTILATION_MODE_ON + assert c.current_option == LENNOX_VENTILATION_MODE_ON + assert c.available is True + + system.ventilationMode = LENNOX_VENTILATION_MODE_OFF + assert c.current_option == LENNOX_VENTILATION_MODE_OFF + assert c.available is True + + +@pytest.mark.asyncio +async def test_select_ventilation_mode_subscription(hass, manager_mz: Manager): + manager = manager_mz + system: lennox_system = manager.api.system_list[0] + system.dehumidificationMode = None + c = VentilationModeSelect(hass, manager, system) + await c.async_added_to_hass() + + with patch.object(c, "schedule_update_ha_state") as update_callback: + update_set = {"ventilationMode": LENNOX_VENTILATION_MODE_OFF} + system.attr_updater(update_set, "ventilationMode") + system.executeOnUpdateCallbacks() + assert update_callback.call_count == 1 + assert c.current_option == LENNOX_VENTILATION_MODE_OFF + assert c.available is True + + with patch.object(c, "schedule_update_ha_state") as update_callback: + update_set = {"ventilationMode": LENNOX_VENTILATION_MODE_ON} + system.attr_updater(update_set, "ventilationMode") + system.executeOnUpdateCallbacks() + assert update_callback.call_count == 1 + assert c.current_option == LENNOX_VENTILATION_MODE_ON + assert c.available is True + + with patch.object(c, "schedule_update_ha_state") as update_callback: + update_set = {"ventilationMode": LENNOX_VENTILATION_MODE_INSTALLER} + system.attr_updater(update_set, "ventilationMode") + system.executeOnUpdateCallbacks() + assert update_callback.call_count == 1 + assert c.current_option == LENNOX_VENTILATION_MODE_INSTALLER + assert c.available is True + + with patch.object(c, "schedule_update_ha_state") as update_callback: + update_set = {"ventilationControlMode": LENNOX_VENTILATION_CONTROL_MODE_ASHRAE} + system.attr_updater(update_set, "ventilationControlMode") + system.executeOnUpdateCallbacks() + assert update_callback.call_count == 1 + assert c.extra_state_attributes["installer_settings"] == LENNOX_VENTILATION_CONTROL_MODE_ASHRAE + assert c.available is True + + conftest_base_entity_availability(manager, system, c) + + +@pytest.mark.asyncio +async def test_select_ventilation_mode_options(hass, manager_mz: Manager): + manager = manager_mz + system: lennox_system = manager.api.system_list[0] + c = VentilationModeSelect(hass, manager, system) + + opt = c.options + assert len(opt) == 3 + assert LENNOX_VENTILATION_MODE_INSTALLER in opt + assert LENNOX_VENTILATION_MODE_ON in opt + assert LENNOX_VENTILATION_MODE_OFF in opt + + +@pytest.mark.asyncio +async def test_select_ventilation_mode_async_select_options(hass, manager_mz: Manager, caplog): + manager = manager_mz + system: lennox_system = manager.api.system_list[0] + c = VentilationModeSelect(hass, manager, system) + + with patch.object(system, "ventilation_on") as set_dehumidificationMode: + await c.async_select_option(LENNOX_VENTILATION_MODE_ON) + assert set_dehumidificationMode.call_count == 1 + + with patch.object(system, "ventilation_off") as set_dehumidificationMode: + await c.async_select_option(LENNOX_VENTILATION_MODE_OFF) + assert set_dehumidificationMode.call_count == 1 + + with patch.object(system, "ventilation_installer") as set_dehumidificationMode: + await c.async_select_option(LENNOX_VENTILATION_MODE_INSTALLER) + assert set_dehumidificationMode.call_count == 1 + + with caplog.at_level(logging.ERROR): + caplog.clear() + with patch.object(system, "set_dehumidificationMode") as set_dehumidificationMode: + ex: HomeAssistantError = None + try: + await c.async_select_option("bad_value") + except HomeAssistantError as err: + ex = err + assert ex is not None + assert set_dehumidificationMode.call_count == 0 + msg = str(ex) + assert "bad_value" in msg + + await conf_test_exception_handling( + system, "ventilation_on", c, c.async_select_option, option=LENNOX_VENTILATION_MODE_ON + ) + await conf_test_select_info_async_select_option(system, "ventilation_on", c, caplog) + + +@pytest.mark.asyncio +async def test_select_ventilation_mode_device_info(hass, manager_mz: Manager): + manager = manager_mz + await manager.create_devices() + system: lennox_system = manager.api.system_list[0] + c = VentilationModeSelect(hass, manager, system) + + identifiers = c.device_info["identifiers"] + for x in identifiers: + assert x[0] == LENNOX_DOMAIN + assert x[1] == system.unique_id