mirror of
https://github.com/home-assistant/core.git
synced 2025-07-19 19:27:45 +00:00
Add sensors to Tesla Wall Connector Integration (#60507)
This commit is contained in:
parent
923cb0f4b7
commit
7ece86ee8d
@ -34,7 +34,7 @@ from .const import (
|
|||||||
WALLCONNECTOR_DEVICE_NAME,
|
WALLCONNECTOR_DEVICE_NAME,
|
||||||
)
|
)
|
||||||
|
|
||||||
PLATFORMS: list[str] = ["binary_sensor"]
|
PLATFORMS: list[str] = ["binary_sensor", "sensor"]
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
141
homeassistant/components/tesla_wall_connector/sensor.py
Normal file
141
homeassistant/components/tesla_wall_connector/sensor.py
Normal 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)
|
@ -1,14 +1,21 @@
|
|||||||
"""Common fixutres with default mocks as well as common test helper methods."""
|
"""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 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.const import CONF_HOST, CONF_SCAN_INTERVAL
|
||||||
from homeassistant.core import HomeAssistant
|
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
|
@pytest.fixture
|
||||||
@ -24,7 +31,7 @@ def mock_wall_connector_version():
|
|||||||
|
|
||||||
def get_default_version_data():
|
def get_default_version_data():
|
||||||
"""Return default version data object for a wall connector."""
|
"""Return default version data object for a wall connector."""
|
||||||
return tesla_wall_connector.wall_connector.Version(
|
return Version(
|
||||||
{
|
{
|
||||||
"serial_number": "abc123",
|
"serial_number": "abc123",
|
||||||
"part_number": "part_123",
|
"part_number": "part_123",
|
||||||
@ -34,39 +41,96 @@ def get_default_version_data():
|
|||||||
|
|
||||||
|
|
||||||
async def create_wall_connector_entry(
|
async def create_wall_connector_entry(
|
||||||
hass: HomeAssistant, side_effect=None
|
hass: HomeAssistant, side_effect=None, vitals_data=None, lifetime_data=None
|
||||||
) -> MockConfigEntry:
|
) -> MockConfigEntry:
|
||||||
"""Create a wall connector entry in hass."""
|
"""Create a wall connector entry in hass."""
|
||||||
entry = MockConfigEntry(
|
entry = MockConfigEntry(
|
||||||
domain=DOMAIN,
|
domain=DOMAIN,
|
||||||
data={CONF_HOST: "1.2.3.4"},
|
data={CONF_HOST: "1.2.3.4"},
|
||||||
options={CONF_SCAN_INTERVAL: 30},
|
options={CONF_SCAN_INTERVAL: DEFAULT_SCAN_INTERVAL},
|
||||||
)
|
)
|
||||||
|
|
||||||
entry.add_to_hass(hass)
|
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(
|
with patch(
|
||||||
"tesla_wall_connector.WallConnector.async_get_version",
|
"tesla_wall_connector.WallConnector.async_get_version",
|
||||||
return_value=get_default_version_data(),
|
return_value=get_default_version_data(),
|
||||||
side_effect=side_effect,
|
side_effect=side_effect,
|
||||||
), patch(
|
), patch(
|
||||||
"tesla_wall_connector.WallConnector.async_get_vitals",
|
"tesla_wall_connector.WallConnector.async_get_vitals",
|
||||||
return_value=fake_vitals,
|
return_value=vitals_data,
|
||||||
side_effect=side_effect,
|
side_effect=side_effect,
|
||||||
), patch(
|
), patch(
|
||||||
"tesla_wall_connector.WallConnector.async_get_lifetime",
|
"tesla_wall_connector.WallConnector.async_get_lifetime",
|
||||||
return_value=None,
|
return_value=lifetime_data,
|
||||||
side_effect=side_effect,
|
side_effect=side_effect,
|
||||||
):
|
):
|
||||||
await hass.config_entries.async_setup(entry.entry_id)
|
await hass.config_entries.async_setup(entry.entry_id)
|
||||||
await hass.async_block_till_done()
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
return entry
|
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}"
|
||||||
|
41
tests/components/tesla_wall_connector/test_binary_sensor.py
Normal file
41
tests/components/tesla_wall_connector/test_binary_sensor.py
Normal 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,
|
||||||
|
)
|
71
tests/components/tesla_wall_connector/test_sensor.py
Normal file
71
tests/components/tesla_wall_connector/test_sensor.py
Normal 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,
|
||||||
|
)
|
Loading…
x
Reference in New Issue
Block a user