Add Goodwe solar inverter integration (#58503)
Co-authored-by: Martin Hjelmare <marhje52@gmail.com> Co-authored-by: starkillerOG <starkiller.og@gmail.com>pull/63411/head
parent
841e22258d
commit
f0acbabd48
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
|
@ -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
|
||||
)
|
|
@ -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"
|
|
@ -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"
|
||||
}
|
|
@ -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()
|
|
@ -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()
|
|
@ -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
|
|
@ -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%]"
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -118,6 +118,7 @@ FLOWS = [
|
|||
"glances",
|
||||
"goalzero",
|
||||
"gogogate2",
|
||||
"goodwe",
|
||||
"google_travel_time",
|
||||
"gpslogger",
|
||||
"gree",
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
"""Tests for the Goodwe integration."""
|
|
@ -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"}
|
Loading…
Reference in New Issue