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

Add growatt_rs232 integration for Growatt inverter with RS232 modbus #37846

Closed
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions CODEOWNERS
Validating CODEOWNERS rules …
Expand Up @@ -156,6 +156,7 @@ homeassistant/components/gpsd/* @fabaff
homeassistant/components/greeneye_monitor/* @jkeljo
homeassistant/components/griddy/* @bdraco
homeassistant/components/group/* @home-assistant/core
homeassistant/components/growatt_rs232/* @ArdescoConsulting
homeassistant/components/growatt_server/* @indykoning
homeassistant/components/guardian/* @bachya
homeassistant/components/harmony/* @ehendrix23 @bramkragten @bdraco
Expand Down
24 changes: 24 additions & 0 deletions homeassistant/components/growatt_rs232/.translations/en.json
@@ -0,0 +1,24 @@
{
"title": "Growatt RS232 inverter.",
"config": {
"flow_title": "Growatt inverter: {model} {serial_number}",
"step": {
"user": {
"description": "Set up Growatt RS232 integration. If you have problems with configuration go to: https://www.home-assistant.io/integrations/growatt_rs232",
"data": {
"port": "USB port of the RS232 interface.",
"address": "Modbus address of the Growatt inverter (1-247)."
}
}
},
"error": {
"connection_error": "[%key:common::config_flow::error::cannot_connect%]",
"modbus_error": "Modbus communication error.",
"port_error": "Invalid usb port."
},
"abort": {
"already_configured": "This Growatt inverter is already configured.",
"serial_number_error": "Unable to get serial number from Growatt inverter."
}
}
}
94 changes: 94 additions & 0 deletions homeassistant/components/growatt_rs232/__init__.py
@@ -0,0 +1,94 @@
"""The Growatt RS232 component."""

import asyncio
from datetime import timedelta
import logging

from growattRS232 import GrowattRS232, ModbusException, PortException

from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_ADDRESS, CONF_PORT
from homeassistant.core import Config, HomeAssistant
from homeassistant.exceptions import ConfigEntryNotReady
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed

from .const import DOMAIN, PLATFORMS, STARTUP_MESSAGE

SCAN_INTERVAL = timedelta(seconds=15)

_LOGGER = logging.getLogger(__name__)


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


async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry):
"""Set up this integration using UI."""
if hass.data.get(DOMAIN) is None:
hass.data.setdefault(DOMAIN, {})
_LOGGER.info(STARTUP_MESSAGE)

port = entry.data[CONF_PORT]
address = entry.data[CONF_ADDRESS]

coordinator = GrowattRS232DataUpdateCoordinator(hass, port=port, address=address)
await coordinator.async_refresh()

if not coordinator.last_update_success:
raise ConfigEntryNotReady

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

for platform in PLATFORMS:
if entry.options.get(platform, True):
coordinator.platforms.append(platform)
hass.async_add_job(
hass.config_entries.async_forward_entry_setup(entry, platform)
)

return True


async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry):
"""Handle removal of an entry."""
coordinator = hass.data[DOMAIN][entry.entry_id]
unloaded = all(
await asyncio.gather(
*[
hass.config_entries.async_forward_entry_unload(entry, platform)
for platform in PLATFORMS
if platform in coordinator.platforms
]
)
)
if unloaded:
hass.data[DOMAIN].pop(entry.entry_id)

return unloaded


async def async_reload_entry(hass: HomeAssistant, entry: ConfigEntry):
"""Reload config entry."""
await async_unload_entry(hass, entry)
await async_setup_entry(hass, entry)


class GrowattRS232DataUpdateCoordinator(DataUpdateCoordinator):
"""Class to manage fetching Growatt data from the inverter."""

def __init__(self, hass, port, address):
"""Initialize."""
self.api = GrowattRS232(port, address)
self.platforms = []

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

async def _async_update_data(self):
"""Update data via library."""
try:
data = await self.api.async_update()
return data
except (PortException, ModbusException, ConnectionError) as exception:
raise UpdateFailed(exception)
73 changes: 73 additions & 0 deletions homeassistant/components/growatt_rs232/config_flow.py
@@ -0,0 +1,73 @@
"""Adds config flow for Growatt RS232."""

import logging

from growattRS232 import (
ATTR_SERIAL_NUMBER,
GrowattRS232,
ModbusException,
PortException,
)
from growattRS232.const import DEFAULT_ADDRESS, DEFAULT_PORT
import voluptuous as vol

from homeassistant import config_entries, exceptions
from homeassistant.const import CONF_ADDRESS, CONF_PORT

from .const import DOMAIN # pylint:disable=unused-import

_LOGGER = logging.getLogger(__name__)

CONF_MANUAL_PATH = "Enter Manually"

DATA_SCHEMA = vol.Schema(
{
vol.Required(CONF_PORT, default=DEFAULT_PORT): str,
vol.Required(CONF_ADDRESS, default=DEFAULT_ADDRESS): int,
}
)


class GrowattRS232FlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
"""Handle a config flow for GrowattRS232 inverter."""

VERSION = 1
CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_POLL

def __init__(self):
"""Initialize."""

async def async_step_user(self, user_input=None):
"""Handle a flow initialized by the user."""
errors = {}

if user_input is not None:
try:
data = {}
growattrs232 = GrowattRS232(
user_input[CONF_PORT], user_input[CONF_ADDRESS]
)
data = await growattrs232.async_update()
serial_number = data.get(ATTR_SERIAL_NUMBER, "").lower()
if serial_number == "":
raise MissingSerialNumber
await self.async_set_unique_id(serial_number)
self._abort_if_unique_id_configured()
title = f"Growatt {serial_number}"
return self.async_create_entry(title=title, data=user_input)
except PortException:
errors[CONF_PORT] = "port_error"
except ModbusException:
errors[CONF_ADDRESS] = "modbus_error"
except ConnectionError:
errors["base"] = "connection_error"
except MissingSerialNumber:
return self.async_abort(reason="serial_number_error")

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


class MissingSerialNumber(exceptions.HomeAssistantError):
"""Error to indicate that the serial number is missing."""