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:
@@ -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,
|
||||
|
||||
@@ -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."""
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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."""
|
||||
|
||||
|
||||
@@ -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"]
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
),
|
||||
)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"]
|
||||
}
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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}"
|
||||
},
|
||||
|
||||
@@ -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
2
requirements_all.txt
generated
@@ -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
|
||||
|
||||
2
requirements_test_all.txt
generated
2
requirements_test_all.txt
generated
@@ -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
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user