Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
53 changes: 51 additions & 2 deletions custom_components/powerocean/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
CONF_SCAN_INTERVAL,
)
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryNotReady
from homeassistant.helpers import device_registry as dr
from homeassistant.helpers.typing import ConfigType
from homeassistant.loader import async_get_integration
Expand Down Expand Up @@ -73,12 +74,14 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
updated = False

if CONF_SCAN_INTERVAL not in options:
options[CONF_SCAN_INTERVAL] = DEFAULT_SCAN_INTERVAL
options[CONF_SCAN_INTERVAL] = entry.data.get("options").get(
"scan_interval", DEFAULT_SCAN_INTERVAL
)
updated = True

if CONF_FRIENDLY_NAME not in options:
options[CONF_FRIENDLY_NAME] = entry.data.get("options", {}).get(
"custom_device_name", DEFAULT_NAME
"friendly_name", DEFAULT_NAME
)
updated = True

Expand All @@ -87,8 +90,17 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
LOGGER.debug("Migrated missing options for %s: %s", entry.title, options)

# --- Ecoflow-Objekt initialisieren ---

device_id = entry.data[CONF_DEVICE_ID]
if not device_id:
msg = "Missing device_id in config entry"
raise ConfigEntryNotReady(msg)

model_id = entry.data[CONF_MODEL_ID]
if not model_id:
msg = "Missing model_id in config entry"
raise ConfigEntryNotReady(msg)

api = EcoflowApi(
hass,
device_id,
Expand Down Expand Up @@ -153,6 +165,43 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
return True


async def async_migrate_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Migrate old config entries to new structure."""
version = entry.version

if version == 1.3:
data = dict(entry.data)
options = dict(entry.options)

# ALT → NEU
if "user_input" in data:
old = data.pop("user_input")

data.update(
{
CONF_DEVICE_ID: old.get(CONF_DEVICE_ID),
CONF_EMAIL: old.get(CONF_EMAIL),
CONF_PASSWORD: old.get(CONF_PASSWORD),
CONF_MODEL_ID: old.get(CONF_MODEL_ID),
}
)

# Optionen auslagern
options.setdefault(
CONF_FRIENDLY_NAME,
old.get(CONF_FRIENDLY_NAME, DEFAULT_NAME),
)

hass.config_entries.async_update_entry(
entry,
data=data,
options=options,
version=2,
)

return True


async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Unload a config entry and clean up resources."""
unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
Expand Down
2 changes: 1 addition & 1 deletion custom_components/powerocean/config_flow.py
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,7 @@ async def validate_input_for_device(hass: HomeAssistant, data: dict[str, Any]) -
class PowerOceanConfigFlow(ConfigFlow, domain=DOMAIN):
"""Handle a config flow for PowerOcean."""

VERSION = 1.3
VERSION = 2

def __init__(self) -> None:
"""Instanzvariablen für den Flow-Verlauf."""
Expand Down
143 changes: 110 additions & 33 deletions custom_components/powerocean/ecoflow.py
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,14 @@ class SensorClassHelper:
SensorStateClass.MEASUREMENT,
),
),
(
re.compile(r"resist", re.IGNORECASE),
(
None,
"Ω",
SensorStateClass.MEASUREMENT,
),
),
]

@classmethod
Expand Down Expand Up @@ -202,27 +210,90 @@ def get_description(key: str) -> str:

@staticmethod
def get_special_icon(key: str) -> str | None:
"""Get special icon for a key."""
# Dictionary für die Zuordnung von Keys zu Icons
icon_mapping = {
"mpptPwr": "mdi:solar-power",
"""Return a Home Assistant icon based on the semantic meaning of the key."""
k = key.lower()

# ---- Status / Diagnose ----
status_icons = {
"online": "mdi:cloud-check",
"sysGridPwr": "mdi:transmission-tower-import",
"sysLoadPwr": "mdi:home-import-outline",
"bpAmp": "mdi:current-dc",
"bponlinesum": "mdi:package-check",
"emsbpalivenum": "mdi:package-check",
"emsbpselfcheckstate": "mdi:checkbox-marked-circle-outline", # Selbsttest Batterie
"emsmpptselfcheckstate": "mdi:checkbox-marked-circle-outline", # Selbsttest MPPT
"emsmpptrunstate": "mdi:run", # MPPT läuft
}
# Standardwert setzen
special_icon = icon_mapping.get(key)

# Zusätzliche Prüfung für Keys, die mit "pv1" oder "pv2" beginnen
if key.startswith(("pv1", "pv2", "pv3")):
special_icon = "mdi:solar-power"
if key.endswith(("Pv1_pwr", "Pv2_pwr", "Pv3_pwr")):
special_icon = "mdi:solar-power"
if key.endswith(("Pv1_amp", "Pv2_amp", "Pv3_amp")):
special_icon = "mdi:current-dc"
if k in status_icons:
return status_icons[k]

if k.endswith("name"):
return "mdi:label-outline"
if k.endswith("bright"):
return "mdi:brightness-percent"
if k.endswith(("faultcode", "warningcode")):
return "mdi:alert-circle-outline"
if k.startswith("bms"):
return "mdi:chip"

# ---- PV / MPPT ----
if k.startswith(("pv", "mpptpv", "mppt")):
if k.endswith("lightsta"):
return "mdi:white-balance-sunny"
if k.endswith(("invpwr", "_pwrtotal", "mpptpwr")):
return "mdi:solar-power"
if k.endswith(("_pwr", "pwr")):
return "mdi:solar-power-variant"
if k.endswith("_amp"):
return "mdi:current-dc"
if k.endswith("resist"):
return "mdi:resistor"
# if k.endswith("_vol"):
# return "mdi:solar-power-variant-outline"

# ---- Netz / Haus / PCS ----
grid_icons = {
"sysgridpwr": "mdi:transmission-tower-import",
"sysloadpwr": "mdi:home-lightning-bolt",
"pcsmeterpower": "mdi:home-lightning-bolt",
}
if k in grid_icons:
return grid_icons[k]

# ---- Leistung / Energie ----
if k.endswith(("actpwr", "_pwr")):
return "mdi:flash"
if k.endswith("apparentpwr"):
return "mdi:flash-outline"
if k.endswith("reactpwr"):
return "mdi:sine-wave"
if k.endswith("electricitygeneration"):
return "mdi:counter"

# ---- Strom / Spannung ----
if k.endswith("_amp"):
return "mdi:current-ac"
# if k.endswith("_vol"):
# return "mdi:lightning-bolt-outline"

# ---- Batterie / Speicher ----
if k.startswith("bp") or k.startswith("bms"):
bp_ems_bms_icons = {
"pwr": "mdi:flash",
"remainwatth": "mdi:car-battery",
"envtemp": "mdi:thermometer",
"maxcelltemp": "mdi:thermometer-high",
"mincelltemp": "mdi:thermometer-low",
"cycles": "mdi:repeat",
"sn": "mdi:barcode",
"sysstate": "mdi:information-outline",
"balancestate": "mdi:battery-sync", # bpBalanceState
"bmschdsgsta": "mdi:swap-horizontal", # Charge/Discharge Status
}

for suffix, icon in bp_ems_bms_icons.items():
if k.lower().endswith(suffix):
return icon

return special_icon
return None


# ecoflow_api to detect device and get device info,
Expand Down Expand Up @@ -613,10 +684,7 @@ def _resolve_device_info(
payload: dict,
report: str,
) -> tuple[str, DeviceInfo]:
"""
Resolve serial number and DeviceInfo for non-boxed reports.
"""

"""Resolve serial number and DeviceInfo for non-boxed reports."""
# bekannte SN-Felder (Reihenfolge = Priorität)
SN_KEYS = ("evSn", "hrSn")

Expand Down Expand Up @@ -691,10 +759,6 @@ def _extract_sensors_from_report(
report: Name des Reports im JSON
suffix: Suffix, das an die Namen der Sensoren angehängt wird
(z.B. für Master/Slave).
battery_mode: Wenn True, werden Batteriedaten speziell behandelt
wallbox_mode: Wenn True, werden Wallboxdaten speziell behandelt
chargebox_mode: Wenn True, werden chargedaten speziell behandelt
ems_heartbeat_mode: Wenn True, wird die spezielle EMS-Heartbeat-Verarbeitung
parallel_energy_stream_mode: Wenn True, wird die spezielle Verarbeitung
für parallele Energie-Streams verwendet.

Expand All @@ -721,7 +785,7 @@ def _extract_sensors_from_report(

sens_select = list(REPORT_DATAPOINTS.get(report, ()))
# Setze Report-Namen korrekt aus response
report = key if key else report
report = key or report
# Battery und Wallbox Handling
if ReportMode.BATTERY.value in report or ReportMode.WALLBOX.value in report:
self._handle_boxed_devices(
Expand Down Expand Up @@ -873,6 +937,19 @@ def _handle_ems_heartbeat_mode(
suffix="",
)

# --- Isolationswiderstand ---
mppt_ins_resist = d["mpptHeartBeat"][0].get("mpptInsResist")
if mppt_ins_resist is not None:
self._collect_sensor(
collector,
self.sn_inverter,
report,
"mpptInsResist",
mppt_ins_resist,
device_info=device_info,
suffix="",
)

def _handle_parallel_energy_stream(
self,
d: dict,
Expand All @@ -884,7 +961,6 @@ def _handle_parallel_energy_stream(
if not isinstance(para_list, list):
LOGGER.warning("paraEnergyStream is not a list")
return

for device_data in para_list:
raw_sn = device_data.get("devSn")
device_sn = self._decode_sn(raw_sn) if raw_sn else None
Expand All @@ -906,19 +982,20 @@ def _handle_parallel_energy_stream(
model=f"PowerOcean {prefix}",
via_sn=self.sn_inverter if device_sn != DeviceRole.ALL.value else None,
)
report = f"{report} paraEnergyStream"
for key, value in device_data.items():
if key == "devSn":
continue
if isinstance(value, dict):
continue
if key.endswith("Sn") and isinstance(value, str):
self._decode_sn(value)
self._collect_sensor(
collector=collector,
device_sn=device_sn,
report=report,
key=f"paraEnergyStream_{key}",
key=key,
value=value,
device_info=device_info,
suffix=suffix,
suffix="",
)

def _handle_standard_mode(
Expand Down Expand Up @@ -948,7 +1025,7 @@ def _handle_standard_mode(
key=key,
value=value,
device_info=device_info,
suffix=suffix,
suffix="",
)


Expand Down
7 changes: 2 additions & 5 deletions custom_components/powerocean/sensor.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,7 @@
from typing import Any

from homeassistant.components.sensor import (
SensorDeviceClass,
SensorEntity,
SensorStateClass,
)
from homeassistant.const import (
EntityCategory,
Expand All @@ -20,7 +18,6 @@

from .const import (
DOMAIN,
LOGGER,
)
from .ecoflow import PowerOceanEndPoint

Expand Down Expand Up @@ -61,8 +58,8 @@ def __init__(self, coordinator, endpoint: PowerOceanEndPoint) -> None:
# HA attributes
self._attr_has_entity_name = True
# wahrscheinlich diese !!!
self._attr_unique_id = self._endpoint_name
# self._attr_unique_id = self._endpoint_id
# self._attr_unique_id = self._endpoint_name
self._attr_unique_id = self._endpoint_id
# self._unique_id = self._endpoint_id
self._attr_name = self._endpoint_friendly_name
self._attr_icon = self._endpoint_icon
Expand Down
Loading
Loading