Skip to content

Commit

Permalink
v0.10.0-pre3: status check and reconnects for all devices
Browse files Browse the repository at this point in the history
  • Loading branch information
tolwi committed Jul 4, 2023
1 parent 98e8950 commit 7b221bf
Show file tree
Hide file tree
Showing 12 changed files with 106 additions and 68 deletions.
5 changes: 3 additions & 2 deletions custom_components/ecoflow_cloud/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,11 @@
}

ATTR_STATUS_SN = "SN"
ATTR_STATUS_QUOTA_UPDATES = "quota_request_count"
ATTR_STATUS_QUOTA_LAST_UPDATE = "quota_last_update"
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):
Expand Down
3 changes: 2 additions & 1 deletion custom_components/ecoflow_cloud/devices/delta2_max.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
MaxGenStopLevelEntity, MinGenStartLevelEntity
from ..select import DictSelectEntity, TimeoutDictSelectEntity
from ..sensor import LevelSensorEntity, RemainSensorEntity, TempSensorEntity, CyclesSensorEntity, \
InWattsSensorEntity, OutWattsSensorEntity, VoltSensorEntity
InWattsSensorEntity, OutWattsSensorEntity, VoltSensorEntity, StatusSensorEntity
from ..switch import BeeperEntity, EnabledEntity


Expand Down Expand Up @@ -61,6 +61,7 @@ def sensors(self, client: EcoflowMQTTClient) -> list[BaseSensorEntity]:
# 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),

]

Expand Down
4 changes: 2 additions & 2 deletions custom_components/ecoflow_cloud/devices/delta_max.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
MaxGenStopLevelEntity, MinGenStartLevelEntity
from ..select import DictSelectEntity, TimeoutDictSelectEntity
from ..sensor import LevelSensorEntity, RemainSensorEntity, TempSensorEntity, CyclesSensorEntity, \
InWattsSensorEntity, OutWattsSensorEntity, VoltSensorEntity
InWattsSensorEntity, OutWattsSensorEntity, VoltSensorEntity, StatusSensorEntity
from ..switch import BeeperEntity, EnabledEntity


Expand Down Expand Up @@ -62,7 +62,7 @@ def sensors(self, client: EcoflowMQTTClient) -> list[BaseSensorEntity]:
#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]:
Expand Down
6 changes: 4 additions & 2 deletions custom_components/ecoflow_cloud/devices/delta_pro.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@
MaxGenStopLevelEntity
from ..select import DictSelectEntity, TimeoutDictSelectEntity
from ..sensor import LevelSensorEntity, WattsSensorEntity, RemainSensorEntity, TempSensorEntity, \
CyclesSensorEntity, InWattsSensorEntity, OutWattsSensorEntity, VoltSensorEntity, InWattsSolarSensorEntity
CyclesSensorEntity, InWattsSensorEntity, OutWattsSensorEntity, VoltSensorEntity, InWattsSolarSensorEntity, \
StatusSensorEntity
from ..switch import BeeperEntity, EnabledEntity


Expand Down Expand Up @@ -56,7 +57,8 @@ def sensors(self, client: EcoflowMQTTClient) -> list[BaseSensorEntity]:
LevelSensorEntity(client, "bmsSlave2.soc", const.SLAVE_2_BATTERY_LEVEL, False, True),
TempSensorEntity(client, "bmsSlave2.temp", const.SLAVE_2_BATTERY_TEMP, False, True),
WattsSensorEntity(client, "bmsSlave2.inputWatts", const.SLAVE_2_IN_POWER, False, True),
WattsSensorEntity(client, "bmsSlave2.outputWatts", const.SLAVE_2_OUT_POWER, False, True)
WattsSensorEntity(client, "bmsSlave2.outputWatts", const.SLAVE_2_OUT_POWER, False, True),
StatusSensorEntity(client),
]

def numbers(self, client: EcoflowMQTTClient) -> list[BaseNumberEntity]:
Expand Down
3 changes: 2 additions & 1 deletion custom_components/ecoflow_cloud/devices/river2.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
from ..number import ChargingPowerEntity, MaxBatteryLevelEntity, MinBatteryLevelEntity
from ..select import DictSelectEntity, TimeoutDictSelectEntity
from ..sensor import LevelSensorEntity, RemainSensorEntity, TempSensorEntity, \
CyclesSensorEntity, InWattsSensorEntity, OutWattsSensorEntity, VoltSensorEntity
CyclesSensorEntity, InWattsSensorEntity, OutWattsSensorEntity, VoltSensorEntity, StatusSensorEntity
from ..switch import EnabledEntity


Expand Down Expand Up @@ -44,6 +44,7 @@ def sensors(self, client: EcoflowMQTTClient) -> list[BaseSensorEntity]:
VoltSensorEntity(client, "bms_bmsStatus.maxCellVol", const.MAX_CELL_VOLT, False),

# FanSensorEntity(client, "bms_emsStatus.fanLevel", "Fan Level"),
StatusSensorEntity(client),

]

Expand Down
4 changes: 3 additions & 1 deletion custom_components/ecoflow_cloud/devices/river2_pro.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
from ..number import ChargingPowerEntity, MaxBatteryLevelEntity, MinBatteryLevelEntity
from ..select import DictSelectEntity, TimeoutDictSelectEntity
from ..sensor import LevelSensorEntity, RemainSensorEntity, TempSensorEntity, \
CyclesSensorEntity, InWattsSensorEntity, OutWattsSensorEntity, VoltSensorEntity
CyclesSensorEntity, InWattsSensorEntity, OutWattsSensorEntity, VoltSensorEntity, QuotasStatusSensorEntity
from ..switch import EnabledEntity


Expand Down Expand Up @@ -46,6 +46,8 @@ def sensors(self, client: EcoflowMQTTClient) -> list[BaseSensorEntity]:
VoltSensorEntity(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]:
Expand Down
5 changes: 3 additions & 2 deletions custom_components/ecoflow_cloud/devices/river_max.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
from ..number import MaxBatteryLevelEntity
from ..select import DictSelectEntity
from ..sensor import LevelSensorEntity, WattsSensorEntity, RemainSensorEntity, TempSensorEntity, \
CyclesSensorEntity, InWattsSensorEntity, OutWattsSensorEntity, VoltSensorEntity
CyclesSensorEntity, InWattsSensorEntity, OutWattsSensorEntity, VoltSensorEntity, StatusSensorEntity
from ..switch import EnabledEntity, BeeperEntity


Expand Down Expand Up @@ -47,7 +47,8 @@ def sensors(self, client: EcoflowMQTTClient) -> list[BaseSensorEntity]:
VoltSensorEntity(client, "bmsSlave1.minCellVol", const.MIN_CELL_VOLT, False),
VoltSensorEntity(client, "bmsSlave1.maxCellVol", const.MAX_CELL_VOLT, False),

CyclesSensorEntity(client, "bmsSlave1.cycles", const.SLAVE_CYCLES, False, True)
CyclesSensorEntity(client, "bmsSlave1.cycles", const.SLAVE_CYCLES, False, True),
StatusSensorEntity(client),
]

def numbers(self, client: EcoflowMQTTClient) -> list[BaseNumberEntity]:
Expand Down
4 changes: 3 additions & 1 deletion custom_components/ecoflow_cloud/devices/river_pro.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@
from ..number import MaxBatteryLevelEntity
from ..select import DictSelectEntity
from ..sensor import LevelSensorEntity, WattsSensorEntity, RemainSensorEntity, TempSensorEntity, \
CyclesSensorEntity, InWattsSensorEntity, OutWattsSensorEntity, InVoltSensorEntity, InAmpSensorEntity
CyclesSensorEntity, InWattsSensorEntity, OutWattsSensorEntity, InVoltSensorEntity, InAmpSensorEntity, \
StatusSensorEntity
from ..switch import EnabledEntity, BeeperEntity


Expand Down Expand Up @@ -33,6 +34,7 @@ def sensors(self, client: EcoflowMQTTClient) -> list[BaseSensorEntity]:
TempSensorEntity(client, "bmsMaster.temp", const.BATTERY_TEMP),

CyclesSensorEntity(client, "bmsMaster.cycles", const.CYCLES),
StatusSensorEntity(client),

]

Expand Down
2 changes: 1 addition & 1 deletion custom_components/ecoflow_cloud/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,5 +12,5 @@
"paho-mqtt==1.6.1",
"reactivex==4.0.4"
],
"version": "0.10.0-pre2"
"version": "0.10.0-pre3"
}
10 changes: 7 additions & 3 deletions custom_components/ecoflow_cloud/mqtt/ecoflow_mqtt.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
from homeassistant.core import HomeAssistant, DOMAIN
from homeassistant.helpers.entity import DeviceInfo
from homeassistant.util import utcnow
from homeassistant.util.dt import UTC
from reactivex import Subject, Observable

from .utils import BoundFifoList
Expand Down Expand Up @@ -107,7 +108,8 @@ def __init__(self, update_period_sec: int, collect_raw: bool = False):

self.raw_data = BoundFifoList[dict[str, Any]]()

self.__params_broadcast_time: datetime = utcnow()
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]]]()
Expand Down Expand Up @@ -142,6 +144,7 @@ def update_to_target_state(self, target_state: dict[str, Any]):

def update_data(self, raw: dict[str, Any]):
self.__add_raw_data(raw)
self.__params_time = datetime.fromtimestamp(raw['timestamp'], UTC)
self.params['timestamp'] = raw['timestamp']
self.params.update(raw['params'])

Expand All @@ -156,8 +159,9 @@ def __add_raw_data(self, raw: dict[str, Any]):
if self.__collect_raw:
self.raw_data.append(raw)

def last_params_broadcast_time(self) -> datetime:
return self.__params_broadcast_time
def params_time(self) -> datetime:
return self.__params_time


class EcoflowMQTTClient:

Expand Down
6 changes: 3 additions & 3 deletions custom_components/ecoflow_cloud/recorder.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
from homeassistant.core import callback, HomeAssistant

from custom_components.ecoflow_cloud import ATTR_STATUS_QUOTA_UPDATES, ATTR_STATUS_DATA_LAST_UPDATE, \
ATTR_STATUS_QUOTA_LAST_UPDATE
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_QUOTA_UPDATES, ATTR_STATUS_DATA_LAST_UPDATE, ATTR_STATUS_QUOTA_LAST_UPDATE}
return {ATTR_STATUS_UPDATES, ATTR_STATUS_DATA_LAST_UPDATE, ATTR_STATUS_LAST_UPDATE, ATTR_STATUS_PHASE}
122 changes: 73 additions & 49 deletions custom_components/ecoflow_cloud/sensor.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import math
from datetime import timedelta, datetime
from typing import Any, Mapping, OrderedDict

Expand All @@ -10,9 +11,10 @@
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, OPTS_REFRESH_PERIOD_SEC, ATTR_STATUS_SN, ATTR_STATUS_DATA_LAST_UPDATE, ATTR_STATUS_QUOTA_UPDATES, \
ATTR_STATUS_QUOTA_LAST_UPDATE, ATTR_STATUS_RECONNECTS
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
from .mqtt.ecoflow_mqtt import EcoflowMQTTClient

Expand Down Expand Up @@ -107,82 +109,104 @@ class InAmpSensorEntity(AmpSensorEntity):
_attr_icon = "mdi:transmission-tower-import"


class QuotasStatusSensorEntity(SensorEntity, EcoFlowAbstractEntity):
# _attr_icon = "mdi:transmission-tower-import"
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):
super().__init__(client, "Status", "status")
self._data_refresh_sec = int(client.config_entry.options[OPTS_REFRESH_PERIOD_SEC])
self.__online = -1
self.__last_quota_update = utcnow()
self.__attrs = OrderedDict[str, Any]()
self.__attrs[ATTR_STATUS_SN] = "Unknown"
self.__attrs[ATTR_STATUS_DATA_LAST_UPDATE] = None
self.__attrs[ATTR_STATUS_QUOTA_UPDATES] = 0
self.__attrs[ATTR_STATUS_QUOTA_LAST_UPDATE] = None
self.__attrs[ATTR_STATUS_RECONNECTS] = 0
self._online = 0
self.__check_interval_sec = 30
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()

get_reply_d = self._client.data.get_reply_observable().subscribe(self.__get_reply_update)
self.async_on_remove(get_reply_d.dispose)

params_d = self._client.data.params_observable().subscribe(self.__params_update)
self.async_on_remove(params_d.dispose)

self.__get_latest_quotas()

self.async_on_remove(
async_track_time_interval(self.hass, self.__check_latest_quotas, timedelta(seconds=self._data_refresh_sec)))
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()

def __check_latest_quotas(self, now: datetime):
update_delta_sec = (now - self._client.data.last_params_broadcast_time()).total_seconds()
data_after_quota = (self._client.data.last_params_broadcast_time() - self.__last_quota_update).total_seconds()
is_data_outdated = update_delta_sec > self._data_refresh_sec * 3
is_data_without_quota = data_after_quota > self._data_refresh_sec * 2
def __params_update(self, data: dict[str, Any]):
self._attrs[ATTR_STATUS_DATA_LAST_UPDATE] = datetime.fromtimestamp(data['timestamp'], UTC)
if self._online == 0:
self._update_status(0)

if self.__online == 1 and is_data_outdated:
# online and outdated - refresh quota to detect if device went offline
self.async_write_ha_state()

if self.__attrs[ATTR_STATUS_QUOTA_UPDATES] % 5 == 0:
# it is time to reconnect to recover data stream as device seems to be online after 5 status checks
self.__attrs[ATTR_STATUS_RECONNECTS] = self.__attrs[ATTR_STATUS_RECONNECTS] + 1
self._client.reconnect()
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.__get_latest_quotas()
elif self.__online != 1 and is_data_without_quota:
# offline but with incoming updates (refresh status)
self._attrs[ATTR_STATUS_LAST_UPDATE] = utcnow()
self._attrs[ATTR_STATUS_UPDATES] = self._attrs[ATTR_STATUS_UPDATES] + 1
self.async_write_ha_state()

self.__get_latest_quotas()
@property
def extra_state_attributes(self) -> Mapping[str, Any] | None:
return self._attrs

def __get_latest_quotas(self):
self.__attrs[ATTR_STATUS_QUOTA_UPDATES] = self.__attrs[ATTR_STATUS_QUOTA_UPDATES] + 1

self.send_get_message({"version": "1.1", "moduleType": 0, "operateType": "latestQuotas", "params": {}})
class QuotasStatusSensorEntity(StatusSensorEntity):
_attr_entity_category = EntityCategory.DIAGNOSTIC

def __params_update(self, data: dict[str, Any]):
self.__attrs[ATTR_STATUS_DATA_LAST_UPDATE] = datetime.fromtimestamp(data['timestamp'])
self.async_write_ha_state()
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):
self._attrs[ATTR_STATUS_UPDATES] = self._attrs[ATTR_STATUS_UPDATES] + 1
self.send_get_message({"version": "1.1", "moduleType": 0, "operateType": "latestQuotas", "params": {}})

def __get_reply_update(self, data: list[dict[str, Any]]):
d = data[0]
if d["operateType"] == "latestQuotas":
self.__online = d["data"]["online"]
self.__last_quota_update = utcnow()
self.__attrs[ATTR_STATUS_QUOTA_LAST_UPDATE] = self.__last_quota_update
self._online = d["data"]["online"]
self._attrs[ATTR_STATUS_LAST_UPDATE] = utcnow()

if self.__online == 1:
self.__attrs[ATTR_STATUS_SN] = d["data"]["sn"]
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.async_write_ha_state()

@property
def extra_state_attributes(self) -> Mapping[str, Any] | None:
return self.__attrs

0 comments on commit 7b221bf

Please sign in to comment.