Repository: suaveolent/ha-hoymiles-wifi Branch: main Commit: 2f6613ef2c49 Files: 35 Total size: 191.1 KB Directory structure: gitextract_y3ez4czx/ ├── .github/ │ ├── FUNDING.yaml │ ├── ISSUE_TEMPLATE/ │ │ ├── bug_report.md │ │ └── feature_request.md │ └── workflows/ │ └── close_inactive_issues.yml ├── .gitignore ├── .pre-commit-config.yaml ├── LICENSE ├── README.md ├── custom_components/ │ ├── __init__.py │ └── hoymiles_wifi/ │ ├── __init__.py │ ├── binary_sensor.py │ ├── button.py │ ├── config_flow.py │ ├── const.py │ ├── coordinator.py │ ├── entity.py │ ├── error.py │ ├── manifest.json │ ├── number.py │ ├── sensor.py │ ├── services.py │ ├── services.yaml │ ├── strings.json │ ├── translations/ │ │ ├── de.json │ │ ├── en.json │ │ └── fr.json │ └── util.py ├── hacs.json ├── requirements.test.txt ├── setup.cfg └── tests/ ├── __init__.py ├── bandit.yaml ├── conftest.py ├── test_config_flow.py └── test_init.py ================================================ FILE CONTENTS ================================================ ================================================ FILE: .github/FUNDING.yaml ================================================ github: suaveolent buy_me_a_coffee: suaveolent ================================================ FILE: .github/ISSUE_TEMPLATE/bug_report.md ================================================ --- name: Bug report about: Create a report to help us improve title: '' labels: '' assignees: '' --- **Describe the bug** A clear and concise description of what the bug is. **To Reproduce** Steps to reproduce the behavior: 1. Go to '...' 2. Click on '....' 3. Scroll down to '....' 4. See error **Expected behavior** A clear and concise description of what you expected to happen. **Screenshots** If applicable, add screenshots to help explain your problem. **Desktop (please complete the following information):** - OS: [e.g. iOS] - Browser [e.g. chrome, safari] - Version [e.g. 22] **Smartphone (please complete the following information):** - Device: [e.g. iPhone6] - OS: [e.g. iOS8.1] - Browser [e.g. stock browser, safari] - Version [e.g. 22] **Additional context** Add any other context about the problem here. ================================================ FILE: .github/ISSUE_TEMPLATE/feature_request.md ================================================ --- name: Feature request about: Suggest an idea for this project title: '' labels: '' assignees: '' --- **Is your feature request related to a problem? Please describe.** A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] **Describe the solution you'd like** A clear and concise description of what you want to happen. **Describe alternatives you've considered** A clear and concise description of any alternative solutions or features you've considered. **Additional context** Add any other context or screenshots about the feature request here. ================================================ FILE: .github/workflows/close_inactive_issues.yml ================================================ name: Close inactive issues on: schedule: - cron: "30 1 * * *" jobs: close-issues: runs-on: ubuntu-latest permissions: issues: write pull-requests: write steps: - uses: actions/stale@v5 with: days-before-issue-stale: 30 days-before-issue-close: 14 stale-issue-label: "stale" stale-issue-message: "This issue is stale because it has been open for 30 days with no activity." close-issue-message: "This issue was closed because it has been inactive for 14 days since being marked as stale." exempt-issue-labels: "bug,documentation,enhancement,good first issue,help wanted,question" days-before-pr-stale: -1 days-before-pr-close: -1 repo-token: ${{ secrets.GITHUB_TOKEN }} ================================================ FILE: .gitignore ================================================ # Byte-compiled / optimized / DLL files __pycache__/ *.py[cod] *$py.class # C extensions *.so # Distribution / packaging .Python build/ develop-eggs/ dist/ downloads/ eggs/ .eggs/ lib/ lib64/ parts/ sdist/ var/ wheels/ share/python-wheels/ *.egg-info/ .installed.cfg *.egg MANIFEST # PyInstaller # Usually these files are written by a python script from a template # before PyInstaller builds the exe, so as to inject date/other infos into it. *.manifest *.spec # Installer logs pip-log.txt pip-delete-this-directory.txt # Unit test / coverage reports htmlcov/ .tox/ .nox/ .coverage .coverage.* .cache nosetests.xml coverage.xml *.cover *.py,cover .hypothesis/ .pytest_cache/ cover/ # Translations *.mo *.pot # Django stuff: *.log local_settings.py db.sqlite3 db.sqlite3-journal # Flask stuff: instance/ .webassets-cache # Scrapy stuff: .scrapy # Sphinx documentation docs/_build/ # PyBuilder .pybuilder/ target/ # Jupyter Notebook .ipynb_checkpoints # IPython profile_default/ ipython_config.py # pyenv # For a library or package, you might want to ignore these files since the code is # intended to run in multiple environments; otherwise, check them in: # .python-version # pipenv # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. # However, in case of collaboration, if having platform-specific dependencies or dependencies # having no cross-platform support, pipenv may install dependencies that don't work, or not # install all needed dependencies. #Pipfile.lock # poetry # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. # This is especially recommended for binary packages to ensure reproducibility, and is more # commonly ignored for libraries. # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control #poetry.lock # pdm # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. #pdm.lock # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it # in version control. # https://pdm.fming.dev/#use-with-ide .pdm.toml # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm __pypackages__/ # Celery stuff celerybeat-schedule celerybeat.pid # SageMath parsed files *.sage.py # Environments .env .venv env/ venv/ ENV/ env.bak/ venv.bak/ # Spyder project settings .spyderproject .spyproject # Rope project settings .ropeproject # mkdocs documentation /site # mypy .mypy_cache/ .dmypy.json dmypy.json # Pyre type checker .pyre/ # pytype static type analyzer .pytype/ # Cython debug symbols cython_debug/ # PyCharm # JetBrains specific template is maintained in a separate JetBrains.gitignore that can # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore # and can be added to the global gitignore or merged into this file. For a more nuclear # option (not recommended) you can uncomment the following to ignore the entire idea folder. #.idea/ # General .DS_Store .AppleDouble .LSOverride # Icon must end with two \r Icon # Thumbnails ._* # Files that might appear in the root of a volume .DocumentRevisions-V100 .fseventsd .Spotlight-V100 .TemporaryItems .Trashes .VolumeIcon.icns .com.apple.timemachine.donotpresent # Directories potentially created on remote AFP share .AppleDB .AppleDesktop Network Trash Folder Temporary Items .apdisk ================================================ FILE: .pre-commit-config.yaml ================================================ repos: - repo: https://github.com/asottile/pyupgrade rev: v2.3.0 hooks: - id: pyupgrade args: [--py37-plus] - repo: https://github.com/psf/black rev: 19.10b0 hooks: - id: black args: - --safe - --quiet files: ^((homeassistant|script|tests)/.+)?[^/]+\.py$ - repo: https://github.com/codespell-project/codespell rev: v1.16.0 hooks: - id: codespell args: - --ignore-words-list=hass,alot,datas,dof,dur,farenheit,hist,iff,ines,ist,lightsensor,mut,nd,pres,referer,ser,serie,te,technik,ue,uint,visability,wan,wanna,withing - --skip="./.*,*.csv,*.json" - --quiet-level=2 exclude_types: [csv, json] - repo: https://gitlab.com/pycqa/flake8 rev: 3.8.1 hooks: - id: flake8 additional_dependencies: - flake8-docstrings==1.5.0 - pydocstyle==5.0.2 files: ^(homeassistant|script|tests)/.+\.py$ - repo: https://github.com/PyCQA/bandit rev: 1.6.2 hooks: - id: bandit args: - --quiet - --format=custom - --configfile=tests/bandit.yaml files: ^(homeassistant|script|tests)/.+\.py$ - repo: https://github.com/pre-commit/mirrors-isort rev: v4.3.21 hooks: - id: isort - repo: https://github.com/pre-commit/pre-commit-hooks rev: v2.4.0 hooks: - id: check-executables-have-shebangs stages: [manual] - id: check-json - repo: https://github.com/pre-commit/mirrors-mypy rev: v0.770 hooks: - id: mypy args: - --pretty - --show-error-codes - --show-error-context ================================================ FILE: LICENSE ================================================ MIT License Copyright (c) 2023 suaveolent Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: README.md ================================================ # Hoymiles for Home Assistant This custom component integrates Hoymiles DTUs, HMS-XXXXW microinverters and hybrid inverters into Home Assistant, providing live inverter data. It uses the [hoymiles-wifi](https://github.com/suaveolent/hoymiles-wifi) Python library to communicate directly with the devices over your local network — no cloud connection required. > [!NOTE] > Disclaimer: This library is not affiliated with Hoymiles. It is an independent project developed to provide tools for interacting with Hoymiles DTUs and Hoymiles HMS-XXXXW series micro-inverters featuring integrated WiFi DTU. Any trademarks or product names mentioned are the property of their respective owners. [!["Buy Me A Coffee"](https://www.buymeacoffee.com/assets/img/custom_images/orange_img.png)](https://www.buymeacoffee.com/suaveolent) ## Supported Devices The custom component was successfully tested with: - Hoymiles HMS-400W-1T - Hoymiles HMS-800W-2T - Hoymiles HMS-1000W-2T - Hoymiles HMS-2000DW-4T - Hoymiles DTU-WLite - Hoymiles DTU-Pro (S) - Hoymiles HAS-5.0LV-EUG1 - Hoymiles HYS-4.6LV-EUG1 - Hoymiles HYT-5.0HV-EUG1 - Hoymiles HAT-8.0HV-EUG1 - Solenso H-1000 (not tested for command, only to get data) - Solenso DTU_SLS (not tested for command, only to get data) ## Warning > [!CAUTION] > Please refrain from using the current power limitation feature for zero feed-in, as it may lead to damaging the inverter due to excessive writes to the EEPROM. ## Installation 1. Open the [HACS](https://hacs.xyz) panel in your Home Assistant frontend. 2. Navigate to the "Integrations" tab. 3. Click the three dots in the top-right corner and select "Custom Repositories." 4. Add a new custom repository: - **URL:** `https://github.com/suaveolent/ha-hoymiles-wifi` - **Category:** Integration 5. Click "Add" 6. Click on the `Hoymiles` integration. 7. Click "DOWNLOAD" 8. Navigate to "Settings" - "Devices & Services" 9. Click "ADD INTEGRATION" and select the `Hoymiles` integration. 10. Insert IP address of hoymiles DTUBI-xxxx in field Host and click on SUBMIT > [!NOTE] > Sometimes the necessary lib > (https://github.com/suaveolent/hoymiles-wifi) is not correctly > installed. In this case you need to manually install the library by > running the `pip install hoymiles-wifi` command yourself. ### Option 2: Manual Installation 1. Download the contents of this repository as a ZIP file. 2. Extract the ZIP file. 3. Copy the entire `custom_components/hoymiles-wifi` directory to your Home Assistant 4. Install the python requirements 5. Restart your Home Assistant instance to apply the changes. ### Docker Users: Workaround for HTTP 500 Error If you encounter an HTTP 500 error when adding the integration in a Home Assistant Docker container, follow this workaround: 1. Create a new Docker image for Home Assistant with the `hoymiles-wifi` library pre-installed: ```dockerfile FROM homeassistant/home-assistant RUN pip install hoymiles-wifi ``` 2. Build the new Docker image: ```bash docker build -t ha-hoymiles . ``` 3. Switch to this newly built image when running Home Assistant. ## Configuration Configuration is done in the UI. 1. `Host`: Enter the IP address or the hostname of your inverter or DTU. > [!NOTE] > To find the IP address or hostname of your inverter/DTU, you can either access your router’s web interface to view connected devices, or use a network scanning tool (such as Fing or Angry IP Scanner) to identify the device on your local network. 2. `Update interval (seconds)`: This defines how frequently the system will request data from the inverter or DTU. Enter the desired time in seconds. > [!NOTE] > Setting the update interval below approximately 32 seconds may disable Hoymiles cloud functionality. To ensure proper communication with Hoymiles servers, keep the update interval at or above this threshold. ## Screenshots ![Integration](/screenshots/integration.png?raw=true) ![Devices](/screenshots/devices.png?raw=true) ![Device](/screenshots/device.png?raw=true) ## Caution Use this custom component responsibly and be aware of potential risks. There are no guarantees provided, and any misuse or incorrect implementation may result in undesirable outcomes. Ensure that your inverter is not compromised during communication. ## Known Limitations > [!NOTE] > **Update Frequency:** The library may experience limitations in fetching updates, potentially around twice per minute. The inverter firmware may enforce a mandatory wait period of approximately 30 seconds between requests. > This issue can be identified when the data returned matches the response from the previous request. > If you encounter this, you can try the _experimental_ performance data mode. (Needs to be enabled on each reboot of the DTU.) > [!NOTE] > **Compatibility:** While developed for the HMS-800W-2T inverter, compatibility with other inverters from the series is untested at the time of writing. Exercise caution and conduct thorough testing if using with different inverter models. ## Attribution This project was generated from [@oncleben31](https://github.com/oncleben31)'s [Home Assistant Custom Component Cookiecutter](https://github.com/oncleben31/cookiecutter-homeassistant-custom-component) template. ================================================ FILE: custom_components/__init__.py ================================================ ================================================ FILE: custom_components/hoymiles_wifi/__init__.py ================================================ """Platform for retrieving values of a Hoymiles inverter.""" from datetime import timedelta import logging import voluptuous as vol from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_HOST, Platform from homeassistant.core import HomeAssistant from homeassistant.helpers.device_registry import DeviceEntry from homeassistant.helpers.typing import ConfigType from homeassistant.helpers import config_validation as cv from homeassistant.helpers.service import SupportsResponse from hoymiles_wifi.dtu import DTU from .const import ( CONF_DTU_SERIAL_NUMBER, CONF_HYBRID_INVERTERS, CONF_INVERTERS, CONF_METERS, CONF_PORTS, CONF_THREE_PHASE_INVERTERS, CONF_TIMEOUT, CONF_UPDATE_INTERVAL, CONFIG_VERSION, CONF_IS_ENCRYPTED, CONF_ENC_RAND, DEFAULT_APP_INFO_UPDATE_INTERVAL_SECONDS, DEFAULT_CONFIG_UPDATE_INTERVAL_SECONDS, DEFAULT_TIMEOUT_SECONDS, DOMAIN, HASS_APP_INFO_COORDINATOR, HASS_CONFIG_COORDINATOR, HASS_DATA_COORDINATOR, HASS_DTU, HASS_ENERGY_STORAGE_DATA_COORDINATOR, ) from .coordinator import ( HoymilesAppInfoUpdateCoordinator, HoymilesConfigUpdateCoordinator, HoymilesRealDataUpdateCoordinator, HoymilesEnergyStorageUpdateCoordinator, ) from .error import CannotConnect from .services import async_handle_set_bms_mode from .util import async_get_config_entry_data_for_host _LOGGER = logging.getLogger(__name__) PLATFORMS = [Platform.SENSOR, Platform.NUMBER, Platform.BINARY_SENSOR, Platform.BUTTON] SET_BMS_SCHEMA = vol.Schema( { vol.Required("bms_mode"): vol.In( ( "self_use", "economic", "backup_power", "pure_off_grid", "forced_charging", "forced_discharge", "peak_shaving", "time_of_use", ) ), vol.Required("rev_soc"): vol.All(vol.Coerce(int), vol.Range(min=0, max=100)), vol.Optional("max_power"): vol.All(vol.Coerce(int), vol.Range(min=0)), vol.Optional("peak_soc"): vol.All(vol.Coerce(int), vol.Range(min=0, max=100)), vol.Optional("peak_meter_power"): vol.All(vol.Coerce(int), vol.Range(min=0)), vol.Optional("time_settings"): str, vol.Optional("time_periods"): str, vol.Optional("device_id"): cv.ensure_list, } ) async def async_setup(hass: HomeAssistant, config: ConfigType): """Set up this integration using YAML is not supported.""" return True async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry): """Set up this integration using UI.""" hass.data.setdefault(DOMAIN, {}) hass_data = dict(config_entry.data) host = config_entry.data.get(CONF_HOST) update_interval = timedelta(seconds=config_entry.data.get(CONF_UPDATE_INTERVAL)) single_phase_inverters = config_entry.data[CONF_INVERTERS] three_phase_inverters = config_entry.data.get(CONF_THREE_PHASE_INVERTERS, []) hybrid_inverters = config_entry.data.get(CONF_HYBRID_INVERTERS, []) meters = config_entry.data.get(CONF_METERS, []) is_encrypted = config_entry.data.get(CONF_IS_ENCRYPTED, False) enc_rand = config_entry.data.get(CONF_ENC_RAND, None) timeout = config_entry.data.get(CONF_TIMEOUT, DEFAULT_TIMEOUT_SECONDS) if is_encrypted: dtu = DTU( host, is_encrypted=is_encrypted, enc_rand=bytes.fromhex(enc_rand), timeout=timeout, ) else: dtu = DTU(host, timeout=timeout) hass_data[HASS_DTU] = dtu if single_phase_inverters or three_phase_inverters or meters: data_coordinator = HoymilesRealDataUpdateCoordinator( hass, dtu=dtu, config_entry=config_entry, update_interval=update_interval ) hass_data[HASS_DATA_COORDINATOR] = data_coordinator config_update_interval = timedelta( seconds=DEFAULT_CONFIG_UPDATE_INTERVAL_SECONDS ) config_coordinator = HoymilesConfigUpdateCoordinator( hass=hass, dtu=dtu, config_entry=config_entry, update_interval=config_update_interval, ) hass_data[HASS_CONFIG_COORDINATOR] = config_coordinator app_info_update_interval = timedelta( seconds=DEFAULT_APP_INFO_UPDATE_INTERVAL_SECONDS ) app_info_update_coordinator = HoymilesAppInfoUpdateCoordinator( hass=hass, dtu=dtu, config_entry=config_entry, update_interval=app_info_update_interval, ) hass_data[HASS_APP_INFO_COORDINATOR] = app_info_update_coordinator if hybrid_inverters: energy_storage_data_coordinator = HoymilesEnergyStorageUpdateCoordinator( hass=hass, dtu=dtu, config_entry=config_entry, update_interval=update_interval, dtu_serial_number=config_entry.data[CONF_DTU_SERIAL_NUMBER], inverters=hybrid_inverters, ) hass_data[HASS_ENERGY_STORAGE_DATA_COORDINATOR] = ( energy_storage_data_coordinator ) _LOGGER.debug(f" hass_data: {hass_data}") # --- IGNORE --- _LOGGER.debug(f" config_entry_id: {config_entry.entry_id}") hass.data[DOMAIN][config_entry.entry_id] = hass_data await hass.config_entries.async_forward_entry_setups(config_entry, PLATFORMS) if single_phase_inverters or three_phase_inverters or meters: await data_coordinator.async_config_entry_first_refresh() await config_coordinator.async_config_entry_first_refresh() await app_info_update_coordinator.async_config_entry_first_refresh() if hybrid_inverters: await energy_storage_data_coordinator.async_config_entry_first_refresh() hass.services.async_register( domain=DOMAIN, service="set_bms_mode", service_func=async_handle_set_bms_mode, schema=SET_BMS_SCHEMA, supports_response=SupportsResponse.NONE, ) _LOGGER.debug("Service set_bms_mode registered") return True async def async_remove_config_entry_device( hass: HomeAssistant, config_entry: ConfigEntry, device_entry: DeviceEntry ) -> bool: """Remove a config entry from a device.""" return True async def async_migrate_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool: """Migrate old entry data to the new entry schema.""" data = config_entry.data.copy() current_version = data.get("version", 1) if current_version != CONFIG_VERSION: _LOGGER.info( "Migrating entry %s to version %s", config_entry.entry_id, CONFIG_VERSION ) new = {**config_entry.data} host = config_entry.data.get(CONF_HOST) try: ( dtu_sn, single_phase_inverters, three_phase_inverters, ports, meters, hybrid_inverters, is_encrypted, enc_rand, ) = await async_get_config_entry_data_for_host(host) except CannotConnect: _LOGGER.error( "Could not retrieve real data information data from inverter: %s. Please ensure inverter is available!", host, ) return False new[CONF_DTU_SERIAL_NUMBER] = dtu_sn new[CONF_INVERTERS] = single_phase_inverters new[CONF_THREE_PHASE_INVERTERS] = three_phase_inverters new[CONF_PORTS] = ports new[CONF_METERS] = meters new[CONF_HYBRID_INVERTERS] = hybrid_inverters new[CONF_IS_ENCRYPTED] = is_encrypted new[CONF_ENC_RAND] = enc_rand hass.config_entries.async_update_entry( config_entry, data=new, version=CONFIG_VERSION ) _LOGGER.info( "Migration of entry %s to version %s successful", config_entry.entry_id, CONFIG_VERSION, ) return True async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload a config entry.""" unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) return unload_ok ================================================ FILE: custom_components/hoymiles_wifi/binary_sensor.py ================================================ """Contains binary sensor entities for Hoymiles WiFi integration.""" import dataclasses from dataclasses import dataclass import logging from homeassistant.components.binary_sensor import ( BinarySensorDeviceClass, BinarySensorEntity, BinarySensorEntityDescription, ) from homeassistant.config_entries import ConfigEntry from homeassistant.const import EntityCategory from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity_platform import AddEntitiesCallback from hoymiles_wifi.dtu import NetworkState from .const import ( CONF_DTU_SERIAL_NUMBER, DOMAIN, HASS_DATA_COORDINATOR, HASS_ENERGY_STORAGE_DATA_COORDINATOR, ) from .entity import HoymilesCoordinatorEntity, HoymilesEntityDescription _LOGGER = logging.getLogger(__name__) @dataclass(frozen=True) class HoymilesBinarySensorEntityDescription( HoymilesEntityDescription, BinarySensorEntityDescription ): """Describes Homiles binary sensor entity.""" BINARY_SENSORS = ( HoymilesBinarySensorEntityDescription( key="DTU", translation_key="dtu", device_class=BinarySensorDeviceClass.CONNECTIVITY, entity_category=EntityCategory.DIAGNOSTIC, is_dtu_sensor=True, ), ) async def async_setup_entry( hass: HomeAssistant, config_entry: ConfigEntry, async_add_entities: AddEntitiesCallback, ) -> None: """Set up sensor platform.""" hass_data = hass.data[DOMAIN][config_entry.entry_id] coordinator = hass_data.get(HASS_DATA_COORDINATOR, None) if coordinator is None: coordinator = hass_data.get(HASS_ENERGY_STORAGE_DATA_COORDINATOR, None) dtu_serial_number = config_entry.data[CONF_DTU_SERIAL_NUMBER] hass_data = hass.data[DOMAIN][config_entry.entry_id] sensors = [] for description in BINARY_SENSORS: updated_description = dataclasses.replace( description, serial_number=dtu_serial_number ) sensors.append( HoymilesInverterSensorEntity(config_entry, updated_description, coordinator) ) async_add_entities(sensors) class HoymilesInverterSensorEntity(HoymilesCoordinatorEntity, BinarySensorEntity): """Represents a binary sensor entity for Hoymiles WiFi integration.""" def __init__( self, config_entry: ConfigEntry, description: HoymilesBinarySensorEntityDescription, coordinator: HoymilesCoordinatorEntity, ): """Initialize the HoymilesInverterSensorEntity.""" super().__init__(config_entry, description, coordinator) self._dtu = coordinator.get_dtu() self._native_value = None self.update_state_value() @callback def _handle_coordinator_update(self) -> None: """Handle updated data from the coordinator.""" self.update_state_value() super()._handle_coordinator_update() @property def is_on(self): """Return the state of the binary sensor.""" return self._native_value def update_state_value(self): """Update the state value of the binary sensor based on the DTU's network state.""" dtu_state = self._dtu.get_state() if dtu_state == NetworkState.Online: self._native_value = True elif dtu_state == NetworkState.Offline: self._native_value = False else: self._native_value = None ================================================ FILE: custom_components/hoymiles_wifi/button.py ================================================ """Support for Hoymiles buttons.""" import dataclasses from inspect import signature from dataclasses import dataclass from homeassistant.components.button import ( ButtonDeviceClass, ButtonEntity, ButtonEntityDescription, ) from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback from hoymiles_wifi.dtu import DTU from .const import ( CONF_DTU_SERIAL_NUMBER, CONF_INVERTERS, CONF_THREE_PHASE_INVERTERS, DOMAIN, HASS_DTU, ) from .entity import HoymilesEntity, HoymilesEntityDescription @dataclass(frozen=True) class HoymilesButtonEntityDescription( HoymilesEntityDescription, ButtonEntityDescription ): """Class to describe a Hoymiles Button entity.""" action: str = "" BUTTONS: tuple[HoymilesButtonEntityDescription, ...] = ( HoymilesButtonEntityDescription( key="restart_dtu", translation_key="restart", device_class=ButtonDeviceClass.RESTART, is_dtu_sensor=True, action="async_restart_dtu", ), HoymilesButtonEntityDescription( key="turn_off_inverter_", translation_key="turn_off", icon="mdi:power-off", action="async_turn_off_inverter", ), HoymilesButtonEntityDescription( key="turn_on_inverter_", translation_key="turn_on", icon="mdi:power-on", action="async_turn_on_inverter", ), HoymilesButtonEntityDescription( key="reboot_inverter_", 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 ) 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[].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[].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[].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[].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[].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[].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[].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[].warning_number", translation_key="inverter_warning_number", ), HoymilesSensorEntityDescription( key="tgs_data[].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[].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[].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[].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[].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[].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[].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[].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[].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[.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[.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[.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[].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[].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[].warning_number", translation_key="inverter_warning_number", ), HoymilesSensorEntityDescription( key="pv_data[].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[].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[].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[].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[].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[].error_code", translation_key="port_error_code", ), HoymilesSensorEntityDescription( key="meter_data[].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[].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[].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[].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[].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[].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[].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[].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[].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[].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[].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[].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[].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[].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[].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[].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[].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[].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[].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[].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[].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[].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[].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[].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="[].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="[].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="[].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="[].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="[].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="[].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="[].pv_panels[].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="[].pv_panels[].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="[].pv_panels[].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="[].pv_panels[].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="[].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="[].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="[].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="[].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="[].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="[].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="[].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="[].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="[].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="[].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="[].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="[].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="[].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="[].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="[].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="[].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="[].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="[].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="[].grid.phases[].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="[].grid.phases[].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="[].grid.phases[].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="[].grid.phases[].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="[].grid.phases[].power_factor", translation_key="grid_power_factor_phase", native_unit_of_measurement=PERCENTAGE, device_class=None, state_class=SensorStateClass.MEASUREMENT, ), HoymilesEnergyStorageSensorEntityDescription( key="[].grid.phases[].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="[].grid.phases[].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="[].load.param.status", translation_key="load_status", device_class=SensorDeviceClass.ENUM, ), HoymilesEnergyStorageSensorEntityDescription( key="[].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="[].load.phases[].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="[].load.phases[].active_power", translation_key="load_active_power_phase", native_unit_of_measurement=UnitOfPower.WATT, device_class=SensorDeviceClass.POWER, state_class=SensorStateClass.MEASUREMENT, ), HoymilesEnergyStorageSensorEntityDescription( key="[].load.phases[].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.param.status", translation_key="inverter_status", device_class=SensorDeviceClass.ENUM, ), HoymilesEnergyStorageSensorEntityDescription( key="[].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.param.isolation_resistance", translation_key="inverter_isolation_resistance", native_unit_of_measurement="kΩ", device_class=None, state_class=SensorStateClass.MEASUREMENT, ), HoymilesEnergyStorageSensorEntityDescription( key="[].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.param.drm_signal", translation_key="inverter_drm_signal", device_class=SensorDeviceClass.ENUM, ), HoymilesEnergyStorageSensorEntityDescription( key="[].inverter.phases[].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.phases[].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.phases[].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.phases[].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.phases[].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.phases[].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.phases[].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.phases[].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.phases[].eps_power", translation_key="inverter_eps_power_phase", native_unit_of_measurement=UnitOfPower.WATT, device_class=SensorDeviceClass.POWER, state_class=SensorStateClass.MEASUREMENT, ), HoymilesEnergyStorageSensorEntityDescription( key="[].pv_inverter.param.status", translation_key="pv_inverter_status", device_class=SensorDeviceClass.ENUM, ), HoymilesEnergyStorageSensorEntityDescription( key="[].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="[].pv_inverter.phases[].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="[].pv_inverter.phases[].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="[].pv_inverter.phases[].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="[].pv_inverter.phases[].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="[].pv_inverter.phases[].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="[].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="[].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="[].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="[].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="[].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="[].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 "" in description.key: for index, inverter_serial in enumerate(inverters): new_key = description.key.replace("", 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 "" 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("", 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("", 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 "" in description.key: for index, inverter in enumerate(inverters): new_key = description.key.replace("", str(index)) if "" in description.key: # TODO: Dynamically determine number of PV panels for pv_index in range(0, 2): new_pv_index_key = new_key.replace( "", 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 "" in description.key: # TODO: Dynamically determine number of phases for phase_index in range(0, 3): new_phase_index_key = new_key.replace( "", 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_HOST] assert result3["data"] == MOCK_DATA_RESULT assert len(mock_setup_entry.mock_calls) == 1 assert len(mock_async_get_real_data_new.mock_calls) == 1 ================================================ FILE: tests/test_init.py ================================================ """Test component setup.""" from homeassistant.setup import async_setup_component from custom_components.hoymiles_wifi.const import DOMAIN async def test_async_setup(hass): """Test the component gets setup.""" assert await async_setup_component(hass, DOMAIN, {}) is True