From 2611f72f5d26efde15d5523506dd1264c8736761 Mon Sep 17 00:00:00 2001 From: mvn23 Date: Wed, 11 Sep 2024 00:12:09 +0200 Subject: [PATCH] Add LED mode select entities to opentherm_gw (#125702) Add select entities for LED mode to opentherm_gw --- .../components/opentherm_gw/select.py | 124 ++++++++++++++- .../components/opentherm_gw/strings.json | 17 +++ tests/components/opentherm_gw/test_select.py | 142 +++++++++++++++--- 3 files changed, 264 insertions(+), 19 deletions(-) diff --git a/homeassistant/components/opentherm_gw/select.py b/homeassistant/components/opentherm_gw/select.py index 49878d6d839..cee1632dc48 100644 --- a/homeassistant/components/opentherm_gw/select.py +++ b/homeassistant/components/opentherm_gw/select.py @@ -5,7 +5,16 @@ from dataclasses import dataclass from enum import IntEnum, StrEnum from functools import partial -from pyotgw.vars import OTGW_GPIO_A, OTGW_GPIO_B +from pyotgw.vars import ( + OTGW_GPIO_A, + OTGW_GPIO_B, + OTGW_LED_A, + OTGW_LED_B, + OTGW_LED_C, + OTGW_LED_D, + OTGW_LED_E, + OTGW_LED_F, +) from homeassistant.components.select import SelectEntity, SelectEntityDescription from homeassistant.config_entries import ConfigEntry @@ -37,6 +46,23 @@ class OpenThermSelectGPIOMode(StrEnum): DHW_BLOCK = "dhw_block" +class OpenThermSelectLEDMode(StrEnum): + """OpenThermGateway LED modes.""" + + RX_ANY = "receive_any" + TX_ANY = "transmit_any" + THERMOSTAT_TRAFFIC = "thermostat_traffic" + BOILER_TRAFFIC = "boiler_traffic" + SETPOINT_OVERRIDE_ACTIVE = "setpoint_override_active" + FLAME_ON = "flame_on" + CENTRAL_HEATING_ON = "central_heating_on" + HOT_WATER_ON = "hot_water_on" + COMFORT_MODE_ON = "comfort_mode_on" + TX_ERROR_DETECTED = "transmit_error_detected" + BOILER_MAINTENANCE_REQUIRED = "boiler_maintenance_required" + RAISED_POWER_MODE_ACTIVE = "raised_power_mode_active" + + class PyotgwGPIOMode(IntEnum): """pyotgw GPIO modes.""" @@ -51,6 +77,34 @@ class PyotgwGPIOMode(IntEnum): DHW_BLOCK = 8 +class PyotgwLEDMode(StrEnum): + """pyotgw LED modes.""" + + RX_ANY = "R" + TX_ANY = "X" + THERMOSTAT_TRAFFIC = "T" + BOILER_TRAFFIC = "B" + SETPOINT_OVERRIDE_ACTIVE = "O" + FLAME_ON = "F" + CENTRAL_HEATING_ON = "H" + HOT_WATER_ON = "W" + COMFORT_MODE_ON = "C" + TX_ERROR_DETECTED = "E" + BOILER_MAINTENANCE_REQUIRED = "M" + RAISED_POWER_MODE_ACTIVE = "P" + + +def pyotgw_led_mode_to_ha_led_mode( + pyotgw_led_mode: PyotgwLEDMode, +) -> OpenThermSelectLEDMode | None: + """Convert pyotgw LED mode to Home Assistant LED mode.""" + return ( + OpenThermSelectLEDMode[PyotgwLEDMode(pyotgw_led_mode).name] + if pyotgw_led_mode in PyotgwLEDMode + else None + ) + + async def set_gpio_mode( gpio_id: str, gw_hub: OpenThermGatewayHub, mode: str ) -> OpenThermSelectGPIOMode | None: @@ -65,6 +119,20 @@ async def set_gpio_mode( ) +async def set_led_mode( + led_id: str, gw_hub: OpenThermGatewayHub, mode: str +) -> OpenThermSelectLEDMode | None: + """Set gpio mode, return selected option or None.""" + value = await gw_hub.gateway.set_led_mode( + led_id, PyotgwLEDMode[OpenThermSelectLEDMode(mode).name] + ) + return ( + OpenThermSelectLEDMode[PyotgwLEDMode(value).name] + if value in PyotgwLEDMode + else None + ) + + @dataclass(frozen=True, kw_only=True) class OpenThermSelectEntityDescription( OpenThermEntityDescription, SelectEntityDescription @@ -106,6 +174,60 @@ SELECT_DESCRIPTIONS: tuple[OpenThermSelectEntityDescription, ...] = ( else None ), ), + OpenThermSelectEntityDescription( + key=OTGW_LED_A, + translation_key="led_mode_n", + translation_placeholders={"led_id": "A"}, + device_description=GATEWAY_DEVICE_DESCRIPTION, + options=list(OpenThermSelectLEDMode), + select_action=partial(set_led_mode, "A"), + convert_pyotgw_state_to_ha_state=pyotgw_led_mode_to_ha_led_mode, + ), + OpenThermSelectEntityDescription( + key=OTGW_LED_B, + translation_key="led_mode_n", + translation_placeholders={"led_id": "B"}, + device_description=GATEWAY_DEVICE_DESCRIPTION, + options=list(OpenThermSelectLEDMode), + select_action=partial(set_led_mode, "B"), + convert_pyotgw_state_to_ha_state=pyotgw_led_mode_to_ha_led_mode, + ), + OpenThermSelectEntityDescription( + key=OTGW_LED_C, + translation_key="led_mode_n", + translation_placeholders={"led_id": "C"}, + device_description=GATEWAY_DEVICE_DESCRIPTION, + options=list(OpenThermSelectLEDMode), + select_action=partial(set_led_mode, "C"), + convert_pyotgw_state_to_ha_state=pyotgw_led_mode_to_ha_led_mode, + ), + OpenThermSelectEntityDescription( + key=OTGW_LED_D, + translation_key="led_mode_n", + translation_placeholders={"led_id": "D"}, + device_description=GATEWAY_DEVICE_DESCRIPTION, + options=list(OpenThermSelectLEDMode), + select_action=partial(set_led_mode, "D"), + convert_pyotgw_state_to_ha_state=pyotgw_led_mode_to_ha_led_mode, + ), + OpenThermSelectEntityDescription( + key=OTGW_LED_E, + translation_key="led_mode_n", + translation_placeholders={"led_id": "E"}, + device_description=GATEWAY_DEVICE_DESCRIPTION, + options=list(OpenThermSelectLEDMode), + select_action=partial(set_led_mode, "E"), + convert_pyotgw_state_to_ha_state=pyotgw_led_mode_to_ha_led_mode, + ), + OpenThermSelectEntityDescription( + key=OTGW_LED_F, + translation_key="led_mode_n", + translation_placeholders={"led_id": "F"}, + device_description=GATEWAY_DEVICE_DESCRIPTION, + options=list(OpenThermSelectLEDMode), + select_action=partial(set_led_mode, "F"), + convert_pyotgw_state_to_ha_state=pyotgw_led_mode_to_ha_led_mode, + ), ) diff --git a/homeassistant/components/opentherm_gw/strings.json b/homeassistant/components/opentherm_gw/strings.json index e4d72ad8fb5..834168eb113 100644 --- a/homeassistant/components/opentherm_gw/strings.json +++ b/homeassistant/components/opentherm_gw/strings.json @@ -172,6 +172,23 @@ "ds1820": "DS1820", "dhw_block": "Block hot water" } + }, + "led_mode_n": { + "name": "LED {led_id} mode", + "state": { + "receive_any": "Receiving on any interface", + "transmit_any": "Transmitting on any interface", + "thermostat_traffic": "Traffic on the thermostat interface", + "boiler_traffic": "Traffic on the boiler interface", + "setpoint_override_active": "Setpoint override is active", + "flame_on": "Boiler flame is on", + "central_heating_on": "Central heating is on", + "hot_water_on": "Hot water is on", + "comfort_mode_on": "Comfort mode is on", + "transmit_error_detected": "Transmit error detected", + "boiler_maintenance_required": "Boiler maintenance required", + "raised_power_mode_active": "Raised power mode active" + } } }, "sensor": { diff --git a/tests/components/opentherm_gw/test_select.py b/tests/components/opentherm_gw/test_select.py index e0c4630b036..f89224b3874 100644 --- a/tests/components/opentherm_gw/test_select.py +++ b/tests/components/opentherm_gw/test_select.py @@ -1,8 +1,18 @@ """Test opentherm_gw select entities.""" +from typing import Any from unittest.mock import AsyncMock, MagicMock -from pyotgw.vars import OTGW_GPIO_A, OTGW_GPIO_B +from pyotgw.vars import ( + OTGW_GPIO_A, + OTGW_GPIO_B, + OTGW_LED_A, + OTGW_LED_B, + OTGW_LED_C, + OTGW_LED_D, + OTGW_LED_E, + OTGW_LED_F, +) import pytest from homeassistant.components.opentherm_gw import DOMAIN as OPENTHERM_DOMAIN @@ -13,7 +23,9 @@ from homeassistant.components.opentherm_gw.const import ( ) from homeassistant.components.opentherm_gw.select import ( OpenThermSelectGPIOMode, + OpenThermSelectLEDMode, PyotgwGPIOMode, + PyotgwLEDMode, ) from homeassistant.components.select import ( ATTR_OPTION, @@ -29,23 +41,90 @@ from tests.common import MockConfigEntry @pytest.mark.parametrize( - ("entity_key", "gpio_id"), + ( + "entity_key", + "target_func_name", + "target_param_1", + "target_param_2", + "resulting_state", + ), [ - (OTGW_GPIO_A, "A"), - (OTGW_GPIO_B, "B"), + ( + OTGW_GPIO_A, + "set_gpio_mode", + "A", + PyotgwGPIOMode.VCC, + OpenThermSelectGPIOMode.VCC, + ), + ( + OTGW_GPIO_B, + "set_gpio_mode", + "B", + PyotgwGPIOMode.HOME, + OpenThermSelectGPIOMode.HOME, + ), + ( + OTGW_LED_A, + "set_led_mode", + "A", + PyotgwLEDMode.TX_ANY, + OpenThermSelectLEDMode.TX_ANY, + ), + ( + OTGW_LED_B, + "set_led_mode", + "B", + PyotgwLEDMode.RX_ANY, + OpenThermSelectLEDMode.RX_ANY, + ), + ( + OTGW_LED_C, + "set_led_mode", + "C", + PyotgwLEDMode.BOILER_TRAFFIC, + OpenThermSelectLEDMode.BOILER_TRAFFIC, + ), + ( + OTGW_LED_D, + "set_led_mode", + "D", + PyotgwLEDMode.THERMOSTAT_TRAFFIC, + OpenThermSelectLEDMode.THERMOSTAT_TRAFFIC, + ), + ( + OTGW_LED_E, + "set_led_mode", + "E", + PyotgwLEDMode.FLAME_ON, + OpenThermSelectLEDMode.FLAME_ON, + ), + ( + OTGW_LED_F, + "set_led_mode", + "F", + PyotgwLEDMode.BOILER_MAINTENANCE_REQUIRED, + OpenThermSelectLEDMode.BOILER_MAINTENANCE_REQUIRED, + ), ], ) -async def test_gpio_mode_select( +async def test_select_change_value( hass: HomeAssistant, entity_registry: er.EntityRegistry, mock_config_entry: MockConfigEntry, mock_pyotgw: MagicMock, entity_key: str, - gpio_id: str, + target_func_name: str, + target_param_1: str, + target_param_2: str | int, + resulting_state: str, ) -> None: """Test GPIO mode selector.""" - mock_pyotgw.return_value.set_gpio_mode = AsyncMock(return_value=PyotgwGPIOMode.VCC) + setattr( + mock_pyotgw.return_value, + target_func_name, + AsyncMock(return_value=target_param_2), + ) mock_config_entry.add_to_hass(hass) await hass.config_entries.async_setup(mock_config_entry.entry_id) @@ -63,29 +142,56 @@ async def test_gpio_mode_select( await hass.services.async_call( SELECT_DOMAIN, SERVICE_SELECT_OPTION, - {ATTR_ENTITY_ID: select_entity_id, ATTR_OPTION: OpenThermSelectGPIOMode.VCC}, + {ATTR_ENTITY_ID: select_entity_id, ATTR_OPTION: resulting_state}, blocking=True, ) - assert hass.states.get(select_entity_id).state == OpenThermSelectGPIOMode.VCC + assert hass.states.get(select_entity_id).state == resulting_state - mock_pyotgw.return_value.set_gpio_mode.assert_awaited_once_with( - gpio_id, PyotgwGPIOMode.VCC.value - ) + target = getattr(mock_pyotgw.return_value, target_func_name) + target.assert_awaited_once_with(target_param_1, target_param_2) @pytest.mark.parametrize( - ("entity_key"), + ("entity_key", "test_value", "resulting_state"), [ - (OTGW_GPIO_A), - (OTGW_GPIO_B), + (OTGW_GPIO_A, PyotgwGPIOMode.AWAY, OpenThermSelectGPIOMode.AWAY), + (OTGW_GPIO_B, PyotgwGPIOMode.LED_F, OpenThermSelectGPIOMode.LED_F), + ( + OTGW_LED_A, + PyotgwLEDMode.SETPOINT_OVERRIDE_ACTIVE, + OpenThermSelectLEDMode.SETPOINT_OVERRIDE_ACTIVE, + ), + ( + OTGW_LED_B, + PyotgwLEDMode.CENTRAL_HEATING_ON, + OpenThermSelectLEDMode.CENTRAL_HEATING_ON, + ), + (OTGW_LED_C, PyotgwLEDMode.HOT_WATER_ON, OpenThermSelectLEDMode.HOT_WATER_ON), + ( + OTGW_LED_D, + PyotgwLEDMode.COMFORT_MODE_ON, + OpenThermSelectLEDMode.COMFORT_MODE_ON, + ), + ( + OTGW_LED_E, + PyotgwLEDMode.TX_ERROR_DETECTED, + OpenThermSelectLEDMode.TX_ERROR_DETECTED, + ), + ( + OTGW_LED_F, + PyotgwLEDMode.RAISED_POWER_MODE_ACTIVE, + OpenThermSelectLEDMode.RAISED_POWER_MODE_ACTIVE, + ), ], ) -async def test_gpio_mode_state_update( +async def test_select_state_update( hass: HomeAssistant, entity_registry: er.EntityRegistry, mock_config_entry: MockConfigEntry, mock_pyotgw: MagicMock, entity_key: str, + test_value: Any, + resulting_state: str, ) -> None: """Test GPIO mode selector.""" @@ -111,10 +217,10 @@ async def test_gpio_mode_state_update( gw_hub.update_signal, { OpenThermDeviceIdentifier.BOILER: {}, - OpenThermDeviceIdentifier.GATEWAY: {entity_key: 4}, + OpenThermDeviceIdentifier.GATEWAY: {entity_key: test_value}, OpenThermDeviceIdentifier.THERMOSTAT: {}, }, ) await hass.async_block_till_done() - assert hass.states.get(select_entity_id).state == OpenThermSelectGPIOMode.LED_F + assert hass.states.get(select_entity_id).state == resulting_state