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
48 changes: 48 additions & 0 deletions .devcontainer.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
{
"image": "mcr.microsoft.com/devcontainers/python:3.13",
"postCreateCommand": "scripts/setup",
"forwardPorts": [
8123
],
"portsAttributes": {
"8123": {
"label": "Home Assistant",
"onAutoForward": "notify"
}
},
"customizations": {
"vscode": {
"extensions": [
"charliermarsh.ruff",
"github.vscode-pull-request-github",
"ms-python.python",
"ms-python.vscode-pylance",
"ryanluker.vscode-coverage-gutters"
],
"settings": {
"files.eol": "\n",
"editor.tabSize": 4,
"editor.formatOnPaste": true,
"editor.formatOnSave": true,
"editor.formatOnType": false,
"files.trimTrailingWhitespace": true,
"python.analysis.typeCheckingMode": "basic",
"python.analysis.autoImportCompletions": true,
"python.defaultInterpreterPath": "/usr/local/bin/python",
"[python]": {
"editor.defaultFormatter": "charliermarsh.ruff"
}
}
}
},
"remoteUser": "vscode",
"features": {
"ghcr.io/devcontainers-extra/features/apt-packages:1": {
"packages": [
"ffmpeg",
"libturbojpeg0",
"libpcap-dev"
]
}
}
}
1 change: 1 addition & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
* text=auto eol=lf
11 changes: 10 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,10 +1,19 @@
# artifacts
__pycache__

# environment
venv
.idea
.DS_Store
.vscode/settings.json
.coverage
requirements.test.txt
requirements.txt

# misc
.coverage
setup.cfg
tests

# Home Assistant configuration
config/*
!config/configuration.yaml
11 changes: 11 additions & 0 deletions .vscode/tasks.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"version": "2.0.0",
"tasks": [
{
"label": "Run Home Assistant on port 8123",
"type": "shell",
"command": "scripts/develop",
"problemMatcher": []
}
]
}
12 changes: 12 additions & 0 deletions config/configuration.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
# https://www.home-assistant.io/integrations/default_config/
default_config:

# https://www.home-assistant.io/integrations/homeassistant/
homeassistant:
debug: true

# https://www.home-assistant.io/integrations/logger/
logger:
default: info
logs:
custom_components.ecoflow_cloud: debug
60 changes: 34 additions & 26 deletions custom_components/ecoflow_cloud/devices/public/data_bridge.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import logging
from typing import Any

_LOGGER = logging.getLogger(__name__)

Expand All @@ -17,40 +18,47 @@
status_to_plain = dict((v, k) for (k, v) in plain_to_status.items())


def to_plain(raw_data: dict[str, any]) -> dict[str, any]:
new_params = {}
def to_plain(raw_data: dict[str, Any]) -> dict[str, Any]:
prefix = ""

if "typeCode" in raw_data:
prefix1 = status_to_plain.get(
raw_data["typeCode"], "unknown_" + raw_data["typeCode"]
prefix = status_to_plain.get(
raw_data["typeCode"], f"unknown_{raw_data['typeCode']}"
)
prefix += f"{prefix1}."
elif "cmdFunc" in raw_data and "cmdId" in raw_data:
prefix += f"{raw_data['cmdFunc']}_{raw_data['cmdId']}."
else :
prefix += ""
prefix = f"{raw_data['cmdFunc']}_{raw_data['cmdId']}"

if "param" in raw_data:
for k, v in raw_data["param"].items():
new_params[f"{prefix}{k}"] = v
flat_params = {}

if "params" in raw_data:
for k, v in raw_data["params"].items():
new_params[f"{prefix}{k}"] = v
for section in ("param", "params"):
if section in raw_data:
section_data = flatten_any(raw_data[section])
for k, v in section_data.items():
full_key = f"{prefix}.{k}" if prefix else k
flat_params[full_key] = v

for k, v in raw_data.items():
if k != "param" and k != "params":
new_params[f"{prefix}{k}"] = v

new_params2 = {}
for k, v in new_params.items():
new_params2[k] = v
if isinstance(v, dict):
for k2, v2 in v.items():
new_params2[f"{k}.{k2}"] = v2

result = {"params": new_params2}
_LOGGER.debug(str(result))
if k not in ("param", "params"):
section_data = flatten_any(v, k)
for sk, sv in section_data.items():
full_key = f"{prefix}.{sk}" if prefix else sk
flat_params[full_key] = sv

result = {"params": flat_params}
_LOGGER.debug(result)
return result


def flatten_any(data, parent_key="", sep="."):
items = {}
if isinstance(data, dict):
for k, v in data.items():
new_key = f"{parent_key}{sep}{k}" if parent_key else k
items.update(flatten_any(v, new_key, sep=sep))
elif isinstance(data, list):
for i, v in enumerate(data):
new_key = f"{parent_key}[{i}]"
items.update(flatten_any(v, new_key, sep=sep))
else:
items[parent_key] = data
return items
112 changes: 112 additions & 0 deletions custom_components/ecoflow_cloud/devices/public/powerocean.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
from ...api import EcoflowApiClient
from .. import BaseDevice
from .data_bridge import to_plain

from ...sensor import (
VoltSensorEntity,
WattsSensorEntity,
LevelSensorEntity,
AmpSensorEntity,
SolarPowerSensorEntity,
SolarAmpSensorEntity,
SystemPowerSensorEntity,
StatusSensorEntity,
)
from ...entities import (
BaseSensorEntity,
BaseNumberEntity,
BaseSwitchEntity,
BaseSelectEntity,
)

import logging
from typing import Any

_LOGGER = logging.getLogger(__name__)


class PowerOcean(BaseDevice):
def flat_json(self):
return True

def sensors(self, client: EcoflowApiClient) -> list[BaseSensorEntity]:
return [
SolarPowerSensorEntity(client, self, "mpptPwr", "mpptPwr"),
LevelSensorEntity(client, self, "bpSoc", "bpSoc"),
WattsSensorEntity(client, self, "bpPwr", "bpPwr"),
SystemPowerSensorEntity(client, self, "sysLoadPwr", "sysLoadPwr"),
SystemPowerSensorEntity(client, self, "sysGridPwr", "sysGridPwr"),
# TODO: flatten Structure?
# String 1
SolarPowerSensorEntity(
client, self, "96_1.mpptHeartBeat[0].mpptPv[0].pwr", "mpptPv1.pwr"
),
SolarAmpSensorEntity(
client, self, "96_1.mpptHeartBeat[0].mpptPv[0].amp", "mpptPv1.amp"
),
VoltSensorEntity(
client, self, "96_1.mpptHeartBeat[0].mpptPv[0].vol", "mpptPv1.vol"
),
# String 2
SolarPowerSensorEntity(
client, self, "96_1.mpptHeartBeat[0].mpptPv[1].pwr", "mpptPv2.pwr"
),
SolarAmpSensorEntity(
client, self, "96_1.mpptHeartBeat[0].mpptPv[1].amp", "mpptPv2.amp"
),
VoltSensorEntity(
client, self, "96_1.mpptHeartBeat[0].mpptPv[1].vol", "mpptPv2.vol"
),
VoltSensorEntity(client, self, "96_1.pcsAPhase.vol", "pcsAPhase.vol"),
AmpSensorEntity(client, self, "96_1.pcsAPhase.amp", "pcsAPhase.amp"),
WattsSensorEntity(
client, self, "96_1.pcsAPhase.actPwr", "pcsAPhase.actPwr"
),
WattsSensorEntity(
client, self, "96_1.pcsAPhase.reactPwr", "pcsAPhase.reactPwr"
),
WattsSensorEntity(
client, self, "96_1.pcsAPhase.apparentPwr", "pcsAPhase.apparentPwr"
),
VoltSensorEntity(client, self, "96_1.pcsBPhase.vol", "pcsBPhase.vol"),
AmpSensorEntity(client, self, "96_1.pcsBPhase.amp", "pcsBPhase.amp"),
WattsSensorEntity(
client, self, "96_1.pcsBPhase.actPwr", "pcsBPhase.actPwr"
),
WattsSensorEntity(
client, self, "96_1.pcsBPhase.reactPwr", "pcsBPhase.reactPwr"
),
WattsSensorEntity(
client, self, "96_1.pcsBPhase.apparentPwr", "pcsBPhase.apparentPwr"
),
VoltSensorEntity(client, self, "96_1.pcsCPhase.vol", "pcsCPhase.vol"),
AmpSensorEntity(client, self, "96_1.pcsCPhase.amp", "pcsCPhase.amp"),
WattsSensorEntity(
client, self, "96_1.pcsCPhase.actPwr", "pcsCPhase.actPwr"
),
WattsSensorEntity(
client, self, "96_1.pcsCPhase.reactPwr", "pcsCPhase.reactPwr"
),
WattsSensorEntity(
client, self, "96_1.pcsCPhase.apparentPwr", "pcsCPhase.apparentPwr"
),
StatusSensorEntity(client, self),
]

def numbers(self, client: EcoflowApiClient) -> list[BaseNumberEntity]:
return []

def switches(self, client: EcoflowApiClient) -> list[BaseSwitchEntity]:
return []

def selects(self, client: EcoflowApiClient) -> list[BaseSelectEntity]:
return []

def _prepare_data(self, raw_data) -> dict[str, "Any"]:
res = super()._prepare_data(raw_data)
_LOGGER.info(f"_prepare_data {raw_data}")
res = to_plain(res)
return res

def _status_sensor(self, client: EcoflowApiClient) -> StatusSensorEntity:
return StatusSensorEntity(client, self)
2 changes: 2 additions & 0 deletions custom_components/ecoflow_cloud/devices/registry.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
powerkit as public_powerkit,
smart_meter as public_smart_meter,
stream_ac as public_stream_ac,
powerocean as public_powerocean,
)
from ..devices import BaseDevice, DiagnosticDevice

Expand Down Expand Up @@ -74,6 +75,7 @@
"Smart Meter": public_smart_meter.SmartMeter,
"Stream AC": public_stream_ac.StreamAC,
"Stream Ultra": public_stream_ac.StreamAC,
"PowerOcean": public_powerocean.PowerOcean, # key is not verified
"Diagnostic": DiagnosticDevice,
}
)
Expand Down
Loading