mirror of
https://github.com/openhab/openhab-addons.git
synced 2025-01-25 06:45:57 +01:00
[growatt] Binding for Growatt solar inverters (#15120)
* [growatt] initial contribution Signed-off-by: Andrew Fiddian-Green <software@whitebear.ch>
This commit is contained in:
parent
9e1f87db86
commit
6f7b5b5f31
@ -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
|
||||
|
@ -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>
|
||||
|
13
bundles/org.openhab.binding.growatt/NOTICE
Normal file
13
bundles/org.openhab.binding.growatt/NOTICE
Normal 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
|
349
bundles/org.openhab.binding.growatt/README.md
Normal file
349
bundles/org.openhab.binding.growatt/README.md
Normal 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.
|
17
bundles/org.openhab.binding.growatt/pom.xml
Normal file
17
bundles/org.openhab.binding.growatt/pom.xml
Normal 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>
|
@ -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>
|
@ -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");
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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.");
|
||||
}
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
@ -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 = "";
|
||||
}
|
@ -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);
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
@ -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 : "";
|
||||
}
|
||||
}
|
@ -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 : "";
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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 : "";
|
||||
}
|
||||
}
|
@ -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();
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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());
|
||||
}
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
@ -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> </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);
|
||||
}
|
||||
}
|
@ -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>
|
@ -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
|
@ -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>
|
@ -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"));
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
@ -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>
|
||||
|
Loading…
Reference in New Issue
Block a user