Add energy select entities to Tessie (#120641)

This commit is contained in:
Brett Adams 2024-07-06 19:49:53 +10:00 committed by GitHub
parent 17daccd38a
commit 8f7c3da456
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 303 additions and 13 deletions

View File

@ -128,7 +128,7 @@ class TessieEnergyEntity(TessieBaseEntity):
key: str,
) -> None:
"""Initialize common aspects of a Tessie energy site entity."""
self.api = data.api
self._attr_unique_id = f"{data.id}-{key}"
self._attr_device_info = data.device

View File

@ -0,0 +1,24 @@
"""Tessie helper functions."""
from typing import Any
from tesla_fleet_api.exceptions import TeslaFleetError
from homeassistant.exceptions import HomeAssistantError
from . import _LOGGER
from .const import DOMAIN
async def handle_command(command) -> dict[str, Any]:
"""Handle a command."""
try:
result = await command
except TeslaFleetError as e:
raise HomeAssistantError(
translation_domain=DOMAIN,
translation_key="command_failed",
translation_placeholders={"message": e.message},
) from e
_LOGGER.debug("Command result: %s", result)
return result

View File

@ -2,6 +2,9 @@
from __future__ import annotations
from itertools import chain
from tesla_fleet_api.const import EnergyExportMode, EnergyOperationMode
from tessie_api import set_seat_heat
from homeassistant.components.select import SelectEntity
@ -10,7 +13,9 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback
from . import TessieConfigEntry
from .const import TessieSeatHeaterOptions
from .entity import TessieEntity
from .entity import TessieEnergyEntity, TessieEntity
from .helpers import handle_command
from .models import TessieEnergyData
SEAT_HEATERS = {
"climate_state_seat_heater_left": "front_left",
@ -29,14 +34,28 @@ async def async_setup_entry(
async_add_entities: AddEntitiesCallback,
) -> None:
"""Set up the Tessie select platform from a config entry."""
data = entry.runtime_data
async_add_entities(
TessieSeatHeaterSelectEntity(vehicle, key)
for vehicle in data.vehicles
for key in SEAT_HEATERS
if key
in vehicle.data_coordinator.data # not all vehicles have rear center or third row
chain(
(
TessieSeatHeaterSelectEntity(vehicle, key)
for vehicle in entry.runtime_data.vehicles
for key in SEAT_HEATERS
if key
in vehicle.data_coordinator.data # not all vehicles have rear center or third row
),
(
TessieOperationSelectEntity(energysite)
for energysite in entry.runtime_data.energysites
if energysite.info_coordinator.data.get("components_battery")
),
(
TessieExportRuleSelectEntity(energysite)
for energysite in entry.runtime_data.energysites
if energysite.info_coordinator.data.get("components_battery")
and energysite.info_coordinator.data.get("components_solar")
),
)
)
@ -60,3 +79,59 @@ class TessieSeatHeaterSelectEntity(TessieEntity, SelectEntity):
level = self._attr_options.index(option)
await self.run(set_seat_heat, seat=SEAT_HEATERS[self.key], level=level)
self.set((self.key, level))
class TessieOperationSelectEntity(TessieEnergyEntity, SelectEntity):
"""Select entity for operation mode select entities."""
_attr_options: list[str] = [
EnergyOperationMode.AUTONOMOUS,
EnergyOperationMode.BACKUP,
EnergyOperationMode.SELF_CONSUMPTION,
]
def __init__(
self,
data: TessieEnergyData,
) -> None:
"""Initialize the operation mode select entity."""
super().__init__(data, data.info_coordinator, "default_real_mode")
def _async_update_attrs(self) -> None:
"""Update the attributes of the entity."""
self._attr_current_option = self._value
async def async_select_option(self, option: str) -> None:
"""Change the selected option."""
await handle_command(self.api.operation(option))
self._attr_current_option = option
self.async_write_ha_state()
class TessieExportRuleSelectEntity(TessieEnergyEntity, SelectEntity):
"""Select entity for export rules select entities."""
_attr_options: list[str] = [
EnergyExportMode.NEVER,
EnergyExportMode.BATTERY_OK,
EnergyExportMode.PV_ONLY,
]
def __init__(
self,
data: TessieEnergyData,
) -> None:
"""Initialize the export rules select entity."""
super().__init__(
data, data.info_coordinator, "components_customer_preferred_export_rule"
)
def _async_update_attrs(self) -> None:
"""Update the attributes of the entity."""
self._attr_current_option = self.get(self.key, EnergyExportMode.NEVER.value)
async def async_select_option(self, option: str) -> None:
"""Change the selected option."""
await handle_command(self.api.grid_import_export(option))
self._attr_current_option = option
self.async_write_ha_state()

View File

@ -300,6 +300,22 @@
"medium": "[%key:component::tessie::entity::select::climate_state_seat_heater_left::state::medium%]",
"high": "[%key:component::tessie::entity::select::climate_state_seat_heater_left::state::high%]"
}
},
"components_customer_preferred_export_rule": {
"name": "Allow export",
"state": {
"battery_ok": "Battery",
"never": "Never",
"pv_only": "Solar only"
}
},
"default_real_mode": {
"name": "Operation mode",
"state": {
"autonomous": "Autonomous",
"backup": "Backup",
"self_consumption": "Self consumption"
}
}
},
"binary_sensor": {
@ -472,6 +488,9 @@
},
"no_cable": {
"message": "Insert cable to lock"
},
"command_failed": {
"message": "Command failed, {message}"
}
},
"issues": {

View File

@ -1,4 +1,118 @@
# serializer version: 1
# name: test_select[select.energy_site_allow_export-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': dict({
'options': list([
<EnergyExportMode.NEVER: 'never'>,
<EnergyExportMode.BATTERY_OK: 'battery_ok'>,
<EnergyExportMode.PV_ONLY: 'pv_only'>,
]),
}),
'config_entry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'select',
'entity_category': None,
'entity_id': 'select.energy_site_allow_export',
'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 export',
'platform': 'tessie',
'previous_unique_id': None,
'supported_features': 0,
'translation_key': 'components_customer_preferred_export_rule',
'unique_id': '123456-components_customer_preferred_export_rule',
'unit_of_measurement': None,
})
# ---
# name: test_select[select.energy_site_allow_export-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'friendly_name': 'Energy Site Allow export',
'options': list([
<EnergyExportMode.NEVER: 'never'>,
<EnergyExportMode.BATTERY_OK: 'battery_ok'>,
<EnergyExportMode.PV_ONLY: 'pv_only'>,
]),
}),
'context': <ANY>,
'entity_id': 'select.energy_site_allow_export',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'pv_only',
})
# ---
# name: test_select[select.energy_site_operation_mode-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': dict({
'options': list([
<EnergyOperationMode.AUTONOMOUS: 'autonomous'>,
<EnergyOperationMode.BACKUP: 'backup'>,
<EnergyOperationMode.SELF_CONSUMPTION: 'self_consumption'>,
]),
}),
'config_entry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'select',
'entity_category': None,
'entity_id': 'select.energy_site_operation_mode',
'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': 'Operation mode',
'platform': 'tessie',
'previous_unique_id': None,
'supported_features': 0,
'translation_key': 'default_real_mode',
'unique_id': '123456-default_real_mode',
'unit_of_measurement': None,
})
# ---
# name: test_select[select.energy_site_operation_mode-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'friendly_name': 'Energy Site Operation mode',
'options': list([
<EnergyOperationMode.AUTONOMOUS: 'autonomous'>,
<EnergyOperationMode.BACKUP: 'backup'>,
<EnergyOperationMode.SELF_CONSUMPTION: 'self_consumption'>,
]),
}),
'context': <ANY>,
'entity_id': 'select.energy_site_operation_mode',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'self_consumption',
})
# ---
# name: test_select[select.test_seat_heater_left-entry]
EntityRegistryEntrySnapshot({
'aliases': set({

View File

@ -4,6 +4,8 @@ from unittest.mock import patch
import pytest
from syrupy import SnapshotAssertion
from tesla_fleet_api.const import EnergyExportMode, EnergyOperationMode
from tesla_fleet_api.exceptions import UnsupportedVehicle
from homeassistant.components.select import (
DOMAIN as SELECT_DOMAIN,
@ -27,9 +29,8 @@ async def test_select(
assert_entities(hass, entry.entry_id, entity_registry, snapshot)
entity_id = "select.test_seat_heater_left"
# Test changing select
entity_id = "select.test_seat_heater_left"
with patch(
"homeassistant.components.tessie.select.set_seat_heat",
return_value=TEST_RESPONSE,
@ -45,14 +46,48 @@ async def test_select(
assert mock_set.call_args[1]["level"] == 1
assert hass.states.get(entity_id) == snapshot(name=SERVICE_SELECT_OPTION)
# Test site operation mode
entity_id = "select.energy_site_operation_mode"
with patch(
"homeassistant.components.teslemetry.EnergySpecific.operation",
return_value=TEST_RESPONSE,
) as call:
await hass.services.async_call(
SELECT_DOMAIN,
SERVICE_SELECT_OPTION,
{
ATTR_ENTITY_ID: entity_id,
ATTR_OPTION: EnergyOperationMode.AUTONOMOUS.value,
},
blocking=True,
)
assert (state := hass.states.get(entity_id))
assert state.state == EnergyOperationMode.AUTONOMOUS.value
call.assert_called_once()
# Test site export mode
entity_id = "select.energy_site_allow_export"
with patch(
"homeassistant.components.teslemetry.EnergySpecific.grid_import_export",
return_value=TEST_RESPONSE,
) as call:
await hass.services.async_call(
SELECT_DOMAIN,
SERVICE_SELECT_OPTION,
{ATTR_ENTITY_ID: entity_id, ATTR_OPTION: EnergyExportMode.BATTERY_OK.value},
blocking=True,
)
assert (state := hass.states.get(entity_id))
assert state.state == EnergyExportMode.BATTERY_OK.value
call.assert_called_once()
async def test_errors(hass: HomeAssistant) -> None:
"""Tests unknown error is handled."""
await setup_platform(hass, [Platform.SELECT])
entity_id = "select.test_seat_heater_left"
# Test setting cover open with unknown error
# Test changing vehicle select with unknown error
with (
patch(
"homeassistant.components.tessie.select.set_seat_heat",
@ -63,8 +98,31 @@ async def test_errors(hass: HomeAssistant) -> None:
await hass.services.async_call(
SELECT_DOMAIN,
SERVICE_SELECT_OPTION,
{ATTR_ENTITY_ID: [entity_id], ATTR_OPTION: TessieSeatHeaterOptions.LOW},
{
ATTR_ENTITY_ID: ["select.test_seat_heater_left"],
ATTR_OPTION: TessieSeatHeaterOptions.LOW,
},
blocking=True,
)
mock_set.assert_called_once()
assert error.value.__cause__ == ERROR_UNKNOWN
# Test changing energy select with unknown error
with (
patch(
"homeassistant.components.tessie.EnergySpecific.operation",
side_effect=UnsupportedVehicle,
) as mock_set,
pytest.raises(HomeAssistantError) as error,
):
await hass.services.async_call(
SELECT_DOMAIN,
SERVICE_SELECT_OPTION,
{
ATTR_ENTITY_ID: ["select.energy_site_operation_mode"],
ATTR_OPTION: EnergyOperationMode.AUTONOMOUS.value,
},
blocking=True,
)
mock_set.assert_called_once()
assert isinstance(error.value.__cause__, UnsupportedVehicle)