master 40e29983fe38 cached
29 files
105.5 KB
24.7k tokens
69 symbols
1 requests
Download .txt
Repository: mletenay/home-assistant-goodwe-inverter
Branch: master
Commit: 40e29983fe38
Files: 29
Total size: 105.5 KB

Directory structure:
gitextract_ho4ycwf4/

├── .github/
│   └── workflows/
│       ├── hassfest.yaml
│       └── validate.yaml
├── .gitignore
├── LICENSE
├── README.md
├── custom_components/
│   └── goodwe/
│       ├── __init__.py
│       ├── button.py
│       ├── config_flow.py
│       ├── const.py
│       ├── coordinator.py
│       ├── diagnostics.py
│       ├── icons.json
│       ├── manifest.json
│       ├── number.py
│       ├── select.py
│       ├── sensor.py
│       ├── services.py
│       ├── services.yaml
│       ├── strings.json
│       ├── switch.py
│       └── translations/
│           ├── cs.json
│           ├── de.json
│           ├── en.json
│           ├── es.json
│           └── sk.json
├── hacs.json
├── info.md
├── inverter_scan.py
└── inverter_test.py

================================================
FILE CONTENTS
================================================

================================================
FILE: .github/workflows/hassfest.yaml
================================================
name: Validate with hassfest

on:
  push:
  pull_request:
  schedule:
    - cron: "0 0 * * *"

jobs:
  validate:
    runs-on: "ubuntu-latest"
    steps:
      - uses: "actions/checkout@v3"
      - uses: home-assistant/actions/hassfest@master


================================================
FILE: .github/workflows/validate.yaml
================================================
name: Validate

on:
  push:
  pull_request:
  schedule:
    - cron: "0 0 * * *"
  workflow_dispatch:

permissions: {}

jobs:
  validate-hacs:
    runs-on: "ubuntu-latest"
    steps:
      - name: HACS validation
        uses: "hacs/action@main"
        with:
          category: "integration"
          ignore: "brands"


================================================
FILE: .gitignore
================================================
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class

# C extensions
*.so

# Distribution / packaging
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
pip-wheel-metadata/
share/python-wheels/
*.egg-info/
.installed.cfg
*.egg
MANIFEST

# PyInstaller
#  Usually these files are written by a python script from a template
#  before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
*.spec

# Installer logs
pip-log.txt
pip-delete-this-directory.txt

# Unit test / coverage reports
htmlcov/
.tox/
.nox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*.cover
*.py,cover
.hypothesis/
.pytest_cache/

# Translations
*.mo
*.pot

# Django stuff:
*.log
local_settings.py
db.sqlite3
db.sqlite3-journal

# Flask stuff:
instance/
.webassets-cache

# Scrapy stuff:
.scrapy

# Sphinx documentation
docs/_build/

# PyBuilder
target/

# Jupyter Notebook
.ipynb_checkpoints

# IPython
profile_default/
ipython_config.py

# pyenv
.python-version

# pipenv
#   According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
#   However, in case of collaboration, if having platform-specific dependencies or dependencies
#   having no cross-platform support, pipenv may install dependencies that don't work, or not
#   install all needed dependencies.
#Pipfile.lock

# PEP 582; used by e.g. github.com/David-OConnor/pyflow
__pypackages__/

# Celery stuff
celerybeat-schedule
celerybeat.pid

# SageMath parsed files
*.sage.py

# Environments
.env
.venv
env/
venv/
ENV/
env.bak/
venv.bak/

# Spyder project settings
.spyderproject
.spyproject

# Rope project settings
.ropeproject

# mkdocs documentation
/site

# mypy
.mypy_cache/
.dmypy.json
dmypy.json

# Pyre type checker
.pyre/
.idea

================================================
FILE: LICENSE
================================================
MIT License

Copyright (c) 2020 mletenay

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.


================================================
FILE: README.md
================================================
[!["Buy Me A Coffee"](https://www.buymeacoffee.com/assets/img/custom_images/orange_img.png)](https://www.buymeacoffee.com/mletenay)
[![hacs_badge](https://img.shields.io/badge/HACS-Default-orange.svg)](https://github.com/hacs/integration)
[![Build Status](https://github.com/mletenay/home-assistant-goodwe-inverter/actions/workflows/hassfest.yaml/badge.svg)](https://github.com/mletenay/home-assistant-goodwe-inverter/actions/workflows/hassfest.yaml)
![GitHub Release](https://img.shields.io/github/v/release/mletenay/home-assistant-goodwe-inverter)
<img src="https://img.shields.io/badge/dynamic/json?color=41BDF5&logo=home-assistant&label=integration%20usage&suffix=%20installs&cacheSeconds=15600&url=https://analytics.home-assistant.io/custom_integrations.json&query=$.goodwe.total">

## GoodWe solar inverter for Home Assistant (experimental)

Support for Goodwe solar inverters is present as native integration of [Home Assistant](https://www.home-assistant.io/integrations/goodwe/) since its release 2022.2 and is recommended for most users.

This custom component is experimental version with features not (yet) present in standard HA's integration and is intended for users with specific needs and early adopters of new features.
Use at own risk.

### Differences between this HACS and native HA integration

- EMS modes
- Special work modes `Eco charge mode` and `Eco discharge mode` (24/7 with defined power and SoC).
- Network configuration parameters `Port`, `Modbus id`, `Scan iterval`, `Network retry attempts`, `Network request timeout`.
- Input `SoC upper limit`, `DoD (backup)`
- Switch `DOD holding`, `Export Limit`. `Load Control`, `Backup supply`
- Switch and SoC/Power inputs for `Fast Charging` functionality.
- `Start inverter` and `Stop inverter` buttons for grid-only inverters.
- Services for getting/setting inverter configuration parameters

### Migration from HACS to HA

If you have been using this custom component and want to migrate to standard HA integration, the migration is straightforward. Just remove the integration from HACS (press Ignore and force uninstall despite the warning the integration is still configured). Afrer restart of Home Assistant, the standard Goodwe integration will start and all your existing settings, entity names, history and statistics should be preserved.

(If you uninstall the integration first, then uninstall HACS component and install integration back again, it will also work, but you will probably loose some history and settings since HA integration uses slightly different default entity names.)

## EMS modes

The integration exposes inverter's EMS mode and EMS power (limit) settings.
The following list should explain individual modes and their behavior.
(The `Xset`/`Xmax` variables below are values of the `EMS power` setting. )

- **Auto**
  - _Scenario:_ Self-use.
  - `PBattery = PInv - Pmeter - Ppv` (Discharge/Charge)
  - The battery power is controlled by the meter power when the meter communication is normal.

- **Charge PV**
  - _Scenario:_ Control the battery to keep charging.
  - `PBattery = Xmax + PV` (Charge)
  - Xmax is to allow the power to be taken from the grid, and PV power is preferred. When set to 0, only PV power is used. Charging power will be limited by charging current limit.
  - _Interpretation:_ Charge Battery from PV (high priority) or Grid (low priority); EmsPowerSet = negative ESS ActivePower (if possible because of PV).
  - Grid: low priority, PV: high priority, Battery: Charge Mode, The control object is 'Grid'

- **Discharge PV**
  - _Scenario:_ Control the battery to keep discharging.
  - `PBattery = Xmax` (Discharge)
  - Xmax is the allowable discharge power of the battery. When the power fed into the grid is limited, PV power will be used first.
  - _Interpretation:_ ESS ActivePower = PV power + EmsPowerSet (i.e. battery discharge); useful for surplus feed-to-grid.
  - PV: high priority, Battery: low priority, Grid: Energy Out Mode, The control object is 'Battery'

- **Import AC**
  - _Scenario:_ The inverter is used as a unit for power grid energy scheduling.
  - `PBattery = Xset + PV` (Charge)
  - Xset refers to the power purchased from the power grid. The power purchased from the grid is preferred. If the PV power is too large, the MPPT power will be limited. (grid side load is not considered)
  - _Interpretation:_ Charge Battery from Grid (high priority) or PV (low priority); EmsPowerSet = negative ESS ActivePower; as long as BMS_CHARGE_MAX_CURRENT is > 0, no AC-Power is exported; when BMS_CHARGE_MAX_CURRENT == 0, PV surplus feed in starts!
  - Grid: high priority, PV: low priority, Battery: Charge Mode, The control object is 'Grid'

- **Export AC**
  - _Scenario:_ The inverter is used as a unit for power grid energy scheduling.
  - `PBattery = Xset` (Discharge)
  - Xset is to sell power to the grid. PV power is preferred. When PV energy is insufficient, the battery will discharge. PV power will be limited by x. (grid side load is not considered)
  - _Interpretation:_ EmsPowerSet = positive ESS ActivePower. But PV will be limited, i.e. remaining power is not used to charge battery.
  - PV: high priority, Battery: low priority, Grid: Energy Out Mode, The control object is 'Grid'

- **Conserve**
  - _Scenario:_ Off-grid reservation mode.
  - `PBattery = PV` (Charge)
  - In on-grid mode, the battery is continuously charged, and only PV power (AC Couple model takes 10% of the rated power of the power grid) is used. The battery can only discharge in off-grid mode.

- **Off-Grid**
  - _Scenario:_ Off-Grid Mode.
  - `PBattery = Pbackup - Ppv` (Charge/Discharge)
  - Forced off-grid operation.

- **Battery Standby**
  - _Scenario:_ The inverter is used as a unit for power grid energy scheduling.
  - `PBattery = 0` (Standby)
  - The battery does not charge and discharge

- **Buy Power**
  - _Scenario:_ Regional energy management.
  - `PBattery = PInv - (Pmeter + Xset) - Ppv` (Charge/Discharge)
  - When the meter communication is normal, the power purchased from the power grid is controlled as Xset. When the PV power is too large, the MPPT power will be limited. When the load is too large, the battery will discharge.
  - _Interpretation:_ Control power at the point of common coupling.
  - Grid: high priority, PV: low priority, Battery: Energy In and Out Mode, The control object is 'Grid'

- **Sell Power**
  - _Scenario:_ Regional energy management.
  - `PBattery = PInv - (Pmeter - Xset) - Ppv` (Charge/Discharge)
  - When the communication of electricity meter is normal, the power sold from the power grid is controlled as Xset, PV power is preferred, and the battery discharges when PV energy is insufficient.PV power will be limited by Xset.
  - _Interpretation:_ Control power at the point of common coupling.
  - PV: high priority, Battery: low priority, Grid: Energy Out Mode, The control object is 'Grid'

- **Charge Battery**
  - _Scenario:_ Force the battery to work at set power value.
  - `PBattery = Xset` (Charge)
  - Xset is the charging power of the battery. PV power is preferred. When PV power is insufficient, it will buy power from the power grid. The charging power is also affected by the charging current limit.
  - _Interpretation:_ Charge Battery from PV (high priority) or Grid (low priority); priorities are inverted compared to IMPORT_AC.
  - PV: high priority, Grid: low priority, Battery: Energy In Mode, The control object is 'Battery'

- **Discharge Battery**
  - _Scenario:_ Force the battery to work at set power value.
  - `PBattery = Xset` (Discharge)
  - Xset is the discharge power of the battery, and the battery discharge has priority. If the PV power is too large, MPPT will be limited. Discharge power is also affected by discharge current limit.
  - _Interpretation:_ ???
  - PV: low priority, Battery: high priority, Grid: Energy In Mode, The control object is 'Battery'

- **Stopped**
  - _Scenario:_ System shutdown.
  - Stop working and turn to wait mode

## Home Assistant Energy Dashboard

The integration provides several values suitable for the energy dashboard introduced to HA in v2021.8.
The best supported are the inverters of ET/EH families, where the sensors `meter_e_total_exp`, `meter_e_total_imp`, `e_total`, `e_bat_charge_total` and `e_bat_discharge_total` are the most suitable for the dashboard measurements and statistics.
For the other inverter families, if such sensors are not directly available from the inverter, they can be calculated, see paragraph below.

## Cumulative energy values

The sensor values reported by the inverter are instant measurements.
To report summary (energy) values like daily/monthly sell or buy (in kWh), these values have to be aggregated over time.

[Riemann Sum](https://www.home-assistant.io/integrations/integration/) integration can be used to convert these instant (W) values into cumulative values (Wh).
[Utility Meter](https://www.home-assistant.io/integrations/utility_meter) can report these values as human readable statistical values.
[Template Sensor](https://www.home-assistant.io/integrations/template/) can be used to separate buy and sell values.

```YAML
sensor:
  - platform: template
    sensors:
      # Template sensor for values of energy bought (active_power < 0)
      energy_buy:
        device_class: power
        friendly_name: "Energy Buy"
        unit_of_measurement: 'W'
        value_template: >-
          {% if states('sensor.goodwe_active_power')|float < 0 %}
            {{ states('sensor.goodwe_active_power')|float * -1 }}
          {% else %}
            {{ 0 }}
          {% endif %}
      # Template sensor for values of energy sold (active_power > 0)
      energy_sell:
        device_class: power
        friendly_name: "Energy Sell"
        unit_of_measurement: 'W'
        value_template: >-
          {% if states('sensor.goodwe_active_power')|float > 0 %}
            {{ states('sensor.goodwe_active_power')|float }}
          {% else %}
            {{ 0 }}
          {% endif %}

  # Sensor for Riemann sum of energy bought (W -> kWh)
  - platform: integration
    source: sensor.energy_buy
    name: energy_buy_sum
    unit_prefix: k
    round: 1
    method: left
  # Sensor for Riemann sum of energy sold (W -> kWh)
  - platform: integration
    source: sensor.energy_sell
    name: energy_sell_sum
    unit_prefix: k
    round: 1
    method: left

utility_meter:
  energy_buy_daily:
    source: sensor.energy_buy_sum
    cycle: daily
  energy_buy_monthly:
    source: sensor.energy_buy_sum
    cycle: monthly
  energy_sell_daily:
    source: sensor.energy_sell_sum
    cycle: daily
  energy_sell_monthly:
    source: sensor.energy_sell_sum
    cycle: monthly
  house_consumption_daily:
    source: sensor.house_consumption_sum
    cycle: daily
  house_consumption_monthly:
    source: sensor.house_consumption_sum
    cycle: monthly
```

## Troubleshooting

If you observe any problems or cannot make it work with your inverter at all, try to increase logging level of the component and check the log files.

```YAML
logger:
  default: warning
  logs:
    custom_components.goodwe: debug
    goodwe: debug
```

## Source code

The source code implementing the actual communication with GoodWe inverters (which was originally part of this plugin) was extracted and moved to standalone [PyPI library](https://pypi.org/project/goodwe/). This repository now contains only the HomeAssistant specific code.

## Inverter discovery and communication testing

To test whether the inverter properly responds to UDP request, just execute the `inverter_test.py` script in your python (3.8+) environment.
The `inverter_scan.py` script can be used to discover inverter(s) on your local network.

## References and inspiration

- https://github.com/marcelblijleven/goodwe
- https://www.photovoltaikforum.com/core/attachment/342066-bluetooth-firmware-update-string-storage-de-v002-pdf/
- https://github.com/robbinjanssen/home-assistant-omnik-inverter


================================================
FILE: custom_components/goodwe/__init__.py
================================================
"""The Goodwe inverter component."""

from goodwe import Inverter, InverterError, connect
from goodwe.const import GOODWE_TCP_PORT, GOODWE_UDP_PORT
from homeassistant.const import CONF_HOST, CONF_PORT, CONF_PROTOCOL, CONF_SCAN_INTERVAL
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryNotReady
from homeassistant.helpers.device_registry import DeviceInfo

from .config_flow import GoodweFlowHandler
from .const import (
    CONF_KEEP_ALIVE,
    CONF_MODBUS_ID,
    CONF_MODEL_FAMILY,
    CONF_NETWORK_RETRIES,
    CONF_NETWORK_TIMEOUT,
    DEFAULT_MODBUS_ID,
    DEFAULT_NETWORK_RETRIES,
    DEFAULT_NETWORK_TIMEOUT,
    DOMAIN,
    PLATFORMS,
)
from .coordinator import GoodweConfigEntry, GoodweRuntimeData, GoodweUpdateCoordinator
from .services import async_setup_services, async_unload_services


async def async_setup_entry(hass: HomeAssistant, entry: GoodweConfigEntry) -> bool:
    """Set up the Goodwe components from a config entry."""
    hass.data.setdefault(DOMAIN, {})
    host = entry.options.get(CONF_HOST, entry.data[CONF_HOST])
    protocol = entry.options.get(CONF_PROTOCOL, entry.data.get(CONF_PROTOCOL, "UDP"))
    port = entry.options.get(
        CONF_PORT,
        entry.data.get(
            CONF_PORT, GOODWE_TCP_PORT if protocol == "TCP" else GOODWE_UDP_PORT
        ),
    )
    keep_alive = entry.options.get(CONF_KEEP_ALIVE, False)
    model_family = entry.options.get(CONF_MODEL_FAMILY, entry.data[CONF_MODEL_FAMILY])
    network_retries = entry.options.get(CONF_NETWORK_RETRIES, DEFAULT_NETWORK_RETRIES)
    network_timeout = entry.options.get(CONF_NETWORK_TIMEOUT, DEFAULT_NETWORK_TIMEOUT)
    modbus_id = entry.options.get(CONF_MODBUS_ID, DEFAULT_MODBUS_ID)

    # Connect to Goodwe inverter
    try:
        inverter = await connect(
            host=host,
            port=port,
            family=model_family,
            comm_addr=modbus_id,
            timeout=network_timeout,
            retries=network_retries,
        )
        inverter.set_keep_alive(keep_alive)
    except InverterError as err:
        try:
            inverter = await async_check_port(hass, entry, host)
        except InverterError:
            raise ConfigEntryNotReady from err

    device_info = DeviceInfo(
        configuration_url="https://semsplus.goodwe.com/",
        identifiers={(DOMAIN, inverter.serial_number)},
        name=entry.title,
        manufacturer="GoodWe",
        model=inverter.model_name,
        sw_version=f"{inverter.firmware} / {inverter.arm_firmware}",
        hw_version=f"{inverter.serial_number[5:8]} {inverter.serial_number[0:5]}",
    )

    # Create update coordinator
    coordinator = GoodweUpdateCoordinator(hass, entry, inverter)

    # Fetch initial data so we have data when entities subscribe
    await coordinator.async_config_entry_first_refresh()

    entry.runtime_data = GoodweRuntimeData(
        inverter=inverter,
        coordinator=coordinator,
        device_info=device_info,
    )

    hass.data[DOMAIN][entry.entry_id] = entry.runtime_data

    entry.async_on_unload(entry.add_update_listener(update_listener))

    await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)

    await async_setup_services(hass)

    return True


async def async_check_port(
    hass: HomeAssistant, entry: GoodweConfigEntry, host: str
) -> Inverter:
    """Check the communication port of the inverter, it may have changed after a firmware update."""
    inverter, port = await GoodweFlowHandler.async_detect_inverter_port(host=host)
    family = type(inverter).__name__
    hass.config_entries.async_update_entry(
        entry,
        data={
            CONF_HOST: host,
            CONF_PORT: port,
            CONF_MODEL_FAMILY: family,
        },
    )
    return inverter


async def async_unload_entry(
    hass: HomeAssistant, config_entry: GoodweConfigEntry
) -> 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)

        if not hass.data[DOMAIN]:
            await async_unload_services(hass)

    return unload_ok


async def update_listener(hass: HomeAssistant, config_entry: GoodweConfigEntry) -> None:
    """Handle options update."""
    await hass.config_entries.async_reload(config_entry.entry_id)


async def async_migrate_entry(
    hass: HomeAssistant, config_entry: GoodweConfigEntry
) -> bool:
    """Migrate old config entries."""

    if config_entry.version > 2:
        # This means the user has downgraded from a future version
        return False

    if config_entry.version == 1:
        # Update from version 1 to version 2 adding the PROTOCOL to the config entry
        host = config_entry.data[CONF_HOST]
        port = config_entry.data.get(
            CONF_PORT,
            config_entry.data.get(
                CONF_PORT,
                (
                    GOODWE_TCP_PORT
                    if config_entry.data.get(CONF_PROTOCOL) == "TCP"
                    else GOODWE_UDP_PORT
                ),
            ),
        )
        if not port:
            try:
                _, port = await GoodweFlowHandler.async_detect_inverter_port(host=host)
            except InverterError as err:
                raise ConfigEntryNotReady from err
        new_data = {
            CONF_HOST: host,
            CONF_PORT: port,
            CONF_PROTOCOL: config_entry.data.get(CONF_PROTOCOL),
            CONF_KEEP_ALIVE: config_entry.data.get(CONF_KEEP_ALIVE),
            CONF_MODEL_FAMILY: config_entry.data.get(CONF_MODEL_FAMILY),
            CONF_SCAN_INTERVAL: config_entry.data.get(CONF_SCAN_INTERVAL),
            CONF_NETWORK_RETRIES: config_entry.data.get(CONF_NETWORK_RETRIES),
            CONF_NETWORK_TIMEOUT: config_entry.data.get(CONF_NETWORK_TIMEOUT),
            CONF_MODBUS_ID: config_entry.data.get(CONF_MODBUS_ID),
        }
        hass.config_entries.async_update_entry(config_entry, data=new_data, version=2)

    return True


================================================
FILE: custom_components/goodwe/button.py
================================================
"""GoodWe PV inverter selection settings entities."""

from collections.abc import Awaitable, Callable
from dataclasses import dataclass
from datetime import datetime
import logging

from goodwe import Inverter, InverterError
from homeassistant.components.button import ButtonEntity, ButtonEntityDescription
from homeassistant.const import EntityCategory
from homeassistant.core import HomeAssistant
from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback

from .const import DOMAIN
from .coordinator import GoodweConfigEntry

_LOGGER = logging.getLogger(__name__)


@dataclass(frozen=True, kw_only=True)
class GoodweButtonEntityDescription(ButtonEntityDescription):
    """Class describing Goodwe button entities."""

    setting: str
    action: Callable[[Inverter], Awaitable[None]]


BUTTONS = (
    GoodweButtonEntityDescription(
        key="synchronize_clock",
        translation_key="synchronize_clock",
        entity_category=EntityCategory.CONFIG,
        setting="time",
        action=lambda inv: inv.write_setting("time", datetime.now()),
    ),
    GoodweButtonEntityDescription(
        key="start_inverter",
        translation_key="start_inverter",
        setting="start",
        action=lambda inv: inv.write_setting("start", 0),
    ),
    GoodweButtonEntityDescription(
        key="stop_inverter",
        translation_key="stop_inverter",
        setting="stop",
        action=lambda inv: inv.write_setting("stop", 0),
    ),
)


async def async_setup_entry(
    hass: HomeAssistant,
    config_entry: GoodweConfigEntry,
    async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
    """Set up the inverter button entities from a config entry."""
    inverter = config_entry.runtime_data.inverter
    device_info = config_entry.runtime_data.device_info

    entities = []

    for description in BUTTONS:
        try:
            await inverter.read_setting(description.setting)
        except (InverterError, ValueError):
            # Inverter model does not support this feature
            _LOGGER.debug("Could not read %s value", description.setting)
        else:
            entities.append(
                GoodweButtonEntity(
                    device_info,
                    description,
                    inverter,
                )
            )

    async_add_entities(entities)


class GoodweButtonEntity(ButtonEntity):
    """Entity representing the inverter clock synchronization button."""

    _attr_should_poll = False
    _attr_has_entity_name = True
    entity_description: GoodweButtonEntityDescription

    def __init__(
        self,
        device_info: DeviceInfo,
        description: GoodweButtonEntityDescription,
        inverter: Inverter,
    ) -> 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._inverter: Inverter = inverter

    async def async_press(self) -> None:
        """Triggers the button press service."""
        await self.entity_description.action(self._inverter)


================================================
FILE: custom_components/goodwe/config_flow.py
================================================
"""Config flow to configure Goodwe inverters using their local API."""

from __future__ import annotations

import logging
from typing import Any

import voluptuous as vol

from goodwe import Inverter, InverterError, connect
from goodwe.const import GOODWE_TCP_PORT, GOODWE_UDP_PORT
from homeassistant.config_entries import (
    ConfigEntry,
    ConfigFlow,
    ConfigFlowResult,
    OptionsFlow,
)
from homeassistant.const import CONF_HOST, CONF_PORT, CONF_PROTOCOL, CONF_SCAN_INTERVAL
from homeassistant.core import callback
from homeassistant.helpers import config_validation as cv

from .const import (
    CONF_KEEP_ALIVE,
    CONF_MODBUS_ID,
    CONF_MODEL_FAMILY,
    CONF_NETWORK_RETRIES,
    CONF_NETWORK_TIMEOUT,
    DEFAULT_MODBUS_ID,
    DEFAULT_NAME,
    DEFAULT_NETWORK_RETRIES,
    DEFAULT_NETWORK_TIMEOUT,
    DEFAULT_SCAN_INTERVAL,
    DOMAIN,
)

PROTOCOL_CHOICES = ["UDP", "TCP"]
CONFIG_SCHEMA = vol.Schema(
    {
        vol.Required(CONF_HOST): str,
        vol.Required(CONF_PROTOCOL, default="UDP"): vol.In(PROTOCOL_CHOICES),
        vol.Required(CONF_MODEL_FAMILY, default="none"): str,
    }
)
OPTIONS_SCHEMA = vol.Schema(
    {
        vol.Required(CONF_HOST): str,
        vol.Optional(CONF_PORT): int,
        vol.Required(CONF_PROTOCOL): vol.In(PROTOCOL_CHOICES),
        vol.Required(CONF_KEEP_ALIVE): cv.boolean,
        vol.Required(CONF_MODEL_FAMILY): str,
        vol.Optional(CONF_SCAN_INTERVAL): int,
        vol.Optional(CONF_MODBUS_ID): int,
        vol.Optional(CONF_NETWORK_RETRIES): cv.positive_int,
        vol.Optional(CONF_NETWORK_TIMEOUT): cv.positive_int,
    }
)

_LOGGER = logging.getLogger(__name__)


class OptionsFlowHandler(OptionsFlow):
    """Options for the component."""

    def __init__(self, config_entry: ConfigEntry) -> None:
        """Init object."""
        self.entry = config_entry

    async def async_step_init(self, user_input: dict | None = None) -> ConfigFlowResult:
        """Manage the options."""
        if user_input is not None:
            return self.async_create_entry(title="", data=user_input)

        host = self.entry.options.get(CONF_HOST, self.entry.data[CONF_HOST])
        port = self.entry.options.get(CONF_PORT, self.entry.data.get(CONF_PORT))
        protocol = self.entry.options.get(
            CONF_PROTOCOL, self.entry.data.get(CONF_PROTOCOL, "UDP")
        )
        keep_alive = self.entry.options.get(CONF_KEEP_ALIVE, False)
        model_family = self.entry.options.get(
            CONF_MODEL_FAMILY, self.entry.data[CONF_MODEL_FAMILY]
        )
        network_retries = self.entry.options.get(
            CONF_NETWORK_RETRIES, DEFAULT_NETWORK_RETRIES
        )
        network_timeout = self.entry.options.get(
            CONF_NETWORK_TIMEOUT, DEFAULT_NETWORK_TIMEOUT
        )
        modbus_id = self.entry.options.get(CONF_MODBUS_ID, DEFAULT_MODBUS_ID)

        return self.async_show_form(
            step_id="init",
            data_schema=self.add_suggested_values_to_schema(
                OPTIONS_SCHEMA,
                {
                    CONF_HOST: host,
                    CONF_PORT: port,
                    CONF_PROTOCOL: protocol,
                    CONF_KEEP_ALIVE: keep_alive,
                    CONF_MODEL_FAMILY: model_family,
                    CONF_SCAN_INTERVAL: self.entry.options.get(
                        CONF_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL
                    ),
                    CONF_NETWORK_RETRIES: network_retries,
                    CONF_NETWORK_TIMEOUT: network_timeout,
                    CONF_MODBUS_ID: modbus_id,
                },
            ),
        )


class GoodweFlowHandler(ConfigFlow, domain=DOMAIN):
    """Handle a Goodwe config flow."""

    MINOR_VERSION = 2

    @staticmethod
    @callback
    def async_get_options_flow(
        config_entry: ConfigEntry,
    ) -> OptionsFlowHandler:
        """Get the options flow."""
        return OptionsFlowHandler(config_entry)

    async def async_handle_successful_connection(
        self,
        inverter: Inverter,
        host: str,
        port: int,
        protocol: str,
    ) -> ConfigFlowResult:
        """Handle a successful connection storing it's values on the entry data."""
        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_PORT: port,
                CONF_PROTOCOL: protocol,
                CONF_MODEL_FAMILY: type(inverter).__name__,
            },
        )

    async def async_step_user(
        self, user_input: dict[str, Any] | None = None
    ) -> ConfigFlowResult:
        """Handle a flow initialized by the user."""
        errors = {}
        if user_input is not None:
            host = user_input[CONF_HOST]
            protocol = user_input[CONF_PROTOCOL]
            model_family = user_input[CONF_MODEL_FAMILY]
            port = user_input.get(
                CONF_PORT, GOODWE_UDP_PORT if protocol == "UDP" else GOODWE_TCP_PORT
            )

            try:
                _LOGGER.debug(
                    "Goodwe connecting to %s:%s protocol=%s family=%s",
                    host,
                    port,
                    protocol,
                    model_family,
                )
                inverter = await connect(
                    host=host, port=port, family=model_family, retries=10
                )
            except InverterError:
                errors[CONF_HOST] = "connection_error"
            else:
                return await self.async_handle_successful_connection(
                    inverter, host, port, protocol
                )
        return self.async_show_form(
            step_id="user", data_schema=CONFIG_SCHEMA, errors=errors
        )

    @staticmethod
    async def async_detect_inverter_port(
        host: str,
    ) -> tuple[Inverter, int]:
        """Detects the port of the Inverter."""
        port = GOODWE_UDP_PORT
        try:
            inverter = await connect(host=host, port=port, retries=10)
        except InverterError:
            port = GOODWE_TCP_PORT
            inverter = await connect(host=host, port=port, retries=10)
        return inverter, port


================================================
FILE: custom_components/goodwe/const.py
================================================
"""Constants for the Goodwe component."""

from datetime import timedelta

from homeassistant.const import Platform

DOMAIN = "goodwe"

PLATFORMS = [
    Platform.BUTTON,
    Platform.NUMBER,
    Platform.SELECT,
    Platform.SENSOR,
    Platform.SWITCH,
]

DEFAULT_NAME = "GoodWe"
SCAN_INTERVAL = timedelta(seconds=10)
DEFAULT_SCAN_INTERVAL = 5
DEFAULT_NETWORK_RETRIES = 10
DEFAULT_NETWORK_TIMEOUT = 1
DEFAULT_MODBUS_ID = 0

CONF_KEEP_ALIVE = "keep_alive"
CONF_MODEL_FAMILY = "model_family"
CONF_NETWORK_RETRIES = "network_retries"
CONF_NETWORK_TIMEOUT = "network_timeout"
CONF_MODBUS_ID = "modbus_id"

SERVICE_GET_PARAMETER = "get_parameter"
SERVICE_SET_PARAMETER = "set_parameter"
ATTR_DEVICE_ID = "device_id"
ATTR_ENTITY_ID = "entity_id"
ATTR_PARAMETER = "parameter"
ATTR_VALUE = "value"


================================================
FILE: custom_components/goodwe/coordinator.py
================================================
"""Update coordinator for Goodwe."""

from __future__ import annotations

from dataclasses import dataclass
from datetime import datetime, timedelta
import logging
from typing import Any

from goodwe import Inverter, InverterError, RequestFailedException
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_SCAN_INTERVAL
from homeassistant.core import HomeAssistant
from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.update_coordinator import (
    BaseCoordinatorEntity,
    DataUpdateCoordinator,
    UpdateFailed,
)

from .const import DEFAULT_SCAN_INTERVAL

_LOGGER = logging.getLogger(__name__)

type GoodweConfigEntry = ConfigEntry[GoodweRuntimeData]


@dataclass
class GoodweRuntimeData:
    """Data class for runtime data."""

    inverter: Inverter
    coordinator: GoodweUpdateCoordinator
    device_info: DeviceInfo


class GoodweUpdateCoordinator(DataUpdateCoordinator[dict[str, Any]]):
    """Gather data for the energy device."""

    config_entry: GoodweConfigEntry

    def __init__(
        self,
        hass: HomeAssistant,
        entry: GoodweConfigEntry,
        inverter: Inverter,
    ) -> None:
        """Initialize update coordinator."""
        super().__init__(
            hass,
            _LOGGER,
            config_entry=entry,
            name=entry.title,
            update_interval=timedelta(
                seconds=entry.options.get(CONF_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL)
            ),
        )
        self.inverter: Inverter = inverter
        self._last_data: dict[str, Any] = {}
        self._polled_entities: dict[BaseCoordinatorEntity, datetime] = {}

    async def _async_update_data(self) -> dict[str, Any]:
        """Fetch data from the inverter."""
        await self._update_polled_entities()

        try:
            self._last_data = self.data or {}
            return await self.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 last known data
                return self._last_data
            # 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

    async def _update_polled_entities(self) -> None:
        for entity, interval in list(self._polled_entities.items()):
            if interval:
                try:
                    await entity.async_update()
                except InverterError:
                    _LOGGER.debug("Failed to update entity %s", entity.name)

    def sensor_value(self, sensor: str) -> Any:
        """Answer current (or last known) value of the sensor."""
        val = self.data.get(sensor)
        return val if val is not None else self._last_data.get(sensor)

    def total_sensor_value(self, sensor: str) -> Any:
        """Answer current value of the 'total' (never 0) sensor."""
        val = self.data.get(sensor)
        return val or self._last_data.get(sensor)

    def reset_sensor(self, sensor: str) -> None:
        """Reset sensor value to 0.

        Intended for "daily" cumulative sensors (e.g. PV energy produced today),
        which should be explicitly reset to 0 at midnight if inverter is suspended.
        """
        self._last_data[sensor] = 0
        self.data[sensor] = 0

    def entity_state_polling(
        self, entity: BaseCoordinatorEntity, interval: int
    ) -> None:
        """Enable/disable polling of entity state."""
        if interval:
            self._polled_entities[entity] = interval
        else:
            self._polled_entities.pop(entity, None)


================================================
FILE: custom_components/goodwe/diagnostics.py
================================================
"""Diagnostics support for Goodwe."""

from __future__ import annotations

from typing import Any

from goodwe import Inverter, InverterError
from homeassistant.core import HomeAssistant

from .coordinator import GoodweConfigEntry


async def async_get_config_entry_diagnostics(
    hass: HomeAssistant, config_entry: GoodweConfigEntry
) -> dict[str, Any]:
    """Return diagnostics for a config entry."""
    inverter = config_entry.runtime_data.inverter

    return {
        "config_entry": config_entry.as_dict(),
        "inverter": {
            "model_name": inverter.model_name,
            "rated_power": inverter.rated_power,
            "firmware": inverter.firmware,
            "arm_firmware": inverter.arm_firmware,
            "dsp1_version": inverter.dsp1_version,
            "dsp2_version": inverter.dsp2_version,
            "dsp_svn_version": inverter.dsp_svn_version,
            "arm_version": inverter.arm_version,
            "arm_svn_version": inverter.arm_svn_version,
            "modbus_address": await _read_register(inverter, 45127),
            "modbus_baudrate": await _read_register(inverter, 45132),
            "log_data_enable": await _read_register(inverter, 47005),
            "data_send_interval": await _read_register(inverter, 47006),
            "wifi_or_lan": await _read_register(inverter, 47009),
            "modbus_tcp_wo_internet": await _read_register(inverter, 47017),
            "wifi_modbus_tcp_enable": await _read_register(inverter, 47040),
        },
    }


async def _read_register(inverter: Inverter, register: int) -> Any:
    try:
        return await inverter.read_setting(f"modbus-{register}")
    except InverterError:
        return None


================================================
FILE: custom_components/goodwe/icons.json
================================================
{
  "entity": {
    "button": {
      "synchronize_clock": {
        "default": "mdi:clock-check-outline"
      },
      "start_inverter": {
        "default": "mdi:power"
      },
      "stop_inverter": {
        "default": "mdi:power-off"
      }
    },
    "number": {
      "battery_discharge_depth": {
        "default": "mdi:battery-arrow-down"
      },
      "battery_discharge_depth_offline": {
        "default": "mdi:battery-arrow-down"
      },
      "eco_mode_power": {
        "default": "mdi:battery-charging-low"
      },
      "eco_mode_soc": {
        "default": "mdi:battery-charging-low"
      },
      "fast_charging_power": {
        "default": "mdi:battery-arrow-up"
      },
      "fast_charging_soc": {
        "default": "mdi:battery-arrow-up"
      },
      "grid_export_limit": {
        "default": "mdi:transmission-tower"
      },
      "soc_upper_limit": {
        "default": "mdi:battery-heart-outline"
      }
    },
    "select": {
      "operation_mode": {
        "default": "mdi:solar-power"
      }
    },
    "switch": {
      "grid_export_limit_switch": {
        "default": "mdi:transmission-tower-import",
        "state": {
          "on": "mdi:transmission-tower",
          "off": "mdi:transmission-tower-import"
        }
      },
      "load_control": {
        "default": "mdi:electric-switch",
        "state": {
          "on": "mdi:electric-switch-closed",
          "off": "mdi:electric-switch"
        }
      },
      "fast_charging_switch": {
        "default": "mdi:battery-medium",
        "state": {
          "on": "mdi:battery-arrow-up",
          "off": "mdi:battery-medium"
        }
      },
      "backup_supply_switch": {
        "default": "mdi:battery-medium",
        "state": {
          "on": "mdi:battery-medium",
          "off": "mdi:battery-medium"
        }
      },
      "dod_holding_switch": {
        "default": "mdi:battery-arrow-down",
        "state": {
          "on": "mdi:battery-arrow-down",
          "off": "mdi:battery-arrow-down"
        }
      }
    }
  }
}


================================================
FILE: custom_components/goodwe/manifest.json
================================================
{
  "domain": "goodwe",
  "name": "GoodWe Inverter",
  "codeowners": [
    "@mletenay",
    "@starkillerOG",
    "@fizcris"
  ],
  "config_flow": true,
  "documentation": "https://github.com/mletenay/home-assistant-goodwe-inverter",
  "integration_type": "device",
  "iot_class": "local_polling",
  "issue_tracker": "https://github.com/mletenay/home-assistant-goodwe-inverter/issues",
  "loggers": ["goodwe"],
  "requirements": ["goodwe==0.4.10"],
  "version": "0.9.9.30"
}


================================================
FILE: custom_components/goodwe/number.py
================================================
"""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 (
    NumberDeviceClass,
    NumberEntity,
    NumberEntityDescription,
)
from homeassistant.const import PERCENTAGE, EntityCategory, UnitOfPower
from homeassistant.core import HomeAssistant
from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback

from .const import DOMAIN
from .coordinator import GoodweConfigEntry

_LOGGER = logging.getLogger(__name__)


@dataclass(frozen=True, kw_only=True)
class GoodweNumberEntityDescription(NumberEntityDescription):
    """Class describing Goodwe number entities."""

    getter: Callable[[Inverter], Awaitable[any]]
    mapper: Callable[[any], int]
    setter: Callable[[Inverter, int], Awaitable[None]]
    filter: Callable[[Inverter], bool]


def _get_setting_unit(inverter: Inverter, setting: str) -> str:
    """Return the unit of an inverter setting."""
    return next((s.unit for s in inverter.settings() if s.id_ == setting), "")


async def set_offline_battery_dod(inverter: Inverter, dod: int) -> None:
    """Sets offline battery dod - dod for backup output."""
    if 10 <= dod <= 100:
        await inverter.write_setting("battery_discharge_depth_offline", 100 - dod)


async def get_offline_battery_dod(inverter: Inverter) -> int:
    """Returns offline battery dod - dod for backup output."""
    return 100 - (await inverter.read_setting("battery_discharge_depth_offline"))


NUMBERS = (
    # Only one of the export limits are added.
    # Availability is checked in the filter method.
    # Export limit in W
    GoodweNumberEntityDescription(
        key="grid_export_limit",
        translation_key="grid_export_limit",
        entity_category=EntityCategory.CONFIG,
        device_class=NumberDeviceClass.POWER,
        native_unit_of_measurement=UnitOfPower.WATT,
        native_step=100,
        native_min_value=0,
        getter=lambda inv: inv.get_grid_export_limit(),
        mapper=lambda v: v,
        setter=lambda inv, val: inv.set_grid_export_limit(val),
        filter=lambda inv: _get_setting_unit(inv, "grid_export_limit") != "%",
    ),
    # Export limit in %
    GoodweNumberEntityDescription(
        key="grid_export_limit",
        translation_key="grid_export_limit",
        entity_category=EntityCategory.CONFIG,
        native_unit_of_measurement=PERCENTAGE,
        native_step=1,
        native_min_value=0,
        native_max_value=200,
        getter=lambda inv: inv.get_grid_export_limit(),
        mapper=lambda v: v,
        setter=lambda inv, val: inv.set_grid_export_limit(val),
        filter=lambda inv: _get_setting_unit(inv, "grid_export_limit") == "%",
    ),
    GoodweNumberEntityDescription(
        key="battery_discharge_depth",
        translation_key="battery_discharge_depth",
        icon="mdi:battery-arrow-down",
        entity_category=EntityCategory.CONFIG,
        native_unit_of_measurement=PERCENTAGE,
        native_step=1,
        native_min_value=0,
        native_max_value=99,
        getter=lambda inv: inv.get_ongrid_battery_dod(),
        mapper=lambda v: v,
        setter=lambda inv, val: inv.set_ongrid_battery_dod(val),
        filter=lambda inv: True,
    ),
    GoodweNumberEntityDescription(
        key="soc_upper_limit",
        translation_key="soc_upper_limit",
        native_unit_of_measurement=PERCENTAGE,
        native_step=1,
        native_min_value=0,
        native_max_value=100,
        getter=lambda inv: inv.read_setting("soc_upper_limit"),
        mapper=lambda v: v,
        setter=lambda inv, val: inv.write_setting("soc_upper_limit", val),
        filter=lambda inv: True,
    ),
    GoodweNumberEntityDescription(
        key="battery_discharge_depth_offline",
        translation_key="battery_discharge_depth_offline",
        entity_category=EntityCategory.CONFIG,
        native_unit_of_measurement=PERCENTAGE,
        native_step=1,
        native_min_value=0,
        native_max_value=99,
        getter=lambda inv: get_offline_battery_dod(inv),
        mapper=lambda v: v,
        setter=lambda inv, val: set_offline_battery_dod(inv, val),
        filter=lambda inv: True,
    ),
    GoodweNumberEntityDescription(
        key="eco_mode_power",
        translation_key="eco_mode_power",
        entity_category=EntityCategory.CONFIG,
        native_unit_of_measurement=PERCENTAGE,
        native_step=1,
        native_min_value=0,
        native_max_value=100,
        getter=lambda inv: inv.read_setting("eco_mode_1"),
        mapper=lambda v: abs(v.get_power()) if v.get_power() else 0,
        setter=None,
        filter=lambda inv: True,
    ),
    GoodweNumberEntityDescription(
        key="eco_mode_soc",
        translation_key="eco_mode_soc",
        entity_category=EntityCategory.CONFIG,
        native_unit_of_measurement=PERCENTAGE,
        native_step=1,
        native_min_value=0,
        native_max_value=100,
        getter=lambda inv: inv.read_setting("eco_mode_1"),
        mapper=lambda v: v.soc or 0,
        setter=None,
        filter=lambda inv: True,
    ),
    GoodweNumberEntityDescription(
        key="fast_charging_power",
        translation_key="fast_charging_power",
        native_unit_of_measurement=PERCENTAGE,
        native_step=1,
        native_min_value=0,
        native_max_value=100,
        getter=lambda inv: inv.read_setting("fast_charging_power"),
        mapper=lambda v: v,
        setter=lambda inv, val: inv.write_setting("fast_charging_power", val),
        filter=lambda inv: True,
    ),
    GoodweNumberEntityDescription(
        key="fast_charging_soc",
        translation_key="fast_charging_soc",
        native_unit_of_measurement=PERCENTAGE,
        native_step=1,
        native_min_value=0,
        native_max_value=100,
        getter=lambda inv: inv.read_setting("fast_charging_soc"),
        mapper=lambda v: v,
        setter=lambda inv, val: inv.write_setting("fast_charging_soc", val),
        filter=lambda inv: True,
    ),
    GoodweNumberEntityDescription(
        key="ems_power_limit",
        translation_key="ems_power_limit",
        entity_category=EntityCategory.CONFIG,
        device_class=NumberDeviceClass.POWER,
        native_unit_of_measurement=UnitOfPower.WATT,
        native_step=100,
        native_min_value=0,
        getter=lambda inv: inv.read_setting("ems_power_limit"),
        mapper=lambda v: v,
        setter=lambda inv, val: inv.write_setting("ems_power_limit", val),
        filter=lambda inv: True,
    ),
    GoodweNumberEntityDescription(
        key="battery_soc_protection",
        translation_key="battery_soc_protection",
        icon="mdi:battery-arrow-down-outline",
        entity_category=EntityCategory.CONFIG,
        native_unit_of_measurement=PERCENTAGE,
        native_step=1,
        native_min_value=0,
        native_max_value=100,
        getter=lambda inv: inv.read_setting("battery_soc_protection"),
        mapper=lambda v: v,
        setter=lambda inv, val: inv.write_setting("battery_soc_protection", val),
        filter=lambda inv: True,
    ),
)


async def async_setup_entry(
    hass: HomeAssistant,
    config_entry: GoodweConfigEntry,
    async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
    """Set up the inverter select entities from a config entry."""
    inverter = config_entry.runtime_data.inverter
    device_info = config_entry.runtime_data.device_info

    entities = []

    for description in filter(lambda dsc: dsc.filter(inverter), NUMBERS):
        try:
            current_value = description.mapper(await description.getter(inverter))
        except (InverterError, ValueError):
            # Inverter model does not support this setting
            _LOGGER.debug("Could not read inverter setting %s", description.key)
            continue

        entity = InverterNumberEntity(device_info, description, inverter, current_value)
        # Set the max value of grid_export_limit and ems_power_limit (W version)
        if (
            description.key in ("grid_export_limit", "ems_power_limit")
            and description.native_unit_of_measurement == UnitOfPower.WATT
        ):
            entity.native_max_value = (
                inverter.rated_power * 2 if inverter.rated_power else 10000
            )
        entities.append(entity)

    async_add_entities(entities)


class InverterNumberEntity(NumberEntity):
    """Inverter numeric setting entity."""

    _attr_should_poll = False
    _attr_has_entity_name = True
    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_native_value = float(current_value)
        self._inverter: Inverter = inverter

    async def async_update(self) -> None:
        """Get the current value from inverter."""
        value = await self.entity_description.getter(self._inverter)
        self._attr_native_value = float(value)

    async def async_set_native_value(self, value: float) -> None:
        """Set new value to inverter."""
        if self.entity_description.setter:
            await self.entity_description.setter(self._inverter, int(value))
        self._attr_native_value = value
        self.async_write_ha_state()


================================================
FILE: custom_components/goodwe/select.py
================================================
"""GoodWe PV inverter selection settings entities."""

from dataclasses import dataclass
import logging

from goodwe import Inverter, InverterError, OperationMode
from goodwe.inverter import EMSMode
from homeassistant.components.select import SelectEntity, SelectEntityDescription
from homeassistant.const import (
    STATE_UNAVAILABLE,
    STATE_UNKNOWN,
    EntityCategory,
    Platform,
)
from homeassistant.core import Event, HomeAssistant
from homeassistant.helpers import entity_registry as er
from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from homeassistant.helpers.event import async_track_state_change_event

from .const import DOMAIN
from .coordinator import GoodweConfigEntry

_LOGGER = logging.getLogger(__name__)


_MODE_TO_OPTION: dict[OperationMode, str] = {
    OperationMode.GENERAL: "general",
    OperationMode.OFF_GRID: "off_grid",
    OperationMode.BACKUP: "backup",
    OperationMode.ECO: "eco",
    OperationMode.PEAK_SHAVING: "peak_shaving",
    OperationMode.SELF_USE: "self_use",
    OperationMode.ECO_CHARGE: "eco_charge",
    OperationMode.ECO_DISCHARGE: "eco_discharge",
}

_OPTION_TO_MODE: dict[str, OperationMode] = {
    value: key for key, value in _MODE_TO_OPTION.items()
}


@dataclass(frozen=True, kw_only=True)
class GoodweSelectEntityDescription(SelectEntityDescription):
    """Class describing Goodwe number entities."""

    options: dict[str, EMSMode]


OPERATION_MODE = SelectEntityDescription(
    key="operation_mode",
    entity_category=EntityCategory.CONFIG,
    translation_key="operation_mode",
)

EMS_MODE = GoodweSelectEntityDescription(
    key="ems_mode",
    entity_category=EntityCategory.CONFIG,
    translation_key="ems_mode",
    options={e.name.lower(): e for e in list(EMSMode)},
)


async def async_setup_entry(
    hass: HomeAssistant,
    config_entry: GoodweConfigEntry,
    async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
    """Set up the inverter select entities from a config entry."""
    inverter = config_entry.runtime_data.inverter
    device_info = config_entry.runtime_data.device_info

    supported_modes = await inverter.get_operation_modes(True)
    # read current operating mode from the inverter
    try:
        active_mode = await inverter.get_operation_mode()
        eco_mode = await inverter.read_setting("eco_mode_1")
        current_eco_power = abs(eco_mode.power) if eco_mode.power else 0
        current_eco_soc = eco_mode.soc or 0
    except (InverterError, ValueError):
        # Inverter model does not support this setting
        _LOGGER.debug("Could not read inverter operation mode", exc_info=True)
    else:
        active_mode_option = _MODE_TO_OPTION.get(active_mode)
        if active_mode_option is not None:
            entity = InverterOperationModeEntity(
                device_info,
                OPERATION_MODE,
                inverter,
                [v for k, v in _MODE_TO_OPTION.items() if k in supported_modes],
                active_mode_option,
                current_eco_power,
                current_eco_soc,
            )
            async_add_entities([entity])
        else:
            _LOGGER.warning(
                "Active mode %s not found in Goodwe Inverter Operation Mode Entity. Skipping entity creation",
                active_mode,
            )

        eco_mode_power_entity_id = er.async_get(hass).async_get_entity_id(
            Platform.NUMBER,
            DOMAIN,
            f"{DOMAIN}-eco_mode_power-{inverter.serial_number}",
        )
        if eco_mode_power_entity_id:
            async_track_state_change_event(
                hass,
                eco_mode_power_entity_id,
                entity.update_eco_mode_power,
            )
        eco_mode_soc_entity_id = er.async_get(hass).async_get_entity_id(
            Platform.NUMBER,
            DOMAIN,
            f"{DOMAIN}-eco_mode_soc-{inverter.serial_number}",
        )
        if eco_mode_soc_entity_id:
            async_track_state_change_event(
                hass,
                eco_mode_soc_entity_id,
                entity.update_eco_mode_soc,
            )

    # read current EMS mode from the inverter
    try:
        ems_mode = await inverter.get_ems_mode()
    except (InverterError, ValueError):
        # Inverter model does not support EMS modes
        _LOGGER.debug("Could not read inverter EMS mode", exc_info=True)
    else:
        entity = InverterEMSModeEntity(
            device_info,
            EMS_MODE,
            inverter,
            ems_mode,
        )
        async_add_entities([entity])


class InverterOperationModeEntity(SelectEntity):
    """Entity representing the inverter operation mode."""

    _attr_should_poll = False
    _attr_has_entity_name = True

    def __init__(
        self,
        device_info: DeviceInfo,
        description: SelectEntityDescription,
        inverter: Inverter,
        supported_options: list[str],
        current_mode: str,
        current_eco_power: int,
        current_eco_soc: int,
    ) -> 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 = supported_options
        self._attr_current_option = current_mode
        self._inverter: Inverter = inverter
        self._eco_mode_power = current_eco_power
        self._eco_mode_soc = current_eco_soc

    async def async_select_option(self, option: str) -> None:
        """Change the selected option."""
        _LOGGER.debug(
            "Setting operation mode to %s, power %d, max SoC %d",
            option,
            self._eco_mode_power,
            self._eco_mode_soc,
        )
        try:
            await self._inverter.set_operation_mode(
                _OPTION_TO_MODE[option], self._eco_mode_power, self._eco_mode_soc
            )
        except InverterError as err:
            _LOGGER.warning(
                "Failed to set operation mode to %s: %s", option, err
            )
            return
        self._attr_current_option = option
        self.async_write_ha_state()

    async def async_update(self) -> None:
        """Get the current value from inverter."""
        value = await self._inverter.get_operation_mode()
        self._attr_current_option = _MODE_TO_OPTION[value]

    async def update_eco_mode_power(self, event: Event) -> None:
        """Update eco mode power value in inverter (when in eco mode)."""
        state = event.data.get("new_state")
        if state is None or state.state in (STATE_UNKNOWN, "", STATE_UNAVAILABLE):
            return

        self._eco_mode_power = int(float(state.state))
        if event.data.get("old_state"):
            operation_mode = _OPTION_TO_MODE[self.current_option]
            if operation_mode in (
                OperationMode.ECO_CHARGE,
                OperationMode.ECO_DISCHARGE,
            ):
                _LOGGER.debug("Setting eco mode power to %d", self._eco_mode_power)
                try:
                    await self._inverter.set_operation_mode(
                        operation_mode, self._eco_mode_power, self._eco_mode_soc
                    )
                except InverterError as err:
                    _LOGGER.warning(
                        "Failed to update eco mode power to %d: %s",
                        self._eco_mode_power,
                        err,
                    )

    async def update_eco_mode_soc(self, event: Event) -> None:
        """Update eco mode SoC value in inverter (when in eco mode)."""
        state = event.data.get("new_state")
        if state is None or state.state in (STATE_UNKNOWN, "", STATE_UNAVAILABLE):
            return

        self._eco_mode_soc = int(float(state.state))
        if event.data.get("old_state"):
            operation_mode = _OPTION_TO_MODE[self.current_option]
            if operation_mode in (
                OperationMode.ECO_CHARGE,
                OperationMode.ECO_DISCHARGE,
            ):
                _LOGGER.debug("Setting eco mode SoC to %d", self._eco_mode_soc)
                try:
                    await self._inverter.set_operation_mode(
                        operation_mode, self._eco_mode_power, self._eco_mode_soc
                    )
                except InverterError as err:
                    _LOGGER.warning(
                        "Failed to update eco mode SoC to %d: %s",
                        self._eco_mode_soc,
                        err,
                    )


class InverterEMSModeEntity(SelectEntity):
    """Entity representing the inverter EMS mode."""

    _attr_should_poll = False
    _attr_has_entity_name = True
    entity_description: GoodweSelectEntityDescription

    def __init__(
        self,
        device_info: DeviceInfo,
        description: GoodweSelectEntityDescription,
        inverter: Inverter,
        current_mode: EMSMode,
    ) -> 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 = list(description.options.keys())
        self._attr_current_option = current_mode.name.lower()
        self._inverter: Inverter = inverter

    async def async_select_option(self, option: str) -> None:
        """Change the EMS mode."""
        _LOGGER.debug("Setting EMS mode to %s", option)
        try:
            await self._inverter.set_ems_mode(self.entity_description.options[option])
        except InverterError as err:
            _LOGGER.warning("Failed to set EMS mode to %s: %s", option, err)
            return
        self._attr_current_option = option
        self.async_write_ha_state()

    async def async_update(self) -> None:
        """Get the current EMS mode from inverter."""
        value = await self._inverter.get_ems_mode()
        self._attr_current_option = value.name.lower()


================================================
FILE: custom_components/goodwe/sensor.py
================================================
"""Support for GoodWe inverter via UDP."""

from __future__ import annotations

from collections.abc import Callable
from dataclasses import dataclass
from datetime import date, datetime, timedelta
from decimal import Decimal
import logging
from typing import Any

from goodwe import Inverter, Sensor, SensorKind
from goodwe.sensor import (
    Enum,
    Enum2,
    EnumBitmap4,
    EnumBitmap22,
    EnumCalculated,
    EnumH,
    EnumL,
)
from homeassistant.components.sensor import (
    SensorDeviceClass,
    SensorEntity,
    SensorEntityDescription,
    SensorStateClass,
)
from homeassistant.const import (
    PERCENTAGE,
    EntityCategory,
    UnitOfApparentPower,
    UnitOfElectricCurrent,
    UnitOfElectricPotential,
    UnitOfEnergy,
    UnitOfFrequency,
    UnitOfPower,
    UnitOfReactivePower,
    UnitOfTemperature,
    UnitOfTime,
)
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from homeassistant.helpers.event import async_track_point_in_time
from homeassistant.helpers.typing import StateType
from homeassistant.helpers.update_coordinator import CoordinatorEntity
from homeassistant.util import dt as dt_util

from .const import DOMAIN
from .coordinator import GoodweConfigEntry, GoodweUpdateCoordinator

_LOGGER = logging.getLogger(__name__)

# Sensor name of battery SoC
BATTERY_SOC = "battery_soc"

# Sensors that are reset to 0 at midnight.
# The inverter is only powered by the solar panels and not mains power, so it goes dead when the sun goes down.
# The "_day" sensors are reset to 0 when the inverter wakes up in the morning when the sun comes up and power to the inverter is restored.
# This makes sure daily values are reset at midnight instead of at sunrise.
# When the inverter has a battery connected, HomeAssistant will not reset the values but let the inverter reset them by looking at the unavailable state of the inverter.
DAILY_RESET = ["e_day", "e_load_day"]

_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: dict[SensorKind, str] = {
    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(frozen=True)
class GoodweSensorEntityDescription(SensorEntityDescription):
    """Class describing Goodwe sensor entities."""

    value: Callable[[GoodweUpdateCoordinator, str], Any] = lambda coordinator, sensor: (
        coordinator.sensor_value(sensor)
    )
    available: Callable[[GoodweUpdateCoordinator], bool] = lambda coordinator: (
        coordinator.last_update_success
    )


_DESCRIPTIONS: dict[str, GoodweSensorEntityDescription] = {
    "A": GoodweSensorEntityDescription(
        key="A",
        device_class=SensorDeviceClass.CURRENT,
        state_class=SensorStateClass.MEASUREMENT,
        native_unit_of_measurement=UnitOfElectricCurrent.AMPERE,
    ),
    "V": GoodweSensorEntityDescription(
        key="V",
        device_class=SensorDeviceClass.VOLTAGE,
        state_class=SensorStateClass.MEASUREMENT,
        native_unit_of_measurement=UnitOfElectricPotential.VOLT,
    ),
    "W": GoodweSensorEntityDescription(
        key="W",
        device_class=SensorDeviceClass.POWER,
        state_class=SensorStateClass.MEASUREMENT,
        native_unit_of_measurement=UnitOfPower.WATT,
    ),
    "kWh": GoodweSensorEntityDescription(
        key="kWh",
        device_class=SensorDeviceClass.ENERGY,
        state_class=SensorStateClass.TOTAL_INCREASING,
        native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
        value=lambda coordinator, sensor: coordinator.total_sensor_value(sensor),
        available=lambda coordinator: coordinator.data is not None,
    ),
    "VA": GoodweSensorEntityDescription(
        key="VA",
        device_class=SensorDeviceClass.APPARENT_POWER,
        state_class=SensorStateClass.MEASUREMENT,
        native_unit_of_measurement=UnitOfApparentPower.VOLT_AMPERE,
        entity_registry_enabled_default=False,
    ),
    "var": GoodweSensorEntityDescription(
        key="var",
        device_class=SensorDeviceClass.REACTIVE_POWER,
        state_class=SensorStateClass.MEASUREMENT,
        native_unit_of_measurement=UnitOfReactivePower.VOLT_AMPERE_REACTIVE,
        entity_registry_enabled_default=False,
    ),
    "C": GoodweSensorEntityDescription(
        key="C",
        device_class=SensorDeviceClass.TEMPERATURE,
        state_class=SensorStateClass.MEASUREMENT,
        native_unit_of_measurement=UnitOfTemperature.CELSIUS,
    ),
    "Hz": GoodweSensorEntityDescription(
        key="Hz",
        device_class=SensorDeviceClass.FREQUENCY,
        state_class=SensorStateClass.MEASUREMENT,
        native_unit_of_measurement=UnitOfFrequency.HERTZ,
    ),
    "h": GoodweSensorEntityDescription(
        key="h",
        device_class=SensorDeviceClass.DURATION,
        state_class=SensorStateClass.MEASUREMENT,
        native_unit_of_measurement=UnitOfTime.HOURS,
        entity_registry_enabled_default=False,
    ),
    "%": GoodweSensorEntityDescription(
        key="%",
        state_class=SensorStateClass.MEASUREMENT,
        native_unit_of_measurement=PERCENTAGE,
    ),
}
DIAG_SENSOR = GoodweSensorEntityDescription(
    key="_",
    state_class=SensorStateClass.MEASUREMENT,
)
TEXT_SENSOR = GoodweSensorEntityDescription(
    key="text",
)
ENUM_SENSOR = GoodweSensorEntityDescription(
    key="enum",
    device_class=SensorDeviceClass.ENUM,
)


async def async_setup_entry(
    hass: HomeAssistant,
    config_entry: GoodweConfigEntry,
    async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
    """Set up the GoodWe inverter from a config entry."""
    entities: list[InverterSensor] = []
    inverter = config_entry.runtime_data.inverter
    coordinator = config_entry.runtime_data.coordinator
    device_info = config_entry.runtime_data.device_info

    # Individual inverter sensors entities
    entities.extend(
        InverterSensor(coordinator, device_info, inverter, sensor)
        for sensor in inverter.sensors()
    )
    async_add_entities(entities)


class InverterSensor(CoordinatorEntity[GoodweUpdateCoordinator], SensorEntity):
    """Entity representing individual inverter sensor."""

    _attr_has_entity_name = True
    entity_description: GoodweSensorEntityDescription

    def __init__(
        self,
        coordinator: GoodweUpdateCoordinator,
        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 = (
            EntityCategory.DIAGNOSTIC if sensor.id_ not in _MAIN_SENSORS else None
        )
        try:
            self.entity_description = _DESCRIPTIONS[sensor.unit]
        except KeyError:
            if isinstance(sensor, (Enum, EnumH, EnumL, Enum2, EnumCalculated)):
                self.entity_description = ENUM_SENSOR
                self._attr_options = list(sensor._labels.values())
            elif (
                isinstance(sensor, (EnumBitmap4, EnumBitmap22))
                or sensor.id_ == "timestamp"
            ):
                self.entity_description = TEXT_SENSOR
            else:
                self.entity_description = DIAG_SENSOR
                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 = SensorDeviceClass.BATTERY
        self._sensor = sensor
        self._stop_reset: Callable[[], None] | None = None

    @property
    def native_value(self) -> StateType | date | datetime | Decimal:
        """Return the value reported by the sensor."""
        return self.entity_description.value(self.coordinator, self._sensor.id_)

    @property
    def available(self) -> bool:
        """Return if entity is available.

        We delegate the behavior to entity description lambda, since
        some sensors (like energy produced today) should report themselves
        as available even when the (non-battery) pv inverter is off-line during night
        and most of the sensors are actually unavailable.
        """
        return self.entity_description.available(self.coordinator)

    @callback
    def async_reset(self, now):
        """Reset the value back to 0 at midnight.

        Some sensors values like daily produced energy are kept available,
        even when the inverter is in sleep mode and no longer responds to request.
        In contrast to "total" sensors, these "daily" sensors need to be reset to 0 on midnight.
        """
        if not self.coordinator.last_update_success:
            self.coordinator.reset_sensor(self._sensor.id_)
            self.async_write_ha_state()
            _LOGGER.debug("Goodwe reset %s to 0", self.name)
        next_midnight = dt_util.start_of_local_day(
            dt_util.now() + timedelta(days=1, minutes=1)
        )
        self._stop_reset = async_track_point_in_time(
            self.hass, self.async_reset, next_midnight
        )

    async def async_added_to_hass(self) -> None:
        """Schedule reset task at midnight."""
        if self._sensor.id_ in DAILY_RESET:
            next_midnight = dt_util.start_of_local_day(
                dt_util.now() + timedelta(days=1)
            )
            self._stop_reset = async_track_point_in_time(
                self.hass, self.async_reset, next_midnight
            )
        await super().async_added_to_hass()

    async def async_will_remove_from_hass(self) -> None:
        """Remove reset task at midnight."""
        if self._sensor.id_ in DAILY_RESET and self._stop_reset is not None:
            self._stop_reset()
        await super().async_will_remove_from_hass()


================================================
FILE: custom_components/goodwe/services.py
================================================
"""Services for Goodwe integration."""

from __future__ import annotations

import logging

import voluptuous as vol

from homeassistant.core import HomeAssistant
from homeassistant.helpers import device_registry as dr, entity_registry as er

from .const import (
    ATTR_DEVICE_ID,
    ATTR_ENTITY_ID,
    ATTR_PARAMETER,
    ATTR_VALUE,
    DOMAIN,
    SERVICE_GET_PARAMETER,
    SERVICE_SET_PARAMETER,
)

_LOGGER = logging.getLogger(__name__)

SERVICE_GET_PARAMETER_SCHEMA = vol.Schema(
    {
        vol.Required(ATTR_DEVICE_ID): str,
        vol.Required(ATTR_PARAMETER): str,
        vol.Required(ATTR_ENTITY_ID): str,
    }
)

SERVICE_SET_PARAMETER_SCHEMA = vol.Schema(
    {
        vol.Required(ATTR_DEVICE_ID): str,
        vol.Required(ATTR_PARAMETER): str,
        vol.Required(ATTR_VALUE): vol.Any(str, int, bool),
    }
)


async def async_setup_services(hass: HomeAssistant) -> None:
    """Set up services for Goodwe integration."""

    if hass.services.has_service(DOMAIN, SERVICE_GET_PARAMETER):
        return

    async def _get_inverter_by_device_id(hass: HomeAssistant, device_id: str):
        """Return a inverter instance given a device_id."""
        device = dr.async_get(hass).async_get(device_id)
        for runtime_data in hass.data[DOMAIN].values():
            if device.identifiers == runtime_data.device_info.get("identifiers"):
                return runtime_data.inverter
        raise ValueError(f"Inverter for device id {device_id} not found")

    async def async_get_parameter(call):
        """Service for setting inverter parameter."""
        device_id = call.data[ATTR_DEVICE_ID]
        parameter = call.data[ATTR_PARAMETER]
        entity_id = call.data[ATTR_ENTITY_ID]

        _LOGGER.debug("Reading inverter parameter '%s'", parameter)
        inverter = await _get_inverter_by_device_id(hass, device_id)
        value = await inverter.read_setting(parameter)

        entity = er.async_get(hass).async_get(entity_id)
        await hass.services.async_call(
            entity.domain,
            "set_value",
            {ATTR_ENTITY_ID: entity_id, ATTR_VALUE: value},
            blocking=True,
        )

    async def async_set_parameter(call):
        """Service for setting inverter parameter."""
        device_id = call.data[ATTR_DEVICE_ID]
        parameter = call.data[ATTR_PARAMETER]
        value = call.data[ATTR_VALUE]

        _LOGGER.info("Setting inverter parameter '%s' to '%s'", parameter, value)
        inverter = await _get_inverter_by_device_id(hass, device_id)
        await inverter.write_setting(parameter, value)

    hass.services.async_register(
        DOMAIN,
        SERVICE_GET_PARAMETER,
        async_get_parameter,
        schema=SERVICE_GET_PARAMETER_SCHEMA,
    )
    hass.services.async_register(
        DOMAIN,
        SERVICE_SET_PARAMETER,
        async_set_parameter,
        schema=SERVICE_SET_PARAMETER_SCHEMA,
    )


async def async_unload_services(hass: HomeAssistant) -> None:
    """Unload services for Goodwe integration."""

    if hass.services.has_service(DOMAIN, SERVICE_GET_PARAMETER):
        hass.services.async_remove(DOMAIN, SERVICE_GET_PARAMETER)

    if hass.services.has_service(DOMAIN, SERVICE_SET_PARAMETER):
        hass.services.async_remove(DOMAIN, SERVICE_SET_PARAMETER)


================================================
FILE: custom_components/goodwe/services.yaml
================================================
get_parameter:
  name: Get inverter configuration parameter
  description: Read inverter configuration parameter and store it in helper entity
  fields:
    device_id:
      name: Inverter device
      description: ID of the inverter device
      required: true
      selector:
        device:
          integration: goodwe
    parameter:
      name: Parameter
      description: Name of the inverter parameter
      required: true
      selector:
        text:
      example: 'battery_charge_current'
    entity_id:
      name: Helper entity
      description: Entity where to store the parameter value
      required: true
      selector:
        entity:
          domain:
            - input_number
            - input_text
set_parameter:
  name: Set inverter configuration parameter - EXPERIMENTAL
  description: BEWARE !!! Improper use may cause damage !
  fields:
    device_id:
      name: Inverter device
      description: ID of the inverter device
      required: true
      selector:
        device:
          integration: goodwe
    parameter:
      name: Parameter
      description: Name of the inverter parameter
      required: true
      selector:
        text:
      example: 'battery_charge_current'
    value:
      name: Value
      description: Value of the parameter to set
      example: '20'
      required: true
      selector:
        object:


================================================
FILE: custom_components/goodwe/strings.json
================================================
{
  "config": {
    "abort": {
      "already_configured": "[%key:common::config_flow::abort::already_configured_device%]",
      "already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]"
    },
    "error": {
      "connection_error": "[%key:common::config_flow::error::cannot_connect%]"
    },
    "step": {
      "user": {
        "data": {
          "host": "[%key:common::config_flow::data::ip%]",
          "protocol": "Protocol",
          "model_family": "Inverter Family (optional)"
        },
        "description": "Connect to inverter",
        "title": "GoodWe inverter"
      }
    }
  },
  "entity": {
    "button": {
      "synchronize_clock": {
        "name": "Synchronize inverter clock"
      }
    },
    "number": {
      "battery_discharge_depth": {
        "name": "Depth of discharge (on-grid)"
      },
      "battery_discharge_depth_offline": {
        "name": "Depth of discharge (backup)"
      },
      "battery_soc_protection": {
        "name": "Battery SoC protection"
      },
      "eco_mode_power": {
        "name": "Eco mode power"
      },
      "eco_mode_soc": {
        "name": "Eco mode SoC"
      },
      "ems_power_limit": {
        "name": "EMS power limit"
      },
      "fast_charging_power": {
        "name": "Fast charging power"
      },
      "fast_charging_soc": {
        "name": "Fast charging SoC"
      },
      "grid_export_limit": {
        "name": "Grid export limit"
      },
      "soc_upper_limit": {
        "name": "SoC upper limit"
      }
    },
    "select": {
      "ems_mode": {
        "name": "EMS mode",
        "state": {
          "auto": "Auto",
          "charge_pv": "Charge PV",
          "discharge_pv": "Discharge PV",
          "import_ac": "Import AC",
          "export_ac": "Export AC",
          "conserve": "Conserve",
          "off_grid": "Off-grid",
          "battery_standby": "Battery Standby",
          "buy_power": "Buy Power",
          "sell_power": "Sell Power",
          "charge_battery": "Charge Battery",
          "discharge_battery": "Discharge Battery"
        }
      },
      "operation_mode": {
        "name": "Inverter operation mode",
        "state": {
          "backup": "Backup mode",
          "eco": "Eco mode",
          "eco_charge": "Eco charge mode",
          "eco_discharge": "Eco discharge mode",
          "general": "General mode",
          "off_grid": "Off-grid mode",
          "peak_shaving": "Peak shaving mode"
        }
      }
    },
    "switch": {
      "backup_supply_switch": {
        "name": "Backup supply switch"
      },
      "dod_holding_switch": {
        "name": "DOD holding"
      },
      "fast_charging_switch": {
        "name": "Fast charging switch"
      },
      "grid_export_limit_switch": {
        "name": "Grid export limit switch"
      },
      "load_control": {
        "name": "Load control"
      }
    }
  },
  "options": {
    "step": {
      "init": {
        "title": "GoodWe optional settings",
        "description": "Specify optional (network) settings",
        "data": {
          "host": "[%key:common::config_flow::data::ip%]",
          "port": "Port (8899/UDP | 502/TCP)",
          "protocol": "Protocol",
          "keep_alive": "TCP Keep alive",
          "model_family": "Protocol Family [ET|DT|ES] (optional)",
          "scan_interval": "Scan interval (s)",
          "network_retries": "Network retry attempts",
          "network_timeout": "Network request timeout (s)"
        }
      }
    }
  }
}


================================================
FILE: custom_components/goodwe/switch.py
================================================
"""GoodWe PV inverter switch entities."""

from dataclasses import dataclass
import logging
from typing import Any

from goodwe import Inverter, InverterError
from homeassistant.components.switch import (
    SwitchDeviceClass,
    SwitchEntity,
    SwitchEntityDescription,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity import DeviceInfo, EntityCategory
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.update_coordinator import BaseCoordinatorEntity

from .coordinator import GoodweUpdateCoordinator

_LOGGER = logging.getLogger(__name__)


@dataclass(frozen=True, kw_only=True)
class GoodweSwitchEntityDescription(SwitchEntityDescription):
    """Class describing Goodwe switch entities."""

    setting: str
    polling_interval: int = 0


SWITCHES = (
    GoodweSwitchEntityDescription(
        key="load_control",
        translation_key="load_control",
        device_class=SwitchDeviceClass.OUTLET,
        setting="load_control_switch",
    ),
    GoodweSwitchEntityDescription(
        key="grid_export_limit_switch",
        translation_key="grid_export_limit_switch",
        entity_category=EntityCategory.CONFIG,
        device_class=SwitchDeviceClass.SWITCH,
        setting="grid_export",
    ),
    GoodweSwitchEntityDescription(
        key="fast_charging_switch",
        translation_key="fast_charging_switch",
        device_class=SwitchDeviceClass.SWITCH,
        setting="fast_charging",
        polling_interval=30,
    ),
    GoodweSwitchEntityDescription(
        key="backup_supply_switch",
        translation_key="backup_supply_switch",
        entity_category=EntityCategory.CONFIG,
        device_class=SwitchDeviceClass.SWITCH,
        setting="backup_supply",
    ),
    GoodweSwitchEntityDescription(
        key="dod_holding_switch",
        translation_key="dod_holding_switch",
        entity_category=EntityCategory.CONFIG,
        device_class=SwitchDeviceClass.SWITCH,
        setting="dod_holding",
    ),
)


async def async_setup_entry(
    hass: HomeAssistant,
    config_entry: ConfigEntry,
    async_add_entities: AddEntitiesCallback,
) -> None:
    """Set up the inverter switch entities from a config entry."""
    inverter = config_entry.runtime_data.inverter
    coordinator = config_entry.runtime_data.coordinator
    device_info = config_entry.runtime_data.device_info

    entities = []

    for description in SWITCHES:
        try:
            current_state = await inverter.read_setting(description.setting)
        except (InverterError, ValueError):
            # Inverter model does not support this feature
            _LOGGER.debug("Could not read %s value", description.setting)
        else:
            entities.append(
                InverterSwitchEntity(
                    coordinator,
                    device_info,
                    description,
                    inverter,
                    current_state == 1,
                )
            )

    async_add_entities(entities)


class InverterSwitchEntity(
    BaseCoordinatorEntity[GoodweUpdateCoordinator], SwitchEntity
):
    """Switch representation of inverter's 'Load Control' relay."""

    _attr_should_poll = False
    _attr_has_entity_name = True
    entity_description: GoodweSwitchEntityDescription

    def __init__(
        self,
        coordinator: GoodweUpdateCoordinator,
        device_info: DeviceInfo,
        description: GoodweSwitchEntityDescription,
        inverter: Inverter,
        current_is_on: bool,
    ) -> None:
        """Initialize the inverter operation mode setting entity."""
        super().__init__(coordinator)
        self.entity_description = description
        self._attr_unique_id = f"{description.key}-{inverter.serial_number}"
        self._attr_device_info = device_info
        self._attr_is_on = current_is_on
        self._inverter: Inverter = inverter
        self._notify_coordinator()

    async def async_turn_on(self, **kwargs: Any) -> None:
        """Turn the switch on."""
        await self._inverter.write_setting(self.entity_description.setting, 1)
        self._attr_is_on = True
        self._notify_coordinator()
        self.async_write_ha_state()

    async def async_turn_off(self, **kwargs: Any) -> None:
        """Turn the switch off."""
        await self._inverter.write_setting(self.entity_description.setting, 0)
        self._attr_is_on = False
        self._notify_coordinator()
        self.async_write_ha_state()

    async def async_update(self) -> None:
        """Get the current value from inverter."""
        value = await self._inverter.read_setting(self.entity_description.setting)
        self._attr_is_on = value == 1
        self._notify_coordinator()

    def _notify_coordinator(self) -> None:
        if self.entity_description.polling_interval:
            self.coordinator.entity_state_polling(
                self,
                self.entity_description.polling_interval if self._attr_is_on else 0,
            )


================================================
FILE: custom_components/goodwe/translations/cs.json
================================================
{
    "config": {
        "abort": {
            "already_configured": "Zařízení je již nastaveno",
            "already_in_progress": "Nastavení již probíhá"
        },
        "error": {
            "connection_error": "Nepodařilo se připojit"
        },
        "flow_title": "GoodWe",
        "step": {
            "user": {
                "data": {
                    "host": "IP adresa",
                    "port": "Port (8899/UDP | 502/TCP)",
                    "protocol": "Protokol",
                    "model_family": "Typ střídače (volitelné)"
                },
                "description": "Připojí se ke střídači",
                "title": "Střídač GoodWe"
            }
        }
    },
    "entity": {
        "button": {
            "synchronize_clock": {
                "name": "Synchronizovat hodiny střídače"
            },
            "start_inverter": {
                "name": "Start střídače"
            },
            "stop_inverter": {
                "name": "Stop střídače"
            }
        },
        "number": {
            "battery_discharge_depth": {
                "name": "Maximum vybití (v síti)"
            },
            "battery_discharge_depth_offline": {
                "name": "Maximum vybití (backup)"
            },
            "eco_mode_power": {
                "name": "Výkon v ekonomickém režimu"
            },
            "eco_mode_soc": {
                "name": "Stav nabítí baterie ekonomickém režimu"
            },
            "grid_export_limit": {
                "name": "Limit dodávky do sítě"
            },
            "fast_charging_power": {
                "name": "Rychlé nabíjení výkon"
            },
            "fast_charging_soc": {
                "name": "Rychlé nabíjení stav baterie"
            }
        },
        "select": {
            "operation_mode": {
                "name": "Provozní režim střídače",
                "state": {
                    "backup": "Režim zálohy",
                    "eco": "Ekonomický režim",
                    "eco_charge": "Režim ekonomického nabíjení",
                    "eco_discharge": "Režim ekonomického vybíjení",
                    "general": "Obecný režim",
                    "off_grid": "Režim vypnuté sítě",
                    "peak_shaving": "Režim pokrývání špiček",
                    "self_use": "Vlastní spotřeba"

                }
            }
        },
        "sensor": {
            "grid_in_out_label": {
                "state": {
                    "Idle": "Nečinnost",
                    "Exporting": "Export",
                    "Importing": "Import"
                }
            }
        },
        "switch": {
            "grid_export_limit_switch": {
                "name": "Řízení dodávky do sítě"
            },
            "fast_charging_switch": {
                "name": "Rychlé nabíjení"
              },
            "load_control": {
                "name": "Řízení zátěže"
            },
            "backup_supply_switch": {
                "name": "Záloha"
            },
            "dod_holding_switch": {
                "name": "Udržovat DOD baterie"
            }
        }
    },
    "options": {
        "step": {
            "init": {
                "data": {
                    "host": "IP adresa",
                    "port": "Port (8899/UDP | 502/TCP)",
                    "protocol": "Protokol",
                    "keep_alive": "TCP Keep alive",
                    "model_family": "Typ protokolu [ET|DT|ES]",
                    "scan_interval": "Interval skenování (s)",
                    "network_retries": "Počet opakování síťového požadavku",
                    "network_timeout": "Časový limit síťového požadavku (s)"
                },
                "description": "Nastaví volitelné (síťové) volby",
                "title": "Volitelné volby GoodWe"
            }
        }
    }
}


================================================
FILE: custom_components/goodwe/translations/de.json
================================================
{
    "config": {
        "abort": {
            "already_configured": "Gerät ist bereits konfiguriert",
            "already_in_progress": "Die Konfiguration wird bereits bearbeitet"
        },
        "error": {
            "connection_error": "Verbindung fehlgeschlagen"
        },
    "flow_title": "GoodWe",
        "step": {
            "user": {
                "data": {
                    "host": "Hostname / IP-Adresse",
                    "port": "Port (8899/UDP | 502/TCP)",
                    "protocol": "Protokoll",
                    "model_family": "Wechselrichterfamilie (optional)"
                },
                "description": "Verbinden zum Wechselrichter",
                "title": "GoodWe Wechselrichter"
            }
        }
    },
    "entity": {
        "button": {
            "synchronize_clock": {
                "name": "Wechselrichter-Uhr synchronisieren"
            },
            "start_inverter": {
                "name": "Wechselrichter starten"
            },
            "stop_inverter": {
                "name": "Wechselrichter stoppen"
            }
        },
        "number": {
            "battery_discharge_depth": {
                "name": "Entladungstiefe (Netzbetrieb)"
            },
            "eco_mode_power": {
                "name": "Öko-Modus Leistung"
            },
            "eco_mode_soc": {
                "name": "Öko-Modus SoC"
            },
            "grid_export_limit": {
                "name": "Netzeinspeisung Limit"
            },
            "fast_charging_power": {
                "name": "Schnellladeleistung"
            },
              "fast_charging_soc": {
                "name": "Schnelllade SoC"
            }
        },
        "select": {
            "operation_mode": {
                "name": "Wechselrichter-Betriebsart",
                "state": {
                    "backup": "Backup Modus",
                    "eco": "Öko Modus",
                    "eco_charge": "Öko Lademodus",
                    "eco_discharge": "Öko Entlademodus",
                    "general": "Allgemeiner Modus",
                    "off_grid": "Netzunabhängiger Modus",
                    "peak_shaving": "Spitzenlastreduzierungs-Modus",
                    "self_use": "Selbstnutzung-Modus"
                }
            }
        },
        "sensor": {
            "grid_in_out_label": {
                "state": {
                    "0": "Untätig",
                    "1": "Exportieren",
                    "2": "Importieren"
                }
            }
        },
        "switch": {
            "grid_export_limit_switch": {
                "name": "Netz-Export Begrenzungsschalter"
            },
            "fast_charging_switch": {
                "name": "Schnellladungs-Schalter"
              },
            "load_control": {
                "name": "Lastkontrolle"
            }
        }
    },
    "options": {
        "step": {
            "init": {
                "data": {
                    "host": "Hostname / IP-Adresse",
                    "port": "Port (8899/UDP | 502/TCP)",
                    "protocol": "Protokoll",
                    "keep_alive": "TCP Aufrechterhaltung",
                    "model_family": "Protokoll Familie [ET|DT|ES]",
                    "scan_interval": "Scan-Intervall (s)",
                    "network_retries": "Netzwiederholungsversuche",
                    "network_timeout": "Zeitüberschreitung bei Netzanfragen(s)"
                },
                "description": "Optionale (Netzwerk-)Einstellungen",
                "title": "GoodWe optionale Einstellungen"
            }
        }
    }
}



================================================
FILE: custom_components/goodwe/translations/en.json
================================================
{
    "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": "Hostname / IP address",
                    "port": "Port (8899/UDP | 502/TCP)",
                    "protocol": "Protocol",
                    "model_family": "Inverter Family (optional)"
                },
                "description": "Connect to inverter",
                "title": "GoodWe inverter"
            }
        }
    },
    "entity": {
        "button": {
            "synchronize_clock": {
                "name": "Synchronize inverter clock"
            },
            "start_inverter": {
                "name": "Start inverter"
            },
            "stop_inverter": {
                "name": "Stop inverter"
            }
        },
        "number": {
            "battery_discharge_depth": {
                "name": "Depth of discharge (on-grid)"
            },
            "battery_discharge_depth_offline": {
                "name": "Depth of discharge (backup)"
            },
            "battery_soc_protection": {
                "name": "Battery SoC protection"
            },
            "eco_mode_power": {
                "name": "Eco mode power"
            },
            "eco_mode_soc": {
                "name": "Eco mode SoC"
            },
            "ems_power_limit": {
                "name": "EMS power limit"
            },
            "grid_export_limit": {
                "name": "Grid export limit"
            },
            "fast_charging_power": {
                "name": "Fast charging power"
            },
              "fast_charging_soc": {
                "name": "Fast charging SoC"
            }
        },
        "select": {
            "ems_mode": {
                "name": "EMS mode",
                "state": {
                "auto": "Auto",
                "charge_pv": "Charge PV",
                "discharge_pv": "Discharge PV",
                "import_ac": "Import AC",
                "export_ac": "Export AC",
                "conserve": "Conserve",
                "off_grid": "Off-grid",
                "battery_standby": "Battery Standby",
                "buy_power": "Buy Power",
                "sell_power": "Sell Power",
                "charge_battery": "Charge Battery",
                "discharge_battery": "Discharge Battery"
                }
            },
            "operation_mode": {
                "name": "Inverter operation mode",
                "state": {
                    "backup": "Backup mode",
                    "eco": "Eco mode",
                    "eco_charge": "Eco charge mode",
                    "eco_discharge": "Eco discharge mode",
                    "general": "General mode",
                    "off_grid": "Off-grid mode",
                    "peak_shaving": "Peak shaving mode",
                    "self_use": "Self Use mode"
                }
            }
        },
        "sensor": {
            "grid_in_out_label": {
                "state": {
                    "0": "Idle",
                    "1": "Exporting",
                    "2": "Importing"
                }
            }
        },
        "switch": {
            "grid_export_limit_switch": {
                "name": "Grid export limit switch"
            },
            "fast_charging_switch": {
                "name": "Fast charging switch"
              },
            "load_control": {
                "name": "Load control"
            },
            "backup_supply_switch": {
                "name": "Backup supply"
            },
            "dod_holding_switch": {
                "name": "DOD holding"
            }
        }
    },
    "options": {
        "step": {
            "init": {
                "data": {
                    "host": "Hostname / IP Address",
                    "port": "Port (8899/UDP | 502/TCP)",
                    "protocol": "Protocol",
                    "keep_alive": "TCP Keep alive",
                    "model_family": "Protocol Family [ET|DT|ES]",
                    "scan_interval": "Scan interval (s)",
                    "network_retries": "Network retry attempts",
                    "network_timeout": "Network request timeout (s)"
                },
                "description": "Specify optional (network) settings",
                "title": "GoodWe optional settings"
            }
        }
    }
}



================================================
FILE: custom_components/goodwe/translations/es.json
================================================
{
    "config": {
        "abort": {
            "already_configured": "El dispositivo ya está configurado",
            "already_in_progress": "El proceso de configuración ya está en curso"
        },
        "error": {
            "connection_error": "No se pudo conectar"
        },
    "flow_title": "GoodWe",
        "step": {
            "user": {
                "data": {
                    "host": "Nombre de equipo / Dirección IP",
                    "port": "Port (8899/UDP | 502/TCP)",
                    "protocol": "Protocolo",
                    "model_family": "Familia de inversores (opcional)"
                },
                "description": "Conectar al inversor",
                "title": "Inversor GoodWe"
            }
        }
    },
    "entity": {
        "button": {
            "synchronize_clock": {
                "name": "Sincronizar reloj del inversor"
            },
            "start_inverter": {
                "name": "Iniciar inversor"
            },
            "stop_inverter": {
                "name": "Detener inversor"
            }
        },
        "number": {
            "battery_discharge_depth": {
                "name": "Profundidad de descarga (en red)"
            },
            "battery_discharge_depth_offline": {
                "name": "Profundidad de descarga (respaldo)"
            },
            "eco_mode_power": {
                "name": "Potencia en modo ecológico"
            },
            "eco_mode_soc": {
                "name": "Estado de carga en modo ecológico"
            },
            "grid_export_limit": {
                "name": "Límite de exportación a la red"
            },
            "fast_charging_power": {
                "name": "Potencia de carga rápida"
            },
              "fast_charging_soc": {
                "name": "Estado de carga para carga rápida"
            }
        },
        "select": {
            "operation_mode": {
                "name": "Modo de funcionamiento del inversor",
                "state": {
                    "backup": "Modo de respaldo",
                    "eco": "Modo ecológico",
                    "eco_charge": "Modo de carga ecológica",
                    "eco_discharge": "Modo de descarga ecológica",
                    "general": "Modo general",
                    "off_grid": "Modo aislado",
                    "peak_shaving": "Modo de recorte de picos",
                    "self_use": "Modo de autoconsumo"
                }
            }
        },
        "sensor": {
            "grid_in_out_label": {
                "state": {
                    "0": "En reposo",
                    "1": "Exportando",
                    "2": "Importando"
                }
            }
        },
        "switch": {
            "grid_export_limit_switch": {
                "name": "Interruptor de límite de exportación a red"
            },
            "fast_charging_switch": {
                "name": "Interruptor de carga rápida"
              },
            "load_control": {
                "name": "Control de carga"
            },
            "backup_supply_switch": {
                "name": "Suministro de respaldo"
            },
            "dod_holding_switch": {
                "name": "Retención de profundidad de descarga"
            }
        }
    },
    "options": {
        "step": {
            "init": {
                "data": {
                    "host": "Nombre de equipo / Dirección IP",
                    "port": "Port (8899/UDP | 502/TCP)",
                    "protocol": "Protocolo",
                    "keep_alive": "Mantenimiento de conexión TCP",
                    "model_family": "Familia de protocolos [ET|DT|ES]",
                    "scan_interval": "Intervalo de escaneo (s)",
                    "network_retries": "Reintentos de red",
                    "network_timeout": "Tiempo de espera de solicitud de red (s)"
                },
                "description": "Especificar configuraciones opcionales (de red)",
                "title": "Configuraciones opcionales de GoodWe"
            }
        }
    }
}



================================================
FILE: custom_components/goodwe/translations/sk.json
================================================
{
    "config": {
        "abort": {
            "already_configured": "Zariadenie je už nastavené",
            "already_in_progress": "Konfigurácia už prebieha"
        },
        "error": {
            "connection_error": "Nepodarilo sa pripojiť"
        },
        "flow_title": "GoodWe",
        "step": {
            "user": {
                "data": {
                    "host": "IP adresa",
                    "port": "Port (8899/UDP | 502/TCP)",
                    "protocol": "Protokol",
                    "model_family": "Typ meniča (voliteľné)"
                },
                "description": "Pripojiť sa k meniča",
                "title": "Menič GoodWe"
            }
        }
    },
    "entity": {
        "button": {
            "synchronize_clock": {
                "name": "Synchronizácia hodín meniča"
            },
            "start_inverter": {
                "name": "Zapnúť menič"
            },
            "stop_inverter": {
                "name": "Zastaviť menič"
            }
        },
        "number": {
            "battery_discharge_depth": {
                "name": "Maximum vybitia (v sieti)"
            },
            "eco_mode_power": {
                "name": "Výkon v ekonomickom režime"
            },
            "eco_mode_soc": {
                "name": "Stav nabitia batérie v ekonomickom režime"
            },
            "grid_export_limit": {
                "name": "Limit dodávky do siete"
            },
            "fast_charging_power": {
                "name": "Rýchle nabíjanie výkon"
            },
            "fast_charging_soc": {
                "name": "Rýchle nabíjanie stav batérie"
            }
        },
        "select": {
            "operation_mode": {
                "name": "Prevádzkový režim meniča",
                "state": {
                    "backup": "Režim zálohovania",
                    "eco": "Ekonomický režim",
                    "eco_charge": "Režim ekonomického nabíjania",
                    "eco_discharge": "Režim ekonomického vybíjania",
                    "general": "Všeobecný režim",
                    "off_grid": "Režim mimo siete",
                    "peak_shaving": "Režim šetrenia v špičke",
                    "self_use": "Vlastná spotreba"

                }
            }
        },
        "sensor": {
            "grid_in_out_label": {
                "state": {
                    "Idle": "Nečinnosť",
                    "Exporting": "Export",
                    "Importing": "Import"
                }
            }
        },
        "switch": {
            "grid_export_limit_switch": {
                "name": "Riadenie dodávky do siete"
            },
            "fast_charging_switch": {
                "name": "Rýchle nabíjanie"
              },
            "load_control": {
                "name": "Riadenie záťaže"
            },
            "backup_supply_switch": {
                "name": "Záloha"
            }
        }
    },
    "options": {
        "step": {
            "init": {
                "data": {
                    "host": "IP adresa",
                    "port": "Port (8899/UDP | 502/TCP)",
                    "protocol": "Protokol",
                    "keep_alive": "TCP Keep alive",
                    "model_family": "Typ protokolu [ET|DT|ES]",
                    "scan_interval": "Interval skenovania (s)",
                    "network_retries": "Počet opakovaní sieťových dopytov",
                    "network_timeout": "Časový limit sieťových dopytov (s)"
                },
                "description": "Nastaví voliteľné (sieťové) parametre",
                "title": "Voliteľné parametre GoodWe"
            }
        }
    }
}


================================================
FILE: hacs.json
================================================
{
  "name": "GoodWe Inverter (experimental)",
  "homeassistant": "2025.12.0"
}


================================================
FILE: info.md
================================================
[!["Buy Me A Coffee"](https://www.buymeacoffee.com/assets/img/custom_images/orange_img.png)](https://www.buymeacoffee.com/mletenay)
[![hacs_badge](https://img.shields.io/badge/HACS-Default-orange.svg)](https://github.com/hacs/integration)
![GitHub Release](https://img.shields.io/github/v/release/mletenay/home-assistant-goodwe-inverter)

## GoodWe solar inverter for Home Assistant (experimental)

Support for Goodwe solar inverters is present as native integration of [Home Assistant](https://www.home-assistant.io/integrations/goodwe/) since its release 2022.2 and is recommended for most users.

This custom component is experimental version with features not (yet) present in standard HA's integration and is intended for users with specific needs and early adopters of new features.
Use at own risk.

### Differences between this HACS and native HA integration

- EMS modes
- Special work modes `Eco charge mode` and `Eco discharge mode` (24/7 with defined power and SoC).
- Network configuration parameters `Scan iterval`, `Network retry attempts`, `Network request timeout`.
- Switch `Export Limit Switch`.
- Switch `Load Control` (for ET+ inverters).
- Switch and SoC/Power inputs for `Fast Charging` functionality.
- `Start inverter` and `Stop inverter` buttons for grid-only inverters.
- Services for getting/setting inverter configuration parameters

### Migration from HACS to HA

If you have been using this custom component and want to migrate to standard HA integration, the migration is straightforward. Just remove the integration from HACS (press Ignore and force uninstall despite the warning the integration is still configured). Afrer restart of Home Assistant, the standard Goodwe integration will start and all your existing settings, entity names, history and statistics should be preserved.

(If you uninstall the integration first, then uninstall HACS component and install integration back again, it will also work, but you will probably loose some history and settings since HA integration uses slightly different default entity names.)

### Documentation

Find the full documentation [here](https://github.com/mletenay/home-assistant-goodwe-inverter).


================================================
FILE: inverter_scan.py
================================================
"""Simple test script to scan inverter present on local network"""
import asyncio
import goodwe
import logging
import sys

logging.basicConfig(
    format="%(asctime)-15s %(funcName)s(%(lineno)d) - %(levelname)s: %(message)s",
    stream=sys.stderr,
    level=getattr(logging, "ERROR", None),
)

result = asyncio.run(goodwe.search_inverters()).decode("utf-8").split(",")
print(f"Located inverter at IP: {result[0]}, mac: {result[1]}, name: {result[2]}")

inverter = asyncio.run(goodwe.discover(result[0], 8899))
print(
    f"Identified inverter model: {inverter.model_name}, serialNr: {inverter.serial_number}"
)


================================================
FILE: inverter_test.py
================================================
"""Simple test script to check inverter UDP protocol communication"""

import asyncio
import goodwe
import logging
import sys


logging.basicConfig(
    format="%(asctime)-15s %(funcName)s(%(lineno)d) - %(levelname)s: %(message)s",
    stream=sys.stderr,
    level=getattr(logging, "ERROR", None),
)

# Set the appropriate IP address
IP_ADDRESS = "192.168.2.14"
PORT = 8899

FAMILY = "ET"  # One of ET, ES, DT or None to detect inverter family automatically
COMM_ADDR = None  # Usually 0xf7 for ET/ES or 0x7f for DT, or None for default value
TIMEOUT = 1
RETRIES = 3

inverter = asyncio.run(
    goodwe.connect(host=IP_ADDRESS, family=FAMILY, timeout=TIMEOUT, retries=RETRIES)
)
print(
    f"Identified inverter:\n"
    f"\tModel:    {inverter.model_name}\n"
    f"\tSerialNr: {inverter.serial_number}\n"
    f"\tFirmware: {inverter.firmware}"
)

response = asyncio.run(inverter.read_runtime_data())

print("\nSensors values:")
for sensor in inverter.sensors():
    if sensor.id_ in response:
        print(
            f"\t{sensor.id_:30}:\t{sensor.name} = {response[sensor.id_]} {sensor.unit}"
        )
Download .txt
gitextract_ho4ycwf4/

├── .github/
│   └── workflows/
│       ├── hassfest.yaml
│       └── validate.yaml
├── .gitignore
├── LICENSE
├── README.md
├── custom_components/
│   └── goodwe/
│       ├── __init__.py
│       ├── button.py
│       ├── config_flow.py
│       ├── const.py
│       ├── coordinator.py
│       ├── diagnostics.py
│       ├── icons.json
│       ├── manifest.json
│       ├── number.py
│       ├── select.py
│       ├── sensor.py
│       ├── services.py
│       ├── services.yaml
│       ├── strings.json
│       ├── switch.py
│       └── translations/
│           ├── cs.json
│           ├── de.json
│           ├── en.json
│           ├── es.json
│           └── sk.json
├── hacs.json
├── info.md
├── inverter_scan.py
└── inverter_test.py
Download .txt
SYMBOL INDEX (69 symbols across 10 files)

FILE: custom_components/goodwe/__init__.py
  function async_setup_entry (line 27) | async def async_setup_entry(hass: HomeAssistant, entry: GoodweConfigEntr...
  function async_check_port (line 94) | async def async_check_port(
  function async_unload_entry (line 111) | async def async_unload_entry(
  function update_listener (line 128) | async def update_listener(hass: HomeAssistant, config_entry: GoodweConfi...
  function async_migrate_entry (line 133) | async def async_migrate_entry(

FILE: custom_components/goodwe/button.py
  class GoodweButtonEntityDescription (line 22) | class GoodweButtonEntityDescription(ButtonEntityDescription):
  function async_setup_entry (line 52) | async def async_setup_entry(
  class GoodweButtonEntity (line 81) | class GoodweButtonEntity(ButtonEntity):
    method __init__ (line 88) | def __init__(
    method async_press (line 100) | async def async_press(self) -> None:

FILE: custom_components/goodwe/config_flow.py
  class OptionsFlowHandler (line 61) | class OptionsFlowHandler(OptionsFlow):
    method __init__ (line 64) | def __init__(self, config_entry: ConfigEntry) -> None:
    method async_step_init (line 68) | async def async_step_init(self, user_input: dict | None = None) -> Con...
  class GoodweFlowHandler (line 111) | class GoodweFlowHandler(ConfigFlow, domain=DOMAIN):
    method async_get_options_flow (line 118) | def async_get_options_flow(
    method async_handle_successful_connection (line 124) | async def async_handle_successful_connection(
    method async_step_user (line 145) | async def async_step_user(
    method async_detect_inverter_port (line 180) | async def async_detect_inverter_port(

FILE: custom_components/goodwe/coordinator.py
  class GoodweRuntimeData (line 29) | class GoodweRuntimeData:
  class GoodweUpdateCoordinator (line 37) | class GoodweUpdateCoordinator(DataUpdateCoordinator[dict[str, Any]]):
    method __init__ (line 42) | def __init__(
    method _async_update_data (line 62) | async def _async_update_data(self) -> dict[str, Any]:
    method _update_polled_entities (line 89) | async def _update_polled_entities(self) -> None:
    method sensor_value (line 97) | def sensor_value(self, sensor: str) -> Any:
    method total_sensor_value (line 102) | def total_sensor_value(self, sensor: str) -> Any:
    method reset_sensor (line 107) | def reset_sensor(self, sensor: str) -> None:
    method entity_state_polling (line 116) | def entity_state_polling(

FILE: custom_components/goodwe/diagnostics.py
  function async_get_config_entry_diagnostics (line 13) | async def async_get_config_entry_diagnostics(
  function _read_register (line 42) | async def _read_register(inverter: Inverter, register: int) -> Any:

FILE: custom_components/goodwe/number.py
  class GoodweNumberEntityDescription (line 27) | class GoodweNumberEntityDescription(NumberEntityDescription):
  function _get_setting_unit (line 36) | def _get_setting_unit(inverter: Inverter, setting: str) -> str:
  function set_offline_battery_dod (line 41) | async def set_offline_battery_dod(inverter: Inverter, dod: int) -> None:
  function get_offline_battery_dod (line 47) | async def get_offline_battery_dod(inverter: Inverter) -> int:
  function async_setup_entry (line 202) | async def async_setup_entry(
  class InverterNumberEntity (line 235) | class InverterNumberEntity(NumberEntity):
    method __init__ (line 242) | def __init__(
    method async_update (line 256) | async def async_update(self) -> None:
    method async_set_native_value (line 261) | async def async_set_native_value(self, value: float) -> None:

FILE: custom_components/goodwe/select.py
  class GoodweSelectEntityDescription (line 44) | class GoodweSelectEntityDescription(SelectEntityDescription):
  function async_setup_entry (line 64) | async def async_setup_entry(
  class InverterOperationModeEntity (line 141) | class InverterOperationModeEntity(SelectEntity):
    method __init__ (line 147) | def __init__(
    method async_select_option (line 167) | async def async_select_option(self, option: str) -> None:
    method async_update (line 187) | async def async_update(self) -> None:
    method update_eco_mode_power (line 192) | async def update_eco_mode_power(self, event: Event) -> None:
    method update_eco_mode_soc (line 217) | async def update_eco_mode_soc(self, event: Event) -> None:
  class InverterEMSModeEntity (line 243) | class InverterEMSModeEntity(SelectEntity):
    method __init__ (line 250) | def __init__(
    method async_select_option (line 265) | async def async_select_option(self, option: str) -> None:
    method async_update (line 276) | async def async_update(self) -> None:

FILE: custom_components/goodwe/sensor.py
  class GoodweSensorEntityDescription (line 87) | class GoodweSensorEntityDescription(SensorEntityDescription):
  function async_setup_entry (line 177) | async def async_setup_entry(
  class InverterSensor (line 196) | class InverterSensor(CoordinatorEntity[GoodweUpdateCoordinator], SensorE...
    method __init__ (line 202) | def __init__(
    method native_value (line 239) | def native_value(self) -> StateType | date | datetime | Decimal:
    method available (line 244) | def available(self) -> bool:
    method async_reset (line 255) | def async_reset(self, now):
    method async_added_to_hass (line 273) | async def async_added_to_hass(self) -> None:
    method async_will_remove_from_hass (line 284) | async def async_will_remove_from_hass(self) -> None:

FILE: custom_components/goodwe/services.py
  function async_setup_services (line 41) | async def async_setup_services(hass: HomeAssistant) -> None:
  function async_unload_services (line 97) | async def async_unload_services(hass: HomeAssistant) -> None:

FILE: custom_components/goodwe/switch.py
  class GoodweSwitchEntityDescription (line 25) | class GoodweSwitchEntityDescription(SwitchEntityDescription):
  function async_setup_entry (line 70) | async def async_setup_entry(
  class InverterSwitchEntity (line 102) | class InverterSwitchEntity(
    method __init__ (line 111) | def __init__(
    method async_turn_on (line 128) | async def async_turn_on(self, **kwargs: Any) -> None:
    method async_turn_off (line 135) | async def async_turn_off(self, **kwargs: Any) -> None:
    method async_update (line 142) | async def async_update(self) -> None:
    method _notify_coordinator (line 148) | def _notify_coordinator(self) -> None:
Condensed preview — 29 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (116K chars).
[
  {
    "path": ".github/workflows/hassfest.yaml",
    "chars": 242,
    "preview": "name: Validate with hassfest\n\non:\n  push:\n  pull_request:\n  schedule:\n    - cron: \"0 0 * * *\"\n\njobs:\n  validate:\n    run"
  },
  {
    "path": ".github/workflows/validate.yaml",
    "chars": 320,
    "preview": "name: Validate\n\non:\n  push:\n  pull_request:\n  schedule:\n    - cron: \"0 0 * * *\"\n  workflow_dispatch:\n\npermissions: {}\n\nj"
  },
  {
    "path": ".gitignore",
    "chars": 1804,
    "preview": "# Byte-compiled / optimized / DLL files\n__pycache__/\n*.py[cod]\n*$py.class\n\n# C extensions\n*.so\n\n# Distribution / packagi"
  },
  {
    "path": "LICENSE",
    "chars": 1065,
    "preview": "MIT License\n\nCopyright (c) 2020 mletenay\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\no"
  },
  {
    "path": "README.md",
    "chars": 11961,
    "preview": "[![\"Buy Me A Coffee\"](https://www.buymeacoffee.com/assets/img/custom_images/orange_img.png)](https://www.buymeacoffee.co"
  },
  {
    "path": "custom_components/goodwe/__init__.py",
    "chars": 6073,
    "preview": "\"\"\"The Goodwe inverter component.\"\"\"\n\nfrom goodwe import Inverter, InverterError, connect\nfrom goodwe.const import GOODW"
  },
  {
    "path": "custom_components/goodwe/button.py",
    "chars": 3252,
    "preview": "\"\"\"GoodWe PV inverter selection settings entities.\"\"\"\n\nfrom collections.abc import Awaitable, Callable\nfrom dataclasses "
  },
  {
    "path": "custom_components/goodwe/config_flow.py",
    "chars": 6304,
    "preview": "\"\"\"Config flow to configure Goodwe inverters using their local API.\"\"\"\n\nfrom __future__ import annotations\n\nimport loggi"
  },
  {
    "path": "custom_components/goodwe/const.py",
    "chars": 792,
    "preview": "\"\"\"Constants for the Goodwe component.\"\"\"\n\nfrom datetime import timedelta\n\nfrom homeassistant.const import Platform\n\nDOM"
  },
  {
    "path": "custom_components/goodwe/coordinator.py",
    "chars": 4337,
    "preview": "\"\"\"Update coordinator for Goodwe.\"\"\"\n\nfrom __future__ import annotations\n\nfrom dataclasses import dataclass\nfrom datetim"
  },
  {
    "path": "custom_components/goodwe/diagnostics.py",
    "chars": 1704,
    "preview": "\"\"\"Diagnostics support for Goodwe.\"\"\"\n\nfrom __future__ import annotations\n\nfrom typing import Any\n\nfrom goodwe import In"
  },
  {
    "path": "custom_components/goodwe/icons.json",
    "chars": 2049,
    "preview": "{\n  \"entity\": {\n    \"button\": {\n      \"synchronize_clock\": {\n        \"default\": \"mdi:clock-check-outline\"\n      },\n     "
  },
  {
    "path": "custom_components/goodwe/manifest.json",
    "chars": 474,
    "preview": "{\n  \"domain\": \"goodwe\",\n  \"name\": \"GoodWe Inverter\",\n  \"codeowners\": [\n    \"@mletenay\",\n    \"@starkillerOG\",\n    \"@fizcr"
  },
  {
    "path": "custom_components/goodwe/number.py",
    "chars": 9813,
    "preview": "\"\"\"GoodWe PV inverter numeric settings entities.\"\"\"\n\nfrom __future__ import annotations\n\nfrom collections.abc import Awa"
  },
  {
    "path": "custom_components/goodwe/select.py",
    "chars": 10183,
    "preview": "\"\"\"GoodWe PV inverter selection settings entities.\"\"\"\n\nfrom dataclasses import dataclass\nimport logging\n\nfrom goodwe imp"
  },
  {
    "path": "custom_components/goodwe/sensor.py",
    "chars": 10354,
    "preview": "\"\"\"Support for GoodWe inverter via UDP.\"\"\"\n\nfrom __future__ import annotations\n\nfrom collections.abc import Callable\nfro"
  },
  {
    "path": "custom_components/goodwe/services.py",
    "chars": 3293,
    "preview": "\"\"\"Services for Goodwe integration.\"\"\"\n\nfrom __future__ import annotations\n\nimport logging\n\nimport voluptuous as vol\n\nfr"
  },
  {
    "path": "custom_components/goodwe/services.yaml",
    "chars": 1370,
    "preview": "get_parameter:\n  name: Get inverter configuration parameter\n  description: Read inverter configuration parameter and sto"
  },
  {
    "path": "custom_components/goodwe/strings.json",
    "chars": 3512,
    "preview": "{\n  \"config\": {\n    \"abort\": {\n      \"already_configured\": \"[%key:common::config_flow::abort::already_configured_device%"
  },
  {
    "path": "custom_components/goodwe/switch.py",
    "chars": 5080,
    "preview": "\"\"\"GoodWe PV inverter switch entities.\"\"\"\n\nfrom dataclasses import dataclass\nimport logging\nfrom typing import Any\n\nfrom"
  },
  {
    "path": "custom_components/goodwe/translations/cs.json",
    "chars": 3898,
    "preview": "{\n    \"config\": {\n        \"abort\": {\n            \"already_configured\": \"Zařízení je již nastaveno\",\n            \"already"
  },
  {
    "path": "custom_components/goodwe/translations/de.json",
    "chars": 3659,
    "preview": "{\n    \"config\": {\n        \"abort\": {\n            \"already_configured\": \"Gerät ist bereits konfiguriert\",\n            \"al"
  },
  {
    "path": "custom_components/goodwe/translations/en.json",
    "chars": 4664,
    "preview": "{\n    \"config\": {\n        \"abort\": {\n            \"already_configured\": \"Device is already configured\",\n            \"alre"
  },
  {
    "path": "custom_components/goodwe/translations/es.json",
    "chars": 4121,
    "preview": "{\n    \"config\": {\n        \"abort\": {\n            \"already_configured\": \"El dispositivo ya está configurado\",\n           "
  },
  {
    "path": "custom_components/goodwe/translations/sk.json",
    "chars": 3712,
    "preview": "{\n    \"config\": {\n        \"abort\": {\n            \"already_configured\": \"Zariadenie je už nastavené\",\n            \"alread"
  },
  {
    "path": "hacs.json",
    "chars": 79,
    "preview": "{\n  \"name\": \"GoodWe Inverter (experimental)\",\n  \"homeassistant\": \"2025.12.0\"\n}\n"
  },
  {
    "path": "info.md",
    "chars": 2182,
    "preview": "[![\"Buy Me A Coffee\"](https://www.buymeacoffee.com/assets/img/custom_images/orange_img.png)](https://www.buymeacoffee.co"
  },
  {
    "path": "inverter_scan.py",
    "chars": 613,
    "preview": "\"\"\"Simple test script to scan inverter present on local network\"\"\"\nimport asyncio\nimport goodwe\nimport logging\nimport sy"
  },
  {
    "path": "inverter_test.py",
    "chars": 1106,
    "preview": "\"\"\"Simple test script to check inverter UDP protocol communication\"\"\"\n\nimport asyncio\nimport goodwe\nimport logging\nimpor"
  }
]

About this extraction

This page contains the full source code of the mletenay/home-assistant-goodwe-inverter GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 29 files (105.5 KB), approximately 24.7k tokens, and a symbol index with 69 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!