diff --git a/homeassistant/components/my_pv/switch.py b/homeassistant/components/my_pv/switch.py index e461b5405b76..e7fc9e8ef84d 100644 --- a/homeassistant/components/my_pv/switch.py +++ b/homeassistant/components/my_pv/switch.py @@ -1,6 +1,8 @@ """Creates Switch entities for the my-PV Home Assistant integration.""" -from typing import Any, override +from typing import Any, Final, override + +from my_pv import MyPVDeviceMainMode from homeassistant.components.switch import ( SwitchDeviceClass, @@ -14,7 +16,20 @@ from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback from . import MyPVConfigEntry from .const import DOMAIN -from .entity import MyPVSetupEntity +from .entity import MyPVBaseEntity, MyPVSetupEntity + +ENTITY_DESCRIPTIONS: Final[dict[str, SwitchEntityDescription]] = { + "bstmode": SwitchEntityDescription( + key="bstmode", + entity_category=EntityCategory.CONFIG, + device_class=SwitchDeviceClass.SWITCH, + translation_key="bstmode", + ), + "devmode": SwitchEntityDescription( + key="devmode", + device_class=SwitchDeviceClass.SWITCH, + ), +} async def async_setup_entry( @@ -24,20 +39,26 @@ async def async_setup_entry( ) -> None: """Set up the my-PV switch.""" coordinator = config_entry.runtime_data - entities = [] + entities: list[SwitchEntity] = [] config = coordinator.device.get_setup_configuration("bstmode") if config and config.get("type") == "boolean": - entity_description = SwitchEntityDescription( - key="bstmode", - entity_category=EntityCategory.CONFIG, - device_class=SwitchDeviceClass.SWITCH, - translation_key="bstmode", - ) entities.append( MyPVSwitch( coordinator, - entity_description, + ENTITY_DESCRIPTIONS["bstmode"], + coordinator.device.serial_number, + ) + ) + + if ( + coordinator.device.supports_main_mode(MyPVDeviceMainMode.HOT_WATER) + and coordinator.device.current_temperature is None + ): + entities.append( + MyPVWaterHeaterSwitch( + coordinator, + ENTITY_DESCRIPTIONS["devmode"], coordinator.device.serial_number, ) ) @@ -74,3 +95,31 @@ class MyPVSwitch(MyPVSetupEntity, SwitchEntity): raise HomeAssistantError( translation_domain=DOMAIN, translation_key="unknown_error" ) + + +class MyPVWaterHeaterSwitch(MyPVBaseEntity, SwitchEntity): + """my-PV water heater switch.""" + + _attr_name = None + + @property + @override + def is_on(self) -> bool | None: + """Return if the switch is on.""" + return self.coordinator.device.is_on + + @override + async def async_turn_on(self, **kwargs: Any) -> None: + """Turn the water heater on.""" + if not await self.coordinator.turn_on(): + raise HomeAssistantError( + translation_domain=DOMAIN, translation_key="unknown_error" + ) + + @override + async def async_turn_off(self, **kwargs: Any) -> None: + """Turn the water heater off.""" + if not await self.coordinator.turn_off(): + raise HomeAssistantError( + translation_domain=DOMAIN, translation_key="unknown_error" + ) diff --git a/tests/components/my_pv/test_switch.py b/tests/components/my_pv/test_switch.py index 10af6f7aadec..fd8787666123 100644 --- a/tests/components/my_pv/test_switch.py +++ b/tests/components/my_pv/test_switch.py @@ -17,7 +17,13 @@ from homeassistant.components.switch import ( SERVICE_TURN_OFF, SERVICE_TURN_ON, ) -from homeassistant.const import ATTR_ENTITY_ID, STATE_UNAVAILABLE, Platform +from homeassistant.const import ( + ATTR_ENTITY_ID, + STATE_OFF, + STATE_ON, + STATE_UNAVAILABLE, + Platform, +) from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryAuthFailed, HomeAssistantError from homeassistant.helpers import entity_registry as er @@ -37,16 +43,15 @@ def setup_values(setup_values: dict[str, Any]) -> dict[str, Any]: return {**setup_values, "bstmode": False} -@pytest.mark.usefixtures("mock_my_pv_client") @pytest.mark.usefixtures("entity_registry_enabled_by_default") async def test_switch( hass: HomeAssistant, mock_config_entry: MockConfigEntry, + mock_my_pv_client: AsyncMock, snapshot: SnapshotAssertion, entity_registry: er.EntityRegistry, ) -> None: """Test successful setup of a switch.""" - with patch("homeassistant.components.my_pv.PLATFORMS", [Platform.SWITCH]): mock_config_entry.add_to_hass(hass) @@ -56,23 +61,69 @@ async def test_switch( await snapshot_platform(hass, entity_registry, snapshot, mock_config_entry.entry_id) +async def test_water_heater_switch_no_temp_sensor( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, + mock_my_pv_client: AsyncMock, +) -> None: + """Test if a switch for the water heater is created when there is no temperature sensor installed.""" + with patch("homeassistant.components.my_pv.PLATFORMS", [Platform.SWITCH]): + mock_config_entry.add_to_hass(hass) + + mock_my_pv_client.current_temperature = None + + assert await hass.config_entries.async_setup(mock_config_entry.entry_id) + await hass.async_block_till_done() + + state = hass.states.get("switch.my_pv_ac_elwa_2") + assert state.state == STATE_ON + + +async def test_water_heater_switch_temp_sensor( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, + mock_my_pv_client: AsyncMock, +) -> None: + """Test that no switch for the water heater is created when there is a temperature sensor installed.""" + with patch("homeassistant.components.my_pv.PLATFORMS", [Platform.SWITCH]): + mock_config_entry.add_to_hass(hass) + + mock_my_pv_client.current_temperature = 30 + + assert await hass.config_entries.async_setup(mock_config_entry.entry_id) + await hass.async_block_till_done() + + state = hass.states.get("switch.my_pv_ac_elwa_2") + assert state is None + + +@pytest.mark.parametrize( + ("entity", "method", "expected_args"), + [ + ("switch.my_pv_ac_elwa_2", "turn_on", ()), + ("switch.my_pv_ac_elwa_2_boost_mode", "set_setup_value", ("bstmode", True)), + ], +) @pytest.mark.usefixtures("entity_registry_enabled_by_default") async def test_switch_unavailable_not_connected( hass: HomeAssistant, mock_config_entry: MockConfigEntry, mock_my_pv_client: AsyncMock, + entity: str, + method: str, + expected_args: tuple, ) -> None: """Test if a switch is unavailable when not connected.""" - with patch("homeassistant.components.my_pv.PLATFORMS", [Platform.SWITCH]): mock_config_entry.add_to_hass(hass) + mock_my_pv_client.current_temperature = None mock_my_pv_client.connected = False assert await hass.config_entries.async_setup(mock_config_entry.entry_id) await hass.async_block_till_done() - state = hass.states.get("switch.my_pv_ac_elwa_2_boost_mode") + state = hass.states.get(entity) assert state.state == STATE_UNAVAILABLE @@ -83,7 +134,6 @@ async def test_switch_unavailable_setup_value_none( mock_my_pv_client: AsyncMock, ) -> None: """Test if a switch is unavailable when setup value is None.""" - with patch("homeassistant.components.my_pv.PLATFORMS", [Platform.SWITCH]): mock_config_entry.add_to_hass(hass) @@ -96,99 +146,156 @@ async def test_switch_unavailable_setup_value_none( assert state.state == STATE_UNAVAILABLE +@pytest.mark.parametrize( + ("entity", "method", "expected_args"), + [ + ("switch.my_pv_ac_elwa_2", "turn_on", ()), + ("switch.my_pv_ac_elwa_2_boost_mode", "set_setup_value", ("bstmode", True)), + ], +) @pytest.mark.usefixtures("entity_registry_enabled_by_default") async def test_switch_turn_on( hass: HomeAssistant, mock_config_entry: MockConfigEntry, mock_my_pv_client: AsyncMock, + entity: str, + method: str, + expected_args: tuple, ) -> None: - """Test setting value.""" + """Test turn on.""" with patch("homeassistant.components.my_pv.PLATFORMS", [Platform.SWITCH]): mock_config_entry.add_to_hass(hass) + mock_my_pv_client.current_temperature = None + mock_my_pv_client.is_on = False + mock_my_pv_client.get_setup_value = Mock(return_value=False) + assert await hass.config_entries.async_setup(mock_config_entry.entry_id) await hass.async_block_till_done() - mock_my_pv_client.get_setup_value = Mock(return_value=False) + client_method = getattr(mock_my_pv_client, method) await hass.services.async_call( SWITCH_DOMAIN, SERVICE_TURN_ON, { - ATTR_ENTITY_ID: "switch.my_pv_ac_elwa_2_boost_mode", + ATTR_ENTITY_ID: entity, }, blocking=True, ) - mock_my_pv_client.set_setup_value.assert_awaited_once_with("bstmode", True) + client_method.assert_awaited_once_with(*expected_args) +@pytest.mark.parametrize( + ("entity", "method", "expected_args"), + [ + ("switch.my_pv_ac_elwa_2", "turn_off", ()), + ("switch.my_pv_ac_elwa_2_boost_mode", "set_setup_value", ("bstmode", False)), + ], +) @pytest.mark.usefixtures("entity_registry_enabled_by_default") async def test_switch_turn_off( hass: HomeAssistant, mock_config_entry: MockConfigEntry, mock_my_pv_client: AsyncMock, + entity: str, + method: str, + expected_args: tuple, ) -> None: - """Test setting value.""" + """Test turn off.""" with patch("homeassistant.components.my_pv.PLATFORMS", [Platform.SWITCH]): mock_config_entry.add_to_hass(hass) + mock_my_pv_client.current_temperature = None + mock_my_pv_client.is_on = True + mock_my_pv_client.get_setup_value = Mock(return_value=True) + assert await hass.config_entries.async_setup(mock_config_entry.entry_id) await hass.async_block_till_done() - mock_my_pv_client.get_setup_value = Mock(return_value=True) + client_method = getattr(mock_my_pv_client, method) await hass.services.async_call( SWITCH_DOMAIN, SERVICE_TURN_OFF, { - ATTR_ENTITY_ID: "switch.my_pv_ac_elwa_2_boost_mode", + ATTR_ENTITY_ID: entity, }, blocking=True, ) - mock_my_pv_client.set_setup_value.assert_awaited_once_with("bstmode", False) + client_method.assert_awaited_once_with(*expected_args) +@pytest.mark.parametrize( + ("entity", "method", "expected_args"), + [ + ("switch.my_pv_ac_elwa_2", "turn_on", ()), + ("switch.my_pv_ac_elwa_2_boost_mode", "set_setup_value", ("bstmode", True)), + ], +) @pytest.mark.usefixtures("entity_registry_enabled_by_default") async def test_switch_toggle( hass: HomeAssistant, mock_config_entry: MockConfigEntry, mock_my_pv_client: AsyncMock, + entity: str, + method: str, + expected_args: tuple, ) -> None: - """Test setting value.""" + """Test toggle.""" with patch("homeassistant.components.my_pv.PLATFORMS", [Platform.SWITCH]): mock_config_entry.add_to_hass(hass) + mock_my_pv_client.current_temperature = None + mock_my_pv_client.is_on = False + assert await hass.config_entries.async_setup(mock_config_entry.entry_id) await hass.async_block_till_done() - state = hass.states.get("switch.my_pv_ac_elwa_2_boost_mode") - assert state.state == "off" + state = hass.states.get(entity) + assert state.state == STATE_OFF + + client_method = getattr(mock_my_pv_client, method) await hass.services.async_call( SWITCH_DOMAIN, SERVICE_TOGGLE, { - ATTR_ENTITY_ID: "switch.my_pv_ac_elwa_2_boost_mode", + ATTR_ENTITY_ID: entity, }, blocking=True, ) - mock_my_pv_client.set_setup_value.assert_awaited_once_with("bstmode", True) + client_method.assert_awaited_once_with(*expected_args) +@pytest.mark.parametrize( + ("entity", "method", "expected_args"), + [ + ("switch.my_pv_ac_elwa_2", "turn_on", ()), + ("switch.my_pv_ac_elwa_2_boost_mode", "set_setup_value", ("bstmode", True)), + ], +) @pytest.mark.usefixtures("entity_registry_enabled_by_default") async def test_switch_toggle_returns_false( hass: HomeAssistant, mock_config_entry: MockConfigEntry, mock_my_pv_client: AsyncMock, + entity: str, + method: str, + expected_args: tuple, ) -> None: - """Test for HomeAssistantError when set_setup_value returns false.""" + """Test for HomeAssistantError when toggle method returns false.""" with patch("homeassistant.components.my_pv.PLATFORMS", [Platform.SWITCH]): mock_config_entry.add_to_hass(hass) + mock_my_pv_client.current_temperature = None + mock_my_pv_client.is_on = False + assert await hass.config_entries.async_setup(mock_config_entry.entry_id) await hass.async_block_till_done() - mock_my_pv_client.set_setup_value = AsyncMock(return_value=False) + client_method = getattr(mock_my_pv_client, method) + client_method.return_value = False with ( pytest.raises(HomeAssistantError), @@ -197,22 +304,74 @@ async def test_switch_toggle_returns_false( SWITCH_DOMAIN, SERVICE_TOGGLE, { - ATTR_ENTITY_ID: "switch.my_pv_ac_elwa_2_boost_mode", + ATTR_ENTITY_ID: entity, }, blocking=True, ) - mock_my_pv_client.set_setup_value.assert_awaited_once_with("bstmode", True) + client_method.assert_awaited_once_with(*expected_args) - state = hass.states.get("switch.my_pv_ac_elwa_2_boost_mode") - assert state.state == "off" + state = hass.states.get(entity) + assert state.state == STATE_OFF @pytest.mark.parametrize( - ("error", "expected_ha_error"), + ( + "entity", + "method", + "expected_args", + "error", + "expected_ha_error", + "expected_state", + ), [ - (MyPVConnectionError(), HomeAssistantError), - (MyPVAuthenticationError(), ConfigEntryAuthFailed), - (MyPVTooManyRequestsError(), HomeAssistantError), + ( + "switch.my_pv_ac_elwa_2", + "turn_off", + (), + MyPVConnectionError(), + HomeAssistantError, + STATE_ON, + ), + ( + "switch.my_pv_ac_elwa_2", + "turn_off", + (), + MyPVAuthenticationError(), + ConfigEntryAuthFailed, + STATE_ON, + ), + ( + "switch.my_pv_ac_elwa_2", + "turn_off", + (), + MyPVTooManyRequestsError(), + HomeAssistantError, + STATE_ON, + ), + ( + "switch.my_pv_ac_elwa_2_boost_mode", + "set_setup_value", + ("bstmode", True), + MyPVConnectionError(), + HomeAssistantError, + STATE_OFF, + ), + ( + "switch.my_pv_ac_elwa_2_boost_mode", + "set_setup_value", + ("bstmode", True), + MyPVAuthenticationError(), + ConfigEntryAuthFailed, + STATE_OFF, + ), + ( + "switch.my_pv_ac_elwa_2_boost_mode", + "set_setup_value", + ("bstmode", True), + MyPVTooManyRequestsError(), + HomeAssistantError, + STATE_OFF, + ), ], ) @pytest.mark.usefixtures("entity_registry_enabled_by_default") @@ -220,17 +379,24 @@ async def test_switch_toggle_raises_error( hass: HomeAssistant, mock_config_entry: MockConfigEntry, mock_my_pv_client: AsyncMock, + entity: str, + method: str, + expected_args: tuple, error: MyPVConnectionError | MyPVAuthenticationError, expected_ha_error: type[HomeAssistantError], + expected_state: str, ) -> None: - """Test for HomeAssistantError when set_setup_value raises error.""" + """Test for HomeAssistantError when method raises error.""" with patch("homeassistant.components.my_pv.PLATFORMS", [Platform.SWITCH]): mock_config_entry.add_to_hass(hass) + mock_my_pv_client.current_temperature = None + assert await hass.config_entries.async_setup(mock_config_entry.entry_id) await hass.async_block_till_done() - mock_my_pv_client.set_setup_value = AsyncMock(side_effect=error) + client_method = getattr(mock_my_pv_client, method) + client_method.side_effect = error with ( pytest.raises(expected_ha_error), @@ -238,10 +404,10 @@ async def test_switch_toggle_raises_error( await hass.services.async_call( SWITCH_DOMAIN, SERVICE_TOGGLE, - {ATTR_ENTITY_ID: "switch.my_pv_ac_elwa_2_boost_mode"}, + {ATTR_ENTITY_ID: entity}, blocking=True, ) - mock_my_pv_client.set_setup_value.assert_awaited_once_with("bstmode", True) + client_method.assert_awaited_once_with(*expected_args) - state = hass.states.get("switch.my_pv_ac_elwa_2_boost_mode") - assert state.state == "off" + state = hass.states.get(entity) + assert state.state == expected_state diff --git a/tests/components/my_pv/test_water_heater.py b/tests/components/my_pv/test_water_heater.py index 32569ab2ccc2..19c994a6ed3e 100644 --- a/tests/components/my_pv/test_water_heater.py +++ b/tests/components/my_pv/test_water_heater.py @@ -56,7 +56,7 @@ async def test_water_heater_no_temp_sensor( mock_config_entry: MockConfigEntry, mock_my_pv_client: AsyncMock, ) -> None: - """Test if a water_heater is not created when there is no temperature sensor connected.""" + """Test if a water_heater is not created when there is no temperature sensor installed.""" mock_config_entry.add_to_hass(hass) mock_my_pv_client.current_temperature = None