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.
[](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



## 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
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
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.