Skip to content

Commit

Permalink
initial commit
Browse files Browse the repository at this point in the history
  • Loading branch information
dkarv committed Jan 25, 2024
1 parent edf085f commit f05ebbc
Show file tree
Hide file tree
Showing 10 changed files with 357 additions and 0 deletions.
18 changes: 18 additions & 0 deletions .github/workflows/validate.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
name: Validate

on:
push:
pull_request:
schedule:
- cron: "0 0 * * *"
workflow_dispatch:

jobs:
validate-hacs:
runs-on: "ubuntu-latest"
steps:
- uses: "actions/checkout@v3"
- name: HACS validation
uses: "hacs/action@main"
with:
category: "integration"
3 changes: 3 additions & 0 deletions copy.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
#!/bin/sh

cp -R ../homeassistant-core/homeassistant/components/lg_ess custom_components/
42 changes: 42 additions & 0 deletions custom_components/lg_ess/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
"""The LG ESS inverter integration."""

import logging

from pyess.aio_ess import ESS, ESSException

from homeassistant.config_entries import ConfigEntry
from homeassistant.const import Platform
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryNotReady

from .const import DOMAIN

_LOGGER = logging.getLogger(__name__)
PLATFORMS: list[Platform] = [Platform.SENSOR]


async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up LG ESS from config entry."""

hass.data.setdefault(DOMAIN, {})

try:
ess = await ESS.create("LG_ESS", entry.data["password"], entry.data["host"])
hass.data[DOMAIN][entry.entry_id] = ess
except ESSException as e:
_LOGGER.exception("Error setting up ESS api")
await ess.destruct()
raise ConfigEntryNotReady from e

await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)

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):
api = hass.data[DOMAIN].pop(entry.entry_id)
await api.destruct()

return unload_ok
68 changes: 68 additions & 0 deletions custom_components/lg_ess/config_flow.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
"""Config flow for LG ESS integration."""
import logging
from typing import Any

from pyess.aio_ess import ESS, ESSAuthException, ESSException
import voluptuous as vol

from homeassistant import config_entries
from homeassistant.const import CONF_HOST, CONF_PASSWORD
from homeassistant.core import HomeAssistant
from homeassistant.data_entry_flow import FlowResult

from .const import DOMAIN

_LOGGER = logging.getLogger(__name__)

STEP_USER_DATA_SCHEMA = vol.Schema(
{
vol.Required(CONF_HOST): str,
vol.Required(CONF_PASSWORD): str,
}
)


async def validate_input(hass: HomeAssistant, data: dict[str, Any]) -> dict[str, Any]:
"""Validate the user input allows us to connect.
Data has the keys from STEP_USER_DATA_SCHEMA with values provided by the user.
"""
# We cannot check with current ESS implementation...

try:
ess = await ESS.create("TEST_ENTRY", data[CONF_HOST], data[CONF_PASSWORD])
except Exception as e:
await ess.destruct()
raise e

# Return info that you want to store in the config entry.
return {"title": "LG ESS"}


class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
"""Handle a config flow for LG ESS."""

VERSION = 1

async def async_step_user(
self, user_input: dict[str, Any] | None = 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)
return self.async_create_entry(title=info["title"], data=user_input)
except ESSAuthException:
_LOGGER.exception("Wrong password")
errors["base"] = "invalid_auth"
except ESSException:
_LOGGER.exception("Generic error setting up the ESS Api")
errors["base"] = "unknown"
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
)
3 changes: 3 additions & 0 deletions custom_components/lg_ess/const.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
"""Constants for the LG ESS Inverter integration."""

DOMAIN = "lg_ess"
65 changes: 65 additions & 0 deletions custom_components/lg_ess/coordinator.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
"""Coordinator to fetch the data once for all sensors."""

from datetime import timedelta
import logging

from pyess.aio_ess import ESS

from homeassistant.core import HomeAssistant
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator

_LOGGER = logging.getLogger(__name__)


class CommonCoordinator(DataUpdateCoordinator):
"""LG ESS coordinator.
Data:
{'PV':
{'brand': 'LGE-SOLAR', 'capacity': '10935',
'pv1_voltage': '52.900002', 'pv2_voltage': '36.099998', 'pv3_voltage': '35.500000',
'pv1_power': '0', 'pv2_power': '1', 'pv3_power': '1',
'pv1_current': '0.010000', 'pv2_current': '0.030000', 'pv3_current': '0.030000',
'today_pv_generation_sum': '16294', 'today_month_pv_generation_sum': '17469'},
'BATT':
{'status': '2', 'soc': '10.3', 'dc_power': '627',
'winter_setting': 'off', 'winter_status': 'off', 'safety_soc': '20',
'backup_setting': 'off', 'backup_status': 'off', 'backup_soc': '30',
'today_batt_discharge_enery': '6855', 'today_batt_charge_energy': '9050', 'month_batt_charge_energy': '9616', 'month_batt_discharge_energy': '9264'},
'GRID':
{'active_power': '9', 'a_phase': '230.199997', 'freq': '50.020000',
'today_grid_feed_in_energy': '968', 'today_grid_power_purchase_energy': '7442',
'month_grid_feed_in_energy': '994', 'month_grid_power_purchase_energy': '13497'},
'LOAD':
{'load_power': '638',
'today_load_consumption_sum': '20573', 'today_pv_direct_consumption_enegy': '6276', 'today_batt_discharge_enery': '6855', 'today_grid_power_purchase_energy': '7442',
'month_load_consumption_sum': '29620', 'month_pv_direct_consumption_energy': '6859', 'month_batt_discharge_energy': '9264', 'month_grid_power_purchase_energy': '13497'},
'PCS':
{'today_self_consumption': '94.1', 'month_co2_reduction_accum': '12402',
'today_pv_generation_sum': '16294', 'today_grid_feed_in_energy': '968',
'month_pv_generation_sum': '17469', 'month_grid_feed_in_energy': '994',
'pcs_stauts': '3', 'feed_in_limitation': '100', 'operation_mode': '0'}}
"""

def __init__(self, hass: HomeAssistant, ess: ESS) -> None:
"""Initialize my coordinator."""
super().__init__(
hass,
_LOGGER,
# Name of the data. For logging purposes.
name="LG ESS common",
# Polling interval. Will only be polled if there are subscribers.
update_interval=timedelta(seconds=5),
)
self.ess = ess

async def _async_update_data(self):
"""Fetch data from API endpoint.
This is the place to pre-process the data to lookup tables
so entities can quickly look up their data.
"""
# try:
# Note: asyncio.TimeoutError and aiohttp.ClientError are already
# handled by the data update coordinator.
return await self.ess.get_common()
15 changes: 15 additions & 0 deletions custom_components/lg_ess/manifest.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
{
"domain": "lg_ess",
"name": "LG ESS Inverter",
"codeowners": ["@dkarv"],
"config_flow": true,
"dependencies": [],
"documentation": "https://github.com/dkarv/hacs-lg-ess/blob/main/README.md",
"version": "0.1.0",
"homekit": {},
"iot_class": "local_polling",
"issue_tracker": "https://github.com/dkarv/hacs-lg-ess/issues",
"requirements": ["pyess==0.1.15"],
"ssdp": [],
"zeroconf": []
}
119 changes: 119 additions & 0 deletions custom_components/lg_ess/sensor.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
"""Example integration using DataUpdateCoordinator."""

import logging

from pyess.aio_ess import ESSAuthException

from homeassistant.components.sensor import (
SensorDeviceClass,
SensorEntity,
SensorStateClass,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import PERCENTAGE, UnitOfEnergy, UnitOfPower
from homeassistant.core import HomeAssistant, callback
from homeassistant.exceptions import ConfigEntryAuthFailed
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.update_coordinator import CoordinatorEntity

from .const import DOMAIN
from .coordinator import CommonCoordinator

_LOGGER = logging.getLogger(__name__)


async def async_setup_entry(
hass: HomeAssistant,
config_entry: ConfigEntry,
async_add_entities: AddEntitiesCallback,
) -> None:
"""Set up sensors from config entry."""
ess = hass.data[DOMAIN][config_entry.entry_id]
coordinator = CommonCoordinator(hass, ess)

# Fetch initial data so we have data when entities subscribe
#
# If the refresh fails, async_config_entry_first_refresh will
# raise ConfigEntryNotReady and setup will try again later
#
# If you do not want to retry setup on failure, use
# coordinator.async_refresh() instead
#
try:
await coordinator.async_config_entry_first_refresh()
except ESSAuthException as e:
# Raising ConfigEntryAuthFailed will cancel future updates
# and start a config flow with SOURCE_REAUTH (async_step_reauth)
raise ConfigEntryAuthFailed from e

async_add_entities(
[
MeasurementSensor(coordinator, "BATT", "soc", PERCENTAGE),
MeasurementSensor(coordinator, "BATT", "dc_power", UnitOfPower.WATT),
MeasurementSensor(coordinator, "LOAD", "load_power", UnitOfPower.WATT),
MeasurementSensor(coordinator, "PCS", "today_self_consumption", PERCENTAGE),
IncreasingSensor(coordinator, "BATT", "today_batt_discharge_enery"),
IncreasingSensor(coordinator, "BATT", "today_batt_charge_energy"),
IncreasingSensor(coordinator, "BATT", "month_batt_discharge_energy"),
IncreasingSensor(coordinator, "BATT", "month_batt_charge_energy"),
IncreasingSensor(coordinator, "LOAD", "today_load_consumption_sum"),
IncreasingSensor(coordinator, "LOAD", "today_pv_direct_consumption_enegy"),
IncreasingSensor(coordinator, "LOAD", "today_grid_power_purchase_energy"),
IncreasingSensor(coordinator, "LOAD", "month_load_consumption_sum"),
IncreasingSensor(coordinator, "LOAD", "month_pv_direct_consumption_energy"),
IncreasingSensor(coordinator, "LOAD", "month_grid_power_purchase_energy"),
IncreasingSensor(coordinator, "PCS", "today_pv_generation_sum"),
IncreasingSensor(coordinator, "PCS", "today_grid_feed_in_energy"),
IncreasingSensor(coordinator, "PCS", "month_pv_generation_sum"),
IncreasingSensor(coordinator, "PCS", "month_grid_feed_in_energy"),
]
)


class EssSensor(CoordinatorEntity[CommonCoordinator], SensorEntity):
"""Basic sensor with common functionality."""

_group: str
_key: str

def __init__(self, coordinator, group: str, key: str) -> None:
"""Initialize the sensor with the common coordinator."""
super().__init__(coordinator)
self._group = group
self._key = key
# Fix typos
self._attr_translation_key = key.replace("_enery", "_energy").replace(
"_enegy", "_energy"
)
self._attr_unique_id = self._attr_translation_key

@callback
def _handle_coordinator_update(self) -> None:
"""Handle updated data from the coordinator."""
self._attr_native_value = self.coordinator.data[self._group][self._key]
self.async_write_ha_state()


class MeasurementSensor(EssSensor):
"""Measurement sensor."""

_attr_state_class = SensorStateClass.MEASUREMENT

def __init__(self, coordinator, group: str, key: str, unit) -> None:
"""Initialize the sensor with the common coordinator."""
super().__init__(coordinator, group, key)
self._attr_native_unit_of_measurement = unit


class IncreasingSensor(EssSensor):
"""Increasing total sensor."""

_attr_state_class = SensorStateClass.TOTAL_INCREASING
_attr_device_class = SensorDeviceClass.ENERGY

def __init__(
self, coordinator, group: str, key: str, unit=UnitOfEnergy.WATT_HOUR
) -> None:
"""Initialize the sensor with the common coordinator."""
super().__init__(coordinator, group, key)
self._attr_native_unit_of_measurement = unit
21 changes: 21 additions & 0 deletions custom_components/lg_ess/strings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
{
"config": {
"step": {
"user": {
"data": {
"host": "[%key:common::config_flow::data::host%]",
"username": "[%key:common::config_flow::data::username%]",
"password": "[%key:common::config_flow::data::password%]"
}
}
},
"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%]"
}
}
}
3 changes: 3 additions & 0 deletions hacs.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"name": "LG ESS Inverter Integration"
}

0 comments on commit f05ebbc

Please sign in to comment.