Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Multi inverter support and under the hood improvements #26

Merged
merged 21 commits into from
Mar 29, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 5 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@

# HMS-XXXXW-2T for Home Assistant
This Home Assistant custom component utilizes the [hoymiles-wifi](https://github.com/suaveolent/hoymiles-wifi) Python library, allowing seamless integration with Hoymiles HMS microinverters, specifically designed for the HMS-XXXXW-2T series.
# Hoymiles for Home Assistant
This Home Assistant custom component utilizes the [hoymiles-wifi](https://github.com/suaveolent/hoymiles-wifi) Python library, allowing seamless integration with Hoymiles HMS microinverters via Hoymiles DTUs and the HMS-XXXXW-2T microinverters.

**Disclaimer: This custom component is an independent project and is not affiliated with Hoymiles. It has been developed to provide Home Assistant users with tools for interacting with Hoymiles HMS-XXXXW-2T series micro-inverters featuring integrated WiFi DTU. Any trademarks or product names mentioned are the property of their respective owners.**

Expand All @@ -10,7 +10,7 @@ This Home Assistant custom component utilizes the [hoymiles-wifi](https://github
The custom component was successfully tested with:

- Hoymiles HMS-800W-2T
- Hoymiles DTU WLite
- Hoymiles DTU Wlite

## Installation

Expand All @@ -22,10 +22,10 @@ The custom component was successfully tested with:
- **Category:** Integration

5. Click "Add"
6. Click on the `Hoymiles HMS-XXXXW-2T` integration.
6. Click on the `Hoymiles` integration.
7. Click "DOWNLOAD"
8. Navigate to "Settings" - "Devices & Services"
9. Click "ADD INTEGRATION" and select the `Hoymiles HMS-XXXXW-2T` integration.
9. Click "ADD INTEGRATION" and select the `Hoymiles` integration.


### Option 2: Manual Installation
Expand Down
81 changes: 71 additions & 10 deletions custom_components/hoymiles_wifi/__init__.py
Original file line number Diff line number Diff line change
@@ -1,33 +1,42 @@
"""Platform for retrieving values of a Hoymiles inverter."""

import asyncio
from datetime import timedelta
import logging

from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_HOST, Platform
from homeassistant.core import Config, HomeAssistant
from hoymiles_wifi.inverter import Inverter
from homeassistant.helpers.device_registry import DeviceEntry
from hoymiles_wifi.dtu import DTU

from .const import (
CONF_DTU_SERIAL_NUMBER,
CONF_INVERTERS,
CONF_PORTS,
CONF_UPDATE_INTERVAL,
CONFIG_VERSION,
DEFAULT_APP_INFO_UPDATE_INTERVAL_SECONDS,
DEFAULT_CONFIG_UPDATE_INTERVAL_SECONDS,
DOMAIN,
HASS_APP_INFO_COORDINATOR,
HASS_CONFIG_COORDINATOR,
HASS_DATA_COORDINATOR,
HASS_INVERTER,
HASS_DTU,
)
from .coordinator import (
HoymilesAppInfoUpdateCoordinator,
HoymilesConfigUpdateCoordinator,
HoymilesRealDataUpdateCoordinator,
)
from .error import CannotConnect
from .util import async_get_config_entry_data_for_host

_LOGGER = logging.getLogger(__name__)

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


async def async_setup(hass: HomeAssistant, config: Config):
"""Set up this integration using YAML is not supported."""
return True
Expand All @@ -43,28 +52,80 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry):
host = entry.data.get(CONF_HOST)
update_interval = timedelta(seconds=entry.data.get(CONF_UPDATE_INTERVAL))

inverter = Inverter(host)
dtu = DTU(host)

hass_data[HASS_INVERTER] = inverter
hass_data[HASS_DTU] = dtu

data_coordinator = HoymilesRealDataUpdateCoordinator(hass, inverter=inverter, entry=entry, update_interval=update_interval)
data_coordinator = HoymilesRealDataUpdateCoordinator(
hass, dtu=dtu, entry=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, inverter=inverter, entry=entry, update_interval=config_update_interval)
config_coordinator = HoymilesConfigUpdateCoordinator(
hass, dtu=dtu, entry=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, inverter=inverter, entry=entry, update_interval=app_info_update_interval)
app_info_update_interval = timedelta(
seconds=DEFAULT_APP_INFO_UPDATE_INTERVAL_SECONDS
)
app_info_update_coordinator = HoymilesAppInfoUpdateCoordinator(
hass, dtu=dtu, entry=entry, update_interval=app_info_update_interval
)
hass_data[HASS_APP_INFO_COORDINATOR] = app_info_update_coordinator

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

await data_coordinator.async_config_entry_first_refresh()
await asyncio.sleep(5)
await asyncio.sleep(2)
await config_coordinator.async_config_entry_first_refresh()
await asyncio.sleep(5)
await asyncio.sleep(2)
await app_info_update_coordinator.async_config_entry_first_refresh()

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 == 1:
_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, inverters, ports = 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] = inverters
new[CONF_PORTS] = ports

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

return True
45 changes: 27 additions & 18 deletions custom_components/hoymiles_wifi/binary_sensor.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
"""Contains binary sensor entities for Hoymiles WiFi integration."""
import dataclasses
from dataclasses import dataclass
import logging

Expand All @@ -11,19 +12,20 @@
from homeassistant.const import EntityCategory
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from hoymiles_wifi.inverter import NetworkState
from hoymiles_wifi.dtu import NetworkState

from .const import DOMAIN, HASS_DATA_COORDINATOR
from .entity import HoymilesCoordinatorEntity
from .const import CONF_DTU_SERIAL_NUMBER, DOMAIN, HASS_DATA_COORDINATOR
from .entity import HoymilesCoordinatorEntity, HoymilesEntityDescription

_LOGGER = logging.getLogger(__name__)


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

is_dtu_sensor: bool = False


BINARY_SENSORS = (
HoymilesBinarySensorEntityDescription(
Expand All @@ -35,6 +37,7 @@ class HoymilesBinarySensorEntityDescription(BinarySensorEntityDescription):
),
)


async def async_setup_entry(
hass: HomeAssistant,
entry: ConfigEntry,
Expand All @@ -43,27 +46,37 @@ async def async_setup_entry(
"""Set up sensor platform."""
hass_data = hass.data[DOMAIN][entry.entry_id]
data_coordinator = hass_data[HASS_DATA_COORDINATOR]
dtu_serial_number = entry.data[CONF_DTU_SERIAL_NUMBER]

sensors = []

for description in BINARY_SENSORS:
sensors.append(HoymilesInverterSensorEntity(entry, description, data_coordinator))
updated_description = dataclasses.replace(
description, serial_number=dtu_serial_number
)
sensors.append(
HoymilesInverterSensorEntity(entry, updated_description, data_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):
def __init__(
self,
config_entry: ConfigEntry,
description: HoymilesBinarySensorEntityDescription,
coordinator: HoymilesCoordinatorEntity,
):
"""Initialize the HoymilesInverterSensorEntity."""
super().__init__(config_entry, description, coordinator)
self._inverter = coordinator.get_inverter()
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."""
Expand All @@ -75,16 +88,12 @@ 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 inverter's network state."""
inverter_state = self._inverter.get_state()
if inverter_state == NetworkState.Online:
"""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 inverter_state == NetworkState.Offline:
elif dtu_state == NetworkState.Offline:
self._native_value = False
else:
self._native_value = None



68 changes: 45 additions & 23 deletions custom_components/hoymiles_wifi/button.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
"""Support for Hoymiles buttons."""

import dataclasses
from dataclasses import dataclass

from homeassistant.components.button import (
Expand All @@ -10,69 +11,90 @@
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from hoymiles_wifi.inverter import Inverter
from hoymiles_wifi.dtu import DTU

from .const import DOMAIN, HASS_INVERTER
from .entity import HoymilesEntity
from .const import CONF_DTU_SERIAL_NUMBER, CONF_INVERTERS, DOMAIN, HASS_DTU
from .entity import HoymilesEntity, HoymilesEntityDescription


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

is_dtu_sensor: bool = False


BUTTONS: tuple[HoymilesButtonEntityDescription, ...] = (
HoymilesButtonEntityDescription(
key="async_restart",
key="async_restart_dtu",
translation_key="restart",
device_class = ButtonDeviceClass.RESTART,
is_dtu_sensor = True
device_class=ButtonDeviceClass.RESTART,
is_dtu_sensor=True,
),
HoymilesButtonEntityDescription(
key="async_turn_off",
key="async_turn_off_inverter",
translation_key="turn_off",
icon="mdi:power-off",
),
HoymilesButtonEntityDescription(
key="async_turn_on",
key="async_turn_on_inverter",
translation_key="turn_on",
icon="mdi:power-on",
),
)


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

buttons = []
for description in BUTTONS:
buttons.append(HoymilesButtonEntity(entry, description, inverter)
)
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:
updated_description = dataclasses.replace(
description, 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, inverter: Inverter) -> None:
def __init__(
self,
config_entry: ConfigEntry,
description: HoymilesButtonEntityDescription,
dtu: DTU,
) -> None:
"""Initialize the HoymilesButtonEntity."""
super().__init__(config_entry, description)
self._inverter = inverter
self._dtu = dtu

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

if hasattr(self._inverter, self.entity_description.key) and callable(getattr(self._inverter, self.entity_description.key)):
if hasattr(self._inverter, self.entity_description.key) and callable(
getattr(self._inverter, self.entity_description.key)
):
await getattr(self._inverter, self.entity_description.key)()
else:
raise NotImplementedError(f"Method '{self.entity_description.key}' not implemented in Inverter class.")



raise NotImplementedError(
f"Method '{self.entity_description.key}' not implemented in Inverter class."
)
Loading