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,189 @@
|
||||
"""config_flow.py: Config flow for PowerOcean integration."""
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
from typing import Any
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant import config_entries
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.data_entry_flow import FlowResult
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.exceptions import IntegrationError
|
||||
|
||||
from .const import _LOGGER, DOMAIN, ISSUE_URL_ERROR_MESSAGE
|
||||
from .ecoflow import Ecoflow, AuthenticationFailed
|
||||
|
||||
|
||||
# This is the first step's schema when setting up the integration, or its devices
|
||||
# The second schema is defined inside the ConfigFlow class as it has dynamic default values set via API call
|
||||
STEP_USER_DATA_SCHEMA = vol.Schema(
|
||||
{
|
||||
vol.Required("serialnumber", default=""): str,
|
||||
vol.Required("username", default=""): str,
|
||||
vol.Required("password", default=""): str,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
async def validate_input_for_device(hass: HomeAssistant, data: dict[str, Any]) -> dict[str, Any]:
|
||||
"""Validate the user input allows us to connect."""
|
||||
|
||||
ecoflow = Ecoflow(data["serialnumber"], data["username"], data["password"])
|
||||
|
||||
try:
|
||||
# Check for authentication
|
||||
# auth_check = await hass.async_add_executor_job(ecoflow.fetch_data) # TODO what else is needed from fetch_data?
|
||||
auth_check = await hass.async_add_executor_job(ecoflow.authorize)
|
||||
if not auth_check:
|
||||
# If authentication check returns False, raise an authentication failure exception
|
||||
raise AuthenticationFailed("Invalid authentication!")
|
||||
|
||||
# Get device info
|
||||
device = await hass.async_add_executor_job(ecoflow.get_device)
|
||||
|
||||
# Return the device object with the device information
|
||||
return device
|
||||
|
||||
# Exception if device cannot be found
|
||||
except IntegrationError as e:
|
||||
_LOGGER.error(f"Failed to connect to PowerOcean device: {e}" + ISSUE_URL_ERROR_MESSAGE)
|
||||
raise CannotConnect from e
|
||||
|
||||
# Exception if authentication fails
|
||||
except AuthenticationFailed as e:
|
||||
_LOGGER.error(f"Authentication failed: {e}" + ISSUE_URL_ERROR_MESSAGE)
|
||||
raise InvalidAuth from e
|
||||
|
||||
|
||||
class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
"""Handle a config flow for PowerOcean."""
|
||||
|
||||
VERSION = 1.3
|
||||
|
||||
# Make sure user input data is passed from one step to the next using user_input_from_step_user
|
||||
def __init__(self):
|
||||
self.user_input_from_step_user = None
|
||||
|
||||
# This is step 1 for the host/port/user/pass function.
|
||||
async def async_step_user(self, user_input: dict[str, Any] | None = None) -> FlowResult:
|
||||
"""Handle the initial step."""
|
||||
errors: dict[str, str] = {}
|
||||
if user_input is not None:
|
||||
try:
|
||||
# Valide the user input whilst setting up integration or adding new devices.
|
||||
# validate_input_for_devices will try to detect the device and get more info from it,
|
||||
# and authenticate and deal with exceptions
|
||||
device = await validate_input_for_device(self.hass, user_input)
|
||||
except CannotConnect:
|
||||
errors["base"] = "cannot_connect"
|
||||
except InvalidAuth:
|
||||
errors["base"] = "invalid_auth"
|
||||
return self.async_show_form(step_id="user", data_schema=STEP_USER_DATA_SCHEMA, errors=errors)
|
||||
except Exception:
|
||||
_LOGGER.exception("Unexpected exception")
|
||||
errors["base"] = "unknown"
|
||||
else:
|
||||
# Create a unique device id as a combination of the device's product and its serial number (which should be unique on its own)
|
||||
# The host can be udpated if necessary, as the IP address of the device may have changed
|
||||
unique_id = f"{device['product']}_{device['serial']}"
|
||||
|
||||
# Checks that the device is actually unique, otherwise abort
|
||||
await self.async_set_unique_id(unique_id)
|
||||
# self._abort_if_unique_id_configured(
|
||||
# updates={"host": user_input["host"]}
|
||||
# )
|
||||
|
||||
# Before creating the entry in the config_entry registry, go to step 2 for the options
|
||||
# However, make sure the steps from the user input are passed on to the next step
|
||||
self.user_input_from_step_user = user_input
|
||||
self.device_info = device
|
||||
|
||||
# Now call the second step but set user_input to None for the first time to force data entry in step 2
|
||||
return await self.async_step_device_options(user_input=None)
|
||||
|
||||
# Show the form for step 1 with the user/host/pass as defined in STEP_USER_DATA_SCHEMA
|
||||
return self.async_show_form(step_id="user", data_schema=STEP_USER_DATA_SCHEMA, errors=errors)
|
||||
|
||||
# This is step 2 for the options such as custom name, group and disable sensors
|
||||
async def async_step_device_options(self, user_input: dict[str, Any] | None = None,) -> FlowResult:
|
||||
"""Handle the device options step."""
|
||||
errors: dict[str, str] = {}
|
||||
|
||||
if user_input is not None:
|
||||
try:
|
||||
# Sanitize the user provided custom device name, which is used for entry and device registry name
|
||||
user_input["custom_device_name"] = sanitize_device_name(
|
||||
user_input["custom_device_name"], self.device_info["name"]
|
||||
)
|
||||
|
||||
# Since we have already set the unique ID and updated host if necessary create
|
||||
# the entry with the additional options.
|
||||
# The title of the integration is the custom friendly device name given by the user in step 2
|
||||
title = user_input["custom_device_name"]
|
||||
return self.async_create_entry(title=title,
|
||||
data={
|
||||
"user_input": self.user_input_from_step_user, # from previous step
|
||||
"device_info": self.device_info, # from device detection
|
||||
"options": user_input, # new options from this step
|
||||
},
|
||||
)
|
||||
except Exception as e:
|
||||
_LOGGER.error(f"Failed to handle device options: {e}" + ISSUE_URL_ERROR_MESSAGE)
|
||||
errors["base"] = "option_error"
|
||||
|
||||
# Prepare the second form's schema as it has dynamic values based on the API call
|
||||
# Use the name from the detected device as default device name
|
||||
default_device_name = (
|
||||
self.device_info["name"]
|
||||
if self.device_info and "name" in self.device_info
|
||||
else "New Device"
|
||||
)
|
||||
step_device_options_schema = vol.Schema(
|
||||
{
|
||||
vol.Required("custom_device_name", default=default_device_name): str,
|
||||
vol.Required("polling_time", default=10): vol.All(
|
||||
vol.Coerce(int), vol.Clamp(min=5)
|
||||
),
|
||||
vol.Required("group_sensors", default=True): bool,
|
||||
vol.Required("disable_sensors", default=False): bool,
|
||||
}
|
||||
)
|
||||
|
||||
# Show the form for step 2 with the device name and other options as defined in STEP_DEVICE_OPTIONS_SCHEMA
|
||||
return self.async_show_form(
|
||||
step_id="device_options",
|
||||
data_schema=step_device_options_schema,
|
||||
errors={},
|
||||
)
|
||||
|
||||
|
||||
class CannotConnect(HomeAssistantError):
|
||||
"""Error to indicate we cannot connect."""
|
||||
|
||||
|
||||
class InvalidAuth(HomeAssistantError):
|
||||
"""Error to indicate there is invalid auth."""
|
||||
|
||||
|
||||
# Helper function to sanitize
|
||||
def sanitize_device_name(device_name: str, fall_back: str, max_length=255) -> str:
|
||||
# Trim whitespace
|
||||
name = device_name.strip()
|
||||
|
||||
# Remove special characters but keep spaces
|
||||
name = re.sub(r"[^\w\s-]", "", name)
|
||||
|
||||
# Replace multiple spaces with a single space
|
||||
name = re.sub(r"\s+", " ", name)
|
||||
|
||||
# Length check
|
||||
if len(name) > max_length:
|
||||
# Split at the last space to avoid cutting off in the middle of a word
|
||||
name = name[:max_length].rsplit(" ", 1)[0]
|
||||
|
||||
# Fallback name
|
||||
if not name:
|
||||
name = fall_back
|
||||
|
||||
return name
|
||||
Reference in New Issue
Block a user