394 lines
14 KiB
Python
394 lines
14 KiB
Python
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()
|