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:
2026-05-02 14:24:53 +02:00
commit 5f01411780
149 changed files with 47886 additions and 0 deletions
+375
View File
@@ -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