Add more sensors to Peblar Rocksolid EV Chargers integration (#133754)
Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>pull/133757/head
parent
5e4e1ce5a7
commit
9dc20b5709
|
@ -5,6 +5,38 @@ from __future__ import annotations
|
|||
import logging
|
||||
from typing import Final
|
||||
|
||||
from peblar import ChargeLimiter, CPState
|
||||
|
||||
DOMAIN: Final = "peblar"
|
||||
|
||||
LOGGER = logging.getLogger(__package__)
|
||||
|
||||
PEBLAR_CHARGE_LIMITER_TO_HOME_ASSISTANT = {
|
||||
ChargeLimiter.CHARGING_CABLE: "charging_cable",
|
||||
ChargeLimiter.CURRENT_LIMITER: "current_limiter",
|
||||
ChargeLimiter.DYNAMIC_LOAD_BALANCING: "dynamic_load_balancing",
|
||||
ChargeLimiter.EXTERNAL_POWER_LIMIT: "external_power_limit",
|
||||
ChargeLimiter.GROUP_LOAD_BALANCING: "group_load_balancing",
|
||||
ChargeLimiter.HARDWARE_LIMITATION: "hardware_limitation",
|
||||
ChargeLimiter.HIGH_TEMPERATURE: "high_temperature",
|
||||
ChargeLimiter.HOUSEHOLD_POWER_LIMIT: "household_power_limit",
|
||||
ChargeLimiter.INSTALLATION_LIMIT: "installation_limit",
|
||||
ChargeLimiter.LOCAL_MODBUS_API: "local_modbus_api",
|
||||
ChargeLimiter.LOCAL_REST_API: "local_rest_api",
|
||||
ChargeLimiter.LOCAL_SCHEDULED: "local_scheduled",
|
||||
ChargeLimiter.OCPP_SMART_CHARGING: "ocpp_smart_charging",
|
||||
ChargeLimiter.OVERCURRENT_PROTECTION: "overcurrent_protection",
|
||||
ChargeLimiter.PHASE_IMBALANCE: "phase_imbalance",
|
||||
ChargeLimiter.POWER_FACTOR: "power_factor",
|
||||
ChargeLimiter.SOLAR_CHARGING: "solar_charging",
|
||||
}
|
||||
|
||||
PEBLAR_CP_STATE_TO_HOME_ASSISTANT = {
|
||||
CPState.CHARGING_SUSPENDED: "suspended",
|
||||
CPState.CHARGING_VENTILATION: "charging",
|
||||
CPState.CHARGING: "charging",
|
||||
CPState.ERROR: "error",
|
||||
CPState.FAULT: "fault",
|
||||
CPState.INVALID: "invalid",
|
||||
CPState.NO_EV_CONNECTED: "no_ev_connected",
|
||||
}
|
||||
|
|
|
@ -24,6 +24,17 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"sensor": {
|
||||
"cp_state": {
|
||||
"default": "mdi:ev-plug-type2"
|
||||
},
|
||||
"charge_current_limit_source": {
|
||||
"default": "mdi:arrow-collapse-up"
|
||||
},
|
||||
"uptime": {
|
||||
"default": "mdi:timer"
|
||||
}
|
||||
},
|
||||
"switch": {
|
||||
"force_single_phase": {
|
||||
"default": "mdi:power-cycle"
|
||||
|
|
|
@ -4,6 +4,7 @@ from __future__ import annotations
|
|||
|
||||
from collections.abc import Callable
|
||||
from dataclasses import dataclass
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
from peblar import PeblarUserConfiguration
|
||||
|
||||
|
@ -24,8 +25,13 @@ from homeassistant.core import HomeAssistant
|
|||
from homeassistant.helpers.device_registry import DeviceInfo
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||
from homeassistant.util.dt import utcnow
|
||||
|
||||
from .const import DOMAIN
|
||||
from .const import (
|
||||
DOMAIN,
|
||||
PEBLAR_CHARGE_LIMITER_TO_HOME_ASSISTANT,
|
||||
PEBLAR_CP_STATE_TO_HOME_ASSISTANT,
|
||||
)
|
||||
from .coordinator import PeblarConfigEntry, PeblarData, PeblarDataUpdateCoordinator
|
||||
|
||||
|
||||
|
@ -34,21 +40,37 @@ class PeblarSensorDescription(SensorEntityDescription):
|
|||
"""Describe a Peblar sensor."""
|
||||
|
||||
has_fn: Callable[[PeblarUserConfiguration], bool] = lambda _: True
|
||||
value_fn: Callable[[PeblarData], int | None]
|
||||
value_fn: Callable[[PeblarData], datetime | int | str | None]
|
||||
|
||||
|
||||
DESCRIPTIONS: tuple[PeblarSensorDescription, ...] = (
|
||||
PeblarSensorDescription(
|
||||
key="current",
|
||||
key="cp_state",
|
||||
translation_key="cp_state",
|
||||
device_class=SensorDeviceClass.ENUM,
|
||||
options=list(PEBLAR_CP_STATE_TO_HOME_ASSISTANT.values()),
|
||||
value_fn=lambda x: PEBLAR_CP_STATE_TO_HOME_ASSISTANT[x.ev.cp_state],
|
||||
),
|
||||
PeblarSensorDescription(
|
||||
key="charge_current_limit_source",
|
||||
translation_key="charge_current_limit_source",
|
||||
device_class=SensorDeviceClass.ENUM,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
options=list(PEBLAR_CHARGE_LIMITER_TO_HOME_ASSISTANT.values()),
|
||||
value_fn=lambda x: PEBLAR_CHARGE_LIMITER_TO_HOME_ASSISTANT[
|
||||
x.ev.charge_current_limit_source
|
||||
],
|
||||
),
|
||||
PeblarSensorDescription(
|
||||
key="current_total",
|
||||
device_class=SensorDeviceClass.CURRENT,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
entity_registry_enabled_default=False,
|
||||
has_fn=lambda x: x.connected_phases == 1,
|
||||
native_unit_of_measurement=UnitOfElectricCurrent.MILLIAMPERE,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
suggested_display_precision=1,
|
||||
suggested_unit_of_measurement=UnitOfElectricCurrent.AMPERE,
|
||||
value_fn=lambda x: x.meter.current_phase_1,
|
||||
value_fn=lambda x: x.meter.current_total,
|
||||
),
|
||||
PeblarSensorDescription(
|
||||
key="current_phase_1",
|
||||
|
@ -193,6 +215,16 @@ DESCRIPTIONS: tuple[PeblarSensorDescription, ...] = (
|
|||
state_class=SensorStateClass.MEASUREMENT,
|
||||
value_fn=lambda x: x.meter.voltage_phase_3,
|
||||
),
|
||||
PeblarSensorDescription(
|
||||
key="uptime",
|
||||
translation_key="uptime",
|
||||
device_class=SensorDeviceClass.TIMESTAMP,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
entity_registry_enabled_default=False,
|
||||
value_fn=lambda x: (
|
||||
utcnow().replace(microsecond=0) - timedelta(seconds=x.system.uptime)
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
|
@ -232,6 +264,6 @@ class PeblarSensorEntity(CoordinatorEntity[PeblarDataUpdateCoordinator], SensorE
|
|||
)
|
||||
|
||||
@property
|
||||
def native_value(self) -> int | None:
|
||||
def native_value(self) -> datetime | int | str | None:
|
||||
"""Return the state of the sensor."""
|
||||
return self.entity_description.value_fn(self.coordinator.data)
|
||||
|
|
|
@ -1,8 +1,16 @@
|
|||
{
|
||||
"config": {
|
||||
"abort": {
|
||||
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]",
|
||||
"no_serial_number": "The discovered Peblar device did not provide a serial number."
|
||||
},
|
||||
"error": {
|
||||
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
|
||||
"invalid_auth": "[%key:common::config_flow::error::invalid_auth%]",
|
||||
"unknown": "[%key:common::config_flow::error::unknown%]"
|
||||
},
|
||||
"step": {
|
||||
"user": {
|
||||
"description": "Set up your Peblar EV charger to integrate with Home Assistant.\n\nTo do so, you will need to get the IP address of your Peblar charger and the password you use to log into the Peblar device' web interface.\n\nHome Assistant will automatically configure your Peblar charger for use with Home Assistant.",
|
||||
"data": {
|
||||
"host": "[%key:common::config_flow::data::host%]",
|
||||
"password": "[%key:common::config_flow::data::password%]"
|
||||
|
@ -10,26 +18,18 @@
|
|||
"data_description": {
|
||||
"host": "The hostname or IP address of your Peblar charger on your home network.",
|
||||
"password": "The same password as you use to log in to the Peblar device' local web interface."
|
||||
}
|
||||
},
|
||||
"description": "Set up your Peblar EV charger to integrate with Home Assistant.\n\nTo do so, you will need to get the IP address of your Peblar charger and the password you use to log into the Peblar device' web interface.\n\nHome Assistant will automatically configure your Peblar charger for use with Home Assistant."
|
||||
},
|
||||
"zeroconf_confirm": {
|
||||
"description": "Set up your Peblar EV charger to integrate with Home Assistant.\n\nTo do so, you will need the password you use to log into the Peblar device' web interface.\n\nHome Assistant will automatically configure your Peblar charger for use with Home Assistant.",
|
||||
"data": {
|
||||
"password": "[%key:common::config_flow::data::password%]"
|
||||
},
|
||||
"data_description": {
|
||||
"password": "[%key:component::peblar::config::step::user::data_description::password%]"
|
||||
}
|
||||
},
|
||||
"description": "Set up your Peblar EV charger to integrate with Home Assistant.\n\nTo do so, you will need the password you use to log into the Peblar device' web interface.\n\nHome Assistant will automatically configure your Peblar charger for use with Home Assistant."
|
||||
}
|
||||
},
|
||||
"error": {
|
||||
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
|
||||
"invalid_auth": "[%key:common::config_flow::error::invalid_auth%]",
|
||||
"unknown": "[%key:common::config_flow::error::unknown%]"
|
||||
},
|
||||
"abort": {
|
||||
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]",
|
||||
"no_serial_number": "The discovered Peblar device did not provide a serial number."
|
||||
}
|
||||
},
|
||||
"entity": {
|
||||
|
@ -59,6 +59,38 @@
|
|||
}
|
||||
},
|
||||
"sensor": {
|
||||
"charge_current_limit_source": {
|
||||
"name": "Limit source",
|
||||
"state": {
|
||||
"charging_cable": "Charging cable",
|
||||
"current_limiter": "Current limiter",
|
||||
"dynamic_load_balancing": "Dynamic load balancing",
|
||||
"external_power_limit": "External power limit",
|
||||
"group_load_balancing": "Group load balancing",
|
||||
"hardware_limitation": "Hardware limitation",
|
||||
"high_temperature": "High temperature",
|
||||
"household_power_limit": "Household power limit",
|
||||
"installation_limit": "Installation limit",
|
||||
"local_modbus_api": "Modbus API",
|
||||
"local_rest_api": "REST API",
|
||||
"ocpp_smart_charging": "OCPP smart charging",
|
||||
"overcurrent_protection": "Overcurrent protection",
|
||||
"phase_imbalance": "Phase imbalance",
|
||||
"power_factor": "Power factor",
|
||||
"solar_charging": "Solar charging"
|
||||
}
|
||||
},
|
||||
"cp_state": {
|
||||
"name": "State",
|
||||
"state": {
|
||||
"charging": "Charging",
|
||||
"error": "Error",
|
||||
"fault": "Fault",
|
||||
"invalid": "Invalid",
|
||||
"no_ev_connected": "No EV connected",
|
||||
"suspended": "Suspended"
|
||||
}
|
||||
},
|
||||
"current_phase_1": {
|
||||
"name": "Current phase 1"
|
||||
},
|
||||
|
@ -83,6 +115,9 @@
|
|||
"power_phase_3": {
|
||||
"name": "Power phase 3"
|
||||
},
|
||||
"uptime": {
|
||||
"name": "Uptime"
|
||||
},
|
||||
"voltage_phase_1": {
|
||||
"name": "Voltage phase 1"
|
||||
},
|
||||
|
|
|
@ -1,4 +1,61 @@
|
|||
# serializer version: 1
|
||||
# name: test_entities[sensor][sensor.peblar_ev_charger_current-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': set({
|
||||
}),
|
||||
'area_id': None,
|
||||
'capabilities': dict({
|
||||
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
|
||||
}),
|
||||
'config_entry_id': <ANY>,
|
||||
'device_class': None,
|
||||
'device_id': <ANY>,
|
||||
'disabled_by': None,
|
||||
'domain': 'sensor',
|
||||
'entity_category': <EntityCategory.DIAGNOSTIC: 'diagnostic'>,
|
||||
'entity_id': 'sensor.peblar_ev_charger_current',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'options': dict({
|
||||
'sensor': dict({
|
||||
'suggested_display_precision': 1,
|
||||
}),
|
||||
'sensor.private': dict({
|
||||
'suggested_unit_of_measurement': <UnitOfElectricCurrent.AMPERE: 'A'>,
|
||||
}),
|
||||
}),
|
||||
'original_device_class': <SensorDeviceClass.CURRENT: 'current'>,
|
||||
'original_icon': None,
|
||||
'original_name': 'Current',
|
||||
'platform': 'peblar',
|
||||
'previous_unique_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': None,
|
||||
'unique_id': '23-45-A4O-MOF_current_total',
|
||||
'unit_of_measurement': <UnitOfElectricCurrent.AMPERE: 'A'>,
|
||||
})
|
||||
# ---
|
||||
# name: test_entities[sensor][sensor.peblar_ev_charger_current-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'device_class': 'current',
|
||||
'friendly_name': 'Peblar EV Charger Current',
|
||||
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
|
||||
'unit_of_measurement': <UnitOfElectricCurrent.AMPERE: 'A'>,
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'sensor.peblar_ev_charger_current',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': '14.242',
|
||||
})
|
||||
# ---
|
||||
# name: test_entities[sensor][sensor.peblar_ev_charger_current_phase_1-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': set({
|
||||
|
@ -227,6 +284,92 @@
|
|||
'state': '880.703',
|
||||
})
|
||||
# ---
|
||||
# name: test_entities[sensor][sensor.peblar_ev_charger_limit_source-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': set({
|
||||
}),
|
||||
'area_id': None,
|
||||
'capabilities': dict({
|
||||
'options': list([
|
||||
'charging_cable',
|
||||
'current_limiter',
|
||||
'dynamic_load_balancing',
|
||||
'external_power_limit',
|
||||
'group_load_balancing',
|
||||
'hardware_limitation',
|
||||
'high_temperature',
|
||||
'household_power_limit',
|
||||
'installation_limit',
|
||||
'local_modbus_api',
|
||||
'local_rest_api',
|
||||
'local_scheduled',
|
||||
'ocpp_smart_charging',
|
||||
'overcurrent_protection',
|
||||
'phase_imbalance',
|
||||
'power_factor',
|
||||
'solar_charging',
|
||||
]),
|
||||
}),
|
||||
'config_entry_id': <ANY>,
|
||||
'device_class': None,
|
||||
'device_id': <ANY>,
|
||||
'disabled_by': None,
|
||||
'domain': 'sensor',
|
||||
'entity_category': <EntityCategory.DIAGNOSTIC: 'diagnostic'>,
|
||||
'entity_id': 'sensor.peblar_ev_charger_limit_source',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'options': dict({
|
||||
}),
|
||||
'original_device_class': <SensorDeviceClass.ENUM: 'enum'>,
|
||||
'original_icon': None,
|
||||
'original_name': 'Limit source',
|
||||
'platform': 'peblar',
|
||||
'previous_unique_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': 'charge_current_limit_source',
|
||||
'unique_id': '23-45-A4O-MOF_charge_current_limit_source',
|
||||
'unit_of_measurement': None,
|
||||
})
|
||||
# ---
|
||||
# name: test_entities[sensor][sensor.peblar_ev_charger_limit_source-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'device_class': 'enum',
|
||||
'friendly_name': 'Peblar EV Charger Limit source',
|
||||
'options': list([
|
||||
'charging_cable',
|
||||
'current_limiter',
|
||||
'dynamic_load_balancing',
|
||||
'external_power_limit',
|
||||
'group_load_balancing',
|
||||
'hardware_limitation',
|
||||
'high_temperature',
|
||||
'household_power_limit',
|
||||
'installation_limit',
|
||||
'local_modbus_api',
|
||||
'local_rest_api',
|
||||
'local_scheduled',
|
||||
'ocpp_smart_charging',
|
||||
'overcurrent_protection',
|
||||
'phase_imbalance',
|
||||
'power_factor',
|
||||
'solar_charging',
|
||||
]),
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'sensor.peblar_ev_charger_limit_source',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': 'current_limiter',
|
||||
})
|
||||
# ---
|
||||
# name: test_entities[sensor][sensor.peblar_ev_charger_power-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': set({
|
||||
|
@ -488,6 +631,119 @@
|
|||
'state': '0.381',
|
||||
})
|
||||
# ---
|
||||
# name: test_entities[sensor][sensor.peblar_ev_charger_state-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': set({
|
||||
}),
|
||||
'area_id': None,
|
||||
'capabilities': dict({
|
||||
'options': list([
|
||||
'suspended',
|
||||
'charging',
|
||||
'charging',
|
||||
'error',
|
||||
'fault',
|
||||
'invalid',
|
||||
'no_ev_connected',
|
||||
]),
|
||||
}),
|
||||
'config_entry_id': <ANY>,
|
||||
'device_class': None,
|
||||
'device_id': <ANY>,
|
||||
'disabled_by': None,
|
||||
'domain': 'sensor',
|
||||
'entity_category': None,
|
||||
'entity_id': 'sensor.peblar_ev_charger_state',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'options': dict({
|
||||
}),
|
||||
'original_device_class': <SensorDeviceClass.ENUM: 'enum'>,
|
||||
'original_icon': None,
|
||||
'original_name': 'State',
|
||||
'platform': 'peblar',
|
||||
'previous_unique_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': 'cp_state',
|
||||
'unique_id': '23-45-A4O-MOF_cp_state',
|
||||
'unit_of_measurement': None,
|
||||
})
|
||||
# ---
|
||||
# name: test_entities[sensor][sensor.peblar_ev_charger_state-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'device_class': 'enum',
|
||||
'friendly_name': 'Peblar EV Charger State',
|
||||
'options': list([
|
||||
'suspended',
|
||||
'charging',
|
||||
'charging',
|
||||
'error',
|
||||
'fault',
|
||||
'invalid',
|
||||
'no_ev_connected',
|
||||
]),
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'sensor.peblar_ev_charger_state',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': 'charging',
|
||||
})
|
||||
# ---
|
||||
# name: test_entities[sensor][sensor.peblar_ev_charger_uptime-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': set({
|
||||
}),
|
||||
'area_id': None,
|
||||
'capabilities': None,
|
||||
'config_entry_id': <ANY>,
|
||||
'device_class': None,
|
||||
'device_id': <ANY>,
|
||||
'disabled_by': None,
|
||||
'domain': 'sensor',
|
||||
'entity_category': <EntityCategory.DIAGNOSTIC: 'diagnostic'>,
|
||||
'entity_id': 'sensor.peblar_ev_charger_uptime',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'options': dict({
|
||||
}),
|
||||
'original_device_class': <SensorDeviceClass.TIMESTAMP: 'timestamp'>,
|
||||
'original_icon': None,
|
||||
'original_name': 'Uptime',
|
||||
'platform': 'peblar',
|
||||
'previous_unique_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': 'uptime',
|
||||
'unique_id': '23-45-A4O-MOF_uptime',
|
||||
'unit_of_measurement': None,
|
||||
})
|
||||
# ---
|
||||
# name: test_entities[sensor][sensor.peblar_ev_charger_uptime-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'device_class': 'timestamp',
|
||||
'friendly_name': 'Peblar EV Charger Uptime',
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'sensor.peblar_ev_charger_uptime',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': '2024-12-18T04:16:46+00:00',
|
||||
})
|
||||
# ---
|
||||
# name: test_entities[sensor][sensor.peblar_ev_charger_voltage_phase_1-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': set({
|
||||
|
|
|
@ -11,6 +11,7 @@ from homeassistant.helpers import device_registry as dr, entity_registry as er
|
|||
from tests.common import MockConfigEntry, snapshot_platform
|
||||
|
||||
|
||||
@pytest.mark.freeze_time("2024-12-21 21:45:00")
|
||||
@pytest.mark.parametrize("init_integration", [Platform.SENSOR], indirect=True)
|
||||
@pytest.mark.usefixtures("entity_registry_enabled_by_default", "init_integration")
|
||||
async def test_entities(
|
||||
|
|
Loading…
Reference in New Issue