diff --git a/homeassistant/components/tesla_fleet/__init__.py b/homeassistant/components/tesla_fleet/__init__.py index 38a5900fcc1..b8b7caf9b71 100644 --- a/homeassistant/components/tesla_fleet/__init__.py +++ b/homeassistant/components/tesla_fleet/__init__.py @@ -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, diff --git a/homeassistant/components/tesla_fleet/climate.py b/homeassistant/components/tesla_fleet/climate.py index 2628a9e134f..627f412a673 100644 --- a/homeassistant/components/tesla_fleet/climate.py +++ b/homeassistant/components/tesla_fleet/climate.py @@ -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.""" diff --git a/homeassistant/components/tesla_fleet/coordinator.py b/homeassistant/components/tesla_fleet/coordinator.py index 59d1c250703..f875372b8ae 100644 --- a/homeassistant/components/tesla_fleet/coordinator.py +++ b/homeassistant/components/tesla_fleet/coordinator.py @@ -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 diff --git a/homeassistant/components/tesla_fleet/entity.py b/homeassistant/components/tesla_fleet/entity.py index 583e92595d0..363ae487e84 100644 --- a/homeassistant/components/tesla_fleet/entity.py +++ b/homeassistant/components/tesla_fleet/entity.py @@ -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.""" diff --git a/homeassistant/components/tesla_fleet/manifest.json b/homeassistant/components/tesla_fleet/manifest.json index 5e79a091d93..4a1d49a52dd 100644 --- a/homeassistant/components/tesla_fleet/manifest.json +++ b/homeassistant/components/tesla_fleet/manifest.json @@ -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"] } diff --git a/homeassistant/components/tesla_fleet/number.py b/homeassistant/components/tesla_fleet/number.py index b4f7e42cafd..9d3787775a4 100644 --- a/homeassistant/components/tesla_fleet/number.py +++ b/homeassistant/components/tesla_fleet/number.py @@ -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", ), ) diff --git a/homeassistant/components/teslemetry/__init__.py b/homeassistant/components/teslemetry/__init__.py index f3a5ab8ef71..26b7533b3d9 100644 --- a/homeassistant/components/teslemetry/__init__.py +++ b/homeassistant/components/teslemetry/__init__.py @@ -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 diff --git a/homeassistant/components/teslemetry/manifest.json b/homeassistant/components/teslemetry/manifest.json index dc942335304..d5399a20e30 100644 --- a/homeassistant/components/teslemetry/manifest.json +++ b/homeassistant/components/teslemetry/manifest.json @@ -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"] } diff --git a/homeassistant/components/teslemetry/services.py b/homeassistant/components/teslemetry/services.py index 99d6936ba17..8bbd002897b 100644 --- a/homeassistant/components/teslemetry/services.py +++ b/homeassistant/components/teslemetry/services.py @@ -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( diff --git a/homeassistant/components/teslemetry/strings.json b/homeassistant/components/teslemetry/strings.json index 84068107768..f99c6f72e20 100644 --- a/homeassistant/components/teslemetry/strings.json +++ b/homeassistant/components/teslemetry/strings.json @@ -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}" }, diff --git a/homeassistant/components/tessie/manifest.json b/homeassistant/components/tessie/manifest.json index 8cf3bcee263..c12bcd5ce10 100644 --- a/homeassistant/components/tessie/manifest.json +++ b/homeassistant/components/tessie/manifest.json @@ -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"] } diff --git a/requirements_all.txt b/requirements_all.txt index 4ad01522179..e614dc70d4b 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -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 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 94119db7563..c30a5015cef 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -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 diff --git a/tests/components/tesla_fleet/conftest.py b/tests/components/tesla_fleet/conftest.py index 10b01caca96..e3aece04d2f 100644 --- a/tests/components/tesla_fleet/conftest.py +++ b/tests/components/tesla_fleet/conftest.py @@ -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", diff --git a/tests/components/tesla_fleet/test_init.py b/tests/components/tesla_fleet/test_init.py index eabb00aafbd..8fca02cba65 100644 --- a/tests/components/tesla_fleet/test_init.py +++ b/tests/components/tesla_fleet/test_init.py @@ -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, diff --git a/tests/components/tesla_fleet/test_number.py b/tests/components/tesla_fleet/test_number.py index 66734c27f6f..d9148464a7b 100644 --- a/tests/components/tesla_fleet/test_number.py +++ b/tests/components/tesla_fleet/test_number.py @@ -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", diff --git a/tests/components/teslemetry/test_init.py b/tests/components/teslemetry/test_init.py index 9f0415459fd..29bb7208100 100644 --- a/tests/components/teslemetry/test_init.py +++ b/tests/components/teslemetry/test_init.py @@ -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 diff --git a/tests/components/teslemetry/test_services.py b/tests/components/teslemetry/test_services.py index fecb8db0092..8137946aa3a 100644 --- a/tests/components/teslemetry/test_services.py +++ b/tests/components/teslemetry/test_services.py @@ -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, + )