Add energy switch entities to Tessie (#121360)

This commit is contained in:
Brett Adams 2024-07-06 20:45:27 +10:00 committed by GitHub
parent e04d612874
commit f46c127423
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 245 additions and 4 deletions

View File

@ -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"
}
}
}
}

View File

@ -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": {

View File

@ -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()

View File

@ -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': <ANY>,
'device_class': None,
'device_id': <ANY>,
'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': <ANY>,
'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': <ANY>,
'entity_id': 'switch.energy_site_allow_charging_from_grid',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'off',
})
# ---
# name: test_switches[switch.energy_site_storm_watch-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': None,
'config_entry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'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': <ANY>,
'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': <ANY>,
'entity_id': 'switch.energy_site_storm_watch',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'on',
})
# ---
# name: test_switches[switch.test_charge-entry]
EntityRegistryEntrySnapshot({
'aliases': set({

View File

@ -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()