Skip to content
This repository has been archived by the owner on Oct 23, 2022. It is now read-only.

Commit

Permalink
Add modbus address selection binsentsu#59
Browse files Browse the repository at this point in the history
Add modbus address selection binsentsu#59
  • Loading branch information
WillCodeForCats authored Oct 18, 2021
1 parent 05ea23e commit e1f6b05
Show file tree
Hide file tree
Showing 10 changed files with 1,301 additions and 0 deletions.
644 changes: 644 additions & 0 deletions solaredge_modbus/__init__.py

Large diffs are not rendered by default.

92 changes: 92 additions & 0 deletions solaredge_modbus/config_flow.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
import ipaddress
import re

import voluptuous as vol

from homeassistant import config_entries
from homeassistant.const import CONF_NAME, CONF_HOST, CONF_PORT, CONF_SCAN_INTERVAL
from .const import (
DOMAIN,
DEFAULT_NAME,
DEFAULT_SCAN_INTERVAL,
DEFAULT_PORT,
DEFAULT_MODBUS_ADDRESS,
CONF_MODBUS_ADDRESS,
CONF_NUMBER_INVERTERS,
DEFAULT_NUMBER_INVERTERS,
CONF_READ_METER1,
CONF_READ_METER2,
CONF_READ_METER3,
DEFAULT_READ_METER1,
DEFAULT_READ_METER2,
DEFAULT_READ_METER3,
)
from homeassistant.core import HomeAssistant, callback

DATA_SCHEMA = vol.Schema(
{
vol.Optional(CONF_NAME, default=DEFAULT_NAME): str,
vol.Required(CONF_HOST): str,
vol.Required(CONF_PORT, default=DEFAULT_PORT): int,
vol.Optional(CONF_MODBUS_ADDRESS, default=DEFAULT_MODBUS_ADDRESS): int,
# Would like to restrict this to only allow numbers between 1-9 but not sure how
vol.Required(CONF_NUMBER_INVERTERS, default=DEFAULT_NUMBER_INVERTERS): int,
vol.Optional(CONF_READ_METER1, default=DEFAULT_READ_METER1): bool,
vol.Optional(CONF_READ_METER2, default=DEFAULT_READ_METER2): bool,
vol.Optional(CONF_READ_METER3, default=DEFAULT_READ_METER3): bool,
vol.Optional(CONF_SCAN_INTERVAL, default=DEFAULT_SCAN_INTERVAL): int,
}
)


def host_valid(host):
"""Return True if hostname or IP address is valid."""
try:
if ipaddress.ip_address(host).version == (4 or 6):
return True
except ValueError:
disallowed = re.compile(r"[^a-zA-Z\d\-]")
return all(x and not disallowed.search(x) for x in host.split("."))


@callback
def solaredge_modbus_entries(hass: HomeAssistant):
"""Return the hosts already configured."""
return set(
entry.data[CONF_HOST] for entry in hass.config_entries.async_entries(DOMAIN)
)


class SolaredgeModbusConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
"""Solaredge Modbus configflow."""

VERSION = 1
CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_POLL

def _host_in_configuration_exists(self, host) -> bool:
"""Return True if host exists in configuration."""
if host in solaredge_modbus_entries(self.hass):
return True
return False

async def async_step_user(self, user_input=None):
"""Handle the initial step."""
errors = {}

if user_input is not None:
host = user_input[CONF_HOST]

if self._host_in_configuration_exists(host):
errors[CONF_HOST] = "already_configured"
elif not host_valid(user_input[CONF_HOST]):
errors[CONF_HOST] = "invalid host IP"
else:
await self.async_set_unique_id(user_input[CONF_HOST])
self._abort_if_unique_id_configured()
return self.async_create_entry(
title=user_input[CONF_NAME], data=user_input
)

return self.async_show_form(
step_id="user", data_schema=DATA_SCHEMA, errors=errors
)
246 changes: 246 additions & 0 deletions solaredge_modbus/const.py

Large diffs are not rendered by default.

10 changes: 10 additions & 0 deletions solaredge_modbus/manifest.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"domain": "solaredge_modbus",
"name": "SolarEdge Modbus",
"documentation": "https://github.com/binsentsu/home-assistant-solaredge-modbus",
"requirements": ["pymodbus==1.5.2"],
"dependencies": [],
"codeowners": ["@binsentsu"],
"config_flow": true,
"version": "1.3.1"
}
178 changes: 178 additions & 0 deletions solaredge_modbus/sensor.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,178 @@
import logging
from typing import Optional, Dict, Any
from .const import (
SENSOR_TYPES,
METER1_SENSOR_TYPES,
METER2_SENSOR_TYPES,
METER3_SENSOR_TYPES,
DOMAIN,
ATTR_STATUS_DESCRIPTION,
DEVICE_STATUSSES,
ATTR_MANUFACTURER,
)
from datetime import datetime
from homeassistant.helpers.entity import Entity
from homeassistant.const import CONF_NAME, DEVICE_CLASS_ENERGY, ENERGY_KILO_WATT_HOUR
from homeassistant.components.sensor import (
PLATFORM_SCHEMA,
STATE_CLASS_MEASUREMENT,
SensorEntity,
)

try: # backward-compatibility to 2021.8
from homeassistant.components.sensor import STATE_CLASS_TOTAL_INCREASING
except ImportError:
from homeassistant.components.sensor import STATE_CLASS_MEASUREMENT as STATE_CLASS_TOTAL_INCREASING


from homeassistant.core import callback
from homeassistant.util import dt as dt_util

_LOGGER = logging.getLogger(__name__)


async def async_setup_entry(hass, entry, async_add_entities):
hub_name = entry.data[CONF_NAME]
hub = hass.data[DOMAIN][hub_name]["hub"]

device_info = {
"identifiers": {(DOMAIN, hub_name)},
"name": hub_name,
"manufacturer": ATTR_MANUFACTURER,
}

entities = []

for inverter_index in range(hub.number_of_inverters):
inverter_variable_prefix = "i" + str(inverter_index + 1) + "_"
inverter_title_prefix = "I" + str(inverter_index + 1) + " "
for sensor_info in SENSOR_TYPES.values():
sensor = SolarEdgeSensor(
hub_name,
hub,
device_info,
inverter_title_prefix + sensor_info[0],
inverter_variable_prefix + sensor_info[1],
sensor_info[2],
sensor_info[3],
)
entities.append(sensor)

if hub.read_meter1 == True:
for meter_sensor_info in METER1_SENSOR_TYPES.values():
sensor = SolarEdgeSensor(
hub_name,
hub,
device_info,
meter_sensor_info[0],
meter_sensor_info[1],
meter_sensor_info[2],
meter_sensor_info[3],
)
entities.append(sensor)

if hub.read_meter2 == True:
for meter_sensor_info in METER2_SENSOR_TYPES.values():
sensor = SolarEdgeSensor(
hub_name,
hub,
device_info,
meter_sensor_info[0],
meter_sensor_info[1],
meter_sensor_info[2],
meter_sensor_info[3],
)
entities.append(sensor)

if hub.read_meter3 == True:
for meter_sensor_info in METER3_SENSOR_TYPES.values():
sensor = SolarEdgeSensor(
hub_name,
hub,
device_info,
meter_sensor_info[0],
meter_sensor_info[1],
meter_sensor_info[2],
meter_sensor_info[3],
)
entities.append(sensor)

async_add_entities(entities)
return True


class SolarEdgeSensor(SensorEntity):
"""Representation of an SolarEdge Modbus sensor."""

def __init__(self, platform_name, hub, device_info, name, key, unit, icon):
"""Initialize the sensor."""
self._platform_name = platform_name
self._hub = hub
self._key = key
self._name = name
self._unit_of_measurement = unit
self._icon = icon
self._device_info = device_info
self._attr_state_class = STATE_CLASS_MEASUREMENT
if self._unit_of_measurement == ENERGY_KILO_WATT_HOUR:
self._attr_state_class = STATE_CLASS_TOTAL_INCREASING
self._attr_device_class = DEVICE_CLASS_ENERGY
if STATE_CLASS_TOTAL_INCREASING == STATE_CLASS_MEASUREMENT: # compatibility to 2021.8
self._attr_last_reset = dt_util.utc_from_timestamp(0)

async def async_added_to_hass(self):
"""Register callbacks."""
self._hub.async_add_solaredge_sensor(self._modbus_data_updated)

async def async_will_remove_from_hass(self) -> None:
self._hub.async_remove_solaredge_sensor(self._modbus_data_updated)

@callback
def _modbus_data_updated(self):
self.async_write_ha_state()

@callback
def _update_state(self):
if self._key in self._hub.data:
self._state = self._hub.data[self._key]

@property
def name(self):
"""Return the name."""
return f"{self._platform_name} ({self._name})"

@property
def unique_id(self) -> Optional[str]:
return f"{self._platform_name}_{self._key}"

@property
def unit_of_measurement(self):
"""Return the unit of measurement."""
return self._unit_of_measurement

@property
def icon(self):
"""Return the sensor icon."""
return self._icon

@property
def state(self):
"""Return the state of the sensor."""
if self._key in self._hub.data:
return self._hub.data[self._key]

@property
def extra_state_attributes(self):
if self._key in ["status", "statusvendor"]:
if self.state in DEVICE_STATUSSES:
return {ATTR_STATUS_DESCRIPTION: DEVICE_STATUSSES[self.state]}
return None

@property
def should_poll(self) -> bool:
"""Data is delivered by the hub"""
return False

@property
def device_info(self) -> Optional[Dict[str, Any]]:
return self._device_info
27 changes: 27 additions & 0 deletions solaredge_modbus/strings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
{
"config": {
"title": "SolarEdge Modbus",
"step": {
"user": {
"title": "Define your SolarEdge modbus connection",
"data": {
"host": "The ip-address of your Solaredge device",
"name": "The prefix to be used for your SolarEdge sensors",
"port": "The TCP port on which to connect to the SolarEdge",
"modbus_address": "The starting modbus address",
"number_of_inverters": "The number of inverters connected",
"read_meter_1": "Read meter 1 data (only for meter models)",
"read_meter_2": "Read meter 2 data (only for meter models)",
"read_meter_3": "Read meter 3 data (only for meter models)",
"scan_interval": "The polling frequency of the modbus registers in seconds"
}
}
},
"error": {
"already_configured": "Device is already configured"
},
"abort": {
"already_configured": "Device is already configured"
}
}
}
26 changes: 26 additions & 0 deletions solaredge_modbus/translations/de.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
{
"config": {
"title": "SolarEdge Modbus",
"step": {
"user": {
"title": "Konfiguriere Deine modbus-Verbindung zum SolarEdge Wechselrichter.",
"data": {
"host": "Die IP-Adresse des Solaredge Wechselrichters",
"name": "Das Prefix, das für die SolarEdge Sensoren verwendet werden soll",
"port": "Der TCP Port des SolarEdge Wechselrichters (z.B. 502)",
"number_of_inverters": "Die Anzahl der angeschlossenen Wechselrichter",
"read_meter_1": "Lese die Stände des Zähler 1 (nur für Modelle mit Zähler)",
"read_meter_2": "Lese die Stände des Zähler 2 (nur für Modelle mit Zähler)",
"read_meter_3": "Lese die Stände des Zähler 3 (nur für Modelle mit Zähler)",
"scan_interval": "Das Abfrageintervall der modbus Register in Sekunden"
}
}
},
"error": {
"already_configured": "Der Wechselrichter ist bereits konfiguriert.,"
},
"abort": {
"already_configured": "Der Wechselrichter ist bereits konfiguriert."
}
}
}
26 changes: 26 additions & 0 deletions solaredge_modbus/translations/en.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
{
"config": {
"title": "SolarEdge Modbus",
"step": {
"user": {
"title": "Define your SolarEdge modbus connection",
"data": {
"host": "The ip-address of your Solaredge inverter",
"name": "The prefix to be used for your SolarEdge sensors",
"port": "The TCP port on which to connect to the SolarEdge inverter",
"number_of_inverters": "The number of inverters connected",
"read_meter_1": "Read meter 1 data (only for meter models)",
"read_meter_2": "Read meter 2 data (only for meter models)",
"read_meter_3": "Read meter 3 data (only for meter models)",
"scan_interval": "The polling frequency of the modbus registers in seconds"
}
}
},
"error": {
"already_configured": "Device is already configured"
},
"abort": {
"already_configured": "Device is already configured"
}
}
}
26 changes: 26 additions & 0 deletions solaredge_modbus/translations/nb.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
{
"config": {
"title": "SolarEdge Modbus",
"step": {
"user": {
"title": "Definer din SolarEdge modbus-tilkobling",
"data": {
"host": "IP-adressen til din Solaredge-omformer",
"name": "Prefikset som skal brukes til SolarEdge-sensorene dine",
"port": "TCP-porten som skal kobles til SolarEdge-omformeren",
"number_of_inverters": "Antall omformere koblet sammen",
"read_meter_1": "Les måler 1-data (bare for målermodeller)",
"read_meter_2": "Les måler 2-data (bare for målermodeller)",
"read_meter_3": "Les måler 3-data (bare for målermodeller)",
"scan_interval": "Avstemningsfrekvensen til modbus registreres på få sekunder"
}
}
},
"error": {
"already_configured": "Enheten er allerede konfigurert"
},
"abort": {
"already_configured": "Enheten er allerede konfigurert"
}
}
}
Loading

0 comments on commit e1f6b05

Please sign in to comment.