diff --git a/CODEOWNERS b/CODEOWNERS
index b389428a27569..2d50da55a03c1 100644
--- a/CODEOWNERS
+++ b/CODEOWNERS
@@ -69,6 +69,7 @@
/bundles/org.openhab.binding.energenie/ @hmerk
/bundles/org.openhab.binding.enigma2/ @gdolfen
/bundles/org.openhab.binding.enocean/ @fruggy83
+/bundles/org.openhab.binding.enphase/ @Hilbrand
/bundles/org.openhab.binding.enturno/ @klocsson
/bundles/org.openhab.binding.epsonprojector/ @mlobstein
/bundles/org.openhab.binding.etherrain/ @dfad1469
diff --git a/bom/openhab-addons/pom.xml b/bom/openhab-addons/pom.xml
index 7b8d6ce5ff569..cdd2aaa0ab5e6 100644
--- a/bom/openhab-addons/pom.xml
+++ b/bom/openhab-addons/pom.xml
@@ -331,6 +331,11 @@
org.openhab.binding.enocean${project.version}
+
+ org.openhab.addons.bundles
+ org.openhab.binding.enphase
+ ${project.version}
+ org.openhab.addons.bundlesorg.openhab.binding.enturno
diff --git a/bundles/org.openhab.binding.enphase/NOTICE b/bundles/org.openhab.binding.enphase/NOTICE
new file mode 100644
index 0000000000000..38d625e349232
--- /dev/null
+++ b/bundles/org.openhab.binding.enphase/NOTICE
@@ -0,0 +1,13 @@
+This content is produced and maintained by the openHAB project.
+
+* Project home: https://www.openhab.org
+
+== Declared Project Licenses
+
+This program and the accompanying materials are made available under the terms
+of the Eclipse Public License 2.0 which is available at
+https://www.eclipse.org/legal/epl-2.0/.
+
+== Source Code
+
+https://github.com/openhab/openhab-addons
diff --git a/bundles/org.openhab.binding.enphase/README.md b/bundles/org.openhab.binding.enphase/README.md
new file mode 100644
index 0000000000000..4f768bb405b4a
--- /dev/null
+++ b/bundles/org.openhab.binding.enphase/README.md
@@ -0,0 +1,113 @@
+# Enphase Binding
+
+This is the binding for the [Enphase](https://enphase.com/) Envoy Solar Panel gateway.
+The binding uses the local API of the Envoy gateway.
+Some calls can be made without authentication and some use a user name and password.
+The default user name is `envoy` and the default password is the last 6 numbers of the serial number.
+The Envoy gateway updates the data every 5 minutes.
+Therefore using a refresh rate shorter doesn't provide more information.
+
+## Supported Things
+
+The follow things are supported:
+
+* `envoy` The Envoy gateway thing, which is a bridge thing.
+* `inverter` A Enphase micro inverter connected to a solar panel.
+* `relay` A Enphase relay.
+
+Not all Envoy gateways support all channels and things.
+Therefore some data on inverters and the relay may not be available.
+The binding auto detects which data is available and will report this in the log on initialization of the gateway bridge.
+
+## Discovery
+
+The binding can discover Envoy gateways, micro inverters and relays.
+
+## Thing Configuration
+
+The Envoy gateway thing `envoy` has the following configuration options:
+
+| 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 |
+| 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 |
+
+The micro inverter `inverter` and `relay` things have only 1 parameter:
+
+| parameter | required | description |
+|--------------|----------|-----------------------------------|
+| serialNumber | yes | The serial number of the inverter |
+
+## Channels
+
+The `envoy` thing has can show both production as well as consumption data.
+There are channel groups for `production` and `consumption` data.
+The `consumption` data is only available if the gateway reports this.
+A example of a production channel name is: `production#wattsNow`.
+
+| channel | type | description |
+|--------------------|---------------|---------------------------------------|
+| wattHoursToday | Number:Energy | Watt hours produced today |
+| wattHoursSevenDays | Number:Energy | Watt hours produced the last 7 days |
+| wattHoursLifetime | Number:Energy | Watt hours produced over the lifetime |
+| wattsNow | Number:Power | Latest watts produced |
+
+The `inverter` thing has the following channels:
+
+| channel | type | description |
+|-----------------|--------------|--------------------------------------|
+| lastReportWatts | Number:Power | Last reported power delivery |
+| maxReportWatts | Number:Power | Maximum reported power |
+| lastReportDate | DateTime | Date of last reported power delivery |
+
+The following channels are only available if supported by the Envoy gateway:
+
+The `relay` thing has the following channels:
+
+| channel | type | description |
+|-----------------|--------------|--------------------------------------------------------|
+| relay | Contact | Status of the relay. |
+| line1Connected | Contact | If power line 1 is connected. If closed it's connected |
+| line2Connected | Contact | If power line 2 is connected. If closed it's connected |
+| line2Connected | Contact | If power line 3 is connected. If closed it's connected |
+
+The `inverter` and `relay` have the following additional advanced channels:
+
+| channel | type | description |
+|-----------------|--------------------|--------------------------------------|
+| producing | Switch (Read Only) | If the device is producing |
+| communicating | Switch (Read Only) | If the device is communicating |
+| provisioned | Switch (Read Only) | If the device is provisioned |
+| operating | Switch (Read Only) | If the device is operating |
+
+## Full Example
+
+Things example:
+
+```
+Bridge enphase:envoy:789012 "Envoy" [ serialNumber="12345789012" ] {
+ Things:
+ inverter 123456 "Enphase Inverter 123456" [ serialNumber="789012123456" ]
+ inverter 223456 "Enphase Inverter 223456" [ serialNumber="789012223456" ]
+}
+```
+
+Items example:
+
+```
+Number:Power envoyWattsNow "Watts Now [%d %unit%]" { channel="enphase:envoy:789012:production#wattsNow" }
+Number:Energy envoyWattHoursToday "Watt Hours Today [%d %unit%]" { channel="enphase:envoy:789012:production#wattHoursToday" }
+Number:Energy envoyWattHours7Days "Watt Hours 7 Days [%.1f kWh]" { channel="enphase:envoy:789012:production#wattHoursSevenDays" }
+Number:Energy envoyWattHoursLifetime "Watt Hours Lifetime [%.1f kWh]" { channel="enphase:envoy:789012:production#wattHoursLifetime" }
+
+Number:Power i1LastReportWatts "Last Report [%d %unit%]" { channel="enphase:inverter:789012:123456:lastReportWatts" }
+Number:Power i1MaxReportWatts "Max Report [%d %unit%]" { channel="enphase:inverter:789012:123456:maxReportWatts" }
+DateTime i1LastReportDate "Last Report Date [%1$tY-%1$tm-%1$td %1$tH:%1$tM]" { channel="enphase:inverter:789012:123456:lastReportDate" }
+
+Number:Power i2LastReportWatts "Last Report [%d %unit%]" { channel="enphase:inverter:789012:223456:lastReportWatts" }
+Number:Power i21MaxReportWatts "Max Report [%d %unit%]" { channel="enphase:inverter:789012:223456:maxReportWatts" }
+DateTime i2LastReportDate "Last Report Date [%1$tY-%1$tm-%1$td %1$tH:%1$tM]" { channel="enphase:inverter:789012:223456:lastReportDate" }
+```
diff --git a/bundles/org.openhab.binding.enphase/pom.xml b/bundles/org.openhab.binding.enphase/pom.xml
new file mode 100644
index 0000000000000..12e5c60bac012
--- /dev/null
+++ b/bundles/org.openhab.binding.enphase/pom.xml
@@ -0,0 +1,17 @@
+
+
+
+ 4.0.0
+
+
+ org.openhab.addons.bundles
+ org.openhab.addons.reactor.bundles
+ 3.1.0-SNAPSHOT
+
+
+ org.openhab.binding.enphase
+
+ openHAB Add-ons :: Bundles :: Enphase Binding
+
+
diff --git a/bundles/org.openhab.binding.enphase/src/main/feature/feature.xml b/bundles/org.openhab.binding.enphase/src/main/feature/feature.xml
new file mode 100644
index 0000000000000..537cf0dc53526
--- /dev/null
+++ b/bundles/org.openhab.binding.enphase/src/main/feature/feature.xml
@@ -0,0 +1,10 @@
+
+
+ mvn:org.openhab.core.features.karaf/org.openhab.core.features.karaf.openhab-core/${ohc.version}/xml/features
+
+
+
+ openhab-runtime-base
+ mvn:org.openhab.addons.bundles/org.openhab.binding.enphase/${project.version}
+
+
diff --git a/bundles/org.openhab.binding.enphase/src/main/java/org/openhab/binding/enphase/internal/EnphaseBindingConstants.java b/bundles/org.openhab.binding.enphase/src/main/java/org/openhab/binding/enphase/internal/EnphaseBindingConstants.java
new file mode 100644
index 0000000000000..4d08743545709
--- /dev/null
+++ b/bundles/org.openhab.binding.enphase/src/main/java/org/openhab/binding/enphase/internal/EnphaseBindingConstants.java
@@ -0,0 +1,114 @@
+/**
+ * Copyright (c) 2010-2021 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;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.core.thing.ThingTypeUID;
+
+/**
+ * The {@link EnphaseBindingConstants} class defines common constants, which are
+ * used across the whole binding.
+ *
+ * @author Hilbrand Bouwkamp - Initial contribution
+ */
+@NonNullByDefault
+public class EnphaseBindingConstants {
+
+ private static final String BINDING_ID = "enphase";
+
+ // List of all Thing Type UIDs
+ public static final ThingTypeUID THING_TYPE_ENPHASE_ENVOY = new ThingTypeUID(BINDING_ID, "envoy");
+ public static final ThingTypeUID THING_TYPE_ENPHASE_INVERTER = new ThingTypeUID(BINDING_ID, "inverter");
+ public static final ThingTypeUID THING_TYPE_ENPHASE_RELAY = new ThingTypeUID(BINDING_ID, "relay");
+
+ // Configuration parameters
+ public static final String CONFIG_SERIAL_NUMBER = "serialNumber";
+ public static final String CONFIG_HOSTNAME = "hostname";
+ public static final String CONFIG_USERNAME = "username";
+ public static final String CONFIG_PASSWORD = "password";
+ public static final String CONFIG_REFRESH = "refresh";
+ public static final String PROPERTY_VERSION = "version";
+
+ // Envoy gateway channels
+ public static final String ENVOY_CHANNELGROUP_CONSUMPTION = "consumption";
+ public static final String ENVOY_WATT_HOURS_TODAY = "wattHoursToday";
+ public static final String ENVOY_WATT_HOURS_SEVEN_DAYS = "wattHoursSevenDays";
+ public static final String ENVOY_WATT_HOURS_LIFETIME = "wattHoursLifetime";
+ public static final String ENVOY_WATTS_NOW = "wattsNow";
+
+ // Device channels
+ public static final String DEVICE_CHANNEL_STATUS = "status";
+ public static final String DEVICE_CHANNEL_PRODUCING = "producing";
+ public static final String DEVICE_CHANNEL_COMMUNICATING = "communicating";
+ public static final String DEVICE_CHANNEL_PROVISIONED = "provisioned";
+ public static final String DEVICE_CHANNEL_OPERATING = "operating";
+
+ // Inverter channels
+ public static final String INVERTER_CHANNEL_LAST_REPORT_WATTS = "lastReportWatts";
+ public static final String INVERTER_CHANNEL_MAX_REPORT_WATTS = "maxReportWatts";
+ public static final String INVERTER_CHANNEL_LAST_REPORT_DATE = "lastReportDate";
+
+ // Relay channels
+ public static final String RELAY_CHANNEL_RELAY = "relay";
+ public static final String RELAY_CHANNEL_LINE_1_CONNECTED = "line1Connected";
+ public static final String RELAY_CHANNEL_LINE_2_CONNECTED = "line2Connected";
+ public static final String RELAY_CHANNEL_LINE_3_CONNECTED = "line3Connected";
+
+ public static final String RELAY_STATUS_CLOSED = "closed";
+
+ // Properties
+ public static final String DEVICE_PROPERTY_PART_NUMBER = "partNumber";
+
+ // Discovery constants
+ public static final String DISCOVERY_SERIAL = "serialnum";
+ public static final String DISCOVERY_VERSION = "protovers";
+
+ // Status messages
+ public static final String DEVICE_STATUS_OK = "envoy.global.ok";
+ public static final String ERROR_NODATA = "error.nodata";
+
+ public enum EnphaseDeviceType {
+ ACB, // AC Battery
+ PSU, // Inverter
+ NSRB; // Network system relay controller
+
+ public static @Nullable EnphaseDeviceType safeValueOf(final String type) {
+ try {
+ return valueOf(type);
+ } catch (final IllegalArgumentException e) {
+ return null;
+ }
+ }
+ }
+
+ /**
+ * Derives the default password from the serial number.
+ *
+ * @param serialNumber serial number to use
+ * @return the default password or empty string if serial number is to short.
+ */
+ public static String defaultPassword(final String serialNumber) {
+ return isValidSerial(serialNumber) ? serialNumber.substring(serialNumber.length() - 6) : "";
+ }
+
+ /**
+ * Checks if the serial number is at least long enough to contain the default password.
+ *
+ * @param serialNumber serial number to check
+ * @return true if not null and at least 6 characters long.
+ */
+ public static boolean isValidSerial(@Nullable final String serialNumber) {
+ return serialNumber != null && serialNumber.length() > 6;
+ }
+}
diff --git a/bundles/org.openhab.binding.enphase/src/main/java/org/openhab/binding/enphase/internal/EnphaseHandlerFactory.java b/bundles/org.openhab.binding.enphase/src/main/java/org/openhab/binding/enphase/internal/EnphaseHandlerFactory.java
new file mode 100644
index 0000000000000..4f267a5cd51c0
--- /dev/null
+++ b/bundles/org.openhab.binding.enphase/src/main/java/org/openhab/binding/enphase/internal/EnphaseHandlerFactory.java
@@ -0,0 +1,83 @@
+/**
+ * Copyright (c) 2010-2021 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;
+
+import static org.openhab.binding.enphase.internal.EnphaseBindingConstants.*;
+
+import java.util.Set;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.eclipse.jetty.client.HttpClient;
+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;
+import org.openhab.core.thing.binding.BaseThingHandlerFactory;
+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.Reference;
+
+/**
+ * The {@link EnphaseHandlerFactory} is responsible for creating things and thing
+ * handlers.
+ *
+ * @author Hilbrand Bouwkamp - Initial contribution
+ */
+@NonNullByDefault
+@Component(configurationPid = "binding.enphase", service = ThingHandlerFactory.class)
+public class EnphaseHandlerFactory extends BaseThingHandlerFactory {
+
+ private static final Set SUPPORTED_THING_TYPES_UIDS = Set.of(THING_TYPE_ENPHASE_ENVOY,
+ THING_TYPE_ENPHASE_INVERTER, THING_TYPE_ENPHASE_RELAY);
+
+ private final MessageTranslator messageTranslator;
+ private final HttpClient commonHttpClient;
+ private final EnvoyHostAddressCache envoyHostAddressCache;
+
+ @Activate
+ public EnphaseHandlerFactory(final @Reference LocaleProvider localeProvider,
+ final @Reference TranslationProvider i18nProvider, final @Reference HttpClientFactory httpClientFactory,
+ @Reference final EnvoyHostAddressCache envoyHostAddressCache) {
+ messageTranslator = new MessageTranslator(localeProvider, i18nProvider);
+ commonHttpClient = httpClientFactory.getCommonHttpClient();
+ this.envoyHostAddressCache = envoyHostAddressCache;
+ }
+
+ @Override
+ public boolean supportsThingType(final ThingTypeUID thingTypeUID) {
+ return SUPPORTED_THING_TYPES_UIDS.contains(thingTypeUID);
+ }
+
+ @Override
+ protected @Nullable ThingHandler createHandler(final Thing thing) {
+ final ThingTypeUID thingTypeUID = thing.getThingTypeUID();
+
+ if (THING_TYPE_ENPHASE_ENVOY.equals(thingTypeUID)) {
+ return new EnvoyBridgeHandler((Bridge) thing, commonHttpClient, envoyHostAddressCache);
+ } else if (THING_TYPE_ENPHASE_INVERTER.equals(thingTypeUID)) {
+ return new EnphaseInverterHandler(thing, messageTranslator);
+ } else if (THING_TYPE_ENPHASE_RELAY.equals(thingTypeUID)) {
+ return new EnphaseRelayHandler(thing, messageTranslator);
+ }
+
+ return null;
+ }
+}
diff --git a/bundles/org.openhab.binding.enphase/src/main/java/org/openhab/binding/enphase/internal/EnvoyConfiguration.java b/bundles/org.openhab.binding.enphase/src/main/java/org/openhab/binding/enphase/internal/EnvoyConfiguration.java
new file mode 100644
index 0000000000000..e9a0aef7d710a
--- /dev/null
+++ b/bundles/org.openhab.binding.enphase/src/main/java/org/openhab/binding/enphase/internal/EnvoyConfiguration.java
@@ -0,0 +1,39 @@
+/**
+ * Copyright (c) 2010-2021 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;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+
+/**
+ * The {@link EnvoyConfiguration} class contains fields mapping thing configuration parameters.
+ *
+ * @author Hilbrand Bouwkamp - Initial contribution
+ */
+@NonNullByDefault
+public class EnvoyConfiguration {
+
+ public static final String DEFAULT_USERNAME = "envoy";
+ private static final int DEFAULT_REFRESH_MINUTES = 5;
+
+ public String serialNumber = "";
+ public String hostname = "";
+ public String username = DEFAULT_USERNAME;
+ public String password = "";
+ public int refresh = DEFAULT_REFRESH_MINUTES;
+
+ @Override
+ public String toString() {
+ return "EnvoyConfiguration [serialNumber=" + serialNumber + ", hostname=" + hostname + ", username=" + username
+ + ", password=" + password + ", refresh=" + refresh + "]";
+ }
+}
diff --git a/bundles/org.openhab.binding.enphase/src/main/java/org/openhab/binding/enphase/internal/EnvoyConnectionException.java b/bundles/org.openhab.binding.enphase/src/main/java/org/openhab/binding/enphase/internal/EnvoyConnectionException.java
new file mode 100644
index 0000000000000..d468d3d32053e
--- /dev/null
+++ b/bundles/org.openhab.binding.enphase/src/main/java/org/openhab/binding/enphase/internal/EnvoyConnectionException.java
@@ -0,0 +1,35 @@
+/**
+ * Copyright (c) 2010-2021 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;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+
+/**
+ * Exception thrown when a connection problem occurs to the Envoy gateway.
+ *
+ * @author Hilbrand Bouwkamp - Initial contribution
+ */
+@NonNullByDefault
+public class EnvoyConnectionException extends Exception {
+
+ private static final long serialVersionUID = 1L;
+
+ public EnvoyConnectionException(final String message) {
+ super(message);
+ }
+
+ public EnvoyConnectionException(final String message, final @Nullable Throwable e) {
+ super(message + (e == null ? "" : e.getMessage()), e);
+ }
+}
diff --git a/bundles/org.openhab.binding.enphase/src/main/java/org/openhab/binding/enphase/internal/EnvoyHostAddressCache.java b/bundles/org.openhab.binding.enphase/src/main/java/org/openhab/binding/enphase/internal/EnvoyHostAddressCache.java
new file mode 100644
index 0000000000000..be6198d3b63ab
--- /dev/null
+++ b/bundles/org.openhab.binding.enphase/src/main/java/org/openhab/binding/enphase/internal/EnvoyHostAddressCache.java
@@ -0,0 +1,33 @@
+/**
+ * Copyright (c) 2010-2021 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;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+
+/**
+ * Service that keeps track of host names/ip addresses of discovered Envoy devices.
+ *
+ * @author Hilbrand Bouwkamp - Initial contribution
+ */
+@NonNullByDefault
+public interface EnvoyHostAddressCache {
+
+ /**
+ * Returns the known host name/ip address for the device with the given serial number.
+ * If not known an empty string is returned.
+ *
+ * @param serialNumber serial number of device to get host address for
+ * @return the known host address or an empty string if not known
+ */
+ String getLastKnownHostAddress(String serialNumber);
+}
diff --git a/bundles/org.openhab.binding.enphase/src/main/java/org/openhab/binding/enphase/internal/EnvoyNoHostnameException.java b/bundles/org.openhab.binding.enphase/src/main/java/org/openhab/binding/enphase/internal/EnvoyNoHostnameException.java
new file mode 100644
index 0000000000000..95c25c6b4e04c
--- /dev/null
+++ b/bundles/org.openhab.binding.enphase/src/main/java/org/openhab/binding/enphase/internal/EnvoyNoHostnameException.java
@@ -0,0 +1,30 @@
+/**
+ * Copyright (c) 2010-2021 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;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+
+/**
+ * Exception thrown when a api call is made while the hostname / ip address is not set.
+ *
+ * @author Hilbrand Bouwkamp - Initial contribution
+ */
+@NonNullByDefault
+public class EnvoyNoHostnameException extends Exception {
+
+ private static final long serialVersionUID = 1L;
+
+ public EnvoyNoHostnameException(final String message) {
+ super(message);
+ }
+}
diff --git a/bundles/org.openhab.binding.enphase/src/main/java/org/openhab/binding/enphase/internal/MessageTranslator.java b/bundles/org.openhab.binding.enphase/src/main/java/org/openhab/binding/enphase/internal/MessageTranslator.java
new file mode 100644
index 0000000000000..71e60408926c9
--- /dev/null
+++ b/bundles/org.openhab.binding.enphase/src/main/java/org/openhab/binding/enphase/internal/MessageTranslator.java
@@ -0,0 +1,51 @@
+/**
+ * Copyright (c) 2010-2021 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;
+
+import static org.openhab.binding.enphase.internal.EnphaseBindingConstants.ERROR_NODATA;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.core.i18n.LocaleProvider;
+import org.openhab.core.i18n.TranslationProvider;
+import org.osgi.framework.Bundle;
+import org.osgi.framework.FrameworkUtil;
+
+/**
+ * Class to get the message for the enphase message code.
+ *
+ * @author Hilbrand Bouwkamp - Initial contribution
+ */
+@NonNullByDefault
+public class MessageTranslator {
+
+ private final LocaleProvider localeProvider;
+ private final TranslationProvider i18nProvider;
+ private final Bundle bundle;
+
+ public MessageTranslator(LocaleProvider localeProvider, TranslationProvider i18nProvider) {
+ this.localeProvider = localeProvider;
+ this.i18nProvider = i18nProvider;
+ bundle = FrameworkUtil.getBundle(this.getClass());
+ }
+
+ /**
+ * Gets the message text for the enphase message code.
+ *
+ * @param key the enphase message code
+ * @return translated key
+ */
+ public @Nullable String translate(String key) {
+ return i18nProvider.getText(bundle, key, ERROR_NODATA, localeProvider.getLocale());
+ }
+}
diff --git a/bundles/org.openhab.binding.enphase/src/main/java/org/openhab/binding/enphase/internal/discovery/EnphaseDevicesDiscoveryService.java b/bundles/org.openhab.binding.enphase/src/main/java/org/openhab/binding/enphase/internal/discovery/EnphaseDevicesDiscoveryService.java
new file mode 100644
index 0000000000000..7b1bfe3fe0409
--- /dev/null
+++ b/bundles/org.openhab.binding.enphase/src/main/java/org/openhab/binding/enphase/internal/discovery/EnphaseDevicesDiscoveryService.java
@@ -0,0 +1,137 @@
+/**
+ * Copyright (c) 2010-2021 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.discovery;
+
+import static org.openhab.binding.enphase.internal.EnphaseBindingConstants.*;
+
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Map.Entry;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+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;
+import org.openhab.binding.enphase.internal.handler.EnvoyBridgeHandler;
+import org.openhab.core.config.discovery.AbstractDiscoveryService;
+import org.openhab.core.config.discovery.DiscoveryResult;
+import org.openhab.core.config.discovery.DiscoveryResultBuilder;
+import org.openhab.core.config.discovery.DiscoveryService;
+import org.openhab.core.thing.ThingTypeUID;
+import org.openhab.core.thing.ThingUID;
+import org.openhab.core.thing.binding.ThingHandler;
+import org.openhab.core.thing.binding.ThingHandlerService;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Discovery service to discovery Enphase inverters connected to an Envoy gateway.
+ *
+ * @author Thomas Hentschel - Initial contribution
+ * @author Hilbrand Bouwkamp - Initial contribution
+ */
+@NonNullByDefault
+public class EnphaseDevicesDiscoveryService extends AbstractDiscoveryService
+ implements ThingHandlerService, DiscoveryService {
+
+ private static final int TIMEOUT_SECONDS = 20;
+
+ private final Logger logger = LoggerFactory.getLogger(EnphaseDevicesDiscoveryService.class);
+ private @Nullable EnvoyBridgeHandler envoyHandler;
+
+ public EnphaseDevicesDiscoveryService() {
+ super(Collections.singleton(THING_TYPE_ENPHASE_INVERTER), TIMEOUT_SECONDS, false);
+ }
+
+ @Override
+ public void setThingHandler(final @Nullable ThingHandler handler) {
+ if (handler instanceof EnvoyBridgeHandler) {
+ envoyHandler = (EnvoyBridgeHandler) handler;
+ }
+ }
+
+ @Override
+ public @Nullable ThingHandler getThingHandler() {
+ return envoyHandler;
+ }
+
+ @Override
+ public void deactivate() {
+ super.deactivate();
+ }
+
+ @Override
+ protected void startScan() {
+ removeOlderResults(getTimestampOfLastScan());
+ final EnvoyBridgeHandler envoyHandler = this.envoyHandler;
+
+ if (envoyHandler == null || !envoyHandler.isOnline()) {
+ logger.debug("Envoy handler not available or online: {}", envoyHandler);
+ return;
+ }
+ final ThingUID uid = envoyHandler.getThing().getUID();
+
+ scanForInverterThings(envoyHandler, uid);
+ scanForDeviceThings(envoyHandler, uid);
+ }
+
+ private void scanForInverterThings(final EnvoyBridgeHandler envoyHandler, final ThingUID bridgeID) {
+ final Map inverters = envoyHandler.getInvertersData(true);
+
+ if (inverters == null) {
+ logger.debug("No inverter data for Enphase inverters in discovery for Envoy {}.", bridgeID);
+ } else {
+ for (final Entry entry : inverters.entrySet()) {
+ discover(bridgeID, entry.getKey(), THING_TYPE_ENPHASE_INVERTER, "Inverter ");
+ }
+ }
+ }
+
+ /**
+ * Scans for other device things ('other' as in: no inverters).
+ *
+ * @param envoyHandler
+ * @param bridgeID
+ */
+ private void scanForDeviceThings(final EnvoyBridgeHandler envoyHandler, final ThingUID bridgeID) {
+ final Map devices = envoyHandler.getDevices(true);
+
+ if (devices == null) {
+ logger.debug("No device data for Enphase devices in discovery for Envoy {}.", bridgeID);
+ } else {
+ for (final Entry entry : devices.entrySet()) {
+ final DeviceDTO dto = entry.getValue();
+ final EnphaseDeviceType type = dto == null ? null : EnphaseDeviceType.safeValueOf(dto.type);
+
+ if (type == EnphaseDeviceType.NSRB) {
+ discover(bridgeID, entry.getKey(), THING_TYPE_ENPHASE_RELAY, "Relay ");
+ }
+ }
+ }
+ }
+
+ private void discover(final ThingUID bridgeID, final String serialNumber, final ThingTypeUID typeUID,
+ final String label) {
+ final String shortSerialNumber = defaultPassword(serialNumber);
+ final ThingUID thingUID = new ThingUID(typeUID, bridgeID, shortSerialNumber);
+ final Map properties = new HashMap<>(1);
+
+ properties.put(CONFIG_SERIAL_NUMBER, serialNumber);
+ final DiscoveryResult discoveryResult = DiscoveryResultBuilder.create(thingUID).withBridge(bridgeID)
+ .withRepresentationProperty(CONFIG_SERIAL_NUMBER).withProperties(properties)
+ .withLabel("Enphase " + label + shortSerialNumber).build();
+ thingDiscovered(discoveryResult);
+ }
+}
diff --git a/bundles/org.openhab.binding.enphase/src/main/java/org/openhab/binding/enphase/internal/discovery/EnvoyDiscoveryParticipant.java b/bundles/org.openhab.binding.enphase/src/main/java/org/openhab/binding/enphase/internal/discovery/EnvoyDiscoveryParticipant.java
new file mode 100644
index 0000000000000..beae1cabfed10
--- /dev/null
+++ b/bundles/org.openhab.binding.enphase/src/main/java/org/openhab/binding/enphase/internal/discovery/EnvoyDiscoveryParticipant.java
@@ -0,0 +1,136 @@
+/**
+ * Copyright (c) 2010-2021 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.discovery;
+
+import static org.openhab.binding.enphase.internal.EnphaseBindingConstants.*;
+
+import java.net.Inet4Address;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
+
+import javax.jmdns.ServiceInfo;
+
+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.EnvoyHostAddressCache;
+import org.openhab.core.config.discovery.DiscoveryResult;
+import org.openhab.core.config.discovery.DiscoveryResultBuilder;
+import org.openhab.core.config.discovery.mdns.MDNSDiscoveryParticipant;
+import org.openhab.core.thing.ThingTypeUID;
+import org.openhab.core.thing.ThingUID;
+import org.osgi.service.component.annotations.Component;
+import org.slf4j.Logger;
+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.
+ *
+ * @author Thomas Hentschel - Initial contribution
+ * @author Hilbrand Bouwkamp - Initial contribution
+ */
+@Component(service = { EnvoyHostAddressCache.class, MDNSDiscoveryParticipant.class })
+@NonNullByDefault
+public class EnvoyDiscoveryParticipant implements MDNSDiscoveryParticipant, EnvoyHostAddressCache {
+ private static final String ENVOY_MDNS_ID = "envoy";
+
+ private final Logger logger = LoggerFactory.getLogger(EnvoyDiscoveryParticipant.class);
+
+ private final Map lastKnownHostAddresses = new ConcurrentHashMap<>();
+
+ @Override
+ public Set getSupportedThingTypeUIDs() {
+ return Collections.singleton(EnphaseBindingConstants.THING_TYPE_ENPHASE_ENVOY);
+ }
+
+ @Override
+ public String getServiceType() {
+ return "_enphase-envoy._tcp.local.";
+ }
+
+ @Override
+ public @Nullable DiscoveryResult createResult(final ServiceInfo info) {
+ final String id = info.getName();
+
+ logger.debug("id found: {} with type: {}", id, info.getType());
+
+ if (!id.contains(ENVOY_MDNS_ID)) {
+ return null;
+ }
+
+ if (info.getInet4Addresses().length == 0 || info.getInet4Addresses()[0] == null) {
+ return null;
+ }
+
+ final ThingUID uid = getThingUID(info);
+
+ if (uid == null) {
+ return null;
+ }
+
+ final Inet4Address hostname = info.getInet4Addresses()[0];
+ final String serialNumber = info.getPropertyString(DISCOVERY_SERIAL);
+
+ if (serialNumber == null) {
+ logger.debug("No serial number found in data for discovered Envoy {}: {}", id, info);
+ return null;
+ }
+ final String version = info.getPropertyString(DISCOVERY_VERSION);
+ final String hostAddress = hostname == null ? "" : hostname.getHostAddress();
+
+ lastKnownHostAddresses.put(serialNumber, hostAddress);
+ final Map properties = new HashMap<>(3);
+
+ properties.put(CONFIG_SERIAL_NUMBER, serialNumber);
+ properties.put(CONFIG_HOSTNAME, hostAddress);
+ properties.put(PROPERTY_VERSION, version);
+ return DiscoveryResultBuilder.create(uid).withProperties(properties)
+ .withRepresentationProperty(CONFIG_SERIAL_NUMBER)
+ .withLabel("Enphase Envoy " + defaultPassword(serialNumber)).build();
+ }
+
+ @Override
+ public String getLastKnownHostAddress(final String serialNumber) {
+ final String hostAddress = lastKnownHostAddresses.get(serialNumber);
+
+ return hostAddress == null ? "" : hostAddress;
+ }
+
+ @Override
+ public @Nullable ThingUID getThingUID(final ServiceInfo info) {
+ final String name = info.getName();
+
+ if (!name.contains(ENVOY_MDNS_ID)) {
+ logger.trace("Found other type of device that is not recognized as an Envoy: {}", name);
+ return null;
+ }
+ if (info.getInet4Addresses().length == 0 || info.getInet4Addresses()[0] == null) {
+ logger.debug("Found an Envoy, but no ip address is given: {}", info);
+ return null;
+ }
+ logger.debug("ServiceInfo addr: {}", info.getInet4Addresses()[0]);
+ if (getServiceType().equals(info.getType())) {
+ final String serial = info.getPropertyString(DISCOVERY_SERIAL);
+
+ logger.debug("Discovered an Envoy with serial number '{}'", serial);
+ return new ThingUID(THING_TYPE_ENPHASE_ENVOY, serial);
+ }
+ return null;
+ }
+}
diff --git a/bundles/org.openhab.binding.enphase/src/main/java/org/openhab/binding/enphase/internal/dto/EnvoyEnergyDTO.java b/bundles/org.openhab.binding.enphase/src/main/java/org/openhab/binding/enphase/internal/dto/EnvoyEnergyDTO.java
new file mode 100644
index 0000000000000..8857858bf6158
--- /dev/null
+++ b/bundles/org.openhab.binding.enphase/src/main/java/org/openhab/binding/enphase/internal/dto/EnvoyEnergyDTO.java
@@ -0,0 +1,25 @@
+/**
+ * Copyright (c) 2010-2021 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 from api/v1/production api call.
+ *
+ * @author Hilbrand Bouwkamp - Initial contribution
+ */
+public class EnvoyEnergyDTO {
+ public int wattHoursToday;
+ public int wattHoursSevenDays;
+ public int wattHoursLifetime;
+ public int wattsNow;
+}
diff --git a/bundles/org.openhab.binding.enphase/src/main/java/org/openhab/binding/enphase/internal/dto/EnvoyErrorDTO.java b/bundles/org.openhab.binding.enphase/src/main/java/org/openhab/binding/enphase/internal/dto/EnvoyErrorDTO.java
new file mode 100644
index 0000000000000..a8a29d8649cf6
--- /dev/null
+++ b/bundles/org.openhab.binding.enphase/src/main/java/org/openhab/binding/enphase/internal/dto/EnvoyErrorDTO.java
@@ -0,0 +1,31 @@
+/**
+ * Copyright (c) 2010-2021 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 handling errors returned by the Envoy gateway.
+ *
+ * @author Hilbrand Bouwkamp - Initial contribution
+ */
+public class EnvoyErrorDTO {
+ public int status;
+ public String error;
+ public String info;
+ public String moreInfo;
+
+ @Override
+ public String toString() {
+ return "EnvoyErrorDTO [status=" + status + ", error=" + error + ", info=" + info + ", moreInfo=" + moreInfo
+ + "]";
+ }
+}
diff --git a/bundles/org.openhab.binding.enphase/src/main/java/org/openhab/binding/enphase/internal/dto/InventoryJsonDTO.java b/bundles/org.openhab.binding.enphase/src/main/java/org/openhab/binding/enphase/internal/dto/InventoryJsonDTO.java
new file mode 100644
index 0000000000000..d6efa5ae0c73f
--- /dev/null
+++ b/bundles/org.openhab.binding.enphase/src/main/java/org/openhab/binding/enphase/internal/dto/InventoryJsonDTO.java
@@ -0,0 +1,58 @@
+/**
+ * Copyright (c) 2010-2021 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;
+
+import com.google.gson.annotations.SerializedName;
+
+/**
+ * @author Hilbrand Bouwkamp - Initial contribution
+ */
+public class InventoryJsonDTO {
+
+ public class DeviceDTO {
+ public String type;
+
+ @SerializedName("part_num")
+ public String partNumber;
+ @SerializedName("serial_num")
+ public String serialNumber;
+
+ @SerializedName("device_status")
+ private String[] deviceStatus;
+ @SerializedName("last_rpt_date")
+ public String lastReportDate;
+ public boolean producing;
+ public boolean communicating;
+ public boolean provisioned;
+ public boolean operating;
+ // NSRB data
+ public String relay;
+ @SerializedName("line1-connected")
+ public boolean line1Connected;
+ @SerializedName("line2-connected")
+ public boolean line2Connected;
+ @SerializedName("line3-connected")
+ public boolean line3Connected;
+
+ public String getSerialNumber() {
+ return serialNumber;
+ }
+
+ public String getDeviceStatus() {
+ return deviceStatus == null || deviceStatus.length == 0 ? "" : deviceStatus[0];
+ }
+ }
+
+ public String type;
+ public DeviceDTO[] devices;
+}
diff --git a/bundles/org.openhab.binding.enphase/src/main/java/org/openhab/binding/enphase/internal/dto/InverterDTO.java b/bundles/org.openhab.binding.enphase/src/main/java/org/openhab/binding/enphase/internal/dto/InverterDTO.java
new file mode 100644
index 0000000000000..28a400054964b
--- /dev/null
+++ b/bundles/org.openhab.binding.enphase/src/main/java/org/openhab/binding/enphase/internal/dto/InverterDTO.java
@@ -0,0 +1,33 @@
+/**
+ * Copyright (c) 2010-2021 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 Inverter data.
+ *
+ * @author Hilbrand Bouwkamp - Initial contribution
+ */
+public class InverterDTO {
+ public String serialNumber;
+ public long lastReportDate;
+ public int devType;
+ public int lastReportWatts;
+ public int maxReportWatts;
+
+ /**
+ * @return the serialNumber
+ */
+ public String getSerialNumber() {
+ return serialNumber;
+ }
+}
diff --git a/bundles/org.openhab.binding.enphase/src/main/java/org/openhab/binding/enphase/internal/dto/ProductionJsonDTO.java b/bundles/org.openhab.binding.enphase/src/main/java/org/openhab/binding/enphase/internal/dto/ProductionJsonDTO.java
new file mode 100644
index 0000000000000..3513e5cd33511
--- /dev/null
+++ b/bundles/org.openhab.binding.enphase/src/main/java/org/openhab/binding/enphase/internal/dto/ProductionJsonDTO.java
@@ -0,0 +1,45 @@
+/**
+ * Copyright (c) 2010-2021 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 Envoy production and consumption data from production.json api call.
+ *
+ * @author Hilbrand Bouwkamp - Initial contribution
+ */
+public class ProductionJsonDTO {
+
+ public static class DataDTO {
+ public String type;
+ public int activeCount;
+ public float whLifetime;
+ public float whLastSevenDays;
+ public float whToday;
+ public float wNow;
+ public float rmsCurrent;
+ public float rmsVoltage;
+ public float reactPwr;
+ public float apprntPwr;
+ public float pwrFactor;
+ public long readingTime;
+ public float varhLeadToday;
+ public float varhLagToday;
+ public float vahToday;
+ public float varhLeadLifetime;
+ public float varhLagLifetime;
+ public float vahLifetime;
+ }
+
+ public DataDTO[] production;
+ public DataDTO[] consumption;
+}
diff --git a/bundles/org.openhab.binding.enphase/src/main/java/org/openhab/binding/enphase/internal/handler/EnphaseDeviceHandler.java b/bundles/org.openhab.binding.enphase/src/main/java/org/openhab/binding/enphase/internal/handler/EnphaseDeviceHandler.java
new file mode 100644
index 0000000000000..7ffcd0f7a795a
--- /dev/null
+++ b/bundles/org.openhab.binding.enphase/src/main/java/org/openhab/binding/enphase/internal/handler/EnphaseDeviceHandler.java
@@ -0,0 +1,146 @@
+/**
+ * Copyright (c) 2010-2021 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 static org.openhab.binding.enphase.internal.EnphaseBindingConstants.*;
+
+import java.util.Map;
+
+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.MessageTranslator;
+import org.openhab.binding.enphase.internal.dto.InventoryJsonDTO.DeviceDTO;
+import org.openhab.core.library.types.OnOffType;
+import org.openhab.core.library.types.StringType;
+import org.openhab.core.thing.Thing;
+import org.openhab.core.thing.ThingStatus;
+import org.openhab.core.thing.ThingStatusDetail;
+import org.openhab.core.thing.binding.BaseThingHandler;
+import org.openhab.core.types.UnDefType;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Generic base Thing handler for different Enphase devices.
+ *
+ * @author Hilbrand Bouwkamp - Initial contribution
+ */
+@NonNullByDefault
+abstract class EnphaseDeviceHandler extends BaseThingHandler {
+ protected final Logger logger = LoggerFactory.getLogger(getClass());
+
+ protected @Nullable DeviceDTO lastKnownDeviceState;
+
+ private final MessageTranslator messageTranslator;
+ private String serialNumber = "";
+
+ public EnphaseDeviceHandler(final Thing thing, MessageTranslator messageTranslator) {
+ super(thing);
+ this.messageTranslator = messageTranslator;
+ }
+
+ /**
+ * @return the serialNumber
+ */
+ public String getSerialNumber() {
+ return serialNumber;
+ }
+
+ protected void handleCommandRefresh(final String channelId) {
+ switch (channelId) {
+ case DEVICE_CHANNEL_STATUS:
+ refreshStatus(lastKnownDeviceState);
+ break;
+ case DEVICE_CHANNEL_PRODUCING:
+ refreshProducing(lastKnownDeviceState);
+ break;
+ case DEVICE_CHANNEL_COMMUNICATING:
+ refreshCommunicating(lastKnownDeviceState);
+ break;
+ case DEVICE_CHANNEL_PROVISIONED:
+ refreshProvisioned(lastKnownDeviceState);
+ break;
+ case DEVICE_CHANNEL_OPERATING:
+ refreshOperating(lastKnownDeviceState);
+ break;
+ }
+ }
+
+ private void refreshStatus(final @Nullable DeviceDTO deviceDTO) {
+ updateState(DEVICE_CHANNEL_STATUS, deviceDTO == null ? UnDefType.UNDEF
+ : new StringType(messageTranslator.translate((deviceDTO.getDeviceStatus()))));
+ }
+
+ private void refreshProducing(final @Nullable DeviceDTO deviceDTO) {
+ updateState(DEVICE_CHANNEL_PRODUCING,
+ deviceDTO == null ? UnDefType.UNDEF : OnOffType.from(deviceDTO.producing));
+ }
+
+ private void refreshCommunicating(final @Nullable DeviceDTO deviceDTO) {
+ updateState(DEVICE_CHANNEL_COMMUNICATING,
+ deviceDTO == null ? UnDefType.UNDEF : OnOffType.from(deviceDTO.communicating));
+ }
+
+ private void refreshProvisioned(final @Nullable DeviceDTO deviceDTO) {
+ updateState(DEVICE_CHANNEL_PROVISIONED,
+ deviceDTO == null ? UnDefType.UNDEF : OnOffType.from(deviceDTO.provisioned));
+ }
+
+ private void refreshOperating(final @Nullable DeviceDTO deviceDTO) {
+ updateState(DEVICE_CHANNEL_OPERATING,
+ deviceDTO == null ? UnDefType.UNDEF : OnOffType.from(deviceDTO.operating));
+ }
+
+ public void refreshDeviceState(final @Nullable DeviceDTO deviceDTO) {
+ refreshStatus(deviceDTO);
+ refreshProducing(deviceDTO);
+ refreshCommunicating(deviceDTO);
+ refreshProvisioned(deviceDTO);
+ refreshOperating(deviceDTO);
+ refreshProperties(deviceDTO);
+ refreshDeviceStatus(deviceDTO != null);
+ }
+
+ public void refreshDeviceStatus(final boolean hasData) {
+ if (isInitialized()) {
+ if (hasData) {
+ if (getThing().getStatus() != ThingStatus.ONLINE) {
+ updateStatus(ThingStatus.ONLINE);
+ }
+ } else {
+ updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
+ messageTranslator.translate(ERROR_NODATA));
+ }
+ }
+ }
+
+ private void refreshProperties(@Nullable final DeviceDTO deviceDTO) {
+ if (deviceDTO != null) {
+ final Map properties = editProperties();
+
+ properties.put(DEVICE_PROPERTY_PART_NUMBER, deviceDTO.partNumber);
+ updateProperties(properties);
+ }
+ }
+
+ @Override
+ public void initialize() {
+ serialNumber = (String) getConfig().get(EnphaseBindingConstants.CONFIG_SERIAL_NUMBER);
+ if (!EnphaseBindingConstants.isValidSerial(serialNumber)) {
+ updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "Serial Number is not valid");
+ } else {
+ updateStatus(ThingStatus.UNKNOWN);
+ }
+ }
+}
diff --git a/bundles/org.openhab.binding.enphase/src/main/java/org/openhab/binding/enphase/internal/handler/EnphaseInverterHandler.java b/bundles/org.openhab.binding.enphase/src/main/java/org/openhab/binding/enphase/internal/handler/EnphaseInverterHandler.java
new file mode 100644
index 0000000000000..7e3af0a6e1740
--- /dev/null
+++ b/bundles/org.openhab.binding.enphase/src/main/java/org/openhab/binding/enphase/internal/handler/EnphaseInverterHandler.java
@@ -0,0 +1,103 @@
+/**
+ * Copyright (c) 2010-2021 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 static org.openhab.binding.enphase.internal.EnphaseBindingConstants.*;
+
+import java.time.Instant;
+import java.time.ZoneId;
+import java.time.ZonedDateTime;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.binding.enphase.internal.MessageTranslator;
+import org.openhab.binding.enphase.internal.dto.InverterDTO;
+import org.openhab.core.library.types.DateTimeType;
+import org.openhab.core.library.types.QuantityType;
+import org.openhab.core.library.unit.Units;
+import org.openhab.core.thing.ChannelUID;
+import org.openhab.core.thing.Thing;
+import org.openhab.core.types.Command;
+import org.openhab.core.types.RefreshType;
+import org.openhab.core.types.State;
+import org.openhab.core.types.UnDefType;
+
+/**
+ * The {@link EnphaseInverterHandler} is responsible for handling commands, which are
+ * sent to one of the channels.
+ *
+ * @author Hilbrand Bouwkamp - Initial contribution
+ */
+@NonNullByDefault
+public class EnphaseInverterHandler extends EnphaseDeviceHandler {
+
+ private @Nullable InverterDTO lastKnownState;
+
+ public EnphaseInverterHandler(final Thing thing, MessageTranslator messageTranslator) {
+ super(thing, messageTranslator);
+ }
+
+ @Override
+ public void handleCommand(final ChannelUID channelUID, final Command command) {
+ if (command instanceof RefreshType) {
+ final String channelId = channelUID.getId();
+
+ switch (channelId) {
+ case INVERTER_CHANNEL_LAST_REPORT_WATTS:
+ refreshLastReportWatts(lastKnownState);
+ break;
+ case INVERTER_CHANNEL_MAX_REPORT_WATTS:
+ refreshMaxReportWatts(lastKnownState);
+ break;
+ case INVERTER_CHANNEL_LAST_REPORT_DATE:
+ refreshLastReportDate(lastKnownState);
+ break;
+ default:
+ super.handleCommandRefresh(channelId);
+ break;
+ }
+ }
+ }
+
+ public void refreshInverterChannels(final @Nullable InverterDTO inverterDTO) {
+ refreshLastReportWatts(inverterDTO);
+ refreshMaxReportWatts(inverterDTO);
+ refreshLastReportDate(inverterDTO);
+ lastKnownState = inverterDTO;
+ }
+
+ private void refreshLastReportWatts(final @Nullable InverterDTO inverterDTO) {
+ updateState(INVERTER_CHANNEL_LAST_REPORT_WATTS,
+ inverterDTO == null ? UnDefType.UNDEF : new QuantityType<>(inverterDTO.lastReportWatts, Units.WATT));
+ }
+
+ private void refreshMaxReportWatts(final @Nullable InverterDTO inverterDTO) {
+ updateState(INVERTER_CHANNEL_MAX_REPORT_WATTS,
+ inverterDTO == null ? UnDefType.UNDEF : new QuantityType<>(inverterDTO.maxReportWatts, Units.WATT));
+ }
+
+ private void refreshLastReportDate(final @Nullable InverterDTO inverterDTO) {
+ final State state;
+
+ if (inverterDTO == null) {
+ state = UnDefType.UNDEF;
+ } else {
+ final Instant instant = Instant.ofEpochSecond(inverterDTO.lastReportDate);
+ final ZonedDateTime zonedDateTime = instant.atZone(ZoneId.systemDefault());
+ logger.trace("[{}] Epoch time {}, zonedDateTime: {}", getThing().getUID(), inverterDTO.lastReportDate,
+ zonedDateTime);
+ state = new DateTimeType(zonedDateTime);
+ }
+ updateState(INVERTER_CHANNEL_LAST_REPORT_DATE, state);
+ }
+}
diff --git a/bundles/org.openhab.binding.enphase/src/main/java/org/openhab/binding/enphase/internal/handler/EnphaseRelayHandler.java b/bundles/org.openhab.binding.enphase/src/main/java/org/openhab/binding/enphase/internal/handler/EnphaseRelayHandler.java
new file mode 100644
index 0000000000000..aadee72348953
--- /dev/null
+++ b/bundles/org.openhab.binding.enphase/src/main/java/org/openhab/binding/enphase/internal/handler/EnphaseRelayHandler.java
@@ -0,0 +1,94 @@
+/**
+ * Copyright (c) 2010-2021 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 static org.openhab.binding.enphase.internal.EnphaseBindingConstants.*;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.binding.enphase.internal.MessageTranslator;
+import org.openhab.binding.enphase.internal.dto.InventoryJsonDTO.DeviceDTO;
+import org.openhab.core.library.types.OpenClosedType;
+import org.openhab.core.thing.ChannelUID;
+import org.openhab.core.thing.Thing;
+import org.openhab.core.types.Command;
+import org.openhab.core.types.RefreshType;
+import org.openhab.core.types.UnDefType;
+
+/**
+ * The {@link EnphaseInverterHandler} is responsible for handling commands, which are
+ * sent to one of the channels.
+ *
+ * @author Hilbrand Bouwkamp - Initial contribution
+ */
+@NonNullByDefault
+public class EnphaseRelayHandler extends EnphaseDeviceHandler {
+
+ public EnphaseRelayHandler(final Thing thing, MessageTranslator messageTranslator) {
+ super(thing, messageTranslator);
+ }
+
+ @Override
+ public void handleCommand(final ChannelUID channelUID, final Command command) {
+ if (command instanceof RefreshType) {
+ final String channelId = channelUID.getId();
+
+ switch (channelId) {
+ case RELAY_CHANNEL_RELAY:
+ refreshRelayChannel(lastKnownDeviceState);
+ break;
+ case RELAY_CHANNEL_LINE_1_CONNECTED:
+ refreshLine1Connect(lastKnownDeviceState);
+ break;
+ case RELAY_CHANNEL_LINE_2_CONNECTED:
+ refreshLine2Connect(lastKnownDeviceState);
+ break;
+ case RELAY_CHANNEL_LINE_3_CONNECTED:
+ refreshLine3Connect(lastKnownDeviceState);
+ break;
+ default:
+ super.handleCommandRefresh(channelId);
+ break;
+ }
+ }
+ }
+
+ private void refreshRelayChannel(@Nullable final DeviceDTO deviceDTO) {
+ updateState(RELAY_CHANNEL_RELAY, deviceDTO == null ? UnDefType.UNDEF
+ : (RELAY_STATUS_CLOSED.equals(deviceDTO.relay) ? OpenClosedType.CLOSED : OpenClosedType.OPEN));
+ }
+
+ private void refreshLine1Connect(@Nullable final DeviceDTO deviceDTO) {
+ updateState(RELAY_CHANNEL_LINE_1_CONNECTED, deviceDTO == null ? UnDefType.UNDEF
+ : (deviceDTO.line1Connected ? OpenClosedType.CLOSED : OpenClosedType.OPEN));
+ }
+
+ private void refreshLine2Connect(@Nullable final DeviceDTO deviceDTO) {
+ updateState(RELAY_CHANNEL_LINE_2_CONNECTED, deviceDTO == null ? UnDefType.UNDEF
+ : (deviceDTO.line2Connected ? OpenClosedType.CLOSED : OpenClosedType.OPEN));
+ }
+
+ private void refreshLine3Connect(@Nullable final DeviceDTO deviceDTO) {
+ updateState(RELAY_CHANNEL_LINE_3_CONNECTED, deviceDTO == null ? UnDefType.UNDEF
+ : (deviceDTO.line3Connected ? OpenClosedType.CLOSED : OpenClosedType.OPEN));
+ }
+
+ @Override
+ public void refreshDeviceState(@Nullable final DeviceDTO deviceDTO) {
+ refreshRelayChannel(deviceDTO);
+ refreshLine1Connect(deviceDTO);
+ refreshLine2Connect(deviceDTO);
+ refreshLine3Connect(deviceDTO);
+ super.refreshDeviceState(deviceDTO);
+ }
+}
diff --git a/bundles/org.openhab.binding.enphase/src/main/java/org/openhab/binding/enphase/internal/handler/EnvoyBridgeHandler.java b/bundles/org.openhab.binding.enphase/src/main/java/org/openhab/binding/enphase/internal/handler/EnvoyBridgeHandler.java
new file mode 100644
index 0000000000000..7074857c27b6d
--- /dev/null
+++ b/bundles/org.openhab.binding.enphase/src/main/java/org/openhab/binding/enphase/internal/handler/EnvoyBridgeHandler.java
@@ -0,0 +1,411 @@
+/**
+ * Copyright (c) 2010-2021 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 static org.openhab.binding.enphase.internal.EnphaseBindingConstants.CONFIG_HOSTNAME;
+import static org.openhab.binding.enphase.internal.EnphaseBindingConstants.ENVOY_CHANNELGROUP_CONSUMPTION;
+import static org.openhab.binding.enphase.internal.EnphaseBindingConstants.ENVOY_WATTS_NOW;
+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 java.time.Duration;
+import java.time.temporal.ChronoUnit;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Map;
+import java.util.concurrent.ScheduledFuture;
+import java.util.concurrent.TimeUnit;
+import java.util.function.Function;
+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.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.core.cache.ExpiringCache;
+import org.openhab.core.config.core.Configuration;
+import org.openhab.core.library.types.QuantityType;
+import org.openhab.core.library.unit.Units;
+import org.openhab.core.thing.Bridge;
+import org.openhab.core.thing.Channel;
+import org.openhab.core.thing.ChannelUID;
+import org.openhab.core.thing.Thing;
+import org.openhab.core.thing.ThingStatus;
+import org.openhab.core.thing.ThingStatusDetail;
+import org.openhab.core.thing.binding.BaseBridgeHandler;
+import org.openhab.core.thing.binding.ThingHandler;
+import org.openhab.core.thing.binding.ThingHandlerService;
+import org.openhab.core.types.Command;
+import org.openhab.core.types.RefreshType;
+import org.openhab.core.types.UnDefType;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * BridgeHandler for the Envoy gateway.
+ *
+ * @author Thomas Hentschel - Initial contribution
+ * @author Hilbrand Bouwkamp - Initial contribution
+ */
+@NonNullByDefault
+public class EnvoyBridgeHandler extends BaseBridgeHandler {
+
+ private enum FeatureStatus {
+ UNKNOWN,
+ SUPPORTED,
+ UNSUPPORTED
+ }
+
+ 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 EnvoyConfiguration configuration = new EnvoyConfiguration();
+ private @Nullable ScheduledFuture> updataDataFuture;
+ private @Nullable ScheduledFuture> updateHostnameFuture;
+ private @Nullable ExpiringCache