1
0
mirror of https://github.com/home-assistant/core.git synced 2026-02-15 07:36:16 +00:00

Bump tesla-fleet-api to 1.4.2 (#159616)

Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
Brett Adams
2026-01-14 10:14:58 +10:00
committed by GitHub
parent 9e842152f7
commit 0612ea4ee8
18 changed files with 237 additions and 76 deletions

View File

@@ -4,7 +4,7 @@ from typing import Final
from aiohttp.client_exceptions import ClientResponseError
import jwt
from tesla_fleet_api import TeslaFleetApi
from tesla_fleet_api import TeslaFleetApi, is_valid_region
from tesla_fleet_api.const import Scope
from tesla_fleet_api.exceptions import (
InvalidRegion,
@@ -14,6 +14,7 @@ from tesla_fleet_api.exceptions import (
OAuthExpired,
TeslaFleetError,
)
from tesla_fleet_api.tesla import VehicleFleet
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_ACCESS_TOKEN, CONF_TOKEN, Platform
@@ -79,7 +80,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: TeslaFleetConfigEntry) -
token = jwt.decode(access_token, options={"verify_signature": False})
scopes: list[Scope] = [Scope(s) for s in token["scp"]]
region: str = token["ou_code"].lower()
region_code = token["ou_code"].lower()
region = region_code if is_valid_region(region_code) else None
oauth_session = OAuth2Session(hass, entry, implementation)
@@ -131,14 +133,15 @@ async def async_setup_entry(hass: HomeAssistant, entry: TeslaFleetConfigEntry) -
product.pop("cached_data", None)
vin = product["vin"]
signing = product["command_signing"] == "required"
api_vehicle: VehicleFleet
if signing:
if not tesla.private_key:
await tesla.get_private_key(hass.config.path("tesla_fleet.key"))
api = tesla.vehicles.createSigned(vin)
api_vehicle = tesla.vehicles.createSigned(vin)
else:
api = tesla.vehicles.createFleet(vin)
api_vehicle = tesla.vehicles.createFleet(vin)
coordinator = TeslaFleetVehicleDataCoordinator(
hass, entry, api, product, Scope.VEHICLE_LOCATION in scopes
hass, entry, api_vehicle, product, Scope.VEHICLE_LOCATION in scopes
)
await coordinator.async_config_entry_first_refresh()
@@ -153,7 +156,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: TeslaFleetConfigEntry) -
vehicles.append(
TeslaFleetVehicleData(
api=api,
api=api_vehicle,
coordinator=coordinator,
vin=vin,
device=device,
@@ -173,14 +176,16 @@ async def async_setup_entry(hass: HomeAssistant, entry: TeslaFleetConfigEntry) -
)
continue
api = tesla.energySites.create(site_id)
api_energy = tesla.energySites.create(site_id)
live_coordinator = TeslaFleetEnergySiteLiveCoordinator(hass, entry, api)
live_coordinator = TeslaFleetEnergySiteLiveCoordinator(
hass, entry, api_energy
)
history_coordinator = TeslaFleetEnergySiteHistoryCoordinator(
hass, entry, api
hass, entry, api_energy
)
info_coordinator = TeslaFleetEnergySiteInfoCoordinator(
hass, entry, api, product
hass, entry, api_energy, product
)
await live_coordinator.async_config_entry_first_refresh()
@@ -214,7 +219,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: TeslaFleetConfigEntry) -
energysites.append(
TeslaFleetEnergyData(
api=api,
api=api_energy,
live_coordinator=live_coordinator,
history_coordinator=history_coordinator,
info_coordinator=info_coordinator,

View File

@@ -79,7 +79,7 @@ class TeslaFleetClimateEntity(TeslaFleetVehicleEntity, ClimateEntity):
self,
data: TeslaFleetVehicleData,
side: TeslaFleetClimateSide,
scopes: Scope,
scopes: list[Scope],
) -> None:
"""Initialize the climate."""
@@ -219,7 +219,7 @@ class TeslaFleetCabinOverheatProtectionEntity(TeslaFleetVehicleEntity, ClimateEn
def __init__(
self,
data: TeslaFleetVehicleData,
scopes: Scope,
scopes: list[Scope],
) -> None:
"""Initialize the cabin overheat climate entity."""

View File

@@ -178,13 +178,15 @@ class TeslaFleetEnergySiteLiveCoordinator(DataUpdateCoordinator[dict[str, Any]])
try:
data = (await self.api.live_status())["response"]
except RateLimited as e:
LOGGER.warning(
"%s rate limited, will retry in %s seconds",
self.name,
e.data.get("after"),
)
if "after" in e.data:
if isinstance(e.data, dict) and "after" in e.data:
LOGGER.warning(
"%s rate limited, will retry in %s seconds",
self.name,
e.data["after"],
)
self.update_interval = timedelta(seconds=int(e.data["after"]))
else:
LOGGER.warning("%s rate limited, will skip refresh", self.name)
return self.data
except (InvalidToken, OAuthExpired, LoginRequired) as e:
raise ConfigEntryAuthFailed from e
@@ -240,13 +242,15 @@ class TeslaFleetEnergySiteHistoryCoordinator(DataUpdateCoordinator[dict[str, Any
try:
data = (await self.api.energy_history(TeslaEnergyPeriod.DAY))["response"]
except RateLimited as e:
LOGGER.warning(
"%s rate limited, will retry in %s seconds",
self.name,
e.data.get("after"),
)
if "after" in e.data:
if isinstance(e.data, dict) and "after" in e.data:
LOGGER.warning(
"%s rate limited, will retry in %s seconds",
self.name,
e.data["after"],
)
self.update_interval = timedelta(seconds=int(e.data["after"]))
else:
LOGGER.warning("%s rate limited, will skip refresh", self.name)
return self.data
except (InvalidToken, OAuthExpired, LoginRequired) as e:
raise ConfigEntryAuthFailed from e
@@ -303,13 +307,15 @@ class TeslaFleetEnergySiteInfoCoordinator(DataUpdateCoordinator[dict[str, Any]])
try:
data = (await self.api.site_info())["response"]
except RateLimited as e:
LOGGER.warning(
"%s rate limited, will retry in %s seconds",
self.name,
e.data.get("after"),
)
if "after" in e.data:
if isinstance(e.data, dict) and "after" in e.data:
LOGGER.warning(
"%s rate limited, will retry in %s seconds",
self.name,
e.data["after"],
)
self.update_interval = timedelta(seconds=int(e.data["after"]))
else:
LOGGER.warning("%s rate limited, will skip refresh", self.name)
return self.data
except (InvalidToken, OAuthExpired, LoginRequired) as e:
raise ConfigEntryAuthFailed from e

View File

@@ -1,7 +1,7 @@
"""Tesla Fleet parent entity class."""
from abc import abstractmethod
from typing import Any
from typing import Any, Generic, TypeVar
from tesla_fleet_api.const import Scope
from tesla_fleet_api.tesla.energysite import EnergySite
@@ -21,6 +21,8 @@ from .coordinator import (
from .helpers import wake_up_vehicle
from .models import TeslaFleetEnergyData, TeslaFleetVehicleData
_ApiT = TypeVar("_ApiT", bound=VehicleFleet | EnergySite)
class TeslaFleetEntity(
CoordinatorEntity[
@@ -28,13 +30,15 @@ class TeslaFleetEntity(
| TeslaFleetEnergySiteLiveCoordinator
| TeslaFleetEnergySiteHistoryCoordinator
| TeslaFleetEnergySiteInfoCoordinator
]
],
Generic[_ApiT],
):
"""Parent class for all TeslaFleet entities."""
_attr_has_entity_name = True
read_only: bool
scoped: bool
api: _ApiT
def __init__(
self,
@@ -42,7 +46,7 @@ class TeslaFleetEntity(
| TeslaFleetEnergySiteLiveCoordinator
| TeslaFleetEnergySiteHistoryCoordinator
| TeslaFleetEnergySiteInfoCoordinator,
api: VehicleFleet | EnergySite,
api: _ApiT,
key: str,
) -> None:
"""Initialize common aspects of a TeslaFleet entity."""
@@ -100,7 +104,7 @@ class TeslaFleetEntity(
)
class TeslaFleetVehicleEntity(TeslaFleetEntity):
class TeslaFleetVehicleEntity(TeslaFleetEntity[VehicleFleet]):
"""Parent class for TeslaFleet Vehicle entities."""
_last_update: int = 0
@@ -128,7 +132,7 @@ class TeslaFleetVehicleEntity(TeslaFleetEntity):
await wake_up_vehicle(self.vehicle)
class TeslaFleetEnergyLiveEntity(TeslaFleetEntity):
class TeslaFleetEnergyLiveEntity(TeslaFleetEntity[EnergySite]):
"""Parent class for TeslaFleet Energy Site Live entities."""
def __init__(
@@ -143,7 +147,7 @@ class TeslaFleetEnergyLiveEntity(TeslaFleetEntity):
super().__init__(data.live_coordinator, data.api, key)
class TeslaFleetEnergyHistoryEntity(TeslaFleetEntity):
class TeslaFleetEnergyHistoryEntity(TeslaFleetEntity[EnergySite]):
"""Parent class for TeslaFleet Energy Site History entities."""
def __init__(
@@ -158,7 +162,7 @@ class TeslaFleetEnergyHistoryEntity(TeslaFleetEntity):
super().__init__(data.history_coordinator, data.api, key)
class TeslaFleetEnergyInfoEntity(TeslaFleetEntity):
class TeslaFleetEnergyInfoEntity(TeslaFleetEntity[EnergySite]):
"""Parent class for TeslaFleet Energy Site Info entities."""
def __init__(
@@ -174,7 +178,7 @@ class TeslaFleetEnergyInfoEntity(TeslaFleetEntity):
class TeslaFleetWallConnectorEntity(
TeslaFleetEntity, CoordinatorEntity[TeslaFleetEnergySiteLiveCoordinator]
TeslaFleetEntity[EnergySite], CoordinatorEntity[TeslaFleetEnergySiteLiveCoordinator]
):
"""Parent class for Tesla Fleet Wall Connector entities."""

View File

@@ -8,5 +8,5 @@
"integration_type": "hub",
"iot_class": "cloud_polling",
"loggers": ["tesla-fleet-api"],
"requirements": ["tesla-fleet-api==1.3.2"]
"requirements": ["tesla-fleet-api==1.4.2"]
}

View File

@@ -33,7 +33,7 @@ PARALLEL_UPDATES = 0
class TeslaFleetNumberVehicleEntityDescription(NumberEntityDescription):
"""Describes TeslaFleet Number entity."""
func: Callable[[VehicleFleet, float], Awaitable[Any]]
func: Callable[[VehicleFleet, int], Awaitable[Any]]
native_min_value: float
native_max_value: float
min_key: str | None = None
@@ -74,19 +74,19 @@ VEHICLE_DESCRIPTIONS: tuple[TeslaFleetNumberVehicleEntityDescription, ...] = (
class TeslaFleetNumberBatteryEntityDescription(NumberEntityDescription):
"""Describes TeslaFleet Number entity."""
func: Callable[[EnergySite, float], Awaitable[Any]]
func: Callable[[EnergySite, int], Awaitable[Any]]
requires: str | None = None
ENERGY_INFO_DESCRIPTIONS: tuple[TeslaFleetNumberBatteryEntityDescription, ...] = (
TeslaFleetNumberBatteryEntityDescription(
key="backup_reserve_percent",
func=lambda api, value: api.backup(int(value)),
func=lambda api, value: api.backup(value),
requires="components_battery",
),
TeslaFleetNumberBatteryEntityDescription(
key="off_grid_vehicle_charging_reserve_percent",
func=lambda api, value: api.off_grid_vehicle_charging_reserve(int(value)),
func=lambda api, value: api.off_grid_vehicle_charging_reserve(value),
requires="components_off_grid_vehicle_charging_reserve_supported",
),
)

View File

@@ -136,14 +136,16 @@ async def async_setup_entry(hass: HomeAssistant, entry: TeslemetryConfigEntry) -
# Remove the protobuff 'cached_data' that we do not use to save memory
product.pop("cached_data", None)
vin = product["vin"]
api = teslemetry.vehicles.create(vin)
coordinator = TeslemetryVehicleDataCoordinator(hass, entry, api, product)
vehicle = teslemetry.vehicles.create(vin)
coordinator = TeslemetryVehicleDataCoordinator(
hass, entry, vehicle, product
)
device = DeviceInfo(
identifiers={(DOMAIN, vin)},
manufacturer="Tesla",
configuration_url="https://teslemetry.com/console",
name=product["display_name"],
model=api.model,
model=vehicle.model,
serial_number=vin,
)
current_devices.add((DOMAIN, vin))
@@ -168,7 +170,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: TeslemetryConfigEntry) -
vehicles.append(
TeslemetryVehicleData(
api=api,
api=vehicle,
config_entry=entry,
coordinator=coordinator,
poll=poll,
@@ -194,7 +196,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: TeslemetryConfigEntry) -
)
continue
api = teslemetry.energySites.create(site_id)
energy_site = teslemetry.energySites.create(site_id)
device = DeviceInfo(
identifiers={(DOMAIN, str(site_id))},
manufacturer="Tesla",
@@ -210,7 +212,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: TeslemetryConfigEntry) -
# Check live status endpoint works before creating its coordinator
try:
live_status = (await api.live_status())["response"]
live_status = (await energy_site.live_status())["response"]
except (InvalidToken, Forbidden, SubscriptionRequired) as e:
raise ConfigEntryAuthFailed from e
except TeslaFleetError as e:
@@ -218,19 +220,19 @@ async def async_setup_entry(hass: HomeAssistant, entry: TeslemetryConfigEntry) -
energysites.append(
TeslemetryEnergyData(
api=api,
api=energy_site,
live_coordinator=(
TeslemetryEnergySiteLiveCoordinator(
hass, entry, api, live_status
hass, entry, energy_site, live_status
)
if isinstance(live_status, dict)
else None
),
info_coordinator=TeslemetryEnergySiteInfoCoordinator(
hass, entry, api, product
hass, entry, energy_site, product
),
history_coordinator=(
TeslemetryEnergyHistoryCoordinator(hass, entry, api)
TeslemetryEnergyHistoryCoordinator(hass, entry, energy_site)
if powerwall
else None
),
@@ -314,7 +316,7 @@ async def async_migrate_entry(
# Convert legacy access token to OAuth tokens using migrate endpoint
try:
data = await Teslemetry(session, access_token).migrate_to_oauth(
CLIENT_ID, access_token, hass.config.location_name
CLIENT_ID, hass.config.location_name
)
except (ClientError, TypeError) as e:
raise ConfigEntryAuthFailed from e

View File

@@ -8,5 +8,5 @@
"integration_type": "hub",
"iot_class": "cloud_polling",
"loggers": ["tesla-fleet-api"],
"requirements": ["tesla-fleet-api==1.3.2", "teslemetry-stream==0.9.0"]
"requirements": ["tesla-fleet-api==1.4.2", "teslemetry-stream==0.9.0"]
}

View File

@@ -149,7 +149,7 @@ def async_setup_services(hass: HomeAssistant) -> None:
config = async_get_config_for_device(hass, device)
vehicle = async_get_vehicle_for_entry(hass, device, config)
time: int | None = None
time: int
# Convert time to minutes since minute
if "time" in call.data:
(hours, minutes, *_seconds) = call.data["time"].split(":")
@@ -158,6 +158,8 @@ def async_setup_services(hass: HomeAssistant) -> None:
raise ServiceValidationError(
translation_domain=DOMAIN, translation_key="set_scheduled_charging_time"
)
else:
time = 0
await handle_vehicle_command(
vehicle.api.set_scheduled_charging(enable=call.data["enable"], time=time)
@@ -198,6 +200,8 @@ def async_setup_services(hass: HomeAssistant) -> None:
translation_domain=DOMAIN,
translation_key="set_scheduled_departure_preconditioning",
)
else:
departure_time = 0
# Off peak charging
off_peak_charging_enabled = call.data.get(ATTR_OFF_PEAK_CHARGING_ENABLED, False)
@@ -214,6 +218,8 @@ def async_setup_services(hass: HomeAssistant) -> None:
translation_domain=DOMAIN,
translation_key="set_scheduled_departure_off_peak",
)
else:
end_off_peak_time = 0
await handle_vehicle_command(
vehicle.api.set_scheduled_departure(
@@ -252,9 +258,7 @@ def async_setup_services(hass: HomeAssistant) -> None:
vehicle = async_get_vehicle_for_entry(hass, device, config)
await handle_vehicle_command(
vehicle.api.set_valet_mode(
call.data.get("enable"), call.data.get("pin", "")
)
vehicle.api.set_valet_mode(call.data["enable"], call.data["pin"])
)
hass.services.async_register(
@@ -276,14 +280,14 @@ def async_setup_services(hass: HomeAssistant) -> None:
config = async_get_config_for_device(hass, device)
vehicle = async_get_vehicle_for_entry(hass, device, config)
enable = call.data.get("enable")
enable = call.data["enable"]
if enable is True:
await handle_vehicle_command(
vehicle.api.speed_limit_activate(call.data.get("pin"))
vehicle.api.speed_limit_activate(call.data["pin"])
)
elif enable is False:
await handle_vehicle_command(
vehicle.api.speed_limit_deactivate(call.data.get("pin"))
vehicle.api.speed_limit_deactivate(call.data["pin"])
)
hass.services.async_register(
@@ -306,7 +310,7 @@ def async_setup_services(hass: HomeAssistant) -> None:
site = async_get_energy_site_for_entry(hass, device, config)
resp = await handle_command(
site.api.time_of_use_settings(call.data.get(ATTR_TOU_SETTINGS))
site.api.time_of_use_settings(call.data[ATTR_TOU_SETTINGS])
)
if "error" in resp:
raise HomeAssistantError(

View File

@@ -1127,6 +1127,15 @@
"no_vehicle_data_for_device": {
"message": "No vehicle data for device ID: {device_id}"
},
"set_scheduled_charging_time": {
"message": "Scheduled charging time is required when enabling"
},
"set_scheduled_departure_off_peak": {
"message": "Off-peak charging end time is required when enabling"
},
"set_scheduled_departure_preconditioning": {
"message": "Preconditioning departure time is required when enabling"
},
"wake_up_failed": {
"message": "Failed to wake up vehicle: {message}"
},

View File

@@ -7,5 +7,5 @@
"integration_type": "hub",
"iot_class": "cloud_polling",
"loggers": ["tessie", "tesla-fleet-api"],
"requirements": ["tessie-api==0.1.1", "tesla-fleet-api==1.3.2"]
"requirements": ["tessie-api==0.1.1", "tesla-fleet-api==1.4.2"]
}

2
requirements_all.txt generated
View File

@@ -2990,7 +2990,7 @@ temperusb==1.6.1
# homeassistant.components.tesla_fleet
# homeassistant.components.teslemetry
# homeassistant.components.tessie
tesla-fleet-api==1.3.2
tesla-fleet-api==1.4.2
# homeassistant.components.powerwall
tesla-powerwall==0.5.2

View File

@@ -2496,7 +2496,7 @@ temperusb==1.6.1
# homeassistant.components.tesla_fleet
# homeassistant.components.teslemetry
# homeassistant.components.tessie
tesla-fleet-api==1.3.2
tesla-fleet-api==1.4.2
# homeassistant.components.powerwall
tesla-powerwall==0.5.2

View File

@@ -35,7 +35,10 @@ def mock_expires_at() -> int:
def create_config_entry(
expires_at: int, scopes: list[Scope], implementation: str = DOMAIN
expires_at: int,
scopes: list[Scope],
implementation: str = DOMAIN,
region: str = "NA",
) -> MockConfigEntry:
"""Create Tesla Fleet entry in Home Assistant."""
access_token = jwt.encode(
@@ -43,7 +46,7 @@ def create_config_entry(
"sub": UID,
"aud": [],
"scp": scopes,
"ou_code": "NA",
"ou_code": region,
},
key="",
algorithm="none",

View File

@@ -230,6 +230,52 @@ async def test_vehicle_refresh_ratelimited(
assert state.state == "unknown"
async def test_vehicle_refresh_ratelimited_no_after(
hass: HomeAssistant,
normal_config_entry: MockConfigEntry,
mock_vehicle_data: AsyncMock,
freezer: FrozenDateTimeFactory,
) -> None:
"""Test coordinator refresh handles 429 without after."""
await setup_platform(hass, normal_config_entry)
# mock_vehicle_data called once during setup
assert mock_vehicle_data.call_count == 1
mock_vehicle_data.side_effect = RateLimited({})
freezer.tick(VEHICLE_INTERVAL)
async_fire_time_changed(hass)
await hass.async_block_till_done()
# Called again during refresh, failed with RateLimited
assert mock_vehicle_data.call_count == 2
freezer.tick(VEHICLE_INTERVAL)
async_fire_time_changed(hass)
await hass.async_block_till_done()
# Called again because skip refresh doesn't change interval
assert mock_vehicle_data.call_count == 3
async def test_init_invalid_region(
hass: HomeAssistant,
expires_at: int,
) -> None:
"""Test init with an invalid region in the token."""
# ou_code 'other' should be caught by the region validation and set to None
config_entry = create_config_entry(
expires_at, [Scope.VEHICLE_DEVICE_DATA], region="other"
)
with patch("homeassistant.components.tesla_fleet.TeslaFleetApi") as mock_api:
await setup_platform(hass, config_entry)
# Check if TeslaFleetApi was called with region=None
mock_api.assert_called()
assert mock_api.call_args.kwargs.get("region") is None
async def test_vehicle_sleep(
hass: HomeAssistant,
normal_config_entry: MockConfigEntry,

View File

@@ -85,6 +85,21 @@ async def test_number_services(
assert state.state == "60"
call.assert_called_once()
# Test float conversion
with patch(
"tesla_fleet_api.tesla.VehicleFleet.set_charge_limit",
return_value=COMMAND_OK,
) as call:
await hass.services.async_call(
NUMBER_DOMAIN,
SERVICE_SET_VALUE,
{ATTR_ENTITY_ID: entity_id, ATTR_VALUE: 60.5},
blocking=True,
)
state = hass.states.get(entity_id)
assert state.state == "60"
call.assert_called_once_with(60)
entity_id = "number.energy_site_backup_reserve"
with patch(
"tesla_fleet_api.tesla.EnergySite.backup",

View File

@@ -18,7 +18,6 @@ from homeassistant.components.teslemetry.coordinator import VEHICLE_INTERVAL
from homeassistant.components.teslemetry.models import TeslemetryData
from homeassistant.config_entries import ConfigEntryState
from homeassistant.const import (
CONF_ACCESS_TOKEN,
STATE_OFF,
STATE_ON,
STATE_UNAVAILABLE,
@@ -319,9 +318,7 @@ async def test_migrate_from_version_1_success(hass: HomeAssistant) -> None:
await hass.config_entries.async_setup(mock_entry.entry_id)
await hass.async_block_till_done()
mock_migrate.assert_called_once_with(
CLIENT_ID, CONFIG_V1[CONF_ACCESS_TOKEN], hass.config.location_name
)
mock_migrate.assert_called_once_with(CLIENT_ID, hass.config.location_name)
assert mock_entry is not None
assert mock_entry.version == 2
@@ -356,9 +353,7 @@ async def test_migrate_from_version_1_token_endpoint_error(hass: HomeAssistant)
await hass.config_entries.async_setup(mock_entry.entry_id)
await hass.async_block_till_done()
mock_migrate.assert_called_once_with(
CLIENT_ID, CONFIG_V1[CONF_ACCESS_TOKEN], hass.config.location_name
)
mock_migrate.assert_called_once_with(CLIENT_ID, hass.config.location_name)
entry = hass.config_entries.async_get_entry(mock_entry.entry_id)
assert entry is not None

View File

@@ -63,6 +63,40 @@ async def test_services(
"sensor.energy_site_battery_power"
).device_id
# Test set_scheduled_charging with enable=False (time should default to 0)
with patch(
"tesla_fleet_api.teslemetry.Vehicle.set_scheduled_charging",
return_value=COMMAND_OK,
) as set_scheduled_charging_off:
await hass.services.async_call(
DOMAIN,
SERVICE_SET_SCHEDULED_CHARGING,
{
CONF_DEVICE_ID: vehicle_device,
ATTR_ENABLE: False,
},
blocking=True,
)
set_scheduled_charging_off.assert_called_once_with(enable=False, time=0)
# Test set_scheduled_departure with enable=False (times should default to 0)
with patch(
"tesla_fleet_api.teslemetry.Vehicle.set_scheduled_departure",
return_value=COMMAND_OK,
) as set_scheduled_departure_off:
await hass.services.async_call(
DOMAIN,
SERVICE_SET_SCHEDULED_DEPARTURE,
{
CONF_DEVICE_ID: vehicle_device,
ATTR_ENABLE: False,
},
blocking=True,
)
set_scheduled_departure_off.assert_called_once_with(
False, False, False, 0, False, False, 0
)
with patch(
"tesla_fleet_api.teslemetry.Vehicle.navigation_gps_request",
return_value=COMMAND_OK,
@@ -308,6 +342,8 @@ async def test_service_validation_errors(
"""Tests that the custom services handle bad data."""
await setup_platform(hass)
entity_registry = er.async_get(hass)
vehicle_device = entity_registry.async_get("sensor.test_charging").device_id
# Bad device ID
with pytest.raises(ServiceValidationError):
@@ -320,3 +356,39 @@ async def test_service_validation_errors(
},
blocking=True,
)
# Test set_scheduled_charging validation error (enable=True but no time)
with pytest.raises(ServiceValidationError):
await hass.services.async_call(
DOMAIN,
SERVICE_SET_SCHEDULED_CHARGING,
{
CONF_DEVICE_ID: vehicle_device,
ATTR_ENABLE: True,
},
blocking=True,
)
# Test set_scheduled_departure validation error (preconditioning_enabled=True but no departure_time)
with pytest.raises(ServiceValidationError):
await hass.services.async_call(
DOMAIN,
SERVICE_SET_SCHEDULED_DEPARTURE,
{
CONF_DEVICE_ID: vehicle_device,
ATTR_PRECONDITIONING_ENABLED: True,
},
blocking=True,
)
# Test set_scheduled_departure validation error (off_peak_charging_enabled=True but no end_off_peak_time)
with pytest.raises(ServiceValidationError):
await hass.services.async_call(
DOMAIN,
SERVICE_SET_SCHEDULED_DEPARTURE,
{
CONF_DEVICE_ID: vehicle_device,
ATTR_OFF_PEAK_CHARGING_ENABLED: True,
},
blocking=True,
)