mirror of
https://github.com/home-assistant/core.git
synced 2026-08-15 01:32:55 +01:00
346 lines
11 KiB
Python
346 lines
11 KiB
Python
"""Home Assistant integration module for Greencell EVSE sensor entities over MQTT."""
|
|
|
|
from collections.abc import Callable
|
|
from dataclasses import dataclass
|
|
import logging
|
|
from typing import Any, override
|
|
|
|
from greencell_client.access import GreencellAccess
|
|
from greencell_client.elec_data import ElecData3Phase, ElecDataSinglePhase
|
|
from greencell_client.mqtt_parser import MqttParser
|
|
from greencell_client.utils import GreencellUtils
|
|
|
|
from homeassistant.components import mqtt
|
|
from homeassistant.components.mqtt import ReceiveMessage
|
|
from homeassistant.components.sensor import (
|
|
SensorDeviceClass,
|
|
SensorEntity,
|
|
SensorEntityDescription,
|
|
SensorStateClass,
|
|
)
|
|
from homeassistant.const import (
|
|
UnitOfElectricCurrent,
|
|
UnitOfElectricPotential,
|
|
UnitOfPower,
|
|
)
|
|
from homeassistant.core import HomeAssistant, callback
|
|
from homeassistant.exceptions import ConfigEntryNotReady, HomeAssistantError
|
|
from homeassistant.helpers.device_registry import DeviceInfo
|
|
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
|
from homeassistant.helpers.typing import StateType
|
|
|
|
from .const import (
|
|
CONF_SERIAL_NUMBER,
|
|
DOMAIN,
|
|
GREENCELL_HABU_DEN,
|
|
GREENCELL_OTHER_DEVICE,
|
|
MANUFACTURER,
|
|
)
|
|
from .models import GreencellConfigEntry
|
|
|
|
_LOGGER = logging.getLogger(__name__)
|
|
|
|
PARALLEL_UPDATES = 0
|
|
|
|
|
|
@dataclass(frozen=True, kw_only=True)
|
|
class GreencellSensorDescription(SensorEntityDescription):
|
|
"""Describe a Greencell sensor."""
|
|
|
|
value_fn: Callable[[Any], StateType]
|
|
|
|
|
|
SENSOR_DESCRIPTIONS = (
|
|
GreencellSensorDescription(
|
|
key="current_l1",
|
|
translation_key="current_l1",
|
|
device_class=SensorDeviceClass.CURRENT,
|
|
state_class=SensorStateClass.MEASUREMENT,
|
|
native_unit_of_measurement=UnitOfElectricCurrent.AMPERE,
|
|
suggested_display_precision=3,
|
|
value_fn=lambda data: data / 1000,
|
|
),
|
|
GreencellSensorDescription(
|
|
key="current_l2",
|
|
translation_key="current_l2",
|
|
device_class=SensorDeviceClass.CURRENT,
|
|
state_class=SensorStateClass.MEASUREMENT,
|
|
native_unit_of_measurement=UnitOfElectricCurrent.AMPERE,
|
|
suggested_display_precision=3,
|
|
value_fn=lambda data: data / 1000,
|
|
),
|
|
GreencellSensorDescription(
|
|
key="current_l3",
|
|
translation_key="current_l3",
|
|
device_class=SensorDeviceClass.CURRENT,
|
|
state_class=SensorStateClass.MEASUREMENT,
|
|
native_unit_of_measurement=UnitOfElectricCurrent.AMPERE,
|
|
suggested_display_precision=3,
|
|
value_fn=lambda data: data / 1000,
|
|
),
|
|
GreencellSensorDescription(
|
|
key="voltage_l1",
|
|
translation_key="voltage_l1",
|
|
device_class=SensorDeviceClass.VOLTAGE,
|
|
state_class=SensorStateClass.MEASUREMENT,
|
|
native_unit_of_measurement=UnitOfElectricPotential.VOLT,
|
|
suggested_display_precision=2,
|
|
value_fn=lambda data: data,
|
|
),
|
|
GreencellSensorDescription(
|
|
key="voltage_l2",
|
|
translation_key="voltage_l2",
|
|
device_class=SensorDeviceClass.VOLTAGE,
|
|
state_class=SensorStateClass.MEASUREMENT,
|
|
native_unit_of_measurement=UnitOfElectricPotential.VOLT,
|
|
suggested_display_precision=2,
|
|
value_fn=lambda data: data,
|
|
),
|
|
GreencellSensorDescription(
|
|
key="voltage_l3",
|
|
translation_key="voltage_l3",
|
|
device_class=SensorDeviceClass.VOLTAGE,
|
|
state_class=SensorStateClass.MEASUREMENT,
|
|
native_unit_of_measurement=UnitOfElectricPotential.VOLT,
|
|
suggested_display_precision=2,
|
|
value_fn=lambda data: data,
|
|
),
|
|
GreencellSensorDescription(
|
|
key="power",
|
|
translation_key="power",
|
|
device_class=SensorDeviceClass.POWER,
|
|
state_class=SensorStateClass.MEASUREMENT,
|
|
native_unit_of_measurement=UnitOfPower.WATT,
|
|
suggested_display_precision=1,
|
|
value_fn=lambda data: data,
|
|
),
|
|
GreencellSensorDescription(
|
|
key="status",
|
|
translation_key="status",
|
|
device_class=SensorDeviceClass.ENUM,
|
|
options=[
|
|
"idle",
|
|
"connected",
|
|
"waiting_for_car",
|
|
"charging",
|
|
"finished",
|
|
"error_car",
|
|
"error_evse",
|
|
],
|
|
value_fn=lambda data: str(data).lower() if isinstance(data, str) else None,
|
|
),
|
|
)
|
|
|
|
|
|
# --- Config Flow Setup ---
|
|
async def async_setup_entry(
|
|
hass: HomeAssistant,
|
|
entry: GreencellConfigEntry,
|
|
async_add_entities: AddConfigEntryEntitiesCallback,
|
|
) -> None:
|
|
"""Set up the Greencell EVSE sensors from a config entry."""
|
|
|
|
serial_number: str = entry.data[CONF_SERIAL_NUMBER]
|
|
|
|
mqtt_topic_current = f"/greencell/evse/{serial_number}/current"
|
|
mqtt_topic_voltage = f"/greencell/evse/{serial_number}/voltage"
|
|
mqtt_topic_power = f"/greencell/evse/{serial_number}/power"
|
|
mqtt_topic_status = f"/greencell/evse/{serial_number}/status"
|
|
mqtt_topic_device_state = f"/greencell/evse/{serial_number}/device_state"
|
|
|
|
desc_map = {desc.key: desc for desc in SENSOR_DESCRIPTIONS}
|
|
|
|
runtime = entry.runtime_data
|
|
access = runtime.access
|
|
current_data_obj = runtime.current_data
|
|
voltage_data_obj = runtime.voltage_data
|
|
power_data_obj = runtime.power_data
|
|
state_data_obj = runtime.state_data
|
|
|
|
data_mapping = {
|
|
"current": current_data_obj,
|
|
"voltage": voltage_data_obj,
|
|
"power": power_data_obj,
|
|
"status": state_data_obj,
|
|
}
|
|
|
|
sensors: list[HabuSensor] = [
|
|
Habu3PhaseSensor(
|
|
sensor_data=data_mapping[description.key.split("_")[0]],
|
|
phase=description.key.split("_")[-1],
|
|
sensor_type=description.key,
|
|
serial_number=serial_number,
|
|
access=access,
|
|
description=description,
|
|
)
|
|
for description in SENSOR_DESCRIPTIONS
|
|
if description.key.startswith(("current_l", "voltage_l"))
|
|
]
|
|
|
|
sensors.extend(
|
|
HabuSingleSensor(
|
|
sensor_data=data_mapping[key],
|
|
serial_number=serial_number,
|
|
sensor_type=key,
|
|
access=access,
|
|
description=desc_map[key],
|
|
)
|
|
for key in ("power", "status")
|
|
)
|
|
|
|
@callback
|
|
def current_message_received(msg: ReceiveMessage) -> None:
|
|
"""Handle the current message."""
|
|
MqttParser.parse_3phase_msg(msg.payload, current_data_obj)
|
|
|
|
@callback
|
|
def voltage_message_received(msg: ReceiveMessage) -> None:
|
|
"""Handle the voltage message."""
|
|
MqttParser.parse_3phase_msg(msg.payload, voltage_data_obj)
|
|
|
|
@callback
|
|
def power_message_received(msg: ReceiveMessage) -> None:
|
|
"""Handle the power message."""
|
|
MqttParser.parse_single_phase_msg(msg.payload, "momentary", power_data_obj)
|
|
|
|
@callback
|
|
def status_message_received(msg: ReceiveMessage) -> None:
|
|
"""Handle the status message. If the device is unavailable, disable the entity."""
|
|
|
|
str_payload = (
|
|
msg.payload.decode("utf-8", errors="ignore")
|
|
if isinstance(msg.payload, (bytes, bytearray))
|
|
else str(msg.payload)
|
|
)
|
|
|
|
if "UNAVAILABLE" in str_payload or "OFFLINE" in str_payload:
|
|
access.update("UNAVAILABLE")
|
|
else:
|
|
MqttParser.parse_single_phase_msg(msg.payload, "state", state_data_obj)
|
|
|
|
@callback
|
|
def device_state_message_received(msg: ReceiveMessage) -> None:
|
|
"""Handle the device state message. If device was unavailable, enable the entity."""
|
|
access.on_msg(msg.payload)
|
|
|
|
try:
|
|
for topic, handler in (
|
|
(mqtt_topic_current, current_message_received),
|
|
(mqtt_topic_voltage, voltage_message_received),
|
|
(mqtt_topic_power, power_message_received),
|
|
(mqtt_topic_status, status_message_received),
|
|
(mqtt_topic_device_state, device_state_message_received),
|
|
):
|
|
unsub = await mqtt.async_subscribe(hass, topic, handler)
|
|
if unsub is not None:
|
|
entry.async_on_unload(unsub)
|
|
except HomeAssistantError as err:
|
|
raise ConfigEntryNotReady(f"MQTT is unavailable: {err}") from err
|
|
|
|
async_add_entities(sensors)
|
|
|
|
|
|
class HabuSensor(SensorEntity):
|
|
"""Abstract base class for Habu sensors integration."""
|
|
|
|
entity_description: GreencellSensorDescription
|
|
_attr_has_entity_name = True
|
|
_remove_listener: Callable[[], None] | None = None
|
|
|
|
def __init__(
|
|
self,
|
|
sensor_type: str,
|
|
serial_number: str,
|
|
access: GreencellAccess,
|
|
description: GreencellSensorDescription,
|
|
) -> None:
|
|
"""Initialize the sensor entity."""
|
|
self._sensor_type = sensor_type
|
|
self._serial_number = serial_number
|
|
self._access = access
|
|
self.entity_description = description
|
|
|
|
self._attr_unique_id = f"{serial_number}_{description.key}"
|
|
|
|
if GreencellUtils.device_is_habu_den(self._serial_number):
|
|
device_name = GREENCELL_HABU_DEN
|
|
else:
|
|
device_name = GREENCELL_OTHER_DEVICE
|
|
|
|
self._attr_device_info = DeviceInfo(
|
|
identifiers={(DOMAIN, serial_number)},
|
|
name=f"{device_name} {serial_number}",
|
|
manufacturer=MANUFACTURER,
|
|
model=device_name,
|
|
serial_number=serial_number,
|
|
)
|
|
|
|
@property
|
|
@override
|
|
def available(self) -> bool:
|
|
"""Return True if the entity is available."""
|
|
return not self._access.is_disabled()
|
|
|
|
@override
|
|
async def async_added_to_hass(self) -> None:
|
|
"""Register the entity with Home Assistant."""
|
|
unsub = self._access.register_listener(self._schedule_update)
|
|
if unsub is not None:
|
|
self.async_on_remove(unsub)
|
|
|
|
def _schedule_update(self) -> None:
|
|
"""Schedule an update for the entity."""
|
|
self.async_schedule_update_ha_state()
|
|
|
|
|
|
class Habu3PhaseSensor(HabuSensor):
|
|
"""Abstract class for 3-phase sensors (e.g. current, voltage)."""
|
|
|
|
def __init__(
|
|
self,
|
|
sensor_data: ElecData3Phase,
|
|
phase: str,
|
|
sensor_type: str,
|
|
serial_number: str,
|
|
access: GreencellAccess,
|
|
description: GreencellSensorDescription,
|
|
) -> None:
|
|
"""Initialize the 3-phase sensor."""
|
|
super().__init__(sensor_type, serial_number, access, description)
|
|
self._sensor_data = sensor_data
|
|
self._phase = phase
|
|
|
|
@property
|
|
@override
|
|
def native_value(self) -> StateType:
|
|
"""Return the state of the sensor."""
|
|
raw_value = self._sensor_data.get_value(self._phase)
|
|
if raw_value is None:
|
|
return None
|
|
return self.entity_description.value_fn(raw_value)
|
|
|
|
|
|
class HabuSingleSensor(HabuSensor):
|
|
"""Example class for sensors that return a single value."""
|
|
|
|
def __init__(
|
|
self,
|
|
sensor_data: ElecDataSinglePhase,
|
|
serial_number: str,
|
|
sensor_type: str,
|
|
access: GreencellAccess,
|
|
description: GreencellSensorDescription,
|
|
) -> None:
|
|
"""Initialize the single-value sensor."""
|
|
super().__init__(sensor_type, serial_number, access, description)
|
|
self._value = sensor_data
|
|
|
|
@property
|
|
@override
|
|
def native_value(self) -> StateType:
|
|
"""Return the state of the sensor."""
|
|
raw_value = self._value.data
|
|
if raw_value is None:
|
|
return None
|
|
return self.entity_description.value_fn(raw_value)
|