From f46c1274238b85581f270da2ac9989da725f98d0 Mon Sep 17 00:00:00 2001 From: Brett Adams Date: Sat, 6 Jul 2024 20:45:27 +1000 Subject: [PATCH] Add energy switch entities to Tessie (#121360) --- homeassistant/components/tessie/icons.json | 6 ++ homeassistant/components/tessie/strings.json | 6 ++ homeassistant/components/tessie/switch.py | 87 +++++++++++++++++- .../tessie/snapshots/test_switch.ambr | 92 +++++++++++++++++++ tests/components/tessie/test_switch.py | 58 +++++++++++- 5 files changed, 245 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/tessie/icons.json b/homeassistant/components/tessie/icons.json index 8c6635eef47..a967c70e285 100644 --- a/homeassistant/components/tessie/icons.json +++ b/homeassistant/components/tessie/icons.json @@ -245,6 +245,12 @@ }, "charge_state_charge_enable_request": { "default": "mdi:ev-plug-ccs2" + }, + "components_disallow_charge_from_grid_with_solar_installed": { + "state": { + "false": "mdi:transmission-tower", + "true": "mdi:solar-power" + } } } } diff --git a/homeassistant/components/tessie/strings.json b/homeassistant/components/tessie/strings.json index 7c28b5df344..72f72558792 100644 --- a/homeassistant/components/tessie/strings.json +++ b/homeassistant/components/tessie/strings.json @@ -448,6 +448,12 @@ }, "climate_state_steering_wheel_heater": { "name": "Steering wheel heater" + }, + "components_disallow_charge_from_grid_with_solar_installed": { + "name": "Allow charging from grid" + }, + "user_settings_storm_mode_enabled": { + "name": "Storm watch" } }, "number": { diff --git a/homeassistant/components/tessie/switch.py b/homeassistant/components/tessie/switch.py index 03bd018cd83..ea1b804a86d 100644 --- a/homeassistant/components/tessie/switch.py +++ b/homeassistant/components/tessie/switch.py @@ -29,8 +29,9 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback from . import TessieConfigEntry -from .entity import TessieEntity -from .models import TessieVehicleData +from .entity import TessieEnergyEntity, TessieEntity +from .helpers import handle_command +from .models import TessieEnergyData, TessieVehicleData @dataclass(frozen=True, kw_only=True) @@ -90,6 +91,17 @@ async def async_setup_entry( TessieChargeSwitchEntity(vehicle, CHARGE_DESCRIPTION) for vehicle in entry.runtime_data.vehicles ), + ( + TessieChargeFromGridSwitchEntity(energysite) + for energysite in entry.runtime_data.energysites + if energysite.info_coordinator.data.get("components_battery") + and energysite.info_coordinator.data.get("components_solar") + ), + ( + TessieStormModeSwitchEntity(energysite) + for energysite in entry.runtime_data.energysites + if energysite.info_coordinator.data.get("components_storm_mode_capable") + ), ) ) @@ -135,3 +147,74 @@ class TessieChargeSwitchEntity(TessieSwitchEntity): if (charge := self.get("charge_state_user_charge_enable_request")) is not None: return charge return self._value + + +class TessieChargeFromGridSwitchEntity(TessieEnergyEntity, SwitchEntity): + """Entity class for Charge From Grid switch.""" + + def __init__( + self, + data: TessieEnergyData, + ) -> None: + """Initialize the switch.""" + super().__init__( + data, + data.info_coordinator, + "components_disallow_charge_from_grid_with_solar_installed", + ) + + def _async_update_attrs(self) -> None: + """Update the attributes of the entity.""" + # When disallow_charge_from_grid_with_solar_installed is missing, its Off. + # But this sensor is flipped to match how the Tesla app works. + self._attr_is_on = not self.get(self.key, False) + + async def async_turn_on(self, **kwargs: Any) -> None: + """Turn on the Switch.""" + await handle_command( + self.api.grid_import_export( + disallow_charge_from_grid_with_solar_installed=False + ) + ) + self._attr_is_on = True + self.async_write_ha_state() + + async def async_turn_off(self, **kwargs: Any) -> None: + """Turn off the Switch.""" + await handle_command( + self.api.grid_import_export( + disallow_charge_from_grid_with_solar_installed=True + ) + ) + self._attr_is_on = False + self.async_write_ha_state() + + +class TessieStormModeSwitchEntity(TessieEnergyEntity, SwitchEntity): + """Entity class for Storm Mode switch.""" + + def __init__( + self, + data: TessieEnergyData, + ) -> None: + """Initialize the switch.""" + super().__init__( + data, data.info_coordinator, "user_settings_storm_mode_enabled" + ) + + def _async_update_attrs(self) -> None: + """Update the attributes of the sensor.""" + self._attr_available = self._value is not None + self._attr_is_on = bool(self._value) + + async def async_turn_on(self, **kwargs: Any) -> None: + """Turn on the Switch.""" + await handle_command(self.api.storm_mode(enabled=True)) + self._attr_is_on = True + self.async_write_ha_state() + + async def async_turn_off(self, **kwargs: Any) -> None: + """Turn off the Switch.""" + await handle_command(self.api.storm_mode(enabled=False)) + self._attr_is_on = False + self.async_write_ha_state() diff --git a/tests/components/tessie/snapshots/test_switch.ambr b/tests/components/tessie/snapshots/test_switch.ambr index db06e028198..3b7a3623de8 100644 --- a/tests/components/tessie/snapshots/test_switch.ambr +++ b/tests/components/tessie/snapshots/test_switch.ambr @@ -1,4 +1,96 @@ # serializer version: 1 +# name: test_switches[switch.energy_site_allow_charging_from_grid-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': None, + 'entity_id': 'switch.energy_site_allow_charging_from_grid', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Allow charging from grid', + 'platform': 'tessie', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'components_disallow_charge_from_grid_with_solar_installed', + 'unique_id': '123456-components_disallow_charge_from_grid_with_solar_installed', + 'unit_of_measurement': None, + }) +# --- +# name: test_switches[switch.energy_site_allow_charging_from_grid-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Energy Site Allow charging from grid', + }), + 'context': , + 'entity_id': 'switch.energy_site_allow_charging_from_grid', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_switches[switch.energy_site_storm_watch-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': None, + 'entity_id': 'switch.energy_site_storm_watch', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Storm watch', + 'platform': 'tessie', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'user_settings_storm_mode_enabled', + 'unique_id': '123456-user_settings_storm_mode_enabled', + 'unit_of_measurement': None, + }) +# --- +# name: test_switches[switch.energy_site_storm_watch-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Energy Site Storm watch', + }), + 'context': , + 'entity_id': 'switch.energy_site_storm_watch', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- # name: test_switches[switch.test_charge-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ diff --git a/tests/components/tessie/test_switch.py b/tests/components/tessie/test_switch.py index 907be29ddcc..499e529b2e8 100644 --- a/tests/components/tessie/test_switch.py +++ b/tests/components/tessie/test_switch.py @@ -2,6 +2,7 @@ from unittest.mock import patch +import pytest from syrupy import SnapshotAssertion from homeassistant.components.switch import ( @@ -9,11 +10,11 @@ from homeassistant.components.switch import ( SERVICE_TURN_OFF, SERVICE_TURN_ON, ) -from homeassistant.const import ATTR_ENTITY_ID, Platform +from homeassistant.const import ATTR_ENTITY_ID, STATE_OFF, STATE_ON, Platform from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er -from .common import assert_entities, setup_platform +from .common import RESPONSE_OK, assert_entities, setup_platform async def test_switches( @@ -52,3 +53,56 @@ async def test_switches( mock_stop_charging.assert_called_once() assert hass.states.get(entity_id) == snapshot(name=SERVICE_TURN_OFF) + + +@pytest.mark.usefixtures("entity_registry_enabled_by_default") +@pytest.mark.parametrize( + ("name", "on", "off"), + [ + ( + "energy_site_storm_watch", + "EnergySpecific.storm_mode", + "EnergySpecific.storm_mode", + ), + ( + "energy_site_allow_charging_from_grid", + "EnergySpecific.grid_import_export", + "EnergySpecific.grid_import_export", + ), + ], +) +async def test_switch_services( + hass: HomeAssistant, name: str, on: str, off: str +) -> None: + """Tests that the switch service calls work.""" + + await setup_platform(hass, [Platform.SWITCH]) + + entity_id = f"switch.{name}" + with patch( + f"homeassistant.components.teslemetry.{on}", + return_value=RESPONSE_OK, + ) as call: + await hass.services.async_call( + SWITCH_DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: entity_id}, + blocking=True, + ) + state = hass.states.get(entity_id) + assert state.state == STATE_ON + call.assert_called_once() + + with patch( + f"homeassistant.components.teslemetry.{off}", + return_value=RESPONSE_OK, + ) as call: + await hass.services.async_call( + SWITCH_DOMAIN, + SERVICE_TURN_OFF, + {ATTR_ENTITY_ID: entity_id}, + blocking=True, + ) + state = hass.states.get(entity_id) + assert state.state == STATE_OFF + call.assert_called_once()