99 EnergyExportMode ,
1010 EnergyOperationMode ,
1111)
12+ from tesla_fleet_api .teslemetry .vehicles import TeslemetryVehicleFleet
1213from teslemetry_stream import Signal
1314
1415from dataclasses import dataclass
1819from homeassistant .core import HomeAssistant
1920from homeassistant .helpers .entity_platform import AddEntitiesCallback
2021from homeassistant .helpers .restore_state import RestoreEntity
22+ from teslemetry_stream .vehicle import TeslemetryStreamVehicle
23+
24+ from custom_components .teslemetry .helpers import handle_vehicle_command
2125
2226from .const import TeslemetryHeaterOptions
2327from .entity import (
28+ TeslemetryRootEntity ,
2429 TeslemetryVehicleEntity ,
2530 TeslemetryEnergyInfoEntity ,
26- TeslemetryVehicleStreamSingleEntity ,
31+ TeslemetryVehicleStreamEntity ,
2732)
2833from .models import TeslemetryEnergyData , TeslemetryVehicleData
2934
@@ -34,23 +39,28 @@ class SeatHeaterDescription(SelectEntityDescription):
3439
3540 position : Seat
3641 supported_fn : Callable = lambda _ : True
37- streaming_key : Signal | None = None
38-
42+ streaming_listener : (
43+ Callable [
44+ [TeslemetryStreamVehicle , Callable [[int | None ], None ]],
45+ Callable [[], None ],
46+ ]
47+ | None
48+ ) = None
3949
4050SEAT_HEATER_DESCRIPTIONS : tuple [SeatHeaterDescription , ...] = (
4151 SeatHeaterDescription (
4252 key = "climate_state_seat_heater_left" ,
43- streaming_key = Signal . SEAT_HEATER_LEFT ,
53+ streaming_listener = lambda x , y : x . listen_SeatHeaterLeft ( y ) ,
4454 position = Seat .FRONT_LEFT ,
4555 ),
4656 SeatHeaterDescription (
4757 key = "climate_state_seat_heater_right" ,
48- streaming_key = Signal . SEAT_HEATER_RIGHT ,
58+ streaming_listener = lambda x , y : x . listen_SeatHeaterRight ( y ) ,
4959 position = Seat .FRONT_RIGHT ,
5060 ),
5161 SeatHeaterDescription (
5262 key = "climate_state_seat_heater_rear_left" ,
53- streaming_key = Signal . SEAT_HEATER_REAR_LEFT ,
63+ streaming_listener = lambda x , y : x . listen_SeatHeaterRearLeft ( y ) ,
5464 position = Seat .REAR_LEFT ,
5565 supported_fn = lambda data : data .get (
5666 "vehicle_config_rear_seat_heaters"
@@ -59,7 +69,7 @@ class SeatHeaterDescription(SelectEntityDescription):
5969 ),
6070 SeatHeaterDescription (
6171 key = "climate_state_seat_heater_rear_center" ,
62- streaming_key = Signal . SEAT_HEATER_REAR_CENTER ,
72+ streaming_listener = lambda x , y : x . listen_SeatHeaterRearCenter ( y ) ,
6373 position = Seat .REAR_CENTER ,
6474 supported_fn = lambda data : data .get (
6575 "vehicle_config_rear_seat_heaters"
@@ -68,7 +78,7 @@ class SeatHeaterDescription(SelectEntityDescription):
6878 ),
6979 SeatHeaterDescription (
7080 key = "climate_state_seat_heater_rear_right" ,
71- streaming_key = Signal . SEAT_HEATER_REAR_RIGHT ,
81+ streaming_listener = lambda x , y : x . listen_SeatHeaterRearRight ( y ) ,
7282 position = Seat .REAR_RIGHT ,
7383 supported_fn = lambda data : data .get (
7484 "vehicle_config_rear_seat_heaters"
@@ -106,7 +116,7 @@ async def async_setup_entry(
106116 chain (
107117 (
108118 TeslemetryPollingSeatHeaterSelectEntity (vehicle , description , scoped )
109- if vehicle .api .pre2021 or vehicle .firmware < "2024.26" or description .streaming_key is None
119+ if vehicle .api .pre2021 or vehicle .firmware < "2024.26" or description .streaming_listener is None
110120 else TeslemetryStreamingSeatHeaterSelectEntity (vehicle , description , scoped )
111121 for description in SEAT_HEATER_DESCRIPTIONS
112122 for vehicle in entry .runtime_data .vehicles
@@ -133,27 +143,28 @@ async def async_setup_entry(
133143 )
134144
135145
136- class TeslemetrySeatHeaterSelectEntity (SelectEntity ):
146+ class TeslemetrySeatHeaterSelectEntity (TeslemetryRootEntity , SelectEntity ):
137147 """Select entity for vehicle seat heater."""
138148
149+ api : TeslemetryVehicleFleet
139150 entity_description : SeatHeaterDescription
140-
141151 _attr_options = [
142152 TeslemetryHeaterOptions .OFF ,
143153 TeslemetryHeaterOptions .LOW ,
144154 TeslemetryHeaterOptions .MEDIUM ,
145155 TeslemetryHeaterOptions .HIGH ,
146156 ]
157+ _climate_state : bool = False
147158
148159 async def async_select_option (self , option : str ) -> None :
149160 """Change the selected option."""
150161 self .raise_for_scope (Scope .VEHICLE_CMDS )
151162
152163 level = self ._attr_options .index (option )
153164 # AC must be on to turn on seat heater
154- if not self .get ( "climate_state_is_climate_on" ) :
155- await self . handle_command (self .api .auto_conditioning_start ())
156- await self . handle_command (
165+ if not self ._climate_state :
166+ await handle_vehicle_command (self .api .auto_conditioning_start ())
167+ await handle_vehicle_command (
157168 self .api .remote_seat_heater_request (self .entity_description .position , level )
158169 )
159170 self ._attr_current_option = option
@@ -177,13 +188,14 @@ def __init__(
177188
178189 def _async_update_attrs (self ) -> None :
179190 """Handle updated data from the coordinator."""
191+ self ._climate_state = bool (self .get ("climate_state_is_climate_on" ))
180192 value = self ._value
181193 if isinstance (value , int ):
182194 self ._attr_current_option = self ._attr_options [value ]
183195 else :
184196 self ._attr_current_option = None
185197
186- class TeslemetryStreamingSeatHeaterSelectEntity (TeslemetryVehicleStreamSingleEntity , TeslemetrySeatHeaterSelectEntity , RestoreEntity ):
198+ class TeslemetryStreamingSeatHeaterSelectEntity (TeslemetryVehicleStreamEntity , TeslemetrySeatHeaterSelectEntity , RestoreEntity ):
187199 """Select entity for vehicle seat heater."""
188200
189201 def __init__ (
@@ -193,27 +205,45 @@ def __init__(
193205 scoped : bool ,
194206 ) -> None :
195207 """Initialize the vehicle seat select entity."""
196- assert description .streaming_key
197208 super ().__init__ (
198- data , description .key , description . streaming_key
209+ data , description .key
199210 )
200211 self .entity_description = description
201212 self .scoped = scoped
202213 self ._attr_current_option = None
203214
204215 async def async_added_to_hass (self ) -> None :
205- """Handle entity which will be added."""
206- await super ().async_added_to_hass ()
207- if (state := await self .async_get_last_state ()) is not None :
208- if (state .state in self ._attr_options ):
209- self ._attr_current_option = state .state
216+ """Handle entity which will be added."""
217+ await super ().async_added_to_hass ()
218+
219+ # Restore state
220+ if (state := await self .async_get_last_state ()) is not None :
221+ if state .state in self .entity_description .options :
222+ self ._attr_current_option = state .state
223+
224+ # Listen for streaming data
225+ assert self .entity_description .streaming_listener is not None
226+ self .async_on_remove (
227+ self .entity_description .streaming_listener (
228+ self .vehicle .stream_vehicle , self ._value_callback
229+ )
230+ )
210231
211- def _async_value_from_stream (self , value ) -> None :
232+ self .async_on_remove (
233+ self .vehicle .stream_vehicle .listen_HvacACEnabled (self ._climate_callback )
234+ )
235+
236+ def _value_callback (self , value : int | None ) -> None :
212237 """Update the value of the entity."""
213- if isinstance (value , int ):
214- self ._attr_current_option = self ._attr_options [value ]
215- else :
238+ if value is None :
216239 self ._attr_current_option = None
240+ else :
241+ self ._attr_current_option = self .entity_description .options [value ]
242+ self .async_write_ha_state ()
243+
244+ def _climate_callback (self , value : bool | None ) -> None :
245+ """Update the value of the entity."""
246+ self ._climate_state = bool (value )
217247
218248
219249class TeslemetryWheelHeaterSelectEntity (SelectEntity ):
0 commit comments