Home Assistant Git Exporter
This commit is contained in:
285
config/custom_components/ecoflow/__init__.py
Normal file
285
config/custom_components/ecoflow/__init__.py
Normal file
@@ -0,0 +1,285 @@
|
||||
from datetime import timedelta
|
||||
from typing import Any, Callable, Optional, TypeVar, cast
|
||||
|
||||
import reactivex.operators as ops
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import CONF_HOST, CONF_MAC, Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import event
|
||||
from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC
|
||||
from homeassistant.helpers.device_registry import async_get as async_get_dr
|
||||
from homeassistant.helpers.entity import DeviceInfo, Entity, EntityCategory
|
||||
from homeassistant.util.dt import utcnow
|
||||
from reactivex import Observable, Subject, compose, throw
|
||||
from reactivex.subject.replaysubject import ReplaySubject
|
||||
|
||||
from . import ecoflow as ef
|
||||
from .ecoflow import receive
|
||||
from .ecoflow.rxtcp import RxTcpAutoConnection
|
||||
|
||||
CONF_PRODUCT = "product"
|
||||
DISCONNECT_TIME = timedelta(seconds=15)
|
||||
DOMAIN = "ecoflow"
|
||||
|
||||
_PLATFORMS = {
|
||||
Platform.BINARY_SENSOR,
|
||||
Platform.LIGHT,
|
||||
Platform.NUMBER,
|
||||
Platform.SELECT,
|
||||
Platform.SENSOR,
|
||||
Platform.SWITCH,
|
||||
}
|
||||
|
||||
_T = TypeVar("_T")
|
||||
|
||||
|
||||
async def to_task(src: Observable[_T]):
|
||||
return await src
|
||||
|
||||
|
||||
async def request(tcp: RxTcpAutoConnection, req: bytes, res: Observable[_T]) -> _T:
|
||||
t = to_task(res.pipe(
|
||||
ops.timeout(5, throw(TimeoutError())),
|
||||
ops.first(),
|
||||
))
|
||||
try:
|
||||
tcp.write(req)
|
||||
except BaseException as ex:
|
||||
t.close()
|
||||
raise ex
|
||||
return await t
|
||||
|
||||
|
||||
def select_bms(idx: int):
|
||||
return compose(
|
||||
ops.filter(lambda x: x[0] == idx),
|
||||
ops.map(lambda x: cast(dict[str, Any], x[1])),
|
||||
)
|
||||
|
||||
|
||||
class HassioEcoFlowClient:
|
||||
__disconnected = None
|
||||
__extra_connected = False
|
||||
|
||||
def __init__(self, hass: HomeAssistant, entry: ConfigEntry):
|
||||
self.tcp = RxTcpAutoConnection(entry.data[CONF_HOST], ef.PORT)
|
||||
self.product: int = entry.data[CONF_PRODUCT]
|
||||
self.serial = entry.unique_id
|
||||
self.diagnostics = dict[str, dict[str, Any]]()
|
||||
dr = async_get_dr(hass)
|
||||
|
||||
self.device_info_main = DeviceInfo(
|
||||
identifiers={(DOMAIN, self.serial)},
|
||||
manufacturer="EcoFlow",
|
||||
name=entry.title,
|
||||
)
|
||||
if mac := entry.data.get(CONF_MAC, None):
|
||||
self.device_info_main["connections"] = {
|
||||
(CONNECTION_NETWORK_MAC, mac),
|
||||
}
|
||||
|
||||
self.received = self.tcp.received.pipe(
|
||||
receive.merge_packet(),
|
||||
ops.map(receive.decode_packet),
|
||||
ops.share(),
|
||||
)
|
||||
self.pd = self.received.pipe(
|
||||
ops.filter(receive.is_pd),
|
||||
ops.map(lambda x: receive.parse_pd(x[3], self.product)),
|
||||
ops.multicast(subject=ReplaySubject(1, DISCONNECT_TIME)),
|
||||
ops.ref_count(),
|
||||
)
|
||||
self.ems = self.received.pipe(
|
||||
ops.filter(receive.is_ems),
|
||||
ops.map(lambda x: receive.parse_ems(x[3], self.product)),
|
||||
ops.multicast(subject=ReplaySubject(1, DISCONNECT_TIME)),
|
||||
ops.ref_count(),
|
||||
)
|
||||
self.inverter = self.received.pipe(
|
||||
ops.filter(receive.is_inverter),
|
||||
ops.map(lambda x: receive.parse_inverter(x[3], self.product)),
|
||||
ops.multicast(subject=ReplaySubject(1, DISCONNECT_TIME)),
|
||||
ops.ref_count(),
|
||||
)
|
||||
self.mppt = self.received.pipe(
|
||||
ops.filter(receive.is_mppt),
|
||||
ops.map(lambda x: receive.parse_mppt(x[3], self.product)),
|
||||
ops.multicast(subject=ReplaySubject(1, DISCONNECT_TIME)),
|
||||
ops.ref_count(),
|
||||
)
|
||||
self.bms = self.received.pipe(
|
||||
ops.filter(receive.is_bms),
|
||||
ops.map(lambda x: receive.parse_bms(x[3], self.product)),
|
||||
ops.multicast(subject=ReplaySubject(1, DISCONNECT_TIME)),
|
||||
ops.ref_count(),
|
||||
)
|
||||
|
||||
self.dc_in_current_config = self.received.pipe(
|
||||
ops.filter(receive.is_dc_in_current_config),
|
||||
ops.map(lambda x: receive.parse_dc_in_current_config(x[3])),
|
||||
)
|
||||
self.dc_in_type = self.received.pipe(
|
||||
ops.filter(receive.is_dc_in_type),
|
||||
ops.map(lambda x: receive.parse_dc_in_type(x[3])),
|
||||
)
|
||||
self.fan_auto = self.received.pipe(
|
||||
ops.filter(receive.is_fan_auto),
|
||||
ops.map(lambda x: receive.parse_fan_auto(x[3])),
|
||||
)
|
||||
self.lcd_timeout = self.received.pipe(
|
||||
ops.filter(receive.is_lcd_timeout),
|
||||
ops.map(lambda x: receive.parse_lcd_timeout(x[3])),
|
||||
)
|
||||
|
||||
self.disconnected = Subject[Optional[int]]()
|
||||
|
||||
def _disconnected(*args):
|
||||
self.__disconnected = None
|
||||
self.tcp.reconnect()
|
||||
self.diagnostics.clear()
|
||||
self.disconnected.on_next(None)
|
||||
if self.__extra_connected:
|
||||
self.__extra_connected = False
|
||||
|
||||
def reset_timer(*args):
|
||||
if self.__disconnected:
|
||||
self.__disconnected()
|
||||
self.__disconnected = event.async_track_point_in_utc_time(
|
||||
hass,
|
||||
_disconnected,
|
||||
utcnow().replace(microsecond=0) + (DISCONNECT_TIME + timedelta(seconds=1)),
|
||||
)
|
||||
|
||||
def end_timer(ex=None):
|
||||
self.disconnected.on_next(None)
|
||||
if ex:
|
||||
self.disconnected.on_error(ex)
|
||||
else:
|
||||
self.disconnected.on_completed()
|
||||
self.received.subscribe(reset_timer, end_timer, end_timer)
|
||||
|
||||
def pd_updated(data: dict[str, Any]):
|
||||
self.diagnostics["pd"] = data
|
||||
self.device_info_main["model"] = ef.get_model_name(
|
||||
self.product, data["model"])
|
||||
dr.async_get_or_create(
|
||||
config_entry_id=entry.entry_id,
|
||||
**self.device_info_main,
|
||||
)
|
||||
if self.__extra_connected != ef.has_extra(self.product, data.get("model", None)):
|
||||
self.__extra_connected = not self.__extra_connected
|
||||
if not self.__extra_connected:
|
||||
self.disconnected.on_next(1)
|
||||
self.pd.subscribe(pd_updated)
|
||||
|
||||
def bms_updated(data: tuple[int, dict[str, Any]]):
|
||||
if "bms" not in self.diagnostics:
|
||||
self.diagnostics["bms"] = dict[str, Any]()
|
||||
self.diagnostics["bms"][data[0]] = data[1]
|
||||
self.bms.subscribe(bms_updated)
|
||||
|
||||
def ems_updated(data: dict[str, Any]):
|
||||
self.diagnostics["ems"] = data
|
||||
self.ems.subscribe(ems_updated)
|
||||
|
||||
def inverter_updated(data: dict[str, Any]):
|
||||
self.diagnostics["inverter"] = data
|
||||
self.inverter.subscribe(inverter_updated)
|
||||
|
||||
def mppt_updated(data: dict[str, Any]):
|
||||
self.diagnostics["mppt"] = data
|
||||
self.mppt.subscribe(mppt_updated)
|
||||
|
||||
async def close(self):
|
||||
self.tcp.close()
|
||||
await self.tcp.wait_closed()
|
||||
|
||||
|
||||
class EcoFlowBaseEntity(Entity):
|
||||
_attr_has_entity_name = True
|
||||
_attr_should_poll = False
|
||||
_connected = False
|
||||
|
||||
def __init__(self, client: HassioEcoFlowClient, bms_id: Optional[int] = None):
|
||||
self._attr_available = False
|
||||
self._client = client
|
||||
self._bms_id = bms_id or 0
|
||||
self._attr_device_info = client.device_info_main
|
||||
self._attr_unique_id = client.serial
|
||||
if bms_id:
|
||||
self._attr_unique_id += f"-{bms_id}"
|
||||
|
||||
async def async_added_to_hass(self):
|
||||
await super().async_added_to_hass()
|
||||
self._subscribe(self._client.disconnected, self.__on_disconnected)
|
||||
|
||||
def _subscribe(self, src: Observable, func: Callable):
|
||||
self.async_on_remove(src.subscribe(func).dispose)
|
||||
|
||||
def __on_disconnected(self, bms_id: Optional[int]):
|
||||
if bms_id is not None and self._bms_id != bms_id:
|
||||
return
|
||||
self._connected = False
|
||||
if self._attr_available:
|
||||
self._attr_available = False
|
||||
self.async_write_ha_state()
|
||||
|
||||
|
||||
class EcoFlowEntity(EcoFlowBaseEntity):
|
||||
def __init__(self, client: HassioEcoFlowClient, src: Observable[dict[str, Any]], key: str, name: str, bms_id: Optional[int] = None):
|
||||
super().__init__(client, bms_id)
|
||||
self._key = key
|
||||
self._src = src
|
||||
self._attr_name = name
|
||||
self._attr_unique_id += f"-{key.replace('_', '-')}"
|
||||
|
||||
async def async_added_to_hass(self):
|
||||
await super().async_added_to_hass()
|
||||
self._subscribe(self._src, self.__updated)
|
||||
|
||||
def __updated(self, data: dict[str, Any]):
|
||||
self._attr_available = True
|
||||
self._on_updated(data)
|
||||
self.async_write_ha_state()
|
||||
|
||||
def _on_updated(self, data: dict[str, Any]):
|
||||
pass
|
||||
|
||||
|
||||
class EcoFlowConfigEntity(EcoFlowBaseEntity):
|
||||
_attr_entity_category = EntityCategory.CONFIG
|
||||
_attr_should_poll = True
|
||||
|
||||
def __init__(self, client: HassioEcoFlowClient, key: str, name: str):
|
||||
super().__init__(client)
|
||||
self._attr_name = name
|
||||
self._attr_unique_id += f"-{key.replace('_', '-')}"
|
||||
|
||||
async def async_added_to_hass(self):
|
||||
await super().async_added_to_hass()
|
||||
self._subscribe(self._client.received, self.__updated)
|
||||
|
||||
def __updated(self, data):
|
||||
if not self._connected:
|
||||
self._connected = True
|
||||
self.async_schedule_update_ha_state(True)
|
||||
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry):
|
||||
if DOMAIN not in hass.data:
|
||||
hass.data[DOMAIN] = {}
|
||||
|
||||
client = HassioEcoFlowClient(hass, entry)
|
||||
|
||||
hass.data[DOMAIN][entry.entry_id] = client
|
||||
hass.config_entries.async_setup_platforms(entry, _PLATFORMS)
|
||||
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: HassioEcoFlowClient = hass.data[DOMAIN].pop(entry.entry_id)
|
||||
await client.close()
|
||||
return True
|
||||
Reference in New Issue
Block a user