1
0
mirror of https://github.com/home-assistant/core.git synced 2026-05-08 17:49:37 +01:00

Add more sensors to openevse (#160904)

Co-authored-by: Joostlek <joostlek@outlook.com>
This commit is contained in:
Colin
2026-01-27 07:47:30 -07:00
committed by GitHub
parent 03eddfa142
commit 19fe9c0f5e
6 changed files with 1881 additions and 16 deletions
@@ -10,6 +10,8 @@ from homeassistant.exceptions import ConfigEntryNotReady
from .coordinator import OpenEVSEConfigEntry, OpenEVSEDataUpdateCoordinator
PLATFORMS = [Platform.SENSOR]
async def async_setup_entry(hass: HomeAssistant, entry: OpenEVSEConfigEntry) -> bool:
"""Set up OpenEVSE from a config entry."""
@@ -29,10 +31,10 @@ async def async_setup_entry(hass: HomeAssistant, entry: OpenEVSEConfigEntry) ->
entry.runtime_data = coordinator
await hass.config_entries.async_forward_entry_setups(entry, [Platform.SENSOR])
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
return True
async def async_unload_entry(hass: HomeAssistant, entry: OpenEVSEConfigEntry) -> bool:
"""Unload a config entry."""
return await hass.config_entries.async_unload_platforms(entry, [Platform.SENSOR])
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
+250 -2
View File
@@ -4,6 +4,7 @@ from __future__ import annotations
from collections.abc import Callable
from dataclasses import dataclass
from datetime import datetime
import logging
from openevsehttp.__main__ import OpenEVSE
@@ -22,7 +23,15 @@ from homeassistant.const import (
ATTR_SERIAL_NUMBER,
CONF_HOST,
CONF_MONITORED_VARIABLES,
PERCENTAGE,
SIGNAL_STRENGTH_DECIBELS,
EntityCategory,
UnitOfElectricCurrent,
UnitOfElectricPotential,
UnitOfEnergy,
UnitOfInformation,
UnitOfLength,
UnitOfPower,
UnitOfTemperature,
UnitOfTime,
)
@@ -49,15 +58,30 @@ PARALLEL_UPDATES = 0
class OpenEVSESensorDescription(SensorEntityDescription):
"""Describes an OpenEVSE sensor entity."""
value_fn: Callable[[OpenEVSE], str | float | None]
value_fn: Callable[[OpenEVSE], str | float | datetime | None]
SENSOR_TYPES: tuple[OpenEVSESensorDescription, ...] = (
# Status sensors
OpenEVSESensorDescription(
key="status",
translation_key="status",
value_fn=lambda ev: ev.status,
),
OpenEVSESensorDescription(
key="service_level",
translation_key="service_level",
device_class=SensorDeviceClass.ENUM,
options=["level_1", "level_2", "automatic"],
value_fn=lambda ev: {
"1": "level_1",
"2": "level_2",
"a": "automatic",
}.get(ev.service_level.lower()),
entity_category=EntityCategory.DIAGNOSTIC,
entity_registry_enabled_default=False,
),
# Timing sensors
OpenEVSESensorDescription(
key="charge_time",
translation_key="charge_time",
@@ -67,6 +91,80 @@ SENSOR_TYPES: tuple[OpenEVSESensorDescription, ...] = (
state_class=SensorStateClass.MEASUREMENT,
value_fn=lambda ev: ev.charge_time_elapsed,
),
OpenEVSESensorDescription(
key="vehicle_eta",
translation_key="vehicle_eta",
device_class=SensorDeviceClass.TIMESTAMP,
value_fn=lambda ev: ev.vehicle_eta,
),
# Electrical sensors
OpenEVSESensorDescription(
key="charging_current",
translation_key="charging_current",
native_unit_of_measurement=UnitOfElectricCurrent.AMPERE,
device_class=SensorDeviceClass.CURRENT,
state_class=SensorStateClass.MEASUREMENT,
value_fn=lambda ev: ev.charging_current,
),
OpenEVSESensorDescription(
key="charging_voltage",
translation_key="charging_voltage",
native_unit_of_measurement=UnitOfElectricPotential.VOLT,
device_class=SensorDeviceClass.VOLTAGE,
state_class=SensorStateClass.MEASUREMENT,
value_fn=lambda ev: ev.charging_voltage,
),
OpenEVSESensorDescription(
key="charging_power",
translation_key="charging_power",
native_unit_of_measurement=UnitOfPower.WATT,
device_class=SensorDeviceClass.POWER,
state_class=SensorStateClass.MEASUREMENT,
value_fn=lambda ev: ev.charging_power,
),
OpenEVSESensorDescription(
key="current_power",
translation_key="current_power",
native_unit_of_measurement=UnitOfPower.WATT,
device_class=SensorDeviceClass.POWER,
state_class=SensorStateClass.MEASUREMENT,
value_fn=lambda ev: ev.current_power,
),
OpenEVSESensorDescription(
key="current_capacity",
translation_key="current_capacity",
native_unit_of_measurement=UnitOfElectricCurrent.AMPERE,
device_class=SensorDeviceClass.CURRENT,
state_class=SensorStateClass.MEASUREMENT,
value_fn=lambda ev: ev.current_capacity,
),
OpenEVSESensorDescription(
key="max_current",
translation_key="max_current",
native_unit_of_measurement=UnitOfElectricCurrent.AMPERE,
device_class=SensorDeviceClass.CURRENT,
entity_category=EntityCategory.DIAGNOSTIC,
value_fn=lambda ev: ev.max_current,
),
OpenEVSESensorDescription(
key="min_amps",
translation_key="min_amps",
native_unit_of_measurement=UnitOfElectricCurrent.AMPERE,
device_class=SensorDeviceClass.CURRENT,
entity_category=EntityCategory.DIAGNOSTIC,
entity_registry_enabled_default=False,
value_fn=lambda ev: ev.min_amps,
),
OpenEVSESensorDescription(
key="max_amps",
translation_key="max_amps",
native_unit_of_measurement=UnitOfElectricCurrent.AMPERE,
device_class=SensorDeviceClass.CURRENT,
entity_category=EntityCategory.DIAGNOSTIC,
entity_registry_enabled_default=False,
value_fn=lambda ev: ev.max_amps,
),
# Temperature sensors
OpenEVSESensorDescription(
key="ambient_temp",
translation_key="ambient_temp",
@@ -93,6 +191,17 @@ SENSOR_TYPES: tuple[OpenEVSESensorDescription, ...] = (
value_fn=lambda ev: ev.rtc_temperature,
entity_registry_enabled_default=False,
),
OpenEVSESensorDescription(
key="esp_temp",
translation_key="esp_temp",
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
device_class=SensorDeviceClass.TEMPERATURE,
state_class=SensorStateClass.MEASUREMENT,
entity_category=EntityCategory.DIAGNOSTIC,
entity_registry_enabled_default=False,
value_fn=lambda ev: ev.esp_temperature,
),
# Energy sensors
OpenEVSESensorDescription(
key="usage_session",
translation_key="usage_session",
@@ -111,6 +220,145 @@ SENSOR_TYPES: tuple[OpenEVSESensorDescription, ...] = (
state_class=SensorStateClass.TOTAL_INCREASING,
value_fn=lambda ev: ev.usage_total,
),
OpenEVSESensorDescription(
key="total_day",
translation_key="total_day",
native_unit_of_measurement=UnitOfEnergy.WATT_HOUR,
suggested_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
device_class=SensorDeviceClass.ENERGY,
state_class=SensorStateClass.TOTAL_INCREASING,
entity_registry_enabled_default=False,
value_fn=lambda ev: ev.total_day,
),
OpenEVSESensorDescription(
key="total_week",
translation_key="total_week",
native_unit_of_measurement=UnitOfEnergy.WATT_HOUR,
suggested_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
device_class=SensorDeviceClass.ENERGY,
state_class=SensorStateClass.TOTAL_INCREASING,
entity_registry_enabled_default=False,
value_fn=lambda ev: ev.total_week,
),
OpenEVSESensorDescription(
key="total_month",
translation_key="total_month",
native_unit_of_measurement=UnitOfEnergy.WATT_HOUR,
suggested_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
device_class=SensorDeviceClass.ENERGY,
state_class=SensorStateClass.TOTAL_INCREASING,
entity_registry_enabled_default=False,
value_fn=lambda ev: ev.total_month,
),
OpenEVSESensorDescription(
key="total_year",
translation_key="total_year",
native_unit_of_measurement=UnitOfEnergy.WATT_HOUR,
suggested_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
device_class=SensorDeviceClass.ENERGY,
state_class=SensorStateClass.TOTAL_INCREASING,
entity_registry_enabled_default=False,
value_fn=lambda ev: ev.total_year,
),
# Vehicle sensors
OpenEVSESensorDescription(
key="vehicle_soc",
translation_key="vehicle_soc",
native_unit_of_measurement=PERCENTAGE,
device_class=SensorDeviceClass.BATTERY,
state_class=SensorStateClass.MEASUREMENT,
value_fn=lambda ev: ev.vehicle_soc,
),
OpenEVSESensorDescription(
key="vehicle_range",
translation_key="vehicle_range",
native_unit_of_measurement=UnitOfLength.KILOMETERS,
device_class=SensorDeviceClass.DISTANCE,
state_class=SensorStateClass.MEASUREMENT,
value_fn=lambda ev: ev.vehicle_range,
),
# Connectivity sensors
OpenEVSESensorDescription(
key="wifi_signal",
native_unit_of_measurement=SIGNAL_STRENGTH_DECIBELS,
device_class=SensorDeviceClass.SIGNAL_STRENGTH,
state_class=SensorStateClass.MEASUREMENT,
entity_category=EntityCategory.DIAGNOSTIC,
entity_registry_enabled_default=False,
value_fn=lambda ev: ev.wifi_signal,
),
# Power shaper sensors
OpenEVSESensorDescription(
key="shaper_live_power",
translation_key="shaper_live_power",
native_unit_of_measurement=UnitOfPower.WATT,
device_class=SensorDeviceClass.POWER,
state_class=SensorStateClass.MEASUREMENT,
entity_registry_enabled_default=False,
value_fn=lambda ev: ev.shaper_live_power,
),
OpenEVSESensorDescription(
key="shaper_available_current",
translation_key="shaper_available_current",
native_unit_of_measurement=UnitOfElectricCurrent.AMPERE,
device_class=SensorDeviceClass.CURRENT,
state_class=SensorStateClass.MEASUREMENT,
entity_registry_enabled_default=False,
value_fn=lambda ev: ev.shaper_available_current,
),
OpenEVSESensorDescription(
key="shaper_max_power",
translation_key="shaper_max_power",
native_unit_of_measurement=UnitOfPower.WATT,
device_class=SensorDeviceClass.POWER,
entity_category=EntityCategory.DIAGNOSTIC,
entity_registry_enabled_default=False,
value_fn=lambda ev: ev.shaper_max_power,
),
# Safety trip count sensors
OpenEVSESensorDescription(
key="gfi_trip_count",
translation_key="gfi_trip_count",
state_class=SensorStateClass.TOTAL,
entity_category=EntityCategory.DIAGNOSTIC,
entity_registry_enabled_default=False,
value_fn=lambda ev: ev.gfi_trip_count,
),
OpenEVSESensorDescription(
key="no_gnd_trip_count",
translation_key="no_gnd_trip_count",
state_class=SensorStateClass.TOTAL,
entity_category=EntityCategory.DIAGNOSTIC,
entity_registry_enabled_default=False,
value_fn=lambda ev: ev.no_gnd_trip_count,
),
OpenEVSESensorDescription(
key="stuck_relay_trip_count",
translation_key="stuck_relay_trip_count",
state_class=SensorStateClass.TOTAL,
entity_category=EntityCategory.DIAGNOSTIC,
entity_registry_enabled_default=False,
value_fn=lambda ev: ev.stuck_relay_trip_count,
),
# System diagnostic sensors
OpenEVSESensorDescription(
key="uptime",
translation_key="uptime",
native_unit_of_measurement=UnitOfTime.SECONDS,
device_class=SensorDeviceClass.DURATION,
entity_category=EntityCategory.DIAGNOSTIC,
entity_registry_enabled_default=False,
value_fn=lambda ev: ev.uptime,
),
OpenEVSESensorDescription(
key="freeram",
translation_key="freeram",
native_unit_of_measurement=UnitOfInformation.BYTES,
device_class=SensorDeviceClass.DATA_SIZE,
entity_category=EntityCategory.DIAGNOSTIC,
entity_registry_enabled_default=False,
value_fn=lambda ev: ev.freeram,
),
)
SENSOR_KEYS: list[str] = [desc.key for desc in SENSOR_TYPES]
@@ -217,6 +465,6 @@ class OpenEVSESensor(CoordinatorEntity[OpenEVSEDataUpdateCoordinator], SensorEnt
self._attr_device_info[ATTR_SERIAL_NUMBER] = unique_id
@property
def native_value(self) -> StateType:
def native_value(self) -> StateType | datetime:
"""Return the state of the sensor."""
return self.entity_description.value_fn(self.coordinator.charger)
@@ -34,23 +34,118 @@
"ambient_temp": {
"name": "Ambient temperature"
},
"available_current": {
"name": "Available current"
},
"charge_rate": {
"name": "Charge rate"
},
"charge_time": {
"name": "Charge time elapsed"
},
"charging_current": {
"name": "Charging current"
},
"charging_power": {
"name": "Charging power"
},
"charging_voltage": {
"name": "Charging voltage"
},
"current_capacity": {
"name": "Current capacity"
},
"current_power": {
"name": "Current power"
},
"esp_temp": {
"name": "ESP temperature"
},
"freeram": {
"name": "Free memory"
},
"gfi_trip_count": {
"name": "GFCI trip count"
},
"ir_temp": {
"name": "IR temperature"
},
"max_amps": {
"name": "Maximum amperage"
},
"max_current": {
"name": "Maximum current"
},
"min_amps": {
"name": "Minimum amperage"
},
"mode": {
"name": "Mode"
},
"no_gnd_trip_count": {
"name": "No ground trip count"
},
"ota_update": {
"name": "OTA update"
},
"rtc_temp": {
"name": "RTC temperature"
},
"service_level": {
"name": "Service level",
"state": {
"automatic": "Automatic",
"level_1": "Level 1 (120V)",
"level_2": "Level 2 (240V)"
}
},
"shaper_available_current": {
"name": "Shaper available current"
},
"shaper_live_power": {
"name": "Shaper live power"
},
"shaper_max_power": {
"name": "Shaper maximum power"
},
"smoothed_available_current": {
"name": "Smoothed available current"
},
"status": {
"name": "Charging status"
},
"stuck_relay_trip_count": {
"name": "Stuck relay trip count"
},
"total_day": {
"name": "Daily energy usage"
},
"total_month": {
"name": "Monthly energy usage"
},
"total_week": {
"name": "Weekly energy usage"
},
"total_year": {
"name": "Yearly energy usage"
},
"uptime": {
"name": "Uptime"
},
"usage_session": {
"name": "Usage this session"
},
"usage_total": {
"name": "Total energy usage"
},
"vehicle_eta": {
"name": "Vehicle charge completion"
},
"vehicle_range": {
"name": "Vehicle range"
},
"vehicle_soc": {
"name": "Vehicle state of charge"
}
}
},
+53 -8
View File
@@ -26,19 +26,64 @@ def mock_charger() -> Generator[MagicMock]:
):
charger = mock.return_value
charger.update = AsyncMock()
charger.status = "Charging"
charger.charge_time_elapsed = 3600 # 60 minutes in seconds
charger.ambient_temperature = 25.5
charger.ir_temperature = 30.2
charger.rtc_temperature = 28.7
charger.usage_session = 15000 # 15 kWh in Wh
charger.usage_total = 500000 # 500 kWh in Wh
charger.charging_current = 32.0
charger.test_and_get = AsyncMock()
charger.test_and_get.return_value = {
"serial": "deadbeeffeed",
"model": "openevse_wifi_v1",
}
# Status sensors
charger.status = "Charging"
charger.vehicle = True
charger.mode = "STA"
charger.charge_mode = "fast"
charger.divertmode = "normal"
charger.manual_override = False
charger.ota_update = "none"
charger.service_level = "2"
# Timing sensors
charger.charge_time_elapsed = 3600 # 60 minutes in seconds
charger.vehicle_eta = None
# Electrical sensors
charger.charging_current = 32.0
charger.charging_voltage = 240
charger.charging_power = 7680.0
charger.current_power = 7680
charger.current_capacity = 32
charger.max_current = 48
charger.min_amps = 6
charger.max_amps = 48
# Divert/solar mode sensors
charger.available_current = 32.0
charger.smoothed_available_current = 32.0
charger.charge_rate = 32.0
# Temperature sensors
charger.ambient_temperature = 25.5
charger.ir_temperature = 30.2
charger.rtc_temperature = 28.7
charger.esp_temperature = 45.0
# Energy sensors
charger.usage_session = 15000 # 15 kWh in Wh
charger.usage_total = 500000 # 500 kWh in Wh
charger.total_day = 10000 # 10 kWh in Wh
charger.total_week = 50000 # 50 kWh in Wh
charger.total_month = 200000 # 200 kWh in Wh
charger.total_year = 2000000 # 2000 kWh in Wh
# Vehicle sensors
charger.vehicle_soc = 75
charger.vehicle_range = 250
# Connectivity sensors
charger.wifi_signal = -65
# Power shaper sensors
charger.shaper_live_power = 5000
charger.shaper_available_current = 20.0
charger.shaper_max_power = 11000
# Safety trip count sensors
charger.gfi_trip_count = 0
charger.no_gnd_trip_count = 0
charger.stuck_relay_trip_count = 0
# System diagnostic sensors
charger.uptime = 86400 # 1 day in seconds
charger.freeram = 50000
yield charger
File diff suppressed because it is too large Load Diff
+27 -4
View File
@@ -1,13 +1,13 @@
"""Tests for the OpenEVSE sensor platform."""
from unittest.mock import MagicMock
from unittest.mock import MagicMock, patch
import pytest
from syrupy.assertion import SnapshotAssertion
from homeassistant.components.openevse.const import DOMAIN
from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN
from homeassistant.const import CONF_HOST, STATE_UNAVAILABLE
from homeassistant.const import CONF_HOST, STATE_UNAVAILABLE, STATE_UNKNOWN, Platform
from homeassistant.core import HomeAssistant
from homeassistant.helpers import entity_registry as er, issue_registry as ir
from homeassistant.setup import async_setup_component
@@ -24,8 +24,9 @@ async def test_entities(
mock_charger: MagicMock,
) -> None:
"""Test the sensor entities."""
mock_config_entry.add_to_hass(hass)
await hass.config_entries.async_setup(mock_config_entry.entry_id)
with patch("homeassistant.components.openevse.PLATFORMS", [Platform.SENSOR]):
mock_config_entry.add_to_hass(hass)
await hass.config_entries.async_setup(mock_config_entry.entry_id)
await snapshot_platform(hass, entity_registry, snapshot, mock_config_entry.entry_id)
@@ -57,6 +58,28 @@ async def test_disabled_by_default_entities(
assert entry.disabled_by is er.RegistryEntryDisabler.INTEGRATION
async def test_missing_sensor_graceful_handling(
hass: HomeAssistant,
mock_config_entry: MockConfigEntry,
mock_charger: MagicMock,
) -> None:
"""Test that missing sensor attributes are handled gracefully."""
mock_charger.vehicle_soc = None
mock_config_entry.add_to_hass(hass)
await hass.config_entries.async_setup(mock_config_entry.entry_id)
# The sensor with missing attribute should be unknown
state = hass.states.get("sensor.openevse_mock_config_vehicle_state_of_charge")
assert state is not None
assert state.state == STATE_UNKNOWN
# Other sensors should still work
state = hass.states.get("sensor.openevse_mock_config_charging_status")
assert state is not None
assert state.state == "Charging"
async def test_sensor_unavailable_on_coordinator_timeout(
hass: HomeAssistant,
mock_config_entry: MockConfigEntry,