Home Assistant Git Exporter
This commit is contained in:
393
config/custom_components/ecoflow_cloud/sensor.py
Normal file
393
config/custom_components/ecoflow_cloud/sensor.py
Normal 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()
|
||||
Reference in New Issue
Block a user