Files
homeassistant_config/config/custom_components/ecoflow_cloud/sensor.py
2024-05-31 13:07:35 +02:00

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()