Skip to content

Commit fd70c5d

Browse files
authored
[sunsynk] Initial contribution (openhab#16753)
* Initial Signed-off-by: LeeC77 <[email protected]>
1 parent c1dbc33 commit fd70c5d

38 files changed

+3888
-0
lines changed

CODEOWNERS

+1
Original file line numberDiff line numberDiff line change
@@ -360,6 +360,7 @@
360360
/bundles/org.openhab.binding.speedtest/ @MikeTheTux
361361
/bundles/org.openhab.binding.spotify/ @Hilbrand
362362
/bundles/org.openhab.binding.squeezebox/ @digitaldan @mhilbush
363+
/bundles/org.openhab.binding.sunsynk/ @leeC77
363364
/bundles/org.openhab.binding.surepetcare/ @renescherer @HerzScheisse
364365
/bundles/org.openhab.binding.synopanalyzer/ @clinique
365366
/bundles/org.openhab.binding.systeminfo/ @mherwege

bom/openhab-addons/pom.xml

+5
Original file line numberDiff line numberDiff line change
@@ -1796,6 +1796,11 @@
17961796
<artifactId>org.openhab.binding.squeezebox</artifactId>
17971797
<version>${project.version}</version>
17981798
</dependency>
1799+
<dependency>
1800+
<groupId>org.openhab.addons.bundles</groupId>
1801+
<artifactId>org.openhab.binding.sunsynk</artifactId>
1802+
<version>${project.version}</version>
1803+
</dependency>
17991804
<dependency>
18001805
<groupId>org.openhab.addons.bundles</groupId>
18011806
<artifactId>org.openhab.binding.surepetcare</artifactId>
+13
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
This content is produced and maintained by the openHAB project.
2+
3+
* Project home: https://www.openhab.org
4+
5+
== Declared Project Licenses
6+
7+
This program and the accompanying materials are made available under the terms
8+
of the Eclipse Public License 2.0 which is available at
9+
https://www.eclipse.org/legal/epl-2.0/.
10+
11+
== Source Code
12+
13+
https://github.com/openhab/openhab-addons

bundles/org.openhab.binding.sunsynk/README.md

+202
Large diffs are not rendered by default.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://maven.apache.org/POM/4.0.0"
3+
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
4+
5+
<modelVersion>4.0.0</modelVersion>
6+
7+
<parent>
8+
<groupId>org.openhab.addons.bundles</groupId>
9+
<artifactId>org.openhab.addons.reactor.bundles</artifactId>
10+
<version>4.3.0-SNAPSHOT</version>
11+
</parent>
12+
13+
<artifactId>org.openhab.binding.sunsynk</artifactId>
14+
15+
<name>openHAB Add-ons :: Bundles :: SunSynk Binding</name>
16+
</project>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<features name="org.openhab.binding.sunsynk-${project.version}" xmlns="http://karaf.apache.org/xmlns/features/v1.4.0">
3+
<repository>mvn:org.openhab.core.features.karaf/org.openhab.core.features.karaf.openhab-core/${ohc.version}/xml/features</repository>
4+
5+
<feature name="openhab-binding-sunsynk" description="SunSynk Binding" version="${project.version}">
6+
<feature>openhab-runtime-base</feature>
7+
<bundle start-level="80">mvn:org.openhab.addons.bundles/org.openhab.binding.sunsynk/${project.version}</bundle>
8+
</feature>
9+
</features>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
/**
2+
* Copyright (c) 2010-2024 Contributors to the openHAB project
3+
*
4+
* See the NOTICE file(s) distributed with this work for additional
5+
* information.
6+
*
7+
* This program and the accompanying materials are made available under the
8+
* terms of the Eclipse Public License 2.0 which is available at
9+
* http://www.eclipse.org/legal/epl-2.0
10+
*
11+
* SPDX-License-Identifier: EPL-2.0
12+
*/
13+
package org.openhab.binding.sunsynk.internal;
14+
15+
import org.eclipse.jdt.annotation.NonNullByDefault;
16+
import org.openhab.core.thing.ThingTypeUID;
17+
18+
/**
19+
* The {@link SunSynkBindingConstants} class defines common constants, which are
20+
* used across the whole binding.
21+
*
22+
* @author Lee Charlton - Initial contribution
23+
*/
24+
25+
@NonNullByDefault
26+
public class SunSynkBindingConstants {
27+
28+
private static final String BINDING_ID = "sunsynk";
29+
30+
// List of all Thing Type UIDs
31+
public static final ThingTypeUID BRIDGE_TYPE_ACCOUNT = new ThingTypeUID(BINDING_ID, "account");
32+
public static final ThingTypeUID THING_TYPE_INVERTER = new ThingTypeUID(BINDING_ID, "inverter");
33+
34+
// List of all Channel ids
35+
public static final String CHANNEL_BATTERY_INTERVAL_1_GRID_CHARGE = "interval-1-grid-charge";
36+
public static final String CHANNEL_BATTERY_INTERVAL_2_GRID_CHARGE = "interval-2-grid-charge";
37+
public static final String CHANNEL_BATTERY_INTERVAL_3_GRID_CHARGE = "interval-3-grid-charge";
38+
public static final String CHANNEL_BATTERY_INTERVAL_4_GRID_CHARGE = "interval-4-grid-charge";
39+
public static final String CHANNEL_BATTERY_INTERVAL_5_GRID_CHARGE = "interval-5-grid-charge";
40+
public static final String CHANNEL_BATTERY_INTERVAL_6_GRID_CHARGE = "interval-6-grid-charge";
41+
42+
public static final String CHANNEL_BATTERY_INTERVAL_1_GEN_CHARGE = "interval-1-gen-charge";
43+
public static final String CHANNEL_BATTERY_INTERVAL_2_GEN_CHARGE = "interval-2-gen-charge";
44+
public static final String CHANNEL_BATTERY_INTERVAL_3_GEN_CHARGE = "interval-3-gen-charge";
45+
public static final String CHANNEL_BATTERY_INTERVAL_4_GEN_CHARGE = "interval-4-gen-charge";
46+
public static final String CHANNEL_BATTERY_INTERVAL_5_GEN_CHARGE = "interval-5-gen-charge";
47+
public static final String CHANNEL_BATTERY_INTERVAL_6_GEN_CHARGE = "interval-6-gen-charge";
48+
49+
public static final String CHANNEL_BATTERY_INTERVAL_1_TIME = "interval-1-grid-time";
50+
public static final String CHANNEL_BATTERY_INTERVAL_2_TIME = "interval-2-grid-time";
51+
public static final String CHANNEL_BATTERY_INTERVAL_3_TIME = "interval-3-grid-time";
52+
public static final String CHANNEL_BATTERY_INTERVAL_4_TIME = "interval-4-grid-time";
53+
public static final String CHANNEL_BATTERY_INTERVAL_5_TIME = "interval-5-grid-time";
54+
public static final String CHANNEL_BATTERY_INTERVAL_6_TIME = "interval-6-grid-time";
55+
56+
public static final String CHANNEL_BATTERY_INTERVAL_1_CAPACITY = "interval-1-grid-capacity";
57+
public static final String CHANNEL_BATTERY_INTERVAL_2_CAPACITY = "interval-2-grid-capacity";
58+
public static final String CHANNEL_BATTERY_INTERVAL_3_CAPACITY = "interval-3-grid-capacity";
59+
public static final String CHANNEL_BATTERY_INTERVAL_4_CAPACITY = "interval-4-grid-capacity";
60+
public static final String CHANNEL_BATTERY_INTERVAL_5_CAPACITY = "interval-5-grid-capacity";
61+
public static final String CHANNEL_BATTERY_INTERVAL_6_CAPACITY = "interval-6-grid-capacity";
62+
63+
public static final String CHANNEL_BATTERY_INTERVAL_1_POWER_LIMIT = "interval-1-grid-power-limit";
64+
public static final String CHANNEL_BATTERY_INTERVAL_2_POWER_LIMIT = "interval-2-grid-power-limit";
65+
public static final String CHANNEL_BATTERY_INTERVAL_3_POWER_LIMIT = "interval-3-grid-power-limit";
66+
public static final String CHANNEL_BATTERY_INTERVAL_4_POWER_LIMIT = "interval-4-grid-power-limit";
67+
public static final String CHANNEL_BATTERY_INTERVAL_5_POWER_LIMIT = "interval-5-grid-power-limit";
68+
public static final String CHANNEL_BATTERY_INTERVAL_6_POWER_LIMIT = "interval-6-grid-power-limit";
69+
70+
public static final String CHANNEL_INVERTER_GRID_POWER = "inverter-grid-power";
71+
public static final String CHANNEL_INVERTER_GRID_VOLTAGE = "inverter-grid-voltage";
72+
public static final String CHANNEL_INVERTER_GRID_CURRENT = "inverter-grid-current";
73+
74+
public static final String CHANNEL_INVERTER_AC_TEMPERATURE = "inverter-ac-temperature";
75+
public static final String CHANNEL_INVERTER_DC_TEMPERATURE = "inverter-dc-temperature";
76+
77+
public static final String CHANNEL_BATTERY_VOLTAGE = "battery-grid-voltage";
78+
public static final String CHANNEL_BATTERY_CURRENT = "battery-grid-current";
79+
public static final String CHANNEL_BATTERY_POWER = "battery-grid-power";
80+
public static final String CHANNEL_BATTERY_SOC = "battery-soc";
81+
public static final String CHANNEL_BATTERY_TEMPERATURE = "battery-temperature";
82+
83+
public static final String CHANNEL_INVERTER_SOLAR_ENERGY_TODAY = "inverter-solar-energy-today";
84+
public static final String CHANNEL_INVERTER_SOLAR_ENERGY_TOTAL = "inverter-solar-energy-total";
85+
public static final String CHANNEL_INVERTER_SOLAR_POWER_NOW = "inverter-solar-power-now";
86+
87+
public static final String CHANNEL_INVERTER_CONTROL_TIMER = "inverter-control-timer";
88+
public static final String CHANNEL_INVERTER_CONTROL_ENERGY_PATTERN = "inverter-control-energy-pattern";
89+
public static final String CHANNEL_INVERTER_CONTROL_WORK_MODE = "inverter-control-work-mode";
90+
91+
// Thing Discovery
92+
public static final String CONFIG_GATE_SERIAL = "gsn";
93+
public static final String CONFIG_SERIAL = "serialnumber";
94+
public static final String CONFIG_NAME = "alias";
95+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,204 @@
1+
/**
2+
* Copyright (c) 2010-2024 Contributors to the openHAB project
3+
*
4+
* See the NOTICE file(s) distributed with this work for additional
5+
* information.
6+
*
7+
* This program and the accompanying materials are made available under the
8+
* terms of the Eclipse Public License 2.0 which is available at
9+
* http://www.eclipse.org/legal/epl-2.0
10+
*
11+
* SPDX-License-Identifier: EPL-2.0
12+
*/
13+
14+
package org.openhab.binding.sunsynk.internal.api;
15+
16+
import java.io.ByteArrayInputStream;
17+
import java.io.IOException;
18+
import java.io.InputStream;
19+
import java.nio.charset.StandardCharsets;
20+
import java.time.Instant;
21+
import java.util.ArrayList;
22+
import java.util.Objects;
23+
import java.util.Properties;
24+
25+
import javax.ws.rs.core.HttpHeaders;
26+
import javax.ws.rs.core.MediaType;
27+
28+
import org.eclipse.jdt.annotation.NonNullByDefault;
29+
import org.eclipse.jdt.annotation.Nullable;
30+
import org.eclipse.jetty.http.HttpMethod;
31+
import org.openhab.binding.sunsynk.internal.api.dto.APIdata;
32+
import org.openhab.binding.sunsynk.internal.api.dto.Client;
33+
import org.openhab.binding.sunsynk.internal.api.dto.Details;
34+
import org.openhab.binding.sunsynk.internal.api.dto.Inverter;
35+
import org.openhab.binding.sunsynk.internal.api.dto.SunSynkLogin;
36+
import org.openhab.binding.sunsynk.internal.api.dto.TokenRefresh;
37+
import org.openhab.binding.sunsynk.internal.api.exception.SunSynkAuthenticateException;
38+
import org.openhab.binding.sunsynk.internal.api.exception.SunSynkInverterDiscoveryException;
39+
import org.openhab.binding.sunsynk.internal.api.exception.SunSynkTokenException;
40+
import org.openhab.core.io.net.http.HttpUtil;
41+
import org.slf4j.Logger;
42+
import org.slf4j.LoggerFactory;
43+
44+
import com.google.gson.Gson;
45+
import com.google.gson.JsonSyntaxException;
46+
47+
/**
48+
* The {@link AccountController} is the internal class for a Sunsynk Connect
49+
* Account.
50+
*
51+
* @author Lee Charlton - Initial contribution
52+
*/
53+
54+
@NonNullByDefault
55+
public class AccountController {
56+
private static final int TIMEOUT_IN_MS = 4000;
57+
private final Logger logger = LoggerFactory.getLogger(AccountController.class);
58+
private static final String BEARER_TYPE = "Bearer ";
59+
private Client sunAccount = new Client();
60+
61+
public AccountController() {
62+
}
63+
64+
/**
65+
* Authenticates a Sunsynk Connect API account using a username and password.
66+
*
67+
* @param username The username you use with Sunsynk Connect App
68+
* @param password The password you use with Sunsynk Connect App
69+
* @throws SunSynkAuthenticateException
70+
* @throws SunSynkTokenException
71+
*/
72+
public void authenticate(String username, String password)
73+
throws SunSynkAuthenticateException, SunSynkTokenException {
74+
String payload = makeLoginBody(username, password);
75+
sendHttp(payload);
76+
}
77+
78+
/**
79+
* Checks if a Sunsynk Connect account token is expired and gets a new one if required.
80+
*
81+
* @param username
82+
* @throws SunSynkAuthenticateException
83+
* @throws SunSynkTokenException
84+
*/
85+
public void refreshAccount(String username) throws SunSynkAuthenticateException, SunSynkTokenException {
86+
Long expiresIn = this.sunAccount.getExpiresIn();
87+
Long issuedAt = this.sunAccount.getIssuedAt();
88+
if ((issuedAt + expiresIn) - Instant.now().getEpochSecond() > 30) { // > 30 seconds
89+
logger.debug("Account configuration token not expired.");
90+
return;
91+
}
92+
logger.debug("Account configuration token expired : {}", this.sunAccount.getData().toString());
93+
String payload = makeRefreshBody(username, this.sunAccount.getRefreshTokenString());
94+
sendHttp(payload);
95+
}
96+
97+
@SuppressWarnings("unused") // We need client to be nullable. Then we check for null. Without this compiler warns of
98+
// unsed block under null check
99+
private void sendHttp(String payload) throws SunSynkAuthenticateException, SunSynkTokenException {
100+
Gson gson = new Gson();
101+
String response = "";
102+
String httpsURL = makeLoginURL("oauth/token");
103+
Properties headers = new Properties();
104+
headers.setProperty(HttpHeaders.ACCEPT, MediaType.APPLICATION_JSON);
105+
InputStream stream = new ByteArrayInputStream(payload.getBytes(StandardCharsets.UTF_8));
106+
try {
107+
response = HttpUtil.executeUrl(HttpMethod.POST.asString(), httpsURL, headers, stream,
108+
MediaType.APPLICATION_JSON, TIMEOUT_IN_MS);
109+
110+
@Nullable
111+
Client client = gson.fromJson(response, Client.class);
112+
if (client == null) {
113+
throw new SunSynkAuthenticateException(
114+
"Sun Synk Account could not be authenticated: Try re-enabling account");
115+
}
116+
this.sunAccount = client;
117+
} catch (IOException | JsonSyntaxException e) {
118+
throw new SunSynkAuthenticateException("Sun Synk Account could not be authenticated", e);
119+
}
120+
if (this.sunAccount.getCode() == 102) {
121+
logger.debug("Sun Synk Account could not be authenticated: {}.", this.sunAccount.getMsg());
122+
throw new SunSynkAuthenticateException(
123+
"Sun Synk Accountfailed to authenticate: Check your password or email.");
124+
}
125+
if (this.sunAccount.getStatus() == 404) {
126+
logger.debug("Sun Synk Account could not be authenticated: 404 {} {}.", this.sunAccount.getError(),
127+
this.sunAccount.getPath());
128+
throw new SunSynkAuthenticateException("Sun Synk Accountfailed to authenticate: 404 Not Found.");
129+
}
130+
getToken();
131+
}
132+
133+
private void getToken() throws SunSynkAuthenticateException {
134+
APIdata data = this.sunAccount.getData();
135+
APIdata.staticAccessToken = data.getAccessToken();
136+
}
137+
138+
/**
139+
* Discovers a list of all inverter tied to a Sunsynk Connect Account
140+
*
141+
* @return List of connected inveters
142+
* @throws SunSynkInverterDiscoveryException
143+
*/
144+
@SuppressWarnings("unused")
145+
public ArrayList<Inverter> getDetails() throws SunSynkInverterDiscoveryException {
146+
Details output = new Details();
147+
ArrayList<Inverter> inverters = new ArrayList<>();
148+
try {
149+
Gson gson = new Gson();
150+
Properties headers = new Properties();
151+
String response = "";
152+
153+
String httpsURL = makeLoginURL(
154+
"api/v1/inverters?page=1&limit=10&total=0&status=-1&sn=&plantId=&type=-2&softVer=&hmiVer=&agentCompanyId=-1&gsn=");
155+
headers.setProperty(HttpHeaders.ACCEPT, MediaType.APPLICATION_JSON);
156+
headers.setProperty(HttpHeaders.AUTHORIZATION, BEARER_TYPE + APIdata.staticAccessToken);
157+
response = HttpUtil.executeUrl(HttpMethod.GET.asString(), httpsURL, headers, null,
158+
MediaType.APPLICATION_JSON, TIMEOUT_IN_MS);
159+
@Nullable
160+
Details maybeDeats = gson.fromJson(response, Details.class);
161+
if (maybeDeats == null) {
162+
throw new SunSynkInverterDiscoveryException("Failed to discover Inverters");
163+
}
164+
output = maybeDeats;
165+
} catch (IOException | JsonSyntaxException e) {
166+
if (logger.isDebugEnabled()) {
167+
String message = Objects.requireNonNullElse(e.getMessage(), "unkown error message");
168+
Throwable cause = e.getCause();
169+
String causeMessage = cause != null ? Objects.requireNonNullElse(cause.getMessage(), "unkown cause")
170+
: "unkown cause";
171+
logger.debug("Error attempting to find inverters registered to account: Msg = {}. Cause = {}.", message,
172+
causeMessage);
173+
}
174+
throw new SunSynkInverterDiscoveryException("Failed to discover Inverters", e);
175+
}
176+
inverters = output.getInverters(APIdata.staticAccessToken);
177+
return inverters;
178+
}
179+
180+
private static String makeLoginURL(String path) {
181+
return "https://api.sunsynk.net" + "/" + path;
182+
}
183+
184+
private static String makeLoginBody(String username, String password) {
185+
Gson gson = new Gson();
186+
SunSynkLogin login = new SunSynkLogin(username, password);
187+
return gson.toJson(login);
188+
}
189+
190+
private static String makeRefreshBody(String username, String refreshToken) {
191+
Gson gson = new Gson();
192+
TokenRefresh refresh = new TokenRefresh(username, refreshToken);
193+
return gson.toJson(refresh);
194+
}
195+
196+
@Override
197+
public String toString() {
198+
try {
199+
return this.sunAccount.getData().toString();
200+
} catch (SunSynkAuthenticateException e) {
201+
return "Tried to print client data, value is null.";
202+
}
203+
}
204+
}

0 commit comments

Comments
 (0)