Skip to content

Commit

Permalink
Add component files
Browse files Browse the repository at this point in the history
  • Loading branch information
wlcrs committed Oct 20, 2021
1 parent b059d08 commit c517a33
Show file tree
Hide file tree
Showing 11 changed files with 514 additions and 0 deletions.
21 changes: 21 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# Huawei Solar Sensors

This integration splits out the various values that are fetched from your
Huawei Solar inverter into separate HomeAssistant sensors. These are properly
configured to allow immediate integration into the HA Energy view.

![sensors](images/sensors-screenshot.png)
![energy-config](images/energy-config.png)

## Installation

1. Install this integration with HACS, or copy the contents of this
repository into the `custom_components/huawei_solar` directory
2. Restart HA
3. Go to `Configuration` -> `Integrations` and click the `+ Add Integration`
button
4. Select `Huawei Solar` from the list
5. Enter the IP address of your inverter (192.168.200.1 if you are connected to
its WiFi AP). Select if you have a battery and/or optimizers. The slave id is
typically 0.

38 changes: 38 additions & 0 deletions __init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
"""The Huawei Solar integration."""
from __future__ import annotations

from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.const import CONF_HOST

from .const import DOMAIN, CONF_SLAVE, DATA_MODBUS_CLIENT

from huawei_solar import HuaweiSolar, ConnectionException

# TODO List the platforms that you want to support.
# For your initial PR, limit it to 1 platform.
PLATFORMS: list[str] = ["sensor"]


async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up Huawei Solar from a config entry."""
# TODO Store an API object for your platforms to access
hass.data.setdefault(DOMAIN, {})[entry.entry_id] = {
DATA_MODBUS_CLIENT: HuaweiSolar(
host=entry.data[CONF_HOST], slave=entry.data[CONF_SLAVE]
)
}

hass.config_entries.async_setup_platforms(entry, PLATFORMS)

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)
if unload_ok:
hass.data[DOMAIN].pop(entry.entry_id)

return unload_ok
"""Huawei Solar integration which connects to the local Modbus TCP endpoint"""
101 changes: 101 additions & 0 deletions config_flow.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
"""Config flow for Huawei Solar integration."""
from __future__ import annotations

import logging
from typing import Any

import voluptuous as vol

from homeassistant import config_entries
from homeassistant.core import HomeAssistant
from homeassistant.data_entry_flow import FlowResult
from homeassistant.exceptions import HomeAssistantError

from homeassistant.const import CONF_HOST
from .const import (
DOMAIN,
CONF_BATTERY,
CONF_OPTIMIZERS,
CONF_SLAVE,
ATTR_MODEL_ID,
ATTR_SERIAL_NUMBER,
)

from huawei_solar import HuaweiSolar, ConnectionException

_LOGGER = logging.getLogger(__name__)

STEP_USER_DATA_SCHEMA = vol.Schema(
{
vol.Required(CONF_HOST): str,
vol.Optional(CONF_OPTIMIZERS, default=False): bool,
vol.Optional(CONF_BATTERY, default=False): bool,
vol.Optional(CONF_SLAVE, default=0): int,
}
)


async def validate_input(hass: HomeAssistant, data: dict[str, Any]) -> dict[str, Any]:
"""Validate the user input allows us to connect.
Data has the keys from STEP_USER_DATA_SCHEMA with values provided by the user.
"""
# TODO validate the data can be used to set up a connection.

# If your PyPI package is not built with async, pass your methods
# to the executor:
# await hass.async_add_executor_job(
# your_validate_func, data["username"], data["password"]
# )

inverter = HuaweiSolar(host=data[CONF_HOST])

try:
model_name = inverter.get(ATTR_MODEL_ID).value
serial_number = inverter.get(ATTR_SERIAL_NUMBER).value

# Return info that you want to store in the config entry.
return dict(model_name=model_name, serial_number=serial_number)
except ConnectionException as ex:
raise CannotConnect from ex


class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
"""Handle a config flow for Huawei Solar."""

VERSION = 1

async def async_step_user(
self, user_input: dict[str, Any] | None = None
) -> FlowResult:
"""Handle the initial step."""
if user_input is None:
return self.async_show_form(
step_id="user", data_schema=STEP_USER_DATA_SCHEMA
)

errors = {}

try:
info = await validate_input(self.hass, user_input)
except CannotConnect:
errors["base"] = "cannot_connect"
except InvalidAuth:
errors["base"] = "invalid_auth"
except Exception: # pylint: disable=broad-except
_LOGGER.exception("Unexpected exception")
errors["base"] = "unknown"
else:
return self.async_create_entry(title=info["model_name"], data=user_input)

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


class CannotConnect(HomeAssistantError):
"""Error to indicate we cannot connect."""


class InvalidAuth(HomeAssistantError):
"""Error to indicate there is invalid auth."""
198 changes: 198 additions & 0 deletions const.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,198 @@
"""Constants for the Huawei Solar integration."""
from dataclasses import dataclass

from homeassistant.const import (
DEVICE_CLASS_BATTERY,
POWER_WATT,
ENERGY_KILO_WATT_HOUR,
DEVICE_CLASS_ENERGY,
DEVICE_CLASS_POWER,
DEVICE_CLASS_POWER_FACTOR,
PERCENTAGE,
)

from homeassistant.components.sensor import (
STATE_CLASS_MEASUREMENT,
STATE_CLASS_TOTAL,
STATE_CLASS_TOTAL_INCREASING,
SensorEntityDescription,
)

DOMAIN = "huawei_solar"

DATA_MODBUS_CLIENT = "client"


ATTR_MODEL_ID = "model_id"
ATTR_SERIAL_NUMBER = "serial_number"

CONF_OPTIMIZERS = "optimizers"
CONF_BATTERY = "battery"
CONF_SLAVE = "slave"

ATTR_DAILY_YIELD = "daily_yield_energy"
ATTR_TOTAL_YIELD = "accumulated_yield_energy"

ATTR_POWER_FACTOR = "power_factor"

ATTR_STORAGE_TOTAL_CHARGE = "storage_total_charge"
ATTR_STORAGE_TOTAL_DISCHARGE = "storage_total_discharge"

ATTR_STORAGE_DAY_CHARGE = "storage_current_day_charge_capacity"
ATTR_STORAGE_DAY_DISCHARGE = "storage_current_day_discharge_capacity"

ATTR_STORAGE_STATE_OF_CAPACITY = "storage_state_of_capacity"
ATTR_STORAGE_CHARGE_DISCHARGE_POWER = "storage_charge_discharge_power"

ATTR_GRID_EXPORTED = "grid_exported_energy"
ATTR_GRID_ACCUMULATED = "grid_accumulated_energy"

ATTR_ACTIVE_POWER = "active_power"
ATTR_INPUT_POWER = "input_power"
ATTR_POWER_METER_ACTIVE_POWER = "power_meter_active_power"

ATTR_NB_OPTIMIZERS = "nb_optimizers"
ATTR_NB_ONLINE_OPTIMIZERS = "nb_online_optimizers"

ATTR_NB_PV_STRINGS = "nb_pv_strings"
ATTR_RATED_POWER = "rated_power"
ATTR_GRID_STANDARD = "grid_standard"
ATTR_GRID_COUNTRY = "grid_country"

ATTR_DAY_POWER_PEAK = "day_active_power_peak"
ATTR_REACTIVE_POWER = "reactive_power"
ATTR_EFFICIENCY = "efficiency"
ATTR_GRID_FREQUENCY = "grid_frequency"
ATTR_GRID_VOLTAGE = "grid_voltage"
ATTR_GRID_CURRENT = "grid_current"
ATTR_STARTUP_TIME = "startup_time"
ATTR_SHUTDOWN_TIME = "shutdown_time"
ATTR_INTERNAL_TEMPERATURE = "internal_temperature"
ATTR_DEVICE_STATUS = "device_status"
ATTR_SYSTEM_TIME = "system_time"


@dataclass
class HuaweiSolarSensorEntityDescription(SensorEntityDescription):
pass


SENSOR_TYPES: tuple[HuaweiSolarSensorEntityDescription] = (
HuaweiSolarSensorEntityDescription(
key=ATTR_DAILY_YIELD,
name="Daily Yield",
icon="mdi:solar-power",
native_unit_of_measurement=ENERGY_KILO_WATT_HOUR,
device_class=DEVICE_CLASS_ENERGY,
state_class=STATE_CLASS_TOTAL_INCREASING,
),
HuaweiSolarSensorEntityDescription(
key=ATTR_TOTAL_YIELD,
name="Total Yield",
native_unit_of_measurement=ENERGY_KILO_WATT_HOUR,
device_class=DEVICE_CLASS_ENERGY,
state_class=STATE_CLASS_TOTAL,
),
HuaweiSolarSensorEntityDescription(
key=ATTR_ACTIVE_POWER,
name="Active Power",
native_unit_of_measurement=POWER_WATT,
device_class=DEVICE_CLASS_POWER,
state_class=STATE_CLASS_MEASUREMENT,
),
HuaweiSolarSensorEntityDescription(
key=ATTR_INPUT_POWER,
name="Input Power",
native_unit_of_measurement=POWER_WATT,
device_class=DEVICE_CLASS_POWER,
state_class=STATE_CLASS_MEASUREMENT,
),
HuaweiSolarSensorEntityDescription(
key=ATTR_POWER_METER_ACTIVE_POWER,
name="Power Meter Active Power",
native_unit_of_measurement=POWER_WATT,
device_class=DEVICE_CLASS_POWER,
state_class=STATE_CLASS_MEASUREMENT,
),
HuaweiSolarSensorEntityDescription(
key=ATTR_POWER_FACTOR,
name="Power Factor",
device_class=DEVICE_CLASS_POWER_FACTOR,
state_class=STATE_CLASS_MEASUREMENT,
),
HuaweiSolarSensorEntityDescription(
key=ATTR_GRID_ACCUMULATED,
name="Grid Consumption",
native_unit_of_measurement=ENERGY_KILO_WATT_HOUR,
device_class=DEVICE_CLASS_ENERGY,
state_class=STATE_CLASS_TOTAL_INCREASING,
),
HuaweiSolarSensorEntityDescription(
key=ATTR_GRID_EXPORTED,
name="Grid Exported",
native_unit_of_measurement=ENERGY_KILO_WATT_HOUR,
device_class=DEVICE_CLASS_ENERGY,
state_class=STATE_CLASS_TOTAL_INCREASING,
),
)

BATTERY_SENSOR_TYPES = (
HuaweiSolarSensorEntityDescription(
key=ATTR_STORAGE_TOTAL_CHARGE,
name="Battery Total Charge",
icon="mdi:battery-plus-variant",
native_unit_of_measurement=ENERGY_KILO_WATT_HOUR,
state_class=STATE_CLASS_TOTAL,
device_class=DEVICE_CLASS_ENERGY,
),
HuaweiSolarSensorEntityDescription(
key=ATTR_STORAGE_DAY_CHARGE,
name="Battery Day Charge",
icon="mdi:battery-plus-variant",
native_unit_of_measurement=ENERGY_KILO_WATT_HOUR,
state_class=STATE_CLASS_TOTAL_INCREASING,
device_class=DEVICE_CLASS_ENERGY,
),
HuaweiSolarSensorEntityDescription(
key=ATTR_STORAGE_TOTAL_DISCHARGE,
name="Battery Total Discharge",
icon="mdi:battery-minus-variant",
native_unit_of_measurement=ENERGY_KILO_WATT_HOUR,
state_class=STATE_CLASS_TOTAL,
device_class=DEVICE_CLASS_ENERGY,
),
HuaweiSolarSensorEntityDescription(
key=ATTR_STORAGE_DAY_DISCHARGE,
name="Battery Day Discharge",
icon="mdi:battery-minus-variant",
native_unit_of_measurement=ENERGY_KILO_WATT_HOUR,
state_class=STATE_CLASS_TOTAL_INCREASING,
device_class=DEVICE_CLASS_ENERGY,
),
HuaweiSolarSensorEntityDescription(
key=ATTR_STORAGE_STATE_OF_CAPACITY,
name="Battery State of Capacity",
icon="mdi:home-battery",
native_unit_of_measurement=PERCENTAGE,
state_class=STATE_CLASS_MEASUREMENT,
),
HuaweiSolarSensorEntityDescription(
key=ATTR_STORAGE_CHARGE_DISCHARGE_POWER,
name="Charge/Discharge Power",
icon="mdi:flash",
native_unit_of_measurement=POWER_WATT,
state_class=STATE_CLASS_MEASUREMENT,
device_class=DEVICE_CLASS_POWER,
),
)

OPTIMIZER_SENSOR_TYPES = (
HuaweiSolarSensorEntityDescription(
key=ATTR_NB_ONLINE_OPTIMIZERS,
name="Optimizers Online",
icon="mdi:solar-panel",
native_unit_of_measurement="optimizers",
state_class=STATE_CLASS_MEASUREMENT,
device_class=DEVICE_CLASS_ENERGY,
),
)
5 changes: 5 additions & 0 deletions hacs.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"name": "Huawei Solar",
"content_in_root": true,
"render_readme": true
}
Binary file added images/energy-config.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added images/sensors-screenshot.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
18 changes: 18 additions & 0 deletions manifest.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
{
"domain": "huawei_solar",
"name": "Huawei Solar",
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/huawei_solar",
"requirements": [
"huawei_solar==1.1.0"
],
"ssdp": [],
"zeroconf": [],
"homekit": {},
"dependencies": [],
"codeowners": [
"@wlcrs"
],
"iot_class": "local_polling",
"version": "2.0.0-alpha1"
}
Loading

0 comments on commit c517a33

Please sign in to comment.