Skip to content

Commit

Permalink
Merge pull request #26 from suaveolent/multi_inverter
Browse files Browse the repository at this point in the history
Multi inverter support, device model detection and under the hood improvements
  • Loading branch information
suaveolent authored Mar 29, 2024
2 parents 386851d + aced6e7 commit ed4b606
Show file tree
Hide file tree
Showing 17 changed files with 615 additions and 339 deletions.
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

0 comments on commit ed4b606

Please sign in to comment.