Skip to content

Commit

Permalink
Add support for auto-detected additional powermeters (#57)
Browse files Browse the repository at this point in the history
As discussed in [Issue
23](#23), here's a PR to
add additional powermeters. What it does:

1. creates an ``e3dc``object as before
2. calls ``e3dc.get_powermeters()`` to identify the powermeters.
3. deletes the ``e3dc``object
4. creates a new object with the config parameter for the additional
powermeters.

Currently, per additional powermeter two entities for power and energy
are created.
Ideally, I'd like to give the entities nicer names based on the
powermeter type (which is known to python-e3dc) but I didn't manage to
name entities dynamically when working with ``strings.json``and
``en.json``.

I tested the implementation with my additional powermeter (Type
LM3p80hhc) for my heat pump which identifies as
``PM_TYPE_ADDITIONAL_CONSUMPTION`` in the python-e3dc lib:

<img width="528" alt="image"
src="https://github.com/torbennehmer/hacs-e3dc/assets/24450990/00f42740-67b0-4e40-ba16-617639fba5f1">
  • Loading branch information
torbennehmer authored Oct 13, 2023
2 parents 61c09be + 8e6faa7 commit 5193bf5
Show file tree
Hide file tree
Showing 3 changed files with 182 additions and 8 deletions.
102 changes: 94 additions & 8 deletions custom_components/e3dc_rscp/coordinator.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ class E3DCCoordinator(DataUpdateCoordinator[dict[str, Any]]):
"""E3DC Coordinator, fetches all relevant data and provides proxies for all service calls."""

e3dc: E3DC = None
_e3dcconfig: dict[str, Any] = {}
_mydata: dict[str, Any] = {}
_sw_version: str = ""
_update_guard_powersettings: bool = False
Expand Down Expand Up @@ -62,6 +63,7 @@ async def async_connect(self):
except Exception as ex:
raise ConfigEntryAuthFailed from ex

await self._async_connect_additional_powermeters()
self._mydata["system-derate-percent"] = self.e3dc.deratePercent
self._mydata["system-derate-power"] = self.e3dc.deratePower
self._mydata["system-additional-source-available"] = (
Expand All @@ -87,6 +89,44 @@ async def async_connect(self):

await self._load_timezone_settings()

async def _async_connect_additional_powermeters(self):
"""Identifies the installed powermeters and re-establishes the connection to the E3DC with the corresponding powermeters config."""
ROOT_PM_INDEX = 0 # Index 0 is always the root powermeter of the E3DC
self._e3dcconfig["powermeters"] = self.e3dc.get_powermeters()
for powermeter in self._e3dcconfig["powermeters"]:
if powermeter["index"] == ROOT_PM_INDEX:
powermeter["name"] = "Root PM"
powermeter["key"] = "root-pm"
else:
powermeter["name"] = (
powermeter["typeName"]
.replace("PM_TYPE_", "")
.replace("_", " ")
.capitalize()
)
powermeter["key"] = (
powermeter["typeName"]
.replace("PM_TYPE_", "")
.replace("_", "-")
.lower()
+ "-"
+ str(powermeter["index"])
)

delete_e3dcinstance(self.e3dc)

try:
self.e3dc: E3DC = await self.hass.async_add_executor_job(
create_e3dcinstance,
self.username,
self.password,
self.host,
self.rscpkey,
self._e3dcconfig,
)
except Exception as ex:
raise ConfigEntryAuthFailed from ex

async def _async_e3dc_request_single_tag(self, tag: str) -> Any:
"""Send a single tag request to E3DC, wraps lib call for async usage, supplies defaults."""

Expand Down Expand Up @@ -125,6 +165,14 @@ async def _async_update_data(self) -> dict[str, Any]:
)
self._process_manual_charge(request_data)

_LOGGER.debug("Polling additional powermeters")
powermeters_data: dict[
str, Any | None
] = await self.hass.async_add_executor_job(
self.e3dc.get_powermeters_data, None, True
)
self._process_powermeters_data(powermeters_data)

# Only poll power statstics once per minute. E3DC updates it only once per 15
# minutes anyway, this should be a good compromise to get the metrics shortly
# before the end of the day.
Expand Down Expand Up @@ -205,6 +253,22 @@ def _process_manual_charge(self, request_data) -> None:
# request_data, "EMS_MANUAL_CHARGE_LASTSTART"
# )[2]

def _process_powermeters_data(self, powermeters_data) -> None:
for powermeter_data in powermeters_data:
if powermeter_data["index"] != 0:
for powermeter_config in self._e3dcconfig["powermeters"]:
if powermeter_data["index"] == powermeter_config["index"]:
self._mydata[powermeter_config["key"]] = (
powermeter_data["power"]["L1"]
+ powermeter_data["power"]["L2"]
+ powermeter_data["power"]["L3"]
)
self._mydata[powermeter_config["key"] + "-total"] = (
powermeter_data["energy"]["L1"]
+ powermeter_data["energy"]["L2"]
+ powermeter_data["energy"]["L3"]
)

async def _load_timezone_settings(self):
"""Load the current timezone offset from the E3DC, using its local timezone data.
Expand Down Expand Up @@ -447,14 +511,36 @@ async def async_manual_charge(self, charge_amount: int) -> None:
else:
_LOGGER.debug("Successfully started manual charging")

def get_e3dcconfig(self) -> dict:
"""Return the E3DC config dict."""
return self._e3dcconfig

def create_e3dcinstance(username: str, password: str, host: str, rscpkey: str) -> E3DC:

def create_e3dcinstance(
username: str, password: str, host: str, rscpkey: str, config: dict = None
) -> E3DC:
"""Create the actual E3DC instance, this will try to connect and authenticate."""
e3dc = E3DC(
E3DC.CONNECT_LOCAL,
username=username,
password=password,
ipAddress=host,
key=rscpkey,
)
if config is None:
e3dc = E3DC(
E3DC.CONNECT_LOCAL,
username=username,
password=password,
ipAddress=host,
key=rscpkey,
)
else:
e3dc = E3DC(
E3DC.CONNECT_LOCAL,
username=username,
password=password,
ipAddress=host,
key=rscpkey,
configuration=config,
)
return e3dc


def delete_e3dcinstance(e3dc: E3DC) -> None:
"""Delete the actual E3DC instance."""
del e3dc
return
34 changes: 34 additions & 0 deletions custom_components/e3dc_rscp/sensor.py
Original file line number Diff line number Diff line change
Expand Up @@ -385,6 +385,40 @@ async def async_setup_entry(
E3DCSensor(coordinator, description, entry.unique_id)
for description in SENSOR_DESCRIPTIONS
]

# Add Sensor descriptions for additional powermeters
for powermeter_config in coordinator.get_e3dcconfig()["powermeters"]:
if powermeter_config["index"] != 0:
energy_description = SensorEntityDescription(
has_entity_name=True,
name=powermeter_config["name"] + " - total",
key=powermeter_config["key"] + "-total",
translation_key=powermeter_config["key"] + "-total",
icon="mdi:meter-electric",
native_unit_of_measurement=UnitOfEnergy.WATT_HOUR,
suggested_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
suggested_display_precision=2,
device_class=SensorDeviceClass.ENERGY,
state_class=SensorStateClass.TOTAL_INCREASING,
)
entities.append(
E3DCSensor(coordinator, energy_description, entry.unique_id)
)

power_description = SensorEntityDescription(
has_entity_name=True,
name=powermeter_config["name"],
key=powermeter_config["key"],
translation_key=powermeter_config["key"],
icon="mdi:meter-electric",
native_unit_of_measurement=UnitOfPower.WATT,
suggested_unit_of_measurement=UnitOfPower.KILO_WATT,
suggested_display_precision=1,
device_class=SensorDeviceClass.POWER,
state_class=SensorStateClass.MEASUREMENT,
)
entities.append(E3DCSensor(coordinator, power_description, entry.unique_id))

async_add_entities(entities)


Expand Down
54 changes: 54 additions & 0 deletions custom_components/e3dc_rscp/strings.json
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,60 @@
},
"manual-charge-energy": {
"name": "Energy charged from grid"
},
"undefined": {
"name": "Undefined Powermeter"
},
"root-pm": {
"name": "Root Powermeter"
},
"additional": {
"name": "Additional powermeter"
},
"additional-production": {
"name": "Additional production"
},
"additional-consumption": {
"name": "Additional consumption"
},
"farm": {
"name": "Farm"
},
"unused": {
"name": "Unused powermeter"
},
"wallbox": {
"name": "Wallbox"
},
"farm-additional": {
"name": "Farm additional powermeter"
},
"undefined-total": {
"name": "Undefined Powermeter - total"
},
"root-pm-total": {
"name": "Root Powermeter - total"
},
"additional-total": {
"name": "Additional Powermeter - total"
},
"additional-production-total": {
"name": "Additional production - total"
},
"additional-consumption-total": {
"name": "Additional consumption - total"
},
"farm-total": {
"name": "Farm - total"
},
"unused-total": {
"name": "Unused powermeter - total"
},
"wallbox-total": {
"name": "Wallbox - total"
},
"farm-additional-total": {
"name": "Farm additional powermeter - total"
}
},
"switch": {
Expand Down

0 comments on commit 5193bf5

Please sign in to comment.