[enphase] Add Entrez/JWT support for newer software versions of Envoy (#15077)

Co-authored-by: Joe Inkenbrandt <joe@inkenbrandt.com>
Signed-off-by: Hilbrand Bouwkamp <hilbrand@h72.nl>
This commit is contained in:
Hilbrand Bouwkamp 2023-10-05 21:29:16 +02:00 committed by GitHub
parent 75e51119fa
commit d58d8b068c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
22 changed files with 1321 additions and 191 deletions

View File

@ -11,3 +11,10 @@ https://www.eclipse.org/legal/epl-2.0/.
== Source Code
https://github.com/openhab/openhab-addons
== Third-party Content
jsoup
* License: MIT License
* Project: https://jsoup.org/
* Source: https://github.com/jhy/jsoup

View File

@ -22,18 +22,47 @@ The binding auto detects which data is available and will report this in the log
## Discovery
The binding can discover Envoy gateways, micro inverters and relays.
If login access is needed the Bridge `envoy` needs to be configured after discovering and adding before other things can be discovered.
In that case, after configuring the login, run discovery again.
## Thing Configuration
The Envoy gateway thing `envoy` has the following configuration options:
### Bridge configuration
Depending on the software version of the Envoy gateway thing `envoy` there are different configuration options needed.
Newer versions of the Envoy software (> version 7) require a different authentication method.
Because the configuration is different, different bridge things are available.
The following options are relevant for all envoy versions:
| parameter | required | description |
|--------------|----------|-------------------------------------------------------------------------------------------------------------|
| serialNumber | yes | The serial number of the Envoy gateway which can be found on the gateway |
| hostname | no | The host name/ip address of the Envoy gateway. Leave empty to auto detect |
| refresh | no | Period between data updates. The default is the same 5 minutes the data is actually refreshed on the Envoy |
#### Envoy below version 7
For Envoy versions below 7 has the following authentication configuration options are relevant:
| parameter | required | description |
|--------------|----------|-------------------------------------------------------------------------------------------------------------|
| username | no | The user name to the Envoy gateway. Leave empty when using the default user name |
| password | no | The password to the Envoy gateway. Leave empty when using the default password |
| refresh | no | Period between data updates. The default is the same 5 minutes the data is actual refreshed on the Envoy |
#### Envoy from version 7
For Envoy versions 7 and newer has the following authentication configuration options are relevant:
| parameter | required | description |
|--------------|----------|-------------------------------------------------------------------------------------------------------------|
| autoJwt | yes | Specify if the JWT access token should be obtained by logging in or if the jwt is provided manually |
| username | yes/no | The user name to the Entrez server. Required if auto Jwt is true |
| password | yes/no | The password to the Entrez server. Required if auto Jwt is true |
| siteName | yes/no | The name of the site. Can be found above the Site Id in the Enphase app. Required when autoJwt is true |
| jwt | yes/no | The jwt is required if autoJWT is false, if it's true jwt is not used |
### Thing configuration
The micro inverter `inverter` and `relay` things have only 1 parameter:

View File

@ -14,4 +14,17 @@
<name>openHAB Add-ons :: Bundles :: Enphase Binding</name>
<properties>
<jsoup.version>1.15.3</jsoup.version>
</properties>
<dependencies>
<dependency>
<groupId>org.jsoup</groupId>
<artifactId>jsoup</artifactId>
<version>${jsoup.version}</version>
<scope>provided</scope>
</dependency>
</dependencies>
</project>

View File

@ -5,6 +5,7 @@
<feature name="openhab-binding-enphase" description="Enphase Binding" version="${project.version}">
<feature>openhab-runtime-base</feature>
<bundle dependency="true">mvn:org.jsoup/jsoup/1.15.3</bundle>
<bundle start-level="80">mvn:org.openhab.addons.bundles/org.openhab.binding.enphase/${project.version}</bundle>
</feature>
</features>

View File

@ -12,19 +12,21 @@
*/
package org.openhab.binding.enphase.internal;
import static org.openhab.binding.enphase.internal.EnphaseBindingConstants.*;
import static org.openhab.binding.enphase.internal.EnphaseBindingConstants.THING_TYPE_ENPHASE_ENVOY;
import static org.openhab.binding.enphase.internal.EnphaseBindingConstants.THING_TYPE_ENPHASE_INVERTER;
import static org.openhab.binding.enphase.internal.EnphaseBindingConstants.THING_TYPE_ENPHASE_RELAY;
import java.util.Set;
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.openhab.binding.enphase.internal.handler.EnphaseInverterHandler;
import org.openhab.binding.enphase.internal.handler.EnphaseRelayHandler;
import org.openhab.binding.enphase.internal.handler.EnvoyBridgeHandler;
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;
@ -33,11 +35,11 @@ import org.openhab.core.thing.binding.ThingHandler;
import org.openhab.core.thing.binding.ThingHandlerFactory;
import org.osgi.service.component.annotations.Activate;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Deactivate;
import org.osgi.service.component.annotations.Reference;
/**
* The {@link EnphaseHandlerFactory} is responsible for creating things and thing
* handlers.
* The {@link EnphaseHandlerFactory} is responsible for creating things and thing handlers.
*
* @author Hilbrand Bouwkamp - Initial contribution
*/
@ -49,16 +51,33 @@ public class EnphaseHandlerFactory extends BaseThingHandlerFactory {
THING_TYPE_ENPHASE_INVERTER, THING_TYPE_ENPHASE_RELAY);
private final MessageTranslator messageTranslator;
private final HttpClient commonHttpClient;
private final EnvoyHostAddressCache envoyHostAddressCache;
private final HttpClient httpClient;
@Activate
public EnphaseHandlerFactory(final @Reference LocaleProvider localeProvider,
final @Reference TranslationProvider i18nProvider, final @Reference HttpClientFactory httpClientFactory,
@Reference final EnvoyHostAddressCache envoyHostAddressCache) {
final @Reference TranslationProvider i18nProvider,
final @Reference EnvoyHostAddressCache envoyHostAddressCache) {
messageTranslator = new MessageTranslator(localeProvider, i18nProvider);
commonHttpClient = httpClientFactory.getCommonHttpClient();
this.envoyHostAddressCache = envoyHostAddressCache;
// Note: Had to switch to using a locally generated httpClient as
// the Envoy server went to a self-signed SSL connection and this
// was the only way to set the client to ignore SSL errors
this.httpClient = new HttpClient(new SslContextFactory.Client(true));
startHttpClient();
}
private void startHttpClient() {
try {
httpClient.start();
} catch (final Exception ex) {
throw new IllegalStateException("Could not start HttpClient.", ex);
}
}
@Deactivate
public void deactivate() {
httpClient.destroy();
}
@Override
@ -71,7 +90,7 @@ public class EnphaseHandlerFactory extends BaseThingHandlerFactory {
final ThingTypeUID thingTypeUID = thing.getThingTypeUID();
if (THING_TYPE_ENPHASE_ENVOY.equals(thingTypeUID)) {
return new EnvoyBridgeHandler((Bridge) thing, commonHttpClient, envoyHostAddressCache);
return new EnvoyBridgeHandler((Bridge) thing, httpClient, envoyHostAddressCache);
} else if (THING_TYPE_ENPHASE_INVERTER.equals(thingTypeUID)) {
return new EnphaseInverterHandler(thing, messageTranslator);
} else if (THING_TYPE_ENPHASE_RELAY.equals(thingTypeUID)) {

View File

@ -29,6 +29,9 @@ public class EnvoyConfiguration {
public String hostname = "";
public String username = DEFAULT_USERNAME;
public String password = "";
public String jwt = "";
public boolean autoJwt = true;
public String siteName = "";
public int refresh = DEFAULT_REFRESH_MINUTES;
@Override

View File

@ -12,7 +12,9 @@
*/
package org.openhab.binding.enphase.internal.discovery;
import static org.openhab.binding.enphase.internal.EnphaseBindingConstants.*;
import static org.openhab.binding.enphase.internal.EnphaseBindingConstants.CONFIG_SERIAL_NUMBER;
import static org.openhab.binding.enphase.internal.EnphaseBindingConstants.THING_TYPE_ENPHASE_INVERTER;
import static org.openhab.binding.enphase.internal.EnphaseBindingConstants.THING_TYPE_ENPHASE_RELAY;
import java.util.HashMap;
import java.util.Map;
@ -21,6 +23,7 @@ import java.util.Set;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.enphase.internal.EnphaseBindingConstants;
import org.openhab.binding.enphase.internal.EnphaseBindingConstants.EnphaseDeviceType;
import org.openhab.binding.enphase.internal.dto.InventoryJsonDTO.DeviceDTO;
import org.openhab.binding.enphase.internal.dto.InverterDTO;
@ -124,7 +127,7 @@ public class EnphaseDevicesDiscoveryService extends AbstractDiscoveryService
private void discover(final ThingUID bridgeID, final String serialNumber, final ThingTypeUID typeUID,
final String label) {
final String shortSerialNumber = defaultPassword(serialNumber);
final String shortSerialNumber = EnphaseBindingConstants.defaultPassword(serialNumber);
final ThingUID thingUID = new ThingUID(typeUID, bridgeID, shortSerialNumber);
final Map<String, Object> properties = new HashMap<>(1);

View File

@ -12,7 +12,12 @@
*/
package org.openhab.binding.enphase.internal.discovery;
import static org.openhab.binding.enphase.internal.EnphaseBindingConstants.*;
import static org.openhab.binding.enphase.internal.EnphaseBindingConstants.CONFIG_HOSTNAME;
import static org.openhab.binding.enphase.internal.EnphaseBindingConstants.CONFIG_SERIAL_NUMBER;
import static org.openhab.binding.enphase.internal.EnphaseBindingConstants.DISCOVERY_SERIAL;
import static org.openhab.binding.enphase.internal.EnphaseBindingConstants.DISCOVERY_VERSION;
import static org.openhab.binding.enphase.internal.EnphaseBindingConstants.PROPERTY_VERSION;
import static org.openhab.binding.enphase.internal.EnphaseBindingConstants.THING_TYPE_ENPHASE_ENVOY;
import java.net.Inet4Address;
import java.util.HashMap;
@ -38,8 +43,7 @@ import org.slf4j.LoggerFactory;
/**
* MDNS discovery participant for discovering Envoy gateways.
* This service also keeps track of any discovered Envoys host name to provide this information for existing Envoy
* bridges
* so the bridge cat get the host name/ip address if that is unknown.
* bridges so the bridge cat get the host name/ip address if that is unknown.
*
* @author Thomas Hentschel - Initial contribution
* @author Hilbrand Bouwkamp - Initial contribution
@ -55,7 +59,7 @@ public class EnvoyDiscoveryParticipant implements MDNSDiscoveryParticipant, Envo
@Override
public Set<ThingTypeUID> getSupportedThingTypeUIDs() {
return Set.of(EnphaseBindingConstants.THING_TYPE_ENPHASE_ENVOY);
return Set.of(THING_TYPE_ENPHASE_ENVOY);
}
@Override
@ -101,7 +105,7 @@ public class EnvoyDiscoveryParticipant implements MDNSDiscoveryParticipant, Envo
properties.put(PROPERTY_VERSION, version);
return DiscoveryResultBuilder.create(uid).withProperties(properties)
.withRepresentationProperty(CONFIG_SERIAL_NUMBER)
.withLabel("Enphase Envoy " + defaultPassword(serialNumber)).build();
.withLabel("Enphase Envoy " + EnphaseBindingConstants.defaultPassword(serialNumber)).build();
}
@Override

View File

@ -0,0 +1,137 @@
/**
* Copyright (c) 2010-2023 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.enphase.internal.dto;
/**
* Data class for Enphase Entrez Portal.
*
* @author Joe Inkenbrandt - Initial contribution
*/
public class EntrezJwtDTO {
public class EntrezJwtHeaderDTO {
private String kid;
private String typ;
private String alg;
public String getKid() {
return kid;
}
public void setKid(final String kid) {
this.kid = kid;
}
public String getTyp() {
return typ;
}
public void setTyp(final String typ) {
this.typ = typ;
}
public String getAlg() {
return alg;
}
public void setAlg(final String alg) {
this.alg = alg;
}
}
public class EntrezJwtBodyDTO {
private String aud;
private String iss;
private String enphaseUser;
private Long exp;
private Long iat;
private String jti;
private String username;
public String getAud() {
return aud;
}
public void setAud(final String aud) {
this.aud = aud;
}
public String getIss() {
return iss;
}
public void setIss(final String iss) {
this.iss = iss;
}
public String getEnphaseUser() {
return enphaseUser;
}
public void setEnphaseUser(final String enphaseUser) {
this.enphaseUser = enphaseUser;
}
public Long getExp() {
return exp;
}
public void setExp(final Long exp) {
this.exp = exp;
}
public Long getIat() {
return iat;
}
public void setIat(final Long iat) {
this.iat = iat;
}
public String getJti() {
return jti;
}
public void setJti(final String jti) {
this.jti = jti;
}
public String getUsername() {
return username;
}
public void setUsername(final String username) {
this.username = username;
}
}
private final EntrezJwtHeaderDTO header;
private final EntrezJwtBodyDTO body;
public EntrezJwtDTO(final EntrezJwtHeaderDTO header, final EntrezJwtBodyDTO body) {
this.header = header;
this.body = body;
}
public boolean isValid() {
return header == null || body == null;
}
public EntrezJwtBodyDTO getBody() {
return body;
}
public EntrezJwtHeaderDTO getHeader() {
return header;
}
}

View File

@ -0,0 +1,35 @@
/**
* Copyright (c) 2010-2023 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.enphase.internal.exception;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
/**
*
*
* @author Hilbrand Bouwkamp - Initial contribution
*/
@NonNullByDefault
public class EnphaseException extends Exception {
private static final long serialVersionUID = 1L;
public EnphaseException(final String message) {
super(message);
}
public EnphaseException(final String message, final @Nullable Throwable throwable) {
super(message, throwable);
}
}

View File

@ -0,0 +1,35 @@
/**
* Copyright (c) 2010-2023 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.enphase.internal.exception;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
/**
* Exception thrown when a connection problem occurs to the Entrez portal.
*
* @author Hilbrand Bouwkamp - Initial contribution
*/
@NonNullByDefault
public class EntrezConnectionException extends EnphaseException {
private static final long serialVersionUID = 1L;
public EntrezConnectionException(final String message) {
super(message);
}
public EntrezConnectionException(final String message, final @Nullable Throwable e) {
super(message + (e == null ? "" : e.getMessage()), e);
}
}

View File

@ -0,0 +1,30 @@
/**
* Copyright (c) 2010-2023 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.enphase.internal.exception;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* Exception thrown when the JWT access token is invalid.
*
* @author Hilbrand Bouwkamp - Initial contribution
*/
@NonNullByDefault
public class EntrezJwtInvalidException extends EnphaseException {
private static final long serialVersionUID = 1L;
public EntrezJwtInvalidException(final String message) {
super(message);
}
}

View File

@ -10,7 +10,7 @@
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.enphase.internal;
package org.openhab.binding.enphase.internal.exception;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
@ -21,7 +21,7 @@ import org.eclipse.jdt.annotation.Nullable;
* @author Hilbrand Bouwkamp - Initial contribution
*/
@NonNullByDefault
public class EnvoyConnectionException extends Exception {
public class EnvoyConnectionException extends EnphaseException {
private static final long serialVersionUID = 1L;

View File

@ -10,7 +10,7 @@
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.enphase.internal;
package org.openhab.binding.enphase.internal.exception;
import org.eclipse.jdt.annotation.NonNullByDefault;
@ -20,7 +20,7 @@ import org.eclipse.jdt.annotation.NonNullByDefault;
* @author Hilbrand Bouwkamp - Initial contribution
*/
@NonNullByDefault
public class EnvoyNoHostnameException extends Exception {
public class EnvoyNoHostnameException extends EnphaseException {
private static final long serialVersionUID = 1L;

View File

@ -0,0 +1,157 @@
/**
* Copyright (c) 2010-2023 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.enphase.internal.handler;
import java.net.HttpCookie;
import java.net.URI;
import java.nio.charset.StandardCharsets;
import java.util.Base64;
import java.util.List;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import org.eclipse.jdt.annotation.NonNullByDefault;
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.HttpHeader;
import org.eclipse.jetty.http.HttpMethod;
import org.eclipse.jetty.util.Fields;
import org.jsoup.Jsoup;
import org.jsoup.nodes.Document;
import org.jsoup.nodes.Element;
import org.jsoup.select.Elements;
import org.openhab.binding.enphase.internal.dto.EntrezJwtDTO;
import org.openhab.binding.enphase.internal.dto.EntrezJwtDTO.EntrezJwtBodyDTO;
import org.openhab.binding.enphase.internal.dto.EntrezJwtDTO.EntrezJwtHeaderDTO;
import org.openhab.binding.enphase.internal.exception.EntrezConnectionException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.JsonSyntaxException;
/**
* Connector logic for connecting to Entrez server
*
* @author Joe Inkenbrandt - Initial contribution
*/
@NonNullByDefault
public class EntrezConnector {
private static final String SESSION_COOKIE_NAME = "SESSION";
private static final String ELEMENT_ID_JWT_TOKEN = "#JWTToken";
private static final String LOGIN_URL = "https://entrez.enphaseenergy.com/login";
private static final String TOKEN_URL = "https://entrez.enphaseenergy.com/entrez_tokens";
private final Logger logger = LoggerFactory.getLogger(EntrezConnector.class);
private final Gson gson = new GsonBuilder().create();
private final HttpClient httpClient;
private static final long CONNECT_TIMEOUT_SECONDS = 10;
public EntrezConnector(final HttpClient httpClient) {
this.httpClient = httpClient;
}
public String retrieveJwt(final String username, final String password, final String siteId, final String serialNum)
throws EntrezConnectionException {
final String session = login(username, password);
final Fields fields = new Fields();
fields.put("Site", siteId);
fields.put("serialNum", serialNum);
final URI uri = URI.create(TOKEN_URL);
logger.trace("Retrieving jwt from '{}'", uri);
final Request request = httpClient.newRequest(uri).method(HttpMethod.POST)
.cookie(new HttpCookie(SESSION_COOKIE_NAME, session)).content(new FormContentProvider(fields))
.timeout(CONNECT_TIMEOUT_SECONDS, TimeUnit.SECONDS);
final ContentResponse response = send(request);
final String contentAsString = response.getContentAsString();
final Document document = Jsoup.parse(contentAsString);
final Elements elements = document.select(ELEMENT_ID_JWT_TOKEN);
final Element first = elements.first();
if (first == null) {
logger.debug("Could not select element '{}' in received data from entrez site. Received data: {}",
ELEMENT_ID_JWT_TOKEN, contentAsString);
throw new EntrezConnectionException("Could not parse data from entrez site");
}
return first.text();
}
public EntrezJwtDTO processJwt(final String jwt) throws EntrezConnectionException {
try {
final String[] parts = jwt.split("\\.", 0);
if (parts.length < 2) {
logger.debug("Could not split data into 2 parts. Recevied data: {}", jwt);
throw new EntrezConnectionException("Could not parse data from entrez site");
}
final EntrezJwtHeaderDTO header = gson.fromJson(
new String(Base64.getUrlDecoder().decode(parts[0]), StandardCharsets.UTF_8),
EntrezJwtHeaderDTO.class);
final EntrezJwtBodyDTO body = gson.fromJson(
new String(Base64.getUrlDecoder().decode(parts[1]), StandardCharsets.UTF_8),
EntrezJwtBodyDTO.class);
return new EntrezJwtDTO(header, body);
} catch (JsonSyntaxException | IllegalArgumentException e) {
throw new EntrezConnectionException("Could not parse data from entrez site:", e);
}
}
private String login(final String username, final String password) throws EntrezConnectionException {
final Fields fields = new Fields();
fields.put("username", username);
fields.put("password", password);
final URI uri = URI.create(LOGIN_URL);
logger.trace("Retrieving session id from '{}'", uri);
final Request request = httpClient.newRequest(uri).method(HttpMethod.POST)
.content(new FormContentProvider(fields)).timeout(CONNECT_TIMEOUT_SECONDS, TimeUnit.SECONDS);
final ContentResponse response = send(request);
if (response.getStatus() == 200 && response.getHeaders().contains(HttpHeader.SET_COOKIE)) {
final List<HttpCookie> cookies = HttpCookie.parse(response.getHeaders().get(HttpHeader.SET_COOKIE));
for (final HttpCookie c : cookies) {
if (SESSION_COOKIE_NAME.equals(c.getName())) {
return c.getValue();
}
}
}
logger.debug("Failed to login to Entrez portal. Portal returned status: {}. Response from Entrez portal: {}",
response.getStatus(), response.getContentAsString());
throw new EntrezConnectionException(
"Could not login to Entrez JWT Portal. Status code:" + response.getStatus());
}
private ContentResponse send(final Request request) throws EntrezConnectionException {
try {
return request.send();
} catch (final InterruptedException e) {
Thread.currentThread().interrupt();
throw new EntrezConnectionException("Interrupted");
} catch (final TimeoutException e) {
logger.debug("TimeoutException: {}", e.getMessage());
throw new EntrezConnectionException("Connection timeout: ", e);
} catch (final ExecutionException e) {
logger.debug("ExecutionException: {}", e.getMessage(), e);
throw new EntrezConnectionException("Could not retrieve data: ", e.getCause());
}
}
}

View File

@ -18,10 +18,12 @@ import static org.openhab.binding.enphase.internal.EnphaseBindingConstants.ENVOY
import static org.openhab.binding.enphase.internal.EnphaseBindingConstants.ENVOY_WATT_HOURS_LIFETIME;
import static org.openhab.binding.enphase.internal.EnphaseBindingConstants.ENVOY_WATT_HOURS_SEVEN_DAYS;
import static org.openhab.binding.enphase.internal.EnphaseBindingConstants.ENVOY_WATT_HOURS_TODAY;
import static org.openhab.binding.enphase.internal.EnphaseBindingConstants.PROPERTY_VERSION;
import java.time.Duration;
import java.time.temporal.ChronoUnit;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ScheduledFuture;
@ -35,13 +37,16 @@ import org.eclipse.jdt.annotation.Nullable;
import org.eclipse.jetty.client.HttpClient;
import org.openhab.binding.enphase.internal.EnphaseBindingConstants;
import org.openhab.binding.enphase.internal.EnvoyConfiguration;
import org.openhab.binding.enphase.internal.EnvoyConnectionException;
import org.openhab.binding.enphase.internal.EnvoyHostAddressCache;
import org.openhab.binding.enphase.internal.EnvoyNoHostnameException;
import org.openhab.binding.enphase.internal.discovery.EnphaseDevicesDiscoveryService;
import org.openhab.binding.enphase.internal.dto.EnvoyEnergyDTO;
import org.openhab.binding.enphase.internal.dto.InventoryJsonDTO.DeviceDTO;
import org.openhab.binding.enphase.internal.dto.InverterDTO;
import org.openhab.binding.enphase.internal.exception.EnphaseException;
import org.openhab.binding.enphase.internal.exception.EntrezConnectionException;
import org.openhab.binding.enphase.internal.exception.EntrezJwtInvalidException;
import org.openhab.binding.enphase.internal.exception.EnvoyConnectionException;
import org.openhab.binding.enphase.internal.exception.EnvoyNoHostnameException;
import org.openhab.core.cache.ExpiringCache;
import org.openhab.core.config.core.Configuration;
import org.openhab.core.library.types.QuantityType;
@ -55,6 +60,8 @@ import org.openhab.core.thing.ThingStatusDetail;
import org.openhab.core.thing.binding.BaseBridgeHandler;
import org.openhab.core.thing.binding.ThingHandler;
import org.openhab.core.thing.binding.ThingHandlerService;
import org.openhab.core.thing.binding.builder.BridgeBuilder;
import org.openhab.core.thing.util.ThingHandlerHelper;
import org.openhab.core.types.Command;
import org.openhab.core.types.RefreshType;
import org.openhab.core.types.UnDefType;
@ -79,10 +86,11 @@ public class EnvoyBridgeHandler extends BaseBridgeHandler {
private static final long RETRY_RECONNECT_SECONDS = 10;
private final Logger logger = LoggerFactory.getLogger(EnvoyBridgeHandler.class);
private final EnvoyConnector connector;
private final EnvoyHostAddressCache envoyHostnameCache;
private final EnvoyConnectorWrapper connectorWrapper;
private EnvoyConfiguration configuration = new EnvoyConfiguration();
private @Nullable ScheduledFuture<?> updataDataFuture;
private @Nullable ScheduledFuture<?> updateHostnameFuture;
private @Nullable ExpiringCache<Map<String, @Nullable InverterDTO>> invertersCache;
@ -95,8 +103,8 @@ public class EnvoyBridgeHandler extends BaseBridgeHandler {
public EnvoyBridgeHandler(final Bridge thing, final HttpClient httpClient,
final EnvoyHostAddressCache envoyHostAddressCache) {
super(thing);
connector = new EnvoyConnector(httpClient);
this.envoyHostnameCache = envoyHostAddressCache;
connectorWrapper = new EnvoyConnectorWrapper(httpClient);
}
@Override
@ -143,14 +151,14 @@ public class EnvoyBridgeHandler extends BaseBridgeHandler {
return;
}
updateStatus(ThingStatus.UNKNOWN);
connector.setConfiguration(configuration);
consumptionSupported = FeatureStatus.UNKNOWN;
jsonSupported = FeatureStatus.UNKNOWN;
invertersCache = new ExpiringCache<>(Duration.of(configuration.refresh, ChronoUnit.MINUTES),
this::refreshInverters);
devicesCache = new ExpiringCache<>(Duration.of(configuration.refresh, ChronoUnit.MINUTES),
this::refreshDevices);
updataDataFuture = scheduler.scheduleWithFixedDelay(this::updateData, 0, configuration.refresh,
connectorWrapper.setVersion(getVersion());
updataDataFuture = scheduler.scheduleWithFixedDelay(() -> updateData(false), 0, configuration.refresh,
TimeUnit.MINUTES);
}
@ -162,21 +170,23 @@ public class EnvoyBridgeHandler extends BaseBridgeHandler {
*/
private @Nullable Map<String, @Nullable InverterDTO> refreshInverters() {
try {
return connector.getInverters().stream()
return connectorWrapper.getConnector().getInverters().stream()
.collect(Collectors.toMap(InverterDTO::getSerialNumber, Function.identity()));
} catch (final EnvoyNoHostnameException e) {
// ignore hostname exception here. It's already handled by others.
} catch (final EnvoyConnectionException e) {
logger.trace("refreshInverters connection problem", e);
} catch (final EnphaseException e) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
}
return null;
}
private @Nullable Map<String, @Nullable DeviceDTO> refreshDevices() {
private @Nullable final Map<String, @Nullable DeviceDTO> refreshDevices() {
try {
if (jsonSupported != FeatureStatus.UNSUPPORTED) {
final Map<String, @Nullable DeviceDTO> devicesData = connector.getInventoryJson().stream()
.flatMap(inv -> Stream.of(inv.devices).map(d -> {
final Map<String, @Nullable DeviceDTO> devicesData = connectorWrapper.getConnector().getInventoryJson()
.stream().flatMap(inv -> Stream.of(inv.devices).map(d -> {
d.type = inv.type;
return d;
})).collect(Collectors.toMap(DeviceDTO::getSerialNumber, Function.identity()));
@ -195,6 +205,8 @@ public class EnvoyBridgeHandler extends BaseBridgeHandler {
} else if (consumptionSupported == FeatureStatus.SUPPORTED) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
}
} catch (final EnphaseException e) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
}
return null;
}
@ -242,24 +254,82 @@ public class EnvoyBridgeHandler extends BaseBridgeHandler {
/**
* Method called by the refresh thread.
*/
public synchronized void updateData() {
public synchronized void updateData(final boolean forceUpdate) {
try {
updateInverters();
updateEnvoy();
updateDevices();
if (!ThingHandlerHelper.isHandlerInitialized(this)) {
logger.debug("Not updating anything. Not initialized: {}", getThing().getStatus());
return;
}
if (checkConnection()) {
updateEnvoy();
updateInverters(forceUpdate);
updateDevices(forceUpdate);
}
} catch (final EnvoyNoHostnameException e) {
scheduleHostnameUpdate(false);
} catch (final EnvoyConnectionException e) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
scheduleHostnameUpdate(false);
} catch (final EntrezConnectionException e) {
logger.debug("EntrezConnectionException in Enphase thing {}: ", getThing().getUID(), e);
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
} catch (final EntrezJwtInvalidException e) {
logger.debug("EntrezJwtInvalidException in Enphase thing {}: ", getThing().getUID(), e);
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, e.getMessage());
} catch (final EnphaseException e) {
logger.debug("EnphaseException in Enphase thing {}: ", getThing().getUID(), e);
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
} catch (final RuntimeException e) {
logger.debug("Unexpected error in Enphase {}: ", getThing().getUID(), e);
logger.debug("Unexpected error in Enphase thing {}: ", getThing().getUID(), e);
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
}
}
private void updateEnvoy() throws EnvoyNoHostnameException, EnvoyConnectionException {
productionDTO = connector.getProduction();
/**
* Checks if there is an active connection. If the configuration isn't valid it will set the bridge offline and
* return false.
*
* @return true if an active connection was found, else returns false
* @throws EnphaseException
*/
private boolean checkConnection() throws EnphaseException {
logger.trace("Check connection");
if (connectorWrapper.hasConnection()) {
return true;
}
final String configurationError = connectorWrapper.setConnector(configuration);
if (configurationError.isBlank()) {
updateVersion();
logger.trace("No configuration error");
return true;
} else {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, configurationError);
return false;
}
}
private @Nullable String getVersion() {
return getThing().getProperties().get(PROPERTY_VERSION);
}
private void updateVersion() {
if (getVersion() == null) {
final String version = connectorWrapper.getVersion();
if (version != null) {
final BridgeBuilder builder = editThing();
final Map<String, String> properties = new HashMap<>(thing.getProperties());
properties.put(PROPERTY_VERSION, version);
builder.withProperties(properties);
updateThing(builder.build());
}
}
}
private void updateEnvoy() throws EnphaseException {
productionDTO = connectorWrapper.getConnector().getProduction();
setConsumptionDTOData();
getThing().getChannels().stream().map(Channel::getUID).filter(this::isLinked).forEach(this::refresh);
if (isInitialized() && !isOnline()) {
@ -269,13 +339,11 @@ public class EnvoyBridgeHandler extends BaseBridgeHandler {
/**
* Retrieve consumption data if supported, and keep track if this feature is supported by the device.
*
* @throws EnvoyConnectionException
*/
private void setConsumptionDTOData() throws EnvoyConnectionException {
private void setConsumptionDTOData() throws EnphaseException {
if (consumptionSupported != FeatureStatus.UNSUPPORTED && isOnline()) {
try {
consumptionDTO = connector.getConsumption();
consumptionDTO = connectorWrapper.getConnector().getConsumption();
consumptionSupported = FeatureStatus.SUPPORTED;
} catch (final EnvoyNoHostnameException e) {
// ignore hostname exception here. It's already handled by others.
@ -294,9 +362,11 @@ public class EnvoyBridgeHandler extends BaseBridgeHandler {
/**
* Updates channels of the inverter things with inverter specific data.
*
* @param forceUpdate if true forces to update the data, otherwise gets the data from the cache
*/
private void updateInverters() {
final Map<String, @Nullable InverterDTO> inverters = getInvertersData(false);
private void updateInverters(final boolean forceUpdate) {
final Map<String, @Nullable InverterDTO> inverters = getInvertersData(forceUpdate);
if (inverters != null) {
getThing().getThings().stream().map(Thing::getHandler).filter(h -> h instanceof EnphaseInverterHandler)
@ -322,9 +392,11 @@ public class EnvoyBridgeHandler extends BaseBridgeHandler {
/**
* Updates channels of the device things with device specific data.
* This data is not available on all envoy devices.
*
* @param forceUpdate if true forces to update the data, otherwise gets the data from the cache
*/
private void updateDevices() {
final Map<String, @Nullable DeviceDTO> devices = getDevices(false);
private void updateDevices(final boolean forceUpdate) {
final Map<String, @Nullable DeviceDTO> devices = getDevices(forceUpdate);
getThing().getThings().stream().map(Thing::getHandler).filter(h -> h instanceof EnphaseDeviceHandler)
.map(EnphaseDeviceHandler.class::cast).forEach(invHandler -> invHandler
@ -367,22 +439,25 @@ public class EnvoyBridgeHandler extends BaseBridgeHandler {
if (lastKnownHostname.isEmpty()) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
"No ip address known of the envoy gateway. If this isn't updated in a few minutes check your connection.");
"No ip address known of the Envoy gateway. If this isn't updated in a few minutes run discovery scan or check your connection.");
scheduleHostnameUpdate(true);
} else {
final Configuration config = editConfiguration();
config.put(CONFIG_HOSTNAME, lastKnownHostname);
logger.info("Enphase Envoy ({}) hostname/ip address set to {}", getThing().getUID(), lastKnownHostname);
configuration.hostname = lastKnownHostname;
connector.setConfiguration(configuration);
updateConfiguration(config);
updateData();
// The task is done so the future can be released by setting it to null.
updateHostnameFuture = null;
updateConfigurationOnHostnameUpdate(lastKnownHostname);
updateData(true);
}
}
private void updateConfigurationOnHostnameUpdate(final String lastKnownHostname) {
final Configuration config = editConfiguration();
config.put(CONFIG_HOSTNAME, lastKnownHostname);
logger.info("Enphase Envoy ({}) hostname/ip address set to {}", getThing().getUID(), lastKnownHostname);
configuration.hostname = lastKnownHostname;
updateConfiguration(config);
// The task is done so the future can be released by setting it to null.
updateHostnameFuture = null;
}
@Override
public void dispose() {
final ScheduledFuture<?> retryFuture = this.updateHostnameFuture;

View File

@ -23,6 +23,7 @@ import java.util.function.Function;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.eclipse.jetty.client.HttpClient;
import org.eclipse.jetty.client.HttpResponseException;
import org.eclipse.jetty.client.api.Authentication;
import org.eclipse.jetty.client.api.Authentication.Result;
import org.eclipse.jetty.client.api.AuthenticationStore;
@ -33,13 +34,14 @@ import org.eclipse.jetty.http.HttpMethod;
import org.eclipse.jetty.http.HttpStatus;
import org.openhab.binding.enphase.internal.EnphaseBindingConstants;
import org.openhab.binding.enphase.internal.EnvoyConfiguration;
import org.openhab.binding.enphase.internal.EnvoyConnectionException;
import org.openhab.binding.enphase.internal.EnvoyNoHostnameException;
import org.openhab.binding.enphase.internal.dto.EnvoyEnergyDTO;
import org.openhab.binding.enphase.internal.dto.EnvoyErrorDTO;
import org.openhab.binding.enphase.internal.dto.InventoryJsonDTO;
import org.openhab.binding.enphase.internal.dto.InverterDTO;
import org.openhab.binding.enphase.internal.dto.ProductionJsonDTO;
import org.openhab.binding.enphase.internal.exception.EnphaseException;
import org.openhab.binding.enphase.internal.exception.EnvoyConnectionException;
import org.openhab.binding.enphase.internal.exception.EnvoyNoHostnameException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@ -53,7 +55,9 @@ import com.google.gson.JsonSyntaxException;
* @author Hilbrand Bouwkamp - Initial contribution
*/
@NonNullByDefault
class EnvoyConnector {
public class EnvoyConnector {
protected static final long CONNECT_TIMEOUT_SECONDS = 10;
private static final String HTTP = "http://";
private static final String PRODUCTION_JSON_URL = "/production.json";
@ -61,55 +65,104 @@ class EnvoyConnector {
private static final String PRODUCTION_URL = "/api/v1/production";
private static final String CONSUMPTION_URL = "/api/v1/consumption";
private static final String INVERTERS_URL = PRODUCTION_URL + "/inverters";
private static final long CONNECT_TIMEOUT_SECONDS = 5;
private static final String INFO_XML = "/info.xml";
private static final String INFO_SOFTWARE_BEGIN = "<software>";
private static final String INFO_SOFTWARE_END = "</software>";
protected final HttpClient httpClient;
private final Logger logger = LoggerFactory.getLogger(EnvoyConnector.class);
private final Gson gson = new GsonBuilder().create();
private final HttpClient httpClient;
private String hostname = "";
private final String schema;
private @Nullable DigestAuthentication envoyAuthn;
private @Nullable URI invertersURI;
protected @NonNullByDefault({}) EnvoyConfiguration configuration;
public EnvoyConnector(final HttpClient httpClient) {
this(httpClient, HTTP);
}
protected EnvoyConnector(final HttpClient httpClient, final String schema) {
this.httpClient = httpClient;
this.schema = schema;
}
/**
* Sets the Envoy connection configuration.
*
* @param configuration the configuration to set
* @return configuration error message or empty string if no configuration errors present
*/
public void setConfiguration(final EnvoyConfiguration configuration) {
hostname = configuration.hostname;
if (hostname.isEmpty()) {
return;
public String setConfiguration(final EnvoyConfiguration configuration) {
this.configuration = configuration;
if (configuration.hostname.isEmpty()) {
return "";
}
final String password = configuration.password.isEmpty()
? EnphaseBindingConstants.defaultPassword(configuration.serialNumber)
: configuration.password;
final String username = configuration.username.isEmpty() ? EnvoyConfiguration.DEFAULT_USERNAME
: configuration.username;
final AuthenticationStore store = httpClient.getAuthenticationStore();
if (envoyAuthn != null) {
store.removeAuthentication(envoyAuthn);
}
invertersURI = URI.create(HTTP + hostname + INVERTERS_URL);
invertersURI = URI.create(schema + configuration.hostname + INVERTERS_URL);
envoyAuthn = new DigestAuthentication(invertersURI, Authentication.ANY_REALM, username, password);
store.addAuthentication(envoyAuthn);
return "";
}
/**
* Checks if data can be read from the Envoy, and to determine the software version returned by the Envoy.
*
* @param hostname hostname of the Envoy.
* @return software version number as reported by the the Envoy or null if connection could be made or software
* version not detected.
*/
protected @Nullable String checkConnection(final String hostname) {
try {
final String url = hostname + INFO_XML;
logger.debug("Check connection to '{}'", url);
final Request createRequest = createRequest(url);
final ContentResponse response = send(createRequest);
logger.debug("Checkconnection status from request is: {}", response.getStatus());
if (response.getStatus() == HttpStatus.OK_200) {
final String content = response.getContentAsString();
final int begin = content.indexOf(INFO_SOFTWARE_BEGIN);
final int end = content.lastIndexOf(INFO_SOFTWARE_END);
if (begin > 0 && end > 0) {
final String version = content.substring(begin + INFO_SOFTWARE_BEGIN.length(), end);
logger.debug("Found Envoy version number '{}' in info.xml", version);
return Character.isDigit(version.charAt(0)) ? version : version.substring(1);
}
}
} catch (EnphaseException | HttpResponseException e) {
logger.debug("Exception trying to check the connection.", e);
}
return null;
}
/**
* @return Returns the production data from the Envoy gateway.
*/
public EnvoyEnergyDTO getProduction() throws EnvoyConnectionException, EnvoyNoHostnameException {
public EnvoyEnergyDTO getProduction() throws EnphaseException {
return retrieveData(PRODUCTION_URL, this::jsonToEnvoyEnergyDTO);
}
/**
* @return Returns the consumption data from the Envoy gateway.
*/
public EnvoyEnergyDTO getConsumption() throws EnvoyConnectionException, EnvoyNoHostnameException {
public EnvoyEnergyDTO getConsumption() throws EnphaseException {
return retrieveData(CONSUMPTION_URL, this::jsonToEnvoyEnergyDTO);
}
@ -120,14 +173,14 @@ class EnvoyConnector {
/**
* @return Returns the production/consumption data from the Envoy gateway.
*/
public ProductionJsonDTO getProductionJson() throws EnvoyConnectionException, EnvoyNoHostnameException {
public ProductionJsonDTO getProductionJson() throws EnphaseException {
return retrieveData(PRODUCTION_JSON_URL, json -> gson.fromJson(json, ProductionJsonDTO.class));
}
/**
* @return Returns the inventory data from the Envoy gateway.
*/
public List<InventoryJsonDTO> getInventoryJson() throws EnvoyConnectionException, EnvoyNoHostnameException {
public List<InventoryJsonDTO> getInventoryJson() throws EnphaseException {
return retrieveData(INVENTORY_JSON_URL, this::jsonToEnvoyInventoryJson);
}
@ -140,7 +193,7 @@ class EnvoyConnector {
/**
* @return Returns the production data for the inverters.
*/
public List<InverterDTO> getInverters() throws EnvoyConnectionException, EnvoyNoHostnameException {
public List<InverterDTO> getInverters() throws EnphaseException {
synchronized (this) {
final AuthenticationStore store = httpClient.getAuthenticationStore();
final Result invertersResult = store.findAuthenticationResult(invertersURI);
@ -153,36 +206,45 @@ class EnvoyConnector {
}
private synchronized <T> T retrieveData(final String urlPath, final Function<String, @Nullable T> jsonConverter)
throws EnvoyConnectionException, EnvoyNoHostnameException {
throws EnphaseException {
final Request request = createRequest(configuration.hostname + urlPath);
constructRequest(request);
final ContentResponse response = send(request);
final String content = response.getContentAsString();
logger.trace("Envoy returned data for '{}' with status {}: {}", urlPath, response.getStatus(), content);
try {
if (hostname.isEmpty()) {
throw new EnvoyNoHostnameException("No host name/ip address known (yet)");
}
final URI uri = URI.create(HTTP + hostname + urlPath);
logger.trace("Retrieving data from '{}'", uri);
final Request request = httpClient.newRequest(uri).method(HttpMethod.GET).timeout(CONNECT_TIMEOUT_SECONDS,
TimeUnit.SECONDS);
final ContentResponse response = request.send();
final String content = response.getContentAsString();
logger.trace("Envoy returned data for '{}' with status {}: {}", urlPath, response.getStatus(), content);
try {
if (response.getStatus() == HttpStatus.OK_200) {
final T result = jsonConverter.apply(content);
if (result == null) {
throw new EnvoyConnectionException("No data received");
}
return result;
} else {
final @Nullable EnvoyErrorDTO error = gson.fromJson(content, EnvoyErrorDTO.class);
logger.debug("Envoy returned an error: {}", error);
throw new EnvoyConnectionException(error == null ? response.getReason() : error.info);
if (response.getStatus() == HttpStatus.OK_200) {
final T result = jsonConverter.apply(content);
if (result == null) {
throw new EnvoyConnectionException("No data received");
}
} catch (final JsonSyntaxException e) {
logger.debug("Error parsing json: {}", content, e);
throw new EnvoyConnectionException("Error parsing data: ", e);
return result;
} else {
final @Nullable EnvoyErrorDTO error = gson.fromJson(content, EnvoyErrorDTO.class);
logger.debug("Envoy returned an error: {}", error);
throw new EnvoyConnectionException(error == null ? response.getReason() : error.info);
}
} catch (final JsonSyntaxException e) {
logger.debug("Error parsing json: {}", content, e);
throw new EnvoyConnectionException("Error parsing data: ", e);
}
}
private Request createRequest(final String urlPath) throws EnvoyNoHostnameException {
return httpClient.newRequest(URI.create(schema + urlPath)).method(HttpMethod.GET)
.timeout(CONNECT_TIMEOUT_SECONDS, TimeUnit.SECONDS);
}
protected void constructRequest(final Request request) throws EnphaseException {
logger.trace("Retrieving data from '{}' ", request.getURI());
}
protected ContentResponse send(final Request request) throws EnvoyConnectionException {
try {
return request.send();
} catch (final InterruptedException e) {
Thread.currentThread().interrupt();
throw new EnvoyConnectionException("Interrupted");

View File

@ -0,0 +1,174 @@
/**
* Copyright (c) 2010-2023 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.enphase.internal.handler;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.eclipse.jetty.client.HttpClient;
import org.openhab.binding.enphase.internal.EnvoyConfiguration;
import org.openhab.binding.enphase.internal.exception.EnphaseException;
import org.openhab.binding.enphase.internal.exception.EnvoyConnectionException;
import org.openhab.binding.enphase.internal.exception.EnvoyNoHostnameException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Wraps around the specific Envoy connector and provides methods to determine which connector to use.
*
* @author Hilbrand Bouwkamp - Initial contribution
*/
@NonNullByDefault
public class EnvoyConnectorWrapper {
private final Logger logger = LoggerFactory.getLogger(EnvoyConnectorWrapper.class);
private final HttpClient httpClient;
private @Nullable EnvoyConnector connector;
private @Nullable String version;
public EnvoyConnectorWrapper(final HttpClient httpClient) {
this.httpClient = httpClient;
}
public @Nullable String getVersion() {
return version;
}
/**
* Sets the Envoy software version as retreived from the Envoy data.
*
* @param version Envoy software version.
*/
public void setVersion(final @Nullable String version) {
if (version == null) {
return;
}
logger.debug("Set Envoy version found in the Envoy data: {}", version);
this.version = version;
}
/**
* Set the connector given the configuration.
*
* @param configuration configuration to use to set the connector.
* @return Returns configuration error message or empty string if no configuration errors present
* @throws EnphaseException
*/
public synchronized String setConnector(final EnvoyConfiguration configuration) throws EnphaseException {
final EnvoyConnector connector = determineConnector(configuration.hostname);
final String message = connector.setConfiguration(configuration);
// Only set connector if no error messages.
if (message.isEmpty()) {
this.connector = connector;
}
return message;
}
/**
* @return Returns true if a connection with the Envoy has been established.
*/
public boolean hasConnection() {
return connector != null;
}
/**
* @return Returns the connector when present. This method should only be called when a connector is present as
* returned by the {@link #hasConnection()} method.
*/
public EnvoyConnector getConnector() throws EnvoyConnectionException {
final EnvoyConnector connector = this.connector;
if (connector == null) {
throw new EnvoyConnectionException("No connection to the Envoy. Check your configuration.");
}
return connector;
}
private EnvoyConnector determineConnector(final String hostname) throws EnphaseException {
final EnvoyConnector connectorByVersion = determineConnectorOnVersion();
if (connectorByVersion != null) {
return connectorByVersion;
}
if (hostname.isBlank()) {
throw new EnvoyNoHostnameException("No hostname available.");
}
final EnvoyConnector envoyConnector = new EnvoyConnector(httpClient);
final String version = envoyConnector.checkConnection(hostname);
if (version != null) {
this.version = version;
final int majorVersionNumber = determineMajorVersionNumber();
if (majorVersionNumber > 0 && majorVersionNumber < 7) {
logger.debug(
"Connection to Envoy determined by getting a reply from the Envoy using the prior to version 7 method.");
return envoyConnector;
}
if (majorVersionNumber >= 7) {
logger.debug(
"Connection to Envoy determined by getting a reply from the Envoy using version 7 connection method.");
return new EnvoyEntrezConnector(httpClient);
}
}
final EnvoyEntrezConnector envoyEntrezConnector = new EnvoyEntrezConnector(httpClient);
final String entrezVersion = envoyEntrezConnector.checkConnection(hostname);
if (entrezVersion != null) {
this.version = entrezVersion;
final int majorVersionNumber = determineMajorVersionNumber();
if (majorVersionNumber >= 7) {
logger.info(
"Connection to Envoy determined by getting a reply from the Envoy using version 7 connection method.");
return envoyEntrezConnector;
}
}
throw new EnphaseException("No connection could be made with the Envoy. Check your connection/hostname.");
}
private @Nullable EnvoyConnector determineConnectorOnVersion() {
final int majorVersionNumber = determineMajorVersionNumber();
if (majorVersionNumber < 0) {
return null;
} else if (majorVersionNumber < 7) {
logger.debug("Connect to Envoy based on version number {} using standard connector", version);
return new EnvoyConnector(httpClient);
} else {
logger.debug("Connect to Envoy based on version number {} using entrez connector", version);
return new EnvoyEntrezConnector(httpClient);
}
}
private int determineMajorVersionNumber() {
final String version = this.version;
if (version == null) {
return -1;
}
logger.debug("Envoy version information used to determine actual version: {}", version);
final int marjorVersionIndex = version.indexOf('.');
if (marjorVersionIndex < 0) {
return -1;
}
try {
return Integer.parseInt(version.substring(0, marjorVersionIndex));
} catch (final NumberFormatException e) {
logger.trace("Could not parse major version number in {}, error message: {}", version, e.getMessage());
return -1;
}
}
}

View File

@ -0,0 +1,181 @@
/**
* Copyright (c) 2010-2023 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.enphase.internal.handler;
import java.net.HttpCookie;
import java.net.URI;
import java.time.Instant;
import java.util.List;
import java.util.Locale;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
import java.util.stream.Stream;
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.http.HttpHeader;
import org.eclipse.jetty.http.HttpMethod;
import org.openhab.binding.enphase.internal.EnvoyConfiguration;
import org.openhab.binding.enphase.internal.exception.EnphaseException;
import org.openhab.binding.enphase.internal.exception.EntrezJwtInvalidException;
import org.openhab.binding.enphase.internal.exception.EnvoyConnectionException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Envoy connector using Entrez portal to obtain an JWT access token.
*
* @author Joe Inkenbrandt - Initial contribution
* @author Hilbrand Bouwkamp - Refactored entrez specific code in it's own sub connector class
*/
@NonNullByDefault
public class EnvoyEntrezConnector extends EnvoyConnector {
// private static final String SESSION = "session";
private static final String HTTPS = "https://";
private static final String LOGIN_URL = "/auth/check_jwt";
private static final String SESSION_COOKIE_NAME = "session";
private final Logger logger = LoggerFactory.getLogger(EnvoyEntrezConnector.class);
private final EntrezConnector entrezConnector;
private @Nullable String sessionKey;
private @Nullable String sessionId;
private String jwt = "";
private Instant jwtExpirationTime = Instant.now();
public EnvoyEntrezConnector(final HttpClient httpClient) {
super(httpClient, HTTPS);
entrezConnector = new EntrezConnector(httpClient);
}
@Override
public String setConfiguration(final EnvoyConfiguration configuration) {
final String message;
if (!configuration.autoJwt) {
message = check(configuration.jwt, "No autoJWT enabled, but jwt parameter is empty.");
if (message.isEmpty()) {
jwt = configuration.jwt;
}
} else {
message = Stream
.of(check(configuration.username, "Username parameter is empty"),
check(configuration.password, "Password parameter is empty"),
check(configuration.siteName, "siteName parameter is empty"))
.filter(s -> !s.isEmpty()).collect(Collectors.joining(", "));
}
if (!message.isEmpty()) {
return message;
}
return super.setConfiguration(configuration);
}
private String check(final String property, final String message) {
return property.isBlank() ? message : "";
}
@Override
protected void constructRequest(final Request request) throws EnphaseException {
// Check if we need a new session ID
if (!checkSessionId()) {
sessionId = getNewSessionId();
}
logger.trace("Retrieving data from '{}' with sessionID '{}'", request.getURI(), sessionId);
request.cookie(new HttpCookie(sessionKey, sessionId));
}
private boolean checkSessionId() {
if (this.sessionId == null) {
return false;
}
final URI uri = URI.create(HTTPS + configuration.hostname + LOGIN_URL);
final Request request = httpClient.newRequest(uri).method(HttpMethod.GET)
.cookie(new HttpCookie(sessionKey, this.sessionId)).timeout(CONNECT_TIMEOUT_SECONDS, TimeUnit.SECONDS);
final ContentResponse response;
try {
response = send(request);
} catch (final EnvoyConnectionException e) {
logger.debug("Session ID ({}) Check TimeoutException: {}", sessionId, e.getMessage());
return false;
}
if (response.getStatus() != 200) {
logger.debug("Session ID ({}) Home Response: {}", sessionId, response.getStatus());
return false;
}
logger.debug("Home Response: {}", response.getContentAsString());
return true;
}
private @Nullable String getNewSessionId() throws EnphaseException {
if (jwt.isEmpty() || isExpired()) {
if (configuration.autoJwt) {
jwt = entrezConnector.retrieveJwt(configuration.username, configuration.password,
configuration.siteName, configuration.serialNumber);
jwtExpirationTime = Instant.ofEpochSecond(entrezConnector.processJwt(jwt).getBody().getExp());
} else {
new EntrezJwtInvalidException("Accesstoken expired. Configure new token or configure autoJwt.");
}
}
return loginWithJWT(jwt);
}
private boolean isExpired() {
return jwtExpirationTime.isBefore(Instant.now());
}
/**
* This function attempts to get a sessionId from the local gateway by submitting the JWT given.
*
* @return the sessionId or null of no session id could be retrieved.
*/
private @Nullable String loginWithJWT(final String jwt) throws EnvoyConnectionException, EntrezJwtInvalidException {
final URI uri = URI.create(HTTPS + configuration.hostname + LOGIN_URL);
// Authorization: Bearer
final Request request = httpClient.newRequest(uri).method(HttpMethod.GET)
.header("Authorization", "Bearer " + jwt).timeout(CONNECT_TIMEOUT_SECONDS, TimeUnit.SECONDS);
final ContentResponse response = send(request);
if (response.getStatus() == 200 && response.getHeaders().contains(HttpHeader.SET_COOKIE)) {
final List<HttpCookie> cookies = HttpCookie.parse(response.getHeaders().get(HttpHeader.SET_COOKIE));
for (final HttpCookie c : cookies) {
final String cookieKey = String.valueOf(c.getName()).toLowerCase(Locale.ROOT);
if (cookieKey.startsWith(SESSION_COOKIE_NAME)) {
logger.debug("Got SessionID: {}", c.getValue());
sessionKey = cookieKey;
return c.getValue();
}
}
logger.debug(
"Failed to find cookie with the JWT token from the Enphase portal. Maybe Enphase changed the website.");
throw new EntrezJwtInvalidException(
"Unable to obtain jwt key from Ephase website. Manully configuring the JWT might make it work. Please report this issue.");
}
logger.debug("Failed to login to Envoy. Evoy returned status: {}. Response from Envoy: {}",
response.getStatus(), response.getContentAsString());
throw new EntrezJwtInvalidException(
"Could not login to Envoy with the access token. Envoy returned status:" + response.getStatus());
}
}

View File

@ -6,6 +6,6 @@
<type>binding</type>
<name>Enphase Envoy Binding</name>
<description>This is the binding for Enphase Envoy solar panels.</description>
<connection>local</connection>
<connection>hybrid</connection>
</addon:addon>

View File

@ -1,80 +1,195 @@
error.nodata=No Data
envoy.global.ok=Normal
# add-on
envoy.cond_flags.acb_ctrl.bmuhardwareerror=BMU Hardware Error
envoy.cond_flags.acb_ctrl.bmuimageerror=BMU Image Error
envoy.cond_flags.acb_ctrl.bmumaxcurrentwarning=BMU Max Current Warning
envoy.cond_flags.acb_ctrl.bmusenseerror=BMU Sense Error
addon.enphase.name = Enphase Envoy Binding
addon.enphase.description = This is the binding for Enphase Envoy solar panels.
envoy.cond_flags.acb_ctrl.cellmaxtemperror=Cell Max Temperature Error
envoy.cond_flags.acb_ctrl.cellmaxtempwarning=Cell Max Temperature Warning
envoy.cond_flags.acb_ctrl.cellmaxvoltageerror=Cell Max Voltage Error
envoy.cond_flags.acb_ctrl.cellmaxvoltagewarning=Cell Max Voltage Warning
envoy.cond_flags.acb_ctrl.cellmintemperror=Cell Min Temperature Error
envoy.cond_flags.acb_ctrl.cellmintempwarning=Cell Min Temperature Warning
envoy.cond_flags.acb_ctrl.cellminvoltageerror=Cell Min Voltage Error
envoy.cond_flags.acb_ctrl.cellminvoltagewarning=Cell Min Voltage Warning
envoy.cond_flags.acb_ctrl.cibcanerror=CIB CAN Error
envoy.cond_flags.acb_ctrl.cibimageerror=CIB Image Error
envoy.cond_flags.acb_ctrl.cibspierror=CIB SPI Error"
envoy.cond_flags.obs_strs.discovering=Discovering
envoy.cond_flags.obs_strs.failure=Failure to report
envoy.cond_flags.obs_strs.flasherror=Flash Error
envoy.cond_flags.obs_strs.notmonitored=Not Monitored
envoy.cond_flags.obs_strs.ok=Normal
envoy.cond_flags.obs_strs.plmerror=PLM Error
envoy.cond_flags.obs_strs.secmodeenterfailure=Secure mode enter failure
envoy.cond_flags.obs_strs.secmodeexitfailure=Secure mode exit failure
envoy.cond_flags.obs_strs.sleeping=Sleeping"
# thing types
envoy.cond_flags.pcu_chan.acMonitorError=AC Monitor Error
envoy.cond_flags.pcu_chan.acfrequencyhigh=AC Frequency High
envoy.cond_flags.pcu_chan.acfrequencylow=AC Frequency Low
envoy.cond_flags.pcu_chan.acfrequencyoor=AC Frequency Out Of Range
envoy.cond_flags.pcu_chan.acvoltage_avg_hi=AC Voltage Average High
envoy.cond_flags.pcu_chan.acvoltagehigh=AC Voltage High
envoy.cond_flags.pcu_chan.acvoltagelow=AC Voltage Low
envoy.cond_flags.pcu_chan.acvoltageoor=AC Voltage Out Of Range
envoy.cond_flags.pcu_chan.acvoltageoosp1=AC Voltage Out Of Range - Phase 1
envoy.cond_flags.pcu_chan.acvoltageoosp2=AC Voltage Out Of Range - Phase 2
envoy.cond_flags.pcu_chan.acvoltageoosp3=AC Voltage Out Of Range - Phase 3
envoy.cond_flags.pcu_chan.agfpowerlimiting=AGF Power Limiting
envoy.cond_flags.pcu_chan.dcresistancelow=DC Resistance Low
envoy.cond_flags.pcu_chan.dcresistancelowpoweroff=DC Resistance Low - Power Off
envoy.cond_flags.pcu_chan.dcvoltagetoohigh=DC Voltage Too High
envoy.cond_flags.pcu_chan.dcvoltagetoolow=DC Voltage Too Low
envoy.cond_flags.pcu_chan.dfdt=AC Frequency Changing too Fast
envoy.cond_flags.pcu_chan.gfitripped=GFI Tripped
envoy.cond_flags.pcu_chan.gridgone=Grid Gone
envoy.cond_flags.pcu_chan.gridinstability=Grid Instability
envoy.cond_flags.pcu_chan.gridoffsethi=Grid Offset Hi
envoy.cond_flags.pcu_chan.gridoffsetlow=Grid Offset Low
envoy.cond_flags.pcu_chan.hardwareError=Hardware Error
envoy.cond_flags.pcu_chan.hardwareWarning=Hardware Warning
envoy.cond_flags.pcu_chan.highskiprate=High Skip Rate
envoy.cond_flags.pcu_chan.invalidinterval=Invalid Interval
envoy.cond_flags.pcu_chan.pwrgenoffbycmd=Power generation off by command
envoy.cond_flags.pcu_chan.skippedcycles=Skipped Cycles
envoy.cond_flags.pcu_chan.vreferror=Voltage Ref Error"
thing-type.enphase.envoy.label = Envoy
thing-type.enphase.envoy.description = Envoy gateway
thing-type.enphase.envoy.group.consumption.label = Consumption
thing-type.enphase.envoy.group.consumption.description = Consumption data from the solar panels
thing-type.enphase.envoy.group.production.label = Production
thing-type.enphase.envoy.group.production.description = Production data from the solar panels
thing-type.enphase.inverter.label = Inverter
thing-type.enphase.inverter.description = Inverter
thing-type.enphase.relay.label = Relay Controller
thing-type.enphase.relay.description = Network system relay controller
thing-type.enphase.relay.channel.line1Connected.label = Line 1 Connection Status
thing-type.enphase.relay.channel.line2Connected.label = Line 2 Connection Status
thing-type.enphase.relay.channel.line3Connected.label = Line 3 Connection Status
envoy.cond_flags.pcu_ctrl.alertactive=Alert Active
envoy.cond_flags.pcu_ctrl.altpwrgenmode=Alternate Power Generation Mode
envoy.cond_flags.pcu_ctrl.altvfsettings=Alternate Voltage and Frequency Settings
envoy.cond_flags.pcu_ctrl.badflashimage=Bad Flash Image
envoy.cond_flags.pcu_ctrl.bricked=No Grid Profile
envoy.cond_flags.pcu_ctrl.commandedreset=Commanded Reset
envoy.cond_flags.pcu_ctrl.criticaltemperature=Critical Temperature
envoy.cond_flags.pcu_ctrl.dc-pwr-low=DC Power Too Low
envoy.cond_flags.pcu_ctrl.iuplinkproblem=IUP Link Problem
envoy.cond_flags.pcu_ctrl.manutestmode=In Manu Test Mode
envoy.cond_flags.pcu_ctrl.nsync=Grid Perturbation Unsynchronized
envoy.cond_flags.pcu_ctrl.overtemperature=Over Temperature
envoy.cond_flags.pcu_ctrl.poweronreset=Power On Reset
envoy.cond_flags.pcu_ctrl.pwrgenoffbycmd=Power generation off by command
envoy.cond_flags.pcu_ctrl.runningonac=Running on AC
envoy.cond_flags.pcu_ctrl.tpmtest=Transient Grid Profile
envoy.cond_flags.pcu_ctrl.unexpectedreset=Unexpected Reset
envoy.cond_flags.pcu_ctrl.watchdogreset=Watchdog Reset
# thing types config
envoy.cond_flags.rgm_chan.check_meter=Meter Error
envoy.cond_flags.rgm_chan.power_quality=Poor Power Quality
thing-type.config.enphase.envoy.autoJwt.label = Auto JWT
thing-type.config.enphase.envoy.autoJwt.description = For versions above version 7, this tells the binding whether to attempt an auto-retrieve of the JWT
thing-type.config.enphase.envoy.hostname.label = Host Name / IP Address
thing-type.config.enphase.envoy.hostname.description = The host name/ip address of the Envoy gateway. Leave empty to auto detect
thing-type.config.enphase.envoy.jwt.label = JWT
thing-type.config.enphase.envoy.jwt.description = For versions above version 7, this is the JWT when not using Auto JWT
thing-type.config.enphase.envoy.password.label = Password
thing-type.config.enphase.envoy.password.description = For versions below version 7, this is the password to the Envoy gateway. Leave empty when using the default password. For newer versions this is the password of the Enphase Entrez Cloud Login when using Auto JWT.
thing-type.config.enphase.envoy.refresh.label = Refresh Time
thing-type.config.enphase.envoy.refresh.description = Period between updates. The default is 5 minutes, the refresh frequency of the Envoy itself
thing-type.config.enphase.envoy.serialNumber.label = Serial Number
thing-type.config.enphase.envoy.serialNumber.description = The serial number of the Envoy gateway which can be found on the gateway
thing-type.config.enphase.envoy.siteName.label = Site Name
thing-type.config.enphase.envoy.siteName.description = The Site Name, which is above the Site Id in Enphase app. Required when using Auto JWT
thing-type.config.enphase.envoy.username.label = User Name
thing-type.config.enphase.envoy.username.description = For versions below version 7, this is the user name to the Envoy gateway. Leave empty when using the default user name. For newer versions this is user name of the Enphase Entrez Cloud Login when using Auto JWT.
thing-type.config.enphase.inverter.serialNumber.label = Serial Number
thing-type.config.enphase.inverter.serialNumber.description = The serial number of the inverter
thing-type.config.enphase.relay.serialNumber.label = Serial Number
thing-type.config.enphase.relay.serialNumber.description = The serial number of the inverter
# channel group types
channel-group-type.enphase.envoy-consumption.label = Envoy Data
channel-group-type.enphase.envoy-consumption.channel.wattHoursLifetime.label = Consumption Lifetime
channel-group-type.enphase.envoy-consumption.channel.wattHoursLifetime.description = Watt hours consumption over the lifetime
channel-group-type.enphase.envoy-consumption.channel.wattHoursSevenDays.label = Consumption 7 Days
channel-group-type.enphase.envoy-consumption.channel.wattHoursSevenDays.description = Watt hours consumption the last 7 days
channel-group-type.enphase.envoy-consumption.channel.wattHoursToday.label = Consumption Today
channel-group-type.enphase.envoy-consumption.channel.wattHoursToday.description = Watt hours consumption today
channel-group-type.enphase.envoy-consumption.channel.wattsNow.label = Latest Consumption Power
channel-group-type.enphase.envoy-consumption.channel.wattsNow.description = Latest Watt consumption
channel-group-type.enphase.envoy-production.label = Envoy Data
channel-group-type.enphase.envoy-production.channel.wattHoursLifetime.description = Watt hours produced over the lifetime
channel-group-type.enphase.envoy-production.channel.wattHoursSevenDays.description = Watt hours produced the last 7 days
channel-group-type.enphase.envoy-production.channel.wattHoursToday.description = Watt hours produced today
channel-group-type.enphase.envoy-production.channel.wattsNow.description = Latest Watt produced
# channel types
channel-type.enphase.communicating.label = Communicating
channel-type.enphase.last-report-date.label = Last Report Date
channel-type.enphase.last-report-date.description = Date of last reported power delivery
channel-type.enphase.last-report-watts.label = Last Report
channel-type.enphase.last-report-watts.description = Last reported power delivery
channel-type.enphase.line-connected.label = Line Connection Status
channel-type.enphase.line-connected.description = When closed power line is connected
channel-type.enphase.max-report-watts.label = Max Report
channel-type.enphase.max-report-watts.description = Maximum reported power
channel-type.enphase.operating.label = Operating
channel-type.enphase.producing.label = Producing
channel-type.enphase.provisioned.label = Provisioned
channel-type.enphase.relay.label = Relay Status
channel-type.enphase.status.label = Status
channel-type.enphase.status.description = The status of the Enphase device
channel-type.enphase.watt-hours-lifetime.label = Produced Lifetime
channel-type.enphase.watt-hours-seven-days.label = Produced 7 Days
channel-type.enphase.watt-hours-today.label = Produced Today
channel-type.enphase.watts-now.label = Latest Produced Power
channel-type.enphase.watts-now.description = Latest watts produced
# channel types
channel-type.enphase.watt-hours-lifetime.description = Watt hours produced over the lifetime
channel-type.enphase.watt-hours-seven-days.description = Watt hours produced the last 7 days
channel-type.enphase.watt-hours-today.description = Watt hours produced today
# thing types
thing-type.enphase.envoy-entrez.label = Envoy JWT
thing-type.enphase.envoy-entrez.description = Envoy gateway that requires a JWT access token
thing-type.enphase.envoy-entrez.group.consumption.label = Consumption
thing-type.enphase.envoy-entrez.group.consumption.description = Consumption data from the solar panels
thing-type.enphase.envoy-entrez.group.production.label = Production
thing-type.enphase.envoy-entrez.group.production.description = Production data from the solar panels
# thing types config
thing-type.config.enphase.envoy-entrez.autoJwt.label = Auto JWT
thing-type.config.enphase.envoy-entrez.autoJwt.description = This tells the binding whether to attempt an auto-retrieve of the JWT
thing-type.config.enphase.envoy-entrez.hostname.label = Host Name / IP Address
thing-type.config.enphase.envoy-entrez.hostname.description = The host name/ip address of the Envoy gateway. Leave empty to auto detect
thing-type.config.enphase.envoy-entrez.jwt.label = JWT
thing-type.config.enphase.envoy-entrez.jwt.description = This is the JWT when not using Auto JWT
thing-type.config.enphase.envoy-entrez.password.label = Password
thing-type.config.enphase.envoy-entrez.password.description = This the Enphase Cloud Password when using Auto JWT
thing-type.config.enphase.envoy-entrez.refresh.label = Refresh Time
thing-type.config.enphase.envoy-entrez.refresh.description = Period between updates. The default is 5 minutes, the refresh frequency of the Envoy itself
thing-type.config.enphase.envoy-entrez.serialNumber.label = Serial Number
thing-type.config.enphase.envoy-entrez.serialNumber.description = The serial number of the Envoy gateway which can be found on the gateway
thing-type.config.enphase.envoy-entrez.siteName.label = Site Name
thing-type.config.enphase.envoy-entrez.siteName.description = The Site Name, which is above the Site Id in Enphase app. Required when using Auto JWT
thing-type.config.enphase.envoy-entrez.username.label = User Name
thing-type.config.enphase.envoy-entrez.username.description = This is the Enphase Cloud Login when using Auto JWT
# channel types
envoy.cond_flags.acb_ctrl.bmuhardwareerror = BMU Hardware Error
envoy.cond_flags.acb_ctrl.bmuimageerror = BMU Image Error
envoy.cond_flags.acb_ctrl.bmumaxcurrentwarning = BMU Max Current Warning
envoy.cond_flags.acb_ctrl.bmusenseerror = BMU Sense Error
envoy.cond_flags.acb_ctrl.cellmaxtemperror = Cell Max Temperature Error
envoy.cond_flags.acb_ctrl.cellmaxtempwarning = Cell Max Temperature Warning
envoy.cond_flags.acb_ctrl.cellmaxvoltageerror = Cell Max Voltage Error
envoy.cond_flags.acb_ctrl.cellmaxvoltagewarning = Cell Max Voltage Warning
envoy.cond_flags.acb_ctrl.cellmintemperror = Cell Min Temperature Error
envoy.cond_flags.acb_ctrl.cellmintempwarning = Cell Min Temperature Warning
envoy.cond_flags.acb_ctrl.cellminvoltageerror = Cell Min Voltage Error
envoy.cond_flags.acb_ctrl.cellminvoltagewarning = Cell Min Voltage Warning
envoy.cond_flags.acb_ctrl.cibcanerror = CIB CAN Error
envoy.cond_flags.acb_ctrl.cibimageerror = CIB Image Error
envoy.cond_flags.acb_ctrl.cibspierror = CIB SPI Error"
envoy.cond_flags.obs_strs.discovering = Discovering
envoy.cond_flags.obs_strs.failure = Failure to report
envoy.cond_flags.obs_strs.flasherror = Flash Error
envoy.cond_flags.obs_strs.notmonitored = Not Monitored
envoy.cond_flags.obs_strs.ok = Normal
envoy.cond_flags.obs_strs.plmerror = PLM Error
envoy.cond_flags.obs_strs.secmodeenterfailure = Secure mode enter failure
envoy.cond_flags.obs_strs.secmodeexitfailure = Secure mode exit failure
envoy.cond_flags.obs_strs.sleeping = Sleeping"
envoy.cond_flags.pcu_chan.acMonitorError = AC Monitor Error
envoy.cond_flags.pcu_chan.acfrequencyhigh = AC Frequency High
envoy.cond_flags.pcu_chan.acfrequencylow = AC Frequency Low
envoy.cond_flags.pcu_chan.acfrequencyoor = AC Frequency Out Of Range
envoy.cond_flags.pcu_chan.acvoltage_avg_hi = AC Voltage Average High
envoy.cond_flags.pcu_chan.acvoltagehigh = AC Voltage High
envoy.cond_flags.pcu_chan.acvoltagelow = AC Voltage Low
envoy.cond_flags.pcu_chan.acvoltageoor = AC Voltage Out Of Range
envoy.cond_flags.pcu_chan.acvoltageoosp1 = AC Voltage Out Of Range - Phase 1
envoy.cond_flags.pcu_chan.acvoltageoosp2 = AC Voltage Out Of Range - Phase 2
envoy.cond_flags.pcu_chan.acvoltageoosp3 = AC Voltage Out Of Range - Phase 3
envoy.cond_flags.pcu_chan.agfpowerlimiting = AGF Power Limiting
envoy.cond_flags.pcu_chan.dcresistancelow = DC Resistance Low
envoy.cond_flags.pcu_chan.dcresistancelowpoweroff = DC Resistance Low - Power Off
envoy.cond_flags.pcu_chan.dcvoltagetoohigh = DC Voltage Too High
envoy.cond_flags.pcu_chan.dcvoltagetoolow = DC Voltage Too Low
envoy.cond_flags.pcu_chan.dfdt = AC Frequency Changing too Fast
envoy.cond_flags.pcu_chan.gfitripped = GFI Tripped
envoy.cond_flags.pcu_chan.gridgone = Grid Gone
envoy.cond_flags.pcu_chan.gridinstability = Grid Instability
envoy.cond_flags.pcu_chan.gridoffsethi = Grid Offset Hi
envoy.cond_flags.pcu_chan.gridoffsetlow = Grid Offset Low
envoy.cond_flags.pcu_chan.hardwareError = Hardware Error
envoy.cond_flags.pcu_chan.hardwareWarning = Hardware Warning
envoy.cond_flags.pcu_chan.highskiprate = High Skip Rate
envoy.cond_flags.pcu_chan.invalidinterval = Invalid Interval
envoy.cond_flags.pcu_chan.pwrgenoffbycmd = Power generation off by command
envoy.cond_flags.pcu_chan.skippedcycles = Skipped Cycles
envoy.cond_flags.pcu_chan.vreferror = Voltage Ref Error"
envoy.cond_flags.pcu_ctrl.alertactive = Alert Active
envoy.cond_flags.pcu_ctrl.altpwrgenmode = Alternate Power Generation Mode
envoy.cond_flags.pcu_ctrl.altvfsettings = Alternate Voltage and Frequency Settings
envoy.cond_flags.pcu_ctrl.badflashimage = Bad Flash Image
envoy.cond_flags.pcu_ctrl.bricked = No Grid Profile
envoy.cond_flags.pcu_ctrl.commandedreset = Commanded Reset
envoy.cond_flags.pcu_ctrl.criticaltemperature = Critical Temperature
envoy.cond_flags.pcu_ctrl.dc-pwr-low = DC Power Too Low
envoy.cond_flags.pcu_ctrl.iuplinkproblem = IUP Link Problem
envoy.cond_flags.pcu_ctrl.manutestmode = In Manu Test Mode
envoy.cond_flags.pcu_ctrl.nsync = Grid Perturbation Unsynchronized
envoy.cond_flags.pcu_ctrl.overtemperature = Over Temperature
envoy.cond_flags.pcu_ctrl.poweronreset = Power On Reset
envoy.cond_flags.pcu_ctrl.pwrgenoffbycmd = Power generation off by command
envoy.cond_flags.pcu_ctrl.runningonac = Running on AC
envoy.cond_flags.pcu_ctrl.tpmtest = Transient Grid Profile
envoy.cond_flags.pcu_ctrl.unexpectedreset = Unexpected Reset
envoy.cond_flags.pcu_ctrl.watchdogreset = Watchdog Reset
envoy.cond_flags.rgm_chan.check_meter = Meter Error
envoy.cond_flags.rgm_chan.power_quality = Poor Power Quality
error.nodata = No Data
envoy.global.ok = Normal

View File

@ -10,11 +10,11 @@
<description>Envoy gateway</description>
<channel-groups>
<channel-group id="production" typeId="envoy-data">
<channel-group id="production" typeId="envoy-production">
<label>Production</label>
<description>Production data from the solar panels</description>
</channel-group>
<channel-group id="consumption" typeId="envoy-data">
<channel-group id="consumption" typeId="envoy-consumption">
<label>Consumption</label>
<description>Consumption data from the solar panels</description>
</channel-group>
@ -34,14 +34,31 @@
</parameter>
<parameter name="username" type="text">
<label>User Name</label>
<description>The user name to the Envoy gateway. Leave empty when using the default user name</description>
<description>For versions below version 7, this is the user name to the Envoy gateway. Leave empty when using the
default user name. For newer versions this is user name of the Enphase Entrez Cloud Login when using Auto JWT.</description>
<default>envoy</default>
<advanced>true</advanced>
</parameter>
<parameter name="password" type="text">
<context>password</context>
<label>Password</label>
<description>The password to the Envoy gateway. Leave empty when using the default password</description>
<description>For versions below version 7, this is the password to the Envoy gateway. Leave empty when using the
default password. For newer versions this is the password of the Enphase Entrez Cloud Login when using Auto JWT.</description>
<advanced>true</advanced>
</parameter>
<parameter name="siteName" type="text">
<label>Site Name</label>
<description>The Site Name, which is above the Site Id in Enphase app. Required when using Auto JWT</description>
</parameter>
<parameter name="jwt" type="text">
<label>JWT</label>
<description>For versions above version 7, this is the JWT when not using Auto JWT</description>
<advanced>true</advanced>
</parameter>
<parameter name="autoJwt" type="boolean">
<label>Auto JWT</label>
<description>For versions above version 7, this tells the binding whether to attempt an auto-retrieve of the JWT</description>
<default>true</default>
<advanced>true</advanced>
</parameter>
<parameter name="refresh" type="integer" unit="min">
@ -53,7 +70,6 @@
</config-description>
</bridge-type>
<thing-type id="inverter">
<supported-bridge-type-refs>
<bridge-type-ref id="envoy"/>
@ -128,38 +144,69 @@
</thing-type>
<!-- Envoy gateway channels -->
<channel-group-type id="envoy-data">
<channel-group-type id="envoy-production">
<label>Envoy Data</label>
<channels>
<channel id="wattHoursToday" typeId="watt-hours-today"/>
<channel id="wattHoursSevenDays" typeId="watt-hours-seven-days"/>
<channel id="wattHoursLifetime" typeId="watt-hours-lifetime"/>
<channel id="wattsNow" typeId="watts-now"/>
<channel id="wattHoursToday" typeId="watt-hours-today">
<description>Watt hours produced today</description>
</channel>
<channel id="wattHoursSevenDays" typeId="watt-hours-seven-days">
<description>Watt hours produced the last 7 days</description>
</channel>
<channel id="wattHoursLifetime" typeId="watt-hours-lifetime">
<description>Watt hours produced over the lifetime</description>
</channel>
<channel id="wattsNow" typeId="watts-now">
<description>Latest Watt produced</description>
</channel>
</channels>
</channel-group-type>
<channel-group-type id="envoy-consumption">
<label>Envoy Data</label>
<channels>
<channel id="wattHoursToday" typeId="watt-hours-today">
<label>Consumption Today</label>
<description>Watt hours consumption today</description>
</channel>
<channel id="wattHoursSevenDays" typeId="watt-hours-seven-days">
<label>Consumption 7 Days</label>
<description>Watt hours consumption the last 7 days</description>
</channel>
<channel id="wattHoursLifetime" typeId="watt-hours-lifetime">
<label>Consumption Lifetime</label>
<description>Watt hours consumption over the lifetime</description>
</channel>
<channel id="wattsNow" typeId="watts-now">
<label>Latest Consumption Power</label>
<description>Latest Watt consumption</description>
</channel>
</channels>
</channel-group-type>
<channel-type id="watt-hours-today">
<item-type>Number:Energy</item-type>
<label>Produced Today</label>
<description>Watt hours produced today</description>
<category>energy</category>
<state pattern="%d %unit%" readOnly="true"/>
</channel-type>
<channel-type id="watt-hours-seven-days">
<item-type>Number:Energy</item-type>
<label>Produced 7 Days</label>
<description>Watt hours produced the last 7 days</description>
<category>energy</category>
<state pattern="%d %unit%" readOnly="true"/>
</channel-type>
<channel-type id="watt-hours-lifetime">
<item-type>Number:Energy</item-type>
<label>Produced Lifetime</label>
<description>Watt hours produced over the lifetime</description>
<category>energy</category>
<state pattern="%d %unit%" readOnly="true"/>
</channel-type>
<channel-type id="watts-now">
<item-type>Number:Power</item-type>
<label>Latest Power</label>
<label>Latest Produced Power</label>
<description>Latest watts produced</description>
<category>energy</category>
<state pattern="%d %unit%" readOnly="true"/>
</channel-type>
@ -168,18 +215,21 @@
<item-type>Number:Power</item-type>
<label>Last Report</label>
<description>Last reported power delivery</description>
<category>energy</category>
<state pattern="%d %unit%" readOnly="true"/>
</channel-type>
<channel-type id="max-report-watts">
<item-type>Number:Power</item-type>
<label>Max Report</label>
<description>Maximum reported power</description>
<category>energy</category>
<state pattern="%d %unit%" readOnly="true"/>
</channel-type>
<channel-type id="last-report-date">
<item-type>DateTime</item-type>
<label>Last Report Date</label>
<description>Date of last reported power delivery</description>
<category>energy</category>
<state readOnly="true"/>
</channel-type>