[solarman] Initial contribution (#16835)

* Initial commit for the Solarman Binding.

Signed-off-by: Catalin Sanda <catalin.sanda@gmail.com>
Signed-off-by: Ciprian Pascu <contact@ciprianpascu.ro>
This commit is contained in:
Catalin Sanda 2024-07-31 00:46:28 +03:00 committed by Ciprian Pascu
parent 3529847699
commit 6ec86dd665
55 changed files with 11589 additions and 0 deletions

View File

@ -344,6 +344,7 @@
/bundles/org.openhab.binding.solaredge/ @alexf2015
/bundles/org.openhab.binding.solarforecast/ @weymann
/bundles/org.openhab.binding.solarlog/ @johannrichard
/bundles/org.openhab.binding.solarman/ @catalinsanda
/bundles/org.openhab.binding.solarmax/ @jamietownsend
/bundles/org.openhab.binding.solarwatt/ @sven-carstens
/bundles/org.openhab.binding.solax/ @theater

View File

@ -1716,6 +1716,11 @@
<artifactId>org.openhab.binding.solarlog</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.openhab.addons.bundles</groupId>
<artifactId>org.openhab.binding.solarman</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.openhab.addons.bundles</groupId>
<artifactId>org.openhab.binding.solarmax</artifactId>

View File

@ -0,0 +1,13 @@
This content is produced and maintained by the openHAB project.
* Project home: https://www.openhab.org
== Declared Project Licenses
This program and the accompanying materials are made available under the terms
of the Eclipse Public License 2.0 which is available at
https://www.eclipse.org/legal/epl-2.0/.
== Source Code
https://github.com/openhab/openhab-addons

View File

@ -0,0 +1,290 @@
# Solarman Logger Binding
Binding used to communicate with Solarman (IGEN-Tech) v5 based solar inverter data loggers in direct-mode over the local network.
More information about the different types of stick loggers is available on the [Solarman](https://www.solarmanpv.com/products/data-logger/stick-logger/) site.
These data loggers are used by inverters from a lot of manufacturers, just to name a few:
- Deye
- Sofar
- Solis
- ZCS Azzurro
- KStar
## Supported Things
The `solarman:logger` thing supports reading data from a Solarman LSW-3 Stick Logger (it might also work with LSE-3 and maybe others) when connected to a supported inverter.
It was tested on a SUN-12K-SG04LP3-EU only, but because the implementation uses the inverter definitions created as part of Stephan Joubert's Home Assistant plugin it **might** work with the other inverters supported by the plugin.
## Thing Configuration
To connect the logger you need the IP address of the logger and its serial number.
The IP address can be obtained from your router and the serial number can either be read from the label of the logger, or by connecting to the logger with a browser (default user/pass: admin/admin) and getting it from the Status page.
*Please note* that you need the "Device serial number" from the "Device information" section, not the "Inverter serial number".
### `logger` Thing Configuration
| Name | Type | Description | Default | Required | Advanced |
|--------------------|---------|--------------------------------------------------------|---------|----------|----------|
| hostname | text | Hostname or IP address of the Solarman logger | N/A | yes | no |
| serialNumber | text | Serial number of the Solarman logger | N/A | yes | no |
| inverterType | text | The type of inverter connected to the logger | N/A | yes | no |
| port | integer | Port of the Solarman logger | 8899 | no | yes |
| refreshInterval | integer | Interval the device is polled in sec. | 60 | no | yes |
| additionalRequests | text | Additional requests besides the ones in the definition | N/A | no | yes |
The `inverterType` parameter governs what registers the binding will read from the logger and what channels it will expose.
Possible values:
| Inverter Type | Inverters supported | Notes |
|--------------------|---------------------------------------------|------------------------------------------------------------------|
| deye_hybrid | DEYE/Sunsynk/SolArk Hybrid inverters | used when no lookup specified |
| deye_sg04lp3 | DEYE/Sunsynk/SolArk Hybrid 8/12K-SG04LP3 | e.g. 12K-SG04LP3-EU |
| deye_string | DEYE/Sunsynk/SolArk String inverters | e.g. SUN-4/5/6/7/8/10/12K-G03 Plus |
| deye_2mppt | DEYE Microinverter with 2 MPPT Trackers | e.g. SUN600G3-EU-230 / SUN800G3-EU-230 / SUN1000G3-EU-230 |
| deye_4mppt | DEYE Microinverter with 4 MPPT Trackers | e.g. SUN1300G3-EU-230 / SUN1600G3-EU-230 / SUN2000G3-EU-230 |
| sofar_lsw3 | SOFAR Inverters | |
| sofar_g3hyd | SOFAR Hybrid Three-Phase inverter | HYD 6000 or rebranded (three-phase), ex. ZCS Azzurro 3PH HYD-ZSS |
| sofar_hyd3k-6k-es | SOFAR Hybrid Single-Phase inverter | HYD 6000 or rebranded (single-phase), ex. ZCS Azzurro HYD-ZSS |
| solis_hybrid | SOLIS Hybrid inverter | |
| solid_1p8k-5g | SOLIS 1P8K-5G | |
| zcs_azzurro-ktl-v3 | ZCS Azzurro KTL-V3 inverters | ZCS Azzurro 3.3/4.4/5.5/6.6 KTL-V3 (rebranded Sofar KTLX-G3) |
The `additionalRequests` allows the user to specify additional address ranges to be polled. The format of the value is `mb_functioncode1:start1-end1, mb_functioncode2:start2-end2,...`
For example `"0x03:0x27D-0x27E"` will issue an additional read for Holding Registers between `0x27D` and `0x27E`.
This is useful when coupled with user defined channels, for example a thing definition like the one below will also read the register for the AC frequency on a Deye inverter, besides the ones pre-defined in the `deye_sg04lp3` inverter definition.
```java
Thing solarman:logger:local [ hostname="x.x.x.x", inverterType="deye_sg04lp3", serialNumber="1234567890", additionalRequests="0x03:0x27D-0x27E" ] {
Channels:
Type number : Inverter_Frequency [scale="0.01", uom="Hz", rule="3", registers="0x27E"]
}
```
**Please note** As of this writing inverter types besides the `deye_sg04lp3` were not tested to work.
If you have one of those inverters and it works, please drop me a message, if it doesn't work, please open an issue and I'll try to fix it.
## Channels
The list of channels is not static, it is generated dynamically based on the inverter type selected.
This is the list you get for the `deye_sg04lp3` inverter type:
| Channel | Type | Read/Write | Description |
|------------------------------------------|--------|--------------|-------------------------------------------------------|
| alert-alert | Number | R | Alert \[0x0229,0x022A,0x022B,0x022C,0x022D,0x022E\] |
| battery-battery-current | Number | R | Battery Current \[0x024F\] |
| battery-battery-power | Number | R | Battery Power \[0x024E\] |
| battery-battery-soc | Number | R | Battery SOC \[0x024C\] |
| battery-battery-temperature | Number | R | Battery Temperature \[0x024A\] |
| battery-battery-voltage | Number | R | Battery Voltage \[0x024B\] |
| battery-daily-battery-charge | Number | R | Daily Battery Charge \[0x0202\] |
| battery-daily-battery-discharge | Number | R | Daily Battery Discharge \[0x0203\] |
| battery-total-battery-charge | Number | R | Total Battery Charge \[0x0204,0x0205\] |
| battery-total-battery-discharge | Number | R | Total Battery Discharge \[0x0206,0x0207\] |
| grid-daily-energy-bought | Number | R | Daily Energy Bought \[0x0208\] |
| grid-daily-energy-sold | Number | R | Daily Energy Sold \[0x0209\] |
| grid-external-ct-l1-power | Number | R | External CT L1 Power \[0x0268\] |
| grid-external-ct-l2-power | Number | R | External CT L2 Power \[0x0269\] |
| grid-external-ct-l3-power | Number | R | External CT L3 Power \[0x026A\] |
| grid-grid-voltage-l1 | Number | R | Grid Voltage L1 \[0x0256\] |
| grid-grid-voltage-l2 | Number | R | Grid Voltage L2 \[0x0257\] |
| grid-grid-voltage-l3 | Number | R | Grid Voltage L3 \[0x0258\] |
| grid-internal-ct-l1-power | Number | R | Internal CT L1 Power \[0x025C\] |
| grid-internal-ct-l2-power | Number | R | Internal CT L2 Power \[0x025D\] |
| grid-internal-ct-l3-power | Number | R | Internal CT L3 Power \[0x025E\] |
| grid-total-energy-bought | Number | R | Total Energy Bought \[0x020A,0x020B\] |
| grid-total-energy-sold | Number | R | Total Energy Sold \[0x020C\] |
| grid-total-grid-power | Number | R | Total Grid Power \[0x0271\] |
| grid-total-grid-production | Number | R | Total Grid Production \[0x020C,0x020D\] |
| inverter-ac-temperature | Number | R | AC Temperature \[0x021D\] |
| inverter-communication-board-version-no- | Number | R | Communication Board Version No \[0x0011\] |
| inverter-control-board-version-no- | Number | R | Control Board Version No \[0x000D\] |
| inverter-current-l1 | Number | R | Current L1 \[0x0276\] |
| inverter-current-l2 | Number | R | Current L2 \[0x0277\] |
| inverter-current-l3 | Number | R | Current L3 \[0x0278\] |
| inverter-dc-temperature | Number | R | DC Temperature \[0x021C\] |
| inverter-frequency | Number | R | Number Value \[0x27E\] |
| inverter-inverter-id | String | R | Inverter ID \[0x0003,0x0004,0x0005,0x0006,0x0007\] |
| inverter-inverter-l1-power | Number | R | Inverter L1 Power \[0x0279\] |
| inverter-inverter-l2-power | Number | R | Inverter L2 Power \[0x027A\] |
| inverter-inverter-l3-power | Number | R | Inverter L3 Power \[0x027B\] |
| solar-daily-production | Number | R | Daily Production \[0x0211\] |
| solar-pv1-current | Number | R | PV1 Current \[0x02A5\] |
| solar-pv1-power | Number | R | PV1 Power \[0x02A0\] |
| solar-pv1-voltage | Number | R | PV1 Voltage \[0x02A4\] |
| solar-pv2-current | Number | R | PV2 Current \[0x02A7\] |
| solar-pv2-power | Number | R | PV2 Power \[0x02A1\] |
| solar-pv2-voltage | Number | R | PV2 Voltage \[0x02A6\] |
| solar-total-production | Number | R | Total Production \[0x0216,0x0217\] |
| upload-daily-load-consumption | Number | R | Daily Load Consumption \[0x020E\] |
| upload-load-l1-power | Number | R | Load L1 Power \[0x028A\] |
| upload-load-l2-power | Number | R | Load L2 Power \[0x028B\] |
| upload-load-l3-power | Number | R | Load L3 Power \[0x028C\] |
| upload-load-voltage-l1 | Number | R | Load Voltage L1 \[0x0284\] |
| upload-load-voltage-l2 | Number | R | Load Voltage L2 \[0x0285\] |
| upload-load-voltage-l3 | Number | R | Load Voltage L3 \[0x0286\] |
| upload-total-load-consumption | Number | R | Total Load Consumption \[0x020F,0x0210\] |
| upload-total-load-power | Number | R | Total Load Power \[0x028D\] |
## Full Example
This is an example for a DEYE 12kW (SUN-12K-SG04LP3-EU) hybrid inverter
### `solarman.things`
Please replace the `hostname` and `serialNumber` with the correct values for your logger.
```java
Thing solarman:logger:local [hostname="x.x.x.x",inverterType="deye_sg04lp3",serialNumber="1234567890"]
```
### `solarman.items`
Items file example for a SUN-12K-SG04LP3-EU inverter
```text
Number:Dimensionless Communication_Board_Version_No "Communication Board Version No [%s]" (solarman) {channel="solarman:logger:local:inverter-communication-board-version-no-"}
Number:Dimensionless Control_Board_Version_No "Control Board Version No [%s]" (solarman) {channel="solarman:logger:local:inverter-control-board-version-no-"}
String Inverter_Id "Inverter Id [%s]" (solarman) {channel="solarman:logger:local:inverter-inverter-id"}
Number:Temperature AC_Temperature "AC Temperature [%.1f °C]" (solarman) {channel="solarman:logger:local:inverter-ac-temperature", unit="°C"}
Number:Temperature DC_Temperature "DC Temperature [%.1f °C]" (solarman) {channel="solarman:logger:local:inverter-dc-temperature", unit="°C"}
Number:Power Inverter_L1_Power "Inverter L1 Power [%d W]" (solarman) {channel="solarman:logger:local:inverter-inverter-l1-power", unit="W"}
Number:Power Inverter_L2_Power "Inverter L2 Power [%d W]" (solarman) {channel="solarman:logger:local:inverter-inverter-l2-power", unit="W"}
Number:Power Inverter_L3_Power "Inverter L3 Power [%d W]" (solarman) {channel="solarman:logger:local:inverter-inverter-l3-power", unit="W"}
Number:ElectricCurrent Current_L1 "Current L1 [%.1f A]" (solarman) {channel="solarman:logger:local:inverter-current-l1", unit="A"}
Number:ElectricCurrent Current_L2 "Current L2 [%.1f A]" (solarman) {channel="solarman:logger:local:inverter-current-l2", unit="A"}
Number:ElectricCurrent Current_L3 "Current L3 [%.1f A]" (solarman) {channel="solarman:logger:local:inverter-current-l3", unit="A"}
Number:Power External_CT_L1_Power "External CT L1 Power [%d W]" (solarman) {channel="solarman:logger:local:grid-external-ct-l1-power", unit="W"}
Number:Power External_CT_L2_Power "External CT L2 Power [%d W]" (solarman) {channel="solarman:logger:local:grid-external-ct-l2-power", unit="W"}
Number:Power External_CT_L3_Power "External CT L3 Power [%d W]" (solarman) {channel="solarman:logger:local:grid-external-ct-l3-power", unit="W"}
Number:Power Internal_CT_L1_Power "Internal CT L1 Power [%d W]" (solarman) {channel="solarman:logger:local:grid-internal-ct-l1-power", unit="W"}
Number:Power Internal_CT_L2_Power "Internal CT L2 Power [%d W]" (solarman) {channel="solarman:logger:local:grid-internal-ct-l2-power", unit="W"}
Number:Power Internal_CT_L3_Power "Internal CT L3 Power [%d W]" (solarman) {channel="solarman:logger:local:grid-internal-ct-l3-power", unit="W"}
Number:ElectricPotential Grid_Voltage_L1 "Grid Voltage L1 [%d V]" (solarman) {channel="solarman:logger:local:grid-grid-voltage-l1", unit="V"}
Number:ElectricPotential Grid_Voltage_L2 "Grid Voltage L2 [%d V]" (solarman) {channel="solarman:logger:local:grid-grid-voltage-l2", unit="V"}
Number:ElectricPotential Grid_Voltage_L3 "Grid Voltage L3 [%d V]" (solarman) {channel="solarman:logger:local:grid-grid-voltage-l3", unit="V"}
Number:Power Total_Grid_Power "Total Instant Grid Power [%d W]" (solarman) {channel="solarman:logger:local:grid-total-grid-power", unit="W"}
Number:Energy Total_Grid_Production "Total Grid Feed-in [%.1f kWh]" (solarman) {channel="solarman:logger:local:grid-total-grid-production", unit="kWh"}
Number:Energy Daily_Energy_Sold "Daily Energy Sold [%d Wh]" (solarman) {channel="solarman:logger:local:grid-daily-energy-sold", unit="Wh"}
Number:Energy Total_Energy_Sold "Total Energy Sold [%d kWh]" (solarman) {channel="solarman:logger:local:grid-total-energy-sold", unit="kWh"}
Number:Energy Total_Energy_Bought "Total Energy Bought [%d kWh]" (solarman) {channel="solarman:logger:local:grid-total-energy-bought", unit="kWh"}
Number:Energy Daily_Energy_Bought "Daily Energy Bought [%d kWh]" (solarman) {channel="solarman:logger:local:grid-daily-energy-bought", unit="kWh"}
Number:Energy Daily_Production "Daily Production [%.1f kWh]" (solarman) {channel="solarman:logger:local:solar-daily-production", unit="kWh"}
Number:Energy Total_Production "Total Production [%d kWh]" (solarman) {channel="solarman:logger:local:solar-total-production", unit="kWh"}
Number:Energy Daily_Load_Consumption "Daily Load Consumption [%.1f kWh]" (solarman) {channel="solarman:logger:local:upload-daily-load-consumption", unit="kWh"}
Number:Energy Total_Load_Consumption "Total Load Consumption [%d kWh]" (solarman) {channel="solarman:logger:local:upload-total-load-consumption", unit="kWh"}
Number:Power Load_L1_Power "Load L1 Power [%d W]" (solarman) {channel="solarman:logger:local:upload-load-l1-power", unit="W"}
Number:Power Load_L2_Power "Load L2 Power [%d W]" (solarman) {channel="solarman:logger:local:upload-load-l2-power", unit="W"}
Number:Power Load_L3_Power "Load L3 Power [%d W]" (solarman) {channel="solarman:logger:local:upload-load-l3-power", unit="W"}
Number:Power Total_Load_Power "Total Load Power [%d W]" (solarman) {channel="solarman:logger:local:upload-total-load-power", unit="W"}
Number:ElectricPotential Load_Voltage_L1 "Load Voltage L1 [%d V]" (solarman) {channel="solarman:logger:local:upload-load-voltage-l1", unit="V"}
Number:ElectricPotential Load_Voltage_L2 "Load Voltage L2 [%d V]" (solarman) {channel="solarman:logger:local:upload-load-voltage-l2", unit="V"}
Number:ElectricPotential Load_Voltage_L3 "Load Voltage L3 [%d V]" (solarman) {channel="solarman:logger:local:upload-load-voltage-l3", unit="V"}
Number:Energy Daily_Energy_Consumption "Daily Energy Consumption [%d kWh]" (solarman) {channel="solarman:logger:local:upload-daily-load-consumption", unit="kWh"}
Number:Energy Total_Energy_Consumption "Total Energy Consumption [%d kWh]" (solarman) {channel="solarman:logger:local:upload-total-load-consumption", unit="kWh"}
Number:ElectricCurrent PV1_Current "PV1 Current [%.1f A]" (solarman) {channel="solarman:logger:local:solar-pv1-current", unit="A"}
Number:Power PV1_Power "PV1 Power [%d W]" (solarman) {channel="solarman:logger:local:solar-pv1-power", unit="W"}
Number:ElectricPotential PV1_Voltage "PV1 Voltage [%d V]" (solarman) {channel="solarman:logger:local:solar-pv1-voltage", unit="V"}
Number:ElectricCurrent PV2_Current "PV2 Current [%.1f A]" (solarman) {channel="solarman:logger:local:solar-pv2-current", unit="A"}
Number:Power PV2_Power "PV2 Power [%d W]" (solarman) {channel="solarman:logger:local:solar-pv2-power", unit="W"}
Number:ElectricPotential PV2_Voltage "PV2 Voltage [%d V]" (solarman) {channel="solarman:logger:local:solar-pv2-voltage", unit="V"}
Number:Dimensionless Battery_SOC "Battery SOC [%d %%]" (solarman) {channel="solarman:logger:local:battery-battery-soc", unit="%"}
Number:ElectricCurrent Battery_Current "Battery Current [%.1f A]" (solarman) {channel="solarman:logger:local:battery-battery-current", unit="A"}
Number:Power Battery_Power "Battery Power [%d W]" (solarman) {channel="solarman:logger:local:battery-battery-power", unit="W"}
Number:ElectricPotential Battery_Voltage "Battery Voltage [%.2f V]" (solarman) {channel="solarman:logger:local:battery-battery-voltage", unit="V"}
Number:Temperature Battery_Temperature "Battery Temperature [%.1f °C]" (solarman) {channel="solarman:logger:local:battery-battery-temperature", unit="°C"}
Number:Energy Daily_Battery_Charge "Daily Battery Charge [%.1f kWh]" (solarman) {channel="solarman:logger:local:battery-daily-battery-charge", unit="kWh"}
Number:Energy Daily_Battery_Discharge "Daily Battery Discharge [%.1f kWh]" (solarman) {channel="solarman:logger:local:battery-daily-battery-discharge", unit="kWh"}
Number:Energy Total_Battery_Charge "Total Battery Charge [%d kWh]" (solarman) {channel="solarman:logger:local:battery-total-battery-charge", unit="kWh"}
Number:Energy Total_Battery_Discharge "Total Battery Discharge [%d kWh]" (solarman) {channel="solarman:logger:local:battery-total-battery-discharge", unit="kWh"}
Number Alert "Alert [%s]" (solarman) {channel="solarman:logger:local:alert-alert"}
```
### `solarman.sitemap`
Sitemap example for a SUN-12K-SG04LP3-EU inverter
```perl
sitemap solarman label="Solarman"
{
Frame label="Inverter"{
Text item=Communication_Board_Version_No icon="solar"
Text item=Control_Board_Version_No icon="solar"
Text item=Inverter_Id icon="solar"
Text item=AC_Temperature icon="temperature"
Text item=DC_Temperature icon="temperature"
Text item=Inverter_L1_Power icon="poweroutlet"
Text item=Inverter_L2_Power icon="poweroutlet"
Text item=Inverter_L3_Power icon="poweroutlet"
Text item=Current_L1 icon="line"
Text item=Current_L2 icon="line"
Text item=Current_L3 icon="line"
}
Frame label="Battery"{
Text item=Battery_SOC icon="battery"
Text item=Battery_Current icon="current"
Text item=Battery_Power icon="power"
Text item=Battery_Voltage icon="voltage"
Text item=Battery_Temperature icon="temperature"
Text item=Daily_Battery_Charge icon="renewable"
Text item=Daily_Battery_Discharge icon="battery"
Text item=Total_Battery_Charge icon="renewable"
Text item=Total_Battery_Discharge icon="battery"
}
Frame label="Solar"{
Text item=Total_Production icon="solar"
Text item=Daily_Production icon="solar"
Text item=PV1_Current icon="solar"
Text item=PV1_Power icon="solar"
Text item=PV1_Voltage icon="solar"
Text item=PV2_Current icon="solar"
Text item=PV2_Power icon="solar"
Text item=PV2_Voltage icon="solar"
}
Frame label="Grid"{
Text item=Total_Grid_Production icon="power"
Text item=Total_Grid_Power icon="power"
Text item=External_CT_L1_Power icon="power"
Text item=External_CT_L2_Power icon="power"
Text item=External_CT_L3_Power icon="power"
Text item=Internal_CT_L1_Power icon="power"
Text item=Internal_CT_L2_Power icon="power"
Text item=Internal_CT_L3_Power icon="power"
Text item=Grid_Voltage_L1 icon="power"
Text item=Grid_Voltage_L2 icon="power"
Text item=Grid_Voltage_L3 icon="power"
Text item=Daily_Energy_Sold icon="power"
Text item=Total_Energy_Sold icon="power"
Text item=Daily_Energy_Bought icon="power"
Text item=Total_Energy_Bought icon="power"
}
Frame label="Load"{
Text item=Daily_Load_Consumption icon="power"
Text item=Total_Load_Consumption icon="power"
Text item=Load_L1_Power icon="power"
Text item=Load_L2_Power icon="power"
Text item=Load_L3_Power icon="power"
Text item=Load_Voltage_L1 icon="power"
Text item=Load_Voltage_L2 icon="power"
Text item=Load_Voltage_L3 icon="power"
Text item=Total_Load_Power icon="power"
}
Frame label="Alert"{
Text item=Alert icon="alert"
}
}
```
## Acknowledgments
The code's creation draws significant inspiration from [Stephan Joubert's Home Assistant plugin](https://github.com/StephanJoubert/home_assistant_solarman), which provides the inverter definitions used in the project.
Additionally, the [pysolarmanv5 module](https://pysolarmanv5.readthedocs.io/en/latest/index.html) was a valuable resource, as it offers an excellent explanation of the Solarman V5 protocol.

View File

@ -0,0 +1,17 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.openhab.addons.bundles</groupId>
<artifactId>org.openhab.addons.reactor.bundles</artifactId>
<version>4.3.0-SNAPSHOT</version>
</parent>
<artifactId>org.openhab.binding.solarman</artifactId>
<name>openHAB Add-ons :: Bundles :: Solarman Binding</name>
</project>

View File

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<features name="org.openhab.binding.solarman-${project.version}" xmlns="http://karaf.apache.org/xmlns/features/v1.4.0">
<repository>mvn:org.openhab.core.features.karaf/org.openhab.core.features.karaf.openhab-core/${ohc.version}/xml/features</repository>
<feature name="openhab-binding-solarman" description="Solarman Binding" version="${project.version}">
<feature>openhab-runtime-base</feature>
<bundle start-level="80">mvn:org.openhab.addons.bundles/org.openhab.binding.solarman/${project.version}</bundle>
</feature>
</features>

View File

@ -0,0 +1,62 @@
/**
* Copyright (c) 2010-2024 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.solarman.internal;
import java.io.IOException;
import java.io.InputStream;
import java.util.Objects;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.solarman.internal.defmodel.InverterDefinition;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.dataformat.yaml.YAMLFactory;
/**
* The {@link DefinitionParser} is parses inverter definitions
*
* @author Catalin Sanda - Initial contribution
*/
@NonNullByDefault
public class DefinitionParser {
private final Logger logger = LoggerFactory.getLogger(DefinitionParser.class);
private final ObjectMapper mapper;
public DefinitionParser() {
mapper = new ObjectMapper(new YAMLFactory());
}
@Nullable
public InverterDefinition parseDefinition(String definitionId) {
ClassLoader cl = Objects.requireNonNull(getClass().getClassLoader());
String definitionFileName = String.format("definitions/%s.yaml", definitionId);
try (InputStream is = cl.getResourceAsStream(definitionFileName)) {
if (is == null) {
logger.warn("Unable to read definition file {}", definitionFileName);
return null;
}
InverterDefinition inverterDefinition = mapper.readValue(is, InverterDefinition.class);
inverterDefinition.setInverterDefinitionId(definitionId);
return inverterDefinition;
} catch (IOException e) {
logger.warn("Error parsing definition with ID: {}", definitionId, e);
return null;
}
}
}

View File

@ -0,0 +1,30 @@
/**
* Copyright (c) 2010-2024 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.solarman.internal;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.core.thing.ThingTypeUID;
/**
* The {@link SolarmanBindingConstants} class defines common constants, which are
* used across the whole binding.
*
* @author Catalin Sanda - Initial contribution
*/
@NonNullByDefault
public class SolarmanBindingConstants {
public static final String SOLARMAN_BINDING_ID = "solarman";
public static final ThingTypeUID THING_TYPE_SOLARMAN_LOGGER = new ThingTypeUID(SOLARMAN_BINDING_ID, "logger");
public static final String DYNAMIC_CHANNEL = "dynamic-channel";
}

View File

@ -0,0 +1,53 @@
/**
* Copyright (c) 2010-2024 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.solarman.internal;
import java.util.Set;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.core.thing.Thing;
import org.openhab.core.thing.ThingTypeUID;
import org.openhab.core.thing.binding.BaseThingHandlerFactory;
import org.openhab.core.thing.binding.ThingHandler;
import org.openhab.core.thing.binding.ThingHandlerFactory;
import org.osgi.service.component.annotations.Component;
/**
* The {@link SolarmanHandlerFactory} is responsible for creating things and thing
* handlers.
*
* @author Catalin Sanda - Initial contribution
*/
@NonNullByDefault
@Component(configurationPid = "binding.solarman", service = ThingHandlerFactory.class)
public class SolarmanHandlerFactory extends BaseThingHandlerFactory {
private static final Set<ThingTypeUID> SUPPORTED_THING_TYPES_UIDS = Set
.of(SolarmanBindingConstants.THING_TYPE_SOLARMAN_LOGGER);
@Override
public boolean supportsThingType(ThingTypeUID thingTypeUID) {
return SUPPORTED_THING_TYPES_UIDS.contains(thingTypeUID);
}
@Override
protected @Nullable ThingHandler createHandler(Thing thing) {
ThingTypeUID thingTypeUID = thing.getThingTypeUID();
if (SolarmanBindingConstants.THING_TYPE_SOLARMAN_LOGGER.equals(thingTypeUID)) {
return new SolarmanLoggerHandler(thing);
}
return null;
}
}

View File

@ -0,0 +1,74 @@
/**
* Copyright (c) 2010-2024 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.solarman.internal;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
/**
* The {@link SolarmanLoggerConfiguration} class contains fields mapping thing configuration parameters.
*
* @author Catalin Sanda - Initial contribution
*/
@NonNullByDefault
public class SolarmanLoggerConfiguration {
/**
* Solarman Logger Thing Configuration Parameters
*/
public String hostname = "";
public Integer port = 8899;
public String serialNumber = "";
public String inverterType = "sg04lp3";
public int refreshInterval = 30;
@Nullable
public String additionalRequests;
public SolarmanLoggerConfiguration() {
}
public SolarmanLoggerConfiguration(String hostname, Integer port, String serialNumber, String inverterType,
int refreshInterval, @Nullable String additionalRequests) {
this.hostname = hostname;
this.port = port;
this.serialNumber = serialNumber;
this.inverterType = inverterType;
this.refreshInterval = refreshInterval;
this.additionalRequests = additionalRequests;
}
public String getHostname() {
return hostname;
}
public Integer getPort() {
return port;
}
public String getSerialNumber() {
return serialNumber;
}
public String getInverterType() {
return inverterType;
}
public int getRefreshInterval() {
return refreshInterval;
}
@Nullable
public String getAdditionalRequests() {
return additionalRequests;
}
}

View File

@ -0,0 +1,225 @@
/**
* Copyright (c) 2010-2024 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.solarman.internal;
import static org.openhab.binding.solarman.internal.SolarmanBindingConstants.DYNAMIC_CHANNEL;
import java.util.AbstractMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.solarman.internal.channel.BaseChannelConfig;
import org.openhab.binding.solarman.internal.channel.SolarmanChannelManager;
import org.openhab.binding.solarman.internal.defmodel.InverterDefinition;
import org.openhab.binding.solarman.internal.defmodel.ParameterItem;
import org.openhab.binding.solarman.internal.defmodel.Request;
import org.openhab.binding.solarman.internal.defmodel.Validation;
import org.openhab.binding.solarman.internal.modbus.SolarmanLoggerConnector;
import org.openhab.binding.solarman.internal.modbus.SolarmanV5Protocol;
import org.openhab.binding.solarman.internal.updater.SolarmanChannelUpdater;
import org.openhab.binding.solarman.internal.updater.SolarmanProcessResult;
import org.openhab.core.thing.Channel;
import org.openhab.core.thing.ChannelUID;
import org.openhab.core.thing.Thing;
import org.openhab.core.thing.ThingStatus;
import org.openhab.core.thing.ThingStatusDetail;
import org.openhab.core.thing.binding.BaseThingHandler;
import org.openhab.core.thing.binding.builder.ThingBuilder;
import org.openhab.core.types.Command;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* The {@link SolarmanLoggerHandler} is responsible for handling commands, which are
* sent to one of the channels.
*
* @author Catalin Sanda - Initial contribution
*/
@NonNullByDefault
public class SolarmanLoggerHandler extends BaseThingHandler {
private final Logger logger = LoggerFactory.getLogger(SolarmanLoggerHandler.class);
private final DefinitionParser definitionParser;
private final SolarmanChannelManager solarmanChannelManager;
@Nullable
private volatile ScheduledFuture<?> scheduledFuture;
public SolarmanLoggerHandler(Thing thing) {
super(thing);
this.definitionParser = new DefinitionParser();
this.solarmanChannelManager = new SolarmanChannelManager();
}
@Override
public void handleCommand(ChannelUID channelUID, Command command) {
}
@Override
public void initialize() {
updateStatus(ThingStatus.UNKNOWN);
SolarmanLoggerConfiguration config = getConfigAs(SolarmanLoggerConfiguration.class);
SolarmanLoggerConnector solarmanLoggerConnector = new SolarmanLoggerConnector(config);
List<Channel> staticChannels = thing.getChannels().stream()
.filter(channel -> !channel.getProperties().containsKey(DYNAMIC_CHANNEL)).toList();
InverterDefinition inverterDefinition = definitionParser.parseDefinition(config.inverterType);
if (inverterDefinition == null) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
"Unable to find a definition for the provided inverter type");
return;
} else {
if (logger.isDebugEnabled()) {
logger.debug("Found definition for {}", config.inverterType);
}
}
SolarmanV5Protocol solarmanV5Protocol = new SolarmanV5Protocol(config);
String additionalRequests = Objects.requireNonNullElse(config.getAdditionalRequests(), "");
List<Request> mergedRequests = !additionalRequests.isBlank()
? mergeRequests(inverterDefinition.getRequests(), extractAdditionalRequests(additionalRequests))
: inverterDefinition.getRequests();
Map<ParameterItem, ChannelUID> paramToChannelMapping = mergeMaps(
extractChannelMappingFromChannels(staticChannels),
setupChannelsForInverterDefinition(inverterDefinition));
SolarmanChannelUpdater solarmanChannelUpdater = new SolarmanChannelUpdater(this::updateState);
scheduledFuture = scheduler
.scheduleWithFixedDelay(
() -> queryLoggerAndUpdateState(solarmanLoggerConnector, solarmanV5Protocol, mergedRequests,
paramToChannelMapping, solarmanChannelUpdater),
0, config.refreshInterval, TimeUnit.SECONDS);
}
private void queryLoggerAndUpdateState(SolarmanLoggerConnector solarmanLoggerConnector,
SolarmanV5Protocol solarmanV5Protocol, List<Request> mergedRequests,
Map<ParameterItem, ChannelUID> paramToChannelMapping, SolarmanChannelUpdater solarmanChannelUpdater) {
try {
SolarmanProcessResult solarmanProcessResult = solarmanChannelUpdater.fetchDataFromLogger(mergedRequests,
solarmanLoggerConnector, solarmanV5Protocol, paramToChannelMapping);
if (solarmanProcessResult.hasSuccessfulResponses()) {
updateStatus(ThingStatus.ONLINE);
} else {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
solarmanProcessResult.toString());
}
} catch (Exception e) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
}
}
private <K, V> Map<K, V> mergeMaps(Map<K, V> map1, Map<K, V> map2) {
return Stream.concat(map1.entrySet().stream(), map2.entrySet().stream())
.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue, (v1, v2) -> v1));
}
private Map<ParameterItem, ChannelUID> extractChannelMappingFromChannels(List<Channel> channels) {
return channels.stream().map(channel -> {
BaseChannelConfig bcc = channel.getConfiguration().as(BaseChannelConfig.class);
@Nullable
String label = channel.getLabel();
if (label == null) {
throw new IllegalStateException("Channel label should not be null");
}
return new AbstractMap.SimpleEntry<>(new ParameterItem(label, "N/A", "N/A", bcc.uom, bcc.scale, bcc.rule,
parseRegisters(bcc.registers), "N/A", new Validation(), bcc.offset, Boolean.FALSE),
channel.getUID());
}).collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
}
private List<Integer> parseRegisters(String registers) {
String[] tokens = registers.split(",");
Pattern pattern = Pattern.compile("\\s*(0x[\\da-fA-F]+|[\\d]+)\\s*");
return Stream.of(tokens).map(pattern::matcher).filter(Matcher::matches).map(matcher -> matcher.group(1))
.map(SolarmanLoggerHandler::parseNumber).toList();
}
// For now just concatenate the list, in the future, merge overlapping requests
private List<Request> mergeRequests(List<Request> requestList1, List<Request> requestList2) {
return Stream.concat(requestList1.stream(), requestList2.stream()).collect(Collectors.toList());
}
private List<Request> extractAdditionalRequests(String channels) {
String[] tokens = channels.split(",");
Pattern pattern = Pattern.compile(
"\\s*(0x[\\da-fA-F]+|[\\d]+)\\s*:\\s*(0x[\\da-fA-F]+|[\\d]+)\\s*-\\s*(0x[\\da-fA-F]+|[\\d]+)\\s*");
return Stream.of(tokens).map(pattern::matcher).filter(Matcher::matches).map(matcher -> {
try {
int functionCode = parseNumber(matcher.group(1));
int start = parseNumber(matcher.group(2));
int end = parseNumber(matcher.group(3));
return new Request(functionCode, start, end);
} catch (NumberFormatException e) {
logger.debug("Invalid number format in token: {} , ignoring additional requests", matcher.group(), e);
return new Request(-1, 0, 0);
}
}).filter(request -> request.getMbFunctioncode() > 0).collect(Collectors.toList());
}
private static int parseNumber(String number) {
return number.startsWith("0x") ? Integer.parseInt(number.substring(2), 16) : Integer.parseInt(number);
}
private Map<ParameterItem, ChannelUID> setupChannelsForInverterDefinition(InverterDefinition inverterDefinition) {
ThingBuilder thingBuilder = editThing();
List<Channel> oldDynamicChannels = thing.getChannels().stream()
.filter(channel -> channel.getProperties().containsKey(DYNAMIC_CHANNEL)).toList();
Map<ParameterItem, Channel> newDynamicItemChannelMap = solarmanChannelManager.generateItemChannelMap(thing,
inverterDefinition);
// Remove old dynamic channels
thingBuilder.withoutChannels(oldDynamicChannels);
// Add new dynamic channels
newDynamicItemChannelMap.values().forEach(thingBuilder::withChannel);
updateThing(thingBuilder.build());
logger.debug("Updated thing with id {} and {} channels", thing.getThingTypeUID(), thing.getChannels().size());
return newDynamicItemChannelMap.entrySet().stream()
.map(entry -> new AbstractMap.SimpleEntry<>(entry.getKey(), entry.getValue().getUID()))
.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
}
@Override
public void dispose() {
super.dispose();
ScheduledFuture<?> scheduledFuture = this.scheduledFuture;
if (scheduledFuture != null) {
scheduledFuture.cancel(false);
this.scheduledFuture = null;
}
}
}

View File

@ -0,0 +1,30 @@
/**
* Copyright (c) 2010-2024 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.solarman.internal.channel;
import java.math.BigDecimal;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
/**
* @author Catalin Sanda - Initial contribution
*/
@NonNullByDefault
public class BaseChannelConfig {
public @Nullable String uom;
public BigDecimal scale = BigDecimal.ONE;
public Integer rule = 1;
public BigDecimal offset = BigDecimal.ZERO;
public String registers = "";
}

View File

@ -0,0 +1,100 @@
/**
* Copyright (c) 2010-2024 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.solarman.internal.channel;
import static org.openhab.binding.solarman.internal.SolarmanBindingConstants.DYNAMIC_CHANNEL;
import static org.openhab.binding.solarman.internal.typeprovider.ChannelUtils.escapeName;
import java.math.BigDecimal;
import java.util.AbstractMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.solarman.internal.defmodel.InverterDefinition;
import org.openhab.binding.solarman.internal.defmodel.ParameterItem;
import org.openhab.binding.solarman.internal.typeprovider.ChannelUtils;
import org.openhab.core.config.core.Configuration;
import org.openhab.core.thing.Channel;
import org.openhab.core.thing.ChannelUID;
import org.openhab.core.thing.Thing;
import org.openhab.core.thing.binding.builder.ChannelBuilder;
import org.openhab.core.thing.type.ChannelKind;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
/**
* @author Catalin Sanda - Initial contribution
*/
@NonNullByDefault
public class SolarmanChannelManager {
private final ObjectMapper objectMapper;
public SolarmanChannelManager() {
objectMapper = new ObjectMapper();
}
public Map<ParameterItem, Channel> generateItemChannelMap(Thing thing, InverterDefinition inverterDefinition) {
return inverterDefinition.getParameters().stream().flatMap(parameter -> {
String groupName = escapeName(parameter.getGroup());
return parameter.getItems().stream().map(item -> {
String channelId = groupName + "-" + escapeName(item.getName());
Channel channel = ChannelBuilder.create(new ChannelUID(thing.getUID(), channelId))
.withType(ChannelUtils.computeChannelTypeId(inverterDefinition.getInverterDefinitionId(),
groupName, item.getName()))
.withLabel(item.getName()).withKind(ChannelKind.STATE)
.withAcceptedItemType(ChannelUtils.getItemType(item))
.withProperties(Map.of(DYNAMIC_CHANNEL, Boolean.TRUE.toString()))
.withConfiguration(buildConfigurationFromItem(item)).build();
return new AbstractMap.SimpleEntry<>(item, channel);
});
}).collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
}
private Configuration buildConfigurationFromItem(ParameterItem item) {
Configuration configuration = new Configuration();
BaseChannelConfig baseChannelConfig = new BaseChannelConfig();
BigDecimal offset = item.getOffset();
if (offset != null) {
baseChannelConfig.offset = offset;
}
BigDecimal scale = item.getScale();
if (scale != null) {
baseChannelConfig.scale = scale;
}
baseChannelConfig.rule = item.getRule();
baseChannelConfig.registers = convertRegisters(item.getRegisters());
baseChannelConfig.uom = item.getUom();
Map<String, Object> configurationMap = objectMapper.convertValue(baseChannelConfig, new TypeReference<>() {
});
configurationMap.forEach(configuration::put);
return configuration;
}
private String convertRegisters(List<Integer> registers) {
return "["
+ registers.stream().map(register -> String.format("0x%04X", register)).collect(Collectors.joining(","))
+ "]";
}
}

View File

@ -0,0 +1,55 @@
/**
* Copyright (c) 2010-2024 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.solarman.internal.defmodel;
import java.util.ArrayList;
import java.util.List;
import org.eclipse.jdt.annotation.NonNullByDefault;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
/**
* @author Catalin Sanda - Initial contribution
*/
@JsonIgnoreProperties(ignoreUnknown = true)
@NonNullByDefault
public class InverterDefinition {
private String inverterDefinitionId = "";
private List<Request> requests = new ArrayList<>();
private List<Parameter> parameters = new ArrayList<>();
public String getInverterDefinitionId() {
return inverterDefinitionId;
}
public void setInverterDefinitionId(String inverterDefinitionId) {
this.inverterDefinitionId = inverterDefinitionId;
}
public List<Request> getRequests() {
return requests;
}
public void setRequests(List<Request> requests) {
this.requests = requests;
}
public List<Parameter> getParameters() {
return parameters;
}
public void setParameters(List<Parameter> parameters) {
this.parameters = parameters;
}
}

View File

@ -0,0 +1,46 @@
/**
* Copyright (c) 2010-2024 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.solarman.internal.defmodel;
import java.util.ArrayList;
import java.util.List;
import org.eclipse.jdt.annotation.NonNullByDefault;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
/**
* @author Catalin Sanda - Initial contribution
*/
@JsonIgnoreProperties(ignoreUnknown = true)
@NonNullByDefault
public class Parameter {
private String group = "";
private List<ParameterItem> items = new ArrayList<ParameterItem>();
public String getGroup() {
return group;
}
public void setGroup(String group) {
this.group = group;
}
public List<ParameterItem> getItems() {
return items;
}
public void setItems(List<ParameterItem> items) {
this.items = items;
}
}

View File

@ -0,0 +1,156 @@
/**
* Copyright (c) 2010-2024 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.solarman.internal.defmodel;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.List;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
/**
* @author Catalin Sanda - Initial contribution
*/
@JsonIgnoreProperties(ignoreUnknown = true)
@NonNullByDefault
public class ParameterItem {
private String name = "";
@Nullable
private String itemClass;
@Nullable
private String stateClass;
@Nullable
private String uom;
@Nullable
private BigDecimal scale;
private Integer rule = 1;
private List<Integer> registers = new ArrayList<>();
@Nullable
private String icon;
@Nullable
private Validation validation;
@Nullable
private BigDecimal offset;
@Nullable
private Boolean isstr;
public ParameterItem() {
}
public ParameterItem(String name, @Nullable String itemClass, @Nullable String stateClass, @Nullable String uom,
@Nullable BigDecimal scale, Integer rule, List<Integer> registers, @Nullable String icon,
@Nullable Validation validation, @Nullable BigDecimal offset, @Nullable Boolean isstr) {
this.name = name;
this.itemClass = itemClass;
this.stateClass = stateClass;
this.uom = uom;
this.scale = scale;
this.rule = rule;
this.registers = registers;
this.icon = icon;
this.validation = validation;
this.offset = offset;
this.isstr = isstr;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public @Nullable String getStateClass() {
return stateClass;
}
public void setStateClass(String stateClass) {
this.stateClass = stateClass;
}
public @Nullable String getUom() {
return uom;
}
public void setUom(String uom) {
this.uom = uom;
}
public @Nullable BigDecimal getScale() {
return scale;
}
public void setScale(BigDecimal scale) {
this.scale = scale;
}
public Integer getRule() {
return rule;
}
public void setRule(Integer rule) {
this.rule = rule;
}
public List<Integer> getRegisters() {
return registers;
}
public void setRegisters(List<Integer> registers) {
this.registers = registers;
}
public @Nullable String getIcon() {
return icon;
}
public void setIcon(String icon) {
this.icon = icon;
}
public @Nullable Validation getValidation() {
return validation;
}
public void setValidation(Validation validation) {
this.validation = validation;
}
public @Nullable BigDecimal getOffset() {
return offset;
}
public void setOffset(BigDecimal offset) {
this.offset = offset;
}
public @Nullable Boolean getIsstr() {
return isstr;
}
public void setIsstr(Boolean isstr) {
this.isstr = isstr;
}
public @Nullable String getItemClass() {
return itemClass;
}
public void setItemClass(String itemClass) {
this.itemClass = itemClass;
}
}

View File

@ -0,0 +1,74 @@
/**
* Copyright (c) 2010-2024 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.solarman.internal.defmodel;
import org.eclipse.jdt.annotation.NonNullByDefault;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonProperty;
/**
* @author Catalin Sanda - Initial contribution
*/
@JsonIgnoreProperties(ignoreUnknown = true)
@NonNullByDefault
public class Request {
public static final Request NONE = new Request(-1, 0, 0);
private Integer start = 0;
private Integer end = 0;
@JsonProperty("mb_functioncode")
private Integer mbFunctioncode = 0x03;
public Request() {
}
public Request(Integer mbFunctioncode, Integer start, Integer end) {
this.mbFunctioncode = mbFunctioncode;
this.start = start;
this.end = end;
}
public Integer getStart() {
return start;
}
public void setStart(Integer start) {
this.start = start;
}
public Integer getEnd() {
return end;
}
public void setEnd(Integer end) {
this.end = end;
}
public Integer getMbFunctioncode() {
return mbFunctioncode;
}
public void setMbFunctioncode(Integer mbFunctioncode) {
this.mbFunctioncode = mbFunctioncode;
}
@Override
public String toString() {
if (this == NONE) {
return "N/A";
} else {
return "Request{" + "start=0x" + Integer.toHexString(start) + ", end=0x" + Integer.toHexString(end)
+ ", mbFunctioncode=0x" + Integer.toHexString(mbFunctioncode) + '}';
}
}
}

View File

@ -0,0 +1,54 @@
/**
* Copyright (c) 2010-2024 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.solarman.internal.defmodel;
import org.eclipse.jdt.annotation.NonNullByDefault;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonProperty;
/**
* @author Catalin Sanda - Initial contribution
*/
@JsonIgnoreProperties(ignoreUnknown = true)
@NonNullByDefault
public class Validation {
private Integer max = 0;
@JsonProperty("invalidate_all")
private Object invalidateAll = new Object();
private Integer min = 0;
public Integer getMax() {
return max;
}
public void setMax(Integer max) {
this.max = max;
}
public Object getInvalidateAll() {
return invalidateAll;
}
public void setInvalidateAll(Object invalidateAll) {
this.invalidateAll = invalidateAll;
}
public Integer getMin() {
return min;
}
public void setMin(Integer min) {
this.min = min;
}
}

View File

@ -0,0 +1,45 @@
/**
* Copyright (c) 2010-2024 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.solarman.internal.modbus;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* @author Catalin Sanda - Initial contribution
*/
@NonNullByDefault
public class CRC16Modbus {
private static final int[] CRC_TABLE = new int[256];
static {
for (int i = 0; i < 256; i++) {
int crc = i;
for (int j = 0; j < 8; j++) {
if ((crc & 0x0001) != 0) {
crc = (crc >>> 1) ^ 0xA001;
} else {
crc = crc >>> 1;
}
}
CRC_TABLE[i] = crc;
}
}
public static int calculate(byte[] data) {
int crc = 0xFFFF;
for (byte b : data) {
crc = (crc >>> 8) ^ CRC_TABLE[(crc ^ b) & 0xFF];
}
return crc;
}
}

View File

@ -0,0 +1,131 @@
/**
* Copyright (c) 2010-2024 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.solarman.internal.modbus;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.net.Socket;
import java.net.SocketAddress;
import java.net.SocketTimeoutException;
import java.util.Arrays;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.solarman.internal.modbus.exception.SolarmanConnectionException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* @author Catalin Sanda - Initial contribution
*/
@NonNullByDefault
public class SolarmanLoggerConnection implements AutoCloseable {
private final Logger logger = LoggerFactory.getLogger(SolarmanLoggerConnection.class);
@Nullable
private Socket socket;
public SolarmanLoggerConnection(String hostName, int port) {
SocketAddress sockaddr = new InetSocketAddress(hostName, port);
Socket localSocket = connectSocket(sockaddr);
if (localSocket == null) {
logger.debug("Error creating socket");
} else {
socket = localSocket;
}
}
public byte[] sendRequest(byte[] reqFrame) throws SolarmanConnectionException {
// Will not be used by multiple threads, so not bothering making it thread safe for now
Socket localSocket = socket;
if (localSocket == null) {
throw new SolarmanConnectionException("Socket is null, not reading data this time");
}
try {
logger.trace("Request frame: {}", bytesToHex(reqFrame));
localSocket.getOutputStream().write(reqFrame);
} catch (IOException e) {
logger.debug("Unable to send frame to logger");
return new byte[0];
}
byte[] buffer = new byte[1024];
int attempts = 5;
while (attempts > 0) {
attempts--;
try {
int bytesRead = localSocket.getInputStream().read(buffer);
if (bytesRead < 0) {
throw new SolarmanConnectionException("No data received");
} else {
byte[] data = Arrays.copyOfRange(buffer, 0, bytesRead);
if (logger.isDebugEnabled()) {
logger.trace("Response frame: {}", bytesToHex(data));
}
return data;
}
} catch (SocketTimeoutException e) {
logger.debug("Connection timeout", e);
if (attempts == 0) {
throw new SolarmanConnectionException("Too many socket timeouts", e);
}
} catch (IOException e) {
throw new SolarmanConnectionException("Error reading data from ", e);
}
}
return new byte[0];
}
private static String bytesToHex(byte[] bytes) {
return IntStream.range(0, bytes.length).mapToObj(i -> String.format("%02X", bytes[i]))
.collect(Collectors.joining());
}
private @Nullable Socket connectSocket(SocketAddress socketAddress) {
try {
Socket clientSocket = new Socket();
clientSocket.setSoTimeout(10_000);
clientSocket.connect(socketAddress, 10_000);
return clientSocket;
} catch (IOException e) {
logger.debug("Could not open socket on IP {}", socketAddress, e);
return null;
}
}
@Override
public void close() {
Socket localSocket = socket;
if (localSocket != null && !localSocket.isClosed()) {
try {
localSocket.close();
} catch (IOException e) {
logger.debug("Unable to close connection");
}
}
socket = null;
}
public boolean isConnected() {
Socket localSocket = socket;
return localSocket != null && localSocket.isConnected();
}
}

View File

@ -0,0 +1,33 @@
/**
* Copyright (c) 2010-2024 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.solarman.internal.modbus;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.solarman.internal.SolarmanLoggerConfiguration;
/**
* @author Catalin Sanda - Initial contribution
*/
@NonNullByDefault
public class SolarmanLoggerConnector {
private final SolarmanLoggerConfiguration solarmanLoggerConfiguration;
public SolarmanLoggerConnector(SolarmanLoggerConfiguration solarmanLoggerConfiguration) {
this.solarmanLoggerConfiguration = solarmanLoggerConfiguration;
}
public SolarmanLoggerConnection createConnection() {
return new SolarmanLoggerConnection(solarmanLoggerConfiguration.getHostname(),
solarmanLoggerConfiguration.getPort());
}
}

View File

@ -0,0 +1,252 @@
/**
* Copyright (c) 2010-2024 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.solarman.internal.modbus;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.solarman.internal.SolarmanLoggerConfiguration;
import org.openhab.binding.solarman.internal.modbus.exception.SolarmanAuthenticationException;
import org.openhab.binding.solarman.internal.modbus.exception.SolarmanConnectionException;
import org.openhab.binding.solarman.internal.modbus.exception.SolarmanException;
import org.openhab.binding.solarman.internal.modbus.exception.SolarmanProtocolException;
/**
* @author Catalin Sanda - Initial contribution
*/
@NonNullByDefault
public class SolarmanV5Protocol {
private final SolarmanLoggerConfiguration solarmanLoggerConfiguration;
public SolarmanV5Protocol(SolarmanLoggerConfiguration solarmanLoggerConfiguration) {
this.solarmanLoggerConfiguration = solarmanLoggerConfiguration;
}
public Map<Integer, byte[]> readRegisters(SolarmanLoggerConnection solarmanLoggerConnection, byte mbFunctionCode,
int firstReg, int lastReg) throws SolarmanException {
byte[] solarmanV5Frame = buildSolarmanV5Frame(mbFunctionCode, firstReg, lastReg);
byte[] respFrame = solarmanLoggerConnection.sendRequest(solarmanV5Frame);
if (respFrame.length > 0) {
byte[] modbusRespFrame = extractModbusResponseFrame(respFrame, solarmanV5Frame);
return parseModbusReadHoldingRegistersResponse(modbusRespFrame, firstReg, lastReg);
} else {
throw new SolarmanConnectionException("Response frame was empty");
}
}
/**
* Builds a SolarMAN V5 frame to request data from firstReg to lastReg.
* Frame format is based on
* <a href="https://pysolarmanv5.readthedocs.io/en/latest/solarmanv5_protocol.html">Solarman V5 Protocol</a>
*
* @param mbFunctionCode
* @param firstReg - the start register
* @param lastReg - the end register
* @return byte array containing the Solarman V5 frame
*/
protected byte[] buildSolarmanV5Frame(byte mbFunctionCode, int firstReg, int lastReg) {
byte[] requestPayload = buildSolarmanV5FrameRequestPayload(mbFunctionCode, firstReg, lastReg);
byte[] header = buildSolarmanV5FrameHeader(requestPayload.length);
byte[] trailer = buildSolarmanV5FrameTrailer(header, requestPayload);
return ByteBuffer.allocate(header.length + requestPayload.length + trailer.length).put(header)
.put(requestPayload).put(trailer).array();
}
private byte[] buildSolarmanV5FrameTrailer(byte[] header, byte[] requestPayload) {
byte[] headerAndPayload = ByteBuffer.allocate(header.length + requestPayload.length).put(header)
.put(requestPayload).array();
// (one byte) Denotes the V5 frame checksum. The checksum is computed on the entire V5 frame except for Start,
// Checksum (obviously!) and End.
// Note, that this field is completely separate to the Modbus RTU checksum, which coincidentally, is the two
// bytes immediately preceding this field.
byte[] checksum = new byte[] {
computeChecksum(Arrays.copyOfRange(headerAndPayload, 1, headerAndPayload.length)) };
// (one byte) Denotes the end of the V5 frame. Always 0x15.
byte[] end = new byte[] { (byte) 0x15 };
return ByteBuffer.allocate(checksum.length + end.length).put(checksum).put(end).array();
}
private byte computeChecksum(byte[] frame) {
// [-91, 23, 0, 16, 69, 0, 0, 46, -13, 90, 102, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 3, 0, 0, 0, 39,
// 5, -48, 122, 21]
int checksumValue = 0;
for (byte b : frame) {
checksumValue += Byte.toUnsignedInt(b);
}
return (byte) (checksumValue & 0xFF);
}
private byte[] buildSolarmanV5FrameHeader(int payloadSize) {
// (one byte) Denotes the start of the V5 frame. Always 0xA5.
byte[] start = new byte[] { (byte) 0xA5 };
// (two bytes) Payload length
byte[] length = ByteBuffer.allocate(Short.BYTES).order(ByteOrder.LITTLE_ENDIAN).putShort((short) payloadSize)
.array();
// (two bytes) Describes the type of V5 frame. For Modbus RTU requests, the control code is 0x4510. For Modbus
// RTU responses, the control code is 0x1510.
byte[] controlCode = new byte[] { (byte) 0x10, (byte) 0x45 };
// (two bytes) This field acts as a two-way sequence number. On outgoing requests, the first byte of this
// field is echoed back in the same position on incoming responses.
// This is done by initialising this byte to a random value, and incrementing for each subsequent request.
// The second byte is incremented by the data logging stick for every response sent (either to Solarman Cloud or
// local requests).
// Note: the increment part is not implemented yet
byte[] serial = new byte[] { (byte) 0x00, (byte) 0x00 };
// (four bytes) Serial number of Solarman data logging stick
byte[] loggerSerial = ByteBuffer.allocate(Integer.BYTES).order(ByteOrder.LITTLE_ENDIAN)
.putInt((int) Long.parseUnsignedLong(solarmanLoggerConfiguration.getSerialNumber())).array();
// Append all fields into the header
return ByteBuffer
.allocate(start.length + length.length + controlCode.length + serial.length + loggerSerial.length)
.put(start).put(length).put(controlCode).put(serial).put(loggerSerial).array();
}
protected byte[] buildSolarmanV5FrameRequestPayload(byte mbFunctionCode, int firstReg, int lastReg) {
// (one byte) Denotes the frame type.
byte[] frameType = new byte[] { 0x02 };
// (two bytes) Denotes the sensor type.
byte[] sensorType = new byte[] { 0x00, 0x00 };
// (four bytes) Denotes the frame total working time. See corresponding response field of same name for
// further details.
byte[] totalWorkingTime = new byte[] { 0x00, 0x00, 0x00, 0x00 };
// (four bytes) Denotes the frame power on time.
byte[] powerOnTime = new byte[] { 0x00, 0x00, 0x00, 0x00 };
// Denotes the frame offset time.
byte[] offsetTime = new byte[] { 0x00, 0x00, 0x00, 0x00 };
// (variable length) Modbus RTU request frame.
byte[] requestFrame = buildModbusReadHoldingRegistersRequestFrame((byte) 0x01, mbFunctionCode, firstReg,
lastReg);
return ByteBuffer
.allocate(frameType.length + sensorType.length + totalWorkingTime.length + powerOnTime.length
+ offsetTime.length + requestFrame.length)
.put(frameType).put(sensorType).put(totalWorkingTime).put(powerOnTime).put(offsetTime).put(requestFrame)
.array();
}
/**
* Based on <a href="https://www.modbustools.com/modbus.html#function03">Function 03 (03hex) Read Holding
* Registers</a>
*
* @param slaveId - Slave Address
* @param mbFunctionCode -
* @param firstReg - Starting Address
* @param lastReg - Ending Address
* @return byte array containing the Modbus request frame
*/
protected byte[] buildModbusReadHoldingRegistersRequestFrame(byte slaveId, byte mbFunctionCode, int firstReg,
int lastReg) {
int regCount = lastReg - firstReg + 1;
byte[] req = ByteBuffer.allocate(6).put(slaveId).put(mbFunctionCode).putShort((short) firstReg)
.putShort((short) regCount).array();
byte[] crc = ByteBuffer.allocate(Short.BYTES).order(ByteOrder.LITTLE_ENDIAN)
.putShort((short) CRC16Modbus.calculate(req)).array();
return ByteBuffer.allocate(req.length + crc.length).put(req).put(crc).array();
}
protected Map<Integer, byte[]> parseModbusReadHoldingRegistersResponse(byte @Nullable [] frame, int firstReg,
int lastReg) throws SolarmanProtocolException {
int regCount = lastReg - firstReg + 1;
Map<Integer, byte[]> registers = new HashMap<>();
int expectedFrameDataLen = 2 + 1 + regCount * 2;
if (frame == null || frame.length < expectedFrameDataLen + 2) {
throw new SolarmanProtocolException("Modbus frame is too short or empty");
}
int actualCrc = ByteBuffer.wrap(frame, expectedFrameDataLen, 2).order(ByteOrder.LITTLE_ENDIAN).getShort()
& 0xFFFF;
int expectedCrc = CRC16Modbus.calculate(Arrays.copyOfRange(frame, 0, expectedFrameDataLen));
if (actualCrc != expectedCrc) {
throw new SolarmanProtocolException(
String.format("Modbus frame crc is not valid. Expected %04x, got %04x", expectedCrc, actualCrc));
}
for (int i = 0; i < regCount; i++) {
int p1 = 3 + (i * 2);
ByteBuffer order = ByteBuffer.wrap(frame, p1, 2).order(ByteOrder.BIG_ENDIAN);
byte[] array = new byte[] { order.get(), order.get() };
registers.put(i + firstReg, array);
}
return registers;
}
protected byte[] extractModbusResponseFrame(byte @Nullable [] responseFrame, byte[] requestFrame)
throws SolarmanException {
if (responseFrame == null || responseFrame.length == 0) {
throw new SolarmanProtocolException("No response frame");
} else if (responseFrame.length == 29) {
parseResponseErrorCode(responseFrame, requestFrame);
throw new IllegalStateException("This should never be reached as previous method should always throw");
} else if (responseFrame.length < (29 + 4)) {
throw new SolarmanProtocolException("Response frame is too short");
} else if (responseFrame[0] != (byte) 0xA5) {
throw new SolarmanProtocolException("Response frame has invalid starting byte");
} else if (responseFrame[responseFrame.length - 1] != (byte) 0x15) {
throw new SolarmanProtocolException("Response frame has invalid ending byte");
}
return Arrays.copyOfRange(responseFrame, 25, responseFrame.length - 2);
}
protected void parseResponseErrorCode(byte[] responseFrame, byte[] requestFrame) throws SolarmanException {
if (responseFrame[0] == (byte) 0xA5 && responseFrame[1] == (byte) 0x10
&& !Arrays.equals(Arrays.copyOfRange(responseFrame, 7, 11), Arrays.copyOfRange(requestFrame, 7, 11))) {
String requestInverterId = parseInverterId(requestFrame);
String responseInverterId = parseInverterId(responseFrame);
String message = String
.format("There was a mismatch between the request logger ID: %s and the response logger ID: %s. "
+ "Make sure you are using the logger ID and not the inverter ID. "
+ "If in doubt, try the one in the response", requestInverterId, responseInverterId);
throw new SolarmanAuthenticationException(message, requestInverterId, responseInverterId);
}
if (responseFrame[1] != (byte) 0x10 || responseFrame[2] != (byte) 0x45) {
throw new SolarmanProtocolException("Unexpected control code in error response frame");
}
int errorCode = responseFrame[25];
String message = switch (errorCode) {
case 0x01 -> "Error response frame: Illegal Function";
case 0x02 -> "Error response frame: Illegal Data Address";
case 0x03 -> "Error response frame: Illegal Data Value";
case 0x04 -> "Error response frame: Slave Device Failure";
default -> String.format("Error response frame: Unknown error code %02x", errorCode);
};
throw new SolarmanProtocolException(message);
}
private static String parseInverterId(byte[] requestFrame) {
byte[] inverterIdBytes = Arrays.copyOfRange(requestFrame, 7, 11);
int inverterIdInt = ByteBuffer.wrap(inverterIdBytes).order(ByteOrder.LITTLE_ENDIAN).getInt();
return String.valueOf(inverterIdInt & 0x00000000ffffffffL);
}
}

View File

@ -0,0 +1,43 @@
/**
* Copyright (c) 2010-2024 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.solarman.internal.modbus.exception;
import java.io.Serial;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* @author Catalin Sanda - Initial contribution
*/
@NonNullByDefault
public class SolarmanAuthenticationException extends SolarmanException {
@Serial
private static final long serialVersionUID = 1L;
private final String requestInverterId;
private final String responseInverterId;
public SolarmanAuthenticationException(String message, String requestInverterId, String responseInverterId) {
super(message);
this.requestInverterId = requestInverterId;
this.responseInverterId = responseInverterId;
}
public String getRequestInverterId() {
return requestInverterId;
}
public String getResponseInverterId() {
return responseInverterId;
}
}

View File

@ -0,0 +1,34 @@
/**
* Copyright (c) 2010-2024 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.solarman.internal.modbus.exception;
import java.io.Serial;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* @author Catalin Sanda - Initial contribution
*/
@NonNullByDefault
public class SolarmanConnectionException extends SolarmanException {
@Serial
private static final long serialVersionUID = 1L;
public SolarmanConnectionException(String message) {
super(message);
}
public SolarmanConnectionException(String message, Exception cause) {
super(message, cause);
}
}

View File

@ -0,0 +1,34 @@
/**
* Copyright (c) 2010-2024 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.solarman.internal.modbus.exception;
import java.io.Serial;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* @author Catalin Sanda - Initial contribution
*/
@NonNullByDefault
public class SolarmanException extends Exception {
@Serial
private static final long serialVersionUID = 1L;
public SolarmanException(String message) {
super(message);
}
public SolarmanException(String message, Exception cause) {
super(message, cause);
}
}

View File

@ -0,0 +1,30 @@
/**
* Copyright (c) 2010-2024 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.solarman.internal.modbus.exception;
import java.io.Serial;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* @author Catalin Sanda - Initial contribution
*/
@NonNullByDefault
public class SolarmanProtocolException extends SolarmanException {
@Serial
private static final long serialVersionUID = 1L;
public SolarmanProtocolException(String message) {
super(message);
}
}

View File

@ -0,0 +1,142 @@
/**
* Copyright (c) 2010-2024 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.solarman.internal.typeprovider;
import javax.measure.Unit;
import javax.measure.quantity.Dimensionless;
import javax.measure.quantity.ElectricCurrent;
import javax.measure.quantity.ElectricPotential;
import javax.measure.quantity.Energy;
import javax.measure.quantity.Frequency;
import javax.measure.quantity.Power;
import javax.measure.quantity.Temperature;
import javax.measure.quantity.Time;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.solarman.internal.SolarmanBindingConstants;
import org.openhab.binding.solarman.internal.defmodel.ParameterItem;
import org.openhab.core.library.CoreItemFactory;
import org.openhab.core.library.unit.MetricPrefix;
import org.openhab.core.library.unit.SIUnits;
import org.openhab.core.library.unit.Units;
import org.openhab.core.thing.type.ChannelTypeUID;
/**
* The {@link ChannelUtils} class provides utility functions for handling channel types and units in the Solarman
* binding.
* It includes methods for determining item types, units of measure, and channel type IDs.
*
* @author Catalin Sanda - Initial contribution
*/
@NonNullByDefault
public class ChannelUtils {
/**
* Determines the item type for a given parameter item.
*
* @param item The parameter item to determine the type for
* @return The item type as a string
*/
public static String getItemType(ParameterItem item) {
@Nullable
Integer rule = item.getRule();
@Nullable
String uom = item.getUom();
if (uom == null) {
uom = "UNKN";
}
return switch (rule) {
case 5, 6, 7, 9 -> CoreItemFactory.STRING;
case 8 -> CoreItemFactory.DATETIME;
default -> {
yield computeNumberType(uom);
}
};
}
/**
* Computes the number type based on the unit of measure (UOM).
*
* @param uom The unit of measure as a string
* @return The number type as a string
*/
private static String computeNumberType(String uom) {
return switch (uom.toUpperCase()) {
case "A" -> CoreItemFactory.NUMBER + ":" + ElectricCurrent.class.getSimpleName();
case "V" -> CoreItemFactory.NUMBER + ":" + ElectricPotential.class.getSimpleName();
case "°C" -> CoreItemFactory.NUMBER + ":" + Temperature.class.getSimpleName();
case "W", "KW", "VA", "KVA", "VAR", "KVAR" -> CoreItemFactory.NUMBER + ":" + Power.class.getSimpleName();
case "WH", "KWH" -> CoreItemFactory.NUMBER + ":" + Energy.class.getSimpleName();
case "S" -> CoreItemFactory.NUMBER + ":" + Time.class.getSimpleName();
case "HZ" -> CoreItemFactory.NUMBER + ":" + Frequency.class.getSimpleName();
case "%" -> CoreItemFactory.NUMBER + ":" + Dimensionless.class.getSimpleName();
default -> CoreItemFactory.NUMBER;
};
}
/**
* Retrieves the unit of measure (UOM) from a string definition.
*
* @param uom The unit of measure as a string
* @return The corresponding {@link Unit}, or null if not found
*/
public static @Nullable Unit<?> getUnitFromDefinition(String uom) {
return switch (uom.toUpperCase()) {
case "A" -> Units.AMPERE;
case "V" -> Units.VOLT;
case "°C" -> SIUnits.CELSIUS;
case "W" -> Units.WATT;
case "KW" -> MetricPrefix.KILO(Units.WATT);
case "VA" -> Units.VOLT_AMPERE;
case "KVA" -> MetricPrefix.KILO(Units.VOLT_AMPERE);
case "VAR" -> Units.VAR;
case "KVAR" -> MetricPrefix.KILO(Units.VAR);
case "WH" -> Units.WATT_HOUR;
case "KWH" -> MetricPrefix.KILO(Units.WATT_HOUR);
case "S" -> Units.SECOND;
case "HZ" -> Units.HERTZ;
case "%" -> Units.PERCENT;
default -> null;
};
}
/**
* Escapes a name string by replacing specific characters with hyphens and converting to lowercase.
*
* @param name The name to escape
* @return The escaped name
*/
public static String escapeName(String name) {
name = name.trim();
name = name.replace("+", "plus");
name = name.toLowerCase();
name = name.replaceAll("[ .()/\\\\&_]", "-");
return name;
}
/**
* Computes a channel type ID based on the inverter definition ID, group, and name.
*
* @param inverterDefinitionId The inverter definition ID
* @param group The group
* @param name The name
* @return The computed {@link ChannelTypeUID}
*/
public static ChannelTypeUID computeChannelTypeId(String inverterDefinitionId, String group, String name) {
return new ChannelTypeUID(SolarmanBindingConstants.SOLARMAN_BINDING_ID,
String.format("%s-%s-%s", escapeName(inverterDefinitionId), escapeName(group), escapeName(name)));
}
}

View File

@ -0,0 +1,131 @@
/**
* Copyright (c) 2010-2024 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.solarman.internal.typeprovider;
import static org.openhab.binding.solarman.internal.typeprovider.ChannelUtils.getItemType;
import java.math.BigDecimal;
import java.net.URI;
import java.net.URL;
import java.util.AbstractMap;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.concurrent.ConcurrentHashMap;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.solarman.internal.DefinitionParser;
import org.openhab.binding.solarman.internal.defmodel.InverterDefinition;
import org.openhab.binding.solarman.internal.defmodel.ParameterItem;
import org.openhab.core.thing.type.ChannelType;
import org.openhab.core.thing.type.ChannelTypeBuilder;
import org.openhab.core.thing.type.ChannelTypeProvider;
import org.openhab.core.thing.type.ChannelTypeUID;
import org.openhab.core.thing.type.StateChannelTypeBuilder;
import org.openhab.core.types.StateDescriptionFragmentBuilder;
import org.osgi.framework.BundleContext;
import org.osgi.service.component.annotations.Activate;
import org.osgi.service.component.annotations.Component;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* @author Catalin Sanda - Initial contribution
*/
@Component(service = { ChannelTypeProvider.class, SolarmanChannelTypeProvider.class })
@NonNullByDefault
public class SolarmanChannelTypeProvider implements ChannelTypeProvider {
private final Logger logger = LoggerFactory.getLogger(SolarmanChannelTypeProvider.class);
private static final DefinitionParser DEFINITION_PARSER = new DefinitionParser();
private static final Pattern INVERTER_DEFINITION_PATTERN = Pattern.compile("/definitions/([^.]+)\\.yaml");
private final Map<ChannelTypeUID, ChannelType> channelTypeMap = new ConcurrentHashMap<>();
@Activate
public SolarmanChannelTypeProvider(BundleContext bundleContext) {
Collections.list(bundleContext.getBundle().findEntries("/definitions", "*", false)).stream().map(URL::getFile)
.map(this::extractInverterDefinitionId).filter(Optional::isPresent).map(Optional::get)
.map(this::parseInverterDefinition).forEach(channelTypeMap::putAll);
}
private Map<ChannelTypeUID, ChannelType> parseInverterDefinition(String inverterDefinitionId) {
InverterDefinition inverterDefinition = DEFINITION_PARSER.parseDefinition(inverterDefinitionId);
if (inverterDefinition == null) {
logger.warn("Unable to parse inverter definition");
return Collections.emptyMap();
}
return inverterDefinition.getParameters().stream()
.flatMap(parameter -> parameter.getItems().stream().map(item -> {
ChannelTypeUID channelTypeUID = ChannelUtils.computeChannelTypeId(inverterDefinitionId,
parameter.getGroup(), item.getName());
return new AbstractMap.SimpleEntry<>(channelTypeUID, buildChannelType(channelTypeUID, item));
})).collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
}
private Optional<String> extractInverterDefinitionId(String file) {
return Stream.of(file).map(INVERTER_DEFINITION_PATTERN::matcher).filter(Matcher::matches)
.map(matcher -> matcher.group(1)).findFirst();
}
public Collection<ChannelType> getChannelTypes(@Nullable Locale locale) {
return List.copyOf(this.channelTypeMap.values());
}
@Override
public @Nullable ChannelType getChannelType(ChannelTypeUID channelTypeUID, @Nullable Locale locale) {
return this.channelTypeMap.get(channelTypeUID);
}
public ChannelType buildChannelType(ChannelTypeUID channelTypeUID, ParameterItem item) {
String itemType = getItemType(item);
StateDescriptionFragmentBuilder stateDescriptionFragmentBuilder = StateDescriptionFragmentBuilder.create()
.withPattern(computePatternForItem(item)).withReadOnly(true);
StateChannelTypeBuilder stateChannelTypeBuilder = ChannelTypeBuilder
.state(channelTypeUID, item.getName(), itemType)
.withConfigDescriptionURI(URI.create("channel-type-config:solarman:dynamic-channel"))
.withDescription(String.format("%s %s", item.getName(), buildRegisterDescription(item)))
.withStateDescriptionFragment(stateDescriptionFragmentBuilder.build());
return stateChannelTypeBuilder.build();
}
private String computePatternForItem(ParameterItem item) {
long decimalPoints = 0;
BigDecimal scale = Objects.requireNonNullElse(item.getScale(), BigDecimal.ONE);
if (scale.compareTo(BigDecimal.ONE) < 0) {
decimalPoints = Math.abs(Math.round(Math.log10(scale.doubleValue())));
}
String uom = item.getUom();
String pattern = (decimalPoints > 0) ? "%." + decimalPoints + "f" : "%d";
return pattern + (uom != null && !uom.isBlank() ? " %unit%" : "");
}
private String buildRegisterDescription(ParameterItem item) {
return String.format("[%s]", item.getRegisters().stream().map(register -> String.format("0x%04X", register))
.collect(Collectors.joining(",")));
}
}

View File

@ -0,0 +1,238 @@
/**
* Copyright (c) 2010-2024 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.solarman.internal.updater;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
import java.time.format.DateTimeParseException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
import javax.measure.Unit;
import javax.measure.format.MeasurementParseException;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.solarman.internal.defmodel.ParameterItem;
import org.openhab.binding.solarman.internal.defmodel.Request;
import org.openhab.binding.solarman.internal.modbus.SolarmanLoggerConnection;
import org.openhab.binding.solarman.internal.modbus.SolarmanLoggerConnector;
import org.openhab.binding.solarman.internal.modbus.SolarmanV5Protocol;
import org.openhab.binding.solarman.internal.modbus.exception.SolarmanConnectionException;
import org.openhab.binding.solarman.internal.modbus.exception.SolarmanException;
import org.openhab.binding.solarman.internal.typeprovider.ChannelUtils;
import org.openhab.binding.solarman.internal.util.StreamUtils;
import org.openhab.core.library.types.DateTimeType;
import org.openhab.core.library.types.DecimalType;
import org.openhab.core.library.types.QuantityType;
import org.openhab.core.library.types.StringType;
import org.openhab.core.thing.ChannelUID;
import org.openhab.core.types.State;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* @author Catalin Sanda - Initial contribution
*/
@NonNullByDefault
public class SolarmanChannelUpdater {
private final Logger logger = LoggerFactory.getLogger(SolarmanChannelUpdater.class);
private final StateUpdater stateUpdater;
public SolarmanChannelUpdater(StateUpdater stateUpdater) {
this.stateUpdater = stateUpdater;
}
public SolarmanProcessResult fetchDataFromLogger(List<Request> requests,
SolarmanLoggerConnector solarmanLoggerConnector, SolarmanV5Protocol solarmanV5Protocol,
Map<ParameterItem, ChannelUID> paramToChannelMapping) {
try (SolarmanLoggerConnection solarmanLoggerConnection = solarmanLoggerConnector.createConnection()) {
logger.debug("Fetching data from logger");
if (!solarmanLoggerConnection.isConnected()) {
return SolarmanProcessResult.ofException(Request.NONE,
new SolarmanConnectionException("Unable to connect to logger"));
}
SolarmanProcessResult solarmanProcessResult = requests.stream().map(request -> {
try {
return SolarmanProcessResult.ofValue(request,
solarmanV5Protocol.readRegisters(solarmanLoggerConnection,
(byte) request.getMbFunctioncode().intValue(), request.getStart(),
request.getEnd()));
} catch (SolarmanException e) {
return SolarmanProcessResult.ofException(request, e);
}
}).reduce(new SolarmanProcessResult(), SolarmanProcessResult::merge);
if (solarmanProcessResult.hasSuccessfulResponses()) {
updateChannelsForReadRegisters(paramToChannelMapping, solarmanProcessResult.getReadRegistersMap());
}
return solarmanProcessResult;
}
}
private void updateChannelsForReadRegisters(Map<ParameterItem, ChannelUID> paramToChannelMapping,
Map<Integer, byte[]> readRegistersMap) {
paramToChannelMapping.forEach((parameterItem, channelUID) -> {
List<Integer> registers = parameterItem.getRegisters();
if (readRegistersMap.keySet().containsAll(registers)) {
switch (parameterItem.getRule()) {
case 1, 3 -> updateChannelWithNumericValue(parameterItem, channelUID, registers, readRegistersMap,
ValueType.UNSIGNED);
case 2, 4 -> updateChannelWithNumericValue(parameterItem, channelUID, registers, readRegistersMap,
ValueType.SIGNED);
case 5 -> updateChannelWithStringValue(channelUID, registers, readRegistersMap);
case 6 -> updateChannelWithRawValue(parameterItem, channelUID, registers, readRegistersMap);
case 7 -> updateChannelWithVersion(channelUID, registers, readRegistersMap);
case 8 -> updateChannelWithDateTime(channelUID, registers, readRegistersMap);
case 9 -> updateChannelWithTime(channelUID, registers, readRegistersMap);
}
} else {
logger.warn("Unable to update channel {} because its registers were not read", channelUID.getId());
}
});
}
private void updateChannelWithTime(ChannelUID channelUID, List<Integer> registers,
Map<Integer, byte[]> readRegistersMap) {
String stringValue = registers.stream().map(readRegistersMap::get).map(v -> ByteBuffer.wrap(v).getShort())
.map(rawVal -> String.format("%02d", rawVal / 100) + ":" + String.format("%02d", rawVal % 100))
.collect(Collectors.joining());
stateUpdater.updateState(channelUID, new StringType(stringValue));
}
private void updateChannelWithDateTime(ChannelUID channelUID, List<Integer> registers,
Map<Integer, byte[]> readRegistersMap) {
String stringValue = StreamUtils.zip(IntStream.range(0, registers.size()).boxed(),
registers.stream().map(readRegistersMap::get).map(v -> ByteBuffer.wrap(v).getShort()),
StreamUtils.Tuple::new).map(t -> {
int index = t.a();
short rawVal = t.b();
return switch (index) {
case 0 -> (rawVal >> 8) + "/" + (rawVal & 0xFF) + "/";
case 1 -> (rawVal >> 8) + " " + (rawVal & 0xFF) + ":";
case 2 -> (rawVal >> 8) + ":" + (rawVal & 0xFF);
default -> (rawVal >> 8) + "" + (rawVal & 0xFF);
};
}).collect(Collectors.joining());
try {
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yy/M/d H:m:s");
LocalDateTime dateTime = LocalDateTime.parse(stringValue, formatter);
stateUpdater.updateState(channelUID, new DateTimeType(dateTime.atZone(ZoneId.systemDefault())));
} catch (DateTimeParseException e) {
logger.debug("Unable to parse string date {} to a DateTime object", stringValue);
}
}
private void updateChannelWithVersion(ChannelUID channelUID, List<Integer> registers,
Map<Integer, byte[]> readRegistersMap) {
String stringValue = registers.stream().map(readRegistersMap::get).map(v -> ByteBuffer.wrap(v).getShort())
.map(rawVal -> (rawVal >> 12) + "." + ((rawVal >> 8) & 0x0F) + "." + ((rawVal >> 4) & 0x0F) + "."
+ (rawVal & 0x0F))
.collect(Collectors.joining());
stateUpdater.updateState(channelUID, new StringType(stringValue));
}
private void updateChannelWithStringValue(ChannelUID channelUID, List<Integer> registers,
Map<Integer, byte[]> readRegistersMap) {
String stringValue = registers.stream().map(readRegistersMap::get).reduce(new StringBuilder(), (acc, val) -> {
short shortValue = ByteBuffer.wrap(val).order(ByteOrder.BIG_ENDIAN).getShort();
return acc.append((char) (shortValue >> 8)).append((char) (shortValue & 0xFF));
}, StringBuilder::append).toString();
stateUpdater.updateState(channelUID, new StringType(stringValue));
}
private void updateChannelWithNumericValue(ParameterItem parameterItem, ChannelUID channelUID,
List<Integer> registers, Map<Integer, byte[]> readRegistersMap, ValueType valueType) {
BigInteger value = extractNumericValue(registers, readRegistersMap, valueType);
BigDecimal convertedValue = convertNumericValue(value, parameterItem.getOffset(), parameterItem.getScale());
String uom = Objects.requireNonNullElse(parameterItem.getUom(), "");
State state;
if (!uom.isBlank()) {
try {
Unit<?> unitFromDefinition = ChannelUtils.getUnitFromDefinition(uom);
if (unitFromDefinition != null) {
state = new QuantityType<>(convertedValue, unitFromDefinition);
} else {
logger.debug("Unable to parse unit: {}", uom);
state = new DecimalType(convertedValue);
}
} catch (MeasurementParseException e) {
state = new DecimalType(convertedValue);
}
} else {
state = new DecimalType(convertedValue);
}
stateUpdater.updateState(channelUID, state);
}
private void updateChannelWithRawValue(ParameterItem parameterItem, ChannelUID channelUID, List<Integer> registers,
Map<Integer, byte[]> readRegistersMap) {
String hexString = String.format("[%s]",
reversed(registers).stream().map(readRegistersMap::get).map(
val -> String.format("0x%02X", ByteBuffer.wrap(val).order(ByteOrder.BIG_ENDIAN).getShort()))
.collect(Collectors.joining(",")));
stateUpdater.updateState(channelUID, new StringType(hexString));
}
private BigDecimal convertNumericValue(BigInteger value, @Nullable BigDecimal offset, @Nullable BigDecimal scale) {
return new BigDecimal(value).subtract(offset != null ? offset : BigDecimal.ZERO)
.multiply(scale != null ? scale : BigDecimal.ONE);
}
private BigInteger extractNumericValue(List<Integer> registers, Map<Integer, byte[]> readRegistersMap,
ValueType valueType) {
return reversed(registers)
.stream().map(readRegistersMap::get).reduce(
BigInteger.ZERO, (acc,
val) -> acc.shiftLeft(Short.SIZE)
.add(BigInteger.valueOf(ByteBuffer.wrap(val).getShort()
& (valueType == ValueType.UNSIGNED ? 0xFFFF : 0xFFFFFFFF))),
BigInteger::add);
}
private enum ValueType {
UNSIGNED,
SIGNED
}
@FunctionalInterface
public interface StateUpdater {
void updateState(ChannelUID channelUID, State state);
}
private <T> List<T> reversed(List<T> initialList) {
List<T> reversedList = new ArrayList<>(initialList);
Collections.reverse(reversedList);
return reversedList;
}
}

View File

@ -0,0 +1,88 @@
/**
* Copyright (c) 2010-2024 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.solarman.internal.updater;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.solarman.internal.defmodel.Request;
import org.openhab.binding.solarman.internal.modbus.exception.SolarmanException;
/**
* @author Catalin Sanda - Initial contribution
*/
@NonNullByDefault
public class SolarmanProcessResult {
private final Map<Request, Map<Integer, byte[]>> successfulRequestMap;
private final Map<Request, SolarmanException> exceptionRequestMap;
public SolarmanProcessResult() {
this(Collections.emptyMap(), Collections.emptyMap());
}
private SolarmanProcessResult(Map<Request, Map<Integer, byte[]>> successfulRequestMap,
Map<Request, SolarmanException> exceptionRequestMap) {
this.successfulRequestMap = successfulRequestMap;
this.exceptionRequestMap = exceptionRequestMap;
}
public static SolarmanProcessResult merge(SolarmanProcessResult result1, SolarmanProcessResult result2) {
return new SolarmanProcessResult(mergeMaps(result1.successfulRequestMap, result2.successfulRequestMap),
mergeMaps(result1.exceptionRequestMap, result2.exceptionRequestMap));
}
public static SolarmanProcessResult ofValue(Request request, Map<Integer, byte[]> readRegisters) {
return new SolarmanProcessResult(Collections.singletonMap(request, readRegisters), new HashMap<>());
}
public static SolarmanProcessResult ofException(Request request, SolarmanException solarmanException) {
return new SolarmanProcessResult(new HashMap<>(), Collections.singletonMap(request, solarmanException));
}
public boolean hasSuccessfulResponses() {
return !successfulRequestMap.isEmpty();
}
public Map<Integer, byte[]> getReadRegistersMap() {
return successfulRequestMap.values().stream().reduce(new HashMap<>(), SolarmanProcessResult::mergeMaps);
}
private static <K, V> Map<K, V> mergeMaps(Map<K, V> map1, Map<K, V> map2) {
return Stream.concat(map1.entrySet().stream(), map2.entrySet().stream())
.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue, (v1, v2) -> v1));
}
@Override
public String toString() {
if (!successfulRequestMap.isEmpty() && exceptionRequestMap.isEmpty()) {
return String.format("Successfully executed %d requests", successfulRequestMap.size());
} else if (successfulRequestMap.isEmpty() && !exceptionRequestMap.isEmpty()) {
return String.format("Error fetching data from logger, here are the errors:\n%s",
buildErrorReport(exceptionRequestMap));
} else if (!successfulRequestMap.isEmpty()) {
return String.format("Successfully executed %d requests, but %d requests failed with:\n%s",
successfulRequestMap.size(), exceptionRequestMap.size(), buildErrorReport(exceptionRequestMap));
} else {
return "Empty SolarmanProcessResult";
}
}
private String buildErrorReport(Map<Request, SolarmanException> exceptionRequestMap) {
return exceptionRequestMap.entrySet().stream().map(entry -> String.format("\tRequest %s returned error: %s\n",
entry.getKey().toString(), entry.getValue().getMessage())).reduce("", String::concat);
}
}

View File

@ -0,0 +1,85 @@
/**
* Copyright (c) 2010-2024 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.solarman.internal.util;
import java.util.Iterator;
import java.util.Objects;
import java.util.Spliterator;
import java.util.Spliterators;
import java.util.function.BiFunction;
import java.util.stream.Stream;
import java.util.stream.StreamSupport;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* Utility class for Stream operations.
*
* @author Catalin Sanda - Initial contribution
*/
@NonNullByDefault
public class StreamUtils {
/**
* Zips two streams into one by applying a zipper function to each pair of elements.
*
* @param <A> The type of the first stream elements
* @param <B> The type of the second stream elements
* @param <C> The type of the resulting stream elements
* @param a The first stream to be zipped
* @param b The second stream to be zipped
* @param zipper The function to apply to each pair of elements
* @return A stream of zipped elements
*/
public static <A, B, C> Stream<C> zip(Stream<? extends A> a, Stream<? extends B> b,
BiFunction<? super A, ? super B, ? extends C> zipper) {
Objects.requireNonNull(zipper);
Spliterator<? extends A> aSpliterator = Objects.requireNonNull(a).spliterator();
Spliterator<? extends B> bSpliterator = Objects.requireNonNull(b).spliterator();
// Zipping looses DISTINCT and SORTED characteristics
int characteristics = aSpliterator.characteristics() & bSpliterator.characteristics()
& ~(Spliterator.DISTINCT | Spliterator.SORTED);
long zipSize = ((characteristics & Spliterator.SIZED) != 0)
? Math.min(aSpliterator.getExactSizeIfKnown(), bSpliterator.getExactSizeIfKnown())
: -1;
Iterator<A> aIterator = Spliterators.iterator(aSpliterator);
Iterator<B> bIterator = Spliterators.iterator(bSpliterator);
Iterator<C> cIterator = new Iterator<C>() {
@Override
public boolean hasNext() {
return aIterator.hasNext() && bIterator.hasNext();
}
@Override
public C next() {
return zipper.apply(aIterator.next(), bIterator.next());
}
};
Spliterator<C> split = Spliterators.spliterator(cIterator, zipSize, characteristics);
return (a.isParallel() || b.isParallel()) ? StreamSupport.stream(split, true)
: StreamSupport.stream(split, false);
}
/**
* A tuple class to hold two related objects.
*
* @param <A> The type of the first object
* @param <B> The type of the second object
*/
public record Tuple<A, B> (A a, B b) {
}
}

View File

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<addon:addon id="solarman" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:addon="https://openhab.org/schemas/addon/v1.0.0"
xsi:schemaLocation="https://openhab.org/schemas/addon/v1.0.0 https://openhab.org/schemas/addon-1.0.0.xsd">
<type>binding</type>
<name>Solarman Logger Binding</name>
<description>This is the binding for Solarman Logger</description>
<connection>local</connection>
</addon:addon>

View File

@ -0,0 +1,46 @@
<?xml version="1.0" encoding="UTF-8"?>
<config-description:config-descriptions
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:config-description="https://openhab.org/schemas/config-description/v1.0.0"
xsi:schemaLocation="https://openhab.org/schemas/config-description/v1.0.0 https://openhab.org/schemas/config-description-1.0.0.xsd">
<config-description uri="channel-type-config:solarman:datetime-channel">
<parameter name="uom" type="text">
<label>Unit of Measurement</label>
<description>The unit of measurement used for this channel</description>
<advanced>true</advanced>
</parameter>
<parameter name="scale" type="decimal">
<label>Scale</label>
<description>The scaling factor, the final value will be scaled by this</description>
<advanced>true</advanced>
</parameter>
<parameter name="rule" type="integer">
<label>Rule</label>
<description>The type of measurement. See explanation for possible values</description>
<advanced>true</advanced>
<options>
<option value="1">Unsigned Short</option>
<option value="2">Signed Short</option>
<option value="3">Unsigned Integer</option>
<option value="4">Signed Integer</option>
<option value="5">Text</option>
<option value="6">Bytes</option>
<option value="7">Version</option>
<option value="8">Date Time</option>
<option value="9">Time</option>
</options>
</parameter>
<parameter name="offset" type="decimal">
<label>Offset</label>
<description>The offset subtracted from the measurement</description>
<advanced>true</advanced>
</parameter>
<parameter name="registers" type="text">
<label>Registers</label>
<description>Comma separated list of registers to read for the measurement</description>
<advanced>true</advanced>
</parameter>
</config-description>
</config-description:config-descriptions>

View File

@ -0,0 +1,46 @@
<?xml version="1.0" encoding="UTF-8"?>
<config-description:config-descriptions
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:config-description="https://openhab.org/schemas/config-description/v1.0.0"
xsi:schemaLocation="https://openhab.org/schemas/config-description/v1.0.0 https://openhab.org/schemas/config-description-1.0.0.xsd">
<config-description uri="channel-type-config:solarman:dynamic-channel">
<parameter name="uom" type="text">
<label>Unit of Measurement</label>
<description>The unit of measurement used for this channel</description>
<advanced>true</advanced>
</parameter>
<parameter name="scale" type="decimal">
<label>Scale</label>
<description>The scaling factor, the final value will be scaled by this</description>
<advanced>true</advanced>
</parameter>
<parameter name="rule" type="integer">
<label>Rule</label>
<description>The type of measurement. See explanation for possible values</description>
<advanced>true</advanced>
<options>
<option value="1">Unsigned Short</option>
<option value="2">Signed Short</option>
<option value="3">Unsigned Integer</option>
<option value="4">Signed Integer</option>
<option value="5">Text</option>
<option value="6">Bytes</option>
<option value="7">Version</option>
<option value="8">Date Time</option>
<option value="9">Time</option>
</options>
</parameter>
<parameter name="offset" type="decimal">
<label>Offset</label>
<description>The offset subtracted from the measurement</description>
<advanced>true</advanced>
</parameter>
<parameter name="registers" type="text">
<label>Registers</label>
<description>Comma separated list of registers to read for the measurement</description>
<advanced>true</advanced>
</parameter>
</config-description>
</config-description:config-descriptions>

View File

@ -0,0 +1,46 @@
<?xml version="1.0" encoding="UTF-8"?>
<config-description:config-descriptions
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:config-description="https://openhab.org/schemas/config-description/v1.0.0"
xsi:schemaLocation="https://openhab.org/schemas/config-description/v1.0.0 https://openhab.org/schemas/config-description-1.0.0.xsd">
<config-description uri="channel-type-config:solarman:number-channel">
<parameter name="uom" type="text">
<label>Unit of Measurement</label>
<description>The unit of measurement used for this channel</description>
<advanced>true</advanced>
</parameter>
<parameter name="scale" type="decimal">
<label>Scale</label>
<description>The scaling factor, the final value will be scaled by this</description>
<advanced>true</advanced>
</parameter>
<parameter name="rule" type="integer">
<label>Rule</label>
<description>The type of measurement. See explanation for possible values</description>
<advanced>true</advanced>
<options>
<option value="1">Unsigned Short</option>
<option value="2">Signed Short</option>
<option value="3">Unsigned Integer</option>
<option value="4">Signed Integer</option>
<option value="5">Text</option>
<option value="6">Bytes</option>
<option value="7">Version</option>
<option value="8">Date Time</option>
<option value="9">Time</option>
</options>
</parameter>
<parameter name="offset" type="decimal">
<label>Offset</label>
<description>The offset subtracted from the measurement</description>
<advanced>true</advanced>
</parameter>
<parameter name="registers" type="text">
<label>Registers</label>
<description>Comma separated list of registers to read for the measurement</description>
<advanced>true</advanced>
</parameter>
</config-description>
</config-description:config-descriptions>

View File

@ -0,0 +1,46 @@
<?xml version="1.0" encoding="UTF-8"?>
<config-description:config-descriptions
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:config-description="https://openhab.org/schemas/config-description/v1.0.0"
xsi:schemaLocation="https://openhab.org/schemas/config-description/v1.0.0 https://openhab.org/schemas/config-description-1.0.0.xsd">
<config-description uri="channel-type-config:solarman:string-channel">
<parameter name="uom" type="text">
<label>Unit of Measurement</label>
<description>The unit of measurement used for this channel</description>
<advanced>true</advanced>
</parameter>
<parameter name="scale" type="decimal">
<label>Scale</label>
<description>The scaling factor, the final value will be scaled by this</description>
<advanced>true</advanced>
</parameter>
<parameter name="rule" type="integer">
<label>Rule</label>
<description>The type of measurement. See explanation for possible values</description>
<advanced>true</advanced>
<options>
<option value="1">Unsigned Short</option>
<option value="2">Signed Short</option>
<option value="3">Unsigned Integer</option>
<option value="4">Signed Integer</option>
<option value="5">Text</option>
<option value="6">Bytes</option>
<option value="7">Version</option>
<option value="8">Date Time</option>
<option value="9">Time</option>
</options>
</parameter>
<parameter name="offset" type="decimal">
<label>Offset</label>
<description>The offset subtracted from the measurement</description>
<advanced>true</advanced>
</parameter>
<parameter name="registers" type="text">
<label>Registers</label>
<description>Comma separated list of registers to read for the measurement</description>
<advanced>true</advanced>
</parameter>
</config-description>
</config-description:config-descriptions>

View File

@ -0,0 +1,123 @@
# add-on
addon.solarman.name = Solarman Logger Binding
addon.solarman.description = This is the binding for Solarman Logger
# thing types
thing-type.solarman.logger.label = Solarman Logger
thing-type.solarman.logger.description = This thing allows communication with Solarman (IGEN-Tech) v5 based solar inverter data loggers over the local network. Compatible with inverters from manufacturers such as Deye, Sofar, Solis, ZCS Azzurro, and KStar.
# thing types config
thing-type.config.solarman.logger.additionalRequests.label = Additional Requests
thing-type.config.solarman.logger.additionalRequests.description = Additional requests besides the ones defined in the inverter definition. Format is mb_functioncode1:start1-end1, mb_functioncode2:start2-end2,... Example 0x03:0x0000-0x0100,0x03:0x0200-0x0300
thing-type.config.solarman.logger.hostname.label = Hostname
thing-type.config.solarman.logger.hostname.description = Hostname or IP address of the Solarman logger.
thing-type.config.solarman.logger.inverterType.label = Inverter Type
thing-type.config.solarman.logger.inverterType.description = The type of inverter connected to the logger (default deye_sg04lp3).
thing-type.config.solarman.logger.inverterType.option.deye_2mppt = DEYE Microinverter with 2 MPPT Trackers (deye_2mppt)
thing-type.config.solarman.logger.inverterType.option.deye_4mppt = DEYE Microinverter with 4 MPPT Trackers (deye_4mppt)
thing-type.config.solarman.logger.inverterType.option.deye_hybrid = Generic DEYE/Sunsynk/SolArk Hybrid inverters (deye_hybrid)
thing-type.config.solarman.logger.inverterType.option.deye_sg04lp3 = DEYE/Sunsynk/SolArk Hybrid 8/12K-SG04LP3 (deye_sg04lp3)
thing-type.config.solarman.logger.inverterType.option.deye_string = Generic DEYE/Sunsynk/SolArk String inverters (deye_string)
thing-type.config.solarman.logger.inverterType.option.kstar_hybrid = KSTAR Hybrid Inverter (kstar_hybrid)
thing-type.config.solarman.logger.inverterType.option.sofar_g3hyd = SOFAR Hybrid Three-Phase Inverter (sofar_g3hyd)
thing-type.config.solarman.logger.inverterType.option.sofar_hyd3k-6k-es = SOFAR Hybrid Single-Phase Inverter (sofar_hyd3k-6k-es)
thing-type.config.solarman.logger.inverterType.option.sofar_lsw3 = SOFAR Inverters (sofar_lsw3)
thing-type.config.solarman.logger.inverterType.option.sofar_wifikit = SOFAR WifiKit (sofar_wifikit)
thing-type.config.solarman.logger.inverterType.option.solis_1p8k-5g = SOLIS 1P8K-5G (solis_1p8k-5g)
thing-type.config.solarman.logger.inverterType.option.solis_hybrid = SOLIS Hybrid Inverter (solis_hybrid)
thing-type.config.solarman.logger.inverterType.option.zcs_azzurro-ktl-v3 = ZCS Azzurro KTL-V3 Inverters (zcs_azzurro-ktl-v3)
thing-type.config.solarman.logger.port.label = Port
thing-type.config.solarman.logger.port.description = Port of the Solarman logger (default 8899).
thing-type.config.solarman.logger.refreshInterval.label = Refresh Interval
thing-type.config.solarman.logger.refreshInterval.description = Interval to query the logger (default 60).
thing-type.config.solarman.logger.serialNumber.label = Serial Number
thing-type.config.solarman.logger.serialNumber.description = Serial number of the Solarman logger.
# channel types
channel-type.solarman.datetime.label = Datetime Value
channel-type.solarman.dynamic.label = Dynamic Channel
channel-type.solarman.number.label = Number Value
channel-type.solarman.string.label = Text Value
# channel types config
channel-type-config.config.solarman.datetime-channel.offset.label = Offset
channel-type-config.config.solarman.datetime-channel.offset.description = The offset subtracted from the measurement
channel-type-config.config.solarman.datetime-channel.registers.label = Registers
channel-type-config.config.solarman.datetime-channel.registers.description = Comma separated list of registers to read for the measurement
channel-type-config.config.solarman.datetime-channel.rule.label = Rule
channel-type-config.config.solarman.datetime-channel.rule.description = The type of measurement. See explanation for possible values
channel-type-config.config.solarman.datetime-channel.rule.option.1 = Unsigned Short
channel-type-config.config.solarman.datetime-channel.rule.option.2 = Signed Short
channel-type-config.config.solarman.datetime-channel.rule.option.3 = Unsigned Integer
channel-type-config.config.solarman.datetime-channel.rule.option.4 = Signed Integer
channel-type-config.config.solarman.datetime-channel.rule.option.5 = Text
channel-type-config.config.solarman.datetime-channel.rule.option.6 = Bytes
channel-type-config.config.solarman.datetime-channel.rule.option.7 = Version
channel-type-config.config.solarman.datetime-channel.rule.option.8 = Date Time
channel-type-config.config.solarman.datetime-channel.rule.option.9 = Time
channel-type-config.config.solarman.datetime-channel.scale.label = Scale
channel-type-config.config.solarman.datetime-channel.scale.description = The scaling factor, the final value will be scaled by this
channel-type-config.config.solarman.datetime-channel.uom.label = Unit of Measurement
channel-type-config.config.solarman.datetime-channel.uom.description = The unit of measurement used for this channel
channel-type-config.config.solarman.dynamic-channel.offset.label = Offset
channel-type-config.config.solarman.dynamic-channel.offset.description = The offset subtracted from the measurement
channel-type-config.config.solarman.dynamic-channel.registers.label = Registers
channel-type-config.config.solarman.dynamic-channel.registers.description = Comma separated list of registers to read for the measurement
channel-type-config.config.solarman.dynamic-channel.rule.label = Rule
channel-type-config.config.solarman.dynamic-channel.rule.description = The type of measurement. See explanation for possible values
channel-type-config.config.solarman.dynamic-channel.rule.option.1 = Unsigned Short
channel-type-config.config.solarman.dynamic-channel.rule.option.2 = Signed Short
channel-type-config.config.solarman.dynamic-channel.rule.option.3 = Unsigned Integer
channel-type-config.config.solarman.dynamic-channel.rule.option.4 = Signed Integer
channel-type-config.config.solarman.dynamic-channel.rule.option.5 = Text
channel-type-config.config.solarman.dynamic-channel.rule.option.6 = Bytes
channel-type-config.config.solarman.dynamic-channel.rule.option.7 = Version
channel-type-config.config.solarman.dynamic-channel.rule.option.8 = Date Time
channel-type-config.config.solarman.dynamic-channel.rule.option.9 = Time
channel-type-config.config.solarman.dynamic-channel.scale.label = Scale
channel-type-config.config.solarman.dynamic-channel.scale.description = The scaling factor, the final value will be scaled by this
channel-type-config.config.solarman.dynamic-channel.uom.label = Unit of Measurement
channel-type-config.config.solarman.dynamic-channel.uom.description = The unit of measurement used for this channel
channel-type-config.config.solarman.number-channel.offset.label = Offset
channel-type-config.config.solarman.number-channel.offset.description = The offset subtracted from the measurement
channel-type-config.config.solarman.number-channel.registers.label = Registers
channel-type-config.config.solarman.number-channel.registers.description = Comma separated list of registers to read for the measurement
channel-type-config.config.solarman.number-channel.rule.label = Rule
channel-type-config.config.solarman.number-channel.rule.description = The type of measurement. See explanation for possible values
channel-type-config.config.solarman.number-channel.rule.option.1 = Unsigned Short
channel-type-config.config.solarman.number-channel.rule.option.2 = Signed Short
channel-type-config.config.solarman.number-channel.rule.option.3 = Unsigned Integer
channel-type-config.config.solarman.number-channel.rule.option.4 = Signed Integer
channel-type-config.config.solarman.number-channel.rule.option.5 = Text
channel-type-config.config.solarman.number-channel.rule.option.6 = Bytes
channel-type-config.config.solarman.number-channel.rule.option.7 = Version
channel-type-config.config.solarman.number-channel.rule.option.8 = Date Time
channel-type-config.config.solarman.number-channel.rule.option.9 = Time
channel-type-config.config.solarman.number-channel.scale.label = Scale
channel-type-config.config.solarman.number-channel.scale.description = The scaling factor, the final value will be scaled by this
channel-type-config.config.solarman.number-channel.uom.label = Unit of Measurement
channel-type-config.config.solarman.number-channel.uom.description = The unit of measurement used for this channel
channel-type-config.config.solarman.string-channel.offset.label = Offset
channel-type-config.config.solarman.string-channel.offset.description = The offset subtracted from the measurement
channel-type-config.config.solarman.string-channel.registers.label = Registers
channel-type-config.config.solarman.string-channel.registers.description = Comma separated list of registers to read for the measurement
channel-type-config.config.solarman.string-channel.rule.label = Rule
channel-type-config.config.solarman.string-channel.rule.description = The type of measurement. See explanation for possible values
channel-type-config.config.solarman.string-channel.rule.option.1 = Unsigned Short
channel-type-config.config.solarman.string-channel.rule.option.2 = Signed Short
channel-type-config.config.solarman.string-channel.rule.option.3 = Unsigned Integer
channel-type-config.config.solarman.string-channel.rule.option.4 = Signed Integer
channel-type-config.config.solarman.string-channel.rule.option.5 = Text
channel-type-config.config.solarman.string-channel.rule.option.6 = Bytes
channel-type-config.config.solarman.string-channel.rule.option.7 = Version
channel-type-config.config.solarman.string-channel.rule.option.8 = Date Time
channel-type-config.config.solarman.string-channel.rule.option.9 = Time
channel-type-config.config.solarman.string-channel.scale.label = Scale
channel-type-config.config.solarman.string-channel.scale.description = The scaling factor, the final value will be scaled by this
channel-type-config.config.solarman.string-channel.uom.label = Unit of Measurement
channel-type-config.config.solarman.string-channel.uom.description = The unit of measurement used for this channel

View File

@ -0,0 +1,31 @@
<?xml version="1.0" encoding="UTF-8"?>
<thing:thing-descriptions bindingId="solarman"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:thing="https://openhab.org/schemas/thing-description/v1.0.0"
xsi:schemaLocation="https://openhab.org/schemas/thing-description/v1.0.0 https://openhab.org/schemas/thing-description-1.0.0.xsd">
<channel-type id="string">
<item-type>String</item-type>
<label>Text Value</label>
<config-description-ref uri="channel-type-config:solarman:string-channel"/>
</channel-type>
<channel-type id="number">
<item-type>Number</item-type>
<label>Number Value</label>
<config-description-ref uri="channel-type-config:solarman:number-channel"/>
</channel-type>
<channel-type id="datetime">
<item-type>DateTime</item-type>
<label>Datetime Value</label>
<config-description-ref uri="channel-type-config:solarman:datetime-channel"/>
</channel-type>
<channel-type id="dynamic">
<item-type>String</item-type>
<label>Dynamic Channel</label>
<config-description-ref uri="channel-type-config:solarman:dynamic-channel"/>
</channel-type>
</thing:thing-descriptions>

View File

@ -0,0 +1,69 @@
<?xml version="1.0" encoding="UTF-8"?>
<thing:thing-descriptions bindingId="solarman"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:thing="https://openhab.org/schemas/thing-description/v1.0.0"
xsi:schemaLocation="https://openhab.org/schemas/thing-description/v1.0.0 https://openhab.org/schemas/thing-description-1.0.0.xsd">
<!-- Sample Thing Type -->
<thing-type id="logger" extensible="true">
<label>Solarman Logger</label>
<description>This thing allows communication with Solarman (IGEN-Tech) v5 based solar inverter data loggers over the
local network. Compatible with inverters from manufacturers such as Deye, Sofar, Solis, ZCS Azzurro, and KStar.</description>
<config-description>
<parameter name="hostname" type="text" required="true">
<context>network-address</context>
<label>Hostname</label>
<description>Hostname or IP address of the Solarman logger.</description>
<advanced>false</advanced>
</parameter>
<parameter name="port" type="integer" required="false">
<label>Port</label>
<description>Port of the Solarman logger (default 8899).</description>
<default>8899</default>
<advanced>true</advanced>
</parameter>
<parameter name="inverterType" type="text" required="true">
<label>Inverter Type</label>
<description>The type of inverter connected to the logger (default deye_sg04lp3).</description>
<advanced>false</advanced>
<options>
<option value="deye_2mppt">DEYE Microinverter with 2 MPPT Trackers (deye_2mppt)</option>
<option value="deye_4mppt">DEYE Microinverter with 4 MPPT Trackers (deye_4mppt)</option>
<option value="deye_hybrid">Generic DEYE/Sunsynk/SolArk Hybrid inverters (deye_hybrid)</option>
<option value="deye_sg04lp3">DEYE/Sunsynk/SolArk Hybrid 8/12K-SG04LP3 (deye_sg04lp3)</option>
<option value="deye_string">Generic DEYE/Sunsynk/SolArk String inverters (deye_string)</option>
<option value="kstar_hybrid">KSTAR Hybrid Inverter (kstar_hybrid)</option>
<option value="sofar_g3hyd">SOFAR Hybrid Three-Phase Inverter (sofar_g3hyd)</option>
<option value="sofar_hyd3k-6k-es">SOFAR Hybrid Single-Phase Inverter (sofar_hyd3k-6k-es)</option>
<option value="sofar_lsw3">SOFAR Inverters (sofar_lsw3)</option>
<option value="sofar_wifikit">SOFAR WifiKit (sofar_wifikit)</option>
<option value="solis_1p8k-5g">SOLIS 1P8K-5G (solis_1p8k-5g)</option>
<option value="solis_hybrid">SOLIS Hybrid Inverter (solis_hybrid)</option>
<option value="zcs_azzurro-ktl-v3">ZCS Azzurro KTL-V3 Inverters (zcs_azzurro-ktl-v3)</option>
</options>
</parameter>
<parameter name="serialNumber" type="text" required="true">
<label>Serial Number</label>
<description>Serial number of the Solarman logger.</description>
<advanced>false</advanced>
</parameter>
<parameter name="refreshInterval" type="integer" required="false" unit="s" min="30">
<label>Refresh Interval</label>
<description>Interval to query the logger (default 60).</description>
<default>60</default>
<advanced>true</advanced>
</parameter>
<parameter name="additionalRequests" type="text" required="false">
<label>Additional Requests</label>
<description>Additional requests besides the ones defined in the inverter definition.
Format is
mb_functioncode1:start1-end1, mb_functioncode2:start2-end2,...
Example 0x03:0x0000-0x0100,0x03:0x0200-0x0300
</description>
<advanced>true</advanced>
</parameter>
</config-description>
</thing-type>
</thing:thing-descriptions>

View File

@ -0,0 +1,137 @@
# First version : 22.2.2023
# Microinverter SUN600G3 (DEYE/VESDAS)
# 2x MPPT, 2x inverter
# 1x Logger, 2x Module,
requests:
- start: 0x0003
end: 0x0080
mb_functioncode: 0x03
parameters:
- group: solar
items:
- name: "PV1 Voltage"
class: "voltage"
state_class: "measurement"
uom: "V"
scale: 0.1
rule: 1
registers: [0x006D]
icon: 'mdi:solar-power'
- name: "PV2 Voltage"
class: "voltage"
state_class: "measurement"
uom: "V"
scale: 0.1
rule: 1
registers: [0x006F]
icon: 'mdi:solar-power'
- name: "PV1 Current"
class: "current"
uom: "A"
scale: 0.1
rule: 1
registers: [0x006E]
icon: 'mdi:solar-power'
- name: "PV2 Current"
class: "current"
state_class: "measurement"
uom: "A"
scale: 0.1
rule: 1
registers: [0x0070]
icon: 'mdi:solar-power'
- name: "Daily Production"
class: "energy"
state_class: "total"
uom: "kWh"
scale: 0.1
rule: 1
registers: [0x003C]
icon: 'mdi:solar-power'
- name: "Total Production"
class: "energy"
state_class: "total_increasing"
uom: "kWh"
scale: 0.1
rule: 3
registers: [0x003F,0x0040]
icon: 'mdi:solar-power'
validation:
min: 0.1
- group: Grid
items:
- name: "AC Voltage"
class: "voltage"
state_class: "measurement"
uom: "V"
scale: 0.1
rule: 1
registers: [0x0049]
icon: 'mdi:transmission-tower'
- name: "AC Output Frequency"
class: "frequency"
state_class: "measurement"
uom: "Hz"
scale: 0.01
rule: 1
registers: [0x004F]
icon: 'mdi:home-lightning-bolt'
- group: Inverter
items:
- name: "Running Status"
class: ""
state_class: ""
uom: ""
scale: 1
rule: 1
registers: [0x003B]
isstr: true
lookup:
- key: 0
value: "Stand-by"
- key: 1
value: "Self-check"
- key: 2
value: "Normal"
- key: 3
value: "Warning"
- key: 4
value: "Fault"
icon: 'mdi:home-lightning-bolt'
- name: "Total AC Output Power (Active)"
class: "power"
state_class: "measurement"
uom: "W"
scale: 0.1
rule: 3
registers: [0x0056, 0x0057]
icon: 'mdi:home-lightning-bolt'
- name: "Radiator Temperature"
class: "temperature"
uom: "°C"
state_class: "measurement"
scale: 0.01
rule: 1
offset: 1000
registers: [0x005a]
- name: "Inverter ID"
class: ""
state_class: ""
uom: ""
scale: 1
rule: 5
registers: [0x0003,0x0004,0x0005,0x0006,0x0007]
isstr: true

View File

@ -0,0 +1,470 @@
#
# Borrowed form https://github.com/StephanJoubert/home_assistant_solarman/
# Additional info from https://github.com/kbialek/deye-inverter-mqtt/blob/19ace123339beec7a574b983f631309f8d285883/deye_sensor.py
#
# First version : 22.2.2023
# Microinverter SUN600G3 (DEYE/VESDAS)
# 2x MPPT, 2x inverter
# 1x Logger, 2x Module,
# Added info for 4x MPPT Microinverters on 2023-06-23
requests:
- start: 0x0003
end: 0x0080
mb_functioncode: 0x03
parameters:
- group: solar
items:
- name: "PV1 Voltage"
class: "voltage"
state_class: "measurement"
uom: "V"
scale: 0.1
rule: 1
registers: [0x006D]
icon: 'mdi:solar-power'
- name: "PV2 Voltage"
class: "voltage"
state_class: "measurement"
uom: "V"
scale: 0.1
rule: 1
registers: [0x006F]
icon: 'mdi:solar-power'
- name: "PV3 Voltage"
class: "voltage"
state_class: "measurement"
uom: "V"
scale: 0.1
rule: 1
registers: [0x0071]
icon: 'mdi:solar-power'
- name: "PV4 Voltage"
class: "voltage"
state_class: "measurement"
uom: "V"
scale: 0.1
rule: 1
registers: [0x0073]
icon: 'mdi:solar-power'
- name: "PV1 Current"
class: "current"
state_class: "measurement"
uom: "A"
scale: 0.1
rule: 1
registers: [0x006E]
icon: 'mdi:solar-power'
- name: "PV2 Current"
class: "current"
state_class: "measurement"
uom: "A"
scale: 0.1
rule: 1
registers: [0x0070]
icon: 'mdi:solar-power'
- name: "PV3 Current"
class: "current"
state_class: "measurement"
uom: "A"
scale: 0.1
rule: 1
registers: [0x0072]
icon: 'mdi:solar-power'
- name: "PV4 Current"
class: "current"
state_class: "measurement"
uom: "A"
scale: 0.1
rule: 1
registers: [0x0074]
icon: 'mdi:solar-power'
- name: "Daily Production"
class: "energy"
state_class: "total"
uom: "kWh"
scale: 0.1
rule: 1
registers: [0x003C]
icon: 'mdi:solar-power'
- name: "Daily Production 1"
class: "energy"
state_class: "total"
uom: "kWh"
scale: 0.1
rule: 1
registers: [0x0041]
icon: 'mdi:solar-power'
- name: "Daily Production 2"
class: "energy"
state_class: "total"
uom: "kWh"
scale: 0.1
rule: 1
registers: [0x0042]
icon: 'mdi:solar-power'
- name: "Daily Production 3"
class: "energy"
state_class: "total"
uom: "kWh"
scale: 0.1
rule: 1
registers: [0x0043]
icon: 'mdi:solar-power'
- name: "Daily Production 4"
class: "energy"
state_class: "total"
uom: "kWh"
scale: 0.1
rule: 1
registers: [0x0044]
icon: 'mdi:solar-power'
- name: "Total Production"
class: "energy"
state_class: "total_increasing"
uom: "kWh"
scale: 0.1
rule: 3
registers: [0x003F,0x0040]
icon: 'mdi:solar-power'
validation:
min: 0.1
- name: "Total Production 1"
class: "energy"
state_class: "total_increasing"
uom: "kWh"
scale: 0.1
rule: 3
registers: [0x0045]
icon: 'mdi:solar-power'
- name: "Total Production 2"
class: "energy"
state_class: "total_increasing"
uom: "kWh"
scale: 0.1
rule: 3
registers: [0x0047]
icon: 'mdi:solar-power'
- name: "Total Production 3"
class: "energy"
state_class: "total_increasing"
uom: "kWh"
scale: 0.1
rule: 3
registers: [0x0046]
icon: 'mdi:solar-power'
- name: "Total Production 4"
class: "energy"
state_class: "total_increasing"
uom: "kWh"
scale: 0.1
rule: 3
registers: [0x0048]
icon: 'mdi:solar-power'
- name: "Active Power Regulations"
class: ""
state_class: ""
uom: "%"
scale: 1
rule: 1
registers: [0x0028]
icon: 'mdi:solar-power'
- group: Grid
items:
- name: "AC Voltage"
class: "voltage"
state_class: "measurement"
uom: "V"
scale: 0.1
rule: 1
registers: [0x0049]
icon: 'mdi:transmission-tower'
- name: "Grid Current"
class: "current"
state_class: "measurement"
uom: "A"
scale: 0.1
rule: 2
registers: [0x004C]
icon: 'mdi:home-lightning-bolt'
- name: "AC Output Frequency"
class: "frequency"
state_class: "measurement"
uom: "Hz"
scale: 0.01
rule: 1
registers: [0x004F]
icon: 'mdi:home-lightning-bolt'
- name: "Grid Voltage Upp Limit"
class: "voltage"
state_class: ""
uom: "V"
scale: 0.1
rule: 1
registers: [0x001B]
icon: 'mdi:transmission-tower'
- name: "Grid Voltage Lower Limit"
class: "voltage"
state_class: ""
uom: "V"
scale: 0.1
rule: 1
registers: [0x001C]
icon: 'mdi:transmission-tower'
- name: "Grid Frequency Upper Limit"
class: "frequency"
state_class: ""
uom: "Hz"
scale: 0.01
rule: 1
registers: [0x001D]
icon: 'mdi:home-lightning-bolt'
- name: "Grid Frequency Lower Limit"
class: "frequency"
state_class: ""
uom: "Hz"
scale: 0.01
rule: 1
registers: [0x001E]
icon: 'mdi:home-lightning-bolt'
- name: "Overfrequency And Load Reduction Starting Point"
class: "frequency"
state_class: ""
uom: "Hz"
scale: 0.01
rule: 1
registers: [0x0022]
icon: 'mdi:home-lightning-bolt'
- name: "Overfrequency And Load Reduction Percentage"
class: ""
state_class: ""
uom: "%"
scale: 1
rule: 1
registers: [0x0023]
icon: ''
- name: "ON-OFF Enable"
class: ""
state_class: ""
uom: ""
scale: 1
rule: 1
registers: [0x002B]
isstr: true
lookup:
- key: 0
value: "OFF"
- key: 1
value: "ON"
icon: 'mdi:toggle-switch'
- name: "Island Protection Enable"
class: ""
state_class: ""
uom: ""
scale: 1
rule: 1
registers: [0x002E]
isstr: true
lookup:
- key: 0
value: "Disabled"
- key: 1
value: "Enabled"
icon: 'mdi:island'
- name: "Overfrequency&Load-shedding Enable"
class: ""
state_class: ""
uom: ""
scale: 1
rule: 1
registers: [0x0031]
isstr: true
lookup:
- key: 0
value: "Disabled"
- key: 1
value: "Enabled"
icon: 'mdi:toggle-switch'
- group: Inverter
items:
- name: "Running Status"
class: ""
state_class: ""
uom: ""
scale: 1
rule: 1
registers: [0x003B]
isstr: true
lookup:
- key: 0
value: "Stand-by"
- key: 1
value: "Self-check"
- key: 2
value: "Normal"
- key: 3
value: "Warning"
- key: 4
value: "Fault"
icon: 'mdi:home-lightning-bolt'
- name: "Total AC Output Power (Active)"
class: "power"
state_class: "measurement"
uom: "W"
scale: 0.1
rule: 3
registers: [0x0056, 0x0057]
icon: 'mdi:home-lightning-bolt'
- name: "Radiator Temperature"
class: "temperature"
uom: "°C"
state_class: "measurement"
scale: 0.01
rule: 1
offset: 1000
registers: [0x005a]
- name: "Inverter ID"
class: ""
state_class: ""
uom: ""
scale: 1
rule: 5
registers: [0x0003,0x0004,0x0005,0x0006,0x0007]
isstr: true
- name: "Hardware Version"
class: ""
state_class: ""
uom: ""
scale: 1
rule: 7
registers: [0x000C]
isstr: true
- name: "DC Master Firmware Version"
class: ""
state_class: ""
uom: ""
scale: 1
rule: 7
registers: [0x000D]
isstr: true
- name: "AC Version. Number"
class: ""
state_class: ""
uom: ""
scale: 1
rule: 7
registers: [0x000E]
isstr: true
- name: "Rated Power"
class: "energy"
state_class: ""
uom: "W"
scale: 0.1
rule: 1
registers: [0x0010]
icon: 'mdi:solar-power'
- name: "Communication Protocol Version"
class: ""
state_class: ""
uom: ""
scale: 1
rule: 7
registers: [0x0012]
isstr: true
- name: "Start-up Self-checking Time "
class: ""
state_class: ""
uom: "s"
scale: 1
rule: 1
registers: [0x0015]
icon: 'mdi:solar-power'
- name: "Update Time"
class: ""
state_class: ""
uom: ""
scale: 1
rule: 8
registers: [0x0016,0x0017,0x0018]
isstr: true
- name: "Soft Start Enable"
class: ""
state_class: ""
uom: ""
scale: 1
rule: 1
registers: [0x002F]
isstr: true
lookup:
- key: 0
value: "Disabled"
- key: 1
value: "Enabled"
icon: 'mdi:toggle-switch'
- name: "Power Factor Regulation"
class: ""
state_class: ""
uom: ""
scale: 0.1
rule: 2
registers: [0x0032]
icon: ''
- name: "Restore Factory Settings"
class: ""
state_class: ""
uom: ""
scale: 1
rule: 1
registers: [0x0036]
isstr: true
lookup:
- key: 0
value: "Disabled"
- key: 1
value: "Enabled"
icon: 'mdi:factory'

View File

@ -0,0 +1,548 @@
requests:
- start: 0x0003
end: 0x0070
mb_functioncode: 0x03
- start: 0x0096
end: 0x00f8
mb_functioncode: 0x03
parameters:
- group: solar
items:
- name: "PV1 Power"
class: "power"
state_class: "measurement"
uom: "W"
scale: 1
rule: 1
registers: [0x00BA]
icon: 'mdi:solar-power'
- name: "PV2 Power"
class: "power"
state_class: "measurement"
uom: "W"
scale: 1
rule: 1
registers: [0x00BB]
icon: 'mdi:solar-power'
- name: "PV1 Voltage"
class: "voltage"
state_class: "measurement"
uom: "V"
scale: 0.1
rule: 1
registers: [0x006D]
icon: 'mdi:solar-power'
- name: "PV2 Voltage"
class: "voltage"
state_class: "measurement"
uom: "V"
scale: 0.1
rule: 1
registers: [0x006F]
icon: 'mdi:solar-power'
- name: "PV1 Current"
class: "current"
uom: "A"
scale: 0.1
rule: 1
registers: [0x006E]
icon: 'mdi:solar-power'
- name: "PV2 Current"
class: "current"
state_class: "measurement"
uom: "A"
scale: 0.1
rule: 1
registers: [0x0070]
icon: 'mdi:solar-power'
- name: "Daily Production"
class: "energy"
state_class: "measurement"
uom: "kWh"
scale: 0.1
rule: 1
registers: [0x006C]
icon: 'mdi:solar-power'
- name: "Total Production"
class: "energy"
state_class: "total_increasing"
uom: "kWh"
scale: 0.1
rule: 3
registers: [0x0060,0x0061]
icon: 'mdi:solar-power'
- name: "Micro-inverter Power"
class: "power"
state_class: "measurement"
uom: "W"
scale: 1
rule: 1
registers: [0x00A6]
icon: 'mdi:solar-power'
- group: Battery
items:
- name: "Total Battery Charge"
class: "energy"
state_class: "total_increasing"
uom: "kWh"
scale: 0.1
rule: 3
registers: [0x0048,0x0049]
icon: 'mdi:battery-plus'
- name: "Total Battery Discharge"
class: "energy"
state_class: "total_increasing"
uom: "kWh"
scale: 0.1
rule: 3
registers: [0x004A,0x004B]
icon: 'mdi:battery-minus'
- name: "Battery Status"
class: ""
state_class: "measurement"
uom: ""
scale: 1
rule: 1
registers: [0x00BD]
isstr: true
lookup:
- key: 0
value: "Charge"
- key: 1
value: "Stand-by"
- key: 2
value: "Discharge"
icon: 'mdi:battery'
- name: "Battery Power"
class: "power"
state_class: "measurement"
uom: "W"
scale: 1
rule: 2
registers: [0x00BE]
icon: 'mdi:battery'
- name: "Battery Voltage"
class: "voltage"
state_class: "measurement"
uom: "V"
scale: 0.01
rule: 1
registers: [0x00B7]
icon: 'mdi:battery'
- name: "Battery SOC"
class: "battery"
state_class: "measurement"
uom: "%"
scale: 1
rule: 1
registers: [0x00B8]
icon: 'mdi:battery'
- name: "Battery Current"
class: "current"
state_class: "measurement"
uom: "A"
scale: 0.01
rule: 2
registers: [0x00BF]
icon: 'mdi:battery'
- name: "Battery Temperature"
class: "temperature"
state_class: "measurement"
uom: "°C"
scale: 0.1
rule: 1
offset: 1000
registers: [0x00B6]
icon: 'mdi:battery'
- group: Grid
items:
- name: "Total Grid Power"
class: "power"
state_class: "measurement"
uom: "W"
scale: 1
rule: 2
registers: [0x00A9]
icon: 'mdi:transmission-tower'
- name: "Grid Voltage L1"
class: "voltage"
state_class: "measurement"
uom: "V"
scale: 0.1
rule: 1
registers: [0x0096]
icon: 'mdi:transmission-tower'
- name: "Grid Voltage L2"
class: "voltage"
state_class: "measurement"
uom: "V"
scale: 0.1
rule: 1
registers: [0x0097]
icon: 'mdi:transmission-tower'
- name: "Internal CT L1 Power"
class: "power"
state_class: "measurement"
uom: "W"
scale: 1
rule: 2
registers: [0x00A7]
icon: 'mdi:transmission-tower'
- name: "Internal CT L2 Power"
class: "power"
state_class: "measurement"
uom: "W"
scale: 1
rule: 2
registers: [0x00A8]
icon: 'mdi:transmission-tower'
- name: "External CT L1 Power"
class: "power"
state_class: "measurement"
uom: "W"
scale: 1
rule: 2
registers: [0x00AA]
icon: 'mdi:transmission-tower'
- name: "External CT L2 Power"
class: "power"
state_class: "measurement"
uom: "W"
scale: 1
rule: 2
registers: [0x00AB]
icon: 'mdi:transmission-tower'
- name: "Daily Energy Bought"
class: "energy"
state_class: "total_increasing"
uom: "kWh"
scale: 0.1
rule: 1
registers: [0x004C]
icon: 'mdi:transmission-tower-export'
- name: "Total Energy Bought"
class: "energy"
state_class: "total_increasing"
uom: "kWh"
scale: 0.1
rule: 1
registers: [0x004E,0x0050]
icon: 'mdi:transmission-tower-export'
- name: "Daily Energy Sold"
class: "energy"
state_class: "total_increasing"
uom: "kWh"
scale: 0.1
rule: 1
registers: [0x004D]
icon: 'mdi:transmission-tower-import'
- name: "Total Energy Sold"
class: "energy"
state_class: "total_increasing"
uom: "kWh"
scale: 0.1
rule: 3
registers: [0x0051,0x0052]
icon: 'mdi:transmission-tower-import'
- name: "Total Grid Production"
class: "energy"
state_class: "total_increasing"
uom: "kWh"
scale: 0.1
rule: 4
registers: [0x003F,0x0040]
icon: 'mdi:transmission-tower'
- group: Upload
items:
- name: "Total Load Power"
class: "power"
state_class: "measurement"
uom: "W"
scale: 1
rule: 1
registers: [0x00B2]
icon: 'mdi:lightning-bolt-outline'
- name: "Load L1 Power"
class: "power"
state_class: "measurement"
uom: "W"
scale: 1
rule: 1
registers: [0x00B0]
icon: 'mdi:lightning-bolt-outline'
- name: "Load L2 Power"
class: "power"
state_class: "measurement"
uom: "W"
scale: 1
rule: 1
registers: [0x00B1]
icon: 'mdi:lightning-bolt-outline'
- name: "Load Voltage"
class: "voltage"
state_class: "measurement"
uom: "V"
scale: 0.1
rule: 1
registers: [0x009D]
icon: 'mdi:lightning-bolt-outline'
- name: "Daily Load Consumption"
class: "energy"
state_class: "total_increasing"
uom: "kWh"
scale: 0.1
rule: 1
registers: [0x0054]
icon: 'mdi:lightning-bolt-outline'
- name: "Total Load Consumption"
class: "energy"
state_class: "total_increasing"
uom: "kWh"
scale: 0.1
rule: 3
registers: [0x0055,0x0056]
icon: 'mdi:lightning-bolt-outline'
- name: "SmartLoad Enable Status"
class: ""
state_class: ""
uom: ""
scale: 1
rule: 1
registers: [0x00C3]
isstr: true
lookup:
- key: 0
value: "OFF"
- key: 1
value: "ON"
icon: 'mdi:lightning-bolt-outline'
- group: Inverter
items:
- name: "Running Status"
class: ""
state_class: ""
uom: ""
scale: 1
rule: 1
registers: [0x003B]
isstr: true
lookup:
- key: 0
value: "Stand-by"
- key: 1
value: "Self-checking"
- key: 2
value: "Normal"
- key: 3
value: "FAULT"
icon: 'mdi:home-lightning-bolt'
- name: "Total Power"
class: "power"
state_class: "measurement"
uom: "W"
scale: 1
rule: 2
registers: [0x00AF]
icon: 'mdi:home-lightning-bolt'
- name: "Current L1"
class: "current"
state_class: "measurement"
uom: "A"
scale: 0.01
rule: 2
registers: [0x00A4]
icon: 'mdi:home-lightning-bolt'
- name: "Current L2"
class: "current"
uom: "A"
scale: 0.01
rule: 2
registers: [0x00A5]
icon: 'mdi:home-lightning-bolt'
- name: "Inverter L1 Power"
class: "power"
state_class: "measurement"
uom: "W"
scale: 1
rule: 2
registers: [0x00AD]
icon: 'mdi:home-lightning-bolt'
- name: "Inverter L2 Power"
class: "power"
state_class: "measurement"
uom: "W"
scale: 1
rule: 2
registers: [0x00AE]
icon: 'mdi:home-lightning-bolt'
- name: "DC Temperature"
class: "temperature"
state_class: "measurement"
uom: "°C"
scale: 0.1
rule: 2
offset: 1000
registers: [0x005A]
icon: 'mdi:thermometer'
- name: "AC Temperature"
class: "temperature"
state_class: "measurement"
uom: "°C"
scale: 0.1
rule: 2
offset: 1000
registers: [0x005B]
icon: 'mdi:thermometer'
- name: "Inverter ID"
class: ""
state_class: ""
uom: ""
scale: 1
rule: 5
registers: [0x0003,0x0004,0x0005,0x0006,0x0007]
isstr: true
- name: "Communication Board Version No."
class: ""
state_class: ""
uom: ""
scale: 1
rule: 1
registers: [0x000E]
isstr: true
- name: "Control Board Version No."
class: ""
state_class: ""
uom: ""
scale: 1
rule: 1
registers: [0x000D]
isstr: true
- name: "Grid-connected Status"
class: ""
state_class: ""
uom: ""
scale: 1
rule: 1
registers: [0x00C2]
isstr: true
lookup:
- key: 0
value: "Off-Grid"
- key: 1
value: "On-Grid"
- name: "Gen-connected Status"
class: ""
uom: ""
state_class: ""
scale: 1
rule: 1
registers: [0x00A6]
isstr: true
lookup:
- key: 0
value: "none"
- key: 1
value: "On"
- name: "Gen Power"
class: "power"
state_class: "measurement"
uom: "W"
scale: 1
rule: 1
registers: [0x00A6]
- name: "Time of use"
class: ""
state_class: ""
uom: ""
scale: 1
rule: 1
registers: [0x00F8]
isstr: true
lookup:
- key: 0
value: "Disable"
- key: 1
value: "Enable"
- name: "Work Mode"
class: ""
state_class: ""
uom: ""
scale: 1
rule: 3
registers: [0x00F4,0x00F7]
isstr: true
lookup:
- key: 0
value: "Selling First"
- key: 1
value: "Zero-Export to Load&Solar Sell"
- key: 2
value: "Zero-Export to Home&Solar Sell"
- key: 3
value: "Zero-Export to Load"
- key: 4
value: "Zero-Export to Home"
icon: 'mdi:home-lightning-bolt'
- group: Alert
items:
- name: "Alert"
class: ""
state_class: ""
uom: ""
scale: 1
rule: 6
registers: [0x0065,0x0066,0x0067,0x0068,0x0069,0x006A]

View File

@ -0,0 +1,523 @@
# SUN-8/12K-SG04LP3-EU | 8/12KW | Three Phase | 2 MPPT | Hybrid Inverter | Low Voltage Battery
# tested with LSW3_15_FFFF_1.0.91R + LSW3_15_FFFF_1.0.84
requests:
- start: 0x0003
end: 0x0059
mb_functioncode: 0x03
- start: 0x0202
end: 0x022E
mb_functioncode: 0x03
- start: 0x024A
end: 0x024F
mb_functioncode: 0x03
- start: 0x0256
end: 0x027C
mb_functioncode: 0x03
- start: 0x0284
end: 0x028D
mb_functioncode: 0x03
- start: 0x02A0
end: 0x02A7
mb_functioncode: 0x03
parameters:
- group: solar
items:
- name: "PV1 Power"
class: "power"
state_class: "measurement"
uom: "W"
scale: 1
rule: 1
registers: [0x02A0]
icon: 'mdi:solar-power'
- name: "PV2 Power"
class: "power"
state_class: "measurement"
uom: "W"
scale: 1
rule: 1
registers: [0x02A1]
icon: 'mdi:solar-power'
- name: "PV1 Voltage"
class: "voltage"
state_class: "measurement"
uom: "V"
scale: 0.1
rule: 1
registers: [0x02A4]
icon: 'mdi:solar-power'
- name: "PV2 Voltage"
class: "voltage"
state_class: "measurement"
uom: "V"
scale: 0.1
rule: 1
registers: [0x02A6]
icon: 'mdi:solar-power'
- name: "PV1 Current"
class: "current"
state_class: "measurement"
uom: "A"
scale: 0.1
rule: 1
registers: [0x02A5]
icon: 'mdi:solar-power'
- name: "PV2 Current"
class: "current"
state_class: "measurement"
uom: "A"
scale: 0.1
rule: 1
registers: [0x02A7]
icon: 'mdi:solar-power'
- name: "Daily Production"
class: "energy"
state_class: "total_increasing"
uom: "kWh"
scale: 0.1
rule: 1
registers: [0x0211]
icon: 'mdi:solar-power'
validation:
max: 100
invalidate_all:
- name: "Total Production"
class: "energy"
state_class: "total_increasing"
uom: "kWh"
scale: 0.1
rule: 3
registers: [0x0216,0x0217]
icon: 'mdi:solar-power'
- group: Battery
items:
- name: "Daily Battery Charge"
class: "energy"
state_class: "total_increasing"
uom: "kWh"
scale: 0.1
rule: 1
registers: [0x0202]
icon: 'mdi:battery-plus'
- name: "Daily Battery Discharge"
class: "energy"
state_class: "total_increasing"
uom: "kWh"
scale: 0.1
rule: 1
registers: [0x0203]
icon: 'mdi:battery-plus'
- name: "Total Battery Charge"
class: "energy"
state_class: "total_increasing"
uom: "kWh"
scale: 0.1
rule: 3
registers: [0x0204,0x0205]
icon: 'mdi:battery-plus'
- name: "Total Battery Discharge"
class: "energy"
state_class: "total_increasing"
uom: "kWh"
scale: 0.1
rule: 3
registers: [0x0206,0x0207]
icon: 'mdi:battery-minus'
- name: "Battery Power"
class: "power"
state_class: "measurement"
uom: "W"
scale: 1
rule: 2
registers: [0x024E]
icon: 'mdi:battery'
- name: "Battery Voltage"
class: "voltage"
state_class: "measurement"
uom: "V"
scale: 0.01
rule: 1
registers: [0x024B]
icon: 'mdi:battery'
- name: "Battery SOC"
class: "battery"
state_class: "measurement"
uom: "%"
scale: 1
rule: 1
registers: [0x024C]
icon: 'mdi:battery'
validation:
min: 0
max: 101
- name: "Battery Current"
class: "current"
state_class: "measurement"
uom: "A"
scale: 0.01
rule: 2
registers: [0x024F]
icon: 'mdi:battery'
- name: "Battery Temperature"
class: "temperature"
state_class: "measurement"
uom: "°C"
scale: 0.1
rule: 1
offset: 1000
registers: [0x024A]
icon: 'mdi:battery'
validation:
min: 1
max: 99
invalidate_all:
- group: Grid
items:
- name: "Total Grid Power"
class: "measurement"
state_class: "measurement"
uom: "W"
scale: 1
rule: 2
registers: [0x0271]
icon: 'mdi:transmission-tower'
- name: "Grid Voltage L1"
class: "voltage"
state_class: "measurement"
uom: "V"
scale: 0.1
rule: 1
registers: [0x0256]
icon: 'mdi:transmission-tower'
- name: "Grid Voltage L2"
class: "voltage"
state_class: "measurement"
uom: "V"
scale: 0.1
rule: 1
registers: [0x0257]
icon: 'mdi:transmission-tower'
- name: "Grid Voltage L3"
class: "voltage"
state_class: "measurement"
uom: "V"
scale: 0.1
rule: 1
registers: [0x0258]
icon: 'mdi:transmission-tower'
- name: "Internal CT L1 Power"
class: "power"
state_class: "measurement"
uom: "W"
scale: 1
rule: 2
registers: [0x025C]
icon: 'mdi:transmission-tower'
- name: "Internal CT L2 Power"
class: "power"
state_class: "measurement"
uom: "W"
scale: 1
rule: 2
registers: [0x025D]
icon: 'mdi:transmission-tower'
- name: "Internal CT L3 Power"
class: "power"
state_class: "measurement"
uom: "W"
scale: 1
rule: 2
registers: [0x025E]
icon: 'mdi:transmission-tower'
- name: "External CT L1 Power"
class: "power"
state_class: "measurement"
uom: "W"
scale: 1
rule: 2
registers: [0x0268]
icon: 'mdi:transmission-tower'
- name: "External CT L2 Power"
class: "power"
state_class: "measurement"
uom: "W"
scale: 1
rule: 2
registers: [0x0269]
icon: 'mdi:transmission-tower'
- name: "External CT L3 Power"
class: "power"
state_class: "measurement"
uom: "W"
scale: 1
rule: 2
registers: [0x026A]
icon: 'mdi:transmission-tower'
- name: "Daily Energy Bought"
class: "energy"
state_class: "total_increasing"
uom: "kWh"
scale: 0.1
rule: 1
registers: [0x0208]
icon: 'mdi:transmission-tower-export'
- name: "Total Energy Bought"
class: "energy"
state_class: "total_increasing"
uom: "kWh"
scale: 0.1
rule: 1
registers: [0x020A,0x020B]
icon: 'mdi:transmission-tower-export'
- name: "Daily Energy Sold"
class: "energy"
state_class: "total_increasing"
uom: "kWh"
scale: 0.1
rule: 1
registers: [0x0209]
icon: 'mdi:transmission-tower-import'
- name: "Total Energy Sold"
class: "energy"
state_class: "total_increasing"
uom: "kWh"
scale: 0.1
rule: 3
registers: [0x020C,0x020D]
icon: 'mdi:transmission-tower-import'
- name: "Total Grid Production"
class: "energy"
state_class: "total_increasing"
uom: "kWh"
scale: 0.1
rule: 4
registers: [0x020C,0x020D]
icon: 'mdi:transmission-tower'
- group: Upload
items:
- name: "Total Load Power"
class: "power"
state_class: "measurement"
uom: "W"
scale: 1
rule: 1
registers: [0x028D]
icon: 'mdi:lightning-bolt-outline'
- name: "Load L1 Power"
class: "power"
state_class: "measurement"
uom: "W"
scale: 1
rule: 1
registers: [0x028A]
icon: 'mdi:lightning-bolt-outline'
- name: "Load L2 Power"
class: "power"
state_class: "measurement"
uom: "W"
scale: 1
rule: 1
registers: [0x028B]
icon: 'mdi:lightning-bolt-outline'
- name: "Load L3 Power"
class: "power"
state_class: "measurement"
uom: "W"
scale: 1
rule: 1
registers: [0x028C]
icon: 'mdi:lightning-bolt-outline'
- name: "Load Voltage L1"
class: "voltage"
state_class: "measurement"
uom: "V"
scale: 0.1
rule: 1
registers: [0x0284]
icon: 'mdi:lightning-bolt-outline'
- name: "Load Voltage L2"
class: "voltage"
state_class: "measurement"
uom: "V"
scale: 0.1
rule: 1
registers: [0x0285]
icon: 'mdi:lightning-bolt-outline'
- name: "Load Voltage L3"
class: "voltage"
state_class: "measurement"
uom: "V"
scale: 0.1
rule: 1
registers: [0x0286]
icon: 'mdi:lightning-bolt-outline'
- name: "Daily Load Consumption"
class: "energy"
state_class: "total_increasing"
uom: "kWh"
scale: 0.1
rule: 1
registers: [0x020E]
icon: 'mdi:lightning-bolt-outline'
- name: "Total Load Consumption"
class: "energy"
state_class: "total_increasing"
uom: "kWh"
scale: 0.1
rule: 3
registers: [0x020F,0x0210]
icon: 'mdi:lightning-bolt-outline'
- group: Inverter
items:
- name: "Current L1"
class: "current"
state_class: "measurement"
uom: "A"
scale: 0.01
rule: 2
registers: [0x0276]
icon: 'mdi:home-lightning-bolt'
- name: "Current L2"
class: "current"
state_class: "measurement"
uom: "A"
scale: 0.01
rule: 2
registers: [0x0277]
icon: 'mdi:home-lightning-bolt'
- name: "Current L3"
class: "current"
uom: "A"
scale: 0.01
rule: 2
registers: [0x0278]
icon: 'mdi:home-lightning-bolt'
- name: "Inverter L1 Power"
class: "power"
state_class: "measurement"
uom: "W"
scale: 1
rule: 2
registers: [0x0279]
icon: 'mdi:home-lightning-bolt'
- name: "Inverter L2 Power"
class: "power"
state_class: "measurement"
uom: "W"
scale: 1
rule: 2
registers: [0x027A]
icon: 'mdi:home-lightning-bolt'
- name: "Inverter L3 Power"
class: "power"
state_class: "measurement"
uom: "W"
scale: 1
rule: 2
registers: [0x027B]
icon: 'mdi:home-lightning-bolt'
- name: "DC Temperature"
class: "temperature"
state_class: "measurement"
uom: "°C"
scale: 0.1
rule: 2
offset: 1000
registers: [0x021C]
icon: 'mdi:thermometer'
- name: "AC Temperature"
class: "temperature"
state_class: "measurement"
uom: "°C"
scale: 0.1
rule: 2
offset: 1000
registers: [0x021D]
icon: 'mdi:thermometer'
- name: "Inverter ID"
class: ""
state_class: ""
uom: ""
scale: 1
rule: 5
registers: [0x0003,0x0004,0x0005,0x0006,0x0007]
isstr: true
- name: "Communication Board Version No."
class: ""
state_class: ""
uom: ""
scale: 1
rule: 1
registers: [0x0011]
isstr: true
- name: "Control Board Version No."
class: ""
state_class: ""
uom: ""
scale: 1
rule: 1
registers: [0x000D]
isstr: true
- group: Alert
items:
- name: "Alert"
class: ""
state_class: ""
uom: ""
scale: 1
rule: 6
registers: [0x0229,0x022A,0x22B,0x022C,0x022D,0x022E]

View File

@ -0,0 +1,206 @@
requests:
- start: 0x0003
end: 0x0070
mb_functioncode: 0x03
parameters:
- group: solar
items:
- name: "PV1 Voltage"
class: "voltage"
state_class: "measurement"
uom: "V"
scale: 0.1
rule: 1
registers: [0x006D]
icon: 'mdi:solar-power'
- name: "PV2 Voltage"
class: "voltage"
state_class: "measurement"
uom: "V"
scale: 0.1
rule: 1
registers: [0x006F]
icon: 'mdi:solar-power'
- name: "PV1 Current"
class: "current"
uom: "A"
scale: 0.1
rule: 1
registers: [0x006E]
icon: 'mdi:solar-power'
- name: "PV2 Current"
class: "current"
state_class: "measurement"
uom: "A"
scale: 0.1
rule: 1
registers: [0x0070]
icon: 'mdi:solar-power'
- name: "Daily Production"
class: "energy"
state_class: "total"
uom: "kWh"
scale: 0.1
rule: 1
registers: [0x003C]
icon: 'mdi:solar-power'
- name: "Total Production"
class: "energy"
state_class: "total_increasing"
uom: "kWh"
scale: 0.1
rule: 3
registers: [0x003F,0x0040]
icon: 'mdi:solar-power'
validation:
min: 0.1
invalidate_all:
- group: Grid
items:
- name: "Grid Voltage L-L(A)"
class: "voltage"
state_class: "measurement"
uom: "V"
scale: 0.1
rule: 1
registers: [0x0049]
icon: 'mdi:transmission-tower'
- name: "Grid Voltage L-L(B))"
class: "voltage"
state_class: "measurement"
uom: "V"
scale: 0.1
rule: 1
registers: [0x004A]
icon: 'mdi:transmission-tower'
- name: "Grid Voltage L-L(C)"
class: "voltage"
state_class: "measurement"
uom: "V"
scale: 0.1
rule: 1
registers: [0x004B]
icon: 'mdi:transmission-tower'
- name: "Grid Current A"
class: "current"
state_class: "measurement"
uom: "A"
scale: 0.1
rule: 2
registers: [0x004C]
icon: 'mdi:home-lightning-bolt'
- name: "Grid Current B"
class: "current"
state_class: "measurement"
uom: "A"
scale: 0.1
rule: 2
registers: [0x004D]
icon: 'mdi:home-lightning-bolt'
- name: "Grid Current C"
class: "current"
state_class: "measurement"
uom: "A"
scale: 0.1
rule: 2
registers: [0x004E]
icon: 'mdi:home-lightning-bolt'
- name: "Grid Frequency"
class: "frequency"
state_class: "measurement"
uom: "Hz"
scale: 0.01
rule: 1
registers: [0x004F]
icon: 'mdi:home-lightning-bolt'
- group: Inverter
items:
- name: "Running Status"
class: ""
state_class: ""
uom: ""
scale: 1
rule: 1
registers: [0x003B]
isstr: true
lookup:
- key: 0
value: "Stand-by"
- key: 1
value: "Self-checking"
- key: 2
value: "Normal"
- key: 3
value: "FAULT"
icon: 'mdi:home-lightning-bolt'
- name: "Total Output AC Power"
class: "power"
state_class: "measurement"
uom: "W"
scale: 0.1
rule: 3
registers: [0x0050,0x0051]
icon: 'mdi:home-lightning-bolt'
- name: "Input Active Power"
class: "power"
state_class: "measurement"
uom: "W"
scale: 0.1
rule: 3
registers: [0x0052, 0x0053]
icon: 'mdi:home-lightning-bolt'
- name: "Output Apparent Power"
class: "apparent_power"
state_class: "measurement"
uom: "VA"
scale: 0.1
rule: 3
registers: [0x0054, 0x0055]
icon: 'mdi:home-lightning-bolt'
- name: "Output Active Power"
class: "energy"
state_class: "measurement"
uom: "W"
scale: 0.1
rule: 3
registers: [0x0056, 0x0057]
icon: 'mdi:home-lightning-bolt'
- name: "Output Reactive Power"
class: "reactive_power"
state_class: "measurement"
uom: "VAR"
rule: 3
scale: 0.1
registers: [0x0058, 0x0059]
icon: 'mdi:home-lightning-bolt'
- name: "Inverter ID"
class: ""
state_class: ""
uom: ""
scale: 1
rule: 5
registers: [0x0003,0x0004,0x0005,0x0006,0x0007]
isstr: true

View File

@ -0,0 +1,793 @@
# KSTAR Hybrid Inverter
# Modbus information taken from "MODBUS RS485 Communication Protocol V2.5" document provided by KSTAR
#INPUT_REGISTERS = 3000 - 3660 # 0x0BB8 - 0x0E4C
#HOLDING_REGISTERS = 3200 - 3237 # 0x0C80 - 0x0C9B
requests:
# Input registers 3000 - 3667
- start: 3000
end: 3125
mb_functioncode: 0x04
# Input registers 3200 - 3228 not read as they would clash with holding registers
- start: 3125
end: 3200
mb_functioncode: 0x04
- start: 3228
end: 3250
mb_functioncode: 0x04
- start: 3250
end: 3375
mb_functioncode: 0x04
- start: 3375
end: 3500
mb_functioncode: 0x04
# Holding registers 3200 - 3237. Inverter system information.
- start: 3200
end: 3218
mb_functioncode: 0x03
parameters:
- group: solar
items:
- name: "PV1 Voltage"
class: "voltage"
state_class: "measurement"
uom: "V"
scale: 0.1
rule: 1
registers: [ 3000 ]
icon: 'mdi:solar-power'
- name: "PV2 Voltage"
class: "voltage"
state_class: "measurement"
uom: "V"
scale: 0.1
rule: 1
registers: [ 3001 ]
icon: 'mdi:solar-power'
- name: "PV1 Current"
class: "current"
state_class: "measurement"
uom: "A"
scale: 0.01
rule: 2
registers: [ 3012 ]
icon: 'mdi:solar-power'
- name: "PV2 Current"
class: "current"
state_class: "measurement"
uom: "A"
scale: 0.01
rule: 2
registers: [ 3013 ]
icon: 'mdi:solar-power'
- name: "PV1 Power"
class: "power"
state_class: "measurement"
uom: "W"
scale: 1
rule: 2
registers: [ 3024 ]
icon: 'mdi:solar-power'
- name: "PV2 Power"
class: "power"
state_class: "measurement"
uom: "W"
scale: 1
rule: 2
registers: [ 3025 ]
icon: 'mdi:solar-power'
- name: "Daily Production"
class: "energy"
state_class: "total_increasing"
uom: "kWh"
scale: 0.1
rule: 1
registers: [ 3036 ]
icon: 'mdi:solar-power'
- name: "Monthly Production"
class: "energy"
state_class: "total_increasing"
uom: "kWh"
scale: 1
rule: 3
registers: [ 3038, 3037 ]
icon: 'mdi:solar-power'
- name: "Yearly Production"
class: "energy"
state_class: "total_increasing"
uom: "kWh"
scale: 1
rule: 3
registers: [ 3040, 3039 ]
icon: 'mdi:solar-power'
- name: "Cumulative Production"
class: "energy"
state_class: "total"
uom: "kWh"
scale: 0.1
rule: 3
registers: [ 3042, 3041 ]
icon: 'mdi:solar-power'
- group: Power Grid
items:
# Should this be the sum of the 3 phases "Meter Power"?
- name: "Total Grid Power"
class: "power"
state_class: "measurement"
uom: "W"
scale: 1
rule: 2
registers: [ 3100 ]
icon: 'mdi:transmission-tower'
- name: "Daily Energy Purchased"
class: "energy"
state_class: "total_increasing"
uom: "kWh"
scale: 0.1
rule: 1
registers: [ 3109 ]
icon: 'mdi:transmission-tower-import'
- name: "Monthly Energy Purchased"
class: "energy"
state_class: "total_increasing"
uom: "kWh"
scale: 1
rule: 3
registers: [ 3111, 3110 ]
icon: 'mdi:transmission-tower-import'
- name: "Yearly Energy Purchased"
class: "energy"
state_class: "total_increasing"
uom: "kWh"
scale: 1
rule: 3
registers: [ 3113, 3112 ]
icon: 'mdi:transmission-tower-import'
- name: "Cumulative Energy Purchased"
class: "energy"
state_class: "total"
uom: "kWh"
scale: 0.1
rule: 3
registers: [ 3115, 3114 ]
icon: 'mdi:transmission-tower-import'
- name: "Daily Energy Feed-In"
class: "energy"
state_class: "total_increasing"
uom: "kWh"
scale: 0.1
rule: 1
registers: [ 3116 ]
icon: 'mdi:transmission-tower-export'
- name: "Monthly Energy Feed-In"
class: "energy"
state_class: "total_increasing"
uom: "kWh"
scale: 1
rule: 3
registers: [ 3118, 3117 ]
icon: 'mdi:transmission-tower-export'
- name: "Yearly Energy Feed-In"
class: "energy"
state_class: "total_increasing"
uom: "kWh"
scale: 1
rule: 3
registers: [ 3120, 3119 ]
icon: 'mdi:transmission-tower-export'
- name: "Cumulative Grid Feed-In"
class: "energy"
state_class: "total"
uom: "kWh"
scale: 0.1
rule: 3
registers: [ 3122, 3121 ]
icon: 'mdi:transmission-tower-export'
- group: Electricity Consumption
items:
# Should this be the sum of the 3 phases "Load Power"?
- name: "Total Consumption Power"
class: "power"
state_class: "measurement"
uom: "W"
scale: 1
rule: 1
registers: [ 3144 ]
icon: 'mdi:home-lightning-bolt'
- name: "Daily Consumption"
class: "energy"
state_class: "total_increasing"
uom: "kWh"
scale: 0.1
rule: 1
registers: [ 3147 ]
icon: 'mdi:home-lightning-bolt'
- name: "Monthly Consumption"
class: "energy"
state_class: "total_increasing"
uom: "kWh"
scale: 1
rule: 3
registers: [ 3149, 3148 ]
icon: 'mdi:home-lightning-bolt'
- name: "Yearly Consumption"
class: "energy"
state_class: "total_increasing"
uom: "kWh"
scale: 1
rule: 3
registers: [ 3151, 3150 ]
icon: 'mdi:home-lightning-bolt'
- name: "Cumulative Consumption"
class: "energy"
state_class: "total"
uom: "kWh"
scale: 0.1
rule: 3
registers: [ 3153, 3152 ]
icon: 'mdi:home-lightning-bolt'
- group: Battery
items:
- name: "Battery Type"
class: "battery"
state_class: "measurement"
uom: ''
scale: 1
rule: 1
registers: [ 3062 ]
icon: 'mdi:battery'
lookup:
- key: 1
value: "Lead-Acid"
- key: 6
value: "LFP"
- name: "Battery Voltage"
class: "voltage"
state_class: "measurement"
uom: "V"
scale: 0.01
rule: 1
registers: [ 3063 ]
icon: 'mdi:battery-charging'
- name: "Battery Current"
class: "current"
state_class: "measurement"
uom: "A"
scale: 0.1
rule: 2
registers: [ 3064 ]
icon: 'mdi:battery-charging-10'
- name: "Battery Power"
class: "power"
state_class: "measurement"
uom: "W"
scale: 1
rule: 2
registers: [ 3065 ]
icon: 'mdi:battery-charging-high'
- name: "Battery SoC"
class: "battery"
state_class: "measurement"
uom: "%"
scale: 0.1
rule: 1
registers: [ 3066 ]
icon: 'mdi:battery'
- name: "Battery Temperature"
class: "temperature"
state_class: "measurement"
uom: "°C"
scale: 0.1
rule: 2
registers: [ 3067 ]
icon: 'mdi:battery-heart-outline'
- name: "Battery Discharge Capacity Depth"
class: "battery"
state_class: "measurement"
uom: "%"
scale: 1
rule: 1
registers: [ 3068 ]
icon: 'mdi:battery-20'
validation:
min: 10
max: 95
- name: "Battery Radiator Temperature"
class: "temperature"
state_class: "measurement"
uom: "°C"
scale: 0.1
rule: 2
registers: [ 3056 ]
icon: 'mdi:battery-heart-outline'
- name: "Battery Total Discharge"
class: "energy"
state_class: "total_increasing"
uom: "kWh"
scale: 1
rule: 3
registers: [ 3293, 3292 ]
icon: 'mdi:battery-minus-variant'
- name: "Battery Daily Discharge"
class: "energy"
state_class: "total_increasing"
uom: "kWh"
scale: 0.1
rule: 1
registers: [ 3294 ]
icon: 'mdi:battery-minus-variant'
- name: "Battery Total Charge"
class: "energy"
state_class: "total_increasing"
uom: "kWh"
scale: 1
rule: 3
registers: [ 3300, 3299 ]
icon: 'mdi:battery-minus-variant'
- name: "Battery Daily Charge"
class: "energy"
state_class: "total_increasing"
uom: "kWh"
scale: 0.1
rule: 1
registers: [ 3301 ]
icon: 'mdi:battery-plus-variant'
- group: Inverter Information
items:
- name: "Inverter Working Mode"
class: ""
state_class: "measurement"
uom: ""
scale: 1
rule: 1
registers: [3044]
lookup:
- key: 0
value: "Self Consumption"
- key: 1
value: "Peak Shift"
- key: 2
value: "Battery Priority"
icon: 'mdi:wrench'
- name: "Inverter Model"
class: ""
state_class: "measurement"
uom: ""
scale: 1
rule: 1
registers: [3045]
lookup:
# Single-phase models
- key: 0
value: "KSE-2K-048S"
- key: 1
value: "KSE-3K-048S"
- key: 2
value: "KSE-3.6K-048S"
- key: 3
value: "KSE-4.6K-048S"
- key: 4
value: "KSE-5K-048S"
- key: 5
value: "KSE-3.6K-048"
- key: 6
value: "KSE-4.6K-048"
- key: 7
value: "KSE-5K-048"
- key: 8
value: "KSE-6K-048"
- key: 9
value: "BluE-S 3680D"
- key: 11
value: "BluE-S 5000D"
- key: 12
value: "BluE-S 6000D"
- key: 14
value: "KSE-3K-048S M1"
- key: 15
value: "BluE-S 3680D M1"
- key: 17
value: "BluE-S 5000D M1"
- key: 18
value: "BluE-S 6000D M1"
# Three-phase models
- key: 32
value: "E10KT"
- key: 33
value: "E8KT"
- key: 34
value: "E12KT"
icon: 'mdi:wrench'
- name: "System status"
class: ""
state_class: "measurement"
uom: ""
scale: 1
rule: 1
registers: [3046]
lookup:
- key: 0
value: "Initialize"
- key: 1
value: "Stand-by"
- key: 2
value: "Hybrid Grid"
- key: 3
value: "Off-Network"
- key: 4
value: "Mains Charging"
- key: 5
value: "PV Charging"
- key: 6
value: "Mains Bypass"
- key: 7
value: "Fault"
- key: 8
value: "Debug"
- key: 9
value: "Forced Charge"
- key: 10
value: "Power on the device separately from the"
- key: 11
value: "DSP Burn"
- key: 12
value: "MCU Burn"
- key: 13
value: "Permanent Error"
icon: 'mdi:wrench'
- name: "Inverter status"
class: ""
state_class: "measurement"
uom: ""
scale: 1
rule: 1
registers: [3047]
lookup:
- key: 0
value: "Stand-by"
- key: 1
value: "Off-Grid"
- key: 2
value: "On-Grid"
- key: 3
value: "Off-Grid to On-Grid"
- key: 4
value: "On-Grid to Off-Grid"
icon: 'mdi:wrench'
- name: "DCDC status"
class: ""
state_class: "measurement"
uom: ""
scale: 1
rule: 1
registers: [3048]
lookup:
- key: 0
value: "Stand-by"
- key: 1
value: "Soft Boot"
- key: 2
value: "Charging Mode"
- key: 3
value: "Discharging Mode"
icon: 'mdi:wrench'
- name: "DSP Alarm Code"
class: ""
state_class: "measurement"
uom: ""
scale: 1
rule: 6
registers: [3050, 3049]
icon: 'mdi:wrench'
- name: "DSP Error Code"
class: ""
state_class: "measurement"
uom: ""
scale: 1
rule: 6
registers: [3052, 3051]
icon: 'mdi:wrench'
- name: "Grid Standard"
class: ""
state_class: "measurement"
uom: ""
scale: 1
rule: 1
registers: [3193]
lookup:
- key: 0
value: "China"
- key: 1
value: "Germany"
- key: 2
value: "Australia"
- key: 3
value: "Italy"
- key: 4
value: "Spain"
- key: 5
value: "UK"
- key: 6
value: "Hungary"
- key: 7
value: "Belgium"
- key: 8
value: "West Australia"
- key: 9
value: "Greece"
- key: 10
value: "France"
- key: 11
value: "Bangkok"
- key: 12
value: "Thailand"
- key: 13
value: "South Africa"
- key: 14
value: "EN50549"
- key: 15
value: "Brazil"
- key: 16
value: "VDE0126"
- key: 17
value: "Ireland"
- key: 18
value: "Israel"
- key: 19
value: "Poland"
- key: 20
value: "Chile"
- key: 21
value: "Local"
icon: 'mdi:wrench'
- name: "Inverter Model Name"
class: ""
state_class: "measurement"
uom: ""
scale: 1
rule: 5
registers: [3200, 3201, 3202, 3203, 3204, 3205, 3206, 3207]
icon: 'mdi:wrench'
- name: "Inverter Battery Name"
class: ""
state_class: "measurement"
uom: ""
scale: 1
rule: 5
registers: [3208, 3209, 3210, 3211, 3212, 3213, 3214, 3215]
icon: 'mdi:wrench'
# ARM AND DSP version numbers ("VX.Y.Z") are set in the two bytes on each register. The first byte contains the
# X.Y part (scale 0.1), and the second by contains the Z part. How should we transform these values from a number
# to a parsed string?
- name: "ARM Version Number"
class: ""
state_class: "measurement"
uom: ""
scale: 1
rule: 1
registers: [3216]
icon: 'mdi:wrench'
- name: "DSP Version Number"
class: ""
state_class: "measurement"
uom: ""
scale: 1
rule: 1
registers: [3217]
icon: 'mdi:wrench'
- name: "Inverter SN Number"
class: ""
state_class: "measurement"
uom: ""
scale: 1
rule: 5
registers: [3228, 3229, 3230, 3231, 3232, 3233, 3234, 3235, 3236, 3237, 3238]
icon: 'mdi:wrench'
- group: Inverter
items:
- name: "Inverter Bus Voltage"
class: "voltage"
state_class: "measurement"
uom: "V"
scale: 0.1
rule: 1
registers: [ 3053 ]
icon: 'mdi:home-lightning-bolt'
- name: "Inverter DC Bus Voltage"
class: "voltage"
state_class: "measurement"
uom: "V"
scale: 0.1
rule: 1
registers: [ 3054 ]
icon: 'mdi:home-lightning-bolt'
- name: "Inverter Radiator Temperature"
class: "temperature"
state_class: "measurement"
uom: "°C"
scale: 0.1
rule: 2
registers: [ 3055 ]
icon: 'mdi:thermometer'
- name: "Chassis Internal Temperature"
class: "temperature"
state_class: "measurement"
uom: "°C"
scale: 0.1
rule: 2
registers: [ 3057 ]
icon: 'mdi:battery-heart-outline'
# Different phases for 3-phase inverters. Only some models have 3 phases, see "Inverter Model" item
# - R: Referent
# - S: Secondary
# - T: Tertiary
- group: R Phase
items:
- name: "R-phase Grid Voltage"
class: "voltage"
state_class: "measurement"
uom: "V"
scale: 0.1
rule: 1
registers: [ 3097 ]
icon: 'mdi:home-lightning-bolt'
- name: "R-phase Grid Frequency"
class: "frequency"
state_class: "measurement"
uom: "Hz"
scale: 0.01
rule: 1
registers: [ 3098 ]
icon: 'mdi:home-lightning-bolt'
- name: "R-phase Meter Current"
class: "current"
state_class: "measurement"
uom: "A"
scale: 0.001
rule: 2
registers: [ 3099 ]
icon: 'mdi:home-lightning-bolt'
- name: "R-phase Grid Power"
class: "power"
state_class: "measurement"
uom: "W"
scale: 1
rule: 2
registers: [ 3100 ]
icon: 'mdi:home-lightning-bolt'
- name: "R-phase Inverter Voltage"
class: "voltage"
state_class: "measurement"
uom: "V"
scale: 0.1
rule: 1
registers: [ 3123 ]
icon: 'mdi:home-lightning-bolt'
- name: "R-phase Inverter Current"
class: "current"
state_class: "measurement"
uom: "A"
scale: 0.01
rule: 2
registers: [ 3124 ]
icon: 'mdi:home-lightning-bolt'
- name: "R-phase Inverter Frequency"
class: "frequency"
state_class: "measurement"
uom: "Hz"
scale: 0.01
rule: 1
registers: [ 3125 ]
icon: 'mdi:home-lightning-bolt'
- name: "R-phase Inverter Power"
class: "power"
state_class: "measurement"
uom: "W"
scale: 1
rule: 2
registers: [ 3126 ]
icon: 'mdi:home-lightning-bolt'
- name: "R-phase Backup Voltage"
class: "voltage"
state_class: "measurement"
uom: "V"
scale: 0.1
rule: 1
registers: [ 3135 ]
icon: 'mdi:home-lightning-bolt'
- name: "R-phase Backup Current"
class: "current"
state_class: "measurement"
uom: "A"
scale: 0.01
rule: 1
registers: [ 3136 ]
icon: 'mdi:home-lightning-bolt'
- name: "R-phase Backup Power"
class: "power"
state_class: "measurement"
uom: "W"
scale: 1
rule: 1
registers: [ 3137 ]
icon: 'mdi:home-lightning-bolt'
- name: "R-phase Load Power"
class: "power"
state_class: "measurement"
uom: "W"
scale: 1
rule: 1
registers: [ 3144 ]
icon: 'mdi:home-lightning-bolt'

View File

@ -0,0 +1,602 @@
requests:
- start: 0x0000
end: 0x0027
mb_functioncode: 0x03
parameters:
- group: solar
items:
- name: "PV1 Power"
class: "power"
state_class: "measurement"
uom: "W"
scale: 10
rule: 1
registers: [0x000A]
icon: 'mdi:solar-power'
- name: "PV2 Power"
class: "power"
state_class: "measurement"
uom: "W"
scale: 10
rule: 1
registers: [0x000B]
icon: 'mdi:solar-power'
- name: "PV1 Voltage"
class: "voltage"
state_class: "measurement"
uom: "V"
scale: 0.1
rule: 1
registers: [0x0006]
icon: 'mdi:solar-power'
- name: "PV2 Voltage"
class: "voltage"
state_class: "measurement"
uom: "V"
scale: 0.1
rule: 1
registers: [0x0008]
icon: 'mdi:solar-power'
- name: "PV1 Current"
class: "current"
state_class: "measurement"
uom: "A"
scale: 0.01
rule: 1
registers: [0x0007]
icon: 'mdi:solar-power'
- name: "PV2 Current"
class: "current"
state_class: "measurement"
uom: "A"
scale: 0.01
rule: 1
registers: [0x0009]
icon: 'mdi:solar-power'
- name: "Daily Production"
class: "energy"
state_class: "total"
uom: "kWh"
scale: 0.01
rule: 1
registers: [0x0019]
icon: 'mdi:solar-power'
- name: "Total Production"
class: "energy"
state_class: "total_increasing"
uom: "kWh"
scale: 1
rule: 3
registers: [0x0016,0x0015]
icon: 'mdi:solar-power'
- name: "Total generation time"
class: ""
state_class: "measurement"
uom: "h"
scale: 1
rule: 3
registers: [0x0018,0x0017]
icon: 'mdi:clock-outline'
- name: "Today generation time"
class: ""
state_class: "measurement"
uom: "min"
scale: 1
rule: 1
registers: [0x001A]
icon: 'mdi:clock-outline'
- group: Output
items:
- name: "Output active power"
class: "power"
state_class: "measurement"
uom: "W"
scale: 10
rule: 1
registers: [0x000C]
icon: 'mdi:home-lightning-bolt'
- name: "Output reactive power"
class: ""
state_class: "measurement"
uom: "kVar"
scale: 0.01
rule: 1
registers: [0x000D]
icon: 'mdi:home-lightning-bolt'
- name: "Grid frequency"
class: "frequency"
state_class: "measurement"
uom: "Hz"
scale: 0.01
rule: 1
registers: [0x000E]
icon: 'mdi:home-lightning-bolt'
- name: "L1 Voltage"
class: "voltage"
state_class: "measurement"
uom: "V"
scale: 0.1
rule: 1
registers: [0x000F]
icon: 'mdi:home-lightning-bolt'
- name: "L1 Current"
class: "current"
state_class: "measurement"
uom: "A"
scale: 0.01
rule: 1
registers: [0x0010]
icon: 'mdi:home-lightning-bolt'
- name: "L2 Voltage"
class: "voltage"
state_class: "measurement"
uom: "V"
scale: 0.1
rule: 1
registers: [0x0011]
icon: 'mdi:home-lightning-bolt'
- name: "L2 Current"
class: "current"
state_class: "measurement"
uom: "A"
scale: 0.01
rule: 1
registers: [0x0012]
icon: 'mdi:home-lightning-bolt'
- name: "L3 Voltage"
class: "voltage"
state_class: "measurement"
uom: "V"
scale: 0.1
rule: 1
registers: [0x0013]
icon: 'mdi:home-lightning-bolt'
- name: "L3 Current"
class: "current"
state_class: "measurement"
uom: "A"
scale: 0.01
rule: 1
registers: [0x0014]
icon: 'mdi:home-lightning-bolt'
- group: Inverter
items:
- name: "Inverter status"
class: ""
state_class: "measurement"
uom: ""
scale: 1
rule: 1
registers: [0x0000]
lookup:
- key: 0
value: "Stand-by"
- key: 1
value: "Self-checking"
- key: 2
value: "Normal"
- key: 3
value: "FAULT"
- key: 4
value: "Permanent"
icon: 'mdi:wrench'
- name: "Inverter module temperature"
class: "temperature"
uom: "°C"
scale: 1
rule: 1
registers: [0x001B]
icon: 'mdi:thermometer'
- name: "Inverter inner temperature"
class: "temperature"
state_class: "measurement"
uom: "°C"
scale: 1
rule: 1
registers: [0x001C]
icon: 'mdi:thermometer'
- name: "Inverter bus voltage"
class: "voltage"
state_class: "measurement"
uom: "V"
scale: 0.1
rule: 1
registers: [0x001D]
icon: 'mdi:home-lightning-bolt'
- name: "PV1 voltage sample by slave CPU"
class: "voltage"
state_class: "measurement"
uom: "V"
scale: 0.1
rule: 1
registers: [0x001E]
icon: 'mdi:home-lightning-bolt'
- name: "PV1 current sample by slave CPU"
class: "current"
state_class: "measurement"
uom: "A"
scale: 0.1
rule: 1
registers: [0x001F]
icon: 'mdi:home-lightning-bolt'
- name: "Countdown time"
class: ""
state_class: "measurement"
uom: "s"
scale: 1
rule: 1
registers: [0x0020]
icon: ''
- name: "Inverter alert message"
class: ""
state_class: ""
uom: ""
scale: 1
rule: 1
registers: [0x0021]
icon: ''
- name: "Input mode"
class: ""
state_class: ""
uom: ""
scale: 1
rule: 1
registers: [0x0022]
icon: ''
- name: "Communication Board inner message"
class: ""
state_class: ""
uom: ""
scale: 1
rule: 1
registers: [0x0023]
icon: ''
- name: "Insulation of PV1+ to ground"
class: ""
state_class: "measurement"
uom: ""
scale: 1
rule: 1
registers: [0x0024]
icon: ''
- name: "Insulation of PV2+ to ground"
class: ""
state_class: "measurement"
uom: ""
scale: 1
rule: 1
registers: [0x0025]
icon: ''
- name: "Insulation of PV- to ground"
class: ""
state_class: "measurement"
uom: ""
scale: 1
rule: 1
registers: [0x0026]
icon: ''
- name: "Country"
class: ""
state_class: ""
uom: ""
scale: 1
rule: 1
registers: [0x0027]
lookup:
- key: 0
value: "Germany"
- key: 1
value: "CEI0-21 Internal"
- key: 2
value: "Australia"
- key: 3
value: "Spain RD1699"
- key: 4
value: "Turkey"
- key: 5
value: "Denmark"
- key: 6
value: "Greece"
- key: 7
value: "Netherland"
- key: 8
value: "Belgium"
- key: 9
value: "UK-G59"
- key: 10
value: "China"
- key: 11
value: "France"
- key: 12
value: "Poland"
- key: 13
value: "Germany BDEW"
- key: 14
value: "Germany VDE0126"
- key: 15
value: "Italy CEI0-16"
- key: 16
value: "UK-G83"
- key: 17
value: "Greece Islands"
- key: 18
value: "EU EN50438"
- key: 19
value: "EU EN61727"
- key: 20
value: "Korea"
- key: 21
value: "Sweden"
- key: 22
value: "Europe General"
- key: 23
value: "CEI0-21 External"
- key: 24
value: "Cyprus"
- key: 25
value: "India"
- key: 26
value: "Philippines"
- key: 27
value: "New Zeland"
- key: 28
value: "Reserve"
- key: 29
value: "Reserve"
icon: ''
- group: Alert
items:
- name: "Fault 1"
class: ""
state_class: ""
uom: ""
scale: 1
rule: 1
registers: [0x0001]
lookup:
- key: 0
value: "No error"
- key: 1
value: "ID01 Grid Over Voltage Protection"
- key: 2
value: "ID02 Grid Under Voltage Protection"
- key: 4
value: "ID03 Grid Over Frequency Protection"
- key: 8
value: "ID04 Grid Under Frequency Protection"
- key: 16
value: "ID05 PV Under Voltage Protection"
- key: 32
value: "ID06 Grid Low Voltage Ride through"
- key: 64
value: "ID07"
- key: 128
value: "ID08"
- key: 256
value: "ID09 PV Over Voltage Protection"
- key: 512
value: "ID10 PV Input Current Unbalanced"
- key: 1024
value: "ID11 PV Input Mode wrong configuration"
- key: 2048
value: "ID12 Ground-Fault circuit interrupters fault"
- key: 4096
value: "ID13 Phase sequence fault"
- key: 8192
value: "ID14 Hardware boost over current protection"
- key: 16384
value: "ID15 Hardware AC over current protection"
- key: 32768
value: "ID16 Grid current is too high"
icon: 'mdi:wrench'
- name: "Fault 2"
class: ""
state_class: ""
uom: ""
scale: 1
rule: 1
registers: [0x0002]
lookup:
- key: 0
value: "No error"
- key: 1
value: "ID17 Grid current sampling error"
- key: 2
value: "ID18 DCI sampling error"
- key: 4
value: "ID19 Grid voltage sampling error"
- key: 8
value: "ID20 GFCI device sampling error"
- key: 16
value: "ID21 Main chip fault"
- key: 32
value: "ID22 Hardware auxiliary power fault"
- key: 64
value: "ID23 Bus voltage zero fault"
- key: 128
value: "ID24 Output current not balanced"
- key: 256
value: "ID25 Bus under voltage protection"
- key: 512
value: "ID26 Bus over voltage protection"
- key: 1024
value: "ID27 Bus voltage unbalanced"
- key: 2048
value: "ID28 DCI is too high"
- key: 4096
value: "ID29 Grid current is too high"
- key: 8192
value: "ID30 Input current is too high"
- key: 16384
value: "ID31"
- key: 32768
value: "ID32"
icon: 'mdi:wrench'
- name: "Fault 3"
class: ""
state_class: ""
uom: ""
scale: 1
rule: 1
registers: [0x0003]
lookup:
- key: 0
value: "No error"
- key: 1
value: "ID33 Reserved"
- key: 2
value: "ID34 Reserved"
- key: 4
value: "ID35 Reserved"
- key: 8
value: "ID36 Reserved"
- key: 16
value: "ID37 Reserved"
- key: 32
value: "ID38 Reserved"
- key: 64
value: "ID39 Reserved"
- key: 128
value: "ID40 Reserved"
- key: 256
value: "ID41 Reserved"
- key: 512
value: "ID42 Reserved"
- key: 1024
value: "ID43 Reserved"
- key: 2048
value: "ID44 Reserved"
- key: 4096
value: "ID45 Reserved"
- key: 8192
value: "ID46 Reserved"
- key: 16384
value: "ID47 Reserved"
- key: 32768
value: "ID48 Reserved"
icon: 'mdi:wrench'
- name: "Fault 4"
class: ""
state_class: ""
uom: ""
scale: 1
rule: 1
registers: [0x0004]
lookup:
- key: 0
value: "No error"
- key: 1
value: "ID49 Grid voltage sampling value between master and slave DSP vary widely"
- key: 2
value: "ID50 Grid frequency sampling value between master and slave DSP vary widely"
- key: 4
value: "ID51 DCI sampling value between master and slave DSP vary widely"
- key: 8
value: "ID52 GFCI sampling value between master and slave DSP vary widely"
- key: 16
value: "ID53 Communication failure between master and slave DSP failure"
- key: 32
value: "ID53 Communication failure between slave and communication board"
- key: 64
value: "ID55 Relay fault"
- key: 128
value: "ID56 Insulation resistance between PV array and the earth is too low"
- key: 256
value: "ID57 Inverter temp is too high"
- key: 512
value: "ID58 Boost temp is too high"
- key: 1024
value: "ID59 Environment temp is too high"
- key: 2048
value: "ID60 Brak podłączenie falownika do kabla PE"
- key: 4096
value: "ID61 Reserved"
- key: 8192
value: "ID62 Reserved"
- key: 16384
value: "ID63 Reserved"
- key: 32768
value: "ID64 Reserved"
icon: 'mdi:wrench'
- name: "Fault 5"
class: ""
state_class: ""
uom: ""
scale: 1
rule: 1
registers: [0x0005]
lookup:
- key: 0
value: "No error"
- key: 1
value: "ID65 Grid current is too high and causes unrecoverable fault"
- key: 2
value: "ID66 Bus voltage is too high and causes unrecoverable fault"
- key: 4
value: "ID67 Grid current is unbalanced and causes unrecoverable fault"
- key: 8
value: "ID68 Input current is unbalanced and causes unrecoverable fault"
- key: 16
value: "ID69 Bus voltage is unbalanced and causes unrecoverable fault"
- key: 32
value: "ID70 Grid current is too high and causes unrecoverable fault"
- key: 64
value: "ID65 PV Input Mode Configuration is wrong and causes unrecoverable fault"
- key: 128
value: "ID72 Reserved"
- key: 256
value: "ID73 Reserved"
- key: 512
value: "ID74 Input current is too high and causes unrecoverable fault"
- key: 1024
value: "ID75 Error reading from EEPROM"
- key: 2048
value: "ID76 Error writing to EEPROM"
- key: 4096
value: "ID77 Relay fauilure causes unrecoverable fault"
- key: 8192
value: "ID78 Reserved"
- key: 16384
value: "ID79 Reserved"
- key: 32768
value: "ID80 Reserved"
icon: 'mdi:wrench'

View File

@ -0,0 +1,769 @@
requests:
# Inverter State
- start: 0x0200
end: 0x0245
mb_functioncode: 0x03
# Inverter Settings
- start: 0x10B0
end: 0x10BC
mb_functioncode: 0x04
# Inverter Information
- start: 0x2000
end: 0x200B
mb_functioncode: 0x04
# Tested with Solarman ME3000-SP with an embedded
# "wifikit" logger. Might work for other devices
# too.
# The ME3000-SP is basically a glorified battery
# charger - it is not directly connected to any
# generation infrastructure, but can calculate
# generated energy based on any CT clamps it is
# connected to. For most people this entity will
# be a generally accurate representation of their
# PV panel output, but since this tracks generation
# from _any_ source, it is named generically.
parameters:
- group: Generation
items:
- name: "Generation Power"
class: "power"
state_class: "measurement"
uom: "kW"
scale: 0.01
rule: 1
registers: [0x0215]
icon: 'mdi:solar-power'
- name: "Daily Generation"
class: "energy"
state_class: "total_increasing"
uom: "kWh"
scale: 0.01
rule: 1
registers: [0x0218]
icon: 'mdi:solar-power'
- name: "Total Generation"
class: "energy"
state_class: "total_increasing"
uom: "kWh"
scale: 1
rule: 3
registers: [0x021D,0x021C]
icon: 'mdi:solar-power'
- name: "Daily Generation Time"
class: "duration"
state_class: "total_increasing"
uom: "min"
scale: 1
rule: 1
registers: [0x0243]
icon: 'mdi:sun-clock-outline'
- name: "Total Generation Time"
class: "duration"
state_class: "total_increasing"
uom: "h"
scale: 1
rule: 3
registers: [0x0245,0x244]
icon: 'mdi:sun-clock-outline'
- group: Load
items:
- name: "Consumption Power"
class: "power"
state_class: "measurement"
uom: "kW"
scale: 0.01
rule: 1
registers: [0x0213]
icon: 'mdi:home-lightning-bolt'
- name: "Daily Consumption"
class: "energy"
state_class: "total_increasing"
uom: "kWh"
scale: 0.01
rule: 1
registers: [0x021B]
icon: 'mdi:home-lightning-bolt'
- name: "Total Consumption"
class: "energy"
state_class: "total_increasing"
uom: "kWh"
scale: 1
rule: 3
registers: [0x0223,0x0222]
icon: 'mdi:home-lightning-bolt-outline'
- group: Grid
items:
- name: "Grid A Voltage"
class: "voltage"
state_class: "measurement"
uom: "V"
scale: 0.1
rule: 1
registers: [0x0206]
icon: 'mdi:transmission-tower'
- name: "Grid A Current"
class: "current"
state_class: "measurement"
uom: "A"
scale: 0.01
rule: 2
registers: [0x0207]
icon: 'mdi:current-ac'
- name: "Grid B Voltage"
class: "voltage"
state_class: "measurement"
uom: "V"
scale: 0.1
rule: 1
registers: [0x0208]
icon: 'mdi:transmission-tower'
- name: "Grid B Current"
class: "current"
state_class: "measurement"
uom: "A"
scale: 0.01
rule: 2
registers: [0x0209]
icon: 'mdi:current-ac'
- name: "Grid C Voltage"
class: "voltage"
state_class: "measurement"
uom: "V"
scale: 0.1
rule: 1
registers: [0x020A]
icon: 'mdi:transmission-tower'
- name: "Grid C Current"
class: "current"
state_class: "measurement"
uom: "A"
scale: 0.01
rule: 2
registers: [0x020B]
icon: 'mdi:current-ac'
- name: "Grid Frequency"
class: "frequency"
state_class: "measurement"
uom: "Hz"
scale: 0.01
rule: 1
registers: [0x20C]
icon: 'mdi:sine-wave'
- name: "Daily Power Sold"
class: "energy"
state_class: "total_increasing"
uom: "kWh"
scale: 0.01
rule: 1
registers: [0x0219]
icon: 'mdi:transmission-tower-export'
- name: "Daily Power Bought"
class: "energy"
state_class: "total_increasing"
uom: "kWh"
scale: 0.01
rule: 1
registers: [0x021A]
icon: 'mdi:transmission-tower-import'
- group: Battery
items:
- name: "Battery Voltage"
class: "voltage"
state_class: "measurement"
uom: "V"
scale: 0.01
rule: 1
registers: [0x020E]
icon: 'mdi:home-battery'
- name: "Battery Current"
class: "current"
state_class: "measurement"
uom: "A"
scale: 0.01
rule: 2
registers: [0x020F]
icon: 'mdi:current-dc'
- name: "Battery Charge / Discharge Power"
class: "power"
state_class: "measurement"
uom: "kW"
scale: 0.01
rule: 2
registers: [0x020D]
icon: 'mdi:home-battery'
- name: "Battery SOC"
class: "battery"
state_class: "measurement"
uom: "%"
scale: 1
rule: 1
registers: [0x210]
icon: 'mdi:battery'
- name: "Battery Temperature"
class: "temperature"
state_class: "measurement"
uom: "°C"
scale: 1
rule: 1
registers: [0x0211]
icon: 'mdi:thermometer'
- name: "Battery Capacity"
class: ""
state_class: ""
uom: ""
scale: 1
rule: 1
registers: [0x10B1]
icon: 'mdi:battery-high'
- name: "Battery Max Charge Voltage"
class: "voltage"
state_class: "measurement"
uom: "V"
scale: 0.1
rule: 1
registers: [0x10B3]
icon: 'mdi:battery'
- name: "Battery Max Charge Current"
class: "current"
state_class: "measurement"
uom: "A"
scale: 0.01
rule: 2
registers: [0x10B4]
icon: 'mdi:current-dc'
- name: "Battery Min Discharge Voltage"
class: "voltage"
state_class: "measurement"
uom: "V"
scale: 0.1
rule: 1
registers: [0x10B6]
icon: 'mdi:battery'
- name: "Battery Max Discharge Current"
class: "current"
state_class: "measurement"
uom: "A"
scale: 0.01
rule: 2
registers: [0x10B7]
icon: 'mdi:current-dc'
- name: "Battery Discharge Depth"
class: "battery"
state_class: "measurement"
uom: "%"
scale: 1
rule: 1
registers: [0x10B9]
icon: 'mdi:battery-high'
- name: "Battery Empty Voltage"
class: "voltage"
state_class: "measurement"
uom: "V"
scale: 0.01
rule: 1
registers: [0x10BB]
icon: 'mdi:battery'
- name: "Battery Full Voltage"
class: "voltage"
state_class: "measurement"
uom: "V"
scale: 0.01
rule: 1
registers: [0x10BC]
icon: 'mdi:battery'
- name: "Battery Type"
class: ""
state_class: "measurement"
uom: ""
scale: 1
rule: 1
registers: [0x10B0]
# This field has different definitions
# depending on a "Version". There is no
# indication what version this is,
# whether it is hardware or software so
# this is the V1.00 list. If your battery
# type is detected incorrectly, you
# probably have V1.20 (whatever that is)
# and need to use a custom inverter
# definition with the commented lookups
# below.
lookup:
- key: 0x0000
value: "DARFON"
- key: 0x0001
value: "PYLON"
- key: 0x0003
value: "SOLTARO"
- key: 0x0080
value: "TELE"
- key: 0x0100
value: "DEFAULT"
# V1.20 Lookups
# lookup:
# - key: 0x0000
# value: "DARFON"
# - key: 0x0001
# value: "PYLON"
# - key: 0x0002
# value: "SOLTARO"
# - key: 0x0003
# value: "ALPHA.ESS"
# - key: 0x0004
# value: "GENERAL"
# - key: 0x0100
# value: "DEFAULT"
- group: InverterStatus
items:
- name: "Inverter Status"
class: ""
state_class: "measurement"
uom: ""
scale: 1
rule: 1
registers: [0x200]
lookup:
- key: 0
value: "Wait"
- key: 1
value: "Self Check"
- key: 2
value: "Charging"
- key: 3
value: "Check Discharge"
- key: 4
value: "Discharging"
- key: 5
value: "EPS"
- key: 6
value: "Fault"
- key: 7
value: "Permanent Fault"
icon: 'mdi:wrench'
- name: "Inverter Temperature"
class: "temperature"
state_class: "measurement"
uom: "°C"
scale: 1
rule: 1
registers: [0x0238]
icon: 'mdi:thermometer'
- name: "Inverter Heat-Sink Temperature"
class: "temperature"
state_class: "measurement"
uom: "°C"
scale: 1
rule: 1
registers: [0x0239]
icon: 'mdi:thermometer'
- name: "Inverter Bus Voltage"
class: "voltage"
state_class: "measurement"
uom: "V"
scale: 0.1
rule: 1
registers: [0x022D]
icon: 'mdi:home-lightning-bolt'
- name: "LLC Bus Voltage"
class: "voltage"
state_class: "measurement"
uom: "V"
scale: 0.1
rule: 1
registers: [0x022E]
icon: 'mdi:home-lightning-bolt'
- name: "Countdown Time"
class: ""
state_class: "measurement"
uom: "s"
scale: 1
rule: 1
registers: [0x022A]
icon: ''
- name: "Inverter Alert Message"
class: ""
state_class: ""
uom: ""
scale: 1
rule: 1
registers: [0x022B]
icon: ''
- name: "Communication Board Inner Message"
class: ""
state_class: ""
uom: ""
scale: 1
rule: 1
registers: [0x0242]
icon: ''
- name: "Country"
class: ""
state_class: ""
uom: ""
scale: 1
rule: 1
registers: [0x023A]
lookup:
- key: 0
value: "Germany"
- key: 1
value: "CEI0-21 Internal"
- key: 2
value: "Australia"
- key: 3
value: "Spain RD1699"
- key: 4
value: "Turkey"
- key: 5
value: "Denmark"
- key: 6
value: "Greece"
- key: 7
value: "Netherland"
- key: 8
value: "Belgium"
- key: 9
value: "UK-G59"
- key: 10
value: "China"
- key: 11
value: "France"
- key: 12
value: "Poland"
- key: 13
value: "Germany BDEW"
- key: 14
value: "Germany VDE0126"
- key: 15
value: "Italy CEI0-16"
- key: 16
value: "UK-G83"
- key: 17
value: "Greece Islands"
- key: 18
value: "EU EN50438"
- key: 19
value: "EU EN61727"
- key: 20
value: "Korea"
- key: 21
value: "Sweden"
- key: 22
value: "Europe General"
- key: 23
value: "CEI0-21 External"
- key: 24
value: "Cyprus"
- key: 25
value: "India"
- key: 26
value: "Philippines"
- key: 27
value: "New Zeland"
- key: 28
value: "Reserve"
- key: 29
value: "Reserve"
icon: ''
- group: Alert
items:
- name: "Fault 1"
class: ""
state_class: ""
uom: ""
scale: 1
rule: 1
registers: [0x0201]
lookup:
- key: 0
value: "No error"
- key: 1
value: "ID01 Grid Over Voltage Protection"
- key: 2
value: "ID02 Grid Under Voltage Protection"
- key: 4
value: "ID03 Grid Over Frequency Protection"
- key: 8
value: "ID04 Grid Under Frequency Protection"
- key: 16
value: "ID05 PV Under Voltage Protection"
- key: 32
value: "ID06 Grid Low Voltage Ride through"
- key: 64
value: "ID07"
- key: 128
value: "ID08"
- key: 256
value: "ID09 PV Over Voltage Protection"
- key: 512
value: "ID10 PV Input Current Unbalanced"
- key: 1024
value: "ID11 PV Input Mode wrong configuration"
- key: 2048
value: "ID12 Ground-Fault circuit interrupters fault"
- key: 4096
value: "ID13 Phase sequence fault"
- key: 8192
value: "ID14 Hardware boost over current protection"
- key: 16384
value: "ID15 Hardware AC over current protection"
- key: 32768
value: "ID16 Grid current is too high"
icon: 'mdi:wrench'
- name: "Fault 2"
class: ""
state_class: ""
uom: ""
scale: 1
rule: 1
registers: [0x0202]
lookup:
- key: 0
value: "No error"
- key: 1
value: "ID17 Grid current sampling error"
- key: 2
value: "ID18 DCI sampling error"
- key: 4
value: "ID19 Grid voltage sampling error"
- key: 8
value: "ID20 GFCI device sampling error"
- key: 16
value: "ID21 Main chip fault"
- key: 32
value: "ID22 Hardware auxiliary power fault"
- key: 64
value: "ID23 Bus voltage zero fault"
- key: 128
value: "ID24 Output current not balanced"
- key: 256
value: "ID25 Bus under voltage protection"
- key: 512
value: "ID26 Bus over voltage protection"
- key: 1024
value: "ID27 Bus voltage unbalanced"
- key: 2048
value: "ID28 DCI is too high"
- key: 4096
value: "ID29 Grid current is too high"
- key: 8192
value: "ID30 Input current is too high"
- key: 16384
value: "ID31"
- key: 32768
value: "ID32"
icon: 'mdi:wrench'
- name: "Fault 3"
class: ""
state_class: ""
uom: ""
scale: 1
rule: 1
registers: [0x0203]
lookup:
- key: 0
value: "No error"
- key: 1
value: "ID33 Reserved"
- key: 2
value: "ID34 Reserved"
- key: 4
value: "ID35 Reserved"
- key: 8
value: "ID36 Reserved"
- key: 16
value: "ID37 Reserved"
- key: 32
value: "ID38 Reserved"
- key: 64
value: "ID39 Reserved"
- key: 128
value: "ID40 Reserved"
- key: 256
value: "ID41 Reserved"
- key: 512
value: "ID42 Reserved"
- key: 1024
value: "ID43 Reserved"
- key: 2048
value: "ID44 Reserved"
- key: 4096
value: "ID45 Reserved"
- key: 8192
value: "ID46 Reserved"
- key: 16384
value: "ID47 Reserved"
- key: 32768
value: "ID48 Reserved"
icon: 'mdi:wrench'
- name: "Fault 4"
class: ""
state_class: ""
uom: ""
scale: 1
rule: 1
registers: [0x0204]
lookup:
- key: 0
value: "No error"
- key: 1
value: "ID49 Grid voltage sampling value between master and slave DSP vary widely"
- key: 2
value: "ID50 Grid frequency sampling value between master and slave DSP vary widely"
- key: 4
value: "ID51 DCI sampling value between master and slave DSP vary widely"
- key: 8
value: "ID52 GFCI sampling value between master and slave DSP vary widely"
- key: 16
value: "ID53 Communication failure between master and slave DSP failure"
- key: 32
value: "ID53 Communication failure between slave and communication board"
- key: 64
value: "ID55 Relay fault"
- key: 128
value: "ID56 Insulation resistance between PV array and the earth is too low"
- key: 256
value: "ID57 Inverter temp is too high"
- key: 512
value: "ID58 Boost temp is too high"
- key: 1024
value: "ID59 Environment temp is too high"
- key: 2048
value: "ID60 Brak podłączenie falownika do kabla PE"
- key: 4096
value: "ID61 Reserved"
- key: 8192
value: "ID62 Reserved"
- key: 16384
value: "ID63 Reserved"
- key: 32768
value: "ID64 Reserved"
icon: 'mdi:wrench'
- name: "Fault 5"
class: ""
state_class: ""
uom: ""
scale: 1
rule: 1
registers: [0x0205]
lookup:
- key: 0
value: "No error"
- key: 1
value: "ID65 Grid current is too high and causes unrecoverable fault"
- key: 2
value: "ID66 Bus voltage is too high and causes unrecoverable fault"
- key: 4
value: "ID67 Grid current is unbalanced and causes unrecoverable fault"
- key: 8
value: "ID68 Input current is unbalanced and causes unrecoverable fault"
- key: 16
value: "ID69 Bus voltage is unbalanced and causes unrecoverable fault"
- key: 32
value: "ID70 Grid current is too high and causes unrecoverable fault"
- key: 64
value: "ID65 PV Input Mode Configuration is wrong and causes unrecoverable fault"
- key: 128
value: "ID72 Reserved"
- key: 256
value: "ID73 Reserved"
- key: 512
value: "ID74 Input current is too high and causes unrecoverable fault"
- key: 1024
value: "ID75 Error reading from EEPROM"
- key: 2048
value: "ID76 Error writing to EEPROM"
- key: 4096
value: "ID77 Relay fauilure causes unrecoverable fault"
- key: 8192
value: "ID78 Reserved"
- key: 16384
value: "ID79 Reserved"
- key: 32768
value: "ID80 Reserved"
icon: 'mdi:wrench'
- group: InverterInformation
items:
- name: "Production Code"
class: ""
state_class: ""
uom: ""
scale: 1
rule: 1
registers: [0x2000]
- name: "Serial Number"
class: ""
state_class: ""
uom: ""
scale: 1
rule: 5
registers: [0x2001,0x2002,0x2003,0x2004,0x2005,0x2006,0x2007]
isstr: true
- name: "Software Version"
class: ""
state_class: ""
uom: ""
scale: 1
rule: 5
registers: [0x2008,0x2009]
isstr: true
- name: "Hardware Version"
class: ""
state_class: ""
uom: ""
scale: 1
rule: 5
registers: [0x200A,0x200B]
isstr: true

View File

@ -0,0 +1,204 @@
# Solis Single Phase Inverter
# 1P8K-5G
# Modbus information derived by test and comparing to Solis Cloud
# Gedger V.0.1 May 2022
#
requests:
- start: 2999
end: 3024
mb_functioncode: 0x04
- start: 3035
end: 3043
mb_functioncode: 0x04
- start: 3071
end: 3071
mb_functioncode: 0x04
parameters:
- group: InverterStatus
items:
- name: "Inverter Status"
class: ""
state_class: ""
uom: ""
scale: 1
rule: 6
registers: [3043]
icon: 'mdi:home-lightning-bolt'
- name: "Operating Status"
class: ""
state_class: ""
uom: ""
scale: 1
rule: 6
registers: [3071]
icon: 'mdi:home-lightning-bolt'
- name: "Inverter Temperature"
class: "temperature"
state_class: "measurement"
uom: "°C"
scale: 0.1
rule: 2
registers: [3041]
icon: 'mdi:thermometer'
# - name: "Inverter ID"
# class: ""
# state_class: ""
# uom: ""
# scale: 1
# rule: 5
# registers: [33004,33005,33006,33007,33008,33009,33010,33011,33012,33013,33014,33015,33016,33017,33018,33019]
# isstr: true
- name: "Product Model"
class: ""
state_class: ""
uom: ""
scale: 1
rule: 6
registers: [2999]
isstr: true
- name: "DSP Software Version"
class: ""
state_class: ""
uom: ""
scale: 1
rule: 6
registers: [3000]
isstr: true
- name: "LCD Software Version"
class: ""
state_class: ""
uom: ""
scale: 1
rule: 6
registers: [3001]
isstr: true
- group: InverterDC
items:
- name: "PV1 Voltage"
class: "voltage"
state_class: "measurement"
uom: "V"
scale: 0.1
rule: 1
registers: [3021]
icon: 'mdi:solar-power'
- name: "PV2 Voltage"
class: "voltage"
state_class: "measurement"
uom: "V"
scale: 0.1
rule: 1
registers: [3023]
icon: 'mdi:solar-power'
- name: "PV1 Current"
class: "current"
uom: "A"
scale: 0.1
rule: 1
registers: [3022]
icon: 'mdi:current-dc'
- name: "PV2 Current"
class: "current"
state_class: "measurement"
uom: "A"
scale: 0.1
rule: 1
registers: [3024]
icon: 'mdi:current-dc'
- name: "Total DC Power"
class: "power"
state_class: "measurement"
uom: "kW"
scale: 0.001
rule: 3
registers: [3007, 3006]
icon: 'mdi:solar-power'
- group: InverterAC
items:
- name: "Inverter AC Power"
class: "power"
state_class: "measurement"
uom: "kW"
scale: 0.001
rule: 3
registers: [3005, 3004]
icon: 'mdi:solar-power'
- name: "Inverter Voltage"
class: "voltage"
state_class: "measurement"
uom: "V"
scale: 0.1
rule: 1
registers: [3035]
icon: 'mdi:transmission-tower'
- name: "Inverter Current"
class: "current"
state_class: "measurement"
uom: "A"
scale: 0.1
rule: 1
registers: [3038]
icon: 'mdi:current-ac'
- name: "Inverter Frequency"
class: "frequency"
state_class: "measurement"
uom: "Hz"
scale: 0.01
rule: 1
registers: [3042]
icon: 'mdi:sine-wave'
- group: Generation
items:
- name: "Daily Generation"
class: "energy"
state_class: "measurement"
uom: "kWh"
scale: 0.1
rule: 1
registers: [3014]
icon: 'mdi:solar-power'
- name: "Monthly Generation"
class: "energy"
state_class: "total_increasing"
uom: "kWh"
scale: 1
rule: 3
registers: [3011, 3010]
icon: 'mdi:solar-power'
- name: "Yearly Generation"
class: "energy"
state_class: "total_increasing"
uom: "kWh"
scale: 1
rule: 3
registers: [3017, 3016]
icon: 'mdi:solar-power'
- name: "Total Generation"
class: "energy"
state_class: "total_increasing"
uom: "kWh"
scale: 1
rule: 3
registers: [3009, 3008]
icon: 'mdi:solar-power'

View File

@ -0,0 +1,783 @@
# Solis Single Phase Hybrid
# RHI-(3-6)K-48ES-5G
# Modbus information retrieved from:
# https://www.scss.tcd.ie/coghlan/Elios4you/RS485_MODBUS-Hybrid-BACoghlan-201811228-1854.pdf
requests:
- start: 33029
end: 33095
mb_functioncode: 0x04
- start: 33116
end: 33179
mb_functioncode: 0x04
- start: 33206
end: 33282
mb_functioncode: 0x04
parameters:
- group: InverterStatus
items:
- name: "Inverter Status"
class: ""
state_class: ""
uom: ""
scale: 1
rule: 1
registers: [33095]
icon: 'mdi:home-lightning-bolt'
lookup:
- key: 0x0
value: "Waiting State"
- key: 0x1
value: "Open Loop Operation"
- key: 0x2
value: "Soft Start"
- key: 0x3
value: "On Grid/Generating"
- key: 0x1004
value: "Grid OverVoltage"
- key: 0x1010
value: "Grid UnderVoltage"
- key: 0x1012
value: "Grid OverFrequency"
- key: 0x1013
value: "Grid UnderFrequency"
- key: 0x1014
value: "Grid Imp too large"
- key: 0x1015
value: "No Grid"
- key: 0x1016
value: "Grid Imbalance"
- key: 0x1017
value: "Grid Freq Jitter"
- key: 0x1018
value: "Grid Overcurrent"
- key: 0x1019
value: "Grid Tracking Fault"
- key: 0x1020
value: "DC OverVoltage"
- key: 0x1021
value: "DC Bus Overvoltage"
- key: 0x1022
value: "DC Bus Uneven Voltage"
- key: 0x1024
value: "DC Bus Uneven Voltage2"
- key: 0x1025
value: "DC A path OverCurrent"
- key: 0x1026
value: "DC B path OverCurrent"
- key: 0x1027
value: "DC Input Disturbance"
- key: 0x1030
value: "Grid Disturbance"
- key: 0x1031
value: "DSP Initialization Protection "
- key: 0x1032
value: "Over Temp Protection"
- key: 0x1033
value: "PV Insulation Fault"
- key: 0x1034
value: "Leakage Current Protection"
- key: 0x1035
value: "Relay Detection Protection"
- key: 0x1036
value: "DSP_B Protection"
- key: 0x1037
value: "DC Component too Large"
- key: 0x1038
value: "12v UnderVoltage Protection"
- key: 0x1039
value: "Under Temperature Protection"
- key: 0x1040
value: "Arc Self-Test Protection"
- key: 0x1041
value: "Arc Protection"
- key: 0x1042
value: "DSP on-chip SRAM exception"
- key: 0x1043
value: "DSP on-chip FLASH exception"
- key: 0x1044
value: "DSP on-chip PC pointer is abnormal"
- key: 0x1045
value: "DSP key register exception"
- key: 0x1046
value: "Grid disturbance 02"
- key: 0x1047
value: "Grid current sampling abnormality"
- key: 0x1048
value: "IGBT overcurrent"
- key: 0x1050
value: "Network current transient overcurrent"
- key: 0x1051
value: "Battery overvoltage hardware failure"
- key: 0x1052
value: "LLC hardware overcurrent"
- key: 0x1053
value: "Battery overvoltage detection"
- key: 0x1054
value: "Battery undervoltage detection"
- key: 0x1055
value: "Battery no connected"
- key: 0x1056
value: "Bypass overvoltage fault"
- key: 0x1057
value: "Bypass overload fault"
- name: "Operating Status"
class: ""
state_class: ""
uom: ""
scale: 1
rule: 1
registers: [33121]
icon: 'mdi:home-lightning-bolt'
lookup:
- key: 0x701
value: "Normal Operation"
- key: 0x702
value: "Initial Standby"
- key: 0x704
value: "Control Shutdown"
- key: 0x708
value: "Downtime"
- key: 0x710
value: "Standby"
- key: 0x720
value: "Derating Operation"
- key: 0x740
value: "Limit Operation"
- key: 0x780
value: "Bypass Overload"
- name: "Grid Fault Status"
class: ""
state_class: ""
uom: ""
scale: 1
rule: 1
registers: [33116]
icon: 'mdi:alert'
lookup:
- key: 0x0000
value: "No Fault"
- key: 0x1
value: "No Grid"
- key: 0x2
value: "Grid OverVoltage"
- key: 0x4
value: "Grid UnderVoltage"
- key: 0x8
value: "Grid OverFrequency"
- key: 0x10
value: "Grid UnderFrequency"
- key: 0x20
value: "Grid Imbalance"
- key: 0x40
value: "Grid Frequncy Jitter"
- key: 0x80
value: "Grid Impedence too Large"
- key: 0x100
value: "Grid Tracking Fault"
- key: 0x200
value: "Meter Comm Failure"
- key: 0x400
value: "Failsafe"
- name: "Backup Load Fault Status"
class: ""
state_class: ""
uom: ""
scale: 1
rule: 1
registers: [33117]
icon: 'mdi:alert'
lookup:
- key: 0x0
value: "No Fault"
- key: 0x1
value: "Bypass OverVoltage Fault"
- key: 0x2
value: "Bypass Overload Fault"
- name: "Battery Fault Status"
class: ""
state_class: ""
uom: ""
scale: 1
rule: 1
registers: [33118]
icon: 'mdi:alert'
lookup:
- key: 0x0
value: "No Fault"
- key: 0x1
value: "Battery Not Connected"
- key: 0x2
value: "Battery OverVoltage Detection"
- key: 0x4
value: "Battery UnderVoltage Detection"
- name: "Fault Status 04 (Device)"
class: ""
state_class: ""
uom: ""
scale: 1
rule: 1
registers: [33119]
icon: 'mdi:alert'
lookup:
- key: 0x0000
value: "No Fault"
- key: 0x1
value: "DC OverVoltage"
- key: 0x2
value: "DC Bus OverVoltage"
- key: 0x4
value: "DC Bus Uneven Voltage"
- key: 0x8
value: "DC Bus UnderVoltage"
- key: 0x10
value: "DC Bus2 Uneven Voltage"
- key: 0x20
value: "DC A path OverCurrent"
- key: 0x40
value: "DC B path OverCurrent"
- key: 0x80
value: "DC Input Disturbance"
- key: 0x100
value: "Grid OverCurrent"
- key: 0x200
value: "IGBT OverCurrent"
- key: 0x400
value: "Grid Disturbance 2"
- key: 0x800
value: "Arc Self-Test Protection"
- key: 0x1000
value: "Arc Fault Reservation"
- key: 0x2000
value: "Grid Current Sample Abnormality"
- name: "Fault Status 05 (Device)"
class: ""
state_class: ""
uom: ""
scale: 1
rule: 1
registers: [33120]
icon: 'mdi:alert'
lookup:
- key: 0x0000
value: "No Fault"
- key: 0x1
value: "Grid Disturbance"
- key: 0x2
value: "DC Component Too Large"
- key: 0x4
value: "Over Temp Protection"
- key: 0x8
value: "Relay Detection Protection"
- key: 0x10
value: "Under Temp Protection"
- key: 0x20
value: "PV Insulation Fault"
- key: 0x40
value: "12V UnderVoltage Protection"
- key: 0x80
value: "Leakage Current Protection"
- key: 0x100
value: "Leakage Current Self-Test"
- key: 0x200
value: "DSP Initialization Protect"
- key: 0x400
value: "DSP B Protection"
- key: 0x800
value: "Battery Overvoltage H/W Failure"
- key: 0x1000
value: "LLC Hardware OverCurrent"
- key: 0x2000
value: "Network Side Transient OverCurrent"
- key: 0x4000
value: "CAN Communication Failed"
- key: 0x8000
value: "DSP Communication Failed"
- name: "Inverter Temperature"
class: "temperature"
state_class: "measurement"
uom: "°C"
scale: 0.1
rule: 2
registers: [33093]
icon: 'mdi:thermometer'
# Sensors below are outside of modbus request ranges.
# If enabling, ensure to amend the request start register.
#
# - name: "Inverter ID"
# class: ""
# state_class: ""
# uom: ""
# scale: 1
# rule: 5
# registers: [33004,33005,33006,33007,33008,33009,33010,33011,33012,33013,33014,33015,33016,33017,33018,33019]
# isstr: true
# - name: "Product Model"
# class: ""
# state_class: ""
# uom: ""
# scale: 1
# rule: 6
# registers: [33000]
# isstr: true
# - name: "DSP Software Version"
# class: ""
# state_class: ""
# uom: ""
# scale: 1
# rule: 6
# registers: [33001]
# isstr: true
# - name: "LCD Software Version"
# class: ""
# state_class: ""
# uom: ""
# scale: 1
# rule: 6
# registers: [33002]
# isstr: true
# - name: "Protocol Software Version"
# class: ""
# state_class: ""
# uom: ""
# scale: 1
# rule: 6
# registers: [33003]
# isstr: true
- name: "Storage Control Mode"
class: ""
state_class: ""
uom: ""
scale: 1
rule: 1
registers: [33132]
icon: 'mdi:battery-clock'
lookup:
- key: 0x21
value: "Spontaneous Mode"
- key: 0x22
value: "Optimized Revenue Mode"
- key: 0x23
value: "Charging from Grid"
- key: 0x24
value: "Off-Grid Storage Mode"
- key: 0x28
value: "Battery Wake-Up"
- group: InverterDC
items:
- name: "PV1 Voltage"
class: "voltage"
state_class: "measurement"
uom: "V"
scale: 0.1
rule: 1
registers: [33049]
icon: 'mdi:solar-power'
- name: "PV2 Voltage"
class: "voltage"
state_class: "measurement"
uom: "V"
scale: 0.1
rule: 1
registers: [33051]
icon: 'mdi:solar-power'
- name: "PV1 Current"
class: "current"
uom: "A"
scale: 0.1
rule: 1
registers: [33050]
icon: 'mdi:current-dc'
- name: "PV2 Current"
class: "current"
state_class: "measurement"
uom: "A"
scale: 0.1
rule: 1
registers: [33052]
icon: 'mdi:current-dc'
- name: "Inverter DC Power"
class: "power"
state_class: "measurement"
uom: "W"
scale: 1
rule: 3
registers: [33058,33057]
icon: 'mdi:solar-power'
- group: InverterAC
items:
- name: "Inverter AC Power"
class: "power"
state_class: "measurement"
uom: "W"
scale: 1
rule: 4
registers: [33152,33151]
icon: 'mdi:solar-power'
- name: "Inverter Voltage"
class: "voltage"
state_class: "measurement"
uom: "V"
scale: 0.1
rule: 1
registers: [33073]
icon: 'mdi:transmission-tower'
- name: "Inverter Current"
class: "current"
state_class: "measurement"
uom: "A"
scale: 0.1
rule: 1
registers: [33076]
icon: 'mdi:current-ac'
- name: "Inverter Active Power"
class: "power"
state_class: "measurement"
uom: "W"
scale: 1
rule: 4
registers: [33080,33079]
icon: 'mdi:transmission-tower'
# Inverter Reactive Power is defined as a signed 32 bit integer
# across 33082 and 33081, however the field appears to be only
# 20 bits wide i.e. the upper 12 bits are always zero.
# Define only the lower signed 16 bits for moment
- name: "Inverter Reactive Power"
class: "reactive_power"
state_class: "measurement"
uom: "var"
scale: 1
rule: 4
registers: [33082]
icon: 'mdi:transmission-tower'
- name: "Inverter Apparent Power"
class: "apparent_power"
state_class: "measurement"
uom: "VA"
scale: 1
rule: 4
registers: [33084,33083]
icon: 'mdi:transmission-tower'
- name: "Inverter Frequency"
class: "frequency"
state_class: "measurement"
uom: "Hz"
scale: 0.01
rule: 1
registers: [33094]
icon: 'mdi:sine-wave'
- group: Generation
items:
- name: "Daily Generation"
class: "energy"
state_class: "total_increasing"
uom: "kWh"
scale: 0.1
rule: 1
registers: [33035]
icon: 'mdi:solar-power'
- name: "Monthly Generation"
class: "energy"
state_class: "total_increasing"
uom: "kWh"
scale: 1
rule: 3
registers: [33032,33031]
icon: 'mdi:solar-power'
- name: "Yearly Generation"
class: "energy"
state_class: "total_increasing"
uom: "kWh"
scale: 1
rule: 3
registers: [33038,33037]
icon: 'mdi:solar-power'
- name: "Total Generation"
class: "energy"
state_class: "total_increasing"
uom: "kWh"
scale: 1
rule: 3
registers: [33030,33029]
icon: 'mdi:solar-power'
- group: Grid
items:
- name: "Meter Frequency"
class: "frequency"
state_class: "measurement"
uom: "Hz"
scale: 0.01
rule: 1
registers: [33282]
icon: 'mdi:sine-wave'
- name: "Meter Power Factor"
class: "power_factor"
state_class: "measurement"
uom: "%"
scale: 0.01
rule: 2
registers: [33281]
icon: 'mdi:transmission-tower'
- name: "Meter Voltage"
class: "voltage"
state_class: "measurement"
uom: "V"
scale: 0.1
rule: 1
registers: [33251]
icon: 'mdi:transmission-tower'
- name: "Meter Current"
class: "current"
state_class: "measurement"
uom: "A"
scale: 0.01
rule: 1
registers: [33252]
icon: 'mdi:current-ac'
- name: "Meter Active Power"
class: "power"
state_class: "measurement"
uom: "W"
scale: 1
rule: 4
registers: [33258,33257]
icon: 'mdi:transmission-tower'
- name: "Meter Reactive Power"
class: "reactive_power"
state_class: "measurement"
uom: "var"
scale: 1
rule: 4
registers: [33266,33265]
icon: 'mdi:transmission-tower'
- name: "Meter Apparent Power"
class: "apparent_power"
state_class: "measurement"
uom: "VA"
scale: 1
rule: 4
registers: [33274,33273]
icon: 'mdi:transmission-tower'
- name: "Daily Energy Imported"
class: "energy"
state_class: "total_increasing"
uom: "kWh"
scale: 0.1
rule: 1
registers: [33171]
icon: 'mdi:home-import-outline'
- name: "Total Energy Imported"
class: "energy"
state_class: "total_increasing"
uom: "kWh"
scale: 1
rule: 3
registers: [33170,33169]
icon: 'mdi:home-import-outline'
- name: "Daily Energy Exported"
class: "energy"
state_class: "total_increasing"
uom: "kWh"
scale: 0.1
rule: 1
registers: [33175]
icon: 'mdi:home-export-outline'
- name: "Total Energy Exported"
class: "energy"
state_class: "total_increasing"
uom: "kWh"
scale: 1
rule: 3
registers: [33174,33173]
icon: 'mdi:home-export-outline'
- group: Load
items:
- name: "House Load Power"
class: "power"
state_class: "measurement"
uom: "W"
scale: 1
rule: 1
registers: [33147]
icon: 'mdi:home-lightning-bolt'
- name: "Backup Load Power"
class: "power"
state_class: "measurement"
uom: "W"
scale: 1
rule: 1
registers: [33148]
icon: 'mdi:home-battery'
- name: "Daily House+Backup Load Consumption"
class: "energy"
state_class: "total_increasing"
uom: "kWh"
scale: 0.1
rule: 1
registers: [33179]
icon: 'mdi:lightning-bolt-outline'
- name: "Total House+Backup Load Consumption"
class: "energy"
state_class: "total_increasing"
uom: "kWh"
scale: 1
rule: 3
registers: [33178,33177]
icon: 'mdi:lightning-bolt-outline'
- group: Battery
items:
- name: "Battery Status"
class: ""
state_class: "measurement"
uom: ""
scale: 1
rule: 1
registers: [33135]
isstr: true
lookup:
- key: 0
value: "Charge"
- key: 1
value: "Discharge"
icon: 'mdi:battery'
- name: "Battery Power"
class: "power"
state_class: "measurement"
uom: "W"
scale: 1
rule: 4
registers: [33150,33149]
icon: 'mdi:battery-charging'
- name: "Battery SOC"
class: "battery"
state_class: "measurement"
uom: "%"
scale: 1
rule: 1
registers: [33139]
icon: 'mdi:battery'
- name: "Battery SOH"
class: "battery"
state_class: "measurement"
uom: "%"
scale: 1
rule: 1
registers: [33140]
icon: 'mdi:battery'
- name: "Battery Current"
class: "current"
state_class: "measurement"
uom: "A"
scale: 0.1
rule: 2
registers: [33134]
icon: 'mdi:current-dc'
- name: "Battery Voltage"
class: "voltage"
state_class: "measurement"
uom: "V"
scale: 0.1
rule: 1
registers: [33133]
icon: 'mdi:battery'
- name: "Today Battery Charge"
class: "energy"
state_class: "total_increasing"
uom: "kWh"
scale: 0.1
rule: 1
registers: [33163]
icon: 'mdi:battery-plus'
- name: "Today Battery Discharge"
class: "energy"
state_class: "total_increasing"
uom: "kWh"
scale: 0.1
rule: 1
registers: [33167]
icon: 'mdi:battery-minus'
- name: "Total Battery Charge"
class: "energy"
state_class: "total_increasing"
uom: "kWh"
scale: 1
rule: 3
registers: [33162,33161]
icon: 'mdi:battery-plus'
- name: "Total Battery Discharge"
class: "energy"
state_class: "total_increasing"
uom: "kWh"
scale: 1
rule: 3
registers: [33166,33165]
icon: 'mdi:battery-minus'
- name: "Battery Charge Current Limit"
class: "current"
state_class: "measurement"
uom: "A"
scale: 0.1
rule: 1
registers: [33206]
icon: 'mdi:battery-arrow-up'
- name: "Battery Discharge Current Limit"
class: "current"
state_class: "measurement"
uom: "A"
scale: 0.1
rule: 1
registers: [33207]
icon: 'mdi:battery-arrow-down'

View File

@ -0,0 +1,745 @@
# ZCS Azzurro 3-phase non-hybrid inverters
# with LSW-3 WiFi logger with SN 23xxxxxxxx and FW LSW3_15_270A_1.53:
# 3PH 3.3KTL-V3
# 3PH 4.4KTL-V3
# 3PH 5.5KTL-V3
# 3PH 6.6KTL-V3
# Not tested, but could probably work:
# ZCS Azzurro 3PH 8.8KTL-V3
# ZCS Azzurro 3PH 11KTL-V3
# ZCS Azzurro 3PH 12KTL-V3
# SOFAR Solar 4.4KTLX-G3
# SOFAR Solar 5.5KTLX-G3
# SOFAR Solar 6.6KTLX-G3
# SOFAR Solar 8.8KTLX-G3
# SOFAR Solar 11KTLX-G3
# SOFAR Solar 12KTLX-G3
requests:
- start: 0x0400
end: 0x042B
mb_functioncode: 0x03
- start: 0x0482
end: 0x04A4
mb_functioncode: 0x03
- start: 0x0582
end: 0x0589
mb_functioncode: 0x03
- start: 0x0682
end: 0x068B
mb_functioncode: 0x03
parameters:
- group: Solar
items:
- name: 'PV Generation today'
class: 'energy'
state_class: 'total'
uom: 'kWh'
scale: 0.01
rule: 1
registers: [0x0685, 0x0684]
icon: 'mdi:solar-power'
- name: 'PV Generation total'
class: 'energy'
state_class: 'total_increasing'
uom: 'kWh'
scale: 0.1
rule: 3
registers: [0x0687, 0x0686]
icon: 'mdi:solar-power'
- name: 'PV1 Power'
class: 'power'
state_class: 'measurement'
uom: 'W'
scale: 10
rule: 1
registers: [0x0586]
icon: 'mdi:solar-power'
- name: 'PV2 Power'
class: 'power'
state_class: 'measurement'
uom: 'W'
scale: 10
rule: 1
registers: [0x0589]
icon: 'mdi:solar-power'
- name: 'PV1 Voltage'
class: 'voltage'
state_class: 'measurement'
uom: 'V'
scale: 0.1
rule: 1
registers: [0x0584]
icon: 'mdi:solar-power'
- name: 'PV2 Voltage'
class: 'voltage'
state_class: 'measurement'
uom: 'V'
scale: 0.1
rule: 1
registers: [0x0587]
icon: 'mdi:solar-power'
- name: 'PV1 Current'
class: 'current'
state_class: 'measurement'
uom: 'A'
scale: 0.01
rule: 1
registers: [0x0585]
icon: 'mdi:solar-power'
- name: 'PV2 Current'
class: 'current'
state_class: 'measurement'
uom: 'A'
scale: 0.01
rule: 1
registers: [0x0588]
icon: 'mdi:solar-power'
- group: Grid
items:
- name: 'Grid Frequency'
class: 'frequency'
state_class: 'measurement'
uom: 'Hz'
scale: 0.01
rule: 1
registers: [0x0484]
icon: 'mdi:home-lightning-bolt'
- name: 'Active Power Output Total'
class: 'power'
state_class: 'measurement'
uom: 'W'
scale: 10
rule: 2
registers: [0x0485]
icon: 'mdi:home-lightning-bolt'
- group: Inverter
items:
- name: 'Inverter status'
class: ''
state_class: 'measurement'
uom: ''
scale: 1
rule: 1
registers: [0x0404]
lookup:
- key: 0
value: 'Stand-by'
- key: 1
value: 'Self-checking'
- key: 2
value: 'Normal'
- key: 3
value: 'FAULT'
- key: 4
value: 'Permanent'
icon: 'mdi:wrench'
- name: 'Module temperature'
class: 'temperature'
uom: '°C'
scale: 0.1
rule: 2
registers: [0x0683]
icon: 'mdi:thermometer'
- name: 'Ambient temperature'
class: 'temperature'
uom: '°C'
scale: 1
rule: 2
registers: [0x0418]
icon: 'mdi:thermometer'
- name: 'Radiator temperature'
class: 'temperature'
uom: '°C'
scale: 1
rule: 2
registers: [0x041A]
icon: 'mdi:thermometer'
- name: 'Insulation Resistance'
class: ''
state_class: 'measurement'
uom: 'Ω'
scale: 1
rule: 1
registers: [0x042B]
icon: 'mdi:omega'
- group: Alert
items:
- name: 'Alert'
class: ''
state_class: ''
uom: ''
scale: 1
rule: 6
registers:
[
0x0405,
0x0406,
0x0407,
0x0408,
0x0409,
0x040A,
0x040B,
0x040C,
0x040D,
0x040E,
0x040F,
0x0410,
]
- name: 'Fault 1'
class: ''
state_class: ''
uom: ''
scale: 1
rule: 1
registers: [0x0405]
isstr: true
icon: 'mdi:wrench'
lookup:
- key: 0
value: 'No error'
- key: 1
value: 'ID01 Grid Over Voltage Protection'
- key: 2
value: 'ID02 Grid Under Voltage Protection'
- key: 4
value: 'ID03 Grid Over Frequency Protection'
- key: 8
value: 'ID04 Grid Under Frequency Protection'
- key: 16
value: 'ID05 Leakage current fault'
- key: 32
value: 'ID06 High penetration error'
- key: 64
value: 'ID07 Low penetration error'
- key: 128
value: 'ID08 Islanding error'
- key: 256
value: 'ID09 Grid voltage transient value overvoltage 1'
- key: 512
value: 'ID10 Grid voltage transient value overvoltage 2'
- key: 1024
value: 'ID11 Grid line voltage error'
- key: 2048
value: 'ID12 Inverter voltage error'
- key: 4096
value: 'ID13 Anti-backflow overload'
- key: 8192
value: 'ID14'
- key: 16384
value: 'ID15'
- key: 32768
value: 'ID16'
- name: 'Fault 2'
class: ''
state_class: ''
uom: ''
scale: 1
rule: 1
icon: 'mdi:wrench'
isstr: true
registers: [0x0406]
lookup:
- key: 0
value: 'No error'
- key: 1
value: 'ID17 Grid current sampling error'
- key: 2
value: 'ID18 Grid current DC component sampling error (AC side)'
- key: 4
value: 'ID19 Grid voltage sampling error (DC side)'
- key: 8
value: 'ID20 Grid voltage sampling error (AC side)'
- key: 16
value: 'ID21 Leakage current sampling error (DC side)'
- key: 32
value: 'ID22 Leakage current sampling error (AC side)'
- key: 64
value: 'ID23 Load voltage DC component sampling error'
- key: 128
value: 'ID24 DC input current sampling error'
- key: 256
value: 'ID25 DC component sampling error of grid current (DC side)'
- key: 512
value: 'ID26 DC input branch current sampling error'
- key: 1024
value: 'ID27'
- key: 2048
value: 'ID28'
- key: 4096
value: 'ID29 Leakage current consistency error'
- key: 8192
value: 'ID30 Grid voltage consistency error'
- key: 16384
value: 'ID31 DCI consistency error'
- key: 32768
value: 'ID32'
- name: 'Fault 3'
class: ''
state_class: ''
uom: ''
scale: 1
rule: 1
icon: 'mdi:wrench'
isstr: true
registers: [0x0407]
lookup:
- key: 0
value: 'No error'
- key: 1
value: 'ID033 SPI communication error (DC side)'
- key: 2
value: 'ID034 SPI communication error (AC side)'
- key: 4
value: 'ID035 Chip error (DC side)'
- key: 8
value: 'ID036 Chip error (AC side)'
- key: 16
value: 'ID037 Auxiliary power error'
- key: 32
value: 'ID038 Inverter soft start failure'
- key: 64
value: 'ID039 '
- key: 128
value: 'ID040 '
- key: 256
value: 'ID041 Relay detection failure'
- key: 512
value: 'ID042 Low insulation impedance'
- key: 1024
value: 'ID043 Grounding error'
- key: 2048
value: 'ID044 Input mode setting error'
- key: 4096
value: 'ID045 CT error'
- key: 8192
value: 'ID046 Input reversal error'
- key: 16384
value: 'ID047 Parallel error'
- key: 32768
value: 'ID048 Serial number error'
- name: 'Fault 4'
class: ''
state_class: ''
uom: ''
scale: 1
rule: 1
icon: 'mdi:wrench'
registers: [0x0408]
isstr: true
lookup:
- key: 0
value: 'No error'
- key: 1
value: 'ID049 Battery temperature protection'
- key: 2
value: 'ID050 Heat sink 1 temperature protection'
- key: 4
value: 'ID051 Heater 2 temperature protection'
- key: 8
value: 'ID052 Heater 3 temperature protection'
- key: 16
value: 'ID053 Heatsink 4 temperature protection'
- key: 32
value: 'ID054 Heatsink 5 temperature protection'
- key: 64
value: 'ID055 Radiator 6 temperature protection'
- key: 128
value: 'ID056 '
- key: 256
value: 'ID057 Ambient temperature 1 protection'
- key: 512
value: 'ID058 Ambient temperature 2 protection'
- key: 1024
value: 'ID059 Module 1 temperature protection'
- key: 2048
value: 'ID060 Module 2 temperature protection'
- key: 4096
value: 'ID061 Module 3 temperature protection'
- key: 8192
value: 'ID062 Module temperature difference is too large'
- key: 16384
value: 'ID063 '
- key: 32768
value: 'ID064 '
- name: 'Fault 5'
class: ''
state_class: ''
uom: ''
scale: 1
rule: 1
icon: 'mdi:wrench'
registers: [0x0409]
isstr: true
lookup:
- key: 0
value: 'No error'
- key: 1
value: 'ID065 Bus voltage RMS unbalance'
- key: 2
value: 'ID066 Bus voltage transient value unbalance'
- key: 4
value: 'ID067 Undervoltage of busbar during grid connection'
- key: 8
value: 'ID068 Bus bar low voltage'
- key: 16
value: 'ID069 PV overvoltage'
- key: 32
value: 'ID070 Battery over-voltage'
- key: 64
value: 'ID071 LLCBus overvoltage protection'
- key: 128
value: 'ID072 Inverter bus voltage RMS software overvoltage'
- key: 256
value: 'ID073 Inverter bus voltage transient value software overvoltage'
- key: 512
value: 'ID074 Flying Cross Capacitor Overvoltage Protection'
- key: 1024
value: 'ID075 Flying Cross capacitor undervoltage protection'
- key: 2048
value: 'ID076 '
- key: 4096
value: 'ID077 '
- key: 8192
value: 'ID078 '
- key: 16384
value: 'ID079 '
- key: 32768
value: 'ID080 '
- name: 'Fault 6'
class: ''
state_class: ''
uom: ''
scale: 1
rule: 1
icon: 'mdi:wrench'
isstr: true
registers: [0x040A]
lookup:
- key: 0
value: 'No error'
- key: 1
value: 'ID081 Battery overcurrent software protection'
- key: 2
value: 'ID082 Dci overcurrent protection'
- key: 4
value: 'ID083 Output transient current protection'
- key: 8
value: 'ID084 BuckBoost software overcurrent'
- key: 16
value: 'ID085 Output RMS current protection'
- key: 32
value: 'ID086 PV instantaneous current overcurrent software protection'
- key: 64
value: 'ID087 PV parallel uneven current'
- key: 128
value: 'ID088 Output current unbalance'
- key: 256
value: 'ID089 PV software overcurrent protection'
- key: 512
value: 'ID090 Balanced circuit overcurrent protection'
- key: 1024
value: 'ID091 Resonance protection'
- key: 2048
value: 'ID092 '
- key: 4096
value: 'ID093 '
- key: 8192
value: 'ID094 '
- key: 16384
value: 'ID095 '
- key: 32768
value: 'ID096 '
- name: 'Fault 7'
class: ''
state_class: ''
uom: ''
scale: 1
rule: 1
icon: 'mdi:wrench'
isstr: true
registers: [0x040B]
lookup:
- key: 0
value: 'No error'
- key: 1
value: 'ID097 LLC bus hardware overvoltage'
- key: 2
value: 'ID098 Inverter bus hardware overvoltage'
- key: 4
value: 'ID099 BuckBoost hardware overcurrent'
- key: 8
value: 'ID100 Battery hardware overcurrent'
- key: 16
value: 'ID101 '
- key: 32
value: 'ID102 PV hardware overcurrent'
- key: 64
value: 'ID103 AC output hardware overcurrent'
- key: 128
value: 'ID104 '
- key: 256
value: 'ID105 Power meter error'
- key: 512
value: 'ID106 Serial number model error'
- key: 1024
value: 'ID107 '
- key: 2048
value: 'ID108 '
- key: 4096
value: 'ID109 '
- key: 8192
value: 'ID110 Overload protection 1'
- key: 16384
value: 'ID111 Overload protection 2'
- key: 32768
value: 'ID112 Overload protection 3'
- name: 'Fault 8'
class: ''
state_class: ''
uom: ''
scale: 1
rule: 1
icon: 'mdi:wrench'
registers: [0x040C]
isstr: true
lookup:
- key: 0
value: 'No error'
- key: 1
value: 'ID113 Overtemperature derating'
- key: 2
value: 'ID114 Frequency down load'
- key: 4
value: 'ID115 Frequency loading'
- key: 8
value: 'ID116 Voltage down load'
- key: 16
value: 'ID117 Voltage loading'
- key: 32
value: 'ID118 '
- key: 64
value: 'ID119 '
- key: 128
value: 'ID120 '
- key: 256
value: 'ID121 Lightning protection failure (DC)'
- key: 512
value: 'ID122 Lightning protection failure (AC)'
- key: 1024
value: 'ID123 '
- key: 2048
value: 'ID124 Battery low voltage protection'
- key: 4096
value: 'ID125 Battery low voltage shutdown'
- key: 8192
value: 'ID126 Battery low voltage pre-alarm'
- key: 16384
value: 'ID127 '
- key: 32768
value: 'ID128 '
- name: 'Fault 9'
class: ''
state_class: ''
uom: ''
scale: 1
rule: 1
icon: 'mdi:wrench'
isstr: true
registers: [0x040D]
lookup:
- key: 0
value: 'No error'
- key: 1
value: 'ID129 Output hardware overcurrent permanent fault'
- key: 2
value: 'ID130 Bus overvoltage permanent fault'
- key: 4
value: 'ID131 Bus hardware over-voltage permanent fault'
- key: 8
value: 'ID132 PV uneven flow permanent fault'
- key: 16
value: 'ID133 Battery overcurrent permanent fault in EPS mode'
- key: 32
value: 'ID134 Output transient overcurrent permanent fault'
- key: 64
value: 'ID135 Output current unbalance permanent fault'
- key: 128
value: 'ID136 '
- key: 256
value: 'ID137 Input mode setting error permanent fault'
- key: 512
value: 'ID138 Input overcurrent permanent fault'
- key: 1024
value: 'ID139 Input hardware overcurrent permanent fault'
- key: 2048
value: 'ID140 Relay permanent fault'
- key: 4096
value: 'ID141 Bus unbalance permanent fault'
- key: 8192
value: 'ID142 Lightning protection permanent fault - DC side'
- key: 16384
value: 'ID143 Lightning protection permanent fault - AC side'
- key: 32768
value: 'ID144 '
- name: 'Fault 10'
class: ''
state_class: ''
uom: ''
scale: 1
rule: 1
icon: 'mdi:wrench'
isstr: true
registers: [0x040E]
lookup:
- key: 0
value: 'No error'
- key: 1
value: 'ID145 USB fault'
- key: 2
value: 'ID146 WIFI fault'
- key: 4
value: 'ID147 Bluetooth fault'
- key: 8
value: 'ID148 RTC clock fault'
- key: 16
value: 'ID149 Communication board EEPROM error'
- key: 32
value: 'ID150 Communication board FLASH error'
- key: 64
value: 'ID151 '
- key: 128
value: 'ID152 Safety regulation version error'
- key: 256
value: 'ID153 SCI communication error (DC side)'
- key: 512
value: 'ID154 SCI communication error (AC side)'
- key: 1024
value: 'ID155 SCI communication error (convergence board side)'
- key: 2048
value: 'ID156 Software version inconsistency'
- key: 4096
value: 'ID157 Lithium battery 1 communication error'
- key: 8192
value: 'ID158 Li-ion battery 2 communication error'
- key: 16384
value: 'ID159 Lithium battery 3 communication error'
- key: 32768
value: 'ID160 Lithium battery 4 communication failure'
- name: 'Fault 11'
class: ''
state_class: ''
uom: ''
scale: 1
rule: 1
icon: 'mdi:wrench'
registers: [0x040F]
isstr: true
lookup:
- key: 0
value: 'No error'
- key: 1
value: 'ID161 Forced shutdown'
- key: 2
value: 'ID162 Remote shutdown'
- key: 4
value: 'ID163 Drms0 shutdown'
- key: 8
value: 'ID164 '
- key: 16
value: 'ID165 Remote down load'
- key: 32
value: 'ID166 Logic interface down load'
- key: 64
value: 'ID167 Anti-Reverse Flow Downgrade'
- key: 128
value: 'ID168 '
- key: 256
value: 'ID169 Fan 1 failure'
- key: 512
value: 'ID170 Fan 2 failure'
- key: 1024
value: 'ID171 Fan 3 failure'
- key: 2048
value: 'ID172 Fan 4 failure'
- key: 4096
value: 'ID173 Fan 5 failure'
- key: 8192
value: 'ID174 Fan 6 failure'
- key: 16384
value: 'ID175 Fan 7 fault'
- key: 32768
value: 'ID176 Meter communication failure'
- name: 'Fault 12'
class: ''
state_class: ''
uom: ''
scale: 1
rule: 1
icon: 'mdi:wrench'
registers: [0x0410]
isstr: true
lookup:
- key: 0
value: 'No error'
- key: 1
value: 'ID177 BMS over-voltage alarm'
- key: 2
value: 'ID178 BMS undervoltage alarm'
- key: 4
value: 'ID179 BMS high temperature alarm'
- key: 8
value: 'ID180 BMS low temperature alarm'
- key: 16
value: 'ID181 BMS charge/discharge overload alarm'
- key: 32
value: 'ID182 BMS short circuit alarm'
- key: 64
value: 'ID183 BMS version inconsistency'
- key: 128
value: 'ID184 BMS CAN version inconsistency'
- key: 256
value: 'ID185 BMS CAN version is too low'
- key: 512
value: 'ID186 '
- key: 1024
value: 'ID187 '
- key: 2048
value: 'ID188 '
- key: 4096
value: 'ID189 Arc device communication failure'
- key: 8192
value: 'ID190 DC arc alarm fault'
- key: 16384
value: 'ID191 PID repair failed'
- key: 32768
value: 'ID192 PLC module heartbeat loss'

View File

@ -0,0 +1,109 @@
/**
* Copyright (c) 2010-2024 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.solarman.internal;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import java.io.IOException;
import java.net.URL;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Enumeration;
import java.util.List;
import java.util.Objects;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.junit.jupiter.api.Test;
import org.openhab.binding.solarman.internal.defmodel.InverterDefinition;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* @author Catalin Sanda - Initial contribution
*/
@NonNullByDefault
public class DefinitionParserTest {
private final Logger logger = LoggerFactory.getLogger(DefinitionParserTest.class);
private final DefinitionParser definitionParser = new DefinitionParser();
@Test
void testInverterDefinitionsCanBeLoaded() throws IOException {
List<String> yamlFiles = scanForYamlFiles("definitions");
List<String> definitionIds = extractDefinitionIdFromYamlFiles(yamlFiles);
assertFalse(definitionIds.isEmpty());
definitionIds.forEach(definitionId -> {
@Nullable
InverterDefinition inverterDefinition = definitionParser.parseDefinition(definitionId);
assertNotNull(inverterDefinition);
});
}
public static List<String> extractDefinitionIdFromYamlFiles(List<String> yamlFiles) {
Pattern pattern = Pattern.compile("definitions/(.*)\\.yaml");
return yamlFiles.stream().map(file -> {
Matcher matcher = pattern.matcher(file);
return matcher.matches() ? matcher.group(1) : file;
}).collect(Collectors.toList());
}
public List<String> scanForYamlFiles(String directoryPath) throws IOException {
List<String> yamlFiles = new ArrayList<>();
ClassLoader classLoader = Objects.requireNonNull(DefinitionParserTest.class.getClassLoader());
Enumeration<URL> resources = classLoader.getResources(directoryPath);
Collections.list(resources).stream().flatMap(resource -> {
try {
if (resource.getProtocol().equals("jar")) {
String path = resource.getPath();
String jarPath = path.substring(5, path.indexOf("!"));
try (JarFile jarFile = new JarFile(jarPath)) {
return jarFile.stream()
.filter(e -> e.getName().startsWith(directoryPath) && e.getName().endsWith(".yaml"))
.map(JarEntry::getName);
}
} else if (resource.getProtocol().equals("file")) {
return scanDirectory(directoryPath).stream();
}
} catch (IOException e) {
logger.error(e.getMessage(), e);
}
return Stream.empty();
}).forEach(yamlFiles::add);
return yamlFiles;
}
private static List<String> scanDirectory(String directoryPath) throws IOException {
URL url = Objects.requireNonNull(DefinitionParserTest.class.getClassLoader()).getResource(directoryPath);
if (url == null) {
throw new IllegalArgumentException("Invalid directory path: " + directoryPath);
}
String[] files = new java.io.File(url.getPath()).list((dir, name) -> name.endsWith(".yaml"));
if (files != null) {
return Arrays.stream(files).map(file -> directoryPath + "/" + file).toList();
}
return Collections.emptyList();
}
}

View File

@ -0,0 +1,148 @@
/**
* Copyright (c) 2010-2024 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.solarman.internal.modbus;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.Mockito.any;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
import java.util.Map;
import javax.validation.constraints.NotNull;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.junit.jupiter.MockitoExtension;
import org.openhab.binding.solarman.internal.SolarmanLoggerConfiguration;
import org.openhab.binding.solarman.internal.modbus.exception.SolarmanException;
/**
* @author Catalin Sanda - Initial contribution
*/
@ExtendWith(MockitoExtension.class)
@NonNullByDefault
class SolarmanV5ProtocolTest {
SolarmanLoggerConnection solarmanLoggerConnection = (@NotNull SolarmanLoggerConnection) mock(
SolarmanLoggerConnection.class);
private SolarmanLoggerConfiguration loggerConfiguration = new SolarmanLoggerConfiguration("192.168.1.1", 8899,
"1234567890", "sg04lp3", 60, null);
private SolarmanV5Protocol solarmanV5Protocol = new SolarmanV5Protocol(loggerConfiguration);
@Test
void testbuildSolarmanV5Frame() {
byte[] requestFrame = solarmanV5Protocol.buildSolarmanV5Frame((byte) 0x03, 0x0000, 0x0020);
byte[] expectedFrame = { (byte) 0xA5, (byte) 0x17, (byte) 0x00, (byte) 0x10, (byte) 0x45, (byte) 0x00,
(byte) 0x00, (byte) 0xD2, (byte) 0x02, (byte) 0x96, (byte) 0x49, (byte) 0x02, (byte) 0x00, (byte) 0x00,
(byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,
(byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x01, (byte) 0x03, (byte) 0x00, (byte) 0x00,
(byte) 0x00, (byte) 0x21, (byte) 0x85, (byte) 0xD2, (byte) 0x9D, (byte) 0x15 };
assertArrayEquals(requestFrame, expectedFrame);
}
@Test
void testReadRegister0x01() throws SolarmanException {
// given
when(solarmanLoggerConnection.sendRequest(any())).thenReturn(
hexStringToByteArray("a5000000000000000000000000000000000000000000000000010301000ac84300000015"));
// when
Map<Integer, byte[]> regValues = solarmanV5Protocol.readRegisters(solarmanLoggerConnection, (byte) 0x03, 1, 1);
// then
assertEquals(1, regValues.size());
assertTrue(regValues.containsKey(1));
assertEquals("000A", bytesToHex(regValues.get(1)));
}
@Test
void testReadRegisters0x02to0x03() throws SolarmanException {
// given
when(solarmanLoggerConnection.sendRequest(any())).thenReturn(
hexStringToByteArray("a5000000000000000000000000000000000000000000000000010302000a000b13f600000015"));
// when
Map<Integer, byte[]> regValues = solarmanV5Protocol.readRegisters(solarmanLoggerConnection, (byte) 0x03, 2, 3);
// then
assertEquals(2, regValues.size());
assertTrue(regValues.containsKey(2));
assertTrue(regValues.containsKey(3));
assertEquals("000A", bytesToHex(regValues.get(2)));
assertEquals("000B", bytesToHex(regValues.get(3)));
}
@Test
void testReadRegisterSUN10KSG04LP3EUPart1() throws SolarmanException {
// given
when(solarmanLoggerConnection.sendRequest(any())).thenReturn(hexStringToByteArray(
"a53b0010150007482ee38d020121d0060091010000403e486301032800ffffff160a12162420ffffffffffffffffffffffffffffffffffff0001ffff0001ffff000003e81fa45115"));
// when
Map<Integer, byte[]> regValues = solarmanV5Protocol.readRegisters(solarmanLoggerConnection, (byte) 0x03, 0x3c,
0x4f);
// then
assertEquals(20, regValues.size());
assertTrue(regValues.containsKey(0x3c));
assertTrue(regValues.containsKey(0x4f));
assertEquals("00FF", bytesToHex(regValues.get(0x3c)));
assertEquals("03E8", bytesToHex(regValues.get(0x4f)));
}
@Test
void testReadRegisterSUN10KSG04LP3EUPart2() throws SolarmanException {
// given
when(solarmanLoggerConnection.sendRequest(any())).thenReturn(hexStringToByteArray(
"a5330010150008482ee38d020122d0060091010000403e486301032000010000ffffffffffff0001ffffffffffffffffffff0000ffff0011ffffffff3a005715"));
// when
Map<Integer, byte[]> regValues = solarmanV5Protocol.readRegisters(solarmanLoggerConnection, (byte) 0x03, 0x50,
0x5f);
// then
assertEquals(16, regValues.size());
assertTrue(regValues.containsKey(0x50));
assertTrue(regValues.containsKey(0x5f));
assertEquals("0001", bytesToHex(regValues.get(0x50)));
assertEquals("FFFF", bytesToHex(regValues.get(0x5f)));
}
private static byte[] hexStringToByteArray(String s) {
int len = s.length();
byte[] data = new byte[len / 2];
for (int i = 0; i < len; i += 2) {
data[i / 2] = (byte) ((Character.digit(s.charAt(i), 16) << 4) + Character.digit(s.charAt(i + 1), 16));
}
return data;
}
@Nullable
private static String bytesToHex(byte @Nullable [] bytes) {
if (bytes == null) {
return null;
}
StringBuilder sb = new StringBuilder();
for (byte b : bytes) {
sb.append(String.format("%02X", b));
}
return sb.toString();
}
}

View File

@ -380,6 +380,7 @@
<module>org.openhab.binding.solaredge</module>
<module>org.openhab.binding.solarforecast</module>
<module>org.openhab.binding.solarlog</module>
<module>org.openhab.binding.solarman</module>
<module>org.openhab.binding.solarmax</module>
<module>org.openhab.binding.solarwatt</module>
<module>org.openhab.binding.solax</module>