Remove sunweg integration (#124230)

* chore(sunweg): remove sunweg integration

* Update homeassistant/components/sunweg/strings.json

Co-authored-by: Abílio Costa <abmantis@users.noreply.github.com>

* Update homeassistant/components/sunweg/manifest.json

Co-authored-by: Abílio Costa <abmantis@users.noreply.github.com>

* feat: added async remove entry

* Clean setup_entry; add tests

---------

Co-authored-by: Abílio Costa <abmantis@users.noreply.github.com>
Co-authored-by: abmantis <amfcalt@gmail.com>
This commit is contained in:
Lucas Mindêllo de Andrade 2025-03-30 14:11:09 -03:00 committed by GitHub
parent 68d1a3c0a2
commit 3d49000c75
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
21 changed files with 105 additions and 1285 deletions

2
CODEOWNERS generated
View File

@ -1480,8 +1480,6 @@ build.json @home-assistant/supervisor
/tests/components/suez_water/ @ooii @jb101010-2
/homeassistant/components/sun/ @Swamp-Ig
/tests/components/sun/ @Swamp-Ig
/homeassistant/components/sunweg/ @rokam
/tests/components/sunweg/ @rokam
/homeassistant/components/supla/ @mwegrzynek
/homeassistant/components/surepetcare/ @benleb @danielhiversen
/tests/components/surepetcare/ @benleb @danielhiversen

View File

@ -1,197 +1,39 @@
"""The Sun WEG inverter sensor integration."""
import datetime
import json
import logging
from sunweg.api import APIHelper
from sunweg.plant import Plant
from homeassistant import config_entries
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_PASSWORD, CONF_USERNAME
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryAuthFailed
from homeassistant.helpers.typing import StateType, UndefinedType
from homeassistant.util import Throttle
from homeassistant.helpers import issue_registry as ir
from .const import CONF_PLANT_ID, DOMAIN, PLATFORMS, DeviceType
SCAN_INTERVAL = datetime.timedelta(minutes=5)
_LOGGER = logging.getLogger(__name__)
DOMAIN = "sunweg"
async def async_setup_entry(
hass: HomeAssistant, entry: config_entries.ConfigEntry
) -> bool:
async def async_setup_entry(hass: HomeAssistant, _: ConfigEntry) -> bool:
"""Load the saved entities."""
api = APIHelper(entry.data[CONF_USERNAME], entry.data[CONF_PASSWORD])
if not await hass.async_add_executor_job(api.authenticate):
raise ConfigEntryAuthFailed("Username or Password may be incorrect!")
hass.data.setdefault(DOMAIN, {})[entry.entry_id] = SunWEGData(
api, entry.data[CONF_PLANT_ID]
ir.async_create_issue(
hass,
DOMAIN,
DOMAIN,
is_fixable=False,
severity=ir.IssueSeverity.ERROR,
translation_key="integration_removed",
translation_placeholders={
"issue": "https://github.com/rokam/sunweg/issues/13",
"entries": "/config/integrations/integration/sunweg",
},
)
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
return True
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Unload a config entry."""
hass.data[DOMAIN].pop(entry.entry_id)
if len(hass.data[DOMAIN]) == 0:
hass.data.pop(DOMAIN)
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
return True
class SunWEGData:
"""The class for handling data retrieval."""
def __init__(
self,
api: APIHelper,
plant_id: int,
) -> None:
"""Initialize the probe."""
self.api = api
self.plant_id = plant_id
self.data: Plant = None
self.previous_values: dict = {}
@Throttle(SCAN_INTERVAL)
def update(self) -> None:
"""Update probe data."""
_LOGGER.debug("Updating data for plant %s", self.plant_id)
try:
self.data = self.api.plant(self.plant_id)
for inverter in self.data.inverters:
self.api.complete_inverter(inverter)
except json.decoder.JSONDecodeError:
_LOGGER.error("Unable to fetch data from SunWEG server")
_LOGGER.debug("Finished updating data for plant %s", self.plant_id)
def get_api_value(
self,
variable: str,
device_type: DeviceType,
inverter_id: int = 0,
deep_name: str | None = None,
):
"""Retrieve from a Plant the desired variable value."""
if device_type == DeviceType.TOTAL:
return self.data.__dict__.get(variable)
inverter_list = [i for i in self.data.inverters if i.id == inverter_id]
if len(inverter_list) == 0:
return None
inverter = inverter_list[0]
if device_type == DeviceType.INVERTER:
return inverter.__dict__.get(variable)
if device_type == DeviceType.PHASE:
for phase in inverter.phases:
if phase.name == deep_name:
return phase.__dict__.get(variable)
elif device_type == DeviceType.STRING:
for mppt in inverter.mppts:
for string in mppt.strings:
if string.name == deep_name:
return string.__dict__.get(variable)
return None
def get_data(
self,
*,
api_variable_key: str,
api_variable_unit: str | None,
deep_name: str | None,
device_type: DeviceType,
inverter_id: int,
name: str | UndefinedType | None,
native_unit_of_measurement: str | None,
never_resets: bool,
previous_value_drop_threshold: float | None,
) -> tuple[StateType | datetime.datetime, str | None]:
"""Get the data."""
_LOGGER.debug(
"Data request for: %s",
name,
)
variable = api_variable_key
previous_unit = native_unit_of_measurement
api_value = self.get_api_value(variable, device_type, inverter_id, deep_name)
previous_value = self.previous_values.get(variable)
return_value = api_value
if api_variable_unit is not None:
native_unit_of_measurement = self.get_api_value(
api_variable_unit,
device_type,
inverter_id,
deep_name,
)
# If we have a 'drop threshold' specified, then check it and correct if needed
if (
previous_value_drop_threshold is not None
and previous_value is not None
and api_value is not None
and previous_unit == native_unit_of_measurement
):
_LOGGER.debug(
(
"%s - Drop threshold specified (%s), checking for drop... API"
" Value: %s, Previous Value: %s"
),
name,
previous_value_drop_threshold,
api_value,
previous_value,
)
diff = float(api_value) - float(previous_value)
# Check if the value has dropped (negative value i.e. < 0) and it has only
# dropped by a small amount, if so, use the previous value.
# Note - The energy dashboard takes care of drops within 10%
# of the current value, however if the value is low e.g. 0.2
# and drops by 0.1 it classes as a reset.
if -(previous_value_drop_threshold) <= diff < 0:
_LOGGER.debug(
(
"Diff is negative, but only by a small amount therefore not a"
" nightly reset, using previous value (%s) instead of api value"
" (%s)"
),
previous_value,
api_value,
)
return_value = previous_value
else:
_LOGGER.debug("%s - No drop detected, using API value", name)
# Lifetime total values should always be increasing, they will never reset,
# however the API sometimes returns 0 values when the clock turns to 00:00
# local time in that scenario we should just return the previous value
# Scenarios:
# 1 - System has a genuine 0 value when it it first commissioned:
# - will return 0 until a non-zero value is registered
# 2 - System has been running fine but temporarily resets to 0 briefly
# at midnight:
# - will return the previous value
# 3 - HA is restarted during the midnight 'outage' - Not handled:
# - Previous value will not exist meaning 0 will be returned
# - This is an edge case that would be better handled by looking
# up the previous value of the entity from the recorder
if never_resets and api_value == 0 and previous_value:
_LOGGER.debug(
(
"API value is 0, but this value should never reset, returning"
" previous value (%s) instead"
),
previous_value,
)
return_value = previous_value
self.previous_values[variable] = return_value
return (return_value, native_unit_of_measurement)
async def async_remove_entry(hass: HomeAssistant, entry: ConfigEntry) -> None:
"""Remove a config entry."""
if not hass.config_entries.async_loaded_entries(DOMAIN):
ir.async_delete_issue(hass, DOMAIN, DOMAIN)
# Remove any remaining disabled or ignored entries
for _entry in hass.config_entries.async_entries(DOMAIN):
hass.async_create_task(hass.config_entries.async_remove(_entry.entry_id))

View File

@ -1,129 +1,11 @@
"""Config flow for Sun WEG integration."""
from collections.abc import Mapping
from typing import Any
from homeassistant.config_entries import ConfigFlow
from sunweg.api import APIHelper, SunWegApiError
import voluptuous as vol
from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
from homeassistant.const import CONF_NAME, CONF_PASSWORD, CONF_USERNAME
from homeassistant.core import callback
from .const import CONF_PLANT_ID, DOMAIN
from . import DOMAIN
class SunWEGConfigFlow(ConfigFlow, domain=DOMAIN):
"""Config flow class."""
VERSION = 1
def __init__(self) -> None:
"""Initialise sun weg server flow."""
self.api: APIHelper = None
self.data: dict[str, Any] = {}
@callback
def _async_show_user_form(self, step_id: str, errors=None) -> ConfigFlowResult:
"""Show the form to the user."""
default_username = ""
if CONF_USERNAME in self.data:
default_username = self.data[CONF_USERNAME]
data_schema = vol.Schema(
{
vol.Required(CONF_USERNAME, default=default_username): str,
vol.Required(CONF_PASSWORD): str,
}
)
return self.async_show_form(
step_id=step_id, data_schema=data_schema, errors=errors
)
def _set_auth_data(
self, step: str, username: str, password: str
) -> ConfigFlowResult | None:
"""Set username and password."""
if self.api:
# Set username and password
self.api.username = username
self.api.password = password
else:
# Initialise the library with the username & password
self.api = APIHelper(username, password)
try:
if not self.api.authenticate():
return self._async_show_user_form(step, {"base": "invalid_auth"})
except SunWegApiError:
return self._async_show_user_form(step, {"base": "timeout_connect"})
return None
async def async_step_user(self, user_input=None) -> ConfigFlowResult:
"""Handle the start of the config flow."""
if not user_input:
return self._async_show_user_form("user")
# Store authentication info
self.data = user_input
conf_result = await self.hass.async_add_executor_job(
self._set_auth_data,
"user",
user_input[CONF_USERNAME],
user_input[CONF_PASSWORD],
)
return await self.async_step_plant() if conf_result is None else conf_result
async def async_step_plant(self, user_input=None) -> ConfigFlowResult:
"""Handle adding a "plant" to Home Assistant."""
plant_list = await self.hass.async_add_executor_job(self.api.listPlants)
if len(plant_list) == 0:
return self.async_abort(reason="no_plants")
plants = {plant.id: plant.name for plant in plant_list}
if user_input is None and len(plant_list) > 1:
data_schema = vol.Schema({vol.Required(CONF_PLANT_ID): vol.In(plants)})
return self.async_show_form(step_id="plant", data_schema=data_schema)
if user_input is None and len(plant_list) == 1:
user_input = {CONF_PLANT_ID: plant_list[0].id}
user_input[CONF_NAME] = plants[user_input[CONF_PLANT_ID]]
await self.async_set_unique_id(user_input[CONF_PLANT_ID])
self._abort_if_unique_id_configured()
self.data.update(user_input)
return self.async_create_entry(title=self.data[CONF_NAME], data=self.data)
async def async_step_reauth(
self, entry_data: Mapping[str, Any]
) -> ConfigFlowResult:
"""Handle reauthorization request from SunWEG."""
self.data.update(entry_data)
return await self.async_step_reauth_confirm()
async def async_step_reauth_confirm(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
"""Handle reauthorization flow."""
if user_input is None:
return self._async_show_user_form("reauth_confirm")
self.data.update(user_input)
conf_result = await self.hass.async_add_executor_job(
self._set_auth_data,
"reauth_confirm",
user_input[CONF_USERNAME],
user_input[CONF_PASSWORD],
)
if conf_result is not None:
return conf_result
return self.async_update_reload_and_abort(
self._get_reauth_entry(), data=self.data
)

View File

@ -1,25 +0,0 @@
"""Define constants for the Sun WEG component."""
from enum import Enum
from homeassistant.const import Platform
class DeviceType(Enum):
"""Device Type Enum."""
TOTAL = 1
INVERTER = 2
PHASE = 3
STRING = 4
CONF_PLANT_ID = "plant_id"
DEFAULT_PLANT_ID = 0
DEFAULT_NAME = "Sun WEG"
DOMAIN = "sunweg"
PLATFORMS = [Platform.SENSOR]

View File

@ -1,10 +1,10 @@
{
"domain": "sunweg",
"name": "Sun WEG",
"codeowners": ["@rokam"],
"codeowners": [],
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/sunweg",
"iot_class": "cloud_polling",
"loggers": ["sunweg"],
"requirements": ["sunweg==3.0.2"]
"loggers": [],
"requirements": []
}

View File

@ -1,178 +0,0 @@
"""Read status of SunWEG inverters."""
from __future__ import annotations
import logging
from types import MappingProxyType
from typing import Any
from sunweg.api import APIHelper
from sunweg.device import Inverter
from sunweg.plant import Plant
from homeassistant.components.sensor import SensorEntity
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_NAME
from homeassistant.core import HomeAssistant
from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from .. import SunWEGData
from ..const import CONF_PLANT_ID, DEFAULT_PLANT_ID, DOMAIN, DeviceType
from .inverter import INVERTER_SENSOR_TYPES
from .phase import PHASE_SENSOR_TYPES
from .sensor_entity_description import SunWEGSensorEntityDescription
from .string import STRING_SENSOR_TYPES
from .total import TOTAL_SENSOR_TYPES
_LOGGER = logging.getLogger(__name__)
def get_device_list(
api: APIHelper, config: MappingProxyType[str, Any]
) -> tuple[list[Inverter], int]:
"""Retrieve the device list for the selected plant."""
plant_id = int(config[CONF_PLANT_ID])
if plant_id == DEFAULT_PLANT_ID:
plant_info: list[Plant] = api.listPlants()
plant_id = plant_info[0].id
devices: list[Inverter] = []
# Get a list of devices for specified plant to add sensors for.
for inverter in api.plant(plant_id).inverters:
api.complete_inverter(inverter)
devices.append(inverter)
return (devices, plant_id)
async def async_setup_entry(
hass: HomeAssistant,
config_entry: ConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up the SunWEG sensor."""
name = config_entry.data[CONF_NAME]
probe: SunWEGData = hass.data[DOMAIN][config_entry.entry_id]
devices, plant_id = await hass.async_add_executor_job(
get_device_list, probe.api, config_entry.data
)
entities = [
SunWEGInverter(
probe,
name=f"{name} Total",
unique_id=f"{plant_id}-{description.key}",
description=description,
device_type=DeviceType.TOTAL,
)
for description in TOTAL_SENSOR_TYPES
]
# Add sensors for each device in the specified plant.
entities.extend(
[
SunWEGInverter(
probe,
name=f"{device.name}",
unique_id=f"{device.sn}-{description.key}",
description=description,
device_type=DeviceType.INVERTER,
inverter_id=device.id,
)
for device in devices
for description in INVERTER_SENSOR_TYPES
]
)
entities.extend(
[
SunWEGInverter(
probe,
name=f"{device.name} {phase.name}",
unique_id=f"{device.sn}-{phase.name}-{description.key}",
description=description,
inverter_id=device.id,
device_type=DeviceType.PHASE,
deep_name=phase.name,
)
for device in devices
for phase in device.phases
for description in PHASE_SENSOR_TYPES
]
)
entities.extend(
[
SunWEGInverter(
probe,
name=f"{device.name} {string.name}",
unique_id=f"{device.sn}-{string.name}-{description.key}",
description=description,
inverter_id=device.id,
device_type=DeviceType.STRING,
deep_name=string.name,
)
for device in devices
for mppt in device.mppts
for string in mppt.strings
for description in STRING_SENSOR_TYPES
]
)
async_add_entities(entities, True)
class SunWEGInverter(SensorEntity):
"""Representation of a SunWEG Sensor."""
entity_description: SunWEGSensorEntityDescription
def __init__(
self,
probe: SunWEGData,
name: str,
unique_id: str,
description: SunWEGSensorEntityDescription,
device_type: DeviceType,
inverter_id: int = 0,
deep_name: str | None = None,
) -> None:
"""Initialize a sensor."""
self.probe = probe
self.entity_description = description
self.device_type = device_type
self.inverter_id = inverter_id
self.deep_name = deep_name
self._attr_name = f"{name} {description.name}"
self._attr_unique_id = unique_id
self._attr_icon = (
description.icon if description.icon is not None else "mdi:solar-power"
)
self._attr_device_info = DeviceInfo(
identifiers={(DOMAIN, str(probe.plant_id))},
manufacturer="SunWEG",
name=name,
)
def update(self) -> None:
"""Get the latest data from the Sun WEG API and updates the state."""
self.probe.update()
(
self._attr_native_value,
self._attr_native_unit_of_measurement,
) = self.probe.get_data(
api_variable_key=self.entity_description.api_variable_key,
api_variable_unit=self.entity_description.api_variable_unit,
deep_name=self.deep_name,
device_type=self.device_type,
inverter_id=self.inverter_id,
name=self.entity_description.name,
native_unit_of_measurement=self.native_unit_of_measurement,
never_resets=self.entity_description.never_resets,
previous_value_drop_threshold=self.entity_description.previous_value_drop_threshold,
)

View File

@ -1,70 +0,0 @@
"""SunWEG Sensor definitions for the Inverter type."""
from __future__ import annotations
from homeassistant.components.sensor import SensorDeviceClass, SensorStateClass
from homeassistant.const import (
UnitOfEnergy,
UnitOfFrequency,
UnitOfPower,
UnitOfTemperature,
)
from .sensor_entity_description import SunWEGSensorEntityDescription
INVERTER_SENSOR_TYPES: tuple[SunWEGSensorEntityDescription, ...] = (
SunWEGSensorEntityDescription(
key="inverter_energy_today",
name="Energy today",
api_variable_key="_today_energy",
api_variable_unit="_today_energy_metric",
native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
device_class=SensorDeviceClass.ENERGY,
state_class=SensorStateClass.TOTAL_INCREASING,
suggested_display_precision=1,
),
SunWEGSensorEntityDescription(
key="inverter_energy_total",
name="Lifetime energy output",
api_variable_key="_total_energy",
api_variable_unit="_total_energy_metric",
native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
device_class=SensorDeviceClass.ENERGY,
suggested_display_precision=1,
state_class=SensorStateClass.TOTAL,
never_resets=True,
),
SunWEGSensorEntityDescription(
key="inverter_frequency",
name="AC frequency",
api_variable_key="_frequency",
native_unit_of_measurement=UnitOfFrequency.HERTZ,
device_class=SensorDeviceClass.FREQUENCY,
suggested_display_precision=1,
),
SunWEGSensorEntityDescription(
key="inverter_current_wattage",
name="Output power",
api_variable_key="_power",
api_variable_unit="_power_metric",
native_unit_of_measurement=UnitOfPower.WATT,
device_class=SensorDeviceClass.POWER,
state_class=SensorStateClass.MEASUREMENT,
suggested_display_precision=1,
),
SunWEGSensorEntityDescription(
key="inverter_temperature",
name="Temperature",
api_variable_key="_temperature",
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
device_class=SensorDeviceClass.TEMPERATURE,
icon="mdi:temperature-celsius",
suggested_display_precision=1,
),
SunWEGSensorEntityDescription(
key="inverter_power_factor",
name="Power Factor",
api_variable_key="_power_factor",
suggested_display_precision=1,
),
)

View File

@ -1,27 +0,0 @@
"""SunWEG Sensor definitions for the Phase type."""
from __future__ import annotations
from homeassistant.components.sensor import SensorDeviceClass
from homeassistant.const import UnitOfElectricCurrent, UnitOfElectricPotential
from .sensor_entity_description import SunWEGSensorEntityDescription
PHASE_SENSOR_TYPES: tuple[SunWEGSensorEntityDescription, ...] = (
SunWEGSensorEntityDescription(
key="voltage",
name="Voltage",
api_variable_key="_voltage",
native_unit_of_measurement=UnitOfElectricPotential.VOLT,
device_class=SensorDeviceClass.VOLTAGE,
suggested_display_precision=2,
),
SunWEGSensorEntityDescription(
key="amperage",
name="Amperage",
api_variable_key="_amperage",
native_unit_of_measurement=UnitOfElectricCurrent.AMPERE,
device_class=SensorDeviceClass.CURRENT,
suggested_display_precision=1,
),
)

View File

@ -1,24 +0,0 @@
"""Sensor Entity Description for the SunWEG integration."""
from __future__ import annotations
from dataclasses import dataclass
from homeassistant.components.sensor import SensorEntityDescription
@dataclass(frozen=True)
class SunWEGRequiredKeysMixin:
"""Mixin for required keys."""
api_variable_key: str
@dataclass(frozen=True)
class SunWEGSensorEntityDescription(SensorEntityDescription, SunWEGRequiredKeysMixin):
"""Describes SunWEG sensor entity."""
api_variable_unit: str | None = None
previous_value_drop_threshold: float | None = None
never_resets: bool = False
icon: str | None = None

View File

@ -1,27 +0,0 @@
"""SunWEG Sensor definitions for the String type."""
from __future__ import annotations
from homeassistant.components.sensor import SensorDeviceClass
from homeassistant.const import UnitOfElectricCurrent, UnitOfElectricPotential
from .sensor_entity_description import SunWEGSensorEntityDescription
STRING_SENSOR_TYPES: tuple[SunWEGSensorEntityDescription, ...] = (
SunWEGSensorEntityDescription(
key="voltage",
name="Voltage",
api_variable_key="_voltage",
native_unit_of_measurement=UnitOfElectricPotential.VOLT,
device_class=SensorDeviceClass.VOLTAGE,
suggested_display_precision=2,
),
SunWEGSensorEntityDescription(
key="amperage",
name="Amperage",
api_variable_key="_amperage",
native_unit_of_measurement=UnitOfElectricCurrent.AMPERE,
device_class=SensorDeviceClass.CURRENT,
suggested_display_precision=1,
),
)

View File

@ -1,50 +0,0 @@
"""SunWEG Sensor definitions for Totals."""
from __future__ import annotations
from homeassistant.components.sensor import SensorDeviceClass, SensorStateClass
from homeassistant.const import UnitOfEnergy, UnitOfPower
from .sensor_entity_description import SunWEGSensorEntityDescription
TOTAL_SENSOR_TYPES: tuple[SunWEGSensorEntityDescription, ...] = (
SunWEGSensorEntityDescription(
key="total_money_total",
name="Money lifetime",
api_variable_key="_saving",
icon="mdi:cash",
native_unit_of_measurement="R$",
suggested_display_precision=2,
),
SunWEGSensorEntityDescription(
key="total_energy_today",
name="Energy Today",
api_variable_key="_today_energy",
api_variable_unit="_today_energy_metric",
native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
device_class=SensorDeviceClass.ENERGY,
state_class=SensorStateClass.TOTAL_INCREASING,
),
SunWEGSensorEntityDescription(
key="total_output_power",
name="Output Power",
api_variable_key="_total_power",
native_unit_of_measurement=UnitOfPower.KILO_WATT,
device_class=SensorDeviceClass.POWER,
),
SunWEGSensorEntityDescription(
key="total_energy_output",
name="Lifetime energy output",
api_variable_key="_total_energy",
native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
device_class=SensorDeviceClass.ENERGY,
state_class=SensorStateClass.TOTAL,
never_resets=True,
),
SunWEGSensorEntityDescription(
key="last_update",
name="Last Update",
api_variable_key="_last_update",
device_class=SensorDeviceClass.DATE,
),
)

View File

@ -1,35 +1,8 @@
{
"config": {
"abort": {
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]",
"no_plants": "No plants have been found on this account",
"reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]"
},
"error": {
"invalid_auth": "[%key:common::config_flow::error::invalid_auth%]",
"timeout_connect": "[%key:common::config_flow::error::timeout_connect%]"
},
"step": {
"plant": {
"data": {
"plant_id": "Plant"
},
"title": "Select your plant"
},
"user": {
"data": {
"password": "[%key:common::config_flow::data::password%]",
"username": "[%key:common::config_flow::data::username%]"
},
"title": "Enter your Sun WEG information"
},
"reauth_confirm": {
"data": {
"password": "[%key:common::config_flow::data::password%]",
"username": "[%key:common::config_flow::data::username%]"
},
"title": "[%key:common::config_flow::title::reauth%]"
}
"issues": {
"integration_removed": {
"title": "The SunWEG integration has been removed",
"description": "The SunWEG integration has been removed from Home Assistant.\n\nThe library that Home Assistant uses to connect with SunWEG services, [doesn't work as expected anymore, demanding daily token renew]({issue}).\n\nTo resolve this issue, please remove the (now defunct) integration entries from your Home Assistant setup. [Click here to see your existing SunWEG integration entries]({entries})."
}
}
}

View File

@ -461,12 +461,10 @@ filterwarnings = [
# Modify app state for testing
"ignore:Changing state of started or joined application is deprecated:DeprecationWarning:tests.components.http.test_ban",
# -- Tests
# Ignore custom pytest marks
"ignore:Unknown pytest.mark.disable_autouse_fixture:pytest.PytestUnknownMarkWarning:tests.components.met",
"ignore:Unknown pytest.mark.dataset:pytest.PytestUnknownMarkWarning:tests.components.screenlogic",
# https://github.com/rokam/sunweg/blob/3.1.0/sunweg/plant.py#L96 - v3.1.0 - 2024-10-02
"ignore:The '(kwh_per_kwp|performance_rate)' property is deprecated and will return 0:DeprecationWarning:tests.components.sunweg.test_init",
# -- Tests
# Ignore custom pytest marks
"ignore:Unknown pytest.mark.disable_autouse_fixture:pytest.PytestUnknownMarkWarning:tests.components.met",
"ignore:Unknown pytest.mark.dataset:pytest.PytestUnknownMarkWarning:tests.components.screenlogic",
# -- design choice 3rd party
# https://github.com/gwww/elkm1/blob/2.2.10/elkm1_lib/util.py#L8-L19

3
requirements_all.txt generated
View File

@ -2830,9 +2830,6 @@ stringcase==1.2.0
# homeassistant.components.subaru
subarulink==0.7.13
# homeassistant.components.sunweg
sunweg==3.0.2
# homeassistant.components.surepetcare
surepy==0.9.0

View File

@ -2289,9 +2289,6 @@ stringcase==1.2.0
# homeassistant.components.subaru
subarulink==0.7.13
# homeassistant.components.sunweg
sunweg==3.0.2
# homeassistant.components.surepetcare
surepy==0.9.0

View File

@ -1050,7 +1050,6 @@
"melnor": 42,
"plaato": 45,
"freedompro": 26,
"sunweg": 3,
"logi_circle": 18,
"proxy": 16,
"statsd": 4,

View File

@ -1 +1 @@
"""Tests for the sunweg component."""
"""Tests for the Sun WEG integration."""

View File

@ -1,22 +0,0 @@
"""Common functions needed to setup tests for Sun WEG."""
from homeassistant.components.sunweg.const import CONF_PLANT_ID, DOMAIN
from homeassistant.const import CONF_NAME, CONF_PASSWORD, CONF_USERNAME
from tests.common import MockConfigEntry
SUNWEG_USER_INPUT = {
CONF_USERNAME: "username",
CONF_PASSWORD: "password",
}
SUNWEG_MOCK_ENTRY = MockConfigEntry(
domain=DOMAIN,
unique_id=0,
data={
CONF_USERNAME: "user@email.com",
CONF_PASSWORD: "password",
CONF_PLANT_ID: 0,
CONF_NAME: "Name",
},
)

View File

@ -1,90 +0,0 @@
"""Conftest for SunWEG tests."""
from datetime import datetime
import pytest
from sunweg.device import MPPT, Inverter, Phase, String
from sunweg.plant import Plant
@pytest.fixture
def string_fixture() -> String:
"""Define String fixture."""
return String("STR1", 450.3, 23.4, 0)
@pytest.fixture
def mppt_fixture(string_fixture) -> MPPT:
"""Define MPPT fixture."""
mppt = MPPT("mppt")
mppt.strings.append(string_fixture)
return mppt
@pytest.fixture
def phase_fixture() -> Phase:
"""Define Phase fixture."""
return Phase("PhaseA", 120.0, 3.2, 0, 0)
@pytest.fixture
def inverter_fixture(phase_fixture, mppt_fixture) -> Inverter:
"""Define inverter fixture."""
inverter = Inverter(
21255,
"INVERSOR01",
"J63T233018RE074",
23.2,
0.0,
0.0,
"MWh",
0,
"kWh",
0.0,
1,
0,
"kW",
)
inverter.phases.append(phase_fixture)
inverter.mppts.append(mppt_fixture)
return inverter
@pytest.fixture
def plant_fixture(inverter_fixture) -> Plant:
"""Define Plant fixture."""
plant = Plant(
123456,
"Plant #123",
29.5,
0.5,
0,
12.786912,
24.0,
"kWh",
332.2,
0.012296,
datetime(2023, 2, 16, 14, 22, 37),
)
plant.inverters.append(inverter_fixture)
return plant
@pytest.fixture
def plant_fixture_alternative(inverter_fixture) -> Plant:
"""Define Plant fixture."""
plant = Plant(
123456,
"Plant #123",
29.5,
0.5,
0,
12.786912,
24.0,
"kWh",
332.2,
0.012296,
None,
)
plant.inverters.append(inverter_fixture)
return plant

View File

@ -1,223 +0,0 @@
"""Tests for the Sun WEG server config flow."""
from unittest.mock import patch
from sunweg.api import APIHelper, SunWegApiError
from homeassistant import config_entries
from homeassistant.components.sunweg.const import CONF_PLANT_ID, DOMAIN
from homeassistant.const import CONF_PASSWORD, CONF_USERNAME
from homeassistant.core import HomeAssistant
from homeassistant.data_entry_flow import FlowResultType
from .common import SUNWEG_MOCK_ENTRY, SUNWEG_USER_INPUT
from tests.common import MockConfigEntry
async def test_show_authenticate_form(hass: HomeAssistant) -> None:
"""Test that the setup form is served."""
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": config_entries.SOURCE_USER}
)
assert result["type"] is FlowResultType.FORM
assert result["step_id"] == "user"
async def test_incorrect_login(hass: HomeAssistant) -> None:
"""Test that it shows the appropriate error when an incorrect username/password/server is entered."""
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": config_entries.SOURCE_USER}
)
with patch.object(APIHelper, "authenticate", return_value=False):
result = await hass.config_entries.flow.async_configure(
result["flow_id"], SUNWEG_USER_INPUT
)
assert result["type"] is FlowResultType.FORM
assert result["step_id"] == "user"
assert result["errors"] == {"base": "invalid_auth"}
async def test_server_unavailable(hass: HomeAssistant) -> None:
"""Test when the SunWEG server don't respond."""
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": config_entries.SOURCE_USER}
)
with patch.object(
APIHelper, "authenticate", side_effect=SunWegApiError("Internal Server Error")
):
result = await hass.config_entries.flow.async_configure(
result["flow_id"], SUNWEG_USER_INPUT
)
assert result["type"] is FlowResultType.FORM
assert result["step_id"] == "user"
assert result["errors"] == {"base": "timeout_connect"}
async def test_reauth(hass: HomeAssistant, plant_fixture, inverter_fixture) -> None:
"""Test reauth flow."""
mock_entry = SUNWEG_MOCK_ENTRY
mock_entry.add_to_hass(hass)
entries = hass.config_entries.async_entries()
assert len(entries) == 1
assert entries[0].data[CONF_USERNAME] == SUNWEG_MOCK_ENTRY.data[CONF_USERNAME]
assert entries[0].data[CONF_PASSWORD] == SUNWEG_MOCK_ENTRY.data[CONF_PASSWORD]
result = await mock_entry.start_reauth_flow(hass)
assert result["type"] is FlowResultType.FORM
assert result["step_id"] == "reauth_confirm"
with patch.object(APIHelper, "authenticate", return_value=False):
result = await hass.config_entries.flow.async_configure(
result["flow_id"],
user_input=SUNWEG_USER_INPUT,
)
assert result["type"] is FlowResultType.FORM
assert result["step_id"] == "reauth_confirm"
assert result["errors"] == {"base": "invalid_auth"}
with patch.object(
APIHelper, "authenticate", side_effect=SunWegApiError("Internal Server Error")
):
result = await hass.config_entries.flow.async_configure(
result["flow_id"],
user_input=SUNWEG_USER_INPUT,
)
assert result["type"] is FlowResultType.FORM
assert result["step_id"] == "reauth_confirm"
assert result["errors"] == {"base": "timeout_connect"}
with (
patch.object(APIHelper, "authenticate", return_value=True),
patch.object(APIHelper, "listPlants", return_value=[plant_fixture]),
patch.object(APIHelper, "plant", return_value=plant_fixture),
patch.object(APIHelper, "inverter", return_value=inverter_fixture),
patch.object(APIHelper, "complete_inverter"),
):
result = await hass.config_entries.flow.async_configure(
result["flow_id"],
user_input=SUNWEG_USER_INPUT,
)
await hass.async_block_till_done()
assert result["type"] is FlowResultType.ABORT
assert result["reason"] == "reauth_successful"
entries = hass.config_entries.async_entries()
assert len(entries) == 1
assert entries[0].data[CONF_USERNAME] == SUNWEG_USER_INPUT[CONF_USERNAME]
assert entries[0].data[CONF_PASSWORD] == SUNWEG_USER_INPUT[CONF_PASSWORD]
async def test_no_plants_on_account(hass: HomeAssistant) -> None:
"""Test registering an integration with wrong auth then with no plants available."""
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": config_entries.SOURCE_USER}
)
with patch.object(APIHelper, "authenticate", return_value=False):
result = await hass.config_entries.flow.async_configure(
result["flow_id"], SUNWEG_USER_INPUT
)
assert result["type"] is FlowResultType.FORM
assert result["step_id"] == "user"
assert result["errors"] == {"base": "invalid_auth"}
with (
patch.object(APIHelper, "authenticate", return_value=True),
patch.object(APIHelper, "listPlants", return_value=[]),
):
result = await hass.config_entries.flow.async_configure(
result["flow_id"], SUNWEG_USER_INPUT
)
assert result["type"] is FlowResultType.ABORT
assert result["reason"] == "no_plants"
async def test_multiple_plant_ids(hass: HomeAssistant, plant_fixture) -> None:
"""Test registering an integration and finishing flow with an selected plant_id."""
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": config_entries.SOURCE_USER}
)
with (
patch.object(APIHelper, "authenticate", return_value=True),
patch.object(
APIHelper, "listPlants", return_value=[plant_fixture, plant_fixture]
),
):
result = await hass.config_entries.flow.async_configure(
result["flow_id"], SUNWEG_USER_INPUT
)
assert result["type"] is FlowResultType.FORM
assert result["step_id"] == "plant"
result = await hass.config_entries.flow.async_configure(
result["flow_id"], {CONF_PLANT_ID: 123456}
)
await hass.async_block_till_done()
assert result["type"] is FlowResultType.CREATE_ENTRY
assert result["data"][CONF_USERNAME] == SUNWEG_USER_INPUT[CONF_USERNAME]
assert result["data"][CONF_PASSWORD] == SUNWEG_USER_INPUT[CONF_PASSWORD]
assert result["data"][CONF_PLANT_ID] == 123456
async def test_one_plant_on_account(hass: HomeAssistant, plant_fixture) -> None:
"""Test registering an integration and finishing flow with current plant_id."""
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": config_entries.SOURCE_USER}
)
with (
patch.object(APIHelper, "authenticate", return_value=True),
patch.object(
APIHelper,
"listPlants",
return_value=[plant_fixture],
),
):
result = await hass.config_entries.flow.async_configure(
result["flow_id"], SUNWEG_USER_INPUT
)
assert result["type"] is FlowResultType.CREATE_ENTRY
assert result["data"][CONF_USERNAME] == SUNWEG_USER_INPUT[CONF_USERNAME]
assert result["data"][CONF_PASSWORD] == SUNWEG_USER_INPUT[CONF_PASSWORD]
assert result["data"][CONF_PLANT_ID] == 123456
async def test_existing_plant_configured(hass: HomeAssistant, plant_fixture) -> None:
"""Test entering an existing plant_id."""
entry = MockConfigEntry(domain=DOMAIN, unique_id=123456)
entry.add_to_hass(hass)
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": config_entries.SOURCE_USER}
)
with (
patch.object(APIHelper, "authenticate", return_value=True),
patch.object(
APIHelper,
"listPlants",
return_value=[plant_fixture],
),
):
result = await hass.config_entries.flow.async_configure(
result["flow_id"], SUNWEG_USER_INPUT
)
assert result["type"] is FlowResultType.ABORT
assert result["reason"] == "already_configured"

View File

@ -1,209 +1,79 @@
"""Tests for the Sun WEG init."""
"""Tests for the Sun WEG integration."""
import json
from unittest.mock import MagicMock, patch
from sunweg.api import APIHelper, SunWegApiError
from homeassistant.components.sunweg import SunWEGData
from homeassistant.components.sunweg.const import DOMAIN, DeviceType
from homeassistant.components.sunweg.sensor.sensor_entity_description import (
SunWEGSensorEntityDescription,
from homeassistant.components.sunweg import DOMAIN
from homeassistant.config_entries import (
SOURCE_IGNORE,
ConfigEntryDisabler,
ConfigEntryState,
)
from homeassistant.config_entries import ConfigEntryState
from homeassistant.core import HomeAssistant
from homeassistant.setup import async_setup_component
from homeassistant.helpers import issue_registry as ir
from .common import SUNWEG_MOCK_ENTRY
from tests.common import MockConfigEntry
async def test_methods(hass: HomeAssistant, plant_fixture, inverter_fixture) -> None:
"""Test methods."""
mock_entry = SUNWEG_MOCK_ENTRY
mock_entry.add_to_hass(hass)
with (
patch.object(APIHelper, "authenticate", return_value=True),
patch.object(APIHelper, "listPlants", return_value=[plant_fixture]),
patch.object(APIHelper, "plant", return_value=plant_fixture),
patch.object(APIHelper, "inverter", return_value=inverter_fixture),
patch.object(APIHelper, "complete_inverter"),
):
assert await async_setup_component(hass, DOMAIN, mock_entry.data)
await hass.async_block_till_done()
assert await hass.config_entries.async_unload(mock_entry.entry_id)
async def test_setup_wrongpass(hass: HomeAssistant) -> None:
"""Test setup with wrong pass."""
mock_entry = SUNWEG_MOCK_ENTRY
mock_entry.add_to_hass(hass)
with patch.object(APIHelper, "authenticate", return_value=False):
assert await async_setup_component(hass, DOMAIN, mock_entry.data)
await hass.async_block_till_done()
async def test_setup_error_500(hass: HomeAssistant) -> None:
"""Test setup with wrong pass."""
mock_entry = SUNWEG_MOCK_ENTRY
mock_entry.add_to_hass(hass)
with patch.object(
APIHelper, "authenticate", side_effect=SunWegApiError("Error 500")
):
assert await async_setup_component(hass, DOMAIN, mock_entry.data)
await hass.async_block_till_done()
async def test_sunwegdata_update_exception() -> None:
"""Test SunWEGData exception on update."""
api = MagicMock()
api.plant = MagicMock(side_effect=json.decoder.JSONDecodeError("Message", "Doc", 1))
data = SunWEGData(api, 0)
data.update()
assert data.data is None
async def test_sunwegdata_update_success(plant_fixture) -> None:
"""Test SunWEGData success on update."""
api = MagicMock()
api.plant = MagicMock(return_value=plant_fixture)
api.complete_inverter = MagicMock()
data = SunWEGData(api, 0)
data.update()
assert data.data.id == plant_fixture.id
assert data.data.name == plant_fixture.name
assert data.data.kwh_per_kwp == plant_fixture.kwh_per_kwp
assert data.data.last_update == plant_fixture.last_update
assert data.data.performance_rate == plant_fixture.performance_rate
assert data.data.saving == plant_fixture.saving
assert len(data.data.inverters) == 1
async def test_sunwegdata_update_success_alternative(plant_fixture_alternative) -> None:
"""Test SunWEGData success on update."""
api = MagicMock()
api.plant = MagicMock(return_value=plant_fixture_alternative)
api.complete_inverter = MagicMock()
data = SunWEGData(api, 0)
data.update()
assert data.data.id == plant_fixture_alternative.id
assert data.data.name == plant_fixture_alternative.name
assert data.data.kwh_per_kwp == plant_fixture_alternative.kwh_per_kwp
assert data.data.last_update == plant_fixture_alternative.last_update
assert data.data.performance_rate == plant_fixture_alternative.performance_rate
assert data.data.saving == plant_fixture_alternative.saving
assert len(data.data.inverters) == 1
async def test_sunwegdata_get_api_value_none(plant_fixture) -> None:
"""Test SunWEGData none return on get_api_value."""
api = MagicMock()
data = SunWEGData(api, 123456)
data.data = plant_fixture
assert data.get_api_value("variable", DeviceType.INVERTER, 0, "deep_name") is None
assert data.get_api_value("variable", DeviceType.STRING, 21255, "deep_name") is None
async def test_sunwegdata_get_data_drop_threshold() -> None:
"""Test SunWEGData get_data with drop threshold."""
api = MagicMock()
data = SunWEGData(api, 123456)
data.get_api_value = MagicMock()
entity_description = SunWEGSensorEntityDescription(
api_variable_key="variable", key="key", previous_value_drop_threshold=0.1
async def test_sunweg_repair_issue(
hass: HomeAssistant, issue_registry: ir.IssueRegistry
) -> None:
"""Test the Sun WEG configuration entry loading/unloading handles the repair."""
config_entry_1 = MockConfigEntry(
title="Example 1",
domain=DOMAIN,
)
data.get_api_value.return_value = 3.0
assert data.get_data(
api_variable_key=entity_description.api_variable_key,
api_variable_unit=entity_description.api_variable_unit,
deep_name=None,
device_type=DeviceType.TOTAL,
inverter_id=0,
name=entity_description.name,
native_unit_of_measurement=entity_description.native_unit_of_measurement,
never_resets=entity_description.never_resets,
previous_value_drop_threshold=entity_description.previous_value_drop_threshold,
) == (3.0, None)
data.get_api_value.return_value = 2.91
assert data.get_data(
api_variable_key=entity_description.api_variable_key,
api_variable_unit=entity_description.api_variable_unit,
deep_name=None,
device_type=DeviceType.TOTAL,
inverter_id=0,
name=entity_description.name,
native_unit_of_measurement=entity_description.native_unit_of_measurement,
never_resets=entity_description.never_resets,
previous_value_drop_threshold=entity_description.previous_value_drop_threshold,
) == (3.0, None)
data.get_api_value.return_value = 2.8
assert data.get_data(
api_variable_key=entity_description.api_variable_key,
api_variable_unit=entity_description.api_variable_unit,
deep_name=None,
device_type=DeviceType.TOTAL,
inverter_id=0,
name=entity_description.name,
native_unit_of_measurement=entity_description.native_unit_of_measurement,
never_resets=entity_description.never_resets,
previous_value_drop_threshold=entity_description.previous_value_drop_threshold,
) == (2.8, None)
config_entry_1.add_to_hass(hass)
await hass.config_entries.async_setup(config_entry_1.entry_id)
await hass.async_block_till_done()
assert config_entry_1.state is ConfigEntryState.LOADED
async def test_sunwegdata_get_data_never_reset() -> None:
"""Test SunWEGData get_data with never reset."""
api = MagicMock()
data = SunWEGData(api, 123456)
data.get_api_value = MagicMock()
entity_description = SunWEGSensorEntityDescription(
api_variable_key="variable", key="key", never_resets=True
# Add a second one
config_entry_2 = MockConfigEntry(
title="Example 2",
domain=DOMAIN,
)
data.get_api_value.return_value = 3.0
assert data.get_data(
api_variable_key=entity_description.api_variable_key,
api_variable_unit=entity_description.api_variable_unit,
deep_name=None,
device_type=DeviceType.TOTAL,
inverter_id=0,
name=entity_description.name,
native_unit_of_measurement=entity_description.native_unit_of_measurement,
never_resets=entity_description.never_resets,
previous_value_drop_threshold=entity_description.previous_value_drop_threshold,
) == (3.0, None)
data.get_api_value.return_value = 0
assert data.get_data(
api_variable_key=entity_description.api_variable_key,
api_variable_unit=entity_description.api_variable_unit,
deep_name=None,
device_type=DeviceType.TOTAL,
inverter_id=0,
name=entity_description.name,
native_unit_of_measurement=entity_description.native_unit_of_measurement,
never_resets=entity_description.never_resets,
previous_value_drop_threshold=entity_description.previous_value_drop_threshold,
) == (3.0, None)
data.get_api_value.return_value = 2.8
assert data.get_data(
api_variable_key=entity_description.api_variable_key,
api_variable_unit=entity_description.api_variable_unit,
deep_name=None,
device_type=DeviceType.TOTAL,
inverter_id=0,
name=entity_description.name,
native_unit_of_measurement=entity_description.native_unit_of_measurement,
never_resets=entity_description.never_resets,
previous_value_drop_threshold=entity_description.previous_value_drop_threshold,
) == (2.8, None)
config_entry_2.add_to_hass(hass)
await hass.config_entries.async_setup(config_entry_2.entry_id)
await hass.async_block_till_done()
assert config_entry_2.state is ConfigEntryState.LOADED
assert issue_registry.async_get_issue(DOMAIN, DOMAIN)
async def test_reauth_started(hass: HomeAssistant) -> None:
"""Test reauth flow started."""
mock_entry = SUNWEG_MOCK_ENTRY
mock_entry.add_to_hass(hass)
with patch.object(APIHelper, "authenticate", return_value=False):
await async_setup_component(hass, DOMAIN, {})
await hass.async_block_till_done()
assert mock_entry.state is ConfigEntryState.SETUP_ERROR
flows = hass.config_entries.flow.async_progress()
assert len(flows) == 1
assert flows[0]["step_id"] == "reauth_confirm"
# Add an ignored entry
config_entry_3 = MockConfigEntry(
source=SOURCE_IGNORE,
domain=DOMAIN,
)
config_entry_3.add_to_hass(hass)
await hass.config_entries.async_setup(config_entry_3.entry_id)
await hass.async_block_till_done()
assert config_entry_3.state is ConfigEntryState.NOT_LOADED
# Add a disabled entry
config_entry_4 = MockConfigEntry(
disabled_by=ConfigEntryDisabler.USER,
domain=DOMAIN,
)
config_entry_4.add_to_hass(hass)
await hass.config_entries.async_setup(config_entry_4.entry_id)
await hass.async_block_till_done()
assert config_entry_4.state is ConfigEntryState.NOT_LOADED
# Remove the first one
await hass.config_entries.async_remove(config_entry_1.entry_id)
await hass.async_block_till_done()
assert config_entry_1.state is ConfigEntryState.NOT_LOADED
assert config_entry_2.state is ConfigEntryState.LOADED
assert issue_registry.async_get_issue(DOMAIN, DOMAIN)
# Remove the second one
await hass.config_entries.async_remove(config_entry_2.entry_id)
await hass.async_block_till_done()
assert config_entry_1.state is ConfigEntryState.NOT_LOADED
assert config_entry_2.state is ConfigEntryState.NOT_LOADED
assert issue_registry.async_get_issue(DOMAIN, DOMAIN) is None
# Check the ignored and disabled entries are removed
assert not hass.config_entries.async_entries(DOMAIN)