Files
core/tests/components/teslemetry/test_select.py

304 lines
10 KiB
Python

"""Test the Teslemetry select platform."""
from copy import deepcopy
from unittest.mock import AsyncMock, patch
from freezegun.api import FrozenDateTimeFactory
import pytest
from syrupy.assertion import SnapshotAssertion
from tesla_fleet_api.const import EnergyExportMode, EnergyOperationMode
from teslemetry_stream.const import Signal
from homeassistant.components.select import (
ATTR_OPTION,
DOMAIN as SELECT_DOMAIN,
SERVICE_SELECT_OPTION,
)
from homeassistant.components.teslemetry.coordinator import ENERGY_INFO_INTERVAL
from homeassistant.components.teslemetry.select import LOW
from homeassistant.const import ATTR_ENTITY_ID, STATE_UNKNOWN, Platform
from homeassistant.core import HomeAssistant
from homeassistant.helpers import entity_registry as er
from . import assert_entities, reload_platform, setup_platform
from .const import COMMAND_OK, SITE_INFO, VEHICLE_DATA_ALT
from tests.common import async_fire_time_changed
@pytest.mark.usefixtures("entity_registry_enabled_by_default")
async def test_select(
hass: HomeAssistant,
snapshot: SnapshotAssertion,
entity_registry: er.EntityRegistry,
mock_legacy: AsyncMock,
) -> None:
"""Tests that the select entities are correct."""
entry = await setup_platform(hass, [Platform.SELECT])
assert_entities(hass, entry.entry_id, entity_registry, snapshot)
async def test_select_services(hass: HomeAssistant, mock_vehicle_data) -> None:
"""Tests that the select services work."""
mock_vehicle_data.return_value = VEHICLE_DATA_ALT
await setup_platform(hass, [Platform.SELECT])
entity_id = "select.test_seat_heater_front_left"
with patch(
"tesla_fleet_api.teslemetry.Vehicle.remote_seat_heater_request",
return_value=COMMAND_OK,
) as call:
await hass.services.async_call(
SELECT_DOMAIN,
SERVICE_SELECT_OPTION,
{ATTR_ENTITY_ID: entity_id, ATTR_OPTION: LOW},
blocking=True,
)
state = hass.states.get(entity_id)
assert state.state == LOW
call.assert_called_once()
entity_id = "select.test_steering_wheel_heater"
with patch(
"tesla_fleet_api.teslemetry.Vehicle.remote_steering_wheel_heat_level_request",
return_value=COMMAND_OK,
) as call:
await hass.services.async_call(
SELECT_DOMAIN,
SERVICE_SELECT_OPTION,
{ATTR_ENTITY_ID: entity_id, ATTR_OPTION: LOW},
blocking=True,
)
state = hass.states.get(entity_id)
assert state.state == LOW
call.assert_called_once()
entity_id = "select.energy_site_operation_mode"
with patch(
"tesla_fleet_api.teslemetry.EnergySite.operation",
return_value=COMMAND_OK,
) as call:
await hass.services.async_call(
SELECT_DOMAIN,
SERVICE_SELECT_OPTION,
{
ATTR_ENTITY_ID: entity_id,
ATTR_OPTION: EnergyOperationMode.AUTONOMOUS.value,
},
blocking=True,
)
state = hass.states.get(entity_id)
assert state.state == EnergyOperationMode.AUTONOMOUS.value
call.assert_called_once()
entity_id = "select.energy_site_allow_export"
with patch(
"tesla_fleet_api.teslemetry.EnergySite.grid_import_export",
return_value=COMMAND_OK,
) 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,
)
state = hass.states.get(entity_id)
assert state.state == EnergyExportMode.BATTERY_OK.value
call.assert_called_once()
async def test_select_invalid_data(
hass: HomeAssistant,
snapshot: SnapshotAssertion,
entity_registry: er.EntityRegistry,
mock_vehicle_data: AsyncMock,
mock_legacy: AsyncMock,
) -> None:
"""Tests that the select entities handle invalid data."""
broken_data = VEHICLE_DATA_ALT.copy()
broken_data["response"]["climate_state"]["seat_heater_left"] = "green"
broken_data["response"]["climate_state"]["steering_wheel_heat_level"] = "yellow"
mock_vehicle_data.return_value = broken_data
await setup_platform(hass, [Platform.SELECT])
state = hass.states.get("select.test_seat_heater_front_left")
assert state.state == STATE_UNKNOWN
state = hass.states.get("select.test_steering_wheel_heater")
assert state.state == STATE_UNKNOWN
@pytest.mark.usefixtures("entity_registry_enabled_by_default")
async def test_select_streaming(
hass: HomeAssistant,
snapshot: SnapshotAssertion,
mock_vehicle_data: AsyncMock,
mock_add_listener: AsyncMock,
) -> None:
"""Tests that the select entities with streaming are correct."""
entry = await setup_platform(hass, [Platform.SELECT])
# Stream update
mock_add_listener.send(
{
"vin": VEHICLE_DATA_ALT["response"]["vin"],
"data": {
Signal.SEAT_HEATER_LEFT: 0,
Signal.SEAT_HEATER_RIGHT: 1,
Signal.SEAT_HEATER_REAR_LEFT: 2,
Signal.SEAT_HEATER_REAR_RIGHT: 3,
Signal.HVAC_STEERING_WHEEL_HEAT_LEVEL: 0,
},
"createdAt": "2024-10-04T10:45:17.537Z",
}
)
await hass.async_block_till_done()
await reload_platform(hass, entry, [Platform.SELECT])
# Assert the entities restored their values
for entity_id in (
"select.test_seat_heater_front_left",
"select.test_seat_heater_front_right",
"select.test_seat_heater_rear_left",
"select.test_seat_heater_rear_center",
"select.test_seat_heater_rear_right",
"select.test_steering_wheel_heater",
):
state = hass.states.get(entity_id)
assert state.state == snapshot(name=entity_id)
async def test_export_rule_restore(
hass: HomeAssistant,
mock_site_info: AsyncMock,
) -> None:
"""Test export rule entity when value is missing due to VPP enrollment."""
# Mock energy site with missing export rule (VPP scenario)
vpp_site_info = deepcopy(SITE_INFO)
# Remove the customer_preferred_export_rule to simulate VPP enrollment
del vpp_site_info["response"]["components"]["customer_preferred_export_rule"]
mock_site_info.side_effect = lambda: vpp_site_info
# Set up platform
entry = await setup_platform(hass, [Platform.SELECT])
# Entity should exist but have no current option initially
entity_id = "select.energy_site_allow_export"
state = hass.states.get(entity_id)
assert state is not None
assert state.state == STATE_UNKNOWN
# Test service call works even when value is missing (VPP enrolled)
with patch(
"tesla_fleet_api.teslemetry.EnergySite.grid_import_export",
return_value=COMMAND_OK,
) 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,
)
state = hass.states.get(entity_id)
assert state.state == EnergyExportMode.BATTERY_OK.value
call.assert_called_once()
# Reload the platform to test state restoration
await reload_platform(hass, entry, [Platform.SELECT])
# The entity should restore the previous state since API value is still missing
state = hass.states.get(entity_id)
assert state.state == EnergyExportMode.BATTERY_OK.value
@pytest.mark.parametrize(
("previous_data", "new_data", "expected_state"),
[
# Path 1: Customer selected export option (has value)
(
{
"customer_preferred_export_rule": "battery_ok",
"non_export_configured": None,
},
{
"customer_preferred_export_rule": "pv_only",
"non_export_configured": None,
},
EnergyExportMode.PV_ONLY.value,
),
# Path 2: In VPP, Export is disabled (non_export_configured is True)
(
{
"customer_preferred_export_rule": "battery_ok",
"non_export_configured": None,
},
{
"customer_preferred_export_rule": None,
"non_export_configured": True,
},
EnergyExportMode.NEVER.value,
),
# Path 3: In VPP, Export enabled but state shows disabled (current_option is NEVER)
(
{
"customer_preferred_export_rule": "never",
"non_export_configured": None,
},
{
"customer_preferred_export_rule": None,
"non_export_configured": None,
},
STATE_UNKNOWN,
),
# Path 4: In VPP Mode, Export isn't disabled, use last known state
(
{
"customer_preferred_export_rule": "battery_ok",
"non_export_configured": None,
},
{
"customer_preferred_export_rule": None,
"non_export_configured": None,
},
EnergyExportMode.BATTERY_OK.value,
),
],
)
async def test_export_rule_update_attrs_logic(
hass: HomeAssistant,
freezer: FrozenDateTimeFactory,
mock_site_info: AsyncMock,
previous_data: dict,
new_data: str | None,
expected_state: str,
) -> None:
"""Test all logic paths in TeslemetryExportRuleSelectEntity._async_update_attrs."""
# Create site info with the test data
test_site_info = deepcopy(SITE_INFO)
test_site_info["response"]["components"].update(previous_data)
mock_site_info.side_effect = lambda: test_site_info
# Set up platform
await setup_platform(hass, [Platform.SELECT])
# Change the state
test_site_info = deepcopy(SITE_INFO)
test_site_info["response"]["components"].update(new_data)
mock_site_info.side_effect = lambda: test_site_info
# Coordinator refresh
freezer.tick(ENERGY_INFO_INTERVAL)
async_fire_time_changed(hass)
await hass.async_block_till_done()
# Check the final state matches expected
state = hass.states.get("select.energy_site_allow_export")
assert state
assert state.state == expected_state