Full Code of suaveolent/ha-hoymiles-wifi for AI

main 2f6613ef2c49 cached
35 files
191.1 KB
46.2k tokens
95 symbols
1 requests
Download .txt
Showing preview only (202K chars total). Download the full file or copy to clipboard to get everything.
Repository: suaveolent/ha-hoymiles-wifi
Branch: main
Commit: 2f6613ef2c49
Files: 35
Total size: 191.1 KB

Directory structure:
gitextract_y3ez4czx/

├── .github/
│   ├── FUNDING.yaml
│   ├── ISSUE_TEMPLATE/
│   │   ├── bug_report.md
│   │   └── feature_request.md
│   └── workflows/
│       └── close_inactive_issues.yml
├── .gitignore
├── .pre-commit-config.yaml
├── LICENSE
├── README.md
├── custom_components/
│   ├── __init__.py
│   └── hoymiles_wifi/
│       ├── __init__.py
│       ├── binary_sensor.py
│       ├── button.py
│       ├── config_flow.py
│       ├── const.py
│       ├── coordinator.py
│       ├── entity.py
│       ├── error.py
│       ├── manifest.json
│       ├── number.py
│       ├── sensor.py
│       ├── services.py
│       ├── services.yaml
│       ├── strings.json
│       ├── translations/
│       │   ├── de.json
│       │   ├── en.json
│       │   └── fr.json
│       └── util.py
├── hacs.json
├── requirements.test.txt
├── setup.cfg
└── tests/
    ├── __init__.py
    ├── bandit.yaml
    ├── conftest.py
    ├── test_config_flow.py
    └── test_init.py

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

================================================
FILE: .github/FUNDING.yaml
================================================
github: suaveolent
buy_me_a_coffee: suaveolent


================================================
FILE: .github/ISSUE_TEMPLATE/bug_report.md
================================================
---
name: Bug report
about: Create a report to help us improve
title: ''
labels: ''
assignees: ''

---

**Describe the bug**
A clear and concise description of what the bug is.

**To Reproduce**
Steps to reproduce the behavior:
1. Go to '...'
2. Click on '....'
3. Scroll down to '....'
4. See error

**Expected behavior**
A clear and concise description of what you expected to happen.

**Screenshots**
If applicable, add screenshots to help explain your problem.

**Desktop (please complete the following information):**
 - OS: [e.g. iOS]
 - Browser [e.g. chrome, safari]
 - Version [e.g. 22]

**Smartphone (please complete the following information):**
 - Device: [e.g. iPhone6]
 - OS: [e.g. iOS8.1]
 - Browser [e.g. stock browser, safari]
 - Version [e.g. 22]

**Additional context**
Add any other context about the problem here.


================================================
FILE: .github/ISSUE_TEMPLATE/feature_request.md
================================================
---
name: Feature request
about: Suggest an idea for this project
title: ''
labels: ''
assignees: ''

---

**Is your feature request related to a problem? Please describe.**
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]

**Describe the solution you'd like**
A clear and concise description of what you want to happen.

**Describe alternatives you've considered**
A clear and concise description of any alternative solutions or features you've considered.

**Additional context**
Add any other context or screenshots about the feature request here.


================================================
FILE: .github/workflows/close_inactive_issues.yml
================================================
name: Close inactive issues
on:
  schedule:
    - cron: "30 1 * * *"

jobs:
  close-issues:
    runs-on: ubuntu-latest
    permissions:
      issues: write
      pull-requests: write
    steps:
      - uses: actions/stale@v5
        with:
          days-before-issue-stale: 30
          days-before-issue-close: 14
          stale-issue-label: "stale"
          stale-issue-message: "This issue is stale because it has been open for 30 days with no activity."
          close-issue-message: "This issue was closed because it has been inactive for 14 days since being marked as stale."
          exempt-issue-labels: "bug,documentation,enhancement,good first issue,help wanted,question"
          days-before-pr-stale: -1
          days-before-pr-close: -1
          repo-token: ${{ secrets.GITHUB_TOKEN }}


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

# C extensions
*.so

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

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

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

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

# Translations
*.mo
*.pot

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

# Flask stuff:
instance/
.webassets-cache

# Scrapy stuff:
.scrapy

# Sphinx documentation
docs/_build/

# PyBuilder
.pybuilder/
target/

# Jupyter Notebook
.ipynb_checkpoints

# IPython
profile_default/
ipython_config.py

# pyenv
#   For a library or package, you might want to ignore these files since the code is
#   intended to run in multiple environments; otherwise, check them in:
# .python-version

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

# poetry
#   Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
#   This is especially recommended for binary packages to ensure reproducibility, and is more
#   commonly ignored for libraries.
#   https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
#poetry.lock

# pdm
#   Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
#pdm.lock
#   pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
#   in version control.
#   https://pdm.fming.dev/#use-with-ide
.pdm.toml

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

# Celery stuff
celerybeat-schedule
celerybeat.pid

# SageMath parsed files
*.sage.py

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

# Spyder project settings
.spyderproject
.spyproject

# Rope project settings
.ropeproject

# mkdocs documentation
/site

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

# Pyre type checker
.pyre/

# pytype static type analyzer
.pytype/

# Cython debug symbols
cython_debug/

# PyCharm
#  JetBrains specific template is maintained in a separate JetBrains.gitignore that can
#  be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
#  and can be added to the global gitignore or merged into this file.  For a more nuclear
#  option (not recommended) you can uncomment the following to ignore the entire idea folder.
#.idea/

# General
.DS_Store
.AppleDouble
.LSOverride

# Icon must end with two \r
Icon


# Thumbnails
._*

# Files that might appear in the root of a volume
.DocumentRevisions-V100
.fseventsd
.Spotlight-V100
.TemporaryItems
.Trashes
.VolumeIcon.icns
.com.apple.timemachine.donotpresent

# Directories potentially created on remote AFP share
.AppleDB
.AppleDesktop
Network Trash Folder
Temporary Items
.apdisk


================================================
FILE: .pre-commit-config.yaml
================================================
repos:
  - repo: https://github.com/asottile/pyupgrade
    rev: v2.3.0
    hooks:
      - id: pyupgrade
        args: [--py37-plus]
  - repo: https://github.com/psf/black
    rev: 19.10b0
    hooks:
      - id: black
        args:
          - --safe
          - --quiet
        files: ^((homeassistant|script|tests)/.+)?[^/]+\.py$
  - repo: https://github.com/codespell-project/codespell
    rev: v1.16.0
    hooks:
      - id: codespell
        args:
          - --ignore-words-list=hass,alot,datas,dof,dur,farenheit,hist,iff,ines,ist,lightsensor,mut,nd,pres,referer,ser,serie,te,technik,ue,uint,visability,wan,wanna,withing
          - --skip="./.*,*.csv,*.json"
          - --quiet-level=2
        exclude_types: [csv, json]
  - repo: https://gitlab.com/pycqa/flake8
    rev: 3.8.1
    hooks:
      - id: flake8
        additional_dependencies:
          - flake8-docstrings==1.5.0
          - pydocstyle==5.0.2
        files: ^(homeassistant|script|tests)/.+\.py$
  - repo: https://github.com/PyCQA/bandit
    rev: 1.6.2
    hooks:
      - id: bandit
        args:
          - --quiet
          - --format=custom
          - --configfile=tests/bandit.yaml
        files: ^(homeassistant|script|tests)/.+\.py$
  - repo: https://github.com/pre-commit/mirrors-isort
    rev: v4.3.21
    hooks:
      - id: isort
  - repo: https://github.com/pre-commit/pre-commit-hooks
    rev: v2.4.0
    hooks:
      - id: check-executables-have-shebangs
        stages: [manual]
      - id: check-json
  - repo: https://github.com/pre-commit/mirrors-mypy
    rev: v0.770
    hooks:
      - id: mypy
        args:
          - --pretty
          - --show-error-codes
          - --show-error-context


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

Copyright (c) 2023 suaveolent

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

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

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


================================================
FILE: README.md
================================================
# Hoymiles for Home Assistant

This custom component integrates Hoymiles DTUs, HMS-XXXXW microinverters and hybrid inverters into Home Assistant, providing live inverter data.
It uses the [hoymiles-wifi](https://github.com/suaveolent/hoymiles-wifi) Python library to communicate directly with the devices over your local network — no cloud connection required.

> [!NOTE]
> Disclaimer: This library is not affiliated with Hoymiles. It is an independent project developed to provide tools for interacting with Hoymiles DTUs and Hoymiles HMS-XXXXW series micro-inverters featuring integrated WiFi DTU. Any trademarks or product names mentioned are the property of their respective owners.

[!["Buy Me A Coffee"](https://www.buymeacoffee.com/assets/img/custom_images/orange_img.png)](https://www.buymeacoffee.com/suaveolent)

## Supported Devices

The custom component was successfully tested with:

- Hoymiles HMS-400W-1T
- Hoymiles HMS-800W-2T
- Hoymiles HMS-1000W-2T
- Hoymiles HMS-2000DW-4T
- Hoymiles DTU-WLite
- Hoymiles DTU-Pro (S)
- Hoymiles HAS-5.0LV-EUG1
- Hoymiles HYS-4.6LV-EUG1
- Hoymiles HYT-5.0HV-EUG1
- Hoymiles HAT-8.0HV-EUG1
- Solenso H-1000 (not tested for command, only to get data)
- Solenso DTU_SLS (not tested for command, only to get data)

## Warning

> [!CAUTION]
> Please refrain from using the current power limitation feature for zero feed-in, as it may lead to damaging the inverter due to excessive writes to the EEPROM.

## Installation

1. Open the [HACS](https://hacs.xyz) panel in your Home Assistant frontend.

2. Navigate to the "Integrations" tab.

3. Click the three dots in the top-right corner and select "Custom Repositories."

4. Add a new custom repository:

- **URL:** `https://github.com/suaveolent/ha-hoymiles-wifi`

- **Category:** Integration

5. Click "Add"

6. Click on the `Hoymiles` integration.

7. Click "DOWNLOAD"

8. Navigate to "Settings" - "Devices & Services"

9. Click "ADD INTEGRATION" and select the `Hoymiles` integration.

10. Insert IP address of hoymiles DTUBI-xxxx in field Host and click on SUBMIT

> [!NOTE]
> Sometimes the necessary lib
> (https://github.com/suaveolent/hoymiles-wifi) is not correctly
> installed. In this case you need to manually install the library by
> running the `pip install hoymiles-wifi` command yourself.

### Option 2: Manual Installation

1. Download the contents of this repository as a ZIP file.

2. Extract the ZIP file.

3. Copy the entire `custom_components/hoymiles-wifi` directory to your Home Assistant

4. Install the python requirements

5. Restart your Home Assistant instance to apply the changes.

### Docker Users: Workaround for HTTP 500 Error

If you encounter an HTTP 500 error when adding the integration in a Home Assistant Docker container, follow this workaround:

1. Create a new Docker image for Home Assistant with the `hoymiles-wifi` library pre-installed:
   ```dockerfile
   FROM homeassistant/home-assistant
   RUN pip install hoymiles-wifi
   ```
2. Build the new Docker image:
   ```bash
   docker build -t ha-hoymiles .
   ```
3. Switch to this newly built image when running Home Assistant.

## Configuration

Configuration is done in the UI.

1. `Host`: Enter the IP address or the hostname of your inverter or DTU.

> [!NOTE]
> To find the IP address or hostname of your inverter/DTU, you can either access your router’s web interface to view connected devices, or use a network scanning tool (such as Fing or Angry IP Scanner) to identify the device on your local network.

2. `Update interval (seconds)`: This defines how frequently the system will request data from the inverter or DTU. Enter the desired time in seconds.

> [!NOTE]
> Setting the update interval below approximately 32 seconds may disable Hoymiles cloud functionality. To ensure proper communication with Hoymiles servers, keep the update interval at or above this threshold.

## Screenshots

![Integration](/screenshots/integration.png?raw=true)
![Devices](/screenshots/devices.png?raw=true)
![Device](/screenshots/device.png?raw=true)

## Caution

Use this custom component responsibly and be aware of potential risks. There are no guarantees provided, and any misuse or incorrect implementation may result in undesirable outcomes. Ensure that your inverter is not compromised during communication.

## Known Limitations

> [!NOTE]
> **Update Frequency:** The library may experience limitations in fetching updates, potentially around twice per minute. The inverter firmware may enforce a mandatory wait period of approximately 30 seconds between requests.
> This issue can be identified when the data returned matches the response from the previous request.
> If you encounter this, you can try the _experimental_ performance data mode. (Needs to be enabled on each reboot of the DTU.)

> [!NOTE]
> **Compatibility:** While developed for the HMS-800W-2T inverter, compatibility with other inverters from the series is untested at the time of writing. Exercise caution and conduct thorough testing if using with different inverter models.

## Attribution

This project was generated from [@oncleben31](https://github.com/oncleben31)'s [Home Assistant Custom Component Cookiecutter](https://github.com/oncleben31/cookiecutter-homeassistant-custom-component) template.


================================================
FILE: custom_components/__init__.py
================================================


================================================
FILE: custom_components/hoymiles_wifi/__init__.py
================================================
"""Platform for retrieving values of a Hoymiles inverter."""

from datetime import timedelta
import logging
import voluptuous as vol

from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_HOST, Platform
from homeassistant.core import HomeAssistant
from homeassistant.helpers.device_registry import DeviceEntry
from homeassistant.helpers.typing import ConfigType
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.service import SupportsResponse
from hoymiles_wifi.dtu import DTU

from .const import (
    CONF_DTU_SERIAL_NUMBER,
    CONF_HYBRID_INVERTERS,
    CONF_INVERTERS,
    CONF_METERS,
    CONF_PORTS,
    CONF_THREE_PHASE_INVERTERS,
    CONF_TIMEOUT,
    CONF_UPDATE_INTERVAL,
    CONFIG_VERSION,
    CONF_IS_ENCRYPTED,
    CONF_ENC_RAND,
    DEFAULT_APP_INFO_UPDATE_INTERVAL_SECONDS,
    DEFAULT_CONFIG_UPDATE_INTERVAL_SECONDS,
    DEFAULT_TIMEOUT_SECONDS,
    DOMAIN,
    HASS_APP_INFO_COORDINATOR,
    HASS_CONFIG_COORDINATOR,
    HASS_DATA_COORDINATOR,
    HASS_DTU,
    HASS_ENERGY_STORAGE_DATA_COORDINATOR,
)
from .coordinator import (
    HoymilesAppInfoUpdateCoordinator,
    HoymilesConfigUpdateCoordinator,
    HoymilesRealDataUpdateCoordinator,
    HoymilesEnergyStorageUpdateCoordinator,
)
from .error import CannotConnect
from .services import async_handle_set_bms_mode
from .util import async_get_config_entry_data_for_host

_LOGGER = logging.getLogger(__name__)

PLATFORMS = [Platform.SENSOR, Platform.NUMBER, Platform.BINARY_SENSOR, Platform.BUTTON]

SET_BMS_SCHEMA = vol.Schema(
    {
        vol.Required("bms_mode"): vol.In(
            (
                "self_use",
                "economic",
                "backup_power",
                "pure_off_grid",
                "forced_charging",
                "forced_discharge",
                "peak_shaving",
                "time_of_use",
            )
        ),
        vol.Required("rev_soc"): vol.All(vol.Coerce(int), vol.Range(min=0, max=100)),
        vol.Optional("max_power"): vol.All(vol.Coerce(int), vol.Range(min=0)),
        vol.Optional("peak_soc"): vol.All(vol.Coerce(int), vol.Range(min=0, max=100)),
        vol.Optional("peak_meter_power"): vol.All(vol.Coerce(int), vol.Range(min=0)),
        vol.Optional("time_settings"): str,
        vol.Optional("time_periods"): str,
        vol.Optional("device_id"): cv.ensure_list,
    }
)


async def async_setup(hass: HomeAssistant, config: ConfigType):
    """Set up this integration using YAML is not supported."""
    return True


async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry):
    """Set up this integration using UI."""

    hass.data.setdefault(DOMAIN, {})

    hass_data = dict(config_entry.data)

    host = config_entry.data.get(CONF_HOST)
    update_interval = timedelta(seconds=config_entry.data.get(CONF_UPDATE_INTERVAL))
    single_phase_inverters = config_entry.data[CONF_INVERTERS]
    three_phase_inverters = config_entry.data.get(CONF_THREE_PHASE_INVERTERS, [])
    hybrid_inverters = config_entry.data.get(CONF_HYBRID_INVERTERS, [])
    meters = config_entry.data.get(CONF_METERS, [])
    is_encrypted = config_entry.data.get(CONF_IS_ENCRYPTED, False)
    enc_rand = config_entry.data.get(CONF_ENC_RAND, None)
    timeout = config_entry.data.get(CONF_TIMEOUT, DEFAULT_TIMEOUT_SECONDS)

    if is_encrypted:
        dtu = DTU(
            host,
            is_encrypted=is_encrypted,
            enc_rand=bytes.fromhex(enc_rand),
            timeout=timeout,
        )
    else:
        dtu = DTU(host, timeout=timeout)

    hass_data[HASS_DTU] = dtu

    if single_phase_inverters or three_phase_inverters or meters:
        data_coordinator = HoymilesRealDataUpdateCoordinator(
            hass, dtu=dtu, config_entry=config_entry, update_interval=update_interval
        )
        hass_data[HASS_DATA_COORDINATOR] = data_coordinator

        config_update_interval = timedelta(
            seconds=DEFAULT_CONFIG_UPDATE_INTERVAL_SECONDS
        )
        config_coordinator = HoymilesConfigUpdateCoordinator(
            hass=hass,
            dtu=dtu,
            config_entry=config_entry,
            update_interval=config_update_interval,
        )
        hass_data[HASS_CONFIG_COORDINATOR] = config_coordinator

        app_info_update_interval = timedelta(
            seconds=DEFAULT_APP_INFO_UPDATE_INTERVAL_SECONDS
        )
        app_info_update_coordinator = HoymilesAppInfoUpdateCoordinator(
            hass=hass,
            dtu=dtu,
            config_entry=config_entry,
            update_interval=app_info_update_interval,
        )
        hass_data[HASS_APP_INFO_COORDINATOR] = app_info_update_coordinator

    if hybrid_inverters:
        energy_storage_data_coordinator = HoymilesEnergyStorageUpdateCoordinator(
            hass=hass,
            dtu=dtu,
            config_entry=config_entry,
            update_interval=update_interval,
            dtu_serial_number=config_entry.data[CONF_DTU_SERIAL_NUMBER],
            inverters=hybrid_inverters,
        )

        hass_data[HASS_ENERGY_STORAGE_DATA_COORDINATOR] = (
            energy_storage_data_coordinator
        )

    _LOGGER.debug(f"  hass_data: {hass_data}")  # --- IGNORE ---
    _LOGGER.debug(f"  config_entry_id: {config_entry.entry_id}")

    hass.data[DOMAIN][config_entry.entry_id] = hass_data
    await hass.config_entries.async_forward_entry_setups(config_entry, PLATFORMS)

    if single_phase_inverters or three_phase_inverters or meters:
        await data_coordinator.async_config_entry_first_refresh()
        await config_coordinator.async_config_entry_first_refresh()
        await app_info_update_coordinator.async_config_entry_first_refresh()
    if hybrid_inverters:
        await energy_storage_data_coordinator.async_config_entry_first_refresh()
        hass.services.async_register(
            domain=DOMAIN,
            service="set_bms_mode",
            service_func=async_handle_set_bms_mode,
            schema=SET_BMS_SCHEMA,
            supports_response=SupportsResponse.NONE,
        )
        _LOGGER.debug("Service set_bms_mode registered")

    return True


async def async_remove_config_entry_device(
    hass: HomeAssistant, config_entry: ConfigEntry, device_entry: DeviceEntry
) -> bool:
    """Remove a config entry from a device."""
    return True


async def async_migrate_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool:
    """Migrate old entry data to the new entry schema."""

    data = config_entry.data.copy()

    current_version = data.get("version", 1)

    if current_version != CONFIG_VERSION:
        _LOGGER.info(
            "Migrating entry %s to version %s", config_entry.entry_id, CONFIG_VERSION
        )
        new = {**config_entry.data}

        host = config_entry.data.get(CONF_HOST)
        try:
            (
                dtu_sn,
                single_phase_inverters,
                three_phase_inverters,
                ports,
                meters,
                hybrid_inverters,
                is_encrypted,
                enc_rand,
            ) = await async_get_config_entry_data_for_host(host)
        except CannotConnect:
            _LOGGER.error(
                "Could not retrieve real data information data from inverter: %s. Please ensure inverter is available!",
                host,
            )
            return False

        new[CONF_DTU_SERIAL_NUMBER] = dtu_sn
        new[CONF_INVERTERS] = single_phase_inverters
        new[CONF_THREE_PHASE_INVERTERS] = three_phase_inverters
        new[CONF_PORTS] = ports
        new[CONF_METERS] = meters
        new[CONF_HYBRID_INVERTERS] = hybrid_inverters
        new[CONF_IS_ENCRYPTED] = is_encrypted
        new[CONF_ENC_RAND] = enc_rand

        hass.config_entries.async_update_entry(
            config_entry, data=new, version=CONFIG_VERSION
        )
        _LOGGER.info(
            "Migration of entry %s to version %s successful",
            config_entry.entry_id,
            CONFIG_VERSION,
        )

    return True


async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
    """Unload a config entry."""
    unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS)

    return unload_ok


================================================
FILE: custom_components/hoymiles_wifi/binary_sensor.py
================================================
"""Contains binary sensor entities for Hoymiles WiFi integration."""

import dataclasses
from dataclasses import dataclass
import logging

from homeassistant.components.binary_sensor import (
    BinarySensorDeviceClass,
    BinarySensorEntity,
    BinarySensorEntityDescription,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import EntityCategory
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from hoymiles_wifi.dtu import NetworkState

from .const import (
    CONF_DTU_SERIAL_NUMBER,
    DOMAIN,
    HASS_DATA_COORDINATOR,
    HASS_ENERGY_STORAGE_DATA_COORDINATOR,
)
from .entity import HoymilesCoordinatorEntity, HoymilesEntityDescription

_LOGGER = logging.getLogger(__name__)


@dataclass(frozen=True)
class HoymilesBinarySensorEntityDescription(
    HoymilesEntityDescription, BinarySensorEntityDescription
):
    """Describes Homiles binary sensor entity."""


BINARY_SENSORS = (
    HoymilesBinarySensorEntityDescription(
        key="DTU",
        translation_key="dtu",
        device_class=BinarySensorDeviceClass.CONNECTIVITY,
        entity_category=EntityCategory.DIAGNOSTIC,
        is_dtu_sensor=True,
    ),
)


async def async_setup_entry(
    hass: HomeAssistant,
    config_entry: ConfigEntry,
    async_add_entities: AddEntitiesCallback,
) -> None:
    """Set up sensor platform."""
    hass_data = hass.data[DOMAIN][config_entry.entry_id]
    coordinator = hass_data.get(HASS_DATA_COORDINATOR, None)
    if coordinator is None:
        coordinator = hass_data.get(HASS_ENERGY_STORAGE_DATA_COORDINATOR, None)

    dtu_serial_number = config_entry.data[CONF_DTU_SERIAL_NUMBER]

    hass_data = hass.data[DOMAIN][config_entry.entry_id]

    sensors = []

    for description in BINARY_SENSORS:
        updated_description = dataclasses.replace(
            description, serial_number=dtu_serial_number
        )
        sensors.append(
            HoymilesInverterSensorEntity(config_entry, updated_description, coordinator)
        )

    async_add_entities(sensors)


class HoymilesInverterSensorEntity(HoymilesCoordinatorEntity, BinarySensorEntity):
    """Represents a binary sensor entity for Hoymiles WiFi integration."""

    def __init__(
        self,
        config_entry: ConfigEntry,
        description: HoymilesBinarySensorEntityDescription,
        coordinator: HoymilesCoordinatorEntity,
    ):
        """Initialize the HoymilesInverterSensorEntity."""
        super().__init__(config_entry, description, coordinator)
        self._dtu = coordinator.get_dtu()
        self._native_value = None

        self.update_state_value()

    @callback
    def _handle_coordinator_update(self) -> None:
        """Handle updated data from the coordinator."""
        self.update_state_value()
        super()._handle_coordinator_update()

    @property
    def is_on(self):
        """Return the state of the binary sensor."""
        return self._native_value

    def update_state_value(self):
        """Update the state value of the binary sensor based on the DTU's network state."""
        dtu_state = self._dtu.get_state()
        if dtu_state == NetworkState.Online:
            self._native_value = True
        elif dtu_state == NetworkState.Offline:
            self._native_value = False
        else:
            self._native_value = None


================================================
FILE: custom_components/hoymiles_wifi/button.py
================================================
"""Support for Hoymiles buttons."""

import dataclasses
from inspect import signature
from dataclasses import dataclass

from homeassistant.components.button import (
    ButtonDeviceClass,
    ButtonEntity,
    ButtonEntityDescription,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from hoymiles_wifi.dtu import DTU

from .const import (
    CONF_DTU_SERIAL_NUMBER,
    CONF_INVERTERS,
    CONF_THREE_PHASE_INVERTERS,
    DOMAIN,
    HASS_DTU,
)
from .entity import HoymilesEntity, HoymilesEntityDescription


@dataclass(frozen=True)
class HoymilesButtonEntityDescription(
    HoymilesEntityDescription, ButtonEntityDescription
):
    """Class to describe a Hoymiles Button entity."""

    action: str = ""


BUTTONS: tuple[HoymilesButtonEntityDescription, ...] = (
    HoymilesButtonEntityDescription(
        key="restart_dtu",
        translation_key="restart",
        device_class=ButtonDeviceClass.RESTART,
        is_dtu_sensor=True,
        action="async_restart_dtu",
    ),
    HoymilesButtonEntityDescription(
        key="turn_off_inverter_<inverter_serial>",
        translation_key="turn_off",
        icon="mdi:power-off",
        action="async_turn_off_inverter",
    ),
    HoymilesButtonEntityDescription(
        key="turn_on_inverter_<inverter_serial>",
        translation_key="turn_on",
        icon="mdi:power-on",
        action="async_turn_on_inverter",
    ),
    HoymilesButtonEntityDescription(
        key="reboot_inverter_<inverter_serial>",
        translation_key="restart",
        icon="mdi:restart",
        action="async_reboot_inverter",
    ),
    HoymilesButtonEntityDescription(
        key="enable_performance_data_mode",
        translation_key="enable_performance_data_mode",
        is_dtu_sensor=True,
        action="async_enable_performance_data_mode",
    ),
)


async def async_setup_entry(
    hass: HomeAssistant,
    config_entry: ConfigEntry,
    async_add_entities: AddEntitiesCallback,
) -> None:
    """Set up the Hoymiles number entities."""
    hass_data = hass.data[DOMAIN][config_entry.entry_id]
    dtu = hass_data[HASS_DTU]
    dtu_serial_number = config_entry.data[CONF_DTU_SERIAL_NUMBER]
    single_phase_inverters = config_entry.data.get(CONF_INVERTERS, [])
    three_phase_inverters = config_entry.data.get(CONF_THREE_PHASE_INVERTERS, [])
    inverters = single_phase_inverters + three_phase_inverters

    if inverters:
        buttons = []
        for description in BUTTONS:
            if description.is_dtu_sensor is True:
                updated_description = dataclasses.replace(
                    description, serial_number=dtu_serial_number
                )
                buttons.append(
                    HoymilesButtonEntity(config_entry, updated_description, dtu)
                )
            else:
                for inverter_serial in inverters:
                    new_key = description.key.replace(
                        "<inverter_serial>", inverter_serial
                    )
                    updated_description = dataclasses.replace(
                        description, key=new_key, serial_number=inverter_serial
                    )
                    buttons.append(
                        HoymilesButtonEntity(config_entry, updated_description, dtu)
                    )
        async_add_entities(buttons)


class HoymilesButtonEntity(HoymilesEntity, ButtonEntity):
    """Hoymiles Number entity."""

    def __init__(
        self,
        config_entry: ConfigEntry,
        description: HoymilesButtonEntityDescription,
        dtu: DTU,
    ) -> None:
        """Initialize the HoymilesButtonEntity."""
        super().__init__(config_entry, description)
        self._dtu = dtu

    async def async_press(self) -> None:
        """Press the button."""

        if hasattr(self._dtu, self.entity_description.action) and callable(
            getattr(self._dtu, self.entity_description.action)
        ):
            method = getattr(self._dtu, self.entity_description.action)
            method_signature = signature(method)
            params = method_signature.parameters
            if "inverter_serial" in params:
                await method(self.entity_description.serial_number)
            else:
                await method()
        else:
            raise NotImplementedError(
                f"Method '{self.entity_description.action}' not implemented in DTU class."
            )


================================================
FILE: custom_components/hoymiles_wifi/config_flow.py
================================================
"""Config flow for Hoymiles."""

from datetime import timedelta
import logging
from typing import Any

import voluptuous as vol

from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
from homeassistant.const import CONF_HOST
from homeassistant.data_entry_flow import FlowResult

from .const import (
    CONF_DTU_SERIAL_NUMBER,
    CONF_HYBRID_INVERTERS,
    CONF_INVERTERS,
    CONF_METERS,
    CONF_PORTS,
    CONF_THREE_PHASE_INVERTERS,
    CONF_TIMEOUT,
    CONF_UPDATE_INTERVAL,
    CONF_IS_ENCRYPTED,
    CONF_ENC_RAND,
    CONFIG_VERSION,
    DEFAULT_TIMEOUT_SECONDS,
    DEFAULT_UPDATE_INTERVAL_SECONDS,
    DOMAIN,
    MIN_UPDATE_INTERVAL_SECONDS,
    MIN_TIMEOUT_SECONDS,
)
from .error import CannotConnect
from .util import async_get_config_entry_data_for_host

_LOGGER = logging.getLogger(__name__)

DATA_SCHEMA = vol.Schema(
    {
        vol.Required(CONF_HOST): str,
        vol.Optional(
            CONF_UPDATE_INTERVAL,
            default=timedelta(seconds=DEFAULT_UPDATE_INTERVAL_SECONDS).seconds,
        ): vol.All(
            vol.Coerce(int),
            vol.Range(min=timedelta(seconds=MIN_UPDATE_INTERVAL_SECONDS).seconds),
        ),
        vol.Optional(
            CONF_TIMEOUT,
            default=timedelta(seconds=DEFAULT_TIMEOUT_SECONDS).seconds,
        ): vol.All(
            vol.Coerce(int),
            vol.Range(min=timedelta(seconds=MIN_TIMEOUT_SECONDS).seconds),
        ),
    }
)


class HoymilesInverterConfigFlowHandler(ConfigFlow, domain=DOMAIN):
    """Hoymiles Inverter config flow."""

    VERSION = CONFIG_VERSION

    async def async_step_user(
        self, user_input: dict[str, Any] | None = None
    ) -> FlowResult:
        """Handle a flow initiated by the user."""
        errors = {}

        if user_input is not None:
            host = user_input[CONF_HOST]
            update_interval = user_input.get(
                CONF_UPDATE_INTERVAL, DEFAULT_UPDATE_INTERVAL_SECONDS
            )
            timeout = user_input.get(CONF_TIMEOUT, DEFAULT_TIMEOUT_SECONDS)

            try:
                (
                    dtu_sn,
                    single_phase_inverters,
                    three_phase_inverters,
                    ports,
                    meters,
                    hybrid_inverters,
                    is_encrypted,
                    enc_rand,
                ) = await async_get_config_entry_data_for_host(host)
            except CannotConnect:
                errors["base"] = "cannot_connect"
            else:
                await self.async_set_unique_id(dtu_sn)
                self._abort_if_unique_id_configured()

                return self.async_create_entry(
                    title=host,
                    data={
                        CONF_HOST: host,
                        CONF_UPDATE_INTERVAL: update_interval,
                        CONF_DTU_SERIAL_NUMBER: dtu_sn,
                        CONF_INVERTERS: single_phase_inverters,
                        CONF_THREE_PHASE_INVERTERS: three_phase_inverters,
                        CONF_PORTS: ports,
                        CONF_METERS: meters,
                        CONF_HYBRID_INVERTERS: hybrid_inverters,
                        CONF_IS_ENCRYPTED: is_encrypted,
                        CONF_ENC_RAND: enc_rand,
                        CONF_TIMEOUT: timeout,
                    },
                )

        return self.async_show_form(
            step_id="user", data_schema=DATA_SCHEMA, errors=errors
        )

    async def async_step_reconfigure(
        self, user_input: dict[str, Any] | None = None
    ) -> ConfigFlowResult:
        """Handle a reconfiguration flow initialized by the user."""

        entry = self.hass.config_entries.async_get_entry(self.context["entry_id"])
        assert entry is not None

        errors = {}

        if user_input is not None:
            host = user_input[CONF_HOST]
            update_interval = user_input.get(
                CONF_UPDATE_INTERVAL, DEFAULT_UPDATE_INTERVAL_SECONDS
            )

            timeout = user_input.get(CONF_TIMEOUT, DEFAULT_TIMEOUT_SECONDS)

            try:
                (
                    dtu_sn,
                    single_phase_inverters,
                    three_phase_inverters,
                    ports,
                    meters,
                    hybrid_inverters,
                    is_encrypted,
                    enc_rand,
                ) = await async_get_config_entry_data_for_host(host)
            except CannotConnect:
                errors["base"] = "cannot_connect"

            else:
                if dtu_sn != entry.unique_id:
                    return self.async_abort(reason="another_device")

                data = {
                    CONF_HOST: host,
                    CONF_UPDATE_INTERVAL: update_interval,
                    CONF_DTU_SERIAL_NUMBER: dtu_sn,
                    CONF_INVERTERS: single_phase_inverters,
                    CONF_THREE_PHASE_INVERTERS: three_phase_inverters,
                    CONF_PORTS: ports,
                    CONF_METERS: meters,
                    CONF_HYBRID_INVERTERS: hybrid_inverters,
                    CONF_IS_ENCRYPTED: is_encrypted,
                    CONF_ENC_RAND: enc_rand,
                    CONF_TIMEOUT: timeout,
                }

                self.hass.config_entries.async_update_entry(
                    entry, data=data, version=CONFIG_VERSION
                )
                result = await self.hass.config_entries.async_reload(entry.entry_id)
                if not result:
                    errors["base"] = "unknown"
                else:
                    return self.async_abort(reason="reconfigure_successful")

        return self.async_show_form(
            step_id="reconfigure",
            data_schema=vol.Schema(
                {
                    vol.Required(CONF_HOST, default=entry.data[CONF_HOST]): str,
                    vol.Optional(
                        CONF_UPDATE_INTERVAL,
                        default=entry.data[CONF_UPDATE_INTERVAL],
                    ): vol.All(
                        vol.Coerce(int),
                        vol.Range(
                            min=timedelta(seconds=MIN_UPDATE_INTERVAL_SECONDS).seconds
                        ),
                    ),
                    vol.Optional(
                        CONF_TIMEOUT,
                        default=entry.data.get(CONF_TIMEOUT, DEFAULT_TIMEOUT_SECONDS),
                    ): vol.All(
                        vol.Coerce(int),
                        vol.Range(min=timedelta(seconds=MIN_TIMEOUT_SECONDS).seconds),
                    ),
                }
            ),
            errors=errors,
        )


================================================
FILE: custom_components/hoymiles_wifi/const.py
================================================
"""Constants for the Hoymiles integration."""

DOMAIN = "hoymiles_wifi"
NAME = "Hoymiles"
DOMAIN = "hoymiles_wifi"
DOMAIN_DATA = f"{DOMAIN}_data"
CONFIG_VERSION = 5

ISSUE_URL = "https://github.com/suaveolent/ha-hoymiles-wifi/issues"

CONF_UPDATE_INTERVAL = "update_interval"
CONF_DTU_SERIAL_NUMBER = "dtu_serial_number"
CONF_INVERTERS = "inverters"
CONF_THREE_PHASE_INVERTERS = "three_phase_inverters"
CONF_HYBRID_INVERTERS = "hybrid_inverters"
CONF_PORTS = "ports"
CONF_METERS = "meters"
CONF_IS_ENCRYPTED = "is_encrypted"
CONF_ENC_RAND = "enc_rand"
CONF_TIMEOUT = "timeout"

DEFAULT_UPDATE_INTERVAL_SECONDS = 35
MIN_UPDATE_INTERVAL_SECONDS = 1
DEFAULT_TIMEOUT_SECONDS = 10
MIN_TIMEOUT_SECONDS = 1

DEFAULT_CONFIG_UPDATE_INTERVAL_SECONDS = 60 * 5
DEFAULT_APP_INFO_UPDATE_INTERVAL_SECONDS = 60 * 60 * 2


HASS_DATA_COORDINATOR = "data_coordinator"
HASS_CONFIG_COORDINATOR = "config_coordinator"
HASS_APP_INFO_COORDINATOR = "app_info_coordinator"
HASS_ENERGY_STORAGE_DATA_COORDINATOR = "energy_stroage_data_coordinator"
HASS_DTU = "dtu"
HASS_DATA_UNSUB_OPTIONS_UPDATE_LISTENER = "unsub_options_update_listener"


FCTN_GENERATE_DTU_VERSION_STRING = "generate_dtu_version_string"
FCTN_GENERATE_INVERTER_HW_VERSION_STRING = "generate_version_string"
FCTN_GENERATE_INVERTER_SW_VERSION_STRING = "generate_sw_version_string"

STARTUP_MESSAGE = f"""

-------------------------------------------------------------------
{NAME}
This is a custom integration!
If you have any issues with it please open an issue here:
{ISSUE_URL}
-------------------------------------------------------------------
"""


================================================
FILE: custom_components/hoymiles_wifi/coordinator.py
================================================
"""Coordinator for Hoymiles integration."""

from datetime import timedelta
import logging

import homeassistant
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_HOST, Platform
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
from hoymiles_wifi.dtu import DTU
from .util import is_encrypted_dtu, async_check_and_update_enc_rand


from .const import DOMAIN

_LOGGER = logging.getLogger(__name__)

PLATFORMS = [Platform.SENSOR, Platform.NUMBER, Platform.BINARY_SENSOR, Platform.BUTTON]


class HoymilesDataUpdateCoordinator(DataUpdateCoordinator):
    """Base data update coordinator for Hoymiles integration."""

    def __init__(
        self,
        hass: homeassistant,
        dtu: DTU,
        config_entry: ConfigEntry,
        update_interval: timedelta,
    ) -> None:
        """Initialize the HoymilesCoordinatorEntity."""
        self._dtu = dtu
        self._hass = hass
        self._config_entry = config_entry

        _LOGGER.debug(
            "Setup entry with update interval %s. IP: %s",
            update_interval,
            config_entry.data.get(CONF_HOST),
        )

        super().__init__(hass, _LOGGER, name=DOMAIN, update_interval=update_interval)

    def get_dtu(self) -> DTU:
        """Get the DTU object."""
        return self._dtu


class HoymilesRealDataUpdateCoordinator(HoymilesDataUpdateCoordinator):
    """Data coordinator for Hoymiles integration."""

    async def _async_update_data(self):
        """Update data via library."""
        _LOGGER.debug("Hoymiles data coordinator update")

        response = await self._dtu.async_get_real_data_new()

        if not response:
            _LOGGER.debug(
                "Unable to retrieve real data new. Inverter might be offline."
            )
        return response


class HoymilesConfigUpdateCoordinator(HoymilesDataUpdateCoordinator):
    """Config coordinator for Hoymiles integration."""

    async def _async_update_data(self):
        """Update data via library."""
        _LOGGER.debug("Hoymiles data coordinator update")

        response = await self._dtu.async_get_config()

        if not response:
            _LOGGER.debug("Unable to retrieve config data. Inverter might be offline.")

        return response


class HoymilesAppInfoUpdateCoordinator(HoymilesDataUpdateCoordinator):
    """App Info coordinator for Hoymiles integration."""

    async def _async_update_data(self):
        """Update data via library."""
        _LOGGER.debug("Hoymiles data coordinator update")

        response = await self._dtu.async_app_information_data()

        if response and response.dtu_info.dfs:
            if is_encrypted_dtu(response.dtu_info.dfs):
                await async_check_and_update_enc_rand(
                    self._hass,
                    self._config_entry,
                    self._dtu,
                    response.dtu_info.enc_rand.hex(),
                )

        if not response:
            _LOGGER.debug(
                "Unable to retrieve app information data. Inverter might be offline."
            )
        return response


class HoymilesGatewayInfoUpdateCoordinator(HoymilesDataUpdateCoordinator):
    """Gateway Info coordinator for Hoymiles integration."""

    async def _async_update_data(self):
        """Update data via library."""
        _LOGGER.debug("Hoymiles gateway info coordinator update")

        response = await self._dtu.async_get_gateway_info()

        if not response:
            _LOGGER.debug("Unable to retrieve gateway info. Inverter might be offline.")
        return response


class HoymilesGatewayNetworkInfoUpdateCoordinator(HoymilesDataUpdateCoordinator):
    """Gateway Network Info coordinator for Hoymiles integration."""

    async def _async_update_data(self):
        """Update data via library."""
        _LOGGER.debug("Hoymiles network info coordinator update")

        response = await self._dtu.async_get_gateway_network_info(
            dtu_serial_number=int(self._dtu_serial_number)
        )

        if not response:
            _LOGGER.debug(
                "Unable to retrieve network information. Inverter might be offline."
            )
        return response


class HoymilesEnergyStorageUpdateCoordinator(HoymilesDataUpdateCoordinator):
    """Energy Storage Update coordinator for Hoymiles integration."""

    def __init__(
        self,
        hass: homeassistant,
        dtu: DTU,
        config_entry: ConfigEntry,
        update_interval: timedelta,
        dtu_serial_number: int,
        inverters: list[int],
    ) -> None:
        self._dtu_serial_number = dtu_serial_number
        self._inverters = inverters
        super().__init__(hass, dtu, config_entry, update_interval)

    async def _async_update_data(self):
        """Update data via library."""
        _LOGGER.debug("Hoymiles energy storage coordinator update")

        responses = []

        for inverter in self._inverters:
            storage_data = await self._dtu.async_get_energy_storage_data(
                dtu_serial_number=int(self._dtu_serial_number),
                inverter_serial_number=inverter["inverter_serial_number"],
            )
            if storage_data is not None:
                responses.append(storage_data)

        if not responses:
            _LOGGER.debug(
                "Unable to retrieve energy storage data. Inverter might be offline."
            )
        return responses


================================================
FILE: custom_components/hoymiles_wifi/entity.py
================================================
"""Entity base for Hoymiles entities."""

from dataclasses import dataclass
import logging

from enum import Enum

from homeassistant.config_entries import ConfigEntry
from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.entity import Entity, EntityDescription
from homeassistant.helpers.update_coordinator import CoordinatorEntity
from hoymiles_wifi.hoymiles import (
    DTUType,
    get_dtu_model_name,
    get_inverter_model_name,
    get_meter_model_name,
)

from .const import CONF_DTU_SERIAL_NUMBER, DOMAIN
from .coordinator import (
    HoymilesDataUpdateCoordinator,
)

_LOGGER = logging.getLogger(__name__)


class DeviceType(Enum):
    """Device type."""

    ALL_DEVICES = 0
    SINGLE_PHASE_METER = 1
    THREE_PHASE_METER = 3


@dataclass(frozen=True)
class HoymilesEntityDescription(EntityDescription):
    """Class to describe a Hoymiles Button entity."""

    is_dtu_sensor: bool = False
    serial_number: str = None
    port_number: int = None
    supported_dtu_types: list[DTUType] = None
    phase: str = None


class HoymilesEntity(Entity):
    """Base class for Hoymiles entities."""

    _attr_has_entity_name = True

    def __init__(self, config_entry: ConfigEntry, description: EntityDescription):
        """Initialize the Hoymiles entity."""
        super().__init__()
        self.entity_description = description
        self._config_entry = config_entry
        self._attr_unique_id = f"hoymiles_{config_entry.entry_id}_{description.key}"

        if description.port_number:
            self._attr_translation_placeholders = {
                "port_number": f"{description.port_number}"
            }
        if description.phase:
            self._attr_translation_placeholders = {"phase": f"{description.phase}"}

        dtu_serial_number = config_entry.data[CONF_DTU_SERIAL_NUMBER]

        serial_number = str(self.entity_description.serial_number)

        if self.entity_description.is_dtu_sensor is True:
            device_translation_key = "dtu"
            device_model = get_dtu_model_name(self.entity_description.serial_number)
        else:
            if "meter" in self.entity_description.key:
                device_model = get_meter_model_name(
                    self.entity_description.serial_number
                )
                device_translation_key = "meter"
            else:
                if (
                    hasattr(self.entity_description, "model_name")
                    and self.entity_description.model_name
                ):
                    device_model = self.entity_description.model_name
                    device_translation_key = "hybrid_inverter"
                else:
                    device_model = get_inverter_model_name(
                        self.entity_description.serial_number
                    )
                    device_translation_key = "inverter"

        device_info = DeviceInfo(
            identifiers={(DOMAIN, serial_number)},
            translation_key=device_translation_key,
            manufacturer="Hoymiles",
            serial_number=serial_number.upper(),
            model=device_model,
        )

        if not self.entity_description.is_dtu_sensor:
            device_info["via_device"] = (DOMAIN, dtu_serial_number)

        self._attr_device_info = device_info


class HoymilesCoordinatorEntity(CoordinatorEntity, HoymilesEntity):
    """Represents a Hoymiles coordinator entity."""

    def __init__(
        self,
        config_entry: ConfigEntry,
        description: EntityDescription,
        coordinator: HoymilesDataUpdateCoordinator,
    ):
        """Pass coordinator to CoordinatorEntity."""
        CoordinatorEntity.__init__(self, coordinator)
        HoymilesEntity.__init__(self, config_entry, description)


================================================
FILE: custom_components/hoymiles_wifi/error.py
================================================
"""Errors for hoymiles-wifi."""

from homeassistant.exceptions import HomeAssistantError


class CannotConnect(HomeAssistantError):
    """Error to indicate we cannot connect."""


================================================
FILE: custom_components/hoymiles_wifi/manifest.json
================================================
{
  "codeowners": ["@suaveolent"],
  "config_flow": true,
  "dependencies": [],
  "documentation": "https://github.com/suaveolent/ha-hoymiles-wifi",
  "issue_tracker": "https://github.com/suaveolent/ha-hoymiles-wifi/issues",
  "domain": "hoymiles_wifi",
  "iot_class": "local_polling",
  "name": "Hoymiles",
  "requirements": ["hoymiles-wifi==0.5.5"],
  "version": "0.5.0"
}


================================================
FILE: custom_components/hoymiles_wifi/number.py
================================================
"""Support for Hoymiles number sensors."""

import dataclasses
from dataclasses import dataclass
from enum import Enum
import logging

from homeassistant.components.number import (
    NumberDeviceClass,
    NumberEntity,
    NumberEntityDescription,
    NumberMode,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.entity_platform import AddEntitiesCallback

from .const import (
    CONF_DTU_SERIAL_NUMBER,
    CONF_INVERTERS,
    CONF_THREE_PHASE_INVERTERS,
    DOMAIN,
    HASS_CONFIG_COORDINATOR,
)
from .entity import HoymilesCoordinatorEntity, HoymilesEntityDescription

from hoymiles_wifi.hoymiles import DTUType, get_dtu_model_type


class SetAction(Enum):
    """Enum for set actions."""

    POWER_LIMIT = 1


@dataclass(frozen=True)
class HoymilesNumberSensorEntityDescriptionMixin:
    """Mixin for required keys."""


@dataclass(frozen=True)
class HoymilesNumberSensorEntityDescription(
    HoymilesEntityDescription, NumberEntityDescription
):
    """Describes Hoymiles number sensor entity."""

    set_action: SetAction = None
    conversion_factor: float = None
    serial_number: str = None
    is_dtu_sensor: bool = False


CONFIG_CONTROL_ENTITIES = (
    HoymilesNumberSensorEntityDescription(
        key="limit_power_mypower",
        translation_key="limit_power_mypower",
        mode=NumberMode.SLIDER,
        device_class=NumberDeviceClass.POWER_FACTOR,
        set_action=SetAction.POWER_LIMIT,
        conversion_factor=0.1,
        is_dtu_sensor=True,
    ),
)

_LOGGER = logging.getLogger(__name__)


async def async_setup_entry(
    hass: HomeAssistant,
    config_entry: ConfigEntry,
    async_add_entities: AddEntitiesCallback,
) -> None:
    """Set up the Hoymiles number entities."""
    hass_data = hass.data[DOMAIN][config_entry.entry_id]
    config_coordinator = hass_data.get(HASS_CONFIG_COORDINATOR, None)
    single_phase_inverters = config_entry.data.get(CONF_INVERTERS, [])
    three_phase_inverters = config_entry.data.get(CONF_THREE_PHASE_INVERTERS, [])
    dtu_serial_number = config_entry.data[CONF_DTU_SERIAL_NUMBER]

    if single_phase_inverters or three_phase_inverters:
        sensors = []
        for description in CONFIG_CONTROL_ENTITIES:
            if description.is_dtu_sensor is True:
                updated_description = dataclasses.replace(
                    description, serial_number=dtu_serial_number
                )
                sensors.append(
                    HoymilesNumberEntity(
                        config_entry, updated_description, config_coordinator
                    )
                )
        async_add_entities(sensors)


class HoymilesNumberEntity(HoymilesCoordinatorEntity, NumberEntity):
    """Hoymiles Number entity."""

    def __init__(
        self,
        config_entry: ConfigEntry,
        description: HoymilesNumberSensorEntityDescription,
        coordinator: HoymilesCoordinatorEntity,
    ) -> None:
        """Initialize the HoymilesNumberEntity."""
        super().__init__(config_entry, description, coordinator)
        self._attribute_name = description.key
        self._conversion_factor = description.conversion_factor
        self._set_action = description.set_action
        self._native_value = None
        self._assumed_state = False

        self.update_state_value()

    @callback
    def _handle_coordinator_update(self) -> None:
        """Handle updated data from the coordinator."""
        self.update_state_value()
        super()._handle_coordinator_update()

    @property
    def native_value(self) -> float:
        """Get the native value of the entity."""
        return self._native_value

    @property
    def assumed_state(self):
        """Return the assumed state of the entity."""
        return self._assumed_state

    async def async_set_native_value(self, value: float) -> None:
        """Set the native value of the entity.

        Args:
            value (float): The value to set.
        """
        if self._set_action == SetAction.POWER_LIMIT:
            dtu = self.coordinator.get_dtu()
            if value < 0 and value > 100:
                _LOGGER.error("Power limit value out of range")
                return
            await dtu.async_set_power_limit(value)
            await self.coordinator.async_request_refresh()
        else:
            _LOGGER.error("Invalid set action!")
            return

        self._assumed_state = True
        self._native_value = value

    def update_state_value(self):
        """Update the state value of the entity."""

        # For the moment, we can only retrive the power limit
        self._native_value = getattr(
            self.coordinator.data,
            self._attribute_name,
            None,
        )

        self._assumed_state = False

        if self._native_value is not None and self._conversion_factor is not None:
            self._native_value *= self._conversion_factor


================================================
FILE: custom_components/hoymiles_wifi/sensor.py
================================================
"""Support for Hoymiles sensors."""

import dataclasses
from dataclasses import dataclass
from datetime import datetime, time, timedelta
from enum import Enum
import logging
import re

from homeassistant.components.sensor import (
    RestoreSensor,
    SensorDeviceClass,
    SensorEntity,
    SensorEntityDescription,
    SensorStateClass,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import (
    PERCENTAGE,
    EntityCategory,
    UnitOfElectricCurrent,
    UnitOfElectricPotential,
    UnitOfEnergy,
    UnitOfFrequency,
    UnitOfPower,
    UnitOfTemperature,
    UnitOfReactivePower,
)
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.entity_platform import AddEntitiesCallback
import hoymiles_wifi.hoymiles
from hoymiles_wifi.hoymiles import DTUType, get_dtu_model_type

from .const import (
    CONF_DTU_SERIAL_NUMBER,
    CONF_INVERTERS,
    CONF_HYBRID_INVERTERS,
    CONF_METERS,
    CONF_PORTS,
    CONF_THREE_PHASE_INVERTERS,
    DOMAIN,
    FCTN_GENERATE_DTU_VERSION_STRING,
    FCTN_GENERATE_INVERTER_HW_VERSION_STRING,
    FCTN_GENERATE_INVERTER_SW_VERSION_STRING,
    HASS_APP_INFO_COORDINATOR,
    HASS_CONFIG_COORDINATOR,
    HASS_DATA_COORDINATOR,
    HASS_ENERGY_STORAGE_DATA_COORDINATOR,
)
from .entity import (
    HoymilesCoordinatorEntity,
    HoymilesEntityDescription,
    DeviceType,
)

_LOGGER = logging.getLogger(__name__)


class ConversionAction(Enum):
    """Enumeration for conversion actions."""

    HEX = 1


@dataclass(frozen=True)
class HoymilesSensorEntityDescriptionMixin:
    """Mixin for required keys."""


@dataclass(frozen=True)
class HoymilesSensorEntityDescription(
    HoymilesEntityDescription, SensorEntityDescription
):
    """Describes Hoymiles data sensor entity."""

    conversion_factor: float = None
    reset_at_midnight: bool = False
    version_translation_function: str = None
    version_prefix: str = None
    assume_state: bool = False
    requires_device_type: int = DeviceType.ALL_DEVICES
    force_keep_maximum_within_day: bool = False


@dataclass(frozen=True)
class HoymilesEnergyStorageSensorEntityDescription(
    HoymilesEntityDescription, SensorEntityDescription
):
    """Describes Hoymiles energy storage data sensor entity."""

    model_name: str = None
    conversion_factor: float = None
    reset_at_midnight: bool = False
    version_translation_function: str = None
    version_prefix: str = None
    assume_state: bool = False
    force_keep_maximum_within_day: bool = False


@dataclass(frozen=True)
class HoymilesDiagnosticEntityDescription(
    HoymilesEntityDescription, SensorEntityDescription
):
    """Describes Hoymiles diagnostic sensor entity."""

    conversion: ConversionAction = None
    separator: str = None


HOYMILES_SENSORS = [
    HoymilesSensorEntityDescription(
        key="dtu_power",
        translation_key="ac_active_power",
        native_unit_of_measurement=UnitOfPower.WATT,
        device_class=SensorDeviceClass.POWER,
        state_class=SensorStateClass.MEASUREMENT,
        conversion_factor=0.1,
        is_dtu_sensor=True,
        supported_dtu_types=[
            DTUType.DTU_G100,
            DTUType.DTU_W100,
            DTUType.DTU_LITE_S,
            DTUType.DTU_LITE,
            DTUType.DTU_PRO,
            DTUType.DTU_PRO_S,
            DTUType.DTUBI,
            DTUType.DTU_W_LITE,
        ],
    ),
    HoymilesSensorEntityDescription(
        key="dtu_daily_energy",
        translation_key="ac_daily_energy",
        native_unit_of_measurement=UnitOfEnergy.WATT_HOUR,
        device_class=SensorDeviceClass.ENERGY,
        state_class=SensorStateClass.TOTAL_INCREASING,
        reset_at_midnight=True,
        force_keep_maximum_within_day=True,
        is_dtu_sensor=True,
        supported_dtu_types=[
            DTUType.DTU_G100,
            DTUType.DTU_W100,
            DTUType.DTU_LITE_S,
            DTUType.DTU_LITE,
            DTUType.DTU_PRO,
            DTUType.DTU_PRO_S,
            DTUType.DTUBI,
            DTUType.DTU_W_LITE,
        ],
    ),
    HoymilesSensorEntityDescription(
        key="sgs_data[<inverter_count>].active_power",
        translation_key="ac_active_power",
        native_unit_of_measurement=UnitOfPower.WATT,
        device_class=SensorDeviceClass.POWER,
        state_class=SensorStateClass.MEASUREMENT,
        conversion_factor=0.1,
    ),
    HoymilesSensorEntityDescription(
        key="sgs_data[<inverter_count>].reactive_power",
        translation_key="ac_reactive_power",
        native_unit_of_measurement=UnitOfReactivePower.VOLT_AMPERE_REACTIVE,
        device_class=SensorDeviceClass.REACTIVE_POWER,
        state_class=SensorStateClass.MEASUREMENT,
        conversion_factor=0.1,
    ),
    HoymilesSensorEntityDescription(
        key="sgs_data[<inverter_count>].voltage",
        translation_key="grid_voltage",
        native_unit_of_measurement=UnitOfElectricPotential.VOLT,
        device_class=SensorDeviceClass.VOLTAGE,
        state_class=SensorStateClass.MEASUREMENT,
        conversion_factor=0.1,
    ),
    HoymilesSensorEntityDescription(
        key="sgs_data[<inverter_count>].current",
        translation_key="ac_current",
        native_unit_of_measurement=UnitOfElectricCurrent.AMPERE,
        device_class=SensorDeviceClass.CURRENT,
        state_class=SensorStateClass.MEASUREMENT,
        conversion_factor=0.01,
    ),
    HoymilesSensorEntityDescription(
        key="sgs_data[<inverter_count>].frequency",
        translation_key="grid_frequency",
        native_unit_of_measurement=UnitOfFrequency.HERTZ,
        device_class=SensorDeviceClass.FREQUENCY,
        state_class=SensorStateClass.MEASUREMENT,
        conversion_factor=0.01,
    ),
    HoymilesSensorEntityDescription(
        key="sgs_data[<inverter_count>].power_factor",
        translation_key="inverter_power_factor",
        native_unit_of_measurement=PERCENTAGE,
        device_class=SensorDeviceClass.POWER_FACTOR,
        state_class=SensorStateClass.MEASUREMENT,
        conversion_factor=0.1,
    ),
    HoymilesSensorEntityDescription(
        key="sgs_data[<inverter_count>].temperature",
        translation_key="inverter_temperature",
        native_unit_of_measurement=UnitOfTemperature.CELSIUS,
        device_class=SensorDeviceClass.TEMPERATURE,
        state_class=SensorStateClass.MEASUREMENT,
        conversion_factor=0.1,
    ),
    HoymilesSensorEntityDescription(
        key="sgs_data[<inverter_count>].warning_number",
        translation_key="inverter_warning_number",
    ),
    HoymilesSensorEntityDescription(
        key="tgs_data[<inverter_count>].active_power",
        translation_key="ac_active_power",
        native_unit_of_measurement=UnitOfPower.WATT,
        device_class=SensorDeviceClass.POWER,
        state_class=SensorStateClass.MEASUREMENT,
        conversion_factor=0.1,
    ),
    HoymilesSensorEntityDescription(
        key="tgs_data[<inverter_count>].reactive_power",
        translation_key="ac_reactive_power",
        native_unit_of_measurement=UnitOfReactivePower.VOLT_AMPERE_REACTIVE,
        device_class=SensorDeviceClass.REACTIVE_POWER,
        state_class=SensorStateClass.MEASUREMENT,
        conversion_factor=0.1,
    ),
    HoymilesSensorEntityDescription(
        key="tgs_data[<inverter_count>].voltage_phase_A",
        translation_key="voltage_phase_A",
        native_unit_of_measurement=UnitOfElectricPotential.VOLT,
        device_class=SensorDeviceClass.VOLTAGE,
        state_class=SensorStateClass.MEASUREMENT,
        conversion_factor=0.1,
    ),
    HoymilesSensorEntityDescription(
        key="tgs_data[<inverter_count>].voltage_phase_B",
        translation_key="voltage_phase_B",
        native_unit_of_measurement=UnitOfElectricPotential.VOLT,
        device_class=SensorDeviceClass.VOLTAGE,
        state_class=SensorStateClass.MEASUREMENT,
        conversion_factor=0.1,
    ),
    HoymilesSensorEntityDescription(
        key="tgs_data[<inverter_count>].voltage_phase_C",
        translation_key="voltage_phase_C",
        native_unit_of_measurement=UnitOfElectricPotential.VOLT,
        device_class=SensorDeviceClass.VOLTAGE,
        state_class=SensorStateClass.MEASUREMENT,
        conversion_factor=0.1,
    ),
    HoymilesSensorEntityDescription(
        key="tgs_data[<inverter_count>].voltage_line_AB",
        translation_key="voltage_line_AB",
        native_unit_of_measurement=UnitOfElectricPotential.VOLT,
        device_class=SensorDeviceClass.VOLTAGE,
        state_class=SensorStateClass.MEASUREMENT,
        conversion_factor=0.1,
    ),
    HoymilesSensorEntityDescription(
        key="tgs_data[<inverter_count>].voltage_line_BC",
        translation_key="voltage_line_BC",
        native_unit_of_measurement=UnitOfElectricPotential.VOLT,
        device_class=SensorDeviceClass.VOLTAGE,
        state_class=SensorStateClass.MEASUREMENT,
        conversion_factor=0.1,
    ),
    HoymilesSensorEntityDescription(
        key="tgs_data[<inverter_count>].voltage_line_CA",
        translation_key="voltage_line_CA",
        native_unit_of_measurement=UnitOfElectricPotential.VOLT,
        device_class=SensorDeviceClass.VOLTAGE,
        state_class=SensorStateClass.MEASUREMENT,
        conversion_factor=0.1,
    ),
    HoymilesSensorEntityDescription(
        key="tgs_data[<inverter_count>].frequency",
        translation_key="grid_frequency",
        native_unit_of_measurement=UnitOfFrequency.HERTZ,
        device_class=SensorDeviceClass.FREQUENCY,
        state_class=SensorStateClass.MEASUREMENT,
        conversion_factor=0.01,
    ),
    HoymilesSensorEntityDescription(
        key="tgs_data[<inverter_count>.current_phase_A",
        translation_key="current_phase_A",
        native_unit_of_measurement=UnitOfElectricCurrent.AMPERE,
        device_class=SensorDeviceClass.CURRENT,
        state_class=SensorStateClass.MEASUREMENT,
        conversion_factor=0.01,
    ),
    HoymilesSensorEntityDescription(
        key="tgs_data[<inverter_count>.current_phase_B",
        translation_key="current_phase_B",
        native_unit_of_measurement=UnitOfElectricCurrent.AMPERE,
        device_class=SensorDeviceClass.CURRENT,
        state_class=SensorStateClass.MEASUREMENT,
        conversion_factor=0.01,
    ),
    HoymilesSensorEntityDescription(
        key="tgs_data[<inverter_count>.current_phase_C",
        translation_key="current_phase_C",
        native_unit_of_measurement=UnitOfElectricCurrent.AMPERE,
        device_class=SensorDeviceClass.CURRENT,
        state_class=SensorStateClass.MEASUREMENT,
        conversion_factor=0.01,
    ),
    HoymilesSensorEntityDescription(
        key="tgs_data[<inverter_count>].power_factor",
        translation_key="inverter_power_factor",
        native_unit_of_measurement=PERCENTAGE,
        device_class=SensorDeviceClass.POWER_FACTOR,
        state_class=SensorStateClass.MEASUREMENT,
        conversion_factor=0.1,
    ),
    HoymilesSensorEntityDescription(
        key="tgs_data[<inverter_count>].temperature",
        translation_key="inverter_temperature",
        native_unit_of_measurement=UnitOfTemperature.CELSIUS,
        device_class=SensorDeviceClass.TEMPERATURE,
        state_class=SensorStateClass.MEASUREMENT,
        conversion_factor=0.1,
    ),
    HoymilesSensorEntityDescription(
        key="tgs_data[<inverter_count>].warning_number",
        translation_key="inverter_warning_number",
    ),
    HoymilesSensorEntityDescription(
        key="pv_data[<pv_count>].voltage",
        translation_key="port_dc_voltage",
        native_unit_of_measurement=UnitOfElectricPotential.VOLT,
        device_class=SensorDeviceClass.VOLTAGE,
        state_class=SensorStateClass.MEASUREMENT,
        conversion_factor=0.1,
    ),
    HoymilesSensorEntityDescription(
        key="pv_data[<pv_count>].current",
        translation_key="port_dc_current",
        native_unit_of_measurement=UnitOfElectricCurrent.AMPERE,
        device_class=SensorDeviceClass.CURRENT,
        state_class=SensorStateClass.MEASUREMENT,
        conversion_factor=0.01,
    ),
    HoymilesSensorEntityDescription(
        key="pv_data[<pv_count>].power",
        translation_key="port_dc_power",
        native_unit_of_measurement=UnitOfPower.WATT,
        device_class=SensorDeviceClass.POWER,
        state_class=SensorStateClass.MEASUREMENT,
        conversion_factor=0.1,
    ),
    HoymilesSensorEntityDescription(
        key="pv_data[<pv_count>].energy_total",
        translation_key="port_dc_total_energy",
        native_unit_of_measurement=UnitOfEnergy.WATT_HOUR,
        device_class=SensorDeviceClass.ENERGY,
        state_class=SensorStateClass.TOTAL_INCREASING,
    ),
    HoymilesSensorEntityDescription(
        key="pv_data[<pv_count>].energy_daily",
        translation_key="port_dc_daily_energy",
        native_unit_of_measurement=UnitOfEnergy.WATT_HOUR,
        device_class=SensorDeviceClass.ENERGY,
        state_class=SensorStateClass.TOTAL_INCREASING,
        reset_at_midnight=True,
    ),
    HoymilesSensorEntityDescription(
        key="pv_data[<pv_count>].error_code",
        translation_key="port_error_code",
    ),
    HoymilesSensorEntityDescription(
        key="meter_data[<meter_count>].phase_total_power",
        translation_key="phase_total_power",
        native_unit_of_measurement=UnitOfPower.WATT,
        device_class=SensorDeviceClass.POWER,
        state_class=SensorStateClass.MEASUREMENT,
        conversion_factor=10,
    ),
    HoymilesSensorEntityDescription(
        key="meter_data[<meter_count>].phase_A_power",
        translation_key="phase_A_power",
        native_unit_of_measurement=UnitOfPower.WATT,
        device_class=SensorDeviceClass.POWER,
        state_class=SensorStateClass.MEASUREMENT,
        conversion_factor=10,
    ),
    HoymilesSensorEntityDescription(
        key="meter_data[<meter_count>].phase_B_power",
        translation_key="phase_B_power",
        native_unit_of_measurement=UnitOfPower.WATT,
        device_class=SensorDeviceClass.POWER,
        state_class=SensorStateClass.MEASUREMENT,
        conversion_factor=10,
        requires_device_type=DeviceType.THREE_PHASE_METER,
    ),
    HoymilesSensorEntityDescription(
        key="meter_data[<meter_count>].phase_C_power",
        translation_key="phase_C_power",
        native_unit_of_measurement=UnitOfPower.WATT,
        device_class=SensorDeviceClass.POWER,
        state_class=SensorStateClass.MEASUREMENT,
        conversion_factor=10,
        requires_device_type=DeviceType.THREE_PHASE_METER,
    ),
    HoymilesSensorEntityDescription(
        key="meter_data[<meter_count>].power_factor_total",
        translation_key="power_factor_total",
        native_unit_of_measurement=PERCENTAGE,
        device_class=SensorDeviceClass.POWER_FACTOR,
        state_class=SensorStateClass.MEASUREMENT,
        conversion_factor=0.1,
    ),
    HoymilesSensorEntityDescription(
        key="meter_data[<meter_count>].energy_total_power",
        translation_key="energy_total_power",
        native_unit_of_measurement=UnitOfEnergy.WATT_HOUR,
        device_class=SensorDeviceClass.ENERGY,
        state_class=SensorStateClass.TOTAL_INCREASING,
        conversion_factor=10.0,
    ),
    HoymilesSensorEntityDescription(
        key="meter_data[<meter_count>].energy_phase_A",
        translation_key="energy_phase_A",
        native_unit_of_measurement=UnitOfEnergy.WATT_HOUR,
        device_class=SensorDeviceClass.ENERGY,
        state_class=SensorStateClass.TOTAL_INCREASING,
        conversion_factor=10.0,
    ),
    HoymilesSensorEntityDescription(
        key="meter_data[<meter_count>].energy_phase_B",
        translation_key="energy_phase_B",
        native_unit_of_measurement=UnitOfEnergy.WATT_HOUR,
        device_class=SensorDeviceClass.ENERGY,
        state_class=SensorStateClass.TOTAL_INCREASING,
        requires_device_type=DeviceType.THREE_PHASE_METER,
        conversion_factor=10.0,
    ),
    HoymilesSensorEntityDescription(
        key="meter_data[<meter_count>].energy_phase_C",
        translation_key="energy_phase_C",
        native_unit_of_measurement=UnitOfEnergy.WATT_HOUR,
        device_class=SensorDeviceClass.ENERGY,
        state_class=SensorStateClass.TOTAL_INCREASING,
        requires_device_type=DeviceType.THREE_PHASE_METER,
        conversion_factor=10.0,
    ),
    HoymilesSensorEntityDescription(
        key="meter_data[<meter_count>].energy_total_consumed",
        translation_key="energy_total_consumed",
        native_unit_of_measurement=UnitOfEnergy.WATT_HOUR,
        device_class=SensorDeviceClass.ENERGY,
        state_class=SensorStateClass.TOTAL_INCREASING,
        conversion_factor=10.0,
    ),
    HoymilesSensorEntityDescription(
        key="meter_data[<meter_count>].energy_phase_A_consumed",
        translation_key="energy_phase_A_consumed",
        native_unit_of_measurement=UnitOfEnergy.WATT_HOUR,
        device_class=SensorDeviceClass.ENERGY,
        state_class=SensorStateClass.TOTAL_INCREASING,
        conversion_factor=10.0,
    ),
    HoymilesSensorEntityDescription(
        key="meter_data[<meter_count>].energy_phase_B_consumed",
        translation_key="energy_phase_B_consumed",
        native_unit_of_measurement=UnitOfEnergy.WATT_HOUR,
        device_class=SensorDeviceClass.ENERGY,
        state_class=SensorStateClass.TOTAL_INCREASING,
        requires_device_type=DeviceType.THREE_PHASE_METER,
        conversion_factor=10.0,
    ),
    HoymilesSensorEntityDescription(
        key="meter_data[<meter_count>].energy_phase_C_consumed",
        translation_key="energy_phase_C_consumed",
        native_unit_of_measurement=UnitOfEnergy.WATT_HOUR,
        device_class=SensorDeviceClass.ENERGY,
        state_class=SensorStateClass.TOTAL_INCREASING,
        requires_device_type=DeviceType.THREE_PHASE_METER,
        conversion_factor=10.0,
    ),
    HoymilesSensorEntityDescription(
        key="meter_data[<meter_count>].voltage_phase_A",
        translation_key="voltage_phase_A",
        native_unit_of_measurement=UnitOfElectricPotential.VOLT,
        device_class=SensorDeviceClass.VOLTAGE,
        state_class=SensorStateClass.MEASUREMENT,
        conversion_factor=0.01,
    ),
    HoymilesSensorEntityDescription(
        key="meter_data[<meter_count>].voltage_phase_B",
        translation_key="voltage_phase_B",
        native_unit_of_measurement=UnitOfElectricPotential.VOLT,
        device_class=SensorDeviceClass.VOLTAGE,
        state_class=SensorStateClass.MEASUREMENT,
        conversion_factor=0.01,
        requires_device_type=DeviceType.THREE_PHASE_METER,
    ),
    HoymilesSensorEntityDescription(
        key="meter_data[<meter_count>].voltage_phase_C",
        translation_key="voltage_phase_C",
        native_unit_of_measurement=UnitOfElectricPotential.VOLT,
        device_class=SensorDeviceClass.VOLTAGE,
        state_class=SensorStateClass.MEASUREMENT,
        conversion_factor=0.01,
        requires_device_type=DeviceType.THREE_PHASE_METER,
    ),
    HoymilesSensorEntityDescription(
        key="meter_data[<meter_count>].current_phase_A",
        translation_key="current_phase_A",
        native_unit_of_measurement=UnitOfElectricCurrent.AMPERE,
        device_class=SensorDeviceClass.CURRENT,
        state_class=SensorStateClass.MEASUREMENT,
        conversion_factor=0.01,
    ),
    HoymilesSensorEntityDescription(
        key="meter_data[<meter_count>].current_phase_B",
        translation_key="current_phase_B",
        native_unit_of_measurement=UnitOfElectricCurrent.AMPERE,
        device_class=SensorDeviceClass.CURRENT,
        state_class=SensorStateClass.MEASUREMENT,
        conversion_factor=0.01,
        requires_device_type=DeviceType.THREE_PHASE_METER,
    ),
    HoymilesSensorEntityDescription(
        key="meter_data[<meter_count>].current_phase_C",
        translation_key="current_phase_C",
        native_unit_of_measurement=UnitOfElectricCurrent.AMPERE,
        device_class=SensorDeviceClass.CURRENT,
        state_class=SensorStateClass.MEASUREMENT,
        conversion_factor=0.01,
        requires_device_type=DeviceType.THREE_PHASE_METER,
    ),
    HoymilesSensorEntityDescription(
        key="meter_data[<meter_count>].power_factor_phase_A",
        translation_key="power_factor_phase_A",
        native_unit_of_measurement=PERCENTAGE,
        device_class=SensorDeviceClass.POWER_FACTOR,
        state_class=SensorStateClass.MEASUREMENT,
        conversion_factor=0.1,
    ),
    HoymilesSensorEntityDescription(
        key="meter_data[<meter_count>].power_factor_phase_B",
        translation_key="power_factor_phase_B",
        native_unit_of_measurement=PERCENTAGE,
        device_class=SensorDeviceClass.POWER_FACTOR,
        state_class=SensorStateClass.MEASUREMENT,
        conversion_factor=0.1,
        requires_device_type=DeviceType.THREE_PHASE_METER,
    ),
    HoymilesSensorEntityDescription(
        key="meter_data[<meter_count>].power_factor_phase_C",
        translation_key="power_factor_phase_C",
        native_unit_of_measurement=PERCENTAGE,
        device_class=SensorDeviceClass.POWER_FACTOR,
        state_class=SensorStateClass.MEASUREMENT,
        conversion_factor=0.1,
        requires_device_type=DeviceType.THREE_PHASE_METER,
    ),
]

CONFIG_DIAGNOSTIC_SENSORS = [
    HoymilesDiagnosticEntityDescription(
        key="wifi_ssid",
        translation_key="wifi_ssid",
        entity_category=EntityCategory.DIAGNOSTIC,
        icon="mdi:wifi",
        is_dtu_sensor=True,
    ),
    HoymilesDiagnosticEntityDescription(
        key="meter_kind",
        translation_key="meter_kind",
        entity_category=EntityCategory.DIAGNOSTIC,
        is_dtu_sensor=True,
    ),
    HoymilesDiagnosticEntityDescription(
        key="wifi_mac_[0-5]",
        translation_key="mac_address",
        entity_category=EntityCategory.DIAGNOSTIC,
        separator=":",
        conversion=ConversionAction.HEX,
        is_dtu_sensor=True,
    ),
    HoymilesDiagnosticEntityDescription(
        key="wifi_ip_addr_[0-3]",
        translation_key="ip_address",
        entity_category=EntityCategory.DIAGNOSTIC,
        separator=".",
        is_dtu_sensor=True,
    ),
    HoymilesDiagnosticEntityDescription(
        key="dtu_ap_ssid",
        translation_key="dtu_ap_ssid",
        entity_category=EntityCategory.DIAGNOSTIC,
        icon="mdi:access-point",
        is_dtu_sensor=True,
    ),
]

APP_INFO_SENSORS: tuple[HoymilesSensorEntityDescription, ...] = (
    HoymilesSensorEntityDescription(
        key="dtu_info.dtu_sw_version",
        translation_key="dtu_sw_version",
        entity_category=EntityCategory.DIAGNOSTIC,
        version_translation_function=FCTN_GENERATE_DTU_VERSION_STRING,
        version_prefix="V",
        is_dtu_sensor=True,
        assume_state=True,
    ),
    HoymilesSensorEntityDescription(
        key="dtu_info.dtu_hw_version",
        translation_key="dtu_hw_version",
        entity_category=EntityCategory.DIAGNOSTIC,
        version_translation_function=FCTN_GENERATE_DTU_VERSION_STRING,
        version_prefix="H",
        is_dtu_sensor=True,
        assume_state=True,
    ),
    HoymilesSensorEntityDescription(
        key="pv_info[<inverter_count>].pv_sw_version",
        translation_key="pv_sw_version",
        entity_category=EntityCategory.DIAGNOSTIC,
        version_translation_function=FCTN_GENERATE_INVERTER_SW_VERSION_STRING,
        version_prefix="V",
        assume_state=True,
    ),
    HoymilesSensorEntityDescription(
        key="pv_info[<inverter_count>].pv_hw_version",
        translation_key="pv_hw_version",
        entity_category=EntityCategory.DIAGNOSTIC,
        version_translation_function=FCTN_GENERATE_INVERTER_HW_VERSION_STRING,
        version_prefix="H",
        assume_state=True,
    ),
    HoymilesSensorEntityDescription(
        key="dtu_info.signal_strength",
        translation_key="signal_strength",
        native_unit_of_measurement=PERCENTAGE,
        entity_category=EntityCategory.DIAGNOSTIC,
        icon="mdi:wifi",
        is_dtu_sensor=True,
    ),
)

HOYMILES_ENERGY_STORAGE_SENSORS = [
    HoymilesEnergyStorageSensorEntityDescription(
        key="[<inverter_count>].production.energy_to_load",
        translation_key="energy_to_load",
        native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
        device_class=SensorDeviceClass.ENERGY,
        state_class=SensorStateClass.TOTAL_INCREASING,
        conversion_factor=0.1,
    ),
    HoymilesEnergyStorageSensorEntityDescription(
        key="[<inverter_count>].production.energy_to_battery",
        translation_key="energy_to_battery",
        native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
        device_class=SensorDeviceClass.ENERGY,
        state_class=SensorStateClass.TOTAL_INCREASING,
        conversion_factor=0.1,
    ),
    HoymilesEnergyStorageSensorEntityDescription(
        key="[<inverter_count>].production.energy_to_grid",
        translation_key="energy_to_grid",
        native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
        device_class=SensorDeviceClass.ENERGY,
        state_class=SensorStateClass.TOTAL_INCREASING,
        conversion_factor=0.1,
    ),
    HoymilesEnergyStorageSensorEntityDescription(
        key="[<inverter_count>].consumption.energy_from_pv",
        translation_key="energy_from_pv",
        native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
        device_class=SensorDeviceClass.ENERGY,
        state_class=SensorStateClass.TOTAL_INCREASING,
        conversion_factor=0.1,
    ),
    HoymilesEnergyStorageSensorEntityDescription(
        key="[<inverter_count>].consumption.energy_from_battery",
        translation_key="energy_from_battery",
        native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
        device_class=SensorDeviceClass.ENERGY,
        state_class=SensorStateClass.TOTAL_INCREASING,
        conversion_factor=0.1,
    ),
    HoymilesEnergyStorageSensorEntityDescription(
        key="[<inverter_count>].consumption.energy_from_grid",
        translation_key="energy_from_grid",
        native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
        device_class=SensorDeviceClass.ENERGY,
        state_class=SensorStateClass.TOTAL_INCREASING,
        conversion_factor=0.1,
    ),
    HoymilesEnergyStorageSensorEntityDescription(
        key="[<inverter_count>].pv_panels[<pv_panel_count>].voltage",
        translation_key="pv_panel_voltage",
        native_unit_of_measurement=UnitOfElectricPotential.VOLT,
        device_class=SensorDeviceClass.VOLTAGE,
        state_class=SensorStateClass.MEASUREMENT,
        conversion_factor=0.1,
    ),
    HoymilesEnergyStorageSensorEntityDescription(
        key="[<inverter_count>].pv_panels[<pv_panel_count>].current",
        translation_key="pv_panel_current",
        native_unit_of_measurement=UnitOfElectricCurrent.AMPERE,
        device_class=SensorDeviceClass.CURRENT,
        state_class=SensorStateClass.MEASUREMENT,
        conversion_factor=0.01,
    ),
    HoymilesEnergyStorageSensorEntityDescription(
        key="[<inverter_count>].pv_panels[<pv_panel_count>].power",
        translation_key="pv_panel_power",
        native_unit_of_measurement=UnitOfPower.WATT,
        device_class=SensorDeviceClass.POWER,
        state_class=SensorStateClass.MEASUREMENT,
        conversion_factor=1,
    ),
    HoymilesEnergyStorageSensorEntityDescription(
        key="[<inverter_count>].pv_panels[<pv_panel_count>].energy",
        translation_key="pv_panel_energy",
        native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
        device_class=SensorDeviceClass.ENERGY,
        state_class=SensorStateClass.TOTAL_INCREASING,
        conversion_factor=0.1,
    ),
    HoymilesEnergyStorageSensorEntityDescription(
        key="[<inverter_count>].battery_management.state_of_charge",
        translation_key="state_of_charge",
        native_unit_of_measurement=PERCENTAGE,
        device_class=SensorDeviceClass.BATTERY,
        state_class=SensorStateClass.MEASUREMENT,
    ),
    HoymilesEnergyStorageSensorEntityDescription(
        key="[<inverter_count>].battery_management.state_of_health",
        translation_key="state_of_health",
        native_unit_of_measurement=PERCENTAGE,
        device_class=SensorDeviceClass.BATTERY,
        state_class=SensorStateClass.MEASUREMENT,
    ),
    HoymilesEnergyStorageSensorEntityDescription(
        key="[<inverter_count>].battery_management.voltage",
        translation_key="battery_voltage",
        native_unit_of_measurement=UnitOfElectricPotential.VOLT,
        device_class=SensorDeviceClass.VOLTAGE,
        state_class=SensorStateClass.MEASUREMENT,
        conversion_factor=0.1,
    ),
    HoymilesEnergyStorageSensorEntityDescription(
        key="[<inverter_count>].battery_management.internal_charge_mode",
        translation_key="internal_charge_mode",
        native_unit_of_measurement=UnitOfElectricCurrent.AMPERE,
        device_class=SensorDeviceClass.CURRENT,
        state_class=SensorStateClass.MEASUREMENT,
        conversion_factor=0.1,
    ),
    HoymilesEnergyStorageSensorEntityDescription(
        key="[<inverter_count>].battery_management.internal_discharge_mode",
        translation_key="internal_discharge_mode",
        native_unit_of_measurement=UnitOfElectricCurrent.AMPERE,
        device_class=SensorDeviceClass.CURRENT,
        state_class=SensorStateClass.MEASUREMENT,
        conversion_factor=0.1,
    ),
    HoymilesEnergyStorageSensorEntityDescription(
        key="[<inverter_count>].battery_management.cell_voltage_high",
        translation_key="cell_voltage_high",
        native_unit_of_measurement=UnitOfElectricPotential.VOLT,
        device_class=SensorDeviceClass.VOLTAGE,
        state_class=SensorStateClass.MEASUREMENT,
        conversion_factor=0.01,
    ),
    HoymilesEnergyStorageSensorEntityDescription(
        key="[<inverter_count>].battery_management.cell_voltage_low",
        translation_key="cell_voltage_low",
        native_unit_of_measurement=UnitOfElectricPotential.VOLT,
        device_class=SensorDeviceClass.VOLTAGE,
        state_class=SensorStateClass.MEASUREMENT,
        conversion_factor=0.01,
    ),
    HoymilesEnergyStorageSensorEntityDescription(
        key="[<inverter_count>].battery_management.temp_high_charge",
        translation_key="temp_high_charge",
        native_unit_of_measurement=UnitOfTemperature.CELSIUS,
        device_class=SensorDeviceClass.TEMPERATURE,
        state_class=SensorStateClass.MEASUREMENT,
        conversion_factor=0.1,
    ),
    HoymilesEnergyStorageSensorEntityDescription(
        key="[<inverter_count>].battery_management.temp_low_charge",
        translation_key="temp_low_charge",
        native_unit_of_measurement=UnitOfTemperature.CELSIUS,
        device_class=SensorDeviceClass.TEMPERATURE,
        state_class=SensorStateClass.MEASUREMENT,
        conversion_factor=0.1,
    ),
    HoymilesEnergyStorageSensorEntityDescription(
        key="[<inverter_count>].battery_management.temp_high_module",
        translation_key="temp_high_module",
        native_unit_of_measurement=UnitOfTemperature.CELSIUS,
        device_class=SensorDeviceClass.TEMPERATURE,
        state_class=SensorStateClass.MEASUREMENT,
        conversion_factor=0.1,
    ),
    HoymilesEnergyStorageSensorEntityDescription(
        key="[<inverter_count>].battery_management.temp_low_module",
        translation_key="temp_low_module",
        native_unit_of_measurement=UnitOfTemperature.CELSIUS,
        device_class=SensorDeviceClass.TEMPERATURE,
        state_class=SensorStateClass.MEASUREMENT,
        conversion_factor=0.1,
    ),
    HoymilesEnergyStorageSensorEntityDescription(
        key="[<inverter_count>].battery_management.energy_charged",
        translation_key="energy_charged",
        native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
        device_class=SensorDeviceClass.ENERGY,
        state_class=SensorStateClass.TOTAL_INCREASING,
        conversion_factor=0.1,
    ),
    HoymilesEnergyStorageSensorEntityDescription(
        key="[<inverter_count>].battery_management.energy_discharged",
        translation_key="energy_discharged",
        native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
        device_class=SensorDeviceClass.ENERGY,
        state_class=SensorStateClass.TOTAL_INCREASING,
        conversion_factor=0.1,
    ),
    HoymilesEnergyStorageSensorEntityDescription(
        key="[<inverter_count>].battery_management.voltage_charge_high",
        translation_key="voltage_charge_high",
        native_unit_of_measurement=UnitOfElectricPotential.VOLT,
        device_class=SensorDeviceClass.VOLTAGE,
        state_class=SensorStateClass.MEASUREMENT,
        conversion_factor=0.001,
    ),
    HoymilesEnergyStorageSensorEntityDescription(
        key="[<inverter_count>].battery_management.voltage_charge_low",
        translation_key="voltage_charge_low",
        native_unit_of_measurement=UnitOfElectricPotential.VOLT,
        device_class=SensorDeviceClass.VOLTAGE,
        state_class=SensorStateClass.MEASUREMENT,
        conversion_factor=0.001,
    ),
    HoymilesEnergyStorageSensorEntityDescription(
        key="[<inverter_count>].battery_management.voltage_module_high",
        translation_key="voltage_module_high",
        native_unit_of_measurement=UnitOfElectricPotential.VOLT,
        device_class=SensorDeviceClass.VOLTAGE,
        state_class=SensorStateClass.MEASUREMENT,
        conversion_factor=0.01,
    ),
    HoymilesEnergyStorageSensorEntityDescription(
        key="[<inverter_count>].battery_management.voltage_module_low",
        translation_key="voltage_module_low",
        native_unit_of_measurement=UnitOfElectricPotential.VOLT,
        device_class=SensorDeviceClass.VOLTAGE,
        state_class=SensorStateClass.MEASUREMENT,
        conversion_factor=0.01,
    ),
    HoymilesEnergyStorageSensorEntityDescription(
        key="[<inverter_count>].grid.param.frequency",
        translation_key="grid_frequency",
        native_unit_of_measurement=UnitOfFrequency.HERTZ,
        device_class=SensorDeviceClass.FREQUENCY,
        state_class=SensorStateClass.MEASUREMENT,
        conversion_factor=0.01,
    ),
    HoymilesEnergyStorageSensorEntityDescription(
        key="[<inverter_count>].grid.phases[<phase_count>].voltage",
        translation_key="grid_voltage_phase",
        native_unit_of_measurement=UnitOfElectricPotential.VOLT,
        device_class=SensorDeviceClass.VOLTAGE,
        state_class=SensorStateClass.MEASUREMENT,
        conversion_factor=0.1,
    ),
    HoymilesEnergyStorageSensorEntityDescription(
        key="[<inverter_count>].grid.phases[<phase_count>].current",
        translation_key="grid_current_phase",
        native_unit_of_measurement=UnitOfElectricCurrent.AMPERE,
        device_class=SensorDeviceClass.CURRENT,
        state_class=SensorStateClass.MEASUREMENT,
        conversion_factor=0.01,
    ),
    HoymilesEnergyStorageSensorEntityDescription(
        key="[<inverter_count>].grid.phases[<phase_count>].active_power",
        translation_key="grid_active_power_phase",
        native_unit_of_measurement=UnitOfPower.WATT,
        device_class=SensorDeviceClass.POWER,
        state_class=SensorStateClass.MEASUREMENT,
        conversion_factor=1,
    ),
    HoymilesEnergyStorageSensorEntityDescription(
        key="[<inverter_count>].grid.phases[<phase_count>].reactive_power",
        translation_key="grid_reactive_power_phase",
        native_unit_of_measurement=UnitOfReactivePower.VOLT_AMPERE_REACTIVE,
        device_class=SensorDeviceClass.REACTIVE_POWER,
        state_class=SensorStateClass.MEASUREMENT,
    ),
    HoymilesEnergyStorageSensorEntityDescription(
        key="[<inverter_count>].grid.phases[<phase_count>].power_factor",
        translation_key="grid_power_factor_phase",
        native_unit_of_measurement=PERCENTAGE,
        device_class=None,
        state_class=SensorStateClass.MEASUREMENT,
    ),
    HoymilesEnergyStorageSensorEntityDescription(
        key="[<inverter_count>].grid.phases[<phase_count>].energy_frequency",
        translation_key="grid_energy_frequency_phase",
        native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
        device_class=SensorDeviceClass.ENERGY,
        state_class=SensorStateClass.TOTAL_INCREASING,
        conversion_factor=0.1,
    ),
    HoymilesEnergyStorageSensorEntityDescription(
        key="[<inverter_count>].grid.phases[<phase_count>].energy_consumed",
        translation_key="grid_energy_consumed_phase",
        native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
        device_class=SensorDeviceClass.ENERGY,
        state_class=SensorStateClass.TOTAL_INCREASING,
        conversion_factor=0.1,
    ),
    HoymilesEnergyStorageSensorEntityDescription(
        key="[<inverter_count>].load.param.status",
        translation_key="load_status",
        device_class=SensorDeviceClass.ENUM,
    ),
    HoymilesEnergyStorageSensorEntityDescription(
        key="[<inverter_count>].load.param.frequency",
        translation_key="load_frequency",
        native_unit_of_measurement=UnitOfFrequency.HERTZ,
        device_class=SensorDeviceClass.FREQUENCY,
        state_class=SensorStateClass.MEASUREMENT,
        conversion_factor=0.01,
    ),
    HoymilesEnergyStorageSensorEntityDescription(
        key="[<inverter_count>].load.phases[<phase_count>].voltage",
        translation_key="load_voltage_phase",
        native_unit_of_measurement=UnitOfElectricPotential.VOLT,
        device_class=SensorDeviceClass.VOLTAGE,
        state_class=SensorStateClass.MEASUREMENT,
        conversion_factor=0.1,
    ),
    HoymilesEnergyStorageSensorEntityDescription(
        key="[<inverter_count>].load.phases[<phase_count>].active_power",
        translation_key="load_active_power_phase",
        native_unit_of_measurement=UnitOfPower.WATT,
        device_class=SensorDeviceClass.POWER,
        state_class=SensorStateClass.MEASUREMENT,
    ),
    HoymilesEnergyStorageSensorEntityDescription(
        key="[<inverter_count>].load.phases[<phase_count>].energy_consumed",
        translation_key="load_energy_consumed_phase",
        native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
        device_class=SensorDeviceClass.ENERGY,
        state_class=SensorStateClass.TOTAL_INCREASING,
        conversion_factor=0.1,
    ),
    HoymilesEnergyStorageSensorEntityDescription(
        key="[<inverter_count>].inverter.param.status",
        translation_key="inverter_status",
        device_class=SensorDeviceClass.ENUM,
    ),
    HoymilesEnergyStorageSensorEntityDescription(
        key="[<inverter_count>].inverter.param.frequency",
        translation_key="inverter_frequency",
        native_unit_of_measurement=UnitOfFrequency.HERTZ,
        device_class=SensorDeviceClass.FREQUENCY,
        state_class=SensorStateClass.MEASUREMENT,
        conversion_factor=0.01,
    ),
    HoymilesEnergyStorageSensorEntityDescription(
        key="[<inverter_count>].inverter.param.isolation_resistance",
        translation_key="inverter_isolation_resistance",
        native_unit_of_measurement="kΩ",
        device_class=None,
        state_class=SensorStateClass.MEASUREMENT,
    ),
    HoymilesEnergyStorageSensorEntityDescription(
        key="[<inverter_count>].inverter.param.leakage_current",
        translation_key="inverter_leakage_current",
        native_unit_of_measurement=UnitOfElectricCurrent.MILLIAMPERE,
        device_class=SensorDeviceClass.CURRENT,
        state_class=SensorStateClass.MEASUREMENT,
    ),
    HoymilesEnergyStorageSensorEntityDescription(
        key="[<inverter_count>].inverter.param.drm_signal",
        translation_key="inverter_drm_signal",
        device_class=SensorDeviceClass.ENUM,
    ),
    HoymilesEnergyStorageSensorEntityDescription(
        key="[<inverter_count>].inverter.phases[<phase_count>].voltage",
        translation_key="inverter_voltage_phase",
        native_unit_of_measurement=UnitOfElectricPotential.VOLT,
        device_class=SensorDeviceClass.VOLTAGE,
        state_class=SensorStateClass.MEASUREMENT,
        conversion_factor=0.1,
    ),
    HoymilesEnergyStorageSensorEntityDescription(
        key="[<inverter_count>].inverter.phases[<phase_count>].current",
        translation_key="inverter_current_phase",
        native_unit_of_measurement=UnitOfElectricCurrent.AMPERE,
        device_class=SensorDeviceClass.CURRENT,
        state_class=SensorStateClass.MEASUREMENT,
        conversion_factor=0.01,
    ),
    HoymilesEnergyStorageSensorEntityDescription(
        key="[<inverter_count>].inverter.phases[<phase_count>].active_power",
        translation_key="inverter_active_power_phase",
        native_unit_of_measurement=UnitOfPower.WATT,
        device_class=SensorDeviceClass.POWER,
        state_class=SensorStateClass.MEASUREMENT,
    ),
    HoymilesEnergyStorageSensorEntityDescription(
        key="[<inverter_count>].inverter.phases[<phase_count>].reactive_power",
        translation_key="inverter_reactive_power_phase",
        native_unit_of_measurement=UnitOfReactivePower.VOLT_AMPERE_REACTIVE,
        device_class=SensorDeviceClass.REACTIVE_POWER,
        state_class=SensorStateClass.MEASUREMENT,
    ),
    HoymilesEnergyStorageSensorEntityDescription(
        key="[<inverter_count>].inverter.phases[<phase_count>].dc_current",
        translation_key="inverter_dc_current_phase",
        native_unit_of_measurement=UnitOfElectricCurrent.MILLIAMPERE,
        device_class=SensorDeviceClass.CURRENT,
        state_class=SensorStateClass.MEASUREMENT,
    ),
    HoymilesEnergyStorageSensorEntityDescription(
        key="[<inverter_count>].inverter.phases[<phase_count>].dc_voltage",
        translation_key="inverter_dc_voltage_phase",
        native_unit_of_measurement=UnitOfElectricPotential.MILLIVOLT,
        device_class=SensorDeviceClass.VOLTAGE,
        state_class=SensorStateClass.MEASUREMENT,
        conversion_factor=0.1,
    ),
    HoymilesEnergyStorageSensorEntityDescription(
        key="[<inverter_count>].inverter.phases[<phase_count>].eps_voltage",
        translation_key="inverter_eps_voltage_phase",
        native_unit_of_measurement=UnitOfElectricPotential.VOLT,
        device_class=SensorDeviceClass.VOLTAGE,
        state_class=SensorStateClass.MEASUREMENT,
        conversion_factor=0.1,
    ),
    HoymilesEnergyStorageSensorEntityDescription(
        key="[<inverter_count>].inverter.phases[<phase_count>].eps_current",
        translation_key="inverter_eps_current_phase",
        native_unit_of_measurement=UnitOfElectricCurrent.AMPERE,
        device_class=SensorDeviceClass.CURRENT,
        state_class=SensorStateClass.MEASUREMENT,
        conversion_factor=0.01,
    ),
    HoymilesEnergyStorageSensorEntityDescription(
        key="[<inverter_count>].inverter.phases[<phase_count>].eps_power",
        translation_key="inverter_eps_power_phase",
        native_unit_of_measurement=UnitOfPower.WATT,
        device_class=SensorDeviceClass.POWER,
        state_class=SensorStateClass.MEASUREMENT,
    ),
    HoymilesEnergyStorageSensorEntityDescription(
        key="[<inverter_count>].pv_inverter.param.status",
        translation_key="pv_inverter_status",
        device_class=SensorDeviceClass.ENUM,
    ),
    HoymilesEnergyStorageSensorEntityDescription(
        key="[<inverter_count>].pv_inverter.param.frequency",
        translation_key="pv_inverter_frequency",
        native_unit_of_measurement=UnitOfFrequency.HERTZ,
        device_class=SensorDeviceClass.FREQUENCY,
        state_class=SensorStateClass.MEASUREMENT,
        conversion_factor=0.01,
    ),
    HoymilesEnergyStorageSensorEntityDescription(
        key="[<inverter_count>].pv_inverter.phases[<phase_count>].voltage",
        translation_key="pv_inverter_voltage_phase",
        native_unit_of_measurement=UnitOfElectricPotential.VOLT,
        device_class=SensorDeviceClass.VOLTAGE,
        state_class=SensorStateClass.MEASUREMENT,
        conversion_factor=0.1,
    ),
    HoymilesEnergyStorageSensorEntityDescription(
        key="[<inverter_count>].pv_inverter.phases[<phase_count>].current",
        translation_key="pv_inverter_current_phase",
        native_unit_of_measurement=UnitOfElectricCurrent.AMPERE,
        device_class=SensorDeviceClass.CURRENT,
        state_class=SensorStateClass.MEASUREMENT,
        conversion_factor=0.01,
    ),
    HoymilesEnergyStorageSensorEntityDescription(
        key="[<inverter_count>].pv_inverter.phases[<phase_count>].active_power",
        translation_key="pv_inverter_active_power_phase",
        native_unit_of_measurement=UnitOfPower.WATT,
        device_class=SensorDeviceClass.POWER,
        state_class=SensorStateClass.MEASUREMENT,
    ),
    HoymilesEnergyStorageSensorEntityDescription(
        key="[<inverter_count>].pv_inverter.phases[<phase_count>].reactive_power",
        translation_key="pv_inverter_reactive_power_phase",
        native_unit_of_measurement=UnitOfReactivePower.VOLT_AMPERE_REACTIVE,
        device_class=SensorDeviceClass.REACTIVE_POWER,
        state_class=SensorStateClass.MEASUREMENT,
    ),
    HoymilesEnergyStorageSensorEntityDescription(
        key="[<inverter_count>].pv_inverter.phases[<phase_count>].energy",
        translation_key="pv_inverter_energy_phase",
        native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
        device_class=SensorDeviceClass.ENERGY,
        state_class=SensorStateClass.TOTAL_INCREASING,
        conversion_factor=0.1,
    ),
    HoymilesEnergyStorageSensorEntityDescription(
        key="[<inverter_count>].power_flow.pv_to_load",
        translation_key="pv_to_load",
        native_unit_of_measurement=UnitOfPower.WATT,
        device_class=SensorDeviceClass.POWER,
        state_class=SensorStateClass.MEASUREMENT,
    ),
    HoymilesEnergyStorageSensorEntityDescription(
        key="[<inverter_count>].power_flow.pv_to_battery",
        translation_key="pv_to_battery",
        native_unit_of_measurement=UnitOfPower.WATT,
        device_class=SensorDeviceClass.POWER,
        state_class=SensorStateClass.MEASUREMENT,
    ),
    HoymilesEnergyStorageSensorEntityDescription(
        key="[<inverter_count>].power_flow.pv_to_grid",
        translation_key="pv_to_grid",
        native_unit_of_measurement=UnitOfPower.WATT,
        device_class=SensorDeviceClass.POWER,
        state_class=SensorStateClass.MEASUREMENT,
    ),
    HoymilesEnergyStorageSensorEntityDescription(
        key="[<inverter_count>].power_flow.battery_to_load",
        translation_key="battery_to_load",
        native_unit_of_measurement=UnitOfPower.WATT,
        device_class=SensorDeviceClass.POWER,
        state_class=SensorStateClass.MEASUREMENT,
    ),
    HoymilesEnergyStorageSensorEntityDescription(
        key="[<inverter_count>].power_flow.grid_to_load",
        translation_key="grid_to_load",
        native_unit_of_measurement=UnitOfPower.WATT,
        device_class=SensorDeviceClass.POWER,
        state_class=SensorStateClass.MEASUREMENT,
    ),
    HoymilesEnergyStorageSensorEntityDescription(
        key="[<inverter_count>].power_flow.battery_to_grid",
        translation_key="battery_to_grid",
        native_unit_of_measurement=UnitOfPower.WATT,
        device_class=SensorDeviceClass.POWER,
        state_class=SensorStateClass.MEASUREMENT,
    ),
]


async def async_setup_entry(
    hass: HomeAssistant,
    config_entry: ConfigEntry,
    async_add_entities: AddEntitiesCallback,
) -> None:
    """Set up sensor platform."""

    hass_data = hass.data[DOMAIN][config_entry.entry_id]
    data_coordinator = hass_data.get(HASS_DATA_COORDINATOR, None)
    config_coordinator = hass_data.get(HASS_CONFIG_COORDINATOR, None)
    app_info_coordinator = hass_data.get(HASS_APP_INFO_COORDINATOR, None)
    energy_storage_data_coordinator = hass_data.get(
        HASS_ENERGY_STORAGE_DATA_COORDINATOR, None
    )
    dtu_serial_number = config_entry.data[CONF_DTU_SERIAL_NUMBER]
    single_phase_inverters = config_entry.data.get(CONF_INVERTERS, [])
    three_phase_inverters = config_entry.data.get(CONF_THREE_PHASE_INVERTERS, [])
    hybrid_inverters = config_entry.data.get(CONF_HYBRID_INVERTERS, [])
    meters = config_entry.data.get(CONF_METERS, [])
    inverters = single_phase_inverters + three_phase_inverters
    ports = config_entry.data[CONF_PORTS]
    sensors = []

    # Real Data Sensors

    if inverters:
        for description in HOYMILES_SENSORS:
            device_class = description.device_class
            if device_class == SensorDeviceClass.ENERGY:
                class_name = HoymilesEnergySensorEntity
            else:
                class_name = HoymilesDataSensorEntity

            if "sgs_data" in description.key and single_phase_inverters:
                sensor_entities = get_sensors_for_description(
                    config_entry,
                    description,
                    data_coordinator,
                    class_name,
                    dtu_serial_number,
                    single_phase_inverters,
                    [],
                )
                sensors.extend(sensor_entities)

            elif "tgs_data" in description.key and three_phase_inverters:
                sensor_entities = get_sensors_for_description(
                    config_entry,
                    description,
                    data_coordinator,
                    class_name,
                    dtu_serial_number,
                    three_phase_inverters,
                    [],
                )
                sensors.extend(sensor_entities)
            elif "meter" in description.key and meters:
                sensor_entities = get_sensors_for_description(
                    config_entry,
                    description,
                    data_coordinator,
                    class_name,
                    dtu_serial_number,
                    [],
                    [],
                    meters,
                )
                sensors.extend(sensor_entities)

            else:
                sensor_entities = get_sensors_for_description(
                    config_entry,
                    description,
                    data_coordinator,
                    class_name,
                    dtu_serial_number,
                    [],
                    ports,
                )
                sensors.extend(sensor_entities)

        for description in CONFIG_DIAGNOSTIC_SENSORS:
            sensor_entities = get_sensors_for_description(
                config_entry,
                description,
                config_coordinator,
                HoymilesDiagnosticSensorEntity,
                dtu_serial_number,
                inverters,
                ports,
            )
            sensors.extend(sensor_entities)

        for description in APP_INFO_SENSORS:
            sensor_entities = get_sensors_for_description(
                config_entry,
                description,
                app_info_coordinator,
                HoymilesDataSensorEntity,
                dtu_serial_number,
                inverters,
                ports,
            )
            sensors.extend(sensor_entities)

    if hybrid_inverters:
        for description in HOYMILES_ENERGY_STORAGE_SENSORS:
            sensor_entities = get_sensors_for_hybrid_inverter_description(
                config_entry,
                description,
                energy_storage_data_coordinator,
                HoymilesEnergyStorageSensorEntity,
                dtu_serial_number,
                hybrid_inverters,
            )
            sensors.extend(sensor_entities)

    async_add_entities(sensors)


def get_sensors_for_description(
    config_entry: ConfigEntry,
    description: SensorEntityDescription,
    coordinator: HoymilesCoordinatorEntity,
    class_name: SensorEntity,
    dtu_serial_number: str,
    inverters: list,
    ports: list,
    meters: list = [],
) -> list[SensorEntity]:
    """Get sensors for the given description."""

    sensors = []

    if "<inverter_count>" in description.key:
        for index, inverter_serial in enumerate(inverters):
            new_key = description.key.replace("<inverter_count>", str(index))
            updated_description = dataclasses.replace(
                description, key=new_key, serial_number=inverter_serial
            )
            sensor = class_name(config_entry, updated_description, coordinator)
            sensors.append(sensor)
    elif "<pv_count>" in description.key:
        for index, port in enumerate(ports):
            inverter_serial = port["inverter_serial_number"]
            port_number = port["port_number"]
            new_key = str(description.key).replace("<pv_count>", str(index))
            updated_description = dataclasses.replace(
                description,
                key=new_key,
                serial_number=inverter_serial,
                port_number=port_number,
            )
            sensor = class_name(config_entry, updated_description, coordinator)
            sensors.append(sensor)
    elif "meter_count" in description.key:
        for index, meter in enumerate(meters):
            meter_serial = meter["meter_serial_number"]
            meter_type = meter["device_type"]

            if description.requires_device_type.value in (
                DeviceType.ALL_DEVICES.value,
                meter_type,
            ):
                new_key = description.key.replace("<meter_count>", str(index))
                updated_description = dataclasses.replace(
                    description, key=new_key, serial_number=meter_serial
                )
                sensor = class_name(config_entry, updated_description, coordinator)
                sensors.append(sensor)
    else:
        if description.supported_dtu_types is not None:
            serial_bytes = bytes.fromhex(dtu_serial_number)

            dtu_type = None
            try:
                dtu_type = get_dtu_model_type(serial_bytes)
            except ValueError as e:
                _LOGGER.error(f"Error getting DTU model type: {e}")

        if (
            description.supported_dtu_types is None
            or dtu_type in description.supported_dtu_types
        ):
            updated_description = dataclasses.replace(
                description, serial_number=dtu_serial_number
            )
            sensor = class_name(config_entry, updated_description, coordinator)
            sensors.append(sensor)

    return sensors


def get_sensors_for_hybrid_inverter_description(
    config_entry: ConfigEntry,
    description: SensorEntityDescription,
    coordinator: HoymilesCoordinatorEntity,
    class_name: SensorEntity,
    dtu_serial_number: str,
    inverters: list,
) -> list[SensorEntity]:
    """Get sensors for the given description."""

    sensors = []

    if "<inverter_count>" in description.key:
        for index, inverter in enumerate(inverters):
            new_key = description.key.replace("<inverter_count>", str(index))

            if "<pv_panel_count>" in description.key:
                # TODO: Dynamically determine number of PV panels
                for pv_index in range(0, 2):
                    new_pv_index_key = new_key.replace(
                        "<pv_panel_count>", str(pv_index)
                    )
                    updated_description = dataclasses.replace(
                        description,
                        key=new_pv_index_key,
                        serial_number=inverter["inverter_serial_number"],
                        model_name=inverter["model_name"],
                        port_number=pv_index + 1,
                    )
                    sensor = class_name(config_entry, updated_description, coordinator)
                    sensors.append(sensor)
            elif "<phase_count>" in description.key:
                # TODO: Dynamically determine number of phases
                for phase_index in range(0, 3):
                    new_phase_index_key = new_key.replace(
                        "<phase_count>", str(phase_index)
                    )
                    updated_description = dataclasses.replace(
                        description,
                        key=new_phase_index_key,
                        serial_number=inverter["inverter_serial_number"],
                        model_name=inverter["model_name"],
                        phase=["A", "B", "C"][phase_index],
                    )
                    sensor = class_name(config_entry, updated_description, coordinator)
                    sensors.append(sensor)

            else:
                updated_description = dataclasses.replace(
                    description,
                    key=new_key,
                    serial_number=inverter["inverter_serial_number"],
                    model_name=inverter["model_name"],
                )
                sensor = class_name(config_entry, updated_description, coordinator)
                sensors.append(sensor)

    else:
        updated_description = dataclasses.replace(
            description, serial_number=dtu_serial_number
        )
        sensor = class_name(config_entry, updated_description, coordinator)
        sensors.append(sensor)

    return sensors


class HoymilesDataSensorEntity(HoymilesCoordinatorEntity, RestoreSensor):
    """Represents a sensor entity for Hoymiles data."""

    def __init__(
        self,
        config_entry: ConfigEntry,
        description: HoymilesSensorEntityDescription,
        coordinator: HoymilesCoordinatorEntity,
    ):
        """Pass coordinator to CoordinatorEntity."""
        super().__init__(config_entry, description, coordinator)

        self._attribute_name = description.key
        self._conversion_factor = description.conversion_factor
        self._version_translation_function = description.version_translation_function
        self._version_prefix = description.version_prefix
        self._native_value = None
        self._assumed_state = False
        self._last_known_value = None
        self._last_successful_update = None
        self._last_update_state = None

        self.update_state_value()

    @callback
    def _handle_coordinator_update(self) -> None:
        """Handle updated data from the coordinator."""
        self.update_state_value()
        super()._handle_coordinator_update()

    @property
    def native_value(self):
        """Return the native value of the sensor."""
        if self._native_value == 0.0:
            if self.entity_description.assume_state:
                return self._last_known_value
            elif (
                self._last_successful_update is not None
                and datetime.now() - self._last_successful_update
                <= timedelta(minutes=3)
            ):
                _LOGGER.debug(
                    "[%s] Returning last known value: %s, instead of 0.0 to cope with inverter in offline mode.",
                    self.name,
                    self._last_known_value,
                )
                self._assumed_state = True
                return self._last_known_value
        else:
            self._last_successful_update = datetime.now()
            self._last_known_value = self._native_value
        self._assumed_state = False
        return self._native_value

    @property
    def assumed_state(self):
        """Return the assumed state of the sensor."""
        return self._assumed_state

    def update_state_value(self):
        """Update the state value of the sensor based on the coordinator data."""
        new_native_value = 0.0

        if self.coordinator is not None and (
            not hasattr(self.coordinator, "data") or self.coordinator.data is None
        ):
            new_native_value = 0.0
        elif "[" in self._attribute_name and "]" in self._attribute_name:
            # Extracting the list index and attribute dynamically
            attribute_name, index = self._attribute_name.split("[")
            index = int(index.split("]")[0])
            nested_attribute = (
                self._attribute_name.split("].")[1]
                if "]." in self._attribute_name
                else None
            )

            attribute = getattr(self.coordinator.data, attribute_name.split("[")[0], [])

            if index < len(attribute):
                if nested_attribute is not None:
                    new_native_value = getattr(attribute[index], nested_attribute, None)
                else:
                    new_native_value = attribute[index]
            else:
                new_native_value = None
        elif "." in self._attribute_name:
            attribute_parts = self._attribute_name.split(".")
            attribute = self.coordinator.data
            for part in attribute_parts:
                attribute = getattr(attribute, part, None)
            new_native_value = attribute

        else:
            new_native_value = getattr(
                self.coordinator.data, self._attribute_name, None
            )

        if new_native_value is not None and self._conversion_factor is not None:
            new_native_value *= self._conversion_factor

        if (
            new_native_value is not None
            and new_native_value != 0.0
            and self._version_translation_function is not None
        ):
            new_native_value = getattr(
                hoymiles_wifi.hoymiles, self._version_translation_function
            )(int(new_native_value))

        if (
            new_native_value is not None
            and new_native_value != 0.0
            and self._version_prefix is not None
        ):
            new_native_value = f"{self._version_prefix}{new_native_value}"

        if (
            self.entity_description.force_keep_maximum_within_day
            and self._last_update_state is not None
            and self._last_update_state.date() == datetime.now().date()
        ):
            new_native_value = max(new_native_value, self._native_value)

        self._last_update_state = datetime.now()
        self._native_value = new_native_value

    async def async_added_to_hass(self) -> None:
        """Call when entity about to be added to hass."""
        await super().async_added_to_hass()

        state = await self.async_get_last_sensor_data()
        if state:
            self.last_known_value = state.native_value


class HoymilesEnergySensorEntity(HoymilesDataSensorEntity, RestoreSensor):
    """Represents an energy sensor entity for Hoymiles data."""

    def __init__(
        self,
        config_entry: ConfigEntry,
        description: HoymilesDiagnosticEntityDescription,
        coordinator: HoymilesCoordinatorEntity,
    ):
        """Initialize the HoymilesEnergySensorEntity."""
        super().__init__(config_entry, description, coordinator)
        # Important to set to None to not mess with long term stats
        self._last_known_value = None

    def schedule_midnight_reset(self, reset_sensor_value: bool = True):
        """Schedule the reset function to run again at the next midnight."""
        now = datetime.now()
        midnight = datetime.combine(now.date(), time(0, 0))
        midnight = midnight + timedelta(days=1) if now > midnight else midnight
        time_until_midnight = (midnight - datetime.now()).total_seconds()

        if reset_sensor_value:
            self.reset_sensor_value()

        self.hass.loop.call_later(time_until_midnight, self.schedule_midnight_reset)

    def reset_sensor_value(self):
        """Reset the sensor value."""
        self._last_known_value = 0

    @property
    def native_value(self):
        """Return the native value of the sensor."""
        super_native_value = super().native_value
        # For an energy sensor a value of 0 would mess up long term stats because of how total_increasing works
        if super_native_value == 0.0:
            _LOGGER.debug(
                "Returning last known value instead of 0.0 for %s to avoid resetting total_increasing counter",
                self.name,
            )
            self._assumed_state = True
            return self._last_known_value
        self._last_known_value = super_native_value
        self._assumed_state = False
        return super_native_value

    async def async_added_to_hass(self) -> None:
        """Call when entity about to be added to hass."""
        await super().async_added_to_hass()

        state = await self.async_get_last_sensor_data()
        if state:
            self._last_known_value = state.native_value

        if self.entity_description.reset_at_midnight:
            self.schedule_midnight_reset(reset_sensor_value=False)


class HoymilesDiagnosticSensorEntity(
    HoymilesCoordinatorEntity, RestoreSensor, SensorEntity
):
    """Represents a diagnostic sensor entity for Hoymiles data."""

    def __init__(self, config_entry, description, coordinator):
        """Initialize the HoymilesSensorEntity."""
        super().__init__(config_entry, description, coordinator)

        self._attribute_name = description.key
        self._conversion = description.conversion
        self._separator = description.separator
        self._native_value = None
        self._assumed_state = False

        self.update_state_value()
        self._last_known_value = self._native_value

    @callback
    def _handle_coordinator_update(self) -> None:
        """Handle updated data from the coordinator."""
        self.update_state_value()
        super()._handle_coordinator_update()

    @property
    def native_value(self):
        """Return the native value of the sensor."""
        if self._native_value is None:
            self._assumed_state = True
            return self._last_known_value

        self._last_known_value = self._native_value
        self._assumed_state = False
        return self._native_value

    def update_state_value(self):
        """Update the state value of the sensor."""

        if "[" in self._attribute_name and "]" in self._attribute_name:
            attribute_parts = self._attribute_name.split("[")
            attribute_name = attribute_parts[0]
            index_range = attribute_parts[1].split("]")[0]
            start, end = map(int, index_range.split("-"))

            new_attribute_names = [
                f"{attribute_name}{i}" for i in range(start, end + 1)
            ]
            attribute_values = [
                str(getattr(self.coordinator.data, attr, ""))
                for attr in new_attribute_names
            ]

            if "" in attribute_values:
                self._native_value = None
            else:
                self._native_value = self._separator.join(attribute_values)
        else:
            self._native_value = getattr(
                self.coordinator.data, self._attribute_name, None
            )

        if self._native_value is not None and self._conversion == ConversionAction.HEX:
            self._native_value = self._separator.join(
                hex(int(value))[2:]
                for value in self._native_value.split(self._separator)
            ).upper()

    async def async_added_to_hass(self) -> None:
        """Call when entity about to be added to hass."""
        await super().async_added_to_hass()
        state = await self.async_get_last_sensor_data()
        if state:
            self._last_known_value = state.native_value


class HoymilesEnergyStorageSensorEntity(HoymilesCoordinatorEntity, RestoreSensor):
    """Represents a sensor entity for Hoymiles data."""

    def __init__(
        self,
        config_entry: ConfigEntry,
        description: HoymilesEnergyStorageSensorEntityDescription,
        coordinator: HoymilesCoordinatorEntity,
    ):
        """Pass coordinator to CoordinatorEntity."""
        super().__init__(config_entry, description, coordinator)

        self._attribute_name = description.key
        self._conversion_factor = description.conversion_factor
        self._version_translation_function = description.version_translation_function
        self._version_prefix = description.version_prefix
        self._native_value = None
        self._assumed_state = False
        self._last_known_value = None
        self._last_successful_update = None
        self._last_update_state = None

        self.update_state_value()

    @callback
    def _handle_coordinator_update(self) -> None:
        """Handle updated data from the coordinator."""
        self.update_state_value()
        super()._handle_coordinator_update()

    @property
    def native_value(self):
        """Return the native value of the sensor."""
        if self._native_value == 0.0:
            if self.entity_description.assume_state:
                return self._last_known_value
            elif (
                self._last_successful_update is not None
                and datetime.now() - self._last_successful_update
                <= timedelta(minutes=3)
            ):
                _LOGGER.debug(
                    "[%s] Returning last known value: %s, instead of 0.0 to cope with inverter in offline mode.",
                    self.name,
                    self._last_known_value,
                )
                self._assumed_state = True
                return self._last_known_value
        else:
            self._last_successful_update = datetime.now()
            self._last_known_value = self._native_value
        self._assumed_state = False
        return self._native_value

    @property
    def assumed_state(self):
        """Return the assumed state of the sensor."""
        return self._assumed_state

    def update_state_value(self):
        """Update the state value of the sensor based on the coordinator data."""
        new_native_value = 0.0

        if (
            self.coordinator is None
            or not hasattr(self.coordinator, "data")
            or self.coordinator.data is None
        ):
            self._native_value = 0.0
            return

        def resolve_path(obj, path):
            tokens = re.findall(r"\w+|\[\d+\]", path)
            for token in tokens:
                if obj is None:
                    return None
                if token.startswith("["):
                    index = int(token[1:-1])
                    try:
                        obj = obj[index]
                    except (IndexError, TypeError):
                        logging.error(
                            "Index %d out of range for object: %s", index, obj
                        )
                        return None
                else:
                    obj = getattr(obj, token, None)
            return obj

        new_native_value = resolve_path(self.coordinator.data, self._attribute_name)

        if new_native_value is not None and self._conversion_factor is not None:
            new_native_value *= self._conversion_factor

        if (
            new_native_value is not None
            and new_native_value != 0.0
            and self._version_translation_function is not None
        ):
            new_native_value = getattr(
                hoymiles_wifi.hoymiles, self._version_translation_function
            )(int(new_native_value))

        if (
            new_native_value is not None
            and new_native_value != 0.0
            and self._version_prefix is not None
        ):
            new_native_value = f"{self._version_prefix}{new_native_value}"

        if (
            self.entity_description.force_keep_maximum_within_day
            and self._last_update_state is not None
            and self._last_update_state.date() == datetime.now().date()
        ):
            new_native_value = max(new_native_value, self._native_value)

        self._last_update_state = datetime.now()
        self._native_value = new_native_value

        async def async_added_to_hass(self) -> None:
            """Call when entity about to be added to hass."""
            await super().async_added_to_hass()

            state = await self.async_get_last_sensor_data()
            if state:
                self.last_known_value = state.native_value


================================================
FILE: custom_components/hoymiles_wifi/services.py
================================================
from homeassistant.core import ServiceCall
from hoymiles_wifi.dtu import DTU

from hoymiles_wifi.hoymiles import BMSWorkingMode

from custom_components.hoymiles_wifi.const import HASS_DTU, DOMAIN
from homeassistant.helpers.device_registry import async_get as async_get_device_registry


from hoymiles_wifi.utils import parse_time_periods_input, parse_time_settings_input

import logging

from .const import CONF_DTU_SERIAL_NUMBER

_LOGGER = logging.getLogger(__name__)


async def async_handle_set_bms_mode(call: ServiceCall):
    hass = call.hass
    device_registry = async_get_device_registry(hass)

    bms_mode_str = call.data.get("bms_mode")
    rev_soc = call.data.get("rev_soc")
    max_power = call.data.get("max_power")
    peak_soc = call.data.get("peak_soc", None)
    peak_meter_power = call.data.get("peak_meter_power", None)
    time_settings_str = call.data.get("time_settings", None)
    time_periods_str = call.data.get("time_periods", None)
    device_ids = call.data.get("device_id", [])

    time_settings = None
    time_periods = None

    bms_working_mode = BMSWorkingMode[bms_mode_str.upper()]

    _LOGGER.debug(f"Setting BMS mode to {bms_working_mode}")
    _LOGGER.debug(f"  rev_soc: {rev_soc}")
    _LOGGER.debug(f"  max_power: {max_power}")
    _LOGGER.debug(f"  peak_soc: {peak_soc}")
    _LOGGER.debug(f"  peak_meter_power: {peak_meter_power}")
    _LOGGER.debug(f"  time_settings_str: {time_settings_str}")
    _LOGGER.debug(f"  time_periods_str: {time_periods_str}")

    if rev_soc is None:
        raise ValueError("No reserve SOC provided!")

    if bms_working_mode == BMSWorkingMode.ECONOMIC:
        time_settings = parse_time_settings_input(time_settings_str)
        if not time_settings:
            raise ValueError("Invalid time settings!")

    elif bms_working_mode in (
        BMSWorkingMode.FORCED_CHARGING,
        BMSWorkingMode.FORCED_DISCHARGE,
    ):
        if max_power is None:
            raise ValueError("No max power provided!")

    elif bms_working_mode == BMSWorkingMode.PEAK_SHAVING:
        if peak_soc is None:
            raise ValueError("No peak SOC provided!")
        if peak_meter_power is None:
            raise ValueError("No peak meter power provided!")

    elif bms_working_mode == BMSWorkingMode.TIME_OF_USE:
        time_periods = parse_time_periods_input(time_periods_str)
        if not time_periods:
            raise ValueError("Invalid time periods!")

    for device_id in device_ids:
        device = device_registry.async_get(device_id)
        if not device:
            _LOGGER.error(f"Device {device_id} not found in registry")
            continue

        for entry_id in device.config_entries:
            hass_data = hass.data[DOMAIN].get(entry_id)
            if not hass_data:
                continue

            dtu = hass_data[HASS_DTU]
            if not dtu or not isinstance(dtu, DTU):
                _LOGGER.error(f"DTU not found for entry {entry_id}")
                continue

            _LOGGER.debug("Found DTU for entry %s -> %s", entry_id, dtu)

            dtu_serial_number_str = hass_data.get(CONF_DTU_SERIAL_NUMBER, None)

            if not dtu_serial_number_str:
                _LOGGER.error(f"DTU serial number not found in config entry {entry_id}")
                continue

            dtu_serial_number = int(dtu_serial_number_str)
            inverter_serial_number = int(device.serial_number)

            _LOGGER.debug(
                f"Setting BMS mode for inverter_serial_number: {inverter_serial_number}"
            )

            await dtu.async_set_energy_storage_working_mode(
                dtu_serial_number=dtu_serial_number,
                inverter_serial_number=inverter_serial_number,
                bms_working_mode=bms_working_mode,
                rev_soc=rev_soc,
                time_settings=time_settings,
                max_power=max_power,
                peak_soc=peak_soc,
                peak_meter_power=peak_meter_power,
                time_periods=time_periods,
            )


================================================
FILE: custom_components/hoymiles_wifi/services.yaml
================================================
set_bms_mode:
  name: Set BMS Working Mode
  description: Sets the working mode of the Hoymiles hybrid inverter.
  target:
    device:
      integration: hoymiles_wifi
  fields:
    bms_mode:
      required: true
      example: self_use
      selector:
        select:
          options:
            - "self_use"
            - "economic"
            - "backup_power"
            - "pure_off_grid"
            - "forced_charging"
            - "forced_discharge"
            - "peak_shaving"
            - "time_of_use"
          translation_key: "bms_mode_type"
    rev_soc:
      required: true
      example: 50
      selector:
        number:
          min: 0
          max: 100
          unit_of_measurement: "%"
    max_power:
      required: false
      example: 60
      selector:
        number:
          min: 0
          max: 100
          unit_of_measurement: "%"
    peak_soc:
      required: false
      example: 80
      selector:
        number:
          min: 0
          max: 100
          unit_of_measurement: "%"
    peak_meter_power:
      required: false
      example: 100
      selector:
        number:
          min: 0
          unit_of_measurement: "W"
    time_settings:
      required: false
      example: "01.01-31.03:1,2,3=06:00-10:00-0.20-0.10,00:00-06:00-0.10-0.05,10:00-18:00-0.15-0.08;4,5=07:00-11:00-0.22-0.11,00:00-07:00-0.08-0.04,11:00-17:00-0.14-0.07"
      selector:
        text:
    time_periods:
      required: false
      example: "06:00-08:00-50-90|18:00-20:00-40-20"
      selector:
        text:


================================================
FILE: custom_components/hoymiles_wifi/strings.json
================================================
{
  "config": {
    "step": {
      "user": {
        "title": "Hoymiles DTU connection",
        "description": "If you need help with the configuration have a look here: https://github.com/suaveolent/ha-hoymiles-wifi",
        "data": {
          "host": "[%key:common::config_flow::data::host%]",
          "update_interval": "Update Interval (seconds)",
          "timeout": "Timeout (seconds)"
        }
      },
      "reconfigure": {
        "title": "Hoymiles DTU connection",
        "description": "If you need help with the configuration have a look here: https://github.com/suaveolent/ha-hoymiles-wifi",
        "data": {
          "host": "[%key:common::config_flow::data::host%]",
          "update_interval": "Update Interval (seconds)",
          "timeout": "Timeout (seconds)"
        }
      }
    },
    "error": {
      "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]"
    },
    "abort": {
      "already_configured": "[%key:common::config_flow::abort::already_configured_device%]"
    }
  },
  "entity": {
    "binary_sensor": {
      "dtu": {
        "name": "DTU"
      }
    },
    "number": {
      "limit_power_mypower": {
        "name": "Power limit"
      }
    },
    "sensor": {
      "ac_active_power": {
        "name": "AC power"
      },
      "ac_daily_energy": {
        "name": "AC daily energy"
      },
      "ac_reactive_power": {
        "name": "AC reactive power"
      },
      "grid_voltage": {
        "name": "Grid voltage"
      },
      "ac_current": {
        "name": "AC current" 
      },
      "grid_frequency": {
        "name": "Grid frequency"
      },
      "inverter_power_factor": {
        "name": "Power factor"
      },
      "inverter_temperature": {
        "name": "Temperature"
      },
      "inverter_warning_number": {
        "name": "Warning number"
      },
      "port_dc_voltage": {
        "name": "Port {port_number} DC voltage"
      },
      "port_dc_current": {
        "name": "Port {port_number} DC current"
      },
      "port_dc_power": {
        "name": "Port {port_number} DC power"
      },
      "port_dc_total_energy": {
        "name": "Port {port_number} DC total energy"
      },
      "port_dc_daily_energy": {
        "name": "Port {port_number} DC daily energy"
      },
      "port_error_code": {
        "name": "Port {port_number} error code"
      },
      "wifi_ssid": {
        "name": "Wi-Fi SSID"
      },
      "meter_kind": {
        "name": "Meter kind"
      },
      "mac_address": {
        "name": "MAC address"
      },
      "ip_address": {
        "name": "IP address"
      },
      "dtu_ap_ssid": {
        "name": "AP SSID"
      },
      "dtu_sw_version": {
        "name": "SW version"
      },
      "dtu_hw_version": {
        "name": "HW version"
      },
      "pv_sw_version": {
        "name": "SW version"
      },
      "pv_hw_version": {
        "name": "HW version"
      },
      "signal_strength": {
        "name": "Signal strength"
      },
      "voltage_phase_A": {
        "name": "Voltage phase A"
      },
      "voltage_phase_B": {
        "name": "Voltage phase B"
      },
      "voltage_phase_C": {
        "name": "Voltage phase C"
      },
      "voltage_line_AB": {
        "name": "Voltage line AB"
      },
      "voltage_line_BC": {
        "name": "Voltage line BC"
      },
      "voltage_line_CA": {
        "name": "Voltage line CA"
      },
      "phase_total_power": {
        "name": "Phase total power"
      },
      "phase_A_power": {
        "name": "Phase A power"
      },
      "phase_B_power": {
        "name": "Phase B power"
      },
      "phase_C_power": {
        "name": "Phase C power"
      },
      "power_factor_total": {
        "name": "Power factor total"
      },
      "energy_total_power": {
        "name": "Energy total power"
      },
      "energy_phase_A": {
        "name": "Energy phase A"
      },
      "energy_phase_B": {
        "name": "Energy phase B"
      },
      "energy_phase_C": {
        "name": "Energy phase C"
      },
      "energy_total_consumed": {
        "name": "Energy total consumed"
      },
      "energy_phase_A_consumed": {
        "name": "Energy phase A consumed"
      },
      "energy_phase_B_consumed": {
        "name": "Energy phase B consumed"
      },
      "energy_phase_C_consumed": {
        "name": "Energy phase C consumed"
      },
      "current_phase_A": {
        "name": "Current phase A"
      },
      "current_phase_B": {
        "name": "Current phase B"
      },
      "current_phase_C": {
        "name": "Current phase C"
      },
      "power_factor_phase_A": {
        "name": "Power factor phase A"
      },
      "power_factor_phase_B": {
        "name": "Power factor phase B"
      },
      "power_factor_phase_C": {
        "name": "Power factor phase C"
      },
      "energy_to_load": {
        "name": "Energy to load"
      },
      "energy_to_battery": {
        "name": "Energy to battery"
      },
      "energy_to_grid": {
        "name": "Energy to grid"
      },
      "energy_from_pv": {
        "name": "Energy from PV"
      },
      "energy_from_battery": {
        "name": "Energy from battery"
      },
      "energy_from_grid": {
        "name": "Energy from grid"
      },
      "pv_panel_voltage": {
        "name": "PV panel {port_number} voltage"
      },
      "pv_panel_current": {
        "name": "PV panel {port_number} current"
      },
      "pv_panel_power": {
        "name": "PV panel {port_number} power"
      },
      "pv_panel_energy": {
        "name": "PV panel {port_number} energy"
      },
      "state_of_charge": {
        "name": "State of charge"
      },
      "state_of_health": {
        "name": "State of health"
      },
      "battery_voltage": {
        "name": "Battery voltage"
      },
      "internal_charge_mode": {
        "name": "Internal charge mode"
      },
      "internal_discharge_mode": {
        "name": "Internal discharge mode"
      },
      "cell_voltage_high": {
        "name": "Cell voltage high"
      },
      "cell_voltage_low": {
        "name": "Cell voltage low"
      },
      "temp_high_charge": {
        "name": "Temperature high (charge)"
      },
      "temp_low_charge": {
        "name": "Temperature low (charge)"
      },
      "temp_high_module": {
        "name": "Module temperature high"
      },
      "temp_low_module": {
        "name": "Module temperature low"
      },
      "energy_charged": {
        "name": "Energy charged"
      },
      "energy_discharged": {
        "name": "Energy discharged"
      },
      "voltage_charge_high": {
        "name": "Voltage charge high"
      },
      "voltage_charge_low": {
        "name": "Voltage charge low"
      },
      "voltage_module_high": {
        "name": "Module voltage high"
      },
      "voltage_module_low": {
        "name": "Module voltage low"
      },
      "grid_status": {
        "name": "Grid status"
      },
      "grid_power_factor_deviation": {
        "name": "Power factor deviation"
      },
      "grid_voltage_phase": {
        "name": "Grid voltage phase {phase}"
      },
      "grid_current_phase": {
        "name": "Grid current phase {phase}"
      },
      "grid_reactive_power_phase": {
        "name": "Grid reactive power phase {phase}"
      },
      "grid_active_power_phase": {
        "name": "Grid active power phase {phase}"
      },
      "grid_power_factor_phase": {
        "name": "Grid power factor phase {phase}"
      },
      "grid_energy_frequency_phase": {
        "name": "Grid energy frequency phase {phase}"
      },
      "grid_energy_consumed_phase": {
        "name": "Grid energy consumed phase {phase}"
      },
      "load_status": {
        "name": "Load status"
      },
      "load_frequency": {
        "name": "Load frequency"
      },
      "load_voltage_phase": {
        "name": "Load voltage phase {phase}"
      },
      "load_active_power_phase": {
        "name": "Load active power phase {phase}"
      },
      "load_energy_consumed_phase": {
        "name": "Load energy consumed phase {phase}"
      },
      "inverter_status": {
        "name": "Inverter status"
      },
      "inverter_frequency": {
        "name": "Inverter frequency"
      },
      "inverter_isolation_resistance": {
        "name": "Inverter isolation resistance"
      },
      "inverter_leakage_current": {
        "name": "Inverter leakage current"
      },
      "inverter_drm_signal": {
        "name": "Inverter DRM signal"
      },
      "inverter_voltage_phase": {
        "name": "Inverter voltage phase {phase}"
      },
      "inverter_current_phase": {
        "name": "Inverter current phase {phase}"
      },
      "inverter_active_power_phase": {
        "name": "Inverter active power phase {phase}"
      },
      "inverter_reactive_power_phase": {
        "name": "Inverter reactive power phase {phase}"
      },
      "inverter_dc_current_phase": {
        "name": "Inverter DC current phase {phase}"
      },
      "inverter_dc_voltage_phase": {
        "name": "Inverter DC voltage phase {phase}"
      },
      "inverter_eps_voltage_phase": {
        "name": "Inverter EPS voltage phase {phase}"
      },
      "inverter_eps_current_phase": {
        "name": "Inverter EPS current phase {phase}"
      },
      "inverter_eps_power_phase": {
        "name": "Inverter EPS power phase {phase}"
      },
      "pv_inverter_status": {
        "name": "PV-Inverter status"
      },
      "pv_inverter_frequency": {
        "name": "PV-Inverter frequency"
      },
      "pv_inverter_voltage_phase": {
        "name": "PV-Inverter voltage phase {phase}"
      },
      "pv_inverter_current_phase": {
        "name": "PV-Inverter current phase {phase}"
      },
      "pv_inverter_active_power_phase": {
        "name": "PV-Inverter active power phase {phase}"
      },
      "pv_inverter_reactive_power_phase": {
        "name": "PV-Inverter reactive power phase {phase}"
      },
      "pv_inverter_energy_phase": {
        "name": "PV-Inverter energy phase {phase}"
      },
      "pv_to_load": {
        "name": "Power PV to load"
      },
      "battery_to_load": {
        "name": "Power battery to load"
      },
      "grid_to_load": {
        "name": "Power grid to load"
      },
      "pv_to_battery": {
        "name": "Power PV to battery"
      },
      "pv_to_grid": {
        "name": "Power PV to grid"
      },
      "battery_to_grid": {
        "name": "Power battery to grid"
      }
    },
    "button": {
      "restart": {
        "name": "Restart"
      },
      "turn_off": {
        "name": "Turn off"
      },
      "turn_on": {
        "name": "Turn on"
      },
      "enable_performance_data_mode": {
        "name": "Experimental: Enable performance data mode"
      }
    }
  },
  "device": {
    "inverter": {
      "name": "Inverter"
    },
    "dtu": {
      "name": "DTU"
    },
    "meter": {
      "name": "Meter"
    },
    "hybrid_inverter": {
      "name": "Hybrid inverter"
    }
  },
  "services": {
    "set_bms_mode": {
      "name": "Set BMS mode",
      "description": "Set the BMS mode of the connected battery.",
      "fields": {
        "bms_mode": {
          "name": "BMS mode",
          "description": "The BMS working mode to set. Possible values are: 'self_use', 'economic', 'backup_power', 'pure_off_grid', 'forced_charging', 'forced_discharge', 'peak_shaving', 'time_of_use'"
        },
        "rev_soc": {
          "name": "Reserved SOC",
          "description": "The reserved state of charge (SOC) to set (in %)."
        },
        "max_power": {
          "name": "Max Power",
          "description": "The maximum charge/discharge power to set (in %)."
        },
        "peak_soc": {
          "name": "Peak SOC",
          "description": "The peak state of charge (SOC) to set (in %)."
        },
        "peak_meter_power": {
          "name": "Peak Meter Power",
          "description": "The peak meter power to set (in W)."
        },
        "time_settings": {
          "name": "Time Settings",
          "description": "Configure time-of-use settings."
        },
        "time_periods": {
          "name": "Time Periods",
          "description": "Define time periods for time-of-use mode."
        }
      }
    }
  },
  "selector": {
    "bms_mode_type": {
      "options": {
        "self_use": "Self-Consumption Mode translated",
        "economic": "Economy Mode",
        "backup_power": "Backup Mode",
        "pure_off_grid": "Off-Grid Mode",
        "forced_charging": "Force Charge Mode",
        "forced_discharge": "Force Discharge Mode",
        "peak_shaving": "Peak Shaving Mode",
        "time_of_use": "Time of Use Mode"
      }
    }
  }
}


================================================
FILE: custom_components/hoymiles_wifi/translations/de.json
================================================
{
  "config": {
    "step": {
      "user": {
        "title": "Hoymiles Verbindung",
        "description": "Wenn Sie Hilfe bei der Konfiguration benötigen, schauen Sie hier vorbei: https://github.com/suaveolent/ha-hoymiles-wifi",
        "data": {
          "host": "Host",
          "update_interval": "Aktualisierungsintervall (Sekunden)",
          "timeout": "Timeout (Sekunden)"
        }
      },
      "reconfigure": {
        "title": "Hoymiles Verbindung",
        "description": "Wenn Sie Hilfe bei der Konfiguration benötigen, schauen Sie hier vorbei: https://github.com/suaveolent/ha-hoymiles-wifi",
        "data": {
          "host": "Host",
          "update_interval": "Aktualisierungsintervall (Sekunden)",
          "timeout": "Timeout (Sekunden)"
        }
      }
    },
    "error": {
      "cannot_connect": "Verbindung nicht möglich."
    },
    "abort": {
      "already_configured": "Bereits konfiguriert."
    }
  },
  "entity": {
    "binary_sensor": {
      "dtu": {
        "name": "DTU"
      }
    },
    "number": {
      "limit_power_mypower": {
        "name": "Leistungsbegrenzung"
      }
    },
    "sensor": {
      "ac_active_power": {
        "name": "AC-Leistung"
      },
      "ac_daily_energy": {
        "name": "AC-Tagesenergie"
      },
      "ac_reactive_power": {
        "name": "AC-Blindleistung"
      },
      "grid_voltage": {
        "name": "Netzspannung"
      },
      "ac_current": {
        "name": "AC-Strom" 
      },
      "grid_frequency": {
        "name": "Netzfrequenz"
      },
      "inverter_power_factor": {
        "name": "Leistungsfaktor"
      },
      "inverter_temperature": {
        "name": "Temperatur"
      },
      "inverter_warning_number": {
        "name": "Warnnummer"
      },
      "port_dc_voltage": {
        "name": "Port {port_number} DC-Spannung"
      },
      "port_dc_current": {
        "name": "Port {port_number} DC-Strom"
      },
      "port_dc_power": {
        "name": "Port {port_number} DC-Leistung"
      },
      "port_dc_total_energy": {
        "name": "Port {port_number} DC-Gesamtenergie"
      },
      "port_dc_daily_energy": {
        "name": "Port {port_number} DC-Tagesenergie"
      },
      "port_error_code": {
        "name": "Port {port_number} Fehlercode"
      },
      "wifi_ssid": {
        "name": "WLAN-SSID"
      },
      "meter_kind": {
        "name": "Zählermodell"
      },
      "mac_address": {
        "name": "MAC-Adresse"
      },
      "ip_address": {
        "name": "IP Adresse"
      },
      "dtu_ap_ssid": {
        "name": "AP-SSID"
      },
      "dtu_sw_version": {
        "name": "SW-Version"
      },
      "dtu_hw_version": {
        "name": "HW-Version"
      },
      "pv_sw_version": {
        "name": "SW-Version"
      },
      "pv_hw_version": {
        "name": "HW-Version"
      },
      "signal_strength": {
        "name": "Signalstärke"
      },
      "voltage_phase_A": {
        "name": "Spannung Phase A"
      },
      "voltage_phase_B": {
        "name": "Spannung Phase B"
      },
      "voltage_phase_C": {
        "name": "Spannung Phase C"
      },
      "voltage_line_AB": {
        "name": "Spannung Phase AB"
      },
      "voltage_line_BC": {
        "name": "Spannung Phase BC"
      },
      "voltage_line_CA": {
        "name": "Spannung Phase CA"
      },
      "phase_total_power": {
        "name": "Phasengesamtleistung"
      },
      "phase_A_power": {
        "name": "Leistung Phase A"
      },
      "phase_B_power": {
        "name": "Leistung Phase B"
      },
      "phase_C_power": {
        "name": "Leistung Phase C"
      },
      "power_factor_total": {
        "name": "Gesamtleistungsfaktor"
      },
      "energy_total_power": {
        "name": "Gesamtenergie"
      },
      "energy_phase_A": {
        "name": "Energie Phase A"
      },
      "energy_phase_B": {
        "name": "Energie Phase B"
      },
      "energy_phase_C": {
        "name": "Energie Phase C"
      },
      "energy_total_consumed": {
        "name": "Gesamtverbrauchte Energie"
      },
      "energy_phase_A_consumed": {
        "name": "Verbrauchte Energie Phase A"
      },
      "energy_phase_B_consumed": {
        "name": "Verbrauchte Energie Phase B"
      },
      "energy_phase_C_consumed": {
        "name": "Verbrauchte Energie Phase C"
      },
      "current_phase_A": {
        "name": "Strom Phase A"
      },
      "current_phase_B": {
        "name": "Strom Phase B"
      },
      "current_phase_C": {
        "name": "Strom Phase C"
      },
      "power_factor_phase_A": {
        "name": "Leistungsfaktor Phase A"
      },
      "power_factor_phase_B": {
        "name": "Leistungsfaktor Phase B"
      },
      "power_factor_phase_C": {
        "name": "Leistungsfaktor Phase C"
      },
      "energy_to_load": {
        "name": "Energie zu Last"
      },
      "energy_to_battery": {
        "name": "Energie zu Batterie"
      },
      "energy_to_grid": {
        "name": "Energie zu Netz"
      },
      "energy_from_pv": {
        "name": "Energie aus PV"
      },
      "energy_from_battery": {
        "name": "Energie aus Batterie"
      },
      "energy_from_grid": {
        "name": "Energie aus Netz"
      },
      "pv_panel_voltage": {
        "name": "PV panel {port_number} voltage"
      },
      "pv_panel_current": {
        "name": "PV panel {port_number} current"
      },
      "pv_panel_power": {
        "name": "PV panel {port_number} power"
      },
      "pv_panel_energy": {
        "name": "PV panel {port_number} energy"
      },
      "state_of_charge": {
        "name": "Ladezustand"
      },
      "state_of_health": {
        "name": "Gesundheitszustand"
      },
      "battery_voltage": {
        "name": "Batteriespannung"
      },
      "internal_charge_mode": {
        "name": "Interner Lademodus"
      },
      "internal_discharge_mode": {
        "name": "Interner Entlademodus"
      },
      "cell_voltage_high": {
        "name": "Maximale Zellenspannung "
      },
      "cell_voltage_low": {
        "name": "Minimale Zellenspannung"
      },
      "temp_high_charge": {
        "name": "Maximale Ladetemperatur"
      },
      "temp_low_charge": {
        "name": "Minimale Ladetemperatur"
      },
      "temp_high_module": {
        "name": "Maximale Modultemperatur"
      },
      "temp_low_module": {
        "name": "Minimale Modultemperatur"
      },
      "energy_charged": {
        "name": "Geladene Energie"
      },
      "energy_discharged": {
        "name": "Entladene Energie"
      },
      "voltage_charge_high": {
        "name": "Maximale Ladespannung"
      },
      "voltage_charge_low": {
        "name": "Minimale Ladespannung"
      },
      "voltage_module_high": {
        "name": "Maximale Modulspannung"
      },
      "voltage_module_low": {
        "name": "Minimale Modulspannung"
      },
      "grid_status": {
        "name": "Netzstatus"
      },
      "grid_power_factor_deviation": {
        "name": "Leistungsfaktorabweichung"
      },
      "grid_voltage_phase": {
        "name": "Netzspannung Phase {phase}"
      },
      "grid_current_phase": {
        "name": "Netzstrom Phase {phase}"
      },
      "grid_reactive_power_phase": {
        "name": "Blindleistung Phase {phase}"
      },
      "grid_active_power_phase": {
        "name": "Wirkleistung Phase {phase}"
      },
      "grid_power_factor_phase": {
        "name": "Leistungsfaktor Phase {phase}"
      },
      "grid_energy_frequency_phase": {
        "name": "Energie-Frequenz Phase {phase}"
      },
      "grid_energy_consumed_phase": {
        "name": "Verbrauchte Energie Phase {phase}"
      },
      "load_status": {
        "name": "Laststatus"
      },
      "load_frequency": {
        "name": "Lastfrequenz"
      },
      "load_voltage_phase": {
        "name": "Lastspannung Phase {phase}"
      },
      "load_active_power_phase": {
        "name": "Wirkleistung Phase {phase}"
      },
      "load_energy_consumed_phase": {
        "name": "Verbrauchte Energie Phase {phase}"
      },
      "inverter_status": {
        "name": "Wechselrichterstatus"
      },
      "inverter_frequency": {
        "name": "Wechselrichterfrequenz"
      },
      "inverter_isolation_resistance": {
        "name": "Isolationswiderstand des Wechselrichters"
      },
      "inverter_leakage_current": {
        "name": "Ableitstrom des Wechselrichters"
      },
      "inverter_drm_signal": {
        "name": "Wechselrichter-DRM-Signal"
      },
      "inverter_voltage_phase": {
        "name": "Wechselrichterspannung Phase {phase}"
      },
      "inverter_current_phase": {
        "name": "Wechselrichterstrom Phase {phase}"
      },
      "inverter_active_power_phase": {
        "name": "Wechselrichterwirkleistung Phase {phase}"
      },
      "inverter_reactive_power_phase": {
        "name": "Wechselrichterblindleistung Phase {phase}"
      },
      "inverter_dc_current_phase": {
        "name": "DC-Strom Wechselrichter Phase {phase}"
      },
      "inverter_dc_voltage_phase": {
        "name": "DC-Spannung Wechselrichter Phase {phase}"
      },
      "inverter_eps_voltage_phase": {
        "name": "EPS-Spannung Wechselrichter Phase {phase}"
      },
      "inverter_eps_current_phase": {
        "name": "EPS-Strom Wechselrichter Phase {phase}"
      },
      "inverter_eps_power_phase": {
        "name": "EPS-Leistung Wechselrichter Phase {phase}"
      },
      "pv_inverter_status": {
        "name": "PV-Wechselrichterstatus"
      },
      "pv_inverter_frequency": {
        "name": "PV-Wechselrichterfrequenz"
      },
      "pv_inverter_voltage_phase": {
        "name": "PV-Wechselrichter Spannung Phase {phase}"
      },
      "pv_inverter_current_phase": {
        "name": "PV-Wechselrichterstrom Phase {phase}"
      },
      "pv_inverter_active_power_phase": {
        "name": "PV-Wechselrichter Wirkleistung Phase {phase}"
      },
      "pv_inverter_reactive_power_phase": {
        "name": "PV-Wechselrichter Blindleistung Phase {phase}"
      },
      "pv_inverter_energy_phase": {
        "name": "PV-Wechselrichter Energie Phase {phase}"
      },
      "pv_to_load": {
        "name": "Leistung PV zu Last"
      },
      "battery_to_load": {
        "name": "Leistung Batterie zu Last"
      },
      "grid_to_load": {
        "name": "Leistung Netz zu Last"
      },
      "pv_to_battery": {
        "name": "Leistung PV zu Batterie"
      },
      "pv_to_grid": {
        "name": "Leistung PV zu Netz"
      },
      "battery_to_grid": {
        "name": "Leistung Batterie zu Netz"
      }
    },
    "button": {
      "restart": {
        "name": "Neustart"
      },
      "turn_off": {
        "name": "Ausschalten"
      },
      "turn_on": {
        "name": "Einschalten"
      },
      "enable_performance_data_mode": {
        "name": "Experimentell: Leistungsdatenmodus aktivieren"
      }
    }
  },
  "device": {
    "inverter": {
      "name": "Wechselrichter"
    },
    "dtu": {
      "name": "DTU"
    },
    "meter": {
      "name": "Zähler"
    },
    "hybrid_inverter": {
      "name": "Hybrid-Wechselrichter"
    }
  },
  "services": {
    "set_bms_mode": {
      "name": "BMS-Modus einstellen",
      "description": "Den BMS-Modus der angeschlossenen Batterie einstellen.",
      "fields": {
        "bms_mode": {
          "name": "BMS-Modus",
          "description": "Der einzustellende BMS-Arbeitsmodus. Mögliche Werte sind: 'self_use', 'economic', 'backup_power', 'pure_off_grid', 'forced_charging', 'forced_discharge', 'peak_shaving', 'time_of_use'"
        },
        "rev_soc": {
          "name": "Reservierter SOC",
          "description": "Der einzustellende reservierte Ladezustand (SOC) in %."
        },
        "max_power": {
          "name": "Maximale Leistung",
          "description": "Die einzustellende maximale Lade-/Entladeleistung in %."
        },
        "peak_soc": {
          "name": "Spitzen-SOC",
          "description": "Der einzustellende Spitzen-Ladezustand (SOC) in %."
        },
        "peak_meter_power": {
          "name": "Spitzenzählerleistung",
          "description": "Die einzustellende Spitzenzählerleistung in W."
        },
        "time_settings": {
          "name": "Zeiteinstellungen",
          "description": "Zeiteinstellungen für den Nutzungszeitmodus konfigurieren."
        },
        "time_periods": {
          "name": "Zeitabschnitte",
          "description": "Zeitabschnitte für den Nutzungszeitmodus festlegen."
        }
      }
    }
  },
  "selector": {
    "bms_mode_type": {
      "options": {
        "self_use": "Eigenverbrauchsmodus",
        "economic": "Sparmodus",
        "backup_power": "Notstrommodus",
        "pure_off_grid": "Inselbetriebsmodus",
        "forced_charging": "Erzwungener Ladebetrieb",
        "forced_discharge": "Erzwungener Entladebetrieb",
        "peak_shaving": "Lastspitzenkappungsmodus",
        "time_of_use": "Nutzungszeitmodus"
      }
    }
  }
}


================================================
FILE: custom_components/hoymiles_wifi/translations/en.json
================================================
{
  "config": {
    "step": {
      "user": {
        "title": "Hoymiles DTU connection",
        "description": "If you need help with the configuration have a look here: https://github.com/suaveolent/ha-hoymiles-wifi",
        "data": {
          "host": "Host",
          "update_interval": "Update Interval (seconds)",
          "timeout": "Timeout (seconds)"
        }
      },
      "reconfigure": {
        "title": "Hoymiles DTU connection",
        "description": "If you need help with the configuration have a look here: https://github.com/suaveolent/ha-hoymiles-wifi",
        "data": {
          "host": "Host",
          "update_interval": "Update Interval (seconds)",
          "timeout": "Timeout (seconds)"
        }
      }
    },
    "error": {
      "cannot_connect": "Failed to connect."
    },
    "abort": {
      "already_configured": "Already configured."
    }
  },
  "entity": {
    "binary_sensor": {
      "dtu": {
        "name": "DTU"
      }
    },
    "number": {
      "limit_power_mypower": {
        "name": "Power limit"
      }
    },
    "sensor": {
      "ac_active_power": {
        "name": "AC power"
      },
      "ac_daily_energy": {
        "name": "AC daily energy"
      },
      "ac_reactive_power": {
        "name": "AC reactive power"
      },
      "grid_voltage": {
        "name": "Grid voltage"
      },
      "ac_current": {
        "name": "AC current" 
      },
      "grid_frequency": {
        "name": "Grid frequency"
      },
      "inverter_power_factor": {
        "name": "Power factor"
      },
      "inverter_temperature": {
        "name": "Temperature"
      },
      "inverter_warning_number": {
        "name": "Warning number"
      },
      "port_dc_voltage": {
        "name": "Port {port_number} DC voltage"
      },
      "port_dc_current": {
        "name": "Port {port_number} DC current"
      },
      "port_dc_power": {
        "name": "Port {port_number} DC power"
      },
      "port_dc_total_energy": {
        "name": "Port {port_number} DC total energy"
      },
      "port_dc_daily_energy": {
        "name": "Port {port_number} DC daily energy"
      },
      "port_error_code": {
        "name": "Port {port_number} error code"
      },
      "wifi_ssid": {
        "name": "Wi-Fi SSID"
      },
      "meter_kind": {
        "name": "Meter kind"
      },
      "mac_address": {
        "name": "MAC address"
      },
      "ip_address": {
        "name": "IP address"
      },
      "dtu_ap_ssid": {
        "name": "AP SSID"
      },
      "dtu_sw_version": {
        "name": "SW version"
      },
      "dtu_hw_version": {
        "name": "HW version"
      },
      "pv_sw_version": {
        "name": "SW version"
      },
      "pv_hw_version": {
        "name": "HW version"
      },
      "signal_strength": {
        "name": "Signal strength"
      },
      "voltage_phase_A": {
        "name": "Voltage phase A"
      },
      "voltage_phase_B": {
        "name": "Voltage phase B"
      },
      "voltage_phase_C": {
        "name": "Voltage phase C"
      },
      "voltage_line_AB": {
        "name": "Voltage line AB"
      },
      "voltage_line_BC": {
        "name": "Voltage line BC"
      },
      "voltage_line_CA": {
        "name": "Voltage line CA"
      },
      "phase_total_power": {
        "name": "Phase total power"
      },
      "phase_A_power": {
        "name": "Phase A power"
      },
      "phase_B_power": {
        "name": "Phase B power"
      },
      "phase_C_power": {
        "name": "Phase C power"
      },
      "power_factor_total": {
        "name": "Power factor total"
      },
      "energy_total_power": {
        "name": "Energy total power"
      },
      "energy_phase_A": {
        "name": "Energy phase A"
      },
      "energy_phase_B": {
        "name": "Energy phase B"
      },
      "energy_phase_C": {
        "name": "Energy phase C"
      },
      "energy_total_consumed": {
        "name": "Energy total consumed"
      },
      "energy_phase_A_consumed": {
        "name": "Energy phase A consumed"
      },
      "energy_phase_B_consumed": {
        "name": "Energy phase B consumed"
      },
      "energy_phase_C_consumed": {
        "name": "Energy phase C consumed"
      },
      "current_phase_A": {
        "name": "Current phase A"
      },
      "current_phase_B": {
        "name": "Current phase B"
      },
      "current_phase_C": {
        "name": "Current phase C"
      },
      "power_factor_phase_A": {
        "name": "Power factor phase A"
      },
      "power_factor_phase_B": {
        "name": "Power factor phase B"
      },
      "power_factor_phase_C": {
        "name": "Power factor phase C"
      },
      "energy_to_load": {
        "name": "Energy to load"
      },
      "energy_to_battery": {
        "name": "Energy to battery"
      },
      "energy_to_grid": {
        "name": "Energy to grid"
      },
      "energy_from_pv": {
        "name": "Energy from PV"
      },
      "energy_from_battery": {
        "name": "Energy from battery"
      },
      "energy_from_grid": {
        "name": "Energy from grid"
      },
      "pv_panel_voltage": {
        "name": "PV panel {port_number} voltage"
      },
      "pv_panel_current": {
        "name": "PV panel {port_number} current"
      },
      "pv_panel_power": {
        "name": "PV panel {port_number} power"
      },
      "pv_panel_energy": {
        "name": "PV panel {port_number} energy"
      },
      "state_of_charge": {
        "name": "State of charge"
      },
      "state_of_health": {
        "name": "State of health"
      },
      "battery_voltage": {
        "name": "Battery voltage"
      },
      "internal_charge_mode": {
        "name": "Internal charge mode"
      },
      "internal_discharge_mode": {
        "name": "Internal discharge mode"
      },
      "cell_voltage_high": {
        "name": "Cell voltage high"
      },
      "cell_voltage_low": {
        "name": "Cell voltage low"
      },
      "temp_high_charge": {
        "name": "Temperature high (charge)"
      },
      "temp_low_charge": {
        "name": "Temperature low (charge)"
      },
      "temp_high_module": {
        "name": "Module temperature high"
      },
      "temp_low_module": {
        "name": "Module temperature low"
      },
      "energy_charged": {
        "name": "Energy charged"
      },
      "energy_discharged": {
        "name": "Energy discharged"
      },
      "voltage_charge_high": {
        "name": "Voltage charge high"
      },
      "voltage_charge_low": {
        "name": "Voltage charge low"
      },
      "voltage_module_high": {
        "name": "Module voltage high"
      },
      "voltage_module_low": {
        "name": "Module voltage low"
      },
      "grid_status": {
        "name": "Grid status"
      },
      "grid_power_factor_deviation": {
        "name": "Power factor deviation"
      },
      "grid_voltage_phase": {
        "name": "Grid voltage phase {phase}"
      },
      "grid_current_phase": {
        "name": "Grid current phase {phase}"
      },
      "grid_reactive_power_phase": {
        "name": "Grid reactive power phase {phase}"
      },
      "grid_active_power_phase": {
        "name": "Grid active power phase {phase}"
      },
      "grid_power_factor_phase": {
        "name": "Grid power factor phase {phase}"
      },
      "grid_energy_frequency_phase": {
        "name": "Grid energy frequency phase {phase}"
      },
      "grid_energy_consumed_phase": {
        "name": "Grid energy consumed phase {phase}"
      },
      "load_status": {
        "name": "Load status"
      },
      "load_frequency": {
        "name": "Load frequency"
      },
      "load_voltage_phase": {
        "name": "Load voltage phase {phase}"
      },
      "load_active_power_phase": {
        "name": "Load active power phase {phase}"
      },
      "load_energy_consumed_phase": {
        "name": "Load energy consumed phase {phase}"
      },
      "inverter_status": {
        "name": "Inverter status"
      },
      "inverter_frequency": {
        "name": "Inverter frequency"
      },
      "inverter_isolation_resistance": {
        "name": "Inverter isolation resistance"
      },
      "inverter_leakage_current": {
        "name": "Inverter leakage current"
      },
      "inverter_drm_signal": {
        "name": "Inverter DRM signal"
      },
      "inverter_voltage_phase": {
        "name": "Inverter voltage phase {phase}"
      },
      "inverter_current_phase": {
        "name": "Inverter current phase {phase}"
      },
      "inverter_active_power_phase": {
        "name": "Inverter active power phase {phase}"
      },
      "inverter_reactive_power_phase": {
        "name": "Inverter reactive power phase {phase}"
      },
      "inverter_dc_current_phase": {
        "name": "Inverter DC current phase {phase}"
      },
      "inverter_dc_voltage_phase": {
        "name": "Inverter DC voltage phase {phase}"
      },
      "inverter_eps_voltage_phase": {
        "name": "Inverter EPS voltage phase {phase}"
      },
      "inverter_eps_current_phase": {
        "name": "Inverter EPS current phase {phase}"
      },
      "inverter_eps_power_phase": {
        "name": "Inverter EPS power phase {phase}"
      },
      "pv_inverter_status": {
        "name": "PV-Inverter status"
      },
      "pv_inverter_frequency": {
        "name": "PV-Inverter frequency"
      },
      "pv_inverter_voltage_phase": {
        "name": "PV-Inverter voltage phase {phase}"
      },
      "pv_inverter_current_phase": {
        "name": "PV-Inverter current phase {phase}"
      },
      "pv_inverter_active_power_phase": {
        "name": "PV-Inverter active power phase {phase}"
      },
      "pv_inverter_reactive_power_phase": {
        "name": "PV-Inverter reactive power phase {phase}"
      },
      "pv_inverter_energy_phase": {
        "name": "PV-Inverter energy phase {phase}"
      },
      "pv_to_load": {
        "name": "Power PV to load"
      },
      "battery_to_load": {
        "name": "Power battery to load"
      },
      "grid_to_load": {
        "name": "Power grid to load"
      },
      "pv_to_battery": {
        "name": "Power PV to battery"
      },
      "pv_to_grid": {
        "name": "Power PV to grid"
      },
      "battery_to_grid": {
        "name": "Power battery to grid"
      }
    },
    "button": {
      "restart": {
        "name": "Restart"
      },
      "turn_off": {
        "name": "Turn off"
      },
      "turn_on": {
        "name": "Turn on"
      },
      "enable_performance_data_mode": {
        "name": "Experimental: Enable performance data mode"
      }
    }
  },
  "device": {
    "inverter": {
      "name": "Inverter"
    },
    "dtu": {
      "name": "DTU"
    },
    "meter": {
      "name": "Meter"
    },
    "hybrid_inverter": {
      "name": "Hybrid inverter"
    }
  },
  "services": {
    "set_bms_mode": {
      "name": "Set BMS mode",
      "description": "Set the BMS mode of the connected battery.",
      "fields": {
        "bms_mode": {
          "name": "BMS mode",
          "description": "The BMS working mode to set. Possible values are: 'self_use', 'economic', 'backup_power', 'pure_off_grid', 'forced_charging', 'forced_discharge', 'peak_shaving', 'time_of_use'"
        },
        "rev_soc": {
          "name": "Reserved SOC",
          "description": "The reserved state of charge (SOC) to set (in %)."
        },
        "max_power": {
          "name": "Max Power",
          "description": "The maximum charge/discharge power to set (in %)."
        },
        "peak_soc": {
          "name": "Peak SOC",
          "description": "The peak state of charge (SOC) to set (in W)."
        },
        "peak_meter_power": {
          "name": "Peak Meter Power",
          "description": "The peak meter power to set (in W)."
        },
        "time_settings": {
          "name": "Time Settings",
          "description": "Configure time-of-use settings."
        },
        "time_periods": {
          "name": "Time Periods",
          "description": "Define time periods for time-of-use mode."
        }
      }
    }
  },
  "selector": {
    "bms_mode_type": {
      "options": {
        "self_use": "Self-Consumption Mode",
        "economic": "Economy Mode",
        "backup_power": "Backup Mode",
        "pure_off_grid": "Off-Grid Mode",
        "forced_charging": "Force Charge Mode",
        "forced_discharge": "Force Discharge Mode",
        "peak_shaving": "Peak Shaving Mode",
        "time_of_use": "Time of Use Mode"
      }
    }
  }
}


================================================
FILE: custom_components/hoymiles_wifi/translations/fr.json
================================================
{
  "config": {
    "step": {
      "user": {
        "title": "Connexion DTU Hoymiles",
        "description": "Si vous avez besoin d'aide pour la configuration, consultez ici : https://github.com/suaveolent/ha-hoymiles-wifi",
        "data": {
          "host": "Hôte",
          "update_interval": "Intervalle de mise à jour (secondes)",
          "timeout": "Délai d'attente (secondes)"
        }
      },
      "reconfigure": {
        "title": "Connexion DTU Hoymiles",
        "description": "Si vous avez besoin d'aide pour la configuration, consultez ici : https://github.com/suaveolent/ha-hoymiles-wifi",
        "data": {
          "host": "Hôte",
          "update_interval": "Intervalle de mise à jour (secondes)",
          "timeout": "Délai d'attente (secondes)"
        }
      }
    },
    "error": {
      "cannot_connect": "Échec de la connexion."
    },
    "abort": {
      "already_configured": "Déjà configuré."
    }
  },
  "entity": {
    "binary_sensor": {
      "dtu": {
        "name": "DTU"
      }
    },
    "number": {
      "limit_power_mypower": {
        "name": "Limite de puissance"
      }
    },
    "sensor": {
      "ac_active_power": {
        "name": "Puissance AC"
      },
      "ac_daily_energy": {
        "name": "Énergie quotidienne AC"
      },
      "ac_reactive_power": {
        "name": "Puissance réactive AC"
      },
      "grid_voltage": {
        "name": "Tension du réseau"
      },
      "ac_current": {
        "name": "Courant AC" 
      },
      "grid_frequency": {
        "name": "Fréquence du réseau"
      },
      "inverter_power_factor": {
        "name": "Facteur de puissance de l'onduleur"
      },
      "inverter_temperature": {
        "name": "Température de l'onduleur"
      },
      "inverter_warning_number": {
        "name": "Numéro d'avertissement"
      },
      "port_dc_voltage": {
        "name": "Tension DC du port {port_number}"
      },
      "port_dc_current": {
        "name": "Courant DC du port {port_number}"
      },
      "port_dc_power": {
        "name": "Puissance DC du port {port_number}"
      },
      "port_dc_total_energy": {
        "name": "Énergie totale DC du port {port_number}"
      },
      "port_dc_daily_energy": {
        "name": "Énergie quotidienne DC du port {port_number}"
      },
      "port_error_code": {
        "name": "Code d'erreur du port {port_number}"
      },
      "wifi_ssid": {
        "name": "SSID Wi-Fi"
      },
      "meter_kind": {
        "name": "Type de compteur"
      },
      "mac_address": {
        "name": "Adresse MAC"
      },
      "ip_address": {
        "name": "Adresse IP"
      },
      "dtu_ap_ssid": {
        "name": "SSID AP"
      },
      "dtu_sw_version": {
        "name": "Version SW"
      },
      "dtu_hw_version": {
        "name": "Version HW"
      },
      "pv_sw_version": {
        "name": "Version SW"
      },
      "pv_hw_version": {
        "name": "Version HW"
      },
      "signal_strength": {
        "name": "Force du signal"
      },
      "voltage_phase_A": {
        "name": "Tension phase A"
      },
      "voltage_phase_B": {
        "name": "Tension phase B"
      },
      "voltage_phase_C": {
        "name": "Tension phase C"
      },
      "voltage_line_AB": {
        "name": "Tension ligne AB"
      },
      "voltage_line_BC": {
        "name": "Tension ligne BC"
      },
      "voltage_line_CA": {
        "name": "Tension ligne CA"
      },
      "phase_total_power": {
        "name": "Puissance totale de phase"
      },
      "phase_A_power": {
        "name": "Puissance phase A"
      },
      "phase_B_power": {
        "name": "Puissance phase A"
      },
      "phase_C_power": {
        "name": "Puissance phase A"
      },
      "power_factor_total": {
        "name": "Facteur de puissance total"
      },
      "energy_total_power": {
        "name": "Énergie totale de puissance"
      },
      "energy_phase_A": {
        "name": "Énergie phase A"
      },
      "energy_phase_B": {
        "name": "Énergie phase B"
      },
      "energy_phase_C": {
        "name": "Énergie phase C"
      },
      "energy_total_consumed": {
        "name": "Énergie totale consommée"
      },
      "energy_phase_A_consumed": {
        "name": "Énergie phase A consommée"
      },
      "energy_phase_B_consumed": {
        "name": "Énergie phase B consommée"
      },
      "energy_phase_C_consumed": {
        "name": "Énergie phase C consommée"
      },
      "current_phase_A": {
        "name": "Courant phase A"
      },
      "current_phase_B": {
        "name": "Courant phase B"
      },
      "current_phase_C": {
        "name": "Courant phase C"
      },
      "power_factor_phase_A": {
        "name": "Facteur de puissance phase A"
      }, 
      "power_factor_phase_B": {
        "name": "Facteur de puissance phase B"
      },
      "power_factor_phase_C": {
        "name": "Facteur de puissance phase C"
      },
      "energy_to_load": {
        "name": "Énergie à charger"
      },
      "energy_to_battery": {
        "name": "Énergie à la batterie"
      },
      "energy_to_grid": {
        "name": "Énergie vers le réseau"
      },
      "energy_from_pv": {
        "name": "Énergie provenant de PV"
      },
      "energy_from_battery": {
        "name": "Énergie provenant de la batterie"
      },
      "energy_from_grid": {
        "name": "Énergie provenant du réseau"
      },
      "pv_panel_voltage": {
        "name": "Tension du panneau PV {port_number}"
      },
      "pv_panel_current": {
        "name": "Courant du panneau PV {port_number}"
      },
      "pv_panel_power": {
        "name": "Puissance du panneau PV {port_number}"
      },
      "pv_panel_energy": {
        "name": "Énergie du panneau PV {port_number}"
      },
      "state_of_charge": {
        "name": "État de charge"
      },
      "state_of_health": {
        "name": "État de santé"
      },
      "battery_voltage": {
        "name": "Tension de la batterie"
      },
      "internal_charge_mode": {
        "name": "Mode de charge interne"
      },
      "internal_discharge_mode": {
        "name": "Mode de décharge interne"
      },
      "cell_voltage_high": {
        "name": "Tension maximale des cellules"
      },
      "cell_voltage_low": {
        "name": "Tension minimale des cellules"
      },
      "temp_high_charge": {
        "name": "Température maximale de charge"
      },
      "temp_low_charge": {
        "name": "Température minimale de charge"
      },
      "temp_high_module": {
        "name": "Température maximale du module"
      },
      "temp_low_module": {
        "name": "Température minimale du module"
      },
      "energy_charged": {
        "name": "Énergie chargée"
      },
      "energy_discharged": {
        "name": "Énergie déchargée"
      },
      "voltage_charge_high": {
        "name": "Tension de charge maximale"
      },
      "voltage_charge_low": {
        "name": "Tension de charge minimale"
      },
      "voltage_module_high": {
        "name": "Tension maximale du module"
      },
      "voltage_module_low": {
        "name": "Tension minimale du module"
      },
      "grid_status": {
        "name": "État du réseau"
      },
      "grid_power_factor_deviation": {
        "name": "Écart du facteur de puissance"
      },
      "grid_voltage_phase": {
        "name": "Tension réseau phase {phase}"
      },
      "grid_current_phase": {
        "name": "Courant réseau phase {phase}"
      },
      "grid_reactive_power_phase": {
        "name": "Puissance réactive phase {phase}"
      },
      "grid_active_power_phase": {
        "name": "Puissance active du réseau phase {phase}"
      },
      "grid_power_factor_phase": {
        "name": "Facteur de puissance phase {phase}"
      },
      "grid_energy_frequency_phase": {
        "name": "Fréquence énergie phase {phase}"
      },
      "grid_energy_consumed_phase": {
        "name": "Énergie consommée phase {phase}"
      },
      "load_status": {
        "name": "État de la charge"
      },
      "load_frequency": {
        "name": "Fréquence de la charge"
      },
      "load_voltage_phase": {
        "name": "Tension charge phase {phase}"
      },
      "load_active_power_phase": {
        "name": "Puissance active phase {phase}"
      },
      "load_energy_consumed_phase": {
        "name": "Énergie consommée phase {phase}"
      },
      "inverter_status": {
        "name": "État de l'onduleur"
      },
      "inverter_frequency": {
         "name": "Fréquence de l'onduleur"
      },
      "inverter_isolation_resistance": {
        "name": "Résistance d'isolement de l'onduleur"
       },
      "inverter_leakage_current": {
        "name": "Courant de fuite de l'onduleur"
      },
      "inverter_drm_signal": {
        "name": "Signal DRM de l'onduleur"
      },
      "inverter_voltage_phase": {
        "name": "Tension de l'onduleur phase {phase}"
      },
      "inverter_current_phase": {
        "name": "Courant de l'onduleur phase {phase}"
      },
      "inverter_active_power_phase": {
        "name": "Puissance active onduleur phase {phase}"
      },
      "inverter_reactive_power_phase": {
        "name": "Puissance réactive onduleur phase {phase}"
      },
      "inverter_dc_current_phase": {
         "name": "Courant DC onduleur phase {phase}"
      },
      "inverter_dc_voltage_phase": {
         "name": "Tension DC onduleur phase {phase}"
      },
      "inverter_eps_voltage_phase": {
         "name": "Tension EPS onduleur phase {phase}"
       },
      "inverter_eps_current_phase": {
         "name": "Courant EPS onduleur phase {phase}"
      },
      "inverter_eps_power_phase": {
         "name": "Puissance EPS onduleur phase {phase}"
      },
      "pv_inverter_status": {
        "name": "État de l'onduleur PV"
      },
      "pv_inverter_frequency": {
        "name": "Fréquence de l'onduleur PV"
      },
      "pv_inverter_voltage_phase": {
        "name": "Tension de phase de l'onduleur PV {phase}"
      },
      "pv_inverter_current_phase": {
        "name": "Courant de phase de l'onduleur PV {phase}"
      },
      "pv_inverter_active_power_phase": {
        "name": "Puissance active de phase de l'onduleur PV {phase}"
      },
      "pv_inverter_reactive_power_phase": {
        "name": "Puissance réactive de phase de l'onduleur PV {phase}"
      },
      "pv_inverter_energy_phase": {
        "name": "Énergie de phase de l'onduleur PV {phase}"
      },
      "pv_to_load": {
        "name": "Puissance PV vers charge"
      },
      "battery_to_load": {
        "name": "Puissance batterie vers charge"
      },
      "grid_to_load": {
        "name": "Puissance réseau vers charge"
      },
      "pv_to_battery": {
        "name": "Puissance PV vers batterie"
      },
      "pv_to_grid": {
        "name": "Puissance PV vers réseau"
      },
      "battery_to_grid": {
        "name": "Puissance batterie vers réseau"
      }
    },
    "button": {
      "restart": {
        "name": "Redémarrer"
      },
      "turn_off": {
        "name": "Éteindre"
      },
      "turn_on": {
        "name": "Allumer"
      },
      "enable_performance_data_mode": {
        "name": "Expérimental: Activer le mode de données de performance"
      }
    }
  },
  "device": {
    "inverter": {
      "name": "Onduleur"
    },
    "dtu": {
      "name": "DTU"
    },
    "meter": {
      "name": "Compteur"
    },
    "hybrid_inverter": {
      "name": "Onduleur hybride"
    }
  },
  "services": {
    "set_bms_mode": {
      "name": "Définir le mode BMS",
      "description": "Définir le mode BMS de la batterie connectée.",
      "fields": {
        "bms_mode": {
          "name": "Mode BMS",
          "description": "Le mode de fonctionnement BMS à définir. Valeurs possibles : 'self_use', 'economic', 'backup_power', 'pure_off_grid', 'forced_charging', 'forced_discharge', 'peak_shaving', 'time_of_use'"
        },
        "rev_soc": {
          "name": "SOC réservé",
          "description": "L’état de charge (SOC) réservé à définir (en %)."
        },
        "max_power": {
          "name": "Puissance maximale",
          "description": "La puissance maximale de charge/décharge à définir (en %)."
        },
        "peak_soc": {
          "name": "SOC de pointe",
          "description": "L’état de charge (SOC) de pointe à définir (en %)."
        },
        "peak_meter_power": {
          "name": "Puissance de pointe du compteur",
          "description": "La puissance de pointe du compteur à définir (en W)."
        },
        "time_settings": {
          "name": "Paramètres temporels",
          "description": "Configurer les paramètres liés au mode heures d’utilisation."
        },
        "time_periods": {
          "name": "Périodes horaires",
          "description": "Définir les périodes pour le mode heures d’utilisation."
        }
      }
    }
  },
  "selector": {
    "bms_mode_type": {
      "options": {
        "self_use": "Mode autoconsommation",
        "economic": "Mode économique",
        "backup_power": "Mode secours",
        "pure_off_grid": "Mode hors réseau",
        "forced_charging": "Mode charge forcée",
        "forced_discharge": "Mode décharge forcée",
        "peak_shaving": "Mode écrêtage de pointe",
        "time_of_use": "Mode heures d’utilisation"
      }
    }
  }
}


================================================
FILE: custom_components/hoymiles_wifi/util.py
================================================
"""Utils for hoymiles-wifi."""

from typing import Union
import asyncio
import logging

from hoymiles_wifi.dtu import DTU
from hoymiles_wifi.hoymiles import generate_inverter_serial_number
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant

from hoymiles_wifi.const import IS_ENCRYPTED_BIT_INDEX

from .error import CannotConnect

from .const import CONF_ENC_RAND, DEFAULT_TIMEOUT_SECONDS

_LOGGER = logging.getLogger(__name__)


async def async_get_config_entry_data_for_host(
    host,
) -> tuple[
    str,
    list[str],
    list[dict[str, Union[str, int]]],
    list[dict[str, Union[str, int]]],
    list[dict[str, Union[str, int]]],
    bool,
    str,
]:
    """Get data for config entry from host."""

    single_phase_inverters = []
    three_phase_inverters = []
    ports = []
    meters = []
    hybrid_inverters = []
    dtu_sn = None
    is_encrypted = False
    enc_rand = ""

    dtu = DTU(host, timeout=DEFAULT_TIMEOUT_SECONDS)

    app_information_data = await dtu.async_app_information_data()

    if app_information_data and app_information_data.dtu_info.dfs:
        if is_encrypted_dtu(app_information_data.dtu_info.dfs):
            logging.debug("DTU is encrypted.")
            is_encrypted = True
            enc_rand = app_information_data.dtu_info.enc_rand.hex()
            dtu = DTU(
                host,
                is_encrypted=is_encrypted,
                enc_rand=bytes.fromhex(enc_rand),
                timeout=DEFAULT_TIMEOUT_SECONDS,
            )
            await asyncio.sleep(2)

    logging.debug("Trying get_real_data_new()!")
    real_data = await dtu.async_get_real_data_new()
    logging.debug(f"RealDataNew call done. Result: {real_data}")

    if real_data:
        dtu_sn = real_data.device_serial_number

        single_phase_inverters = [
            generate_inverter_serial_number(sgs_data.serial_number)
            for sgs_data in real_data.sgs_data
        ]

        three_phase_inverters = [
            generate_inverter_serial_number(tgs_data.serial_number)
            for tgs_data in real_data.tgs_data
        ]

        ports = [
            {
                "inverter_serial_number": generate_inverter_serial_number(
                    pv_data.serial_number
                ),
                "port_number": pv_data.port_number,
            }
            for pv_data in real_data.pv_data
        ]

        meters = [
            {
                "meter_serial_number": generate_inverter_serial_number(
                    meter_data.serial_number
                ),
                "device_type": meter_data.device_type,
            }
            for meter_data in real_data.meter_data
        ]
    else:
        logging.debug(
            "RealDataNew is None. Sleeping for 5s before trying get_gateway_info()!"
        )
        await asyncio.sleep(5)
        gateway_info = await dtu.async_get_gateway_info()
        logging.debug(f"GatewayInfo call done. Result: {gateway_info}")

        if gateway_info:
            logging.debug("Trying get energy storage registry call.")
            registry = await dtu.async_get_energy_storage_registry(
                dtu_serial_number=gateway_info.serial_number
            )
            logging.debug(f"Get energy storage registry call done. Result: {registry}")

            if registry:
                dtu_sn = str(gateway_info.serial_number)

                hybrid_inverters = [
                    {
                        "inverter_serial_number": inverter.serial_number,
                        "model_name": inverter.model_name,
                    }
                    for inverter in registry.inverters
                ]
                logging.debug(f"Hybrid inverters: {hybrid_inverters}")
            else:
                logging.error(
                    "Energy storage registry is None. Cannot connect to DTU or invalid response received!"
                )
                raise CannotConnect
        else:
            logging.error(
                "RealDataNew and GatewayInfo is None. Cannot connect to DTU or invalid response received!"
            )
            raise CannotConnect

    return (
        dtu_sn,
        single_phase_inverters,
        three_phase_inverters,
        ports,
        meters,
        hybrid_inverters,
        is_encrypted,
        enc_rand,
    )


def is_encrypted_dtu(dfs: int) -> bool:
    """Check if the DTU is encrypted."""
    return (dfs >> IS_ENCRYPTED_BIT_INDEX) & 1


async def async_check_and_update_enc_rand(
    hass: HomeAssistant, config_entry: ConfigEntry, dtu: DTU, enc_rand: str
) -> None:
    """Check and update the enc_rand if necessary."""
    enc_rand_old = config_entry.data.get(CONF_ENC_RAND, None)

    if enc_rand_old is None or enc_rand_old != enc_rand:
        _LOGGER.debug(
            "Updating enc_rand in config entry and DTU from %s to %s",
            enc_rand_old,
            enc_rand,
        )
        dtu.enc_rand = bytes.fromhex(enc_rand)
        new_data = {**config_entry.data, CONF_ENC_RAND: enc_rand}
        await hass.config_entries.async_update_entry(config_entry, data=new_data)


================================================
FILE: hacs.json
================================================
{
  "name": "Hoymiles",
  "render_readme": true,
  "iot_class": "local_polling",
  "homeassistant": "2025.6.0"
}


================================================
FILE: requirements.test.txt
================================================
pytest
pytest-cov>=4.1.0
pytest-asyncio>=0.23.5
pytest-homeassistant-custom-component
hoymiles-wifi>=0.2.1



================================================
FILE: setup.cfg
================================================
[coverage:run]
source =
  custom_components

[coverage:report]
exclude_lines =
    pragma: no cover
    raise NotImplemented()
    if __name__ == '__main__':
    main()
show_missing = true

[tool:pytest]
asyncio_mode = auto
testpaths = tests
norecursedirs = .git
addopts =
    --strict
    --cov=custom_components

[flake8]
# https://github.com/ambv/black#line-length
max-line-length = 88
# E501: line too long
# W503: Line break occurred before a binary operator
# E203: Whitespace before ':'
# D202 No blank lines allowed after function docstring
# W504 line break after binary operator
ignore =
    E501,
    W503,
    E203,
    D202,
    W504

[isort]
# https://github.com/timothycrosley/isort
# https://github.com/timothycrosley/isort/wiki/isort-Settings
# splits long import on multiple lines indented by 4 spaces
multi_line_output = 3
include_trailing_comma=True
force_grid_wrap=0
use_parentheses=True
line_length=88
indent = "    "
# by default isort don't check module indexes
not_skip = __init__.py
# will group `import x` and `from x import` of the same module.
force_sort_within_sections = true
sections = FUTURE,STDLIB,INBETWEENS,THIRDPARTY,FIRSTPARTY,LOCALFOLDER
default_section = THIRDPARTY
known_first_party = custom_components,tests
forced_separate = tests
combine_as_imports = true

[mypy]
python_version = 3.10
ignore_errors = true
follow_imports = silent
ignore_missing_imports = true
warn_incomplete_stub = true
warn_redundant_casts = true
warn_unused_configs = true


================================================
FILE: tests/__init__.py
================================================


================================================
FILE: tests/bandit.yaml
================================================
# https://bandit.readthedocs.io/en/latest/config.html

tests:
  - B108
  - B306
  - B307
  - B313
  - B314
  - B315
  - B316
  - B317
  - B318
  - B319
  - B320
  - B325
  - B602
  - B604


================================================
FILE: tests/conftest.py
================================================
"""Global fixtures for hoymiles_wifi integration."""

# Fixtures allow you to replace functions with a Mock object. You can perform
# many options via the Mock to reflect a particular behavior from the original
# function that you want to see without going through the function's actual logic.
# Fixtures can either be passed into tests as parameters, or if autouse=True, they
# will automatically be used across all tests.
#
# Fixtures that are defined in conftest.py are available across all tests. You can also
# define fixtures within a particular test file to scope them locally.
#
# pytest_homeassistant_custom_component provides some fixtures that are provided by
# Home Assistant core. You can find those fixture definitions here:
# https://github.com/MatthewFlamm/pytest-homeassistant-custom-component/blob/master/pytest_homeassistant_custom_component/common.py
#
# See here for more info: https://docs.pytest.org/en/latest/fixture.html (note that
# pytest includes fixtures OOB which you can use as defined on this page)
import pytest

pytest_plugins = "pytest_homeassistant_custom_component"


@pytest.fixture(autouse=True)
def auto_enable_custom_integrations(enable_custom_integrations):
    """Enable custom integrations"""
    yield


================================================
FILE: tests/test_config_flow.py
================================================
"""Unit tests for the Hoymiles config flow."""

from json import JSONDecodeError
from unittest.mock import patch

import pytest

from homeassistant import config_entries
from custom_components.hoymiles_wifi.const import (
    DOMAIN,
    CONF_UPDATE_INTERVAL,
    CONF_INVERTERS,
    CONF_PORTS,
    CONF_DTU_SERIAL_NUMBER,
    DEFAULT_UPDATE_INTERVAL_SECONDS,
)
from custom_components.hoymiles_wifi.error import CannotConnect

from homeassistant.const import CONF_HOST
from homeassistant.core import HomeAssistant
from homeassistant.data_entry_flow import FlowResultType

from pytest_homeassistant_custom_component.common import MockConfigEntry

from hoymiles_wifi.protobuf import (
    RealDataNew_pb2,
)

DTU_TEST_HOST = "DTUBI-123456789101.lan"

DTU_TEST_SERIAL_NUMBER = "414312345678"

MOCK_DATA_STEP = {
    CONF_HOST: DTU_TEST_HOST,
    CONF_UPDATE_INTERVAL: DEFAULT_UPDATE_INTERVAL_SECONDS,
}

MOCK_DATA_RESULT = {
    CONF_HOST: DTU_TEST_HOST,
    CONF_DTU_SERIAL_NUMBER: DTU_TEST_SERIAL_NUMBER,
    CONF_UPDATE_INTERVAL: DEFAULT_UPDATE_INTERVAL_SECONDS,
    CONF_INVERTERS: [],
    CONF_PORTS: [],
}


MOCK_DATA_REAL_DATA_NEW = RealDataNew_pb2.RealDataNewReqDTO()
MOCK_DATA_REAL_DATA_NEW.device_serial_number = DTU_TEST_SERIAL_NUMBER


async def test_form_valid_input(hass: HomeAssistant) -> None:
    """Test handling valid user input."""

    result = await hass.config_entries.flow.async_init(
        DOMAIN, context={"source": config_entries.SOURCE_USER}
    )
    assert result["type"] == FlowResultType.FORM
    assert result["errors"] == {}

    with (
        patch(
            "custom_components.hoymiles_wifi.async_setup_entry",
            return_value=True,
        ) as mock_setup_entry,
        patch(
            "hoymiles_wifi.dtu.DTU.async_get_real_data_new",
            return_value=MOCK_DATA_REAL_DATA_NEW,
        ) as mock_async_get_real_data_new,
    ):
        result2 = await hass.config_entries.flow.async_configure(
            result["flow_id"],
            MOCK_DATA_STEP,
        )
    await hass.async_block_till_done()

    assert result2["type"] == FlowResultType.CREATE_ENTRY
    assert result2["title"] == MOCK_DATA_STEP[CONF_HOST]
    assert result2["data"] == MOCK_DATA_RESULT
    assert len(mock_setup_entry.mock_calls) == 1
    assert len(mock_async_get_real_data_new.mock_calls) == 1


@pytest.mark.parametrize(
    ("raise_error", "text_error"),
    [
        (CannotConnect("Test hoymiles exception"), "cannot_connect"),
    ],
)
async def test_flow_user_init_data_error_and_recover(
    hass: HomeAssistant, raise_error, text_error
) -> None:
    """Test exceptions and recovery."""
    result = await hass.config_entries.flow.async_init(
        DOMAIN, context={"source": config_entries.SOURCE_USER}
    )
    assert result["type"] == FlowResultType.FORM
    assert result["errors"] == {}

    with patch(
        "custom_components.hoymiles_wifi.util.DTU.async_get_real_data_new",
        side_effect=raise_error,
    ) as mock_async_get_config_entry_data_for_host:
        result2 = await hass.config_entries.flow.async_configure(
            result["flow_id"],
            MOCK_DATA_STEP,
        )
        await hass.async_block_till_done()

    assert result2["type"] == FlowResultType.FORM
    assert result2["errors"] == {"base": text_error}

    assert len(mock_async_get_config_entry_data_for_host.mock_calls) == 1

    # Recover
    with (
        patch(
            "custom_components.hoymiles_wifi.async_setup_entry",
            return_value=True,
        ) as mock_setup_entry,
        patch(
            "hoymiles_wifi.dtu.DTU.async_get_real_data_new",
            return_value=MOCK_DATA_REAL_DATA_NEW,
        ) as mock_async_get_real_data_new,
    ):
        result3 = await hass.config_entries.flow.async_configure(
            result["flow_id"],
            MOCK_DATA_STEP,
        )

    await hass.async_block_till_done()

    assert result3["type"] == FlowResultType.CREATE_ENTRY
    assert result3["title"] == MOCK_DATA_STEP[CONF_H
Download .txt
gitextract_y3ez4czx/

├── .github/
│   ├── FUNDING.yaml
│   ├── ISSUE_TEMPLATE/
│   │   ├── bug_report.md
│   │   └── feature_request.md
│   └── workflows/
│       └── close_inactive_issues.yml
├── .gitignore
├── .pre-commit-config.yaml
├── LICENSE
├── README.md
├── custom_components/
│   ├── __init__.py
│   └── hoymiles_wifi/
│       ├── __init__.py
│       ├── binary_sensor.py
│       ├── button.py
│       ├── config_flow.py
│       ├── const.py
│       ├── coordinator.py
│       ├── entity.py
│       ├── error.py
│       ├── manifest.json
│       ├── number.py
│       ├── sensor.py
│       ├── services.py
│       ├── services.yaml
│       ├── strings.json
│       ├── translations/
│       │   ├── de.json
│       │   ├── en.json
│       │   └── fr.json
│       └── util.py
├── hacs.json
├── requirements.test.txt
├── setup.cfg
└── tests/
    ├── __init__.py
    ├── bandit.yaml
    ├── conftest.py
    ├── test_config_flow.py
    └── test_init.py
Download .txt
SYMBOL INDEX (95 symbols across 14 files)

FILE: custom_components/hoymiles_wifi/__init__.py
  function async_setup (line 77) | async def async_setup(hass: HomeAssistant, config: ConfigType):
  function async_setup_entry (line 82) | async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEnt...
  function async_remove_config_entry_device (line 177) | async def async_remove_config_entry_device(
  function async_migrate_entry (line 184) | async def async_migrate_entry(hass: HomeAssistant, config_entry: ConfigE...
  function async_unload_entry (line 237) | async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) ->...

FILE: custom_components/hoymiles_wifi/binary_sensor.py
  class HoymilesBinarySensorEntityDescription (line 30) | class HoymilesBinarySensorEntityDescription(
  function async_setup_entry (line 47) | async def async_setup_entry(
  class HoymilesInverterSensorEntity (line 75) | class HoymilesInverterSensorEntity(HoymilesCoordinatorEntity, BinarySens...
    method __init__ (line 78) | def __init__(
    method _handle_coordinator_update (line 92) | def _handle_coordinator_update(self) -> None:
    method is_on (line 98) | def is_on(self):
    method update_state_value (line 102) | def update_state_value(self):

FILE: custom_components/hoymiles_wifi/button.py
  class HoymilesButtonEntityDescription (line 28) | class HoymilesButtonEntityDescription(
  function async_setup_entry (line 71) | async def async_setup_entry(
  class HoymilesButtonEntity (line 108) | class HoymilesButtonEntity(HoymilesEntity, ButtonEntity):
    method __init__ (line 111) | def __init__(
    method async_press (line 121) | async def async_press(self) -> None:

FILE: custom_components/hoymiles_wifi/config_flow.py
  class HoymilesInverterConfigFlowHandler (line 57) | class HoymilesInverterConfigFlowHandler(ConfigFlow, domain=DOMAIN):
    method async_step_user (line 62) | async def async_step_user(
    method async_step_reconfigure (line 113) | async def async_step_reconfigure(

FILE: custom_components/hoymiles_wifi/coordinator.py
  class HoymilesDataUpdateCoordinator (line 21) | class HoymilesDataUpdateCoordinator(DataUpdateCoordinator):
    method __init__ (line 24) | def __init__(
    method get_dtu (line 44) | def get_dtu(self) -> DTU:
  class HoymilesRealDataUpdateCoordinator (line 49) | class HoymilesRealDataUpdateCoordinator(HoymilesDataUpdateCoordinator):
    method _async_update_data (line 52) | async def _async_update_data(self):
  class HoymilesConfigUpdateCoordinator (line 65) | class HoymilesConfigUpdateCoordinator(HoymilesDataUpdateCoordinator):
    method _async_update_data (line 68) | async def _async_update_data(self):
  class HoymilesAppInfoUpdateCoordinator (line 80) | class HoymilesAppInfoUpdateCoordinator(HoymilesDataUpdateCoordinator):
    method _async_update_data (line 83) | async def _async_update_data(self):
  class HoymilesGatewayInfoUpdateCoordinator (line 105) | class HoymilesGatewayInfoUpdateCoordinator(HoymilesDataUpdateCoordinator):
    method _async_update_data (line 108) | async def _async_update_data(self):
  class HoymilesGatewayNetworkInfoUpdateCoordinator (line 119) | class HoymilesGatewayNetworkInfoUpdateCoordinator(HoymilesDataUpdateCoor...
    method _async_update_data (line 122) | async def _async_update_data(self):
  class HoymilesEnergyStorageUpdateCoordinator (line 137) | class HoymilesEnergyStorageUpdateCoordinator(HoymilesDataUpdateCoordinat...
    method __init__ (line 140) | def __init__(
    method _async_update_data (line 153) | async def _async_update_data(self):

FILE: custom_components/hoymiles_wifi/entity.py
  class DeviceType (line 27) | class DeviceType(Enum):
  class HoymilesEntityDescription (line 36) | class HoymilesEntityDescription(EntityDescription):
  class HoymilesEntity (line 46) | class HoymilesEntity(Entity):
    method __init__ (line 51) | def __init__(self, config_entry: ConfigEntry, description: EntityDescr...
  class HoymilesCoordinatorEntity (line 105) | class HoymilesCoordinatorEntity(CoordinatorEntity, HoymilesEntity):
    method __init__ (line 108) | def __init__(

FILE: custom_components/hoymiles_wifi/error.py
  class CannotConnect (line 6) | class CannotConnect(HomeAssistantError):

FILE: custom_components/hoymiles_wifi/number.py
  class SetAction (line 30) | class SetAction(Enum):
  class HoymilesNumberSensorEntityDescriptionMixin (line 37) | class HoymilesNumberSensorEntityDescriptionMixin:
  class HoymilesNumberSensorEntityDescription (line 42) | class HoymilesNumberSensorEntityDescription(
  function async_setup_entry (line 68) | async def async_setup_entry(
  class HoymilesNumberEntity (line 95) | class HoymilesNumberEntity(HoymilesCoordinatorEntity, NumberEntity):
    method __init__ (line 98) | def __init__(
    method _handle_coordinator_update (line 115) | def _handle_coordinator_update(self) -> None:
    method native_value (line 121) | def native_value(self) -> float:
    method assumed_state (line 126) | def assumed_state(self):
    method async_set_native_value (line 130) | async def async_set_native_value(self, value: float) -> None:
    method update_state_value (line 150) | def update_state_value(self):

FILE: custom_components/hoymiles_wifi/sensor.py
  class ConversionAction (line 59) | class ConversionAction(Enum):
  class HoymilesSensorEntityDescriptionMixin (line 66) | class HoymilesSensorEntityDescriptionMixin:
  class HoymilesSensorEntityDescription (line 71) | class HoymilesSensorEntityDescription(
  class HoymilesEnergyStorageSensorEntityDescription (line 86) | class HoymilesEnergyStorageSensorEntityDescription(
  class HoymilesDiagnosticEntityDescription (line 101) | class HoymilesDiagnosticEntityDescription(
  function async_setup_entry (line 1151) | async def async_setup_entry(
  function get_sensors_for_description (line 1271) | def get_sensors_for_description(
  function get_sensors_for_hybrid_inverter_description (line 1344) | def get_sensors_for_hybrid_inverter_description(
  class HoymilesDataSensorEntity (line 1411) | class HoymilesDataSensorEntity(HoymilesCoordinatorEntity, RestoreSensor):
    method __init__ (line 1414) | def __init__(
    method _handle_coordinator_update (line 1436) | def _handle_coordinator_update(self) -> None:
    method native_value (line 1442) | def native_value(self):
    method assumed_state (line 1466) | def assumed_state(self):
    method update_state_value (line 1470) | def update_state_value(self):
    method async_added_to_hass (line 1538) | async def async_added_to_hass(self) -> None:
  class HoymilesEnergySensorEntity (line 1547) | class HoymilesEnergySensorEntity(HoymilesDataSensorEntity, RestoreSensor):
    method __init__ (line 1550) | def __init__(
    method schedule_midnight_reset (line 1561) | def schedule_midnight_reset(self, reset_sensor_value: bool = True):
    method reset_sensor_value (line 1573) | def reset_sensor_value(self):
    method native_value (line 1578) | def native_value(self):
    method async_added_to_hass (line 1593) | async def async_added_to_hass(self) -> None:
  class HoymilesDiagnosticSensorEntity (line 1605) | class HoymilesDiagnosticSensorEntity(
    method __init__ (line 1610) | def __init__(self, config_entry, description, coordinator):
    method _handle_coordinator_update (line 1624) | def _handle_coordinator_update(self) -> None:
    method native_value (line 1630) | def native_value(self):
    method update_state_value (line 1640) | def update_state_value(self):
    method async_added_to_hass (line 1672) | async def async_added_to_hass(self) -> None:
  class HoymilesEnergyStorageSensorEntity (line 1680) | class HoymilesEnergyStorageSensorEntity(HoymilesCoordinatorEntity, Resto...
    method __init__ (line 1683) | def __init__(
    method _handle_coordinator_update (line 1705) | def _handle_coordinator_update(self) -> None:
    method native_value (line 1711) | def native_value(self):
    method assumed_state (line 1735) | def assumed_state(self):
    method update_state_value (line 1739) | def update_state_value(self):

FILE: custom_components/hoymiles_wifi/services.py
  function async_handle_set_bms_mode (line 19) | async def async_handle_set_bms_mode(call: ServiceCall):

FILE: custom_components/hoymiles_wifi/util.py
  function async_get_config_entry_data_for_host (line 21) | async def async_get_config_entry_data_for_host(
  function is_encrypted_dtu (line 145) | def is_encrypted_dtu(dfs: int) -> bool:
  function async_check_and_update_enc_rand (line 150) | async def async_check_and_update_enc_rand(

FILE: tests/conftest.py
  function auto_enable_custom_integrations (line 24) | def auto_enable_custom_integrations(enable_custom_integrations):

FILE: tests/test_config_flow.py
  function test_form_valid_input (line 51) | async def test_form_valid_input(hass: HomeAssistant) -> None:
  function test_flow_user_init_data_error_and_recover (line 89) | async def test_flow_user_init_data_error_and_recover(

FILE: tests/test_init.py
  function test_async_setup (line 8) | async def test_async_setup(hass):
Condensed preview — 35 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (210K chars).
[
  {
    "path": ".github/FUNDING.yaml",
    "chars": 47,
    "preview": "github: suaveolent\nbuy_me_a_coffee: suaveolent\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/bug_report.md",
    "chars": 834,
    "preview": "---\nname: Bug report\nabout: Create a report to help us improve\ntitle: ''\nlabels: ''\nassignees: ''\n\n---\n\n**Describe the b"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/feature_request.md",
    "chars": 595,
    "preview": "---\nname: Feature request\nabout: Suggest an idea for this project\ntitle: ''\nlabels: ''\nassignees: ''\n\n---\n\n**Is your fea"
  },
  {
    "path": ".github/workflows/close_inactive_issues.yml",
    "chars": 806,
    "preview": "name: Close inactive issues\non:\n  schedule:\n    - cron: \"30 1 * * *\"\n\njobs:\n  close-issues:\n    runs-on: ubuntu-latest\n "
  },
  {
    "path": ".gitignore",
    "chars": 3480,
    "preview": "# Byte-compiled / optimized / DLL files\n__pycache__/\n*.py[cod]\n*$py.class\n\n# C extensions\n*.so\n\n# Distribution / packagi"
  },
  {
    "path": ".pre-commit-config.yaml",
    "chars": 1685,
    "preview": "repos:\n  - repo: https://github.com/asottile/pyupgrade\n    rev: v2.3.0\n    hooks:\n      - id: pyupgrade\n        args: [-"
  },
  {
    "path": "LICENSE",
    "chars": 1067,
    "preview": "MIT License\n\nCopyright (c) 2023 suaveolent\n\nPermission is hereby granted, free of charge, to any person obtaining a copy"
  },
  {
    "path": "README.md",
    "chars": 5270,
    "preview": "# Hoymiles for Home Assistant\n\nThis custom component integrates Hoymiles DTUs, HMS-XXXXW microinverters and hybrid inver"
  },
  {
    "path": "custom_components/__init__.py",
    "chars": 0,
    "preview": ""
  },
  {
    "path": "custom_components/hoymiles_wifi/__init__.py",
    "chars": 8270,
    "preview": "\"\"\"Platform for retrieving values of a Hoymiles inverter.\"\"\"\n\nfrom datetime import timedelta\nimport logging\nimport volup"
  },
  {
    "path": "custom_components/hoymiles_wifi/binary_sensor.py",
    "chars": 3393,
    "preview": "\"\"\"Contains binary sensor entities for Hoymiles WiFi integration.\"\"\"\n\nimport dataclasses\nfrom dataclasses import datacla"
  },
  {
    "path": "custom_components/hoymiles_wifi/button.py",
    "chars": 4532,
    "preview": "\"\"\"Support for Hoymiles buttons.\"\"\"\n\nimport dataclasses\nfrom inspect import signature\nfrom dataclasses import dataclass\n"
  },
  {
    "path": "custom_components/hoymiles_wifi/config_flow.py",
    "chars": 6726,
    "preview": "\"\"\"Config flow for Hoymiles.\"\"\"\n\nfrom datetime import timedelta\nimport logging\nfrom typing import Any\n\nimport voluptuous"
  },
  {
    "path": "custom_components/hoymiles_wifi/const.py",
    "chars": 1591,
    "preview": "\"\"\"Constants for the Hoymiles integration.\"\"\"\n\nDOMAIN = \"hoymiles_wifi\"\nNAME = \"Hoymiles\"\nDOMAIN = \"hoymiles_wifi\"\nDOMAI"
  },
  {
    "path": "custom_components/hoymiles_wifi/coordinator.py",
    "chars": 5472,
    "preview": "\"\"\"Coordinator for Hoymiles integration.\"\"\"\n\nfrom datetime import timedelta\nimport logging\n\nimport homeassistant\nfrom ho"
  },
  {
    "path": "custom_components/hoymiles_wifi/entity.py",
    "chars": 3791,
    "preview": "\"\"\"Entity base for Hoymiles entities.\"\"\"\n\nfrom dataclasses import dataclass\nimport logging\n\nfrom enum import Enum\n\nfrom "
  },
  {
    "path": "custom_components/hoymiles_wifi/error.py",
    "chars": 179,
    "preview": "\"\"\"Errors for hoymiles-wifi.\"\"\"\n\nfrom homeassistant.exceptions import HomeAssistantError\n\n\nclass CannotConnect(HomeAssis"
  },
  {
    "path": "custom_components/hoymiles_wifi/manifest.json",
    "chars": 375,
    "preview": "{\n  \"codeowners\": [\"@suaveolent\"],\n  \"config_flow\": true,\n  \"dependencies\": [],\n  \"documentation\": \"https://github.com/s"
  },
  {
    "path": "custom_components/hoymiles_wifi/number.py",
    "chars": 4993,
    "preview": "\"\"\"Support for Hoymiles number sensors.\"\"\"\n\nimport dataclasses\nfrom dataclasses import dataclass\nfrom enum import Enum\ni"
  },
  {
    "path": "custom_components/hoymiles_wifi/sensor.py",
    "chars": 72129,
    "preview": "\"\"\"Support for Hoymiles sensors.\"\"\"\n\nimport dataclasses\nfrom dataclasses import dataclass\nfrom datetime import datetime,"
  },
  {
    "path": "custom_components/hoymiles_wifi/services.py",
    "chars": 4039,
    "preview": "from homeassistant.core import ServiceCall\nfrom hoymiles_wifi.dtu import DTU\n\nfrom hoymiles_wifi.hoymiles import BMSWork"
  },
  {
    "path": "custom_components/hoymiles_wifi/services.yaml",
    "chars": 1544,
    "preview": "set_bms_mode:\n  name: Set BMS Working Mode\n  description: Sets the working mode of the Hoymiles hybrid inverter.\n  targe"
  },
  {
    "path": "custom_components/hoymiles_wifi/strings.json",
    "chars": 12726,
    "preview": "{\n  \"config\": {\n    \"step\": {\n      \"user\": {\n        \"title\": \"Hoymiles DTU connection\",\n        \"description\": \"If you"
  },
  {
    "path": "custom_components/hoymiles_wifi/translations/de.json",
    "chars": 12976,
    "preview": "{\n  \"config\": {\n    \"step\": {\n      \"user\": {\n        \"title\": \"Hoymiles Verbindung\",\n        \"description\": \"Wenn Sie H"
  },
  {
    "path": "custom_components/hoymiles_wifi/translations/en.json",
    "chars": 12571,
    "preview": "{\n  \"config\": {\n    \"step\": {\n      \"user\": {\n        \"title\": \"Hoymiles DTU connection\",\n        \"description\": \"If you"
  },
  {
    "path": "custom_components/hoymiles_wifi/translations/fr.json",
    "chars": 13828,
    "preview": "{\r\n  \"config\": {\r\n    \"step\": {\r\n      \"user\": {\r\n        \"title\": \"Connexion DTU Hoymiles\",\r\n        \"description\": \"Si"
  },
  {
    "path": "custom_components/hoymiles_wifi/util.py",
    "chars": 5158,
    "preview": "\"\"\"Utils for hoymiles-wifi.\"\"\"\n\nfrom typing import Union\nimport asyncio\nimport logging\n\nfrom hoymiles_wifi.dtu import DT"
  },
  {
    "path": "hacs.json",
    "chars": 113,
    "preview": "{\n  \"name\": \"Hoymiles\",\n  \"render_readme\": true,\n  \"iot_class\": \"local_polling\",\n  \"homeassistant\": \"2025.6.0\"\n}\n"
  },
  {
    "path": "requirements.test.txt",
    "chars": 108,
    "preview": "pytest\npytest-cov>=4.1.0\npytest-asyncio>=0.23.5\npytest-homeassistant-custom-component\nhoymiles-wifi>=0.2.1\n\n"
  },
  {
    "path": "setup.cfg",
    "chars": 1488,
    "preview": "[coverage:run]\nsource =\n  custom_components\n\n[coverage:report]\nexclude_lines =\n    pragma: no cover\n    raise NotImpleme"
  },
  {
    "path": "tests/__init__.py",
    "chars": 0,
    "preview": ""
  },
  {
    "path": "tests/bandit.yaml",
    "chars": 188,
    "preview": "# https://bandit.readthedocs.io/en/latest/config.html\n\ntests:\n  - B108\n  - B306\n  - B307\n  - B313\n  - B314\n  - B315\n  - "
  },
  {
    "path": "tests/conftest.py",
    "chars": 1247,
    "preview": "\"\"\"Global fixtures for hoymiles_wifi integration.\"\"\"\n\n# Fixtures allow you to replace functions with a Mock object. You "
  },
  {
    "path": "tests/test_config_flow.py",
    "chars": 4173,
    "preview": "\"\"\"Unit tests for the Hoymiles config flow.\"\"\"\n\nfrom json import JSONDecodeError\nfrom unittest.mock import patch\n\nimport"
  },
  {
    "path": "tests/test_init.py",
    "chars": 284,
    "preview": "\"\"\"Test component setup.\"\"\"\n\nfrom homeassistant.setup import async_setup_component\n\nfrom custom_components.hoymiles_wifi"
  }
]

About this extraction

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

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

Copied to clipboard!