From e15b2ec0cb966d60d0b88565a4c2a2e20ec15870 Mon Sep 17 00:00:00 2001 From: Colin <486199+c00w@users.noreply.github.com> Date: Fri, 9 Jan 2026 07:02:07 -0700 Subject: [PATCH] openevse: Add device_info and unique_id to sensors (#160543) Co-authored-by: Joostlek --- homeassistant/components/openevse/sensor.py | 39 +- .../components/openevse/strings.json | 25 ++ tests/components/openevse/conftest.py | 1 + .../openevse/snapshots/test_sensor.ambr | 385 ++++++++++++++++++ tests/components/openevse/test_sensor.py | 60 +-- 5 files changed, 473 insertions(+), 37 deletions(-) create mode 100644 tests/components/openevse/snapshots/test_sensor.ambr diff --git a/homeassistant/components/openevse/sensor.py b/homeassistant/components/openevse/sensor.py index d687b7ffa35..330f1eeec82 100644 --- a/homeassistant/components/openevse/sensor.py +++ b/homeassistant/components/openevse/sensor.py @@ -17,6 +17,8 @@ from homeassistant.components.sensor import ( ) from homeassistant.config_entries import SOURCE_IMPORT from homeassistant.const import ( + ATTR_CONNECTIONS, + ATTR_SERIAL_NUMBER, CONF_HOST, CONF_MONITORED_VARIABLES, UnitOfEnergy, @@ -26,6 +28,7 @@ from homeassistant.const import ( from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResultType from homeassistant.helpers import config_validation as cv, issue_registry as ir +from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC, DeviceInfo from homeassistant.helpers.entity_platform import ( AddConfigEntryEntitiesCallback, AddEntitiesCallback, @@ -40,25 +43,25 @@ _LOGGER = logging.getLogger(__name__) SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( SensorEntityDescription( key="status", - name="Charging Status", + translation_key="status", ), SensorEntityDescription( key="charge_time", - name="Charge Time Elapsed", + translation_key="charge_time", native_unit_of_measurement=UnitOfTime.MINUTES, device_class=SensorDeviceClass.DURATION, state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key="ambient_temp", - name="Ambient Temperature", + translation_key="ambient_temp", native_unit_of_measurement=UnitOfTemperature.CELSIUS, device_class=SensorDeviceClass.TEMPERATURE, state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key="ir_temp", - name="IR Temperature", + translation_key="ir_temp", native_unit_of_measurement=UnitOfTemperature.CELSIUS, device_class=SensorDeviceClass.TEMPERATURE, state_class=SensorStateClass.MEASUREMENT, @@ -66,7 +69,7 @@ SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( ), SensorEntityDescription( key="rtc_temp", - name="RTC Temperature", + translation_key="rtc_temp", native_unit_of_measurement=UnitOfTemperature.CELSIUS, device_class=SensorDeviceClass.TEMPERATURE, state_class=SensorStateClass.MEASUREMENT, @@ -74,14 +77,14 @@ SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( ), SensorEntityDescription( key="usage_session", - name="Usage this Session", + translation_key="usage_session", native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, device_class=SensorDeviceClass.ENERGY, state_class=SensorStateClass.TOTAL_INCREASING, ), SensorEntityDescription( key="usage_total", - name="Total Usage", + translation_key="usage_total", native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, device_class=SensorDeviceClass.ENERGY, state_class=SensorStateClass.TOTAL_INCREASING, @@ -158,9 +161,10 @@ async def async_setup_entry( async_add_entities( ( OpenEVSESensor( - config_entry.data[CONF_HOST], config_entry.runtime_data, description, + config_entry.entry_id, + config_entry.unique_id, ) for description in SENSOR_TYPES ), @@ -171,17 +175,32 @@ async def async_setup_entry( class OpenEVSESensor(SensorEntity): """Implementation of an OpenEVSE sensor.""" + _attr_has_entity_name = True + def __init__( self, - host: str, charger: OpenEVSE, description: SensorEntityDescription, + entry_id: str, + unique_id: str | None, ) -> None: """Initialize the sensor.""" self.entity_description = description - self.host = host self.charger = charger + identifier = unique_id or entry_id + self._attr_unique_id = f"{identifier}-{description.key}" + + self._attr_device_info = DeviceInfo( + identifiers={(DOMAIN, identifier)}, + manufacturer="OpenEVSE", + ) + if unique_id: + self._attr_device_info[ATTR_CONNECTIONS] = { + (CONNECTION_NETWORK_MAC, unique_id) + } + self._attr_device_info[ATTR_SERIAL_NUMBER] = unique_id + async def async_update(self) -> None: """Get the monitored data from the charger.""" try: diff --git a/homeassistant/components/openevse/strings.json b/homeassistant/components/openevse/strings.json index 07b6025cae8..593b1ff1bbc 100644 --- a/homeassistant/components/openevse/strings.json +++ b/homeassistant/components/openevse/strings.json @@ -18,6 +18,31 @@ } } }, + "entity": { + "sensor": { + "ambient_temp": { + "name": "Ambient temperature" + }, + "charge_time": { + "name": "Charge time elapsed" + }, + "ir_temp": { + "name": "IR temperature" + }, + "rtc_temp": { + "name": "RTC temperature" + }, + "status": { + "name": "Charging status" + }, + "usage_session": { + "name": "Usage this session" + }, + "usage_total": { + "name": "Total energy usage" + } + } + }, "issues": { "yaml_deprecated": { "description": "Configuring OpenEVSE using YAML is being removed. Your existing YAML configuration has been imported into the UI automatically. Remove the `openevse` configuration from your configuration.yaml file and restart Home Assistant to fix this issue.", diff --git a/tests/components/openevse/conftest.py b/tests/components/openevse/conftest.py index 9b770011d14..5a06f8a4b14 100644 --- a/tests/components/openevse/conftest.py +++ b/tests/components/openevse/conftest.py @@ -69,6 +69,7 @@ def serial_number(has_serial_number: bool) -> str | None: def mock_config_entry(serial_number: str) -> MockConfigEntry: """Create a mock config entry.""" return MockConfigEntry( + title="openevse_mock_config", domain=DOMAIN, data={CONF_HOST: "192.168.1.100"}, entry_id="FAKE", diff --git a/tests/components/openevse/snapshots/test_sensor.ambr b/tests/components/openevse/snapshots/test_sensor.ambr new file mode 100644 index 00000000000..6d4473fdcf4 --- /dev/null +++ b/tests/components/openevse/snapshots/test_sensor.ambr @@ -0,0 +1,385 @@ +# serializer version: 1 +# name: test_entities[sensor.openevse_mock_config_ambient_temperature-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.openevse_mock_config_ambient_temperature', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Ambient temperature', + 'platform': 'openevse', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'ambient_temp', + 'unique_id': 'deadbeeffeed-ambient_temp', + 'unit_of_measurement': , + }) +# --- +# name: test_entities[sensor.openevse_mock_config_ambient_temperature-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'temperature', + 'friendly_name': 'openevse_mock_config Ambient temperature', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.openevse_mock_config_ambient_temperature', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '25.5', + }) +# --- +# name: test_entities[sensor.openevse_mock_config_charge_time_elapsed-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.openevse_mock_config_charge_time_elapsed', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Charge time elapsed', + 'platform': 'openevse', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'charge_time', + 'unique_id': 'deadbeeffeed-charge_time', + 'unit_of_measurement': , + }) +# --- +# name: test_entities[sensor.openevse_mock_config_charge_time_elapsed-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'duration', + 'friendly_name': 'openevse_mock_config Charge time elapsed', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.openevse_mock_config_charge_time_elapsed', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '60.0', + }) +# --- +# name: test_entities[sensor.openevse_mock_config_charging_status-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.openevse_mock_config_charging_status', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Charging status', + 'platform': 'openevse', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'status', + 'unique_id': 'deadbeeffeed-status', + 'unit_of_measurement': None, + }) +# --- +# name: test_entities[sensor.openevse_mock_config_charging_status-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'openevse_mock_config Charging status', + }), + 'context': , + 'entity_id': 'sensor.openevse_mock_config_charging_status', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'Charging', + }) +# --- +# name: test_entities[sensor.openevse_mock_config_ir_temperature-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.openevse_mock_config_ir_temperature', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'IR temperature', + 'platform': 'openevse', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'ir_temp', + 'unique_id': 'deadbeeffeed-ir_temp', + 'unit_of_measurement': , + }) +# --- +# name: test_entities[sensor.openevse_mock_config_ir_temperature-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'temperature', + 'friendly_name': 'openevse_mock_config IR temperature', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.openevse_mock_config_ir_temperature', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '30.2', + }) +# --- +# name: test_entities[sensor.openevse_mock_config_rtc_temperature-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.openevse_mock_config_rtc_temperature', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'RTC temperature', + 'platform': 'openevse', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'rtc_temp', + 'unique_id': 'deadbeeffeed-rtc_temp', + 'unit_of_measurement': , + }) +# --- +# name: test_entities[sensor.openevse_mock_config_rtc_temperature-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'temperature', + 'friendly_name': 'openevse_mock_config RTC temperature', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.openevse_mock_config_rtc_temperature', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '28.7', + }) +# --- +# name: test_entities[sensor.openevse_mock_config_total_energy_usage-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.openevse_mock_config_total_energy_usage', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Total energy usage', + 'platform': 'openevse', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'usage_total', + 'unique_id': 'deadbeeffeed-usage_total', + 'unit_of_measurement': , + }) +# --- +# name: test_entities[sensor.openevse_mock_config_total_energy_usage-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'energy', + 'friendly_name': 'openevse_mock_config Total energy usage', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.openevse_mock_config_total_energy_usage', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '500.0', + }) +# --- +# name: test_entities[sensor.openevse_mock_config_usage_this_session-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.openevse_mock_config_usage_this_session', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Usage this session', + 'platform': 'openevse', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'usage_session', + 'unique_id': 'deadbeeffeed-usage_session', + 'unit_of_measurement': , + }) +# --- +# name: test_entities[sensor.openevse_mock_config_usage_this_session-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'energy', + 'friendly_name': 'openevse_mock_config Usage this session', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.openevse_mock_config_usage_this_session', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '15.0', + }) +# --- diff --git a/tests/components/openevse/test_sensor.py b/tests/components/openevse/test_sensor.py index 352a6432e6b..377314f62d2 100644 --- a/tests/components/openevse/test_sensor.py +++ b/tests/components/openevse/test_sensor.py @@ -2,46 +2,52 @@ from unittest.mock import MagicMock +import pytest +from syrupy.assertion import SnapshotAssertion + from homeassistant.core import HomeAssistant +from homeassistant.helpers import entity_registry as er -from tests.common import MockConfigEntry +from tests.common import MockConfigEntry, snapshot_platform -async def test_sensor_setup( +@pytest.mark.usefixtures("entity_registry_enabled_by_default") +async def test_entities( hass: HomeAssistant, + entity_registry: er.EntityRegistry, + snapshot: SnapshotAssertion, mock_config_entry: MockConfigEntry, mock_charger: MagicMock, ) -> None: - """Test setting up the sensor platform.""" + """Test the sensor entities.""" mock_config_entry.add_to_hass(hass) + await hass.config_entries.async_setup(mock_config_entry.entry_id) - assert await hass.config_entries.async_setup(mock_config_entry.entry_id) - await hass.async_block_till_done() + await snapshot_platform(hass, entity_registry, snapshot, mock_config_entry.entry_id) - state = hass.states.get("sensor.charging_status") - assert state is not None - assert state.state == "Charging" - state = hass.states.get("sensor.charge_time_elapsed") - assert state is not None - assert state.state == "60.0" +async def test_disabled_by_default_entities( + hass: HomeAssistant, + entity_registry: er.EntityRegistry, + mock_config_entry: MockConfigEntry, + mock_charger: MagicMock, +) -> None: + """Test the disabled by default sensor entities.""" + mock_config_entry.add_to_hass(hass) + await hass.config_entries.async_setup(mock_config_entry.entry_id) - state = hass.states.get("sensor.ambient_temperature") - assert state is not None - assert state.state == "25.5" + state = hass.states.get("sensor.openevse_mock_config_ir_temperature") + assert state is None - state = hass.states.get("sensor.usage_this_session") - assert state is not None - assert state.state == "15.0" + entry = entity_registry.async_get("sensor.openevse_mock_config_ir_temperature") + assert entry + assert entry.disabled + assert entry.disabled_by is er.RegistryEntryDisabler.INTEGRATION - state = hass.states.get("sensor.total_usage") - assert state is not None - assert state.state == "500.0" + state = hass.states.get("sensor.openevse_mock_config_temperature") + assert state is None - state = hass.states.get("sensor.ir_temperature") - assert state is not None - assert state.state == "30.2" - - state = hass.states.get("sensor.rtc_temperature") - assert state is not None - assert state.state == "28.7" + entry = entity_registry.async_get("sensor.openevse_mock_config_rtc_temperature") + assert entry + assert entry.disabled + assert entry.disabled_by is er.RegistryEntryDisabler.INTEGRATION