Add sensors to Tesla Wall Connector Integration (#60507)

pull/59477/head
einarhauks 2021-11-29 16:05:14 +00:00 committed by GitHub
parent 923cb0f4b7
commit 7ece86ee8d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 335 additions and 18 deletions

View File

@ -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__)

View File

@ -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)

View File

@ -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}"

View File

@ -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,
)

View File

@ -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,
)