-
-
Notifications
You must be signed in to change notification settings - Fork 36.7k
Add individual battery banks as devices #108339
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
6b4a34e
32ac6a5
19594ca
b6dfa2f
9e24a06
aa2823e
0f95c3b
debdafe
e59928d
e568cbe
02ec6a1
373bdbb
74f9dab
f864ff1
6070999
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -5,7 +5,7 @@ | |
| from dataclasses import dataclass | ||
| from typing import TYPE_CHECKING, Generic, TypeVar | ||
|
|
||
| from tesla_powerwall import MeterResponse, MeterType | ||
| from tesla_powerwall import GridState, MeterResponse, MeterType | ||
|
|
||
| from homeassistant.components.sensor import ( | ||
| SensorDeviceClass, | ||
|
|
@@ -16,6 +16,7 @@ | |
| from homeassistant.config_entries import ConfigEntry | ||
| from homeassistant.const import ( | ||
| PERCENTAGE, | ||
| EntityCategory, | ||
| UnitOfElectricCurrent, | ||
| UnitOfElectricPotential, | ||
| UnitOfEnergy, | ||
|
|
@@ -27,14 +28,14 @@ | |
| from homeassistant.helpers.entity_platform import AddEntitiesCallback | ||
|
|
||
| from .const import DOMAIN, POWERWALL_COORDINATOR | ||
| from .entity import PowerWallEntity | ||
| from .models import PowerwallRuntimeData | ||
| from .entity import BatteryEntity, PowerWallEntity | ||
| from .models import BatteryResponse, PowerwallRuntimeData | ||
|
|
||
| _METER_DIRECTION_EXPORT = "export" | ||
| _METER_DIRECTION_IMPORT = "import" | ||
|
|
||
| _ValueParamT = TypeVar("_ValueParamT") | ||
| _ValueT = TypeVar("_ValueT", bound=float) | ||
| _ValueT = TypeVar("_ValueT", bound=float | int | str) | ||
|
|
||
|
|
||
| @dataclass(frozen=True) | ||
|
|
@@ -112,6 +113,116 @@ def _get_meter_average_voltage(meter: MeterResponse) -> float: | |
| ) | ||
|
|
||
|
|
||
| def _get_battery_charge(battery_data: BatteryResponse) -> float: | ||
| """Get the current value in %.""" | ||
| ratio = float(battery_data.energy_remaining) / float(battery_data.capacity) | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We don't allow calculating state of entities in integrations that integrate devices or services. The API data should be shown as is, while following our entity design guidelines. Only exception is around time state. Please remove this sensor.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. For sure! What is the alternative to provide the same information to the user?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. State of charge for batteries is naturally a computation based on models for the battery chemistry, so wouldn't it make sense that it is a computed value? Either the computation is done explicitly here or buried lower in the stack in support libraries or in the firmware of the powerwall that this is representing. I'm happy to update this to meet any guidelines, but please go easy on me since these are my first contributions and I'd like to get to learn HA. :)
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The other option is to have the underlying library return the value so we don't do the calculation in HA |
||
| return round(100 * ratio, 1) | ||
|
|
||
|
|
||
| BATTERY_INSTANT_SENSORS: list[PowerwallSensorEntityDescription] = [ | ||
| PowerwallSensorEntityDescription[BatteryResponse, int]( | ||
| key="battery_capacity", | ||
| translation_key="battery_capacity", | ||
| entity_category=EntityCategory.DIAGNOSTIC, | ||
| state_class=SensorStateClass.MEASUREMENT, | ||
| device_class=SensorDeviceClass.ENERGY_STORAGE, | ||
| native_unit_of_measurement=UnitOfEnergy.WATT_HOUR, | ||
| suggested_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, | ||
| suggested_display_precision=1, | ||
| value_fn=lambda battery_data: battery_data.capacity, | ||
| ), | ||
| PowerwallSensorEntityDescription[BatteryResponse, float]( | ||
| key="battery_instant_voltage", | ||
| translation_key="battery_instant_voltage", | ||
| entity_category=EntityCategory.DIAGNOSTIC, | ||
| state_class=SensorStateClass.MEASUREMENT, | ||
| device_class=SensorDeviceClass.VOLTAGE, | ||
| native_unit_of_measurement=UnitOfElectricPotential.VOLT, | ||
| value_fn=lambda battery_data: round(battery_data.v_out, 1), | ||
| ), | ||
| PowerwallSensorEntityDescription[BatteryResponse, float]( | ||
| key="instant_frequency", | ||
| translation_key="instant_frequency", | ||
| entity_category=EntityCategory.DIAGNOSTIC, | ||
| state_class=SensorStateClass.MEASUREMENT, | ||
| device_class=SensorDeviceClass.FREQUENCY, | ||
| native_unit_of_measurement=UnitOfFrequency.HERTZ, | ||
| entity_registry_enabled_default=False, | ||
| value_fn=lambda battery_data: round(battery_data.f_out, 1), | ||
| ), | ||
| PowerwallSensorEntityDescription[BatteryResponse, float]( | ||
| key="instant_current", | ||
| translation_key="instant_current", | ||
| entity_category=EntityCategory.DIAGNOSTIC, | ||
| state_class=SensorStateClass.MEASUREMENT, | ||
| device_class=SensorDeviceClass.CURRENT, | ||
| native_unit_of_measurement=UnitOfElectricCurrent.AMPERE, | ||
| entity_registry_enabled_default=False, | ||
| value_fn=lambda battery_data: round(battery_data.i_out, 1), | ||
| ), | ||
| PowerwallSensorEntityDescription[BatteryResponse, int]( | ||
| key="instant_power", | ||
| translation_key="instant_power", | ||
| entity_category=EntityCategory.DIAGNOSTIC, | ||
| state_class=SensorStateClass.MEASUREMENT, | ||
| device_class=SensorDeviceClass.POWER, | ||
| native_unit_of_measurement=UnitOfPower.WATT, | ||
| value_fn=lambda battery_data: battery_data.p_out, | ||
| ), | ||
| PowerwallSensorEntityDescription[BatteryResponse, float]( | ||
| key="battery_export", | ||
| translation_key="battery_export", | ||
| entity_category=EntityCategory.DIAGNOSTIC, | ||
| state_class=SensorStateClass.TOTAL_INCREASING, | ||
| device_class=SensorDeviceClass.ENERGY, | ||
| native_unit_of_measurement=UnitOfEnergy.WATT_HOUR, | ||
| suggested_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, | ||
| suggested_display_precision=0, | ||
| value_fn=lambda battery_data: battery_data.energy_discharged, | ||
| ), | ||
| PowerwallSensorEntityDescription[BatteryResponse, float]( | ||
| key="battery_import", | ||
| translation_key="battery_import", | ||
| entity_category=EntityCategory.DIAGNOSTIC, | ||
| state_class=SensorStateClass.TOTAL_INCREASING, | ||
| device_class=SensorDeviceClass.ENERGY, | ||
| native_unit_of_measurement=UnitOfEnergy.WATT_HOUR, | ||
| suggested_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, | ||
| suggested_display_precision=0, | ||
| value_fn=lambda battery_data: battery_data.energy_charged, | ||
| ), | ||
| PowerwallSensorEntityDescription[BatteryResponse, int]( | ||
| key="battery_remaining", | ||
| translation_key="battery_remaining", | ||
| entity_category=EntityCategory.DIAGNOSTIC, | ||
| state_class=SensorStateClass.MEASUREMENT, | ||
| device_class=SensorDeviceClass.ENERGY_STORAGE, | ||
| native_unit_of_measurement=UnitOfEnergy.WATT_HOUR, | ||
| suggested_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, | ||
| suggested_display_precision=1, | ||
| value_fn=lambda battery_data: battery_data.energy_remaining, | ||
| ), | ||
| PowerwallSensorEntityDescription[BatteryResponse, float]( | ||
| key="charge", | ||
| translation_key="charge", | ||
| entity_category=EntityCategory.DIAGNOSTIC, | ||
| state_class=SensorStateClass.MEASUREMENT, | ||
| device_class=SensorDeviceClass.BATTERY, | ||
| native_unit_of_measurement=PERCENTAGE, | ||
| suggested_display_precision=0, | ||
| value_fn=_get_battery_charge, | ||
| ), | ||
| PowerwallSensorEntityDescription[BatteryResponse, str]( | ||
| key="grid_state", | ||
| translation_key="grid_state", | ||
| entity_category=EntityCategory.DIAGNOSTIC, | ||
| device_class=SensorDeviceClass.ENUM, | ||
| options=[state.value.lower() for state in GridState], | ||
| value_fn=lambda battery_data: battery_data.grid_state.value.lower(), | ||
| ), | ||
| ] | ||
|
|
||
|
|
||
| async def async_setup_entry( | ||
| hass: HomeAssistant, | ||
| config_entry: ConfigEntry, | ||
|
|
@@ -137,6 +248,12 @@ async def async_setup_entry( | |
| for description in POWERWALL_INSTANT_SENSORS | ||
| ) | ||
|
|
||
| for battery in data.batteries.values(): | ||
| entities.extend( | ||
| PowerWallBatterySensor(powerwall_data, battery, description) | ||
| for description in BATTERY_INSTANT_SENSORS | ||
| ) | ||
|
|
||
| async_add_entities(entities) | ||
|
|
||
|
|
||
|
|
@@ -281,3 +398,26 @@ def native_value(self) -> float | None: | |
| if TYPE_CHECKING: | ||
| assert meter is not None | ||
| return meter.get_energy_imported() | ||
|
|
||
|
|
||
| class PowerWallBatterySensor(BatteryEntity, SensorEntity, Generic[_ValueT]): | ||
| """Representation of an Powerwall Battery sensor.""" | ||
|
|
||
| entity_description: PowerwallSensorEntityDescription[BatteryResponse, _ValueT] | ||
|
|
||
| def __init__( | ||
| self, | ||
| powerwall_data: PowerwallRuntimeData, | ||
| battery: BatteryResponse, | ||
| description: PowerwallSensorEntityDescription[BatteryResponse, _ValueT], | ||
| ) -> None: | ||
| """Initialize the sensor.""" | ||
| self.entity_description = description | ||
| super().__init__(powerwall_data, battery) | ||
| self._attr_translation_key = description.translation_key | ||
| self._attr_unique_id = f"{self.base_unique_id}_{description.key}" | ||
|
|
||
| @property | ||
| def native_value(self) -> float | int | str: | ||
| """Get the current value.""" | ||
| return self.entity_description.value_fn(self.battery_data) | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,32 @@ | ||
| [ | ||
| { | ||
| "PackagePartNumber": "3012170-05-C", | ||
| "PackageSerialNumber": "TG0123456789AB", | ||
| "energy_charged": 2693355, | ||
| "energy_discharged": 2358235, | ||
| "nominal_energy_remaining": 14715, | ||
| "nominal_full_pack_energy": 14715, | ||
| "wobble_detected": false, | ||
| "p_out": -100, | ||
| "q_out": -1080, | ||
| "v_out": 245.70000000000002, | ||
| "f_out": 50.037, | ||
| "i_out": 0.30000000000000004, | ||
| "pinv_grid_state": "Grid_Compliant" | ||
| }, | ||
| { | ||
| "PackagePartNumber": "3012170-05-C", | ||
| "PackageSerialNumber": "TG9876543210BA", | ||
| "energy_charged": 610483, | ||
| "energy_discharged": 509907, | ||
| "nominal_energy_remaining": 15137, | ||
| "nominal_full_pack_energy": 15137, | ||
| "wobble_detected": false, | ||
| "p_out": -100, | ||
| "q_out": -1090, | ||
| "v_out": 245.60000000000002, | ||
| "f_out": 50.037, | ||
| "i_out": 0.1, | ||
| "pinv_grid_state": "Grid_Compliant" | ||
| } | ||
| ] |
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It would be so much better if PowerwallRuntimeData was a dataclass as we wouldn't have to use as many type hints. This integration predates us being able to use dataclasses so it's never been converted.