Skip to content

Commit

Permalink
Smart Home Panel (draft)
Browse files Browse the repository at this point in the history
  • Loading branch information
tolwi committed Nov 8, 2023
1 parent 4c814ed commit bac8c48
Show file tree
Hide file tree
Showing 9 changed files with 129 additions and 16 deletions.
1 change: 1 addition & 0 deletions custom_components/ecoflow_cloud/config/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ class EcoflowModel(Enum):
DELTA_2_MAX = 9,
DELTA_MINI = 15, # productType = 15
POWERSTREAM = 51,
SMART_HOME_PANEL = 60, # random
DIAGNOSTIC = 99

@classmethod
Expand Down
10 changes: 8 additions & 2 deletions custom_components/ecoflow_cloud/devices/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,6 @@
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"
Expand Down Expand Up @@ -171,7 +170,6 @@
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"
Expand All @@ -180,3 +178,11 @@

BATTERY_AMP = "Battery Current"
SLAVE_BATTERY_AMP = "Slave Battery Current"

SHP_CIRCUIT_N_MODE = "Circuit %i Mode"
SHP_CIRCUIT_N_POWER = "Circuit %i Power"
SHP_AC_N_OUT_POWER = "AC%i Out Power"
SHP_AC_N_IN_POWER = "AC%i In Power"
SHP_AC_N_BATTERY_LEVEL = "AC%i Battery Level"
SHP_AC_N_CHARGE_TIME = "AC%i Charge Time"
SHP_AC_N_DISCHARGE_TIME = "AC%i Discharge Time"
2 changes: 2 additions & 0 deletions custom_components/ecoflow_cloud/devices/registry.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
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.smart_home_panel import SmartHomePanel

devices: dict[str, BaseDevice] = {
EcoflowModel.DELTA_2.name: Delta2(),
Expand All @@ -24,5 +25,6 @@
EcoflowModel.DELTA_MAX.name: DeltaMax(),
EcoflowModel.DELTA_2_MAX.name: Delta2Max(),
EcoflowModel.POWERSTREAM.name: PowerStream(),
EcoflowModel.SMART_HOME_PANEL.name: SmartHomePanel(),
EcoflowModel.DIAGNOSTIC.name: DiagnosticDevice()
}
74 changes: 74 additions & 0 deletions custom_components/ecoflow_cloud/devices/smart_home_panel.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
from typing import Any

from . import BaseDevice, const
from .. import EcoflowMQTTClient
from ..entities import BaseSensorEntity, BaseNumberEntity, BaseSwitchEntity, BaseSelectEntity
from ..select import DictSelectEntity
from ..sensor import OutWattsSensorEntity, InWattsSensorEntity, LevelSensorEntity, \
RemainSensorEntity

MODES = {
"Auto": {"sta": 0, "ctrl_mode": 0},
"Grid": {"sta": 0, "ctrl_mode": 1},
"Battery": {"sta": 1, "ctrl_mode": 1},
"Off": {"sta": 2, "ctrl_mode": 1}
}


class ModeDictSelectEntity(DictSelectEntity):

def __init__(self, client: EcoflowMQTTClient, n: int):
super().__init__(client, "loadCmdChCtrlInfos[%i]" % n, const.SHP_CIRCUIT_N_MODE % (n + 1), MODES,
lambda value: {"sta": value.sta, "ctrlMode": value.ctrl_mode, "ch": 0, "cmdSet": 11, "id": 16})

def _update_value(self, val: Any) -> bool:
return super()._update_value({"sta": val.sta, "ctrl_mode": val.ctrlMode})


class SmartHomePanel(BaseDevice):

def sensors(self, client: EcoflowMQTTClient) -> list[BaseSensorEntity]:
res = [
LevelSensorEntity(client, "energyInfos[0].batteryPercentage", const.SHP_AC_N_BATTERY_LEVEL % 1),
OutWattsSensorEntity(client, "energyInfos[0].outputPower", const.SHP_AC_N_OUT_POWER % 1),
InWattsSensorEntity(client, "energyInfos[0].lcdInputWatts", const.SHP_AC_N_IN_POWER % 1),
RemainSensorEntity(client, "energyInfos[0].chargeTime", const.SHP_AC_N_CHARGE_TIME % 1),
RemainSensorEntity(client, "energyInfos[0].dischargeTime", const.SHP_AC_N_DISCHARGE_TIME % 1),

LevelSensorEntity(client, "energyInfos[1].batteryPercentage", const.SHP_AC_N_BATTERY_LEVEL % 2),
OutWattsSensorEntity(client, "energyInfos[1].outputPower", const.SHP_AC_N_OUT_POWER % 2),
InWattsSensorEntity(client, "energyInfos[1].lcdInputWatts", const.SHP_AC_N_IN_POWER % 2),
RemainSensorEntity(client, "energyInfos[1].chargeTime", const.SHP_AC_N_CHARGE_TIME % 2),
RemainSensorEntity(client, "energyInfos[1].dischargeTime", const.SHP_AC_N_DISCHARGE_TIME % 2),
]

return res

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

]

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

]

def selects(self, client: EcoflowMQTTClient) -> list[BaseSelectEntity]:
res = []
res.extend(self.generate_mode_selects(client))

return res

def generate_power_sensors(self, client: EcoflowMQTTClient) -> list[BaseSensorEntity]:
res = []
for i in range(0, 11):
res.append(InWattsSensorEntity(client, "infoList[%i].chWatt", const.SHP_CIRCUIT_N_POWER % (i + 1)))
return res


def generate_mode_selects(self, client: EcoflowMQTTClient) -> list[DictSelectEntity]:
res = []
for i in range(0, 10):
res.append(ModeDictSelectEntity(client, i))
return res
30 changes: 24 additions & 6 deletions custom_components/ecoflow_cloud/entities/__init__.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
from __future__ import annotations

import inspect
import logging
from typing import Any, Callable, Optional, OrderedDict, Mapping

import jsonpath_ng.ext as jp
from homeassistant.components.number import NumberEntity
from homeassistant.components.select import SelectEntity
from homeassistant.components.sensor import SensorEntity
Expand All @@ -10,6 +13,8 @@

from ..mqtt.ecoflow_mqtt import EcoflowMQTTClient

_LOGGER = logging.getLogger(__name__)


class EcoFlowAbstractEntity(Entity):
_attr_has_entity_name = True
Expand All @@ -29,7 +34,11 @@ def send_set_message(self, target_dict: dict[str, Any] | None, command: dict):

@staticmethod
def gen_unique_id(sn: str, key: str):
return 'ecoflow-' + sn + '-' + key.replace('.', '-').replace('_', '-')
return ('ecoflow-' + sn + '-' + key.replace('.', '-')
.replace('_', '-')
.replace('[', '-')
.replace(']', '-')
)


class EcoFlowDictEntity(EcoFlowAbstractEntity):
Expand All @@ -38,6 +47,7 @@ def __init__(self, client: EcoflowMQTTClient, mqtt_key: str, title: str, enabled
auto_enable: bool = False) -> object:
super().__init__(client, title, mqtt_key)
self._mqtt_key = mqtt_key
self._mqtt_key_expr = jp.parse(self.__adopted_mqtt_key())
self._auto_enable = auto_enable
self._attr_entity_registry_enabled_default = enabled
self.__attributes_mapping: dict[str, str] = {}
Expand All @@ -48,6 +58,12 @@ def attr(self, mqtt_key: str, title: str, default: Any) -> EcoFlowDictEntity:
self.__attrs[title] = default
return self

def __adopted_mqtt_key(self):
if self._client.is_flat_params():
return "'" + self._mqtt_key + "'"
else:
return self._mqtt_key

@property
def mqtt_key(self):
return self._mqtt_key
Expand All @@ -57,7 +73,7 @@ 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)
super().send_set_message({self.__adopted_mqtt_key(): target_value}, command)

@property
def enabled_default(self):
Expand All @@ -75,12 +91,14 @@ def _updated(self, data: dict[str, Any]):
self.__attrs[title] = data[key]

# update value
if self._mqtt_key in data:
values = self._mqtt_key_expr.find(data)
if len(values) == 1:
# 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]):
if self._update_value(values[0].value):
self.async_write_ha_state()

@property
Expand All @@ -93,12 +111,12 @@ def _update_value(self, val: Any) -> bool:

class EcoFlowBaseCommandEntity(EcoFlowDictEntity):
def __init__(self, client: EcoflowMQTTClient, mqtt_key: str, title: str,
command: Callable[[int, Optional[dict[str, Any]]], dict[str, Any]] | None,
command: Callable[[any, 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:
def command_dict(self, value: any) -> dict[str, any] | None:
if self._command:
p_count = len(inspect.signature(self._command).parameters)
if p_count == 1:
Expand Down
5 changes: 3 additions & 2 deletions custom_components/ecoflow_cloud/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,8 @@
"requirements": [
"paho-mqtt==1.6.1",
"reactivex==4.0.4",
"protobuf>=4.23.0"
"protobuf>=4.23.0",
"jsonpath-ng>=1.6.0"
],
"version": "0.12.4"
"version": "0.13.0-pre"
}
13 changes: 12 additions & 1 deletion custom_components/ecoflow_cloud/mqtt/ecoflow_mqtt.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,14 @@
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
import jsonpath_ng.ext as jp

_LOGGER = logging.getLogger(__name__)

FLAT_PARAMS = {
EcoflowModel.SMART_HOME_PANEL.name: False
}


class EcoflowException(Exception):
def __init__(self, *args, **kwargs):
Expand Down Expand Up @@ -139,7 +144,9 @@ def add_get_reply_message(self, msg: dict[str, Any]):
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)
# key can be xpath!
for key, value in target_state.items():
jp.parse(key).update(self.params, value)
self.__broadcast()

def update_data(self, raw: dict[str, Any]):
Expand Down Expand Up @@ -171,6 +178,7 @@ def __init__(self, hass: HomeAssistant, entry: ConfigEntry, auth: EcoflowAuthent
self.auth = auth
self.config_entry = entry
self.device_type = entry.data[CONF_DEVICE_TYPE]
self.flat_params = FLAT_PARAMS.get(self.device_type, True)
self.device_sn = entry.data[CONF_DEVICE_ID]

self._data_topic = f"/app/device/property/{self.device_sn}"
Expand Down Expand Up @@ -203,6 +211,9 @@ def __init__(self, hass: HomeAssistant, entry: ConfigEntry, auth: EcoflowAuthent
self.client.connect(self.auth.mqtt_url, self.auth.mqtt_port, 30)
self.client.loop_start()

def is_flat_params(self):
return self.flat_params

def is_connected(self):
return self.client.is_connected()

Expand Down
8 changes: 4 additions & 4 deletions custom_components/ecoflow_cloud/select.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,17 +20,17 @@ 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):
def __init__(self, client: EcoflowMQTTClient, mqtt_key: str, title: str, options: dict[str, any],
command: Callable[[any], 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]:
def options_dict(self) -> dict[str, any]:
return self.__options_dict

def _update_value(self, val: Any) -> bool:
ival = int(val)
ival = val
lval = [k for k, v in self.__options_dict.items() if v == ival]
if len(lval) == 1:
self._attr_current_option = lval[0]
Expand Down
2 changes: 1 addition & 1 deletion custom_components/ecoflow_cloud/sensor.py
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ class RemainSensorEntity(BaseSensorEntity):

def _update_value(self, val: Any) -> Any:
ival = int(val)
if ival < 0 or ival > 5000:
if ival < 0 or ival >= 5999:
ival = 0

return super()._update_value(ival)
Expand Down

0 comments on commit bac8c48

Please sign in to comment.