Project skeleton
This commit is contained in:
79
custom_components/pv_microinverter/__init__.py
Normal file
79
custom_components/pv_microinverter/__init__.py
Normal file
@@ -0,0 +1,79 @@
|
||||
"""The PV Microinverter integration."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import ConfigEntryNotReady
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
|
||||
from .api import PVMicroinverterApiClient
|
||||
from .api import PVMicroinverterApiClientError as PVMicroinverterApiClientError
|
||||
from .const import (
|
||||
CONF_STATION_ID,
|
||||
CONF_UPDATE_INTERVAL,
|
||||
DEFAULT_UPDATE_INTERVAL,
|
||||
DOMAIN,
|
||||
)
|
||||
from .coordinator import PVMicroinverterDataUpdateCoordinator
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
# List of platforms to support
|
||||
PLATFORMS: list[Platform] = [Platform.SENSOR]
|
||||
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
"""Set up PV Microinverter from a config entry."""
|
||||
hass.data.setdefault(DOMAIN, {})
|
||||
|
||||
# Get configuration from the config entry
|
||||
station_id = entry.data[CONF_STATION_ID]
|
||||
update_interval = entry.data.get(CONF_UPDATE_INTERVAL, DEFAULT_UPDATE_INTERVAL)
|
||||
|
||||
# Create API client
|
||||
session = async_get_clientsession(hass)
|
||||
api_client = PVMicroinverterApiClient(
|
||||
session=session,
|
||||
station_id=station_id,
|
||||
)
|
||||
|
||||
# Initialize coordinator
|
||||
coordinator = PVMicroinverterDataUpdateCoordinator(
|
||||
hass=hass,
|
||||
api_client=api_client,
|
||||
update_interval=update_interval,
|
||||
)
|
||||
|
||||
# Fetch initial data
|
||||
try:
|
||||
await coordinator.async_config_entry_first_refresh()
|
||||
except ConfigEntryNotReady as error:
|
||||
raise ConfigEntryNotReady(f"Failed to load initial data: {error}") from error
|
||||
|
||||
# Store coordinator in hass.data
|
||||
hass.data[DOMAIN][entry.entry_id] = coordinator
|
||||
|
||||
# Set up all platforms
|
||||
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
||||
|
||||
# Add update listener for config entry changes
|
||||
entry.async_on_unload(entry.add_update_listener(async_update_options))
|
||||
|
||||
return True
|
||||
|
||||
|
||||
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
"""Unload a config entry."""
|
||||
if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS):
|
||||
hass.data[DOMAIN].pop(entry.entry_id)
|
||||
|
||||
return unload_ok
|
||||
|
||||
|
||||
async def async_update_options(hass: HomeAssistant, entry: ConfigEntry) -> None:
|
||||
"""Update options."""
|
||||
await hass.config_entries.async_reload(entry.entry_id)
|
||||
163
custom_components/pv_microinverter/api.py
Normal file
163
custom_components/pv_microinverter/api.py
Normal file
@@ -0,0 +1,163 @@
|
||||
"""API client for PV Microinverter."""
|
||||
|
||||
import logging
|
||||
from dataclasses import dataclass
|
||||
from datetime import datetime
|
||||
from enum import StrEnum
|
||||
from typing import Any
|
||||
|
||||
import aiohttp
|
||||
|
||||
from pv_microinverter.units import WATT, Dimension
|
||||
|
||||
from .const import PVMicroinverterData
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class ApiEndpoints(StrEnum):
|
||||
GET_STATION_INFO = "GetStationInfo"
|
||||
|
||||
|
||||
@dataclass
|
||||
class StationInfoData:
|
||||
UnitCapacity: str
|
||||
UnitEToday: str
|
||||
UnitEMonth: str
|
||||
UnitEYear: str
|
||||
UnitETotal: str
|
||||
Power: float
|
||||
PowerStr: str
|
||||
Capacity: float
|
||||
LoadPower: str
|
||||
GridPower: str
|
||||
StrCO2: str
|
||||
StrTrees: str
|
||||
StrIncome: str
|
||||
PwImg: str
|
||||
StationName: str
|
||||
InvModel1: str
|
||||
InvModel2: str | None
|
||||
Lat: str
|
||||
Lng: str
|
||||
TimeZone: str
|
||||
StrPeakPower: str
|
||||
Installer: str | None
|
||||
CreateTime: datetime
|
||||
CreateYear: int
|
||||
CreateMonth: int
|
||||
Etoday: float
|
||||
InvTotal: int
|
||||
|
||||
|
||||
@dataclass
|
||||
class StationInfoResponse:
|
||||
Status: int
|
||||
Result: Any
|
||||
Data: StationInfoData
|
||||
|
||||
|
||||
class PVMicroinverterApiClientError(Exception):
|
||||
"""Exception to indicate an error with the API client."""
|
||||
|
||||
|
||||
class PVMicroinverterApiClient:
|
||||
"""API client for PV Microinverter."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
session: aiohttp.ClientSession,
|
||||
station_id: str,
|
||||
base_url: str = "https://www.envertecportal.com/ApiStations",
|
||||
) -> None:
|
||||
"""Initialize the Envertech API client.
|
||||
|
||||
Args:
|
||||
session: The aiohttp client session
|
||||
station_id: The station identifier
|
||||
base_url: The base URL for the API
|
||||
"""
|
||||
self._session = session
|
||||
self._station_id = station_id
|
||||
self._base_url = base_url
|
||||
|
||||
async def async_get_data(self) -> PVMicroinverterData:
|
||||
"""Get data from the API.
|
||||
|
||||
Returns:
|
||||
PVMicroinverterData: The data from the API
|
||||
|
||||
Raises:
|
||||
PVMicroinverterApiClientError: If the API request fails
|
||||
"""
|
||||
try:
|
||||
# Make the request to the API
|
||||
response = await self._session.post(
|
||||
f"{self._base_url}/{ApiEndpoints.GET_STATION_INFO}",
|
||||
json={"stationId": self._station_id},
|
||||
headers={
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
)
|
||||
|
||||
response.raise_for_status()
|
||||
data = await response.json()
|
||||
|
||||
# Process the response
|
||||
return self._process_data(data)
|
||||
|
||||
except aiohttp.ClientError as error:
|
||||
_LOGGER.error("Error fetching data: %s", error)
|
||||
raise PVMicroinverterApiClientError(
|
||||
"Error fetching data from API"
|
||||
) from error
|
||||
except Exception as error:
|
||||
_LOGGER.exception("Unexpected error: %s", error)
|
||||
raise PVMicroinverterApiClientError("Unexpected error occurred") from error
|
||||
|
||||
def _process_data(self, data: dict[str, Any]) -> PVMicroinverterData:
|
||||
"""Process the API response data.
|
||||
|
||||
Args:
|
||||
data: The data from the API
|
||||
|
||||
Returns:
|
||||
PVMicroinverterData: The processed data
|
||||
"""
|
||||
|
||||
station_data = StationInfoData(**data.get("Data", {}))
|
||||
station_info = StationInfoResponse(
|
||||
Status=data.get("Status"),
|
||||
Result=data.get("Result"),
|
||||
Data=station_data,
|
||||
)
|
||||
|
||||
if station_info.Status != "0":
|
||||
raise PVMicroinverterApiClientError(f"API error: {station_info.Result}")
|
||||
|
||||
return PVMicroinverterData(
|
||||
current_power=Dimension(station_data.Power, WATT),
|
||||
today_energy=Dimension.parse(station_data.UnitEToday).to_base_unit(),
|
||||
lifetime_energy=Dimension.parse(station_data.UnitETotal).to_base_unit(),
|
||||
last_updated=datetime.now().isoformat(),
|
||||
)
|
||||
|
||||
async def async_check_connection(self) -> bool:
|
||||
"""Test the API connection to verify credentials.
|
||||
|
||||
Returns:
|
||||
bool: True if connection is successful, False otherwise
|
||||
"""
|
||||
try:
|
||||
response = await self._session.post(
|
||||
f"{self._base_url}/{ApiEndpoints.GET_STATION_INFO}",
|
||||
headers={
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
json={"stationId": self._station_id},
|
||||
)
|
||||
response.raise_for_status()
|
||||
return True
|
||||
except Exception as error:
|
||||
_LOGGER.error("Connection test failed: %s", error)
|
||||
return False
|
||||
137
custom_components/pv_microinverter/config_flow.py
Normal file
137
custom_components/pv_microinverter/config_flow.py
Normal file
@@ -0,0 +1,137 @@
|
||||
"""Config flow for PV Microinverter integration."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
import voluptuous as vol
|
||||
from homeassistant import config_entries
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.data_entry_flow import FlowResult
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
|
||||
from .api import PVMicroinverterApiClient
|
||||
from .const import (
|
||||
CONF_STATION_ID,
|
||||
CONF_UPDATE_INTERVAL,
|
||||
DEFAULT_UPDATE_INTERVAL,
|
||||
DOMAIN,
|
||||
)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
STEP_USER_DATA_SCHEMA = vol.Schema({
|
||||
vol.Required(CONF_STATION_ID): str,
|
||||
vol.Optional(CONF_UPDATE_INTERVAL, default=DEFAULT_UPDATE_INTERVAL): int,
|
||||
})
|
||||
|
||||
|
||||
async def validate_input(hass: HomeAssistant, data: dict[str, Any]) -> dict[str, Any]:
|
||||
"""Validate the user input allows us to connect to the API.
|
||||
|
||||
Args:
|
||||
hass: The Home Assistant instance
|
||||
data: The user input
|
||||
|
||||
Returns:
|
||||
Dict[str, Any]: The validated data
|
||||
|
||||
Raises:
|
||||
CannotConnect: If the API connection cannot be established
|
||||
InvalidAuth: If the API key is invalid
|
||||
"""
|
||||
session = async_get_clientsession(hass)
|
||||
|
||||
api_client = PVMicroinverterApiClient(
|
||||
session=session,
|
||||
station_id=data[CONF_STATION_ID],
|
||||
)
|
||||
|
||||
# Test connection and authentication
|
||||
connection_successful = await api_client.async_check_connection()
|
||||
|
||||
if not connection_successful:
|
||||
raise CannotConnect
|
||||
|
||||
# Return validated data
|
||||
return {
|
||||
CONF_STATION_ID: data[CONF_STATION_ID],
|
||||
CONF_UPDATE_INTERVAL: data[CONF_UPDATE_INTERVAL],
|
||||
}
|
||||
|
||||
|
||||
class PVMicroinverterConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
"""Handle a config flow for PV Microinverter."""
|
||||
|
||||
VERSION = 1
|
||||
|
||||
async def async_step_user(self, user_input: dict[str, Any] = None) -> FlowResult:
|
||||
"""Handle the initial step."""
|
||||
errors: dict[str, str] = {}
|
||||
|
||||
if user_input is not None:
|
||||
try:
|
||||
info = await validate_input(self.hass, user_input)
|
||||
|
||||
# Check if we already have an entry for this system_id
|
||||
await self.async_set_unique_id(user_input[CONF_STATION_ID])
|
||||
self._abort_if_unique_id_configured()
|
||||
|
||||
return self.async_create_entry(
|
||||
title=f"PV Microinverter {user_input[CONF_STATION_ID]}",
|
||||
data=info,
|
||||
)
|
||||
except CannotConnect:
|
||||
errors["base"] = "cannot_connect"
|
||||
except InvalidAuth:
|
||||
errors["base"] = "invalid_auth"
|
||||
except Exception: # pylint: disable=broad-except
|
||||
_LOGGER.exception("Unexpected exception")
|
||||
errors["base"] = "unknown"
|
||||
|
||||
return self.async_show_form(
|
||||
step_id="user", data_schema=STEP_USER_DATA_SCHEMA, errors=errors
|
||||
)
|
||||
|
||||
async def async_step_reauth(self, user_input: dict[str, Any] = None) -> FlowResult:
|
||||
"""Handle re-authentication."""
|
||||
errors: dict[str, str] = {}
|
||||
|
||||
if user_input is not None:
|
||||
try:
|
||||
info = await validate_input(self.hass, user_input)
|
||||
|
||||
# Get existing entry
|
||||
existing_entry = await self.async_set_unique_id(
|
||||
user_input[CONF_STATION_ID]
|
||||
)
|
||||
|
||||
if existing_entry:
|
||||
self.hass.config_entries.async_update_entry(
|
||||
existing_entry, data=info
|
||||
)
|
||||
await self.hass.config_entries.async_reload(existing_entry.entry_id)
|
||||
return self.async_abort(reason="reauth_successful")
|
||||
|
||||
return self.async_abort(reason="reauth_failed_existing_entry_not_found")
|
||||
except CannotConnect:
|
||||
errors["base"] = "cannot_connect"
|
||||
except InvalidAuth:
|
||||
errors["base"] = "invalid_auth"
|
||||
except Exception: # pylint: disable=broad-except
|
||||
_LOGGER.exception("Unexpected exception")
|
||||
errors["base"] = "unknown"
|
||||
|
||||
return self.async_show_form(
|
||||
step_id="reauth", data_schema=STEP_USER_DATA_SCHEMA, errors=errors
|
||||
)
|
||||
|
||||
|
||||
class CannotConnect(HomeAssistantError):
|
||||
"""Error to indicate we cannot connect."""
|
||||
|
||||
|
||||
class InvalidAuth(HomeAssistantError):
|
||||
"""Error to indicate there is invalid auth."""
|
||||
52
custom_components/pv_microinverter/const.py
Normal file
52
custom_components/pv_microinverter/const.py
Normal file
@@ -0,0 +1,52 @@
|
||||
"""Constants for the PV Microinverter integration."""
|
||||
|
||||
from dataclasses import dataclass
|
||||
from typing import Final
|
||||
|
||||
DOMAIN: Final = "pv_microinverter"
|
||||
MANUFACTURER: Final = "Envertech"
|
||||
|
||||
# Config flow
|
||||
CONF_STATION_ID: Final = "station_id"
|
||||
CONF_UPDATE_INTERVAL: Final = "update_interval"
|
||||
|
||||
# Default values
|
||||
DEFAULT_UPDATE_INTERVAL: Final = 60 # 1 minute
|
||||
|
||||
# Entity attributes
|
||||
ATTR_LAST_UPDATED: Final = "last_updated"
|
||||
|
||||
# Sensors
|
||||
SENSOR_TYPES: Final = {
|
||||
"current_power": {
|
||||
"name": "Current Power",
|
||||
"icon": "mdi:solar-power",
|
||||
"unit": "W",
|
||||
"device_class": "power",
|
||||
"state_class": "measurement",
|
||||
},
|
||||
"today_energy": {
|
||||
"name": "Today's Energy",
|
||||
"icon": "mdi:solar-power",
|
||||
"unit": "kWh",
|
||||
"device_class": "energy",
|
||||
"state_class": "total_increasing",
|
||||
},
|
||||
"lifetime_energy": {
|
||||
"name": "Lifetime Energy",
|
||||
"icon": "mdi:solar-power",
|
||||
"unit": "kWh",
|
||||
"device_class": "energy",
|
||||
"state_class": "total_increasing",
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@dataclass
|
||||
class PVMicroinverterData:
|
||||
"""Class to hold PV microinverter data."""
|
||||
|
||||
current_power: float
|
||||
today_energy: float
|
||||
lifetime_energy: float
|
||||
last_updated: str
|
||||
54
custom_components/pv_microinverter/coordinator.py
Normal file
54
custom_components/pv_microinverter/coordinator.py
Normal file
@@ -0,0 +1,54 @@
|
||||
"""Data update coordinator for PV Microinverter integration."""
|
||||
|
||||
import logging
|
||||
from datetime import timedelta
|
||||
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.update_coordinator import (
|
||||
DataUpdateCoordinator,
|
||||
UpdateFailed,
|
||||
)
|
||||
|
||||
from .api import PVMicroinverterApiClient, PVMicroinverterApiClientError
|
||||
from .const import DOMAIN, PVMicroinverterData
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class PVMicroinverterDataUpdateCoordinator(DataUpdateCoordinator[PVMicroinverterData]):
|
||||
"""Class to manage fetching PV Microinverter data."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
hass: HomeAssistant,
|
||||
api_client: PVMicroinverterApiClient,
|
||||
update_interval: int,
|
||||
) -> None:
|
||||
"""Initialize the coordinator.
|
||||
|
||||
Args:
|
||||
hass: The Home Assistant instance
|
||||
api_client: The API client
|
||||
update_interval: The update interval in seconds
|
||||
"""
|
||||
super().__init__(
|
||||
hass,
|
||||
_LOGGER,
|
||||
name=DOMAIN,
|
||||
update_interval=timedelta(seconds=update_interval),
|
||||
)
|
||||
self.api_client = api_client
|
||||
|
||||
async def _async_update_data(self) -> PVMicroinverterData:
|
||||
"""Fetch data from the API.
|
||||
|
||||
Returns:
|
||||
PVMicroinverterData: The fetched data
|
||||
|
||||
Raises:
|
||||
UpdateFailed: If the update fails
|
||||
"""
|
||||
try:
|
||||
return await self.api_client.async_get_data()
|
||||
except PVMicroinverterApiClientError as error:
|
||||
raise UpdateFailed(f"Error communicating with API: {error}") from error
|
||||
42
custom_components/pv_microinverter/entity.py
Normal file
42
custom_components/pv_microinverter/entity.py
Normal file
@@ -0,0 +1,42 @@
|
||||
"""Base entity for PV Microinverter integration."""
|
||||
|
||||
from homeassistant.helpers.device_registry import DeviceEntryType
|
||||
from homeassistant.helpers.entity import DeviceInfo
|
||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||
|
||||
from .const import DOMAIN, MANUFACTURER
|
||||
from .coordinator import PVMicroinverterDataUpdateCoordinator
|
||||
|
||||
|
||||
class PVMicroinverterEntity(CoordinatorEntity[PVMicroinverterDataUpdateCoordinator]):
|
||||
"""Base entity for PV Microinverter integration."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
coordinator: PVMicroinverterDataUpdateCoordinator,
|
||||
station_id: str,
|
||||
sensor_type: str,
|
||||
) -> None:
|
||||
"""Initialize the entity.
|
||||
|
||||
Args:
|
||||
coordinator: The data update coordinator
|
||||
system_id: The system identifier
|
||||
sensor_type: The sensor type
|
||||
"""
|
||||
super().__init__(coordinator)
|
||||
self._station_id = station_id
|
||||
self._sensor_type = sensor_type
|
||||
self._attr_unique_id = f"{station_id}_{sensor_type}"
|
||||
self._attr_device_info = DeviceInfo(
|
||||
identifiers={(DOMAIN, station_id)},
|
||||
name=f"PV Microinverter {station_id}",
|
||||
manufacturer=MANUFACTURER,
|
||||
model="Microinverter",
|
||||
entry_type=DeviceEntryType.SERVICE,
|
||||
)
|
||||
|
||||
@property
|
||||
def available(self) -> bool:
|
||||
"""Return if entity is available."""
|
||||
return self.coordinator.last_update_success and super().available
|
||||
123
custom_components/pv_microinverter/sensor.py
Normal file
123
custom_components/pv_microinverter/sensor.py
Normal file
@@ -0,0 +1,123 @@
|
||||
"""Sensor platform for PV Microinverter integration."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from typing import Any, Final
|
||||
|
||||
from homeassistant.components.sensor import (
|
||||
SensorDeviceClass,
|
||||
SensorEntity,
|
||||
SensorStateClass,
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import UnitOfEnergy, UnitOfPower
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
|
||||
from .const import ATTR_LAST_UPDATED, DOMAIN, SENSOR_TYPES
|
||||
from .coordinator import PVMicroinverterDataUpdateCoordinator
|
||||
from .entity import PVMicroinverterEntity
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
# Map to convert API units to HA units
|
||||
UNIT_MAP: Final = {
|
||||
"W": UnitOfPower.WATT,
|
||||
"kWh": UnitOfEnergy.KILO_WATT_HOUR,
|
||||
}
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
entry: ConfigEntry,
|
||||
async_add_entities: AddEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up PV Microinverter sensors based on a config entry."""
|
||||
coordinator: PVMicroinverterDataUpdateCoordinator = hass.data[DOMAIN][
|
||||
entry.entry_id
|
||||
]
|
||||
system_id = entry.data["system_id"]
|
||||
|
||||
entities = []
|
||||
|
||||
# Create a sensor entity for each sensor type
|
||||
for sensor_key, sensor_info in SENSOR_TYPES.items():
|
||||
entities.append(
|
||||
PVMicroinverterSensor(
|
||||
coordinator=coordinator,
|
||||
system_id=system_id,
|
||||
sensor_type=sensor_key,
|
||||
sensor_info=sensor_info,
|
||||
)
|
||||
)
|
||||
|
||||
async_add_entities(entities, True)
|
||||
|
||||
|
||||
class PVMicroinverterSensor(PVMicroinverterEntity, SensorEntity):
|
||||
"""Representation of a PV Microinverter sensor."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
coordinator: PVMicroinverterDataUpdateCoordinator,
|
||||
system_id: str,
|
||||
sensor_type: str,
|
||||
sensor_info: dict[str, Any],
|
||||
) -> None:
|
||||
"""Initialize the sensor.
|
||||
|
||||
Args:
|
||||
coordinator: The data update coordinator
|
||||
system_id: The system identifier
|
||||
sensor_type: The sensor type
|
||||
sensor_info: The sensor information
|
||||
"""
|
||||
super().__init__(coordinator, system_id, sensor_type)
|
||||
|
||||
self._attr_name = sensor_info["name"]
|
||||
self._attr_icon = sensor_info["icon"]
|
||||
|
||||
# Convert API units to HA units
|
||||
unit = sensor_info["unit"]
|
||||
self._attr_native_unit_of_measurement = UNIT_MAP.get(unit, unit)
|
||||
|
||||
# Set device class if available
|
||||
device_class = sensor_info.get("device_class")
|
||||
if device_class:
|
||||
if device_class == "power":
|
||||
self._attr_device_class = SensorDeviceClass.POWER
|
||||
elif device_class == "energy":
|
||||
self._attr_device_class = SensorDeviceClass.ENERGY
|
||||
|
||||
# Set state class if available
|
||||
state_class = sensor_info.get("state_class")
|
||||
if state_class:
|
||||
if state_class == "measurement":
|
||||
self._attr_state_class = SensorStateClass.MEASUREMENT
|
||||
elif state_class == "total_increasing":
|
||||
self._attr_state_class = SensorStateClass.TOTAL_INCREASING
|
||||
|
||||
@property
|
||||
def native_value(self) -> float:
|
||||
"""Return the state of the sensor."""
|
||||
data = self.coordinator.data
|
||||
if not data:
|
||||
return None
|
||||
|
||||
if self._sensor_type == "current_power":
|
||||
return data.current_power
|
||||
elif self._sensor_type == "today_energy":
|
||||
return data.today_energy
|
||||
elif self._sensor_type == "lifetime_energy":
|
||||
return data.lifetime_energy
|
||||
return None
|
||||
|
||||
@property
|
||||
def extra_state_attributes(self) -> dict[str, Any]:
|
||||
"""Return the state attributes of the sensor."""
|
||||
return {
|
||||
ATTR_LAST_UPDATED: self.coordinator.data.last_updated
|
||||
if self.coordinator.data
|
||||
else None,
|
||||
}
|
||||
47
custom_components/pv_microinverter/translations/en.json
Normal file
47
custom_components/pv_microinverter/translations/en.json
Normal file
@@ -0,0 +1,47 @@
|
||||
{
|
||||
"config": {
|
||||
"step": {
|
||||
"user": {
|
||||
"title": "Connect to PV Microinverter",
|
||||
"description": "Enter your PV Microinverter API credentials.",
|
||||
"data": {
|
||||
"api_key": "API Key",
|
||||
"system_id": "System ID",
|
||||
"update_interval": "Update interval (seconds)"
|
||||
}
|
||||
},
|
||||
"reauth": {
|
||||
"title": "Reauthenticate with PV Microinverter",
|
||||
"description": "The PV Microinverter integration needs to re-authenticate your account.",
|
||||
"data": {
|
||||
"api_key": "API Key",
|
||||
"system_id": "System ID",
|
||||
"update_interval": "Update interval (seconds)"
|
||||
}
|
||||
}
|
||||
},
|
||||
"error": {
|
||||
"cannot_connect": "Failed to connect, please try again",
|
||||
"invalid_auth": "Invalid authentication",
|
||||
"unknown": "Unexpected error"
|
||||
},
|
||||
"abort": {
|
||||
"already_configured": "This system is already configured",
|
||||
"reauth_successful": "Re-authentication was successful",
|
||||
"reauth_failed_existing_entry_not_found": "Could not find existing config entry to re-authenticate"
|
||||
}
|
||||
},
|
||||
"entity": {
|
||||
"sensor": {
|
||||
"current_power": {
|
||||
"name": "Current Power"
|
||||
},
|
||||
"today_energy": {
|
||||
"name": "Today's Energy"
|
||||
},
|
||||
"lifetime_energy": {
|
||||
"name": "Lifetime Energy"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
114
custom_components/pv_microinverter/units.py
Normal file
114
custom_components/pv_microinverter/units.py
Normal file
@@ -0,0 +1,114 @@
|
||||
from typing import Self
|
||||
|
||||
# SI prefix multipliers
|
||||
SI_PREFIXES = {
|
||||
"Y": 1e24,
|
||||
"Z": 1e21,
|
||||
"E": 1e18,
|
||||
"P": 1e15,
|
||||
"T": 1e12,
|
||||
"G": 1e9,
|
||||
"M": 1e6,
|
||||
"k": 1e3,
|
||||
"h": 1e2,
|
||||
"da": 1e1,
|
||||
"d": 1e-1,
|
||||
"c": 1e-2,
|
||||
"m": 1e-3,
|
||||
"µ": 1e-6,
|
||||
"u": 1e-6,
|
||||
"n": 1e-9,
|
||||
"p": 1e-12,
|
||||
"f": 1e-15,
|
||||
"a": 1e-18,
|
||||
"z": 1e-21,
|
||||
"y": 1e-24,
|
||||
}
|
||||
|
||||
|
||||
def _get_unit_symbol(unit_val: str) -> str:
|
||||
"""Extract the unit symbol from a unit value string."""
|
||||
# Assuming the unit value is in the format "value unit"
|
||||
return unit_val.strip().split(" ")[-1]
|
||||
|
||||
|
||||
class SIUnit:
|
||||
"""SI Units with automatic unit conversion."""
|
||||
|
||||
def __init__(self, name: str, quantity: str, symbol: str, factor: float):
|
||||
self.name = name
|
||||
self.quantity = quantity
|
||||
self.symbol = symbol
|
||||
self.factor = factor
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.name} ({self.symbol})"
|
||||
|
||||
def __repr__(self):
|
||||
return f"SIUnit(name={self.name}, symbol={self.symbol}, factor={self.factor})"
|
||||
|
||||
@classmethod
|
||||
def parse(cls, unit_str: str) -> Self:
|
||||
"""
|
||||
Return an SIUnit by parsing the given unit string, supporting SI prefixes.
|
||||
"""
|
||||
# If the unit is registered directly, return it.
|
||||
if unit_str in BASE_UNITS:
|
||||
return BASE_UNITS[unit_str]
|
||||
# Otherwise, check for a valid prefix.
|
||||
for prefix in sorted(SI_PREFIXES, key=len, reverse=True):
|
||||
if unit_str.startswith(prefix):
|
||||
base_symbol = unit_str[len(prefix) :]
|
||||
if base_symbol in BASE_UNITS:
|
||||
base_unit = BASE_UNITS[base_symbol]
|
||||
multiplier = SI_PREFIXES[prefix]
|
||||
return SIUnit(
|
||||
f"{prefix}{base_unit.name}",
|
||||
base_unit.quantity,
|
||||
f"{prefix}{base_unit.symbol}",
|
||||
base_unit.factor * multiplier,
|
||||
)
|
||||
raise ValueError(f"Unknown unit: {unit_str}")
|
||||
|
||||
|
||||
class Dimension:
|
||||
"""Class to represent a dimension with a unit."""
|
||||
|
||||
def __init__(self, value: float, unit: SIUnit):
|
||||
self.value = value
|
||||
self.unit = unit
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.value} {self.unit.symbol}"
|
||||
|
||||
def to_base_unit(self) -> float:
|
||||
"""Convert the dimension value to its base unit."""
|
||||
return float(self.value) * self.unit.factor
|
||||
|
||||
def __repr__(self):
|
||||
return f"Dimension(value={self.value}, unit={self.unit})"
|
||||
|
||||
@classmethod
|
||||
def parse(cls, unit_val: str) -> Self:
|
||||
"""
|
||||
Parse a unit value string into a Dimension object, supporting SI unit prefixes.
|
||||
Expects the format "value unit", e.g. "3.5 kW" or "10 Wh".
|
||||
"""
|
||||
parts = unit_val.strip().split()
|
||||
if len(parts) != 2:
|
||||
raise ValueError(f"Invalid unit value format: {unit_val}")
|
||||
value, unit_str = parts
|
||||
unit = SIUnit.parse(unit_str)
|
||||
return cls(float(value), unit)
|
||||
|
||||
|
||||
# Define SI units
|
||||
WATT = SIUnit("Watt", "Power", "W", 1)
|
||||
WATT_HOUR = SIUnit("Watt-hour", "Energy", "Wh", 1)
|
||||
|
||||
|
||||
# Mapping of base unit symbols to registered SIUnit objects
|
||||
BASE_UNITS = {
|
||||
"W": WATT,
|
||||
"Wh": WATT_HOUR,
|
||||
}
|
||||
Reference in New Issue
Block a user