mirror of
https://github.com/openhab/openhab-addons.git
synced 2025-01-10 15:11:59 +01:00
[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:
parent
75e51119fa
commit
d58d8b068c
@ -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
|
||||
|
@ -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:
|
||||
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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)) {
|
||||
|
@ -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
|
||||
|
@ -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);
|
||||
|
||||
|
@ -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
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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;
|
||||
|
@ -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;
|
||||
|
@ -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());
|
||||
}
|
||||
}
|
||||
}
|
@ -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();
|
||||
if (!ThingHandlerHelper.isHandlerInitialized(this)) {
|
||||
logger.debug("Not updating anything. Not initialized: {}", getThing().getStatus());
|
||||
return;
|
||||
}
|
||||
if (checkConnection()) {
|
||||
updateEnvoy();
|
||||
updateDevices();
|
||||
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,21 +439,24 @@ 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 {
|
||||
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;
|
||||
connector.setConfiguration(configuration);
|
||||
updateConfiguration(config);
|
||||
updateData();
|
||||
// The task is done so the future can be released by setting it to null.
|
||||
updateHostnameFuture = null;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void dispose() {
|
||||
|
@ -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,16 +206,11 @@ class EnvoyConnector {
|
||||
}
|
||||
|
||||
private synchronized <T> T retrieveData(final String urlPath, final Function<String, @Nullable T> jsonConverter)
|
||||
throws EnvoyConnectionException, EnvoyNoHostnameException {
|
||||
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();
|
||||
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);
|
||||
@ -183,6 +231,20 @@ class EnvoyConnector {
|
||||
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");
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
@ -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());
|
||||
}
|
||||
}
|
@ -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>
|
||||
|
@ -1,11 +1,127 @@
|
||||
error.nodata=No Data
|
||||
envoy.global.ok=Normal
|
||||
# add-on
|
||||
|
||||
addon.enphase.name = Enphase Envoy Binding
|
||||
addon.enphase.description = This is the binding for Enphase Envoy solar panels.
|
||||
|
||||
# thing types
|
||||
|
||||
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
|
||||
|
||||
# thing types config
|
||||
|
||||
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
|
||||
@ -26,7 +142,6 @@ 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
|
||||
@ -56,7 +171,6 @@ 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
|
||||
@ -75,6 +189,7 @@ 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
|
||||
|
@ -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>
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user