chore: initial baseline before refactoring
Initial snapshot of Home Assistant config (HAOS 2026.4.4)
prior to dashboard restructuring and helper YAML migration.
Pre-commit cleanup applied:
- Removed blueprints/switch_manager/ (9.4 MB stale, component already gone)
- Removed .storage/switch_manager
- Removed home-assistant.log.{1,old,fault}
- Removed zigbee2mqtt/configuration_backup_v{1..4}.yaml
- Removed zigbee2mqtt/database.db.tmp.2024-09-27 (1.5y old)
- Created empty themes/ to satisfy configuration.yaml include
.gitignore uses allowlist strategy for .storage/ to keep
all tokens, keys, and PINs out of version control.
This commit is contained in:
@@ -0,0 +1,375 @@
|
||||
from datetime import timedelta
|
||||
from collections import defaultdict
|
||||
|
||||
from homeassistant.components.sensor import SensorEntity
|
||||
from homeassistant.components.sensor import SensorDeviceClass
|
||||
from homeassistant.components.sensor import SensorStateClass
|
||||
from homeassistant.helpers.entity import EntityCategory
|
||||
from homeassistant.helpers.event import async_track_time_interval
|
||||
from homeassistant.helpers import entity_registry
|
||||
from homeassistant.exceptions import IntegrationError
|
||||
|
||||
from .const import (
|
||||
DOMAIN,
|
||||
_LOGGER,
|
||||
ATTR_PRODUCT_DESCRIPTION,
|
||||
ATTR_DESTINATION_NAME,
|
||||
ATTR_SOURCE_NAME,
|
||||
ATTR_UNIQUE_ID,
|
||||
ATTR_PRODUCT_SERIAL,
|
||||
ATTR_PRODUCT_NAME,
|
||||
ATTR_PRODUCT_VENDOR,
|
||||
ATTR_PRODUCT_BUILD,
|
||||
ATTR_PRODUCT_VERSION,
|
||||
ATTR_PRODUCT_FEATURES,
|
||||
ISSUE_URL_ERROR_MESSAGE,
|
||||
)
|
||||
|
||||
from .ecoflow import Ecoflow, AuthenticationFailed
|
||||
|
||||
|
||||
# Setting up the adding and updating of sensor entities
|
||||
async def async_setup_entry(hass, config_entry, async_add_entities):
|
||||
# Retrieve the API instance from the config_entry data
|
||||
ecoflow = hass.data[DOMAIN][config_entry.entry_id]
|
||||
device_id = ecoflow.device["serial"]
|
||||
|
||||
# Call EcoFlow to get access to the API data
|
||||
try:
|
||||
auth_check = await hass.async_add_executor_job(ecoflow.authorize)
|
||||
|
||||
if not auth_check:
|
||||
# If device returns False or is empty, log an error and return
|
||||
_LOGGER.warning(
|
||||
f"{device_id}: It appears the PowerOcean device is offline or has changed host."
|
||||
+ ISSUE_URL_ERROR_MESSAGE
|
||||
)
|
||||
|
||||
except AuthenticationFailed as error:
|
||||
_LOGGER.warning(f"{device_id}: Authentication failed: {error}")
|
||||
return
|
||||
|
||||
try:
|
||||
# Fetch the sensor data from the device
|
||||
data = await hass.async_add_executor_job(ecoflow.fetch_data)
|
||||
|
||||
if not data:
|
||||
# If data returns False or is empty, log an error and return
|
||||
_LOGGER.warning(
|
||||
f"{device_id}: Failed to fetch sensor data => authentication failed or no data."
|
||||
+ ISSUE_URL_ERROR_MESSAGE
|
||||
)
|
||||
return
|
||||
|
||||
# Exception if data cannot be fetched
|
||||
except IntegrationError as error:
|
||||
_LOGGER.warning(
|
||||
f"{device_id}: Failed to fetch sensor data: {error}"
|
||||
+ ISSUE_URL_ERROR_MESSAGE
|
||||
)
|
||||
return
|
||||
|
||||
# Get device id and then reset the device specific list of sensors for updates
|
||||
# to ensure it's empty before adding new entries
|
||||
|
||||
# Initialize or clear the sensor list for this device
|
||||
hass.data[DOMAIN]["device_specific_sensors"][device_id] = []
|
||||
|
||||
# Register entities and add them to the list for schedule updates on each device
|
||||
# which is stored within hass.data
|
||||
for unique_id, endpoint in data.items():
|
||||
# Get individual sensor entry from API
|
||||
sensor = PowerOceanSensor(ecoflow, endpoint)
|
||||
|
||||
# Add sensors to the device specific list of sensors to be updated, via hass.data as also used in unload
|
||||
hass.data[DOMAIN]["device_specific_sensors"][device_id].append(sensor)
|
||||
|
||||
# Register sensor
|
||||
async_add_entities([sensor], False)
|
||||
|
||||
device_specific_sensors = hass.data[DOMAIN]["device_specific_sensors"]
|
||||
_LOGGER.debug(
|
||||
f"{device_id}: List of device_specific_sensors[device_id]: "
|
||||
f"{device_specific_sensors[device_id]}"
|
||||
)
|
||||
|
||||
# Log the number of sensors registered (and added to the update list)
|
||||
_LOGGER.debug(
|
||||
f"{device_id}: All '{len(device_specific_sensors[device_id])}' sensors have registered."
|
||||
)
|
||||
|
||||
# Schedule updates
|
||||
async def async_update_data(now):
|
||||
# If device deleted but HASS not restarted, then don't bother continuing
|
||||
if device_id not in hass.data.get(DOMAIN, {}).get(
|
||||
"device_specific_sensors", {}
|
||||
):
|
||||
return False
|
||||
|
||||
_LOGGER.debug(f"{device_id}: Preparing to update sensors at {now}")
|
||||
|
||||
# Fetch the full dataset once from the API
|
||||
try:
|
||||
full_data = await hass.async_add_executor_job(ecoflow.fetch_data)
|
||||
|
||||
except Exception as e:
|
||||
_LOGGER.error(
|
||||
f"{device_id}: Error fetching data from the device: {e}"
|
||||
+ ISSUE_URL_ERROR_MESSAGE
|
||||
)
|
||||
return
|
||||
|
||||
# Fetch the registry and check if sensors are enabled
|
||||
registry = entity_registry.async_get(hass)
|
||||
|
||||
# Set counters to zero
|
||||
counter_updated = 0 # Successfully updated sensors
|
||||
counter_disabled = 0 # Disabled sensors, not to be updated
|
||||
counter_unchanged = 0 # Skipped sensors since value has not changed
|
||||
counter_error = 0 # Skipped sensors due to some error, such as registry not found or no data from API
|
||||
|
||||
# Get the list of device specific sensors from hass.data
|
||||
if device_id in hass.data.get(DOMAIN, {}).get("device_specific_sensors", {}):
|
||||
device_specific_sensors = hass.data[DOMAIN]["device_specific_sensors"]
|
||||
|
||||
# ----------------------------------------------
|
||||
# Now loop through the sensors to be updated
|
||||
# ----------------------------------------------
|
||||
for sensor in device_specific_sensors[device_id]:
|
||||
entity_id = registry.async_get_entity_id(
|
||||
"sensor", DOMAIN, sensor.unique_id
|
||||
)
|
||||
if entity_id:
|
||||
entity = registry.entities.get(entity_id) # get entity
|
||||
# entity is enabled
|
||||
if entity and not entity.disabled_by:
|
||||
sensor_data = full_data.get(sensor.unique_id)
|
||||
# _LOGGER.debug(f"{device_id}: Sensor {sensor.name} enabled.")
|
||||
if sensor_data:
|
||||
# _LOGGER.debug(
|
||||
# f"{device_id}: Sensor {sensor.name} has API data to update {sensor_data}"
|
||||
# )
|
||||
|
||||
# Check if current state value differs from new API value,
|
||||
# or current state has not initialized
|
||||
if (
|
||||
str(sensor._state).strip()
|
||||
!= str(sensor_data.value).strip()
|
||||
):
|
||||
# _LOGGER.debug(
|
||||
# f"{device_id}: Sensor {sensor.name} marked for update: current state = "
|
||||
# f"{sensor._state} with new value = {sensor_data.value}"
|
||||
# )
|
||||
# Now update the sensor with new values
|
||||
# update_status returns 1 for upated, 0 for skipped or error
|
||||
update_status = await sensor.async_update(sensor_data)
|
||||
counter_updated = counter_updated + update_status
|
||||
else:
|
||||
# _LOGGER.debug(
|
||||
# f"{device_id}: Sensor {sensor.name} skipped update! Current value = "
|
||||
# f"{sensor._state}, new value = {sensor_data.value}"
|
||||
# )
|
||||
counter_unchanged = counter_unchanged + 1
|
||||
else:
|
||||
_LOGGER.warning(
|
||||
f"{device_id}: Sensor {sensor.name}: found no data for update!"
|
||||
+ ISSUE_URL_ERROR_MESSAGE
|
||||
)
|
||||
counter_error = counter_error + 1
|
||||
else:
|
||||
# _LOGGER.debug(
|
||||
# f"{device_id}: Sensor {sensor.name} is disabled, skipping update"
|
||||
# )
|
||||
counter_disabled = counter_disabled + 1
|
||||
else:
|
||||
_LOGGER.warning(
|
||||
f"{device_id}: Sensor {sensor.name} not found in the registry, skipping update"
|
||||
+ ISSUE_URL_ERROR_MESSAGE
|
||||
)
|
||||
counter_error = counter_error + 1
|
||||
|
||||
# Log summary of updates
|
||||
_LOGGER.debug(
|
||||
f"{device_id}: A total of {counter_updated} sensors have been updated. "
|
||||
f"Number of disabled sensors or skipped updates = {counter_disabled} "
|
||||
f"Number of sensors with constant values = {counter_unchanged} "
|
||||
f"Number of sensors with errors = {counter_error}"
|
||||
)
|
||||
|
||||
# Device not in list: must have been deleted, will resolve post re-start
|
||||
else:
|
||||
_LOGGER.warning(
|
||||
f"{device_id}: Sensor must have been deleted, re-start of HA recommended."
|
||||
)
|
||||
|
||||
# Get the polling interval from the options, defaulting to 5 seconds if not set
|
||||
polling_interval = timedelta(
|
||||
seconds=config_entry.options.get("polling_interval", 5)
|
||||
)
|
||||
|
||||
async_track_time_interval(hass, async_update_data, polling_interval)
|
||||
|
||||
|
||||
# This is the actual instance of SensorEntity class
|
||||
class PowerOceanSensor(SensorEntity):
|
||||
"""Representation of a PowerOcean Sensor."""
|
||||
|
||||
def __init__(self, ecoflow: Ecoflow, endpoint):
|
||||
"""Initialize the sensor."""
|
||||
# Make Ecoflow and the endpoint parameters from the Sensor API available
|
||||
self.ecoflow = ecoflow
|
||||
self.endpoint = endpoint
|
||||
|
||||
# Set Friendly name when sensor is first created
|
||||
self._attr_unique_id = endpoint.name
|
||||
self._attr_has_entity_name = True
|
||||
self._attr_name = endpoint.friendly_name
|
||||
self._name = endpoint.friendly_name
|
||||
|
||||
# The unique identifier for this sensor within Home Assistant
|
||||
# has nothing to do with the entity_id, it is the internal unique_id of the sensor entity registry
|
||||
self._unique_id = endpoint.internal_unique_id
|
||||
|
||||
# Set the icon for the sensor based on its unit, ensure the icon_mapper is defined
|
||||
# Default handled in function
|
||||
# self._icon = PowerOceanSensor.icon_mapper.get(endpoint.unit)
|
||||
self._icon = endpoint.icon
|
||||
|
||||
# The initial state/value of the sensor
|
||||
self._state = endpoint.value
|
||||
|
||||
# The unit of measurement for the sensor
|
||||
self._unit = endpoint.unit
|
||||
|
||||
# Set entity category to diagnostic for sensors with no unit
|
||||
if ecoflow.options.get("group_sensors") and not endpoint.unit:
|
||||
self._attr_entity_category = EntityCategory.DIAGNOSTIC
|
||||
|
||||
# If diagnostics entity then disable sensor by default
|
||||
if ecoflow.options.get("disable_sensors") and not endpoint.unit:
|
||||
self._attr_entity_registry_enabled_default = False
|
||||
|
||||
@property
|
||||
def should_poll(self):
|
||||
"""async_track_time_intervals handles updates."""
|
||||
return False
|
||||
|
||||
@property
|
||||
def unique_id(self):
|
||||
"""Return the unique ID of the sensor."""
|
||||
return self._unique_id
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the sensor."""
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def state(self):
|
||||
"""Return the state of the sensor."""
|
||||
return self._state
|
||||
|
||||
@property
|
||||
def unit_of_measurement(self):
|
||||
"""Return the unit of measurement."""
|
||||
return self._unit
|
||||
|
||||
@property
|
||||
def device_class(self):
|
||||
"""Return the device class of this entity, if any."""
|
||||
if self._unit == "°C":
|
||||
return SensorDeviceClass.TEMPERATURE
|
||||
elif self._unit == "%":
|
||||
return SensorDeviceClass.BATTERY
|
||||
elif self._unit in {"Wh", "kWh"}:
|
||||
return SensorDeviceClass.ENERGY
|
||||
elif self._unit == "W":
|
||||
return SensorDeviceClass.POWER
|
||||
elif self._unit == "V":
|
||||
return SensorDeviceClass.VOLTAGE
|
||||
elif self._unit == "A":
|
||||
return SensorDeviceClass.CURRENT
|
||||
else:
|
||||
return None
|
||||
|
||||
@property
|
||||
def state_class(self):
|
||||
"""Return the state class of this entity, if any."""
|
||||
if self._unit in {"°C", "h", "W", "V", "A"}:
|
||||
return SensorStateClass.MEASUREMENT
|
||||
elif self._unit in {"Wh", "kWh"}:
|
||||
return SensorStateClass.TOTAL_INCREASING
|
||||
else:
|
||||
return None
|
||||
|
||||
@property
|
||||
def extra_state_attributes(self):
|
||||
"""Return the state attributes of this device."""
|
||||
attr = {}
|
||||
|
||||
attr[ATTR_PRODUCT_DESCRIPTION] = self.endpoint.description
|
||||
attr[ATTR_UNIQUE_ID] = self.endpoint.internal_unique_id
|
||||
attr[ATTR_PRODUCT_VENDOR] = self.ecoflow.device["vendor"]
|
||||
attr[ATTR_PRODUCT_NAME] = self.ecoflow.device["name"]
|
||||
attr[ATTR_PRODUCT_SERIAL] = self.endpoint.serial
|
||||
attr[ATTR_PRODUCT_VERSION] = self.ecoflow.device["version"]
|
||||
attr[ATTR_PRODUCT_BUILD] = self.ecoflow.device["build"]
|
||||
attr[ATTR_PRODUCT_FEATURES] = self.ecoflow.device["features"]
|
||||
|
||||
return attr
|
||||
|
||||
@property
|
||||
def device_info(self):
|
||||
"""Return device specific attributes."""
|
||||
# The unique identifier of the device is the serial number
|
||||
return {
|
||||
"identifiers": {(DOMAIN, self.ecoflow.device["serial"])},
|
||||
"name": self.ecoflow.device["name"],
|
||||
"manufacturer": "ECOFLOW",
|
||||
}
|
||||
|
||||
@property
|
||||
def icon(self):
|
||||
"""Return the icon of the sensor."""
|
||||
return self._icon
|
||||
|
||||
# icon_mapper = defaultdict(
|
||||
# lambda: "mdi:alert-circle",
|
||||
# {
|
||||
# "°C": "mdi:thermometer",
|
||||
# "%": "mdi:flash",
|
||||
# "s": "mdi:timer",
|
||||
# "Wh": "mdi:solar-power-variant-outline",
|
||||
# "h": "mdi:timer-sand",
|
||||
# },
|
||||
# )
|
||||
|
||||
# This is to register the icon settings
|
||||
async def async_added_to_hass(self):
|
||||
"""Call when the sensor is added to Home Assistant."""
|
||||
self.async_write_ha_state()
|
||||
|
||||
# Update of Sensor values
|
||||
async def async_update(self, sensor_data=None):
|
||||
"""Update the sensor with the provided data."""
|
||||
if sensor_data is None:
|
||||
_LOGGER.warning(
|
||||
f"{self.ecoflow.device['serial']}: No new data provided for sensor '{self.name}' update"
|
||||
+ ISSUE_URL_ERROR_MESSAGE
|
||||
)
|
||||
update_status = 0
|
||||
return
|
||||
|
||||
try:
|
||||
self._state = sensor_data.value
|
||||
update_status = 1
|
||||
self.async_write_ha_state()
|
||||
|
||||
except Exception as error:
|
||||
_LOGGER.error(
|
||||
f"{self.ecoflow.device['serial']}: Error updating sensor {self.name}: {error}"
|
||||
+ ISSUE_URL_ERROR_MESSAGE
|
||||
)
|
||||
update_status = 0
|
||||
|
||||
return update_status
|
||||
Reference in New Issue
Block a user