chore: initial baseline before refactoring
Initial snapshot of Home Assistant config (HAOS 2026.4.4)
prior to dashboard restructuring and helper YAML migration.
Pre-commit cleanup applied:
- Removed blueprints/switch_manager/ (9.4 MB stale, component already gone)
- Removed .storage/switch_manager
- Removed home-assistant.log.{1,old,fault}
- Removed zigbee2mqtt/configuration_backup_v{1..4}.yaml
- Removed zigbee2mqtt/database.db.tmp.2024-09-27 (1.5y old)
- Created empty themes/ to satisfy configuration.yaml include
.gitignore uses allowlist strategy for .storage/ to keep
all tokens, keys, and PINs out of version control.
This commit is contained in:
@@ -0,0 +1,86 @@
|
||||
"""__init__.py: The PowerOcean integration."""
|
||||
from __future__ import annotations
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import device_registry as dr
|
||||
|
||||
from .const import DOMAIN, PLATFORMS, _LOGGER, DOMAIN, ISSUE_URL_ERROR_MESSAGE, STARTUP_MESSAGE
|
||||
from .ecoflow import Ecoflow
|
||||
|
||||
|
||||
_LOGGER.info(STARTUP_MESSAGE)
|
||||
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
"""Set up PowerOcean from a config entry."""
|
||||
|
||||
# Setup DOMAIN as default
|
||||
hass.data.setdefault(DOMAIN, {})
|
||||
|
||||
# Setup device specific sensor list (used in updates) on HASS so it is available within integration (reuqired for unload)
|
||||
hass.data[DOMAIN]["device_specific_sensors"] = {}
|
||||
|
||||
# Store an instance of the API instance in hass.data[domain]
|
||||
user_input = entry.data["user_input"] # This user_input object was stored after the device
|
||||
# was setup and has the user/pass/serial info
|
||||
device_info = entry.data.get("device_info") # This device_info object was stored after the device
|
||||
# was setup and has the name and serial needed etc.
|
||||
options = entry.data["options"] # These are the options during setup, including custom device name
|
||||
ecoflow = Ecoflow(user_input["serialnumber"], user_input["username"], user_input["password"])
|
||||
|
||||
if device_info:
|
||||
ecoflow.device = device_info # Store the device information
|
||||
ecoflow.options = options # Store the options
|
||||
hass.data[DOMAIN][entry.entry_id] = ecoflow
|
||||
|
||||
# Forward to sensor platform
|
||||
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
||||
|
||||
# Get the device registry
|
||||
device_registry = dr.async_get(hass)
|
||||
|
||||
# Fetching device info from device registry
|
||||
# During the config flow, the device info is saved in the entry's data under 'device_info' key.
|
||||
device_info = entry.data.get("device_info")
|
||||
|
||||
# If the device_info was provided, register the device
|
||||
if device_info:
|
||||
device_registry.async_get_or_create(
|
||||
config_entry_id=entry.entry_id,
|
||||
identifiers={(DOMAIN, device_info["serial"])},
|
||||
manufacturer=device_info.get("vendor", "ECOFLOW"),
|
||||
serial_number=device_info.get("serial"),
|
||||
name=options.get("custom_device_name"), # Custom device name from user step 2 (options)
|
||||
model=device_info.get("product"),
|
||||
sw_version=device_info.get("version"),
|
||||
configuration_url="https://api-e.ecoflow.com",
|
||||
suggested_area="Boiler Room",
|
||||
)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
"""Unload a config entry."""
|
||||
# Unload all platforms associated with this entry
|
||||
unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
|
||||
|
||||
if unload_ok:
|
||||
# Clean up hass.data if any reference exists
|
||||
hass.data[DOMAIN].pop(entry.entry_id, None)
|
||||
|
||||
# Additionally, clear the device-specific sensors list if it exists
|
||||
device_id = entry.data.get("device_info").get("serial")
|
||||
device_name = entry.data.get("options").get("custom_device_name")
|
||||
if device_id in hass.data.get(DOMAIN, {}).get("device_specific_sensors", {}):
|
||||
hass.data[DOMAIN]["device_specific_sensors"].pop(device_id, None)
|
||||
_LOGGER.debug(
|
||||
f"{device_id}: Cleared sensor update list for device with custom name '{device_name}'"
|
||||
)
|
||||
|
||||
return unload_ok
|
||||
|
||||
|
||||
async def update_listener(hass: HomeAssistant, entry: ConfigEntry) -> None:
|
||||
await hass.config_entries.async_reload(entry.entry_id)
|
||||
@@ -0,0 +1,25 @@
|
||||
,
|
||||
"evSn",
|
||||
"workMode",
|
||||
"useGridFirst",
|
||||
"evOnoffSet",
|
||||
"errorCode",
|
||||
"evPhaseSet",
|
||||
"orderState",
|
||||
"orderTime",
|
||||
"orderEndTimestamp",
|
||||
"defaultVehicleId",
|
||||
"expectChargingEnergy",
|
||||
|
||||
"switchBits",
|
||||
"evDetectPhase",
|
||||
"orderStartTimestamp",
|
||||
"onlineBits",
|
||||
"updateTime",
|
||||
"evUserManual",
|
||||
"evChargingEnergy",
|
||||
"evCurrSet",
|
||||
"chargeVehicleId",
|
||||
"evPlugAndPlay",
|
||||
"evExeMode",
|
||||
"stopChargingSoc"
|
||||
@@ -0,0 +1,189 @@
|
||||
"""config_flow.py: Config flow for PowerOcean integration."""
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
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.exceptions import IntegrationError
|
||||
|
||||
from .const import _LOGGER, DOMAIN, ISSUE_URL_ERROR_MESSAGE
|
||||
from .ecoflow import Ecoflow, AuthenticationFailed
|
||||
|
||||
|
||||
# This is the first step's schema when setting up the integration, or its devices
|
||||
# The second schema is defined inside the ConfigFlow class as it has dynamic default values set via API call
|
||||
STEP_USER_DATA_SCHEMA = vol.Schema(
|
||||
{
|
||||
vol.Required("serialnumber", default=""): str,
|
||||
vol.Required("username", default=""): str,
|
||||
vol.Required("password", default=""): str,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
async def validate_input_for_device(hass: HomeAssistant, data: dict[str, Any]) -> dict[str, Any]:
|
||||
"""Validate the user input allows us to connect."""
|
||||
|
||||
ecoflow = Ecoflow(data["serialnumber"], data["username"], data["password"])
|
||||
|
||||
try:
|
||||
# Check for authentication
|
||||
# auth_check = await hass.async_add_executor_job(ecoflow.fetch_data) # TODO what else is needed from fetch_data?
|
||||
auth_check = await hass.async_add_executor_job(ecoflow.authorize)
|
||||
if not auth_check:
|
||||
# If authentication check returns False, raise an authentication failure exception
|
||||
raise AuthenticationFailed("Invalid authentication!")
|
||||
|
||||
# Get device info
|
||||
device = await hass.async_add_executor_job(ecoflow.get_device)
|
||||
|
||||
# Return the device object with the device information
|
||||
return device
|
||||
|
||||
# Exception if device cannot be found
|
||||
except IntegrationError as e:
|
||||
_LOGGER.error(f"Failed to connect to PowerOcean device: {e}" + ISSUE_URL_ERROR_MESSAGE)
|
||||
raise CannotConnect from e
|
||||
|
||||
# Exception if authentication fails
|
||||
except AuthenticationFailed as e:
|
||||
_LOGGER.error(f"Authentication failed: {e}" + ISSUE_URL_ERROR_MESSAGE)
|
||||
raise InvalidAuth from e
|
||||
|
||||
|
||||
class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
"""Handle a config flow for PowerOcean."""
|
||||
|
||||
VERSION = 1.3
|
||||
|
||||
# Make sure user input data is passed from one step to the next using user_input_from_step_user
|
||||
def __init__(self):
|
||||
self.user_input_from_step_user = None
|
||||
|
||||
# This is step 1 for the host/port/user/pass function.
|
||||
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:
|
||||
# Valide the user input whilst setting up integration or adding new devices.
|
||||
# validate_input_for_devices will try to detect the device and get more info from it,
|
||||
# and authenticate and deal with exceptions
|
||||
device = await validate_input_for_device(self.hass, user_input)
|
||||
except CannotConnect:
|
||||
errors["base"] = "cannot_connect"
|
||||
except InvalidAuth:
|
||||
errors["base"] = "invalid_auth"
|
||||
return self.async_show_form(step_id="user", data_schema=STEP_USER_DATA_SCHEMA, errors=errors)
|
||||
except Exception:
|
||||
_LOGGER.exception("Unexpected exception")
|
||||
errors["base"] = "unknown"
|
||||
else:
|
||||
# Create a unique device id as a combination of the device's product and its serial number (which should be unique on its own)
|
||||
# The host can be udpated if necessary, as the IP address of the device may have changed
|
||||
unique_id = f"{device['product']}_{device['serial']}"
|
||||
|
||||
# Checks that the device is actually unique, otherwise abort
|
||||
await self.async_set_unique_id(unique_id)
|
||||
# self._abort_if_unique_id_configured(
|
||||
# updates={"host": user_input["host"]}
|
||||
# )
|
||||
|
||||
# Before creating the entry in the config_entry registry, go to step 2 for the options
|
||||
# However, make sure the steps from the user input are passed on to the next step
|
||||
self.user_input_from_step_user = user_input
|
||||
self.device_info = device
|
||||
|
||||
# Now call the second step but set user_input to None for the first time to force data entry in step 2
|
||||
return await self.async_step_device_options(user_input=None)
|
||||
|
||||
# Show the form for step 1 with the user/host/pass as defined in STEP_USER_DATA_SCHEMA
|
||||
return self.async_show_form(step_id="user", data_schema=STEP_USER_DATA_SCHEMA, errors=errors)
|
||||
|
||||
# This is step 2 for the options such as custom name, group and disable sensors
|
||||
async def async_step_device_options(self, user_input: dict[str, Any] | None = None,) -> FlowResult:
|
||||
"""Handle the device options step."""
|
||||
errors: dict[str, str] = {}
|
||||
|
||||
if user_input is not None:
|
||||
try:
|
||||
# Sanitize the user provided custom device name, which is used for entry and device registry name
|
||||
user_input["custom_device_name"] = sanitize_device_name(
|
||||
user_input["custom_device_name"], self.device_info["name"]
|
||||
)
|
||||
|
||||
# Since we have already set the unique ID and updated host if necessary create
|
||||
# the entry with the additional options.
|
||||
# The title of the integration is the custom friendly device name given by the user in step 2
|
||||
title = user_input["custom_device_name"]
|
||||
return self.async_create_entry(title=title,
|
||||
data={
|
||||
"user_input": self.user_input_from_step_user, # from previous step
|
||||
"device_info": self.device_info, # from device detection
|
||||
"options": user_input, # new options from this step
|
||||
},
|
||||
)
|
||||
except Exception as e:
|
||||
_LOGGER.error(f"Failed to handle device options: {e}" + ISSUE_URL_ERROR_MESSAGE)
|
||||
errors["base"] = "option_error"
|
||||
|
||||
# Prepare the second form's schema as it has dynamic values based on the API call
|
||||
# Use the name from the detected device as default device name
|
||||
default_device_name = (
|
||||
self.device_info["name"]
|
||||
if self.device_info and "name" in self.device_info
|
||||
else "New Device"
|
||||
)
|
||||
step_device_options_schema = vol.Schema(
|
||||
{
|
||||
vol.Required("custom_device_name", default=default_device_name): str,
|
||||
vol.Required("polling_time", default=10): vol.All(
|
||||
vol.Coerce(int), vol.Clamp(min=5)
|
||||
),
|
||||
vol.Required("group_sensors", default=True): bool,
|
||||
vol.Required("disable_sensors", default=False): bool,
|
||||
}
|
||||
)
|
||||
|
||||
# Show the form for step 2 with the device name and other options as defined in STEP_DEVICE_OPTIONS_SCHEMA
|
||||
return self.async_show_form(
|
||||
step_id="device_options",
|
||||
data_schema=step_device_options_schema,
|
||||
errors={},
|
||||
)
|
||||
|
||||
|
||||
class CannotConnect(HomeAssistantError):
|
||||
"""Error to indicate we cannot connect."""
|
||||
|
||||
|
||||
class InvalidAuth(HomeAssistantError):
|
||||
"""Error to indicate there is invalid auth."""
|
||||
|
||||
|
||||
# Helper function to sanitize
|
||||
def sanitize_device_name(device_name: str, fall_back: str, max_length=255) -> str:
|
||||
# Trim whitespace
|
||||
name = device_name.strip()
|
||||
|
||||
# Remove special characters but keep spaces
|
||||
name = re.sub(r"[^\w\s-]", "", name)
|
||||
|
||||
# Replace multiple spaces with a single space
|
||||
name = re.sub(r"\s+", " ", name)
|
||||
|
||||
# Length check
|
||||
if len(name) > max_length:
|
||||
# Split at the last space to avoid cutting off in the middle of a word
|
||||
name = name[:max_length].rsplit(" ", 1)[0]
|
||||
|
||||
# Fallback name
|
||||
if not name:
|
||||
name = fall_back
|
||||
|
||||
return name
|
||||
@@ -0,0 +1,38 @@
|
||||
"""Constants for the PowerOcean integration."""
|
||||
|
||||
import logging
|
||||
|
||||
from homeassistant.const import Platform
|
||||
|
||||
DOMAIN = "powerocean"
|
||||
NAME = "Ecoflow PowerOcean"
|
||||
VERSION = "2024.08.27"
|
||||
ISSUE_URL = "https://github.com/niltrip/powerocean/issues"
|
||||
ISSUE_URL_ERROR_MESSAGE = " Please log any issues here: " + ISSUE_URL
|
||||
|
||||
|
||||
PLATFORMS: list[Platform] = [Platform.SENSOR]
|
||||
|
||||
_LOGGER = logging.getLogger(f"custom_components.{DOMAIN}")
|
||||
|
||||
ATTR_PRODUCT_DESCRIPTION = "Product Description"
|
||||
ATTR_DESTINATION_NAME = "Destination Name"
|
||||
ATTR_SOURCE_NAME = "Source Name"
|
||||
ATTR_UNIQUE_ID = "Internal Unique ID"
|
||||
ATTR_PRODUCT_VENDOR = "Vendor"
|
||||
ATTR_PRODUCT_SERIAL = "Vendor Product Serial"
|
||||
ATTR_PRODUCT_NAME = "Device Name"
|
||||
ATTR_PRODUCT_VERSION = "Vendor Firmware Version"
|
||||
ATTR_PRODUCT_BUILD = "Vendor Product Build"
|
||||
ATTR_PRODUCT_FEATURES = "Vendor Product Features"
|
||||
|
||||
|
||||
STARTUP_MESSAGE = f"""
|
||||
----------------------------------------------------------------------------
|
||||
{NAME}
|
||||
Version: {VERSION}
|
||||
Domain: {DOMAIN}
|
||||
If you have any issues with this custom component please open an issue here:
|
||||
{ISSUE_URL}
|
||||
----------------------------------------------------------------------------
|
||||
"""
|
||||
@@ -0,0 +1,72 @@
|
||||
{
|
||||
"data": [
|
||||
"sysLoadPwr",
|
||||
"sysGridPwr",
|
||||
"mpptPwr",
|
||||
"bpPwr",
|
||||
"online",
|
||||
"dcdcPwr",
|
||||
"todayElectricityGeneration",
|
||||
"monthElectricityGeneration",
|
||||
"yearElectricityGeneration",
|
||||
"totalElectricityGeneration",
|
||||
"systemName",
|
||||
"createTime"
|
||||
],
|
||||
"JTS1_ENERGY_STREAM_REPORT": [
|
||||
"pv1Pwr",
|
||||
"pvInvPwr",
|
||||
"pv2Pwr"
|
||||
],
|
||||
"JTS1_EMS_CHANGE_REPORT": [
|
||||
"bpTotalChgEnergy",
|
||||
"bpTotalDsgEnergy",
|
||||
"bpSoc",
|
||||
"bpOnlineSum",
|
||||
"emsCtrlLedBright"
|
||||
],
|
||||
"JTS1_BP_STA_REPORT": [
|
||||
"bpPwr",
|
||||
"bpSoc",
|
||||
"bpSoh",
|
||||
"bpVol",
|
||||
"bpAmp",
|
||||
"bpCycles",
|
||||
"bpSysState",
|
||||
"bpRemainWatth"
|
||||
],
|
||||
"JTS1_EMS_HEARTBEAT": [
|
||||
"bpRemainWatth",
|
||||
"emsBpAliveNum",
|
||||
"emsBpPower",
|
||||
"pcsActPwr",
|
||||
"pcsMeterPower"
|
||||
],
|
||||
"JTS1_EVCHARGING_REPORT": [
|
||||
"chargingStatus",
|
||||
"evPwr",
|
||||
"evSn",
|
||||
"workMode",
|
||||
"useGridFirst",
|
||||
"evOnoffSet",
|
||||
"errorCode",
|
||||
"evPhaseSet",
|
||||
"orderState",
|
||||
"orderTime",
|
||||
"orderEndTimestamp",
|
||||
"defaultVehicleId",
|
||||
"expectChargingEnergy",
|
||||
"switchBits",
|
||||
"evDetectPhase",
|
||||
"orderStartTimestamp",
|
||||
"onlineBits",
|
||||
"updateTime",
|
||||
"evUserManual",
|
||||
"evChargingEnergy",
|
||||
"evCurrSet",
|
||||
"chargeVehicleId",
|
||||
"evPlugAndPlay",
|
||||
"evExeMode",
|
||||
"stopChargingSoc"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,501 @@
|
||||
"""ecoflow.py: API for PowerOcean integration."""
|
||||
|
||||
import base64
|
||||
import re
|
||||
from collections import namedtuple
|
||||
from pathlib import Path
|
||||
|
||||
import requests
|
||||
from homeassistant.exceptions import IntegrationError
|
||||
from homeassistant.util.json import json_loads
|
||||
from requests.exceptions import RequestException
|
||||
|
||||
from .const import _LOGGER, ISSUE_URL_ERROR_MESSAGE, DOMAIN
|
||||
|
||||
# Mock path to response.json file
|
||||
mocked_response = Path("documentation/response.json")
|
||||
# Dict with entity names to use
|
||||
SENSSELECT = Path(f"custom_components/{DOMAIN}/datapoints.json")
|
||||
|
||||
# Better storage of PowerOcean endpoint
|
||||
PowerOceanEndPoint = namedtuple(
|
||||
"PowerOceanEndPoint",
|
||||
"internal_unique_id, serial, name, friendly_name, value, unit, description, icon",
|
||||
)
|
||||
|
||||
|
||||
# ecoflow_api to detect device and get device info, fetch the actual data from the PowerOcean device, and parse it
|
||||
# Rename, there is an official API since june
|
||||
class Ecoflow:
|
||||
"""Class representing Ecoflow"""
|
||||
|
||||
def __init__(self, serialnumber: str, username: str, password: str):
|
||||
self.sn = serialnumber
|
||||
self.unique_id = serialnumber
|
||||
self.ecoflow_username = username
|
||||
self.ecoflow_password = password
|
||||
self.token = None
|
||||
self.device = None
|
||||
self.session = requests.Session()
|
||||
self.url_iot_app = "https://api.ecoflow.com/auth/login"
|
||||
self.url_user_fetch = f"https://api-e.ecoflow.com/provider-service/user/device/detail?sn={self.sn}"
|
||||
|
||||
def get_device(self):
|
||||
"""Get device info."""
|
||||
self.device = {
|
||||
"product": "PowerOcean",
|
||||
"vendor": "Ecoflow",
|
||||
"serial": self.sn,
|
||||
"version": "5.1.25", # TODO: woher bekommt man diese Info?
|
||||
"build": "8", # TODO: wo finde ich das?
|
||||
"name": "PowerOcean",
|
||||
"features": "Photovoltaik",
|
||||
}
|
||||
|
||||
return self.device
|
||||
|
||||
def authorize(self):
|
||||
"""Function authorize"""
|
||||
auth_ok = False # default
|
||||
headers = {"lang": "en_US", "content-type": "application/json"}
|
||||
data = {
|
||||
"email": self.ecoflow_username,
|
||||
"password": base64.b64encode(self.ecoflow_password.encode()).decode(),
|
||||
"scene": "IOT_APP",
|
||||
"userType": "ECOFLOW",
|
||||
}
|
||||
|
||||
try:
|
||||
url = self.url_iot_app
|
||||
_LOGGER.info("Login to EcoFlow API %s", {url})
|
||||
request = requests.post(url, json=data, headers=headers, timeout=10)
|
||||
response = self.get_json_response(request)
|
||||
|
||||
except ConnectionError:
|
||||
error = f"Unable to connect to {self.url_iot_app}. Device might be offline."
|
||||
_LOGGER.warning(error + ISSUE_URL_ERROR_MESSAGE)
|
||||
raise IntegrationError(error)
|
||||
|
||||
try:
|
||||
self.token = response["data"]["token"]
|
||||
# self.user_id = response["data"]["user"]["userId"]
|
||||
# user_name = response["data"]["user"].get("name", "<no user name>")
|
||||
auth_ok = True
|
||||
except KeyError as key:
|
||||
raise Exception(f"Failed to extract key {key} from response: {response}")
|
||||
|
||||
_LOGGER.info("Successfully logged in.")
|
||||
|
||||
self.get_device() # collect device info
|
||||
|
||||
return auth_ok
|
||||
|
||||
def get_json_response(self, request):
|
||||
"""Function get json response"""
|
||||
if request.status_code != 200:
|
||||
raise Exception(
|
||||
f"Got HTTP status code {request.status_code}: {request.text}"
|
||||
)
|
||||
try:
|
||||
response = json_loads(request.text)
|
||||
response_message = response["message"]
|
||||
except KeyError as key:
|
||||
raise Exception(
|
||||
f"Failed to extract key {key} from {json_loads(request.text)}"
|
||||
)
|
||||
except Exception as error:
|
||||
raise Exception(f"Failed to parse response: {request.text} Error: {error}")
|
||||
|
||||
if response_message.lower() != "success":
|
||||
raise Exception(f"{response_message}")
|
||||
|
||||
return response
|
||||
|
||||
# Fetch the data from the PowerOcean device, which then constitues the Sensors
|
||||
def fetch_data(self):
|
||||
"""Fetch data from Url."""
|
||||
url = self.url_user_fetch
|
||||
try:
|
||||
headers = {
|
||||
"authorization": f"Bearer {self.token}",
|
||||
# "accept": "application/json, text/plain, */*",
|
||||
"user-agent": "Firefox/133.0",
|
||||
# "platform": "web",
|
||||
"product-type": "83",
|
||||
}
|
||||
request = requests.get(self.url_user_fetch, headers=headers, timeout=30)
|
||||
response = self.get_json_response(request)
|
||||
|
||||
try:
|
||||
# TESTING!!! create response file and use it as data
|
||||
with Path.open(mocked_response, "r", encoding="utf-8") as datei:
|
||||
response = json_loads(datei.read())
|
||||
except FileExistsError:
|
||||
error = f"Data from: {mocked_response}"
|
||||
_LOGGER.warning(error)
|
||||
except FileNotFoundError:
|
||||
error = f"Responsefile not present: {mocked_response}"
|
||||
_LOGGER.debug(error)
|
||||
|
||||
_LOGGER.debug(f"{response}")
|
||||
return self._get_sensors(response)
|
||||
|
||||
except ConnectionError:
|
||||
error = f"ConnectionError in fetch_data: Unable to connect to {url}. Device might be offline."
|
||||
_LOGGER.warning(error + ISSUE_URL_ERROR_MESSAGE)
|
||||
raise IntegrationError(error)
|
||||
|
||||
except RequestException as e:
|
||||
error = f"RequestException in fetch_data: Error while fetching data from {url}: {e}"
|
||||
_LOGGER.warning(error + ISSUE_URL_ERROR_MESSAGE)
|
||||
raise IntegrationError(error)
|
||||
|
||||
def __get_unit(self, key):
|
||||
"""Get unit from key name."""
|
||||
if key.endswith(("pwr", "Pwr", "Power")):
|
||||
unit = "W"
|
||||
elif key.endswith(("amp", "Amp")):
|
||||
unit = "A"
|
||||
elif key.endswith(("soc", "Soc", "soh", "Soh")):
|
||||
unit = "%"
|
||||
elif key.endswith(("vol", "Vol")):
|
||||
unit = "V"
|
||||
elif key.endswith(("Watth", "Energy")):
|
||||
unit = "Wh"
|
||||
elif "Generation" in key:
|
||||
unit = "kWh"
|
||||
elif key.startswith("bpTemp"):
|
||||
unit = "°C"
|
||||
else:
|
||||
unit = None
|
||||
|
||||
return unit
|
||||
|
||||
def __get_description(self, key):
|
||||
# TODO: hier könnte man noch mehr definieren bzw ein translation dict erstellen +1
|
||||
# Comment: Ich glaube hier brauchen wir n
|
||||
description = key # default description
|
||||
if key == "sysLoadPwr":
|
||||
description = "Hausnetz"
|
||||
if key == "sysGridPwr":
|
||||
description = "Stromnetz"
|
||||
if key == "mpptPwr":
|
||||
description = "Solarertrag"
|
||||
if key == "bpPwr":
|
||||
description = "Batterieleistung"
|
||||
if key == "bpSoc":
|
||||
description = "Ladezustand der Batterie"
|
||||
if key == "online":
|
||||
description = "Online"
|
||||
if key == "systemName":
|
||||
description = "System Name"
|
||||
if key == "createTime":
|
||||
description = "Installations Datum"
|
||||
# Battery descriptions
|
||||
if key == "bpVol":
|
||||
description = "Batteriespannung"
|
||||
if key == "bpAmp":
|
||||
description = "Batteriestrom"
|
||||
if key == "bpCycles":
|
||||
description = "Ladezyklen"
|
||||
if key == "bpTemp":
|
||||
description = "Temperatur der Batteriezellen"
|
||||
|
||||
return description
|
||||
|
||||
def __get_sens_select(self, report):
|
||||
# open File an read
|
||||
with Path.open(SENSSELECT) as file:
|
||||
config = json_loads(file.read())
|
||||
|
||||
return config[report]
|
||||
|
||||
def _get_sensors(self, response):
|
||||
# get sensors from response['data']
|
||||
sensors = self.__get_sensors_data(response)
|
||||
|
||||
# get sensors from 'JTS1_ENERGY_STREAM_REPORT'
|
||||
sensors = self.__get_sensors_energy_stream(response, sensors)
|
||||
|
||||
# get sensors from 'JTS1_EMS_CHANGE_REPORT'
|
||||
sensors = self.__get_sensors_ems_change(response, sensors)
|
||||
|
||||
# get info from batteries => JTS1_BP_STA_REPORT
|
||||
sensors = self.__get_sensors_battery(response, sensors)
|
||||
|
||||
# get info from PV strings => JTS1_EMS_HEARTBEAT
|
||||
sensors = self.__get_sensors_ems_heartbeat(response, sensors)
|
||||
|
||||
# get info from "JTS1_EVCHARGING_REPORT"
|
||||
sensors = self.__get_sensors_evcharging_report(response, sensors)
|
||||
|
||||
return sensors
|
||||
|
||||
def __get_sensors_data(self, response):
|
||||
d = response["data"].copy()
|
||||
|
||||
sens_select = self.__get_sens_select("data")
|
||||
|
||||
sensors = dict() # start with empty dict
|
||||
for key, value in d.items():
|
||||
if key in sens_select: # use only sensors in sens_select
|
||||
if not isinstance(value, dict):
|
||||
# default uid, unit and descript
|
||||
unique_id = f"{self.sn}_{key}"
|
||||
special_icon = None
|
||||
if key == "mpptPwr":
|
||||
special_icon = "mdi:solar-power"
|
||||
if key == "online":
|
||||
special_icon = "mdi:cloud-check"
|
||||
if key == "sysGridPwr":
|
||||
special_icon = "mdi:transmission-tower-import"
|
||||
if key == "sysLoadPwr":
|
||||
special_icon = "mdi:home-import-outline"
|
||||
|
||||
sensors[unique_id] = PowerOceanEndPoint(
|
||||
internal_unique_id=unique_id,
|
||||
serial=self.sn,
|
||||
name=f"{self.sn}_{key}",
|
||||
friendly_name=key,
|
||||
value=value,
|
||||
unit=self.__get_unit(key),
|
||||
description=self.__get_description(key),
|
||||
icon=special_icon,
|
||||
)
|
||||
|
||||
return sensors
|
||||
|
||||
def __get_sensors_evcharging_report(self, response, sensors):
|
||||
report = "JTS1_EVCHARGING_REPORT"
|
||||
#_LOGGER.warning(f"{report=}")
|
||||
d = response["data"]["quota"][report]
|
||||
sens_select = self.__get_sens_select(report)
|
||||
|
||||
data = {}
|
||||
for key, value in d.items():
|
||||
if key in sens_select:
|
||||
# default uid, unit and descript
|
||||
unique_id = f"{self.sn}_{report}_{key}"
|
||||
description_tmp = self.__get_description(key)
|
||||
data[unique_id] = PowerOceanEndPoint(
|
||||
internal_unique_id=unique_id,
|
||||
serial=self.sn,
|
||||
name=f"{self.sn}_{key}",
|
||||
friendly_name=key,
|
||||
value=value,
|
||||
unit=self.__get_unit(key),
|
||||
description=description_tmp,
|
||||
icon=None,
|
||||
)
|
||||
|
||||
dict.update(sensors, data)
|
||||
|
||||
return sensors
|
||||
|
||||
def __get_sensors_energy_stream(self, response, sensors):
|
||||
report = "JTS1_ENERGY_STREAM_REPORT"
|
||||
d = response["data"]["quota"][report]
|
||||
sens_select = self.__get_sens_select(report)
|
||||
|
||||
data = {}
|
||||
for key, value in d.items():
|
||||
if key in sens_select:
|
||||
# default uid, unit and descript
|
||||
unique_id = f"{self.sn}_{report}_{key}"
|
||||
special_icon = None
|
||||
if key.startswith(("pv1", "pv2")):
|
||||
special_icon = "mdi:solar-power"
|
||||
description_tmp = self.__get_description(key)
|
||||
data[unique_id] = PowerOceanEndPoint(
|
||||
internal_unique_id=unique_id,
|
||||
serial=self.sn,
|
||||
name=f"{self.sn}_{key}",
|
||||
friendly_name=key,
|
||||
value=value,
|
||||
unit=self.__get_unit(key),
|
||||
description=description_tmp,
|
||||
icon=special_icon,
|
||||
)
|
||||
|
||||
dict.update(sensors, data)
|
||||
|
||||
return sensors
|
||||
|
||||
def __get_sensors_ems_change(self, response, sensors):
|
||||
report = "JTS1_EMS_CHANGE_REPORT"
|
||||
d = response["data"]["quota"][report]
|
||||
|
||||
sens_select = self.__get_sens_select(report)
|
||||
|
||||
# add mppt Warning/Fault Codes
|
||||
keys = d.keys()
|
||||
r = re.compile("mppt.*Code")
|
||||
wfc = list(filter(r.match, keys)) # warning/fault code keys
|
||||
sens_select += wfc
|
||||
|
||||
data = {}
|
||||
for key, value in d.items():
|
||||
if key in sens_select: # use only sensors in sens_select
|
||||
# default uid, unit and descript
|
||||
unique_id = f"{self.sn}_{report}_{key}"
|
||||
|
||||
data[unique_id] = PowerOceanEndPoint(
|
||||
internal_unique_id=unique_id,
|
||||
serial=self.sn,
|
||||
name=f"{self.sn}_{key}",
|
||||
friendly_name=key,
|
||||
value=value,
|
||||
unit=self.__get_unit(key),
|
||||
description=self.__get_description(key),
|
||||
icon=None,
|
||||
)
|
||||
dict.update(sensors, data)
|
||||
|
||||
return sensors
|
||||
|
||||
def __get_sensors_battery(self, response, sensors):
|
||||
report = "JTS1_BP_STA_REPORT"
|
||||
d = response["data"]["quota"][report]
|
||||
keys = list(d.keys())
|
||||
|
||||
# loop over N batteries:
|
||||
batts = [s for s in keys if len(s) > 12]
|
||||
bat_sens_select = self.__get_sens_select(report)
|
||||
|
||||
data = {}
|
||||
prefix = "bpack"
|
||||
for ibat, bat in enumerate(batts):
|
||||
name = prefix + "%i_" % (ibat + 1)
|
||||
d_bat = json_loads(d[bat])
|
||||
for key, value in d_bat.items():
|
||||
if key in bat_sens_select:
|
||||
# default uid, unit and descript
|
||||
unique_id = f"{self.sn}_{report}_{bat}_{key}"
|
||||
description_tmp = f"{name}" + self.__get_description(key)
|
||||
special_icon = None
|
||||
if key == "bpAmp":
|
||||
special_icon = "mdi:current-dc"
|
||||
data[unique_id] = PowerOceanEndPoint(
|
||||
internal_unique_id=unique_id,
|
||||
serial=self.sn,
|
||||
name=f"{self.sn}_{name + key}",
|
||||
friendly_name=name + key,
|
||||
value=value,
|
||||
unit=self.__get_unit(key),
|
||||
description=description_tmp,
|
||||
icon=special_icon,
|
||||
)
|
||||
# compute mean temperature of cells
|
||||
key = "bpTemp"
|
||||
temp = d_bat[key]
|
||||
value = sum(temp) / len(temp)
|
||||
unique_id = f"{self.sn}_{report}_{bat}_{key}"
|
||||
description_tmp = f"{name}" + self.__get_description(key)
|
||||
data[unique_id] = PowerOceanEndPoint(
|
||||
internal_unique_id=unique_id,
|
||||
serial=self.sn,
|
||||
name=f"{self.sn}_{name + key}",
|
||||
friendly_name=name + key,
|
||||
value=value,
|
||||
unit=self.__get_unit(key),
|
||||
description=description_tmp,
|
||||
icon=None,
|
||||
)
|
||||
|
||||
dict.update(sensors, data)
|
||||
|
||||
return sensors
|
||||
|
||||
def __get_sensors_ems_heartbeat(self, response, sensors):
|
||||
report = "JTS1_EMS_HEARTBEAT"
|
||||
d = response["data"]["quota"][report]
|
||||
sens_select = self.__get_sens_select(report)
|
||||
|
||||
data = {}
|
||||
for key, value in d.items():
|
||||
if key in sens_select:
|
||||
# default uid, unit and descript
|
||||
unique_id = f"{self.sn}_{report}_{key}"
|
||||
description_tmp = self.__get_description(key)
|
||||
data[unique_id] = PowerOceanEndPoint(
|
||||
internal_unique_id=unique_id,
|
||||
serial=self.sn,
|
||||
name=f"{self.sn}_{key}",
|
||||
friendly_name=key,
|
||||
value=value,
|
||||
unit=self.__get_unit(key),
|
||||
description=description_tmp,
|
||||
icon=None,
|
||||
)
|
||||
|
||||
# special for phases
|
||||
phases = ["pcsAPhase", "pcsBPhase", "pcsCPhase"]
|
||||
if phases[1] in d:
|
||||
for i, phase in enumerate(phases):
|
||||
for key, value in d[phase].items():
|
||||
name = phase + "_" + key
|
||||
unique_id = f"{self.sn}_{report}_{name}"
|
||||
|
||||
data[unique_id] = PowerOceanEndPoint(
|
||||
internal_unique_id=unique_id,
|
||||
serial=self.sn,
|
||||
name=f"{self.sn}_{name}",
|
||||
friendly_name=f"{name}",
|
||||
value=value,
|
||||
unit=self.__get_unit(key),
|
||||
description=self.__get_description(key),
|
||||
icon=None,
|
||||
)
|
||||
|
||||
# special for mpptPv
|
||||
if "mpptHeartBeat" in d:
|
||||
n_strings = len(d["mpptHeartBeat"][0]["mpptPv"]) # TODO: auch als Sensor?
|
||||
mpptpvs = []
|
||||
for i in range(1, n_strings + 1):
|
||||
mpptpvs.append(f"mpptPv{i}")
|
||||
mpptPv_sum = 0.0
|
||||
for i, mpptpv in enumerate(mpptpvs):
|
||||
for key, value in d["mpptHeartBeat"][0]["mpptPv"][i].items():
|
||||
unique_id = f"{self.sn}_{report}_mpptHeartBeat_{mpptpv}_{key}"
|
||||
special_icon = None
|
||||
if key.endswith("amp"):
|
||||
special_icon = "mdi:current-dc"
|
||||
if key.endswith("pwr"):
|
||||
special_icon = "mdi:solar-power"
|
||||
|
||||
data[unique_id] = PowerOceanEndPoint(
|
||||
internal_unique_id=unique_id,
|
||||
serial=self.sn,
|
||||
name=f"{self.sn}_{mpptpv}_{key}",
|
||||
friendly_name=f"{mpptpv}_{key}",
|
||||
value=value,
|
||||
unit=self.__get_unit(key),
|
||||
description=self.__get_description(key),
|
||||
icon=special_icon,
|
||||
)
|
||||
# sum power of all strings
|
||||
if key == "pwr":
|
||||
mpptPv_sum += value
|
||||
|
||||
# create total power sensor of all strings
|
||||
name = "mpptPv_pwrTotal"
|
||||
unique_id = f"{self.sn}_{report}_mpptHeartBeat_{name}"
|
||||
|
||||
data[unique_id] = PowerOceanEndPoint(
|
||||
internal_unique_id=unique_id,
|
||||
serial=self.sn,
|
||||
name=f"{self.sn}_{name}",
|
||||
friendly_name=f"{name}",
|
||||
value=mpptPv_sum,
|
||||
unit=self.__get_unit(key),
|
||||
description="Solarertrag aller Strings",
|
||||
icon="mdi:solar-power",
|
||||
)
|
||||
|
||||
dict.update(sensors, data)
|
||||
|
||||
return sensors
|
||||
|
||||
|
||||
class AuthenticationFailed(Exception):
|
||||
"""Exception to indicate authentication failure."""
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 4.7 KiB |
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"domain": "powerocean",
|
||||
"name": "Ecoflow PowerOcean",
|
||||
"codeowners": [
|
||||
"@niltrip"
|
||||
],
|
||||
"config_flow": true,
|
||||
"documentation": "https://github.com/niltrip/powerocean",
|
||||
"iot_class": "cloud_polling",
|
||||
"issue_tracker": "https://github.com/niltrip/powerocean/issues",
|
||||
"version": "2025.01.07"
|
||||
}
|
||||
@@ -0,0 +1,635 @@
|
||||
{
|
||||
"code": "0",
|
||||
"message": "Success",
|
||||
"data": {
|
||||
"sysLoadPwr": 823,
|
||||
"sysGridPwr": -13,
|
||||
"mpptPwr": 0,
|
||||
"bpPwr": -836,
|
||||
"bpSoc": 88,
|
||||
"sysBatChgUpLimit": 0,
|
||||
"sysBatDsgDownLimit": 0,
|
||||
"sysGridSta": 0,
|
||||
"sysOnOffMachineStat": 0,
|
||||
"online": 1,
|
||||
"todayElectricityGeneration": "95.30",
|
||||
"monthElectricityGeneration": "192.67",
|
||||
"yearElectricityGeneration": "5298.98",
|
||||
"totalElectricityGeneration": "7243.73",
|
||||
"systemName": "Florian Krebs",
|
||||
"createTime": "2024-09-21 20:36:04",
|
||||
"location": "Germany-Bayern-Augsburg-Klostergasse 20a",
|
||||
"timezone": "Europe/Berlin",
|
||||
"quota": {
|
||||
"JTS1_ENERGY_STREAM_REPORT": {
|
||||
"bpSoc": 88,
|
||||
"mpptPwr": 0,
|
||||
"bpPwr": -836,
|
||||
"updateTime": "2025-05-03 03:15:22",
|
||||
"sysLoadPwr": 823,
|
||||
"sysGridPwr": -13,
|
||||
"pv1Pwr": 0,
|
||||
"pvInvPwr": 0,
|
||||
"pv2Pwr": 0
|
||||
},
|
||||
"JTS1_EMS_CHANGE_REPORT": {
|
||||
"sys14aEnable": false,
|
||||
"afciFaultMaxValueCh1": 0,
|
||||
"pcsHighVolOnGrid": 253,
|
||||
"bpChgDsgSta": 0,
|
||||
"pcsLowVolTime2": 300,
|
||||
"duration": 0,
|
||||
"pcsOngridReconnectFlag": 0,
|
||||
"emsSgReadyEn": false,
|
||||
"pcsLowFreqTime1": 100,
|
||||
"wireless4gIccid": "",
|
||||
"pcsSendEnd": 0,
|
||||
"sysBatDsgDownLimit": 15,
|
||||
"bpOnlineSum": 2,
|
||||
"emsWorkState": 0,
|
||||
"pcsOverVolDeratingSwitch": 0,
|
||||
"emsSgRunStat": 0,
|
||||
"mppt1WarningCode": 0,
|
||||
"pcsAntiBackFlowImportCurrentLimit": 0,
|
||||
"pcsAntiBackFlowSwitch": 1,
|
||||
"pcsOverFreqDeratingStartDelay": 0,
|
||||
"pcsActivePowerDeratingPercent": 1,
|
||||
"mppt1FaultCode": 2,
|
||||
"pcs10minOverVolTime": 100,
|
||||
"pcsOverVolTime1": 100,
|
||||
"pcsAvgOvpProtectValue": 0,
|
||||
"pcsLowVolOnGrid": 195.5,
|
||||
"afciFaultValueCh1": 0,
|
||||
"ethWanStat": 0,
|
||||
"pcsCospPf4": 0,
|
||||
"pcsAcWarningCode": 0,
|
||||
"pcs10minOverVol": 253,
|
||||
"bpLineOffFlag": 0,
|
||||
"pcsUnderFreqIncrementRecoverSlope": 0.09,
|
||||
"pcsReactPwrModeSelect": 0,
|
||||
"pcsPowerDeratingSet": 200,
|
||||
"pcsLowVolTime3": 240,
|
||||
"afciProtectValueCh2": 0,
|
||||
"devMaxPower": 0,
|
||||
"sysRateCtrlTime": 60,
|
||||
"pcsUnderFreqIncrementStart": 49.8,
|
||||
"afciFaultValueCh2": 0,
|
||||
"pcsLowVolRideThroughStart1": 184,
|
||||
"chgDsgMode": 0,
|
||||
"iot4gSta": 2,
|
||||
"pcsOfpProtectCnt": 0,
|
||||
"pcsCospP1": 0.1,
|
||||
"pcsFaultRecoverOnGridWaitTime": 60000,
|
||||
"batRelayCloseFailFlag": 0,
|
||||
"mppt2FaultCode": 2,
|
||||
"pcsLowVol2": 103.5,
|
||||
"endTimestamp": 0,
|
||||
"afciFaultFlagCh2": 0,
|
||||
"pcsFunctionEnable": 0,
|
||||
"pcsOverVolDeratingTimeConst": 10,
|
||||
"pcsLowVolRideThroughRecover": 195.5,
|
||||
"pcsLowFreqTime2": 100,
|
||||
"pcsLowVolRideThroughProtectTime3": 1000,
|
||||
"pcsLowVolRideThroughProtectTime2": 3000,
|
||||
"sysCalStat": 0,
|
||||
"pcsReactPwrPercent": 0,
|
||||
"pcsLowVolRideThroughProtectTime1": 5200,
|
||||
"pcsOverVolRideThroughStart1": 265.65,
|
||||
"pcsAutoTestPercent": 0,
|
||||
"pcsRelayStateShow": 13732943,
|
||||
"pcsOverVolDeratingStart": 253,
|
||||
"pcsQuQ3": 0,
|
||||
"pcsUvp2ProtectValue": 0,
|
||||
"pcsFaultRecoverHighFreqOnGrid": 50.1,
|
||||
"pcsPowerDeratingFlag": 5,
|
||||
"pcsOverFreqDeratingCutoffPower": 0,
|
||||
"pcsActivePowerSoftstartSwitch": 1,
|
||||
"pcsCospPf2": -1,
|
||||
"wifiStaStat": 0,
|
||||
"pcsFastCheck": 0,
|
||||
"pcsQuLockinPower": 0,
|
||||
"sysMulPeakSwitch": false,
|
||||
"pcsUvp1ProtectCnt": 0,
|
||||
"pcsOverFreqDeratingRecoverSlopeSwitch": 1,
|
||||
"pcsFaultRecoverLowFreqOnGrid": 47.53,
|
||||
"pcsCospPf1": -1,
|
||||
"iot4gErr": 7,
|
||||
"pcsCospP3": 1,
|
||||
"iot4gOn": 1,
|
||||
"pcsOverVolTime2": 100,
|
||||
"emsFeedPwr": 0,
|
||||
"afciEnSet": 0,
|
||||
"pcsOvpProtectCnt": 0,
|
||||
"pcsCospP4": 0,
|
||||
"bpReverseFlag": 0,
|
||||
"pcsActivePowerSoftstartTime": 666,
|
||||
"afciSelfTestCmdState": 0,
|
||||
"afciProtectValueCh1": 0,
|
||||
"parallelTypeCur": 0,
|
||||
"emsFeedRatio": 100,
|
||||
"emsCtrlLedBright": 10,
|
||||
"userRole": 0,
|
||||
"pcsOverFreqTime2": 100,
|
||||
"pcsOverVolDeratingStartingPower": 1,
|
||||
"afciFaultClearState": 0,
|
||||
"pcsFaultRecoverHighVolOnGrid": 253,
|
||||
"pcsUfpProtectCnt": 0,
|
||||
"sysStateBit": 0,
|
||||
"pcsOverVolTime3": 0,
|
||||
"pcsRunSta": "RUNSTA_RUN",
|
||||
"virtualHardEdition": 1,
|
||||
"afciSwitchFreqCh1": 0,
|
||||
"emsFeedMode": 1,
|
||||
"batRealyStatus": 2,
|
||||
"pcsQuQ1": 0.6,
|
||||
"pcsReconnectGridDetectSwitch": 1,
|
||||
"pcsUnderFreqIncrementRecoverSlopeSwitch": 1,
|
||||
"pcsRunFsmState": 8669282,
|
||||
"pcsHvrtLvrtSwitch": 1,
|
||||
"emsBackupEvent": 0,
|
||||
"pcsCospP2": 0.5,
|
||||
"pcsOfpProtectValue": 0,
|
||||
"sysOnOffMachineStat": 0,
|
||||
"pcsFaultRecoverLowVolOnGrid": 195.5,
|
||||
"pcsOverFreqDeratingRecoverSlope": 0.09,
|
||||
"parallelTypeSet": 0,
|
||||
"afciEnableCmdState": 0,
|
||||
"pcsLowVolRideThroughStart2": 103.5,
|
||||
"afciIsExist": 0,
|
||||
"afciEn": 0,
|
||||
"pcsUnderFreqIncrementEnd": 49.8,
|
||||
"pcsOverFreqDeratingFrozeSwitch": 0,
|
||||
"bpTotalDsgEnergy": 1368561,
|
||||
"bpTotalChgEnergy": 1419246,
|
||||
"sys14aType": 1,
|
||||
"rateCtrlSwtich": false,
|
||||
"pcsActivePowerNormalRampUpRate": 60,
|
||||
"pcsOverVolDeratingEnd": 257.6,
|
||||
"emsStopAll": 0,
|
||||
"updateTime": "2025-05-03 03:17:49",
|
||||
"bpSoc": 87,
|
||||
"pcsOnGridWaitTime": 60000,
|
||||
"pcsActivePowerDeratingSwitch": 0,
|
||||
"pcsReactPwrCompensation": 0.0062,
|
||||
"pcsOverVolRideThroughProtectTime2": 1000,
|
||||
"pcsQuMinimumCosphi": 0.4,
|
||||
"pcsPcsAntiBackFlowProtectSwitch": 0,
|
||||
"pcsVolRecoverTime": 1000,
|
||||
"pcsAutoTestState": 0,
|
||||
"pcsOverFreq1": 51.5,
|
||||
"parallelType": 0,
|
||||
"pcsActivePowerSoftStartRate": 0.1,
|
||||
"pcsLowVolTime1": 3000,
|
||||
"pcsOverVolDeratingEndPower": 0,
|
||||
"parallelAllowState": false,
|
||||
"sysHeatStat": 0,
|
||||
"bpRestartFlag": 1,
|
||||
"pcsOverVol2": 287.5,
|
||||
"pcsUnderFreqIncrementSlope": 0.4,
|
||||
"pcsAcErrCode": 0,
|
||||
"pcsOverFreqDeratingEndDelay": 0,
|
||||
"pcsLowVolRecover": 195.5,
|
||||
"pcsFreqRecoverTime": 1000,
|
||||
"pcs10minOverVolSwitch": 1,
|
||||
"sysMulPeakTime": 1200,
|
||||
"pcsOverVolDeratingDaleyTime": 0,
|
||||
"pcsOverFreqTime1": 100,
|
||||
"afciFaultCntCh1": 0,
|
||||
"pcsUvp1ProtectValue": 0,
|
||||
"pcsOverVolRideThroughProtectTime1": 5500,
|
||||
"pcsHighVolRideThroughRecover": 253,
|
||||
"pcsOverFreq2": 51.5,
|
||||
"pcsQuV2": 223.1,
|
||||
"pcsUnderFreqIncrementFrozeSwitch": 0,
|
||||
"pcsFreqLocalCommand": 1,
|
||||
"pcsHighFreqOnGrid": 50.1,
|
||||
"pcsUvp2ProtectCnt": 0,
|
||||
"pcsDcErrCode": 0,
|
||||
"pcsAntiBackFlowExportCurrentLimit": 0,
|
||||
"pcsOverVol1": 287.5,
|
||||
"sysTypeCfg": 0,
|
||||
"pcsOverVol3": 0,
|
||||
"pcsPfValue": 1,
|
||||
"sysMeterCfg": 0,
|
||||
"emsWordMode": "WORKMODE_SELFUSE",
|
||||
"pcsOverFreqDeratingPowerBased": 2,
|
||||
"relay14a": 0,
|
||||
"pcsLowVol1": 184,
|
||||
"emsCtrlLedType": 1,
|
||||
"afciFaultCntCh2": 0,
|
||||
"pcsOverVolRecover": 253,
|
||||
"afciFaultFlagCh1": 0,
|
||||
"pcsIslandDetectSwitch": 1,
|
||||
"pcsActivePowerGradient": 0.0033,
|
||||
"pcsOverVolRideThroughStart2": 287.5,
|
||||
"pcsOverFreqRecover": 50.1,
|
||||
"pcsQuQ2": 0,
|
||||
"afciSwitchFreqCh2": 0,
|
||||
"pcsCospPf3": -0.9,
|
||||
"pcsQuTimeConst": 10,
|
||||
"pcsQuV4": 246.1,
|
||||
"pcsLowFreq2": 47.5,
|
||||
"pcsLowVol3": 57.5,
|
||||
"pcsLowFreqRecover": 47.53,
|
||||
"pcsOvpProtectValue": 0,
|
||||
"pcsLowFreq1": 47.5,
|
||||
"pcsQuV1": 213.90001,
|
||||
"pcsAutoTestFlag": 0,
|
||||
"pcsOverFreqDeratingEnd": 50.2,
|
||||
"pcsQuQ4": -0.6,
|
||||
"pcsOverFreqDeratingSlope": 0.4,
|
||||
"pcsFreqExternalSignal": 0,
|
||||
"chgDsgPwr": 700,
|
||||
"pcsOverFreqDeratingSwitch": 0,
|
||||
"evBindList": {
|
||||
"evSn": [
|
||||
"AC31ZEH4AG130052"
|
||||
]
|
||||
},
|
||||
"pcsUfpProtectValue": 0,
|
||||
"pcsLowFreqOnGrid": 47.53,
|
||||
"mppt2WarningCode": 0,
|
||||
"pcsUnderFreqIncrementEndDelay": 0,
|
||||
"afciFaultMaxValueCh2": 0,
|
||||
"pcsUnderFreqIncrementSwitch": 0,
|
||||
"pcsQuV3": 236.9,
|
||||
"pcsQuLockoutPower": 0,
|
||||
"afciSellfTestResult": 0,
|
||||
"iot4gPdp": -1,
|
||||
"pcsLowVolRideThroughStart3": 34.5,
|
||||
"pcsSafetyCountryCodeSelection": 4,
|
||||
"pcsRelaySelfCheckSta": 0,
|
||||
"pcsAvgOvpProtectCnt": 0,
|
||||
"sysGridSta": 0,
|
||||
"batSoftRelayStatus": 0,
|
||||
"pcsUnderFreqIncrementStartDelay": 0,
|
||||
"pcsOverFreqDeratingStart": 50.2
|
||||
},
|
||||
"JTS1_BP_STA_REPORT": {
|
||||
"HJ3AZDH5ZG3G0490": "{\n \"bpPwr\": -0.8074197,\n \"bpSoc\": 88,\n \"bpSoh\": 100,\n \"bpTemp\": [29.0, 30.0, 29.0, 30.0, 30.0, 29.0, 29.0, 29.0, 29.0],\n \"bpCellMaxVol\": 3319.0,\n \"bpCellMinVol\": 3318.0,\n \"bpRunSta\": \"RUNSTA_RUN\",\n \"bpVol\": 53.066,\n \"bpAmp\": -0.015215387,\n \"bpBusVol\": 799.216,\n \"bpErrCode\": 0,\n \"bpCellVol\": [3319.0, 3319.0, 3318.0, 3318.0, 3318.0, 3319.0, 3318.0, 3318.0, 3318.0, 3318.0, 3318.0, 3318.0, 3318.0, 3319.0, 3318.0, 3318.0],\n \"bpDsrc\": 1,\n \"bpSn\": \"SEozQVpESDVaRzNHMDQ5MA==\",\n \"bpCycles\": 155,\n \"bpBalanceState\": 0,\n \"bpHvMosTemp\": 40.0,\n \"bpLvMosTemp\": 38.0,\n \"bpPtcTemp\": 29.0,\n \"bpHtsTemp\": 34.0,\n \"bpBusNegTemp\": 35.0,\n \"bpBusPosTemp\": 36.0,\n \"bpEnvTemp\": 35.0,\n \"bpAccuChgCap\": 13263555,\n \"bpAccuDsgCap\": 13246290,\n \"bpDesignCap\": 100000,\n \"bpFullCap\": 100000,\n \"bpMaxCellTemp\": 30.0,\n \"bpMinCellTemp\": 29.0,\n \"bpMaxMosTemp\": 40.0,\n \"bpMinMosTemp\": 38.0,\n \"bpBmsFault\": 0,\n \"bpEcloundSoc\": 65535,\n \"bpHeartbeatVer\": 33,\n \"bpTimestamp\": 1746213449,\n \"bpRealSoc\": ...",
|
||||
"updateTime": "2025-05-03 03:17:39",
|
||||
"HJ3AZDH5ZG3G0384": "{\n \"bpPwr\": -841.428,\n \"bpSoc\": 88,\n \"bpSoh\": 100,\n \"bpTemp\": [30.0, 31.0, 31.0, 31.0, 31.0, 30.0, 30.0, 31.0, 31.0],\n \"bpCellMaxVol\": 3315.0,\n \"bpCellMinVol\": 3313.0,\n \"bpRunSta\": \"RUNSTA_RUN\",\n \"bpVol\": 53.005,\n \"bpAmp\": -15.874502,\n \"bpBusVol\": 798.7922,\n \"bpErrCode\": 0,\n \"bpCellVol\": [3315.0, 3314.0, 3314.0, 3314.0, 3314.0, 3313.0, 3313.0, 3314.0, 3313.0, 3314.0, 3314.0, 3314.0, 3314.0, 3314.0, 3314.0, 3314.0],\n \"bpDsrc\": 2,\n \"bpSn\": \"SEozQVpESDVaRzNHMDM4NA==\",\n \"bpCycles\": 160,\n \"bpBalanceState\": 0,\n \"bpHvMosTemp\": 38.0,\n \"bpLvMosTemp\": 37.0,\n \"bpPtcTemp\": 31.0,\n \"bpHtsTemp\": 35.0,\n \"bpBusNegTemp\": 35.0,\n \"bpBusPosTemp\": 36.0,\n \"bpEnvTemp\": 35.0,\n \"bpAccuChgCap\": 13259968,\n \"bpAccuDsgCap\": 13221698,\n \"bpDesignCap\": 100000,\n \"bpFullCap\": 100000,\n \"bpMaxCellTemp\": 31.0,\n \"bpMinCellTemp\": 30.0,\n \"bpMaxMosTemp\": 38.0,\n \"bpMinMosTemp\": 37.0,\n \"bpBmsFault\": 0,\n \"bpEcloundSoc\": 65535,\n \"bpHeartbeatVer\": 33,\n \"bpTimestamp\": 1746213455,\n \"bpRealSoc\": 88....",
|
||||
"": "{\n \"bpTemp\": [],\n \"bpCellVol\": []\n}"
|
||||
},
|
||||
"JTS1_LOGY_DEV_REPORT": {
|
||||
"HPReport": {
|
||||
"devSn": "",
|
||||
"online": 0,
|
||||
"errorCode": ""
|
||||
},
|
||||
"updateTime": "2025-05-03 03:12:27"
|
||||
},
|
||||
"JTS1_EMS_PARAM_CHANGE_REPORT": {
|
||||
"smartCtrl": false,
|
||||
"updateTime": "2025-05-03 03:12:27",
|
||||
"breakerCapacityMax": 32,
|
||||
"energyEfficientEnable": false,
|
||||
"breakerEnableState": true,
|
||||
"bpBurst": false,
|
||||
"lowerPowerStat": false,
|
||||
"sysZone": 0,
|
||||
"sysTimeTab": 0,
|
||||
"devSoc": 80
|
||||
},
|
||||
"JTS1_ERROR_CODE_MASK_REPORT": {
|
||||
"errorCode": [
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0
|
||||
],
|
||||
"updateTime": "2025-05-03 02:01:35"
|
||||
},
|
||||
"JTS1_EDEV_BIND_LIST_REPORT": {
|
||||
"213": "{\n \"devAddr\": 213,\n \"devItem\": []\n}",
|
||||
"214": "{\n \"devAddr\": 214,\n \"devItem\": []\n}",
|
||||
"updateTime": "2025-05-03 02:01:37"
|
||||
},
|
||||
"JTS1_EMS_ALL_TIMER_TASK_REPORT": {
|
||||
"timeTaskCfg": [],
|
||||
"updateTime": "2025-05-02 15:34:18"
|
||||
},
|
||||
"JT303_EDEV_SYS_REPORT": {
|
||||
"devLastInfo": 31,
|
||||
"devInfoFix": {
|
||||
"devSubInfoFix": [
|
||||
{
|
||||
"devAttrCur": 197424,
|
||||
"bindType": 1,
|
||||
"devInfo": {
|
||||
"devAddr": 209,
|
||||
"devSn": "AC31ZEH4AG130052",
|
||||
"subAddr": 1
|
||||
},
|
||||
"devFlag": 131175,
|
||||
"errorCode": 0,
|
||||
"onlineBits": 2,
|
||||
"modbusAddr": 52,
|
||||
"warnCode": 0,
|
||||
"memDevIdx": 0,
|
||||
"targetMax": 11000,
|
||||
"faultCode": 0,
|
||||
"sceneType": 3,
|
||||
"currentPrio": 1,
|
||||
"devStateInfo": 5
|
||||
}
|
||||
]
|
||||
},
|
||||
"freeNum": 35,
|
||||
"devInfoVar": {
|
||||
"devSubInfoVar": [
|
||||
{
|
||||
"allocatedPower": 0,
|
||||
"realPowerLock": 0,
|
||||
"refPower": 0,
|
||||
"devInfo": {
|
||||
"devAddr": 209,
|
||||
"devSn": "AC31ZEH4AG130052",
|
||||
"subAddr": 1
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"updateTime": "2025-05-03 03:11:48",
|
||||
"usedNum": 1,
|
||||
"sysFlag": 35,
|
||||
"devLastMinInfo": 31,
|
||||
"socDev": 80,
|
||||
"dispatchType": 2,
|
||||
"socCur": 88.93504,
|
||||
"stratType": 1,
|
||||
"solarFlag": 985,
|
||||
"devFirstInfo": 32,
|
||||
"startState": 9,
|
||||
"pclPwrBase": -851.92706,
|
||||
"feedPwrCap": 12000
|
||||
},
|
||||
"JTS1_EMS_HEARTBEAT": {
|
||||
"emsBusVoltRipple": 1.241394,
|
||||
"pcsInterruptOccupancyRate": 88,
|
||||
"bpRemainWatth": 9355,
|
||||
"emsStartFsmState": 1078644260,
|
||||
"emsAcMakeupMinSoc": 0,
|
||||
"pcsActivePowerLimitDn": -12000,
|
||||
"emsMpptStartupState": 1,
|
||||
"pcsAverageVoltage": 1.0132734,
|
||||
"updateTime": "2025-05-03 03:18:33",
|
||||
"pcsActivePowerRef": 663.75104,
|
||||
"pcsAPhase": {
|
||||
"vol": 232.77829,
|
||||
"amp": 1.2337433,
|
||||
"actPwr": -220.07922,
|
||||
"reactPwr": 184.506,
|
||||
"apparentPwr": 287.18866
|
||||
},
|
||||
"pcsRelayStateShow": 13732943,
|
||||
"emsMpptRunState": 0,
|
||||
"pcsBpPower": -816.97534,
|
||||
"pcsVbusRef": 770,
|
||||
"pcsVgridThd": 0.0016662398,
|
||||
"pcsLoadInfo": [
|
||||
{
|
||||
"vol": 232.77829,
|
||||
"amp": 0,
|
||||
"freq": 49.990864,
|
||||
"pwr": 0
|
||||
},
|
||||
{
|
||||
"vol": 232.9028,
|
||||
"amp": 0,
|
||||
"freq": 49.99184,
|
||||
"pwr": 0
|
||||
},
|
||||
{
|
||||
"vol": 232.37206,
|
||||
"amp": 0,
|
||||
"freq": 49.99131,
|
||||
"pwr": 0
|
||||
}
|
||||
],
|
||||
"emsBpSelfcheckState": 1,
|
||||
"meterHeartBeat": [
|
||||
{
|
||||
"meterAddr": 106,
|
||||
"meterType": 15,
|
||||
"meterData": [
|
||||
-135.6,
|
||||
234.4,
|
||||
-110.5,
|
||||
-11.700012
|
||||
]
|
||||
}
|
||||
],
|
||||
"pcsActPwr": -656.1483,
|
||||
"emsBpStartupState": 1,
|
||||
"pcsDci": 0.0012805257,
|
||||
"emsBpPower": -813.38104,
|
||||
"emsBusVoltErrSlidFilter": 0.66655725,
|
||||
"emsLpStateFlag": 0,
|
||||
"pcsBpPowerChgLimit": 4999.1465,
|
||||
"emsBpChgRequest": 0,
|
||||
"pcsAcFreq": 49.99,
|
||||
"pcsGridSafetyFuncRecord": 234905714,
|
||||
"emsMpptSelfcheckState": 1,
|
||||
"pcsBusVolt": 797.13464,
|
||||
"emsPvInvPwr": 0,
|
||||
"pcsActivePowerLimitUp": 663.75104,
|
||||
"mpptHeartBeat": [
|
||||
{
|
||||
"mpptPv": [
|
||||
{
|
||||
"vol": 42.0932,
|
||||
"amp": 0.05277126,
|
||||
"lightSta": true,
|
||||
"pwr": 2.221311
|
||||
},
|
||||
{
|
||||
"vol": 41.54573,
|
||||
"amp": 0.08966458,
|
||||
"lightSta": true,
|
||||
"pwr": 3.7251804
|
||||
}
|
||||
],
|
||||
"mpptTempVal": [
|
||||
41.2,
|
||||
43.5,
|
||||
43.1
|
||||
],
|
||||
"mpptInsResist": 0
|
||||
}
|
||||
],
|
||||
"emsBpChg": 5000.0005,
|
||||
"emsMpptHbState": 17,
|
||||
"emsLpMpptCnt": 0,
|
||||
"pcsMeterPower": -13.199997,
|
||||
"emsAcMakeupCnt": 0,
|
||||
"emsBpDsg": -6600,
|
||||
"emsLpState": 0,
|
||||
"emsLpType": 0,
|
||||
"emsNtcTempMax": 43.5,
|
||||
"emsAcMakeupTriggleSoc": 17,
|
||||
"emsLpBpCnt": 0,
|
||||
"pcsBPhase": {
|
||||
"vol": 232.9028,
|
||||
"amp": 1.2401944,
|
||||
"actPwr": -218.97543,
|
||||
"reactPwr": 188.36415,
|
||||
"apparentPwr": 288.84476
|
||||
},
|
||||
"pcsGridSafetyStateRecord": 9486428,
|
||||
"emsMpptModStat": 1,
|
||||
"pcsReactivePowerRef": 0,
|
||||
"pcsDcv": 0,
|
||||
"emsBpAliveNum": 2,
|
||||
"emsAcMakeupExitSoc": 19,
|
||||
"emsBusVolt": 800.071,
|
||||
"emsActiveOffGridCmd": 0,
|
||||
"emsSocCalibState": 0,
|
||||
"pcsGridInvErrorRms": 6.710472,
|
||||
"emsSysCfg": 7,
|
||||
"bpDsgTime": 672,
|
||||
"pcsLeakAmp": -6.4453125,
|
||||
"pcsCommInterfaceState": 17120,
|
||||
"emsSocCalibRequest": 0,
|
||||
"emsPcsSelfcheckState": 1,
|
||||
"pcsCPhase": {
|
||||
"vol": 232.37206,
|
||||
"amp": 1.2358344,
|
||||
"actPwr": -217.09364,
|
||||
"reactPwr": 187.98643,
|
||||
"apparentPwr": 287.17337
|
||||
},
|
||||
"emsPcsStartupState": 1
|
||||
},
|
||||
"JTS1_EMS_PV_INV_ENERGY_STREAM_REPORT": {
|
||||
"pvInvPwr": 0,
|
||||
"updateTime": "2025-05-03 03:15:22"
|
||||
},
|
||||
"JT303_EDEV_PRIORITY_LIST_REPORT": {
|
||||
"devPrioReport": [
|
||||
{
|
||||
"devPrio": 1,
|
||||
"data": "Aw==",
|
||||
"enable": true,
|
||||
"devInfo": {
|
||||
"devAddr": 209,
|
||||
"devSn": "AC31ZEH4AG130052",
|
||||
"subAddr": 1
|
||||
}
|
||||
}
|
||||
],
|
||||
"updateTime": "2025-05-03 02:11:24"
|
||||
},
|
||||
"JTS1_EVCHARGING_REPORT": {
|
||||
"evSn": "AC31ZEH4AG130052",
|
||||
"workMode": 2,
|
||||
"useGridFirst": 1,
|
||||
"evOnoffSet": 0,
|
||||
"errorCode": "AAAAAAAAAAA=",
|
||||
"evPhaseSet": 1,
|
||||
"orderState": 2,
|
||||
"orderTime": 29670,
|
||||
"orderEndTimestamp": 1746213532,
|
||||
"defaultVehicleId": "534",
|
||||
"chargingStatus": "EV_CHG_STS_SUSPENDED_EVSE",
|
||||
"expectChargingEnergy": 0,
|
||||
"evPwr": 0,
|
||||
"switchBits": 0,
|
||||
"evDetectPhase": 3,
|
||||
"orderStartTimestamp": 1746183862,
|
||||
"onlineBits": 2,
|
||||
"updateTime": "2025-05-03 03:18:53",
|
||||
"evUserManual": 0,
|
||||
"evChargingEnergy": 17256,
|
||||
"evCurrSet": 0,
|
||||
"chargeVehicleId": "534",
|
||||
"evPlugAndPlay": 1,
|
||||
"evExeMode": 2,
|
||||
"stopChargingSoc": 255
|
||||
},
|
||||
"JTS1_ERROR_CHANGE_REPORT": {
|
||||
"bpErrCode": [
|
||||
{
|
||||
"errCode": [],
|
||||
"moduleSn": "SEozQVpESDVaRzNHMDQ5MA=="
|
||||
},
|
||||
{
|
||||
"errCode": [],
|
||||
"moduleSn": "SEozQVpESDVaRzNHMDM4NA=="
|
||||
}
|
||||
],
|
||||
"emsErrCode": {
|
||||
"errCode": [],
|
||||
"moduleSn": "SEozN1pESDVaRzVXMDEwOQ=="
|
||||
},
|
||||
"pcsErrCode": {
|
||||
"errCode": [],
|
||||
"moduleSn": "SEozMTIxMDJCRzRKMTMzMQ=="
|
||||
},
|
||||
"updateTime": "2025-05-03 03:12:28"
|
||||
},
|
||||
"JTS1_ECOLOGY_DEV_BIND_LIST_REPORT": {
|
||||
"devItem": [],
|
||||
"updateTime": "2025-05-03 02:01:36"
|
||||
},
|
||||
"JTS1_EV_CHARGING_ENERGY_STREAM_REPORT": {
|
||||
"updateTime": "2025-05-03 03:15:24",
|
||||
"evStreamShow": [
|
||||
{
|
||||
"evSn": "AC31ZEH4AG130052",
|
||||
"evPwr": 0
|
||||
}
|
||||
]
|
||||
},
|
||||
"JTS1_EV_CHARGING_TIMER_TASK_REPORT": {
|
||||
"timeTaskCfg": [
|
||||
{
|
||||
"taskIndex": 1,
|
||||
"timeParam": 0,
|
||||
"param": 16,
|
||||
"isCfg": 0,
|
||||
"isEffect": false,
|
||||
"timeMode": 65,
|
||||
"timeTable": [
|
||||
19660800
|
||||
],
|
||||
"type": 1,
|
||||
"isEnable": false
|
||||
}
|
||||
],
|
||||
"evSn": "AC31ZEH4AG130052",
|
||||
"updateTime": "2025-05-03 02:01:36"
|
||||
}
|
||||
}
|
||||
},
|
||||
"eagleEyeTraceId": "ea1a2a582917462135513308071d0007",
|
||||
"tid": ""
|
||||
}
|
||||
@@ -0,0 +1,375 @@
|
||||
from datetime import timedelta
|
||||
from collections import defaultdict
|
||||
|
||||
from homeassistant.components.sensor import SensorEntity
|
||||
from homeassistant.components.sensor import SensorDeviceClass
|
||||
from homeassistant.components.sensor import SensorStateClass
|
||||
from homeassistant.helpers.entity import EntityCategory
|
||||
from homeassistant.helpers.event import async_track_time_interval
|
||||
from homeassistant.helpers import entity_registry
|
||||
from homeassistant.exceptions import IntegrationError
|
||||
|
||||
from .const import (
|
||||
DOMAIN,
|
||||
_LOGGER,
|
||||
ATTR_PRODUCT_DESCRIPTION,
|
||||
ATTR_DESTINATION_NAME,
|
||||
ATTR_SOURCE_NAME,
|
||||
ATTR_UNIQUE_ID,
|
||||
ATTR_PRODUCT_SERIAL,
|
||||
ATTR_PRODUCT_NAME,
|
||||
ATTR_PRODUCT_VENDOR,
|
||||
ATTR_PRODUCT_BUILD,
|
||||
ATTR_PRODUCT_VERSION,
|
||||
ATTR_PRODUCT_FEATURES,
|
||||
ISSUE_URL_ERROR_MESSAGE,
|
||||
)
|
||||
|
||||
from .ecoflow import Ecoflow, AuthenticationFailed
|
||||
|
||||
|
||||
# Setting up the adding and updating of sensor entities
|
||||
async def async_setup_entry(hass, config_entry, async_add_entities):
|
||||
# Retrieve the API instance from the config_entry data
|
||||
ecoflow = hass.data[DOMAIN][config_entry.entry_id]
|
||||
device_id = ecoflow.device["serial"]
|
||||
|
||||
# Call EcoFlow to get access to the API data
|
||||
try:
|
||||
auth_check = await hass.async_add_executor_job(ecoflow.authorize)
|
||||
|
||||
if not auth_check:
|
||||
# If device returns False or is empty, log an error and return
|
||||
_LOGGER.warning(
|
||||
f"{device_id}: It appears the PowerOcean device is offline or has changed host."
|
||||
+ ISSUE_URL_ERROR_MESSAGE
|
||||
)
|
||||
|
||||
except AuthenticationFailed as error:
|
||||
_LOGGER.warning(f"{device_id}: Authentication failed: {error}")
|
||||
return
|
||||
|
||||
try:
|
||||
# Fetch the sensor data from the device
|
||||
data = await hass.async_add_executor_job(ecoflow.fetch_data)
|
||||
|
||||
if not data:
|
||||
# If data returns False or is empty, log an error and return
|
||||
_LOGGER.warning(
|
||||
f"{device_id}: Failed to fetch sensor data => authentication failed or no data."
|
||||
+ ISSUE_URL_ERROR_MESSAGE
|
||||
)
|
||||
return
|
||||
|
||||
# Exception if data cannot be fetched
|
||||
except IntegrationError as error:
|
||||
_LOGGER.warning(
|
||||
f"{device_id}: Failed to fetch sensor data: {error}"
|
||||
+ ISSUE_URL_ERROR_MESSAGE
|
||||
)
|
||||
return
|
||||
|
||||
# Get device id and then reset the device specific list of sensors for updates
|
||||
# to ensure it's empty before adding new entries
|
||||
|
||||
# Initialize or clear the sensor list for this device
|
||||
hass.data[DOMAIN]["device_specific_sensors"][device_id] = []
|
||||
|
||||
# Register entities and add them to the list for schedule updates on each device
|
||||
# which is stored within hass.data
|
||||
for unique_id, endpoint in data.items():
|
||||
# Get individual sensor entry from API
|
||||
sensor = PowerOceanSensor(ecoflow, endpoint)
|
||||
|
||||
# Add sensors to the device specific list of sensors to be updated, via hass.data as also used in unload
|
||||
hass.data[DOMAIN]["device_specific_sensors"][device_id].append(sensor)
|
||||
|
||||
# Register sensor
|
||||
async_add_entities([sensor], False)
|
||||
|
||||
device_specific_sensors = hass.data[DOMAIN]["device_specific_sensors"]
|
||||
_LOGGER.debug(
|
||||
f"{device_id}: List of device_specific_sensors[device_id]: "
|
||||
f"{device_specific_sensors[device_id]}"
|
||||
)
|
||||
|
||||
# Log the number of sensors registered (and added to the update list)
|
||||
_LOGGER.debug(
|
||||
f"{device_id}: All '{len(device_specific_sensors[device_id])}' sensors have registered."
|
||||
)
|
||||
|
||||
# Schedule updates
|
||||
async def async_update_data(now):
|
||||
# If device deleted but HASS not restarted, then don't bother continuing
|
||||
if device_id not in hass.data.get(DOMAIN, {}).get(
|
||||
"device_specific_sensors", {}
|
||||
):
|
||||
return False
|
||||
|
||||
_LOGGER.debug(f"{device_id}: Preparing to update sensors at {now}")
|
||||
|
||||
# Fetch the full dataset once from the API
|
||||
try:
|
||||
full_data = await hass.async_add_executor_job(ecoflow.fetch_data)
|
||||
|
||||
except Exception as e:
|
||||
_LOGGER.error(
|
||||
f"{device_id}: Error fetching data from the device: {e}"
|
||||
+ ISSUE_URL_ERROR_MESSAGE
|
||||
)
|
||||
return
|
||||
|
||||
# Fetch the registry and check if sensors are enabled
|
||||
registry = entity_registry.async_get(hass)
|
||||
|
||||
# Set counters to zero
|
||||
counter_updated = 0 # Successfully updated sensors
|
||||
counter_disabled = 0 # Disabled sensors, not to be updated
|
||||
counter_unchanged = 0 # Skipped sensors since value has not changed
|
||||
counter_error = 0 # Skipped sensors due to some error, such as registry not found or no data from API
|
||||
|
||||
# Get the list of device specific sensors from hass.data
|
||||
if device_id in hass.data.get(DOMAIN, {}).get("device_specific_sensors", {}):
|
||||
device_specific_sensors = hass.data[DOMAIN]["device_specific_sensors"]
|
||||
|
||||
# ----------------------------------------------
|
||||
# Now loop through the sensors to be updated
|
||||
# ----------------------------------------------
|
||||
for sensor in device_specific_sensors[device_id]:
|
||||
entity_id = registry.async_get_entity_id(
|
||||
"sensor", DOMAIN, sensor.unique_id
|
||||
)
|
||||
if entity_id:
|
||||
entity = registry.entities.get(entity_id) # get entity
|
||||
# entity is enabled
|
||||
if entity and not entity.disabled_by:
|
||||
sensor_data = full_data.get(sensor.unique_id)
|
||||
# _LOGGER.debug(f"{device_id}: Sensor {sensor.name} enabled.")
|
||||
if sensor_data:
|
||||
# _LOGGER.debug(
|
||||
# f"{device_id}: Sensor {sensor.name} has API data to update {sensor_data}"
|
||||
# )
|
||||
|
||||
# Check if current state value differs from new API value,
|
||||
# or current state has not initialized
|
||||
if (
|
||||
str(sensor._state).strip()
|
||||
!= str(sensor_data.value).strip()
|
||||
):
|
||||
# _LOGGER.debug(
|
||||
# f"{device_id}: Sensor {sensor.name} marked for update: current state = "
|
||||
# f"{sensor._state} with new value = {sensor_data.value}"
|
||||
# )
|
||||
# Now update the sensor with new values
|
||||
# update_status returns 1 for upated, 0 for skipped or error
|
||||
update_status = await sensor.async_update(sensor_data)
|
||||
counter_updated = counter_updated + update_status
|
||||
else:
|
||||
# _LOGGER.debug(
|
||||
# f"{device_id}: Sensor {sensor.name} skipped update! Current value = "
|
||||
# f"{sensor._state}, new value = {sensor_data.value}"
|
||||
# )
|
||||
counter_unchanged = counter_unchanged + 1
|
||||
else:
|
||||
_LOGGER.warning(
|
||||
f"{device_id}: Sensor {sensor.name}: found no data for update!"
|
||||
+ ISSUE_URL_ERROR_MESSAGE
|
||||
)
|
||||
counter_error = counter_error + 1
|
||||
else:
|
||||
# _LOGGER.debug(
|
||||
# f"{device_id}: Sensor {sensor.name} is disabled, skipping update"
|
||||
# )
|
||||
counter_disabled = counter_disabled + 1
|
||||
else:
|
||||
_LOGGER.warning(
|
||||
f"{device_id}: Sensor {sensor.name} not found in the registry, skipping update"
|
||||
+ ISSUE_URL_ERROR_MESSAGE
|
||||
)
|
||||
counter_error = counter_error + 1
|
||||
|
||||
# Log summary of updates
|
||||
_LOGGER.debug(
|
||||
f"{device_id}: A total of {counter_updated} sensors have been updated. "
|
||||
f"Number of disabled sensors or skipped updates = {counter_disabled} "
|
||||
f"Number of sensors with constant values = {counter_unchanged} "
|
||||
f"Number of sensors with errors = {counter_error}"
|
||||
)
|
||||
|
||||
# Device not in list: must have been deleted, will resolve post re-start
|
||||
else:
|
||||
_LOGGER.warning(
|
||||
f"{device_id}: Sensor must have been deleted, re-start of HA recommended."
|
||||
)
|
||||
|
||||
# Get the polling interval from the options, defaulting to 5 seconds if not set
|
||||
polling_interval = timedelta(
|
||||
seconds=config_entry.options.get("polling_interval", 5)
|
||||
)
|
||||
|
||||
async_track_time_interval(hass, async_update_data, polling_interval)
|
||||
|
||||
|
||||
# This is the actual instance of SensorEntity class
|
||||
class PowerOceanSensor(SensorEntity):
|
||||
"""Representation of a PowerOcean Sensor."""
|
||||
|
||||
def __init__(self, ecoflow: Ecoflow, endpoint):
|
||||
"""Initialize the sensor."""
|
||||
# Make Ecoflow and the endpoint parameters from the Sensor API available
|
||||
self.ecoflow = ecoflow
|
||||
self.endpoint = endpoint
|
||||
|
||||
# Set Friendly name when sensor is first created
|
||||
self._attr_unique_id = endpoint.name
|
||||
self._attr_has_entity_name = True
|
||||
self._attr_name = endpoint.friendly_name
|
||||
self._name = endpoint.friendly_name
|
||||
|
||||
# The unique identifier for this sensor within Home Assistant
|
||||
# has nothing to do with the entity_id, it is the internal unique_id of the sensor entity registry
|
||||
self._unique_id = endpoint.internal_unique_id
|
||||
|
||||
# Set the icon for the sensor based on its unit, ensure the icon_mapper is defined
|
||||
# Default handled in function
|
||||
# self._icon = PowerOceanSensor.icon_mapper.get(endpoint.unit)
|
||||
self._icon = endpoint.icon
|
||||
|
||||
# The initial state/value of the sensor
|
||||
self._state = endpoint.value
|
||||
|
||||
# The unit of measurement for the sensor
|
||||
self._unit = endpoint.unit
|
||||
|
||||
# Set entity category to diagnostic for sensors with no unit
|
||||
if ecoflow.options.get("group_sensors") and not endpoint.unit:
|
||||
self._attr_entity_category = EntityCategory.DIAGNOSTIC
|
||||
|
||||
# If diagnostics entity then disable sensor by default
|
||||
if ecoflow.options.get("disable_sensors") and not endpoint.unit:
|
||||
self._attr_entity_registry_enabled_default = False
|
||||
|
||||
@property
|
||||
def should_poll(self):
|
||||
"""async_track_time_intervals handles updates."""
|
||||
return False
|
||||
|
||||
@property
|
||||
def unique_id(self):
|
||||
"""Return the unique ID of the sensor."""
|
||||
return self._unique_id
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the sensor."""
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def state(self):
|
||||
"""Return the state of the sensor."""
|
||||
return self._state
|
||||
|
||||
@property
|
||||
def unit_of_measurement(self):
|
||||
"""Return the unit of measurement."""
|
||||
return self._unit
|
||||
|
||||
@property
|
||||
def device_class(self):
|
||||
"""Return the device class of this entity, if any."""
|
||||
if self._unit == "°C":
|
||||
return SensorDeviceClass.TEMPERATURE
|
||||
elif self._unit == "%":
|
||||
return SensorDeviceClass.BATTERY
|
||||
elif self._unit in {"Wh", "kWh"}:
|
||||
return SensorDeviceClass.ENERGY
|
||||
elif self._unit == "W":
|
||||
return SensorDeviceClass.POWER
|
||||
elif self._unit == "V":
|
||||
return SensorDeviceClass.VOLTAGE
|
||||
elif self._unit == "A":
|
||||
return SensorDeviceClass.CURRENT
|
||||
else:
|
||||
return None
|
||||
|
||||
@property
|
||||
def state_class(self):
|
||||
"""Return the state class of this entity, if any."""
|
||||
if self._unit in {"°C", "h", "W", "V", "A"}:
|
||||
return SensorStateClass.MEASUREMENT
|
||||
elif self._unit in {"Wh", "kWh"}:
|
||||
return SensorStateClass.TOTAL_INCREASING
|
||||
else:
|
||||
return None
|
||||
|
||||
@property
|
||||
def extra_state_attributes(self):
|
||||
"""Return the state attributes of this device."""
|
||||
attr = {}
|
||||
|
||||
attr[ATTR_PRODUCT_DESCRIPTION] = self.endpoint.description
|
||||
attr[ATTR_UNIQUE_ID] = self.endpoint.internal_unique_id
|
||||
attr[ATTR_PRODUCT_VENDOR] = self.ecoflow.device["vendor"]
|
||||
attr[ATTR_PRODUCT_NAME] = self.ecoflow.device["name"]
|
||||
attr[ATTR_PRODUCT_SERIAL] = self.endpoint.serial
|
||||
attr[ATTR_PRODUCT_VERSION] = self.ecoflow.device["version"]
|
||||
attr[ATTR_PRODUCT_BUILD] = self.ecoflow.device["build"]
|
||||
attr[ATTR_PRODUCT_FEATURES] = self.ecoflow.device["features"]
|
||||
|
||||
return attr
|
||||
|
||||
@property
|
||||
def device_info(self):
|
||||
"""Return device specific attributes."""
|
||||
# The unique identifier of the device is the serial number
|
||||
return {
|
||||
"identifiers": {(DOMAIN, self.ecoflow.device["serial"])},
|
||||
"name": self.ecoflow.device["name"],
|
||||
"manufacturer": "ECOFLOW",
|
||||
}
|
||||
|
||||
@property
|
||||
def icon(self):
|
||||
"""Return the icon of the sensor."""
|
||||
return self._icon
|
||||
|
||||
# icon_mapper = defaultdict(
|
||||
# lambda: "mdi:alert-circle",
|
||||
# {
|
||||
# "°C": "mdi:thermometer",
|
||||
# "%": "mdi:flash",
|
||||
# "s": "mdi:timer",
|
||||
# "Wh": "mdi:solar-power-variant-outline",
|
||||
# "h": "mdi:timer-sand",
|
||||
# },
|
||||
# )
|
||||
|
||||
# This is to register the icon settings
|
||||
async def async_added_to_hass(self):
|
||||
"""Call when the sensor is added to Home Assistant."""
|
||||
self.async_write_ha_state()
|
||||
|
||||
# Update of Sensor values
|
||||
async def async_update(self, sensor_data=None):
|
||||
"""Update the sensor with the provided data."""
|
||||
if sensor_data is None:
|
||||
_LOGGER.warning(
|
||||
f"{self.ecoflow.device['serial']}: No new data provided for sensor '{self.name}' update"
|
||||
+ ISSUE_URL_ERROR_MESSAGE
|
||||
)
|
||||
update_status = 0
|
||||
return
|
||||
|
||||
try:
|
||||
self._state = sensor_data.value
|
||||
update_status = 1
|
||||
self.async_write_ha_state()
|
||||
|
||||
except Exception as error:
|
||||
_LOGGER.error(
|
||||
f"{self.ecoflow.device['serial']}: Error updating sensor {self.name}: {error}"
|
||||
+ ISSUE_URL_ERROR_MESSAGE
|
||||
)
|
||||
update_status = 0
|
||||
|
||||
return update_status
|
||||
@@ -0,0 +1,31 @@
|
||||
{
|
||||
"config": {
|
||||
"step": {
|
||||
"user": {
|
||||
"description": "[%key:common::config_flow::data::description%]",
|
||||
"data": {
|
||||
"serialnumber": "[%key:common::config_flow::data::serialnumber%]",
|
||||
"username": "[%key:common::config_flow::data::username%]",
|
||||
"password": "[%key:common::config_flow::data::password%]"
|
||||
}
|
||||
},
|
||||
"device_options": {
|
||||
"description": "[%key:common::config_flow::data::description%]",
|
||||
"data": {
|
||||
"custom_device_name": "[%key:common::config_flow::data::custom_device_name%]",
|
||||
"polling_time": "[%key:common::config_flow::data::polling_time%]",
|
||||
"group_sensors": "[%key:common::config_flow::data::group_sensors%]",
|
||||
"disable_sensors": "[%key:common::config_flow::data::disable_sensors%]"
|
||||
}
|
||||
}
|
||||
},
|
||||
"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%]"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
{
|
||||
"config": {
|
||||
"abort": {
|
||||
"already_configured": "Das Gerät ist bereits konfiguriert"
|
||||
},
|
||||
"error": {
|
||||
"cannot_connect": "Verbindung fehlgeschlagen",
|
||||
"invalid_auth": "Ungültige Authentifizierung",
|
||||
"unknown": "Unerwarteter Fehler"
|
||||
},
|
||||
"step": {
|
||||
"user": {
|
||||
"description": "Bitte geben Sie unten Ihre PowerOcean Geräteinformationen ein:",
|
||||
"data": {
|
||||
"serialnumber": "Seriennummer des Wechselrichters",
|
||||
"password": "Passwort",
|
||||
"username": "Benutzername"
|
||||
}
|
||||
},
|
||||
"device_options": {
|
||||
"description": "Bitte wählen Sie die folgenden Optionen aus:",
|
||||
"data": {
|
||||
"custom_device_name": "Benutzerfreundlicher Gerätename",
|
||||
"polling_time": "Abfragezeit (in Sekunden), um Sensoren vom Gerät zu aktualisieren",
|
||||
"group_sensors": "Gruppieren Sie Sensoren auf der Geräteseite",
|
||||
"disable_sensors": "Diagnosesensoren deaktivieren"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
{
|
||||
"config": {
|
||||
"abort": {
|
||||
"already_configured": "Device is already configured"
|
||||
},
|
||||
"error": {
|
||||
"cannot_connect": "Failed to connect",
|
||||
"invalid_auth": "Invalid authentication",
|
||||
"unknown": "Unexpected error"
|
||||
},
|
||||
"step": {
|
||||
"user": {
|
||||
"description": "Please enter your PowerOcean device information below:",
|
||||
"data": {
|
||||
"serialnumber": "Inverter serial number",
|
||||
"password": "Password",
|
||||
"username": "Username"
|
||||
}
|
||||
},
|
||||
"device_options": {
|
||||
"description": "Please select the following options:",
|
||||
"data": {
|
||||
"custom_device_name": "Friendly device name",
|
||||
"polling_time": "Polling time (in seconds) to update sensors from device",
|
||||
"group_sensors": "Group sensors on device page",
|
||||
"disable_sensors": "Disable diagnostics sensors"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
{
|
||||
"data": [
|
||||
"sysLoadPwr",
|
||||
"sysGridPwr",
|
||||
"mpptPwr",
|
||||
"bpPwr",
|
||||
"bpSoc",
|
||||
"online",
|
||||
"systemName",
|
||||
"createTime",
|
||||
"location",
|
||||
"timezone"
|
||||
],
|
||||
"JTS1_ENERGY_STREAM_REPORT": [],
|
||||
"JTS1_EMS_CHANGE_REPORT": [],
|
||||
"JTS1_BP_STA_REPORT": [],
|
||||
"JTS1_ENERGY_STREAM_REPORT": [
|
||||
"bpSoc",
|
||||
"heatingPower",
|
||||
"dcdcPwr",
|
||||
"bpPwr",
|
||||
"pvPwr",
|
||||
"gridPwr",
|
||||
"loadPwr",
|
||||
"timezone",
|
||||
"updateTime",
|
||||
"timestamp"
|
||||
],
|
||||
"JTS1_EMS_HEARTBEAT": [
|
||||
"emsSystemState",
|
||||
"mpptRatePower",
|
||||
"gridOutDayEnergy",
|
||||
"meterACurrent",
|
||||
"meterBCurrent",
|
||||
"bpInDayEnergy",
|
||||
"meterPowerFactor",
|
||||
"isPvToInvDirectly",
|
||||
"bpRemainWatth",
|
||||
"bp1SocCoefficient",
|
||||
"sysRunMeterFeedPower",
|
||||
"inverterBand",
|
||||
"loadDayEnergy",
|
||||
"startDischargePower",
|
||||
"bpDichargeAbilityAdjustValueLv2",
|
||||
"bpDichargeAbilityAdjustValueLv1",
|
||||
"meterAddress",
|
||||
"gridFeedRate",
|
||||
"currentNetif",
|
||||
"batterySocLowerLimit",
|
||||
"bpDsgTime",
|
||||
"bpChgDsgSta",
|
||||
"autoDetectStartPowerEn",
|
||||
"meterPhasePower",
|
||||
"sysErrCode",
|
||||
"meterTotalPower",
|
||||
"bpBusVoltageBaseCoefficient",
|
||||
"bpSaveSocStopDelay",
|
||||
"bpOutDayEnergy",
|
||||
"bpBusVoltCoefficient",
|
||||
"pvInvWiringMode",
|
||||
"underVoltageProtectPoint",
|
||||
"batterySocUpperLimit",
|
||||
"bpRunDelay",
|
||||
"meterBVoltage",
|
||||
"bpStopDelay",
|
||||
"errCode",
|
||||
"gridInDayEnergy",
|
||||
"sysRunMeterTakePower",
|
||||
"bpSoc",
|
||||
"bp3SocCoefficient",
|
||||
"bpErrorCodeMask",
|
||||
"workingMode",
|
||||
"appCtrlState",
|
||||
"bpSocBaseCoefficient",
|
||||
"emsErrorCodeMask",
|
||||
"meterRatio",
|
||||
"sysWorkSta",
|
||||
"pv2ErrorCodeMask",
|
||||
"pv1ErrorCodeMask",
|
||||
"manualSetStartChargePower",
|
||||
"pvWiringType",
|
||||
"emsErrCode",
|
||||
"bp2SocCoefficient",
|
||||
"mpptWithstandVoltage",
|
||||
"meterCVoltage",
|
||||
"updateTime",
|
||||
"pvaErrorCodeMask",
|
||||
"meterAVoltage",
|
||||
"invWiringType",
|
||||
"disOrChargeAbilityStepValue",
|
||||
"reportCycleTime",
|
||||
"pvInDayEnergy",
|
||||
"meterType",
|
||||
"meterCCurrent",
|
||||
"mpptVoltageMinimum",
|
||||
"mpptVoltageMaximum",
|
||||
"gridFeedPowerMinimum",
|
||||
"epoSwitchState"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
{
|
||||
"data": [
|
||||
"sysLoadPwr",
|
||||
"sysGridPwr",
|
||||
"mpptPwr",
|
||||
"bpPwr",
|
||||
"online",
|
||||
"dcdcPwr",
|
||||
"todayElectricityGeneration",
|
||||
"monthElectricityGeneration",
|
||||
"yearElectricityGeneration",
|
||||
"totalElectricityGeneration",
|
||||
"systemName",
|
||||
"createTime"
|
||||
],
|
||||
"JTS1_ENERGY_STREAM_REPORT": [
|
||||
"pv1Pwr",
|
||||
"pvInvPwr",
|
||||
"pv2Pwr"
|
||||
],
|
||||
"JTS1_EMS_CHANGE_REPORT": [
|
||||
"bpTotalChgEnergy",
|
||||
"bpTotalDsgEnergy",
|
||||
"bpSoc",
|
||||
"bpOnlineSum",
|
||||
"emsCtrlLedBright"
|
||||
],
|
||||
"JTS1_BP_STA_REPORT": [
|
||||
"bpPwr",
|
||||
"bpSoc",
|
||||
"bpSoh",
|
||||
"bpVol",
|
||||
"bpAmp",
|
||||
"bpCycles",
|
||||
"bpSysState",
|
||||
"bpRemainWatth"
|
||||
],
|
||||
"JTS1_EMS_HEARTBEAT": [
|
||||
"bpRemainWatth",
|
||||
"emsBpAliveNum",
|
||||
"emsBpPower",
|
||||
"pcsActPwr",
|
||||
"pcsMeterPower"
|
||||
]
|
||||
}
|
||||
Reference in New Issue
Block a user