[
  {
    "path": ".github/workflows/hassfest.yaml",
    "content": "name: Validate with hassfest\n\non:\n  push:\n  pull_request:\n  schedule:\n    - cron: \"0 0 * * *\"\n\njobs:\n  validate:\n    runs-on: \"ubuntu-latest\"\n    steps:\n      - uses: \"actions/checkout@v3\"\n      - uses: home-assistant/actions/hassfest@master\n"
  },
  {
    "path": ".github/workflows/validate.yaml",
    "content": "name: Validate\n\non:\n  push:\n  pull_request:\n  schedule:\n    - cron: \"0 0 * * *\"\n  workflow_dispatch:\n\npermissions: {}\n\njobs:\n  validate-hacs:\n    runs-on: \"ubuntu-latest\"\n    steps:\n      - name: HACS validation\n        uses: \"hacs/action@main\"\n        with:\n          category: \"integration\"\n          ignore: \"brands\"\n"
  },
  {
    "path": ".gitignore",
    "content": "# Byte-compiled / optimized / DLL files\n__pycache__/\n*.py[cod]\n*$py.class\n\n# C extensions\n*.so\n\n# Distribution / packaging\n.Python\nbuild/\ndevelop-eggs/\ndist/\ndownloads/\neggs/\n.eggs/\nlib/\nlib64/\nparts/\nsdist/\nvar/\nwheels/\npip-wheel-metadata/\nshare/python-wheels/\n*.egg-info/\n.installed.cfg\n*.egg\nMANIFEST\n\n# PyInstaller\n#  Usually these files are written by a python script from a template\n#  before PyInstaller builds the exe, so as to inject date/other infos into it.\n*.manifest\n*.spec\n\n# Installer logs\npip-log.txt\npip-delete-this-directory.txt\n\n# Unit test / coverage reports\nhtmlcov/\n.tox/\n.nox/\n.coverage\n.coverage.*\n.cache\nnosetests.xml\ncoverage.xml\n*.cover\n*.py,cover\n.hypothesis/\n.pytest_cache/\n\n# Translations\n*.mo\n*.pot\n\n# Django stuff:\n*.log\nlocal_settings.py\ndb.sqlite3\ndb.sqlite3-journal\n\n# Flask stuff:\ninstance/\n.webassets-cache\n\n# Scrapy stuff:\n.scrapy\n\n# Sphinx documentation\ndocs/_build/\n\n# PyBuilder\ntarget/\n\n# Jupyter Notebook\n.ipynb_checkpoints\n\n# IPython\nprofile_default/\nipython_config.py\n\n# pyenv\n.python-version\n\n# pipenv\n#   According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.\n#   However, in case of collaboration, if having platform-specific dependencies or dependencies\n#   having no cross-platform support, pipenv may install dependencies that don't work, or not\n#   install all needed dependencies.\n#Pipfile.lock\n\n# PEP 582; used by e.g. github.com/David-OConnor/pyflow\n__pypackages__/\n\n# Celery stuff\ncelerybeat-schedule\ncelerybeat.pid\n\n# SageMath parsed files\n*.sage.py\n\n# Environments\n.env\n.venv\nenv/\nvenv/\nENV/\nenv.bak/\nvenv.bak/\n\n# Spyder project settings\n.spyderproject\n.spyproject\n\n# Rope project settings\n.ropeproject\n\n# mkdocs documentation\n/site\n\n# mypy\n.mypy_cache/\n.dmypy.json\ndmypy.json\n\n# Pyre type checker\n.pyre/\n.idea"
  },
  {
    "path": "LICENSE",
    "content": "MIT License\n\nCopyright (c) 2020 mletenay\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
  },
  {
    "path": "README.md",
    "content": "[![\"Buy Me A Coffee\"](https://www.buymeacoffee.com/assets/img/custom_images/orange_img.png)](https://www.buymeacoffee.com/mletenay)\n[![hacs_badge](https://img.shields.io/badge/HACS-Default-orange.svg)](https://github.com/hacs/integration)\n[![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)\n![GitHub Release](https://img.shields.io/github/v/release/mletenay/home-assistant-goodwe-inverter)\n<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\">\n\n## GoodWe solar inverter for Home Assistant (experimental)\n\nSupport 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.\n\nThis 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.\nUse at own risk.\n\n### Differences between this HACS and native HA integration\n\n- EMS modes\n- Special work modes `Eco charge mode` and `Eco discharge mode` (24/7 with defined power and SoC).\n- Network configuration parameters `Port`, `Modbus id`, `Scan iterval`, `Network retry attempts`, `Network request timeout`.\n- Input `SoC upper limit`, `DoD (backup)`\n- Switch `DOD holding`, `Export Limit`. `Load Control`, `Backup supply`\n- Switch and SoC/Power inputs for `Fast Charging` functionality.\n- `Start inverter` and `Stop inverter` buttons for grid-only inverters.\n- Services for getting/setting inverter configuration parameters\n\n### Migration from HACS to HA\n\nIf 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.\n\n(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.)\n\n## EMS modes\n\nThe integration exposes inverter's EMS mode and EMS power (limit) settings.\nThe following list should explain individual modes and their behavior.\n(The `Xset`/`Xmax` variables below are values of the `EMS power` setting. )\n\n- **Auto**\n  - _Scenario:_ Self-use.\n  - `PBattery = PInv - Pmeter - Ppv` (Discharge/Charge)\n  - The battery power is controlled by the meter power when the meter communication is normal.\n\n- **Charge PV**\n  - _Scenario:_ Control the battery to keep charging.\n  - `PBattery = Xmax + PV` (Charge)\n  - 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.\n  - _Interpretation:_ Charge Battery from PV (high priority) or Grid (low priority); EmsPowerSet = negative ESS ActivePower (if possible because of PV).\n  - Grid: low priority, PV: high priority, Battery: Charge Mode, The control object is 'Grid'\n\n- **Discharge PV**\n  - _Scenario:_ Control the battery to keep discharging.\n  - `PBattery = Xmax` (Discharge)\n  - Xmax is the allowable discharge power of the battery. When the power fed into the grid is limited, PV power will be used first.\n  - _Interpretation:_ ESS ActivePower = PV power + EmsPowerSet (i.e. battery discharge); useful for surplus feed-to-grid.\n  - PV: high priority, Battery: low priority, Grid: Energy Out Mode, The control object is 'Battery'\n\n- **Import AC**\n  - _Scenario:_ The inverter is used as a unit for power grid energy scheduling.\n  - `PBattery = Xset + PV` (Charge)\n  - 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)\n  - _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!\n  - Grid: high priority, PV: low priority, Battery: Charge Mode, The control object is 'Grid'\n\n- **Export AC**\n  - _Scenario:_ The inverter is used as a unit for power grid energy scheduling.\n  - `PBattery = Xset` (Discharge)\n  - 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)\n  - _Interpretation:_ EmsPowerSet = positive ESS ActivePower. But PV will be limited, i.e. remaining power is not used to charge battery.\n  - PV: high priority, Battery: low priority, Grid: Energy Out Mode, The control object is 'Grid'\n\n- **Conserve**\n  - _Scenario:_ Off-grid reservation mode.\n  - `PBattery = PV` (Charge)\n  - 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.\n\n- **Off-Grid**\n  - _Scenario:_ Off-Grid Mode.\n  - `PBattery = Pbackup - Ppv` (Charge/Discharge)\n  - Forced off-grid operation.\n\n- **Battery Standby**\n  - _Scenario:_ The inverter is used as a unit for power grid energy scheduling.\n  - `PBattery = 0` (Standby)\n  - The battery does not charge and discharge\n\n- **Buy Power**\n  - _Scenario:_ Regional energy management.\n  - `PBattery = PInv - (Pmeter + Xset) - Ppv` (Charge/Discharge)\n  - 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.\n  - _Interpretation:_ Control power at the point of common coupling.\n  - Grid: high priority, PV: low priority, Battery: Energy In and Out Mode, The control object is 'Grid'\n\n- **Sell Power**\n  - _Scenario:_ Regional energy management.\n  - `PBattery = PInv - (Pmeter - Xset) - Ppv` (Charge/Discharge)\n  - 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.\n  - _Interpretation:_ Control power at the point of common coupling.\n  - PV: high priority, Battery: low priority, Grid: Energy Out Mode, The control object is 'Grid'\n\n- **Charge Battery**\n  - _Scenario:_ Force the battery to work at set power value.\n  - `PBattery = Xset` (Charge)\n  - 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.\n  - _Interpretation:_ Charge Battery from PV (high priority) or Grid (low priority); priorities are inverted compared to IMPORT_AC.\n  - PV: high priority, Grid: low priority, Battery: Energy In Mode, The control object is 'Battery'\n\n- **Discharge Battery**\n  - _Scenario:_ Force the battery to work at set power value.\n  - `PBattery = Xset` (Discharge)\n  - 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.\n  - _Interpretation:_ ???\n  - PV: low priority, Battery: high priority, Grid: Energy In Mode, The control object is 'Battery'\n\n- **Stopped**\n  - _Scenario:_ System shutdown.\n  - Stop working and turn to wait mode\n\n## Home Assistant Energy Dashboard\n\nThe integration provides several values suitable for the energy dashboard introduced to HA in v2021.8.\nThe 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.\nFor the other inverter families, if such sensors are not directly available from the inverter, they can be calculated, see paragraph below.\n\n## Cumulative energy values\n\nThe sensor values reported by the inverter are instant measurements.\nTo report summary (energy) values like daily/monthly sell or buy (in kWh), these values have to be aggregated over time.\n\n[Riemann Sum](https://www.home-assistant.io/integrations/integration/) integration can be used to convert these instant (W) values into cumulative values (Wh).\n[Utility Meter](https://www.home-assistant.io/integrations/utility_meter) can report these values as human readable statistical values.\n[Template Sensor](https://www.home-assistant.io/integrations/template/) can be used to separate buy and sell values.\n\n```YAML\nsensor:\n  - platform: template\n    sensors:\n      # Template sensor for values of energy bought (active_power < 0)\n      energy_buy:\n        device_class: power\n        friendly_name: \"Energy Buy\"\n        unit_of_measurement: 'W'\n        value_template: >-\n          {% if states('sensor.goodwe_active_power')|float < 0 %}\n            {{ states('sensor.goodwe_active_power')|float * -1 }}\n          {% else %}\n            {{ 0 }}\n          {% endif %}\n      # Template sensor for values of energy sold (active_power > 0)\n      energy_sell:\n        device_class: power\n        friendly_name: \"Energy Sell\"\n        unit_of_measurement: 'W'\n        value_template: >-\n          {% if states('sensor.goodwe_active_power')|float > 0 %}\n            {{ states('sensor.goodwe_active_power')|float }}\n          {% else %}\n            {{ 0 }}\n          {% endif %}\n\n  # Sensor for Riemann sum of energy bought (W -> kWh)\n  - platform: integration\n    source: sensor.energy_buy\n    name: energy_buy_sum\n    unit_prefix: k\n    round: 1\n    method: left\n  # Sensor for Riemann sum of energy sold (W -> kWh)\n  - platform: integration\n    source: sensor.energy_sell\n    name: energy_sell_sum\n    unit_prefix: k\n    round: 1\n    method: left\n\nutility_meter:\n  energy_buy_daily:\n    source: sensor.energy_buy_sum\n    cycle: daily\n  energy_buy_monthly:\n    source: sensor.energy_buy_sum\n    cycle: monthly\n  energy_sell_daily:\n    source: sensor.energy_sell_sum\n    cycle: daily\n  energy_sell_monthly:\n    source: sensor.energy_sell_sum\n    cycle: monthly\n  house_consumption_daily:\n    source: sensor.house_consumption_sum\n    cycle: daily\n  house_consumption_monthly:\n    source: sensor.house_consumption_sum\n    cycle: monthly\n```\n\n## Troubleshooting\n\nIf 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.\n\n```YAML\nlogger:\n  default: warning\n  logs:\n    custom_components.goodwe: debug\n    goodwe: debug\n```\n\n## Source code\n\nThe 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.\n\n## Inverter discovery and communication testing\n\nTo test whether the inverter properly responds to UDP request, just execute the `inverter_test.py` script in your python (3.8+) environment.\nThe `inverter_scan.py` script can be used to discover inverter(s) on your local network.\n\n## References and inspiration\n\n- https://github.com/marcelblijleven/goodwe\n- https://www.photovoltaikforum.com/core/attachment/342066-bluetooth-firmware-update-string-storage-de-v002-pdf/\n- https://github.com/robbinjanssen/home-assistant-omnik-inverter\n"
  },
  {
    "path": "custom_components/goodwe/__init__.py",
    "content": "\"\"\"The Goodwe inverter component.\"\"\"\n\nfrom goodwe import Inverter, InverterError, connect\nfrom goodwe.const import GOODWE_TCP_PORT, GOODWE_UDP_PORT\nfrom homeassistant.const import CONF_HOST, CONF_PORT, CONF_PROTOCOL, CONF_SCAN_INTERVAL\nfrom homeassistant.core import HomeAssistant\nfrom homeassistant.exceptions import ConfigEntryNotReady\nfrom homeassistant.helpers.device_registry import DeviceInfo\n\nfrom .config_flow import GoodweFlowHandler\nfrom .const import (\n    CONF_KEEP_ALIVE,\n    CONF_MODBUS_ID,\n    CONF_MODEL_FAMILY,\n    CONF_NETWORK_RETRIES,\n    CONF_NETWORK_TIMEOUT,\n    DEFAULT_MODBUS_ID,\n    DEFAULT_NETWORK_RETRIES,\n    DEFAULT_NETWORK_TIMEOUT,\n    DOMAIN,\n    PLATFORMS,\n)\nfrom .coordinator import GoodweConfigEntry, GoodweRuntimeData, GoodweUpdateCoordinator\nfrom .services import async_setup_services, async_unload_services\n\n\nasync def async_setup_entry(hass: HomeAssistant, entry: GoodweConfigEntry) -> bool:\n    \"\"\"Set up the Goodwe components from a config entry.\"\"\"\n    hass.data.setdefault(DOMAIN, {})\n    host = entry.options.get(CONF_HOST, entry.data[CONF_HOST])\n    protocol = entry.options.get(CONF_PROTOCOL, entry.data.get(CONF_PROTOCOL, \"UDP\"))\n    port = entry.options.get(\n        CONF_PORT,\n        entry.data.get(\n            CONF_PORT, GOODWE_TCP_PORT if protocol == \"TCP\" else GOODWE_UDP_PORT\n        ),\n    )\n    keep_alive = entry.options.get(CONF_KEEP_ALIVE, False)\n    model_family = entry.options.get(CONF_MODEL_FAMILY, entry.data[CONF_MODEL_FAMILY])\n    network_retries = entry.options.get(CONF_NETWORK_RETRIES, DEFAULT_NETWORK_RETRIES)\n    network_timeout = entry.options.get(CONF_NETWORK_TIMEOUT, DEFAULT_NETWORK_TIMEOUT)\n    modbus_id = entry.options.get(CONF_MODBUS_ID, DEFAULT_MODBUS_ID)\n\n    # Connect to Goodwe inverter\n    try:\n        inverter = await connect(\n            host=host,\n            port=port,\n            family=model_family,\n            comm_addr=modbus_id,\n            timeout=network_timeout,\n            retries=network_retries,\n        )\n        inverter.set_keep_alive(keep_alive)\n    except InverterError as err:\n        try:\n            inverter = await async_check_port(hass, entry, host)\n        except InverterError:\n            raise ConfigEntryNotReady from err\n\n    device_info = DeviceInfo(\n        configuration_url=\"https://semsplus.goodwe.com/\",\n        identifiers={(DOMAIN, inverter.serial_number)},\n        name=entry.title,\n        manufacturer=\"GoodWe\",\n        model=inverter.model_name,\n        sw_version=f\"{inverter.firmware} / {inverter.arm_firmware}\",\n        hw_version=f\"{inverter.serial_number[5:8]} {inverter.serial_number[0:5]}\",\n    )\n\n    # Create update coordinator\n    coordinator = GoodweUpdateCoordinator(hass, entry, inverter)\n\n    # Fetch initial data so we have data when entities subscribe\n    await coordinator.async_config_entry_first_refresh()\n\n    entry.runtime_data = GoodweRuntimeData(\n        inverter=inverter,\n        coordinator=coordinator,\n        device_info=device_info,\n    )\n\n    hass.data[DOMAIN][entry.entry_id] = entry.runtime_data\n\n    entry.async_on_unload(entry.add_update_listener(update_listener))\n\n    await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)\n\n    await async_setup_services(hass)\n\n    return True\n\n\nasync def async_check_port(\n    hass: HomeAssistant, entry: GoodweConfigEntry, host: str\n) -> Inverter:\n    \"\"\"Check the communication port of the inverter, it may have changed after a firmware update.\"\"\"\n    inverter, port = await GoodweFlowHandler.async_detect_inverter_port(host=host)\n    family = type(inverter).__name__\n    hass.config_entries.async_update_entry(\n        entry,\n        data={\n            CONF_HOST: host,\n            CONF_PORT: port,\n            CONF_MODEL_FAMILY: family,\n        },\n    )\n    return inverter\n\n\nasync def async_unload_entry(\n    hass: HomeAssistant, config_entry: GoodweConfigEntry\n) -> bool:\n    \"\"\"Unload a config entry.\"\"\"\n    unload_ok = await hass.config_entries.async_unload_platforms(\n        config_entry, PLATFORMS\n    )\n\n    if unload_ok:\n        hass.data[DOMAIN].pop(config_entry.entry_id)\n\n        if not hass.data[DOMAIN]:\n            await async_unload_services(hass)\n\n    return unload_ok\n\n\nasync def update_listener(hass: HomeAssistant, config_entry: GoodweConfigEntry) -> None:\n    \"\"\"Handle options update.\"\"\"\n    await hass.config_entries.async_reload(config_entry.entry_id)\n\n\nasync def async_migrate_entry(\n    hass: HomeAssistant, config_entry: GoodweConfigEntry\n) -> bool:\n    \"\"\"Migrate old config entries.\"\"\"\n\n    if config_entry.version > 2:\n        # This means the user has downgraded from a future version\n        return False\n\n    if config_entry.version == 1:\n        # Update from version 1 to version 2 adding the PROTOCOL to the config entry\n        host = config_entry.data[CONF_HOST]\n        port = config_entry.data.get(\n            CONF_PORT,\n            config_entry.data.get(\n                CONF_PORT,\n                (\n                    GOODWE_TCP_PORT\n                    if config_entry.data.get(CONF_PROTOCOL) == \"TCP\"\n                    else GOODWE_UDP_PORT\n                ),\n            ),\n        )\n        if not port:\n            try:\n                _, port = await GoodweFlowHandler.async_detect_inverter_port(host=host)\n            except InverterError as err:\n                raise ConfigEntryNotReady from err\n        new_data = {\n            CONF_HOST: host,\n            CONF_PORT: port,\n            CONF_PROTOCOL: config_entry.data.get(CONF_PROTOCOL),\n            CONF_KEEP_ALIVE: config_entry.data.get(CONF_KEEP_ALIVE),\n            CONF_MODEL_FAMILY: config_entry.data.get(CONF_MODEL_FAMILY),\n            CONF_SCAN_INTERVAL: config_entry.data.get(CONF_SCAN_INTERVAL),\n            CONF_NETWORK_RETRIES: config_entry.data.get(CONF_NETWORK_RETRIES),\n            CONF_NETWORK_TIMEOUT: config_entry.data.get(CONF_NETWORK_TIMEOUT),\n            CONF_MODBUS_ID: config_entry.data.get(CONF_MODBUS_ID),\n        }\n        hass.config_entries.async_update_entry(config_entry, data=new_data, version=2)\n\n    return True\n"
  },
  {
    "path": "custom_components/goodwe/button.py",
    "content": "\"\"\"GoodWe PV inverter selection settings entities.\"\"\"\n\nfrom collections.abc import Awaitable, Callable\nfrom dataclasses import dataclass\nfrom datetime import datetime\nimport logging\n\nfrom goodwe import Inverter, InverterError\nfrom homeassistant.components.button import ButtonEntity, ButtonEntityDescription\nfrom homeassistant.const import EntityCategory\nfrom homeassistant.core import HomeAssistant\nfrom homeassistant.helpers.device_registry import DeviceInfo\nfrom homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback\n\nfrom .const import DOMAIN\nfrom .coordinator import GoodweConfigEntry\n\n_LOGGER = logging.getLogger(__name__)\n\n\n@dataclass(frozen=True, kw_only=True)\nclass GoodweButtonEntityDescription(ButtonEntityDescription):\n    \"\"\"Class describing Goodwe button entities.\"\"\"\n\n    setting: str\n    action: Callable[[Inverter], Awaitable[None]]\n\n\nBUTTONS = (\n    GoodweButtonEntityDescription(\n        key=\"synchronize_clock\",\n        translation_key=\"synchronize_clock\",\n        entity_category=EntityCategory.CONFIG,\n        setting=\"time\",\n        action=lambda inv: inv.write_setting(\"time\", datetime.now()),\n    ),\n    GoodweButtonEntityDescription(\n        key=\"start_inverter\",\n        translation_key=\"start_inverter\",\n        setting=\"start\",\n        action=lambda inv: inv.write_setting(\"start\", 0),\n    ),\n    GoodweButtonEntityDescription(\n        key=\"stop_inverter\",\n        translation_key=\"stop_inverter\",\n        setting=\"stop\",\n        action=lambda inv: inv.write_setting(\"stop\", 0),\n    ),\n)\n\n\nasync def async_setup_entry(\n    hass: HomeAssistant,\n    config_entry: GoodweConfigEntry,\n    async_add_entities: AddConfigEntryEntitiesCallback,\n) -> None:\n    \"\"\"Set up the inverter button entities from a config entry.\"\"\"\n    inverter = config_entry.runtime_data.inverter\n    device_info = config_entry.runtime_data.device_info\n\n    entities = []\n\n    for description in BUTTONS:\n        try:\n            await inverter.read_setting(description.setting)\n        except (InverterError, ValueError):\n            # Inverter model does not support this feature\n            _LOGGER.debug(\"Could not read %s value\", description.setting)\n        else:\n            entities.append(\n                GoodweButtonEntity(\n                    device_info,\n                    description,\n                    inverter,\n                )\n            )\n\n    async_add_entities(entities)\n\n\nclass GoodweButtonEntity(ButtonEntity):\n    \"\"\"Entity representing the inverter clock synchronization button.\"\"\"\n\n    _attr_should_poll = False\n    _attr_has_entity_name = True\n    entity_description: GoodweButtonEntityDescription\n\n    def __init__(\n        self,\n        device_info: DeviceInfo,\n        description: GoodweButtonEntityDescription,\n        inverter: Inverter,\n    ) -> None:\n        \"\"\"Initialize the inverter operation mode setting entity.\"\"\"\n        self.entity_description = description\n        self._attr_unique_id = f\"{DOMAIN}-{description.key}-{inverter.serial_number}\"\n        self._attr_device_info = device_info\n        self._inverter: Inverter = inverter\n\n    async def async_press(self) -> None:\n        \"\"\"Triggers the button press service.\"\"\"\n        await self.entity_description.action(self._inverter)\n"
  },
  {
    "path": "custom_components/goodwe/config_flow.py",
    "content": "\"\"\"Config flow to configure Goodwe inverters using their local API.\"\"\"\n\nfrom __future__ import annotations\n\nimport logging\nfrom typing import Any\n\nimport voluptuous as vol\n\nfrom goodwe import Inverter, InverterError, connect\nfrom goodwe.const import GOODWE_TCP_PORT, GOODWE_UDP_PORT\nfrom homeassistant.config_entries import (\n    ConfigEntry,\n    ConfigFlow,\n    ConfigFlowResult,\n    OptionsFlow,\n)\nfrom homeassistant.const import CONF_HOST, CONF_PORT, CONF_PROTOCOL, CONF_SCAN_INTERVAL\nfrom homeassistant.core import callback\nfrom homeassistant.helpers import config_validation as cv\n\nfrom .const import (\n    CONF_KEEP_ALIVE,\n    CONF_MODBUS_ID,\n    CONF_MODEL_FAMILY,\n    CONF_NETWORK_RETRIES,\n    CONF_NETWORK_TIMEOUT,\n    DEFAULT_MODBUS_ID,\n    DEFAULT_NAME,\n    DEFAULT_NETWORK_RETRIES,\n    DEFAULT_NETWORK_TIMEOUT,\n    DEFAULT_SCAN_INTERVAL,\n    DOMAIN,\n)\n\nPROTOCOL_CHOICES = [\"UDP\", \"TCP\"]\nCONFIG_SCHEMA = vol.Schema(\n    {\n        vol.Required(CONF_HOST): str,\n        vol.Required(CONF_PROTOCOL, default=\"UDP\"): vol.In(PROTOCOL_CHOICES),\n        vol.Required(CONF_MODEL_FAMILY, default=\"none\"): str,\n    }\n)\nOPTIONS_SCHEMA = vol.Schema(\n    {\n        vol.Required(CONF_HOST): str,\n        vol.Optional(CONF_PORT): int,\n        vol.Required(CONF_PROTOCOL): vol.In(PROTOCOL_CHOICES),\n        vol.Required(CONF_KEEP_ALIVE): cv.boolean,\n        vol.Required(CONF_MODEL_FAMILY): str,\n        vol.Optional(CONF_SCAN_INTERVAL): int,\n        vol.Optional(CONF_MODBUS_ID): int,\n        vol.Optional(CONF_NETWORK_RETRIES): cv.positive_int,\n        vol.Optional(CONF_NETWORK_TIMEOUT): cv.positive_int,\n    }\n)\n\n_LOGGER = logging.getLogger(__name__)\n\n\nclass OptionsFlowHandler(OptionsFlow):\n    \"\"\"Options for the component.\"\"\"\n\n    def __init__(self, config_entry: ConfigEntry) -> None:\n        \"\"\"Init object.\"\"\"\n        self.entry = config_entry\n\n    async def async_step_init(self, user_input: dict | None = None) -> ConfigFlowResult:\n        \"\"\"Manage the options.\"\"\"\n        if user_input is not None:\n            return self.async_create_entry(title=\"\", data=user_input)\n\n        host = self.entry.options.get(CONF_HOST, self.entry.data[CONF_HOST])\n        port = self.entry.options.get(CONF_PORT, self.entry.data.get(CONF_PORT))\n        protocol = self.entry.options.get(\n            CONF_PROTOCOL, self.entry.data.get(CONF_PROTOCOL, \"UDP\")\n        )\n        keep_alive = self.entry.options.get(CONF_KEEP_ALIVE, False)\n        model_family = self.entry.options.get(\n            CONF_MODEL_FAMILY, self.entry.data[CONF_MODEL_FAMILY]\n        )\n        network_retries = self.entry.options.get(\n            CONF_NETWORK_RETRIES, DEFAULT_NETWORK_RETRIES\n        )\n        network_timeout = self.entry.options.get(\n            CONF_NETWORK_TIMEOUT, DEFAULT_NETWORK_TIMEOUT\n        )\n        modbus_id = self.entry.options.get(CONF_MODBUS_ID, DEFAULT_MODBUS_ID)\n\n        return self.async_show_form(\n            step_id=\"init\",\n            data_schema=self.add_suggested_values_to_schema(\n                OPTIONS_SCHEMA,\n                {\n                    CONF_HOST: host,\n                    CONF_PORT: port,\n                    CONF_PROTOCOL: protocol,\n                    CONF_KEEP_ALIVE: keep_alive,\n                    CONF_MODEL_FAMILY: model_family,\n                    CONF_SCAN_INTERVAL: self.entry.options.get(\n                        CONF_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL\n                    ),\n                    CONF_NETWORK_RETRIES: network_retries,\n                    CONF_NETWORK_TIMEOUT: network_timeout,\n                    CONF_MODBUS_ID: modbus_id,\n                },\n            ),\n        )\n\n\nclass GoodweFlowHandler(ConfigFlow, domain=DOMAIN):\n    \"\"\"Handle a Goodwe config flow.\"\"\"\n\n    MINOR_VERSION = 2\n\n    @staticmethod\n    @callback\n    def async_get_options_flow(\n        config_entry: ConfigEntry,\n    ) -> OptionsFlowHandler:\n        \"\"\"Get the options flow.\"\"\"\n        return OptionsFlowHandler(config_entry)\n\n    async def async_handle_successful_connection(\n        self,\n        inverter: Inverter,\n        host: str,\n        port: int,\n        protocol: str,\n    ) -> ConfigFlowResult:\n        \"\"\"Handle a successful connection storing it's values on the entry data.\"\"\"\n        await self.async_set_unique_id(inverter.serial_number)\n        self._abort_if_unique_id_configured()\n\n        return self.async_create_entry(\n            title=DEFAULT_NAME,\n            data={\n                CONF_HOST: host,\n                CONF_PORT: port,\n                CONF_PROTOCOL: protocol,\n                CONF_MODEL_FAMILY: type(inverter).__name__,\n            },\n        )\n\n    async def async_step_user(\n        self, user_input: dict[str, Any] | None = None\n    ) -> ConfigFlowResult:\n        \"\"\"Handle a flow initialized by the user.\"\"\"\n        errors = {}\n        if user_input is not None:\n            host = user_input[CONF_HOST]\n            protocol = user_input[CONF_PROTOCOL]\n            model_family = user_input[CONF_MODEL_FAMILY]\n            port = user_input.get(\n                CONF_PORT, GOODWE_UDP_PORT if protocol == \"UDP\" else GOODWE_TCP_PORT\n            )\n\n            try:\n                _LOGGER.debug(\n                    \"Goodwe connecting to %s:%s protocol=%s family=%s\",\n                    host,\n                    port,\n                    protocol,\n                    model_family,\n                )\n                inverter = await connect(\n                    host=host, port=port, family=model_family, retries=10\n                )\n            except InverterError:\n                errors[CONF_HOST] = \"connection_error\"\n            else:\n                return await self.async_handle_successful_connection(\n                    inverter, host, port, protocol\n                )\n        return self.async_show_form(\n            step_id=\"user\", data_schema=CONFIG_SCHEMA, errors=errors\n        )\n\n    @staticmethod\n    async def async_detect_inverter_port(\n        host: str,\n    ) -> tuple[Inverter, int]:\n        \"\"\"Detects the port of the Inverter.\"\"\"\n        port = GOODWE_UDP_PORT\n        try:\n            inverter = await connect(host=host, port=port, retries=10)\n        except InverterError:\n            port = GOODWE_TCP_PORT\n            inverter = await connect(host=host, port=port, retries=10)\n        return inverter, port\n"
  },
  {
    "path": "custom_components/goodwe/const.py",
    "content": "\"\"\"Constants for the Goodwe component.\"\"\"\n\nfrom datetime import timedelta\n\nfrom homeassistant.const import Platform\n\nDOMAIN = \"goodwe\"\n\nPLATFORMS = [\n    Platform.BUTTON,\n    Platform.NUMBER,\n    Platform.SELECT,\n    Platform.SENSOR,\n    Platform.SWITCH,\n]\n\nDEFAULT_NAME = \"GoodWe\"\nSCAN_INTERVAL = timedelta(seconds=10)\nDEFAULT_SCAN_INTERVAL = 5\nDEFAULT_NETWORK_RETRIES = 10\nDEFAULT_NETWORK_TIMEOUT = 1\nDEFAULT_MODBUS_ID = 0\n\nCONF_KEEP_ALIVE = \"keep_alive\"\nCONF_MODEL_FAMILY = \"model_family\"\nCONF_NETWORK_RETRIES = \"network_retries\"\nCONF_NETWORK_TIMEOUT = \"network_timeout\"\nCONF_MODBUS_ID = \"modbus_id\"\n\nSERVICE_GET_PARAMETER = \"get_parameter\"\nSERVICE_SET_PARAMETER = \"set_parameter\"\nATTR_DEVICE_ID = \"device_id\"\nATTR_ENTITY_ID = \"entity_id\"\nATTR_PARAMETER = \"parameter\"\nATTR_VALUE = \"value\"\n"
  },
  {
    "path": "custom_components/goodwe/coordinator.py",
    "content": "\"\"\"Update coordinator for Goodwe.\"\"\"\n\nfrom __future__ import annotations\n\nfrom dataclasses import dataclass\nfrom datetime import datetime, timedelta\nimport logging\nfrom typing import Any\n\nfrom goodwe import Inverter, InverterError, RequestFailedException\nfrom homeassistant.config_entries import ConfigEntry\nfrom homeassistant.const import CONF_SCAN_INTERVAL\nfrom homeassistant.core import HomeAssistant\nfrom homeassistant.helpers.device_registry import DeviceInfo\nfrom homeassistant.helpers.update_coordinator import (\n    BaseCoordinatorEntity,\n    DataUpdateCoordinator,\n    UpdateFailed,\n)\n\nfrom .const import DEFAULT_SCAN_INTERVAL\n\n_LOGGER = logging.getLogger(__name__)\n\ntype GoodweConfigEntry = ConfigEntry[GoodweRuntimeData]\n\n\n@dataclass\nclass GoodweRuntimeData:\n    \"\"\"Data class for runtime data.\"\"\"\n\n    inverter: Inverter\n    coordinator: GoodweUpdateCoordinator\n    device_info: DeviceInfo\n\n\nclass GoodweUpdateCoordinator(DataUpdateCoordinator[dict[str, Any]]):\n    \"\"\"Gather data for the energy device.\"\"\"\n\n    config_entry: GoodweConfigEntry\n\n    def __init__(\n        self,\n        hass: HomeAssistant,\n        entry: GoodweConfigEntry,\n        inverter: Inverter,\n    ) -> None:\n        \"\"\"Initialize update coordinator.\"\"\"\n        super().__init__(\n            hass,\n            _LOGGER,\n            config_entry=entry,\n            name=entry.title,\n            update_interval=timedelta(\n                seconds=entry.options.get(CONF_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL)\n            ),\n        )\n        self.inverter: Inverter = inverter\n        self._last_data: dict[str, Any] = {}\n        self._polled_entities: dict[BaseCoordinatorEntity, datetime] = {}\n\n    async def _async_update_data(self) -> dict[str, Any]:\n        \"\"\"Fetch data from the inverter.\"\"\"\n        await self._update_polled_entities()\n\n        try:\n            self._last_data = self.data or {}\n            return await self.inverter.read_runtime_data()\n        except RequestFailedException as ex:\n            # UDP communication with inverter is by definition unreliable.\n            # It is rather normal in many environments to fail to receive\n            # proper response in usual time, so we intentionally ignore isolated\n            # failures and report problem with availability only after\n            # consecutive streak of 3 of failed requests.\n            if ex.consecutive_failures_count < 3:\n                _LOGGER.debug(\n                    \"No response received (streak of %d)\", ex.consecutive_failures_count\n                )\n                # return last known data\n                return self._last_data\n            # Inverter does not respond anymore (e.g. it went to sleep mode)\n            _LOGGER.debug(\n                \"Inverter not responding (streak of %d)\", ex.consecutive_failures_count\n            )\n            raise UpdateFailed(ex) from ex\n        except InverterError as ex:\n            raise UpdateFailed(ex) from ex\n\n    async def _update_polled_entities(self) -> None:\n        for entity, interval in list(self._polled_entities.items()):\n            if interval:\n                try:\n                    await entity.async_update()\n                except InverterError:\n                    _LOGGER.debug(\"Failed to update entity %s\", entity.name)\n\n    def sensor_value(self, sensor: str) -> Any:\n        \"\"\"Answer current (or last known) value of the sensor.\"\"\"\n        val = self.data.get(sensor)\n        return val if val is not None else self._last_data.get(sensor)\n\n    def total_sensor_value(self, sensor: str) -> Any:\n        \"\"\"Answer current value of the 'total' (never 0) sensor.\"\"\"\n        val = self.data.get(sensor)\n        return val or self._last_data.get(sensor)\n\n    def reset_sensor(self, sensor: str) -> None:\n        \"\"\"Reset sensor value to 0.\n\n        Intended for \"daily\" cumulative sensors (e.g. PV energy produced today),\n        which should be explicitly reset to 0 at midnight if inverter is suspended.\n        \"\"\"\n        self._last_data[sensor] = 0\n        self.data[sensor] = 0\n\n    def entity_state_polling(\n        self, entity: BaseCoordinatorEntity, interval: int\n    ) -> None:\n        \"\"\"Enable/disable polling of entity state.\"\"\"\n        if interval:\n            self._polled_entities[entity] = interval\n        else:\n            self._polled_entities.pop(entity, None)\n"
  },
  {
    "path": "custom_components/goodwe/diagnostics.py",
    "content": "\"\"\"Diagnostics support for Goodwe.\"\"\"\n\nfrom __future__ import annotations\n\nfrom typing import Any\n\nfrom goodwe import Inverter, InverterError\nfrom homeassistant.core import HomeAssistant\n\nfrom .coordinator import GoodweConfigEntry\n\n\nasync def async_get_config_entry_diagnostics(\n    hass: HomeAssistant, config_entry: GoodweConfigEntry\n) -> dict[str, Any]:\n    \"\"\"Return diagnostics for a config entry.\"\"\"\n    inverter = config_entry.runtime_data.inverter\n\n    return {\n        \"config_entry\": config_entry.as_dict(),\n        \"inverter\": {\n            \"model_name\": inverter.model_name,\n            \"rated_power\": inverter.rated_power,\n            \"firmware\": inverter.firmware,\n            \"arm_firmware\": inverter.arm_firmware,\n            \"dsp1_version\": inverter.dsp1_version,\n            \"dsp2_version\": inverter.dsp2_version,\n            \"dsp_svn_version\": inverter.dsp_svn_version,\n            \"arm_version\": inverter.arm_version,\n            \"arm_svn_version\": inverter.arm_svn_version,\n            \"modbus_address\": await _read_register(inverter, 45127),\n            \"modbus_baudrate\": await _read_register(inverter, 45132),\n            \"log_data_enable\": await _read_register(inverter, 47005),\n            \"data_send_interval\": await _read_register(inverter, 47006),\n            \"wifi_or_lan\": await _read_register(inverter, 47009),\n            \"modbus_tcp_wo_internet\": await _read_register(inverter, 47017),\n            \"wifi_modbus_tcp_enable\": await _read_register(inverter, 47040),\n        },\n    }\n\n\nasync def _read_register(inverter: Inverter, register: int) -> Any:\n    try:\n        return await inverter.read_setting(f\"modbus-{register}\")\n    except InverterError:\n        return None\n"
  },
  {
    "path": "custom_components/goodwe/icons.json",
    "content": "{\n  \"entity\": {\n    \"button\": {\n      \"synchronize_clock\": {\n        \"default\": \"mdi:clock-check-outline\"\n      },\n      \"start_inverter\": {\n        \"default\": \"mdi:power\"\n      },\n      \"stop_inverter\": {\n        \"default\": \"mdi:power-off\"\n      }\n    },\n    \"number\": {\n      \"battery_discharge_depth\": {\n        \"default\": \"mdi:battery-arrow-down\"\n      },\n      \"battery_discharge_depth_offline\": {\n        \"default\": \"mdi:battery-arrow-down\"\n      },\n      \"eco_mode_power\": {\n        \"default\": \"mdi:battery-charging-low\"\n      },\n      \"eco_mode_soc\": {\n        \"default\": \"mdi:battery-charging-low\"\n      },\n      \"fast_charging_power\": {\n        \"default\": \"mdi:battery-arrow-up\"\n      },\n      \"fast_charging_soc\": {\n        \"default\": \"mdi:battery-arrow-up\"\n      },\n      \"grid_export_limit\": {\n        \"default\": \"mdi:transmission-tower\"\n      },\n      \"soc_upper_limit\": {\n        \"default\": \"mdi:battery-heart-outline\"\n      }\n    },\n    \"select\": {\n      \"operation_mode\": {\n        \"default\": \"mdi:solar-power\"\n      }\n    },\n    \"switch\": {\n      \"grid_export_limit_switch\": {\n        \"default\": \"mdi:transmission-tower-import\",\n        \"state\": {\n          \"on\": \"mdi:transmission-tower\",\n          \"off\": \"mdi:transmission-tower-import\"\n        }\n      },\n      \"load_control\": {\n        \"default\": \"mdi:electric-switch\",\n        \"state\": {\n          \"on\": \"mdi:electric-switch-closed\",\n          \"off\": \"mdi:electric-switch\"\n        }\n      },\n      \"fast_charging_switch\": {\n        \"default\": \"mdi:battery-medium\",\n        \"state\": {\n          \"on\": \"mdi:battery-arrow-up\",\n          \"off\": \"mdi:battery-medium\"\n        }\n      },\n      \"backup_supply_switch\": {\n        \"default\": \"mdi:battery-medium\",\n        \"state\": {\n          \"on\": \"mdi:battery-medium\",\n          \"off\": \"mdi:battery-medium\"\n        }\n      },\n      \"dod_holding_switch\": {\n        \"default\": \"mdi:battery-arrow-down\",\n        \"state\": {\n          \"on\": \"mdi:battery-arrow-down\",\n          \"off\": \"mdi:battery-arrow-down\"\n        }\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "custom_components/goodwe/manifest.json",
    "content": "{\n  \"domain\": \"goodwe\",\n  \"name\": \"GoodWe Inverter\",\n  \"codeowners\": [\n    \"@mletenay\",\n    \"@starkillerOG\",\n    \"@fizcris\"\n  ],\n  \"config_flow\": true,\n  \"documentation\": \"https://github.com/mletenay/home-assistant-goodwe-inverter\",\n  \"integration_type\": \"device\",\n  \"iot_class\": \"local_polling\",\n  \"issue_tracker\": \"https://github.com/mletenay/home-assistant-goodwe-inverter/issues\",\n  \"loggers\": [\"goodwe\"],\n  \"requirements\": [\"goodwe==0.4.10\"],\n  \"version\": \"0.9.9.30\"\n}\n"
  },
  {
    "path": "custom_components/goodwe/number.py",
    "content": "\"\"\"GoodWe PV inverter numeric settings entities.\"\"\"\n\nfrom __future__ import annotations\n\nfrom collections.abc import Awaitable, Callable\nfrom dataclasses import dataclass\nimport logging\n\nfrom goodwe import Inverter, InverterError\nfrom homeassistant.components.number import (\n    NumberDeviceClass,\n    NumberEntity,\n    NumberEntityDescription,\n)\nfrom homeassistant.const import PERCENTAGE, EntityCategory, UnitOfPower\nfrom homeassistant.core import HomeAssistant\nfrom homeassistant.helpers.device_registry import DeviceInfo\nfrom homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback\n\nfrom .const import DOMAIN\nfrom .coordinator import GoodweConfigEntry\n\n_LOGGER = logging.getLogger(__name__)\n\n\n@dataclass(frozen=True, kw_only=True)\nclass GoodweNumberEntityDescription(NumberEntityDescription):\n    \"\"\"Class describing Goodwe number entities.\"\"\"\n\n    getter: Callable[[Inverter], Awaitable[any]]\n    mapper: Callable[[any], int]\n    setter: Callable[[Inverter, int], Awaitable[None]]\n    filter: Callable[[Inverter], bool]\n\n\ndef _get_setting_unit(inverter: Inverter, setting: str) -> str:\n    \"\"\"Return the unit of an inverter setting.\"\"\"\n    return next((s.unit for s in inverter.settings() if s.id_ == setting), \"\")\n\n\nasync def set_offline_battery_dod(inverter: Inverter, dod: int) -> None:\n    \"\"\"Sets offline battery dod - dod for backup output.\"\"\"\n    if 10 <= dod <= 100:\n        await inverter.write_setting(\"battery_discharge_depth_offline\", 100 - dod)\n\n\nasync def get_offline_battery_dod(inverter: Inverter) -> int:\n    \"\"\"Returns offline battery dod - dod for backup output.\"\"\"\n    return 100 - (await inverter.read_setting(\"battery_discharge_depth_offline\"))\n\n\nNUMBERS = (\n    # Only one of the export limits are added.\n    # Availability is checked in the filter method.\n    # Export limit in W\n    GoodweNumberEntityDescription(\n        key=\"grid_export_limit\",\n        translation_key=\"grid_export_limit\",\n        entity_category=EntityCategory.CONFIG,\n        device_class=NumberDeviceClass.POWER,\n        native_unit_of_measurement=UnitOfPower.WATT,\n        native_step=100,\n        native_min_value=0,\n        getter=lambda inv: inv.get_grid_export_limit(),\n        mapper=lambda v: v,\n        setter=lambda inv, val: inv.set_grid_export_limit(val),\n        filter=lambda inv: _get_setting_unit(inv, \"grid_export_limit\") != \"%\",\n    ),\n    # Export limit in %\n    GoodweNumberEntityDescription(\n        key=\"grid_export_limit\",\n        translation_key=\"grid_export_limit\",\n        entity_category=EntityCategory.CONFIG,\n        native_unit_of_measurement=PERCENTAGE,\n        native_step=1,\n        native_min_value=0,\n        native_max_value=200,\n        getter=lambda inv: inv.get_grid_export_limit(),\n        mapper=lambda v: v,\n        setter=lambda inv, val: inv.set_grid_export_limit(val),\n        filter=lambda inv: _get_setting_unit(inv, \"grid_export_limit\") == \"%\",\n    ),\n    GoodweNumberEntityDescription(\n        key=\"battery_discharge_depth\",\n        translation_key=\"battery_discharge_depth\",\n        icon=\"mdi:battery-arrow-down\",\n        entity_category=EntityCategory.CONFIG,\n        native_unit_of_measurement=PERCENTAGE,\n        native_step=1,\n        native_min_value=0,\n        native_max_value=99,\n        getter=lambda inv: inv.get_ongrid_battery_dod(),\n        mapper=lambda v: v,\n        setter=lambda inv, val: inv.set_ongrid_battery_dod(val),\n        filter=lambda inv: True,\n    ),\n    GoodweNumberEntityDescription(\n        key=\"soc_upper_limit\",\n        translation_key=\"soc_upper_limit\",\n        native_unit_of_measurement=PERCENTAGE,\n        native_step=1,\n        native_min_value=0,\n        native_max_value=100,\n        getter=lambda inv: inv.read_setting(\"soc_upper_limit\"),\n        mapper=lambda v: v,\n        setter=lambda inv, val: inv.write_setting(\"soc_upper_limit\", val),\n        filter=lambda inv: True,\n    ),\n    GoodweNumberEntityDescription(\n        key=\"battery_discharge_depth_offline\",\n        translation_key=\"battery_discharge_depth_offline\",\n        entity_category=EntityCategory.CONFIG,\n        native_unit_of_measurement=PERCENTAGE,\n        native_step=1,\n        native_min_value=0,\n        native_max_value=99,\n        getter=lambda inv: get_offline_battery_dod(inv),\n        mapper=lambda v: v,\n        setter=lambda inv, val: set_offline_battery_dod(inv, val),\n        filter=lambda inv: True,\n    ),\n    GoodweNumberEntityDescription(\n        key=\"eco_mode_power\",\n        translation_key=\"eco_mode_power\",\n        entity_category=EntityCategory.CONFIG,\n        native_unit_of_measurement=PERCENTAGE,\n        native_step=1,\n        native_min_value=0,\n        native_max_value=100,\n        getter=lambda inv: inv.read_setting(\"eco_mode_1\"),\n        mapper=lambda v: abs(v.get_power()) if v.get_power() else 0,\n        setter=None,\n        filter=lambda inv: True,\n    ),\n    GoodweNumberEntityDescription(\n        key=\"eco_mode_soc\",\n        translation_key=\"eco_mode_soc\",\n        entity_category=EntityCategory.CONFIG,\n        native_unit_of_measurement=PERCENTAGE,\n        native_step=1,\n        native_min_value=0,\n        native_max_value=100,\n        getter=lambda inv: inv.read_setting(\"eco_mode_1\"),\n        mapper=lambda v: v.soc or 0,\n        setter=None,\n        filter=lambda inv: True,\n    ),\n    GoodweNumberEntityDescription(\n        key=\"fast_charging_power\",\n        translation_key=\"fast_charging_power\",\n        native_unit_of_measurement=PERCENTAGE,\n        native_step=1,\n        native_min_value=0,\n        native_max_value=100,\n        getter=lambda inv: inv.read_setting(\"fast_charging_power\"),\n        mapper=lambda v: v,\n        setter=lambda inv, val: inv.write_setting(\"fast_charging_power\", val),\n        filter=lambda inv: True,\n    ),\n    GoodweNumberEntityDescription(\n        key=\"fast_charging_soc\",\n        translation_key=\"fast_charging_soc\",\n        native_unit_of_measurement=PERCENTAGE,\n        native_step=1,\n        native_min_value=0,\n        native_max_value=100,\n        getter=lambda inv: inv.read_setting(\"fast_charging_soc\"),\n        mapper=lambda v: v,\n        setter=lambda inv, val: inv.write_setting(\"fast_charging_soc\", val),\n        filter=lambda inv: True,\n    ),\n    GoodweNumberEntityDescription(\n        key=\"ems_power_limit\",\n        translation_key=\"ems_power_limit\",\n        entity_category=EntityCategory.CONFIG,\n        device_class=NumberDeviceClass.POWER,\n        native_unit_of_measurement=UnitOfPower.WATT,\n        native_step=100,\n        native_min_value=0,\n        getter=lambda inv: inv.read_setting(\"ems_power_limit\"),\n        mapper=lambda v: v,\n        setter=lambda inv, val: inv.write_setting(\"ems_power_limit\", val),\n        filter=lambda inv: True,\n    ),\n    GoodweNumberEntityDescription(\n        key=\"battery_soc_protection\",\n        translation_key=\"battery_soc_protection\",\n        icon=\"mdi:battery-arrow-down-outline\",\n        entity_category=EntityCategory.CONFIG,\n        native_unit_of_measurement=PERCENTAGE,\n        native_step=1,\n        native_min_value=0,\n        native_max_value=100,\n        getter=lambda inv: inv.read_setting(\"battery_soc_protection\"),\n        mapper=lambda v: v,\n        setter=lambda inv, val: inv.write_setting(\"battery_soc_protection\", val),\n        filter=lambda inv: True,\n    ),\n)\n\n\nasync def async_setup_entry(\n    hass: HomeAssistant,\n    config_entry: GoodweConfigEntry,\n    async_add_entities: AddConfigEntryEntitiesCallback,\n) -> None:\n    \"\"\"Set up the inverter select entities from a config entry.\"\"\"\n    inverter = config_entry.runtime_data.inverter\n    device_info = config_entry.runtime_data.device_info\n\n    entities = []\n\n    for description in filter(lambda dsc: dsc.filter(inverter), NUMBERS):\n        try:\n            current_value = description.mapper(await description.getter(inverter))\n        except (InverterError, ValueError):\n            # Inverter model does not support this setting\n            _LOGGER.debug(\"Could not read inverter setting %s\", description.key)\n            continue\n\n        entity = InverterNumberEntity(device_info, description, inverter, current_value)\n        # Set the max value of grid_export_limit and ems_power_limit (W version)\n        if (\n            description.key in (\"grid_export_limit\", \"ems_power_limit\")\n            and description.native_unit_of_measurement == UnitOfPower.WATT\n        ):\n            entity.native_max_value = (\n                inverter.rated_power * 2 if inverter.rated_power else 10000\n            )\n        entities.append(entity)\n\n    async_add_entities(entities)\n\n\nclass InverterNumberEntity(NumberEntity):\n    \"\"\"Inverter numeric setting entity.\"\"\"\n\n    _attr_should_poll = False\n    _attr_has_entity_name = True\n    entity_description: GoodweNumberEntityDescription\n\n    def __init__(\n        self,\n        device_info: DeviceInfo,\n        description: GoodweNumberEntityDescription,\n        inverter: Inverter,\n        current_value: int,\n    ) -> None:\n        \"\"\"Initialize the number inverter setting entity.\"\"\"\n        self.entity_description = description\n        self._attr_unique_id = f\"{DOMAIN}-{description.key}-{inverter.serial_number}\"\n        self._attr_device_info = device_info\n        self._attr_native_value = float(current_value)\n        self._inverter: Inverter = inverter\n\n    async def async_update(self) -> None:\n        \"\"\"Get the current value from inverter.\"\"\"\n        value = await self.entity_description.getter(self._inverter)\n        self._attr_native_value = float(value)\n\n    async def async_set_native_value(self, value: float) -> None:\n        \"\"\"Set new value to inverter.\"\"\"\n        if self.entity_description.setter:\n            await self.entity_description.setter(self._inverter, int(value))\n        self._attr_native_value = value\n        self.async_write_ha_state()\n"
  },
  {
    "path": "custom_components/goodwe/select.py",
    "content": "\"\"\"GoodWe PV inverter selection settings entities.\"\"\"\n\nfrom dataclasses import dataclass\nimport logging\n\nfrom goodwe import Inverter, InverterError, OperationMode\nfrom goodwe.inverter import EMSMode\nfrom homeassistant.components.select import SelectEntity, SelectEntityDescription\nfrom homeassistant.const import (\n    STATE_UNAVAILABLE,\n    STATE_UNKNOWN,\n    EntityCategory,\n    Platform,\n)\nfrom homeassistant.core import Event, HomeAssistant\nfrom homeassistant.helpers import entity_registry as er\nfrom homeassistant.helpers.device_registry import DeviceInfo\nfrom homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback\nfrom homeassistant.helpers.event import async_track_state_change_event\n\nfrom .const import DOMAIN\nfrom .coordinator import GoodweConfigEntry\n\n_LOGGER = logging.getLogger(__name__)\n\n\n_MODE_TO_OPTION: dict[OperationMode, str] = {\n    OperationMode.GENERAL: \"general\",\n    OperationMode.OFF_GRID: \"off_grid\",\n    OperationMode.BACKUP: \"backup\",\n    OperationMode.ECO: \"eco\",\n    OperationMode.PEAK_SHAVING: \"peak_shaving\",\n    OperationMode.SELF_USE: \"self_use\",\n    OperationMode.ECO_CHARGE: \"eco_charge\",\n    OperationMode.ECO_DISCHARGE: \"eco_discharge\",\n}\n\n_OPTION_TO_MODE: dict[str, OperationMode] = {\n    value: key for key, value in _MODE_TO_OPTION.items()\n}\n\n\n@dataclass(frozen=True, kw_only=True)\nclass GoodweSelectEntityDescription(SelectEntityDescription):\n    \"\"\"Class describing Goodwe number entities.\"\"\"\n\n    options: dict[str, EMSMode]\n\n\nOPERATION_MODE = SelectEntityDescription(\n    key=\"operation_mode\",\n    entity_category=EntityCategory.CONFIG,\n    translation_key=\"operation_mode\",\n)\n\nEMS_MODE = GoodweSelectEntityDescription(\n    key=\"ems_mode\",\n    entity_category=EntityCategory.CONFIG,\n    translation_key=\"ems_mode\",\n    options={e.name.lower(): e for e in list(EMSMode)},\n)\n\n\nasync def async_setup_entry(\n    hass: HomeAssistant,\n    config_entry: GoodweConfigEntry,\n    async_add_entities: AddConfigEntryEntitiesCallback,\n) -> None:\n    \"\"\"Set up the inverter select entities from a config entry.\"\"\"\n    inverter = config_entry.runtime_data.inverter\n    device_info = config_entry.runtime_data.device_info\n\n    supported_modes = await inverter.get_operation_modes(True)\n    # read current operating mode from the inverter\n    try:\n        active_mode = await inverter.get_operation_mode()\n        eco_mode = await inverter.read_setting(\"eco_mode_1\")\n        current_eco_power = abs(eco_mode.power) if eco_mode.power else 0\n        current_eco_soc = eco_mode.soc or 0\n    except (InverterError, ValueError):\n        # Inverter model does not support this setting\n        _LOGGER.debug(\"Could not read inverter operation mode\", exc_info=True)\n    else:\n        active_mode_option = _MODE_TO_OPTION.get(active_mode)\n        if active_mode_option is not None:\n            entity = InverterOperationModeEntity(\n                device_info,\n                OPERATION_MODE,\n                inverter,\n                [v for k, v in _MODE_TO_OPTION.items() if k in supported_modes],\n                active_mode_option,\n                current_eco_power,\n                current_eco_soc,\n            )\n            async_add_entities([entity])\n        else:\n            _LOGGER.warning(\n                \"Active mode %s not found in Goodwe Inverter Operation Mode Entity. Skipping entity creation\",\n                active_mode,\n            )\n\n        eco_mode_power_entity_id = er.async_get(hass).async_get_entity_id(\n            Platform.NUMBER,\n            DOMAIN,\n            f\"{DOMAIN}-eco_mode_power-{inverter.serial_number}\",\n        )\n        if eco_mode_power_entity_id:\n            async_track_state_change_event(\n                hass,\n                eco_mode_power_entity_id,\n                entity.update_eco_mode_power,\n            )\n        eco_mode_soc_entity_id = er.async_get(hass).async_get_entity_id(\n            Platform.NUMBER,\n            DOMAIN,\n            f\"{DOMAIN}-eco_mode_soc-{inverter.serial_number}\",\n        )\n        if eco_mode_soc_entity_id:\n            async_track_state_change_event(\n                hass,\n                eco_mode_soc_entity_id,\n                entity.update_eco_mode_soc,\n            )\n\n    # read current EMS mode from the inverter\n    try:\n        ems_mode = await inverter.get_ems_mode()\n    except (InverterError, ValueError):\n        # Inverter model does not support EMS modes\n        _LOGGER.debug(\"Could not read inverter EMS mode\", exc_info=True)\n    else:\n        entity = InverterEMSModeEntity(\n            device_info,\n            EMS_MODE,\n            inverter,\n            ems_mode,\n        )\n        async_add_entities([entity])\n\n\nclass InverterOperationModeEntity(SelectEntity):\n    \"\"\"Entity representing the inverter operation mode.\"\"\"\n\n    _attr_should_poll = False\n    _attr_has_entity_name = True\n\n    def __init__(\n        self,\n        device_info: DeviceInfo,\n        description: SelectEntityDescription,\n        inverter: Inverter,\n        supported_options: list[str],\n        current_mode: str,\n        current_eco_power: int,\n        current_eco_soc: int,\n    ) -> None:\n        \"\"\"Initialize the inverter operation mode setting entity.\"\"\"\n        self.entity_description = description\n        self._attr_unique_id = f\"{DOMAIN}-{description.key}-{inverter.serial_number}\"\n        self._attr_device_info = device_info\n        self._attr_options = supported_options\n        self._attr_current_option = current_mode\n        self._inverter: Inverter = inverter\n        self._eco_mode_power = current_eco_power\n        self._eco_mode_soc = current_eco_soc\n\n    async def async_select_option(self, option: str) -> None:\n        \"\"\"Change the selected option.\"\"\"\n        _LOGGER.debug(\n            \"Setting operation mode to %s, power %d, max SoC %d\",\n            option,\n            self._eco_mode_power,\n            self._eco_mode_soc,\n        )\n        try:\n            await self._inverter.set_operation_mode(\n                _OPTION_TO_MODE[option], self._eco_mode_power, self._eco_mode_soc\n            )\n        except InverterError as err:\n            _LOGGER.warning(\n                \"Failed to set operation mode to %s: %s\", option, err\n            )\n            return\n        self._attr_current_option = option\n        self.async_write_ha_state()\n\n    async def async_update(self) -> None:\n        \"\"\"Get the current value from inverter.\"\"\"\n        value = await self._inverter.get_operation_mode()\n        self._attr_current_option = _MODE_TO_OPTION[value]\n\n    async def update_eco_mode_power(self, event: Event) -> None:\n        \"\"\"Update eco mode power value in inverter (when in eco mode).\"\"\"\n        state = event.data.get(\"new_state\")\n        if state is None or state.state in (STATE_UNKNOWN, \"\", STATE_UNAVAILABLE):\n            return\n\n        self._eco_mode_power = int(float(state.state))\n        if event.data.get(\"old_state\"):\n            operation_mode = _OPTION_TO_MODE[self.current_option]\n            if operation_mode in (\n                OperationMode.ECO_CHARGE,\n                OperationMode.ECO_DISCHARGE,\n            ):\n                _LOGGER.debug(\"Setting eco mode power to %d\", self._eco_mode_power)\n                try:\n                    await self._inverter.set_operation_mode(\n                        operation_mode, self._eco_mode_power, self._eco_mode_soc\n                    )\n                except InverterError as err:\n                    _LOGGER.warning(\n                        \"Failed to update eco mode power to %d: %s\",\n                        self._eco_mode_power,\n                        err,\n                    )\n\n    async def update_eco_mode_soc(self, event: Event) -> None:\n        \"\"\"Update eco mode SoC value in inverter (when in eco mode).\"\"\"\n        state = event.data.get(\"new_state\")\n        if state is None or state.state in (STATE_UNKNOWN, \"\", STATE_UNAVAILABLE):\n            return\n\n        self._eco_mode_soc = int(float(state.state))\n        if event.data.get(\"old_state\"):\n            operation_mode = _OPTION_TO_MODE[self.current_option]\n            if operation_mode in (\n                OperationMode.ECO_CHARGE,\n                OperationMode.ECO_DISCHARGE,\n            ):\n                _LOGGER.debug(\"Setting eco mode SoC to %d\", self._eco_mode_soc)\n                try:\n                    await self._inverter.set_operation_mode(\n                        operation_mode, self._eco_mode_power, self._eco_mode_soc\n                    )\n                except InverterError as err:\n                    _LOGGER.warning(\n                        \"Failed to update eco mode SoC to %d: %s\",\n                        self._eco_mode_soc,\n                        err,\n                    )\n\n\nclass InverterEMSModeEntity(SelectEntity):\n    \"\"\"Entity representing the inverter EMS mode.\"\"\"\n\n    _attr_should_poll = False\n    _attr_has_entity_name = True\n    entity_description: GoodweSelectEntityDescription\n\n    def __init__(\n        self,\n        device_info: DeviceInfo,\n        description: GoodweSelectEntityDescription,\n        inverter: Inverter,\n        current_mode: EMSMode,\n    ) -> None:\n        \"\"\"Initialize the inverter operation mode setting entity.\"\"\"\n        self.entity_description = description\n        self._attr_unique_id = f\"{DOMAIN}-{description.key}-{inverter.serial_number}\"\n        self._attr_device_info = device_info\n        self._attr_options = list(description.options.keys())\n        self._attr_current_option = current_mode.name.lower()\n        self._inverter: Inverter = inverter\n\n    async def async_select_option(self, option: str) -> None:\n        \"\"\"Change the EMS mode.\"\"\"\n        _LOGGER.debug(\"Setting EMS mode to %s\", option)\n        try:\n            await self._inverter.set_ems_mode(self.entity_description.options[option])\n        except InverterError as err:\n            _LOGGER.warning(\"Failed to set EMS mode to %s: %s\", option, err)\n            return\n        self._attr_current_option = option\n        self.async_write_ha_state()\n\n    async def async_update(self) -> None:\n        \"\"\"Get the current EMS mode from inverter.\"\"\"\n        value = await self._inverter.get_ems_mode()\n        self._attr_current_option = value.name.lower()\n"
  },
  {
    "path": "custom_components/goodwe/sensor.py",
    "content": "\"\"\"Support for GoodWe inverter via UDP.\"\"\"\n\nfrom __future__ import annotations\n\nfrom collections.abc import Callable\nfrom dataclasses import dataclass\nfrom datetime import date, datetime, timedelta\nfrom decimal import Decimal\nimport logging\nfrom typing import Any\n\nfrom goodwe import Inverter, Sensor, SensorKind\nfrom goodwe.sensor import (\n    Enum,\n    Enum2,\n    EnumBitmap4,\n    EnumBitmap22,\n    EnumCalculated,\n    EnumH,\n    EnumL,\n)\nfrom homeassistant.components.sensor import (\n    SensorDeviceClass,\n    SensorEntity,\n    SensorEntityDescription,\n    SensorStateClass,\n)\nfrom homeassistant.const import (\n    PERCENTAGE,\n    EntityCategory,\n    UnitOfApparentPower,\n    UnitOfElectricCurrent,\n    UnitOfElectricPotential,\n    UnitOfEnergy,\n    UnitOfFrequency,\n    UnitOfPower,\n    UnitOfReactivePower,\n    UnitOfTemperature,\n    UnitOfTime,\n)\nfrom homeassistant.core import HomeAssistant, callback\nfrom homeassistant.helpers.device_registry import DeviceInfo\nfrom homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback\nfrom homeassistant.helpers.event import async_track_point_in_time\nfrom homeassistant.helpers.typing import StateType\nfrom homeassistant.helpers.update_coordinator import CoordinatorEntity\nfrom homeassistant.util import dt as dt_util\n\nfrom .const import DOMAIN\nfrom .coordinator import GoodweConfigEntry, GoodweUpdateCoordinator\n\n_LOGGER = logging.getLogger(__name__)\n\n# Sensor name of battery SoC\nBATTERY_SOC = \"battery_soc\"\n\n# Sensors that are reset to 0 at midnight.\n# The inverter is only powered by the solar panels and not mains power, so it goes dead when the sun goes down.\n# 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.\n# This makes sure daily values are reset at midnight instead of at sunrise.\n# 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.\nDAILY_RESET = [\"e_day\", \"e_load_day\"]\n\n_MAIN_SENSORS = (\n    \"ppv\",\n    \"house_consumption\",\n    \"active_power\",\n    \"battery_soc\",\n    \"e_day\",\n    \"e_total\",\n    \"meter_e_total_exp\",\n    \"meter_e_total_imp\",\n    \"e_bat_charge_total\",\n    \"e_bat_discharge_total\",\n)\n\n_ICONS: dict[SensorKind, str] = {\n    SensorKind.PV: \"mdi:solar-power\",\n    SensorKind.AC: \"mdi:power-plug-outline\",\n    SensorKind.UPS: \"mdi:power-plug-off-outline\",\n    SensorKind.BAT: \"mdi:battery-high\",\n    SensorKind.GRID: \"mdi:transmission-tower\",\n}\n\n\n@dataclass(frozen=True)\nclass GoodweSensorEntityDescription(SensorEntityDescription):\n    \"\"\"Class describing Goodwe sensor entities.\"\"\"\n\n    value: Callable[[GoodweUpdateCoordinator, str], Any] = lambda coordinator, sensor: (\n        coordinator.sensor_value(sensor)\n    )\n    available: Callable[[GoodweUpdateCoordinator], bool] = lambda coordinator: (\n        coordinator.last_update_success\n    )\n\n\n_DESCRIPTIONS: dict[str, GoodweSensorEntityDescription] = {\n    \"A\": GoodweSensorEntityDescription(\n        key=\"A\",\n        device_class=SensorDeviceClass.CURRENT,\n        state_class=SensorStateClass.MEASUREMENT,\n        native_unit_of_measurement=UnitOfElectricCurrent.AMPERE,\n    ),\n    \"V\": GoodweSensorEntityDescription(\n        key=\"V\",\n        device_class=SensorDeviceClass.VOLTAGE,\n        state_class=SensorStateClass.MEASUREMENT,\n        native_unit_of_measurement=UnitOfElectricPotential.VOLT,\n    ),\n    \"W\": GoodweSensorEntityDescription(\n        key=\"W\",\n        device_class=SensorDeviceClass.POWER,\n        state_class=SensorStateClass.MEASUREMENT,\n        native_unit_of_measurement=UnitOfPower.WATT,\n    ),\n    \"kWh\": GoodweSensorEntityDescription(\n        key=\"kWh\",\n        device_class=SensorDeviceClass.ENERGY,\n        state_class=SensorStateClass.TOTAL_INCREASING,\n        native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,\n        value=lambda coordinator, sensor: coordinator.total_sensor_value(sensor),\n        available=lambda coordinator: coordinator.data is not None,\n    ),\n    \"VA\": GoodweSensorEntityDescription(\n        key=\"VA\",\n        device_class=SensorDeviceClass.APPARENT_POWER,\n        state_class=SensorStateClass.MEASUREMENT,\n        native_unit_of_measurement=UnitOfApparentPower.VOLT_AMPERE,\n        entity_registry_enabled_default=False,\n    ),\n    \"var\": GoodweSensorEntityDescription(\n        key=\"var\",\n        device_class=SensorDeviceClass.REACTIVE_POWER,\n        state_class=SensorStateClass.MEASUREMENT,\n        native_unit_of_measurement=UnitOfReactivePower.VOLT_AMPERE_REACTIVE,\n        entity_registry_enabled_default=False,\n    ),\n    \"C\": GoodweSensorEntityDescription(\n        key=\"C\",\n        device_class=SensorDeviceClass.TEMPERATURE,\n        state_class=SensorStateClass.MEASUREMENT,\n        native_unit_of_measurement=UnitOfTemperature.CELSIUS,\n    ),\n    \"Hz\": GoodweSensorEntityDescription(\n        key=\"Hz\",\n        device_class=SensorDeviceClass.FREQUENCY,\n        state_class=SensorStateClass.MEASUREMENT,\n        native_unit_of_measurement=UnitOfFrequency.HERTZ,\n    ),\n    \"h\": GoodweSensorEntityDescription(\n        key=\"h\",\n        device_class=SensorDeviceClass.DURATION,\n        state_class=SensorStateClass.MEASUREMENT,\n        native_unit_of_measurement=UnitOfTime.HOURS,\n        entity_registry_enabled_default=False,\n    ),\n    \"%\": GoodweSensorEntityDescription(\n        key=\"%\",\n        state_class=SensorStateClass.MEASUREMENT,\n        native_unit_of_measurement=PERCENTAGE,\n    ),\n}\nDIAG_SENSOR = GoodweSensorEntityDescription(\n    key=\"_\",\n    state_class=SensorStateClass.MEASUREMENT,\n)\nTEXT_SENSOR = GoodweSensorEntityDescription(\n    key=\"text\",\n)\nENUM_SENSOR = GoodweSensorEntityDescription(\n    key=\"enum\",\n    device_class=SensorDeviceClass.ENUM,\n)\n\n\nasync def async_setup_entry(\n    hass: HomeAssistant,\n    config_entry: GoodweConfigEntry,\n    async_add_entities: AddConfigEntryEntitiesCallback,\n) -> None:\n    \"\"\"Set up the GoodWe inverter from a config entry.\"\"\"\n    entities: list[InverterSensor] = []\n    inverter = config_entry.runtime_data.inverter\n    coordinator = config_entry.runtime_data.coordinator\n    device_info = config_entry.runtime_data.device_info\n\n    # Individual inverter sensors entities\n    entities.extend(\n        InverterSensor(coordinator, device_info, inverter, sensor)\n        for sensor in inverter.sensors()\n    )\n    async_add_entities(entities)\n\n\nclass InverterSensor(CoordinatorEntity[GoodweUpdateCoordinator], SensorEntity):\n    \"\"\"Entity representing individual inverter sensor.\"\"\"\n\n    _attr_has_entity_name = True\n    entity_description: GoodweSensorEntityDescription\n\n    def __init__(\n        self,\n        coordinator: GoodweUpdateCoordinator,\n        device_info: DeviceInfo,\n        inverter: Inverter,\n        sensor: Sensor,\n    ) -> None:\n        \"\"\"Initialize an inverter sensor.\"\"\"\n        super().__init__(coordinator)\n        self._attr_name = sensor.name.strip()\n        self._attr_unique_id = f\"{DOMAIN}-{sensor.id_}-{inverter.serial_number}\"\n        self._attr_device_info = device_info\n        self._attr_entity_category = (\n            EntityCategory.DIAGNOSTIC if sensor.id_ not in _MAIN_SENSORS else None\n        )\n        try:\n            self.entity_description = _DESCRIPTIONS[sensor.unit]\n        except KeyError:\n            if isinstance(sensor, (Enum, EnumH, EnumL, Enum2, EnumCalculated)):\n                self.entity_description = ENUM_SENSOR\n                self._attr_options = list(sensor._labels.values())\n            elif (\n                isinstance(sensor, (EnumBitmap4, EnumBitmap22))\n                or sensor.id_ == \"timestamp\"\n            ):\n                self.entity_description = TEXT_SENSOR\n            else:\n                self.entity_description = DIAG_SENSOR\n                self._attr_native_unit_of_measurement = sensor.unit\n        self._attr_icon = _ICONS.get(sensor.kind)\n        # Set the inverter SoC as main device battery sensor\n        if sensor.id_ == BATTERY_SOC:\n            self._attr_device_class = SensorDeviceClass.BATTERY\n        self._sensor = sensor\n        self._stop_reset: Callable[[], None] | None = None\n\n    @property\n    def native_value(self) -> StateType | date | datetime | Decimal:\n        \"\"\"Return the value reported by the sensor.\"\"\"\n        return self.entity_description.value(self.coordinator, self._sensor.id_)\n\n    @property\n    def available(self) -> bool:\n        \"\"\"Return if entity is available.\n\n        We delegate the behavior to entity description lambda, since\n        some sensors (like energy produced today) should report themselves\n        as available even when the (non-battery) pv inverter is off-line during night\n        and most of the sensors are actually unavailable.\n        \"\"\"\n        return self.entity_description.available(self.coordinator)\n\n    @callback\n    def async_reset(self, now):\n        \"\"\"Reset the value back to 0 at midnight.\n\n        Some sensors values like daily produced energy are kept available,\n        even when the inverter is in sleep mode and no longer responds to request.\n        In contrast to \"total\" sensors, these \"daily\" sensors need to be reset to 0 on midnight.\n        \"\"\"\n        if not self.coordinator.last_update_success:\n            self.coordinator.reset_sensor(self._sensor.id_)\n            self.async_write_ha_state()\n            _LOGGER.debug(\"Goodwe reset %s to 0\", self.name)\n        next_midnight = dt_util.start_of_local_day(\n            dt_util.now() + timedelta(days=1, minutes=1)\n        )\n        self._stop_reset = async_track_point_in_time(\n            self.hass, self.async_reset, next_midnight\n        )\n\n    async def async_added_to_hass(self) -> None:\n        \"\"\"Schedule reset task at midnight.\"\"\"\n        if self._sensor.id_ in DAILY_RESET:\n            next_midnight = dt_util.start_of_local_day(\n                dt_util.now() + timedelta(days=1)\n            )\n            self._stop_reset = async_track_point_in_time(\n                self.hass, self.async_reset, next_midnight\n            )\n        await super().async_added_to_hass()\n\n    async def async_will_remove_from_hass(self) -> None:\n        \"\"\"Remove reset task at midnight.\"\"\"\n        if self._sensor.id_ in DAILY_RESET and self._stop_reset is not None:\n            self._stop_reset()\n        await super().async_will_remove_from_hass()\n"
  },
  {
    "path": "custom_components/goodwe/services.py",
    "content": "\"\"\"Services for Goodwe integration.\"\"\"\n\nfrom __future__ import annotations\n\nimport logging\n\nimport voluptuous as vol\n\nfrom homeassistant.core import HomeAssistant\nfrom homeassistant.helpers import device_registry as dr, entity_registry as er\n\nfrom .const import (\n    ATTR_DEVICE_ID,\n    ATTR_ENTITY_ID,\n    ATTR_PARAMETER,\n    ATTR_VALUE,\n    DOMAIN,\n    SERVICE_GET_PARAMETER,\n    SERVICE_SET_PARAMETER,\n)\n\n_LOGGER = logging.getLogger(__name__)\n\nSERVICE_GET_PARAMETER_SCHEMA = vol.Schema(\n    {\n        vol.Required(ATTR_DEVICE_ID): str,\n        vol.Required(ATTR_PARAMETER): str,\n        vol.Required(ATTR_ENTITY_ID): str,\n    }\n)\n\nSERVICE_SET_PARAMETER_SCHEMA = vol.Schema(\n    {\n        vol.Required(ATTR_DEVICE_ID): str,\n        vol.Required(ATTR_PARAMETER): str,\n        vol.Required(ATTR_VALUE): vol.Any(str, int, bool),\n    }\n)\n\n\nasync def async_setup_services(hass: HomeAssistant) -> None:\n    \"\"\"Set up services for Goodwe integration.\"\"\"\n\n    if hass.services.has_service(DOMAIN, SERVICE_GET_PARAMETER):\n        return\n\n    async def _get_inverter_by_device_id(hass: HomeAssistant, device_id: str):\n        \"\"\"Return a inverter instance given a device_id.\"\"\"\n        device = dr.async_get(hass).async_get(device_id)\n        for runtime_data in hass.data[DOMAIN].values():\n            if device.identifiers == runtime_data.device_info.get(\"identifiers\"):\n                return runtime_data.inverter\n        raise ValueError(f\"Inverter for device id {device_id} not found\")\n\n    async def async_get_parameter(call):\n        \"\"\"Service for setting inverter parameter.\"\"\"\n        device_id = call.data[ATTR_DEVICE_ID]\n        parameter = call.data[ATTR_PARAMETER]\n        entity_id = call.data[ATTR_ENTITY_ID]\n\n        _LOGGER.debug(\"Reading inverter parameter '%s'\", parameter)\n        inverter = await _get_inverter_by_device_id(hass, device_id)\n        value = await inverter.read_setting(parameter)\n\n        entity = er.async_get(hass).async_get(entity_id)\n        await hass.services.async_call(\n            entity.domain,\n            \"set_value\",\n            {ATTR_ENTITY_ID: entity_id, ATTR_VALUE: value},\n            blocking=True,\n        )\n\n    async def async_set_parameter(call):\n        \"\"\"Service for setting inverter parameter.\"\"\"\n        device_id = call.data[ATTR_DEVICE_ID]\n        parameter = call.data[ATTR_PARAMETER]\n        value = call.data[ATTR_VALUE]\n\n        _LOGGER.info(\"Setting inverter parameter '%s' to '%s'\", parameter, value)\n        inverter = await _get_inverter_by_device_id(hass, device_id)\n        await inverter.write_setting(parameter, value)\n\n    hass.services.async_register(\n        DOMAIN,\n        SERVICE_GET_PARAMETER,\n        async_get_parameter,\n        schema=SERVICE_GET_PARAMETER_SCHEMA,\n    )\n    hass.services.async_register(\n        DOMAIN,\n        SERVICE_SET_PARAMETER,\n        async_set_parameter,\n        schema=SERVICE_SET_PARAMETER_SCHEMA,\n    )\n\n\nasync def async_unload_services(hass: HomeAssistant) -> None:\n    \"\"\"Unload services for Goodwe integration.\"\"\"\n\n    if hass.services.has_service(DOMAIN, SERVICE_GET_PARAMETER):\n        hass.services.async_remove(DOMAIN, SERVICE_GET_PARAMETER)\n\n    if hass.services.has_service(DOMAIN, SERVICE_SET_PARAMETER):\n        hass.services.async_remove(DOMAIN, SERVICE_SET_PARAMETER)\n"
  },
  {
    "path": "custom_components/goodwe/services.yaml",
    "content": "get_parameter:\n  name: Get inverter configuration parameter\n  description: Read inverter configuration parameter and store it in helper entity\n  fields:\n    device_id:\n      name: Inverter device\n      description: ID of the inverter device\n      required: true\n      selector:\n        device:\n          integration: goodwe\n    parameter:\n      name: Parameter\n      description: Name of the inverter parameter\n      required: true\n      selector:\n        text:\n      example: 'battery_charge_current'\n    entity_id:\n      name: Helper entity\n      description: Entity where to store the parameter value\n      required: true\n      selector:\n        entity:\n          domain:\n            - input_number\n            - input_text\nset_parameter:\n  name: Set inverter configuration parameter - EXPERIMENTAL\n  description: BEWARE !!! Improper use may cause damage !\n  fields:\n    device_id:\n      name: Inverter device\n      description: ID of the inverter device\n      required: true\n      selector:\n        device:\n          integration: goodwe\n    parameter:\n      name: Parameter\n      description: Name of the inverter parameter\n      required: true\n      selector:\n        text:\n      example: 'battery_charge_current'\n    value:\n      name: Value\n      description: Value of the parameter to set\n      example: '20'\n      required: true\n      selector:\n        object:\n"
  },
  {
    "path": "custom_components/goodwe/strings.json",
    "content": "{\n  \"config\": {\n    \"abort\": {\n      \"already_configured\": \"[%key:common::config_flow::abort::already_configured_device%]\",\n      \"already_in_progress\": \"[%key:common::config_flow::abort::already_in_progress%]\"\n    },\n    \"error\": {\n      \"connection_error\": \"[%key:common::config_flow::error::cannot_connect%]\"\n    },\n    \"step\": {\n      \"user\": {\n        \"data\": {\n          \"host\": \"[%key:common::config_flow::data::ip%]\",\n          \"protocol\": \"Protocol\",\n          \"model_family\": \"Inverter Family (optional)\"\n        },\n        \"description\": \"Connect to inverter\",\n        \"title\": \"GoodWe inverter\"\n      }\n    }\n  },\n  \"entity\": {\n    \"button\": {\n      \"synchronize_clock\": {\n        \"name\": \"Synchronize inverter clock\"\n      }\n    },\n    \"number\": {\n      \"battery_discharge_depth\": {\n        \"name\": \"Depth of discharge (on-grid)\"\n      },\n      \"battery_discharge_depth_offline\": {\n        \"name\": \"Depth of discharge (backup)\"\n      },\n      \"battery_soc_protection\": {\n        \"name\": \"Battery SoC protection\"\n      },\n      \"eco_mode_power\": {\n        \"name\": \"Eco mode power\"\n      },\n      \"eco_mode_soc\": {\n        \"name\": \"Eco mode SoC\"\n      },\n      \"ems_power_limit\": {\n        \"name\": \"EMS power limit\"\n      },\n      \"fast_charging_power\": {\n        \"name\": \"Fast charging power\"\n      },\n      \"fast_charging_soc\": {\n        \"name\": \"Fast charging SoC\"\n      },\n      \"grid_export_limit\": {\n        \"name\": \"Grid export limit\"\n      },\n      \"soc_upper_limit\": {\n        \"name\": \"SoC upper limit\"\n      }\n    },\n    \"select\": {\n      \"ems_mode\": {\n        \"name\": \"EMS mode\",\n        \"state\": {\n          \"auto\": \"Auto\",\n          \"charge_pv\": \"Charge PV\",\n          \"discharge_pv\": \"Discharge PV\",\n          \"import_ac\": \"Import AC\",\n          \"export_ac\": \"Export AC\",\n          \"conserve\": \"Conserve\",\n          \"off_grid\": \"Off-grid\",\n          \"battery_standby\": \"Battery Standby\",\n          \"buy_power\": \"Buy Power\",\n          \"sell_power\": \"Sell Power\",\n          \"charge_battery\": \"Charge Battery\",\n          \"discharge_battery\": \"Discharge Battery\"\n        }\n      },\n      \"operation_mode\": {\n        \"name\": \"Inverter operation mode\",\n        \"state\": {\n          \"backup\": \"Backup mode\",\n          \"eco\": \"Eco mode\",\n          \"eco_charge\": \"Eco charge mode\",\n          \"eco_discharge\": \"Eco discharge mode\",\n          \"general\": \"General mode\",\n          \"off_grid\": \"Off-grid mode\",\n          \"peak_shaving\": \"Peak shaving mode\"\n        }\n      }\n    },\n    \"switch\": {\n      \"backup_supply_switch\": {\n        \"name\": \"Backup supply switch\"\n      },\n      \"dod_holding_switch\": {\n        \"name\": \"DOD holding\"\n      },\n      \"fast_charging_switch\": {\n        \"name\": \"Fast charging switch\"\n      },\n      \"grid_export_limit_switch\": {\n        \"name\": \"Grid export limit switch\"\n      },\n      \"load_control\": {\n        \"name\": \"Load control\"\n      }\n    }\n  },\n  \"options\": {\n    \"step\": {\n      \"init\": {\n        \"title\": \"GoodWe optional settings\",\n        \"description\": \"Specify optional (network) settings\",\n        \"data\": {\n          \"host\": \"[%key:common::config_flow::data::ip%]\",\n          \"port\": \"Port (8899/UDP | 502/TCP)\",\n          \"protocol\": \"Protocol\",\n          \"keep_alive\": \"TCP Keep alive\",\n          \"model_family\": \"Protocol Family [ET|DT|ES] (optional)\",\n          \"scan_interval\": \"Scan interval (s)\",\n          \"network_retries\": \"Network retry attempts\",\n          \"network_timeout\": \"Network request timeout (s)\"\n        }\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "custom_components/goodwe/switch.py",
    "content": "\"\"\"GoodWe PV inverter switch entities.\"\"\"\n\nfrom dataclasses import dataclass\nimport logging\nfrom typing import Any\n\nfrom goodwe import Inverter, InverterError\nfrom homeassistant.components.switch import (\n    SwitchDeviceClass,\n    SwitchEntity,\n    SwitchEntityDescription,\n)\nfrom homeassistant.config_entries import ConfigEntry\nfrom homeassistant.core import HomeAssistant\nfrom homeassistant.helpers.entity import DeviceInfo, EntityCategory\nfrom homeassistant.helpers.entity_platform import AddEntitiesCallback\nfrom homeassistant.helpers.update_coordinator import BaseCoordinatorEntity\n\nfrom .coordinator import GoodweUpdateCoordinator\n\n_LOGGER = logging.getLogger(__name__)\n\n\n@dataclass(frozen=True, kw_only=True)\nclass GoodweSwitchEntityDescription(SwitchEntityDescription):\n    \"\"\"Class describing Goodwe switch entities.\"\"\"\n\n    setting: str\n    polling_interval: int = 0\n\n\nSWITCHES = (\n    GoodweSwitchEntityDescription(\n        key=\"load_control\",\n        translation_key=\"load_control\",\n        device_class=SwitchDeviceClass.OUTLET,\n        setting=\"load_control_switch\",\n    ),\n    GoodweSwitchEntityDescription(\n        key=\"grid_export_limit_switch\",\n        translation_key=\"grid_export_limit_switch\",\n        entity_category=EntityCategory.CONFIG,\n        device_class=SwitchDeviceClass.SWITCH,\n        setting=\"grid_export\",\n    ),\n    GoodweSwitchEntityDescription(\n        key=\"fast_charging_switch\",\n        translation_key=\"fast_charging_switch\",\n        device_class=SwitchDeviceClass.SWITCH,\n        setting=\"fast_charging\",\n        polling_interval=30,\n    ),\n    GoodweSwitchEntityDescription(\n        key=\"backup_supply_switch\",\n        translation_key=\"backup_supply_switch\",\n        entity_category=EntityCategory.CONFIG,\n        device_class=SwitchDeviceClass.SWITCH,\n        setting=\"backup_supply\",\n    ),\n    GoodweSwitchEntityDescription(\n        key=\"dod_holding_switch\",\n        translation_key=\"dod_holding_switch\",\n        entity_category=EntityCategory.CONFIG,\n        device_class=SwitchDeviceClass.SWITCH,\n        setting=\"dod_holding\",\n    ),\n)\n\n\nasync def async_setup_entry(\n    hass: HomeAssistant,\n    config_entry: ConfigEntry,\n    async_add_entities: AddEntitiesCallback,\n) -> None:\n    \"\"\"Set up the inverter switch entities from a config entry.\"\"\"\n    inverter = config_entry.runtime_data.inverter\n    coordinator = config_entry.runtime_data.coordinator\n    device_info = config_entry.runtime_data.device_info\n\n    entities = []\n\n    for description in SWITCHES:\n        try:\n            current_state = await inverter.read_setting(description.setting)\n        except (InverterError, ValueError):\n            # Inverter model does not support this feature\n            _LOGGER.debug(\"Could not read %s value\", description.setting)\n        else:\n            entities.append(\n                InverterSwitchEntity(\n                    coordinator,\n                    device_info,\n                    description,\n                    inverter,\n                    current_state == 1,\n                )\n            )\n\n    async_add_entities(entities)\n\n\nclass InverterSwitchEntity(\n    BaseCoordinatorEntity[GoodweUpdateCoordinator], SwitchEntity\n):\n    \"\"\"Switch representation of inverter's 'Load Control' relay.\"\"\"\n\n    _attr_should_poll = False\n    _attr_has_entity_name = True\n    entity_description: GoodweSwitchEntityDescription\n\n    def __init__(\n        self,\n        coordinator: GoodweUpdateCoordinator,\n        device_info: DeviceInfo,\n        description: GoodweSwitchEntityDescription,\n        inverter: Inverter,\n        current_is_on: bool,\n    ) -> None:\n        \"\"\"Initialize the inverter operation mode setting entity.\"\"\"\n        super().__init__(coordinator)\n        self.entity_description = description\n        self._attr_unique_id = f\"{description.key}-{inverter.serial_number}\"\n        self._attr_device_info = device_info\n        self._attr_is_on = current_is_on\n        self._inverter: Inverter = inverter\n        self._notify_coordinator()\n\n    async def async_turn_on(self, **kwargs: Any) -> None:\n        \"\"\"Turn the switch on.\"\"\"\n        await self._inverter.write_setting(self.entity_description.setting, 1)\n        self._attr_is_on = True\n        self._notify_coordinator()\n        self.async_write_ha_state()\n\n    async def async_turn_off(self, **kwargs: Any) -> None:\n        \"\"\"Turn the switch off.\"\"\"\n        await self._inverter.write_setting(self.entity_description.setting, 0)\n        self._attr_is_on = False\n        self._notify_coordinator()\n        self.async_write_ha_state()\n\n    async def async_update(self) -> None:\n        \"\"\"Get the current value from inverter.\"\"\"\n        value = await self._inverter.read_setting(self.entity_description.setting)\n        self._attr_is_on = value == 1\n        self._notify_coordinator()\n\n    def _notify_coordinator(self) -> None:\n        if self.entity_description.polling_interval:\n            self.coordinator.entity_state_polling(\n                self,\n                self.entity_description.polling_interval if self._attr_is_on else 0,\n            )\n"
  },
  {
    "path": "custom_components/goodwe/translations/cs.json",
    "content": "{\n    \"config\": {\n        \"abort\": {\n            \"already_configured\": \"Zařízení je již nastaveno\",\n            \"already_in_progress\": \"Nastavení již probíhá\"\n        },\n        \"error\": {\n            \"connection_error\": \"Nepodařilo se připojit\"\n        },\n        \"flow_title\": \"GoodWe\",\n        \"step\": {\n            \"user\": {\n                \"data\": {\n                    \"host\": \"IP adresa\",\n                    \"port\": \"Port (8899/UDP | 502/TCP)\",\n                    \"protocol\": \"Protokol\",\n                    \"model_family\": \"Typ střídače (volitelné)\"\n                },\n                \"description\": \"Připojí se ke střídači\",\n                \"title\": \"Střídač GoodWe\"\n            }\n        }\n    },\n    \"entity\": {\n        \"button\": {\n            \"synchronize_clock\": {\n                \"name\": \"Synchronizovat hodiny střídače\"\n            },\n            \"start_inverter\": {\n                \"name\": \"Start střídače\"\n            },\n            \"stop_inverter\": {\n                \"name\": \"Stop střídače\"\n            }\n        },\n        \"number\": {\n            \"battery_discharge_depth\": {\n                \"name\": \"Maximum vybití (v síti)\"\n            },\n            \"battery_discharge_depth_offline\": {\n                \"name\": \"Maximum vybití (backup)\"\n            },\n            \"eco_mode_power\": {\n                \"name\": \"Výkon v ekonomickém režimu\"\n            },\n            \"eco_mode_soc\": {\n                \"name\": \"Stav nabítí baterie ekonomickém režimu\"\n            },\n            \"grid_export_limit\": {\n                \"name\": \"Limit dodávky do sítě\"\n            },\n            \"fast_charging_power\": {\n                \"name\": \"Rychlé nabíjení výkon\"\n            },\n            \"fast_charging_soc\": {\n                \"name\": \"Rychlé nabíjení stav baterie\"\n            }\n        },\n        \"select\": {\n            \"operation_mode\": {\n                \"name\": \"Provozní režim střídače\",\n                \"state\": {\n                    \"backup\": \"Režim zálohy\",\n                    \"eco\": \"Ekonomický režim\",\n                    \"eco_charge\": \"Režim ekonomického nabíjení\",\n                    \"eco_discharge\": \"Režim ekonomického vybíjení\",\n                    \"general\": \"Obecný režim\",\n                    \"off_grid\": \"Režim vypnuté sítě\",\n                    \"peak_shaving\": \"Režim pokrývání špiček\",\n                    \"self_use\": \"Vlastní spotřeba\"\n\n                }\n            }\n        },\n        \"sensor\": {\n            \"grid_in_out_label\": {\n                \"state\": {\n                    \"Idle\": \"Nečinnost\",\n                    \"Exporting\": \"Export\",\n                    \"Importing\": \"Import\"\n                }\n            }\n        },\n        \"switch\": {\n            \"grid_export_limit_switch\": {\n                \"name\": \"Řízení dodávky do sítě\"\n            },\n            \"fast_charging_switch\": {\n                \"name\": \"Rychlé nabíjení\"\n              },\n            \"load_control\": {\n                \"name\": \"Řízení zátěže\"\n            },\n            \"backup_supply_switch\": {\n                \"name\": \"Záloha\"\n            },\n            \"dod_holding_switch\": {\n                \"name\": \"Udržovat DOD baterie\"\n            }\n        }\n    },\n    \"options\": {\n        \"step\": {\n            \"init\": {\n                \"data\": {\n                    \"host\": \"IP adresa\",\n                    \"port\": \"Port (8899/UDP | 502/TCP)\",\n                    \"protocol\": \"Protokol\",\n                    \"keep_alive\": \"TCP Keep alive\",\n                    \"model_family\": \"Typ protokolu [ET|DT|ES]\",\n                    \"scan_interval\": \"Interval skenování (s)\",\n                    \"network_retries\": \"Počet opakování síťového požadavku\",\n                    \"network_timeout\": \"Časový limit síťového požadavku (s)\"\n                },\n                \"description\": \"Nastaví volitelné (síťové) volby\",\n                \"title\": \"Volitelné volby GoodWe\"\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "custom_components/goodwe/translations/de.json",
    "content": "{\n    \"config\": {\n        \"abort\": {\n            \"already_configured\": \"Gerät ist bereits konfiguriert\",\n            \"already_in_progress\": \"Die Konfiguration wird bereits bearbeitet\"\n        },\n        \"error\": {\n            \"connection_error\": \"Verbindung fehlgeschlagen\"\n        },\n    \"flow_title\": \"GoodWe\",\n        \"step\": {\n            \"user\": {\n                \"data\": {\n                    \"host\": \"Hostname / IP-Adresse\",\n                    \"port\": \"Port (8899/UDP | 502/TCP)\",\n                    \"protocol\": \"Protokoll\",\n                    \"model_family\": \"Wechselrichterfamilie (optional)\"\n                },\n                \"description\": \"Verbinden zum Wechselrichter\",\n                \"title\": \"GoodWe Wechselrichter\"\n            }\n        }\n    },\n    \"entity\": {\n        \"button\": {\n            \"synchronize_clock\": {\n                \"name\": \"Wechselrichter-Uhr synchronisieren\"\n            },\n            \"start_inverter\": {\n                \"name\": \"Wechselrichter starten\"\n            },\n            \"stop_inverter\": {\n                \"name\": \"Wechselrichter stoppen\"\n            }\n        },\n        \"number\": {\n            \"battery_discharge_depth\": {\n                \"name\": \"Entladungstiefe (Netzbetrieb)\"\n            },\n            \"eco_mode_power\": {\n                \"name\": \"Öko-Modus Leistung\"\n            },\n            \"eco_mode_soc\": {\n                \"name\": \"Öko-Modus SoC\"\n            },\n            \"grid_export_limit\": {\n                \"name\": \"Netzeinspeisung Limit\"\n            },\n            \"fast_charging_power\": {\n                \"name\": \"Schnellladeleistung\"\n            },\n              \"fast_charging_soc\": {\n                \"name\": \"Schnelllade SoC\"\n            }\n        },\n        \"select\": {\n            \"operation_mode\": {\n                \"name\": \"Wechselrichter-Betriebsart\",\n                \"state\": {\n                    \"backup\": \"Backup Modus\",\n                    \"eco\": \"Öko Modus\",\n                    \"eco_charge\": \"Öko Lademodus\",\n                    \"eco_discharge\": \"Öko Entlademodus\",\n                    \"general\": \"Allgemeiner Modus\",\n                    \"off_grid\": \"Netzunabhängiger Modus\",\n                    \"peak_shaving\": \"Spitzenlastreduzierungs-Modus\",\n                    \"self_use\": \"Selbstnutzung-Modus\"\n                }\n            }\n        },\n        \"sensor\": {\n            \"grid_in_out_label\": {\n                \"state\": {\n                    \"0\": \"Untätig\",\n                    \"1\": \"Exportieren\",\n                    \"2\": \"Importieren\"\n                }\n            }\n        },\n        \"switch\": {\n            \"grid_export_limit_switch\": {\n                \"name\": \"Netz-Export Begrenzungsschalter\"\n            },\n            \"fast_charging_switch\": {\n                \"name\": \"Schnellladungs-Schalter\"\n              },\n            \"load_control\": {\n                \"name\": \"Lastkontrolle\"\n            }\n        }\n    },\n    \"options\": {\n        \"step\": {\n            \"init\": {\n                \"data\": {\n                    \"host\": \"Hostname / IP-Adresse\",\n                    \"port\": \"Port (8899/UDP | 502/TCP)\",\n                    \"protocol\": \"Protokoll\",\n                    \"keep_alive\": \"TCP Aufrechterhaltung\",\n                    \"model_family\": \"Protokoll Familie [ET|DT|ES]\",\n                    \"scan_interval\": \"Scan-Intervall (s)\",\n                    \"network_retries\": \"Netzwiederholungsversuche\",\n                    \"network_timeout\": \"Zeitüberschreitung bei Netzanfragen(s)\"\n                },\n                \"description\": \"Optionale (Netzwerk-)Einstellungen\",\n                \"title\": \"GoodWe optionale Einstellungen\"\n            }\n        }\n    }\n}\n\n"
  },
  {
    "path": "custom_components/goodwe/translations/en.json",
    "content": "{\n    \"config\": {\n        \"abort\": {\n            \"already_configured\": \"Device is already configured\",\n            \"already_in_progress\": \"Configuration flow is already in progress\"\n        },\n        \"error\": {\n            \"connection_error\": \"Failed to connect\"\n        },\n    \"flow_title\": \"GoodWe\",\n        \"step\": {\n            \"user\": {\n                \"data\": {\n                    \"host\": \"Hostname / IP address\",\n                    \"port\": \"Port (8899/UDP | 502/TCP)\",\n                    \"protocol\": \"Protocol\",\n                    \"model_family\": \"Inverter Family (optional)\"\n                },\n                \"description\": \"Connect to inverter\",\n                \"title\": \"GoodWe inverter\"\n            }\n        }\n    },\n    \"entity\": {\n        \"button\": {\n            \"synchronize_clock\": {\n                \"name\": \"Synchronize inverter clock\"\n            },\n            \"start_inverter\": {\n                \"name\": \"Start inverter\"\n            },\n            \"stop_inverter\": {\n                \"name\": \"Stop inverter\"\n            }\n        },\n        \"number\": {\n            \"battery_discharge_depth\": {\n                \"name\": \"Depth of discharge (on-grid)\"\n            },\n            \"battery_discharge_depth_offline\": {\n                \"name\": \"Depth of discharge (backup)\"\n            },\n            \"battery_soc_protection\": {\n                \"name\": \"Battery SoC protection\"\n            },\n            \"eco_mode_power\": {\n                \"name\": \"Eco mode power\"\n            },\n            \"eco_mode_soc\": {\n                \"name\": \"Eco mode SoC\"\n            },\n            \"ems_power_limit\": {\n                \"name\": \"EMS power limit\"\n            },\n            \"grid_export_limit\": {\n                \"name\": \"Grid export limit\"\n            },\n            \"fast_charging_power\": {\n                \"name\": \"Fast charging power\"\n            },\n              \"fast_charging_soc\": {\n                \"name\": \"Fast charging SoC\"\n            }\n        },\n        \"select\": {\n            \"ems_mode\": {\n                \"name\": \"EMS mode\",\n                \"state\": {\n                \"auto\": \"Auto\",\n                \"charge_pv\": \"Charge PV\",\n                \"discharge_pv\": \"Discharge PV\",\n                \"import_ac\": \"Import AC\",\n                \"export_ac\": \"Export AC\",\n                \"conserve\": \"Conserve\",\n                \"off_grid\": \"Off-grid\",\n                \"battery_standby\": \"Battery Standby\",\n                \"buy_power\": \"Buy Power\",\n                \"sell_power\": \"Sell Power\",\n                \"charge_battery\": \"Charge Battery\",\n                \"discharge_battery\": \"Discharge Battery\"\n                }\n            },\n            \"operation_mode\": {\n                \"name\": \"Inverter operation mode\",\n                \"state\": {\n                    \"backup\": \"Backup mode\",\n                    \"eco\": \"Eco mode\",\n                    \"eco_charge\": \"Eco charge mode\",\n                    \"eco_discharge\": \"Eco discharge mode\",\n                    \"general\": \"General mode\",\n                    \"off_grid\": \"Off-grid mode\",\n                    \"peak_shaving\": \"Peak shaving mode\",\n                    \"self_use\": \"Self Use mode\"\n                }\n            }\n        },\n        \"sensor\": {\n            \"grid_in_out_label\": {\n                \"state\": {\n                    \"0\": \"Idle\",\n                    \"1\": \"Exporting\",\n                    \"2\": \"Importing\"\n                }\n            }\n        },\n        \"switch\": {\n            \"grid_export_limit_switch\": {\n                \"name\": \"Grid export limit switch\"\n            },\n            \"fast_charging_switch\": {\n                \"name\": \"Fast charging switch\"\n              },\n            \"load_control\": {\n                \"name\": \"Load control\"\n            },\n            \"backup_supply_switch\": {\n                \"name\": \"Backup supply\"\n            },\n            \"dod_holding_switch\": {\n                \"name\": \"DOD holding\"\n            }\n        }\n    },\n    \"options\": {\n        \"step\": {\n            \"init\": {\n                \"data\": {\n                    \"host\": \"Hostname / IP Address\",\n                    \"port\": \"Port (8899/UDP | 502/TCP)\",\n                    \"protocol\": \"Protocol\",\n                    \"keep_alive\": \"TCP Keep alive\",\n                    \"model_family\": \"Protocol Family [ET|DT|ES]\",\n                    \"scan_interval\": \"Scan interval (s)\",\n                    \"network_retries\": \"Network retry attempts\",\n                    \"network_timeout\": \"Network request timeout (s)\"\n                },\n                \"description\": \"Specify optional (network) settings\",\n                \"title\": \"GoodWe optional settings\"\n            }\n        }\n    }\n}\n\n"
  },
  {
    "path": "custom_components/goodwe/translations/es.json",
    "content": "{\n    \"config\": {\n        \"abort\": {\n            \"already_configured\": \"El dispositivo ya está configurado\",\n            \"already_in_progress\": \"El proceso de configuración ya está en curso\"\n        },\n        \"error\": {\n            \"connection_error\": \"No se pudo conectar\"\n        },\n    \"flow_title\": \"GoodWe\",\n        \"step\": {\n            \"user\": {\n                \"data\": {\n                    \"host\": \"Nombre de equipo / Dirección IP\",\n                    \"port\": \"Port (8899/UDP | 502/TCP)\",\n                    \"protocol\": \"Protocolo\",\n                    \"model_family\": \"Familia de inversores (opcional)\"\n                },\n                \"description\": \"Conectar al inversor\",\n                \"title\": \"Inversor GoodWe\"\n            }\n        }\n    },\n    \"entity\": {\n        \"button\": {\n            \"synchronize_clock\": {\n                \"name\": \"Sincronizar reloj del inversor\"\n            },\n            \"start_inverter\": {\n                \"name\": \"Iniciar inversor\"\n            },\n            \"stop_inverter\": {\n                \"name\": \"Detener inversor\"\n            }\n        },\n        \"number\": {\n            \"battery_discharge_depth\": {\n                \"name\": \"Profundidad de descarga (en red)\"\n            },\n            \"battery_discharge_depth_offline\": {\n                \"name\": \"Profundidad de descarga (respaldo)\"\n            },\n            \"eco_mode_power\": {\n                \"name\": \"Potencia en modo ecológico\"\n            },\n            \"eco_mode_soc\": {\n                \"name\": \"Estado de carga en modo ecológico\"\n            },\n            \"grid_export_limit\": {\n                \"name\": \"Límite de exportación a la red\"\n            },\n            \"fast_charging_power\": {\n                \"name\": \"Potencia de carga rápida\"\n            },\n              \"fast_charging_soc\": {\n                \"name\": \"Estado de carga para carga rápida\"\n            }\n        },\n        \"select\": {\n            \"operation_mode\": {\n                \"name\": \"Modo de funcionamiento del inversor\",\n                \"state\": {\n                    \"backup\": \"Modo de respaldo\",\n                    \"eco\": \"Modo ecológico\",\n                    \"eco_charge\": \"Modo de carga ecológica\",\n                    \"eco_discharge\": \"Modo de descarga ecológica\",\n                    \"general\": \"Modo general\",\n                    \"off_grid\": \"Modo aislado\",\n                    \"peak_shaving\": \"Modo de recorte de picos\",\n                    \"self_use\": \"Modo de autoconsumo\"\n                }\n            }\n        },\n        \"sensor\": {\n            \"grid_in_out_label\": {\n                \"state\": {\n                    \"0\": \"En reposo\",\n                    \"1\": \"Exportando\",\n                    \"2\": \"Importando\"\n                }\n            }\n        },\n        \"switch\": {\n            \"grid_export_limit_switch\": {\n                \"name\": \"Interruptor de límite de exportación a red\"\n            },\n            \"fast_charging_switch\": {\n                \"name\": \"Interruptor de carga rápida\"\n              },\n            \"load_control\": {\n                \"name\": \"Control de carga\"\n            },\n            \"backup_supply_switch\": {\n                \"name\": \"Suministro de respaldo\"\n            },\n            \"dod_holding_switch\": {\n                \"name\": \"Retención de profundidad de descarga\"\n            }\n        }\n    },\n    \"options\": {\n        \"step\": {\n            \"init\": {\n                \"data\": {\n                    \"host\": \"Nombre de equipo / Dirección IP\",\n                    \"port\": \"Port (8899/UDP | 502/TCP)\",\n                    \"protocol\": \"Protocolo\",\n                    \"keep_alive\": \"Mantenimiento de conexión TCP\",\n                    \"model_family\": \"Familia de protocolos [ET|DT|ES]\",\n                    \"scan_interval\": \"Intervalo de escaneo (s)\",\n                    \"network_retries\": \"Reintentos de red\",\n                    \"network_timeout\": \"Tiempo de espera de solicitud de red (s)\"\n                },\n                \"description\": \"Especificar configuraciones opcionales (de red)\",\n                \"title\": \"Configuraciones opcionales de GoodWe\"\n            }\n        }\n    }\n}\n\n"
  },
  {
    "path": "custom_components/goodwe/translations/sk.json",
    "content": "{\n    \"config\": {\n        \"abort\": {\n            \"already_configured\": \"Zariadenie je už nastavené\",\n            \"already_in_progress\": \"Konfigurácia už prebieha\"\n        },\n        \"error\": {\n            \"connection_error\": \"Nepodarilo sa pripojiť\"\n        },\n        \"flow_title\": \"GoodWe\",\n        \"step\": {\n            \"user\": {\n                \"data\": {\n                    \"host\": \"IP adresa\",\n                    \"port\": \"Port (8899/UDP | 502/TCP)\",\n                    \"protocol\": \"Protokol\",\n                    \"model_family\": \"Typ meniča (voliteľné)\"\n                },\n                \"description\": \"Pripojiť sa k meniča\",\n                \"title\": \"Menič GoodWe\"\n            }\n        }\n    },\n    \"entity\": {\n        \"button\": {\n            \"synchronize_clock\": {\n                \"name\": \"Synchronizácia hodín meniča\"\n            },\n            \"start_inverter\": {\n                \"name\": \"Zapnúť menič\"\n            },\n            \"stop_inverter\": {\n                \"name\": \"Zastaviť menič\"\n            }\n        },\n        \"number\": {\n            \"battery_discharge_depth\": {\n                \"name\": \"Maximum vybitia (v sieti)\"\n            },\n            \"eco_mode_power\": {\n                \"name\": \"Výkon v ekonomickom režime\"\n            },\n            \"eco_mode_soc\": {\n                \"name\": \"Stav nabitia batérie v ekonomickom režime\"\n            },\n            \"grid_export_limit\": {\n                \"name\": \"Limit dodávky do siete\"\n            },\n            \"fast_charging_power\": {\n                \"name\": \"Rýchle nabíjanie výkon\"\n            },\n            \"fast_charging_soc\": {\n                \"name\": \"Rýchle nabíjanie stav batérie\"\n            }\n        },\n        \"select\": {\n            \"operation_mode\": {\n                \"name\": \"Prevádzkový režim meniča\",\n                \"state\": {\n                    \"backup\": \"Režim zálohovania\",\n                    \"eco\": \"Ekonomický režim\",\n                    \"eco_charge\": \"Režim ekonomického nabíjania\",\n                    \"eco_discharge\": \"Režim ekonomického vybíjania\",\n                    \"general\": \"Všeobecný režim\",\n                    \"off_grid\": \"Režim mimo siete\",\n                    \"peak_shaving\": \"Režim šetrenia v špičke\",\n                    \"self_use\": \"Vlastná spotreba\"\n\n                }\n            }\n        },\n        \"sensor\": {\n            \"grid_in_out_label\": {\n                \"state\": {\n                    \"Idle\": \"Nečinnosť\",\n                    \"Exporting\": \"Export\",\n                    \"Importing\": \"Import\"\n                }\n            }\n        },\n        \"switch\": {\n            \"grid_export_limit_switch\": {\n                \"name\": \"Riadenie dodávky do siete\"\n            },\n            \"fast_charging_switch\": {\n                \"name\": \"Rýchle nabíjanie\"\n              },\n            \"load_control\": {\n                \"name\": \"Riadenie záťaže\"\n            },\n            \"backup_supply_switch\": {\n                \"name\": \"Záloha\"\n            }\n        }\n    },\n    \"options\": {\n        \"step\": {\n            \"init\": {\n                \"data\": {\n                    \"host\": \"IP adresa\",\n                    \"port\": \"Port (8899/UDP | 502/TCP)\",\n                    \"protocol\": \"Protokol\",\n                    \"keep_alive\": \"TCP Keep alive\",\n                    \"model_family\": \"Typ protokolu [ET|DT|ES]\",\n                    \"scan_interval\": \"Interval skenovania (s)\",\n                    \"network_retries\": \"Počet opakovaní sieťových dopytov\",\n                    \"network_timeout\": \"Časový limit sieťových dopytov (s)\"\n                },\n                \"description\": \"Nastaví voliteľné (sieťové) parametre\",\n                \"title\": \"Voliteľné parametre GoodWe\"\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "hacs.json",
    "content": "{\n  \"name\": \"GoodWe Inverter (experimental)\",\n  \"homeassistant\": \"2025.12.0\"\n}\n"
  },
  {
    "path": "info.md",
    "content": "[![\"Buy Me A Coffee\"](https://www.buymeacoffee.com/assets/img/custom_images/orange_img.png)](https://www.buymeacoffee.com/mletenay)\n[![hacs_badge](https://img.shields.io/badge/HACS-Default-orange.svg)](https://github.com/hacs/integration)\n![GitHub Release](https://img.shields.io/github/v/release/mletenay/home-assistant-goodwe-inverter)\n\n## GoodWe solar inverter for Home Assistant (experimental)\n\nSupport 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.\n\nThis 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.\nUse at own risk.\n\n### Differences between this HACS and native HA integration\n\n- EMS modes\n- Special work modes `Eco charge mode` and `Eco discharge mode` (24/7 with defined power and SoC).\n- Network configuration parameters `Scan iterval`, `Network retry attempts`, `Network request timeout`.\n- Switch `Export Limit Switch`.\n- Switch `Load Control` (for ET+ inverters).\n- Switch and SoC/Power inputs for `Fast Charging` functionality.\n- `Start inverter` and `Stop inverter` buttons for grid-only inverters.\n- Services for getting/setting inverter configuration parameters\n\n### Migration from HACS to HA\n\nIf 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.\n\n(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.)\n\n### Documentation\n\nFind the full documentation [here](https://github.com/mletenay/home-assistant-goodwe-inverter).\n"
  },
  {
    "path": "inverter_scan.py",
    "content": "\"\"\"Simple test script to scan inverter present on local network\"\"\"\nimport asyncio\nimport goodwe\nimport logging\nimport sys\n\nlogging.basicConfig(\n    format=\"%(asctime)-15s %(funcName)s(%(lineno)d) - %(levelname)s: %(message)s\",\n    stream=sys.stderr,\n    level=getattr(logging, \"ERROR\", None),\n)\n\nresult = asyncio.run(goodwe.search_inverters()).decode(\"utf-8\").split(\",\")\nprint(f\"Located inverter at IP: {result[0]}, mac: {result[1]}, name: {result[2]}\")\n\ninverter = asyncio.run(goodwe.discover(result[0], 8899))\nprint(\n    f\"Identified inverter model: {inverter.model_name}, serialNr: {inverter.serial_number}\"\n)\n"
  },
  {
    "path": "inverter_test.py",
    "content": "\"\"\"Simple test script to check inverter UDP protocol communication\"\"\"\n\nimport asyncio\nimport goodwe\nimport logging\nimport sys\n\n\nlogging.basicConfig(\n    format=\"%(asctime)-15s %(funcName)s(%(lineno)d) - %(levelname)s: %(message)s\",\n    stream=sys.stderr,\n    level=getattr(logging, \"ERROR\", None),\n)\n\n# Set the appropriate IP address\nIP_ADDRESS = \"192.168.2.14\"\nPORT = 8899\n\nFAMILY = \"ET\"  # One of ET, ES, DT or None to detect inverter family automatically\nCOMM_ADDR = None  # Usually 0xf7 for ET/ES or 0x7f for DT, or None for default value\nTIMEOUT = 1\nRETRIES = 3\n\ninverter = asyncio.run(\n    goodwe.connect(host=IP_ADDRESS, family=FAMILY, timeout=TIMEOUT, retries=RETRIES)\n)\nprint(\n    f\"Identified inverter:\\n\"\n    f\"\\tModel:    {inverter.model_name}\\n\"\n    f\"\\tSerialNr: {inverter.serial_number}\\n\"\n    f\"\\tFirmware: {inverter.firmware}\"\n)\n\nresponse = asyncio.run(inverter.read_runtime_data())\n\nprint(\"\\nSensors values:\")\nfor sensor in inverter.sensors():\n    if sensor.id_ in response:\n        print(\n            f\"\\t{sensor.id_:30}:\\t{sensor.name} = {response[sensor.id_]} {sensor.unit}\"\n        )\n"
  }
]