[growatt] Binding for Growatt solar inverters (#15120)

* [growatt] initial contribution

Signed-off-by: Andrew Fiddian-Green <software@whitebear.ch>
This commit is contained in:
Andrew Fiddian-Green 2024-01-20 13:25:07 +00:00 committed by GitHub
parent 9e1f87db86
commit 6f7b5b5f31
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
35 changed files with 4331 additions and 1 deletions

View File

@ -130,6 +130,7 @@
/bundles/org.openhab.binding.gree/ @markus7017
/bundles/org.openhab.binding.groheondus/ @FlorianSW
/bundles/org.openhab.binding.groupepsa/ @arjanmels
/bundles/org.openhab.binding.growatt/ @andrewfg
/bundles/org.openhab.binding.guntamatic/ @MikeTheTux
/bundles/org.openhab.binding.haassohnpelletstove/ @chingon007
/bundles/org.openhab.binding.harmonyhub/ @digitaldan

View File

@ -641,6 +641,11 @@
<artifactId>org.openhab.binding.groupepsa</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.openhab.addons.bundles</groupId>
<artifactId>org.openhab.binding.growatt</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.openhab.addons.bundles</groupId>
<artifactId>org.openhab.binding.guntamatic</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,349 @@
# Growatt Binding
This binding supports the integration of Growatt solar inverters.
It depends on the independent [Grott](https://github.com/johanmeijer/grott#the-growatt-inverter-monitor) proxy server application.
This intercepts the logging data that the Growatt inverter data logger normally sends directly to the Growatt cloud server.
It sends the original (encoded) data onwards to the cloud server (so the cloud server will not notice anything different).
But it also sends a (decoded) copy to openHAB as well.
## Supported Things
The binding supports two types of things:
- `bridge`: The bridge is the interface to the Grott application; it receives the data from all inverters.
- `inverter`: The inverter thing contains channels which are updated with solor production and consumption data.
## Discovery
There is no automatic discovery of the bridge.
However if a bridge exists and it receives inverter data, then a matching inverter thing is created in the Inbox.
## Thing Configuration
The `bridge` thing allows configuration of the user credentials, which are only required if you want to send inverter commands via the Growatt cloud server:
| Name | Type | Description | Advanced |Required |
|-----------|---------|------------------------------------------------------------------------------------------|----------|---------|
| userName | text | User name for the Growatt Shine app. Only needed if using [Rule Actions](#rule-actions) | yes | no |
| password | text | Password for the Growatt Shine app. Only needed if using [Rule Actions](#rule-actions) | yes | no |
The `inverter` thing requires configuration of its serial number resp. `deviceId`:
| Name | Type | Description | Required |
|-----------|---------|------------------------------------------------------------------------------------------|----------|
| deviceId | text | Device serial number or id as configured in the Growatt cloud and the Grott application. | yes |
## Channels
The `bridge` thing has no channels.
The `inverter` thing supports many possible channels relating to solar generation and consumption.
All channels are read-only.
Depending on the inverter model, and its configuration, not all of the channels will be present.
The list of all possible channels is as follows:
| Channel | Type | Description | Advanced |
|-------------------------------|---------------------------|------------------------------------------------------|----------|
| system-status | Number:Dimensionless | Inverter status code. | |
| pv1-voltage | Number:ElectricPotential | DC voltage from solar panel string #1. | yes |
| pv2-voltage | Number:ElectricPotential | DC voltage from solar panel string #2. | yes |
| pv1-current | Number:ElectricCurrent | DC current from solar panel string #1. | yes |
| pv2-current | Number:ElectricCurrent | DC current from solar panel string #2. | yes |
| pv-power | Number:Power | Total DC solar input power. | |
| pv1-power | Number:Power | DC power from solar panel string #1. | yes |
| pv2-power | Number:Power | DC power from solar panel string #2. | yes |
| grid-frequency | Number:Frequency | Frequency of the grid. | yes |
| grid-voltage-r | Number:ElectricPotential | Voltage of the grid (phase #R). | |
| grid-voltage-s | Number:ElectricPotential | Voltage of the grid phase #S. | yes |
| grid-voltage-t | Number:ElectricPotential | Voltage of the grid phase #T. | yes |
| grid-voltage-rs | Number:ElectricPotential | Voltage of the grid phases #RS. | yes |
| grid-voltage-st | Number:ElectricPotential | Voltage of the grid phases #ST. | yes |
| grid-voltage-tr | Number:ElectricPotential | Voltage of the grid phases #TR. | yes |
| inverter-current-r | Number:ElectricCurrent | AC current from inverter (phase #R). | yes |
| inverter-current-s | Number:ElectricCurrent | AC current from inverter phase #S. | yes |
| inverter-current-t | Number:ElectricCurrent | AC current from inverter phase #T. | yes |
| inverter-power | Number:Power | Total AC output power from inverter. | |
| inverter-power-r | Number:Power | AC power from inverter (phase #R). | |
| inverter-power-s | Number:Power | AC power from inverter phase #S. | yes |
| inverter-power-t | Number:Power | AC power from inverter phase #T. | yes |
| inverter-va | Number:Power | AC VA from inverter. | yes |
| export-power | Number:Power | Power exported to grid. | |
| export-power-r | Number:Power | Power exported to grid phase #R. | yes |
| export-power-s | Number:Power | Power exported to grid phase #S. | yes |
| export-power-t | Number:Power | Power exported to grid phase #T. | yes |
| import-power | Number:Power | Power imported from grid. | |
| import-power-r | Number:Power | Power imported from grid phase #R. | yes |
| import-power-s | Number:Power | Power imported from grid phase #S. | yes |
| import-power-t | Number:Power | Power imported from grid phase #T. | yes |
| load-power | Number:Power | Power supplied to load. | |
| load-power-r | Number:Power | Power supplied to load phase #R. | yes |
| load-power-s | Number:Power | Power supplied to load phase #S. | yes |
| load-power-t | Number:Power | Power supplied to load phase #T. | yes |
| charge-power | Number:Power | Battery charge power. | |
| charge-current | Number:ElectricCurrent | Battery charge current. | yes |
| discharge-power | Number:Power | Battery discharge power. | |
| discharge-va | Number:Power | Battery discharge VA. | yes |
| pv-energy-today | Number:Energy | DC energy collected by solar panels today. | |
| pv1-energy-today | Number:Energy | DC energy collected by solar panels string #1 today. | yes |
| pv2-energy-today | Number:Energy | DC energy collected by solar panels string #2 today. | yes |
| pv-energy-total | Number:Energy | Total DC energy collected by solar panels. | |
| pv1-energy-total | Number:Energy | Total DC energy collected by solar panels string #1. | yes |
| pv2-energy-total | Number:Energy | Total DC energy collected by solar panels string #2. | yes |
| inverter-energy-today | Number:Energy | AC energy produced by inverter today. | |
| inverter-energy-total | Number:Energy | Total AC energy produced by inverter. | |
| export-energy-today | Number:Energy | Energy exported today. | |
| export-energy-total | Number:Energy | Total energy exported. | |
| import-energy-today | Number:Energy | Energy imported today. | |
| import-energy-total | Number:Energy | Total energy imported. | |
| load-energy-today | Number:Energy | Energy supplied to load today. | |
| load-energy-total | Number:Energy | Total energy supplied to load. | |
| import-charge-energy-today | Number:Energy | Energy imported to charge battery today. | |
| import-charge-energy-total | Number:Energy | Total energy imported to charge battery. | |
| inverter-charge-energy-today | Number:Energy | Inverter energy to charge battery today. | |
| inverter-charge-energy-total | Number:Energy | Total inverter energy to charge battery. | |
| discharge-energy-today | Number:Energy | Energy consumed from battery. | |
| discharge-energy-total | Number:Energy | Total energy consumed from battery. | |
| total-work-time | Number:Time | Total work time of the system. | yes |
| p-bus-voltage | Number:ElectricPotential | P Bus voltage. | yes |
| n-bus-voltage | Number:ElectricPotential | N Bus voltage. | yes |
| sp-bus-voltage | Number:ElectricPotential | SP Bus voltage. | yes |
| pv-temperature | Number:Temperature | Temperature of the solar panels (string #1). | yes |
| pv-ipm-temperature | Number:Temperature | Temperature of the IPM. | yes |
| pv-boost-temperature | Number:Temperature | Boost temperature. | yes |
| temperature-4 | Number:Temperature | Temperature #4. | yes |
| pv2-temperature | Number:Temperature | Temperature of the solar panels (string #2). | yes |
| battery-type | Number:Dimensionless | Type code of the battery. | yes |
| battery-temperature | Number:Temperature | Battery temperature. | yes |
| battery-voltage | Number:ElectricPotential | Battery voltage. | yes |
| battery-display | Number:Dimensionless | Battery display code. | yes |
| battery-soc | Number:Dimensionless | Battery State of Charge percent. | yes |
| system-fault-0 | Number:Dimensionless | System fault code #0. | yes |
| system-fault-1 | Number:Dimensionless | System fault code #1. | yes |
| system-fault-2 | Number:Dimensionless | System fault code #2. | yes |
| system-fault-3 | Number:Dimensionless | System fault code #3. | yes |
| system-fault-4 | Number:Dimensionless | System fault code #4. | yes |
| system-fault-5 | Number:Dimensionless | System fault code #5. | yes |
| system-fault-6 | Number:Dimensionless | System fault code #6. | yes |
| system-fault-7 | Number:Dimensionless | System fault code #7. | yes |
| system-work-mode | Number:Dimensionless | System work mode code. | yes |
| sp-display-status | Number:Dimensionless | Solar panel display status code. | yes |
| constant-power-ok | Number:Dimensionless | Constant power OK code. | yes |
| load-percent | Number:Dimensionless | Percent of full load. | yes |
| rac | Number:Power | Reactive 'power' (var). | yes |
| erac-today | Number:Energy | Reactive 'energy' today (kvarh). | yes |
| erac-total | Number:Energy | Total reactive 'energy' (kvarh). | yes |
## Rule Actions
This binding includes rule actions, which allow you to setup programs for battery charging and discharging.
Each inverter thing has a separate actions instance, which can be retrieved as follows.
```php
val growattActions = getActions("growatt", "growatt:inverter:home:sph")
```
Where the first parameter must always be `growatt` and the second must be the full inverter thing UID.
Once the action instance has been retrieved, you can invoke the following method:
```php
growattActions.setupBatteryProgram(int programMode, @Nullable Integer powerLevel, @Nullable Integer stopSOC, @Nullable Boolean enableAcCharging, @Nullable String startTime, @Nullable String stopTime, @Nullable Boolean enableProgram)
```
The meaning of the method parameters is as follows:
| Parameter | Description |
|-------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------|
| programMode | The program mode to set i.e. 'Load First' (0), 'Battery First' (1), 'Grid First' (2). |
| powerLevel<sup>2)</sup> | The percentage rate of battery (dis-)charge e.g. 100 - in 'Battery First' mode => charge power, otherwise => discharge power. |
| stopSOC<sup>2)</sup> | The battery SOC (state of charge) percentage when the program shall stop e.g. 20 - in 'Battery First' mode => max. SOC, otherwise => min. SOC. |
| enableAcCharging<sup>2)</sup> | Allow the battery to be charged from the AC mains supply e.g. true, false. |
| startTime<sup>1,2)</sup> | String representation of the local time when the program `time segment` shall start e.g. "00:15" |
| stopTime<sup>1,2)</sup> | String representation of the local time when the program `time segment` shall stop e.g. "06:45" |
| enableProgram<sup>1,2)</sup> | Enable / disable the program `time segment` e.g. true, false |
Notes:
-1) ***WARNING*** inverters have different program `time segment`'s for each `programMode`.
To prevent unexpected results do not overlap the `time segment`'s.
-2) Depending on inverter type and `programMode` certain parameters may accept 'null' values.
The 'mix', 'sph' and 'spa' types set the battery program in a single command, so all parameters - except `enableAcCharging` - <u>**must**</u> be ***non-***'null'.
By contrast 'tlx' types set the battery program in up to four partial commands, and you may pass 'null' parameters in order to omit a partial command.
The permission for passing 'null' parameters, and the effect of such 'null' parameters, is shown in detail in the table below:
| Parameter | Permission for.. / effect of.. passing a 'null' parameter |
|------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------|
| programMode | Shall <u>**not**</u> be 'null' under any circumstance! |
| powerLevel | May be 'null' on 'tlx' inverters whereby the prior `programMode` / `powerLevel` continues to apply. |
| stopSOC | May be 'null' on 'tlx' inverters whereby the prior `programMode` / `stopSOC` continues to apply. |
| enableAcCharging | If 'null' the prior `enableAcCharging` (if any) continues to apply. Shall <u>**not**</u> be 'null' on 'mix' inverter 'Battery First' program. |
| startTime, stopTime, enableProgram | May be 'null' on 'tlx' inverters whereby the prior `programMode` / `time segment` continues to apply - note all 'null' resp. non-'null'. |
Example:
```php
rule "Setup Solar Battery Charging Program"
when
Time cron "0 10 0 ? * * *"
then
val growattActions = getActions("growatt", "growatt:inverter:home:ABCD1234") // thing UID
if (growattActions === null) {
logWarn("Rules", "growattActions is null")
} else {
// fixed algorithm parameters
val Integer programMode = 1 // 0 = Load First, 1 = Battery First, 2 = Grid First
val Integer powerLevel = 23 // percent
val Boolean enableAcCharging = true
val String startTime = "00:20"
val String stopTime = "07:00"
val Boolean enableProgram = true
// calculation intermediaries
val batteryFull = 6500.0 // Wh
val batteryMin = 500.0 // Wh
val daylightConsumption = 10000.0 // Wh
val maximumSOC = 100.0 // percent
val minimumSOC = 20.0 // percent
// calculate stop SOC based on weather forecast
val Double solarForecast = (ForecastSolar_PV_Whole_Site_Forecast_Today.state as QuantityType<Energy>).toUnit("Wh").doubleValue()
var Double targetSOC = (100.0 * (batteryMin + daylightConsumption - solarForecast)) / batteryFull
if (targetSOC > maximumSOC) {
targetSOC = maximumSOC
} else if (targetSOC < minimumSOC) {
targetSOC = minimumSOC
}
// convert to integer
val Integer stopSOC = targetSOC.intValue() // percent
logInfo("Rules", "Setup Charging Program:{solarForecast:" + solarForecast + "Wh, programMode:" + programMode + ", powerLevel:" + powerLevel + "%, stopSOC:" + stopSOC + "%, enableCharging:" + enableAcCharging + ", startTime:" + startTime + ", stopTime:" + stopTime + ", enableProgram:" + enableProgram +"}")
growattActions.setupBatteryProgram(programMode, powerLevel, stopSOC, enableAcCharging, startTime, stopTime, enableProgram)
}
end
```
## Full Example
### Example `.things` file
```java
Bridge growatt:bridge:home "Growattt Bridge" [userName="USERNAME", password="PASSWORD"] {
Thing inverter sph "Growatt SPH Inverter" [deviceId="INVERTERTID"]
}
```
### Example `.items` file
```java
Number:ElectricPotential Solar_String1_Voltage "Solar String #1 PV Voltage" {channel="growatt:inverter:home:sph:pv1-voltage"}
Number:ElectricCurrent Solar_String1_Current "Solar String #1 PV Current" {channel="growatt:inverter:home:sph:pv1-current"}
Number:Power Solar_String1_Power "Solar String #1 PV Power" {channel="growatt:inverter:home:sph:pv1-power"}
Number:Energy Solar_Output_Energy "Solar Output Energy Total" {channel="growatt:inverter:home:sph:pv-energy-total"}
```
Example using a transform profile to invert an item value:
```java
// charge item with positive value
Number:Power Charge_Power "Charge Power [%.0f W]" <energy> {channel="growatt:inverter:home:sph:charge-power"}
// discarge item with negative value
Number:Power Discharge_Power "Discharge Power [%.0f W]" <energy> {channel="growatt:inverter:home:sph:discharge-power" [ profile="transform:JS", toItemScript="| Quantity(input).multiply(-1).toString();" ] }
```
## Grott Application Installation and Setup
You can install the Grott application either on the same computer as openHAB or on another.
The following assumes you will be running it on the same computer.
The Grott application acts as a proxy server between your Growatt inverter and the Growatt cloud server.
It intercepts data packets between the inverter and the cloud server, and it sends a copy of the intercepted data also to openHAB.
**NOTE**: make sure that the Grott application is **FULLY OPERATIONAL** for your inverter **BEFORE** you create any things in openHAB!
Otherwise the binding might create a wrong (or even empty) list of channels for the inverter thing.
(Yet if you do make that mistake you can rectify it by deleting and recreating the thing).
You should configure the Grott application via its `grott.ini` file.
Configure Grott to match your inverter according to the [instructions](https://github.com/johanmeijer/grott#the-growatt-inverter-monitor).
### Install Python
If Python is not already installed on you computer, then install it first.
And install the following additional necessary python packages:
```bash
sudo pip3 install paho-mqtt
sudo pip3 install requests
```
### Install Grott
First install the Grott application and the Grott application extension files in a Grott specific home folder.
Note that Grott requires the `grottext.py` application extension in addition to the standard application files.
The installation is as follows:
- Create a 'home' sub-folder for Grott e.g. `/home/<username>/grott/`.
- Copy `grott.py`, `grottconf.py`, `grottdata.py`, `grottproxy.py`, `grottsniffer.py`, `grottserver.py` to the home folder.
- Copy `grottext.py` application extension to the home folder.
- Copy `grott.ini` configuration file to the home folder.
- Modify `grott.ini` to run in proxy mode; not in compatibility mode; show your inverter type; not run MQTT; not run PVOutput; enable the `grottext` extension; and set the openHAB `/growatt` servlet url.
A suggested Grott configuration for openHAB is as follows:
```php
[Generic]
mode = proxy
compat = False
invtype = sph // your inverter type
[MQTT]
nomqtt = True // disable mqtt
[PVOutput]
pvoutput = False // disable pvoutput
[extension] // enable the 'grottext' extension
extension = True
extname = grottext
extvar = {"url": "http://127.0.0.1:8080/growatt"} // or ip address of openHAB (if remote)
```
### Start Grott as a Service
Finally you should set your computer to starts the Grott application automatically as a service when your computer starts.
For Windows see wiki: https://github.com/johanmeijer/grott/wiki/Grott-as-a-service-(Windows)
For Linux see wiki: https://github.com/johanmeijer/grott/wiki/Grott-as-a-service-(Linux)
The service configuration for Linux is summarised below:
- Copy the `grott.service` file to the `/etc/systemd/system/` folder
- Modify `grott.service` to enter your user name; the Grott settings; the path to Python; and the path to the Grott application:
```php
[Service]
SyslogIdentifier=grott
User=<username> // your username
WorkingDirectory=/home/<username>/grott/ // your home grott folder
ExecStart=-/usr/bin/python3 -u /home/<username>/grott/grott.py -v // ditto
```
And finally enable the Grott service:
```bash
sudo systemctl enable grott
```
### Route Growatt Inverter Logging via Grott Proxy
Normally the Growatt inverter sends its logging data directly to port `5279` on the Growatt server at `server.growatt.com` (ip=47.91.67.66) on the cloud.
Grott is a proxy server that interposes itself beween the inverter and the cloud server.
i.e. it receives the inverter logging data and forwards it unchanged to the cloud server.
**WARNING**: make sure that Grott is running on a computer with a **STATIC IP ADDRESS** (and note this safely)!
Otherwise if the computer changes its ip address dynamically, it can no longer intercept the inverter data.
This means **YOU WILL NO LONGER BE ABLE TO RESET THE INVERTER** to its original settings!
You need to use the Growatt App to tell the inverter to send its logging data to the Grott proxy instead of to the cloud.
See wiki: https://github.com/johanmeijer/grott/wiki/Rerouting-Growatt-Wifi-TCPIP-data-via-your-Grott-Server for more information.

View File

@ -0,0 +1,17 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://maven.apache.org/POM/4.0.0"
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.2.0-SNAPSHOT</version>
</parent>
<artifactId>org.openhab.binding.growatt</artifactId>
<name>openHAB Add-ons :: Bundles :: Growatt Binding</name>
</project>

View File

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<features name="org.openhab.binding.growatt-${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-growatt" description="Growatt Binding" version="${project.version}">
<feature>openhab-runtime-base</feature>
<bundle start-level="80">mvn:org.openhab.addons.bundles/org.openhab.binding.growatt/${project.version}</bundle>
</feature>
</features>

View File

@ -0,0 +1,31 @@
/**
* 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.growatt.internal;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.core.thing.ThingTypeUID;
/**
* The {@link GrowattBindingConstants} class defines common constants, which are
* used across the whole binding.
*
* @author Andrew Fiddian-Green - Initial contribution
*/
@NonNullByDefault
public class GrowattBindingConstants {
public static final String BINDING_ID = "growatt";
public static final ThingTypeUID THING_TYPE_BRIDGE = new ThingTypeUID(BINDING_ID, "bridge");
public static final ThingTypeUID THING_TYPE_INVERTER = new ThingTypeUID(BINDING_ID, "inverter");
}

View File

@ -0,0 +1,196 @@
/**
* 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.growatt.internal;
import java.util.AbstractMap;
import java.util.Map;
import javax.measure.Unit;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.core.library.unit.SIUnits;
import org.openhab.core.library.unit.Units;
/**
* The {@link GrowattChannels} class defines the channel ids and respective UoM and scaling factors.
*
* @author Andrew Fiddian-Green - Initial contribution
*/
@NonNullByDefault
public class GrowattChannels {
/**
* Class encapsulating units of measure and scale information.
*/
public static class UoM {
public final Unit<?> units;
public final float divisor;
public UoM(Unit<?> units, float divisor) {
this.units = units;
this.divisor = divisor;
}
}
/**
* Map of the channel ids to their respective UoM and scaling factors
*/
private static final Map<String, UoM> CHANNEL_ID_UOM_MAP = Map.ofEntries(
// inverter state
new AbstractMap.SimpleEntry<String, UoM>("system-status", new UoM(Units.ONE, 1)),
// solar generation
new AbstractMap.SimpleEntry<String, UoM>("pv-power", new UoM(Units.WATT, 10)),
// electric data for strings #1 and #2
new AbstractMap.SimpleEntry<String, UoM>("pv1-voltage", new UoM(Units.VOLT, 10)),
new AbstractMap.SimpleEntry<String, UoM>("pv1-current", new UoM(Units.AMPERE, 10)),
new AbstractMap.SimpleEntry<String, UoM>("pv1-power", new UoM(Units.WATT, 10)),
new AbstractMap.SimpleEntry<String, UoM>("pv2-voltage", new UoM(Units.VOLT, 10)),
new AbstractMap.SimpleEntry<String, UoM>("pv2-current", new UoM(Units.AMPERE, 10)),
new AbstractMap.SimpleEntry<String, UoM>("pv2-power", new UoM(Units.WATT, 10)),
// grid electric data (1-phase resp. 3-phase)
new AbstractMap.SimpleEntry<String, UoM>("grid-frequency", new UoM(Units.HERTZ, 100)),
new AbstractMap.SimpleEntry<String, UoM>("grid-voltage-r", new UoM(Units.VOLT, 10)),
new AbstractMap.SimpleEntry<String, UoM>("grid-voltage-s", new UoM(Units.VOLT, 10)),
new AbstractMap.SimpleEntry<String, UoM>("grid-voltage-t", new UoM(Units.VOLT, 10)),
new AbstractMap.SimpleEntry<String, UoM>("grid-voltage-rs", new UoM(Units.VOLT, 10)),
new AbstractMap.SimpleEntry<String, UoM>("grid-voltage-st", new UoM(Units.VOLT, 10)),
new AbstractMap.SimpleEntry<String, UoM>("grid-voltage-tr", new UoM(Units.VOLT, 10)),
// inverter output
new AbstractMap.SimpleEntry<String, UoM>("inverter-current-r", new UoM(Units.AMPERE, 10)),
new AbstractMap.SimpleEntry<String, UoM>("inverter-current-s", new UoM(Units.AMPERE, 10)),
new AbstractMap.SimpleEntry<String, UoM>("inverter-current-t", new UoM(Units.AMPERE, 10)),
new AbstractMap.SimpleEntry<String, UoM>("inverter-power", new UoM(Units.WATT, 10)),
new AbstractMap.SimpleEntry<String, UoM>("inverter-power-r", new UoM(Units.WATT, 10)),
new AbstractMap.SimpleEntry<String, UoM>("inverter-power-s", new UoM(Units.WATT, 10)),
new AbstractMap.SimpleEntry<String, UoM>("inverter-power-t", new UoM(Units.WATT, 10)),
new AbstractMap.SimpleEntry<String, UoM>("inverter-va", new UoM(Units.VOLT_AMPERE, 10)),
// battery discharge / charge power
new AbstractMap.SimpleEntry<String, UoM>("charge-current", new UoM(Units.AMPERE, 10)),
new AbstractMap.SimpleEntry<String, UoM>("charge-power", new UoM(Units.WATT, 10)),
new AbstractMap.SimpleEntry<String, UoM>("discharge-power", new UoM(Units.WATT, 10)),
new AbstractMap.SimpleEntry<String, UoM>("discharge-va", new UoM(Units.VOLT_AMPERE, 10)),
// export power to grid
new AbstractMap.SimpleEntry<String, UoM>("export-power", new UoM(Units.WATT, 10)),
new AbstractMap.SimpleEntry<String, UoM>("export-power-r", new UoM(Units.WATT, 10)),
new AbstractMap.SimpleEntry<String, UoM>("export-power-s", new UoM(Units.WATT, 10)),
new AbstractMap.SimpleEntry<String, UoM>("export-power-t", new UoM(Units.WATT, 10)),
// power to user
new AbstractMap.SimpleEntry<String, UoM>("import-power", new UoM(Units.WATT, 10)),
new AbstractMap.SimpleEntry<String, UoM>("import-power-r", new UoM(Units.WATT, 10)),
new AbstractMap.SimpleEntry<String, UoM>("import-power-s", new UoM(Units.WATT, 10)),
new AbstractMap.SimpleEntry<String, UoM>("import-power-t", new UoM(Units.WATT, 10)),
// power to local
new AbstractMap.SimpleEntry<String, UoM>("load-power", new UoM(Units.WATT, 10)),
new AbstractMap.SimpleEntry<String, UoM>("load-power-r", new UoM(Units.WATT, 10)),
new AbstractMap.SimpleEntry<String, UoM>("load-power-s", new UoM(Units.WATT, 10)),
new AbstractMap.SimpleEntry<String, UoM>("load-power-t", new UoM(Units.WATT, 10)),
// inverter output energy
new AbstractMap.SimpleEntry<String, UoM>("inverter-energy-today", new UoM(Units.KILOWATT_HOUR, 10)),
new AbstractMap.SimpleEntry<String, UoM>("inverter-energy-total", new UoM(Units.KILOWATT_HOUR, 10)),
// solar DC input energy
new AbstractMap.SimpleEntry<String, UoM>("pv-energy-today", new UoM(Units.KILOWATT_HOUR, 10)),
new AbstractMap.SimpleEntry<String, UoM>("pv1-energy-today", new UoM(Units.KILOWATT_HOUR, 10)),
new AbstractMap.SimpleEntry<String, UoM>("pv2-energy-today", new UoM(Units.KILOWATT_HOUR, 10)),
new AbstractMap.SimpleEntry<String, UoM>("pv-energy-total", new UoM(Units.KILOWATT_HOUR, 10)),
new AbstractMap.SimpleEntry<String, UoM>("pv1-energy-total", new UoM(Units.KILOWATT_HOUR, 10)),
new AbstractMap.SimpleEntry<String, UoM>("pv2-energy-total", new UoM(Units.KILOWATT_HOUR, 10)),
// energy exported to grid
new AbstractMap.SimpleEntry<String, UoM>("export-energy-today", new UoM(Units.KILOWATT_HOUR, 10)),
new AbstractMap.SimpleEntry<String, UoM>("export-energy-total", new UoM(Units.KILOWATT_HOUR, 10)),
// energy imported from grid
new AbstractMap.SimpleEntry<String, UoM>("import-energy-today", new UoM(Units.KILOWATT_HOUR, 10)),
new AbstractMap.SimpleEntry<String, UoM>("import-energy-total", new UoM(Units.KILOWATT_HOUR, 10)),
// energy supplied to load
new AbstractMap.SimpleEntry<String, UoM>("load-energy-today", new UoM(Units.KILOWATT_HOUR, 10)),
new AbstractMap.SimpleEntry<String, UoM>("load-energy-total", new UoM(Units.KILOWATT_HOUR, 10)),
// energy imported to charge
new AbstractMap.SimpleEntry<String, UoM>("import-charge-energy-today", new UoM(Units.KILOWATT_HOUR, 10)),
new AbstractMap.SimpleEntry<String, UoM>("import-charge-energy-total", new UoM(Units.KILOWATT_HOUR, 10)),
// inverter energy to charge
new AbstractMap.SimpleEntry<String, UoM>("inverter-charge-energy-today", new UoM(Units.KILOWATT_HOUR, 10)),
new AbstractMap.SimpleEntry<String, UoM>("inverter-charge-energy-total", new UoM(Units.KILOWATT_HOUR, 10)),
// energy supplied from discharge
new AbstractMap.SimpleEntry<String, UoM>("discharge-energy-today", new UoM(Units.KILOWATT_HOUR, 10)),
new AbstractMap.SimpleEntry<String, UoM>("discharge-energy-total", new UoM(Units.KILOWATT_HOUR, 10)),
// inverter up time
new AbstractMap.SimpleEntry<String, UoM>("total-work-time", new UoM(Units.HOUR, 7200)),
// bus voltages
new AbstractMap.SimpleEntry<String, UoM>("p-bus-voltage", new UoM(Units.VOLT, 10)),
new AbstractMap.SimpleEntry<String, UoM>("n-bus-voltage", new UoM(Units.VOLT, 10)),
new AbstractMap.SimpleEntry<String, UoM>("sp-bus-voltage", new UoM(Units.VOLT, 10)),
// temperatures
new AbstractMap.SimpleEntry<String, UoM>("pv-temperature", new UoM(SIUnits.CELSIUS, 10)),
new AbstractMap.SimpleEntry<String, UoM>("pv-ipm-temperature", new UoM(SIUnits.CELSIUS, 10)),
new AbstractMap.SimpleEntry<String, UoM>("pv-boost-temperature", new UoM(SIUnits.CELSIUS, 10)),
new AbstractMap.SimpleEntry<String, UoM>("temperature-4", new UoM(SIUnits.CELSIUS, 10)),
new AbstractMap.SimpleEntry<String, UoM>("pv2-temperature", new UoM(SIUnits.CELSIUS, 10)),
// battery data
new AbstractMap.SimpleEntry<String, UoM>("battery-type", new UoM(Units.ONE, 1)),
new AbstractMap.SimpleEntry<String, UoM>("battery-voltage", new UoM(Units.VOLT, 10)),
new AbstractMap.SimpleEntry<String, UoM>("battery-temperature", new UoM(SIUnits.CELSIUS, 10)),
new AbstractMap.SimpleEntry<String, UoM>("battery-display", new UoM(Units.ONE, 10)),
new AbstractMap.SimpleEntry<String, UoM>("battery-soc", new UoM(Units.PERCENT, 1)),
// fault codes
new AbstractMap.SimpleEntry<String, UoM>("system-fault-0", new UoM(Units.ONE, 1)),
new AbstractMap.SimpleEntry<String, UoM>("system-fault-1", new UoM(Units.ONE, 1)),
new AbstractMap.SimpleEntry<String, UoM>("system-fault-2", new UoM(Units.ONE, 1)),
new AbstractMap.SimpleEntry<String, UoM>("system-fault-3", new UoM(Units.ONE, 1)),
new AbstractMap.SimpleEntry<String, UoM>("system-fault-4", new UoM(Units.ONE, 1)),
new AbstractMap.SimpleEntry<String, UoM>("system-fault-5", new UoM(Units.ONE, 1)),
new AbstractMap.SimpleEntry<String, UoM>("system-fault-6", new UoM(Units.ONE, 1)),
new AbstractMap.SimpleEntry<String, UoM>("system-fault-7", new UoM(Units.ONE, 1)),
// miscellaneous
new AbstractMap.SimpleEntry<String, UoM>("system-work-mode", new UoM(Units.ONE, 1)),
new AbstractMap.SimpleEntry<String, UoM>("sp-display-status", new UoM(Units.ONE, 10)),
new AbstractMap.SimpleEntry<String, UoM>("constant-power-ok", new UoM(Units.ONE, 1)),
new AbstractMap.SimpleEntry<String, UoM>("load-percent", new UoM(Units.PERCENT, 10)),
// reactive 'power' resp. 'energy'
new AbstractMap.SimpleEntry<String, UoM>("rac", new UoM(Units.VAR, 10)),
new AbstractMap.SimpleEntry<String, UoM>("erac-today", new UoM(Units.KILOVAR_HOUR, 10)),
new AbstractMap.SimpleEntry<String, UoM>("erac-total", new UoM(Units.KILOVAR_HOUR, 10))
//
);
public static Map<String, UoM> getMap() {
return GrowattChannels.CHANNEL_ID_UOM_MAP;
}
}

View File

@ -0,0 +1,76 @@
/**
* 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.growatt.internal.action;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.growatt.internal.handler.GrowattInverterHandler;
import org.openhab.core.automation.annotation.ActionInput;
import org.openhab.core.automation.annotation.RuleAction;
import org.openhab.core.thing.binding.ThingActions;
import org.openhab.core.thing.binding.ThingActionsScope;
import org.openhab.core.thing.binding.ThingHandler;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Implementation of the {@link ThingActions} interface used for setting up battery charging and discharging programs.
*
* @author Andrew Fiddian-Green - Initial contribution
*/
@ThingActionsScope(name = "growatt")
@NonNullByDefault
public class GrowattActions implements ThingActions {
private final Logger logger = LoggerFactory.getLogger(GrowattActions.class);
private @Nullable GrowattInverterHandler handler;
public static void setupBatteryProgram(ThingActions actions, Integer programMode, @Nullable Integer powerLevel,
@Nullable Integer stopSOC, @Nullable Boolean enableAcCharging, @Nullable String startTime,
@Nullable String stopTime, @Nullable Boolean enableProgram) {
if (actions instanceof GrowattActions growattActions) {
growattActions.setupBatteryProgram(programMode, powerLevel, stopSOC, enableAcCharging, startTime, stopTime,
enableProgram);
} else {
throw new IllegalArgumentException("The 'actions' argument is not an instance of GrowattActions");
}
}
@Override
public @Nullable ThingHandler getThingHandler() {
return handler;
}
@Override
public void setThingHandler(@Nullable ThingHandler handler) {
this.handler = (handler instanceof GrowattInverterHandler growattHandler) ? growattHandler : null;
}
@RuleAction(label = "@text/actions.battery-program.label", description = "@text/actions.battery-program.description")
public void setupBatteryProgram(
@ActionInput(name = "program-mode", label = "@text/actions.program-mode.label", description = "@text/actions.program-mode.description") Integer programMode,
@ActionInput(name = "power-level", label = "@text/actions.power-level.label", description = "@text/actions.power-level.description") @Nullable Integer powerLevel,
@ActionInput(name = "stop-soc", label = "@text/actions.stop-soc.label", description = "@text/actions.stop-soc.description") @Nullable Integer stopSOC,
@ActionInput(name = "enable-ac-charging", label = "@text/actions.enable-ac-charging.label", description = "@text/actions.enable-ac-charging.description") @Nullable Boolean enableAcCharging,
@ActionInput(name = "start-time", label = "@text/actions.start-time.label", description = "@text/actions.start-time.description") @Nullable String startTime,
@ActionInput(name = "stop-time", label = "@text/actions.stop-time.label", description = "@text/actions.stop-time.description") @Nullable String stopTime,
@ActionInput(name = "enable-program", label = "@text/actions.enable-program.label", description = "@text/actions.enable-program.description") @Nullable Boolean enableProgram) {
GrowattInverterHandler handler = this.handler;
if (handler != null) {
handler.setupBatteryProgram(programMode, powerLevel, stopSOC, enableAcCharging, startTime, stopTime,
enableProgram);
} else {
logger.warn("ThingHandler is null.");
}
}
}

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.growatt.internal.cloud;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* The {@link GrowattApiException} is thrown if a call to the Growatt cloud API server fails.
*
* @author Andrew Fiddian-Green - Initial contribution.
*/
@NonNullByDefault
public class GrowattApiException extends Exception {
private static final long serialVersionUID = 218139823621683189L;
public GrowattApiException(String message) {
super(message);
}
public GrowattApiException(String message, Throwable cause) {
super(message, cause);
}
}

View File

@ -0,0 +1,904 @@
/**
* 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.growatt.internal.cloud;
import java.lang.reflect.Type;
import java.net.HttpCookie;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.time.DateTimeException;
import java.time.Duration;
import java.time.LocalTime;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.eclipse.jetty.client.HttpClient;
import org.eclipse.jetty.client.api.ContentResponse;
import org.eclipse.jetty.client.api.Request;
import org.eclipse.jetty.client.util.FormContentProvider;
import org.eclipse.jetty.http.HttpMethod;
import org.eclipse.jetty.http.HttpStatus;
import org.eclipse.jetty.util.Fields;
import org.openhab.binding.growatt.internal.GrowattBindingConstants;
import org.openhab.binding.growatt.internal.config.GrowattBridgeConfiguration;
import org.openhab.binding.growatt.internal.dto.GrowattDevice;
import org.openhab.binding.growatt.internal.dto.GrowattPlant;
import org.openhab.binding.growatt.internal.dto.GrowattPlantList;
import org.openhab.binding.growatt.internal.dto.GrowattUser;
import org.openhab.core.io.net.http.HttpClientFactory;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.google.gson.Gson;
import com.google.gson.JsonArray;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
import com.google.gson.JsonPrimitive;
import com.google.gson.JsonSyntaxException;
import com.google.gson.reflect.TypeToken;
/**
* The {@link GrowattCloud} class allows the binding to access the inverter state and settings via HTTP calls to the
* remote Growatt cloud API server (instead of receiving the data from the local Grott proxy server).
* <p>
* This class is necessary since the Grott proxy server does not (yet) support easy access to some inverter register
* settings, such as the settings for the battery charging and discharging programs.
*
* @author Andrew Fiddian-Green - Initial contribution
*/
@NonNullByDefault
public class GrowattCloud implements AutoCloseable {
// JSON field names for the battery charging program
public static final String CHARGE_PROGRAM_POWER = "chargePowerCommand";
public static final String CHARGE_PROGRAM_TARGET_SOC = "wchargeSOCLowLimit2";
public static final String CHARGE_PROGRAM_ALLOW_AC_CHARGING = "acChargeEnable";
public static final String CHARGE_PROGRAM_START_TIME = "forcedChargeTimeStart1";
public static final String CHARGE_PROGRAM_STOP_TIME = "forcedChargeTimeStop1";
public static final String CHARGE_PROGRAM_ENABLE = "forcedChargeStopSwitch1";
// JSON field names for the battery discharging program
public static final String DISCHARGE_PROGRAM_POWER = "disChargePowerCommand";
public static final String DISCHARGE_PROGRAM_TARGET_SOC = "wdisChargeSOCLowLimit2";
public static final String DISCHARGE_PROGRAM_START_TIME = "forcedDischargeTimeStart1";
public static final String DISCHARGE_PROGRAM_STOP_TIME = "forcedDischargeTimeStop1";
public static final String DISCHARGE_PROGRAM_ENABLE = "forcedDischargeStopSwitch1";
// API server URL
private static final String SERVER_URL = "https://server-api.growatt.com/";
// API end points
private static final String LOGIN_API_ENDPOINT = "newTwoLoginAPI.do";
private static final String PLANT_LIST_API_ENDPOINT = "PlantListAPI.do";
private static final String PLANT_INFO_API_ENDPOINT = "newTwoPlantAPI.do";
private static final String NEW_TCP_SET_API_ENDPOINT = "newTcpsetAPI.do";
private static final String FMT_NEW_DEVICE_TYPE_API_DO = "new%sApi.do";
// command operations
private static final String OP_GET_ALL_DEVICE_LIST = "getAllDeviceList";
// enum of device types
private static enum DeviceType {
MIX,
MAX,
MIN,
SPA,
SPH,
TLX
}
/*
* Map of device types vs. field parameters for GET requests to FMT_NEW_DEVICE_TYPE_API_DO end-points.
* Note: some values are guesses which have not yet been confirmed by users
*/
private static final Map<DeviceType, String> SUPPORTED_TYPES_GET_PARAM = Map.of(
// @formatter:off
DeviceType.MIX, "getMixSetParams",
DeviceType.MAX, "getMaxSetData",
DeviceType.MIN, "getMinSetData",
DeviceType.SPA, "getSpaSetData",
DeviceType.SPH, "getSphSetData",
DeviceType.TLX, "getTlxSetData"
// @formatter:on
);
/*
* Map of device types vs. field parameters for POST commands to NEW_TCP_SET_API_ENDPOINT.
* Note: some values are guesses which have not yet been confirmed by users
*/
private static final Map<DeviceType, String> SUPPORTED_TYPE_POST_PARAM = Map.of(
// @formatter:off
DeviceType.MIX, "mixSetApiNew", // was "mixSetApi"
DeviceType.MAX, "maxSetApi",
DeviceType.MIN, "minSetApi",
DeviceType.SPA, "spaSetApi",
DeviceType.SPH, "sphSet",
DeviceType.TLX, "tlxSet"
// @formatter:on
);
// enum to select charge resp. discharge program
private static enum ProgramType {
CHARGE,
DISCHARGE
}
// enum of program modes
public static enum ProgramMode {
LOAD_FIRST,
BATTERY_FIRST,
GRID_FIRST
}
// @formatter:off
private static final Type DEVICE_LIST_TYPE = new TypeToken<List<GrowattDevice>>() {}.getType();
// @formatter:on
// HTTP headers (user agent is spoofed to mimic the Growatt Android Shine app)
private static final String USER_AGENT = "Dalvik/2.1.0 (Linux; U; Android 12; https://www.openhab.org)";
private static final String FORM_CONTENT = "application/x-www-form-urlencoded";
private static final Duration HTTP_TIMEOUT = Duration.ofSeconds(10);
private final Logger logger = LoggerFactory.getLogger(GrowattCloud.class);
private final HttpClient httpClient;
private final GrowattBridgeConfiguration configuration;
private final Gson gson = new Gson();
private final List<String> plantIds = new ArrayList<>();
private final Map<String, DeviceType> deviceIdTypeMap = new ConcurrentHashMap<>();
private String userId = "";
/**
* Constructor.
*
* @param configuration the bridge configuration parameters.
* @param httpClientFactory the OH core {@link HttpClientFactory} instance.
* @throws Exception if anything goes wrong.
*/
public GrowattCloud(GrowattBridgeConfiguration configuration, HttpClientFactory httpClientFactory)
throws Exception {
this.configuration = configuration;
this.httpClient = httpClientFactory.createHttpClient(GrowattBindingConstants.BINDING_ID);
this.httpClient.start();
}
@Override
public void close() throws Exception {
httpClient.stop();
}
/**
* Create a hash of the given password using normal MD5, except add 'c' if a byte of the digest is less than 10
*
* @param password the plain text password
* @return the hash of the password
* @throws GrowattApiException if MD5 algorithm is not supported
*/
private static String createHash(String password) throws GrowattApiException {
MessageDigest md;
try {
md = MessageDigest.getInstance("MD5");
} catch (NoSuchAlgorithmException e) {
throw new GrowattApiException("Hash algorithm error", e);
}
byte[] bytes = md.digest(password.getBytes());
StringBuilder result = new StringBuilder();
for (byte b : bytes) {
result.append(String.format("%02x", b));
}
for (int i = 0; i < result.length(); i += 2) {
if (result.charAt(i) == '0') {
result.replace(i, i + 1, "c");
}
}
return result.toString();
}
/**
* Refresh the login cookies.
*
* @throws GrowattApiException if any error occurs.
*/
private void refreshCookies() throws GrowattApiException {
List<HttpCookie> cookies = httpClient.getCookieStore().getCookies();
if (cookies.isEmpty() || cookies.stream().anyMatch(HttpCookie::hasExpired)) {
postLoginCredentials();
}
}
/**
* Login to the server (if necessary) and then execute an HTTP request using the given HTTP method, to the given end
* point, and with the given request URL parameters and/or request form fields. If the cookies are not valid first
* login to the server before making the actual HTTP request.
*
* @param method the HTTP method to use.
* @param endPoint the API end point.
* @param params the request URL parameters (may be null).
* @param fields the request form fields (may be null).
* @return a Map of JSON elements containing the server response.
* @throws GrowattApiException if any error occurs.
*/
private Map<String, JsonElement> doHttpRequest(HttpMethod method, String endPoint,
@Nullable Map<String, String> params, @Nullable Fields fields) throws GrowattApiException {
refreshCookies();
return doHttpRequestInner(method, endPoint, params, fields);
}
/**
* Inner method to execute an HTTP request using the given HTTP method, to the given end point, and with the given
* request URL parameters and/or request form fields.
*
* @param method the HTTP method to use.
* @param endPoint the API end point.
* @param params the request URL parameters (may be null).
* @param fields the request form fields (may be null).
* @return a Map of JSON elements containing the server response.
* @throws GrowattApiException if any error occurs.
*/
private Map<String, JsonElement> doHttpRequestInner(HttpMethod method, String endPoint,
@Nullable Map<String, String> params, @Nullable Fields fields) throws GrowattApiException {
//
Request request = httpClient.newRequest(SERVER_URL + endPoint).method(method).agent(USER_AGENT)
.timeout(HTTP_TIMEOUT.getSeconds(), TimeUnit.SECONDS);
if (params != null) {
params.entrySet().forEach(p -> request.param(p.getKey(), p.getValue()));
}
if (fields != null) {
request.content(new FormContentProvider(fields), FORM_CONTENT);
}
if (logger.isTraceEnabled()) {
logger.trace("{} {}{} {} {}", method, request.getPath(), params == null ? "" : "?" + request.getQuery(),
request.getVersion(), fields == null ? "" : "? " + FormContentProvider.convert(fields));
}
ContentResponse response;
try {
response = request.send();
} catch (InterruptedException | TimeoutException | ExecutionException e) {
throw new GrowattApiException("HTTP I/O Exception", e);
}
int status = response.getStatus();
String content = response.getContentAsString();
logger.trace("HTTP {} {} {}", status, HttpStatus.getMessage(status), content);
if (status != HttpStatus.OK_200) {
throw new GrowattApiException(String.format("HTTP %d %s", status, HttpStatus.getMessage(status)));
}
if (content == null || content.isBlank()) {
throw new GrowattApiException("Response is " + (content == null ? "null" : "blank"));
}
if (content.contains("<html>")) {
logger.warn("HTTP {} {} {}", status, HttpStatus.getMessage(status), content);
throw new GrowattApiException("Response is HTML");
}
try {
JsonElement jsonObject = JsonParser.parseString(content).getAsJsonObject();
if (jsonObject instanceof JsonObject jsonElement) {
return jsonElement.asMap();
}
throw new GrowattApiException("Response JSON invalid");
} catch (JsonSyntaxException | IllegalStateException e) {
throw new GrowattApiException("Response JSON syntax exception", e);
}
}
/**
* Get the deviceType for the given deviceId. If the deviceIdTypeMap is empty then download it freshly.
*
* @param the deviceId to get.
* @return the deviceType.
* @throws GrowattApiException if any error occurs.
*/
private DeviceType getDeviceTypeChecked(String deviceId) throws GrowattApiException {
if (deviceIdTypeMap.isEmpty()) {
if (plantIds.isEmpty()) {
refreshCookies();
}
for (String plantId : plantIds) {
for (GrowattDevice device : getPlantInfo(plantId)) {
try {
deviceIdTypeMap.put(device.getId(), DeviceType.valueOf(device.getType().toUpperCase()));
} catch (IllegalArgumentException e) {
// just ignore unsupported device types
}
}
}
logger.debug("Downloaded deviceTypes:{}", deviceIdTypeMap);
}
if (deviceId.isBlank()) {
throw new GrowattApiException("Device id is blank");
}
DeviceType deviceType = deviceIdTypeMap.get(deviceId);
if (deviceType != null) {
return deviceType;
}
throw new GrowattApiException("Unsupported device:" + deviceId);
}
/**
* Get the inverter device settings.
*
* @param the deviceId to get.
* @return a Map of JSON elements containing the server response.
* @throws GrowattApiException if any error occurs.
*/
public Map<String, JsonElement> getDeviceSettings(String deviceId) throws GrowattApiException {
DeviceType deviceType = getDeviceTypeChecked(deviceId);
String dt = deviceType.name().toLowerCase();
String endPoint = String.format(FMT_NEW_DEVICE_TYPE_API_DO, dt.substring(0, 1).toUpperCase() + dt.substring(1));
Map<String, String> params = new LinkedHashMap<>(); // keep params in order
params.put("op", Objects.requireNonNull(SUPPORTED_TYPES_GET_PARAM.get(deviceType)));
params.put("serialNum", deviceId);
params.put("kind", "0");
Map<String, JsonElement> result = doHttpRequest(HttpMethod.GET, endPoint, params, null);
JsonElement obj = result.get("obj");
if (obj instanceof JsonObject object) {
Map<String, JsonElement> map = object.asMap();
Optional<String> key = map.keySet().stream().filter(k -> k.toLowerCase().endsWith("bean")).findFirst();
if (key.isPresent()) {
JsonElement beanJson = map.get(key.get());
if (beanJson instanceof JsonObject bean) {
return bean.asMap();
}
}
}
throw new GrowattApiException("Invalid JSON response");
}
/**
* Get the plant information.
*
* @param the plantId to get.
* @return a list of {@link GrowattDevice} containing the server response.
* @throws GrowattApiException if any error occurs.
*/
public List<GrowattDevice> getPlantInfo(String plantId) throws GrowattApiException {
Map<String, String> params = new LinkedHashMap<>(); // keep params in order
params.put("op", OP_GET_ALL_DEVICE_LIST);
params.put("plantId", plantId);
params.put("pageNum", "1");
params.put("pageSize", "1");
Map<String, JsonElement> result = doHttpRequest(HttpMethod.GET, PLANT_INFO_API_ENDPOINT, params, null);
JsonElement deviceList = result.get("deviceList");
if (deviceList instanceof JsonArray deviceArray) {
try {
List<GrowattDevice> devices = gson.fromJson(deviceArray, DEVICE_LIST_TYPE);
if (devices != null) {
return devices;
}
} catch (JsonSyntaxException e) {
// fall through
}
}
throw new GrowattApiException("Invalid JSON response");
}
/**
* Get the plant list.
*
* @param the userId to get from.
* @return a {@link GrowattPlantList} containing the server response.
* @throws GrowattApiException if any error occurs.
*/
public GrowattPlantList getPlantList(String userId) throws GrowattApiException {
Map<String, String> params = new LinkedHashMap<>(); // keep params in order
params.put("userId", userId);
Map<String, JsonElement> result = doHttpRequest(HttpMethod.GET, PLANT_LIST_API_ENDPOINT, params, null);
JsonElement back = result.get("back");
if (back instanceof JsonObject backObject) {
try {
GrowattPlantList plantList = gson.fromJson(backObject, GrowattPlantList.class);
if (plantList != null && plantList.getSuccess()) {
return plantList;
}
} catch (JsonSyntaxException e) {
// fall through
}
}
throw new GrowattApiException("Invalid JSON response");
}
/**
* Attempt to login to the remote server by posting the given user credentials.
*
* @throws GrowattApiException if any error occurs.
*/
private void postLoginCredentials() throws GrowattApiException {
String userName = configuration.userName;
if (userName == null || userName.isBlank()) {
throw new GrowattApiException("User name missing");
}
String password = configuration.password;
if (password == null || password.isBlank()) {
throw new GrowattApiException("Password missing");
}
Fields fields = new Fields();
fields.put("userName", userName);
fields.put("password", createHash(password));
Map<String, JsonElement> result = doHttpRequestInner(HttpMethod.POST, LOGIN_API_ENDPOINT, null, fields);
JsonElement back = result.get("back");
if (back instanceof JsonObject backObject) {
try {
GrowattPlantList plantList = gson.fromJson(backObject, GrowattPlantList.class);
if (plantList != null && plantList.getSuccess()) {
GrowattUser user = plantList.getUserId();
userId = user != null ? user.getId() : userId;
plantIds.clear();
plantIds.addAll(plantList.getPlants().stream().map(GrowattPlant::getId).toList());
logger.debug("Logged in userId:{}, plantIds:{}", userId, plantIds);
return;
}
} catch (JsonSyntaxException e) {
// fall through
}
}
throw new GrowattApiException("Login failed");
}
/**
* Post a command to setup the inverter battery charging program.
*
* @param the deviceId to set up
* @param programModeInt index of the type of program Load First (0) / Battery First (1) / Grid First (2)
* @param powerLevel the rate of charging / discharging
* @param stopSOC the SOC at which to stop charging / discharging
* @param enableAcCharging allow charging from AC power
* @param startTime the start time of the charging / discharging program
* @param stopTime the stop time of the charging / discharging program
* @param enableProgram charging / discharging program shall be enabled
*
* @throws GrowattApiException if any error occurs
*/
public void setupBatteryProgram(String deviceId, int programModeInt, @Nullable Integer powerLevel,
@Nullable Integer stopSOC, @Nullable Boolean enableAcCharging, @Nullable String startTime,
@Nullable String stopTime, @Nullable Boolean enableProgram) throws GrowattApiException {
//
if (deviceId.isBlank()) {
throw new GrowattApiException("Device id is blank");
}
ProgramMode programMode;
try {
programMode = ProgramMode.values()[programModeInt];
} catch (IndexOutOfBoundsException e) {
throw new GrowattApiException("Program mode is out of range (0..2)");
}
DeviceType deviceType = getDeviceTypeChecked(deviceId);
switch (deviceType) {
case MIX:
case SPA:
setTimeProgram(deviceId, deviceType,
programMode == ProgramMode.BATTERY_FIRST ? ProgramType.CHARGE : ProgramType.DISCHARGE,
powerLevel, stopSOC, enableAcCharging, startTime, stopTime, enableProgram);
return;
case TLX:
if (enableAcCharging != null) {
setEnableAcCharging(deviceId, deviceType, enableAcCharging);
}
if (powerLevel != null) {
setPowerLevel(deviceId, deviceType, programMode, powerLevel);
}
if (stopSOC != null) {
setStopSOC(deviceId, deviceType, programMode, stopSOC);
}
if (startTime != null || stopTime != null || enableProgram != null) {
setTimeSegment(deviceId, deviceType, programMode, startTime, stopTime, enableProgram);
}
return;
default:
}
throw new GrowattApiException("Unsupported device type:" + deviceType.name());
}
/**
* Look for an entry in the given Map, and return its value as a boolean.
*
* @param map the source map.
* @param key the key to search for in the map.
* @return the boolean value.
* @throws GrowattApiException if any error occurs.
*/
public static boolean mapGetBoolean(Map<String, JsonElement> map, String key) throws GrowattApiException {
JsonElement element = map.get(key);
if (element instanceof JsonPrimitive primitive) {
if (primitive.isBoolean()) {
return primitive.getAsBoolean();
} else if (primitive.isNumber() || primitive.isString()) {
try {
switch (primitive.getAsInt()) {
case 0:
return false;
case 1:
return true;
}
} catch (NumberFormatException e) {
throw new GrowattApiException("Boolean bad value", e);
}
}
}
throw new GrowattApiException("Boolean missing or bad value");
}
/**
* Look for an entry in the given Map, and return its value as an integer.
*
* @param map the source map.
* @param key the key to search for in the map.
* @return the integer value.
* @throws GrowattApiException if any error occurs.
*/
public static int mapGetInteger(Map<String, JsonElement> map, String key) throws GrowattApiException {
JsonElement element = map.get(key);
if (element instanceof JsonPrimitive primitive) {
try {
return primitive.getAsInt();
} catch (NumberFormatException e) {
throw new GrowattApiException("Integer bad value", e);
}
}
throw new GrowattApiException("Integer missing or bad value");
}
/**
* Look for an entry in the given Map, and return its value as a LocalTime.
*
* @param source the source map.
* @param key the key to search for in the map.
* @return the LocalTime.
* @throws GrowattApiException if any error occurs.
*/
public static LocalTime mapGetLocalTime(Map<String, JsonElement> source, String key) throws GrowattApiException {
JsonElement element = source.get(key);
if ((element instanceof JsonPrimitive primitive) && primitive.isString()) {
try {
return localTimeOf(primitive.getAsString());
} catch (DateTimeException e) {
throw new GrowattApiException("LocalTime bad value", e);
}
}
throw new GrowattApiException("LocalTime missing or bad value");
}
/**
* Parse a time formatted string into a LocalTime entity.
* <p>
* Note: unlike the standard LocalTime.parse() method, this method accepts hour and minute fields from the Growatt
* server that are without leading zeros e.g. "1:1" and it accepts the conventional "01:01" format too.
*
* @param localTime a time formatted string e.g. "12:34"
* @return a corresponding LocalTime entity.
* @throws DateTimeException if any error occurs.
*/
public static LocalTime localTimeOf(String localTime) throws DateTimeException {
String splitParts[] = localTime.split(":");
if (splitParts.length < 2) {
throw new DateTimeException("LocalTime bad value");
}
try {
return LocalTime.of(Integer.valueOf(splitParts[0]), Integer.valueOf(splitParts[1]));
} catch (NumberFormatException | DateTimeException e) {
throw new DateTimeException("LocalTime bad value", e);
}
}
/**
* Post a command to set up the inverter battery charging / discharging program.
*
* @param the deviceId to set up
* @param the deviceType to set up
* @param programType selects whether the program is for charge or discharge
* @param powerLevel the rate of charging / discharging 1%..100%
* @param stopSOC the SOC at which to stop the program 5%..100%
* @param enableAcCharging allow charging from AC power (only applies to hybrid/mix inverters)
* @param startTime the start time of the program
* @param stopTime the stop time of the program
* @param enableProgram the program shall be enabled
*
* @throws GrowattApiException if any error occurs
*/
private void setTimeProgram(String deviceId, DeviceType deviceType, ProgramType programType,
@Nullable Integer powerLevel, @Nullable Integer stopSOC, @Nullable Boolean enableAcCharging,
@Nullable String startTime, @Nullable String stopTime, @Nullable Boolean enableProgram)
throws GrowattApiException {
//
if (powerLevel == null || powerLevel < 1 || powerLevel > 100) {
throw new GrowattApiException("Power level parameter is null or out of range (1%..100%)");
}
if (stopSOC == null || stopSOC < 5 || stopSOC > 100) {
throw new GrowattApiException("Target SOC parameter is null out of range (5%..100%)");
}
if (startTime == null) {
throw new GrowattApiException("Start time parameter is null");
}
if (stopTime == null) {
throw new GrowattApiException("Stop time parameter is null");
}
if (enableProgram == null) {
throw new GrowattApiException("Program enable parameter is null");
}
boolean isMixChargeCommand = deviceType == DeviceType.MIX && programType == ProgramType.CHARGE;
if (isMixChargeCommand && enableAcCharging == null) {
throw new GrowattApiException("Allow ac charging parameter is null");
}
LocalTime localStartTime;
try {
localStartTime = GrowattCloud.localTimeOf(startTime);
} catch (DateTimeException e) {
throw new GrowattApiException("Start time is invalid");
}
LocalTime localStopTime;
try {
localStopTime = GrowattCloud.localTimeOf(stopTime);
} catch (DateTimeException e) {
throw new GrowattApiException("Stop time is invalid");
}
Fields fields = new Fields();
fields.put("op", Objects.requireNonNull(SUPPORTED_TYPE_POST_PARAM.get(deviceType)));
fields.put("serialNum", deviceId);
fields.put("type", String.format("%s_ac_%s_time_period", deviceType.name().toLowerCase(),
programType.name().toLowerCase()));
int paramId = 1;
paramId = addParam(fields, paramId, String.format("%d", powerLevel));
paramId = addParam(fields, paramId, String.format("%d", stopSOC));
if (isMixChargeCommand) {
paramId = addParam(fields, paramId, enableAcCharging ? "1" : "0");
}
paramId = addParam(fields, paramId, String.format("%02d", localStartTime.getHour()));
paramId = addParam(fields, paramId, String.format("%02d", localStartTime.getMinute()));
paramId = addParam(fields, paramId, String.format("%02d", localStopTime.getHour()));
paramId = addParam(fields, paramId, String.format("%02d", localStopTime.getMinute()));
paramId = addParam(fields, paramId, enableProgram ? "1" : "0");
paramId = addParam(fields, paramId, "00");
paramId = addParam(fields, paramId, "00");
paramId = addParam(fields, paramId, "00");
paramId = addParam(fields, paramId, "00");
paramId = addParam(fields, paramId, "0");
paramId = addParam(fields, paramId, "00");
paramId = addParam(fields, paramId, "00");
paramId = addParam(fields, paramId, "00");
paramId = addParam(fields, paramId, "00");
paramId = addParam(fields, paramId, "0");
postSetCommandForm(fields);
}
/**
* Add a new entry in the given {@link Fields} map in the form "paramN" = paramValue where N is the parameter index.
*
* @param fields the map to be added to.
* @param parameterIndex the parameter index.
* @param parameterValue the parameter value.
*
* @return the next parameter index.
*/
private int addParam(Fields fields, int parameterIndex, String parameterValue) {
fields.put(String.format("param%d", parameterIndex), parameterValue);
return parameterIndex + 1;
}
/**
* Inner method to execute a POST setup command using the given form fields.
*
* @param fields the form fields to be posted.
*
* @throws GrowattApiException if any error occurs
*/
private void postSetCommandForm(Fields fields) throws GrowattApiException {
Map<String, JsonElement> result = doHttpRequest(HttpMethod.POST, NEW_TCP_SET_API_ENDPOINT, null, fields);
JsonElement success = result.get("success");
if (success instanceof JsonPrimitive sucessPrimitive) {
if (sucessPrimitive.getAsBoolean()) {
return;
}
}
throw new GrowattApiException("Command failed");
}
/**
* Post a command to enable / disable ac charging.
*
* @param the deviceId to set up
* @param the deviceType to set up
* @param enableAcCharging enable or disable the function
*
* @throws GrowattApiException if any error occurs
*/
private void setEnableAcCharging(String deviceId, DeviceType deviceType, boolean enableAcCharging)
throws GrowattApiException {
//
Fields fields = new Fields();
fields.put("op", Objects.requireNonNull(SUPPORTED_TYPE_POST_PARAM.get(deviceType)));
fields.put("serialNum", deviceId);
fields.put("type", "ac_charge");
fields.put("param1", enableAcCharging ? "1" : "0");
postSetCommandForm(fields);
}
/**
* Post a command to set up a program charge / discharge power level.
*
* @param the deviceId to set up
* @param the deviceType to set up
* @param programMode the program mode that the setting shall apply to
* @param powerLevel the rate of charging / discharging 1%..100%
*
* @throws GrowattApiException if any error occurs
*/
private void setPowerLevel(String deviceId, DeviceType deviceType, ProgramMode programMode, int powerLevel)
throws GrowattApiException {
//
if (powerLevel < 1 || powerLevel > 100) {
throw new GrowattApiException("Power level out of range (1%..100%)");
}
String typeParam;
switch (programMode) {
case BATTERY_FIRST:
typeParam = "charge_power";
break;
case GRID_FIRST:
case LOAD_FIRST:
typeParam = "discharge_power";
break;
default:
throw new GrowattApiException("Unexpected exception");
}
Fields fields = new Fields();
fields.put("op", Objects.requireNonNull(SUPPORTED_TYPE_POST_PARAM.get(deviceType)));
fields.put("serialNum", deviceId);
fields.put("type", typeParam);
fields.put("param1", String.format("%d", powerLevel));
postSetCommandForm(fields);
}
/**
* Post a command to set up a program target (stop) SOC level.
*
* @param the deviceId to set up
* @param the deviceType to set up
* @param programMode the program mode that the setting shall apply to
* @param stopSOC the SOC at which to stop the program 11%..100%
*
* @throws GrowattApiException if any error occurs
*/
private void setStopSOC(String deviceId, DeviceType deviceType, ProgramMode programMode, int stopSOC)
throws GrowattApiException {
//
if (stopSOC < 11 || stopSOC > 100) {
throw new GrowattApiException("Target SOC out of range (11%..100%)");
}
String typeParam;
switch (programMode) {
case BATTERY_FIRST:
typeParam = "charge_stop_soc";
break;
case GRID_FIRST:
typeParam = "on_grid_discharge_stop_soc";
break;
case LOAD_FIRST:
typeParam = "discharge_stop_soc";
break;
default:
throw new GrowattApiException("Unexpected exception");
}
Fields fields = new Fields();
fields.put("op", Objects.requireNonNull(SUPPORTED_TYPE_POST_PARAM.get(deviceType)));
fields.put("serialNum", deviceId);
fields.put("type", typeParam);
fields.put("param1", String.format("%d", stopSOC));
postSetCommandForm(fields);
}
/**
* Post a command to set up a time segment program.
* Note: uses separate dedicated time segments for Load First, Battery First, Grid First modes.
*
* @param the deviceId to set up
* @param the deviceType to set up
* @param programMode the program mode for the time segment
* @param startTime the start time of the program
* @param stopTime the stop time of the program
* @param enableProgram the program shall be enabled
*
* @throws GrowattApiException if any error occurs
*/
private void setTimeSegment(String deviceId, DeviceType deviceType, ProgramMode programMode,
@Nullable String startTime, @Nullable String stopTime, @Nullable Boolean enableProgram)
throws GrowattApiException {
//
if (startTime == null) {
throw new GrowattApiException("Start time parameter is null");
}
if (stopTime == null) {
throw new GrowattApiException("Stop time parameter is null");
}
if (enableProgram == null) {
throw new GrowattApiException("Program enable parameter is null");
}
LocalTime localStartTime;
try {
localStartTime = GrowattCloud.localTimeOf(startTime);
} catch (DateTimeException e) {
throw new GrowattApiException("Start time is invalid");
}
LocalTime localStopTime;
try {
localStopTime = GrowattCloud.localTimeOf(stopTime);
} catch (DateTimeException e) {
throw new GrowattApiException("Stop time is invalid");
}
Fields fields = new Fields();
fields.put("op", Objects.requireNonNull(SUPPORTED_TYPE_POST_PARAM.get(deviceType)));
fields.put("serialNum", deviceId);
fields.put("type", String.format("time_segment%d", programMode.ordinal() + 1));
fields.put("param1", String.format("%d", programMode.ordinal()));
fields.put("param2", String.format("%02d", localStartTime.getHour()));
fields.put("param3", String.format("%02d", localStartTime.getMinute()));
fields.put("param4", String.format("%02d", localStopTime.getHour()));
fields.put("param5", String.format("%02d", localStopTime.getMinute()));
fields.put("param6", enableProgram ? "1" : "0");
postSetCommandForm(fields);
}
}

View File

@ -0,0 +1,31 @@
/**
* 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.growatt.internal.config;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
/**
* The {@link GrowattBridgeConfiguration} class contains fields mapping thing configuration parameters.
*
* @author Andrew Fiddian-Green - Initial contribution
*/
@NonNullByDefault
public class GrowattBridgeConfiguration {
public static final String USER_NAME = "userName";
public static final String PASSWORD = "password";
public @Nullable String userName;
public @Nullable String password;
}

View File

@ -0,0 +1,28 @@
/**
* 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.growatt.internal.config;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* The {@link GrowattInverterConfiguration} class contains fields mapping thing configuration parameters.
*
* @author Andrew Fiddian-Green - Initial contribution
*/
@NonNullByDefault
public class GrowattInverterConfiguration {
public static final String DEVICE_ID = "deviceId";
public String deviceId = "";
}

View File

@ -0,0 +1,68 @@
/**
* 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.growatt.internal.discovery;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.growatt.internal.GrowattBindingConstants;
import org.openhab.binding.growatt.internal.config.GrowattInverterConfiguration;
import org.openhab.core.config.discovery.AbstractDiscoveryService;
import org.openhab.core.config.discovery.DiscoveryResult;
import org.openhab.core.config.discovery.DiscoveryResultBuilder;
import org.openhab.core.i18n.LocaleProvider;
import org.openhab.core.i18n.TranslationProvider;
import org.openhab.core.thing.ThingUID;
/**
* The {@link GrowattDiscoveryService} does discovery for Growatt inverters.
*
* @author Andrew Fiddian-Green - Initial contribution
*/
@NonNullByDefault
public class GrowattDiscoveryService extends AbstractDiscoveryService {
private final Map<ThingUID, Set<String>> bridgeInverterIds = new ConcurrentHashMap<>();
public GrowattDiscoveryService(TranslationProvider i18nProvider, LocaleProvider localeProvider)
throws IllegalArgumentException {
super(Set.of(GrowattBindingConstants.THING_TYPE_INVERTER), 5, false);
this.i18nProvider = i18nProvider;
this.localeProvider = localeProvider;
}
public void putInverters(ThingUID bridgeUID, Set<String> inverterIds) {
if (inverterIds.isEmpty()) {
bridgeInverterIds.remove(bridgeUID);
} else {
bridgeInverterIds.put(bridgeUID, inverterIds);
startScan();
}
}
@Override
protected void startScan() {
bridgeInverterIds.forEach((bridgeUID, inverterIds) -> {
inverterIds.forEach(inverterId -> {
DiscoveryResult inverter = DiscoveryResultBuilder
.create(new ThingUID(GrowattBindingConstants.THING_TYPE_INVERTER, bridgeUID, inverterId))
.withBridge(bridgeUID).withLabel("@text/discovery.growatt-inverter")
.withProperty(GrowattInverterConfiguration.DEVICE_ID, inverterId)
.withRepresentationProperty(GrowattInverterConfiguration.DEVICE_ID).build();
thingDiscovered(inverter);
});
});
}
}

View File

@ -0,0 +1,47 @@
/**
* 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.growatt.internal.dto;
import java.lang.reflect.Type;
import java.util.ArrayList;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import com.google.gson.annotations.SerializedName;
import com.google.gson.reflect.TypeToken;
/**
* The {@link GrottDevice} is a DTO containing data fields received from the Grott application.
*
* @author Andrew Fiddian-Green - Initial contribution
*/
@NonNullByDefault
public class GrottDevice {
// @formatter:off
public static final Type GROTT_DEVICE_ARRAY = new TypeToken<ArrayList<GrottDevice>>() {}.getType();
// @formatter:on
private @Nullable @SerializedName("device") String deviceId;
private @Nullable GrottValues values;
public String getDeviceId() {
String deviceId = this.deviceId;
return deviceId != null ? deviceId : "unknown";
}
public @Nullable GrottValues getValues() {
return values;
}
}

View File

@ -0,0 +1,188 @@
/**
* 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.growatt.internal.dto;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import com.google.gson.annotations.SerializedName;
/**
* The {@link GrottValues} is a DTO containing inverter value fields received from the Grott application.
*
* @author Andrew Fiddian-Green - Initial contribution
*/
@NonNullByDefault
public class GrottValues {
/**
* Convert Java field name to openHAB channel id
*/
public static String getChannelId(String fieldName) {
return fieldName.replace("_", "-");
}
/**
* Convert openHAB channel id to Java field name
*/
public static String getFieldName(String channelId) {
return channelId.replace("-", "_");
}
// @formatter:off
// inverter state
public @Nullable @SerializedName(value = "pvstatus") Integer system_status;
// solar AC and DC generation
public @Nullable @SerializedName(value = "pvpowerin") Integer pv_power; // from DC solar
public @Nullable @SerializedName(value = "pvpowerout") Integer inverter_power; // to AC mains
// DC electric data for strings #1 and #2
public @Nullable @SerializedName(value = "pv1voltage", alternate = { "vpv1" }) Integer pv1_voltage;
public @Nullable @SerializedName(value = "pv1current", alternate = { "buck1curr" }) Integer pv1_current;
public @Nullable @SerializedName(value = "pv1watt", alternate = { "ppv1" }) Integer pv1_power;
public @Nullable @SerializedName(value = "pv2voltage", alternate = { "vpv2" }) Integer pv2_voltage;
public @Nullable @SerializedName(value = "pv2current", alternate = { "buck2curr" }) Integer pv2_current;
public @Nullable @SerializedName(value = "pv2watt", alternate = { "ppv2" }) Integer pv2_power;
// AC mains electric data (1-phase resp. 3-phase)
public @Nullable @SerializedName(value = "pvfrequentie", alternate = { "line_freq", "outputfreq" }) Integer grid_frequency;
public @Nullable @SerializedName(value = "pvgridvoltage", alternate = { "grid_volt", "outputvolt", "voltage_l1" }) Integer grid_voltage_r;
public @Nullable @SerializedName(value = "pvgridvoltage2", alternate = { "voltage_l2" }) Integer grid_voltage_s;
public @Nullable @SerializedName(value = "pvgridvoltage3", alternate = { "voltage_l3" }) Integer grid_voltage_t;
public @Nullable @SerializedName(value = "Vac_RS", alternate = { "vacrs", "L1-2_voltage" }) Integer grid_voltage_rs;
public @Nullable @SerializedName(value = "Vac_ST", alternate = { "vacst", "L2-3_voltage" }) Integer grid_voltage_st;
public @Nullable @SerializedName(value = "Vac_TR", alternate = { "vactr", "L3-1_voltage" }) Integer grid_voltage_tr;
// solar AC mains power
public @Nullable @SerializedName(value = "pvgridcurrent", alternate = { "OP_Curr", "Inv_Curr", "Current_l1" }) Integer inverter_current_r;
public @Nullable @SerializedName(value = "pvgridcurrent2", alternate = { "Current_l2" }) Integer inverter_current_s;
public @Nullable @SerializedName(value = "pvgridcurrent3", alternate = { "Current_l3" }) Integer inverter_current_t;
public @Nullable @SerializedName(value = "pvgridpower", alternate = { "op_watt", "AC_InWatt" }) Integer inverter_power_r;
public @Nullable @SerializedName(value = "pvgridpower2") Integer inverter_power_s;
public @Nullable @SerializedName(value = "pvgridpower3") Integer inverter_power_t;
// apparent power VA
public @Nullable @SerializedName(value = "op_va", alternate = { "AC_InVA" }) Integer inverter_va;
// battery discharge / charge power
public @Nullable @SerializedName(value = "p1charge1", alternate = { "acchr_watt", "BatWatt", "bdc1_pchr" }) Integer charge_power;
public @Nullable @SerializedName(value = "pdischarge1", alternate = { "ACDischarWatt", "BatDischarWatt", "bdc1_pdischr" }) Integer discharge_power;
// miscellaneous battery
public @Nullable @SerializedName(value = "ACCharCurr") Integer charge_current;
public @Nullable @SerializedName(value = "ACDischarVA", alternate = { "BatDischarVA", "acchar_VA" }) Integer discharge_va;
// power exported to utility company
public @Nullable @SerializedName(value = "pactogridtot", alternate = { "ptogridtotal" }) Integer export_power;
public @Nullable @SerializedName(value = "pactogridr") Integer export_power_r;
public @Nullable @SerializedName(value = "pactogrids") Integer export_power_s;
public @Nullable @SerializedName(value = "pactogridt") Integer export_power_t;
// power imported from utility company
public @Nullable @SerializedName(value = "pactousertot", alternate = { "ptousertotal", "pos_rev_act_power" }) Integer import_power;
public @Nullable @SerializedName(value = "pactouserr", alternate = { "act_power_l1" }) Integer import_power_r;
public @Nullable @SerializedName(value = "pactousers", alternate = { "act_power_l2" }) Integer import_power_s;
public @Nullable @SerializedName(value = "pactousert", alternate = { "act_power_l3" }) Integer import_power_t;
// power delivered to internal load
public @Nullable @SerializedName(value = "plocaloadtot", alternate = { "ptoloadtotal" }) Integer load_power;
public @Nullable @SerializedName(value = "plocaloadr") Integer load_power_r;
public @Nullable @SerializedName(value = "plocaloads") Integer load_power_s;
public @Nullable @SerializedName(value = "plocaloadt") Integer load_power_t;
// inverter AC energy
public @Nullable @SerializedName(value = "eactoday", alternate = { "pvenergytoday" }) Integer inverter_energy_today;
public @Nullable @SerializedName(value = "eactotal", alternate = { "pvenergytotal" }) Integer inverter_energy_total;
// solar DC pv energy
public @Nullable @SerializedName(value = "epvtoday") Integer pv_energy_today;
public @Nullable @SerializedName(value = "epv1today", alternate = { "epv1tod" }) Integer pv1_energy_today;
public @Nullable @SerializedName(value = "epv2today", alternate = { "epv2tod" }) Integer pv2_energy_today;
public @Nullable @SerializedName(value = "epvtotal") Integer pv_energy_total;
public @Nullable @SerializedName(value = "epv1total", alternate = { "epv1tot" }) Integer pv1_energy_total;
public @Nullable @SerializedName(value = "epv2total", alternate = { "epv2tot" }) Integer pv2_energy_total;
// energy exported to utility company
public @Nullable @SerializedName(value = "etogrid_tod", alternate = { "etogridtoday" }) Integer export_energy_today;
public @Nullable @SerializedName(value = "etogrid_tot", alternate = { "etogridtotal", "rev_act_energy" }) Integer export_energy_total;
// energy imported from utility company
public @Nullable @SerializedName(value = "etouser_tod", alternate = { "etousertoday" }) Integer import_energy_today;
public @Nullable @SerializedName(value = "etouser_tot", alternate = { "etousertotal", "pos_act_energy" }) Integer import_energy_total;
// energy supplied to local load
public @Nullable @SerializedName(value = "elocalload_tod", alternate = { "eloadtoday" }) Integer load_energy_today;
public @Nullable @SerializedName(value = "elocalload_tot", alternate = { "eloadtotal" }) Integer load_energy_total;
// charging energy from import
public @Nullable @SerializedName(value = "eacharge_today", alternate = { "eacCharToday", "eacchrtoday" }) Integer import_charge_energy_today;
public @Nullable @SerializedName(value = "eacharge_total", alternate = { "eacCharTotal", "eacchrtotal" }) Integer import_charge_energy_total;
// charging energy from solar
public @Nullable @SerializedName(value = "eharge1_tod", alternate = { "echrtoday" }) Integer inverter_charge_energy_today;
public @Nullable @SerializedName(value = "eharge1_tot", alternate = { "echrtotal" }) Integer inverter_charge_energy_total;
// discharging energy
public @Nullable @SerializedName(value = "edischarge1_tod", alternate = { "eacDischarToday", "ebatDischarToday", "edischrtoday" }) Integer discharge_energy_today;
public @Nullable @SerializedName(value = "edischarge1_tot", alternate = { "eacDischarTotal", "ebatDischarTotal", "edischrtotal" }) Integer discharge_energy_total;
// inverter up time
public @Nullable @SerializedName(value = "totworktime") Integer total_work_time;
// bus voltages
public @Nullable @SerializedName(value = "pbusvolt", alternate = { "bus_volt", "pbusvoltage" }) Integer p_bus_voltage;
public @Nullable @SerializedName(value = "nbusvolt", alternate = { "nbusvoltage" }) Integer n_bus_voltage;
public @Nullable @SerializedName(value = "spbusvolt") Integer sp_bus_voltage;
// temperatures
public @Nullable @SerializedName(value = "pvtemperature", alternate = { "dcdctemp", "buck1_ntc" }) Integer pv_temperature;
public @Nullable @SerializedName(value = "pvipmtemperature", alternate = { "invtemp" }) Integer pv_ipm_temperature;
public @Nullable @SerializedName(value = "pvboosttemp", alternate = { "pvboottemperature", "temp3" }) Integer pv_boost_temperature;
public @Nullable @SerializedName(value = "temp4") Integer temperature_4;
public @Nullable @SerializedName(value = "buck2_ntc", alternate = { "temp5" }) Integer pv2_temperature;
// battery data
public @Nullable @SerializedName(value = "batterytype") Integer battery_type;
public @Nullable @SerializedName(value = "batttemp", alternate = { "bdc1_tempa" }) Integer battery_temperature;
public @Nullable @SerializedName(value = "vbat", alternate = { "uwBatVolt_DSP", "bat_Volt", "bms_batteryvolt" }) Integer battery_voltage;
public @Nullable @SerializedName(value = "bat_dsp") Integer battery_display;
public @Nullable @SerializedName(value = "SOC", alternate = { "batterySOC", "bms_soc" }) Integer battery_soc;
// fault codes
public @Nullable @SerializedName(value = "systemfaultword0", alternate = { "isof", "faultBit" }) Integer system_fault_0;
public @Nullable @SerializedName(value = "systemfaultword1", alternate = { "gfcif", "faultValue" }) Integer system_fault_1;
public @Nullable @SerializedName(value = "systemfaultword2", alternate = { "dcif", "warningBit" }) Integer system_fault_2;
public @Nullable @SerializedName(value = "systemfaultword3", alternate = { "vpvfault", "warningValue" }) Integer system_fault_3;
public @Nullable @SerializedName(value = "systemfaultword4", alternate = { "vacfault" }) Integer system_fault_4;
public @Nullable @SerializedName(value = "systemfaultword5", alternate = { "facfault" }) Integer system_fault_5;
public @Nullable @SerializedName(value = "systemfaultword6", alternate = { "tempfault" }) Integer system_fault_6;
public @Nullable @SerializedName(value = "systemfaultword7", alternate = { "faultcode" }) Integer system_fault_7;
// miscellaneous
public @Nullable @SerializedName(value = "uwsysworkmode") Integer system_work_mode;
public @Nullable @SerializedName(value = "spdspstatus") Integer sp_display_status;
public @Nullable @SerializedName(value = "constantPowerOK") Integer constant_power_ok;
public @Nullable @SerializedName(value = "loadpercent") Integer load_percent;
// reactive 'power' resp. 'energy'
public @Nullable @SerializedName(value = "rac", alternate = { "react_power" }) Integer rac;
public @Nullable @SerializedName(value = "eractoday", alternate = { "react_energy_kvar" }) Integer erac_today;
public @Nullable @SerializedName(value = "eractotal") Integer erac_total;
// @formatter:on
}

View File

@ -0,0 +1,38 @@
/**
* 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.growatt.internal.dto;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
/**
* The {@link GrowattDevice} is a DTO containing device data fields received from the Growatt cloud server.
*
* @author Andrew Fiddian-Green - Initial contribution
*/
@NonNullByDefault
public class GrowattDevice {
private @Nullable String deviceType;
private @Nullable String deviceSn;
public String getId() {
String deviceSn = this.deviceSn;
return deviceSn != null ? deviceSn : "";
}
public String getType() {
String deviceType = this.deviceType;
return deviceType != null ? deviceType : "";
}
}

View File

@ -0,0 +1,38 @@
/**
* 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.growatt.internal.dto;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
/**
* The {@link GrowattPlant} is a DTO containing plant data fields received from the Growatt cloud server.
*
* @author Andrew Fiddian-Green - Initial contribution
*/
@NonNullByDefault
public class GrowattPlant {
private @Nullable String plantId;
private @Nullable String plantName;
public String getId() {
String plantId = this.plantId;
return plantId != null ? plantId : "";
}
public String getName() {
String plantName = this.plantName;
return plantName != null ? plantName : "";
}
}

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.growatt.internal.dto;
import java.util.List;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
/**
* The {@link GrowattPlantList} is a DTO containing plant list and user data fields received from the Growatt cloud
* server.
*
* @author Andrew Fiddian-Green - Initial contribution
*/
@NonNullByDefault
public class GrowattPlantList {
private @Nullable List<GrowattPlant> data;
private @Nullable GrowattUser user;
private @Nullable Boolean success;
public List<GrowattPlant> getPlants() {
List<GrowattPlant> data = this.data;
return data != null ? data : List.of();
}
public Boolean getSuccess() {
Boolean success = this.success;
return success != null ? success : false;
}
public @Nullable GrowattUser getUserId() {
return user;
}
}

View File

@ -0,0 +1,32 @@
/**
* 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.growatt.internal.dto;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
/**
* The {@link GrowattUser} is a DTO containing user data fields received from the Growatt cloud server.
*
* @author Andrew Fiddian-Green - Initial contribution
*/
@NonNullByDefault
public class GrowattUser {
private @Nullable String id;
public String getId() {
String id = this.id;
return id != null ? id : "";
}
}

View File

@ -0,0 +1,48 @@
/**
* 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.growatt.internal.dto.helper;
import java.lang.reflect.Type;
import java.util.Objects;
import org.eclipse.jdt.annotation.NonNull;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import com.google.gson.JsonDeserializationContext;
import com.google.gson.JsonDeserializer;
import com.google.gson.JsonElement;
import com.google.gson.JsonParseException;
/**
* Special deserializer for integer values. It processes inputs which overflow the Integer.MAX_VALUE limit by
* transposing them to negative numbers by means of the 2's complement process.
*
* @author Andrew Fiddian-Green - Initial contribution
*/
@NonNullByDefault
public class GrottIntegerDeserializer implements JsonDeserializer<Integer> {
private static final long INT_BIT_MASK = 0xffffffff;
@Override
public @NonNull Integer deserialize(@Nullable JsonElement json, @Nullable Type typeOfT,
@Nullable JsonDeserializationContext context) throws JsonParseException {
long value = Long.parseLong(Objects.requireNonNull(json).getAsString());
if (value > Integer.MAX_VALUE) {
// transpose values above Integer.MAX_VALUE to a negative int by 2's complement
return Integer.valueOf(1 - (int) (value ^ INT_BIT_MASK));
}
return Long.valueOf(value).intValue();
}
}

View File

@ -0,0 +1,57 @@
/**
* 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.growatt.internal.dto.helper;
import java.util.HashMap;
import java.util.Map;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.growatt.internal.GrowattChannels;
import org.openhab.binding.growatt.internal.GrowattChannels.UoM;
import org.openhab.binding.growatt.internal.dto.GrottValues;
import org.openhab.core.library.types.QuantityType;
/**
* Helper routines for the {@link GrottValues} DTO class.
*
* @author Andrew Fiddian-Green - Initial contribution
*/
@NonNullByDefault
public class GrottValuesHelper {
/**
* Return the valid values from the given target DTO in a map between channel id and respective QuantityType states.
*
* @return a map of channel ids and respective QuantityType state values.
*/
public static Map<String, QuantityType<?>> getChannelStates(GrottValues target)
throws NoSuchFieldException, SecurityException, IllegalAccessException, IllegalArgumentException {
Map<String, QuantityType<?>> map = new HashMap<>();
GrowattChannels.getMap().entrySet().forEach(entry -> {
String channelId = entry.getKey();
try {
Object field = target.getClass().getField(GrottValues.getFieldName(channelId)).get(target);
if (field instanceof Integer) {
UoM uom = entry.getValue();
map.put(channelId, QuantityType.valueOf(((Integer) field).doubleValue() / uom.divisor, uom.units));
}
} catch (NoSuchFieldException | SecurityException | IllegalAccessException | IllegalArgumentException e) {
// Exceptions should never actually occur at run time; nevertheless the caller logs if one would occur..
// - NoSuchFieldException never occurs since we have explicitly tested this in the JUnit tests.
// - SecurityException, IllegalAccessException never occur since all fields are public.
// - IllegalArgumentException never occurs since we are explicitly working within this same class.
}
});
return map;
}
}

View File

@ -0,0 +1,153 @@
/**
* 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.growatt.internal.factory;
import static org.openhab.binding.growatt.internal.GrowattBindingConstants.*;
import java.util.Collections;
import java.util.HashSet;
import java.util.Hashtable;
import java.util.Objects;
import java.util.Set;
import javax.servlet.ServletException;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.growatt.internal.discovery.GrowattDiscoveryService;
import org.openhab.binding.growatt.internal.handler.GrowattBridgeHandler;
import org.openhab.binding.growatt.internal.handler.GrowattInverterHandler;
import org.openhab.binding.growatt.internal.servlet.GrowattHttpServlet;
import org.openhab.core.config.discovery.DiscoveryService;
import org.openhab.core.i18n.LocaleProvider;
import org.openhab.core.i18n.TranslationProvider;
import org.openhab.core.io.net.http.HttpClientFactory;
import org.openhab.core.thing.Bridge;
import org.openhab.core.thing.Thing;
import org.openhab.core.thing.ThingTypeUID;
import org.openhab.core.thing.ThingUID;
import org.openhab.core.thing.binding.BaseThingHandlerFactory;
import org.openhab.core.thing.binding.ThingHandler;
import org.openhab.core.thing.binding.ThingHandlerFactory;
import org.osgi.framework.ServiceRegistration;
import org.osgi.service.component.ComponentContext;
import org.osgi.service.component.annotations.Activate;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Reference;
import org.osgi.service.http.HttpService;
import org.osgi.service.http.NamespaceException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* The {@link GrowattHandlerFactory} is responsible for creating things and thing
* handlers.
*
* @author Andrew Fiddian-Green - Initial contribution
*/
@NonNullByDefault
@Component(configurationPid = "binding.growatt", service = ThingHandlerFactory.class)
public class GrowattHandlerFactory extends BaseThingHandlerFactory {
private static final Set<ThingTypeUID> SUPPORTED_THING_TYPES_UIDS = Set.of(THING_TYPE_BRIDGE, THING_TYPE_INVERTER);
private final Logger logger = LoggerFactory.getLogger(GrowattHandlerFactory.class);
private final HttpService httpService;
private final HttpClientFactory httpClientFactory;
private final TranslationProvider i18nProvider;
private final LocaleProvider localeProvider;
private final Set<ThingUID> bridges = Collections.synchronizedSet(new HashSet<>());
private final GrowattHttpServlet httpServlet = new GrowattHttpServlet();
private @Nullable GrowattDiscoveryService discoveryService;
private @Nullable ServiceRegistration<?> discoveryServiceRegistration;
@Activate
public GrowattHandlerFactory(@Reference HttpService httpService, @Reference HttpClientFactory httpClientFactory,
@Reference TranslationProvider i18nProvider, @Reference LocaleProvider localeProvider) {
this.httpService = httpService;
this.httpClientFactory = httpClientFactory;
this.i18nProvider = i18nProvider;
this.localeProvider = localeProvider;
try {
httpService.registerServlet(GrowattHttpServlet.PATH, httpServlet, null, null);
} catch (ServletException | NamespaceException e) {
logger.warn("GrowattHandlerFactory() failed to register servlet", e);
}
}
@Override
protected @Nullable ThingHandler createHandler(Thing thing) {
ThingTypeUID thingTypeUID = thing.getThingTypeUID();
if (THING_TYPE_BRIDGE.equals(thingTypeUID)) {
discoveryRegister();
bridges.add(thing.getUID());
return new GrowattBridgeHandler((Bridge) thing, Objects.requireNonNull(httpServlet),
Objects.requireNonNull(discoveryService), httpClientFactory);
}
if (THING_TYPE_INVERTER.equals(thingTypeUID)) {
return new GrowattInverterHandler(thing);
}
return null;
}
@Override
protected void deactivate(ComponentContext componentContext) {
bridges.clear();
discoveryUnregister();
httpService.unregister(GrowattHttpServlet.PATH);
super.deactivate(componentContext);
}
private void discoveryRegister() {
GrowattDiscoveryService discoveryService = this.discoveryService;
if (discoveryService == null) {
discoveryService = new GrowattDiscoveryService(i18nProvider, localeProvider);
this.discoveryService = discoveryService;
}
ServiceRegistration<?> temp = this.discoveryServiceRegistration;
if (temp == null) {
temp = bundleContext.registerService(DiscoveryService.class.getName(), discoveryService, new Hashtable<>());
this.discoveryServiceRegistration = temp;
}
}
private void discoveryUnregister() {
ServiceRegistration<?> discoveryServiceRegistration = this.discoveryServiceRegistration;
if (discoveryServiceRegistration != null) {
discoveryServiceRegistration.unregister();
}
this.discoveryService = null;
this.discoveryServiceRegistration = null;
}
@Override
protected void removeHandler(ThingHandler thingHandler) {
if (thingHandler instanceof GrowattBridgeHandler) {
bridges.remove(thingHandler.getThing().getUID());
if (bridges.isEmpty()) {
discoveryUnregister();
}
}
super.removeHandler(thingHandler);
}
@Override
public boolean supportsThingType(ThingTypeUID thingTypeUID) {
return SUPPORTED_THING_TYPES_UIDS.contains(thingTypeUID);
}
}

View File

@ -0,0 +1,143 @@
/**
* 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.growatt.internal.handler;
import java.util.HashMap;
import java.util.Map;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.growatt.internal.cloud.GrowattCloud;
import org.openhab.binding.growatt.internal.config.GrowattBridgeConfiguration;
import org.openhab.binding.growatt.internal.discovery.GrowattDiscoveryService;
import org.openhab.binding.growatt.internal.dto.GrottDevice;
import org.openhab.binding.growatt.internal.dto.helper.GrottIntegerDeserializer;
import org.openhab.binding.growatt.internal.servlet.GrowattHttpServlet;
import org.openhab.core.io.net.http.HttpClientFactory;
import org.openhab.core.thing.Bridge;
import org.openhab.core.thing.ChannelUID;
import org.openhab.core.thing.ThingStatus;
import org.openhab.core.thing.binding.BaseBridgeHandler;
import org.openhab.core.types.Command;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.JsonElement;
import com.google.gson.JsonParser;
import com.google.gson.JsonSyntaxException;
/**
* The {@link GrowattBridgeHandler} is a bridge handler for accessing Growatt inverters via the Grott application.
*
* @author Andrew Fiddian-Green - Initial contribution
*/
@NonNullByDefault
public class GrowattBridgeHandler extends BaseBridgeHandler {
private final Logger logger = LoggerFactory.getLogger(GrowattBridgeHandler.class);
private final Gson gson = new GsonBuilder().registerTypeAdapter(Integer.class, new GrottIntegerDeserializer())
.create();
private final GrowattDiscoveryService discoveryService;
private final Map<String, GrottDevice> inverters = new HashMap<>();
private final GrowattHttpServlet httpServlet;
private final HttpClientFactory httpClientFactory;
private @Nullable GrowattCloud growattCloud;
public GrowattBridgeHandler(Bridge bridge, GrowattHttpServlet httpServlet, GrowattDiscoveryService discoveryService,
HttpClientFactory httpClientFactory) {
super(bridge);
this.httpServlet = httpServlet;
this.discoveryService = discoveryService;
this.httpClientFactory = httpClientFactory;
}
@Override
public void dispose() {
inverters.clear();
httpServlet.handlerRemove(this);
discoveryService.putInverters(thing.getUID(), inverters.keySet());
}
public GrowattCloud getGrowattCloud() throws IllegalStateException {
GrowattCloud growattCloud = this.growattCloud;
if (growattCloud == null) {
try {
growattCloud = new GrowattCloud(getConfigAs(GrowattBridgeConfiguration.class), httpClientFactory);
} catch (Exception e) {
throw new IllegalStateException("GrowattCloud not created", e);
}
this.growattCloud = growattCloud;
}
return growattCloud;
}
@Override
public void handleCommand(ChannelUID channelUID, Command command) {
// everything is read only so do nothing
}
/**
* Process JSON content posted to the Grott application servlet.
*/
@SuppressWarnings("null")
public void handleGrottContent(String json) {
logger.trace("handleGrottContent() json:{}", json);
JsonElement jsonElement;
try {
jsonElement = JsonParser.parseString(json);
if (jsonElement.isJsonPrimitive()) {
// strip double escaping from Grott JSON
jsonElement = JsonParser.parseString(jsonElement.getAsString());
}
if (!jsonElement.isJsonObject()) {
throw new JsonSyntaxException("Unsupported JSON element type");
}
} catch (JsonSyntaxException e) {
logger.debug("handleGrottContent() invalid JSON '{}'", json, e);
return;
}
try {
GrottDevice inverter = gson.fromJson(jsonElement, GrottDevice.class);
if (inverter == null) {
throw new JsonSyntaxException("Inverter object is null");
}
putInverter(inverter);
} catch (JsonSyntaxException e) {
logger.debug("handleGrottContent() error parsing JSON '{}'", json, e);
return;
}
getThing().getThings().stream().map(thing -> thing.getHandler())
.filter(handler -> (handler instanceof GrowattInverterHandler))
.forEach(handler -> ((GrowattInverterHandler) handler).updateInverters(inverters.values()));
}
@Override
public void initialize() {
httpServlet.handlerAdd(this);
updateStatus(ThingStatus.ONLINE);
}
/**
* Put the given GrottDevice in our inverters map, and notify the discovery service if it was not already there.
*
* @param inverter a GrottDevice inverter object.
*/
private void putInverter(GrottDevice inverter) {
if (inverters.put(inverter.getDeviceId(), inverter) == null) {
discoveryService.putInverters(thing.getUID(), inverters.keySet());
}
}
}

View File

@ -0,0 +1,194 @@
/**
* 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.growatt.internal.handler;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.growatt.internal.action.GrowattActions;
import org.openhab.binding.growatt.internal.cloud.GrowattApiException;
import org.openhab.binding.growatt.internal.cloud.GrowattCloud;
import org.openhab.binding.growatt.internal.config.GrowattInverterConfiguration;
import org.openhab.binding.growatt.internal.dto.GrottDevice;
import org.openhab.binding.growatt.internal.dto.GrottValues;
import org.openhab.binding.growatt.internal.dto.helper.GrottValuesHelper;
import org.openhab.core.library.types.QuantityType;
import org.openhab.core.thing.Bridge;
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.ThingHandlerService;
import org.openhab.core.types.Command;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* The {@link GrowattInverterHandler} is a thing handler for Growatt inverters.
*
* @author Andrew Fiddian-Green - Initial contribution
*/
@NonNullByDefault
public class GrowattInverterHandler extends BaseThingHandler {
// data-logger sends packets each 5 minutes; timeout means 2 packets missed
private static final int AWAITING_DATA_TIMEOUT_MINUTES = 11;
private final Logger logger = LoggerFactory.getLogger(GrowattInverterHandler.class);
private String deviceId = "unknown";
private @Nullable ScheduledFuture<?> awaitingDataTimeoutTask;
public GrowattInverterHandler(Thing thing) {
super(thing);
}
@Override
public void dispose() {
ScheduledFuture<?> task = awaitingDataTimeoutTask;
if (task != null) {
task.cancel(true);
}
}
@Override
public Collection<Class<? extends ThingHandlerService>> getServices() {
return List.of(GrowattActions.class);
}
@Override
public void handleCommand(ChannelUID channelUID, Command command) {
// everything is read only so do nothing
}
@Override
public void initialize() {
GrowattInverterConfiguration config = getConfigAs(GrowattInverterConfiguration.class);
deviceId = config.deviceId;
thing.setProperty(GrowattInverterConfiguration.DEVICE_ID, deviceId);
updateStatus(ThingStatus.UNKNOWN, ThingStatusDetail.NONE, "@text/status.awaiting-data");
scheduleAwaitingDataTimeoutTask();
logger.debug("initialize() thing has {} channels", thing.getChannels().size());
}
private void scheduleAwaitingDataTimeoutTask() {
ScheduledFuture<?> task = awaitingDataTimeoutTask;
if (task != null) {
task.cancel(true);
}
awaitingDataTimeoutTask = scheduler.schedule(() -> {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
"@text/status.awaiting-data-timeout");
}, AWAITING_DATA_TIMEOUT_MINUTES, TimeUnit.MINUTES);
}
/**
* Receives a collection of GrottDevice inverter objects containing potential data for this thing. If the collection
* contains an entry matching the things's deviceId, and it contains GrottValues, then process it further. Otherwise
* go offline with a configuration error.
*
* @param inverters collection of GrottDevice objects.
*/
public void updateInverters(Collection<GrottDevice> inverters) {
inverters.stream().filter(inverter -> deviceId.equals(inverter.getDeviceId()))
.map(inverter -> inverter.getValues()).filter(values -> values != null).findAny()
.ifPresentOrElse(values -> {
updateStatus(ThingStatus.ONLINE);
scheduleAwaitingDataTimeoutTask();
updateInverterValues(values);
}, () -> {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR);
});
}
/**
* Receives a GrottValues object containing state values for this thing. Process the respective values and update
* the channels accordingly.
*
* @param inverter a GrottDevice object containing the new status values.
*/
public void updateInverterValues(GrottValues inverterValues) {
// get channel states
Map<String, QuantityType<?>> channelStates;
try {
channelStates = GrottValuesHelper.getChannelStates(inverterValues);
} catch (NoSuchFieldException | SecurityException | IllegalAccessException | IllegalArgumentException e) {
logger.warn("updateInverterValues() unexpected exception:{}, message:{}", e.getClass().getName(),
e.getMessage(), e);
return;
}
// find unused channels
List<Channel> actualChannels = thing.getChannels();
List<Channel> unusedChannels = actualChannels.stream()
.filter(channel -> !channelStates.containsKey(channel.getUID().getId())).collect(Collectors.toList());
// remove unused channels
if (!unusedChannels.isEmpty()) {
updateThing(editThing().withoutChannels(unusedChannels).build());
logger.debug("updateInverterValues() channel count {} reduced by {} to {}", actualChannels.size(),
unusedChannels.size(), thing.getChannels().size());
}
List<String> thingChannelIds = thing.getChannels().stream().map(channel -> channel.getUID().getId())
.collect(Collectors.toList());
// update channel states
channelStates.forEach((channelId, state) -> {
if (thingChannelIds.contains(channelId)) {
updateState(channelId, state);
} else {
logger.warn("updateInverterValues() channel '{}' not found; try re-creating the thing", channelId);
}
});
}
private GrowattCloud getGrowattCloud() throws IllegalStateException {
Bridge bridge = getBridge();
if (bridge != null && (bridge.getHandler() instanceof GrowattBridgeHandler bridgeHandler)) {
return bridgeHandler.getGrowattCloud();
}
throw new IllegalStateException("Unable to get GrowattCloud from bridge handler");
}
/**
* This method is called from a Rule Action to setup the battery charging program.
*
* @param programMode indicates if the program is Load first (0), Battery first (1), Grid first (2)
* @param powerLevel the rate of charging / discharging 0%..100%
* @param stopSOC the SOC at which to stop charging / discharging 0%..100%
* @param enableAcCharging allow the battery to be charged from AC power
* @param startTime the start time of the charging program; a time formatted string e.g. "12:34"
* @param stopTime the stop time of the charging program; a time formatted string e.g. "12:34"
* @param enableProgram charge / discharge program shall be enabled
*/
public void setupBatteryProgram(Integer programMode, @Nullable Integer powerLevel, @Nullable Integer stopSOC,
@Nullable Boolean enableAcCharging, @Nullable String startTime, @Nullable String stopTime,
@Nullable Boolean enableProgram) {
try {
getGrowattCloud().setupBatteryProgram(deviceId, programMode, powerLevel, stopSOC, enableAcCharging,
startTime, stopTime, enableProgram);
} catch (GrowattApiException e) {
logger.warn("setupBatteryProgram() error", e);
}
}
}

View File

@ -0,0 +1,86 @@
/**
* 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.growatt.internal.servlet;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.Collections;
import java.util.HashSet;
import java.util.Set;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.ws.rs.core.MediaType;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.growatt.internal.handler.GrowattBridgeHandler;
/**
* The {@link GrowattHttpServlet} is an HttpServlet to handle data posted by the Grott application.
*
* @author Andrew Fiddian-Green - Initial contribution
*/
@NonNullByDefault
public class GrowattHttpServlet extends HttpServlet {
public static final String PATH = "/growatt";
private static final String HTML = ""
// @formatter:off
+ "<html>"
+ "<body>"
+ "<h1 style=\"font-family: Arial\">Growatt Binding Servlet</h1>"
+ "<p>&nbsp;</p>"
+ "<h3 style=\"font-family: Arial\">Status: <span style=\"color: #%s;\">%s</span></h3>"
+ "</body>"
+ "</html>";
// @formatter:on
private static final String COLOR_READY = "ff6600";
private static final String COLOR_ONLINE = "339966";
private static final String MESSAGE_READY = "Ready";
private static final String MESSAGE_ONLINE = "Bridge Online";
private static final long serialVersionUID = 36178542423191036L;
private final Set<GrowattBridgeHandler> handlers = Collections.synchronizedSet(new HashSet<>());
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse response) throws ServletException, IOException {
response.setStatus(HttpServletResponse.SC_OK);
response.setContentType(MediaType.TEXT_HTML);
response.getWriter().write(String.format(HTML, handlers.isEmpty() ? COLOR_READY : COLOR_ONLINE,
handlers.isEmpty() ? COLOR_READY : MESSAGE_ONLINE));
}
@Override
protected void doPost(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
response.setStatus(HttpServletResponse.SC_OK);
response.getWriter().write(handlers.isEmpty() ? MESSAGE_READY : MESSAGE_ONLINE);
if (request.getContentLength() > 0) {
String content = new String(request.getInputStream().readAllBytes(), StandardCharsets.UTF_8);
handlers.forEach(handler -> handler.handleGrottContent(content));
}
}
public void handlerAdd(GrowattBridgeHandler handler) {
handlers.add(handler);
}
public void handlerRemove(GrowattBridgeHandler handler) {
handlers.remove(handler);
}
}

View File

@ -0,0 +1,11 @@
<?xml version="1.0" encoding="UTF-8"?>
<addon:addon id="growatt" 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>Growatt Binding</name>
<description>This is the binding for Growatt solar inverters.</description>
<connection>hybrid</connection>
</addon:addon>

View File

@ -0,0 +1,241 @@
# add-on
addon.growatt.name = Growatt Binding
addon.growatt.description = This is the binding for Growatt solar inverters.
# thing types
thing-type.growatt.bridge.label = Growatt Bridge
thing-type.growatt.bridge.description = Bridge Thing for Growatt Binding
thing-type.growatt.inverter.label = Growatt Inverter
thing-type.growatt.inverter.description = Inverter Thing for Growatt Binding
thing-type.growatt.inverter.channel.battery-display.label = Battery Display
thing-type.growatt.inverter.channel.battery-display.description = Battery display code.
thing-type.growatt.inverter.channel.battery-soc.label = Battery Charge
thing-type.growatt.inverter.channel.battery-soc.description = Battery state of charge.
thing-type.growatt.inverter.channel.battery-temperature.label = Battery Temperature
thing-type.growatt.inverter.channel.battery-temperature.description = Battery temperature.
thing-type.growatt.inverter.channel.battery-type.label = Battery Type
thing-type.growatt.inverter.channel.battery-type.description = Type code of the battery.
thing-type.growatt.inverter.channel.battery-voltage.label = Battery Voltage
thing-type.growatt.inverter.channel.battery-voltage.description = Battery voltage.
thing-type.growatt.inverter.channel.charge-current.label = Charge Current
thing-type.growatt.inverter.channel.charge-current.description = Charge current to battery.
thing-type.growatt.inverter.channel.charge-power.label = Charge Power
thing-type.growatt.inverter.channel.charge-power.description = Charge power to battery.
thing-type.growatt.inverter.channel.constant-power-ok.label = Constant Power OK
thing-type.growatt.inverter.channel.constant-power-ok.description = Constant power OK code.
thing-type.growatt.inverter.channel.discharge-energy-today.label = Battery Energy Today
thing-type.growatt.inverter.channel.discharge-energy-today.description = Energy consumed from battery today.
thing-type.growatt.inverter.channel.discharge-energy-total.label = Battery Energy Total
thing-type.growatt.inverter.channel.discharge-energy-total.description = Total energy consumed from battery.
thing-type.growatt.inverter.channel.discharge-power.label = Discharge Power
thing-type.growatt.inverter.channel.discharge-power.description = Discharge power from battery.
thing-type.growatt.inverter.channel.discharge-va.label = Discharge VA
thing-type.growatt.inverter.channel.discharge-va.description = Discharge VA from battery.
thing-type.growatt.inverter.channel.erac-today.label = Reactive Energy Today
thing-type.growatt.inverter.channel.erac-today.description = Reactive energy supplied today.
thing-type.growatt.inverter.channel.erac-total.label = Total Reactive Energy
thing-type.growatt.inverter.channel.erac-total.description = Total reactive energy supplied.
thing-type.growatt.inverter.channel.export-energy-today.label = Export Energy Today
thing-type.growatt.inverter.channel.export-energy-today.description = Energy exported to grid today.
thing-type.growatt.inverter.channel.export-energy-total.label = Export Energy Total
thing-type.growatt.inverter.channel.export-energy-total.description = Total energy exported to grid.
thing-type.growatt.inverter.channel.export-power.label = Export Power
thing-type.growatt.inverter.channel.export-power.description = Power exported to grid.
thing-type.growatt.inverter.channel.export-power-r.label = Export Power #R
thing-type.growatt.inverter.channel.export-power-r.description = Power exported to grid phase #R.
thing-type.growatt.inverter.channel.export-power-s.label = Export Power #S
thing-type.growatt.inverter.channel.export-power-s.description = Power exported to grid phase #S.
thing-type.growatt.inverter.channel.export-power-t.label = Export Power #T
thing-type.growatt.inverter.channel.export-power-t.description = Power exported to grid phase #T.
thing-type.growatt.inverter.channel.grid-frequency.label = Grid Frequency
thing-type.growatt.inverter.channel.grid-frequency.description = Frequency of the grid.
thing-type.growatt.inverter.channel.grid-voltage-r.label = Grid Voltage (#R)
thing-type.growatt.inverter.channel.grid-voltage-r.description = Voltage of the grid (phase #R).
thing-type.growatt.inverter.channel.grid-voltage-rs.label = Grid Voltage #RS
thing-type.growatt.inverter.channel.grid-voltage-rs.description = Voltage of the grid phases #RS.
thing-type.growatt.inverter.channel.grid-voltage-s.label = Grid Voltage #S
thing-type.growatt.inverter.channel.grid-voltage-s.description = Voltage of the grid phase #S.
thing-type.growatt.inverter.channel.grid-voltage-st.label = Grid Voltage #ST
thing-type.growatt.inverter.channel.grid-voltage-st.description = Voltage of the grid phases #ST.
thing-type.growatt.inverter.channel.grid-voltage-t.label = Grid Voltage #T
thing-type.growatt.inverter.channel.grid-voltage-t.description = Voltage of the grid phase #T.
thing-type.growatt.inverter.channel.grid-voltage-tr.label = Grid Voltage #TR
thing-type.growatt.inverter.channel.grid-voltage-tr.description = Voltage of the grid phases #TR.
thing-type.growatt.inverter.channel.import-charge-energy-today.label = Battery Import Energy Today
thing-type.growatt.inverter.channel.import-charge-energy-today.description = Energy imported from grid to charge battery today.
thing-type.growatt.inverter.channel.import-charge-energy-total.label = Battery Import Energy Totals
thing-type.growatt.inverter.channel.import-charge-energy-total.description = Total energy imported from grid to charge battery.
thing-type.growatt.inverter.channel.import-energy-today.label = Import Energy Today
thing-type.growatt.inverter.channel.import-energy-today.description = Energy imported from grid today.
thing-type.growatt.inverter.channel.import-energy-total.label = Import Energy Total
thing-type.growatt.inverter.channel.import-energy-total.description = Total energy imported from grid.
thing-type.growatt.inverter.channel.import-power.label = Import Power
thing-type.growatt.inverter.channel.import-power.description = Power imported.
thing-type.growatt.inverter.channel.import-power-r.label = Import Power #R
thing-type.growatt.inverter.channel.import-power-r.description = Power imported phase #R.
thing-type.growatt.inverter.channel.import-power-s.label = Import Power #S
thing-type.growatt.inverter.channel.import-power-s.description = Power imported phase #S.
thing-type.growatt.inverter.channel.import-power-t.label = Import Power #T
thing-type.growatt.inverter.channel.import-power-t.description = Power imported phase #T.
thing-type.growatt.inverter.channel.inverter-charge-energy-today.label = Battery Inverter Energy Today
thing-type.growatt.inverter.channel.inverter-charge-energy-today.description = Energy from inverter to charge battery today.
thing-type.growatt.inverter.channel.inverter-charge-energy-total.label = Battery Inverter Energy Total
thing-type.growatt.inverter.channel.inverter-charge-energy-total.description = Total energy from inverter to charge battery.
thing-type.growatt.inverter.channel.inverter-current-r.label = Inverter Current (#R)
thing-type.growatt.inverter.channel.inverter-current-r.description = AC current from inverter (phase #R).
thing-type.growatt.inverter.channel.inverter-current-s.label = Inverter Current #S
thing-type.growatt.inverter.channel.inverter-current-s.description = AC current from inverter phase #S.
thing-type.growatt.inverter.channel.inverter-current-t.label = Inverter Current #T
thing-type.growatt.inverter.channel.inverter-current-t.description = AC current from inverter phase #T.
thing-type.growatt.inverter.channel.inverter-energy-today.label = Inverter Energy Today
thing-type.growatt.inverter.channel.inverter-energy-today.description = Inverter output energy produced today.
thing-type.growatt.inverter.channel.inverter-energy-total.label = Inverter Energy Total
thing-type.growatt.inverter.channel.inverter-energy-total.description = Total inverter output energy produced.
thing-type.growatt.inverter.channel.inverter-power.label = Inverter Power
thing-type.growatt.inverter.channel.inverter-power.description = AC power the inverter (total).
thing-type.growatt.inverter.channel.inverter-power-r.label = Inverter Power (#R)
thing-type.growatt.inverter.channel.inverter-power-r.description = AC power from inverter (phase #R).
thing-type.growatt.inverter.channel.inverter-power-s.label = Inverter Power #S
thing-type.growatt.inverter.channel.inverter-power-s.description = AC power from inverter phase #S.
thing-type.growatt.inverter.channel.inverter-power-t.label = Inverter Power #T
thing-type.growatt.inverter.channel.inverter-power-t.description = AC power from inverter phase #T.
thing-type.growatt.inverter.channel.inverter-va.label = Inverter VA
thing-type.growatt.inverter.channel.inverter-va.description = AC VA produced by inverter.
thing-type.growatt.inverter.channel.load-energy-today.label = Load Energy Today
thing-type.growatt.inverter.channel.load-energy-today.description = Energy supplied to load today.
thing-type.growatt.inverter.channel.load-energy-total.label = Load Energy Total
thing-type.growatt.inverter.channel.load-energy-total.description = Total energy supplied to load.
thing-type.growatt.inverter.channel.load-percent.label = Load Percent
thing-type.growatt.inverter.channel.load-percent.description = Percent of full load.
thing-type.growatt.inverter.channel.load-power.label = Load Power
thing-type.growatt.inverter.channel.load-power.description = Power supplied to load.
thing-type.growatt.inverter.channel.load-power-r.label = Load Power #R
thing-type.growatt.inverter.channel.load-power-r.description = Power supplied to load phase #R.
thing-type.growatt.inverter.channel.load-power-s.label = Load Power #S
thing-type.growatt.inverter.channel.load-power-s.description = Power supplied to load phase #S.
thing-type.growatt.inverter.channel.load-power-t.label = Load Power #T
thing-type.growatt.inverter.channel.load-power-t.description = Power supplied to load phase #T.
thing-type.growatt.inverter.channel.n-bus-voltage.label = N Bus Voltage
thing-type.growatt.inverter.channel.n-bus-voltage.description = N Bus voltage.
thing-type.growatt.inverter.channel.p-bus-voltage.label = P Bus Voltage
thing-type.growatt.inverter.channel.p-bus-voltage.description = P Bus voltage.
thing-type.growatt.inverter.channel.pv-boost-temperature.label = Boost Temperature
thing-type.growatt.inverter.channel.pv-boost-temperature.description = Boost temperature.
thing-type.growatt.inverter.channel.pv-energy-today.label = DC Energy Today
thing-type.growatt.inverter.channel.pv-energy-today.description = Solar DC energy collected.
thing-type.growatt.inverter.channel.pv-energy-total.label = DC Energy Total
thing-type.growatt.inverter.channel.pv-energy-total.description = Total solar energy supplied to grid.
thing-type.growatt.inverter.channel.pv-ipm-temperature.label = Solar IPM Temperature
thing-type.growatt.inverter.channel.pv-ipm-temperature.description = Temperature of the IPM.
thing-type.growatt.inverter.channel.pv-power.label = Solar Input Power
thing-type.growatt.inverter.channel.pv-power.description = Power from solar panels.
thing-type.growatt.inverter.channel.pv-temperature.label = Solar Panel Temperature
thing-type.growatt.inverter.channel.pv-temperature.description = Temperature of the solar panels (string #1).
thing-type.growatt.inverter.channel.pv1-current.label = String #1 Current
thing-type.growatt.inverter.channel.pv1-current.description = Current from solar panel string #1.
thing-type.growatt.inverter.channel.pv1-energy-today.label = DC Energy #1 Today
thing-type.growatt.inverter.channel.pv1-energy-today.description = Solar DC energy collected by string #1 to grid today.
thing-type.growatt.inverter.channel.pv1-energy-total.label = DC Energy #1 Total
thing-type.growatt.inverter.channel.pv1-energy-total.description = Total solar DC collected by string #1.
thing-type.growatt.inverter.channel.pv1-power.label = String #1 Power
thing-type.growatt.inverter.channel.pv1-power.description = Power from solar panel string #1.
thing-type.growatt.inverter.channel.pv1-voltage.label = String #1 Voltage
thing-type.growatt.inverter.channel.pv1-voltage.description = Voltage from solar panel string #1.
thing-type.growatt.inverter.channel.pv2-current.label = String #2 Current
thing-type.growatt.inverter.channel.pv2-current.description = Current from solar panel string #2.
thing-type.growatt.inverter.channel.pv2-energy-today.label = DC Energy #2 Today
thing-type.growatt.inverter.channel.pv2-energy-today.description = Solar DC energy collected by string #2 to grid today.
thing-type.growatt.inverter.channel.pv2-energy-total.label = DC Energy #2 Total
thing-type.growatt.inverter.channel.pv2-energy-total.description = Total solar DC collected by string #2.
thing-type.growatt.inverter.channel.pv2-power.label = String #2 Power
thing-type.growatt.inverter.channel.pv2-power.description = Power from solar panel string #2.
thing-type.growatt.inverter.channel.pv2-temperature.label = Solar Panel Temperature #2
thing-type.growatt.inverter.channel.pv2-temperature.description = Temperature of the solar panels (string #2).
thing-type.growatt.inverter.channel.pv2-voltage.label = String #2 Voltage
thing-type.growatt.inverter.channel.pv2-voltage.description = Voltage from solar panel string #2.
thing-type.growatt.inverter.channel.rac.label = Reactive Power
thing-type.growatt.inverter.channel.rac.description = Reactive power output.
thing-type.growatt.inverter.channel.sp-bus-voltage.label = SP Bus Voltage
thing-type.growatt.inverter.channel.sp-bus-voltage.description = SP Bus voltage.
thing-type.growatt.inverter.channel.sp-display-status.label = Solar Panel Display
thing-type.growatt.inverter.channel.sp-display-status.description = Solar panel display status code.
thing-type.growatt.inverter.channel.system-fault-0.label = Fault Code #0
thing-type.growatt.inverter.channel.system-fault-0.description = System fault code #0.
thing-type.growatt.inverter.channel.system-fault-1.label = Fault Code #1
thing-type.growatt.inverter.channel.system-fault-1.description = System fault code #1.
thing-type.growatt.inverter.channel.system-fault-2.label = Fault Code #2
thing-type.growatt.inverter.channel.system-fault-2.description = System fault code #2.
thing-type.growatt.inverter.channel.system-fault-3.label = Fault Code #3
thing-type.growatt.inverter.channel.system-fault-3.description = System fault code #3.
thing-type.growatt.inverter.channel.system-fault-4.label = Fault Code #4
thing-type.growatt.inverter.channel.system-fault-4.description = System fault code #4.
thing-type.growatt.inverter.channel.system-fault-5.label = Fault Code #5
thing-type.growatt.inverter.channel.system-fault-5.description = System fault code #5.
thing-type.growatt.inverter.channel.system-fault-6.label = Fault Code #6
thing-type.growatt.inverter.channel.system-fault-6.description = System fault code #6.
thing-type.growatt.inverter.channel.system-fault-7.label = Fault Code #7
thing-type.growatt.inverter.channel.system-fault-7.description = System fault code #7.
thing-type.growatt.inverter.channel.system-status.label = Inverter Status
thing-type.growatt.inverter.channel.system-status.description = Status code of the inverter.
thing-type.growatt.inverter.channel.system-work-mode.label = System Work Mode
thing-type.growatt.inverter.channel.system-work-mode.description = System work mode code.
thing-type.growatt.inverter.channel.temperature-4.label = Temperature #4
thing-type.growatt.inverter.channel.temperature-4.description = Temperature #4.
thing-type.growatt.inverter.channel.total-work-time.label = Total Working Time
thing-type.growatt.inverter.channel.total-work-time.description = Total inverter working time.
# thing types config
thing-type.config.growatt.bridge.password.label = Password
thing-type.config.growatt.bridge.password.description = Password to login to the Shine App.
thing-type.config.growatt.bridge.userName.label = User Name
thing-type.config.growatt.bridge.userName.description = User name to login to the Shine App.
thing-type.config.growatt.inverter.deviceId.label = Device Id
thing-type.config.growatt.inverter.deviceId.description = Id (serial number) of the inverter.
# channel types
channel-type.growatt.advanced-electric-current.label = Electric Current
channel-type.growatt.advanced-electric-energy.label = Electric Energy
channel-type.growatt.advanced-electric-frequency.label = Electric Frequency
channel-type.growatt.advanced-electric-kvarh.label = Electric Reactive Energy
channel-type.growatt.advanced-electric-power.label = Electric Power
channel-type.growatt.advanced-electric-va.label = Electric VA
channel-type.growatt.advanced-electric-var.label = Electric Reactive Power
channel-type.growatt.advanced-electric-voltage.label = Electric Voltage
channel-type.growatt.advanced-fault-code.label = Fault Code
channel-type.growatt.advanced-outdoor-temperature.label = Outdoor Temperature
channel-type.growatt.advanced-percent.label = Percentage
channel-type.growatt.advanced-status-code.label = Status Code
channel-type.growatt.advanced-work-time.label = Work Time
channel-type.growatt.system-status-code.label = Status Code
# discovery
discovery.growatt-inverter = Growatt Inverter
# thing status
status.awaiting-data = Waiting for data from Grott application
status.awaiting-data-timeout = Timed out waiting for data from Grott application
# actions
actions.battery-program.label = Setup Battery Program
actions.battery-program.description = Setup the battery charging / discharging program
actions.enable-ac-charging.label = Enable AC Charging
actions.enable-ac-charging.description = Enable the battery to be charged from AC supply
actions.enable-program.label = Program Enable
actions.enable-program.description = Enable / disable the battery charging / discharging program
actions.power-level.label = Charge / Discharge Power
actions.power-level.description = The rate of battery charging / discharging (1%..100%)
actions.program-mode.label = Battery Program Mode
actions.program-mode.description = Select Load First (0), Battery First (2), Grid First (2)
actions.start-time.label = Start Time
actions.start-time.description = The time when the program shall start
actions.stop-soc.label = Stop SOC Level
actions.stop-soc.description = The battery SOC target for when the program is completed
actions.stop-time.label = Stop Time
actions.stop-time.description = The time when the program shall stop

View File

@ -0,0 +1,551 @@
<?xml version="1.0" encoding="UTF-8"?>
<thing:thing-descriptions bindingId="growatt"
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">
<!-- Bridge Thing Type -->
<bridge-type id="bridge">
<label>Growatt Bridge</label>
<description>Bridge Thing for Growatt Binding</description>
<config-description>
<parameter name="userName" type="text" required="false">
<label>User Name</label>
<description>User name to login to the Shine App.</description>
<advanced>true</advanced>
</parameter>
<parameter name="password" type="text" required="false">
<context>password</context>
<label>Password</label>
<description>Password to login to the Shine App.</description>
<advanced>true</advanced>
</parameter>
</config-description>
</bridge-type>
<!-- Inverter Thing Type -->
<thing-type id="inverter">
<supported-bridge-type-refs>
<bridge-type-ref id="bridge"/>
</supported-bridge-type-refs>
<label>Growatt Inverter</label>
<description>Inverter Thing for Growatt Binding</description>
<!-- All known channels; unused channels are dynamically deleted -->
<channels>
<channel id="system-status" typeId="system-status-code">
<label>Inverter Status</label>
<description>Status code of the inverter.</description>
</channel>
<!-- solar generation -->
<channel id="pv-power" typeId="system.electric-power">
<label>Solar Input Power</label>
<description>Power from solar panels.</description>
</channel>
<!-- electric data for strings #1 and #2 -->
<channel id="pv1-voltage" typeId="advanced-electric-voltage">
<label>String #1 Voltage</label>
<description>Voltage from solar panel string #1.</description>
</channel>
<channel id="pv2-voltage" typeId="advanced-electric-voltage">
<label>String #2 Voltage</label>
<description>Voltage from solar panel string #2.</description>
</channel>
<channel id="pv1-current" typeId="advanced-electric-current">
<label>String #1 Current</label>
<description>Current from solar panel string #1.</description>
</channel>
<channel id="pv2-current" typeId="advanced-electric-current">
<label>String #2 Current</label>
<description>Current from solar panel string #2.</description>
</channel>
<channel id="pv1-power" typeId="advanced-electric-power">
<label>String #1 Power</label>
<description>Power from solar panel string #1.</description>
</channel>
<channel id="pv2-power" typeId="advanced-electric-power">
<label>String #2 Power</label>
<description>Power from solar panel string #2.</description>
</channel>
<!-- grid electric data (1-phase resp. 3-phase) -->
<channel id="grid-frequency" typeId="advanced-electric-frequency">
<label>Grid Frequency</label>
<description>Frequency of the grid.</description>
</channel>
<channel id="grid-voltage-r" typeId="system.electric-voltage">
<label>Grid Voltage (#R)</label>
<description>Voltage of the grid (phase #R).</description>
</channel>
<channel id="grid-voltage-s" typeId="advanced-electric-voltage">
<label>Grid Voltage #S</label>
<description>Voltage of the grid phase #S.</description>
</channel>
<channel id="grid-voltage-t" typeId="advanced-electric-voltage">
<label>Grid Voltage #T</label>
<description>Voltage of the grid phase #T.</description>
</channel>
<channel id="grid-voltage-rs" typeId="advanced-electric-voltage">
<label>Grid Voltage #RS</label>
<description>Voltage of the grid phases #RS.</description>
</channel>
<channel id="grid-voltage-st" typeId="advanced-electric-voltage">
<label>Grid Voltage #ST</label>
<description>Voltage of the grid phases #ST.</description>
</channel>
<channel id="grid-voltage-tr" typeId="advanced-electric-voltage">
<label>Grid Voltage #TR</label>
<description>Voltage of the grid phases #TR.</description>
</channel>
<!-- inverter power to grid -->
<channel id="inverter-current-r" typeId="system.electric-current">
<label>Inverter Current (#R)</label>
<description>AC current from inverter (phase #R).</description>
</channel>
<channel id="inverter-current-s" typeId="advanced-electric-current">
<label>Inverter Current #S</label>
<description>AC current from inverter phase #S.</description>
</channel>
<channel id="inverter-current-t" typeId="advanced-electric-current">
<label>Inverter Current #T</label>
<description>AC current from inverter phase #T.</description>
</channel>
<channel id="inverter-power" typeId="system.electric-power">
<label>Inverter Power</label>
<description>AC power the inverter (total).</description>
</channel>
<channel id="inverter-power-r" typeId="system.electric-power">
<label>Inverter Power (#R)</label>
<description>AC power from inverter (phase #R).</description>
</channel>
<channel id="inverter-power-s" typeId="advanced-electric-power">
<label>Inverter Power #S</label>
<description>AC power from inverter phase #S.</description>
</channel>
<channel id="inverter-power-t" typeId="advanced-electric-power">
<label>Inverter Power #T</label>
<description>AC power from inverter phase #T.</description>
</channel>
<channel id="inverter-va" typeId="advanced-electric-va">
<label>Inverter VA</label>
<description>AC VA produced by inverter.</description>
</channel>
<!-- battery power -->
<channel id="charge-power" typeId="system.electric-power">
<label>Charge Power </label>
<description>Charge power to battery.</description>
</channel>
<channel id="charge-current" typeId="system.electric-current">
<label>Charge Current</label>
<description>Charge current to battery.</description>
</channel>
<channel id="discharge-power" typeId="system.electric-power">
<label>Discharge Power</label>
<description>Discharge power from battery.</description>
</channel>
<channel id="discharge-va" typeId="advanced-electric-va">
<label>Discharge VA</label>
<description>Discharge VA from battery.</description>
</channel>
<!-- power export to grid -->
<channel id="export-power" typeId="system.electric-power">
<label>Export Power</label>
<description>Power exported to grid.</description>
</channel>
<channel id="export-power-r" typeId="advanced-electric-power">
<label>Export Power #R</label>
<description>Power exported to grid phase #R.</description>
</channel>
<channel id="export-power-s" typeId="advanced-electric-power">
<label>Export Power #S</label>
<description>Power exported to grid phase #S.</description>
</channel>
<channel id="export-power-t" typeId="advanced-electric-power">
<label>Export Power #T</label>
<description>Power exported to grid phase #T.</description>
</channel>
<!-- power import from grid user -->
<channel id="import-power" typeId="system.electric-power">
<label>Import Power</label>
<description>Power imported.</description>
</channel>
<channel id="import-power-r" typeId="advanced-electric-power">
<label>Import Power #R</label>
<description>Power imported phase #R.</description>
</channel>
<channel id="import-power-s" typeId="advanced-electric-power">
<label>Import Power #S</label>
<description>Power imported phase #S.</description>
</channel>
<channel id="import-power-t" typeId="advanced-electric-power">
<label>Import Power #T</label>
<description>Power imported phase #T.</description>
</channel>
<!-- power to local load -->
<channel id="load-power" typeId="system.electric-power">
<label>Load Power</label>
<description>Power supplied to load.</description>
</channel>
<channel id="load-power-r" typeId="advanced-electric-power">
<label>Load Power #R</label>
<description>Power supplied to load phase #R.</description>
</channel>
<channel id="load-power-s" typeId="advanced-electric-power">
<label>Load Power #S</label>
<description>Power supplied to load phase #S.</description>
</channel>
<channel id="load-power-t" typeId="advanced-electric-power">
<label>Load Power #T</label>
<description>Power supplied to load phase #T.</description>
</channel>
<!-- inverter AC output energy -->
<channel id="inverter-energy-today" typeId="system.electric-energy">
<label>Inverter Energy Today</label>
<description>Inverter output energy produced today.</description>
</channel>
<channel id="inverter-energy-total" typeId="system.electric-energy">
<label>Inverter Energy Total</label>
<description>Total inverter output energy produced.</description>
</channel>
<!-- solar DC energy -->
<channel id="pv-energy-today" typeId="system.electric-energy">
<label>DC Energy Today</label>
<description>Solar DC energy collected.</description>
</channel>
<channel id="pv1-energy-today" typeId="advanced-electric-energy">
<label>DC Energy #1 Today</label>
<description>Solar DC energy collected by string #1 to grid today.</description>
</channel>
<channel id="pv2-energy-today" typeId="advanced-electric-energy">
<label>DC Energy #2 Today</label>
<description>Solar DC energy collected by string #2 to grid today.</description>
</channel>
<channel id="pv-energy-total" typeId="system.electric-energy">
<label>DC Energy Total</label>
<description>Total solar energy supplied to grid.</description>
</channel>
<channel id="pv1-energy-total" typeId="advanced-electric-energy">
<label>DC Energy #1 Total</label>
<description>Total solar DC collected by string #1.</description>
</channel>
<channel id="pv2-energy-total" typeId="advanced-electric-energy">
<label>DC Energy #2 Total</label>
<description>Total solar DC collected by string #2.</description>
</channel>
<!-- energy exported to grid -->
<channel id="export-energy-today" typeId="system.electric-energy">
<label>Export Energy Today</label>
<description>Energy exported to grid today.</description>
</channel>
<channel id="export-energy-total" typeId="system.electric-energy">
<label>Export Energy Total</label>
<description>Total energy exported to grid.</description>
</channel>
<!-- energy imported from grid -->
<channel id="import-energy-today" typeId="system.electric-energy">
<label>Import Energy Today</label>
<description>Energy imported from grid today.</description>
</channel>
<channel id="import-energy-total" typeId="system.electric-energy">
<label>Import Energy Total</label>
<description>Total energy imported from grid.</description>
</channel>
<!-- energy supplied to local -->
<channel id="load-energy-today" typeId="system.electric-energy">
<label>Load Energy Today</label>
<description>Energy supplied to load today.</description>
</channel>
<channel id="load-energy-total" typeId="system.electric-energy">
<label>Load Energy Total</label>
<description>Total energy supplied to load.</description>
</channel>
<!-- energy imported from grid to charge -->
<channel id="import-charge-energy-today" typeId="system.electric-energy">
<label>Battery Import Energy Today</label>
<description>Energy imported from grid to charge battery today.</description>
</channel>
<channel id="import-charge-energy-total" typeId="system.electric-energy">
<label>Battery Import Energy Totals</label>
<description>Total energy imported from grid to charge battery.</description>
</channel>
<!-- inverter energy to charge -->
<channel id="inverter-charge-energy-today" typeId="system.electric-energy">
<label>Battery Inverter Energy Today</label>
<description>Energy from inverter to charge battery today.</description>
</channel>
<channel id="inverter-charge-energy-total" typeId="system.electric-energy">
<label>Battery Inverter Energy Total</label>
<description>Total energy from inverter to charge battery.</description>
</channel>
<!-- energy consumed from battery -->
<channel id="discharge-energy-today" typeId="system.electric-energy">
<label>Battery Energy Today</label>
<description>Energy consumed from battery today.</description>
</channel>
<channel id="discharge-energy-total" typeId="system.electric-energy">
<label>Battery Energy Total</label>
<description>Total energy consumed from battery.</description>
</channel>
<!-- inverter up time -->
<channel id="total-work-time" typeId="advanced-work-time">
<label>Total Working Time</label>
<description>Total inverter working time.</description>
</channel>
<!-- bus voltages -->
<channel id="p-bus-voltage" typeId="advanced-electric-voltage">
<label>P Bus Voltage</label>
<description>P Bus voltage.</description>
</channel>
<channel id="n-bus-voltage" typeId="advanced-electric-voltage">
<label>N Bus Voltage</label>
<description>N Bus voltage.</description>
</channel>
<channel id="sp-bus-voltage" typeId="advanced-electric-voltage">
<label>SP Bus Voltage</label>
<description>SP Bus voltage.</description>
</channel>
<!-- temperatures -->
<channel id="pv-temperature" typeId="advanced-outdoor-temperature">
<label>Solar Panel Temperature</label>
<description>Temperature of the solar panels (string #1).</description>
</channel>
<channel id="pv-ipm-temperature" typeId="advanced-outdoor-temperature">
<label>Solar IPM Temperature</label>
<description>Temperature of the IPM.</description>
</channel>
<channel id="pv-boost-temperature" typeId="advanced-outdoor-temperature">
<label>Boost Temperature</label>
<description>Boost temperature.</description>
</channel>
<channel id="temperature-4" typeId="advanced-outdoor-temperature">
<label>Temperature #4</label>
<description>Temperature #4.</description>
</channel>
<channel id="pv2-temperature" typeId="advanced-outdoor-temperature">
<label>Solar Panel Temperature #2</label>
<description>Temperature of the solar panels (string #2).</description>
</channel>
<!-- battery data -->
<channel id="battery-type" typeId="advanced-status-code">
<label>Battery Type</label>
<description>Type code of the battery.</description>
</channel>
<channel id="battery-temperature" typeId="advanced-outdoor-temperature">
<label>Battery Temperature</label>
<description>Battery temperature.</description>
</channel>
<channel id="battery-voltage" typeId="advanced-electric-voltage">
<label>Battery Voltage</label>
<description>Battery voltage.</description>
</channel>
<channel id="battery-display" typeId="advanced-status-code">
<label>Battery Display</label>
<description>Battery display code.</description>
</channel>
<channel id="battery-soc" typeId="advanced-percent">
<label>Battery Charge</label>
<description>Battery state of charge.</description>
</channel>
<!-- fault codes -->
<channel id="system-fault-0" typeId="advanced-fault-code">
<label>Fault Code #0</label>
<description>System fault code #0.</description>
</channel>
<channel id="system-fault-1" typeId="advanced-fault-code">
<label>Fault Code #1</label>
<description>System fault code #1.</description>
</channel>
<channel id="system-fault-2" typeId="advanced-fault-code">
<label>Fault Code #2</label>
<description>System fault code #2.</description>
</channel>
<channel id="system-fault-3" typeId="advanced-fault-code">
<label>Fault Code #3</label>
<description>System fault code #3.</description>
</channel>
<channel id="system-fault-4" typeId="advanced-fault-code">
<label>Fault Code #4</label>
<description>System fault code #4.</description>
</channel>
<channel id="system-fault-5" typeId="advanced-fault-code">
<label>Fault Code #5</label>
<description>System fault code #5.</description>
</channel>
<channel id="system-fault-6" typeId="advanced-fault-code">
<label>Fault Code #6</label>
<description>System fault code #6.</description>
</channel>
<channel id="system-fault-7" typeId="advanced-fault-code">
<label>Fault Code #7</label>
<description>System fault code #7.</description>
</channel>
<!-- miscellaneous -->
<channel id="system-work-mode" typeId="advanced-status-code">
<label>System Work Mode</label>
<description>System work mode code.</description>
</channel>
<channel id="sp-display-status" typeId="advanced-status-code">
<label>Solar Panel Display</label>
<description>Solar panel display status code.</description>
</channel>
<channel id="constant-power-ok" typeId="advanced-status-code">
<label>Constant Power OK</label>
<description>Constant power OK code.</description>
</channel>
<channel id="load-percent" typeId="advanced-percent">
<label>Load Percent</label>
<description>Percent of full load.</description>
</channel>
<!-- reactive power resp. energy -->
<channel id="rac" typeId="advanced-electric-var">
<label>Reactive Power</label>
<description>Reactive power output.</description>
</channel>
<channel id="erac-today" typeId="advanced-electric-kvarh">
<label>Reactive Energy Today</label>
<description>Reactive energy supplied today.</description>
</channel>
<channel id="erac-total" typeId="advanced-electric-kvarh">
<label>Total Reactive Energy</label>
<description>Total reactive energy supplied.</description>
</channel>
</channels>
<config-description>
<parameter name="deviceId" type="text" required="true">
<label>Device Id</label>
<description>Id (serial number) of the inverter.</description>
</parameter>
</config-description>
</thing-type>
<!-- Binding Specific Channel Types -->
<channel-type id="system-status-code">
<item-type>Number:Dimensionless</item-type>
<label>Status Code</label>
<category>Status</category>
<state readOnly="true" pattern="%00d"/>
</channel-type>
<channel-type id="advanced-status-code" advanced="true">
<item-type>Number:Dimensionless</item-type>
<label>Status Code</label>
<category>Status</category>
<state readOnly="true" pattern="%00d"/>
</channel-type>
<channel-type id="advanced-fault-code" advanced="true">
<item-type>Number:Dimensionless</item-type>
<label>Fault Code</label>
<category>Siren</category>
<state readOnly="true" pattern="%00d"/>
</channel-type>
<channel-type id="advanced-percent" advanced="true">
<item-type>Number:Dimensionless</item-type>
<label>Percentage</label>
<state readOnly="true" pattern="%0.1f %%"/>
</channel-type>
<channel-type id="advanced-electric-frequency" advanced="true">
<item-type>Number:Frequency</item-type>
<label>Electric Frequency</label>
<category>Energy</category>
<state readOnly="true" pattern="%0.2f Hz"/>
</channel-type>
<channel-type id="advanced-electric-va" advanced="true">
<item-type>Number:Power</item-type>
<label>Electric VA</label>
<category>Energy</category>
<state readOnly="true" pattern="%0.0f VA"/>
</channel-type>
<channel-type id="advanced-work-time" advanced="true">
<item-type>Number:Time</item-type>
<label>Work Time</label>
<category>Time</category>
<state readOnly="true" pattern="%0.1f h"/>
</channel-type>
<channel-type id="advanced-electric-power" advanced="true">
<item-type>Number:Power</item-type>
<label>Electric Power</label>
<category>Energy</category>
<state readOnly="true" pattern="%0.0f W"/>
</channel-type>
<channel-type id="advanced-electric-current" advanced="true">
<item-type>Number:ElectricCurrent</item-type>
<label>Electric Current</label>
<category>Energy</category>
<state readOnly="true" pattern="%0.1f A"/>
</channel-type>
<channel-type id="advanced-electric-voltage" advanced="true">
<item-type>Number:ElectricPotential</item-type>
<label>Electric Voltage</label>
<category>Energy</category>
<state readOnly="true" pattern="%0.1f V"/>
</channel-type>
<channel-type id="advanced-electric-energy" advanced="true">
<item-type>Number:Energy</item-type>
<label>Electric Energy</label>
<category>Energy</category>
<state readOnly="true" pattern="%0.0f kWh"/>
</channel-type>
<channel-type id="advanced-electric-var" advanced="true">
<item-type>Number:Power</item-type>
<label>Electric Reactive Power</label>
<category>Energy</category>
<state readOnly="true" pattern="%0.0f var"/>
</channel-type>
<channel-type id="advanced-electric-kvarh" advanced="true">
<item-type>Number:Energy</item-type>
<label>Electric Reactive Energy</label>
<category>Energy</category>
<state readOnly="true" pattern="%0.0f kvarh"/>
</channel-type>
<channel-type id="advanced-outdoor-temperature" advanced="true">
<item-type>Number:Temperature</item-type>
<label>Outdoor Temperature</label>
<category>Temperature</category>
<state readOnly="true" pattern="%0.0f °C"/>
</channel-type>
</thing:thing-descriptions>

View File

@ -0,0 +1,386 @@
/**
* 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.growatt.test;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.*;
import java.io.BufferedReader;
import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.IOException;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.stream.Collectors;
import javax.net.ssl.SSLSession;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.eclipse.jetty.client.HttpClient;
import org.eclipse.jetty.util.ssl.SslContextFactory;
import org.junit.jupiter.api.Test;
import org.openhab.binding.growatt.internal.GrowattChannels;
import org.openhab.binding.growatt.internal.GrowattChannels.UoM;
import org.openhab.binding.growatt.internal.cloud.GrowattCloud;
import org.openhab.binding.growatt.internal.config.GrowattBridgeConfiguration;
import org.openhab.binding.growatt.internal.dto.GrottDevice;
import org.openhab.binding.growatt.internal.dto.GrottValues;
import org.openhab.binding.growatt.internal.dto.helper.GrottIntegerDeserializer;
import org.openhab.binding.growatt.internal.dto.helper.GrottValuesHelper;
import org.openhab.core.io.net.http.HttpClientFactory;
import org.openhab.core.library.types.QuantityType;
import org.openhab.core.library.unit.SIUnits;
import org.openhab.core.library.unit.Units;
import org.openhab.core.types.State;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
/**
* The {@link GrowattTest} is a JUnit test suite for the Growatt binding.
*
* @author Andrew Fiddian-Green - Initial contribution
*/
@NonNullByDefault
public class GrowattTest {
private final Gson gson = new GsonBuilder().registerTypeAdapter(Integer.class, new GrottIntegerDeserializer())
.create();
/**
* Load a (JSON) string from a file
*
* @throws IOException
* @throws FileNotFoundException
*/
private String load(String fileName) throws FileNotFoundException, IOException {
try (FileReader file = new FileReader(String.format("src/test/resources/%s.json", fileName));
BufferedReader reader = new BufferedReader(file)) {
StringBuilder builder = new StringBuilder();
String line;
while ((line = reader.readLine()) != null) {
builder.append(line).append("\n");
}
return builder.toString();
}
}
/**
* Load a GrottValues class from a JSON payload.
*
* @param fileName the file containing the JSON payload.
* @return a GrottValues DTO.
* @throws IOException
* @throws FileNotFoundException
*/
private GrottValues loadGrottValues(String fileName) throws FileNotFoundException, IOException {
String json = load(fileName);
GrottDevice device = gson.fromJson(json, GrottDevice.class);
assertNotNull(device);
GrottValues grottValues = device.getValues();
assertNotNull(grottValues);
return grottValues;
}
@Test
void testGrottValuesAccessibility() throws FileNotFoundException, IOException {
testGrottValuesAccessibility("simple");
testGrottValuesAccessibility("sph");
}
/**
* For the given JSON file, test that GrottValues implements the same fields as the Map returned from
* GrowattChannels.getMap(). Test that all fields can be accessed and that they are either null or an Integer
* instance.
*
* @param fileName the name of the JSON file to be tested.
* @throws IOException
* @throws FileNotFoundException
*/
private void testGrottValuesAccessibility(String fileName) throws FileNotFoundException, IOException {
GrottValues grottValues = loadGrottValues(fileName);
List<String> fields = Arrays.asList(GrottValues.class.getFields()).stream().map(f -> f.getName())
.collect(Collectors.toList());
// test that the GrottValues DTO has identical field names to the CHANNEL_ID_UOM_MAP channel ids
for (String channel : GrowattChannels.getMap().keySet()) {
assertTrue(fields.contains(GrottValues.getFieldName(channel)));
}
// test that the CHANNEL_ID_UOM_MAP has identical channel ids to the GrottValues DTO field names
for (String field : fields) {
assertTrue(GrowattChannels.getMap().containsKey(GrottValues.getChannelId(field)));
}
// test that the CHANNEL_ID_UOM_MAP and the GrottValues DTO have the same number of fields resp. channel ids
assertEquals(fields.size(), GrowattChannels.getMap().size());
List<String> errors = new ArrayList<>();
for (Entry<String, UoM> entry : GrowattChannels.getMap().entrySet()) {
String channelId = entry.getKey();
Field field;
// test that the field can be accessed
try {
field = GrottValues.class.getField(GrottValues.getFieldName(channelId));
} catch (NoSuchFieldException | SecurityException e) {
String msg = e.getMessage();
errors.add(msg != null ? msg : e.getClass().getName());
continue;
}
// test that the field value is either null or an Integer
try {
Object value = field.get(grottValues);
assertTrue(value == null || (value instanceof Integer));
} catch (IllegalArgumentException | IllegalAccessException e) {
String msg = e.getMessage();
errors.add(msg != null ? msg : e.getClass().getName());
continue;
}
}
if (!errors.isEmpty()) {
fail(errors.toString());
}
}
/**
* Spot checks to test that GrottValues is loaded with the correct contents from the "simple" JSON file.
*
* @throws IOException
* @throws FileNotFoundException
* @throws IllegalArgumentException
* @throws IllegalAccessException
* @throws SecurityException
* @throws NoSuchFieldException
*/
@Test
void testGrottValuesContents() throws FileNotFoundException, IOException, NoSuchFieldException, SecurityException,
IllegalAccessException, IllegalArgumentException {
GrottValues grottValues = loadGrottValues("simple");
assertEquals(1, grottValues.system_status);
assertEquals(1622, grottValues.pv_power);
assertEquals(4997, grottValues.grid_frequency);
assertEquals(2353, grottValues.grid_voltage_r);
assertEquals(7, grottValues.inverter_current_r);
assertEquals(1460, grottValues.inverter_power);
assertEquals(1460, grottValues.inverter_power_r);
assertEquals(273, grottValues.pv_temperature);
assertEquals(87, grottValues.inverter_energy_today);
assertEquals(43265, grottValues.inverter_energy_total);
assertEquals(90, grottValues.pv1_energy_today);
assertEquals(45453, grottValues.pv1_energy_total);
assertEquals(45453, grottValues.pv_energy_total);
assertEquals(0, grottValues.pv2_voltage);
assertEquals(0, grottValues.pv2_current);
assertEquals(0, grottValues.pv2_power);
assertEquals(65503878, grottValues.total_work_time);
Map<String, QuantityType<?>> channelStates = null;
channelStates = GrottValuesHelper.getChannelStates(grottValues);
assertNotNull(channelStates);
assertEquals(29, channelStates.size());
channelStates.forEach((channelId, state) -> {
assertTrue(state instanceof QuantityType<?>);
});
assertEquals(QuantityType.ONE, channelStates.get("system-status"));
assertEquals(QuantityType.valueOf(162.2, Units.WATT), channelStates.get("pv-power"));
assertEquals(QuantityType.valueOf(49.97, Units.HERTZ), channelStates.get("grid-frequency"));
assertEquals(QuantityType.valueOf(235.3, Units.VOLT), channelStates.get("grid-voltage-r"));
assertEquals(QuantityType.valueOf(0.7, Units.AMPERE), channelStates.get("inverter-current-r"));
assertEquals(QuantityType.valueOf(146, Units.WATT), channelStates.get("inverter-power"));
assertEquals(QuantityType.valueOf(146, Units.WATT), channelStates.get("inverter-power-r"));
assertEquals(QuantityType.valueOf(27.3, SIUnits.CELSIUS), channelStates.get("pv-temperature"));
assertEquals(QuantityType.valueOf(8.7, Units.KILOWATT_HOUR), channelStates.get("inverter-energy-today"));
assertEquals(QuantityType.valueOf(4326.5, Units.KILOWATT_HOUR), channelStates.get("inverter-energy-total"));
assertEquals(QuantityType.valueOf(9, Units.KILOWATT_HOUR), channelStates.get("pv1-energy-today"));
assertEquals(QuantityType.valueOf(4545.3, Units.KILOWATT_HOUR), channelStates.get("pv1-energy-total"));
assertEquals(QuantityType.valueOf(4545.3, Units.KILOWATT_HOUR), channelStates.get("pv-energy-total"));
assertEquals(QuantityType.valueOf(0, Units.VOLT), channelStates.get("pv2-voltage"));
assertEquals(QuantityType.valueOf(0, Units.AMPERE), channelStates.get("pv2-current"));
assertEquals(QuantityType.valueOf(0, Units.WATT), channelStates.get("pv2-power"));
State state = channelStates.get("total-work-time");
assertTrue(state instanceof QuantityType<?>);
if (state instanceof QuantityType<?>) {
QuantityType<?> seconds = ((QuantityType<?>) state).toUnit(Units.SECOND);
assertNotNull(seconds);
assertEquals(QuantityType.valueOf(32751939, Units.SECOND).doubleValue(), seconds.doubleValue(), 0.1);
}
assertNull(channelStates.get("aardvark"));
}
@Test
void testJsonFieldsMappedToDto() throws FileNotFoundException, IOException {
testJsonFieldsMappedToDto("simple");
testJsonFieldsMappedToDto("sph");
}
/**
* For the given JSON test file name, check that each field in its JSON is mapped to precisely one field in the
* values DTO.
*
* @param fileName the name of the JSON file to be tested.
* @throws IOException
* @throws FileNotFoundException
*/
private void testJsonFieldsMappedToDto(String fileName) throws FileNotFoundException, IOException {
Field[] fields = GrottValues.class.getFields();
String json = load(fileName);
JsonParser.parseString(json).getAsJsonObject().get("values").getAsJsonObject().entrySet().forEach(e -> {
String key = e.getKey();
if (!"datalogserial".equals(key) && !"pvserial".equals(key)) {
JsonObject testJsonObject = new JsonObject();
testJsonObject.add(key, e.getValue());
GrottValues testDto = gson.fromJson(testJsonObject, GrottValues.class);
int mappedFieldCount = 0;
List<String> errors = new ArrayList<>();
for (Field field : fields) {
try {
if (field.get(testDto) != null) {
mappedFieldCount++;
}
} catch (IllegalAccessException | IllegalArgumentException ex) {
String msg = ex.getMessage();
errors.add(msg != null ? msg : ex.getClass().getName());
}
}
if (!errors.isEmpty()) {
fail(errors.toString());
}
assertEquals(1, mappedFieldCount);
}
});
}
/**
* Test the Growatt remote cloud API server.
* Will not run unless actual user credentials are provided.
*
* @throws Exception
*/
@Test
void testServer() throws Exception {
GrowattBridgeConfiguration configuration = new GrowattBridgeConfiguration();
String deviceId = "";
/*
* To test on an actual inverter, populate its plant data and user credentials below.
*
* configuration.userName = "aa";
* configuration.password ="bb";
* deviceId = "cc";
*
*/
if (configuration.userName == null) {
return;
}
SslContextFactory.Client sslContextFactory = new SslContextFactory.Client.Client();
sslContextFactory.setHostnameVerifier((@Nullable String host, @Nullable SSLSession session) -> true);
sslContextFactory.setValidatePeerCerts(false);
HttpClientFactory httpClientFactory = mock(HttpClientFactory.class);
when(httpClientFactory.createHttpClient(anyString())).thenReturn(new HttpClient(sslContextFactory));
try (GrowattCloud api = new GrowattCloud(configuration, httpClientFactory)) {
Integer programMode = GrowattCloud.ProgramMode.BATTERY_FIRST.ordinal();
Integer chargingPower = 97;
Integer targetSOC = 23;
Boolean allowAcCharging = false;
String startTime = "01:16";
String stopTime = "02:17";
Boolean programEnable = false;
api.setupBatteryProgram(deviceId, programMode, chargingPower, targetSOC, allowAcCharging, startTime,
stopTime, programEnable);
Map<String, JsonElement> result = api.getDeviceSettings(deviceId);
assertFalse(result.isEmpty());
assertEquals(chargingPower, GrowattCloud.mapGetInteger(result, GrowattCloud.CHARGE_PROGRAM_POWER));
assertEquals(targetSOC, GrowattCloud.mapGetInteger(result, GrowattCloud.CHARGE_PROGRAM_TARGET_SOC));
assertEquals(allowAcCharging,
GrowattCloud.mapGetBoolean(result, GrowattCloud.CHARGE_PROGRAM_ALLOW_AC_CHARGING));
assertEquals(GrowattCloud.localTimeOf(startTime),
GrowattCloud.mapGetLocalTime(result, GrowattCloud.CHARGE_PROGRAM_START_TIME));
assertEquals(GrowattCloud.localTimeOf(stopTime),
GrowattCloud.mapGetLocalTime(result, GrowattCloud.CHARGE_PROGRAM_STOP_TIME));
assertEquals(programEnable, GrowattCloud.mapGetBoolean(result, GrowattCloud.CHARGE_PROGRAM_ENABLE));
chargingPower = 100;
targetSOC = 20;
allowAcCharging = true;
startTime = "00:15";
stopTime = "06:45";
programEnable = true;
api.setupBatteryProgram(deviceId, programMode, chargingPower, targetSOC, allowAcCharging, startTime,
stopTime, programEnable);
result = api.getDeviceSettings(deviceId);
assertFalse(result.isEmpty());
assertEquals(chargingPower, GrowattCloud.mapGetInteger(result, GrowattCloud.CHARGE_PROGRAM_POWER));
assertEquals(targetSOC, GrowattCloud.mapGetInteger(result, GrowattCloud.CHARGE_PROGRAM_TARGET_SOC));
assertEquals(allowAcCharging,
GrowattCloud.mapGetBoolean(result, GrowattCloud.CHARGE_PROGRAM_ALLOW_AC_CHARGING));
assertEquals(GrowattCloud.localTimeOf(startTime),
GrowattCloud.mapGetLocalTime(result, GrowattCloud.CHARGE_PROGRAM_START_TIME));
assertEquals(GrowattCloud.localTimeOf(stopTime),
GrowattCloud.mapGetLocalTime(result, GrowattCloud.CHARGE_PROGRAM_STOP_TIME));
assertEquals(programEnable, GrowattCloud.mapGetBoolean(result, GrowattCloud.CHARGE_PROGRAM_ENABLE));
}
}
@Test
void testThreePhaseGrottValuesContents() throws FileNotFoundException, IOException, NoSuchFieldException,
SecurityException, IllegalAccessException, IllegalArgumentException {
GrottValues grottValues = loadGrottValues("3phase");
assertNotNull(grottValues);
Map<String, QuantityType<?>> channelStates = GrottValuesHelper.getChannelStates(grottValues);
assertNotNull(channelStates);
assertEquals(64, channelStates.size());
assertEquals(QuantityType.valueOf(-36.5, Units.WATT), channelStates.get("inverter-power"));
assertEquals(QuantityType.valueOf(11, Units.PERCENT), channelStates.get("battery-soc"));
assertEquals(QuantityType.valueOf(408.4, Units.VOLT), channelStates.get("grid-voltage-rs"));
assertEquals(QuantityType.valueOf(326.5, Units.VOLT), channelStates.get("n-bus-voltage"));
assertEquals(QuantityType.valueOf(404.1, Units.VOLT), channelStates.get("battery-voltage"));
}
@Test
void testMeterGrottValuesContents() throws FileNotFoundException, IOException, NoSuchFieldException,
SecurityException, IllegalAccessException, IllegalArgumentException {
GrottValues grottValues = loadGrottValues("meter");
assertNotNull(grottValues);
Map<String, QuantityType<?>> channelStates = GrottValuesHelper.getChannelStates(grottValues);
assertNotNull(channelStates);
assertEquals(16, channelStates.size());
assertEquals(QuantityType.valueOf(809.8, Units.WATT), channelStates.get("import-power"));
assertEquals(QuantityType.valueOf(171.0, Units.WATT), channelStates.get("import-power-s"));
assertEquals(QuantityType.valueOf(237.4, Units.VOLT), channelStates.get("grid-voltage-s"));
assertEquals(QuantityType.valueOf(409.5, Units.VOLT), channelStates.get("grid-voltage-rs"));
assertEquals(QuantityType.valueOf(1.5, Units.AMPERE), channelStates.get("inverter-current-s"));
}
}

View File

@ -0,0 +1,158 @@
{
"device": "KLN0D6L034",
"time": "2023-12-26T21:48:33",
"buffered": "no",
"values": {
"pvserial": "KLN0D6L034",
"pvstatus": 1,
"pvpowerin": 0,
"pv1voltage": 669,
"pv1current": 0,
"pv1watt": 0,
"pv2voltage": 695,
"pv2current": 0,
"pv2watt": 0,
"pv3voltage": 0,
"pv3current": 0,
"pv3watt": 0,
"pv4voltage": 0,
"pv4current": 0,
"pv4watt": 0,
"pvpowerout": 4294966929,
"pvfrequentie": 5001,
"pvgridvoltage": 2359,
"pvgridcurrent": 7,
"pvgridpower": 1651,
"pvgridvoltage2": 2367,
"pvgridcurrent2": 8,
"pvgridpower2": 1893,
"pvgridvoltage3": 2379,
"pvgridcurrent3": 8,
"pvgridpower3": 1903,
"vacrs": 4084,
"vacst": 4118,
"vactr": 4104,
"ptousertotal": 8099,
"ptogridtotal": 0,
"ptoloadtotal": 8239,
"totworktime": 79652,
"pvenergytoday": 178,
"pvenergytotal": 178,
"epvtotal ": 162,
"epv1today ": 79,
"epv1total": 79,
"epv2today": 83,
"epv2total": 83,
"epv3today": 0,
"epv3total": 0,
"etousertoday": 64,
"etousertotal": 64,
"etogridtoday": 1,
"etogridtotal": 1,
"eloadtoday": 237,
"eloadtotal": 0,
"deratingmode": 0,
"iso": 15997,
"dcir": 0,
"dcis": 0,
"dcit": 0,
"gfci": 137645,
"pvtemperature": 296,
"pvipmtemperature": 410,
"temp3": 296,
"temp4": 0,
"temp5": 319,
"pbusvoltage": 3311,
"nbusvoltage": 3265,
"ipf": 20000,
"realoppercent": 0,
"opfullwatt": 150000,
"standbyflag": 0,
"faultcode": 0,
"warningcode": 0,
"systemfaultword0": 0,
"systemfaultword1": 0,
"systemfaultword2": 0,
"systemfaultword3": 0,
"systemfaultword4": 0,
"systemfaultword5": 0,
"systemfaultword6": 0,
"systemfaultword7": 0,
"invstartdelaytime": 60,
"bdconoffstate": 1,
"drycontactstate": 0,
"edischrtoday": 103,
"edischrtotal": 1843,
"echrtoday": 91,
"echrtotal": 3005,
"eacchrtoday": 5,
"eacchrtotal": 5,
"priority": 1,
"epsfac": 0,
"epsvac1": 0,
"epsiac1": 0,
"epspac1": 0,
"epsvac2": 0,
"epsiac2": 0,
"epspac2": 0,
"epsvac3": 0,
"epsiac3": 0,
"epspac3": 0,
"epspac": 0,
"loadpercent": 0,
"pf": 10000,
"dcv": 0,
"bdc1_sysstatemode": 513,
"bdc1_faultcode": 0,
"bdc1_warncode": 701,
"bdc1_vbat": 6582,
"bdc1_ibat": 0,
"bdc1_soc": 11,
"bdc1_vbus1": 6582,
"bdc1_vbus2": 3303,
"bdc1_ibb": 0,
"bdc1_illc": 0,
"bdc1_tempa": 409,
"bdc1_tempb": 291,
"bdc1_pdischr": 100,
"bdc1_pchr": 0,
"bdc1_edischrtotal": 1843,
"bdc1_echrtotal": 3005,
"bdc1_flag": 1,
"bdc2_sysstatemode": 17,
"bdc2_faultcode": 12,
"bdc2_warncode": 248,
"bdc2_vbat": 266,
"bdc2_ibat": 223,
"bdc2_soc": 19,
"bdc2_vbus1": 49,
"bdc2_vbus2": 11,
"bdc2_ibb": 11,
"bdc2_illc": 4,
"bdc2_tempa": 0,
"bdc2_tempb": 394,
"bdc2_pdischr": 26214400,
"bdc2_pchr": 0,
"bdc2_edischrtotal": 0,
"bdc2_echrtotal": 0,
"bdc2_flag": 0,
"bms_status": 4,
"bms_error": 0,
"bms_warninfo": 0,
"bms_soc": 11,
"bms_batteryvolt": 4041,
"bms_batterycurr": 0,
"bms_batterytemp": 0,
"bms_maxcurr": 2200,
"bms_deltavolt": 2200,
"bms_cyclecnt": 0,
"bms_soh": 100,
"bms_constantvolt": 568,
"bms_bms_info": 464,
"bms_packinfo": 0,
"bms_usingcap": 0,
"bms_fw": 1400,
"bms_mcuversion": 0,
"bms_commtype": 1
}
}

View File

@ -0,0 +1,39 @@
{
"device": "GZL0DA804M",
"time": "2023-12-26T21:48:36",
"buffered": "no",
"values": {
"datalogserial": "GZL0DA804M",
"pvserial": "KLN0D6L034",
"voltage_l1": 2355,
"voltage_l2": 2374,
"voltage_l3": 2376,
"Current_l1": 22,
"Current_l2": 15,
"Current_l3": 12,
"act_power_l1": 4909,
"act_power_l2": 1710,
"act_power_l3": 1478,
"app_power_l1": 5058,
"app_power_l2": 3321,
"app_power_l3": 2712,
"react_power_l1": -1222,
"react_power_l2": -2847,
"react_power_l3": -2275,
"powerfactor_l1": 936,
"powerfactor_l2": 481,
"powerfactor_l3": 502,
"pos_rev_act_power": 8098,
"pos_act_power": 8098,
"rev_act_power": 8098,
"app_power": 11091,
"react_power": -6346,
"powerfactor": 690,
"frequency": 500,
"L1-2_voltage": 4095,
"L2-3_voltage": 4113,
"L3-1_voltage": 4097,
"pos_act_energy": 9587,
"rev_act_energy": 1387
}
}

View File

@ -0,0 +1,36 @@
{
"device": "INVERTID",
"time": "2021-02-13T16:34:28",
"buffered": "no",
"values": {
"pvstatus": 1,
"pvpowerin": 1622,
"pv1voltage": 2475,
"pv1current": 6,
"pv1watt": 1622,
"pv2voltage": 0,
"pv2current": 0,
"pv2watt": 0,
"pvpowerout": 1460,
"pvfrequentie": 4997,
"pvgridvoltage": 2353,
"pvgridcurrent": 7,
"pvgridpower": 1460,
"pvgridvoltage2": 0,
"pvgridcurrent2": 0,
"pvgridpower2": 0,
"pvgridvoltage3": 0,
"pvgridcurrent3": 0,
"pvgridpower3": 0,
"pvenergytoday": 87,
"pvenergytotal": 43265,
"totworktime": 65503878,
"pvtemperature": 273,
"pvipmtemperature": 0,
"epv1today": 90,
"epv1total": 45453,
"epv2today": 0,
"epv2total": 0,
"epvtotal": 45453
}
}

View File

@ -0,0 +1,75 @@
{
"device": "KUM0CLU03Y",
"time": "2023-09-10T12:23:13",
"buffered": "no",
"values": {
"datalogserial": "GPG0DBJ05N",
"pvserial": "KUM0CLU03Y",
"pvstatus": 5,
"pvpowerin": 16100,
"pv1voltage": 1805,
"pv1current": 89,
"pv1watt": 16169,
"pv2voltage": 0,
"pv2current": 0,
"pv2watt": 0,
"pvpowerout": 4285,
"pvfrequentie": 5003,
"pvgridvoltage": 2443,
"pvgridcurrent": 18,
"pvgridpower": 4609,
"pvgridvoltage2": 0,
"pvgridcurrent2": 0,
"pvgridpower2": 0,
"pvgridvoltage3": 0,
"pvgridcurrent3": 0,
"pvgridpower3": 0,
"totworktime": 6723587,
"eactoday": 27,
"pvenergytoday": 27,
"eactotal": 3571,
"epvtotal": 4105,
"epv1today": 64,
"epv1total": 4057,
"epv2today": 0,
"epv2total": 0,
"pvtemperature": 576,
"pvipmtemperature": 527,
"pvboosttemp": 572,
"bat_dsp": 541,
"eacharge_today": 10,
"eacharge_total": 277,
"batterytype": 1,
"uwsysworkmode": 5,
"systemfaultword0": 0,
"systemfaultword1": 0,
"systemfaultword2": 0,
"systemfaultword3": 0,
"systemfaultword4": 0,
"systemfaultword5": 0,
"systemfaultword6": 0,
"systemfaultword7": 0,
"pdischarge1": 0,
"p1charge1": 10284,
"vbat": 539,
"SOC": 69,
"pactouserr": 0,
"pactousertot": 0,
"pactogridr": 0,
"pactogridtot": 0,
"plocaloadr": 5800,
"plocaloadtot": 5800,
"spdspstatus": 5,
"spbusvolt": 3290,
"etouser_tod": 54,
"etouser_tot": 2330,
"etogrid_tod": 1,
"etogrid_tot": 707,
"edischarge1_tod": 3,
"edischarge1_tot": 1652,
"eharge1_tod": 41,
"eharge1_tot": 1524,
"elocalload_tod": 80,
"elocalload_tot": 5856
}
}

View File

@ -160,8 +160,9 @@
<module>org.openhab.binding.globalcache</module>
<module>org.openhab.binding.gpstracker</module>
<module>org.openhab.binding.gree</module>
<module>org.openhab.binding.groupepsa</module>
<module>org.openhab.binding.groheondus</module>
<module>org.openhab.binding.groupepsa</module>
<module>org.openhab.binding.growatt</module>
<module>org.openhab.binding.guntamatic</module>
<module>org.openhab.binding.haassohnpelletstove</module>
<module>org.openhab.binding.harmonyhub</module>