Home Assistant Git Exporter

This commit is contained in:
root
2024-05-31 13:07:35 +02:00
parent 64a0536537
commit 60abdd866c
275 changed files with 71113 additions and 1 deletions

View File

@@ -0,0 +1,102 @@
import logging
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import Platform
from homeassistant.core import HomeAssistant
from homeassistant.helpers import entity_registry as er
from .config.const import CONF_DEVICE_TYPE, CONF_USERNAME, CONF_PASSWORD, OPTS_POWER_STEP, OPTS_REFRESH_PERIOD_SEC, \
DEFAULT_REFRESH_PERIOD_SEC, CONF_DEVICE_ID
from .mqtt.ecoflow_mqtt import EcoflowMQTTClient, EcoflowAuthentication
_LOGGER = logging.getLogger(__name__)
DOMAIN = "ecoflow_cloud"
CONFIG_VERSION = 3
_PLATFORMS = {
Platform.NUMBER,
Platform.SELECT,
Platform.SENSOR,
Platform.SWITCH,
Platform.BUTTON,
}
ATTR_STATUS_SN = "SN"
ATTR_STATUS_UPDATES = "status_request_count"
ATTR_STATUS_LAST_UPDATE = "status_last_update"
ATTR_STATUS_DATA_LAST_UPDATE = "data_last_update"
ATTR_STATUS_RECONNECTS = "reconnects"
ATTR_STATUS_PHASE = "status_phase"
async def async_migrate_entry(hass, config_entry: ConfigEntry):
"""Migrate old entry."""
if config_entry.version == 1:
from .devices.registry import devices as device_registry
device = device_registry[config_entry.data[CONF_DEVICE_TYPE]]
new_data = {**config_entry.data}
new_options = {OPTS_POWER_STEP: device.charging_power_step(),
OPTS_REFRESH_PERIOD_SEC: DEFAULT_REFRESH_PERIOD_SEC}
config_entry.version = 2
hass.config_entries.async_update_entry(config_entry, data=new_data, options=new_options)
_LOGGER.info("Migration to version %s successful", config_entry.version)
if config_entry.version < CONFIG_VERSION:
from .devices.registry import devices as device_registry
from .entities import EcoFlowAbstractEntity
from .devices import EntityMigration, MigrationAction
device = device_registry[config_entry.data[CONF_DEVICE_TYPE]]
device_sn = config_entry.data[CONF_DEVICE_ID]
entity_registry = er.async_get(hass)
for v in (config_entry.version, CONFIG_VERSION):
migrations: list[EntityMigration] = device.migrate(v)
for m in migrations:
if m.action == MigrationAction.REMOVE:
entity_id = entity_registry.async_get_entity_id(
domain=m.domain,
platform=DOMAIN,
unique_id=EcoFlowAbstractEntity.gen_unique_id(sn=device_sn, key=m.key))
if entity_id:
_LOGGER.info(".... removing entity_id = %s", entity_id)
entity_registry.async_remove(entity_id)
config_entry.version = CONFIG_VERSION
hass.config_entries.async_update_entry(config_entry)
_LOGGER.info("Migration to version %s successful", config_entry.version)
return True
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry):
if DOMAIN not in hass.data:
hass.data[DOMAIN] = {}
auth = EcoflowAuthentication(entry.data[CONF_USERNAME], entry.data[CONF_PASSWORD])
await hass.async_add_executor_job(auth.authorize)
client = EcoflowMQTTClient(hass, entry, auth)
hass.data[DOMAIN][entry.entry_id] = client
await hass.config_entries.async_forward_entry_setups(entry, _PLATFORMS)
entry.async_on_unload(entry.add_update_listener(update_listener))
return True
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry):
if not await hass.config_entries.async_unload_platforms(entry, _PLATFORMS):
return False
client: EcoflowMQTTClient = hass.data[DOMAIN].pop(entry.entry_id)
client.stop()
return True
async def update_listener(hass: HomeAssistant, entry: ConfigEntry) -> None:
await hass.config_entries.async_reload(entry.entry_id)

View File

@@ -0,0 +1,34 @@
import logging
from typing import Any
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity import EntityCategory
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from . import DOMAIN
from .entities import BaseButtonEntity
from .mqtt.ecoflow_mqtt import EcoflowMQTTClient
_LOGGER = logging.getLogger(__name__)
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback):
client: EcoflowMQTTClient = hass.data[DOMAIN][entry.entry_id]
from .devices.registry import devices
async_add_entities(devices[client.device_type].buttons(client))
class EnabledButtonEntity(BaseButtonEntity):
def press(self, **kwargs: Any) -> None:
if self._command:
self.send_set_message(0, self.command_dict(0))
class DisabledButtonEntity(BaseButtonEntity):
async def async_press(self, **kwargs: Any) -> None:
if self._command:
self.send_set_message(0, self.command_dict(0))

View File

@@ -0,0 +1,35 @@
from enum import Enum
from typing import Final
from homeassistant import const
CONF_USERNAME: Final = const.CONF_USERNAME
CONF_PASSWORD: Final = const.CONF_PASSWORD
CONF_DEVICE_TYPE: Final = const.CONF_TYPE
CONF_DEVICE_NAME: Final = const.CONF_NAME
CONF_DEVICE_ID: Final = const.CONF_DEVICE_ID
OPTS_POWER_STEP: Final = "power_step"
OPTS_REFRESH_PERIOD_SEC: Final = "refresh_period_sec"
DEFAULT_REFRESH_PERIOD_SEC: Final = 5
class EcoflowModel(Enum):
DELTA_2 = 1,
RIVER_2 = 2,
RIVER_2_MAX = 3,
RIVER_2_PRO = 4,
DELTA_PRO = 5,
RIVER_MAX = 6,
RIVER_PRO = 7,
DELTA_MAX = 8, # productType = 13
DELTA_2_MAX = 9, # productType = 81
DELTA_MINI = 15, # productType = 15
POWERSTREAM = 51,
GLACIER = 46,
WAVE_2 = 45, # productType = 45
DIAGNOSTIC = 99
@classmethod
def list(cls) -> list[str]:
return [e.name for e in EcoflowModel]

View File

@@ -0,0 +1,72 @@
import logging
from typing import Dict, Any
import voluptuous as vol
from homeassistant import const
from homeassistant.config_entries import ConfigFlow, ConfigEntry, OptionsFlow
from homeassistant.core import callback
from homeassistant.data_entry_flow import FlowResult
from homeassistant.helpers import selector
from . import DOMAIN, CONFIG_VERSION
from .config.const import EcoflowModel, CONF_USERNAME, CONF_PASSWORD, CONF_DEVICE_TYPE, CONF_DEVICE_NAME, \
CONF_DEVICE_ID, OPTS_POWER_STEP, OPTS_REFRESH_PERIOD_SEC, DEFAULT_REFRESH_PERIOD_SEC
_LOGGER = logging.getLogger(__name__)
class EcoflowConfigFlow(ConfigFlow, domain=DOMAIN):
VERSION = CONFIG_VERSION
async def async_step_user(self, user_input: dict[str, Any] | None = None):
errors: Dict[str, str] = {}
if user_input is not None and not errors:
from .devices.registry import devices
device = devices[user_input[CONF_DEVICE_TYPE]]
options = {OPTS_POWER_STEP: device.charging_power_step(), OPTS_REFRESH_PERIOD_SEC: DEFAULT_REFRESH_PERIOD_SEC}
return self.async_create_entry(title=user_input[const.CONF_NAME], data=user_input, options=options)
return self.async_show_form(
step_id="user",
last_step=True,
data_schema=vol.Schema({
vol.Required(CONF_USERNAME): str,
vol.Required(CONF_PASSWORD): str,
vol.Required(CONF_DEVICE_TYPE): selector.SelectSelector(
selector.SelectSelectorConfig(options=EcoflowModel.list(),
mode=selector.SelectSelectorMode.DROPDOWN),
),
vol.Required(CONF_DEVICE_NAME): str,
vol.Required(CONF_DEVICE_ID): str,
})
)
@staticmethod
@callback
def async_get_options_flow(config_entry: ConfigEntry) -> OptionsFlow:
return EcoflowOptionsFlow(config_entry)
class EcoflowOptionsFlow(OptionsFlow):
def __init__(self, config_entry: ConfigEntry) -> None:
self.config_entry = config_entry
async def async_step_init(
self, user_input: dict[str, Any] | None = None
) -> FlowResult:
if user_input is not None:
return self.async_create_entry(title="", data=user_input)
return self.async_show_form(
step_id="init",
last_step=True,
data_schema=vol.Schema({
vol.Optional(OPTS_POWER_STEP,
default=self.config_entry.options[OPTS_POWER_STEP]): int,
vol.Optional(OPTS_REFRESH_PERIOD_SEC,
default=self.config_entry.options[OPTS_REFRESH_PERIOD_SEC]): int,
})
)

View File

@@ -0,0 +1,70 @@
from abc import ABC, abstractmethod
from enum import StrEnum
from homeassistant.components.number import NumberEntity
from homeassistant.components.select import SelectEntity
from homeassistant.components.sensor import SensorEntity
from homeassistant.components.switch import SwitchEntity
from homeassistant.components.button import ButtonEntity
from homeassistant.const import Platform
from ..mqtt.ecoflow_mqtt import EcoflowMQTTClient
class MigrationAction(StrEnum):
REMOVE = "remove"
class EntityMigration:
def __init__(self, key: str, domain: Platform, action: MigrationAction, **kwargs):
self.key = key
self.domain = domain
self.action = action
self.args = kwargs
class BaseDevice(ABC):
def charging_power_step(self) -> int:
return 100
@abstractmethod
def sensors(self, client: EcoflowMQTTClient) -> list[SensorEntity]:
pass
@abstractmethod
def numbers(self, client: EcoflowMQTTClient) -> list[NumberEntity]:
pass
@abstractmethod
def switches(self, client: EcoflowMQTTClient) -> list[SwitchEntity]:
pass
@abstractmethod
def selects(self, client: EcoflowMQTTClient) -> list[SelectEntity]:
pass
def buttons(self, client: EcoflowMQTTClient) -> list[ButtonEntity]:
return []
def migrate(self, version) -> list[EntityMigration]:
return []
class DiagnosticDevice(BaseDevice):
def sensors(self, client: EcoflowMQTTClient) -> list[SensorEntity]:
return []
def numbers(self, client: EcoflowMQTTClient) -> list[NumberEntity]:
return []
def switches(self, client: EcoflowMQTTClient) -> list[SwitchEntity]:
return []
def buttons(self, client: EcoflowMQTTClient) -> list[ButtonEntity]:
return []
def selects(self, client: EcoflowMQTTClient) -> list[SelectEntity]:
return []

View File

@@ -0,0 +1,255 @@
DC_MODE_OPTIONS = {
"Auto": 0,
"Solar Recharging": 1,
"Car Recharging": 2,
}
DC_ICONS = {
"Auto": None,
"MPPT": "mdi:solar-power",
"DC": "mdi:current-dc",
}
SCREEN_TIMEOUT_OPTIONS = {
"Never": 0,
"10 sec": 10,
"30 sec": 30,
"1 min": 60,
"5 min": 300,
"30 min": 1800,
}
UNIT_TIMEOUT_OPTIONS = {
"Never": 0,
"30 min": 30,
"1 hr": 60,
"2 hr": 120,
"4 hr": 240,
"6 hr": 360,
"12 hr": 720,
"24 hr": 1440
}
UNIT_TIMEOUT_OPTIONS_LIMITED = {
"Never": 0,
"30 min": 30,
"1 hr": 60,
"2 hr": 120,
"6 hr": 360,
"12 hr": 720
}
AC_TIMEOUT_OPTIONS = {
"Never": 0,
"30 min": 30,
"1 hr": 60,
"2 hr": 120,
"4 hr": 240,
"6 hr": 360,
"12 hr": 720,
"24 hr": 1440,
}
AC_TIMEOUT_OPTIONS_LIMITED = {
"Never": 0,
"2 hr": 120,
"4 hr": 240,
"6 hr": 360,
"12 hr": 720,
"24 hr": 1440,
}
DC_TIMEOUT_OPTIONS = {
"Never": 0,
"30 min": 30,
"1 hr": 60,
"2 hr": 120,
"4 hr": 240,
"6 hr": 360,
"12 hr": 720,
"24 hr": 1440,
}
DC_TIMEOUT_OPTIONS_LIMITED = {
"Never": 0,
"2 hr": 120,
"4 hr": 240,
"6 hr": 360,
"12 hr": 720,
"24 hr": 1440,
}
DC_CHARGE_CURRENT_OPTIONS = {
"4A": 4000,
"6A": 6000,
"8A": 8000
}
MAIN_MODE_OPTIONS = {
"Cool": 0,
"Heat": 1,
"Fan": 2
}
FAN_MODE_OPTIONS = {
"Low": 0,
"Medium": 1,
"High": 2
}
REMOTE_MODE_OPTIONS = {
"Startup": 1,
"Standby": 2,
"Shutdown": 3
}
POWER_SUB_MODE_OPTIONS = {
"Max": 0,
"Sleep": 1,
"Eco": 2,
"Manual": 3
}
COMBINED_BATTERY_LEVEL = "Battery Level"
COMBINED_BATTERY_LEVEL_F32 = "Battery Level (Precise)"
BATTERY_CHARGING_STATE = "Battery Charging State"
ATTR_DESIGN_CAPACITY = "Design Capacity (mAh)"
ATTR_FULL_CAPACITY = "Full Capacity (mAh)"
ATTR_REMAIN_CAPACITY = "Remain Capacity (mAh)"
MAIN_DESIGN_CAPACITY = "Main Design Capacity"
MAIN_FULL_CAPACITY = "Main Full Capacity"
MAIN_REMAIN_CAPACITY = "Main Remain Capacity"
SLAVE_DESIGN_CAPACITY = "Slave Design Capacity"
SLAVE_FULL_CAPACITY = "Slave Full Capacity"
SLAVE_REMAIN_CAPACITY = "Slave Remain Capacity"
SLAVE_N_DESIGN_CAPACITY = "Slave %i Design Capacity"
SLAVE_N_FULL_CAPACITY = "Slave %i Full Capacity"
SLAVE_N_REMAIN_CAPACITY = "Slave %i Remain Capacity"
MAIN_BATTERY_LEVEL = "Main Battery Level"
MAIN_BATTERY_LEVEL_F32 = "Main Battery Level (Precise)"
MAIN_BATTERY_CURRENT = "Main Battery Current"
TOTAL_IN_POWER = "Total In Power"
SOLAR_IN_POWER = "Solar In Power"
SOLAR_1_IN_POWER = "Solar (1) In Power"
SOLAR_2_IN_POWER = "Solar (2) In Power"
AC_IN_POWER = "AC In Power"
AC_IN_VOLT = "AC In Volts"
AC_OUT_VOLT = "AC Out Volts"
TYPE_C_IN_POWER = "Type-C In Power"
SOLAR_IN_CURRENT = "Solar In Current"
SOLAR_IN_VOLTAGE = "Solar In Voltage"
SOLAR_IN_ENERGY = "Solar In Energy"
CHARGE_AC_ENERGY = "Battery Charge Energy from AC"
CHARGE_DC_ENERGY = "Battery Charge Energy from DC"
DISCHARGE_AC_ENERGY = "Battery Discharge Energy to AC"
DISCHARGE_DC_ENERGY = "Battery Discharge Energy to DC"
TOTAL_OUT_POWER = "Total Out Power"
AC_OUT_POWER = "AC Out Power"
DC_OUT_POWER = "DC Out Power"
DC_OUT_VOLTAGE = "DC Out Voltage"
DC_CAR_OUT_POWER = "DC Car Out Power"
DC_ANDERSON_OUT_POWER = "DC Anderson Out Power"
TYPEC_OUT_POWER = "Type-C Out Power"
TYPEC_1_OUT_POWER = "Type-C (1) Out Power"
TYPEC_2_OUT_POWER = "Type-C (2) Out Power"
USB_OUT_POWER = "USB Out Power"
USB_1_OUT_POWER = "USB (1) Out Power"
USB_2_OUT_POWER = "USB (2) Out Power"
USB_3_OUT_POWER = "USB (3) Out Power"
USB_QC_1_OUT_POWER = "USB QC (1) Out Power"
USB_QC_2_OUT_POWER = "USB QC (2) Out Power"
REMAINING_TIME = "Remaining Time"
CHARGE_REMAINING_TIME = "Charge Remaining Time"
DISCHARGE_REMAINING_TIME = "Discharge Remaining Time"
CYCLES = "Cycles"
SOH = "State of Health"
SLAVE_BATTERY_LEVEL = "Slave Battery Level"
SLAVE_N_BATTERY_LEVEL = "Slave %i Battery Level"
SLAVE_N_BATTERY_LEVEL_F32 = "Slave %i Battery Level (Precise)"
SLAVE_BATTERY_TEMP = "Slave Battery Temperature"
SLAVE_N_BATTERY_TEMP = "Slave %i Battery Temperature"
SLAVE_MIN_CELL_TEMP = "Slave Min Cell Temperature"
SLAVE_MAX_CELL_TEMP = "Slave Max Cell Temperature"
SLAVE_N_MIN_CELL_TEMP = "Slave %i Min Cell Temperature"
SLAVE_N_MAX_CELL_TEMP = "Slave %i Max Cell Temperature"
SLAVE_CYCLES = "Slave Cycles"
SLAVE_N_CYCLES = "Slave %i Cycles"
SLAVE_SOH = "Slave State of Health"
SLAVE_N_SOH = "Slave %i State of Health"
SLAVE_IN_POWER = "Slave In Power"
SLAVE_N_IN_POWER = "Slave %i In Power"
SLAVE_OUT_POWER = "Slave Out Power"
SLAVE_N_OUT_POWER = "Slave %i Out Power"
SLAVE_BATTERY_VOLT = "Slave Battery Volts"
SLAVE_MIN_CELL_VOLT = "Slave Min Cell Volts"
SLAVE_MAX_CELL_VOLT = "Slave Max Cell Volts"
SLAVE_N_BATTERY_VOLT = "Slave %i Battery Volts"
SLAVE_N_MIN_CELL_VOLT = "Slave %i Min Cell Volts"
SLAVE_N_MAX_CELL_VOLT = "Slave %i Max Cell Volts"
SLAVE_N_BATTERY_CURRENT = "Slave %i Battery Current"
MAX_CHARGE_LEVEL = "Max Charge Level"
MIN_DISCHARGE_LEVEL = "Min Discharge Level"
BACKUP_RESERVE_LEVEL = "Backup Reserve Level"
AC_CHARGING_POWER = "AC Charging Power"
SCREEN_TIMEOUT = "Screen Timeout"
UNIT_TIMEOUT = "Unit Timeout"
AC_TIMEOUT = "AC Timeout"
DC_TIMEOUT = "DC (12V) Timeout"
DC_CHARGE_CURRENT = "DC (12V) Charge Current"
GEN_AUTO_START_LEVEL = "Generator Auto Start Level"
GEN_AUTO_STOP_LEVEL = "Generator Auto Stop Level"
BEEPER = "Beeper"
USB_ENABLED = "USB Enabled"
AC_ENABLED = "AC Enabled"
DC_ENABLED = "DC (12V) Enabled"
XBOOST_ENABLED = "X-Boost Enabled"
AC_ALWAYS_ENABLED = "AC Always On"
PV_PRIO = "Prio Solar Charging"
BP_ENABLED = "Backup Reserve Enabled"
AUTO_FAN_SPEED = "Auto Fan Speed"
AC_SLOW_CHARGE = "AC Slow Charging"
DC_MODE = "DC Mode"
BATTERY_TEMP = "Battery Temperature"
MIN_CELL_TEMP = "Min Cell Temperature"
MAX_CELL_TEMP = "Max Cell Temperature"
INV_IN_TEMP = "Inverter Inside Temperature"
INV_OUT_TEMP = "Inverter Outside Temperature"
DC_CAR_OUT_TEMP = "DC Temperature"
USB_C_TEMP = "USB C Temperature"
ATTR_MIN_CELL_TEMP = MIN_CELL_TEMP
ATTR_MAX_CELL_TEMP = MAX_CELL_TEMP
BATTERY_VOLT = "Battery Volts"
MIN_CELL_VOLT = "Min Cell Volts"
MAX_CELL_VOLT = "Max Cell Volts"
ATTR_MIN_CELL_VOLT = MIN_CELL_VOLT
ATTR_MAX_CELL_VOLT = MAX_CELL_VOLT
BATTERY_AMP = "Battery Current"
SLAVE_BATTERY_AMP = "Slave Battery Current"
FAN_MODE = "Wind speed"
MAIN_MODE = "Main mode"
REMOTE_MODE = "Remote startup/shutdown"
POWER_SUB_MODE = "Sub-mode"

View File

@@ -0,0 +1,200 @@
from homeassistant.const import Platform
from . import const, BaseDevice, EntityMigration, MigrationAction
from .. import EcoflowMQTTClient
from ..entities import BaseSensorEntity, BaseNumberEntity, BaseSwitchEntity, BaseSelectEntity
from ..number import ChargingPowerEntity, MinBatteryLevelEntity, MaxBatteryLevelEntity, \
MaxGenStopLevelEntity, MinGenStartLevelEntity, BatteryBackupLevel
from ..select import DictSelectEntity, TimeoutDictSelectEntity
from ..sensor import LevelSensorEntity, RemainSensorEntity, TempSensorEntity, CyclesSensorEntity, \
InWattsSensorEntity, OutWattsSensorEntity, QuotasStatusSensorEntity, MilliVoltSensorEntity, InMilliVoltSensorEntity, \
OutMilliVoltSensorEntity, CapacitySensorEntity
from ..switch import BeeperEntity, EnabledEntity
class Delta2(BaseDevice):
def sensors(self, client: EcoflowMQTTClient) -> list[BaseSensorEntity]:
return [
LevelSensorEntity(client, "bms_bmsStatus.soc", const.MAIN_BATTERY_LEVEL)
.attr("bms_bmsStatus.designCap", const.ATTR_DESIGN_CAPACITY, 0)
.attr("bms_bmsStatus.fullCap", const.ATTR_FULL_CAPACITY, 0)
.attr("bms_bmsStatus.remainCap", const.ATTR_REMAIN_CAPACITY, 0),
CapacitySensorEntity(client, "bms_bmsStatus.designCap", const.MAIN_DESIGN_CAPACITY, False),
CapacitySensorEntity(client, "bms_bmsStatus.fullCap", const.MAIN_FULL_CAPACITY, False),
CapacitySensorEntity(client, "bms_bmsStatus.remainCap", const.MAIN_REMAIN_CAPACITY, False),
LevelSensorEntity(client, "bms_bmsStatus.soh", const.SOH),
LevelSensorEntity(client, "bms_emsStatus.lcdShowSoc", const.COMBINED_BATTERY_LEVEL),
InWattsSensorEntity(client, "pd.wattsInSum", const.TOTAL_IN_POWER),
OutWattsSensorEntity(client, "pd.wattsOutSum", const.TOTAL_OUT_POWER),
InWattsSensorEntity(client, "inv.inputWatts", const.AC_IN_POWER),
OutWattsSensorEntity(client, "inv.outputWatts", const.AC_OUT_POWER),
InMilliVoltSensorEntity(client, "inv.acInVol", const.AC_IN_VOLT),
OutMilliVoltSensorEntity(client, "inv.invOutVol", const.AC_OUT_VOLT),
InWattsSensorEntity(client, "mppt.inWatts", const.SOLAR_IN_POWER),
# OutWattsSensorEntity(client, "pd.carWatts", const.DC_OUT_POWER),
# the same value as pd.carWatts
OutWattsSensorEntity(client, "mppt.outWatts", const.DC_OUT_POWER),
OutWattsSensorEntity(client, "pd.typec1Watts", const.TYPEC_1_OUT_POWER),
OutWattsSensorEntity(client, "pd.typec2Watts", const.TYPEC_2_OUT_POWER),
OutWattsSensorEntity(client, "pd.usb1Watts", const.USB_1_OUT_POWER),
OutWattsSensorEntity(client, "pd.usb2Watts", const.USB_2_OUT_POWER),
OutWattsSensorEntity(client, "pd.qcUsb1Watts", const.USB_QC_1_OUT_POWER),
OutWattsSensorEntity(client, "pd.qcUsb2Watts", const.USB_QC_2_OUT_POWER),
RemainSensorEntity(client, "bms_emsStatus.chgRemainTime", const.CHARGE_REMAINING_TIME),
RemainSensorEntity(client, "bms_emsStatus.dsgRemainTime", const.DISCHARGE_REMAINING_TIME),
TempSensorEntity(client, "inv.outTemp", "Inv Out Temperature"),
CyclesSensorEntity(client, "bms_bmsStatus.cycles", const.CYCLES),
TempSensorEntity(client, "bms_bmsStatus.temp", const.BATTERY_TEMP)
.attr("bms_bmsStatus.minCellTemp", const.ATTR_MIN_CELL_TEMP, 0)
.attr("bms_bmsStatus.maxCellTemp", const.ATTR_MAX_CELL_TEMP, 0),
TempSensorEntity(client, "bms_bmsStatus.minCellTemp", const.MIN_CELL_TEMP, False),
TempSensorEntity(client, "bms_bmsStatus.maxCellTemp", const.MAX_CELL_TEMP, False),
MilliVoltSensorEntity(client, "bms_bmsStatus.vol", const.BATTERY_VOLT, False)
.attr("bms_bmsStatus.minCellVol", const.ATTR_MIN_CELL_VOLT, 0)
.attr("bms_bmsStatus.maxCellVol", const.ATTR_MAX_CELL_VOLT, 0),
MilliVoltSensorEntity(client, "bms_bmsStatus.minCellVol", const.MIN_CELL_VOLT, False),
MilliVoltSensorEntity(client, "bms_bmsStatus.maxCellVol", const.MAX_CELL_VOLT, False),
# Optional Slave Battery
LevelSensorEntity(client, "bms_slave.soc", const.SLAVE_BATTERY_LEVEL, False, True)
.attr("bms_slave.designCap", const.ATTR_DESIGN_CAPACITY, 0)
.attr("bms_slave.fullCap", const.ATTR_FULL_CAPACITY, 0)
.attr("bms_slave.remainCap", const.ATTR_REMAIN_CAPACITY, 0),
CapacitySensorEntity(client, "bms_slave.designCap", const.SLAVE_DESIGN_CAPACITY, False),
CapacitySensorEntity(client, "bms_slave.fullCap", const.SLAVE_FULL_CAPACITY, False),
CapacitySensorEntity(client, "bms_slave.remainCap", const.SLAVE_REMAIN_CAPACITY, False),
LevelSensorEntity(client, "bms_slave.soh", const.SLAVE_SOH),
TempSensorEntity(client, "bms_slave.temp", const.SLAVE_BATTERY_TEMP, False, True)
.attr("bms_slave.minCellTemp", const.ATTR_MIN_CELL_TEMP, 0)
.attr("bms_slave.maxCellTemp", const.ATTR_MAX_CELL_TEMP, 0),
TempSensorEntity(client, "bms_slave.minCellTemp", const.SLAVE_MIN_CELL_TEMP, False),
TempSensorEntity(client, "bms_slave.maxCellTemp", const.SLAVE_MAX_CELL_TEMP, False),
MilliVoltSensorEntity(client, "bms_slave.vol", const.SLAVE_BATTERY_VOLT, False)
.attr("bms_slave.minCellVol", const.ATTR_MIN_CELL_VOLT, 0)
.attr("bms_slave.maxCellVol", const.ATTR_MAX_CELL_VOLT, 0),
MilliVoltSensorEntity(client, "bms_slave.minCellVol", const.SLAVE_MIN_CELL_VOLT, False),
MilliVoltSensorEntity(client, "bms_slave.maxCellVol", const.SLAVE_MAX_CELL_VOLT, False),
CyclesSensorEntity(client, "bms_slave.cycles", const.SLAVE_CYCLES, False, True),
InWattsSensorEntity(client, "bms_slave.inputWatts", const.SLAVE_IN_POWER, False, True),
OutWattsSensorEntity(client, "bms_slave.outputWatts", const.SLAVE_OUT_POWER, False, True),
QuotasStatusSensorEntity(client),
]
def numbers(self, client: EcoflowMQTTClient) -> list[BaseNumberEntity]:
return [
MaxBatteryLevelEntity(client, "bms_emsStatus.maxChargeSoc", const.MAX_CHARGE_LEVEL, 50, 100,
lambda value: {"moduleType": 2, "operateType": "upsConfig",
"params": {"maxChgSoc": int(value)}}),
MinBatteryLevelEntity(client, "bms_emsStatus.minDsgSoc", const.MIN_DISCHARGE_LEVEL, 0, 30,
lambda value: {"moduleType": 2, "operateType": "dsgCfg",
"params": {"minDsgSoc": int(value)}}),
BatteryBackupLevel(client, "pd.bpPowerSoc", const.BACKUP_RESERVE_LEVEL, 5, 100,
"bms_emsStatus.minDsgSoc", "bms_emsStatus.maxChargeSoc",
lambda value: {"moduleType": 1, "operateType": "watthConfig",
"params": {"isConfig": 1, "bpPowerSoc": int(value), "minDsgSoc": 0,
"minChgSoc": 0}}),
MinGenStartLevelEntity(client, "bms_emsStatus.minOpenOilEb", const.GEN_AUTO_START_LEVEL, 0, 30,
lambda value: {"moduleType": 2, "operateType": "openOilSoc",
"params": {"openOilSoc": value}}),
MaxGenStopLevelEntity(client, "bms_emsStatus.maxCloseOilEb", const.GEN_AUTO_STOP_LEVEL, 50, 100,
lambda value: {"moduleType": 2, "operateType": "closeOilSoc",
"params": {"closeOilSoc": value}}),
ChargingPowerEntity(client, "mppt.cfgChgWatts", const.AC_CHARGING_POWER, 200, 1200,
lambda value: {"moduleType": 5, "operateType": "acChgCfg",
"params": {"chgWatts": int(value), "chgPauseFlag": 255}})
]
def switches(self, client: EcoflowMQTTClient) -> list[BaseSwitchEntity]:
return [
BeeperEntity(client, "mppt.beepState", const.BEEPER,
lambda value: {"moduleType": 5, "operateType": "quietMode", "params": {"enabled": value}}),
EnabledEntity(client, "pd.dcOutState", const.USB_ENABLED,
lambda value: {"moduleType": 1, "operateType": "dcOutCfg", "params": {"enabled": value}}),
EnabledEntity(client, "pd.acAutoOutConfig", const.AC_ALWAYS_ENABLED,
lambda value, params: {"moduleType": 1, "operateType": "acAutoOutConfig",
"params": {"acAutoOutConfig": value,
"minAcOutSoc": int(params.get("bms_emsStatus.minDsgSoc", 0)) + 5}}),
EnabledEntity(client, "pd.pvChgPrioSet", const.PV_PRIO,
lambda value: {"moduleType": 1, "operateType": "pvChangePrio",
"params": {"pvChangeSet": value}}),
EnabledEntity(client, "mppt.cfgAcEnabled", const.AC_ENABLED,
lambda value: {"moduleType": 5, "operateType": "acOutCfg",
"params": {"enabled": value, "out_voltage": -1, "out_freq": 255,
"xboost": 255}}),
EnabledEntity(client, "mppt.cfgAcXboost", const.XBOOST_ENABLED,
lambda value: {"moduleType": 5, "operateType": "acOutCfg",
"params": {"enabled": 255, "out_voltage": -1, "out_freq": 255,
"xboost": value}}),
EnabledEntity(client, "pd.carState", const.DC_ENABLED,
lambda value: {"moduleType": 5, "operateType": "mpptCar", "params": {"enabled": value}}),
EnabledEntity(client, "pd.bpPowerSoc", const.BP_ENABLED,
lambda value: {"moduleType": 1,
"operateType": "watthConfig",
"params": {"bpPowerSoc": value,
"minChgSoc": 0,
"isConfig": value,
"minDsgSoc": 0}}),
]
def selects(self, client: EcoflowMQTTClient) -> list[BaseSelectEntity]:
return [
DictSelectEntity(client, "mppt.dcChgCurrent", const.DC_CHARGE_CURRENT, const.DC_CHARGE_CURRENT_OPTIONS,
lambda value: {"moduleType": 5, "operateType": "dcChgCfg",
"params": {"dcChgCfg": value}}),
TimeoutDictSelectEntity(client, "pd.lcdOffSec", const.SCREEN_TIMEOUT, const.SCREEN_TIMEOUT_OPTIONS,
lambda value: {"moduleType": 1, "operateType": "lcdCfg",
"params": {"brighLevel": 255, "delayOff": value}}),
TimeoutDictSelectEntity(client, "pd.standbyMin", const.UNIT_TIMEOUT, const.UNIT_TIMEOUT_OPTIONS,
lambda value: {"moduleType": 1, "operateType": "standbyTime",
"params": {"standbyMin": value}}),
TimeoutDictSelectEntity(client, "mppt.acStandbyMins", const.AC_TIMEOUT, const.AC_TIMEOUT_OPTIONS,
lambda value: {"moduleType": 5, "operateType": "standbyTime",
"params": {"standbyMins": value}}),
TimeoutDictSelectEntity(client, "mppt.carStandbyMin", const.DC_TIMEOUT, const.DC_TIMEOUT_OPTIONS,
lambda value: {"moduleType": 5, "operateType": "carStandby",
"params": {"standbyMins": value}})
]
def migrate(self, version) -> list[EntityMigration]:
if version == 2:
return [
EntityMigration("pd.soc", Platform.SENSOR, MigrationAction.REMOVE)
]
return []

View File

@@ -0,0 +1,229 @@
from homeassistant.const import Platform
from . import const, BaseDevice, EntityMigration, MigrationAction
from .. import EcoflowMQTTClient
from ..entities import BaseSensorEntity, BaseNumberEntity, BaseSwitchEntity, BaseSelectEntity
from ..number import ChargingPowerEntity, MinBatteryLevelEntity, MaxBatteryLevelEntity, \
MaxGenStopLevelEntity, MinGenStartLevelEntity, BatteryBackupLevel
from ..select import TimeoutDictSelectEntity
from ..sensor import LevelSensorEntity, RemainSensorEntity, TempSensorEntity, CyclesSensorEntity, \
InWattsSensorEntity, OutWattsSensorEntity, StatusSensorEntity, MilliVoltSensorEntity, \
InMilliVoltSensorEntity, OutMilliVoltSensorEntity, CapacitySensorEntity
from ..switch import BeeperEntity, EnabledEntity
class Delta2Max(BaseDevice):
def sensors(self, client: EcoflowMQTTClient) -> list[BaseSensorEntity]:
return [
LevelSensorEntity(client, "bms_bmsStatus.soc", const.MAIN_BATTERY_LEVEL)
.attr("bms_bmsStatus.designCap", const.ATTR_DESIGN_CAPACITY, 0)
.attr("bms_bmsStatus.fullCap", const.ATTR_FULL_CAPACITY, 0)
.attr("bms_bmsStatus.remainCap", const.ATTR_REMAIN_CAPACITY, 0),
CapacitySensorEntity(client, "bms_bmsStatus.designCap", const.MAIN_DESIGN_CAPACITY, False),
CapacitySensorEntity(client, "bms_bmsStatus.fullCap", const.MAIN_FULL_CAPACITY, False),
CapacitySensorEntity(client, "bms_bmsStatus.remainCap", const.MAIN_REMAIN_CAPACITY, False),
LevelSensorEntity(client, "bms_bmsStatus.soh", const.SOH),
LevelSensorEntity(client, "bms_emsStatus.lcdShowSoc", const.COMBINED_BATTERY_LEVEL),
InWattsSensorEntity(client, "pd.wattsInSum", const.TOTAL_IN_POWER),
OutWattsSensorEntity(client, "pd.wattsOutSum", const.TOTAL_OUT_POWER),
InWattsSensorEntity(client, "inv.inputWatts", const.AC_IN_POWER),
OutWattsSensorEntity(client, "inv.outputWatts", const.AC_OUT_POWER),
InMilliVoltSensorEntity(client, "inv.acInVol", const.AC_IN_VOLT),
OutMilliVoltSensorEntity(client, "inv.invOutVol", const.AC_OUT_VOLT),
InWattsSensorEntity(client, "mppt.inWatts", const.SOLAR_1_IN_POWER),
InWattsSensorEntity(client, "mppt.pv2InWatts", const.SOLAR_2_IN_POWER),
# OutWattsSensorEntity(client, "pd.carWatts", const.DC_OUT_POWER),
# the same value as pd.carWatts
OutWattsSensorEntity(client, "mppt.outWatts", const.DC_OUT_POWER),
OutWattsSensorEntity(client, "pd.typec1Watts", const.TYPEC_1_OUT_POWER),
OutWattsSensorEntity(client, "pd.typec2Watts", const.TYPEC_2_OUT_POWER),
OutWattsSensorEntity(client, "pd.usb1Watts", const.USB_1_OUT_POWER),
OutWattsSensorEntity(client, "pd.usb2Watts", const.USB_2_OUT_POWER),
OutWattsSensorEntity(client, "pd.qcUsb1Watts", const.USB_QC_1_OUT_POWER),
OutWattsSensorEntity(client, "pd.qcUsb2Watts", const.USB_QC_2_OUT_POWER),
RemainSensorEntity(client, "bms_emsStatus.chgRemainTime", const.CHARGE_REMAINING_TIME),
RemainSensorEntity(client, "bms_emsStatus.dsgRemainTime", const.DISCHARGE_REMAINING_TIME),
TempSensorEntity(client, "inv.outTemp", "Inv Out Temperature"),
CyclesSensorEntity(client, "bms_bmsStatus.cycles", const.CYCLES),
TempSensorEntity(client, "bms_bmsStatus.temp", const.BATTERY_TEMP)
.attr("bms_bmsStatus.minCellTemp", const.ATTR_MIN_CELL_TEMP, 0)
.attr("bms_bmsStatus.maxCellTemp", const.ATTR_MAX_CELL_TEMP, 0),
TempSensorEntity(client, "bms_bmsStatus.minCellTemp", const.MIN_CELL_TEMP, False),
TempSensorEntity(client, "bms_bmsStatus.maxCellTemp", const.MAX_CELL_TEMP, False),
MilliVoltSensorEntity(client, "bms_bmsStatus.vol", const.BATTERY_VOLT, False)
.attr("bms_bmsStatus.minCellVol", const.ATTR_MIN_CELL_VOLT, 0)
.attr("bms_bmsStatus.maxCellVol", const.ATTR_MAX_CELL_VOLT, 0),
MilliVoltSensorEntity(client, "bms_bmsStatus.minCellVol", const.MIN_CELL_VOLT, False),
MilliVoltSensorEntity(client, "bms_bmsStatus.maxCellVol", const.MAX_CELL_VOLT, False),
# Optional Slave 1 Battery
LevelSensorEntity(client, "bms_slave_bmsSlaveStatus_1.soc", const.SLAVE_N_BATTERY_LEVEL % 1, False, True)
.attr("bms_slave_bmsSlaveStatus_1.designCap", const.ATTR_DESIGN_CAPACITY, 0)
.attr("bms_slave_bmsSlaveStatus_1.fullCap", const.ATTR_FULL_CAPACITY, 0)
.attr("bms_slave_bmsSlaveStatus_1.remainCap", const.ATTR_REMAIN_CAPACITY, 0),
CapacitySensorEntity(client, "bms_slave_bmsSlaveStatus_1.designCap", const.SLAVE_N_DESIGN_CAPACITY % 1, False),
CapacitySensorEntity(client, "bms_slave_bmsSlaveStatus_1.fullCap", const.SLAVE_N_FULL_CAPACITY % 1, False),
CapacitySensorEntity(client, "bms_slave_bmsSlaveStatus_1.remainCap", const.SLAVE_N_REMAIN_CAPACITY % 1, False),
TempSensorEntity(client, "bms_slave_bmsSlaveStatus_1.temp", const.SLAVE_N_BATTERY_TEMP % 1, False, True)
.attr("bms_slave_bmsSlaveStatus_1.minCellTemp", const.ATTR_MIN_CELL_TEMP, 0)
.attr("bms_slave_bmsSlaveStatus_1.maxCellTemp", const.ATTR_MAX_CELL_TEMP, 0),
TempSensorEntity(client, "bms_slave_bmsSlaveStatus_1.minCellTemp", const.SLAVE_N_MIN_CELL_TEMP % 1, False),
TempSensorEntity(client, "bms_slave_bmsSlaveStatus_1.maxCellTemp", const.SLAVE_N_MAX_CELL_TEMP % 1, False),
MilliVoltSensorEntity(client, "bms_slave_bmsSlaveStatus_1.vol", const.SLAVE_N_BATTERY_VOLT % 1, False)
.attr("bms_slave_bmsSlaveStatus_1.minCellVol", const.ATTR_MIN_CELL_VOLT, 0)
.attr("bms_slave_bmsSlaveStatus_1.maxCellVol", const.ATTR_MAX_CELL_VOLT, 0),
MilliVoltSensorEntity(client, "bms_slave_bmsSlaveStatus_1.minCellVol", const.SLAVE_N_MIN_CELL_VOLT % 1, False),
MilliVoltSensorEntity(client, "bms_slave_bmsSlaveStatus_1.maxCellVol", const.SLAVE_N_MAX_CELL_VOLT % 1, False),
CyclesSensorEntity(client, "bms_slave_bmsSlaveStatus_1.cycles", const.SLAVE_N_CYCLES % 1, False, True),
LevelSensorEntity(client, "bms_slave_bmsSlaveStatus_1.soh", const.SLAVE_N_SOH % 1, False,True),
InWattsSensorEntity(client, "bms_slave_bmsSlaveStatus_1.inputWatts", const.SLAVE_N_IN_POWER % 1, False, True),
OutWattsSensorEntity(client, "bms_slave_bmsSlaveStatus_1.outputWatts", const.SLAVE_N_OUT_POWER % 1, False, True),
# Optional Slave 2 Battery
LevelSensorEntity(client, "bms_slave_bmsSlaveStatus_2.soc", const.SLAVE_N_BATTERY_LEVEL % 2, False, True)
.attr("bms_slave_bmsSlaveStatus_2.designCap", const.ATTR_DESIGN_CAPACITY, 0)
.attr("bms_slave_bmsSlaveStatus_2.fullCap", const.ATTR_FULL_CAPACITY, 0)
.attr("bms_slave_bmsSlaveStatus_2.remainCap", const.ATTR_REMAIN_CAPACITY, 0),
CapacitySensorEntity(client, "bms_slave_bmsSlaveStatus_2.designCap", const.SLAVE_N_DESIGN_CAPACITY % 2, False),
CapacitySensorEntity(client, "bms_slave_bmsSlaveStatus_2.fullCap", const.SLAVE_N_FULL_CAPACITY % 2, False),
CapacitySensorEntity(client, "bms_slave_bmsSlaveStatus_2.remainCap", const.SLAVE_N_REMAIN_CAPACITY % 2, False),
TempSensorEntity(client, "bms_slave_bmsSlaveStatus_2.temp", const.SLAVE_N_BATTERY_TEMP % 2, False, True)
.attr("bms_slave_bmsSlaveStatus_2.minCellTemp", const.ATTR_MIN_CELL_TEMP, 0)
.attr("bms_slave_bmsSlaveStatus_2.maxCellTemp", const.ATTR_MAX_CELL_TEMP, 0),
TempSensorEntity(client, "bms_slave_bmsSlaveStatus_2.minCellTemp", const.SLAVE_N_MIN_CELL_TEMP % 2, False),
TempSensorEntity(client, "bms_slave_bmsSlaveStatus_2.maxCellTemp", const.SLAVE_N_MAX_CELL_TEMP % 2, False),
MilliVoltSensorEntity(client, "bms_slave_bmsSlaveStatus_2.vol", const.SLAVE_N_BATTERY_VOLT % 2, False)
.attr("bms_slave_bmsSlaveStatus_2.minCellVol", const.ATTR_MIN_CELL_VOLT, 0)
.attr("bms_slave_bmsSlaveStatus_2.maxCellVol", const.ATTR_MAX_CELL_VOLT, 0),
MilliVoltSensorEntity(client, "bms_slave_bmsSlaveStatus_2.minCellVol", const.SLAVE_N_MIN_CELL_VOLT % 2, False),
MilliVoltSensorEntity(client, "bms_slave_bmsSlaveStatus_2.maxCellVol", const.SLAVE_N_MAX_CELL_VOLT % 2, False),
CyclesSensorEntity(client, "bms_slave_bmsSlaveStatus_2.cycles", const.SLAVE_N_CYCLES % 2, False, True),
LevelSensorEntity(client, "bms_slave_bmsSlaveStatus_2.soh", const.SLAVE_N_SOH % 2, False, True),
InWattsSensorEntity(client, "bms_slave_bmsSlaveStatus_2.inputWatts", const.SLAVE_N_IN_POWER % 2, False, True),
OutWattsSensorEntity(client, "bms_slave_bmsSlaveStatus_2.outputWatts", const.SLAVE_N_OUT_POWER % 2, False, True),
StatusSensorEntity(client),
]
def numbers(self, client: EcoflowMQTTClient) -> list[BaseNumberEntity]:
return [
MaxBatteryLevelEntity(client, "bms_emsStatus.maxChargeSoc", const.MAX_CHARGE_LEVEL, 50, 100,
lambda value: {"moduleType": 2, "operateType": "upsConfig",
"moduleSn": client.device_sn,
"params": {"maxChgSoc": int(value)}}),
MinBatteryLevelEntity(client, "bms_emsStatus.minDsgSoc", const.MIN_DISCHARGE_LEVEL, 0, 30,
lambda value: {"moduleType": 2, "operateType": "dsgCfg",
"moduleSn": client.device_sn,
"params": {"minDsgSoc": int(value)}}),
BatteryBackupLevel(client, "pd.bpPowerSoc", const.BACKUP_RESERVE_LEVEL, 5, 100,
"bms_emsStatus.minDsgSoc", "bms_emsStatus.maxChargeSoc",
lambda value: {"moduleType": 1, "operateType": "watthConfig",
"params": {"isConfig": 1, "bpPowerSoc": int(value), "minDsgSoc": 0,
"minChgSoc": 0}}),
MinGenStartLevelEntity(client, "bms_emsStatus.minOpenOilEbSoc", const.GEN_AUTO_START_LEVEL, 0, 30,
lambda value: {"moduleType": 2, "operateType": "openOilSoc",
"moduleSn": client.device_sn,
"params": {"openOilSoc": value}}),
MaxGenStopLevelEntity(client, "bms_emsStatus.maxCloseOilEbSoc", const.GEN_AUTO_STOP_LEVEL, 50, 100,
lambda value: {"moduleType": 2, "operateType": "closeOilSoc",
"moduleSn": client.device_sn,
"params": {"closeOilSoc": value}}),
ChargingPowerEntity(client, "inv.SlowChgWatts", const.AC_CHARGING_POWER, 200, 2400,
lambda value: {"moduleType": 3, "operateType": "acChgCfg",
"moduleSn": client.device_sn,
"params": {"slowChgWatts": int(value), "fastChgWatts": 255,
"chgPauseFlag": 0}})
]
def switches(self, client: EcoflowMQTTClient) -> list[BaseSwitchEntity]:
return [
BeeperEntity(client, "pd.beepMode", const.BEEPER,
lambda value: {"moduleType": 1, "operateType": "quietCfg",
"moduleSn": client.device_sn,
"params": {"enabled": value}}),
EnabledEntity(client, "pd.dcOutState", const.USB_ENABLED,
lambda value: {"moduleType": 1, "operateType": "dcOutCfg",
"moduleSn": client.device_sn,
"params": {"enabled": value}}),
EnabledEntity(client, "pd.newAcAutoOnCfg", const.AC_ALWAYS_ENABLED,
lambda value: {"moduleType": 1, "operateType": "newAcAutoOnCfg",
"moduleSn": client.device_sn,
"params": {"enabled": value, "minAcSoc": 5}}),
EnabledEntity(client, "inv.cfgAcEnabled", const.AC_ENABLED,
lambda value: {"moduleType": 3, "operateType": "acOutCfg",
"moduleSn": client.device_sn,
"params": {"enabled": value, "out_voltage": -1,
"out_freq": 255, "xboost": 255}}),
EnabledEntity(client, "inv.cfgAcXboost", const.XBOOST_ENABLED,
lambda value: {"moduleType": 3, "operateType": "acOutCfg",
"moduleSn": client.device_sn,
"params": {"xboost": value}}),
EnabledEntity(client, "pd.carState", const.DC_ENABLED,
lambda value: {"moduleType": 5, "operateType": "mpptCar",
"params": {"enabled": value}}),
EnabledEntity(client, "pd.bpPowerSoc", const.BP_ENABLED,
lambda value: {"moduleType": 1,
"operateType": "watthConfig",
"params": {"bpPowerSoc": value,
"minChgSoc": 0,
"isConfig": value,
"minDsgSoc": 0}}),
]
def selects(self, client: EcoflowMQTTClient) -> list[BaseSelectEntity]:
return [
TimeoutDictSelectEntity(client, "pd.lcdOffSec", const.SCREEN_TIMEOUT, const.SCREEN_TIMEOUT_OPTIONS,
lambda value: {"moduleType": 1, "operateType": "lcdCfg",
"moduleSn": client.device_sn,
"params": {"brighLevel": 255, "delayOff": value}}),
TimeoutDictSelectEntity(client, "inv.standbyMin", const.UNIT_TIMEOUT, const.UNIT_TIMEOUT_OPTIONS,
lambda value: {"moduleType": 1, "operateType": "standbyTime",
"moduleSn": client.device_sn,
"params": {"standbyMin": value}}),
TimeoutDictSelectEntity(client, "mppt.carStandbyMin", const.AC_TIMEOUT, const.AC_TIMEOUT_OPTIONS,
lambda value: {"moduleType": 5, "operateType": "standbyTime",
"moduleSn": client.device_sn,
"params": {"standbyMins": value}}),
]
def migrate(self, version) -> list[EntityMigration]:
if version == 2:
return [
EntityMigration("pd.soc", Platform.SENSOR, MigrationAction.REMOVE),
EntityMigration("bms_emsStatus.f32LcdShowSoc", Platform.SENSOR, MigrationAction.REMOVE)
]
return []

View File

@@ -0,0 +1,160 @@
from homeassistant.const import Platform
from . import const, BaseDevice, EntityMigration, MigrationAction
from .. import EcoflowMQTTClient
from ..entities import BaseSensorEntity, BaseNumberEntity, BaseSwitchEntity, BaseSelectEntity
from ..number import ChargingPowerEntity, MinBatteryLevelEntity, MaxBatteryLevelEntity, \
MaxGenStopLevelEntity, MinGenStartLevelEntity
from ..sensor import LevelSensorEntity, RemainSensorEntity, TempSensorEntity, CyclesSensorEntity, \
InWattsSensorEntity, OutWattsSensorEntity, StatusSensorEntity, MilliVoltSensorEntity, \
InMilliVoltSensorEntity, OutMilliVoltSensorEntity, CapacitySensorEntity, InWattsSolarSensorEntity, \
OutWattsDcSensorEntity
from ..switch import BeeperEntity, EnabledEntity
class DeltaMax(BaseDevice):
def sensors(self, client: EcoflowMQTTClient) -> list[BaseSensorEntity]:
return [
LevelSensorEntity(client, "bmsMaster.soc", const.MAIN_BATTERY_LEVEL)
.attr("bmsMaster.designCap", const.ATTR_DESIGN_CAPACITY, 0)
.attr("bmsMaster.fullCap", const.ATTR_FULL_CAPACITY, 0)
.attr("bmsMaster.remainCap", const.ATTR_REMAIN_CAPACITY, 0),
CapacitySensorEntity(client, "bmsMaster.designCap", const.MAIN_DESIGN_CAPACITY, False),
CapacitySensorEntity(client, "bmsMaster.fullCap", const.MAIN_FULL_CAPACITY, False),
CapacitySensorEntity(client, "bmsMaster.remainCap", const.MAIN_REMAIN_CAPACITY, False),
LevelSensorEntity(client, "ems.lcdShowSoc", const.COMBINED_BATTERY_LEVEL),
InWattsSensorEntity(client, "pd.wattsInSum", const.TOTAL_IN_POWER),
OutWattsSensorEntity(client, "pd.wattsOutSum", const.TOTAL_OUT_POWER),
InWattsSensorEntity(client, "inv.inputWatts", const.AC_IN_POWER),
OutWattsSensorEntity(client, "inv.outputWatts", const.AC_OUT_POWER),
InMilliVoltSensorEntity(client, "inv.acInVol", const.AC_IN_VOLT),
OutMilliVoltSensorEntity(client, "inv.invOutVol", const.AC_OUT_VOLT),
InWattsSolarSensorEntity(client, "mppt.inWatts", const.SOLAR_IN_POWER),
OutWattsDcSensorEntity(client, "mppt.outWatts", const.DC_OUT_POWER),
OutWattsSensorEntity(client, "pd.typec1Watts", const.TYPEC_1_OUT_POWER),
OutWattsSensorEntity(client, "pd.typec2Watts", const.TYPEC_2_OUT_POWER),
OutWattsSensorEntity(client, "pd.usb1Watts", const.USB_1_OUT_POWER),
OutWattsSensorEntity(client, "pd.usb2Watts", const.USB_2_OUT_POWER),
OutWattsSensorEntity(client, "pd.qcUsb1Watts", const.USB_QC_1_OUT_POWER),
OutWattsSensorEntity(client, "pd.qcUsb2Watts", const.USB_QC_2_OUT_POWER),
RemainSensorEntity(client, "ems.chgRemainTime", const.CHARGE_REMAINING_TIME),
RemainSensorEntity(client, "ems.dsgRemainTime", const.DISCHARGE_REMAINING_TIME),
TempSensorEntity(client, "inv.outTemp", "Inv Out Temperature"),
CyclesSensorEntity(client, "bmsMaster.cycles", const.CYCLES),
TempSensorEntity(client, "bmsMaster.temp", const.BATTERY_TEMP)
.attr("bmsMaster.minCellTemp", const.ATTR_MIN_CELL_TEMP, 0)
.attr("bmsMaster.maxCellTemp", const.ATTR_MAX_CELL_TEMP, 0),
TempSensorEntity(client, "bmsMaster.minCellTemp", const.MIN_CELL_TEMP, False),
TempSensorEntity(client, "bmsMaster.maxCellTemp", const.MAX_CELL_TEMP, False),
MilliVoltSensorEntity(client, "bmsMaster.vol", const.BATTERY_VOLT, False)
.attr("bmsMaster.minCellVol", const.ATTR_MIN_CELL_VOLT, 0)
.attr("bmsMaster.maxCellVol", const.ATTR_MAX_CELL_VOLT, 0),
MilliVoltSensorEntity(client, "bmsMaster.minCellVol", const.MIN_CELL_VOLT, False),
MilliVoltSensorEntity(client, "bmsMaster.maxCellVol", const.MAX_CELL_VOLT, False),
# Optional Slave Battery
#LevelSensorEntity(client, "bms_slave.soc", const.SLAVE_BATTERY_LEVEL, False, True),
#TempSensorEntity(client, "bms_slave.temp", const.SLAVE_BATTERY_TEMP, False, True),
#TempSensorEntity(client, "bms_slave.minCellTemp", const.SLAVE_MIN_CELL_TEMP, False),
#TempSensorEntity(client, "bms_slave.maxCellTemp", const.SLAVE_MAX_CELL_TEMP, False),
#VoltSensorEntity(client, "bms_slave.vol", const.SLAVE_BATTERY_VOLT, False),
#VoltSensorEntity(client, "bms_slave.minCellVol", const.SLAVE_MIN_CELL_VOLT, False),
#VoltSensorEntity(client, "bms_slave.maxCellVol", const.SLAVE_MAX_CELL_VOLT, False),
#CyclesSensorEntity(client, "bms_slave.cycles", const.SLAVE_CYCLES, False, True),
#InWattsSensorEntity(client, "bms_slave.inputWatts", const.SLAVE_IN_POWER, False, True),
#OutWattsSensorEntity(client, "bms_slave.outputWatts", const.SLAVE_OUT_POWER, False, True)
StatusSensorEntity(client),
]
def numbers(self, client: EcoflowMQTTClient) -> list[BaseNumberEntity]:
return [
MaxBatteryLevelEntity(client, "ems.maxChargeSoc", const.MAX_CHARGE_LEVEL, 50, 100,
lambda value: {"moduleType": 2, "operateType": "TCP",
"params": {"id": 49, "maxChgSoc": value}}),
MinBatteryLevelEntity(client, "ems.minDsgSoc", const.MIN_DISCHARGE_LEVEL, 0, 30,
lambda value: {"moduleType": 2, "operateType": "TCP",
"params": {"id": 51, "minDsgSoc": value}}),
MinGenStartLevelEntity(client, "ems.minOpenOilEbSoc", const.GEN_AUTO_START_LEVEL, 0, 30,
lambda value: {"moduleType": 2, "operateType": "TCP",
"params": {"id": 52, "openOilSoc": value}}),
MaxGenStopLevelEntity(client, "ems.maxCloseOilEbSoc", const.GEN_AUTO_STOP_LEVEL, 50, 100,
lambda value: {"moduleType": 2, "operateType": "TCP",
"params": {"id": 53, "closeOilSoc": value}}),
ChargingPowerEntity(client, "inv.cfgFastChgWatt", const.AC_CHARGING_POWER, 200, 2000,
lambda value: {"moduleType": 0, "operateType": "TCP",
"params": {"slowChgPower": value, "id": 69}}),
]
def switches(self, client: EcoflowMQTTClient) -> list[BaseSwitchEntity]:
return [
BeeperEntity(client, "pd.beepState", const.BEEPER,
lambda value: {"moduleType": 5, "operateType": "TCP", "params": {"id": 38, "enabled": value}}),
EnabledEntity(client, "pd.dcOutState", const.USB_ENABLED,
lambda value: {"moduleType": 0, "operateType": "TCP", "params": {"enabled": value, "id": 34 }}),
EnabledEntity(client, "pd.acAutoOnCfg", const.AC_ALWAYS_ENABLED,
lambda value: {"moduleType": 1, "operateType": "acAutoOn", "params": {"cfg": value}}),
EnabledEntity(client, "pd.pvChgPrioSet", const.PV_PRIO,
lambda value: {"moduleType": 1, "operateType": "pvChangePrio", "params": {"pvChangeSet": value}}),
EnabledEntity(client, "inv.cfgAcEnabled", const.AC_ENABLED,
lambda value: {"moduleType": 0, "operateType": "TCP", "params": {"enabled": value, "id": 66 }}),
EnabledEntity(client, "inv.cfgAcXboost", const.XBOOST_ENABLED,
lambda value: {"moduleType": 5, "operateType": "TCP", "params": {"id": 66, "xboost": value}}),
EnabledEntity(client, "mppt.carState", const.DC_ENABLED,
lambda value: {"moduleType": 0, "operateType": "TCP", "params": {"enabled": value, "id": 81 }}),
]
def selects(self, client: EcoflowMQTTClient) -> list[BaseSelectEntity]:
return [
#DictSelectEntity(client, "mppt.cfgDcChgCurrent", const.DC_CHARGE_CURRENT, const.DC_CHARGE_CURRENT_OPTIONS,
# lambda value: {"moduleType": 5, "operateType": "dcChgCfg",
# "params": {"dcChgCfg": value}}),
#TimeoutDictSelectEntity(client, "pd.lcdOffSec", const.SCREEN_TIMEOUT, const.SCREEN_TIMEOUT_OPTIONS,
# lambda value: {"moduleType": 1, "operateType": "lcdCfg",
# "params": {"brighLevel": 255, "delayOff": value}}),
#TimeoutDictSelectEntity(client, "inv.cfgStandbyMin", const.UNIT_TIMEOUT, const.UNIT_TIMEOUT_OPTIONS,
# lambda value: {"moduleType": 1, "operateType": "standbyTime",
# "params": {"standbyMin": value}}),
#TimeoutDictSelectEntity(client, "mppt.acStandbyMins", const.AC_TIMEOUT, const.AC_TIMEOUT_OPTIONS,
# lambda value: {"moduleType": 5, "operateType": "standbyTime",
# "params": {"standbyMins": value}}),
#TimeoutDictSelectEntity(client, "mppt.carStandbyMin", const.DC_TIMEOUT, const.DC_TIMEOUT_OPTIONS,
# lambda value: {"moduleType": 5, "operateType": "carStandby",
# "params": {"standbyMins": value}})
]
def migrate(self, version) -> list[EntityMigration]:
if version == 2:
return [
EntityMigration("pd.soc", Platform.SENSOR, MigrationAction.REMOVE),
]
return []

View File

@@ -0,0 +1,147 @@
from homeassistant.const import Platform
from . import const, BaseDevice, EntityMigration, MigrationAction
from ..entities import BaseSensorEntity, BaseNumberEntity, BaseSwitchEntity, BaseSelectEntity
from ..mqtt.ecoflow_mqtt import EcoflowMQTTClient
from ..number import ChargingPowerEntity, MaxBatteryLevelEntity, MinBatteryLevelEntity
from ..select import DictSelectEntity, TimeoutDictSelectEntity
from ..sensor import LevelSensorEntity, WattsSensorEntity, RemainSensorEntity, TempSensorEntity, \
CyclesSensorEntity, InWattsSensorEntity, OutWattsSensorEntity, OutWattsDcSensorEntity, InWattsSolarSensorEntity, \
StatusSensorEntity, InEnergySensorEntity, OutEnergySensorEntity, MilliVoltSensorEntity, InMilliVoltSensorEntity, \
OutMilliVoltSensorEntity, CapacitySensorEntity
from ..switch import BeeperEntity, EnabledEntity
class DeltaMini(BaseDevice):
def sensors(self, client: EcoflowMQTTClient) -> list[BaseSensorEntity]:
return [
LevelSensorEntity(client, "bmsMaster.soc", const.MAIN_BATTERY_LEVEL)
.attr("bmsMaster.designCap", const.ATTR_DESIGN_CAPACITY, 0)
.attr("bmsMaster.fullCap", const.ATTR_FULL_CAPACITY, 0)
.attr("bmsMaster.remainCap", const.ATTR_REMAIN_CAPACITY, 0),
CapacitySensorEntity(client, "bmsMaster.designCap", const.MAIN_DESIGN_CAPACITY, False),
CapacitySensorEntity(client, "bmsMaster.fullCap", const.MAIN_FULL_CAPACITY, False),
CapacitySensorEntity(client, "bmsMaster.remainCap", const.MAIN_REMAIN_CAPACITY, False),
LevelSensorEntity(client, "bmsMaster.soh", const.SOH),
LevelSensorEntity(client, "ems.lcdShowSoc", const.COMBINED_BATTERY_LEVEL),
WattsSensorEntity(client, "pd.wattsInSum", const.TOTAL_IN_POWER),
WattsSensorEntity(client, "pd.wattsOutSum", const.TOTAL_OUT_POWER),
InWattsSensorEntity(client, "inv.inputWatts", const.AC_IN_POWER),
OutWattsSensorEntity(client, "inv.outputWatts", const.AC_OUT_POWER),
InMilliVoltSensorEntity(client, "inv.acInVol", const.AC_IN_VOLT),
OutMilliVoltSensorEntity(client, "inv.invOutVol", const.AC_OUT_VOLT),
InWattsSolarSensorEntity(client, "mppt.inWatts", const.SOLAR_IN_POWER),
OutWattsDcSensorEntity(client, "mppt.outWatts", const.DC_OUT_POWER),
OutWattsDcSensorEntity(client, "mppt.carOutWatts", const.DC_CAR_OUT_POWER),
OutWattsSensorEntity(client, "mppt.dcdc12vWatts", const.DC_ANDERSON_OUT_POWER),
OutWattsSensorEntity(client, "pd.typec1Watts", const.TYPEC_1_OUT_POWER),
OutWattsSensorEntity(client, "pd.typec2Watts", const.TYPEC_2_OUT_POWER),
OutWattsSensorEntity(client, "pd.usb1Watts", const.USB_1_OUT_POWER),
OutWattsSensorEntity(client, "pd.usb2Watts", const.USB_2_OUT_POWER),
OutWattsSensorEntity(client, "pd.qcUsb1Watts", const.USB_QC_1_OUT_POWER),
OutWattsSensorEntity(client, "pd.qcUsb2Watts", const.USB_QC_2_OUT_POWER),
RemainSensorEntity(client, "ems.chgRemainTime", const.CHARGE_REMAINING_TIME),
RemainSensorEntity(client, "ems.dsgRemainTime", const.DISCHARGE_REMAINING_TIME),
CyclesSensorEntity(client, "bmsMaster.cycles", const.CYCLES),
TempSensorEntity(client, "bmsMaster.temp", const.BATTERY_TEMP, False)
.attr("bmsMaster.minCellTemp", const.ATTR_MIN_CELL_TEMP, 0)
.attr("bmsMaster.maxCellTemp", const.ATTR_MAX_CELL_TEMP, 0),
MilliVoltSensorEntity(client, "bmsMaster.vol", const.BATTERY_VOLT, False)
.attr("bmsMaster.minCellVol", const.ATTR_MIN_CELL_VOLT, 0)
.attr("bmsMaster.maxCellVol", const.ATTR_MAX_CELL_VOLT, 0),
# https://github.com/tolwi/hassio-ecoflow-cloud/discussions/87
InEnergySensorEntity(client, "pd.chgSunPower", const.SOLAR_IN_ENERGY),
InEnergySensorEntity(client, "pd.chgPowerAc", const.CHARGE_AC_ENERGY),
InEnergySensorEntity(client, "pd.chgPowerDc", const.CHARGE_DC_ENERGY),
OutEnergySensorEntity(client, "pd.dsgPowerAc", const.DISCHARGE_AC_ENERGY),
OutEnergySensorEntity(client, "pd.dsgPowerDc", const.DISCHARGE_DC_ENERGY),
StatusSensorEntity(client),
]
def numbers(self, client: EcoflowMQTTClient) -> list[BaseNumberEntity]:
return [
MaxBatteryLevelEntity(client, "ems.maxChargeSoc", const.MAX_CHARGE_LEVEL, 50, 100,
lambda value: {"moduleType": 0, "operateType": "TCP",
"params": {"id": 49, "maxChgSoc": value}}),
MinBatteryLevelEntity(client, "ems.minDsgSoc", const.MIN_DISCHARGE_LEVEL, 0, 30,
lambda value: {"moduleType": 0, "operateType": "TCP",
"params": {"id": 51, "minDsgSoc": value}}),
# MaxBatteryLevelEntity(client, "pd.bpPowerSoc", const.BACKUP_RESERVE_LEVEL, 5, 100,
# lambda value: {"moduleType": 0, "operateType": "TCP",
# "params": {"isConfig": 1, "bpPowerSoc": int(value), "minDsgSoc": 0, "maxChgSoc": 0, "id": 94}}),
# MinGenStartLevelEntity(client, "ems.minOpenOilEbSoc", const.GEN_AUTO_START_LEVEL, 0, 30,
# lambda value: {"moduleType": 0, "operateType": "TCP",
# "params": {"openOilSoc": value, "id": 52}}),
#
# MaxGenStopLevelEntity(client, "ems.maxCloseOilEbSoc", const.GEN_AUTO_STOP_LEVEL, 50, 100,
# lambda value: {"moduleType": 0, "operateType": "TCP",
# "params": {"closeOilSoc": value, "id": 53}}),
ChargingPowerEntity(client, "inv.cfgSlowChgWatts", const.AC_CHARGING_POWER, 200, 900,
lambda value: {"moduleType": 0, "operateType": "TCP",
"params": {"slowChgPower": value, "id": 69}}),
]
def switches(self, client: EcoflowMQTTClient) -> list[BaseSwitchEntity]:
return [
BeeperEntity(client, "mppt.beepState", const.BEEPER,
lambda value: {"moduleType": 0, "operateType": "TCP", "params": {"id": 38, "enabled": value}}),
EnabledEntity(client, "mppt.carState", const.DC_ENABLED,
lambda value: {"moduleType": 0, "operateType": "TCP",
"params": {"id": 81, "enabled": value}}),
EnabledEntity(client, "inv.cfgAcEnabled", const.AC_ENABLED,
lambda value: {"moduleType": 0, "operateType": "TCP",
"params": {"id": 66, "enabled": value}}),
EnabledEntity(client, "inv.cfgAcXboost", const.XBOOST_ENABLED,
lambda value: {"moduleType": 0, "operateType": "TCP", "params": {"id": 66, "xboost": value}}),
# EnabledEntity(client, "inv.acPassByAutoEn", const.AC_ALWAYS_ENABLED,
# lambda value: {"moduleType": 0, "operateType": "TCP", "params": {"id": 84, "enabled": value}}),
# EnabledEntity(client, "pd.bpPowerSoc", const.BP_ENABLED,
# lambda value: {"moduleType": 0, "operateType": "TCP", "params": {"isConfig": value}}),
]
def selects(self, client: EcoflowMQTTClient) -> list[BaseSelectEntity]:
return [
DictSelectEntity(client, "mppt.cfgDcChgCurrent", const.DC_CHARGE_CURRENT, const.DC_CHARGE_CURRENT_OPTIONS,
lambda value: {"moduleType": 0, "operateType": "TCP",
"params": {"currMa": value, "id": 71}}),
TimeoutDictSelectEntity(client, "pd.lcdOffSec", const.SCREEN_TIMEOUT, const.SCREEN_TIMEOUT_OPTIONS,
lambda value: {"moduleType": 0, "operateType": "TCP",
"params": {"lcdTime": value, "id": 39}}),
TimeoutDictSelectEntity(client, "pd.standByMode", const.UNIT_TIMEOUT, const.UNIT_TIMEOUT_OPTIONS_LIMITED,
lambda value: {"moduleType": 0, "operateType": "TCP",
"params": {"standByMode": value, "id": 33}}),
TimeoutDictSelectEntity(client, "inv.cfgStandbyMin", const.AC_TIMEOUT, const.AC_TIMEOUT_OPTIONS,
lambda value: {"moduleType": 0, "operateType": "TCP",
"params": {"standByMins": value, "id": 153}}),
]
def migrate(self, version) -> list[EntityMigration]:
if version == 2:
return [
EntityMigration("pd.soc", Platform.SENSOR, MigrationAction.REMOVE),
]
return []

View File

@@ -0,0 +1,212 @@
from homeassistant.const import Platform
from . import const, BaseDevice, EntityMigration, MigrationAction
from ..entities import BaseSensorEntity, BaseNumberEntity, BaseSwitchEntity, BaseSelectEntity
from ..mqtt.ecoflow_mqtt import EcoflowMQTTClient
from ..number import ChargingPowerEntity, MaxBatteryLevelEntity, MinBatteryLevelEntity, MinGenStartLevelEntity, \
MaxGenStopLevelEntity
from ..select import DictSelectEntity, TimeoutDictSelectEntity
from ..sensor import LevelSensorEntity, WattsSensorEntity, RemainSensorEntity, TempSensorEntity, \
CyclesSensorEntity, InWattsSensorEntity, OutWattsSensorEntity, OutWattsDcSensorEntity, VoltSensorEntity, \
InWattsSolarSensorEntity, InVoltSolarSensorEntity, InAmpSolarSensorEntity, OutVoltDcSensorEntity, \
StatusSensorEntity, InEnergySensorEntity, OutEnergySensorEntity, MilliVoltSensorEntity, InMilliVoltSensorEntity, \
OutMilliVoltSensorEntity, AmpSensorEntity, CapacitySensorEntity
from ..switch import BeeperEntity, EnabledEntity
class DeltaPro(BaseDevice):
def sensors(self, client: EcoflowMQTTClient) -> list[BaseSensorEntity]:
return [
LevelSensorEntity(client, "bmsMaster.soc", const.MAIN_BATTERY_LEVEL)
.attr("bmsMaster.designCap", const.ATTR_DESIGN_CAPACITY, 0)
.attr("bmsMaster.fullCap", const.ATTR_FULL_CAPACITY, 0)
.attr("bmsMaster.remainCap", const.ATTR_REMAIN_CAPACITY, 0),
LevelSensorEntity(client, "bmsMaster.f32ShowSoc", const.MAIN_BATTERY_LEVEL_F32, False)
.attr("bmsMaster.designCap", const.ATTR_DESIGN_CAPACITY, 0)
.attr("bmsMaster.fullCap", const.ATTR_FULL_CAPACITY, 0)
.attr("bmsMaster.remainCap", const.ATTR_REMAIN_CAPACITY, 0),
CapacitySensorEntity(client, "bmsMaster.designCap", const.MAIN_DESIGN_CAPACITY, False),
CapacitySensorEntity(client, "bmsMaster.fullCap", const.MAIN_FULL_CAPACITY, False),
CapacitySensorEntity(client, "bmsMaster.remainCap", const.MAIN_REMAIN_CAPACITY, False),
LevelSensorEntity(client, "bmsMaster.soh", const.SOH),
LevelSensorEntity(client, "ems.lcdShowSoc", const.COMBINED_BATTERY_LEVEL),
LevelSensorEntity(client, "ems.f32LcdShowSoc", const.COMBINED_BATTERY_LEVEL_F32, False),
WattsSensorEntity(client, "pd.wattsInSum", const.TOTAL_IN_POWER),
WattsSensorEntity(client, "pd.wattsOutSum", const.TOTAL_OUT_POWER),
AmpSensorEntity(client, "bmsMaster.amp", const.MAIN_BATTERY_CURRENT),
InWattsSensorEntity(client, "inv.inputWatts", const.AC_IN_POWER),
OutWattsSensorEntity(client, "inv.outputWatts", const.AC_OUT_POWER),
InMilliVoltSensorEntity(client, "inv.acInVol", const.AC_IN_VOLT),
OutMilliVoltSensorEntity(client, "inv.invOutVol", const.AC_OUT_VOLT),
InWattsSolarSensorEntity(client, "mppt.inWatts", const.SOLAR_IN_POWER),
InVoltSolarSensorEntity(client, "mppt.inVol", const.SOLAR_IN_VOLTAGE),
InAmpSolarSensorEntity(client, "mppt.inAmp", const.SOLAR_IN_CURRENT),
OutWattsDcSensorEntity(client, "mppt.outWatts", const.DC_OUT_POWER),
OutVoltDcSensorEntity(client, "mppt.outVol", const.DC_OUT_VOLTAGE),
OutWattsSensorEntity(client, "mppt.carOutWatts", const.DC_CAR_OUT_POWER),
OutWattsSensorEntity(client, "mppt.dcdc12vWatts", const.DC_ANDERSON_OUT_POWER),
OutWattsSensorEntity(client, "pd.typec1Watts", const.TYPEC_1_OUT_POWER),
OutWattsSensorEntity(client, "pd.typec2Watts", const.TYPEC_2_OUT_POWER),
OutWattsSensorEntity(client, "pd.usb1Watts", const.USB_1_OUT_POWER),
OutWattsSensorEntity(client, "pd.usb2Watts", const.USB_2_OUT_POWER),
OutWattsSensorEntity(client, "pd.qcUsb1Watts", const.USB_QC_1_OUT_POWER),
OutWattsSensorEntity(client, "pd.qcUsb2Watts", const.USB_QC_2_OUT_POWER),
RemainSensorEntity(client, "ems.chgRemainTime", const.CHARGE_REMAINING_TIME),
RemainSensorEntity(client, "ems.dsgRemainTime", const.DISCHARGE_REMAINING_TIME),
CyclesSensorEntity(client, "bmsMaster.cycles", const.CYCLES),
TempSensorEntity(client, "bmsMaster.temp", const.BATTERY_TEMP)
.attr("bmsMaster.minCellTemp", const.ATTR_MIN_CELL_TEMP, 0)
.attr("bmsMaster.maxCellTemp", const.ATTR_MAX_CELL_TEMP, 0),
TempSensorEntity(client, "bmsMaster.minCellTemp", const.MIN_CELL_TEMP, False),
TempSensorEntity(client, "bmsMaster.maxCellTemp", const.MAX_CELL_TEMP, False),
MilliVoltSensorEntity(client, "bmsMaster.vol", const.BATTERY_VOLT, False)
.attr("bmsMaster.minCellVol", const.ATTR_MIN_CELL_VOLT, 0)
.attr("bmsMaster.maxCellVol", const.ATTR_MAX_CELL_VOLT, 0),
MilliVoltSensorEntity(client, "bmsMaster.minCellVol", const.MIN_CELL_VOLT, False),
MilliVoltSensorEntity(client, "bmsMaster.maxCellVol", const.MAX_CELL_VOLT, False),
# https://github.com/tolwi/hassio-ecoflow-cloud/discussions/87
InEnergySensorEntity(client, "pd.chgSunPower", const.SOLAR_IN_ENERGY),
InEnergySensorEntity(client, "pd.chgPowerAc", const.CHARGE_AC_ENERGY),
InEnergySensorEntity(client, "pd.chgPowerDc", const.CHARGE_DC_ENERGY),
OutEnergySensorEntity(client, "pd.dsgPowerAc", const.DISCHARGE_AC_ENERGY),
OutEnergySensorEntity(client, "pd.dsgPowerDc", const.DISCHARGE_DC_ENERGY),
# Optional Slave Batteries
LevelSensorEntity(client, "bmsSlave1.soc", const.SLAVE_N_BATTERY_LEVEL % 1, False, True)
.attr("bmsSlave1.designCap", const.ATTR_DESIGN_CAPACITY, 0)
.attr("bmsSlave1.fullCap", const.ATTR_FULL_CAPACITY, 0)
.attr("bmsSlave1.remainCap", const.ATTR_REMAIN_CAPACITY, 0),
LevelSensorEntity(client, "bmsSlave1.f32ShowSoc", const.SLAVE_N_BATTERY_LEVEL_F32 % 1, False, False)
.attr("bmsSlave1.designCap", const.ATTR_DESIGN_CAPACITY, 0)
.attr("bmsSlave1.fullCap", const.ATTR_FULL_CAPACITY, 0)
.attr("bmsSlave1.remainCap", const.ATTR_REMAIN_CAPACITY, 0),
CapacitySensorEntity(client, "bmsSlave1.designCap", const.SLAVE_N_DESIGN_CAPACITY % 1, False),
CapacitySensorEntity(client, "bmsSlave1.fullCap", const.SLAVE_N_FULL_CAPACITY % 1, False),
CapacitySensorEntity(client, "bmsSlave1.remainCap", const.SLAVE_N_REMAIN_CAPACITY % 1, False),
LevelSensorEntity(client, "bmsSlave1.soh", const.SLAVE_N_SOH % 1),
TempSensorEntity(client, "bmsSlave1.temp", const.SLAVE_N_BATTERY_TEMP % 1, False, True)
.attr("bmsSlave1.minCellTemp", const.ATTR_MIN_CELL_TEMP, 0)
.attr("bmsSlave1.maxCellTemp", const.ATTR_MAX_CELL_TEMP, 0),
WattsSensorEntity(client, "bmsSlave1.inputWatts", const.SLAVE_N_IN_POWER % 1, False, True),
WattsSensorEntity(client, "bmsSlave1.outputWatts", const.SLAVE_N_OUT_POWER % 1, False, True),
LevelSensorEntity(client, "bmsSlave2.soc", const.SLAVE_N_BATTERY_LEVEL % 2, False, True)
.attr("bmsSlave2.designCap", const.ATTR_DESIGN_CAPACITY, 0)
.attr("bmsSlave2.fullCap", const.ATTR_FULL_CAPACITY, 0)
.attr("bmsSlave2.remainCap", const.ATTR_REMAIN_CAPACITY, 0),
LevelSensorEntity(client, "bmsSlave2.f32ShowSoc", const.SLAVE_N_BATTERY_LEVEL_F32 % 2, False, False)
.attr("bmsSlave2.designCap", const.ATTR_DESIGN_CAPACITY, 0)
.attr("bmsSlave2.fullCap", const.ATTR_FULL_CAPACITY, 0)
.attr("bmsSlave2.remainCap", const.ATTR_REMAIN_CAPACITY, 0),
CapacitySensorEntity(client, "bmsSlave2.designCap", const.SLAVE_N_DESIGN_CAPACITY % 2, False),
CapacitySensorEntity(client, "bmsSlave2.fullCap", const.SLAVE_N_FULL_CAPACITY % 2, False),
CapacitySensorEntity(client, "bmsSlave2.remainCap", const.SLAVE_N_REMAIN_CAPACITY % 2, False),
LevelSensorEntity(client, "bmsSlave2.soh", const.SLAVE_N_SOH % 2),
MilliVoltSensorEntity(client, "bmsSlave1.vol", const.SLAVE_N_BATTERY_VOLT % 1, False),
MilliVoltSensorEntity(client, "bmsSlave1.minCellVol", const.SLAVE_N_MIN_CELL_VOLT % 1, False),
MilliVoltSensorEntity(client, "bmsSlave1.maxCellVol", const.SLAVE_N_MAX_CELL_VOLT % 1, False),
AmpSensorEntity(client, "bmsSlave1.amp", const.SLAVE_N_BATTERY_CURRENT % 1, False),
MilliVoltSensorEntity(client, "bmsSlave2.vol", const.SLAVE_N_BATTERY_VOLT % 2, False),
MilliVoltSensorEntity(client, "bmsSlave2.minCellVol", const.SLAVE_N_MIN_CELL_VOLT % 2, False),
MilliVoltSensorEntity(client, "bmsSlave2.maxCellVol", const.SLAVE_N_MAX_CELL_VOLT % 2, False),
AmpSensorEntity(client, "bmsSlave2.amp", const.SLAVE_N_BATTERY_CURRENT % 2, False),
TempSensorEntity(client, "bmsSlave2.temp", const.SLAVE_N_BATTERY_TEMP % 2, False, True)
.attr("bmsSlave2.minCellTemp", const.ATTR_MIN_CELL_TEMP, 0)
.attr("bmsSlave2.maxCellTemp", const.ATTR_MAX_CELL_TEMP, 0),
WattsSensorEntity(client, "bmsSlave2.inputWatts", const.SLAVE_N_IN_POWER % 2, False, True),
WattsSensorEntity(client, "bmsSlave2.outputWatts", const.SLAVE_N_OUT_POWER % 2, False, True),
CyclesSensorEntity(client, "bmsSlave1.cycles", const.SLAVE_N_CYCLES % 1, False),
CyclesSensorEntity(client, "bmsSlave2.cycles", const.SLAVE_N_CYCLES % 2, False),
StatusSensorEntity(client),
]
def numbers(self, client: EcoflowMQTTClient) -> list[BaseNumberEntity]:
return [
MaxBatteryLevelEntity(client, "ems.maxChargeSoc", const.MAX_CHARGE_LEVEL, 50, 100,
lambda value: {"moduleType": 0, "operateType": "TCP",
"params": {"id": 49, "maxChgSoc": value}}),
MinBatteryLevelEntity(client, "ems.minDsgSoc", const.MIN_DISCHARGE_LEVEL, 0, 30,
lambda value: {"moduleType": 0, "operateType": "TCP",
"params": {"id": 51, "minDsgSoc": value}}),
MaxBatteryLevelEntity(client, "pd.bpPowerSoc", const.BACKUP_RESERVE_LEVEL, 5, 100,
lambda value: {"moduleType": 0, "operateType": "TCP",
"params": {"isConfig": 1, "bpPowerSoc": int(value), "minDsgSoc": 0,
"maxChgSoc": 0, "id": 94}}),
MinGenStartLevelEntity(client, "ems.minOpenOilEbSoc", const.GEN_AUTO_START_LEVEL, 0, 30,
lambda value: {"moduleType": 0, "operateType": "TCP",
"params": {"openOilSoc": value, "id": 52}}),
MaxGenStopLevelEntity(client, "ems.maxCloseOilEbSoc", const.GEN_AUTO_STOP_LEVEL, 50, 100,
lambda value: {"moduleType": 0, "operateType": "TCP",
"params": {"closeOilSoc": value, "id": 53}}),
ChargingPowerEntity(client, "inv.cfgSlowChgWatts", const.AC_CHARGING_POWER, 200, 2900,
lambda value: {"moduleType": 0, "operateType": "TCP",
"params": {"slowChgPower": value, "id": 69}}),
]
def switches(self, client: EcoflowMQTTClient) -> list[BaseSwitchEntity]:
return [
BeeperEntity(client, "pd.beepState", const.BEEPER,
lambda value: {"moduleType": 0, "operateType": "TCP", "params": {"id": 38, "enabled": value}}),
EnabledEntity(client, "mppt.carState", const.DC_ENABLED,
lambda value: {"moduleType": 0, "operateType": "TCP",
"params": {"id": 81, "enabled": value}}),
EnabledEntity(client, "inv.cfgAcEnabled", const.AC_ENABLED,
lambda value: {"moduleType": 0, "operateType": "TCP",
"params": {"id": 66, "enabled": value}}),
EnabledEntity(client, "inv.cfgAcXboost", const.XBOOST_ENABLED,
lambda value: {"moduleType": 0, "operateType": "TCP", "params": {"id": 66, "xboost": value}}),
EnabledEntity(client, "pd.acautooutConfig", const.AC_ALWAYS_ENABLED,
lambda value: {"moduleType": 0, "operateType": "TCP",
"params": {"id": 95, "acautooutConfig": value}}),
EnabledEntity(client, "pd.bppowerSoc", const.BP_ENABLED,
lambda value, params: {"moduleType": 0, "operateType": "TCP",
"params": {"id": 94, "isConfig": value,
"bpPowerSoc": int(params.get("pd.bppowerSoc", 0)),
"minDsgSoc": 0,
"maxChgSoc": 0}}),
]
def selects(self, client: EcoflowMQTTClient) -> list[BaseSelectEntity]:
return [
DictSelectEntity(client, "mppt.cfgDcChgCurrent", const.DC_CHARGE_CURRENT, const.DC_CHARGE_CURRENT_OPTIONS,
lambda value: {"moduleType": 0, "operateType": "TCP",
"params": {"currMa": value, "id": 71}}),
TimeoutDictSelectEntity(client, "pd.lcdOffSec", const.SCREEN_TIMEOUT, const.SCREEN_TIMEOUT_OPTIONS,
lambda value: {"moduleType": 0, "operateType": "TCP",
"params": {"lcdTime": value, "id": 39}}),
TimeoutDictSelectEntity(client, "pd.standByMode", const.UNIT_TIMEOUT, const.UNIT_TIMEOUT_OPTIONS_LIMITED,
lambda value: {"moduleType": 0, "operateType": "TCP",
"params": {"standByMode": value, "id": 33}}),
TimeoutDictSelectEntity(client, "inv.cfgStandbyMin", const.AC_TIMEOUT, const.AC_TIMEOUT_OPTIONS,
lambda value: {"moduleType": 0, "operateType": "TCP",
"params": {"standByMins": value, "id": 153}}),
]
def migrate(self, version) -> list[EntityMigration]:
if version == 2:
return [
EntityMigration("pd.soc", Platform.SENSOR, MigrationAction.REMOVE),
]
return []

View File

@@ -0,0 +1,129 @@
from . import const, BaseDevice
from ..button import EnabledButtonEntity
from ..entities import BaseSensorEntity, BaseNumberEntity, BaseSwitchEntity, BaseSelectEntity, BaseButtonEntity
from ..mqtt.ecoflow_mqtt import EcoflowMQTTClient
from ..number import SetTempEntity
from ..sensor import LevelSensorEntity, RemainSensorEntity, SecondsRemainSensorEntity, TempSensorEntity, \
CyclesSensorEntity, InWattsSensorEntity, OutWattsSensorEntity, VoltSensorEntity, QuotasStatusSensorEntity, \
MilliVoltSensorEntity, ChargingStateSensorEntity, \
FanSensorEntity, MiscBinarySensorEntity, DecicelsiusSensorEntity, MiscSensorEntity, CapacitySensorEntity
from ..switch import EnabledEntity, InvertedBeeperEntity
class Glacier(BaseDevice):
def charging_power_step(self) -> int:
return 50
def sensors(self, client: EcoflowMQTTClient) -> list[BaseSensorEntity]:
return [
# Power and Battery Entities
LevelSensorEntity(client, "bms_bmsStatus.soc", const.MAIN_BATTERY_LEVEL)
.attr("bms_bmsStatus.designCap", const.ATTR_DESIGN_CAPACITY, 0)
.attr("bms_bmsStatus.fullCap", const.ATTR_FULL_CAPACITY, 0)
.attr("bms_bmsStatus.remainCap", const.ATTR_REMAIN_CAPACITY, 0),
CapacitySensorEntity(client, "bms_bmsStatus.designCap", const.MAIN_DESIGN_CAPACITY, False),
CapacitySensorEntity(client, "bms_bmsStatus.fullCap", const.MAIN_FULL_CAPACITY, False),
CapacitySensorEntity(client, "bms_bmsStatus.remainCap", const.MAIN_REMAIN_CAPACITY, False),
LevelSensorEntity(client, "bms_emsStatus.f32LcdSoc", const.COMBINED_BATTERY_LEVEL),
ChargingStateSensorEntity(client, "bms_emsStatus.chgState", const.BATTERY_CHARGING_STATE),
InWattsSensorEntity(client, "bms_bmsStatus.inWatts", const.TOTAL_IN_POWER),
OutWattsSensorEntity(client, "bms_bmsStatus.outWatts", const.TOTAL_OUT_POWER),
OutWattsSensorEntity(client, "pd.motorWat", "Motor Power"),
RemainSensorEntity(client, "bms_emsStatus.chgRemain", const.CHARGE_REMAINING_TIME),
RemainSensorEntity(client, "bms_emsStatus.dsgRemain", const.DISCHARGE_REMAINING_TIME),
CyclesSensorEntity(client, "bms_bmsStatus.cycles", const.CYCLES),
TempSensorEntity(client, "bms_bmsStatus.tmp", const.BATTERY_TEMP)
.attr("bms_bmsStatus.minCellTemp", const.ATTR_MIN_CELL_TEMP, 0)
.attr("bms_bmsStatus.maxCellTemp", const.ATTR_MAX_CELL_TEMP, 0),
TempSensorEntity(client, "bms_bmsStatus.minCellTmp", const.MIN_CELL_TEMP, False),
TempSensorEntity(client, "bms_bmsStatus.maxCellTmp", const.MAX_CELL_TEMP, False),
VoltSensorEntity(client, "bms_bmsStatus.vol", const.BATTERY_VOLT, False)
.attr("bms_bmsStatus.minCellVol", const.ATTR_MIN_CELL_VOLT, 0)
.attr("bms_bmsStatus.maxCellVol", const.ATTR_MAX_CELL_VOLT, 0),
MilliVoltSensorEntity(client, "bms_bmsStatus.minCellVol", const.MIN_CELL_VOLT, False),
MilliVoltSensorEntity(client, "bms_bmsStatus.maxCellVol", const.MAX_CELL_VOLT, False),
MiscBinarySensorEntity(client,"pd.batFlag", "Battery Present"),
MiscSensorEntity(client, "pd.xt60InState", "XT60 State"),
#Fridge Entities
FanSensorEntity(client, "bms_emsStatus.fanLvl", "Fan Level"),
DecicelsiusSensorEntity(client, "pd.ambientTmp", "Ambient Temperature"),
DecicelsiusSensorEntity(client, "pd.exhaustTmp", "Exhaust Temperature"),
DecicelsiusSensorEntity(client, "pd.tempWater", "Water Temperature"),
DecicelsiusSensorEntity(client, "pd.tmpL", "Left Temperature"),
DecicelsiusSensorEntity(client, "pd.tmpR", "Right Temperature"),
MiscBinarySensorEntity(client,"pd.flagTwoZone","Dual Zone Mode"),
SecondsRemainSensorEntity(client, "pd.iceTm", "Ice Time Remain"),
LevelSensorEntity(client, "pd.icePercent", "Ice Percentage"),
MiscSensorEntity(client, "pd.iceMkMode", "Ice Make Mode"),
MiscBinarySensorEntity(client,"pd.iceAlert","Ice Alert"),
MiscBinarySensorEntity(client,"pd.waterLine","Ice Water Level OK"),
QuotasStatusSensorEntity(client)
]
def numbers(self, client: EcoflowMQTTClient) -> list[BaseNumberEntity]:
return [
SetTempEntity(client,"pd.tmpLSet", "Left Set Temperature",-25, 10,
lambda value, params: {"moduleType": 1, "operateType": "temp",
"params": {"tmpM": int(params.get("pd.tmpMSet", 0)),
"tmpL": int(value),
"tmpR": int(params.get("pd.tmpRSet", 0))}}),
SetTempEntity(client,"pd.tmpMSet", "Combined Set Temperature",-25, 10,
lambda value, params: {"moduleType": 1, "operateType": "temp",
"params": {"tmpM": int(value),
"tmpL": int(params.get("pd.tmpLSet", 0)),
"tmpR": int(params.get("pd.tmpRSet", 0))}}),
SetTempEntity(client,"pd.tmpRSet", "Right Set Temperature",-25, 10,
lambda value, params: {"moduleType": 1, "operateType": "temp",
"params": {"tmpM": int(params.get("pd.tmpMSet", 0)),
"tmpL": int(params.get("pd.tmpLSet", 0)),
"tmpR": int(value)}}),
]
def switches(self, client: EcoflowMQTTClient) -> list[BaseSwitchEntity]:
return [
InvertedBeeperEntity(client, "pd.beepEn", const.BEEPER,
lambda value: {"moduleType": 1, "operateType": "beepEn", "params": {"flag": value}}),
EnabledEntity(client, "pd.coolMode", "Eco Mode",
lambda value: {"moduleType": 1, "operateType": "ecoMode", "params": {"mode": value}}),
#power parameter is inverted for some reason
EnabledEntity(client, "pd.pwrState", "Power",
lambda value: {"moduleType": 1, "operateType": "powerOff", "params": {"enable": value}}),
]
def buttons(self, client: EcoflowMQTTClient) -> list[BaseButtonEntity]:
return [
EnabledButtonEntity(client, "smlice", "Make Small Ice", lambda value: {"moduleType": 1, "operateType": "iceMake", "params": {"enable": 1, "iceShape": 0}}),
EnabledButtonEntity(client, "lrgice", "Make Large Ice", lambda value: {"moduleType": 1, "operateType": "iceMake", "params": {"enable": 1, "iceShape": 1}}),
EnabledButtonEntity(client, "deice", "Detach Ice", lambda value: {"moduleType": 1, "operateType": "deIce", "params": {"enable": 1}})
]
def selects(self, client: EcoflowMQTTClient) -> list[BaseSelectEntity]:
return [
]

View File

@@ -0,0 +1,104 @@
from . import BaseDevice
from .. import EcoflowMQTTClient
from ..entities import (
BaseSensorEntity, BaseNumberEntity, BaseSelectEntity, BaseSwitchEntity
)
from ..sensor import (
AmpSensorEntity, CentivoltSensorEntity, DeciampSensorEntity,
DecicelsiusSensorEntity, DecihertzSensorEntity, DeciwattsSensorEntity,
DecivoltSensorEntity, InWattsSolarSensorEntity, LevelSensorEntity,
MiscSensorEntity, RemainSensorEntity, StatusSensorEntity,
)
# from ..number import MinBatteryLevelEntity, MaxBatteryLevelEntity
# from ..select import DictSelectEntity
class PowerStream(BaseDevice):
def sensors(self, client: EcoflowMQTTClient) -> list[BaseSensorEntity]:
return [
InWattsSolarSensorEntity(client, "pv1_input_watts", "Solar 1 Watts"),
DecivoltSensorEntity(client, "pv1_input_volt", "Solar 1 Input Potential"),
CentivoltSensorEntity(client, "pv1_op_volt", "Solar 1 Op Potential"),
DeciampSensorEntity(client, "pv1_input_cur", "Solar 1 Currrent"),
DecicelsiusSensorEntity(client, "pv1_temp", "Solar 1 Temperature"),
MiscSensorEntity(client, "pv1_relay_status", "Solar 1 Relay Status"),
MiscSensorEntity(client, "pv1_error_code", "Solar 1 Error Code", False),
MiscSensorEntity(client, "pv1_warning_code", "Solar 1 Warning Code", False),
MiscSensorEntity(client, "pv1_status", "Solar 1 Status", False),
InWattsSolarSensorEntity(client, "pv2_input_watts", "Solar 2 Watts"),
DecivoltSensorEntity(client, "pv2_input_volt", "Solar 2 Input Potential"),
CentivoltSensorEntity(client, "pv2_op_volt", "Solar 2 Op Potential"),
DeciampSensorEntity(client, "pv2_input_cur", "Solar 2 Current"),
DecicelsiusSensorEntity(client, "pv2_temp", "Solar 2 Temperature"),
MiscSensorEntity(client, "pv2_relay_status", "Solar 2 Relay Status"),
MiscSensorEntity(client, "pv2_error_code", "Solar 2 Error Code", False),
MiscSensorEntity(client, "pv2_warning_code", "Solar 2 Warning Code", False),
MiscSensorEntity(client, "pv2_status", "Solar 2 Status", False),
MiscSensorEntity(client, "bp_type", "Battery Type", False),
LevelSensorEntity(client, "bat_soc", "Battery Charge"),
DeciwattsSensorEntity(client, "bat_input_watts", "Battery Input Watts"),
DecivoltSensorEntity(client, "bat_input_volt", "Battery Input Potential"),
DecivoltSensorEntity(client, "bat_op_volt", "Battery Op Potential"),
AmpSensorEntity(client, "bat_input_cur", "Battery Input Current"),
DecicelsiusSensorEntity(client, "bat_temp", "Battery Temperature"),
RemainSensorEntity(client, "battery_charge_remain", "Charge Time"),
RemainSensorEntity(client, "battery_discharge_remain", "Discharge Time"),
MiscSensorEntity(client, "bat_error_code", "Battery Error Code", False),
MiscSensorEntity(client, "bat_warning_code", "Battery Warning Code", False),
MiscSensorEntity(client, "bat_status", "Battery Status", False),
DecivoltSensorEntity(client, "llc_input_volt", "LLC Input Potential", False),
DecivoltSensorEntity(client, "llc_op_volt", "LLC Op Potential", False),
MiscSensorEntity(client, "llc_error_code", "LLC Error Code", False),
MiscSensorEntity(client, "llc_warning_code", "LLC Warning Code", False),
MiscSensorEntity(client, "llc_status", "LLC Status", False),
MiscSensorEntity(client, "inv_on_off", "Inverter On/Off Status"),
DeciwattsSensorEntity(client, "inv_output_watts", "Inverter Output Watts"),
DecivoltSensorEntity(client, "inv_input_volt", "Inverter Output Potential", False),
DecivoltSensorEntity(client, "inv_op_volt", "Inverter Op Potential"),
AmpSensorEntity(client, "inv_output_cur", "Inverter Output Current"),
AmpSensorEntity(client, "inv_dc_cur", "Inverter DC Current"),
DecihertzSensorEntity(client, "inv_freq", "Inverter Frequency"),
DecicelsiusSensorEntity(client, "inv_temp", "Inverter Temperature"),
MiscSensorEntity(client, "inv_relay_status", "Inverter Relay Status"),
MiscSensorEntity(client, "inv_error_code", "Inverter Error Code", False),
MiscSensorEntity(client, "inv_warning_code", "Inverter Warning Code", False),
MiscSensorEntity(client, "inv_status", "Inverter Status", False),
DeciwattsSensorEntity(client, "permanent_watts", "Other Loads"),
DeciwattsSensorEntity(client, "dynamic_watts", "Smart Plug Loads"),
DeciwattsSensorEntity(client, "rated_power", "Rated Power"),
MiscSensorEntity(client, "lower_limit", "Lower Battery Limit", False),
MiscSensorEntity(client, "upper_limit", "Upper Battery Limit", False),
MiscSensorEntity(client, "wireless_error_code", "Wireless Error Code", False),
MiscSensorEntity(client, "wireless_warning_code", "Wireless Warning Code", False),
MiscSensorEntity(client, "inv_brightness", "LED Brightness", False),
MiscSensorEntity(client, "heartbeat_frequency", "Heartbeat Frequency", False),
StatusSensorEntity(client)
]
def numbers(self, client: EcoflowMQTTClient) -> list[BaseNumberEntity]:
return [
# These will likely be some form of serialised data rather than JSON will look into it later
# MinBatteryLevelEntity(client, "lowerLimit", "Min Disharge Level", 50, 100,
# lambda value: {"moduleType": 0, "operateType": "TCP",
# "params": {"id": 00, "lowerLimit": value}}),
# MaxBatteryLevelEntity(client, "upperLimit", "Max Charge Level", 0, 30,
# lambda value: {"moduleType": 0, "operateType": "TCP",
# "params": {"id": 00, "upperLimit": value}}),
]
def switches(self, client: EcoflowMQTTClient) -> list[BaseSwitchEntity]:
return []
def selects(self, client: EcoflowMQTTClient) -> list[BaseSelectEntity]:
return [
# DictSelectEntity(client, "supplyPriority", "Power supply mode", {"Prioritize power supply", "Prioritize power storage"},
# lambda value: {"moduleType": 00, "operateType": "supplyPriority",
# "params": {"supplyPriority": value}}),
]

View File

@@ -0,0 +1,32 @@
from custom_components.ecoflow_cloud.config_flow import EcoflowModel
from custom_components.ecoflow_cloud.devices import BaseDevice, DiagnosticDevice
from custom_components.ecoflow_cloud.devices.delta2 import Delta2
from custom_components.ecoflow_cloud.devices.delta_mini import DeltaMini
from custom_components.ecoflow_cloud.devices.delta_pro import DeltaPro
from custom_components.ecoflow_cloud.devices.river2 import River2
from custom_components.ecoflow_cloud.devices.river2_max import River2Max
from custom_components.ecoflow_cloud.devices.river2_pro import River2Pro
from custom_components.ecoflow_cloud.devices.river_max import RiverMax
from custom_components.ecoflow_cloud.devices.river_pro import RiverPro
from custom_components.ecoflow_cloud.devices.delta_max import DeltaMax
from custom_components.ecoflow_cloud.devices.delta2_max import Delta2Max
from custom_components.ecoflow_cloud.devices.powerstream import PowerStream
from custom_components.ecoflow_cloud.devices.glacier import Glacier
from custom_components.ecoflow_cloud.devices.wave2 import Wave2
devices: dict[str, BaseDevice] = {
EcoflowModel.DELTA_2.name: Delta2(),
EcoflowModel.RIVER_2.name: River2(),
EcoflowModel.RIVER_2_MAX.name: River2Max(),
EcoflowModel.RIVER_2_PRO.name: River2Pro(),
EcoflowModel.DELTA_PRO.name: DeltaPro(),
EcoflowModel.RIVER_MAX.name: RiverMax(),
EcoflowModel.RIVER_PRO.name: RiverPro(),
EcoflowModel.DELTA_MINI.name: DeltaMini(),
EcoflowModel.DELTA_MAX.name: DeltaMax(),
EcoflowModel.DELTA_2_MAX.name: Delta2Max(),
EcoflowModel.POWERSTREAM.name: PowerStream(),
EcoflowModel.GLACIER.name: Glacier(),
EcoflowModel.WAVE_2.name: Wave2(),
EcoflowModel.DIAGNOSTIC.name: DiagnosticDevice()
}

View File

@@ -0,0 +1,135 @@
from homeassistant.const import Platform
from . import const, BaseDevice, EntityMigration, MigrationAction
from ..entities import BaseSensorEntity, BaseNumberEntity, BaseSwitchEntity, BaseSelectEntity
from ..mqtt.ecoflow_mqtt import EcoflowMQTTClient
from ..number import ChargingPowerEntity, MaxBatteryLevelEntity, MinBatteryLevelEntity
from ..select import DictSelectEntity, TimeoutDictSelectEntity
from ..sensor import LevelSensorEntity, RemainSensorEntity, TempSensorEntity, \
CyclesSensorEntity, InWattsSensorEntity, OutWattsSensorEntity, VoltSensorEntity, StatusSensorEntity, \
MilliVoltSensorEntity, InMilliVoltSensorEntity, OutMilliVoltSensorEntity, ChargingStateSensorEntity, \
CapacitySensorEntity
from ..switch import EnabledEntity
class River2(BaseDevice):
def charging_power_step(self) -> int:
return 50
def sensors(self, client: EcoflowMQTTClient) -> list[BaseSensorEntity]:
return [
LevelSensorEntity(client, "bms_bmsStatus.soc", const.MAIN_BATTERY_LEVEL)
.attr("bms_bmsStatus.designCap", const.ATTR_DESIGN_CAPACITY, 0)
.attr("bms_bmsStatus.fullCap", const.ATTR_FULL_CAPACITY, 0)
.attr("bms_bmsStatus.remainCap", const.ATTR_REMAIN_CAPACITY, 0),
CapacitySensorEntity(client, "bms_bmsStatus.designCap", const.MAIN_DESIGN_CAPACITY, False),
CapacitySensorEntity(client, "bms_bmsStatus.fullCap", const.MAIN_FULL_CAPACITY, False),
CapacitySensorEntity(client, "bms_bmsStatus.remainCap", const.MAIN_REMAIN_CAPACITY, False),
LevelSensorEntity(client, "bms_bmsStatus.soh", const.SOH),
LevelSensorEntity(client, "bms_emsStatus.lcdShowSoc", const.COMBINED_BATTERY_LEVEL),
ChargingStateSensorEntity(client, "bms_emsStatus.chgState", const.BATTERY_CHARGING_STATE),
InWattsSensorEntity(client, "pd.wattsInSum", const.TOTAL_IN_POWER),
OutWattsSensorEntity(client, "pd.wattsOutSum", const.TOTAL_OUT_POWER),
InWattsSensorEntity(client, "inv.inputWatts", const.AC_IN_POWER),
OutWattsSensorEntity(client, "inv.outputWatts", const.AC_OUT_POWER),
InMilliVoltSensorEntity(client, "inv.acInVol", const.AC_IN_VOLT),
OutMilliVoltSensorEntity(client, "inv.invOutVol", const.AC_OUT_VOLT),
InWattsSensorEntity(client, "pd.typecChaWatts", const.TYPE_C_IN_POWER),
InWattsSensorEntity(client, "mppt.inWatts", const.SOLAR_IN_POWER),
OutWattsSensorEntity(client, "pd.carWatts", const.DC_OUT_POWER),
OutWattsSensorEntity(client, "pd.typec1Watts", const.TYPEC_1_OUT_POWER),
# both USB-A Ports (the small RIVER 2 has only two) are being summarized under "pd.usb1Watts" - https://github.com/tolwi/hassio-ecoflow-cloud/issues/12#issuecomment-1432837393
OutWattsSensorEntity(client, "pd.usb1Watts", const.USB_OUT_POWER),
RemainSensorEntity(client, "bms_emsStatus.chgRemainTime", const.CHARGE_REMAINING_TIME),
RemainSensorEntity(client, "bms_emsStatus.dsgRemainTime", const.DISCHARGE_REMAINING_TIME),
TempSensorEntity(client, "inv.outTemp", "Inv Out Temperature"),
CyclesSensorEntity(client, "bms_bmsStatus.cycles", const.CYCLES),
TempSensorEntity(client, "bms_bmsStatus.temp", const.BATTERY_TEMP)
.attr("bms_bmsStatus.minCellTemp", const.ATTR_MIN_CELL_TEMP, 0)
.attr("bms_bmsStatus.maxCellTemp", const.ATTR_MAX_CELL_TEMP, 0),
TempSensorEntity(client, "bms_bmsStatus.minCellTemp", const.MIN_CELL_TEMP, False),
TempSensorEntity(client, "bms_bmsStatus.maxCellTemp", const.MAX_CELL_TEMP, False),
VoltSensorEntity(client, "bms_bmsStatus.vol", const.BATTERY_VOLT, False)
.attr("bms_bmsStatus.minCellVol", const.ATTR_MIN_CELL_VOLT, 0)
.attr("bms_bmsStatus.maxCellVol", const.ATTR_MAX_CELL_VOLT, 0),
MilliVoltSensorEntity(client, "bms_bmsStatus.minCellVol", const.MIN_CELL_VOLT, False),
MilliVoltSensorEntity(client, "bms_bmsStatus.maxCellVol", const.MAX_CELL_VOLT, False),
# FanSensorEntity(client, "bms_emsStatus.fanLevel", "Fan Level"),
StatusSensorEntity(client),
]
def numbers(self, client: EcoflowMQTTClient) -> list[BaseNumberEntity]:
return [
MaxBatteryLevelEntity(client, "bms_emsStatus.maxChargeSoc", const.MAX_CHARGE_LEVEL, 50, 100,
lambda value: {"moduleType": 2, "operateType": "upsConfig",
"params": {"maxChgSoc": int(value)}}),
MinBatteryLevelEntity(client, "bms_emsStatus.minDsgSoc", const.MIN_DISCHARGE_LEVEL, 0, 30,
lambda value: {"moduleType": 2, "operateType": "dsgCfg",
"params": {"minDsgSoc": int(value)}}),
ChargingPowerEntity(client, "mppt.cfgChgWatts", const.AC_CHARGING_POWER, 100, 360,
lambda value: {"moduleType": 5, "operateType": "acChgCfg",
"params": {"chgWatts": int(value), "chgPauseFlag": 255}}),
]
def switches(self, client: EcoflowMQTTClient) -> list[BaseSwitchEntity]:
return [
EnabledEntity(client, "mppt.cfgAcEnabled", const.AC_ENABLED,
lambda value: {"moduleType": 5, "operateType": "acOutCfg",
"params": {"enabled": value, "out_voltage": -1, "out_freq": 255,
"xboost": 255}}),
EnabledEntity(client, "mppt.cfgAcXboost", const.XBOOST_ENABLED,
lambda value: {"moduleType": 5, "operateType": "acOutCfg",
"params": {"enabled": 255, "out_voltage": -1, "out_freq": 255,
"xboost": value}}),
EnabledEntity(client, "pd.carState", const.DC_ENABLED,
lambda value: {"moduleType": 5, "operateType": "mpptCar", "params": {"enabled": value}})
]
def selects(self, client: EcoflowMQTTClient) -> list[BaseSelectEntity]:
return [
DictSelectEntity(client, "mppt.dcChgCurrent", const.DC_CHARGE_CURRENT, const.DC_CHARGE_CURRENT_OPTIONS,
lambda value: {"moduleType": 5, "operateType": "dcChgCfg",
"params": {"dcChgCfg": value}}),
DictSelectEntity(client, "mppt.cfgChgType", const.DC_MODE, const.DC_MODE_OPTIONS,
lambda value: {"moduleType": 5, "operateType": "chaType",
"params": {"chaType": value}}),
TimeoutDictSelectEntity(client, "mppt.scrStandbyMin", const.SCREEN_TIMEOUT, const.SCREEN_TIMEOUT_OPTIONS,
lambda value: {"moduleType": 5, "operateType": "lcdCfg",
"params": {"brighLevel": 255, "delayOff": value}}),
TimeoutDictSelectEntity(client, "mppt.powStandbyMin", const.UNIT_TIMEOUT, const.UNIT_TIMEOUT_OPTIONS,
lambda value: {"moduleType": 5, "operateType": "standby",
"params": {"standbyMins": value}}),
TimeoutDictSelectEntity(client, "mppt.acStandbyMins", const.AC_TIMEOUT, const.AC_TIMEOUT_OPTIONS,
lambda value: {"moduleType": 5, "operateType": "acStandby",
"params": {"standbyMins": value}})
]
def migrate(self, version) -> list[EntityMigration]:
if version == 2:
return [
EntityMigration("pd.soc", Platform.SENSOR, MigrationAction.REMOVE),
]
return []

View File

@@ -0,0 +1,163 @@
from homeassistant.const import Platform
from . import const, BaseDevice, EntityMigration, MigrationAction
from .const import ATTR_DESIGN_CAPACITY, ATTR_FULL_CAPACITY, ATTR_REMAIN_CAPACITY, BATTERY_CHARGING_STATE, \
MAIN_DESIGN_CAPACITY, MAIN_FULL_CAPACITY, MAIN_REMAIN_CAPACITY
from ..entities import BaseSensorEntity, BaseNumberEntity, BaseSwitchEntity, BaseSelectEntity
from ..mqtt.ecoflow_mqtt import EcoflowMQTTClient
from ..number import ChargingPowerEntity, MaxBatteryLevelEntity, MinBatteryLevelEntity, BatteryBackupLevel
from ..select import DictSelectEntity, TimeoutDictSelectEntity
from ..sensor import LevelSensorEntity, RemainSensorEntity, TempSensorEntity, \
CyclesSensorEntity, InWattsSensorEntity, OutWattsSensorEntity, VoltSensorEntity, InAmpSensorEntity, \
InVoltSensorEntity, QuotasStatusSensorEntity, MilliVoltSensorEntity, InMilliVoltSensorEntity, \
OutMilliVoltSensorEntity, ChargingStateSensorEntity, CapacitySensorEntity
from ..switch import EnabledEntity
class River2Max(BaseDevice):
def charging_power_step(self) -> int:
return 50
def sensors(self, client: EcoflowMQTTClient) -> list[BaseSensorEntity]:
return [
LevelSensorEntity(client, "bms_bmsStatus.soc", const.MAIN_BATTERY_LEVEL)
.attr("bms_bmsStatus.designCap", ATTR_DESIGN_CAPACITY, 0)
.attr("bms_bmsStatus.fullCap", ATTR_FULL_CAPACITY, 0)
.attr("bms_bmsStatus.remainCap", ATTR_REMAIN_CAPACITY, 0),
CapacitySensorEntity(client, "bms_bmsStatus.designCap", MAIN_DESIGN_CAPACITY, False),
CapacitySensorEntity(client, "bms_bmsStatus.fullCap", MAIN_FULL_CAPACITY, False),
CapacitySensorEntity(client, "bms_bmsStatus.remainCap", MAIN_REMAIN_CAPACITY, False),
LevelSensorEntity(client, "bms_bmsStatus.soh", const.SOH),
LevelSensorEntity(client, "bms_emsStatus.lcdShowSoc", const.COMBINED_BATTERY_LEVEL),
ChargingStateSensorEntity(client, "bms_emsStatus.chgState", BATTERY_CHARGING_STATE),
InWattsSensorEntity(client, "pd.wattsInSum", const.TOTAL_IN_POWER),
OutWattsSensorEntity(client, "pd.wattsOutSum", const.TOTAL_OUT_POWER),
InAmpSensorEntity(client, "inv.dcInAmp", const.SOLAR_IN_CURRENT),
InVoltSensorEntity(client, "inv.dcInVol", const.SOLAR_IN_VOLTAGE),
InWattsSensorEntity(client, "inv.inputWatts", const.AC_IN_POWER),
OutWattsSensorEntity(client, "inv.outputWatts", const.AC_OUT_POWER),
InMilliVoltSensorEntity(client, "inv.acInVol", const.AC_IN_VOLT),
OutMilliVoltSensorEntity(client, "inv.invOutVol", const.AC_OUT_VOLT),
InWattsSensorEntity(client, "pd.typecChaWatts", const.TYPE_C_IN_POWER),
InWattsSensorEntity(client, "mppt.inWatts", const.SOLAR_IN_POWER),
OutWattsSensorEntity(client, "pd.carWatts", const.DC_OUT_POWER),
OutWattsSensorEntity(client, "pd.typec1Watts", const.TYPEC_OUT_POWER),
OutWattsSensorEntity(client, "pd.usb1Watts", const.USB_OUT_POWER),
# OutWattsSensorEntity(client, "pd.usb2Watts", const.USB_2_OUT_POWER),
RemainSensorEntity(client, "bms_emsStatus.chgRemainTime", const.CHARGE_REMAINING_TIME),
RemainSensorEntity(client, "bms_emsStatus.dsgRemainTime", const.DISCHARGE_REMAINING_TIME),
RemainSensorEntity(client, "pd.remainTime", const.REMAINING_TIME),
TempSensorEntity(client, "inv.outTemp", "Inv Out Temperature"),
CyclesSensorEntity(client, "bms_bmsStatus.cycles", const.CYCLES),
TempSensorEntity(client, "bms_bmsStatus.temp", const.BATTERY_TEMP)
.attr("bms_bmsStatus.minCellTemp", const.ATTR_MIN_CELL_TEMP, 0)
.attr("bms_bmsStatus.maxCellTemp", const.ATTR_MAX_CELL_TEMP, 0),
TempSensorEntity(client, "bms_bmsStatus.minCellTemp", const.MIN_CELL_TEMP, False),
TempSensorEntity(client, "bms_bmsStatus.maxCellTemp", const.MAX_CELL_TEMP, False),
VoltSensorEntity(client, "bms_bmsStatus.vol", const.BATTERY_VOLT, False)
.attr("bms_bmsStatus.minCellVol", const.ATTR_MIN_CELL_VOLT, 0)
.attr("bms_bmsStatus.maxCellVol", const.ATTR_MAX_CELL_VOLT, 0),
MilliVoltSensorEntity(client, "bms_bmsStatus.minCellVol", const.MIN_CELL_VOLT, False),
MilliVoltSensorEntity(client, "bms_bmsStatus.maxCellVol", const.MAX_CELL_VOLT, False),
QuotasStatusSensorEntity(client),
# FanSensorEntity(client, "bms_emsStatus.fanLevel", "Fan Level"),
]
def numbers(self, client: EcoflowMQTTClient) -> list[BaseNumberEntity]:
return [
MaxBatteryLevelEntity(client, "bms_emsStatus.maxChargeSoc", const.MAX_CHARGE_LEVEL, 50, 100,
lambda value: {"moduleType": 2, "operateType": "upsConfig",
"params": {"maxChgSoc": int(value)}}),
MinBatteryLevelEntity(client, "bms_emsStatus.minDsgSoc", const.MIN_DISCHARGE_LEVEL, 0, 30,
lambda value: {"moduleType": 2, "operateType": "dsgCfg",
"params": {"minDsgSoc": int(value)}}),
ChargingPowerEntity(client, "mppt.cfgChgWatts", const.AC_CHARGING_POWER, 50, 660,
lambda value: {"moduleType": 5, "operateType": "acChgCfg",
"params": {"chgWatts": int(value), "chgPauseFlag": 255}}),
BatteryBackupLevel(client, "pd.bpPowerSoc", const.BACKUP_RESERVE_LEVEL, 5, 100,
"bms_emsStatus.minDsgSoc", "bms_emsStatus.maxChargeSoc",
lambda value: {"moduleType": 1, "operateType": "watthConfig",
"params": {"isConfig": 1,
"bpPowerSoc": int(value),
"minDsgSoc": 0,
"minChgSoc": 0}}),
]
def switches(self, client: EcoflowMQTTClient) -> list[BaseSwitchEntity]:
return [
EnabledEntity(client, "mppt.cfgAcEnabled", const.AC_ENABLED,
lambda value: {"moduleType": 5, "operateType": "acOutCfg",
"params": {"enabled": value, "out_voltage": -1, "out_freq": 255,
"xboost": 255}}),
EnabledEntity(client, "pd.acAutoOutConfig", const.AC_ALWAYS_ENABLED,
lambda value, params: {"moduleType": 1, "operateType": "acAutoOutConfig",
"params": {"acAutoOutConfig": value,
"minAcOutSoc": int(params.get("bms_emsStatus.minDsgSoc", 0)) + 5}}
),
EnabledEntity(client, "mppt.cfgAcXboost", const.XBOOST_ENABLED,
lambda value: {"moduleType": 5, "operateType": "acOutCfg",
"params": {"enabled": 255, "out_voltage": -1, "out_freq": 255,
"xboost": value}}),
EnabledEntity(client, "pd.carState", const.DC_ENABLED,
lambda value: {"moduleType": 5, "operateType": "mpptCar", "params": {"enabled": value}}),
EnabledEntity(client, "pd.bpPowerSoc", const.BP_ENABLED,
lambda value, params: {"moduleType": 1, "operateType": "watthConfig",
"params": {"isConfig": value,
"bpPowerSoc": value,
"minDsgSoc": 0,
"minChgSoc": 0}})
]
def selects(self, client: EcoflowMQTTClient) -> list[BaseSelectEntity]:
return [
DictSelectEntity(client, "mppt.dcChgCurrent", const.DC_CHARGE_CURRENT, const.DC_CHARGE_CURRENT_OPTIONS,
lambda value: {"moduleType": 5, "operateType": "dcChgCfg",
"params": {"dcChgCfg": value}}),
DictSelectEntity(client, "mppt.cfgChgType", const.DC_MODE, const.DC_MODE_OPTIONS,
lambda value: {"moduleType": 5, "operateType": "chaType",
"params": {"chaType": value}}),
TimeoutDictSelectEntity(client, "mppt.scrStandbyMin", const.SCREEN_TIMEOUT, const.SCREEN_TIMEOUT_OPTIONS,
lambda value: {"moduleType": 5, "operateType": "lcdCfg",
"params": {"brighLevel": 255, "delayOff": value}}),
TimeoutDictSelectEntity(client, "mppt.powStandbyMin", const.UNIT_TIMEOUT, const.UNIT_TIMEOUT_OPTIONS,
lambda value: {"moduleType": 5, "operateType": "standby",
"params": {"standbyMins": value}}),
TimeoutDictSelectEntity(client, "mppt.acStandbyMins", const.AC_TIMEOUT, const.AC_TIMEOUT_OPTIONS,
lambda value: {"moduleType": 5, "operateType": "acStandby",
"params": {"standbyMins": value}})
]
def migrate(self, version) -> list[EntityMigration]:
if version == 2:
return [
EntityMigration("pd.soc", Platform.SENSOR, MigrationAction.REMOVE),
]
return []

View File

@@ -0,0 +1,136 @@
from homeassistant.const import Platform
from . import const, BaseDevice, EntityMigration, MigrationAction
from ..entities import BaseSensorEntity, BaseNumberEntity, BaseSwitchEntity, BaseSelectEntity
from ..mqtt.ecoflow_mqtt import EcoflowMQTTClient
from ..number import ChargingPowerEntity, MaxBatteryLevelEntity, MinBatteryLevelEntity
from ..select import DictSelectEntity, TimeoutDictSelectEntity
from ..sensor import LevelSensorEntity, RemainSensorEntity, TempSensorEntity, \
CyclesSensorEntity, InWattsSensorEntity, OutWattsSensorEntity, VoltSensorEntity, QuotasStatusSensorEntity, \
MilliVoltSensorEntity, InMilliVoltSensorEntity, OutMilliVoltSensorEntity, ChargingStateSensorEntity, \
CapacitySensorEntity
from ..switch import EnabledEntity
class River2Pro(BaseDevice):
def charging_power_step(self) -> int:
return 50
def sensors(self, client: EcoflowMQTTClient) -> list[BaseSensorEntity]:
return [
LevelSensorEntity(client, "bms_bmsStatus.soc", const.MAIN_BATTERY_LEVEL)
.attr("bms_bmsStatus.designCap", const.ATTR_DESIGN_CAPACITY, 0)
.attr("bms_bmsStatus.fullCap", const.ATTR_FULL_CAPACITY, 0)
.attr("bms_bmsStatus.remainCap", const.ATTR_REMAIN_CAPACITY, 0),
CapacitySensorEntity(client, "bms_bmsStatus.designCap", const.MAIN_DESIGN_CAPACITY, False),
CapacitySensorEntity(client, "bms_bmsStatus.fullCap", const.MAIN_FULL_CAPACITY, False),
CapacitySensorEntity(client, "bms_bmsStatus.remainCap", const.MAIN_REMAIN_CAPACITY, False),
LevelSensorEntity(client, "bms_bmsStatus.soh", const.SOH),
LevelSensorEntity(client, "bms_emsStatus.lcdShowSoc", const.COMBINED_BATTERY_LEVEL),
ChargingStateSensorEntity(client, "bms_emsStatus.chgState", const.BATTERY_CHARGING_STATE),
InWattsSensorEntity(client, "pd.wattsInSum", const.TOTAL_IN_POWER),
OutWattsSensorEntity(client, "pd.wattsOutSum", const.TOTAL_OUT_POWER),
InWattsSensorEntity(client, "inv.inputWatts", const.AC_IN_POWER),
OutWattsSensorEntity(client, "inv.outputWatts", const.AC_OUT_POWER),
InMilliVoltSensorEntity(client, "inv.acInVol", const.AC_IN_VOLT),
OutMilliVoltSensorEntity(client, "inv.invOutVol", const.AC_OUT_VOLT),
InWattsSensorEntity(client, "pd.typecChaWatts", const.TYPE_C_IN_POWER),
InWattsSensorEntity(client, "mppt.inWatts", const.SOLAR_IN_POWER),
OutWattsSensorEntity(client, "pd.carWatts", const.DC_OUT_POWER),
OutWattsSensorEntity(client, "pd.typec1Watts", const.TYPEC_OUT_POWER),
OutWattsSensorEntity(client, "pd.usb1Watts", const.USB_OUT_POWER),
# OutWattsSensorEntity(client, "pd.usb2Watts", const.USB_2_OUT_POWER),
RemainSensorEntity(client, "bms_emsStatus.chgRemainTime", const.CHARGE_REMAINING_TIME),
RemainSensorEntity(client, "bms_emsStatus.dsgRemainTime", const.DISCHARGE_REMAINING_TIME),
RemainSensorEntity(client, "pd.remainTime", const.REMAINING_TIME),
TempSensorEntity(client, "inv.outTemp", "Inv Out Temperature"),
CyclesSensorEntity(client, "bms_bmsStatus.cycles", const.CYCLES),
TempSensorEntity(client, "bms_bmsStatus.temp", const.BATTERY_TEMP)
.attr("bms_bmsStatus.minCellTemp", const.ATTR_MIN_CELL_TEMP, 0)
.attr("bms_bmsStatus.maxCellTemp", const.ATTR_MAX_CELL_TEMP, 0),
TempSensorEntity(client, "bms_bmsStatus.minCellTemp", const.MIN_CELL_TEMP, False),
TempSensorEntity(client, "bms_bmsStatus.maxCellTemp", const.MAX_CELL_TEMP, False),
VoltSensorEntity(client, "bms_bmsStatus.vol", const.BATTERY_VOLT, False)
.attr("bms_bmsStatus.minCellVol", const.ATTR_MIN_CELL_VOLT, 0)
.attr("bms_bmsStatus.maxCellVol", const.ATTR_MAX_CELL_VOLT, 0),
MilliVoltSensorEntity(client, "bms_bmsStatus.minCellVol", const.MIN_CELL_VOLT, False),
MilliVoltSensorEntity(client, "bms_bmsStatus.maxCellVol", const.MAX_CELL_VOLT, False),
# FanSensorEntity(client, "bms_emsStatus.fanLevel", "Fan Level"),
QuotasStatusSensorEntity(client),
]
def numbers(self, client: EcoflowMQTTClient) -> list[BaseNumberEntity]:
return [
MaxBatteryLevelEntity(client, "bms_emsStatus.maxChargeSoc", const.MAX_CHARGE_LEVEL, 50, 100,
lambda value: {"moduleType": 2, "operateType": "upsConfig",
"params": {"maxChgSoc": int(value)}}),
MinBatteryLevelEntity(client, "bms_emsStatus.minDsgSoc", const.MIN_DISCHARGE_LEVEL, 0, 30,
lambda value: {"moduleType": 2, "operateType": "dsgCfg",
"params": {"minDsgSoc": int(value)}}),
ChargingPowerEntity(client, "mppt.cfgChgWatts", const.AC_CHARGING_POWER, 100, 950,
lambda value: {"moduleType": 5, "operateType": "acChgCfg",
"params": {"chgWatts": int(value), "chgPauseFlag": 255}}),
]
def switches(self, client: EcoflowMQTTClient) -> list[BaseSwitchEntity]:
return [
EnabledEntity(client, "mppt.cfgAcEnabled", const.AC_ENABLED,
lambda value: {"moduleType": 5, "operateType": "acOutCfg",
"params": {"enabled": value, "out_voltage": -1, "out_freq": 255,
"xboost": 255}}),
EnabledEntity(client, "mppt.cfgAcXboost", const.XBOOST_ENABLED,
lambda value: {"moduleType": 5, "operateType": "acOutCfg",
"params": {"enabled": 255, "out_voltage": -1, "out_freq": 255,
"xboost": value}}),
EnabledEntity(client, "pd.carState", const.DC_ENABLED,
lambda value: {"moduleType": 5, "operateType": "mpptCar", "params": {"enabled": value}})
]
def selects(self, client: EcoflowMQTTClient) -> list[BaseSelectEntity]:
return [
DictSelectEntity(client, "mppt.dcChgCurrent", const.DC_CHARGE_CURRENT, const.DC_CHARGE_CURRENT_OPTIONS,
lambda value: {"moduleType": 5, "operateType": "dcChgCfg",
"params": {"dcChgCfg": value}}),
DictSelectEntity(client, "mppt.cfgChgType", const.DC_MODE, const.DC_MODE_OPTIONS,
lambda value: {"moduleType": 5, "operateType": "chaType",
"params": {"chaType": value}}),
TimeoutDictSelectEntity(client, "mppt.scrStandbyMin", const.SCREEN_TIMEOUT, const.SCREEN_TIMEOUT_OPTIONS,
lambda value: {"moduleType": 5, "operateType": "lcdCfg",
"params": {"brighLevel": 255, "delayOff": value}}),
TimeoutDictSelectEntity(client, "mppt.powStandbyMin", const.UNIT_TIMEOUT, const.UNIT_TIMEOUT_OPTIONS,
lambda value: {"moduleType": 5, "operateType": "standby",
"params": {"standbyMins": value}}),
TimeoutDictSelectEntity(client, "mppt.acStandbyMins", const.AC_TIMEOUT, const.AC_TIMEOUT_OPTIONS,
lambda value: {"moduleType": 5, "operateType": "acStandby",
"params": {"standbyMins": value}})
]
def migrate(self, version) -> list[EntityMigration]:
if version == 2:
return [
EntityMigration("pd.soc", Platform.SENSOR, MigrationAction.REMOVE),
]
return []

View File

@@ -0,0 +1,117 @@
from homeassistant.const import Platform
from . import const, BaseDevice, MigrationAction, EntityMigration
from ..entities import BaseSensorEntity, BaseNumberEntity, BaseSwitchEntity, BaseSelectEntity
from ..mqtt.ecoflow_mqtt import EcoflowMQTTClient
from ..number import MaxBatteryLevelEntity
from ..select import DictSelectEntity
from ..sensor import LevelSensorEntity, WattsSensorEntity, RemainSensorEntity, TempSensorEntity, \
CyclesSensorEntity, InWattsSensorEntity, OutWattsSensorEntity, StatusSensorEntity, \
InEnergySensorEntity, OutEnergySensorEntity, MilliVoltSensorEntity, InMilliVoltSensorEntity, \
OutMilliVoltSensorEntity, CapacitySensorEntity
from ..switch import EnabledEntity, BeeperEntity
class RiverMax(BaseDevice):
def sensors(self, client: EcoflowMQTTClient) -> list[BaseSensorEntity]:
return [
LevelSensorEntity(client, "bmsMaster.soc", const.MAIN_BATTERY_LEVEL)
.attr("bmsMaster.designCap", const.ATTR_DESIGN_CAPACITY, 0)
.attr("bmsMaster.fullCap", const.ATTR_FULL_CAPACITY, 0)
.attr("bmsMaster.remainCap", const.ATTR_REMAIN_CAPACITY, 0),
CapacitySensorEntity(client, "bmsMaster.designCap", const.MAIN_DESIGN_CAPACITY, False),
CapacitySensorEntity(client, "bmsMaster.fullCap", const.MAIN_FULL_CAPACITY, False),
CapacitySensorEntity(client, "bmsMaster.remainCap", const.MAIN_REMAIN_CAPACITY, False),
WattsSensorEntity(client, "pd.wattsInSum", const.TOTAL_IN_POWER),
WattsSensorEntity(client, "pd.wattsOutSum", const.TOTAL_OUT_POWER),
InWattsSensorEntity(client, "inv.inputWatts", const.AC_IN_POWER),
OutWattsSensorEntity(client, "inv.outputWatts", const.AC_OUT_POWER),
InMilliVoltSensorEntity(client, "inv.acInVol", const.AC_IN_VOLT),
OutMilliVoltSensorEntity(client, "inv.invOutVol", const.AC_OUT_VOLT),
OutWattsSensorEntity(client, "pd.carWatts", const.DC_OUT_POWER),
OutWattsSensorEntity(client, "pd.typecWatts", const.TYPEC_OUT_POWER),
OutWattsSensorEntity(client, "pd.usb1Watts", const.USB_1_OUT_POWER),
OutWattsSensorEntity(client, "pd.usb2Watts", const.USB_2_OUT_POWER),
OutWattsSensorEntity(client, "pd.usb3Watts", const.USB_3_OUT_POWER),
RemainSensorEntity(client, "pd.remainTime", const.REMAINING_TIME),
CyclesSensorEntity(client, "bmsMaster.cycles", const.CYCLES),
TempSensorEntity(client, "bmsMaster.temp", const.BATTERY_TEMP)
.attr("bmsMaster.minCellTemp", const.ATTR_MIN_CELL_TEMP, 0)
.attr("bmsMaster.maxCellTemp", const.ATTR_MAX_CELL_TEMP, 0),
TempSensorEntity(client, "bmsMaster.minCellTemp", const.MIN_CELL_TEMP, False),
TempSensorEntity(client, "bmsMaster.maxCellTemp", const.MAX_CELL_TEMP, False),
MilliVoltSensorEntity(client, "bmsMaster.vol", const.BATTERY_VOLT, False)
.attr("bmsMaster.minCellVol", const.ATTR_MIN_CELL_VOLT, 0)
.attr("bmsMaster.maxCellVol", const.ATTR_MAX_CELL_VOLT, 0),
MilliVoltSensorEntity(client, "bmsMaster.minCellVol", const.MIN_CELL_VOLT, False),
MilliVoltSensorEntity(client, "bmsMaster.maxCellVol", const.MAX_CELL_VOLT, False),
# https://github.com/tolwi/hassio-ecoflow-cloud/discussions/87
InEnergySensorEntity(client, "pd.chgSunPower", const.SOLAR_IN_ENERGY),
InEnergySensorEntity(client, "pd.chgPowerAC", const.CHARGE_AC_ENERGY),
InEnergySensorEntity(client, "pd.chgPowerDC", const.CHARGE_DC_ENERGY),
OutEnergySensorEntity(client, "pd.dsgPowerAC", const.DISCHARGE_AC_ENERGY),
OutEnergySensorEntity(client, "pd.dsgPowerDC", const.DISCHARGE_DC_ENERGY),
LevelSensorEntity(client, "bmsSlave1.soc", const.SLAVE_BATTERY_LEVEL, False, True)
.attr("bmsSlave1.designCap", const.ATTR_DESIGN_CAPACITY, 0)
.attr("bmsSlave1.fullCap", const.ATTR_FULL_CAPACITY, 0)
.attr("bmsSlave1.remainCap", const.ATTR_REMAIN_CAPACITY, 0),
CapacitySensorEntity(client, "bmsSlave1.designCap", const.SLAVE_DESIGN_CAPACITY, False),
CapacitySensorEntity(client, "bmsSlave1.fullCap", const.SLAVE_FULL_CAPACITY, False),
CapacitySensorEntity(client, "bmsSlave1.remainCap", const.SLAVE_REMAIN_CAPACITY, False),
TempSensorEntity(client, "bmsSlave1.temp", const.SLAVE_BATTERY_TEMP, False, True)
.attr("bmsSlave1.minCellTemp", const.ATTR_MIN_CELL_TEMP, 0)
.attr("bmsSlave1.maxCellTemp", const.ATTR_MAX_CELL_TEMP, 0),
TempSensorEntity(client, "bmsSlave1.minCellTemp", const.SLAVE_MIN_CELL_TEMP, False),
TempSensorEntity(client, "bmsSlave1.maxCellTemp", const.SLAVE_MAX_CELL_TEMP, False),
MilliVoltSensorEntity(client, "bmsSlave1.vol", const.BATTERY_VOLT, False)
.attr("bmsSlave1.minCellVol", const.ATTR_MIN_CELL_VOLT, 0)
.attr("bmsSlave1.maxCellVol", const.ATTR_MAX_CELL_VOLT, 0),
MilliVoltSensorEntity(client, "bmsSlave1.minCellVol", const.MIN_CELL_VOLT, False),
MilliVoltSensorEntity(client, "bmsSlave1.maxCellVol", const.MAX_CELL_VOLT, False),
CyclesSensorEntity(client, "bmsSlave1.cycles", const.SLAVE_CYCLES, False, True),
StatusSensorEntity(client),
]
def numbers(self, client: EcoflowMQTTClient) -> list[BaseNumberEntity]:
return [
MaxBatteryLevelEntity(client, "bmsMaster.maxChargeSoc", const.MAX_CHARGE_LEVEL, 30, 100, None),
# MinBatteryLevelEntity(client, "bmsMaster.minDsgSoc", const.MIN_DISCHARGE_LEVEL, 0, 30, None),
]
def switches(self, client: EcoflowMQTTClient) -> list[BaseSwitchEntity]:
return [
BeeperEntity(client, "pd.beepState", const.BEEPER, lambda value: {"moduleType": 0, "operateType": "TCP", "params": {"id": 38, "enabled": value}}),
EnabledEntity(client, "inv.cfgAcEnabled", const.AC_ENABLED, lambda value: {"moduleType": 0, "operateType": "TCP", "params": {"id": 66, "enabled": value}}),
EnabledEntity(client, "pd.carSwitch", const.DC_ENABLED, lambda value: {"moduleType": 0, "operateType": "TCP", "params": {"id": 34, "enabled": value}}),
EnabledEntity(client, "inv.cfgAcXboost", const.XBOOST_ENABLED, lambda value: {"moduleType": 0, "operateType": "TCP", "params": {"id": 66, "xboost": value}})
]
def selects(self, client: EcoflowMQTTClient) -> list[BaseSelectEntity]:
return [
DictSelectEntity(client, "pd.standByMode", const.UNIT_TIMEOUT, const.UNIT_TIMEOUT_OPTIONS, lambda value: {"moduleType": 0, "operateType": "TCP", "params": {"id": 33, "standByMode": value}}),
DictSelectEntity(client, "inv.cfgStandbyMin", const.AC_TIMEOUT, const.AC_TIMEOUT_OPTIONS, lambda value: {"moduleType": 0, "operateType": "TCP", "params": {"id": 153, "standByMins": value}}),
]
def migrate(self, version) -> list[EntityMigration]:
if version == 2:
return [
EntityMigration("pd.soc", Platform.SENSOR, MigrationAction.REMOVE),
]
return []

View File

@@ -0,0 +1,138 @@
from homeassistant.const import Platform
from . import const, BaseDevice, EntityMigration, MigrationAction
from ..entities import BaseSensorEntity, BaseNumberEntity, BaseSwitchEntity, BaseSelectEntity
from ..mqtt.ecoflow_mqtt import EcoflowMQTTClient
from ..number import MaxBatteryLevelEntity
from ..select import TimeoutDictSelectEntity
from ..sensor import LevelSensorEntity, WattsSensorEntity, RemainSensorEntity, TempSensorEntity, \
CyclesSensorEntity, InEnergySensorEntity, InWattsSensorEntity, OutEnergySensorEntity, OutWattsSensorEntity, VoltSensorEntity, InVoltSensorEntity, \
InAmpSensorEntity, AmpSensorEntity, StatusSensorEntity, MilliVoltSensorEntity, InMilliVoltSensorEntity, \
OutMilliVoltSensorEntity, CapacitySensorEntity
from ..switch import EnabledEntity, BeeperEntity
class RiverPro(BaseDevice):
def sensors(self, client: EcoflowMQTTClient) -> list[BaseSensorEntity]:
return [
LevelSensorEntity(client, "bmsMaster.soc", const.MAIN_BATTERY_LEVEL)
.attr("bmsMaster.designCap", const.ATTR_DESIGN_CAPACITY, 0)
.attr("bmsMaster.fullCap", const.ATTR_FULL_CAPACITY, 0)
.attr("bmsMaster.remainCap", const.ATTR_REMAIN_CAPACITY, 0),
CapacitySensorEntity(client, "bmsMaster.designCap", const.MAIN_DESIGN_CAPACITY, False),
CapacitySensorEntity(client, "bmsMaster.fullCap", const.MAIN_FULL_CAPACITY, False),
CapacitySensorEntity(client, "bmsMaster.remainCap", const.MAIN_REMAIN_CAPACITY, False),
WattsSensorEntity(client, "pd.wattsInSum", const.TOTAL_IN_POWER),
WattsSensorEntity(client, "pd.wattsOutSum", const.TOTAL_OUT_POWER),
InAmpSensorEntity(client, "inv.dcInAmp", const.SOLAR_IN_CURRENT),
InVoltSensorEntity(client, "inv.dcInVol", const.SOLAR_IN_VOLTAGE),
InWattsSensorEntity(client, "inv.inputWatts", const.AC_IN_POWER),
OutWattsSensorEntity(client, "inv.outputWatts", const.AC_OUT_POWER),
InMilliVoltSensorEntity(client, "inv.acInVol", const.AC_IN_VOLT),
OutMilliVoltSensorEntity(client, "inv.invOutVol", const.AC_OUT_VOLT),
OutWattsSensorEntity(client, "pd.carWatts", const.DC_OUT_POWER),
OutWattsSensorEntity(client, "pd.typecWatts", const.TYPEC_OUT_POWER),
# disabled by default because they aren't terribly useful
TempSensorEntity(client, "pd.carTemp", const.DC_CAR_OUT_TEMP, False),
TempSensorEntity(client, "pd.typecTemp", const.USB_C_TEMP, False),
OutWattsSensorEntity(client, "pd.usb1Watts", const.USB_1_OUT_POWER),
OutWattsSensorEntity(client, "pd.usb2Watts", const.USB_2_OUT_POWER),
OutWattsSensorEntity(client, "pd.usb3Watts", const.USB_3_OUT_POWER),
RemainSensorEntity(client, "pd.remainTime", const.REMAINING_TIME),
TempSensorEntity(client, "bmsMaster.temp", const.BATTERY_TEMP)
.attr("bmsMaster.minCellTemp", const.ATTR_MIN_CELL_TEMP, 0)
.attr("bmsMaster.maxCellTemp", const.ATTR_MAX_CELL_TEMP, 0),
TempSensorEntity(client, "bmsMaster.minCellTemp", const.MIN_CELL_TEMP, False),
TempSensorEntity(client, "bmsMaster.maxCellTemp", const.MAX_CELL_TEMP, False),
TempSensorEntity(client, "inv.inTemp", const.INV_IN_TEMP),
TempSensorEntity(client, "inv.outTemp", const.INV_OUT_TEMP),
# https://github.com/tolwi/hassio-ecoflow-cloud/discussions/87
InEnergySensorEntity(client, "pd.chgSunPower", const.SOLAR_IN_ENERGY),
InEnergySensorEntity(client, "pd.chgPowerAC", const.CHARGE_AC_ENERGY),
InEnergySensorEntity(client, "pd.chgPowerDC", const.CHARGE_DC_ENERGY),
OutEnergySensorEntity(client, "pd.dsgPowerAC", const.DISCHARGE_AC_ENERGY),
OutEnergySensorEntity(client, "pd.dsgPowerDC", const.DISCHARGE_DC_ENERGY),
AmpSensorEntity(client, "bmsMaster.amp", const.BATTERY_AMP, False),
MilliVoltSensorEntity(client, "bmsMaster.vol", const.BATTERY_VOLT, False)
.attr("bmsMaster.minCellVol", const.ATTR_MIN_CELL_VOLT, 0)
.attr("bmsMaster.maxCellVol", const.ATTR_MAX_CELL_VOLT, 0),
MilliVoltSensorEntity(client, "bmsMaster.minCellVol", const.MIN_CELL_VOLT, False),
MilliVoltSensorEntity(client, "bmsMaster.maxCellVol", const.MAX_CELL_VOLT, False),
CyclesSensorEntity(client, "bmsMaster.cycles", const.CYCLES),
# Optional Slave Batteries
LevelSensorEntity(client, "bmsSlave1.soc", const.SLAVE_BATTERY_LEVEL, False, True)
.attr("bmsSlave1.designCap", const.ATTR_DESIGN_CAPACITY, 0)
.attr("bmsSlave1.fullCap", const.ATTR_FULL_CAPACITY, 0)
.attr("bmsSlave1.remainCap", const.ATTR_REMAIN_CAPACITY, 0),
CapacitySensorEntity(client, "bmsSlave1.designCap", const.SLAVE_DESIGN_CAPACITY, False),
CapacitySensorEntity(client, "bmsSlave1.fullCap", const.SLAVE_FULL_CAPACITY, False),
CapacitySensorEntity(client, "bmsSlave1.remainCap", const.SLAVE_REMAIN_CAPACITY, False),
CyclesSensorEntity(client, "bmsSlave1.cycles", const.SLAVE_CYCLES, False, True),
TempSensorEntity(client, "bmsSlave1.temp", const.SLAVE_BATTERY_TEMP, False, True)
.attr("bmsSlave1.minCellTemp", const.ATTR_MIN_CELL_TEMP, 0)
.attr("bmsSlave1.maxCellTemp", const.ATTR_MAX_CELL_TEMP, 0),
AmpSensorEntity(client, "bmsSlave1.amp", const.SLAVE_BATTERY_AMP, False),
MilliVoltSensorEntity(client, "bmsSlave1.vol", const.SLAVE_BATTERY_VOLT, False)
.attr("bmsSlave1.minCellVol", const.ATTR_MIN_CELL_VOLT, 0)
.attr("bmsSlave1.maxCellVol", const.ATTR_MAX_CELL_VOLT, 0),
MilliVoltSensorEntity(client, "bmsSlave1.minCellVol", const.SLAVE_MIN_CELL_VOLT, False),
MilliVoltSensorEntity(client, "bmsSlave1.maxCellVol", const.SLAVE_MAX_CELL_VOLT, False),
StatusSensorEntity(client),
]
def numbers(self, client: EcoflowMQTTClient) -> list[BaseNumberEntity]:
return [
MaxBatteryLevelEntity(client, "bmsMaster.maxChargeSoc", const.MAX_CHARGE_LEVEL, 30, 100,
lambda value: {"moduleType": 0, "operateType": "TCP",
"params": {"id": 49, "maxChgSoc": value}}),
# MinBatteryLevelEntity(client, "bmsMaster.minDsgSoc", const.MIN_DISCHARGE_LEVEL, 0, 30, None),
]
def switches(self, client: EcoflowMQTTClient) -> list[BaseSwitchEntity]:
return [
BeeperEntity(client, "pd.beepState", const.BEEPER, lambda value: {"moduleType": 0, "operateType": "TCP", "params": {"id": 38, "enabled": value}}),
EnabledEntity(client, "inv.acAutoOutConfig", const.AC_ALWAYS_ENABLED,
lambda value: {"moduleType": 0, "operateType": "TCP", "params": {"id": 95, "acautooutConfig": value, "minAcoutSoc": 255}}),
EnabledEntity(client, "pd.carSwitch", const.DC_ENABLED, lambda value: {"moduleType": 0, "operateType": "TCP", "params": {"id": 34, "enabled": value}}),
EnabledEntity(client, "inv.cfgAcEnabled", const.AC_ENABLED, lambda value: {"moduleType": 0, "operateType": "TCP", "params": {"id": 66, "enabled": value}}),
EnabledEntity(client, "inv.cfgAcXboost", const.XBOOST_ENABLED, lambda value: {"moduleType": 0, "operateType": "TCP", "params": {"id": 66, "xboost": value}}),
EnabledEntity(client, "inv.cfgAcChgModeFlg", const.AC_SLOW_CHARGE, lambda value: {"moduleType": 0, "operateType": "TCP", "params": {"id": 65, "workMode": value}}),
EnabledEntity(client, "inv.cfgFanMode", const.AUTO_FAN_SPEED, lambda value: {"moduleType": 0, "operateType": "TCP", "params": {"id": 73, "fanMode": value}})
]
def selects(self, client: EcoflowMQTTClient) -> list[BaseSelectEntity]:
return [
TimeoutDictSelectEntity(client, "pd.standByMode", const.UNIT_TIMEOUT, const.UNIT_TIMEOUT_OPTIONS_LIMITED, lambda value: {"moduleType": 0, "operateType": "TCP", "params": {"id": 33, "standByMode": value}}),
TimeoutDictSelectEntity(client, "pd.carDelayOffMin", const.DC_TIMEOUT, const.DC_TIMEOUT_OPTIONS_LIMITED, lambda value: {"moduleType": 0, "operateType": "TCP", "params": {"cmdSet": 32, "id": 84, "carDelayOffMin": value}}),
TimeoutDictSelectEntity(client, "inv.cfgStandbyMin", const.AC_TIMEOUT, const.AC_TIMEOUT_OPTIONS_LIMITED, lambda value: {"moduleType": 0, "operateType": "TCP", "params": {"id": 153, "standByMins": value}})
# lambda is confirmed correct, but pd.lcdOffSec is missing from status
# TimeoutDictSelectEntity(client, "pd.lcdOffSec", const.SCREEN_TIMEOUT, const.SCREEN_TIMEOUT_OPTIONS,
# lambda value: {"moduleType": 0, "operateType": "TCP",
# "params": {"lcdTime": value, "id": 39}})
]
def migrate(self, version) -> list[EntityMigration]:
if version == 2:
return [
EntityMigration("pd.soc", Platform.SENSOR, MigrationAction.REMOVE),
]
return []

View File

@@ -0,0 +1,91 @@
from homeassistant.components.switch import SwitchEntity
from . import const, BaseDevice
from .. import EcoflowMQTTClient
from ..entities import BaseSensorEntity, BaseNumberEntity, BaseSelectEntity
from ..number import SetTempEntity
from ..select import DictSelectEntity
from ..sensor import LevelSensorEntity, RemainSensorEntity, TempSensorEntity, \
WattsSensorEntity, QuotasStatusSensorEntity, \
MilliCelsiusSensorEntity, CapacitySensorEntity
class Wave2(BaseDevice):
def sensors(self, client: EcoflowMQTTClient) -> list[BaseSensorEntity]:
return [
# Power and Battery Entities
LevelSensorEntity(client, "bms.soc", const.MAIN_BATTERY_LEVEL)
.attr("bms.remainCap", const.ATTR_REMAIN_CAPACITY, 0),
CapacitySensorEntity(client, "bms.remainCap", const.MAIN_REMAIN_CAPACITY, False),
TempSensorEntity(client, "bms.tmp", const.BATTERY_TEMP)
.attr("bms.minCellTemp", const.ATTR_MIN_CELL_TEMP, 0)
.attr("bms.maxCellTemp", const.ATTR_MAX_CELL_TEMP, 0),
TempSensorEntity(client, "bms.minCellTmp", const.MIN_CELL_TEMP, False),
TempSensorEntity(client, "bms.maxCellTmp", const.MAX_CELL_TEMP, False),
RemainSensorEntity(client, "pd.batChgRemain", const.CHARGE_REMAINING_TIME),
RemainSensorEntity(client, "pd.batDsgRemain", const.DISCHARGE_REMAINING_TIME),
# heat pump
MilliCelsiusSensorEntity(client, "pd.condTemp", "Condensation temperature", False),
MilliCelsiusSensorEntity(client, "pd.heatEnv", "Return air temperature in condensation zone", False),
MilliCelsiusSensorEntity(client, "pd.coolEnv", "Air outlet temperature", False),
MilliCelsiusSensorEntity(client, "pd.evapTemp", "Evaporation temperature", False),
MilliCelsiusSensorEntity(client, "pd.motorOutTemp", "Exhaust temperature", False),
MilliCelsiusSensorEntity(client, "pd.airInTemp", "Evaporation zone return air temperature", False),
TempSensorEntity(client, "pd.coolTemp", "Air outlet temperature", False),
TempSensorEntity(client, "pd.envTemp", "Ambient temperature", False),
# power (pd)
WattsSensorEntity(client, "pd.mpptPwr", "PV input power"),
WattsSensorEntity(client, "pd.batPwrOut", "Battery output power"),
WattsSensorEntity(client, "pd.pvPower", "PV charging power"),
WattsSensorEntity(client, "pd.acPwrIn", "AC input power"),
WattsSensorEntity(client, "pd.psdrPower ", "Power supply power"),
WattsSensorEntity(client, "pd.sysPowerWatts", "System power"),
WattsSensorEntity(client, "pd.batPower ", "Battery power"),
# power (motor)
WattsSensorEntity(client, "motor.power", "Motor operating power"),
# power (power)
WattsSensorEntity(client, "power.batPwrOut", "Battery output power"),
WattsSensorEntity(client, "power.acPwrI", "AC input power"),
WattsSensorEntity(client, "power.mpptPwr ", "PV input power"),
QuotasStatusSensorEntity(client)
]
def numbers(self, client: EcoflowMQTTClient) -> list[BaseNumberEntity]:
return [
SetTempEntity(client, "pd.setTemp", "Set Temperature", 0, 40,
lambda value: {"moduleType": 1, "operateType": "setTemp",
"sn": client.device_sn,
"params": {"setTemp": int(value)}}),
]
def selects(self, client: EcoflowMQTTClient) -> list[BaseSelectEntity]:
return [
DictSelectEntity(client, "pd.fanValue", const.FAN_MODE, const.FAN_MODE_OPTIONS,
lambda value: {"moduleType": 1, "operateType": "fanValue",
"sn": client.device_sn,
"params": {"fanValue": value}}),
DictSelectEntity(client, "pd.mainMode", const.MAIN_MODE, const.MAIN_MODE_OPTIONS,
lambda value: {"moduleType": 1, "operateType": "mainMode",
"sn": client.device_sn,
"params": {"mainMode": value}}),
DictSelectEntity(client, "pd.powerMode", const.REMOTE_MODE, const.REMOTE_MODE_OPTIONS,
lambda value: {"moduleType": 1, "operateType": "powerMode",
"sn": client.device_sn,
"params": {"powerMode": value}}),
DictSelectEntity(client, "pd.subMode", const.POWER_SUB_MODE, const.POWER_SUB_MODE_OPTIONS,
lambda value: {"moduleType": 1, "operateType": "subMode",
"sn": client.device_sn,
"params": {"subMode": value}}),
]
def switches(self, client: EcoflowMQTTClient) -> list[SwitchEntity]:
return []

View File

@@ -0,0 +1,30 @@
from datetime import timedelta
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from . import DOMAIN
from .mqtt.ecoflow_mqtt import EcoflowMQTTClient
def _to_serializable(x):
t = type(x)
if t is dict:
x = {y: _to_serializable(x[y]) for y in x}
if t is timedelta:
x = x.__str__()
return x
async def async_get_config_entry_diagnostics(hass: HomeAssistant, entry: ConfigEntry):
client: EcoflowMQTTClient = hass.data[DOMAIN][entry.entry_id]
values = {
'device': client.device_type,
'params': dict(sorted(client.data.params.items())),
'set': [dict(sorted(k.items())) for k in client.data.set],
'set_reply': [dict(sorted(k.items())) for k in client.data.set_reply],
'get': [dict(sorted(k.items())) for k in client.data.get],
'get_reply': [dict(sorted(k.items())) for k in client.data.get_reply],
'raw_data': client.data.raw_data,
}
return values

View File

@@ -0,0 +1,151 @@
from __future__ import annotations
import inspect
from typing import Any, Callable, Optional, OrderedDict, Mapping
from homeassistant.components.number import NumberEntity
from homeassistant.components.select import SelectEntity
from homeassistant.components.sensor import SensorEntity
from homeassistant.components.switch import SwitchEntity
from homeassistant.components.button import ButtonEntity
from homeassistant.helpers.entity import Entity, EntityCategory
from ..mqtt.ecoflow_mqtt import EcoflowMQTTClient
class EcoFlowAbstractEntity(Entity):
_attr_has_entity_name = True
_attr_should_poll = False
def __init__(self, client: EcoflowMQTTClient, title: str, key: str):
self._client = client
self._attr_name = title
self._attr_device_info = client.device_info_main
self._attr_unique_id = self.gen_unique_id(client.device_sn, key)
def send_get_message(self, command: dict):
self._client.send_get_message(command)
def send_set_message(self, target_dict: dict[str, Any] | None, command: dict):
self._client.send_set_message(target_dict, command)
@staticmethod
def gen_unique_id(sn: str, key: str):
return 'ecoflow-' + sn + '-' + key.replace('.', '-').replace('_', '-')
class EcoFlowDictEntity(EcoFlowAbstractEntity):
def __init__(self, client: EcoflowMQTTClient, mqtt_key: str, title: str, enabled: bool = True,
auto_enable: bool = False) -> object:
super().__init__(client, title, mqtt_key)
self._mqtt_key = mqtt_key
self._auto_enable = auto_enable
self._attr_entity_registry_enabled_default = enabled
self.__attributes_mapping: dict[str, str] = {}
self.__attrs = OrderedDict[str, Any]()
def attr(self, mqtt_key: str, title: str, default: Any) -> EcoFlowDictEntity:
self.__attributes_mapping[mqtt_key] = title
self.__attrs[title] = default
return self
@property
def mqtt_key(self):
return self._mqtt_key
@property
def auto_enable(self):
return self._auto_enable
def send_set_message(self, target_value: Any, command: dict):
super().send_set_message({self._mqtt_key: target_value}, command)
@property
def enabled_default(self):
return self._attr_entity_registry_enabled_default
async def async_added_to_hass(self):
await super().async_added_to_hass()
d = self._client.data.params_observable().subscribe(self._updated)
self.async_on_remove(d.dispose)
def _updated(self, data: dict[str, Any]):
# update attributes
for key, title in self.__attributes_mapping.items():
if key in data:
self.__attrs[title] = data[key]
# update value
if self._mqtt_key in data:
self._attr_available = True
if self._auto_enable:
self._attr_entity_registry_enabled_default = True
if self._update_value(data[self._mqtt_key]):
self.schedule_update_ha_state()
@property
def extra_state_attributes(self) -> Mapping[str, Any] | None:
return self.__attrs
def _update_value(self, val: Any) -> bool:
return False
class EcoFlowBaseCommandEntity(EcoFlowDictEntity):
def __init__(self, client: EcoflowMQTTClient, mqtt_key: str, title: str,
command: Callable[[int, Optional[dict[str, Any]]], dict[str, Any]] | None,
enabled: bool = True, auto_enable: bool = False):
super().__init__(client, mqtt_key, title, enabled, auto_enable)
self._command = command
def command_dict(self, value: int) -> dict[str, any] | None:
if self._command:
p_count = len(inspect.signature(self._command).parameters)
if p_count == 1:
return self._command(value)
elif p_count == 2:
return self._command(value, self._client.data.params)
else:
return None
class BaseNumberEntity(NumberEntity, EcoFlowBaseCommandEntity):
_attr_entity_category = EntityCategory.CONFIG
def __init__(self, client: EcoflowMQTTClient, mqtt_key: str, title: str, min_value: int, max_value: int,
command: Callable[[int], dict[str, any]] | None, enabled: bool = True,
auto_enable: bool = False):
super().__init__(client, mqtt_key, title, command, enabled, auto_enable)
self._attr_native_max_value = max_value
self._attr_native_min_value = min_value
def _update_value(self, val: Any) -> bool:
if self._attr_native_value != val:
self._attr_native_value = val
return True
else:
return False
class BaseSensorEntity(SensorEntity, EcoFlowDictEntity):
def _update_value(self, val: Any) -> bool:
if self._attr_native_value != val:
self._attr_native_value = val
return True
else:
return False
class BaseSwitchEntity(SwitchEntity, EcoFlowBaseCommandEntity):
pass
class BaseSelectEntity(SelectEntity, EcoFlowBaseCommandEntity):
pass
class BaseButtonEntity(ButtonEntity, EcoFlowBaseCommandEntity):
pass

View File

@@ -0,0 +1,17 @@
{
"domain": "ecoflow_cloud",
"name": "Ecoflow-Cloud",
"codeowners": [
"@tolwi"
],
"config_flow": true,
"documentation": "https://github.com/tolwi/hassio-ecoflow-cloud",
"iot_class": "cloud_push",
"issue_tracker": "https://github.com/tolwi/hassio-ecoflow-cloud/issues",
"requirements": [
"paho-mqtt==1.6.1",
"reactivex==4.0.4",
"protobuf>=4.23.0"
],
"version": "0.13.3"
}

View File

@@ -0,0 +1,340 @@
import base64
import json
import logging
import random
import ssl
import time
import uuid
from datetime import datetime
from typing import Any
import paho.mqtt.client as mqtt_client
import requests
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant, DOMAIN
from homeassistant.helpers.entity import DeviceInfo
from homeassistant.util import utcnow
from reactivex import Subject, Observable
from .proto import powerstream_pb2 as powerstream, ecopacket_pb2 as ecopacket
from .utils import BoundFifoList
from ..config.const import CONF_DEVICE_TYPE, CONF_DEVICE_ID, OPTS_REFRESH_PERIOD_SEC, EcoflowModel
_LOGGER = logging.getLogger(__name__)
class EcoflowException(Exception):
def __init__(self, *args, **kwargs):
super().__init__(args, kwargs)
class EcoflowAuthentication:
def __init__(self, ecoflow_username, ecoflow_password):
self.ecoflow_username = ecoflow_username
self.ecoflow_password = ecoflow_password
self.user_id = None
self.token = None
self.mqtt_url = "mqtt.mqtt.com"
self.mqtt_port = 8883
self.mqtt_username = None
self.mqtt_password = None
def authorize(self):
url = "https://api.ecoflow.com/auth/login"
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"}
_LOGGER.info(f"Login to EcoFlow API {url}")
request = requests.post(url, json=data, headers=headers)
response = self.get_json_response(request)
try:
self.token = response["data"]["token"]
self.user_id = response["data"]["user"]["userId"]
user_name = response["data"]["user"].get("name", "<no user name>")
except KeyError as key:
raise EcoflowException(f"Failed to extract key {key} from response: {response}")
_LOGGER.info(f"Successfully logged in: {user_name}")
url = "https://api.ecoflow.com/iot-auth/app/certification"
headers = {"lang": "en_US", "authorization": f"Bearer {self.token}"}
data = {"userId": self.user_id}
_LOGGER.info(f"Requesting IoT MQTT credentials {url}")
request = requests.get(url, data=data, headers=headers)
response = self.get_json_response(request)
try:
self.mqtt_url = response["data"]["url"]
self.mqtt_port = int(response["data"]["port"])
self.mqtt_username = response["data"]["certificateAccount"]
self.mqtt_password = response["data"]["certificatePassword"]
except KeyError as key:
raise EcoflowException(f"Failed to extract key {key} from {response}")
_LOGGER.info(f"Successfully extracted account: {self.mqtt_username}")
def get_json_response(self, request):
if request.status_code != 200:
raise EcoflowException(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 EcoflowException(f"Failed to extract key {key} from {response}")
except Exception as error:
raise EcoflowException(f"Failed to parse response: {request.text} Error: {error}")
if response_message.lower() != "success":
raise EcoflowException(f"{response_message}")
return response
class EcoflowDataHolder:
def __init__(self, update_period_sec: int, collect_raw: bool = False):
self.__update_period_sec = update_period_sec
self.__collect_raw = collect_raw
self.set = BoundFifoList[dict[str, Any]]()
self.set_reply = BoundFifoList[dict[str, Any]]()
self.get = BoundFifoList[dict[str, Any]]()
self.get_reply = BoundFifoList[dict[str, Any]]()
self.params = dict[str, Any]()
self.raw_data = BoundFifoList[dict[str, Any]]()
self.__params_time = utcnow().replace(year=2000, month=1, day=1, hour=0, minute=0, second=0)
self.__params_broadcast_time = utcnow().replace(year=2000, month=1, day=1, hour=0, minute=0, second=0)
self.__params_observable = Subject[dict[str, Any]]()
self.__set_reply_observable = Subject[list[dict[str, Any]]]()
self.__get_reply_observable = Subject[list[dict[str, Any]]]()
def params_observable(self) -> Observable[dict[str, Any]]:
return self.__params_observable
def get_reply_observable(self) -> Observable[list[dict[str, Any]]]:
return self.__get_reply_observable
def set_reply_observable(self) -> Observable[list[dict[str, Any]]]:
return self.__set_reply_observable
def add_set_message(self, msg: dict[str, Any]):
self.set.append(msg)
def add_set_reply_message(self, msg: dict[str, Any]):
self.set_reply.append(msg)
self.__set_reply_observable.on_next(self.set_reply)
def add_get_message(self, msg: dict[str, Any]):
self.get.append(msg)
def add_get_reply_message(self, msg: dict[str, Any]):
self.get_reply.append(msg)
self.__get_reply_observable.on_next(self.get_reply)
def update_to_target_state(self, target_state: dict[str, Any]):
self.params.update(target_state)
self.__broadcast()
def update_data(self, raw: dict[str, Any]):
self.__add_raw_data(raw)
# self.__params_time = datetime.fromtimestamp(raw['timestamp'], UTC)
self.__params_time = utcnow()
self.params['timestamp'] = raw['timestamp']
self.params.update(raw['params'])
if (utcnow() - self.__params_broadcast_time).total_seconds() > self.__update_period_sec:
self.__broadcast()
def __broadcast(self):
self.__params_broadcast_time = utcnow()
self.__params_observable.on_next(self.params)
def __add_raw_data(self, raw: dict[str, Any]):
if self.__collect_raw:
self.raw_data.append(raw)
def params_time(self) -> datetime:
return self.__params_time
class EcoflowMQTTClient:
def __init__(self, hass: HomeAssistant, entry: ConfigEntry, auth: EcoflowAuthentication):
self.auth = auth
self.config_entry = entry
self.device_type = entry.data[CONF_DEVICE_TYPE]
self.device_sn = entry.data[CONF_DEVICE_ID]
self._data_topic = f"/app/device/property/{self.device_sn}"
self._set_topic = f"/app/{auth.user_id}/{self.device_sn}/thing/property/set"
self._set_reply_topic = f"/app/{auth.user_id}/{self.device_sn}/thing/property/set_reply"
self._get_topic = f"/app/{auth.user_id}/{self.device_sn}/thing/property/get"
self._get_reply_topic = f"/app/{auth.user_id}/{self.device_sn}/thing/property/get_reply"
self.data = EcoflowDataHolder(entry.options.get(OPTS_REFRESH_PERIOD_SEC), self.device_type == "DIAGNOSTIC")
self.device_info_main = DeviceInfo(
identifiers={(DOMAIN, self.device_sn)},
manufacturer="EcoFlow",
name=entry.title,
model=self.device_type,
)
self.client = mqtt_client.Client(client_id=f'ANDROID_-{str(uuid.uuid4()).upper()}_{auth.user_id}',
clean_session=True, reconnect_on_failure=True)
self.client.username_pw_set(self.auth.mqtt_username, self.auth.mqtt_password)
self.client.tls_set(certfile=None, keyfile=None, cert_reqs=ssl.CERT_REQUIRED)
self.client.tls_insecure_set(False)
self.client.on_connect = self.on_connect
self.client.on_disconnect = self.on_disconnect
if self.device_type == EcoflowModel.POWERSTREAM.name:
self.client.on_message = self.on_bytes_message
else:
self.client.on_message = self.on_json_message
_LOGGER.info(f"Connecting to MQTT Broker {self.auth.mqtt_url}:{self.auth.mqtt_port}")
self.client.connect(self.auth.mqtt_url, self.auth.mqtt_port, 30)
self.client.loop_start()
def is_connected(self):
return self.client.is_connected()
def reconnect(self) -> bool:
try:
_LOGGER.info(f"Re-connecting to MQTT Broker {self.auth.mqtt_url}:{self.auth.mqtt_port}")
self.client.loop_stop(True)
self.client.reconnect()
self.client.loop_start()
return True
except Exception as e:
_LOGGER.error(e)
return False
def on_connect(self, client, userdata, flags, rc):
match rc:
case 0:
self.client.subscribe([(self._data_topic, 1),
(self._set_topic, 1), (self._set_reply_topic, 1),
(self._get_topic, 1), (self._get_reply_topic, 1)])
_LOGGER.info(f"Subscribed to MQTT topic {self._data_topic}")
case -1:
_LOGGER.error("Failed to connect to MQTT: connection timed out")
case 1:
_LOGGER.error("Failed to connect to MQTT: incorrect protocol version")
case 2:
_LOGGER.error("Failed to connect to MQTT: invalid client identifier")
case 3:
_LOGGER.error("Failed to connect to MQTT: server unavailable")
case 4:
_LOGGER.error("Failed to connect to MQTT: bad username or password")
case 5:
_LOGGER.error("Failed to connect to MQTT: not authorised")
case _:
_LOGGER.error(f"Failed to connect to MQTT: another error occured: {rc}")
return client
def on_disconnect(self, client, userdata, rc):
if rc != 0:
_LOGGER.error(f"Unexpected MQTT disconnection: {rc}. Will auto-reconnect")
time.sleep(5)
# self.client.reconnect() ??
def on_json_message(self, client, userdata, message):
try:
payload = message.payload.decode("utf-8", errors='ignore')
raw = json.loads(payload)
if message.topic == self._data_topic:
self.data.update_data(raw)
elif message.topic == self._set_topic:
self.data.add_set_message(raw)
elif message.topic == self._set_reply_topic:
self.data.add_set_reply_message(raw)
elif message.topic == self._get_topic:
self.data.add_get_message(raw)
elif message.topic == self._get_reply_topic:
self.data.add_get_reply_message(raw)
except UnicodeDecodeError as error:
_LOGGER.error(f"UnicodeDecodeError: {error}. Ignoring message and waiting for the next one.")
def on_bytes_message(self, client, userdata, message):
try:
payload = message.payload
while True:
packet = ecopacket.SendHeaderMsg()
packet.ParseFromString(payload)
_LOGGER.debug("cmd id %u payload \"%s\"", packet.msg.cmd_id, payload.hex())
if packet.msg.cmd_id != 1:
_LOGGER.info("Unsupported EcoPacket cmd id %u", packet.msg.cmd_id)
else:
heartbeat = powerstream.InverterHeartbeat()
heartbeat.ParseFromString(packet.msg.pdata)
raw = {"params": {}}
for descriptor in heartbeat.DESCRIPTOR.fields:
if not heartbeat.HasField(descriptor.name):
continue
raw["params"][descriptor.name] = getattr(heartbeat, descriptor.name)
_LOGGER.info("Found %u fields", len(raw["params"]))
raw["timestamp"] = utcnow()
self.data.update_data(raw)
if packet.ByteSize() >= len(payload):
break
_LOGGER.info("Found another frame in payload")
packetLength = len(payload) - packet.ByteSize()
payload = payload[:packetLength]
except Exception as error:
_LOGGER.error(error)
_LOGGER.info(message.payload.hex())
message_id = 999900000 + random.randint(10000, 99999)
def __prepare_payload(self, command: dict):
self.message_id += 1
payload = {"from": "HomeAssistant",
"id": f"{self.message_id}",
"version": "1.0"}
payload.update(command)
return payload
def __send(self, topic: str, message: str):
try:
info = self.client.publish(topic, message, 1)
_LOGGER.debug("Sending " + message + " :" + str(info) + "(" + str(info.is_published()) + ")")
except RuntimeError as error:
_LOGGER.error(error)
def send_get_message(self, command: dict):
payload = self.__prepare_payload(command)
self.__send(self._get_topic, json.dumps(payload))
def send_set_message(self, mqtt_state: dict[str, Any], command: dict):
self.data.update_to_target_state(mqtt_state)
payload = self.__prepare_payload(command)
self.__send(self._set_topic, json.dumps(payload))
def stop(self):
self.client.loop_stop()
self.client.disconnect()

View File

@@ -0,0 +1,57 @@
syntax = "proto3";
message Header
{
optional bytes pdata = 1;
optional int32 src = 2;
optional int32 dest = 3;
optional int32 d_src= 4;
optional int32 d_dest = 5;
optional int32 enc_type = 6;
optional int32 check_type = 7;
optional int32 cmd_func = 8;
optional int32 cmd_id = 9;
optional int32 data_len = 10;
optional int32 need_ack = 11;
optional int32 is_ack = 12;
optional int32 seq = 14;
optional int32 product_id = 15;
optional int32 version = 16;
optional int32 payload_ver = 17;
optional int32 time_snap = 18;
optional int32 is_rw_cmd = 19;
optional int32 is_queue = 20;
optional int32 ack_type= 21;
optional string code = 22;
optional string from = 23;
optional string module_sn = 24;
optional string device_sn = 25;
}
message SendHeaderMsg
{
optional Header msg = 1;
}
message SendMsgHart
{
optional int32 link_id = 1;
optional int32 src = 2;
optional int32 dest = 3;
optional int32 d_src = 4;
optional int32 d_dest = 5;
optional int32 enc_type = 6;
optional int32 check_type = 7;
optional int32 cmd_func = 8;
optional int32 cmd_id = 9;
optional int32 data_len = 10;
optional int32 need_ack = 11;
optional int32 is_ack = 12;
optional int32 ack_type = 13;
optional int32 seq = 14;
optional int32 time_snap = 15;
optional int32 is_rw_cmd = 16;
optional int32 is_queue = 17;
optional int32 product_id = 18;
optional int32 version = 19;
}

View File

@@ -0,0 +1,30 @@
# -*- coding: utf-8 -*-
# Generated by the protocol buffer compiler. DO NOT EDIT!
# source: ecopacket.proto
"""Generated protocol buffer code."""
from google.protobuf import descriptor as _descriptor
from google.protobuf import descriptor_pool as _descriptor_pool
from google.protobuf import symbol_database as _symbol_database
from google.protobuf.internal import builder as _builder
# @@protoc_insertion_point(imports)
_sym_db = _symbol_database.Default()
DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x0f\x65\x63opacket.proto\"\xb8\x06\n\x06Header\x12\x12\n\x05pdata\x18\x01 \x01(\x0cH\x00\x88\x01\x01\x12\x10\n\x03src\x18\x02 \x01(\x05H\x01\x88\x01\x01\x12\x11\n\x04\x64\x65st\x18\x03 \x01(\x05H\x02\x88\x01\x01\x12\x12\n\x05\x64_src\x18\x04 \x01(\x05H\x03\x88\x01\x01\x12\x13\n\x06\x64_dest\x18\x05 \x01(\x05H\x04\x88\x01\x01\x12\x15\n\x08\x65nc_type\x18\x06 \x01(\x05H\x05\x88\x01\x01\x12\x17\n\ncheck_type\x18\x07 \x01(\x05H\x06\x88\x01\x01\x12\x15\n\x08\x63md_func\x18\x08 \x01(\x05H\x07\x88\x01\x01\x12\x13\n\x06\x63md_id\x18\t \x01(\x05H\x08\x88\x01\x01\x12\x15\n\x08\x64\x61ta_len\x18\n \x01(\x05H\t\x88\x01\x01\x12\x15\n\x08need_ack\x18\x0b \x01(\x05H\n\x88\x01\x01\x12\x13\n\x06is_ack\x18\x0c \x01(\x05H\x0b\x88\x01\x01\x12\x10\n\x03seq\x18\x0e \x01(\x05H\x0c\x88\x01\x01\x12\x17\n\nproduct_id\x18\x0f \x01(\x05H\r\x88\x01\x01\x12\x14\n\x07version\x18\x10 \x01(\x05H\x0e\x88\x01\x01\x12\x18\n\x0bpayload_ver\x18\x11 \x01(\x05H\x0f\x88\x01\x01\x12\x16\n\ttime_snap\x18\x12 \x01(\x05H\x10\x88\x01\x01\x12\x16\n\tis_rw_cmd\x18\x13 \x01(\x05H\x11\x88\x01\x01\x12\x15\n\x08is_queue\x18\x14 \x01(\x05H\x12\x88\x01\x01\x12\x15\n\x08\x61\x63k_type\x18\x15 \x01(\x05H\x13\x88\x01\x01\x12\x11\n\x04\x63ode\x18\x16 \x01(\tH\x14\x88\x01\x01\x12\x11\n\x04\x66rom\x18\x17 \x01(\tH\x15\x88\x01\x01\x12\x16\n\tmodule_sn\x18\x18 \x01(\tH\x16\x88\x01\x01\x12\x16\n\tdevice_sn\x18\x19 \x01(\tH\x17\x88\x01\x01\x42\x08\n\x06_pdataB\x06\n\x04_srcB\x07\n\x05_destB\x08\n\x06_d_srcB\t\n\x07_d_destB\x0b\n\t_enc_typeB\r\n\x0b_check_typeB\x0b\n\t_cmd_funcB\t\n\x07_cmd_idB\x0b\n\t_data_lenB\x0b\n\t_need_ackB\t\n\x07_is_ackB\x06\n\x04_seqB\r\n\x0b_product_idB\n\n\x08_versionB\x0e\n\x0c_payload_verB\x0c\n\n_time_snapB\x0c\n\n_is_rw_cmdB\x0b\n\t_is_queueB\x0b\n\t_ack_typeB\x07\n\x05_codeB\x07\n\x05_fromB\x0c\n\n_module_snB\x0c\n\n_device_sn\"2\n\rSendHeaderMsg\x12\x19\n\x03msg\x18\x01 \x01(\x0b\x32\x07.HeaderH\x00\x88\x01\x01\x42\x06\n\x04_msg\"\x93\x05\n\x0bSendMsgHart\x12\x14\n\x07link_id\x18\x01 \x01(\x05H\x00\x88\x01\x01\x12\x10\n\x03src\x18\x02 \x01(\x05H\x01\x88\x01\x01\x12\x11\n\x04\x64\x65st\x18\x03 \x01(\x05H\x02\x88\x01\x01\x12\x12\n\x05\x64_src\x18\x04 \x01(\x05H\x03\x88\x01\x01\x12\x13\n\x06\x64_dest\x18\x05 \x01(\x05H\x04\x88\x01\x01\x12\x15\n\x08\x65nc_type\x18\x06 \x01(\x05H\x05\x88\x01\x01\x12\x17\n\ncheck_type\x18\x07 \x01(\x05H\x06\x88\x01\x01\x12\x15\n\x08\x63md_func\x18\x08 \x01(\x05H\x07\x88\x01\x01\x12\x13\n\x06\x63md_id\x18\t \x01(\x05H\x08\x88\x01\x01\x12\x15\n\x08\x64\x61ta_len\x18\n \x01(\x05H\t\x88\x01\x01\x12\x15\n\x08need_ack\x18\x0b \x01(\x05H\n\x88\x01\x01\x12\x13\n\x06is_ack\x18\x0c \x01(\x05H\x0b\x88\x01\x01\x12\x15\n\x08\x61\x63k_type\x18\r \x01(\x05H\x0c\x88\x01\x01\x12\x10\n\x03seq\x18\x0e \x01(\x05H\r\x88\x01\x01\x12\x16\n\ttime_snap\x18\x0f \x01(\x05H\x0e\x88\x01\x01\x12\x16\n\tis_rw_cmd\x18\x10 \x01(\x05H\x0f\x88\x01\x01\x12\x15\n\x08is_queue\x18\x11 \x01(\x05H\x10\x88\x01\x01\x12\x17\n\nproduct_id\x18\x12 \x01(\x05H\x11\x88\x01\x01\x12\x14\n\x07version\x18\x13 \x01(\x05H\x12\x88\x01\x01\x42\n\n\x08_link_idB\x06\n\x04_srcB\x07\n\x05_destB\x08\n\x06_d_srcB\t\n\x07_d_destB\x0b\n\t_enc_typeB\r\n\x0b_check_typeB\x0b\n\t_cmd_funcB\t\n\x07_cmd_idB\x0b\n\t_data_lenB\x0b\n\t_need_ackB\t\n\x07_is_ackB\x0b\n\t_ack_typeB\x06\n\x04_seqB\x0c\n\n_time_snapB\x0c\n\n_is_rw_cmdB\x0b\n\t_is_queueB\r\n\x0b_product_idB\n\n\x08_versionb\x06proto3')
_globals = globals()
_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals)
_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'ecopacket_pb2', _globals)
if _descriptor._USE_C_DESCRIPTORS == False:
DESCRIPTOR._options = None
_globals['_HEADER']._serialized_start=20
_globals['_HEADER']._serialized_end=844
_globals['_SENDHEADERMSG']._serialized_start=846
_globals['_SENDHEADERMSG']._serialized_end=896
_globals['_SENDMSGHART']._serialized_start=899
_globals['_SENDMSGHART']._serialized_end=1558
# @@protoc_insertion_point(module_scope)

View File

@@ -0,0 +1,105 @@
syntax = "proto3";
message EnergyItem
{
optional uint32 timestamp = 1;
optional uint32 watth_type = 2;
repeated uint32 watth = 3;
}
message EnergyTotalReport
{
optional uint32 watth_seq = 1;
optional EnergyItem watth_item = 2;
}
message BatchEnergyTotalReport
{
optional uint32 watth_seq = 1;
repeated EnergyItem watth_item = 2;
}
message EnergyTotalReportAck
{
optional uint32 result = 1;
optional uint32 watth_seq = 2;
optional uint32 watth_type = 3;
}
message EventRecordItem
{
optional uint32 timestamp = 1;
optional uint32 sys_ms = 2;
optional uint32 event_no = 3;
repeated float event_detail = 4;
}
message EventRecordReport
{
optional uint32 event_ver = 1;
optional uint32 event_seq = 2;
repeated EventRecordItem event_item = 3;
}
message EventInfoReportAck
{
optional uint32 result = 1;
optional uint32 event_seq = 2;
optional uint32 event_item_num =3;
}
message ProductNameSet
{
optional string name = 1;
}
message ProductNameSetAck
{
optional uint32 result = 1;
}
message ProductNameGet { }
message ProductNameGetAck
{
optional string name = 3;
}
message RTCTimeGet { }
message RTCTimeGetAck
{
optional uint32 timestamp = 1;
optional int32 timezone = 2;
}
message RTCTimeSet
{
optional uint32 timestamp = 1;
optional int32 timezone = 2;
}
message RTCTimeSetAck
{
optional uint32 result = 1;
}
message country_town_message
{
optional uint32 country = 1;
optional uint32 town = 2;
}
enum PlCmdSets
{
PL_NONE_CMD_SETS = 0;
PL_BASIC_CMD_SETS = 1;
PL_EXT_CMD_SETS = 254;
}
enum PlCmdId
{
PL_CMD_ID_NONE = 0;
PL_CMD_ID_XLOG = 16;
PL_CMD_ID_WATTH = 32;
}

View File

@@ -0,0 +1,60 @@
# -*- coding: utf-8 -*-
# Generated by the protocol buffer compiler. DO NOT EDIT!
# source: platform.proto
"""Generated protocol buffer code."""
from google.protobuf import descriptor as _descriptor
from google.protobuf import descriptor_pool as _descriptor_pool
from google.protobuf import symbol_database as _symbol_database
from google.protobuf.internal import builder as _builder
# @@protoc_insertion_point(imports)
_sym_db = _symbol_database.Default()
DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x0eplatform.proto\"i\n\nEnergyItem\x12\x16\n\ttimestamp\x18\x01 \x01(\rH\x00\x88\x01\x01\x12\x17\n\nwatth_type\x18\x02 \x01(\rH\x01\x88\x01\x01\x12\r\n\x05watth\x18\x03 \x03(\rB\x0c\n\n_timestampB\r\n\x0b_watth_type\"n\n\x11\x45nergyTotalReport\x12\x16\n\twatth_seq\x18\x01 \x01(\rH\x00\x88\x01\x01\x12$\n\nwatth_item\x18\x02 \x01(\x0b\x32\x0b.EnergyItemH\x01\x88\x01\x01\x42\x0c\n\n_watth_seqB\r\n\x0b_watth_item\"_\n\x16\x42\x61tchEnergyTotalReport\x12\x16\n\twatth_seq\x18\x01 \x01(\rH\x00\x88\x01\x01\x12\x1f\n\nwatth_item\x18\x02 \x03(\x0b\x32\x0b.EnergyItemB\x0c\n\n_watth_seq\"\x84\x01\n\x14\x45nergyTotalReportAck\x12\x13\n\x06result\x18\x01 \x01(\rH\x00\x88\x01\x01\x12\x16\n\twatth_seq\x18\x02 \x01(\rH\x01\x88\x01\x01\x12\x17\n\nwatth_type\x18\x03 \x01(\rH\x02\x88\x01\x01\x42\t\n\x07_resultB\x0c\n\n_watth_seqB\r\n\x0b_watth_type\"\x91\x01\n\x0f\x45ventRecordItem\x12\x16\n\ttimestamp\x18\x01 \x01(\rH\x00\x88\x01\x01\x12\x13\n\x06sys_ms\x18\x02 \x01(\rH\x01\x88\x01\x01\x12\x15\n\x08\x65vent_no\x18\x03 \x01(\rH\x02\x88\x01\x01\x12\x14\n\x0c\x65vent_detail\x18\x04 \x03(\x02\x42\x0c\n\n_timestampB\t\n\x07_sys_msB\x0b\n\t_event_no\"\x85\x01\n\x11\x45ventRecordReport\x12\x16\n\tevent_ver\x18\x01 \x01(\rH\x00\x88\x01\x01\x12\x16\n\tevent_seq\x18\x02 \x01(\rH\x01\x88\x01\x01\x12$\n\nevent_item\x18\x03 \x03(\x0b\x32\x10.EventRecordItemB\x0c\n\n_event_verB\x0c\n\n_event_seq\"\x8a\x01\n\x12\x45ventInfoReportAck\x12\x13\n\x06result\x18\x01 \x01(\rH\x00\x88\x01\x01\x12\x16\n\tevent_seq\x18\x02 \x01(\rH\x01\x88\x01\x01\x12\x1b\n\x0e\x65vent_item_num\x18\x03 \x01(\rH\x02\x88\x01\x01\x42\t\n\x07_resultB\x0c\n\n_event_seqB\x11\n\x0f_event_item_num\",\n\x0eProductNameSet\x12\x11\n\x04name\x18\x01 \x01(\tH\x00\x88\x01\x01\x42\x07\n\x05_name\"3\n\x11ProductNameSetAck\x12\x13\n\x06result\x18\x01 \x01(\rH\x00\x88\x01\x01\x42\t\n\x07_result\"\x10\n\x0eProductNameGet\"/\n\x11ProductNameGetAck\x12\x11\n\x04name\x18\x03 \x01(\tH\x00\x88\x01\x01\x42\x07\n\x05_name\"\x0c\n\nRTCTimeGet\"Y\n\rRTCTimeGetAck\x12\x16\n\ttimestamp\x18\x01 \x01(\rH\x00\x88\x01\x01\x12\x15\n\x08timezone\x18\x02 \x01(\x05H\x01\x88\x01\x01\x42\x0c\n\n_timestampB\x0b\n\t_timezone\"V\n\nRTCTimeSet\x12\x16\n\ttimestamp\x18\x01 \x01(\rH\x00\x88\x01\x01\x12\x15\n\x08timezone\x18\x02 \x01(\x05H\x01\x88\x01\x01\x42\x0c\n\n_timestampB\x0b\n\t_timezone\"/\n\rRTCTimeSetAck\x12\x13\n\x06result\x18\x01 \x01(\rH\x00\x88\x01\x01\x42\t\n\x07_result\"T\n\x14\x63ountry_town_message\x12\x14\n\x07\x63ountry\x18\x01 \x01(\rH\x00\x88\x01\x01\x12\x11\n\x04town\x18\x02 \x01(\rH\x01\x88\x01\x01\x42\n\n\x08_countryB\x07\n\x05_town*N\n\tPlCmdSets\x12\x14\n\x10PL_NONE_CMD_SETS\x10\x00\x12\x15\n\x11PL_BASIC_CMD_SETS\x10\x01\x12\x14\n\x0fPL_EXT_CMD_SETS\x10\xfe\x01*F\n\x07PlCmdId\x12\x12\n\x0ePL_CMD_ID_NONE\x10\x00\x12\x12\n\x0ePL_CMD_ID_XLOG\x10\x10\x12\x13\n\x0fPL_CMD_ID_WATTH\x10 b\x06proto3')
_globals = globals()
_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals)
_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'platform_pb2', _globals)
if _descriptor._USE_C_DESCRIPTORS == False:
DESCRIPTOR._options = None
_globals['_PLCMDSETS']._serialized_start=1388
_globals['_PLCMDSETS']._serialized_end=1466
_globals['_PLCMDID']._serialized_start=1468
_globals['_PLCMDID']._serialized_end=1538
_globals['_ENERGYITEM']._serialized_start=18
_globals['_ENERGYITEM']._serialized_end=123
_globals['_ENERGYTOTALREPORT']._serialized_start=125
_globals['_ENERGYTOTALREPORT']._serialized_end=235
_globals['_BATCHENERGYTOTALREPORT']._serialized_start=237
_globals['_BATCHENERGYTOTALREPORT']._serialized_end=332
_globals['_ENERGYTOTALREPORTACK']._serialized_start=335
_globals['_ENERGYTOTALREPORTACK']._serialized_end=467
_globals['_EVENTRECORDITEM']._serialized_start=470
_globals['_EVENTRECORDITEM']._serialized_end=615
_globals['_EVENTRECORDREPORT']._serialized_start=618
_globals['_EVENTRECORDREPORT']._serialized_end=751
_globals['_EVENTINFOREPORTACK']._serialized_start=754
_globals['_EVENTINFOREPORTACK']._serialized_end=892
_globals['_PRODUCTNAMESET']._serialized_start=894
_globals['_PRODUCTNAMESET']._serialized_end=938
_globals['_PRODUCTNAMESETACK']._serialized_start=940
_globals['_PRODUCTNAMESETACK']._serialized_end=991
_globals['_PRODUCTNAMEGET']._serialized_start=993
_globals['_PRODUCTNAMEGET']._serialized_end=1009
_globals['_PRODUCTNAMEGETACK']._serialized_start=1011
_globals['_PRODUCTNAMEGETACK']._serialized_end=1058
_globals['_RTCTIMEGET']._serialized_start=1060
_globals['_RTCTIMEGET']._serialized_end=1072
_globals['_RTCTIMEGETACK']._serialized_start=1074
_globals['_RTCTIMEGETACK']._serialized_end=1163
_globals['_RTCTIMESET']._serialized_start=1165
_globals['_RTCTIMESET']._serialized_end=1251
_globals['_RTCTIMESETACK']._serialized_start=1253
_globals['_RTCTIMESETACK']._serialized_end=1300
_globals['_COUNTRY_TOWN_MESSAGE']._serialized_start=1302
_globals['_COUNTRY_TOWN_MESSAGE']._serialized_end=1386
# @@protoc_insertion_point(module_scope)

View File

@@ -0,0 +1,127 @@
syntax = "proto3";
message InverterHeartbeat {
optional uint32 inv_error_code = 1;
optional uint32 inv_warning_code = 3;
optional uint32 pv1_error_code = 2;
optional uint32 pv1_warning_code = 4;
optional uint32 pv2_error_code = 5;
optional uint32 pv2_warning_code = 6;
optional uint32 bat_error_code = 7;
optional uint32 bat_warning_code = 8;
optional uint32 llc_error_code = 9;
optional uint32 llc_warning_code = 10;
optional uint32 pv1_status = 11;
optional uint32 pv2_status = 12;
optional uint32 bat_status = 13;
optional uint32 llc_status = 14;
optional uint32 inv_status = 15;
optional int32 pv1_input_volt = 16;
optional int32 pv1_op_volt = 17;
optional int32 pv1_input_cur = 18;
optional int32 pv1_input_watts = 19;
optional int32 pv1_temp = 20;
optional int32 pv2_input_volt = 21;
optional int32 pv2_op_volt = 22;
optional int32 pv2_input_cur = 23;
optional int32 pv2_input_watts = 24;
optional int32 pv2_temp = 25;
optional int32 bat_input_volt = 26;
optional int32 bat_op_volt = 27;
optional int32 bat_input_cur = 28;
optional int32 bat_input_watts = 29;
optional int32 bat_temp = 30;
optional uint32 bat_soc = 31;
optional int32 llc_input_volt = 32;
optional int32 llc_op_volt = 33;
optional int32 llc_temp = 34;
optional int32 inv_input_volt = 35;
optional int32 inv_op_volt = 36;
optional int32 inv_output_cur = 37;
optional int32 inv_output_watts = 38;
optional int32 inv_temp = 39;
optional int32 inv_freq = 40;
optional int32 inv_dc_cur = 41;
optional int32 bp_type = 42;
optional int32 inv_relay_status = 43;
optional int32 pv1_relay_status = 44;
optional int32 pv2_relay_status = 45;
optional uint32 install_country = 46;
optional uint32 install_town = 47;
optional uint32 permanent_watts = 48;
optional uint32 dynamic_watts = 49;
optional uint32 supply_priority = 50;
optional uint32 lower_limit = 51;
optional uint32 upper_limit = 52;
optional uint32 inv_on_off = 53;
optional uint32 wireless_error_code = 54;
optional uint32 wireless_warning_code = 55;
optional uint32 inv_brightness = 56;
optional uint32 heartbeat_frequency = 57;
optional uint32 rated_power = 58;
optional uint32 battery_charge_remain = 59;
optional uint32 battery_discharge_remain = 60;
}
message PermanentWattsPack
{
optional uint32 permanent_watts = 1;
}
message SupplyPriorityPack
{
optional uint32 supply_priority = 1;
}
message BatLowerPack
{
optional int32 lower_limit = 1;
}
message BatUpperPack
{
optional int32 upper_limit = 1;
}
message BrightnessPack
{
optional int32 brightness = 1;
}
message PowerItem
{
optional uint32 timestamp = 1;
optional sint32 timezone = 2;
optional uint32 inv_to_grid_power = 3;
optional uint32 inv_to_plug_power = 4;
optional int32 battery_power = 5;
optional uint32 pv1_output_power = 6;
optional uint32 pv2_output_power = 7;
}
message PowerPack
{
optional uint32 sys_seq = 1;
repeated PowerItem sys_power_stream = 2;
}
message PowerAckPack
{
optional uint32 sys_seq = 1;
}
message NodeMassage
{
optional string sn = 1;
optional bytes mac = 2;
}
message MeshChildNodeInfo
{
optional uint32 topology_type = 1;
optional uint32 mesh_protocol = 2;
optional uint32 max_sub_device_num = 3;
optional bytes parent_mac_id = 4;
optional bytes mesh_id = 5;
repeated NodeMassage sub_device_list = 6;
}

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,34 @@
from collections import OrderedDict
from typing import Callable, List, TypeVar
class LimitedSizeOrderedDict(OrderedDict):
def __init__(self, maxlen=20):
"""Initialize a new DedupStore."""
super().__init__()
self.maxlen = maxlen
def append(self, key, val, on_delete: Callable = None):
self[key] = val
self.move_to_end(key)
if len(self) > self.maxlen:
# Removes the first record which should also be the oldest
itm = self.popitem(last=False)
if on_delete:
on_delete(itm)
_T = TypeVar("_T")
class BoundFifoList(List):
def __init__(self, maxlen=20) -> None:
super().__init__()
self.maxlen = maxlen
def append(self, __object: _T) -> None:
super().insert(0, __object)
while len(self) >= self.maxlen:
self.pop()

View File

@@ -0,0 +1,86 @@
from typing import Callable, Any
from homeassistant.components.number import NumberMode
from homeassistant.components.sensor import SensorDeviceClass
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import PERCENTAGE, UnitOfPower, UnitOfTemperature
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from . import DOMAIN, OPTS_POWER_STEP
from .entities import BaseNumberEntity
from .mqtt.ecoflow_mqtt import EcoflowMQTTClient
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback):
client: EcoflowMQTTClient = hass.data[DOMAIN][entry.entry_id]
from .devices.registry import devices
async_add_entities(devices[client.device_type].numbers(client))
class ValueUpdateEntity(BaseNumberEntity):
_attr_native_step = 1
_attr_mode = NumberMode.SLIDER
async def async_set_native_value(self, value: float):
if self._command:
ival = int(value)
self.send_set_message(ival, self.command_dict(ival))
class ChargingPowerEntity(ValueUpdateEntity):
_attr_icon = "mdi:transmission-tower-import"
_attr_native_unit_of_measurement = UnitOfPower.WATT
_attr_device_class = SensorDeviceClass.POWER
def __init__(self, client: EcoflowMQTTClient, mqtt_key: str, title: str, min_value: int, max_value: int,
command: Callable[[int], dict[str, any]] | None, enabled: bool = True, auto_enable: bool = False):
super().__init__(client, mqtt_key, title, min_value, max_value, command, enabled, auto_enable)
self._attr_native_step = client.config_entry.options[OPTS_POWER_STEP]
class BatteryBackupLevel(ValueUpdateEntity):
_attr_icon = "mdi:battery-charging-90"
_attr_native_unit_of_measurement = PERCENTAGE
def __init__(self, client: EcoflowMQTTClient, mqtt_key: str, title: str,
min_value: int, max_value: int,
min_key: str, max_key: str,
command: Callable[[int], dict[str, any]] | None):
super().__init__(client, mqtt_key, title, min_value, max_value, command, True, False)
self.__min_key = min_key
self.__max_key = max_key
def _updated(self, data: dict[str, Any]):
if self.__min_key in data:
self._attr_native_min_value = int(data[self.__min_key]) + 5 # min + 5%
if self.__max_key in data:
self._attr_native_max_value = int(data[self.__max_key])
super()._updated(data)
class LevelEntity(ValueUpdateEntity):
_attr_native_unit_of_measurement = PERCENTAGE
class MinBatteryLevelEntity(LevelEntity):
_attr_icon = "mdi:battery-charging-10"
class MaxBatteryLevelEntity(LevelEntity):
_attr_icon = "mdi:battery-charging-90"
class MinGenStartLevelEntity(LevelEntity):
_attr_icon = "mdi:engine"
class MaxGenStopLevelEntity(LevelEntity):
_attr_icon = "mdi:engine-off"
class SetTempEntity(ValueUpdateEntity):
_attr_native_unit_of_measurement = UnitOfTemperature.CELSIUS

View File

@@ -0,0 +1,9 @@
from homeassistant.core import callback, HomeAssistant
from custom_components.ecoflow_cloud import ATTR_STATUS_UPDATES, ATTR_STATUS_DATA_LAST_UPDATE, \
ATTR_STATUS_LAST_UPDATE, ATTR_STATUS_PHASE
@callback
def exclude_attributes(hass: HomeAssistant) -> set[str]:
return {ATTR_STATUS_UPDATES, ATTR_STATUS_DATA_LAST_UPDATE, ATTR_STATUS_LAST_UPDATE, ATTR_STATUS_PHASE}

View File

@@ -0,0 +1,48 @@
from typing import Callable, Any
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity import EntityCategory
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from custom_components.ecoflow_cloud import EcoflowMQTTClient, DOMAIN
from custom_components.ecoflow_cloud.entities import BaseSelectEntity
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback):
client: EcoflowMQTTClient = hass.data[DOMAIN][entry.entry_id]
from .devices.registry import devices
async_add_entities(devices[client.device_type].selects(client))
class DictSelectEntity(BaseSelectEntity):
_attr_entity_category = EntityCategory.CONFIG
_attr_available = False
def __init__(self, client: EcoflowMQTTClient, mqtt_key: str, title: str, options: dict[str, int],
command: Callable[[int], dict[str, any]] | None, enabled: bool = True, auto_enable: bool = False):
super().__init__(client, mqtt_key, title, command, enabled, auto_enable)
self.__options_dict = options
self._attr_options = list(options.keys())
def options_dict(self) -> dict[str, int]:
return self.__options_dict
def _update_value(self, val: Any) -> bool:
ival = int(val)
lval = [k for k, v in self.__options_dict.items() if v == ival]
if len(lval) == 1:
self._attr_current_option = lval[0]
return True
else:
return False
async def async_select_option(self, option: str):
if self._command:
val = self.__options_dict[option]
self.send_set_message(val, self.command_dict(val))
class TimeoutDictSelectEntity(DictSelectEntity):
_attr_icon = "mdi:timer-outline"

View File

@@ -0,0 +1,393 @@
import math
import logging
from datetime import timedelta, datetime
from typing import Any, Mapping, OrderedDict
from homeassistant.components.binary_sensor import BinarySensorEntity, BinarySensorDeviceClass
from homeassistant.components.sensor import (SensorDeviceClass, SensorStateClass, SensorEntity)
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import (PERCENTAGE,
UnitOfElectricCurrent, UnitOfElectricPotential, UnitOfEnergy, UnitOfFrequency,
UnitOfPower, UnitOfTemperature, UnitOfTime)
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity import EntityCategory
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.event import async_track_time_interval
from homeassistant.util import utcnow
from homeassistant.util.dt import UTC
from . import DOMAIN, ATTR_STATUS_SN, ATTR_STATUS_DATA_LAST_UPDATE, ATTR_STATUS_UPDATES, \
ATTR_STATUS_LAST_UPDATE, ATTR_STATUS_RECONNECTS, ATTR_STATUS_PHASE
from .entities import BaseSensorEntity, EcoFlowAbstractEntity, EcoFlowDictEntity
from .mqtt.ecoflow_mqtt import EcoflowMQTTClient
_LOGGER = logging.getLogger(__name__)
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback):
client: EcoflowMQTTClient = hass.data[DOMAIN][entry.entry_id]
from .devices.registry import devices
async_add_entities(devices[client.device_type].sensors(client))
class MiscBinarySensorEntity(BinarySensorEntity, EcoFlowDictEntity):
def _update_value(self, val: Any) -> bool:
self._attr_is_on = bool(val)
return True
class ChargingStateSensorEntity(BaseSensorEntity):
_attr_entity_category = EntityCategory.DIAGNOSTIC
_attr_icon = "mdi:battery-charging"
_attr_device_class = BinarySensorDeviceClass.BATTERY_CHARGING
def _update_value(self, val: Any) -> bool:
if val == 0:
return super()._update_value("unused")
elif val == 1:
return super()._update_value("charging")
elif val == 2:
return super()._update_value("discharging")
else:
return False
class CyclesSensorEntity(BaseSensorEntity):
_attr_entity_category = EntityCategory.DIAGNOSTIC
_attr_icon = "mdi:battery-heart-variant"
_attr_state_class = SensorStateClass.TOTAL_INCREASING
class FanSensorEntity(BaseSensorEntity):
_attr_state_class = SensorStateClass.MEASUREMENT
_attr_icon = "mdi:fan"
class MiscSensorEntity(BaseSensorEntity):
_attr_entity_category = EntityCategory.DIAGNOSTIC
class LevelSensorEntity(BaseSensorEntity):
_attr_device_class = SensorDeviceClass.BATTERY
_attr_native_unit_of_measurement = PERCENTAGE
_attr_state_class = SensorStateClass.MEASUREMENT
class RemainSensorEntity(BaseSensorEntity):
_attr_device_class = SensorDeviceClass.DURATION
_attr_native_unit_of_measurement = UnitOfTime.MINUTES
_attr_state_class = SensorStateClass.MEASUREMENT
_attr_native_value = 0
def _update_value(self, val: Any) -> Any:
ival = int(val)
if ival < 0 or ival > 5000:
ival = 0
return super()._update_value(ival)
class SecondsRemainSensorEntity(BaseSensorEntity):
_attr_device_class = SensorDeviceClass.DURATION
_attr_native_unit_of_measurement = UnitOfTime.SECONDS
_attr_state_class = SensorStateClass.MEASUREMENT
_attr_native_value = 0
def _update_value(self, val: Any) -> Any:
ival = int(val)
if ival < 0 or ival > 5000:
ival = 0
return super()._update_value(ival)
class TempSensorEntity(BaseSensorEntity):
_attr_device_class = SensorDeviceClass.TEMPERATURE
_attr_entity_category = EntityCategory.DIAGNOSTIC
_attr_native_unit_of_measurement = UnitOfTemperature.CELSIUS
_attr_state_class = SensorStateClass.MEASUREMENT
_attr_native_value = -1
class DecicelsiusSensorEntity(TempSensorEntity):
def _update_value(self, val: Any) -> bool:
return super()._update_value(int(val) / 10)
class MilliCelsiusSensorEntity(TempSensorEntity):
def _update_value(self, val: Any) -> bool:
return super()._update_value(int(val) / 100)
class VoltSensorEntity(BaseSensorEntity):
_attr_device_class = SensorDeviceClass.VOLTAGE
_attr_entity_category = EntityCategory.DIAGNOSTIC
_attr_native_unit_of_measurement = UnitOfElectricPotential.VOLT
_attr_state_class = SensorStateClass.MEASUREMENT
_attr_native_value = 0
class MilliVoltSensorEntity(BaseSensorEntity):
_attr_device_class = SensorDeviceClass.VOLTAGE
_attr_entity_category = EntityCategory.DIAGNOSTIC
_attr_native_unit_of_measurement = UnitOfElectricPotential.MILLIVOLT
_attr_suggested_unit_of_measurement = UnitOfElectricPotential.VOLT
_attr_state_class = SensorStateClass.MEASUREMENT
_attr_native_value = 3
class InMilliVoltSensorEntity(MilliVoltSensorEntity):
_attr_icon = "mdi:transmission-tower-import"
_attr_suggested_display_precision = 0
class OutMilliVoltSensorEntity(MilliVoltSensorEntity):
_attr_icon = "mdi:transmission-tower-export"
_attr_suggested_display_precision = 0
class DecivoltSensorEntity(BaseSensorEntity):
_attr_device_class = SensorDeviceClass.VOLTAGE
_attr_entity_category = EntityCategory.DIAGNOSTIC
_attr_native_unit_of_measurement = UnitOfElectricPotential.VOLT
_attr_state_class = SensorStateClass.MEASUREMENT
_attr_native_value = 0
def _update_value(self, val: Any) -> bool:
return super()._update_value(int(val) / 10)
class CentivoltSensorEntity(DecivoltSensorEntity):
def _update_value(self, val: Any) -> bool:
return super()._update_value(int(val) / 10)
class AmpSensorEntity(BaseSensorEntity):
_attr_device_class = SensorDeviceClass.CURRENT
_attr_entity_category = EntityCategory.DIAGNOSTIC
_attr_native_unit_of_measurement = UnitOfElectricCurrent.MILLIAMPERE
_attr_state_class = SensorStateClass.MEASUREMENT
_attr_native_value = 0
class DeciampSensorEntity(BaseSensorEntity):
_attr_device_class = SensorDeviceClass.CURRENT
_attr_entity_category = EntityCategory.DIAGNOSTIC
_attr_native_unit_of_measurement = UnitOfElectricCurrent.AMPERE
_attr_state_class = SensorStateClass.MEASUREMENT
_attr_native_value = 0
def _update_value(self, val: Any) -> bool:
return super()._update_value(int(val) / 10)
class WattsSensorEntity(BaseSensorEntity):
_attr_entity_category = EntityCategory.DIAGNOSTIC
_attr_device_class = SensorDeviceClass.POWER
_attr_native_unit_of_measurement = UnitOfPower.WATT
_attr_state_class = SensorStateClass.MEASUREMENT
_attr_native_value = 0
class EnergySensorEntity(BaseSensorEntity):
_attr_device_class = SensorDeviceClass.ENERGY
_attr_native_unit_of_measurement = UnitOfEnergy.WATT_HOUR
_attr_state_class = SensorStateClass.TOTAL_INCREASING
def _update_value(self, val: Any) -> bool:
ival = int(val)
if ival > 0:
return super()._update_value(ival)
else:
return False
class CapacitySensorEntity(BaseSensorEntity):
_attr_device_class = SensorDeviceClass.CURRENT
_attr_native_unit_of_measurement = "mAh"
_attr_state_class = SensorStateClass.MEASUREMENT
class DeciwattsSensorEntity(WattsSensorEntity):
def _update_value(self, val: Any) -> bool:
return super()._update_value(int(val) / 10)
class InWattsSensorEntity(WattsSensorEntity):
_attr_icon = "mdi:transmission-tower-import"
class InWattsSolarSensorEntity(InWattsSensorEntity):
_attr_icon = "mdi:solar-power"
def _update_value(self, val: Any) -> bool:
return super()._update_value(int(val) / 10)
class OutWattsSensorEntity(WattsSensorEntity):
_attr_icon = "mdi:transmission-tower-export"
class OutWattsDcSensorEntity(WattsSensorEntity):
_attr_icon = "mdi:transmission-tower-export"
def _update_value(self, val: Any) -> bool:
return super()._update_value(int(val) / 10)
class InVoltSensorEntity(VoltSensorEntity):
_attr_icon = "mdi:transmission-tower-import"
class InVoltSolarSensorEntity(VoltSensorEntity):
_attr_icon = "mdi:solar-power"
def _update_value(self, val: Any) -> bool:
return super()._update_value(int(val) / 10)
class OutVoltDcSensorEntity(VoltSensorEntity):
_attr_icon = "mdi:transmission-tower-export"
def _update_value(self, val: Any) -> bool:
return super()._update_value(int(val) / 10)
class InAmpSensorEntity(AmpSensorEntity):
_attr_icon = "mdi:transmission-tower-import"
class InAmpSolarSensorEntity(AmpSensorEntity):
_attr_icon = "mdi:solar-power"
def _update_value(self, val: Any) -> bool:
return super()._update_value(int(val) * 10)
class InEnergySensorEntity(EnergySensorEntity):
_attr_icon = "mdi:transmission-tower-import"
class OutEnergySensorEntity(EnergySensorEntity):
_attr_icon = "mdi:transmission-tower-export"
class FrequencySensorEntity(BaseSensorEntity):
_attr_device_class = SensorDeviceClass.FREQUENCY
_attr_entity_category = EntityCategory.DIAGNOSTIC
_attr_native_unit_of_measurement = UnitOfFrequency.HERTZ
_attr_state_class = SensorStateClass.MEASUREMENT
class DecihertzSensorEntity(FrequencySensorEntity):
def _update_value(self, val: Any) -> bool:
return super()._update_value(int(val) / 10)
class StatusSensorEntity(SensorEntity, EcoFlowAbstractEntity):
_attr_entity_category = EntityCategory.DIAGNOSTIC
DEADLINE_PHASE = 10
CHECK_PHASES = [2, 4, 6]
CONNECT_PHASES = [3, 5, 7]
def __init__(self, client: EcoflowMQTTClient, check_interval_sec=30):
super().__init__(client, "Status", "status")
self._online = 0
self.__check_interval_sec = check_interval_sec
self._attrs = OrderedDict[str, Any]()
self._attrs[ATTR_STATUS_SN] = client.device_sn
self._attrs[ATTR_STATUS_DATA_LAST_UPDATE] = self._client.data.params_time()
self._attrs[ATTR_STATUS_UPDATES] = 0
self._attrs[ATTR_STATUS_LAST_UPDATE] = None
self._attrs[ATTR_STATUS_RECONNECTS] = 0
self._attrs[ATTR_STATUS_PHASE] = 0
async def async_added_to_hass(self):
await super().async_added_to_hass()
params_d = self._client.data.params_observable().subscribe(self.__params_update)
self.async_on_remove(params_d.dispose)
self.async_on_remove(
async_track_time_interval(self.hass, self.__check_status, timedelta(seconds=self.__check_interval_sec)))
self._update_status((utcnow() - self._client.data.params_time()).total_seconds())
def __check_status(self, now: datetime):
data_outdated_sec = (now - self._client.data.params_time()).total_seconds()
phase = math.ceil(data_outdated_sec / self.__check_interval_sec)
self._attrs[ATTR_STATUS_PHASE] = phase
time_to_reconnect = phase in self.CONNECT_PHASES
time_to_check_status = phase in self.CHECK_PHASES
if self._online == 1:
if time_to_check_status or phase >= self.DEADLINE_PHASE:
# online and outdated - refresh status to detect if device went offline
self._update_status(data_outdated_sec)
elif time_to_reconnect:
# online, updated and outdated - reconnect
self._attrs[ATTR_STATUS_RECONNECTS] = self._attrs[ATTR_STATUS_RECONNECTS] + 1
self._client.reconnect()
self.schedule_update_ha_state()
elif not self._client.is_connected(): # validate connection even for offline device
self._attrs[ATTR_STATUS_RECONNECTS] = self._attrs[ATTR_STATUS_RECONNECTS] + 1
self._client.reconnect()
self.schedule_update_ha_state()
def __params_update(self, data: dict[str, Any]):
self._attrs[ATTR_STATUS_DATA_LAST_UPDATE] = self._client.data.params_time()
if self._online == 0:
self._update_status(0)
self.schedule_update_ha_state()
def _update_status(self, data_outdated_sec):
if data_outdated_sec > self.__check_interval_sec * self.DEADLINE_PHASE:
self._online = 0
self._attr_native_value = "assume_offline"
else:
self._online = 1
self._attr_native_value = "assume_online"
self._attrs[ATTR_STATUS_LAST_UPDATE] = utcnow()
self._attrs[ATTR_STATUS_UPDATES] = self._attrs[ATTR_STATUS_UPDATES] + 1
self.schedule_update_ha_state()
@property
def extra_state_attributes(self) -> Mapping[str, Any] | None:
return self._attrs
class QuotasStatusSensorEntity(StatusSensorEntity):
_attr_entity_category = EntityCategory.DIAGNOSTIC
def __init__(self, client: EcoflowMQTTClient):
super().__init__(client)
async def async_added_to_hass(self):
get_reply_d = self._client.data.get_reply_observable().subscribe(self.__get_reply_update)
self.async_on_remove(get_reply_d.dispose)
await super().async_added_to_hass()
def _update_status(self, update_delta_sec):
if self._client.is_connected():
self._attrs[ATTR_STATUS_UPDATES] = self._attrs[ATTR_STATUS_UPDATES] + 1
self.send_get_message({"version": "1.1", "moduleType": 0, "operateType": "latestQuotas", "params": {}})
else:
super()._update_status(update_delta_sec)
def __get_reply_update(self, data: list[dict[str, Any]]):
d = data[0]
if d["operateType"] == "latestQuotas":
self._online = d["data"]["online"]
self._attrs[ATTR_STATUS_LAST_UPDATE] = utcnow()
if self._online == 1:
self._attrs[ATTR_STATUS_SN] = d["data"]["sn"]
self._attr_native_value = "online"
# ?? self._client.data.update_data(d["data"]["quotaMap"])
else:
self._attr_native_value = "offline"
self.schedule_update_ha_state()

View File

@@ -0,0 +1,73 @@
import logging
from typing import Any
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity import EntityCategory
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from . import DOMAIN
from .entities import BaseSwitchEntity
from .mqtt.ecoflow_mqtt import EcoflowMQTTClient
_LOGGER = logging.getLogger(__name__)
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback):
client: EcoflowMQTTClient = hass.data[DOMAIN][entry.entry_id]
from .devices.registry import devices
async_add_entities(devices[client.device_type].switches(client))
class EnabledEntity(BaseSwitchEntity):
def _update_value(self, val: Any) -> bool:
_LOGGER.debug("Updating switch " + self._attr_unique_id + " to " + str(val))
self._attr_is_on = bool(val)
return True
def turn_on(self, **kwargs: Any) -> None:
if self._command:
self.send_set_message(1, self.command_dict(1))
def turn_off(self, **kwargs: Any) -> None:
if self._command:
self.send_set_message(0, self.command_dict(0))
class DisabledEntity(BaseSwitchEntity):
def _update_value(self, val: Any) -> bool:
_LOGGER.debug("Updating switch " + self._attr_unique_id + " to " + str(val))
self._attr_is_on = not bool(val)
return True
async def async_turn_on(self, **kwargs: Any) -> None:
if self._command:
self.send_set_message(0, self.command_dict(0))
async def async_turn_off(self, **kwargs: Any) -> None:
if self._command:
self.send_set_message(1, self.command_dict(1))
class BeeperEntity(DisabledEntity):
_attr_entity_category = EntityCategory.CONFIG
@property
def icon(self) -> str | None:
if self.is_on:
return "mdi:volume-high"
else:
return "mdi:volume-mute"
class InvertedBeeperEntity(EnabledEntity):
_attr_entity_category = EntityCategory.CONFIG
@property
def icon(self) -> str | None:
if self.is_on:
return "mdi:volume-high"
else:
return "mdi:volume-mute"

View File

@@ -0,0 +1,26 @@
{
"title": "EcoFlow-Cloud",
"config": {
"step": {
"user": {
"data": {
"username": "Benutzer-Email",
"password": "Benutzer-Passwort",
"type": "Gerätetyp",
"name": "Gerätename",
"device_id": "Seriennummer des Gerät"
}
}
}
},
"options": {
"step": {
"init": {
"data": {
"power_step": "Ladeleistung (Schritte für Schieberegler)",
"refresh_period_sec": "Datenaktualisierung in Sekunden"
}
}
}
}
}

View File

@@ -0,0 +1,26 @@
{
"title": "EcoFlow-Cloud",
"config": {
"step": {
"user": {
"data": {
"username": "User email",
"password": "User password",
"type": "Device type",
"name": "Device name",
"device_id": "Device SN"
}
}
}
},
"options": {
"step": {
"init": {
"data": {
"power_step": "Charging power slider step",
"refresh_period_sec": "Data refresh period (sec)"
}
}
}
}
}

View File

@@ -0,0 +1,27 @@
{
"title": "EcoFlow-Cloud",
"config": {
"step": {
"user": {
"data": {
"username": "Adresse e-mail",
"password": "Mot de passe",
"type": "Type d'appareil",
"name": "Nom de l'appareil",
"device_id": "Numéro de série"
}
}
}
},
"options": {
"step": {
"init": {
"data": {
"power_step": "Pas du curseur de puissance de charge",
"refresh_period_sec": "Durée actualisation des données (secondes)"
}
}
}
}
}