Home Assistant Unofficial Reference 2024.12.1
sensor.py
Go to the documentation of this file.
1 """Support for powerwall sensors."""
2 
3 from __future__ import annotations
4 
5 from collections.abc import Callable
6 from dataclasses import dataclass
7 from operator import attrgetter, methodcaller
8 from typing import TYPE_CHECKING, Generic, TypeVar
9 
10 from tesla_powerwall import GridState, MeterResponse, MeterType
11 
13  SensorDeviceClass,
14  SensorEntity,
15  SensorEntityDescription,
16  SensorStateClass,
17 )
18 from homeassistant.const import (
19  PERCENTAGE,
20  EntityCategory,
21  UnitOfElectricCurrent,
22  UnitOfElectricPotential,
23  UnitOfEnergy,
24  UnitOfFrequency,
25  UnitOfPower,
26 )
27 from homeassistant.core import HomeAssistant
28 from homeassistant.helpers.entity import Entity
29 from homeassistant.helpers.entity_platform import AddEntitiesCallback
30 
31 from .const import POWERWALL_COORDINATOR
32 from .entity import BatteryEntity, PowerWallEntity
33 from .models import BatteryResponse, PowerwallConfigEntry, PowerwallRuntimeData
34 
35 _METER_DIRECTION_EXPORT = "export"
36 _METER_DIRECTION_IMPORT = "import"
37 
38 _ValueParamT = TypeVar("_ValueParamT")
39 _ValueT = TypeVar("_ValueT", bound=float | int | str | None)
40 
41 
42 @dataclass(frozen=True, kw_only=True)
44  SensorEntityDescription,
45  Generic[_ValueParamT, _ValueT],
46 ):
47  """Describes Powerwall entity."""
48 
49  value_fn: Callable[[_ValueParamT], _ValueT]
50 
51 
52 def _get_meter_power(meter: MeterResponse) -> float:
53  """Get the current value in kW."""
54  return meter.get_power(precision=3)
55 
56 
57 def _get_meter_frequency(meter: MeterResponse) -> float:
58  """Get the current value in Hz."""
59  return round(meter.frequency, 1)
60 
61 
62 def _get_meter_average_voltage(meter: MeterResponse) -> float:
63  """Get the current value in V."""
64  return round(meter.instant_average_voltage, 1)
65 
66 
67 POWERWALL_INSTANT_SENSORS = (
68  PowerwallSensorEntityDescription[MeterResponse, float](
69  key="instant_power",
70  translation_key="instant_power",
71  state_class=SensorStateClass.MEASUREMENT,
72  device_class=SensorDeviceClass.POWER,
73  native_unit_of_measurement=UnitOfPower.KILO_WATT,
74  value_fn=_get_meter_power,
75  ),
76  PowerwallSensorEntityDescription[MeterResponse, float](
77  key="instant_frequency",
78  translation_key="instant_frequency",
79  state_class=SensorStateClass.MEASUREMENT,
80  device_class=SensorDeviceClass.FREQUENCY,
81  native_unit_of_measurement=UnitOfFrequency.HERTZ,
82  entity_registry_enabled_default=False,
83  value_fn=_get_meter_frequency,
84  ),
85  PowerwallSensorEntityDescription[MeterResponse, float](
86  key="instant_current",
87  translation_key="instant_current",
88  state_class=SensorStateClass.MEASUREMENT,
89  device_class=SensorDeviceClass.CURRENT,
90  native_unit_of_measurement=UnitOfElectricCurrent.AMPERE,
91  entity_registry_enabled_default=False,
92  value_fn=methodcaller("get_instant_total_current"),
93  ),
94  PowerwallSensorEntityDescription[MeterResponse, float](
95  key="instant_voltage",
96  translation_key="instant_voltage",
97  state_class=SensorStateClass.MEASUREMENT,
98  device_class=SensorDeviceClass.VOLTAGE,
99  native_unit_of_measurement=UnitOfElectricPotential.VOLT,
100  entity_registry_enabled_default=False,
101  value_fn=_get_meter_average_voltage,
102  ),
103 )
104 
105 
106 def _get_instant_voltage(battery: BatteryResponse) -> float | None:
107  """Get the current value in V."""
108  return None if battery.v_out is None else round(battery.v_out, 1)
109 
110 
111 def _get_instant_frequency(battery: BatteryResponse) -> float | None:
112  """Get the current value in Hz."""
113  return None if battery.f_out is None else round(battery.f_out, 1)
114 
115 
116 def _get_instant_current(battery: BatteryResponse) -> float | None:
117  """Get the current value in A."""
118  return None if battery.i_out is None else round(battery.i_out, 1)
119 
120 
121 BATTERY_INSTANT_SENSORS: list[PowerwallSensorEntityDescription] = [
122  PowerwallSensorEntityDescription[BatteryResponse, int](
123  key="battery_capacity",
124  translation_key="battery_capacity",
125  entity_category=EntityCategory.DIAGNOSTIC,
126  state_class=SensorStateClass.MEASUREMENT,
127  device_class=SensorDeviceClass.ENERGY_STORAGE,
128  native_unit_of_measurement=UnitOfEnergy.WATT_HOUR,
129  suggested_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
130  suggested_display_precision=1,
131  value_fn=attrgetter("capacity"),
132  ),
133  PowerwallSensorEntityDescription[BatteryResponse, float | None](
134  key="battery_instant_voltage",
135  translation_key="battery_instant_voltage",
136  entity_category=EntityCategory.DIAGNOSTIC,
137  state_class=SensorStateClass.MEASUREMENT,
138  device_class=SensorDeviceClass.VOLTAGE,
139  native_unit_of_measurement=UnitOfElectricPotential.VOLT,
140  value_fn=_get_instant_voltage,
141  ),
142  PowerwallSensorEntityDescription[BatteryResponse, float | None](
143  key="instant_frequency",
144  translation_key="instant_frequency",
145  entity_category=EntityCategory.DIAGNOSTIC,
146  state_class=SensorStateClass.MEASUREMENT,
147  device_class=SensorDeviceClass.FREQUENCY,
148  native_unit_of_measurement=UnitOfFrequency.HERTZ,
149  entity_registry_enabled_default=False,
150  value_fn=_get_instant_frequency,
151  ),
152  PowerwallSensorEntityDescription[BatteryResponse, float | None](
153  key="instant_current",
154  translation_key="instant_current",
155  entity_category=EntityCategory.DIAGNOSTIC,
156  state_class=SensorStateClass.MEASUREMENT,
157  device_class=SensorDeviceClass.CURRENT,
158  native_unit_of_measurement=UnitOfElectricCurrent.AMPERE,
159  entity_registry_enabled_default=False,
160  value_fn=_get_instant_current,
161  ),
162  PowerwallSensorEntityDescription[BatteryResponse, int | None](
163  key="instant_power",
164  translation_key="instant_power",
165  entity_category=EntityCategory.DIAGNOSTIC,
166  state_class=SensorStateClass.MEASUREMENT,
167  device_class=SensorDeviceClass.POWER,
168  native_unit_of_measurement=UnitOfPower.WATT,
169  value_fn=attrgetter("p_out"),
170  ),
171  PowerwallSensorEntityDescription[BatteryResponse, float | None](
172  key="battery_export",
173  translation_key="battery_export",
174  entity_category=EntityCategory.DIAGNOSTIC,
175  state_class=SensorStateClass.TOTAL_INCREASING,
176  device_class=SensorDeviceClass.ENERGY,
177  native_unit_of_measurement=UnitOfEnergy.WATT_HOUR,
178  suggested_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
179  suggested_display_precision=0,
180  value_fn=attrgetter("energy_discharged"),
181  ),
182  PowerwallSensorEntityDescription[BatteryResponse, float | None](
183  key="battery_import",
184  translation_key="battery_import",
185  entity_category=EntityCategory.DIAGNOSTIC,
186  state_class=SensorStateClass.TOTAL_INCREASING,
187  device_class=SensorDeviceClass.ENERGY,
188  native_unit_of_measurement=UnitOfEnergy.WATT_HOUR,
189  suggested_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
190  suggested_display_precision=0,
191  value_fn=attrgetter("energy_charged"),
192  ),
193  PowerwallSensorEntityDescription[BatteryResponse, int](
194  key="battery_remaining",
195  translation_key="battery_remaining",
196  entity_category=EntityCategory.DIAGNOSTIC,
197  state_class=SensorStateClass.MEASUREMENT,
198  device_class=SensorDeviceClass.ENERGY_STORAGE,
199  native_unit_of_measurement=UnitOfEnergy.WATT_HOUR,
200  suggested_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
201  suggested_display_precision=1,
202  value_fn=attrgetter("energy_remaining"),
203  ),
204  PowerwallSensorEntityDescription[BatteryResponse, str](
205  key="grid_state",
206  translation_key="grid_state",
207  entity_category=EntityCategory.DIAGNOSTIC,
208  device_class=SensorDeviceClass.ENUM,
209  options=[state.value.lower() for state in GridState],
210  value_fn=lambda battery_data: battery_data.grid_state.value.lower(),
211  ),
212 ]
213 
214 
216  hass: HomeAssistant,
217  entry: PowerwallConfigEntry,
218  async_add_entities: AddEntitiesCallback,
219 ) -> None:
220  """Set up the powerwall sensors."""
221  powerwall_data = entry.runtime_data
222  coordinator = powerwall_data[POWERWALL_COORDINATOR]
223  assert coordinator is not None
224  data = coordinator.data
225  entities: list[Entity] = [
226  PowerWallChargeSensor(powerwall_data),
227  ]
228 
229  if data.backup_reserve is not None:
230  entities.append(PowerWallBackupReserveSensor(powerwall_data))
231 
232  for meter in data.meters.meters:
233  entities.append(PowerWallExportSensor(powerwall_data, meter))
234  entities.append(PowerWallImportSensor(powerwall_data, meter))
235  entities.extend(
236  PowerWallEnergySensor(powerwall_data, meter, description)
237  for description in POWERWALL_INSTANT_SENSORS
238  )
239 
240  for battery in data.batteries.values():
241  entities.extend(
242  PowerWallBatterySensor(powerwall_data, battery, description)
243  for description in BATTERY_INSTANT_SENSORS
244  )
245 
246  async_add_entities(entities)
247 
248 
250  """Representation of an Powerwall charge sensor."""
251 
252  _attr_translation_key = "charge"
253  _attr_state_class = SensorStateClass.MEASUREMENT
254  _attr_native_unit_of_measurement = PERCENTAGE
255  _attr_device_class = SensorDeviceClass.BATTERY
256 
257  @property
258  def unique_id(self) -> str:
259  """Device Uniqueid."""
260  return f"{self.base_unique_id}_charge"
261 
262  @property
263  def native_value(self) -> int:
264  """Get the current value in percentage."""
265  return round(self.datadatadata.charge)
266 
267 
269  """Representation of an Powerwall Energy sensor."""
270 
271  entity_description: PowerwallSensorEntityDescription[MeterResponse, float]
272 
273  def __init__(
274  self,
275  powerwall_data: PowerwallRuntimeData,
276  meter: MeterType,
277  description: PowerwallSensorEntityDescription[MeterResponse, float],
278  ) -> None:
279  """Initialize the sensor."""
280  self.entity_descriptionentity_description = description
281  super().__init__(powerwall_data)
282  self._meter_meter = meter
283  self._attr_translation_key_attr_translation_key = f"{meter.value}_{description.translation_key}"
284  self._attr_unique_id_attr_unique_id = f"{self.base_unique_id}_{meter.value}_{description.key}"
285 
286  @property
287  def native_value(self) -> float | None:
288  """Get the current value."""
289  meter = self.datadatadata.meters.get_meter(self._meter_meter)
290  if meter is not None:
291  return self.entity_descriptionentity_description.value_fn(meter)
292 
293  return None
294 
295 
297  """Representation of the Powerwall backup reserve setting."""
298 
299  _attr_translation_key = "backup_reserve"
300  _attr_state_class = SensorStateClass.MEASUREMENT
301  _attr_native_unit_of_measurement = PERCENTAGE
302  _attr_device_class = SensorDeviceClass.BATTERY
303 
304  @property
305  def unique_id(self) -> str:
306  """Device Uniqueid."""
307  return f"{self.base_unique_id}_backup_reserve"
308 
309  @property
310  def native_value(self) -> int | None:
311  """Get the current value in percentage."""
312  if self.datadatadata.backup_reserve is None:
313  return None
314  return round(self.datadatadata.backup_reserve)
315 
316 
318  """Representation of an Powerwall Direction Energy sensor."""
319 
320  _attr_state_class = SensorStateClass.TOTAL
321  _attr_native_unit_of_measurement = UnitOfEnergy.KILO_WATT_HOUR
322  _attr_device_class = SensorDeviceClass.ENERGY
323 
324  def __init__(
325  self,
326  powerwall_data: PowerwallRuntimeData,
327  meter: MeterType,
328  meter_direction: str,
329  ) -> None:
330  """Initialize the sensor."""
331  super().__init__(powerwall_data)
332  self._meter_meter = meter
333  self._attr_translation_key_attr_translation_key = f"{meter.value}_{meter_direction}"
334  self._attr_unique_id_attr_unique_id = f"{self.base_unique_id}_{meter.value}_{meter_direction}"
335 
336  @property
337  def available(self) -> bool:
338  """Check if the reading is actually available.
339 
340  The device reports 0 when something goes wrong which
341  we do not want to include in statistics and its a
342  transient data error.
343  """
344  return super().available and self.metermeter is not None
345 
346  @property
347  def meter(self) -> MeterResponse | None:
348  """Get the meter for the sensor."""
349  return self.datadatadata.meters.get_meter(self._meter_meter)
350 
351 
353  """Representation of an Powerwall Export sensor."""
354 
355  def __init__(
356  self,
357  powerwall_data: PowerwallRuntimeData,
358  meter: MeterType,
359  ) -> None:
360  """Initialize the sensor."""
361  super().__init__(powerwall_data, meter, _METER_DIRECTION_EXPORT)
362 
363  @property
364  def native_value(self) -> float | None:
365  """Get the current value in kWh."""
366  meter = self.metermeter
367  if TYPE_CHECKING:
368  assert meter is not None
369  return meter.get_energy_exported()
370 
371 
373  """Representation of an Powerwall Import sensor."""
374 
375  def __init__(
376  self,
377  powerwall_data: PowerwallRuntimeData,
378  meter: MeterType,
379  ) -> None:
380  """Initialize the sensor."""
381  super().__init__(powerwall_data, meter, _METER_DIRECTION_IMPORT)
382 
383  @property
384  def native_value(self) -> float | None:
385  """Get the current value in kWh."""
386  meter = self.metermeter
387  if TYPE_CHECKING:
388  assert meter is not None
389  return meter.get_energy_imported()
390 
391 
393  """Representation of an Powerwall Battery sensor."""
394 
395  entity_description: PowerwallSensorEntityDescription[BatteryResponse, _ValueT]
396 
397  def __init__(
398  self,
399  powerwall_data: PowerwallRuntimeData,
400  battery: BatteryResponse,
401  description: PowerwallSensorEntityDescription[BatteryResponse, _ValueT],
402  ) -> None:
403  """Initialize the sensor."""
404  self.entity_descriptionentity_description = description
405  super().__init__(powerwall_data, battery)
406  self._attr_translation_key_attr_translation_key = description.translation_key
407  self._attr_unique_id_attr_unique_id = f"{self.base_unique_id}_{description.key}"
408 
409  @property
410  def native_value(self) -> float | int | str | None:
411  """Get the current value."""
412  return self.entity_descriptionentity_description.value_fn(self.battery_databattery_data)
None __init__(self, PowerwallRuntimeData powerwall_data, BatteryResponse battery, PowerwallSensorEntityDescription[BatteryResponse, _ValueT] description)
Definition: sensor.py:402
None __init__(self, PowerwallRuntimeData powerwall_data, MeterType meter, str meter_direction)
Definition: sensor.py:329
None __init__(self, PowerwallRuntimeData powerwall_data, MeterType meter, PowerwallSensorEntityDescription[MeterResponse, float] description)
Definition: sensor.py:278
None __init__(self, PowerwallRuntimeData powerwall_data, MeterType meter)
Definition: sensor.py:359
None __init__(self, PowerwallRuntimeData powerwall_data, MeterType meter)
Definition: sensor.py:379
float _get_meter_average_voltage(MeterResponse meter)
Definition: sensor.py:62
float|None _get_instant_frequency(BatteryResponse battery)
Definition: sensor.py:111
float _get_meter_power(MeterResponse meter)
Definition: sensor.py:52
float|None _get_instant_voltage(BatteryResponse battery)
Definition: sensor.py:106
float _get_meter_frequency(MeterResponse meter)
Definition: sensor.py:57
float|None _get_instant_current(BatteryResponse battery)
Definition: sensor.py:116
None async_setup_entry(HomeAssistant hass, PowerwallConfigEntry entry, AddEntitiesCallback async_add_entities)
Definition: sensor.py:219