diff --git a/.coveragerc b/.coveragerc index 8687ba5a230..dca566c8a26 100644 --- a/.coveragerc +++ b/.coveragerc @@ -398,6 +398,11 @@ omit = homeassistant/components/glances/sensor.py homeassistant/components/gntp/notify.py homeassistant/components/goalfeed/* + homeassistant/components/goodwe/__init__.py + homeassistant/components/goodwe/const.py + homeassistant/components/goodwe/number.py + homeassistant/components/goodwe/select.py + homeassistant/components/goodwe/sensor.py homeassistant/components/google/__init__.py homeassistant/components/google_cloud/tts.py homeassistant/components/google_maps/device_tracker.py diff --git a/CODEOWNERS b/CODEOWNERS index 000c21f388c..53f0359fede 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -344,6 +344,8 @@ homeassistant/components/goalzero/* @tkdrob tests/components/goalzero/* @tkdrob homeassistant/components/gogogate2/* @vangorra @bdraco tests/components/gogogate2/* @vangorra @bdraco +homeassistant/components/goodwe/* @mletenay @starkillerOG +tests/components/goodwe/* @mletenay @starkillerOG homeassistant/components/google_assistant/* @home-assistant/cloud tests/components/google_assistant/* @home-assistant/cloud homeassistant/components/google_cloud/* @lufton diff --git a/homeassistant/components/goodwe/__init__.py b/homeassistant/components/goodwe/__init__.py new file mode 100644 index 00000000000..e48590931ba --- /dev/null +++ b/homeassistant/components/goodwe/__init__.py @@ -0,0 +1,116 @@ +"""The Goodwe inverter component.""" +import logging + +from goodwe import InverterError, RequestFailedException, connect + +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_HOST +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import ConfigEntryNotReady +from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed + +from .const import ( + CONF_MODEL_FAMILY, + DOMAIN, + KEY_COORDINATOR, + KEY_DEVICE_INFO, + KEY_INVERTER, + PLATFORMS, + SCAN_INTERVAL, +) + +_LOGGER = logging.getLogger(__name__) + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Set up the Goodwe components from a config entry.""" + hass.data.setdefault(DOMAIN, {}) + name = entry.title + host = entry.data[CONF_HOST] + model_family = entry.data[CONF_MODEL_FAMILY] + + # Connect to Goodwe inverter + try: + inverter = await connect( + host=host, + family=model_family, + retries=10, + ) + except InverterError as err: + raise ConfigEntryNotReady from err + + device_info = DeviceInfo( + configuration_url="https://www.semsportal.com", + identifiers={(DOMAIN, inverter.serial_number)}, + name=entry.title, + manufacturer="GoodWe", + model=inverter.model_name, + sw_version=f"{inverter.software_version} ({inverter.arm_version})", + ) + + async def async_update_data(): + """Fetch data from the inverter.""" + try: + return await inverter.read_runtime_data() + except RequestFailedException as ex: + # UDP communication with inverter is by definition unreliable. + # It is rather normal in many environments to fail to receive + # proper response in usual time, so we intentionally ignore isolated + # failures and report problem with availability only after + # consecutive streak of 3 of failed requests. + if ex.consecutive_failures_count < 3: + _LOGGER.debug( + "No response received (streak of %d)", ex.consecutive_failures_count + ) + # return empty dictionary, sensors will keep their previous values + return {} + # Inverter does not respond anymore (e.g. it went to sleep mode) + _LOGGER.debug( + "Inverter not responding (streak of %d)", ex.consecutive_failures_count + ) + raise UpdateFailed(ex) from ex + except InverterError as ex: + raise UpdateFailed(ex) from ex + + # Create update coordinator + coordinator = DataUpdateCoordinator( + hass, + _LOGGER, + name=name, + update_method=async_update_data, + # Polling interval. Will only be polled if there are subscribers. + update_interval=SCAN_INTERVAL, + ) + + # Fetch initial data so we have data when entities subscribe + await coordinator.async_config_entry_first_refresh() + + hass.data[DOMAIN][entry.entry_id] = { + KEY_INVERTER: inverter, + KEY_COORDINATOR: coordinator, + KEY_DEVICE_INFO: device_info, + } + + entry.async_on_unload(entry.add_update_listener(update_listener)) + + hass.config_entries.async_setup_platforms(entry, PLATFORMS) + + return True + + +async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool: + """Unload a config entry.""" + unload_ok = await hass.config_entries.async_unload_platforms( + config_entry, PLATFORMS + ) + + if unload_ok: + hass.data[DOMAIN].pop(config_entry.entry_id) + + return unload_ok + + +async def update_listener(hass: HomeAssistant, config_entry: ConfigEntry) -> None: + """Handle options update.""" + await hass.config_entries.async_reload(config_entry.entry_id) diff --git a/homeassistant/components/goodwe/config_flow.py b/homeassistant/components/goodwe/config_flow.py new file mode 100644 index 00000000000..ab82d4c453f --- /dev/null +++ b/homeassistant/components/goodwe/config_flow.py @@ -0,0 +1,53 @@ +"""Config flow to configure Goodwe inverters using their local API.""" +from __future__ import annotations + +import logging + +from goodwe import InverterError, connect +import voluptuous as vol + +from homeassistant import config_entries +from homeassistant.const import CONF_HOST +from homeassistant.data_entry_flow import FlowResult + +from .const import CONF_MODEL_FAMILY, DEFAULT_NAME, DOMAIN + +CONFIG_SCHEMA = vol.Schema( + { + vol.Required(CONF_HOST): str, + } +) + +_LOGGER = logging.getLogger(__name__) + + +class GoodweFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): + """Handle a Goodwe config flow.""" + + VERSION = 1 + + async def async_step_user(self, user_input: dict | None = None) -> FlowResult: + """Handle a flow initialized by the user.""" + errors = {} + if user_input is not None: + host = user_input[CONF_HOST] + + try: + inverter = await connect(host=host, retries=10) + except InverterError: + errors[CONF_HOST] = "connection_error" + else: + await self.async_set_unique_id(inverter.serial_number) + self._abort_if_unique_id_configured() + + return self.async_create_entry( + title=DEFAULT_NAME, + data={ + CONF_HOST: host, + CONF_MODEL_FAMILY: type(inverter).__name__, + }, + ) + + return self.async_show_form( + step_id="user", data_schema=CONFIG_SCHEMA, errors=errors + ) diff --git a/homeassistant/components/goodwe/const.py b/homeassistant/components/goodwe/const.py new file mode 100644 index 00000000000..0e40601ccdb --- /dev/null +++ b/homeassistant/components/goodwe/const.py @@ -0,0 +1,17 @@ +"""Constants for the Goodwe component.""" +from datetime import timedelta + +from homeassistant.const import Platform + +DOMAIN = "goodwe" + +PLATFORMS = [Platform.NUMBER, Platform.SELECT, Platform.SENSOR] + +DEFAULT_NAME = "GoodWe" +SCAN_INTERVAL = timedelta(seconds=10) + +CONF_MODEL_FAMILY = "model_family" + +KEY_INVERTER = "inverter" +KEY_COORDINATOR = "coordinator" +KEY_DEVICE_INFO = "device_info" diff --git a/homeassistant/components/goodwe/manifest.json b/homeassistant/components/goodwe/manifest.json new file mode 100644 index 00000000000..25c56a6f29c --- /dev/null +++ b/homeassistant/components/goodwe/manifest.json @@ -0,0 +1,12 @@ +{ + "domain": "goodwe", + "name": "GoodWe Inverter", + "documentation": "https://www.home-assistant.io/integrations/goodwe", + "codeowners": [ + "@mletenay", + "@starkillerOG" + ], + "requirements": ["goodwe==0.2.10"], + "config_flow": true, + "iot_class": "local_polling" +} \ No newline at end of file diff --git a/homeassistant/components/goodwe/number.py b/homeassistant/components/goodwe/number.py new file mode 100644 index 00000000000..790af1afaa8 --- /dev/null +++ b/homeassistant/components/goodwe/number.py @@ -0,0 +1,109 @@ +"""GoodWe PV inverter numeric settings entities.""" +from __future__ import annotations + +from collections.abc import Awaitable, Callable +from dataclasses import dataclass +import logging + +from goodwe import Inverter, InverterError + +from homeassistant.components.number import NumberEntity, NumberEntityDescription +from homeassistant.const import ENTITY_CATEGORY_CONFIG, PERCENTAGE, POWER_WATT +from homeassistant.helpers.entity import DeviceInfo + +from .const import DOMAIN, KEY_DEVICE_INFO, KEY_INVERTER + +_LOGGER = logging.getLogger(__name__) + + +@dataclass +class GoodweNumberEntityDescriptionBase: + """Required values when describing Goodwe number entities.""" + + getter: Callable[[Inverter], Awaitable[int]] + setter: Callable[[Inverter, int], Awaitable[None]] + + +@dataclass +class GoodweNumberEntityDescription( + NumberEntityDescription, GoodweNumberEntityDescriptionBase +): + """Class describing Goodwe number entities.""" + + +NUMBERS = ( + GoodweNumberEntityDescription( + key="grid_export_limit", + name="Grid export limit", + icon="mdi:transmission-tower", + entity_category=ENTITY_CATEGORY_CONFIG, + unit_of_measurement=POWER_WATT, + getter=lambda inv: inv.get_grid_export_limit(), + setter=lambda inv, val: inv.set_grid_export_limit(val), + step=100, + min_value=0, + max_value=10000, + ), + GoodweNumberEntityDescription( + key="battery_discharge_depth", + name="Depth of discharge (on-grid)", + icon="mdi:battery-arrow-down", + entity_category=ENTITY_CATEGORY_CONFIG, + unit_of_measurement=PERCENTAGE, + getter=lambda inv: inv.get_ongrid_battery_dod(), + setter=lambda inv, val: inv.set_ongrid_battery_dod(val), + step=1, + min_value=0, + max_value=99, + ), +) + + +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up the inverter select entities from a config entry.""" + inverter = hass.data[DOMAIN][config_entry.entry_id][KEY_INVERTER] + device_info = hass.data[DOMAIN][config_entry.entry_id][KEY_DEVICE_INFO] + + entities = [] + + for description in NUMBERS: + try: + current_value = await description.getter(inverter) + except InverterError: + # Inverter model does not support this setting + _LOGGER.debug("Could not read inverter setting %s", description.key) + continue + + entities.append( + InverterNumberEntity(device_info, description, inverter, current_value), + ) + + async_add_entities(entities) + + +class InverterNumberEntity(NumberEntity): + """Inverter numeric setting entity.""" + + _attr_should_poll = False + entity_description: GoodweNumberEntityDescription + + def __init__( + self, + device_info: DeviceInfo, + description: GoodweNumberEntityDescription, + inverter: Inverter, + current_value: int, + ) -> None: + """Initialize the number inverter setting entity.""" + self.entity_description = description + self._attr_unique_id = f"{DOMAIN}-{description.key}-{inverter.serial_number}" + self._attr_device_info = device_info + self._attr_value = float(current_value) + self._inverter: Inverter = inverter + + async def async_set_value(self, value: float) -> None: + """Set new value.""" + if self.entity_description.setter: + await self.entity_description.setter(self._inverter, int(value)) + self._attr_value = value + self.async_write_ha_state() diff --git a/homeassistant/components/goodwe/select.py b/homeassistant/components/goodwe/select.py new file mode 100644 index 00000000000..214b672111a --- /dev/null +++ b/homeassistant/components/goodwe/select.py @@ -0,0 +1,79 @@ +"""GoodWe PV inverter selection settings entities.""" +import logging + +from goodwe import Inverter, InverterError + +from homeassistant.components.select import SelectEntity, SelectEntityDescription +from homeassistant.const import ENTITY_CATEGORY_CONFIG +from homeassistant.helpers.entity import DeviceInfo + +from .const import DOMAIN, KEY_DEVICE_INFO, KEY_INVERTER + +_LOGGER = logging.getLogger(__name__) + + +INVERTER_OPERATION_MODES = [ + "General mode", + "Off grid mode", + "Backup mode", + "Eco mode", +] + +OPERATION_MODE = SelectEntityDescription( + key="operation_mode", + name="Inverter operation mode", + icon="mdi:solar-power", + entity_category=ENTITY_CATEGORY_CONFIG, +) + + +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up the inverter select entities from a config entry.""" + inverter = hass.data[DOMAIN][config_entry.entry_id][KEY_INVERTER] + device_info = hass.data[DOMAIN][config_entry.entry_id][KEY_DEVICE_INFO] + + # read current operating mode from the inverter + try: + active_mode = await inverter.get_operation_mode() + except InverterError: + # Inverter model does not support this setting + _LOGGER.debug("Could not read inverter operation mode") + else: + if 0 <= active_mode < len(INVERTER_OPERATION_MODES): + async_add_entities( + [ + InverterOperationModeEntity( + device_info, + OPERATION_MODE, + inverter, + INVERTER_OPERATION_MODES[active_mode], + ) + ] + ) + + +class InverterOperationModeEntity(SelectEntity): + """Entity representing the inverter operation mode.""" + + _attr_should_poll = False + + def __init__( + self, + device_info: DeviceInfo, + description: SelectEntityDescription, + inverter: Inverter, + current_mode: str, + ) -> None: + """Initialize the inverter operation mode setting entity.""" + self.entity_description = description + self._attr_unique_id = f"{DOMAIN}-{description.key}-{inverter.serial_number}" + self._attr_device_info = device_info + self._attr_options = INVERTER_OPERATION_MODES + self._attr_current_option = current_mode + self._inverter: Inverter = inverter + + async def async_select_option(self, option: str) -> None: + """Change the selected option.""" + await self._inverter.set_operation_mode(INVERTER_OPERATION_MODES.index(option)) + self._attr_current_option = option + self.async_write_ha_state() diff --git a/homeassistant/components/goodwe/sensor.py b/homeassistant/components/goodwe/sensor.py new file mode 100644 index 00000000000..b6a4b739553 --- /dev/null +++ b/homeassistant/components/goodwe/sensor.py @@ -0,0 +1,176 @@ +"""Support for GoodWe inverter via UDP.""" +from __future__ import annotations + +from collections.abc import Callable +from dataclasses import dataclass +from typing import Any + +from goodwe import Inverter, Sensor, SensorKind + +from homeassistant.components.sensor import ( + STATE_CLASS_MEASUREMENT, + STATE_CLASS_TOTAL_INCREASING, + SensorEntity, + SensorEntityDescription, +) +from homeassistant.const import ( + DEVICE_CLASS_BATTERY, + DEVICE_CLASS_CURRENT, + DEVICE_CLASS_ENERGY, + DEVICE_CLASS_POWER, + DEVICE_CLASS_TEMPERATURE, + DEVICE_CLASS_VOLTAGE, + ELECTRIC_CURRENT_AMPERE, + ELECTRIC_POTENTIAL_VOLT, + ENERGY_KILO_WATT_HOUR, + ENTITY_CATEGORY_DIAGNOSTIC, + FREQUENCY_HERTZ, + PERCENTAGE, + POWER_WATT, + TEMP_CELSIUS, +) +from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.update_coordinator import ( + CoordinatorEntity, + DataUpdateCoordinator, +) + +from .const import DOMAIN, KEY_COORDINATOR, KEY_DEVICE_INFO, KEY_INVERTER + +# Sensor name of battery SoC +BATTERY_SOC = "battery_soc" + +_MAIN_SENSORS = ( + "ppv", + "house_consumption", + "active_power", + "battery_soc", + "e_day", + "e_total", + "meter_e_total_exp", + "meter_e_total_imp", + "e_bat_charge_total", + "e_bat_discharge_total", +) + +_ICONS = { + SensorKind.PV: "mdi:solar-power", + SensorKind.AC: "mdi:power-plug-outline", + SensorKind.UPS: "mdi:power-plug-off-outline", + SensorKind.BAT: "mdi:battery-high", + SensorKind.GRID: "mdi:transmission-tower", +} + + +@dataclass +class GoodweSensorEntityDescription(SensorEntityDescription): + """Class describing Goodwe sensor entities.""" + + value: Callable[[str, Any, Any], Any] = lambda sensor, prev, val: val + + +_DESCRIPTIONS = { + "A": GoodweSensorEntityDescription( + key="A", + device_class=DEVICE_CLASS_CURRENT, + state_class=STATE_CLASS_MEASUREMENT, + native_unit_of_measurement=ELECTRIC_CURRENT_AMPERE, + ), + "V": GoodweSensorEntityDescription( + key="V", + device_class=DEVICE_CLASS_VOLTAGE, + state_class=STATE_CLASS_MEASUREMENT, + native_unit_of_measurement=ELECTRIC_POTENTIAL_VOLT, + ), + "W": GoodweSensorEntityDescription( + key="W", + device_class=DEVICE_CLASS_POWER, + state_class=STATE_CLASS_MEASUREMENT, + native_unit_of_measurement=POWER_WATT, + ), + "kWh": GoodweSensorEntityDescription( + key="kWh", + device_class=DEVICE_CLASS_ENERGY, + state_class=STATE_CLASS_TOTAL_INCREASING, + native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, + value=lambda sensor, prev, val: prev if "total" in sensor and not val else val, + ), + "C": GoodweSensorEntityDescription( + key="C", + device_class=DEVICE_CLASS_TEMPERATURE, + state_class=STATE_CLASS_MEASUREMENT, + native_unit_of_measurement=TEMP_CELSIUS, + ), + "Hz": GoodweSensorEntityDescription( + key="Hz", + device_class=DEVICE_CLASS_VOLTAGE, + state_class=STATE_CLASS_MEASUREMENT, + native_unit_of_measurement=FREQUENCY_HERTZ, + ), + "%": GoodweSensorEntityDescription( + key="%", + state_class=STATE_CLASS_MEASUREMENT, + native_unit_of_measurement=PERCENTAGE, + ), +} +DIAG_SENSOR = GoodweSensorEntityDescription( + key="_", + state_class=STATE_CLASS_MEASUREMENT, +) + + +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up the GoodWe inverter from a config entry.""" + entities = [] + inverter = hass.data[DOMAIN][config_entry.entry_id][KEY_INVERTER] + coordinator = hass.data[DOMAIN][config_entry.entry_id][KEY_COORDINATOR] + device_info = hass.data[DOMAIN][config_entry.entry_id][KEY_DEVICE_INFO] + + # Individual inverter sensors entities + entities.extend( + InverterSensor(coordinator, device_info, inverter, sensor) + for sensor in inverter.sensors() + if not sensor.id_.startswith("xx") + ) + + async_add_entities(entities) + + +class InverterSensor(CoordinatorEntity, SensorEntity): + """Entity representing individual inverter sensor.""" + + def __init__( + self, + coordinator: DataUpdateCoordinator, + device_info: DeviceInfo, + inverter: Inverter, + sensor: Sensor, + ) -> None: + """Initialize an inverter sensor.""" + super().__init__(coordinator) + self._attr_name = sensor.name.strip() + self._attr_unique_id = f"{DOMAIN}-{sensor.id_}-{inverter.serial_number}" + self._attr_device_info = device_info + self._attr_entity_category = ( + ENTITY_CATEGORY_DIAGNOSTIC if sensor.id_ not in _MAIN_SENSORS else None + ) + self.entity_description = _DESCRIPTIONS.get(sensor.unit, DIAG_SENSOR) + if not self.entity_description.native_unit_of_measurement: + self._attr_native_unit_of_measurement = sensor.unit + self._attr_icon = _ICONS.get(sensor.kind) + # Set the inverter SoC as main device battery sensor + if sensor.id_ == BATTERY_SOC: + self._attr_device_class = DEVICE_CLASS_BATTERY + self._sensor = sensor + self._previous_value = None + + @property + def native_value(self): + """Return the value reported by the sensor.""" + value = self.entity_description.value( + self._sensor.id_, + self._previous_value, + self.coordinator.data.get(self._sensor.id_, self._previous_value), + ) + self._previous_value = value + return value diff --git a/homeassistant/components/goodwe/strings.json b/homeassistant/components/goodwe/strings.json new file mode 100644 index 00000000000..61da6961df1 --- /dev/null +++ b/homeassistant/components/goodwe/strings.json @@ -0,0 +1,20 @@ +{ + "config": { + "step": { + "user": { + "title": "GoodWe inverter", + "description": "Connect to inverter", + "data": { + "host": "[%key:common::config_flow::data::ip%]" + } + } + }, + "error": { + "connection_error": "[%key:common::config_flow::error::cannot_connect%]" + }, + "abort": { + "already_configured": "[%key:common::config_flow::abort::already_configured_device%]", + "already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]" + } + } +} diff --git a/homeassistant/components/goodwe/translations/en.json b/homeassistant/components/goodwe/translations/en.json new file mode 100644 index 00000000000..88dc65e4e68 --- /dev/null +++ b/homeassistant/components/goodwe/translations/en.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "Device is already configured", + "already_in_progress": "Configuration flow is already in progress" + }, + "error": { + "connection_error": "Failed to connect" + }, + "flow_title": "GoodWe", + "step": { + "user": { + "data": { + "host": "IP Address" + }, + "description": "Connect to inverter", + "title": "GoodWe inverter" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index c2c2ce53ed7..a64a3bf9b53 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -118,6 +118,7 @@ FLOWS = [ "glances", "goalzero", "gogogate2", + "goodwe", "google_travel_time", "gpslogger", "gree", diff --git a/requirements_all.txt b/requirements_all.txt index b57616d1d83..54b0c8a6c32 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -742,6 +742,9 @@ gntp==1.0.3 # homeassistant.components.goalzero goalzero==0.2.1 +# homeassistant.components.goodwe +goodwe==0.2.10 + # homeassistant.components.google google-api-python-client==1.6.4 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 0d88f76c740..4de8171a4a8 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -467,6 +467,9 @@ glances_api==0.2.0 # homeassistant.components.goalzero goalzero==0.2.1 +# homeassistant.components.goodwe +goodwe==0.2.10 + # homeassistant.components.google google-api-python-client==1.6.4 diff --git a/tests/components/goodwe/__init__.py b/tests/components/goodwe/__init__.py new file mode 100644 index 00000000000..21c1ce6f543 --- /dev/null +++ b/tests/components/goodwe/__init__.py @@ -0,0 +1 @@ +"""Tests for the Goodwe integration.""" diff --git a/tests/components/goodwe/test_config_flow.py b/tests/components/goodwe/test_config_flow.py new file mode 100644 index 00000000000..89dfd68a783 --- /dev/null +++ b/tests/components/goodwe/test_config_flow.py @@ -0,0 +1,107 @@ +"""Test the Goodwe config flow.""" +from unittest.mock import AsyncMock, patch + +from goodwe import InverterError + +from homeassistant.components.goodwe.const import ( + CONF_MODEL_FAMILY, + DEFAULT_NAME, + DOMAIN, +) +from homeassistant.config_entries import SOURCE_USER +from homeassistant.const import CONF_HOST +from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import ( + RESULT_TYPE_ABORT, + RESULT_TYPE_CREATE_ENTRY, + RESULT_TYPE_FORM, +) + +from tests.common import MockConfigEntry + +TEST_HOST = "1.2.3.4" +TEST_SERIAL = "123456789" + + +def mock_inverter(): + """Get a mock object of the inverter.""" + goodwe_inverter = AsyncMock() + goodwe_inverter.serial_number = TEST_SERIAL + return goodwe_inverter + + +async def test_manual_setup(hass: HomeAssistant): + """Test manually setting up.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER} + ) + assert result["type"] == RESULT_TYPE_FORM + assert result["step_id"] == "user" + assert not result["errors"] + + with patch( + "homeassistant.components.goodwe.config_flow.connect", + return_value=mock_inverter(), + ), patch( + "homeassistant.components.goodwe.async_setup_entry", return_value=True + ) as mock_setup_entry: + result = await hass.config_entries.flow.async_configure( + result["flow_id"], {CONF_HOST: TEST_HOST} + ) + await hass.async_block_till_done() + + assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["title"] == DEFAULT_NAME + assert result["data"] == { + CONF_HOST: TEST_HOST, + CONF_MODEL_FAMILY: "AsyncMock", + } + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_manual_setup_already_exists(hass: HomeAssistant): + """Test manually setting up and the device already exists.""" + entry = MockConfigEntry( + domain=DOMAIN, data={CONF_HOST: TEST_HOST}, unique_id=TEST_SERIAL + ) + entry.add_to_hass(hass) + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER} + ) + assert result["type"] == RESULT_TYPE_FORM + assert result["step_id"] == "user" + assert not result["errors"] + + with patch( + "homeassistant.components.goodwe.config_flow.connect", + return_value=mock_inverter(), + ), patch("homeassistant.components.goodwe.async_setup_entry", return_value=True): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], {CONF_HOST: TEST_HOST} + ) + await hass.async_block_till_done() + + assert result["type"] == RESULT_TYPE_ABORT + assert result["reason"] == "already_configured" + + +async def test_manual_setup_device_offline(hass: HomeAssistant): + """Test manually setting up, device offline.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER} + ) + assert result["type"] == RESULT_TYPE_FORM + assert result["step_id"] == "user" + assert not result["errors"] + + with patch( + "homeassistant.components.goodwe.config_flow.connect", + side_effect=InverterError, + ): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], {CONF_HOST: TEST_HOST} + ) + await hass.async_block_till_done() + + assert result["type"] == RESULT_TYPE_FORM + assert result["errors"] == {CONF_HOST: "connection_error"}