Make powerwall attribute sensors their own sensors (#68345)

pull/68350/head
J. Nick Koston 2022-03-18 10:23:26 -10:00 committed by GitHub
parent 619c1f014b
commit 4cc8998ea7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 140 additions and 100 deletions

View File

@ -1,28 +1,30 @@
"""Support for powerwall sensors."""
from __future__ import annotations
from typing import Any
from collections.abc import Callable
from dataclasses import dataclass
from tesla_powerwall import Meter, MeterType
from homeassistant.components.sensor import (
SensorDeviceClass,
SensorEntity,
SensorEntityDescription,
SensorStateClass,
)
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.helpers.entity_platform import AddEntitiesCallback
from .const import (
ATTR_FREQUENCY,
ATTR_INSTANT_AVERAGE_VOLTAGE,
ATTR_INSTANT_TOTAL_CURRENT,
ATTR_IS_ACTIVE,
DOMAIN,
POWERWALL_COORDINATOR,
)
from .const import DOMAIN, POWERWALL_COORDINATOR
from .entity import PowerWallEntity
from .models import PowerwallData, PowerwallRuntimeData
@ -30,6 +32,79 @@ _METER_DIRECTION_EXPORT = "export"
_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(
hass: HomeAssistant,
config_entry: ConfigEntry,
@ -40,24 +115,17 @@ async def async_setup_entry(
coordinator = powerwall_data[POWERWALL_COORDINATOR]
assert coordinator is not None
data: PowerwallData = coordinator.data
entities: list[
PowerWallEnergySensor
| PowerWallImportSensor
| PowerWallExportSensor
| PowerWallChargeSensor
| PowerWallBackupReserveSensor
] = [
entities: list[PowerWallEntity] = [
PowerWallChargeSensor(powerwall_data),
PowerWallBackupReserveSensor(powerwall_data),
]
for meter in data.meters.meters:
entities.append(PowerWallExportSensor(powerwall_data, meter))
entities.append(PowerWallImportSensor(powerwall_data, meter))
entities.extend(
[
PowerWallEnergySensor(powerwall_data, meter),
PowerWallExportSensor(powerwall_data, meter),
PowerWallImportSensor(powerwall_data, meter),
]
PowerWallEnergySensor(powerwall_data, meter, description)
for description in POWERWALL_INSTANT_SENSORS
)
async_add_entities(entities)
@ -85,34 +153,27 @@ class PowerWallChargeSensor(PowerWallEntity, SensorEntity):
class PowerWallEnergySensor(PowerWallEntity, SensorEntity):
"""Representation of an Powerwall Energy sensor."""
_attr_state_class = SensorStateClass.MEASUREMENT
_attr_native_unit_of_measurement = POWER_KILO_WATT
_attr_device_class = SensorDeviceClass.POWER
entity_description: PowerwallSensorEntityDescription
def __init__(self, powerwall_data: PowerwallRuntimeData, meter: MeterType) -> None:
def __init__(
self,
powerwall_data: PowerwallRuntimeData,
meter: MeterType,
description: PowerwallSensorEntityDescription,
) -> None:
"""Initialize the sensor."""
self.entity_description = description
super().__init__(powerwall_data)
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 = (
f"{self.base_unique_id}_{self._meter.value}_instant_power"
f"{self.base_unique_id}_{self._meter.value}_{description.key}"
)
@property
def native_value(self) -> float:
"""Get the current value in kW."""
return self.data.meters.get_meter(self._meter).get_power(precision=3)
@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(),
}
"""Get the current value."""
return self.entity_description.value_fn(self.data.meters.get_meter(self._meter))
class PowerWallBackupReserveSensor(PowerWallEntity, SensorEntity):

View File

@ -2,7 +2,14 @@
from unittest.mock import patch
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 .mocks import _mock_powerwall_with_fixtures
@ -10,7 +17,7 @@ from .mocks import _mock_powerwall_with_fixtures
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."""
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.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")
assert state.state == "1.971"
expected_attributes = {
"frequency": 60,
"instant_average_voltage": 120.7,
"unit_of_measurement": "kW",
"friendly_name": "Powerwall Load 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
attributes = state.attributes
assert attributes[ATTR_DEVICE_CLASS] == "power"
assert attributes[ATTR_UNIT_OF_MEASUREMENT] == "kW"
assert attributes[ATTR_STATE_CLASS] == "measurement"
assert attributes[ATTR_FRIENDLY_NAME] == "Powerwall Load Now"
state = hass.states.get("sensor.powerwall_load_frequency_now")
assert state.state == "60"
attributes = state.attributes
assert attributes[ATTR_DEVICE_CLASS] == "frequency"
assert attributes[ATTR_UNIT_OF_MEASUREMENT] == "Hz"
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_import").state) == 4693.0
state = hass.states.get("sensor.powerwall_battery_now")
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_import").state) == 4216.2
state = hass.states.get("sensor.powerwall_solar_now")
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_import").state) == 28.2