Repository: home-assistant/core
Pull request #138689: Add streaming to Climate platform in Teslemetry
Author: login: Bre77
Labels: name: cla-signed
name: new-feature
name: by-code-owner
name: Quality Scale: No score
name: integration: teslemetry
Created: 2025-02-17T04:10:53Z | Merged: 2025-03-25T20:09:45Z

## Proposed change
<!--
  Describe the big picture of your changes here to communicate to the
  maintainers why we should accept this pull request. If it fixes a bug
  or resolves a feature request, be sure to link to that issue in the
  additional information section.
-->
Add streaming to Climate platform in Teslemetry. Also removes any related wake_up commands, checks, and tests, because this is handled server side.

## Type of change
<!--
  What type of change does your PR introduce to Home Assistant?
  NOTE: Please, check only 1! box!
  If your PR requires multiple boxes to be checked, you'll most likely need to
  split it into multiple PRs. This makes things easier and faster to code review.
-->

- [ ] Dependency upgrade
- [ ] Bugfix (non-breaking change which fixes an issue)
- [ ] New integration (thank you!)
- [x] New feature (which adds functionality to an existing integration)
- [ ] Deprecation (breaking change to happen in the future)
- [ ] Breaking change (fix/feature causing existing functionality to break)
- [ ] Code quality improvements to existing code or addition of tests

## Additional information
<!--
  Details are important, and help maintainers processing your PR.
  Please be sure to fill out additional details, if applicable.
-->

- This PR fixes or closes issue: fixes #
- This PR is related to issue: 
- Link to documentation pull request: 
- Link to developer documentation pull request: 
- Link to frontend pull request: 

## Checklist
<!--
  Put an `x` in the boxes that apply. You can also fill these out after
  creating the PR. If you're unsure about any of them, don't hesitate to ask.
  We're here to help! This is simply a reminder of what we are going to look
  for before merging your code.
-->

- [x] The code change is tested and works locally.
- [x] Local tests pass. **Your PR cannot be merged unless tests pass**
- [x] There is no commented out code in this PR.
- [x] I have followed the [development checklist][dev-checklist]
- [x] I have followed the [perfect PR recommendations][perfect-pr]
- [x] The code has been formatted using Ruff (`ruff format homeassistant tests`)
- [x] Tests have been added to verify that the new code works.

If user exposed functionality or configuration variables are added/changed:

- [ ] Documentation added/updated for [www.home-assistant.io][docs-repository]

If the code communicates with devices, web services, or third-party tools:

- [ ] The [manifest file][manifest-docs] has all fields filled out correctly.  
      Updated and included derived files by running: `python3 -m script.hassfest`.
- [ ] New or updated dependencies have been added to `requirements_all.txt`.  
      Updated by running `python3 -m script.gen_requirements_all`.
- [ ] For the updated dependencies - a link to the changelog, or at minimum a diff between library versions is added to the PR description.

<!--
  This project is very active and we have a high turnover of pull requests.

  Unfortunately, the number of incoming pull requests is higher than what our
  reviewers can review and merge so there is a long backlog of pull requests
  waiting for review. You can help here!
  
  By reviewing another pull request, you will help raise the code quality of
  that pull request and the final review will be faster. This way the general
  pace of pull request reviews will go up and your wait time will go down.
  
  When picking a pull request to review, try to choose one that hasn't yet
  been reviewed.

  Thanks for helping out!
-->

To help with the load of incoming pull requests:

- [ ] I have reviewed two other [open pull requests][prs] in this repository.

[prs]: https://github.com/home-assistant/core/pulls?q=is%3Aopen+is%3Apr+-author%3A%40me+-draft%3Atrue+-label%3Awaiting-for-upstream+sort%3Acreated-desc+review%3Anone+-status%3Afailure

<!--
  Thank you for contributing <3

  Below, some useful links you could explore:
-->
[dev-checklist]: https://developers.home-assistant.io/docs/development_checklist/
[manifest-docs]: https://developers.home-assistant.io/docs/creating_integration_manifest/
[quality-scale]: https://developers.home-assistant.io/docs/integration_quality_scale_index/
[docs-repository]: https://github.com/home-assistant/home-assistant.io
[perfect-pr]: https://developers.home-assistant.io/docs/review-process/#creating-the-perfect-pr



Diff:
diff --git a/homeassistant/components/teslemetry/climate.py b/homeassistant/components/teslemetry/climate.py
index 86811131ab68eb..3aaf5f0516c583 100644
--- a/homeassistant/components/teslemetry/climate.py
+++ b/homeassistant/components/teslemetry/climate.py
@@ -6,9 +6,11 @@
 from typing import Any, cast
 
 from tesla_fleet_api.const import CabinOverheatProtectionTemp, Scope
+from tesla_fleet_api.vehicle import VehicleSpecific
 
 from homeassistant.components.climate import (
     ATTR_HVAC_MODE,
+    HVAC_MODES,
     ClimateEntity,
     ClimateEntityFeature,
     HVACMode,
@@ -22,15 +24,32 @@
 from homeassistant.core import HomeAssistant
 from homeassistant.exceptions import ServiceValidationError
 from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
+from homeassistant.helpers.restore_state import RestoreEntity
 
 from . import TeslemetryConfigEntry
 from .const import DOMAIN, TeslemetryClimateSide
-from .entity import TeslemetryVehicleEntity
+from .entity import (
+    TeslemetryRootEntity,
+    TeslemetryVehicleEntity,
+    TeslemetryVehicleStreamEntity,
+)
 from .helpers import handle_vehicle_command
 from .models import TeslemetryVehicleData
 
 DEFAULT_MIN_TEMP = 15
 DEFAULT_MAX_TEMP = 28
+COP_TEMPERATURES = {
+    30: CabinOverheatProtectionTemp.LOW,
+    35: CabinOverheatProtectionTemp.MEDIUM,
+    40: CabinOverheatProtectionTemp.HIGH,
+}
+PRESET_MODES = {
+    "Off": "off",
+    "On": "keep",
+    "Dog": "dog",
+    "Party": "camp",
+}
+
 
 PARALLEL_UPDATES = 0
 
@@ -45,13 +64,21 @@ async def async_setup_entry(
     async_add_entities(
         chain(
             (
-                TeslemetryClimateEntity(
+                TeslemetryPollingClimateEntity(
+                    vehicle, TeslemetryClimateSide.DRIVER, entry.runtime_data.scopes
+                )
+                if vehicle.api.pre2021 or vehicle.firmware < "2024.44.25"
+                else TeslemetryStreamingClimateEntity(
                     vehicle, TeslemetryClimateSide.DRIVER, entry.runtime_data.scopes
                 )
                 for vehicle in entry.runtime_data.vehicles
             ),
             (
-                TeslemetryCabinOverheatProtectionEntity(
+                TeslemetryPollingCabinOverheatProtectionEntity(
+                    vehicle, entry.runtime_data.scopes
+                )
+                if vehicle.api.pre2021 or vehicle.firmware < "2024.44.25"
+                else TeslemetryStreamingCabinOverheatProtectionEntity(
                     vehicle, entry.runtime_data.scopes
                 )
                 for vehicle in entry.runtime_data.vehicles
@@ -60,66 +87,22 @@ async def async_setup_entry(
     )
 
 
-class TeslemetryClimateEntity(TeslemetryVehicleEntity, ClimateEntity):
-    """Telemetry vehicle climate entity."""
+class TeslemetryClimateEntity(TeslemetryRootEntity, ClimateEntity):
+    """Vehicle Climate Control."""
 
-    _attr_precision = PRECISION_HALVES
+    api: VehicleSpecific
 
+    _attr_precision = PRECISION_HALVES
     _attr_temperature_unit = UnitOfTemperature.CELSIUS
     _attr_hvac_modes = [HVACMode.HEAT_COOL, HVACMode.OFF]
-    _attr_supported_features = (
-        ClimateEntityFeature.TURN_ON
-        | ClimateEntityFeature.TURN_OFF
-        | ClimateEntityFeature.TARGET_TEMPERATURE
-        | ClimateEntityFeature.PRESET_MODE
-    )
-    _attr_preset_modes = ["off", "keep", "dog", "camp"]
-
-    def __init__(
-        self,
-        data: TeslemetryVehicleData,
-        side: TeslemetryClimateSide,
-        scopes: Scope,
-    ) -> None:
-        """Initialize the climate."""
-        self.scoped = Scope.VEHICLE_CMDS in scopes
-
-        if not self.scoped:
-            self._attr_supported_features = ClimateEntityFeature(0)
-            self._attr_hvac_modes = []
-
-        super().__init__(
-            data,
-            side,
-        )
-
-    def _async_update_attrs(self) -> None:
-        """Update the attributes of the entity."""
-        value = self.get("climate_state_is_climate_on")
-        if value:
-            self._attr_hvac_mode = HVACMode.HEAT_COOL
-        else:
-            self._attr_hvac_mode = HVACMode.OFF
-
-        # If not scoped, prevent the user from changing the HVAC mode by making it the only option
-        if self._attr_hvac_mode and not self.scoped:
-            self._attr_hvac_modes = [self._attr_hvac_mode]
-
-        self._attr_current_temperature = self.get("climate_state_inside_temp")
-        self._attr_target_temperature = self.get(f"climate_state_{self.key}_setting")
-        self._attr_preset_mode = self.get("climate_state_climate_keeper_mode")
-        self._attr_min_temp = cast(
-            float, self.get("climate_state_min_avail_temp", DEFAULT_MIN_TEMP)
-        )
-        self._attr_max_temp = cast(
-            float, self.get("climate_state_max_avail_temp", DEFAULT_MAX_TEMP)
-        )
+    _attr_preset_modes = list(PRESET_MODES.values())
+    _attr_fan_modes = ["off", "bioweapon"]
+    _enable_turn_on_off_backwards_compatibility = False
 
     async def async_turn_on(self) -> None:
         """Set the climate state to on."""
-
         self.raise_for_scope(Scope.VEHICLE_CMDS)
-        await self.wake_up_if_asleep()
+
         await handle_vehicle_command(self.api.auto_conditioning_start())
 
         self._attr_hvac_mode = HVACMode.HEAT_COOL
@@ -127,19 +110,21 @@ async def async_turn_on(self) -> None:
 
     async def async_turn_off(self) -> None:
         """Set the climate state to off."""
-
         self.raise_for_scope(Scope.VEHICLE_CMDS)
-        await self.wake_up_if_asleep()
+
         await handle_vehicle_command(self.api.auto_conditioning_stop())
 
         self._attr_hvac_mode = HVACMode.OFF
         self._attr_preset_mode = self._attr_preset_modes[0]
+        self._attr_fan_mode = self._attr_fan_modes[0]
         self.async_write_ha_state()
 
     async def async_set_temperature(self, **kwargs: Any) -> None:
         """Set the climate temperature."""
+
         if temp := kwargs.get(ATTR_TEMPERATURE):
-            await self.wake_up_if_asleep()
+            self.raise_for_scope(Scope.VEHICLE_CMDS)
+
             await handle_vehicle_command(
                 self.api.set_temps(
                     driver_temp=temp,
@@ -163,74 +148,317 @@ async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None:
 
     async def async_set_preset_mode(self, preset_mode: str) -> None:
         """Set the climate preset mode."""
-        await self.wake_up_if_asleep()
+        self.raise_for_scope(Scope.VEHICLE_CMDS)
+
         await handle_vehicle_command(
             self.api.set_climate_keeper_mode(
                 climate_keeper_mode=self._attr_preset_modes.index(preset_mode)
             )
         )
         self._attr_preset_mode = preset_mode
-        if preset_mode != self._attr_preset_modes[0]:
-            # Changing preset mode will also turn on climate
+        if preset_mode == self._attr_preset_modes[0]:
+            self._attr_hvac_mode = HVACMode.OFF
+        else:
+            self._attr_hvac_mode = HVACMode.HEAT_COOL
+        self.async_write_ha_state()
+
+    async def async_set_fan_mode(self, fan_mode: str) -> None:
+        """Set the Bioweapon defense mode."""
+        self.raise_for_scope(Scope.VEHICLE_CMDS)
+
+        await handle_vehicle_command(
+            self.api.set_bioweapon_mode(
+                on=(fan_mode != "off"),
+                manual_override=True,
+            )
+        )
+        self._attr_fan_mode = fan_mode
+        if fan_mode == self._attr_fan_modes[1]:
             self._attr_hvac_mode = HVACMode.HEAT_COOL
         self.async_write_ha_state()
 
 
+class TeslemetryPollingClimateEntity(TeslemetryClimateEntity, TeslemetryVehicleEntity):
+    """Polling vehicle climate entity."""
+
+    _attr_supported_features = (
+        ClimateEntityFeature.TURN_ON
+        | ClimateEntityFeature.TURN_OFF
+        | ClimateEntityFeature.TARGET_TEMPERATURE
+        | ClimateEntityFeature.PRESET_MODE
+        | ClimateEntityFeature.FAN_MODE
+    )
+
+    def __init__(
+        self,
+        data: TeslemetryVehicleData,
+        side: TeslemetryClimateSide,
+        scopes: list[Scope],
+    ) -> None:
+        """Initialize the climate."""
+        self.scoped = Scope.VEHICLE_CMDS in scopes
+        if not self.scoped:
+            self._attr_supported_features = ClimateEntityFeature(0)
+
+        super().__init__(data, side)
+
+    def _async_update_attrs(self) -> None:
+        """Update the attributes of the entity."""
+        value = self.get("climate_state_is_climate_on")
+        if value is None:
+            self._attr_hvac_mode = None
+        if value:
+            self._attr_hvac_mode = HVACMode.HEAT_COOL
+        else:
+            self._attr_hvac_mode = HVACMode.OFF
+
+        self._attr_current_temperature = self.get("climate_state_inside_temp")
+        self._attr_target_temperature = self.get(f"climate_state_{self.key}_setting")
+        self._attr_preset_mode = self.get("climate_state_climate_keeper_mode")
+        if self.get("climate_state_bioweapon_mode"):
+            self._attr_fan_mode = "bioweapon"
+        else:
+            self._attr_fan_mode = "off"
+        self._attr_min_temp = cast(
+            float, self.get("climate_state_min_avail_temp", DEFAULT_MIN_TEMP)
+        )
+        self._attr_max_temp = cast(
+            float, self.get("climate_state_max_avail_temp", DEFAULT_MAX_TEMP)
+        )
+
+
+class TeslemetryStreamingClimateEntity(
+    TeslemetryClimateEntity, TeslemetryVehicleStreamEntity, RestoreEntity
+):
+    """Teslemetry steering wheel climate control."""
+
+    _attr_supported_features = (
+        ClimateEntityFeature.TURN_ON
+        | ClimateEntityFeature.TURN_OFF
+        | ClimateEntityFeature.TARGET_TEMPERATURE
+        | ClimateEntityFeature.PRESET_MODE
+    )
+
+    def __init__(
+        self,
+        data: TeslemetryVehicleData,
+        side: TeslemetryClimateSide,
+        scopes: list[Scope],
+    ) -> None:
+        """Initialize the climate."""
+
+        # Initialize defaults
+        self._attr_hvac_mode = None
+        self._attr_current_temperature = None
+        self._attr_target_temperature = None
+        self._attr_fan_mode = None
+        self._attr_preset_mode = None
+
+        self.scoped = Scope.VEHICLE_CMDS in scopes
+        if not self.scoped:
+            self._attr_supported_features = ClimateEntityFeature(0)
+        self.side = side
+        super().__init__(
+            data,
+            side,
+        )
+
+        self._attr_min_temp = cast(
+            float,
+            data.coordinator.data.get("climate_state_min_avail_temp", DEFAULT_MIN_TEMP),
+        )
+        self._attr_max_temp = cast(
+            float,
+            data.coordinator.data.get("climate_state_max_avail_temp", DEFAULT_MAX_TEMP),
+        )
+        self.rhd: bool = data.coordinator.data.get("vehicle_config_rhd", False)
+
+    async def async_added_to_hass(self) -> None:
+        """Handle entity which will be added."""
+        await super().async_added_to_hass()
+        if (state := await self.async_get_last_state()) is not None:
+            self._attr_hvac_mode = (
+                HVACMode(state.state) if state.state in HVAC_MODES else None
+            )
+            self._attr_current_temperature = state.attributes.get("current_temperature")
+            self._attr_target_temperature = state.attributes.get("temperature")
+            self._attr_preset_mode = state.attributes.get("preset_mode")
+
+        self.async_on_remove(
+            self.vehicle.stream_vehicle.listen_InsideTemp(
+                self._async_handle_inside_temp
+            )
+        )
+        self.async_on_remove(
+            self.vehicle.stream_vehicle.listen_HvacACEnabled(
+                self._async_handle_hvac_ac_enabled
+            )
+        )
+        self.async_on_remove(
+            self.vehicle.stream_vehicle.listen_ClimateKeeperMode(
+                self._async_handle_climate_keeper_mode
+            )
+        )
+        self.async_on_remove(
+            self.vehicle.stream_vehicle.listen_RightHandDrive(self._async_handle_rhd)
+        )
+
+        if self.side == TeslemetryClimateSide.DRIVER:
+            if self.rhd:
+                self.async_on_remove(
+                    self.vehicle.stream_vehicle.listen_HvacRightTemperatureRequest(
+                        self._async_handle_hvac_temperature_request
+                    )
+                )
+            else:
+                self.async_on_remove(
+                    self.vehicle.stream_vehicle.listen_HvacLeftTemperatureRequest(
+                        self._async_handle_hvac_temperature_request
+                    )
+                )
+        elif self.side == TeslemetryClimateSide.PASSENGER:
+            if self.rhd:
+                self.async_on_remove(
+                    self.vehicle.stream_vehicle.listen_HvacLeftTemperatureRequest(
+                        self._async_handle_hvac_temperature_request
+                    )
+                )
+            else:
+                self.async_on_remove(
+                    self.vehicle.stream_vehicle.listen_HvacRightTemperatureRequest(
+                        self._async_handle_hvac_temperature_request
+                    )
+                )
+
+    def _async_handle_inside_temp(self, data: float | None):
+        self._attr_current_temperature = data
+        self.async_write_ha_state()
+
+    def _async_handle_hvac_ac_enabled(self, data: bool | None):
+        self._attr_hvac_mode = (
+            None if data is None else HVACMode.HEAT_COOL if data else HVACMode.OFF
+        )
+        self.async_write_ha_state()
+
+    def _async_handle_climate_keeper_mode(self, data: str | None):
+        self._attr_preset_mode = PRESET_MODES.get(data) if data else None
+        self.async_write_ha_state()
+
+    def _async_handle_hvac_temperature_request(self, data: float | None):
+        self._attr_target_temperature = data
+        self.async_write_ha_state()
+
+    def _async_handle_rhd(self, data: bool | None):
+        if data is not None:
+            self.rhd = data
+
+
 COP_MODES = {
     "Off": HVACMode.OFF,
     "On": HVACMode.COOL,
     "FanOnly": HVACMode.FAN_ONLY,
 }
 
-# String to celsius
 COP_LEVELS = {
     "Low": 30,
     "Medium": 35,
     "High": 40,
 }
 
-# Celsius to IntEnum
-TEMP_LEVELS = {
-    30: CabinOverheatProtectionTemp.LOW,
-    35: CabinOverheatProtectionTemp.MEDIUM,
-    40: CabinOverheatProtectionTemp.HIGH,
-}
 
+class TeslemetryCabinOverheatProtectionEntity(TeslemetryRootEntity, ClimateEntity):
+    """Vehicle Cabin Overheat Protection."""
 
-class TeslemetryCabinOverheatProtectionEntity(TeslemetryVehicleEntity, ClimateEntity):
-    """Telemetry vehicle cabin overheat protection entity."""
+    api: VehicleSpecific
 
     _attr_precision = PRECISION_WHOLE
     _attr_target_temperature_step = 5
-    _attr_min_temp = COP_LEVELS["Low"]
-    _attr_max_temp = COP_LEVELS["High"]
+    _attr_min_temp = 30
+    _attr_max_temp = 40
     _attr_temperature_unit = UnitOfTemperature.CELSIUS
     _attr_hvac_modes = list(COP_MODES.values())
-
     _attr_entity_registry_enabled_default = False
 
+    _enable_turn_on_off_backwards_compatibility = False
+
+    async def async_turn_on(self) -> None:
+        """Set the climate state to on."""
+        await self.async_set_hvac_mode(HVACMode.COOL)
+
+    async def async_turn_off(self) -> None:
+        """Set the climate state to off."""
+        await self.async_set_hvac_mode(HVACMode.OFF)
+
+    async def async_set_temperature(self, **kwargs: Any) -> None:
+        """Set the climate temperature."""
+
+        if temp := kwargs.get(ATTR_TEMPERATURE):
+            if (cop_mode := COP_TEMPERATURES.get(temp)) is None:
+                raise ServiceValidationError(
+                    translation_domain=DOMAIN,
+                    translation_key="invalid_cop_temp",
+                )
+            self.raise_for_scope(Scope.VEHICLE_CMDS)
+
+            await handle_vehicle_command(self.api.set_cop_temp(cop_mode))
+            self._attr_target_temperature = temp
+
+        if mode := kwargs.get(ATTR_HVAC_MODE):
+            # Set HVAC mode will call write_ha_state
+            await self.async_set_hvac_mode(mode)
+        else:
+            self.async_write_ha_state()
+
+    async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None:
+        """Set the climate mode and state."""
+        self.raise_for_scope(Scope.VEHICLE_CMDS)
+
+        if hvac_mode == HVACMode.OFF:
+            await handle_vehicle_command(
+                self.api.set_cabin_overheat_protection(on=False, fan_only=False)
+            )
+        elif hvac_mode == HVACMode.COOL:
+            await handle_vehicle_command(
+                self.api.set_cabin_overheat_protection(on=True, fan_only=False)
+            )
+        elif hvac_mode == HVACMode.FAN_ONLY:
+            await handle_vehicle_command(
+                self.api.set_cabin_overheat_protection(on=True, fan_only=True)
+            )
+
+        self._attr_hvac_mode = hvac_mode
+        self.async_write_ha_state()
+
+
+class TeslemetryPollingCabinOverheatProtectionEntity(
+    TeslemetryVehicleEntity, TeslemetryCabinOverheatProtectionEntity
+):
+    """Vehicle Cabin Overheat Protection."""
+
     def __init__(
         self,
         data: TeslemetryVehicleData,
-        scopes: Scope,
+        scopes: list[Scope],
     ) -> None:
         """Initialize the climate."""
 
-        self.scoped = Scope.VEHICLE_CMDS in scopes
-        if self.scoped:
-            self._attr_supported_features = (
-                ClimateEntityFeature.TURN_ON | ClimateEntityFeature.TURN_OFF
-            )
-        else:
-            self._attr_supported_features = ClimateEntityFeature(0)
-            self._attr_hvac_modes = []
-
-        super().__init__(data, "climate_state_cabin_overheat_protection")
+        super().__init__(
+            data,
+            "climate_state_cabin_overheat_protection",
+        )
 
-        # Supported Features from data
-        if self.scoped and self.get("vehicle_config_cop_user_set_temp_supported"):
+        # Supported Features
+        self._attr_supported_features = (
+            ClimateEntityFeature.TURN_ON | ClimateEntityFeature.TURN_OFF
+        )
+        if self.get("vehicle_config_cop_user_set_temp_supported"):
             self._attr_supported_features |= ClimateEntityFeature.TARGET_TEMPERATURE
 
+        # Scopes
+        self.scoped = Scope.VEHICLE_CMDS in scopes
+        if not self.scoped:
+            self._attr_supported_features = ClimateEntityFeature(0)
+
     def _async_update_attrs(self) -> None:
         """Update the attributes of the entity."""
 
@@ -239,10 +467,6 @@ def _async_update_attrs(self) -> None:
         else:
             self._attr_hvac_mode = COP_MODES.get(state)
 
-        # If not scoped, prevent the user from changing the HVAC mode by making it the only option
-        if self._attr_hvac_mode and not self.scoped:
-            self._attr_hvac_modes = [self._attr_hvac_mode]
-
         if (level := self.get("climate_state_cop_activation_temperature")) is None:
             self._attr_target_temperature = None
         else:
@@ -250,54 +474,78 @@ def _async_update_attrs(self) -> None:
 
         self._attr_current_temperature = self.get("climate_state_inside_temp")
 
-    async def async_turn_on(self) -> None:
-        """Set the climate state to on."""
-        await self.async_set_hvac_mode(HVACMode.COOL)
 
-    async def async_turn_off(self) -> None:
-        """Set the climate state to off."""
-        await self.async_set_hvac_mode(HVACMode.OFF)
+class TeslemetryStreamingCabinOverheatProtectionEntity(
+    TeslemetryVehicleStreamEntity,
+    TeslemetryCabinOverheatProtectionEntity,
+    RestoreEntity,
+):
+    """Vehicle Cabin Overheat Protection."""
 
-    async def async_set_temperature(self, **kwargs: Any) -> None:
-        """Set the climate temperature."""
-        self.raise_for_scope(Scope.VEHICLE_CMDS)
+    def __init__(
+        self,
+        data: TeslemetryVehicleData,
+        scopes: list[Scope],
+    ) -> None:
+        """Initialize the climate."""
 
-        if (temp := kwargs.get(ATTR_TEMPERATURE)) is None or (
-            cop_mode := TEMP_LEVELS.get(temp)
-        ) is None:
-            raise ServiceValidationError(
-                translation_domain=DOMAIN,
-                translation_key="invalid_cop_temp",
-            )
+        # Initialize defaults
+        self._attr_hvac_mode = None
+        self._attr_current_temperature = None
+        self._attr_target_temperature = None
+        self._attr_fan_mode = None
+        self._attr_preset_mode = None
 
-        await self.wake_up_if_asleep()
-        await handle_vehicle_command(self.api.set_cop_temp(cop_mode))
-        self._attr_target_temperature = temp
+        super().__init__(data, "climate_state_cabin_overheat_protection")
 
-        if mode := kwargs.get(ATTR_HVAC_MODE):
-            await self._async_set_cop(mode)
+        # Supported Features
+        self._attr_supported_features = (
+            ClimateEntityFeature.TURN_ON | ClimateEntityFeature.TURN_OFF
+        )
+        if data.coordinator.data.get("vehicle_config_cop_user_set_temp_supported"):
+            self._attr_supported_features |= ClimateEntityFeature.TARGET_TEMPERATURE
 
-        self.async_write_ha_state()
+        # Scopes
+        self.scoped = Scope.VEHICLE_CMDS in scopes
+        if not self.scoped:
+            self._attr_supported_features = ClimateEntityFeature(0)
 
-    async def _async_set_cop(self, hvac_mode: HVACMode) -> None:
-        if hvac_mode == HVACMode.OFF:
-            await handle_vehicle_command(
-                self.api.set_cabin_overheat_protection(on=False, fan_only=False)
+    async def async_added_to_hass(self) -> None:
+        """Handle entity which will be added."""
+        await super().async_added_to_hass()
+        if (state := await self.async_get_last_state()) is not None:
+            self._attr_hvac_mode = (
+                HVACMode(state.state) if state.state in HVAC_MODES else None
             )
-        elif hvac_mode == HVACMode.COOL:
-            await handle_vehicle_command(
-                self.api.set_cabin_overheat_protection(on=True, fan_only=False)
+            self._attr_current_temperature = state.attributes.get("temperature")
+            self._attr_target_temperature = state.attributes.get("target_temperature")
+
+        self.async_on_remove(
+            self.vehicle.stream_vehicle.listen_InsideTemp(
+                self._async_handle_inside_temp
             )
-        elif hvac_mode == HVACMode.FAN_ONLY:
-            await handle_vehicle_command(
-                self.api.set_cabin_overheat_protection(on=True, fan_only=True)
+        )
+        self.async_on_remove(
+            self.vehicle.stream_vehicle.listen_CabinOverheatProtectionMode(
+                self._async_handle_protection_mode
+            )
+        )
+        self.async_on_remove(
+            self.vehicle.stream_vehicle.listen_CabinOverheatProtectionTemperatureLimit(
+                self._async_handle_temperature_limit
             )
+        )
 
-        self._attr_hvac_mode = hvac_mode
+    def _async_handle_inside_temp(self, value: float | None):
+        self._attr_current_temperature = value
+        self.async_write_ha_state()
 
-    async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None:
-        """Set the climate mode and state."""
-        self.raise_for_scope(Scope.VEHICLE_CMDS)
-        await self.wake_up_if_asleep()
-        await self._async_set_cop(hvac_mode)
+    def _async_handle_protection_mode(self, value: str | None):
+        self._attr_hvac_mode = COP_MODES.get(value) if value is not None else None
+        self.async_write_ha_state()
+
+    def _async_handle_temperature_limit(self, value: str | None):
+        self._attr_target_temperature = (
+            COP_LEVELS.get(value) if value is not None else None
+        )
         self.async_write_ha_state()
diff --git a/homeassistant/components/teslemetry/strings.json b/homeassistant/components/teslemetry/strings.json
index c1df7d5aa5769b..ceb8b3c1af9f39 100644
--- a/homeassistant/components/teslemetry/strings.json
+++ b/homeassistant/components/teslemetry/strings.json
@@ -226,6 +226,12 @@
               "dog": "Dog mode",
               "camp": "Camp mode"
             }
+          },
+          "fan_mode": {
+            "state": {
+              "off": "[%key:common::state::off%]",
+              "bioweapon": "Bioweapon defense"
+            }
           }
         }
       }
diff --git a/tests/components/teslemetry/snapshots/test_climate.ambr b/tests/components/teslemetry/snapshots/test_climate.ambr
index 4c265c00cb8e84..e0e68f23c798e0 100644
--- a/tests/components/teslemetry/snapshots/test_climate.ambr
+++ b/tests/components/teslemetry/snapshots/test_climate.ambr
@@ -1,10 +1,4 @@
 # serializer version: 1
-# name: test_asleep_or_offline[HomeAssistantError]
-  'Timed out trying to wake up vehicle'
-# ---
-# name: test_asleep_or_offline[InvalidCommand]
-  'Failed to wake up vehicle: The data request or command is unknown.'
-# ---
 # name: test_climate[climate.test_cabin_overheat_protection-entry]
   EntityRegistryEntrySnapshot({
     'aliases': set({
@@ -78,6 +72,10 @@
     }),
     'area_id': None,
     'capabilities': dict({
+      'fan_modes': list([
+        'off',
+        'bioweapon',
+      ]),
       'hvac_modes': list([
         <HVACMode.HEAT_COOL: 'heat_cool'>,
         <HVACMode.OFF: 'off'>,
@@ -113,7 +111,7 @@
     'original_name': 'Climate',
     'platform': 'teslemetry',
     'previous_unique_id': None,
-    'supported_features': <ClimateEntityFeature: 401>,
+    'supported_features': <ClimateEntityFeature: 409>,
     'translation_key': <TeslemetryClimateSide.DRIVER: 'driver_temp'>,
     'unique_id': 'LRW3F7EK4NC700000-driver_temp',
     'unit_of_measurement': None,
@@ -123,6 +121,11 @@
   StateSnapshot({
     'attributes': ReadOnlyDict({
       'current_temperature': 30.0,
+      'fan_mode': 'off',
+      'fan_modes': list([
+        'off',
+        'bioweapon',
+      ]),
       'friendly_name': 'Test Climate',
       'hvac_modes': list([
         <HVACMode.HEAT_COOL: 'heat_cool'>,
@@ -137,7 +140,7 @@
         'dog',
         'camp',
       ]),
-      'supported_features': <ClimateEntityFeature: 401>,
+      'supported_features': <ClimateEntityFeature: 409>,
       'temperature': 22.0,
     }),
     'context': <ANY>,
@@ -220,6 +223,10 @@
     }),
     'area_id': None,
     'capabilities': dict({
+      'fan_modes': list([
+        'off',
+        'bioweapon',
+      ]),
       'hvac_modes': list([
         <HVACMode.HEAT_COOL: 'heat_cool'>,
         <HVACMode.OFF: 'off'>,
@@ -255,7 +262,7 @@
     'original_name': 'Climate',
     'platform': 'teslemetry',
     'previous_unique_id': None,
-    'supported_features': <ClimateEntityFeature: 401>,
+    'supported_features': <ClimateEntityFeature: 409>,
     'translation_key': <TeslemetryClimateSide.DRIVER: 'driver_temp'>,
     'unique_id': 'LRW3F7EK4NC700000-driver_temp',
     'unit_of_measurement': None,
@@ -265,6 +272,11 @@
   StateSnapshot({
     'attributes': ReadOnlyDict({
       'current_temperature': 30.0,
+      'fan_mode': 'off',
+      'fan_modes': list([
+        'off',
+        'bioweapon',
+      ]),
       'friendly_name': 'Test Climate',
       'hvac_modes': list([
         <HVACMode.HEAT_COOL: 'heat_cool'>,
@@ -279,7 +291,7 @@
         'dog',
         'camp',
       ]),
-      'supported_features': <ClimateEntityFeature: 401>,
+      'supported_features': <ClimateEntityFeature: 409>,
       'temperature': 22.0,
     }),
     'context': <ANY>,
@@ -297,7 +309,9 @@
     'area_id': None,
     'capabilities': dict({
       'hvac_modes': list([
+        <HVACMode.OFF: 'off'>,
         <HVACMode.COOL: 'cool'>,
+        <HVACMode.FAN_ONLY: 'fan_only'>,
       ]),
       'max_temp': 40,
       'min_temp': 30,
@@ -339,6 +353,7 @@
     'capabilities': dict({
       'hvac_modes': list([
         <HVACMode.HEAT_COOL: 'heat_cool'>,
+        <HVACMode.OFF: 'off'>,
       ]),
       'max_temp': 28.0,
       'min_temp': 15.0,
@@ -374,3 +389,85 @@
 # name: test_invalid_error[error]
   'Command returned exception: The data request or command is unknown.'
 # ---
+# name: test_select_streaming[climate.test_cabin_overheat_protection]
+  StateSnapshot({
+    'attributes': ReadOnlyDict({
+      'current_temperature': None,
+      'friendly_name': 'Test Cabin overheat protection',
+      'hvac_modes': list([
+        <HVACMode.OFF: 'off'>,
+        <HVACMode.COOL: 'cool'>,
+        <HVACMode.FAN_ONLY: 'fan_only'>,
+      ]),
+      'max_temp': 40,
+      'min_temp': 30,
+      'supported_features': <ClimateEntityFeature: 385>,
+      'target_temp_step': 5,
+      'temperature': None,
+    }),
+    'context': <ANY>,
+    'entity_id': 'climate.test_cabin_overheat_protection',
+    'last_changed': <ANY>,
+    'last_reported': <ANY>,
+    'last_updated': <ANY>,
+    'state': 'cool',
+  })
+# ---
+# name: test_select_streaming[climate.test_climate LHD]
+  StateSnapshot({
+    'attributes': ReadOnlyDict({
+      'current_temperature': 26.0,
+      'friendly_name': 'Test Climate',
+      'hvac_modes': list([
+        <HVACMode.HEAT_COOL: 'heat_cool'>,
+        <HVACMode.OFF: 'off'>,
+      ]),
+      'max_temp': 28.0,
+      'min_temp': 15.0,
+      'preset_mode': None,
+      'preset_modes': list([
+        'off',
+        'keep',
+        'dog',
+        'camp',
+      ]),
+      'supported_features': <ClimateEntityFeature: 401>,
+      'temperature': 21.0,
+    }),
+    'context': <ANY>,
+    'entity_id': 'climate.test_climate',
+    'last_changed': <ANY>,
+    'last_reported': <ANY>,
+    'last_updated': <ANY>,
+    'state': 'heat_cool',
+  })
+# ---
+# name: test_select_streaming[climate.test_climate]
+  StateSnapshot({
+    'attributes': ReadOnlyDict({
+      'current_temperature': 26.0,
+      'friendly_name': 'Test Climate',
+      'hvac_modes': list([
+        <HVACMode.HEAT_COOL: 'heat_cool'>,
+        <HVACMode.OFF: 'off'>,
+      ]),
+      'max_temp': 28.0,
+      'min_temp': 15.0,
+      'preset_mode': None,
+      'preset_modes': list([
+        'off',
+        'keep',
+        'dog',
+        'camp',
+      ]),
+      'supported_features': <ClimateEntityFeature: 401>,
+      'temperature': 21.0,
+    }),
+    'context': <ANY>,
+    'entity_id': 'climate.test_climate',
+    'last_changed': <ANY>,
+    'last_reported': <ANY>,
+    'last_updated': <ANY>,
+    'state': 'heat_cool',
+  })
+# ---
diff --git a/tests/components/teslemetry/test_climate.py b/tests/components/teslemetry/test_climate.py
index 33f2e134806901..948fbffa881750 100644
--- a/tests/components/teslemetry/test_climate.py
+++ b/tests/components/teslemetry/test_climate.py
@@ -2,10 +2,10 @@
 
 from unittest.mock import AsyncMock, patch
 
-from freezegun.api import FrozenDateTimeFactory
 import pytest
 from syrupy.assertion import SnapshotAssertion
 from tesla_fleet_api.exceptions import InvalidCommand
+from teslemetry_stream import Signal
 
 from homeassistant.components.climate import (
     ATTR_HVAC_MODE,
@@ -24,15 +24,12 @@
 from homeassistant.exceptions import HomeAssistantError, ServiceValidationError
 from homeassistant.helpers import entity_registry as er
 
-from . import assert_entities, setup_platform
+from . import assert_entities, reload_platform, setup_platform
 from .const import (
     COMMAND_ERRORS,
     COMMAND_IGNORED_REASON,
     METADATA_NOSCOPE,
     VEHICLE_DATA_ALT,
-    VEHICLE_DATA_ASLEEP,
-    WAKE_UP_ASLEEP,
-    WAKE_UP_ONLINE,
 )
 
 
@@ -41,6 +38,7 @@ async def test_climate(
     hass: HomeAssistant,
     snapshot: SnapshotAssertion,
     entity_registry: er.EntityRegistry,
+    mock_legacy: AsyncMock,
 ) -> None:
     """Tests that the climate entity is correct."""
 
@@ -195,6 +193,7 @@ async def test_climate_alt(
     snapshot: SnapshotAssertion,
     entity_registry: er.EntityRegistry,
     mock_vehicle_data: AsyncMock,
+    mock_legacy: AsyncMock,
 ) -> None:
     """Tests that the climate entity is correct."""
 
@@ -269,71 +268,12 @@ async def test_ignored_error(
         mock_on.assert_called_once()
 
 
-@pytest.mark.usefixtures("entity_registry_enabled_by_default")
-async def test_asleep_or_offline(
-    hass: HomeAssistant,
-    mock_vehicle_data: AsyncMock,
-    mock_wake_up: AsyncMock,
-    mock_vehicle: AsyncMock,
-    freezer: FrozenDateTimeFactory,
-    snapshot: SnapshotAssertion,
-) -> None:
-    """Tests asleep is handled."""
-
-    mock_vehicle_data.return_value = VEHICLE_DATA_ASLEEP
-    await setup_platform(hass, [Platform.CLIMATE])
-    entity_id = "climate.test_climate"
-
-    # Run a command but fail trying to wake up the vehicle
-    mock_wake_up.side_effect = InvalidCommand
-    with pytest.raises(HomeAssistantError) as error:
-        await hass.services.async_call(
-            CLIMATE_DOMAIN,
-            SERVICE_TURN_ON,
-            {ATTR_ENTITY_ID: [entity_id]},
-            blocking=True,
-        )
-    assert str(error.value) == snapshot(name="InvalidCommand")
-    mock_wake_up.assert_called_once()
-
-    mock_wake_up.side_effect = None
-    mock_wake_up.reset_mock()
-
-    # Run a command but timeout trying to wake up the vehicle
-    mock_wake_up.return_value = WAKE_UP_ASLEEP
-    mock_vehicle.return_value = WAKE_UP_ASLEEP
-    with (
-        patch("homeassistant.components.teslemetry.helpers.asyncio.sleep"),
-        pytest.raises(HomeAssistantError) as error,
-    ):
-        await hass.services.async_call(
-            CLIMATE_DOMAIN,
-            SERVICE_TURN_ON,
-            {ATTR_ENTITY_ID: [entity_id]},
-            blocking=True,
-        )
-    assert str(error.value) == snapshot(name="HomeAssistantError")
-    mock_wake_up.assert_called_once()
-    mock_vehicle.assert_called()
-
-    mock_wake_up.reset_mock()
-    mock_vehicle.reset_mock()
-    mock_wake_up.return_value = WAKE_UP_ONLINE
-    mock_vehicle.return_value = WAKE_UP_ONLINE
-
-    # Run a command and wake up the vehicle immediately
-    await hass.services.async_call(
-        CLIMATE_DOMAIN, SERVICE_TURN_ON, {ATTR_ENTITY_ID: [entity_id]}, blocking=True
-    )
-    await hass.async_block_till_done()
-    mock_wake_up.assert_called_once()
-
-
 async def test_climate_noscope(
     hass: HomeAssistant,
     snapshot: SnapshotAssertion,
     entity_registry: er.EntityRegistry,
     mock_metadata: AsyncMock,
+    mock_legacy: AsyncMock,
 ) -> None:
     """Tests that the climate entity is correct."""
     mock_metadata.return_value = METADATA_NOSCOPE
@@ -363,3 +303,47 @@ async def test_climate_noscope(
             {ATTR_ENTITY_ID: [entity_id], ATTR_TEMPERATURE: 20},
             blocking=True,
         )
+
+
+@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.CLIMATE])
+
+    # Stream update
+    mock_add_listener.send(
+        {
+            "vin": VEHICLE_DATA_ALT["response"]["vin"],
+            "data": {
+                Signal.INSIDE_TEMP: 26,
+                Signal.HVAC_AC_ENABLED: True,
+                Signal.CLIMATE_KEEPER_MODE: "ClimateKeeperModeOn",
+                Signal.RIGHT_HAND_DRIVE: True,
+                Signal.HVAC_LEFT_TEMPERATURE_REQUEST: 22,
+                Signal.HVAC_RIGHT_TEMPERATURE_REQUEST: 21,
+                Signal.CABIN_OVERHEAT_PROTECTION_MODE: "CabinOverheatProtectionModeStateOn",
+                Signal.CABIN_OVERHEAT_PROTECTION_TEMPERATURE_LIMIT: 35,
+            },
+            "createdAt": "2024-10-04T10:45:17.537Z",
+        }
+    )
+    await hass.async_block_till_done()
+
+    assert hass.states.get("climate.test_climate") == snapshot(
+        name="climate.test_climate LHD"
+    )
+
+    await reload_platform(hass, entry, [Platform.CLIMATE])
+
+    # Assert the entities restored their values
+    for entity_id in (
+        "climate.test_climate",
+        "climate.test_cabin_overheat_protection",
+    ):
+        assert hass.states.get(entity_id) == snapshot(name=entity_id)
diff --git a/tests/components/teslemetry/test_init.py b/tests/components/teslemetry/test_init.py
index 5481e6cc034712..fcf9c76c939753 100644
--- a/tests/components/teslemetry/test_init.py
+++ b/tests/components/teslemetry/test_init.py
@@ -2,17 +2,14 @@
 
 from unittest.mock import AsyncMock
 
-from freezegun.api import FrozenDateTimeFactory
 import pytest
 from syrupy.assertion import SnapshotAssertion
 from tesla_fleet_api.exceptions import (
     InvalidToken,
     SubscriptionRequired,
     TeslaFleetError,
-    VehicleOffline,
 )
 
-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 STATE_OFF, STATE_ON, Platform
@@ -22,8 +19,6 @@
 from . import setup_platform
 from .const import VEHICLE_DATA_ALT
 
-from tests.common import async_fire_time_changed
-
 ERRORS = [
     (InvalidToken, ConfigEntryState.SETUP_ERROR),
     (SubscriptionRequired, ConfigEntryState.SETUP_ERROR),
@@ -69,22 +64,6 @@ async def test_devices(
         assert device == snapshot(name=f"{device.identifiers}")
 
 
-async def test_vehicle_refresh_offline(
-    hass: HomeAssistant, mock_vehicle_data: AsyncMock, freezer: FrozenDateTimeFactory
-) -> None:
-    """Test coordinator refresh with an error."""
-    entry = await setup_platform(hass, [Platform.CLIMATE])
-    assert entry.state is ConfigEntryState.LOADED
-    mock_vehicle_data.assert_called_once()
-    mock_vehicle_data.reset_mock()
-
-    mock_vehicle_data.side_effect = VehicleOffline
-    freezer.tick(VEHICLE_INTERVAL)
-    async_fire_time_changed(hass)
-    await hass.async_block_till_done()
-    mock_vehicle_data.assert_called_once()
-
-
 @pytest.mark.parametrize(("side_effect", "state"), ERRORS)
 async def test_vehicle_refresh_error(
     hass: HomeAssistant,


Reviews:
user: login: joostlek
state: CHANGES_REQUESTED
author_association: MEMBER
submitted_at: 2025-03-14T14:18:27Z
user: login: joostlek
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-14T14:19:08Z
user: login: Bre77
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-14T21:08:11Z
user: login: Bre77
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-14T23:27:50Z
user: login: Bre77
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-14T23:30:41Z
user: login: Bre77
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-14T23:33:12Z
user: login: joostlek
state: CHANGES_REQUESTED
author_association: MEMBER
submitted_at: 2025-03-25T16:12:32Z
user: login: joostlek
state: APPROVED
author_association: MEMBER
submitted_at: 2025-03-25T20:09:38Z

Review comments:
diff_hunk: @@ -163,74 +144,329 @@ async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None:
 
     async def async_set_preset_mode(self, preset_mode: str) -> None:
         """Set the climate preset mode."""
-        await self.wake_up_if_asleep()
+        self.raise_for_scope(Scope.VEHICLE_CMDS)
+
         await handle_vehicle_command(
             self.api.set_climate_keeper_mode(
                 climate_keeper_mode=self._attr_preset_modes.index(preset_mode)
             )
         )
         self._attr_preset_mode = preset_mode
-        if preset_mode != self._attr_preset_modes[0]:
-            # Changing preset mode will also turn on climate
+        if preset_mode == self._attr_preset_modes[0]:
+            self._attr_hvac_mode = HVACMode.OFF
+        else:
+            self._attr_hvac_mode = HVACMode.HEAT_COOL
+        self.async_write_ha_state()
+
+    async def async_set_fan_mode(self, fan_mode: str) -> None:
+        """Set the Bioweapon defense mode."""
+        self.raise_for_scope(Scope.VEHICLE_CMDS)
+
+        await handle_vehicle_command(
+            self.api.set_bioweapon_mode(
+                on=(fan_mode != "off"),
+                manual_override=True,
+            )
+        )
+        self._attr_fan_mode = fan_mode
+        if fan_mode == self._attr_fan_modes[1]:
             self._attr_hvac_mode = HVACMode.HEAT_COOL
         self.async_write_ha_state()
 
 
+class TeslemetryPollingClimateEntity(TeslemetryClimateEntity, TeslemetryVehicleEntity):
+    """Polling vehicle climate entity."""
+
+    _attr_supported_features = (
+        ClimateEntityFeature.TURN_ON
+        | ClimateEntityFeature.TURN_OFF
+        | ClimateEntityFeature.TARGET_TEMPERATURE
+        | ClimateEntityFeature.PRESET_MODE
+        | ClimateEntityFeature.FAN_MODE
+    )
+
+    def __init__(
+        self,
+        data: TeslemetryVehicleData,
+        side: TeslemetryClimateSide,
+        scopes: list[Scope],
+    ) -> None:
+        """Initialize the climate."""
+        self.scoped = Scope.VEHICLE_CMDS in scopes
+        if not self.scoped:
+            self._attr_supported_features = ClimateEntityFeature(0)
+
+        super().__init__(
+            data,
+            side,
+        )
+
+    def _async_update_attrs(self) -> None:
+        """Update the attributes of the entity."""
+        value = self.get("climate_state_is_climate_on")
+        if value is None:
+            self._attr_hvac_mode = None
+        if value:
+            self._attr_hvac_mode = HVACMode.HEAT_COOL
+        else:
+            self._attr_hvac_mode = HVACMode.OFF
+
+        self._attr_current_temperature = self.get("climate_state_inside_temp")
+        self._attr_target_temperature = self.get(f"climate_state_{self.key}_setting")
+        self._attr_preset_mode = self.get("climate_state_climate_keeper_mode")
+        if self.get("climate_state_bioweapon_mode"):
+            self._attr_fan_mode = "bioweapon"
+        else:
+            self._attr_fan_mode = "off"
+        self._attr_min_temp = cast(
+            float, self.get("climate_state_min_avail_temp", DEFAULT_MIN_TEMP)
+        )
+        self._attr_max_temp = cast(
+            float, self.get("climate_state_max_avail_temp", DEFAULT_MAX_TEMP)
+        )
+
+
+class TeslemetryStreamingClimateEntity(
+    TeslemetryClimateEntity, TeslemetryVehicleStreamEntity, RestoreEntity
+):
+    """Teslemetry steering wheel climate control."""
+
+    _attr_supported_features = (
+        ClimateEntityFeature.TURN_ON
+        | ClimateEntityFeature.TURN_OFF
+        | ClimateEntityFeature.TARGET_TEMPERATURE
+        | ClimateEntityFeature.PRESET_MODE
+    )
+
+    side: TeslemetryClimateSide
+    rhd: bool
+
+    def __init__(
+        self,
+        data: TeslemetryVehicleData,
+        side: TeslemetryClimateSide,
+        scopes: list[Scope],
+    ) -> None:
+        """Initialize the climate."""
+
+        # Initialize defaults
+        self._attr_hvac_mode = None
+        self._attr_current_temperature = None
+        self._attr_target_temperature = None
+        self._attr_fan_mode = None
+        self._attr_preset_mode = None
+
+        self.scoped = Scope.VEHICLE_CMDS in scopes
+        if not self.scoped:
+            self._attr_supported_features = ClimateEntityFeature(0)
+        self.side = side
+        super().__init__(
+            data,
+            side,
+        )
+
+        self._attr_min_temp = cast(
+            float,
+            data.coordinator.data.get("climate_state_min_avail_temp", DEFAULT_MIN_TEMP),
+        )
+        self._attr_max_temp = cast(
+            float,
+            data.coordinator.data.get("climate_state_max_avail_temp", DEFAULT_MAX_TEMP),
+        )
+        self.rhd = data.coordinator.data.get("vehicle_config_rhd", False)
+
path: homeassistant/components/teslemetry/climate.py
user: login: joostlek
body: ```suggestion

    def __init__(
        self,
        data: TeslemetryVehicleData,
        side: TeslemetryClimateSide,
        scopes: list[Scope],
    ) -> None:
        """Initialize the climate."""

        # Initialize defaults
        self._attr_hvac_mode = None
        self._attr_current_temperature = None
        self._attr_target_temperature = None
        self._attr_fan_mode = None
        self._attr_preset_mode = None

        self.scoped = Scope.VEHICLE_CMDS in scopes
        if not self.scoped:
            self._attr_supported_features = ClimateEntityFeature(0)
        self.side = side
        super().__init__(
            data,
            side,
        )

        self._attr_min_temp = cast(
            float,
            data.coordinator.data.get("climate_state_min_avail_temp", DEFAULT_MIN_TEMP),
        )
        self._attr_max_temp = cast(
            float,
            data.coordinator.data.get("climate_state_max_avail_temp", DEFAULT_MAX_TEMP),
        )
        self.rhd: bool = data.coordinator.data.get("vehicle_config_rhd", False)

```
What is `rhd`?
created_at: 2025-03-14T14:15:39Z
author_association: MEMBER
diff_hunk: @@ -163,74 +144,329 @@ async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None:
 
     async def async_set_preset_mode(self, preset_mode: str) -> None:
         """Set the climate preset mode."""
-        await self.wake_up_if_asleep()
+        self.raise_for_scope(Scope.VEHICLE_CMDS)
+
         await handle_vehicle_command(
             self.api.set_climate_keeper_mode(
                 climate_keeper_mode=self._attr_preset_modes.index(preset_mode)
             )
         )
         self._attr_preset_mode = preset_mode
-        if preset_mode != self._attr_preset_modes[0]:
-            # Changing preset mode will also turn on climate
+        if preset_mode == self._attr_preset_modes[0]:
+            self._attr_hvac_mode = HVACMode.OFF
+        else:
+            self._attr_hvac_mode = HVACMode.HEAT_COOL
+        self.async_write_ha_state()
+
+    async def async_set_fan_mode(self, fan_mode: str) -> None:
+        """Set the Bioweapon defense mode."""
+        self.raise_for_scope(Scope.VEHICLE_CMDS)
+
+        await handle_vehicle_command(
+            self.api.set_bioweapon_mode(
+                on=(fan_mode != "off"),
+                manual_override=True,
+            )
+        )
+        self._attr_fan_mode = fan_mode
+        if fan_mode == self._attr_fan_modes[1]:
             self._attr_hvac_mode = HVACMode.HEAT_COOL
         self.async_write_ha_state()
 
 
+class TeslemetryPollingClimateEntity(TeslemetryClimateEntity, TeslemetryVehicleEntity):
+    """Polling vehicle climate entity."""
+
+    _attr_supported_features = (
+        ClimateEntityFeature.TURN_ON
+        | ClimateEntityFeature.TURN_OFF
+        | ClimateEntityFeature.TARGET_TEMPERATURE
+        | ClimateEntityFeature.PRESET_MODE
+        | ClimateEntityFeature.FAN_MODE
+    )
+
+    def __init__(
+        self,
+        data: TeslemetryVehicleData,
+        side: TeslemetryClimateSide,
+        scopes: list[Scope],
+    ) -> None:
+        """Initialize the climate."""
+        self.scoped = Scope.VEHICLE_CMDS in scopes
+        if not self.scoped:
+            self._attr_supported_features = ClimateEntityFeature(0)
+
+        super().__init__(
+            data,
+            side,
+        )
+
+    def _async_update_attrs(self) -> None:
+        """Update the attributes of the entity."""
+        value = self.get("climate_state_is_climate_on")
+        if value is None:
+            self._attr_hvac_mode = None
+        if value:
+            self._attr_hvac_mode = HVACMode.HEAT_COOL
+        else:
+            self._attr_hvac_mode = HVACMode.OFF
+
+        self._attr_current_temperature = self.get("climate_state_inside_temp")
+        self._attr_target_temperature = self.get(f"climate_state_{self.key}_setting")
+        self._attr_preset_mode = self.get("climate_state_climate_keeper_mode")
+        if self.get("climate_state_bioweapon_mode"):
+            self._attr_fan_mode = "bioweapon"
+        else:
+            self._attr_fan_mode = "off"
+        self._attr_min_temp = cast(
+            float, self.get("climate_state_min_avail_temp", DEFAULT_MIN_TEMP)
+        )
+        self._attr_max_temp = cast(
+            float, self.get("climate_state_max_avail_temp", DEFAULT_MAX_TEMP)
+        )
+
+
+class TeslemetryStreamingClimateEntity(
+    TeslemetryClimateEntity, TeslemetryVehicleStreamEntity, RestoreEntity
+):
+    """Teslemetry steering wheel climate control."""
+
+    _attr_supported_features = (
+        ClimateEntityFeature.TURN_ON
+        | ClimateEntityFeature.TURN_OFF
+        | ClimateEntityFeature.TARGET_TEMPERATURE
+        | ClimateEntityFeature.PRESET_MODE
+    )
+
+    side: TeslemetryClimateSide
path: homeassistant/components/teslemetry/climate.py
user: login: joostlek
body: Side can be inferred
created_at: 2025-03-14T14:16:02Z
author_association: MEMBER
diff_hunk: @@ -60,86 +75,52 @@ async def async_setup_entry(
     )
 
 
-class TeslemetryClimateEntity(TeslemetryVehicleEntity, ClimateEntity):
-    """Telemetry vehicle climate entity."""
+PRESET_MODES = {
+    "Off": "off",
+    "On": "keep",
+    "Dog": "dog",
+    "Party": "camp",
+}
 
-    _attr_precision = PRECISION_HALVES
 
-    _attr_temperature_unit = UnitOfTemperature.CELSIUS
-    _attr_hvac_modes = [HVACMode.HEAT_COOL, HVACMode.OFF]
-    _attr_supported_features = (
-        ClimateEntityFeature.TURN_ON
-        | ClimateEntityFeature.TURN_OFF
-        | ClimateEntityFeature.TARGET_TEMPERATURE
-        | ClimateEntityFeature.PRESET_MODE
-    )
-    _attr_preset_modes = ["off", "keep", "dog", "camp"]
+class TeslemetryClimateEntity(TeslemetryRootEntity, ClimateEntity):
+    """Vehicle Climate Control."""
 
-    def __init__(
-        self,
-        data: TeslemetryVehicleData,
-        side: TeslemetryClimateSide,
-        scopes: Scope,
-    ) -> None:
-        """Initialize the climate."""
-        self.scoped = Scope.VEHICLE_CMDS in scopes
+    api: VehicleSpecific
 
-        if not self.scoped:
-            self._attr_supported_features = ClimateEntityFeature(0)
-            self._attr_hvac_modes = []
-
-        super().__init__(
-            data,
-            side,
-        )
-
-    def _async_update_attrs(self) -> None:
-        """Update the attributes of the entity."""
-        value = self.get("climate_state_is_climate_on")
-        if value:
-            self._attr_hvac_mode = HVACMode.HEAT_COOL
-        else:
-            self._attr_hvac_mode = HVACMode.OFF
-
-        # If not scoped, prevent the user from changing the HVAC mode by making it the only option
-        if self._attr_hvac_mode and not self.scoped:
-            self._attr_hvac_modes = [self._attr_hvac_mode]
-
-        self._attr_current_temperature = self.get("climate_state_inside_temp")
-        self._attr_target_temperature = self.get(f"climate_state_{self.key}_setting")
-        self._attr_preset_mode = self.get("climate_state_climate_keeper_mode")
-        self._attr_min_temp = cast(
-            float, self.get("climate_state_min_avail_temp", DEFAULT_MIN_TEMP)
-        )
-        self._attr_max_temp = cast(
-            float, self.get("climate_state_max_avail_temp", DEFAULT_MAX_TEMP)
-        )
+    _attr_precision = PRECISION_HALVES
+    _attr_temperature_unit = UnitOfTemperature.CELSIUS
+    _attr_hvac_modes = [HVACMode.HEAT_COOL, HVACMode.OFF]
+    _attr_preset_modes = list(PRESET_MODES.values())
+    _attr_fan_modes = ["off", "bioweapon"]
path: homeassistant/components/teslemetry/climate.py
user: login: joostlek
body: does this need translations?
created_at: 2025-03-14T14:18:20Z
author_association: MEMBER
diff_hunk: @@ -163,74 +144,329 @@ async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None:
 
     async def async_set_preset_mode(self, preset_mode: str) -> None:
         """Set the climate preset mode."""
-        await self.wake_up_if_asleep()
+        self.raise_for_scope(Scope.VEHICLE_CMDS)
+
         await handle_vehicle_command(
             self.api.set_climate_keeper_mode(
                 climate_keeper_mode=self._attr_preset_modes.index(preset_mode)
             )
         )
         self._attr_preset_mode = preset_mode
-        if preset_mode != self._attr_preset_modes[0]:
-            # Changing preset mode will also turn on climate
+        if preset_mode == self._attr_preset_modes[0]:
+            self._attr_hvac_mode = HVACMode.OFF
+        else:
+            self._attr_hvac_mode = HVACMode.HEAT_COOL
+        self.async_write_ha_state()
+
+    async def async_set_fan_mode(self, fan_mode: str) -> None:
+        """Set the Bioweapon defense mode."""
+        self.raise_for_scope(Scope.VEHICLE_CMDS)
+
+        await handle_vehicle_command(
+            self.api.set_bioweapon_mode(
+                on=(fan_mode != "off"),
+                manual_override=True,
+            )
+        )
+        self._attr_fan_mode = fan_mode
+        if fan_mode == self._attr_fan_modes[1]:
             self._attr_hvac_mode = HVACMode.HEAT_COOL
         self.async_write_ha_state()
 
 
+class TeslemetryPollingClimateEntity(TeslemetryClimateEntity, TeslemetryVehicleEntity):
+    """Polling vehicle climate entity."""
+
+    _attr_supported_features = (
+        ClimateEntityFeature.TURN_ON
+        | ClimateEntityFeature.TURN_OFF
+        | ClimateEntityFeature.TARGET_TEMPERATURE
+        | ClimateEntityFeature.PRESET_MODE
+        | ClimateEntityFeature.FAN_MODE
+    )
+
+    def __init__(
+        self,
+        data: TeslemetryVehicleData,
+        side: TeslemetryClimateSide,
+        scopes: list[Scope],
+    ) -> None:
+        """Initialize the climate."""
+        self.scoped = Scope.VEHICLE_CMDS in scopes
+        if not self.scoped:
+            self._attr_supported_features = ClimateEntityFeature(0)
+
+        super().__init__(
+            data,
+            side,
+        )
+
+    def _async_update_attrs(self) -> None:
+        """Update the attributes of the entity."""
+        value = self.get("climate_state_is_climate_on")
+        if value is None:
+            self._attr_hvac_mode = None
+        if value:
+            self._attr_hvac_mode = HVACMode.HEAT_COOL
+        else:
+            self._attr_hvac_mode = HVACMode.OFF
+
+        self._attr_current_temperature = self.get("climate_state_inside_temp")
+        self._attr_target_temperature = self.get(f"climate_state_{self.key}_setting")
+        self._attr_preset_mode = self.get("climate_state_climate_keeper_mode")
+        if self.get("climate_state_bioweapon_mode"):
+            self._attr_fan_mode = "bioweapon"
+        else:
+            self._attr_fan_mode = "off"
+        self._attr_min_temp = cast(
+            float, self.get("climate_state_min_avail_temp", DEFAULT_MIN_TEMP)
+        )
+        self._attr_max_temp = cast(
+            float, self.get("climate_state_max_avail_temp", DEFAULT_MAX_TEMP)
+        )
+
+
+class TeslemetryStreamingClimateEntity(
+    TeslemetryClimateEntity, TeslemetryVehicleStreamEntity, RestoreEntity
+):
+    """Teslemetry steering wheel climate control."""
+
+    _attr_supported_features = (
+        ClimateEntityFeature.TURN_ON
+        | ClimateEntityFeature.TURN_OFF
+        | ClimateEntityFeature.TARGET_TEMPERATURE
+        | ClimateEntityFeature.PRESET_MODE
+    )
+
+    side: TeslemetryClimateSide
+    rhd: bool
+
+    def __init__(
+        self,
+        data: TeslemetryVehicleData,
+        side: TeslemetryClimateSide,
+        scopes: list[Scope],
+    ) -> None:
+        """Initialize the climate."""
+
+        # Initialize defaults
+        self._attr_hvac_mode = None
+        self._attr_current_temperature = None
+        self._attr_target_temperature = None
+        self._attr_fan_mode = None
+        self._attr_preset_mode = None
+
+        self.scoped = Scope.VEHICLE_CMDS in scopes
+        if not self.scoped:
+            self._attr_supported_features = ClimateEntityFeature(0)
+        self.side = side
+        super().__init__(
+            data,
+            side,
+        )
+
+        self._attr_min_temp = cast(
+            float,
+            data.coordinator.data.get("climate_state_min_avail_temp", DEFAULT_MIN_TEMP),
+        )
+        self._attr_max_temp = cast(
+            float,
+            data.coordinator.data.get("climate_state_max_avail_temp", DEFAULT_MAX_TEMP),
+        )
+        self.rhd = data.coordinator.data.get("vehicle_config_rhd", False)
+
+    async def async_added_to_hass(self) -> None:
+        """Handle entity which will be added."""
+        await super().async_added_to_hass()
+        if (state := await self.async_get_last_state()) is not None:
+            self._attr_hvac_mode = (
+                HVACMode(state.state) if state.state in HVAC_MODES else None
+            )
+            self._attr_current_temperature = state.attributes.get("current_temperature")
+            self._attr_target_temperature = state.attributes.get("temperature")
+            self._attr_preset_mode = state.attributes.get("preset_mode")
+
+        self.async_on_remove(
+            self.vehicle.stream_vehicle.listen_InsideTemp(
+                self._async_handle_inside_temp
+            )
+        )
+        self.async_on_remove(
+            self.vehicle.stream_vehicle.listen_HvacACEnabled(
+                self._async_handle_hvac_ac_enabled
+            )
+        )
+        self.async_on_remove(
+            self.vehicle.stream_vehicle.listen_ClimateKeeperMode(
+                self._async_handle_climate_keeper_mode
+            )
+        )
+        self.async_on_remove(
+            self.vehicle.stream_vehicle.listen_RightHandDrive(self._async_handle_rhd)
+        )
+
+        if self.side == TeslemetryClimateSide.DRIVER:
+            if self.rhd:
+                self.async_on_remove(
+                    self.vehicle.stream_vehicle.listen_HvacRightTemperatureRequest(
+                        self._async_handle_hvac_temperature_request
+                    )
+                )
+            else:
+                self.async_on_remove(
+                    self.vehicle.stream_vehicle.listen_HvacLeftTemperatureRequest(
+                        self._async_handle_hvac_temperature_request
+                    )
+                )
+        elif self.side == TeslemetryClimateSide.PASSENGER:
+            if self.rhd:
+                self.async_on_remove(
+                    self.vehicle.stream_vehicle.listen_HvacLeftTemperatureRequest(
+                        self._async_handle_hvac_temperature_request
+                    )
+                )
+            else:
+                self.async_on_remove(
+                    self.vehicle.stream_vehicle.listen_HvacRightTemperatureRequest(
+                        self._async_handle_hvac_temperature_request
+                    )
+                )
+
+    def _async_handle_inside_temp(self, data: float | None):
+        self._attr_current_temperature = data
+        self.async_write_ha_state()
+
+    def _async_handle_hvac_ac_enabled(self, data: bool | None):
+        self._attr_hvac_mode = (
+            None if data is None else HVACMode.HEAT_COOL if data else HVACMode.OFF
+        )
+        self.async_write_ha_state()
+
+    def _async_handle_climate_keeper_mode(self, data: str | None):
+        self._attr_preset_mode = PRESET_MODES.get(data) if data else None
+        self.async_write_ha_state()
+
+    def _async_handle_hvac_temperature_request(self, data: float | None):
+        self._attr_target_temperature = data
+        self.async_write_ha_state()
+
+    def _async_handle_rhd(self, data: bool | None):
+        if data is not None:
+            self.rhd = data
+
+
 COP_MODES = {
     "Off": HVACMode.OFF,
     "On": HVACMode.COOL,
     "FanOnly": HVACMode.FAN_ONLY,
 }
 
-# String to celsius
 COP_LEVELS = {
     "Low": 30,
     "Medium": 35,
     "High": 40,
 }
 
-# Celsius to IntEnum
-TEMP_LEVELS = {
-    30: CabinOverheatProtectionTemp.LOW,
-    35: CabinOverheatProtectionTemp.MEDIUM,
-    40: CabinOverheatProtectionTemp.HIGH,
-}
 
+class TeslemetryCabinOverheatProtectionEntity(TeslemetryRootEntity, ClimateEntity):
+    """Vehicle Cabin Overheat Protection."""
 
-class TeslemetryCabinOverheatProtectionEntity(TeslemetryVehicleEntity, ClimateEntity):
-    """Telemetry vehicle cabin overheat protection entity."""
+    api: VehicleSpecific
 
     _attr_precision = PRECISION_WHOLE
     _attr_target_temperature_step = 5
-    _attr_min_temp = COP_LEVELS["Low"]
-    _attr_max_temp = COP_LEVELS["High"]
+    _attr_min_temp = 30
+    _attr_max_temp = 40
     _attr_temperature_unit = UnitOfTemperature.CELSIUS
     _attr_hvac_modes = list(COP_MODES.values())
-
     _attr_entity_registry_enabled_default = False
 
+    _enable_turn_on_off_backwards_compatibility = False
+
+    async def async_turn_on(self) -> None:
+        """Set the climate state to on."""
+        await self.async_set_hvac_mode(HVACMode.COOL)
+
+    async def async_turn_off(self) -> None:
+        """Set the climate state to off."""
+        await self.async_set_hvac_mode(HVACMode.OFF)
+
+    async def async_set_temperature(self, **kwargs: Any) -> None:
+        """Set the climate temperature."""
+
+        if temp := kwargs.get(ATTR_TEMPERATURE):
+            if temp == 30:
+                cop_mode = CabinOverheatProtectionTemp.LOW
+            elif temp == 35:
+                cop_mode = CabinOverheatProtectionTemp.MEDIUM
+            elif temp == 40:
+                cop_mode = CabinOverheatProtectionTemp.HIGH
path: homeassistant/components/teslemetry/climate.py
user: login: joostlek
body: can this be a dict?
created_at: 2025-03-14T14:19:08Z
author_association: MEMBER
diff_hunk: @@ -163,74 +144,329 @@ async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None:
 
     async def async_set_preset_mode(self, preset_mode: str) -> None:
         """Set the climate preset mode."""
-        await self.wake_up_if_asleep()
+        self.raise_for_scope(Scope.VEHICLE_CMDS)
+
         await handle_vehicle_command(
             self.api.set_climate_keeper_mode(
                 climate_keeper_mode=self._attr_preset_modes.index(preset_mode)
             )
         )
         self._attr_preset_mode = preset_mode
-        if preset_mode != self._attr_preset_modes[0]:
-            # Changing preset mode will also turn on climate
+        if preset_mode == self._attr_preset_modes[0]:
+            self._attr_hvac_mode = HVACMode.OFF
+        else:
+            self._attr_hvac_mode = HVACMode.HEAT_COOL
+        self.async_write_ha_state()
+
+    async def async_set_fan_mode(self, fan_mode: str) -> None:
+        """Set the Bioweapon defense mode."""
+        self.raise_for_scope(Scope.VEHICLE_CMDS)
+
+        await handle_vehicle_command(
+            self.api.set_bioweapon_mode(
+                on=(fan_mode != "off"),
+                manual_override=True,
+            )
+        )
+        self._attr_fan_mode = fan_mode
+        if fan_mode == self._attr_fan_modes[1]:
             self._attr_hvac_mode = HVACMode.HEAT_COOL
         self.async_write_ha_state()
 
 
+class TeslemetryPollingClimateEntity(TeslemetryClimateEntity, TeslemetryVehicleEntity):
+    """Polling vehicle climate entity."""
+
+    _attr_supported_features = (
+        ClimateEntityFeature.TURN_ON
+        | ClimateEntityFeature.TURN_OFF
+        | ClimateEntityFeature.TARGET_TEMPERATURE
+        | ClimateEntityFeature.PRESET_MODE
+        | ClimateEntityFeature.FAN_MODE
+    )
+
+    def __init__(
+        self,
+        data: TeslemetryVehicleData,
+        side: TeslemetryClimateSide,
+        scopes: list[Scope],
+    ) -> None:
+        """Initialize the climate."""
+        self.scoped = Scope.VEHICLE_CMDS in scopes
+        if not self.scoped:
+            self._attr_supported_features = ClimateEntityFeature(0)
+
+        super().__init__(
+            data,
+            side,
+        )
+
+    def _async_update_attrs(self) -> None:
+        """Update the attributes of the entity."""
+        value = self.get("climate_state_is_climate_on")
+        if value is None:
+            self._attr_hvac_mode = None
+        if value:
+            self._attr_hvac_mode = HVACMode.HEAT_COOL
+        else:
+            self._attr_hvac_mode = HVACMode.OFF
+
+        self._attr_current_temperature = self.get("climate_state_inside_temp")
+        self._attr_target_temperature = self.get(f"climate_state_{self.key}_setting")
+        self._attr_preset_mode = self.get("climate_state_climate_keeper_mode")
+        if self.get("climate_state_bioweapon_mode"):
+            self._attr_fan_mode = "bioweapon"
+        else:
+            self._attr_fan_mode = "off"
+        self._attr_min_temp = cast(
+            float, self.get("climate_state_min_avail_temp", DEFAULT_MIN_TEMP)
+        )
+        self._attr_max_temp = cast(
+            float, self.get("climate_state_max_avail_temp", DEFAULT_MAX_TEMP)
+        )
+
+
+class TeslemetryStreamingClimateEntity(
+    TeslemetryClimateEntity, TeslemetryVehicleStreamEntity, RestoreEntity
+):
+    """Teslemetry steering wheel climate control."""
+
+    _attr_supported_features = (
+        ClimateEntityFeature.TURN_ON
+        | ClimateEntityFeature.TURN_OFF
+        | ClimateEntityFeature.TARGET_TEMPERATURE
+        | ClimateEntityFeature.PRESET_MODE
+    )
+
+    side: TeslemetryClimateSide
+    rhd: bool
+
+    def __init__(
+        self,
+        data: TeslemetryVehicleData,
+        side: TeslemetryClimateSide,
+        scopes: list[Scope],
+    ) -> None:
+        """Initialize the climate."""
+
+        # Initialize defaults
+        self._attr_hvac_mode = None
+        self._attr_current_temperature = None
+        self._attr_target_temperature = None
+        self._attr_fan_mode = None
+        self._attr_preset_mode = None
+
+        self.scoped = Scope.VEHICLE_CMDS in scopes
+        if not self.scoped:
+            self._attr_supported_features = ClimateEntityFeature(0)
+        self.side = side
+        super().__init__(
+            data,
+            side,
+        )
+
+        self._attr_min_temp = cast(
+            float,
+            data.coordinator.data.get("climate_state_min_avail_temp", DEFAULT_MIN_TEMP),
+        )
+        self._attr_max_temp = cast(
+            float,
+            data.coordinator.data.get("climate_state_max_avail_temp", DEFAULT_MAX_TEMP),
+        )
+        self.rhd = data.coordinator.data.get("vehicle_config_rhd", False)
+
path: homeassistant/components/teslemetry/climate.py
user: login: Bre77
body: Right hand drive 
created_at: 2025-03-14T21:08:11Z
author_association: CONTRIBUTOR
diff_hunk: @@ -163,74 +144,329 @@ async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None:
 
     async def async_set_preset_mode(self, preset_mode: str) -> None:
         """Set the climate preset mode."""
-        await self.wake_up_if_asleep()
+        self.raise_for_scope(Scope.VEHICLE_CMDS)
+
         await handle_vehicle_command(
             self.api.set_climate_keeper_mode(
                 climate_keeper_mode=self._attr_preset_modes.index(preset_mode)
             )
         )
         self._attr_preset_mode = preset_mode
-        if preset_mode != self._attr_preset_modes[0]:
-            # Changing preset mode will also turn on climate
+        if preset_mode == self._attr_preset_modes[0]:
+            self._attr_hvac_mode = HVACMode.OFF
+        else:
+            self._attr_hvac_mode = HVACMode.HEAT_COOL
+        self.async_write_ha_state()
+
+    async def async_set_fan_mode(self, fan_mode: str) -> None:
+        """Set the Bioweapon defense mode."""
+        self.raise_for_scope(Scope.VEHICLE_CMDS)
+
+        await handle_vehicle_command(
+            self.api.set_bioweapon_mode(
+                on=(fan_mode != "off"),
+                manual_override=True,
+            )
+        )
+        self._attr_fan_mode = fan_mode
+        if fan_mode == self._attr_fan_modes[1]:
             self._attr_hvac_mode = HVACMode.HEAT_COOL
         self.async_write_ha_state()
 
 
+class TeslemetryPollingClimateEntity(TeslemetryClimateEntity, TeslemetryVehicleEntity):
+    """Polling vehicle climate entity."""
+
+    _attr_supported_features = (
+        ClimateEntityFeature.TURN_ON
+        | ClimateEntityFeature.TURN_OFF
+        | ClimateEntityFeature.TARGET_TEMPERATURE
+        | ClimateEntityFeature.PRESET_MODE
+        | ClimateEntityFeature.FAN_MODE
+    )
+
+    def __init__(
+        self,
+        data: TeslemetryVehicleData,
+        side: TeslemetryClimateSide,
+        scopes: list[Scope],
+    ) -> None:
+        """Initialize the climate."""
+        self.scoped = Scope.VEHICLE_CMDS in scopes
+        if not self.scoped:
+            self._attr_supported_features = ClimateEntityFeature(0)
+
+        super().__init__(
+            data,
+            side,
+        )
+
+    def _async_update_attrs(self) -> None:
+        """Update the attributes of the entity."""
+        value = self.get("climate_state_is_climate_on")
+        if value is None:
+            self._attr_hvac_mode = None
+        if value:
+            self._attr_hvac_mode = HVACMode.HEAT_COOL
+        else:
+            self._attr_hvac_mode = HVACMode.OFF
+
+        self._attr_current_temperature = self.get("climate_state_inside_temp")
+        self._attr_target_temperature = self.get(f"climate_state_{self.key}_setting")
+        self._attr_preset_mode = self.get("climate_state_climate_keeper_mode")
+        if self.get("climate_state_bioweapon_mode"):
+            self._attr_fan_mode = "bioweapon"
+        else:
+            self._attr_fan_mode = "off"
+        self._attr_min_temp = cast(
+            float, self.get("climate_state_min_avail_temp", DEFAULT_MIN_TEMP)
+        )
+        self._attr_max_temp = cast(
+            float, self.get("climate_state_max_avail_temp", DEFAULT_MAX_TEMP)
+        )
+
+
+class TeslemetryStreamingClimateEntity(
+    TeslemetryClimateEntity, TeslemetryVehicleStreamEntity, RestoreEntity
+):
+    """Teslemetry steering wheel climate control."""
+
+    _attr_supported_features = (
+        ClimateEntityFeature.TURN_ON
+        | ClimateEntityFeature.TURN_OFF
+        | ClimateEntityFeature.TARGET_TEMPERATURE
+        | ClimateEntityFeature.PRESET_MODE
+    )
+
+    side: TeslemetryClimateSide
+    rhd: bool
+
+    def __init__(
+        self,
+        data: TeslemetryVehicleData,
+        side: TeslemetryClimateSide,
+        scopes: list[Scope],
+    ) -> None:
+        """Initialize the climate."""
+
+        # Initialize defaults
+        self._attr_hvac_mode = None
+        self._attr_current_temperature = None
+        self._attr_target_temperature = None
+        self._attr_fan_mode = None
+        self._attr_preset_mode = None
+
+        self.scoped = Scope.VEHICLE_CMDS in scopes
+        if not self.scoped:
+            self._attr_supported_features = ClimateEntityFeature(0)
+        self.side = side
+        super().__init__(
+            data,
+            side,
+        )
+
+        self._attr_min_temp = cast(
+            float,
+            data.coordinator.data.get("climate_state_min_avail_temp", DEFAULT_MIN_TEMP),
+        )
+        self._attr_max_temp = cast(
+            float,
+            data.coordinator.data.get("climate_state_max_avail_temp", DEFAULT_MAX_TEMP),
+        )
+        self.rhd = data.coordinator.data.get("vehicle_config_rhd", False)
+
+    async def async_added_to_hass(self) -> None:
+        """Handle entity which will be added."""
+        await super().async_added_to_hass()
+        if (state := await self.async_get_last_state()) is not None:
+            self._attr_hvac_mode = (
+                HVACMode(state.state) if state.state in HVAC_MODES else None
+            )
+            self._attr_current_temperature = state.attributes.get("current_temperature")
+            self._attr_target_temperature = state.attributes.get("temperature")
+            self._attr_preset_mode = state.attributes.get("preset_mode")
+
+        self.async_on_remove(
+            self.vehicle.stream_vehicle.listen_InsideTemp(
+                self._async_handle_inside_temp
+            )
+        )
+        self.async_on_remove(
+            self.vehicle.stream_vehicle.listen_HvacACEnabled(
+                self._async_handle_hvac_ac_enabled
+            )
+        )
+        self.async_on_remove(
+            self.vehicle.stream_vehicle.listen_ClimateKeeperMode(
+                self._async_handle_climate_keeper_mode
+            )
+        )
+        self.async_on_remove(
+            self.vehicle.stream_vehicle.listen_RightHandDrive(self._async_handle_rhd)
+        )
+
+        if self.side == TeslemetryClimateSide.DRIVER:
+            if self.rhd:
+                self.async_on_remove(
+                    self.vehicle.stream_vehicle.listen_HvacRightTemperatureRequest(
+                        self._async_handle_hvac_temperature_request
+                    )
+                )
+            else:
+                self.async_on_remove(
+                    self.vehicle.stream_vehicle.listen_HvacLeftTemperatureRequest(
+                        self._async_handle_hvac_temperature_request
+                    )
+                )
+        elif self.side == TeslemetryClimateSide.PASSENGER:
+            if self.rhd:
+                self.async_on_remove(
+                    self.vehicle.stream_vehicle.listen_HvacLeftTemperatureRequest(
+                        self._async_handle_hvac_temperature_request
+                    )
+                )
+            else:
+                self.async_on_remove(
+                    self.vehicle.stream_vehicle.listen_HvacRightTemperatureRequest(
+                        self._async_handle_hvac_temperature_request
+                    )
+                )
+
+    def _async_handle_inside_temp(self, data: float | None):
+        self._attr_current_temperature = data
+        self.async_write_ha_state()
+
+    def _async_handle_hvac_ac_enabled(self, data: bool | None):
+        self._attr_hvac_mode = (
+            None if data is None else HVACMode.HEAT_COOL if data else HVACMode.OFF
+        )
+        self.async_write_ha_state()
+
+    def _async_handle_climate_keeper_mode(self, data: str | None):
+        self._attr_preset_mode = PRESET_MODES.get(data) if data else None
+        self.async_write_ha_state()
+
+    def _async_handle_hvac_temperature_request(self, data: float | None):
+        self._attr_target_temperature = data
+        self.async_write_ha_state()
+
+    def _async_handle_rhd(self, data: bool | None):
+        if data is not None:
+            self.rhd = data
+
+
 COP_MODES = {
     "Off": HVACMode.OFF,
     "On": HVACMode.COOL,
     "FanOnly": HVACMode.FAN_ONLY,
 }
 
-# String to celsius
 COP_LEVELS = {
     "Low": 30,
     "Medium": 35,
     "High": 40,
 }
 
-# Celsius to IntEnum
-TEMP_LEVELS = {
-    30: CabinOverheatProtectionTemp.LOW,
-    35: CabinOverheatProtectionTemp.MEDIUM,
-    40: CabinOverheatProtectionTemp.HIGH,
-}
 
+class TeslemetryCabinOverheatProtectionEntity(TeslemetryRootEntity, ClimateEntity):
+    """Vehicle Cabin Overheat Protection."""
 
-class TeslemetryCabinOverheatProtectionEntity(TeslemetryVehicleEntity, ClimateEntity):
-    """Telemetry vehicle cabin overheat protection entity."""
+    api: VehicleSpecific
 
     _attr_precision = PRECISION_WHOLE
     _attr_target_temperature_step = 5
-    _attr_min_temp = COP_LEVELS["Low"]
-    _attr_max_temp = COP_LEVELS["High"]
+    _attr_min_temp = 30
+    _attr_max_temp = 40
     _attr_temperature_unit = UnitOfTemperature.CELSIUS
     _attr_hvac_modes = list(COP_MODES.values())
-
     _attr_entity_registry_enabled_default = False
 
+    _enable_turn_on_off_backwards_compatibility = False
+
+    async def async_turn_on(self) -> None:
+        """Set the climate state to on."""
+        await self.async_set_hvac_mode(HVACMode.COOL)
+
+    async def async_turn_off(self) -> None:
+        """Set the climate state to off."""
+        await self.async_set_hvac_mode(HVACMode.OFF)
+
+    async def async_set_temperature(self, **kwargs: Any) -> None:
+        """Set the climate temperature."""
+
+        if temp := kwargs.get(ATTR_TEMPERATURE):
+            if temp == 30:
+                cop_mode = CabinOverheatProtectionTemp.LOW
+            elif temp == 35:
+                cop_mode = CabinOverheatProtectionTemp.MEDIUM
+            elif temp == 40:
+                cop_mode = CabinOverheatProtectionTemp.HIGH
path: homeassistant/components/teslemetry/climate.py
user: login: Bre77
body: Yes, implemented.
created_at: 2025-03-14T23:27:50Z
author_association: CONTRIBUTOR
diff_hunk: @@ -60,86 +75,52 @@ async def async_setup_entry(
     )
 
 
-class TeslemetryClimateEntity(TeslemetryVehicleEntity, ClimateEntity):
-    """Telemetry vehicle climate entity."""
+PRESET_MODES = {
+    "Off": "off",
+    "On": "keep",
+    "Dog": "dog",
+    "Party": "camp",
+}
 
-    _attr_precision = PRECISION_HALVES
 
-    _attr_temperature_unit = UnitOfTemperature.CELSIUS
-    _attr_hvac_modes = [HVACMode.HEAT_COOL, HVACMode.OFF]
-    _attr_supported_features = (
-        ClimateEntityFeature.TURN_ON
-        | ClimateEntityFeature.TURN_OFF
-        | ClimateEntityFeature.TARGET_TEMPERATURE
-        | ClimateEntityFeature.PRESET_MODE
-    )
-    _attr_preset_modes = ["off", "keep", "dog", "camp"]
+class TeslemetryClimateEntity(TeslemetryRootEntity, ClimateEntity):
+    """Vehicle Climate Control."""
 
-    def __init__(
-        self,
-        data: TeslemetryVehicleData,
-        side: TeslemetryClimateSide,
-        scopes: Scope,
-    ) -> None:
-        """Initialize the climate."""
-        self.scoped = Scope.VEHICLE_CMDS in scopes
+    api: VehicleSpecific
 
-        if not self.scoped:
-            self._attr_supported_features = ClimateEntityFeature(0)
-            self._attr_hvac_modes = []
-
-        super().__init__(
-            data,
-            side,
-        )
-
-    def _async_update_attrs(self) -> None:
-        """Update the attributes of the entity."""
-        value = self.get("climate_state_is_climate_on")
-        if value:
-            self._attr_hvac_mode = HVACMode.HEAT_COOL
-        else:
-            self._attr_hvac_mode = HVACMode.OFF
-
-        # If not scoped, prevent the user from changing the HVAC mode by making it the only option
-        if self._attr_hvac_mode and not self.scoped:
-            self._attr_hvac_modes = [self._attr_hvac_mode]
-
-        self._attr_current_temperature = self.get("climate_state_inside_temp")
-        self._attr_target_temperature = self.get(f"climate_state_{self.key}_setting")
-        self._attr_preset_mode = self.get("climate_state_climate_keeper_mode")
-        self._attr_min_temp = cast(
-            float, self.get("climate_state_min_avail_temp", DEFAULT_MIN_TEMP)
-        )
-        self._attr_max_temp = cast(
-            float, self.get("climate_state_max_avail_temp", DEFAULT_MAX_TEMP)
-        )
+    _attr_precision = PRECISION_HALVES
+    _attr_temperature_unit = UnitOfTemperature.CELSIUS
+    _attr_hvac_modes = [HVACMode.HEAT_COOL, HVACMode.OFF]
+    _attr_preset_modes = list(PRESET_MODES.values())
+    _attr_fan_modes = ["off", "bioweapon"]
path: homeassistant/components/teslemetry/climate.py
user: login: Bre77
body: Yes, added
created_at: 2025-03-14T23:30:41Z
author_association: CONTRIBUTOR
diff_hunk: @@ -163,74 +144,329 @@ async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None:
 
     async def async_set_preset_mode(self, preset_mode: str) -> None:
         """Set the climate preset mode."""
-        await self.wake_up_if_asleep()
+        self.raise_for_scope(Scope.VEHICLE_CMDS)
+
         await handle_vehicle_command(
             self.api.set_climate_keeper_mode(
                 climate_keeper_mode=self._attr_preset_modes.index(preset_mode)
             )
         )
         self._attr_preset_mode = preset_mode
-        if preset_mode != self._attr_preset_modes[0]:
-            # Changing preset mode will also turn on climate
+        if preset_mode == self._attr_preset_modes[0]:
+            self._attr_hvac_mode = HVACMode.OFF
+        else:
+            self._attr_hvac_mode = HVACMode.HEAT_COOL
+        self.async_write_ha_state()
+
+    async def async_set_fan_mode(self, fan_mode: str) -> None:
+        """Set the Bioweapon defense mode."""
+        self.raise_for_scope(Scope.VEHICLE_CMDS)
+
+        await handle_vehicle_command(
+            self.api.set_bioweapon_mode(
+                on=(fan_mode != "off"),
+                manual_override=True,
+            )
+        )
+        self._attr_fan_mode = fan_mode
+        if fan_mode == self._attr_fan_modes[1]:
             self._attr_hvac_mode = HVACMode.HEAT_COOL
         self.async_write_ha_state()
 
 
+class TeslemetryPollingClimateEntity(TeslemetryClimateEntity, TeslemetryVehicleEntity):
+    """Polling vehicle climate entity."""
+
+    _attr_supported_features = (
+        ClimateEntityFeature.TURN_ON
+        | ClimateEntityFeature.TURN_OFF
+        | ClimateEntityFeature.TARGET_TEMPERATURE
+        | ClimateEntityFeature.PRESET_MODE
+        | ClimateEntityFeature.FAN_MODE
+    )
+
+    def __init__(
+        self,
+        data: TeslemetryVehicleData,
+        side: TeslemetryClimateSide,
+        scopes: list[Scope],
+    ) -> None:
+        """Initialize the climate."""
+        self.scoped = Scope.VEHICLE_CMDS in scopes
+        if not self.scoped:
+            self._attr_supported_features = ClimateEntityFeature(0)
+
+        super().__init__(
+            data,
+            side,
+        )
+
+    def _async_update_attrs(self) -> None:
+        """Update the attributes of the entity."""
+        value = self.get("climate_state_is_climate_on")
+        if value is None:
+            self._attr_hvac_mode = None
+        if value:
+            self._attr_hvac_mode = HVACMode.HEAT_COOL
+        else:
+            self._attr_hvac_mode = HVACMode.OFF
+
+        self._attr_current_temperature = self.get("climate_state_inside_temp")
+        self._attr_target_temperature = self.get(f"climate_state_{self.key}_setting")
+        self._attr_preset_mode = self.get("climate_state_climate_keeper_mode")
+        if self.get("climate_state_bioweapon_mode"):
+            self._attr_fan_mode = "bioweapon"
+        else:
+            self._attr_fan_mode = "off"
+        self._attr_min_temp = cast(
+            float, self.get("climate_state_min_avail_temp", DEFAULT_MIN_TEMP)
+        )
+        self._attr_max_temp = cast(
+            float, self.get("climate_state_max_avail_temp", DEFAULT_MAX_TEMP)
+        )
+
+
+class TeslemetryStreamingClimateEntity(
+    TeslemetryClimateEntity, TeslemetryVehicleStreamEntity, RestoreEntity
+):
+    """Teslemetry steering wheel climate control."""
+
+    _attr_supported_features = (
+        ClimateEntityFeature.TURN_ON
+        | ClimateEntityFeature.TURN_OFF
+        | ClimateEntityFeature.TARGET_TEMPERATURE
+        | ClimateEntityFeature.PRESET_MODE
+    )
+
+    side: TeslemetryClimateSide
path: homeassistant/components/teslemetry/climate.py
user: login: Bre77
body: Removed
created_at: 2025-03-14T23:33:11Z
author_association: CONTRIBUTOR
diff_hunk: @@ -226,6 +226,12 @@
               "dog": "Dog mode",
               "camp": "Camp mode"
             }
+          },
+          "fan_mode": {
+            "state": {
+              "off": "Off",
path: homeassistant/components/teslemetry/strings.json
user: login: joostlek
body: ```suggestion
              "off": "[%key:common::state::off%]",
```
created_at: 2025-03-25T16:10:41Z
author_association: MEMBER
diff_hunk: @@ -226,6 +226,12 @@
               "dog": "Dog mode",
               "camp": "Camp mode"
             }
+          },
+          "fan_mode": {
+            "state": {
+              "off": "Off",
+              "bioweapon": "Bioweapon Defense"
path: homeassistant/components/teslemetry/strings.json
user: login: joostlek
body: ```suggestion
              "bioweapon": "Bioweapon defense"
```
created_at: 2025-03-25T16:10:46Z
author_association: MEMBER
diff_hunk: @@ -163,74 +148,320 @@ async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None:
 
     async def async_set_preset_mode(self, preset_mode: str) -> None:
         """Set the climate preset mode."""
-        await self.wake_up_if_asleep()
+        self.raise_for_scope(Scope.VEHICLE_CMDS)
+
         await handle_vehicle_command(
             self.api.set_climate_keeper_mode(
                 climate_keeper_mode=self._attr_preset_modes.index(preset_mode)
             )
         )
         self._attr_preset_mode = preset_mode
-        if preset_mode != self._attr_preset_modes[0]:
-            # Changing preset mode will also turn on climate
+        if preset_mode == self._attr_preset_modes[0]:
+            self._attr_hvac_mode = HVACMode.OFF
+        else:
+            self._attr_hvac_mode = HVACMode.HEAT_COOL
+        self.async_write_ha_state()
+
+    async def async_set_fan_mode(self, fan_mode: str) -> None:
+        """Set the Bioweapon defense mode."""
+        self.raise_for_scope(Scope.VEHICLE_CMDS)
+
+        await handle_vehicle_command(
+            self.api.set_bioweapon_mode(
+                on=(fan_mode != "off"),
+                manual_override=True,
+            )
+        )
+        self._attr_fan_mode = fan_mode
+        if fan_mode == self._attr_fan_modes[1]:
             self._attr_hvac_mode = HVACMode.HEAT_COOL
         self.async_write_ha_state()
 
 
+class TeslemetryPollingClimateEntity(TeslemetryClimateEntity, TeslemetryVehicleEntity):
+    """Polling vehicle climate entity."""
+
+    _attr_supported_features = (
+        ClimateEntityFeature.TURN_ON
+        | ClimateEntityFeature.TURN_OFF
+        | ClimateEntityFeature.TARGET_TEMPERATURE
+        | ClimateEntityFeature.PRESET_MODE
+        | ClimateEntityFeature.FAN_MODE
+    )
+
+    def __init__(
+        self,
+        data: TeslemetryVehicleData,
+        side: TeslemetryClimateSide,
+        scopes: list[Scope],
+    ) -> None:
+        """Initialize the climate."""
+        self.scoped = Scope.VEHICLE_CMDS in scopes
+        if not self.scoped:
+            self._attr_supported_features = ClimateEntityFeature(0)
+
+        super().__init__(
+            data,
+            side,
+        )
path: homeassistant/components/teslemetry/climate.py
user: login: joostlek
body: ```suggestion
        super().__init__(data, side)
```
created_at: 2025-03-25T16:11:18Z
author_association: MEMBER

PR comments:
user: login: home-assistant[bot]
created_at: 2025-03-14T14:18:30Z
body: <!-- ReviewDrafterComment -->
Please take a look at the requested changes, and use the **Ready for review** button when you are done, thanks :+1:

[_Learn more about our pull request process._](https://developers.home-assistant.io/docs/review-process#prs-are-being-drafted-when-changes-are-needed)

author_association: CONTRIBUTOR
user: login: Bre77
created_at: 2025-03-18T23:14:36Z
body: Hey @joostlek just a bump on this, all feedback should be addressed now. This PR is a blocker for my next. Thanks <3
author_association: CONTRIBUTOR