mirror of
https://github.com/home-assistant/core.git
synced 2025-07-23 05:07:41 +00:00
Make powerwall attribute sensors their own sensors (#68345)
This commit is contained in:
parent
619c1f014b
commit
4cc8998ea7
@ -1,28 +1,30 @@
|
|||||||
"""Support for powerwall sensors."""
|
"""Support for powerwall sensors."""
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from typing import Any
|
from collections.abc import Callable
|
||||||
|
from dataclasses import dataclass
|
||||||
|
|
||||||
from tesla_powerwall import Meter, MeterType
|
from tesla_powerwall import Meter, MeterType
|
||||||
|
|
||||||
from homeassistant.components.sensor import (
|
from homeassistant.components.sensor import (
|
||||||
SensorDeviceClass,
|
SensorDeviceClass,
|
||||||
SensorEntity,
|
SensorEntity,
|
||||||
|
SensorEntityDescription,
|
||||||
SensorStateClass,
|
SensorStateClass,
|
||||||
)
|
)
|
||||||
from homeassistant.config_entries import ConfigEntry
|
from homeassistant.config_entries import ConfigEntry
|
||||||
from homeassistant.const import ENERGY_KILO_WATT_HOUR, PERCENTAGE, POWER_KILO_WATT
|
from homeassistant.const import (
|
||||||
|
ELECTRIC_CURRENT_AMPERE,
|
||||||
|
ELECTRIC_POTENTIAL_VOLT,
|
||||||
|
ENERGY_KILO_WATT_HOUR,
|
||||||
|
FREQUENCY_HERTZ,
|
||||||
|
PERCENTAGE,
|
||||||
|
POWER_KILO_WATT,
|
||||||
|
)
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||||
|
|
||||||
from .const import (
|
from .const import DOMAIN, POWERWALL_COORDINATOR
|
||||||
ATTR_FREQUENCY,
|
|
||||||
ATTR_INSTANT_AVERAGE_VOLTAGE,
|
|
||||||
ATTR_INSTANT_TOTAL_CURRENT,
|
|
||||||
ATTR_IS_ACTIVE,
|
|
||||||
DOMAIN,
|
|
||||||
POWERWALL_COORDINATOR,
|
|
||||||
)
|
|
||||||
from .entity import PowerWallEntity
|
from .entity import PowerWallEntity
|
||||||
from .models import PowerwallData, PowerwallRuntimeData
|
from .models import PowerwallData, PowerwallRuntimeData
|
||||||
|
|
||||||
@ -30,6 +32,79 @@ _METER_DIRECTION_EXPORT = "export"
|
|||||||
_METER_DIRECTION_IMPORT = "import"
|
_METER_DIRECTION_IMPORT = "import"
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class PowerwallRequiredKeysMixin:
|
||||||
|
"""Mixin for required keys."""
|
||||||
|
|
||||||
|
value_fn: Callable[[Meter], float]
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class PowerwallSensorEntityDescription(
|
||||||
|
SensorEntityDescription, PowerwallRequiredKeysMixin
|
||||||
|
):
|
||||||
|
"""Describes Powerwall entity."""
|
||||||
|
|
||||||
|
|
||||||
|
def _get_meter_power(meter: Meter) -> float:
|
||||||
|
"""Get the current value in kW."""
|
||||||
|
return meter.get_power(precision=3)
|
||||||
|
|
||||||
|
|
||||||
|
def _get_meter_frequency(meter: Meter) -> float:
|
||||||
|
"""Get the current value in Hz."""
|
||||||
|
return round(meter.frequency, 1)
|
||||||
|
|
||||||
|
|
||||||
|
def _get_meter_total_current(meter: Meter) -> float:
|
||||||
|
"""Get the current value in A."""
|
||||||
|
return meter.get_instant_total_current()
|
||||||
|
|
||||||
|
|
||||||
|
def _get_meter_average_voltage(meter: Meter) -> float:
|
||||||
|
"""Get the current value in V."""
|
||||||
|
return round(meter.average_voltage, 1)
|
||||||
|
|
||||||
|
|
||||||
|
POWERWALL_INSTANT_SENSORS = (
|
||||||
|
PowerwallSensorEntityDescription(
|
||||||
|
key="instant_power",
|
||||||
|
name="Now",
|
||||||
|
state_class=SensorStateClass.MEASUREMENT,
|
||||||
|
device_class=SensorDeviceClass.POWER,
|
||||||
|
native_unit_of_measurement=POWER_KILO_WATT,
|
||||||
|
value_fn=_get_meter_power,
|
||||||
|
),
|
||||||
|
PowerwallSensorEntityDescription(
|
||||||
|
key="instant_frequency",
|
||||||
|
name="Frequency Now",
|
||||||
|
state_class=SensorStateClass.MEASUREMENT,
|
||||||
|
device_class=SensorDeviceClass.FREQUENCY,
|
||||||
|
native_unit_of_measurement=FREQUENCY_HERTZ,
|
||||||
|
entity_registry_enabled_default=False,
|
||||||
|
value_fn=_get_meter_frequency,
|
||||||
|
),
|
||||||
|
PowerwallSensorEntityDescription(
|
||||||
|
key="instant_current",
|
||||||
|
name="Average Current Now",
|
||||||
|
state_class=SensorStateClass.MEASUREMENT,
|
||||||
|
device_class=SensorDeviceClass.CURRENT,
|
||||||
|
native_unit_of_measurement=ELECTRIC_CURRENT_AMPERE,
|
||||||
|
entity_registry_enabled_default=False,
|
||||||
|
value_fn=_get_meter_total_current,
|
||||||
|
),
|
||||||
|
PowerwallSensorEntityDescription(
|
||||||
|
key="instant_voltage",
|
||||||
|
name="Average Voltage Now",
|
||||||
|
state_class=SensorStateClass.MEASUREMENT,
|
||||||
|
device_class=SensorDeviceClass.VOLTAGE,
|
||||||
|
native_unit_of_measurement=ELECTRIC_POTENTIAL_VOLT,
|
||||||
|
entity_registry_enabled_default=False,
|
||||||
|
value_fn=_get_meter_average_voltage,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
async def async_setup_entry(
|
async def async_setup_entry(
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
config_entry: ConfigEntry,
|
config_entry: ConfigEntry,
|
||||||
@ -40,24 +115,17 @@ async def async_setup_entry(
|
|||||||
coordinator = powerwall_data[POWERWALL_COORDINATOR]
|
coordinator = powerwall_data[POWERWALL_COORDINATOR]
|
||||||
assert coordinator is not None
|
assert coordinator is not None
|
||||||
data: PowerwallData = coordinator.data
|
data: PowerwallData = coordinator.data
|
||||||
entities: list[
|
entities: list[PowerWallEntity] = [
|
||||||
PowerWallEnergySensor
|
|
||||||
| PowerWallImportSensor
|
|
||||||
| PowerWallExportSensor
|
|
||||||
| PowerWallChargeSensor
|
|
||||||
| PowerWallBackupReserveSensor
|
|
||||||
] = [
|
|
||||||
PowerWallChargeSensor(powerwall_data),
|
PowerWallChargeSensor(powerwall_data),
|
||||||
PowerWallBackupReserveSensor(powerwall_data),
|
PowerWallBackupReserveSensor(powerwall_data),
|
||||||
]
|
]
|
||||||
|
|
||||||
for meter in data.meters.meters:
|
for meter in data.meters.meters:
|
||||||
|
entities.append(PowerWallExportSensor(powerwall_data, meter))
|
||||||
|
entities.append(PowerWallImportSensor(powerwall_data, meter))
|
||||||
entities.extend(
|
entities.extend(
|
||||||
[
|
PowerWallEnergySensor(powerwall_data, meter, description)
|
||||||
PowerWallEnergySensor(powerwall_data, meter),
|
for description in POWERWALL_INSTANT_SENSORS
|
||||||
PowerWallExportSensor(powerwall_data, meter),
|
|
||||||
PowerWallImportSensor(powerwall_data, meter),
|
|
||||||
]
|
|
||||||
)
|
)
|
||||||
|
|
||||||
async_add_entities(entities)
|
async_add_entities(entities)
|
||||||
@ -85,34 +153,27 @@ class PowerWallChargeSensor(PowerWallEntity, SensorEntity):
|
|||||||
class PowerWallEnergySensor(PowerWallEntity, SensorEntity):
|
class PowerWallEnergySensor(PowerWallEntity, SensorEntity):
|
||||||
"""Representation of an Powerwall Energy sensor."""
|
"""Representation of an Powerwall Energy sensor."""
|
||||||
|
|
||||||
_attr_state_class = SensorStateClass.MEASUREMENT
|
entity_description: PowerwallSensorEntityDescription
|
||||||
_attr_native_unit_of_measurement = POWER_KILO_WATT
|
|
||||||
_attr_device_class = SensorDeviceClass.POWER
|
|
||||||
|
|
||||||
def __init__(self, powerwall_data: PowerwallRuntimeData, meter: MeterType) -> None:
|
def __init__(
|
||||||
|
self,
|
||||||
|
powerwall_data: PowerwallRuntimeData,
|
||||||
|
meter: MeterType,
|
||||||
|
description: PowerwallSensorEntityDescription,
|
||||||
|
) -> None:
|
||||||
"""Initialize the sensor."""
|
"""Initialize the sensor."""
|
||||||
|
self.entity_description = description
|
||||||
super().__init__(powerwall_data)
|
super().__init__(powerwall_data)
|
||||||
self._meter = meter
|
self._meter = meter
|
||||||
self._attr_name = f"Powerwall {self._meter.value.title()} Now"
|
self._attr_name = f"Powerwall {self._meter.value.title()} {description.name}"
|
||||||
self._attr_unique_id = (
|
self._attr_unique_id = (
|
||||||
f"{self.base_unique_id}_{self._meter.value}_instant_power"
|
f"{self.base_unique_id}_{self._meter.value}_{description.key}"
|
||||||
)
|
)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def native_value(self) -> float:
|
def native_value(self) -> float:
|
||||||
"""Get the current value in kW."""
|
"""Get the current value."""
|
||||||
return self.data.meters.get_meter(self._meter).get_power(precision=3)
|
return self.entity_description.value_fn(self.data.meters.get_meter(self._meter))
|
||||||
|
|
||||||
@property
|
|
||||||
def extra_state_attributes(self) -> dict[str, Any]:
|
|
||||||
"""Return the device specific state attributes."""
|
|
||||||
meter = self.data.meters.get_meter(self._meter)
|
|
||||||
return {
|
|
||||||
ATTR_FREQUENCY: round(meter.frequency, 1),
|
|
||||||
ATTR_INSTANT_AVERAGE_VOLTAGE: round(meter.average_voltage, 1),
|
|
||||||
ATTR_INSTANT_TOTAL_CURRENT: meter.get_instant_total_current(),
|
|
||||||
ATTR_IS_ACTIVE: meter.is_active(),
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
class PowerWallBackupReserveSensor(PowerWallEntity, SensorEntity):
|
class PowerWallBackupReserveSensor(PowerWallEntity, SensorEntity):
|
||||||
|
@ -2,7 +2,14 @@
|
|||||||
from unittest.mock import patch
|
from unittest.mock import patch
|
||||||
|
|
||||||
from homeassistant.components.powerwall.const import DOMAIN
|
from homeassistant.components.powerwall.const import DOMAIN
|
||||||
from homeassistant.const import CONF_IP_ADDRESS, PERCENTAGE
|
from homeassistant.components.sensor import ATTR_STATE_CLASS
|
||||||
|
from homeassistant.const import (
|
||||||
|
ATTR_DEVICE_CLASS,
|
||||||
|
ATTR_FRIENDLY_NAME,
|
||||||
|
ATTR_UNIT_OF_MEASUREMENT,
|
||||||
|
CONF_IP_ADDRESS,
|
||||||
|
PERCENTAGE,
|
||||||
|
)
|
||||||
from homeassistant.helpers import device_registry as dr
|
from homeassistant.helpers import device_registry as dr
|
||||||
|
|
||||||
from .mocks import _mock_powerwall_with_fixtures
|
from .mocks import _mock_powerwall_with_fixtures
|
||||||
@ -10,7 +17,7 @@ from .mocks import _mock_powerwall_with_fixtures
|
|||||||
from tests.common import MockConfigEntry
|
from tests.common import MockConfigEntry
|
||||||
|
|
||||||
|
|
||||||
async def test_sensors(hass):
|
async def test_sensors(hass, entity_registry_enabled_by_default):
|
||||||
"""Test creation of the sensors."""
|
"""Test creation of the sensors."""
|
||||||
|
|
||||||
mock_powerwall = await _mock_powerwall_with_fixtures(hass)
|
mock_powerwall = await _mock_powerwall_with_fixtures(hass)
|
||||||
@ -35,77 +42,49 @@ async def test_sensors(hass):
|
|||||||
assert reg_device.manufacturer == "Tesla"
|
assert reg_device.manufacturer == "Tesla"
|
||||||
assert reg_device.name == "MySite"
|
assert reg_device.name == "MySite"
|
||||||
|
|
||||||
state = hass.states.get("sensor.powerwall_site_now")
|
|
||||||
assert state.state == "0.032"
|
|
||||||
expected_attributes = {
|
|
||||||
"frequency": 60,
|
|
||||||
"instant_average_voltage": 120.7,
|
|
||||||
"unit_of_measurement": "kW",
|
|
||||||
"friendly_name": "Powerwall Site Now",
|
|
||||||
"device_class": "power",
|
|
||||||
"is_active": False,
|
|
||||||
}
|
|
||||||
# Only test for a subset of attributes in case
|
|
||||||
# HA changes the implementation and a new one appears
|
|
||||||
for key, value in expected_attributes.items():
|
|
||||||
assert state.attributes[key] == value
|
|
||||||
|
|
||||||
assert float(hass.states.get("sensor.powerwall_site_export").state) == 10429.5
|
|
||||||
assert float(hass.states.get("sensor.powerwall_site_import").state) == 4824.2
|
|
||||||
|
|
||||||
export_attributes = hass.states.get("sensor.powerwall_site_export").attributes
|
|
||||||
assert export_attributes["unit_of_measurement"] == "kWh"
|
|
||||||
|
|
||||||
state = hass.states.get("sensor.powerwall_load_now")
|
state = hass.states.get("sensor.powerwall_load_now")
|
||||||
assert state.state == "1.971"
|
assert state.state == "1.971"
|
||||||
expected_attributes = {
|
attributes = state.attributes
|
||||||
"frequency": 60,
|
assert attributes[ATTR_DEVICE_CLASS] == "power"
|
||||||
"instant_average_voltage": 120.7,
|
assert attributes[ATTR_UNIT_OF_MEASUREMENT] == "kW"
|
||||||
"unit_of_measurement": "kW",
|
assert attributes[ATTR_STATE_CLASS] == "measurement"
|
||||||
"friendly_name": "Powerwall Load Now",
|
assert attributes[ATTR_FRIENDLY_NAME] == "Powerwall Load Now"
|
||||||
"device_class": "power",
|
|
||||||
"is_active": True,
|
state = hass.states.get("sensor.powerwall_load_frequency_now")
|
||||||
}
|
assert state.state == "60"
|
||||||
# Only test for a subset of attributes in case
|
attributes = state.attributes
|
||||||
# HA changes the implementation and a new one appears
|
assert attributes[ATTR_DEVICE_CLASS] == "frequency"
|
||||||
for key, value in expected_attributes.items():
|
assert attributes[ATTR_UNIT_OF_MEASUREMENT] == "Hz"
|
||||||
assert state.attributes[key] == value
|
assert attributes[ATTR_STATE_CLASS] == "measurement"
|
||||||
|
assert attributes[ATTR_FRIENDLY_NAME] == "Powerwall Load Frequency Now"
|
||||||
|
|
||||||
|
state = hass.states.get("sensor.powerwall_load_average_voltage_now")
|
||||||
|
assert state.state == "120.7"
|
||||||
|
attributes = state.attributes
|
||||||
|
assert attributes[ATTR_DEVICE_CLASS] == "voltage"
|
||||||
|
assert attributes[ATTR_UNIT_OF_MEASUREMENT] == "V"
|
||||||
|
assert attributes[ATTR_STATE_CLASS] == "measurement"
|
||||||
|
assert attributes[ATTR_FRIENDLY_NAME] == "Powerwall Load Average Voltage Now"
|
||||||
|
|
||||||
|
state = hass.states.get("sensor.powerwall_load_average_current_now")
|
||||||
|
assert state.state == "0"
|
||||||
|
attributes = state.attributes
|
||||||
|
assert attributes[ATTR_DEVICE_CLASS] == "current"
|
||||||
|
assert attributes[ATTR_UNIT_OF_MEASUREMENT] == "A"
|
||||||
|
assert attributes[ATTR_STATE_CLASS] == "measurement"
|
||||||
|
assert attributes[ATTR_FRIENDLY_NAME] == "Powerwall Load Average Current Now"
|
||||||
|
|
||||||
assert float(hass.states.get("sensor.powerwall_load_export").state) == 1056.8
|
assert float(hass.states.get("sensor.powerwall_load_export").state) == 1056.8
|
||||||
assert float(hass.states.get("sensor.powerwall_load_import").state) == 4693.0
|
assert float(hass.states.get("sensor.powerwall_load_import").state) == 4693.0
|
||||||
|
|
||||||
state = hass.states.get("sensor.powerwall_battery_now")
|
state = hass.states.get("sensor.powerwall_battery_now")
|
||||||
assert state.state == "-8.55"
|
assert state.state == "-8.55"
|
||||||
expected_attributes = {
|
|
||||||
"frequency": 60.0,
|
|
||||||
"instant_average_voltage": 240.6,
|
|
||||||
"unit_of_measurement": "kW",
|
|
||||||
"friendly_name": "Powerwall Battery Now",
|
|
||||||
"device_class": "power",
|
|
||||||
"is_active": True,
|
|
||||||
}
|
|
||||||
# Only test for a subset of attributes in case
|
|
||||||
# HA changes the implementation and a new one appears
|
|
||||||
for key, value in expected_attributes.items():
|
|
||||||
assert state.attributes[key] == value
|
|
||||||
|
|
||||||
assert float(hass.states.get("sensor.powerwall_battery_export").state) == 3620.0
|
assert float(hass.states.get("sensor.powerwall_battery_export").state) == 3620.0
|
||||||
assert float(hass.states.get("sensor.powerwall_battery_import").state) == 4216.2
|
assert float(hass.states.get("sensor.powerwall_battery_import").state) == 4216.2
|
||||||
|
|
||||||
state = hass.states.get("sensor.powerwall_solar_now")
|
state = hass.states.get("sensor.powerwall_solar_now")
|
||||||
assert state.state == "10.49"
|
assert state.state == "10.49"
|
||||||
expected_attributes = {
|
|
||||||
"frequency": 60,
|
|
||||||
"instant_average_voltage": 120.7,
|
|
||||||
"unit_of_measurement": "kW",
|
|
||||||
"friendly_name": "Powerwall Solar Now",
|
|
||||||
"device_class": "power",
|
|
||||||
"is_active": True,
|
|
||||||
}
|
|
||||||
# Only test for a subset of attributes in case
|
|
||||||
# HA changes the implementation and a new one appears
|
|
||||||
for key, value in expected_attributes.items():
|
|
||||||
assert state.attributes[key] == value
|
|
||||||
|
|
||||||
assert float(hass.states.get("sensor.powerwall_solar_export").state) == 9864.2
|
assert float(hass.states.get("sensor.powerwall_solar_export").state) == 9864.2
|
||||||
assert float(hass.states.get("sensor.powerwall_solar_import").state) == 28.2
|
assert float(hass.states.get("sensor.powerwall_solar_import").state) == 28.2
|
||||||
|
Loading…
x
Reference in New Issue
Block a user