Add sensors to Tesla Wall Connector Integration (#60507)
parent
923cb0f4b7
commit
7ece86ee8d
|
@ -34,7 +34,7 @@ from .const import (
|
|||
WALLCONNECTOR_DEVICE_NAME,
|
||||
)
|
||||
|
||||
PLATFORMS: list[str] = ["binary_sensor"]
|
||||
PLATFORMS: list[str] = ["binary_sensor", "sensor"]
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
|
|
@ -0,0 +1,141 @@
|
|||
"""Sensors for Tesla Wall Connector."""
|
||||
from dataclasses import dataclass
|
||||
import logging
|
||||
|
||||
from homeassistant.components.sensor import (
|
||||
STATE_CLASS_MEASUREMENT,
|
||||
STATE_CLASS_TOTAL_INCREASING,
|
||||
SensorEntity,
|
||||
SensorEntityDescription,
|
||||
)
|
||||
from homeassistant.const import (
|
||||
DEVICE_CLASS_ENERGY,
|
||||
DEVICE_CLASS_POWER,
|
||||
DEVICE_CLASS_TEMPERATURE,
|
||||
DEVICE_CLASS_VOLTAGE,
|
||||
ELECTRIC_POTENTIAL_VOLT,
|
||||
ENERGY_KILO_WATT_HOUR,
|
||||
ENTITY_CATEGORY_DIAGNOSTIC,
|
||||
FREQUENCY_HERTZ,
|
||||
POWER_KILO_WATT,
|
||||
TEMP_CELSIUS,
|
||||
)
|
||||
|
||||
from . import (
|
||||
WallConnectorData,
|
||||
WallConnectorEntity,
|
||||
WallConnectorLambdaValueGetterMixin,
|
||||
prefix_entity_name,
|
||||
)
|
||||
from .const import DOMAIN, WALLCONNECTOR_DATA_LIFETIME, WALLCONNECTOR_DATA_VITALS
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@dataclass
|
||||
class WallConnectorSensorDescription(
|
||||
SensorEntityDescription, WallConnectorLambdaValueGetterMixin
|
||||
):
|
||||
"""Sensor entity description with a function pointer for getting sensor value."""
|
||||
|
||||
|
||||
WALL_CONNECTOR_SENSORS = [
|
||||
WallConnectorSensorDescription(
|
||||
key="evse_state",
|
||||
name=prefix_entity_name("State"),
|
||||
entity_category=ENTITY_CATEGORY_DIAGNOSTIC,
|
||||
value_fn=lambda data: data[WALLCONNECTOR_DATA_VITALS].evse_state,
|
||||
),
|
||||
WallConnectorSensorDescription(
|
||||
key="handle_temp_c",
|
||||
name=prefix_entity_name("Handle Temperature"),
|
||||
native_unit_of_measurement=TEMP_CELSIUS,
|
||||
value_fn=lambda data: round(data[WALLCONNECTOR_DATA_VITALS].handle_temp_c, 1),
|
||||
device_class=DEVICE_CLASS_TEMPERATURE,
|
||||
entity_category=ENTITY_CATEGORY_DIAGNOSTIC,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
),
|
||||
WallConnectorSensorDescription(
|
||||
key="grid_v",
|
||||
name=prefix_entity_name("Grid Voltage"),
|
||||
native_unit_of_measurement=ELECTRIC_POTENTIAL_VOLT,
|
||||
value_fn=lambda data: round(data[WALLCONNECTOR_DATA_VITALS].grid_v, 1),
|
||||
device_class=DEVICE_CLASS_VOLTAGE,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
entity_category=ENTITY_CATEGORY_DIAGNOSTIC,
|
||||
),
|
||||
WallConnectorSensorDescription(
|
||||
key="grid_hz",
|
||||
name=prefix_entity_name("Grid Frequency"),
|
||||
native_unit_of_measurement=FREQUENCY_HERTZ,
|
||||
value_fn=lambda data: round(data[WALLCONNECTOR_DATA_VITALS].grid_hz, 3),
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
entity_category=ENTITY_CATEGORY_DIAGNOSTIC,
|
||||
),
|
||||
WallConnectorSensorDescription(
|
||||
key="power",
|
||||
name=prefix_entity_name("Power"),
|
||||
native_unit_of_measurement=POWER_KILO_WATT,
|
||||
value_fn=lambda data: round(
|
||||
(
|
||||
(
|
||||
data[WALLCONNECTOR_DATA_VITALS].currentA_a
|
||||
* data[WALLCONNECTOR_DATA_VITALS].voltageA_v
|
||||
)
|
||||
+ (
|
||||
data[WALLCONNECTOR_DATA_VITALS].currentB_a
|
||||
* data[WALLCONNECTOR_DATA_VITALS].voltageB_v
|
||||
)
|
||||
+ (
|
||||
data[WALLCONNECTOR_DATA_VITALS].currentC_a
|
||||
* data[WALLCONNECTOR_DATA_VITALS].voltageC_v
|
||||
)
|
||||
)
|
||||
/ 1000.0,
|
||||
1,
|
||||
),
|
||||
device_class=DEVICE_CLASS_POWER,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
),
|
||||
WallConnectorSensorDescription(
|
||||
key="total_energy_kWh",
|
||||
name=prefix_entity_name("Total Energy"),
|
||||
native_unit_of_measurement=ENERGY_KILO_WATT_HOUR,
|
||||
value_fn=lambda data: data[WALLCONNECTOR_DATA_LIFETIME].energy_wh / 1000.0,
|
||||
state_class=STATE_CLASS_TOTAL_INCREASING,
|
||||
device_class=DEVICE_CLASS_ENERGY,
|
||||
),
|
||||
]
|
||||
|
||||
|
||||
async def async_setup_entry(hass, config_entry, async_add_devices):
|
||||
"""Create the Wall Connector sensor devices."""
|
||||
wall_connector_data = hass.data[DOMAIN][config_entry.entry_id]
|
||||
|
||||
all_entities = [
|
||||
WallConnectorSensorEntity(wall_connector_data, description)
|
||||
for description in WALL_CONNECTOR_SENSORS
|
||||
]
|
||||
|
||||
async_add_devices(all_entities)
|
||||
|
||||
|
||||
class WallConnectorSensorEntity(WallConnectorEntity, SensorEntity):
|
||||
"""Wall Connector Sensor Entity."""
|
||||
|
||||
entity_description: WallConnectorSensorDescription
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
wall_connector_data: WallConnectorData,
|
||||
description: WallConnectorSensorDescription,
|
||||
) -> None:
|
||||
"""Initialize WallConnectorSensorEntity."""
|
||||
self.entity_description = description
|
||||
super().__init__(wall_connector_data)
|
||||
|
||||
@property
|
||||
def native_value(self):
|
||||
"""Return the state of the sensor."""
|
||||
|
||||
return self.entity_description.value_fn(self.coordinator.data)
|
|
@ -1,14 +1,21 @@
|
|||
"""Common fixutres with default mocks as well as common test helper methods."""
|
||||
from unittest.mock import patch
|
||||
from dataclasses import dataclass
|
||||
from datetime import timedelta
|
||||
from typing import Any
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
import pytest
|
||||
import tesla_wall_connector
|
||||
from tesla_wall_connector.wall_connector import Lifetime, Version, Vitals
|
||||
|
||||
from homeassistant.components.tesla_wall_connector.const import DOMAIN
|
||||
from homeassistant.components.tesla_wall_connector.const import (
|
||||
DEFAULT_SCAN_INTERVAL,
|
||||
DOMAIN,
|
||||
)
|
||||
from homeassistant.const import CONF_HOST, CONF_SCAN_INTERVAL
|
||||
from homeassistant.core import HomeAssistant
|
||||
import homeassistant.util.dt as dt_util
|
||||
|
||||
from tests.common import MockConfigEntry
|
||||
from tests.common import MockConfigEntry, async_fire_time_changed
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
|
@ -24,7 +31,7 @@ def mock_wall_connector_version():
|
|||
|
||||
def get_default_version_data():
|
||||
"""Return default version data object for a wall connector."""
|
||||
return tesla_wall_connector.wall_connector.Version(
|
||||
return Version(
|
||||
{
|
||||
"serial_number": "abc123",
|
||||
"part_number": "part_123",
|
||||
|
@ -34,39 +41,96 @@ def get_default_version_data():
|
|||
|
||||
|
||||
async def create_wall_connector_entry(
|
||||
hass: HomeAssistant, side_effect=None
|
||||
hass: HomeAssistant, side_effect=None, vitals_data=None, lifetime_data=None
|
||||
) -> MockConfigEntry:
|
||||
"""Create a wall connector entry in hass."""
|
||||
entry = MockConfigEntry(
|
||||
domain=DOMAIN,
|
||||
data={CONF_HOST: "1.2.3.4"},
|
||||
options={CONF_SCAN_INTERVAL: 30},
|
||||
options={CONF_SCAN_INTERVAL: DEFAULT_SCAN_INTERVAL},
|
||||
)
|
||||
|
||||
entry.add_to_hass(hass)
|
||||
|
||||
# We need to return vitals with a contactor_closed attribute
|
||||
# Since that is used to determine the update scan interval
|
||||
fake_vitals = tesla_wall_connector.wall_connector.Vitals(
|
||||
{
|
||||
"contactor_closed": "false",
|
||||
}
|
||||
)
|
||||
|
||||
with patch(
|
||||
"tesla_wall_connector.WallConnector.async_get_version",
|
||||
return_value=get_default_version_data(),
|
||||
side_effect=side_effect,
|
||||
), patch(
|
||||
"tesla_wall_connector.WallConnector.async_get_vitals",
|
||||
return_value=fake_vitals,
|
||||
return_value=vitals_data,
|
||||
side_effect=side_effect,
|
||||
), patch(
|
||||
"tesla_wall_connector.WallConnector.async_get_lifetime",
|
||||
return_value=None,
|
||||
return_value=lifetime_data,
|
||||
side_effect=side_effect,
|
||||
):
|
||||
await hass.config_entries.async_setup(entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
return entry
|
||||
|
||||
|
||||
def get_vitals_mock() -> Vitals:
|
||||
"""Get mocked vitals object."""
|
||||
vitals = MagicMock(auto_spec=Vitals)
|
||||
return vitals
|
||||
|
||||
|
||||
def get_lifetime_mock() -> Lifetime:
|
||||
"""Get mocked lifetime object."""
|
||||
lifetime = MagicMock(auto_spec=Lifetime)
|
||||
return lifetime
|
||||
|
||||
|
||||
@dataclass
|
||||
class EntityAndExpectedValues:
|
||||
"""Class for keeping entity id along with expected value for first and second data updates."""
|
||||
|
||||
entity_id: str
|
||||
first_value: Any
|
||||
second_value: Any
|
||||
|
||||
|
||||
async def _test_sensors(
|
||||
hass: HomeAssistant,
|
||||
entities_and_expected_values,
|
||||
vitals_first_update: Vitals,
|
||||
vitals_second_update: Vitals,
|
||||
lifetime_first_update: Lifetime,
|
||||
lifetime_second_update: Lifetime,
|
||||
) -> None:
|
||||
"""Test update of sensor values."""
|
||||
|
||||
# First Update: Data is fetched when the integration is initialized
|
||||
await create_wall_connector_entry(
|
||||
hass, vitals_data=vitals_first_update, lifetime_data=lifetime_first_update
|
||||
)
|
||||
|
||||
# Verify expected vs actual values of first update
|
||||
for entity in entities_and_expected_values:
|
||||
state = hass.states.get(entity.entity_id)
|
||||
assert state, f"Unable to get state of {entity.entity_id}"
|
||||
assert (
|
||||
state.state == entity.first_value
|
||||
), f"First update: {entity.entity_id} is expected to have state {entity.first_value} but has {state.state}"
|
||||
|
||||
# Simulate second data update
|
||||
with patch(
|
||||
"tesla_wall_connector.WallConnector.async_get_vitals",
|
||||
return_value=vitals_second_update,
|
||||
), patch(
|
||||
"tesla_wall_connector.WallConnector.async_get_lifetime",
|
||||
return_value=lifetime_second_update,
|
||||
):
|
||||
async_fire_time_changed(
|
||||
hass, dt_util.utcnow() + timedelta(seconds=DEFAULT_SCAN_INTERVAL)
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
# Verify expected vs actual values of second update
|
||||
for entity in entities_and_expected_values:
|
||||
state = hass.states.get(entity.entity_id)
|
||||
assert (
|
||||
state.state == entity.second_value
|
||||
), f"Second update: {entity.entity_id} is expected to have state {entity.second_value} but has {state.state}"
|
||||
|
|
|
@ -0,0 +1,41 @@
|
|||
"""Tests for binary sensors."""
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
from .conftest import (
|
||||
EntityAndExpectedValues,
|
||||
_test_sensors,
|
||||
get_lifetime_mock,
|
||||
get_vitals_mock,
|
||||
)
|
||||
|
||||
|
||||
async def test_sensors(hass: HomeAssistant) -> None:
|
||||
"""Test all binary sensors."""
|
||||
|
||||
entity_and_expected_values = [
|
||||
EntityAndExpectedValues(
|
||||
"binary_sensor.tesla_wall_connector_contactor_closed", "off", "on"
|
||||
),
|
||||
EntityAndExpectedValues(
|
||||
"binary_sensor.tesla_wall_connector_vehicle_connected", "on", "off"
|
||||
),
|
||||
]
|
||||
|
||||
mock_vitals_first_update = get_vitals_mock()
|
||||
mock_vitals_first_update.contactor_closed = False
|
||||
mock_vitals_first_update.vehicle_connected = True
|
||||
|
||||
mock_vitals_second_update = get_vitals_mock()
|
||||
mock_vitals_second_update.contactor_closed = True
|
||||
mock_vitals_second_update.vehicle_connected = False
|
||||
|
||||
lifetime_mock = get_lifetime_mock()
|
||||
|
||||
await _test_sensors(
|
||||
hass,
|
||||
entities_and_expected_values=entity_and_expected_values,
|
||||
vitals_first_update=mock_vitals_first_update,
|
||||
vitals_second_update=mock_vitals_second_update,
|
||||
lifetime_first_update=lifetime_mock,
|
||||
lifetime_second_update=lifetime_mock,
|
||||
)
|
|
@ -0,0 +1,71 @@
|
|||
"""Tests for sensors."""
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
from .conftest import (
|
||||
EntityAndExpectedValues,
|
||||
_test_sensors,
|
||||
get_lifetime_mock,
|
||||
get_vitals_mock,
|
||||
)
|
||||
|
||||
|
||||
async def test_sensors(hass: HomeAssistant) -> None:
|
||||
"""Test all sensors."""
|
||||
|
||||
entity_and_expected_values = [
|
||||
EntityAndExpectedValues("sensor.tesla_wall_connector_state", "1", "2"),
|
||||
EntityAndExpectedValues(
|
||||
"sensor.tesla_wall_connector_handle_temperature", "25.5", "-1.4"
|
||||
),
|
||||
EntityAndExpectedValues(
|
||||
"sensor.tesla_wall_connector_grid_voltage", "230.2", "229.2"
|
||||
),
|
||||
EntityAndExpectedValues(
|
||||
"sensor.tesla_wall_connector_grid_frequency", "50.021", "49.981"
|
||||
),
|
||||
EntityAndExpectedValues("sensor.tesla_wall_connector_power", "7.6", "7.6"),
|
||||
EntityAndExpectedValues(
|
||||
"sensor.tesla_wall_connector_total_energy", "988.022", "989.0"
|
||||
),
|
||||
]
|
||||
|
||||
mock_vitals_first_update = get_vitals_mock()
|
||||
mock_vitals_first_update.evse_state = 1
|
||||
mock_vitals_first_update.handle_temp_c = 25.51
|
||||
mock_vitals_first_update.grid_v = 230.15
|
||||
mock_vitals_first_update.grid_hz = 50.021
|
||||
# to calculate power, we calculate power of each phase and sum up
|
||||
# (230.1*10) + (231.1*11) + (232.1*12) = 7628.3 W
|
||||
mock_vitals_first_update.voltageA_v = 230.1
|
||||
mock_vitals_first_update.voltageB_v = 231.1
|
||||
mock_vitals_first_update.voltageC_v = 232.1
|
||||
mock_vitals_first_update.currentA_a = 10
|
||||
mock_vitals_first_update.currentB_a = 11
|
||||
mock_vitals_first_update.currentC_a = 12
|
||||
|
||||
mock_vitals_second_update = get_vitals_mock()
|
||||
mock_vitals_second_update.evse_state = 2
|
||||
mock_vitals_second_update.handle_temp_c = -1.42
|
||||
mock_vitals_second_update.grid_v = 229.21
|
||||
mock_vitals_second_update.grid_hz = 49.981
|
||||
# (228.1*10) + (229.1*11) + (230.1*12) = 7562.3 W
|
||||
mock_vitals_second_update.voltageB_v = 228.1
|
||||
mock_vitals_second_update.voltageC_v = 229.1
|
||||
mock_vitals_second_update.voltageA_v = 230.1
|
||||
mock_vitals_second_update.currentA_a = 10
|
||||
mock_vitals_second_update.currentB_a = 11
|
||||
mock_vitals_second_update.currentC_a = 12
|
||||
|
||||
lifetime_mock_first_update = get_lifetime_mock()
|
||||
lifetime_mock_first_update.energy_wh = 988022
|
||||
lifetime_mock_second_update = get_lifetime_mock()
|
||||
lifetime_mock_second_update.energy_wh = 989000
|
||||
|
||||
await _test_sensors(
|
||||
hass,
|
||||
entities_and_expected_values=entity_and_expected_values,
|
||||
vitals_first_update=mock_vitals_first_update,
|
||||
vitals_second_update=mock_vitals_second_update,
|
||||
lifetime_first_update=lifetime_mock_first_update,
|
||||
lifetime_second_update=lifetime_mock_second_update,
|
||||
)
|
Loading…
Reference in New Issue