]> git.basschouten.com Git - openhab-addons.git/commitdiff
[enphase] Initial contribution (#9883)
authorHilbrand Bouwkamp <hilbrand@h72.nl>
Sun, 11 Apr 2021 17:54:08 +0000 (19:54 +0200)
committerGitHub <noreply@github.com>
Sun, 11 Apr 2021 17:54:08 +0000 (19:54 +0200)
Signed-off-by: Hilbrand Bouwkamp <hilbrand@h72.nl>
29 files changed:
CODEOWNERS
bom/openhab-addons/pom.xml
bundles/org.openhab.binding.enphase/NOTICE [new file with mode: 0644]
bundles/org.openhab.binding.enphase/README.md [new file with mode: 0644]
bundles/org.openhab.binding.enphase/pom.xml [new file with mode: 0644]
bundles/org.openhab.binding.enphase/src/main/feature/feature.xml [new file with mode: 0644]
bundles/org.openhab.binding.enphase/src/main/java/org/openhab/binding/enphase/internal/EnphaseBindingConstants.java [new file with mode: 0644]
bundles/org.openhab.binding.enphase/src/main/java/org/openhab/binding/enphase/internal/EnphaseHandlerFactory.java [new file with mode: 0644]
bundles/org.openhab.binding.enphase/src/main/java/org/openhab/binding/enphase/internal/EnvoyConfiguration.java [new file with mode: 0644]
bundles/org.openhab.binding.enphase/src/main/java/org/openhab/binding/enphase/internal/EnvoyConnectionException.java [new file with mode: 0644]
bundles/org.openhab.binding.enphase/src/main/java/org/openhab/binding/enphase/internal/EnvoyHostAddressCache.java [new file with mode: 0644]
bundles/org.openhab.binding.enphase/src/main/java/org/openhab/binding/enphase/internal/EnvoyNoHostnameException.java [new file with mode: 0644]
bundles/org.openhab.binding.enphase/src/main/java/org/openhab/binding/enphase/internal/MessageTranslator.java [new file with mode: 0644]
bundles/org.openhab.binding.enphase/src/main/java/org/openhab/binding/enphase/internal/discovery/EnphaseDevicesDiscoveryService.java [new file with mode: 0644]
bundles/org.openhab.binding.enphase/src/main/java/org/openhab/binding/enphase/internal/discovery/EnvoyDiscoveryParticipant.java [new file with mode: 0644]
bundles/org.openhab.binding.enphase/src/main/java/org/openhab/binding/enphase/internal/dto/EnvoyEnergyDTO.java [new file with mode: 0644]
bundles/org.openhab.binding.enphase/src/main/java/org/openhab/binding/enphase/internal/dto/EnvoyErrorDTO.java [new file with mode: 0644]
bundles/org.openhab.binding.enphase/src/main/java/org/openhab/binding/enphase/internal/dto/InventoryJsonDTO.java [new file with mode: 0644]
bundles/org.openhab.binding.enphase/src/main/java/org/openhab/binding/enphase/internal/dto/InverterDTO.java [new file with mode: 0644]
bundles/org.openhab.binding.enphase/src/main/java/org/openhab/binding/enphase/internal/dto/ProductionJsonDTO.java [new file with mode: 0644]
bundles/org.openhab.binding.enphase/src/main/java/org/openhab/binding/enphase/internal/handler/EnphaseDeviceHandler.java [new file with mode: 0644]
bundles/org.openhab.binding.enphase/src/main/java/org/openhab/binding/enphase/internal/handler/EnphaseInverterHandler.java [new file with mode: 0644]
bundles/org.openhab.binding.enphase/src/main/java/org/openhab/binding/enphase/internal/handler/EnphaseRelayHandler.java [new file with mode: 0644]
bundles/org.openhab.binding.enphase/src/main/java/org/openhab/binding/enphase/internal/handler/EnvoyBridgeHandler.java [new file with mode: 0644]
bundles/org.openhab.binding.enphase/src/main/java/org/openhab/binding/enphase/internal/handler/EnvoyConnector.java [new file with mode: 0644]
bundles/org.openhab.binding.enphase/src/main/resources/OH-INF/binding/binding.xml [new file with mode: 0644]
bundles/org.openhab.binding.enphase/src/main/resources/OH-INF/i18n/enphase_en.properties [new file with mode: 0644]
bundles/org.openhab.binding.enphase/src/main/resources/OH-INF/thing/thing-types.xml [new file with mode: 0644]
bundles/pom.xml

index 04c7e195cc2d3f92130a6f20c8dd07285ef18f39..7dafbd35251e73f5e6c6642e186e85258d446278 100644 (file)
@@ -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
index d26a9a58099871ec5bea7d539de740d71a47aaec..b216d38b601a0d48bf9aeea92e7d890210c6dc51 100644 (file)
       <artifactId>org.openhab.binding.enocean</artifactId>
       <version>${project.version}</version>
     </dependency>
+    <dependency>
+      <groupId>org.openhab.addons.bundles</groupId>
+      <artifactId>org.openhab.binding.enphase</artifactId>
+      <version>${project.version}</version>
+    </dependency>
     <dependency>
       <groupId>org.openhab.addons.bundles</groupId>
       <artifactId>org.openhab.binding.enturno</artifactId>
diff --git a/bundles/org.openhab.binding.enphase/NOTICE b/bundles/org.openhab.binding.enphase/NOTICE
new file mode 100644 (file)
index 0000000..38d625e
--- /dev/null
@@ -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 (file)
index 0000000..4f768bb
--- /dev/null
@@ -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 (file)
index 0000000..12e5c60
--- /dev/null
@@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://maven.apache.org/POM/4.0.0"
+  xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+
+  <modelVersion>4.0.0</modelVersion>
+
+  <parent>
+    <groupId>org.openhab.addons.bundles</groupId>
+    <artifactId>org.openhab.addons.reactor.bundles</artifactId>
+    <version>3.1.0-SNAPSHOT</version>
+  </parent>
+
+  <artifactId>org.openhab.binding.enphase</artifactId>
+
+  <name>openHAB Add-ons :: Bundles :: Enphase Binding</name>
+
+</project>
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 (file)
index 0000000..537cf0d
--- /dev/null
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<features name="org.openhab.binding.enphase-${project.version}" xmlns="http://karaf.apache.org/xmlns/features/v1.4.0">
+       <repository>mvn:org.openhab.core.features.karaf/org.openhab.core.features.karaf.openhab-core/${ohc.version}/xml/features
+       </repository>
+
+       <feature name="openhab-binding-enphase" description="Enphase Binding" version="${project.version}">
+               <feature>openhab-runtime-base</feature>
+               <bundle start-level="80">mvn:org.openhab.addons.bundles/org.openhab.binding.enphase/${project.version}</bundle>
+       </feature>
+</features>
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 (file)
index 0000000..4d08743
--- /dev/null
@@ -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 (file)
index 0000000..4f267a5
--- /dev/null
@@ -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<ThingTypeUID> 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 (file)
index 0000000..e9a0aef
--- /dev/null
@@ -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 (file)
index 0000000..d468d3d
--- /dev/null
@@ -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 (file)
index 0000000..be6198d
--- /dev/null
@@ -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 (file)
index 0000000..95c25c6
--- /dev/null
@@ -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 (file)
index 0000000..71e6040
--- /dev/null
@@ -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 (file)
index 0000000..7b1bfe3
--- /dev/null
@@ -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<String, @Nullable InverterDTO> inverters = envoyHandler.getInvertersData(true);
+
+        if (inverters == null) {
+            logger.debug("No inverter data for Enphase inverters in discovery for Envoy {}.", bridgeID);
+        } else {
+            for (final Entry<String, @Nullable InverterDTO> 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<String, @Nullable DeviceDTO> devices = envoyHandler.getDevices(true);
+
+        if (devices == null) {
+            logger.debug("No device data for Enphase devices in discovery for Envoy {}.", bridgeID);
+        } else {
+            for (final Entry<String, @Nullable DeviceDTO> 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<String, Object> 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 (file)
index 0000000..beae1ca
--- /dev/null
@@ -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<String, @Nullable String> lastKnownHostAddresses = new ConcurrentHashMap<>();
+
+    @Override
+    public Set<ThingTypeUID> 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<String, Object> 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 (file)
index 0000000..8857858
--- /dev/null
@@ -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 (file)
index 0000000..a8a29d8
--- /dev/null
@@ -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 (file)
index 0000000..d6efa5a
--- /dev/null
@@ -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 (file)
index 0000000..28a4000
--- /dev/null
@@ -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 (file)
index 0000000..3513e5c
--- /dev/null
@@ -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 (file)
index 0000000..7ffcd0f
--- /dev/null
@@ -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<String, String> 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 (file)
index 0000000..7e3af0a
--- /dev/null
@@ -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 (file)
index 0000000..aadee72
--- /dev/null
@@ -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 (file)
index 0000000..7074857
--- /dev/null
@@ -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<Map<String, @Nullable InverterDTO>> invertersCache;
+    private @Nullable ExpiringCache<Map<String, @Nullable DeviceDTO>> devicesCache;
+    private @Nullable EnvoyEnergyDTO productionDTO;
+    private @Nullable EnvoyEnergyDTO consumptionDTO;
+    private FeatureStatus consumptionSupported = FeatureStatus.UNKNOWN;
+    private FeatureStatus jsonSupported = FeatureStatus.UNKNOWN;
+
+    public EnvoyBridgeHandler(final Bridge thing, final HttpClient httpClient,
+            final EnvoyHostAddressCache envoyHostAddressCache) {
+        super(thing);
+        connector = new EnvoyConnector(httpClient);
+        this.envoyHostnameCache = envoyHostAddressCache;
+    }
+
+    @Override
+    public void handleCommand(final ChannelUID channelUID, final Command command) {
+        if (command instanceof RefreshType) {
+            refresh(channelUID);
+        }
+    }
+
+    private void refresh(final ChannelUID channelUID) {
+        final EnvoyEnergyDTO data = ENVOY_CHANNELGROUP_CONSUMPTION.equals(channelUID.getGroupId()) ? consumptionDTO
+                : productionDTO;
+
+        if (data == null) {
+            updateState(channelUID, UnDefType.UNDEF);
+        } else {
+            switch (channelUID.getIdWithoutGroup()) {
+                case ENVOY_WATT_HOURS_TODAY:
+                    updateState(channelUID, new QuantityType<>(data.wattHoursToday, Units.WATT_HOUR));
+                    break;
+                case ENVOY_WATT_HOURS_SEVEN_DAYS:
+                    updateState(channelUID, new QuantityType<>(data.wattHoursSevenDays, Units.WATT_HOUR));
+                    break;
+                case ENVOY_WATT_HOURS_LIFETIME:
+                    updateState(channelUID, new QuantityType<>(data.wattHoursLifetime, Units.WATT_HOUR));
+                    break;
+                case ENVOY_WATTS_NOW:
+                    updateState(channelUID, new QuantityType<>(data.wattsNow, Units.WATT));
+                    break;
+            }
+        }
+    }
+
+    @Override
+    public Collection<Class<? extends ThingHandlerService>> getServices() {
+        return Collections.singleton(EnphaseDevicesDiscoveryService.class);
+    }
+
+    @Override
+    public void initialize() {
+        configuration = getConfigAs(EnvoyConfiguration.class);
+        if (!EnphaseBindingConstants.isValidSerial(configuration.serialNumber)) {
+            updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "Serial number is not valid");
+            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,
+                TimeUnit.MINUTES);
+    }
+
+    /**
+     * Method called by the ExpiringCache when no inverter data is present to get the data from the Envoy gateway.
+     * When there are connection problems it will start a scheduled job to try to reconnect to the
+     *
+     * @return the inverter data from the Envoy gateway or null if no data is available.
+     */
+    private @Nullable Map<String, @Nullable InverterDTO> refreshInverters() {
+        try {
+            return connector.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);
+        }
+        return null;
+    }
+
+    private @Nullable 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 -> {
+                            d.type = inv.type;
+                            return d;
+                        })).collect(Collectors.toMap(DeviceDTO::getSerialNumber, Function.identity()));
+
+                jsonSupported = FeatureStatus.SUPPORTED;
+                return devicesData;
+            }
+        } catch (final EnvoyNoHostnameException e) {
+            // ignore hostname exception here. It's already handled by others.
+        } catch (final EnvoyConnectionException e) {
+            if (jsonSupported == FeatureStatus.UNKNOWN) {
+                logger.info(
+                        "This Ephase Envoy device ({}) doesn't seem to support json data. So not all channels are set.",
+                        getThing().getUID());
+                jsonSupported = FeatureStatus.UNSUPPORTED;
+            } else if (consumptionSupported == FeatureStatus.SUPPORTED) {
+                updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
+            }
+        }
+        return null;
+    }
+
+    /**
+     * Returns the data for the inverters. It get the data from cache or updates the cache if possible in case no data
+     * is available.
+     *
+     * @param force force a cache refresh
+     * @return data if present or null
+     */
+    public @Nullable Map<String, @Nullable InverterDTO> getInvertersData(final boolean force) {
+        final ExpiringCache<Map<String, @Nullable InverterDTO>> invertersCache = this.invertersCache;
+
+        if (invertersCache == null || !isOnline()) {
+            return null;
+        } else {
+            if (force) {
+                invertersCache.invalidateValue();
+            }
+            return invertersCache.getValue();
+        }
+    }
+
+    /**
+     * Returns the data for the devices. It get the data from cache or updates the cache if possible in case no data
+     * is available.
+     *
+     * @param force force a cache refresh
+     * @return data if present or null
+     */
+    public @Nullable Map<String, @Nullable DeviceDTO> getDevices(final boolean force) {
+        final ExpiringCache<Map<String, @Nullable DeviceDTO>> devicesCache = this.devicesCache;
+
+        if (devicesCache == null || !isOnline()) {
+            return null;
+        } else {
+            if (force) {
+                devicesCache.invalidateValue();
+            }
+            return devicesCache.getValue();
+        }
+    }
+
+    /**
+     * Method called by the refresh thread.
+     */
+    public synchronized void updateData() {
+        try {
+            updateInverters();
+            updateEnvoy();
+            updateDevices();
+        } catch (final EnvoyNoHostnameException e) {
+            scheduleHostnameUpdate(false);
+        } catch (final EnvoyConnectionException e) {
+            updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
+            scheduleHostnameUpdate(false);
+        } catch (final RuntimeException e) {
+            logger.debug("Unexpected error in Enphase {}: ", getThing().getUID(), e);
+            updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
+        }
+    }
+
+    private void updateEnvoy() throws EnvoyNoHostnameException, EnvoyConnectionException {
+        productionDTO = connector.getProduction();
+        setConsumptionDTOData();
+        getThing().getChannels().stream().map(Channel::getUID).filter(this::isLinked).forEach(this::refresh);
+        if (isInitialized() && !isOnline()) {
+            updateStatus(ThingStatus.ONLINE);
+        }
+    }
+
+    /**
+     * Retrieve consumption data if supported, and keep track if this feature is supported by the device.
+     *
+     * @throws EnvoyConnectionException
+     */
+    private void setConsumptionDTOData() throws EnvoyConnectionException {
+        if (consumptionSupported != FeatureStatus.UNSUPPORTED && isOnline()) {
+            try {
+                consumptionDTO = connector.getConsumption();
+                consumptionSupported = FeatureStatus.SUPPORTED;
+            } catch (final EnvoyNoHostnameException e) {
+                // ignore hostname exception here. It's already handled by others.
+            } catch (final EnvoyConnectionException e) {
+                if (consumptionSupported == FeatureStatus.UNKNOWN) {
+                    logger.info(
+                            "This Enphase Envoy device ({}) doesn't seem to support consumption data. So no consumption channels are set.",
+                            getThing().getUID());
+                    consumptionSupported = FeatureStatus.UNSUPPORTED;
+                } else if (consumptionSupported == FeatureStatus.SUPPORTED) {
+                    throw e;
+                }
+            }
+        }
+    }
+
+    /**
+     * Updates channels of the inverter things with inverter specific data.
+     */
+    private void updateInverters() {
+        final Map<String, @Nullable InverterDTO> inverters = getInvertersData(false);
+
+        if (inverters != null) {
+            getThing().getThings().stream().map(Thing::getHandler).filter(h -> h instanceof EnphaseInverterHandler)
+                    .map(EnphaseInverterHandler.class::cast)
+                    .forEach(invHandler -> updateInverter(inverters, invHandler));
+        }
+    }
+
+    private void updateInverter(final @Nullable Map<String, @Nullable InverterDTO> inverters,
+            final EnphaseInverterHandler invHandler) {
+        if (inverters == null) {
+            return;
+        }
+        final InverterDTO inverterDTO = inverters.get(invHandler.getSerialNumber());
+
+        invHandler.refreshInverterChannels(inverterDTO);
+        if (jsonSupported == FeatureStatus.UNSUPPORTED) {
+            // if inventory json is supported device status is set in #updateDevices
+            invHandler.refreshDeviceStatus(inverterDTO != null);
+        }
+    }
+
+    /**
+     * Updates channels of the device things with device specific data.
+     * This data is not available on all envoy devices.
+     */
+    private void updateDevices() {
+        final Map<String, @Nullable DeviceDTO> devices = getDevices(false);
+
+        getThing().getThings().stream().map(Thing::getHandler).filter(h -> h instanceof EnphaseDeviceHandler)
+                .map(EnphaseDeviceHandler.class::cast).forEach(invHandler -> invHandler
+                        .refreshDeviceState(devices == null ? null : devices.get(invHandler.getSerialNumber())));
+    }
+
+    /**
+     * Schedules a hostname update, but only schedules the task when not yet running or forced.
+     * Force is used to reschedule the task and should only be used from within {@link #updateHostname()}.
+     *
+     * @param force if true will always schedule the task
+     */
+    private synchronized void scheduleHostnameUpdate(final boolean force) {
+        if (force || updateHostnameFuture == null) {
+            logger.debug("Schedule hostname/ip address update for thing {} in {} seconds.", getThing().getUID(),
+                    RETRY_RECONNECT_SECONDS);
+            updateHostnameFuture = scheduler.schedule(this::updateHostname, RETRY_RECONNECT_SECONDS, TimeUnit.SECONDS);
+        }
+    }
+
+    @Override
+    public void childHandlerInitialized(final ThingHandler childHandler, final Thing childThing) {
+        if (childHandler instanceof EnphaseInverterHandler) {
+            updateInverter(getInvertersData(false), (EnphaseInverterHandler) childHandler);
+        }
+        if (childHandler instanceof EnphaseDeviceHandler) {
+            final Map<String, @Nullable DeviceDTO> devices = getDevices(false);
+
+            if (devices != null) {
+                ((EnphaseDeviceHandler) childHandler)
+                        .refreshDeviceState(devices.get(((EnphaseDeviceHandler) childHandler).getSerialNumber()));
+            }
+        }
+    }
+
+    /**
+     * Handles a host name / ip address update.
+     */
+    private void updateHostname() {
+        final String lastKnownHostname = envoyHostnameCache.getLastKnownHostAddress(configuration.serialNumber);
+
+        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.");
+            scheduleHostnameUpdate(true);
+        } else {
+            final Configuration config = editConfiguration();
+
+            config.put(CONFIG_HOSTNAME, lastKnownHostname);
+            logger.info("Enphase Envoy ({}) hostname/ip address set to {}", getThing().getUID(), lastKnownHostname);
+            configuration.hostname = lastKnownHostname;
+            connector.setConfiguration(configuration);
+            updateConfiguration(config);
+            updateData();
+            // The task is done so the future can be released by setting it to null.
+            updateHostnameFuture = null;
+        }
+    }
+
+    @Override
+    public void dispose() {
+        final ScheduledFuture<?> retryFuture = this.updateHostnameFuture;
+        if (retryFuture != null) {
+            retryFuture.cancel(true);
+        }
+        final ScheduledFuture<?> inverterFuture = this.updataDataFuture;
+
+        if (inverterFuture != null) {
+            inverterFuture.cancel(true);
+        }
+    }
+
+    /**
+     * @return Returns true if the bridge is online and not has an configuration pending.
+     */
+    public boolean isOnline() {
+        return getThing().getStatus() == ThingStatus.ONLINE;
+    }
+
+    @Override
+    public String toString() {
+        return "EnvoyBridgeHandler(" + thing.getUID() + ") Status: " + thing.getStatus();
+    }
+}
diff --git a/bundles/org.openhab.binding.enphase/src/main/java/org/openhab/binding/enphase/internal/handler/EnvoyConnector.java b/bundles/org.openhab.binding.enphase/src/main/java/org/openhab/binding/enphase/internal/handler/EnvoyConnector.java
new file mode 100644 (file)
index 0000000..d604b3f
--- /dev/null
@@ -0,0 +1,197 @@
+/**
+ * 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 java.net.URI;
+import java.util.Arrays;
+import java.util.List;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+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.api.Authentication;
+import org.eclipse.jetty.client.api.Authentication.Result;
+import org.eclipse.jetty.client.api.AuthenticationStore;
+import org.eclipse.jetty.client.api.ContentResponse;
+import org.eclipse.jetty.client.api.Request;
+import org.eclipse.jetty.client.util.DigestAuthentication;
+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.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.google.gson.Gson;
+import com.google.gson.GsonBuilder;
+import com.google.gson.JsonSyntaxException;
+
+/**
+ * Methods to make API calls to the Envoy gateway.
+ *
+ * @author Hilbrand Bouwkamp - Initial contribution
+ */
+@NonNullByDefault
+class EnvoyConnector {
+
+    private static final String HTTP = "http://";
+    private static final String PRODUCTION_JSON_URL = "/production.json";
+    private static final String INVENTORY_JSON_URL = "/inventory.json";
+    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 final Logger logger = LoggerFactory.getLogger(EnvoyConnector.class);
+    private final Gson gson = new GsonBuilder().create();
+    private final HttpClient httpClient;
+    private String hostname = "";
+    private @Nullable DigestAuthentication envoyAuthn;
+    private @Nullable URI invertersURI;
+
+    public EnvoyConnector(final HttpClient httpClient) {
+        this.httpClient = httpClient;
+    }
+
+    /**
+     * Sets the Envoy connection configuration.
+     *
+     * @param configuration the configuration to set
+     */
+    public void setConfiguration(final EnvoyConfiguration configuration) {
+        hostname = configuration.hostname;
+        if (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);
+        envoyAuthn = new DigestAuthentication(invertersURI, Authentication.ANY_REALM, username, password);
+        store.addAuthentication(envoyAuthn);
+    }
+
+    /**
+     * @return Returns the production data from the Envoy gateway.
+     */
+    public EnvoyEnergyDTO getProduction() throws EnvoyConnectionException, EnvoyNoHostnameException {
+        return retrieveData(PRODUCTION_URL, this::jsonToEnvoyEnergyDTO);
+    }
+
+    /**
+     * @return Returns the consumption data from the Envoy gateway.
+     */
+    public EnvoyEnergyDTO getConsumption() throws EnvoyConnectionException, EnvoyNoHostnameException {
+        return retrieveData(CONSUMPTION_URL, this::jsonToEnvoyEnergyDTO);
+    }
+
+    private @Nullable EnvoyEnergyDTO jsonToEnvoyEnergyDTO(final String json) {
+        return gson.fromJson(json, EnvoyEnergyDTO.class);
+    }
+
+    /**
+     * @return Returns the production/consumption data from the Envoy gateway.
+     */
+    public ProductionJsonDTO getProductionJson() throws EnvoyConnectionException, EnvoyNoHostnameException {
+        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 {
+        return retrieveData(INVENTORY_JSON_URL, this::jsonToEnvoyInventoryJson);
+    }
+
+    private @Nullable List<InventoryJsonDTO> jsonToEnvoyInventoryJson(final String json) {
+        final InventoryJsonDTO @Nullable [] list = gson.fromJson(json, InventoryJsonDTO[].class);
+
+        return list == null ? null : Arrays.asList(list);
+    }
+
+    /**
+     * @return Returns the production data for the inverters.
+     */
+    public List<InverterDTO> getInverters() throws EnvoyConnectionException, EnvoyNoHostnameException {
+        synchronized (this) {
+            final AuthenticationStore store = httpClient.getAuthenticationStore();
+            final Result invertersResult = store.findAuthenticationResult(invertersURI);
+
+            if (invertersResult != null) {
+                store.removeAuthenticationResult(invertersResult);
+            }
+        }
+        return retrieveData(INVERTERS_URL, json -> Arrays.asList(gson.fromJson(json, InverterDTO[].class)));
+    }
+
+    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();
+            final String content = response.getContentAsString();
+
+            logger.trace("Envoy returned data for '{}' with status {}: {}", urlPath, response.getStatus(), content);
+            try {
+                if (response.getStatus() == HttpStatus.OK_200) {
+                    final T result = jsonConverter.apply(content);
+                    if (result == null) {
+                        throw new EnvoyConnectionException("No data received");
+                    }
+                    return result;
+                } else {
+                    final @Nullable EnvoyErrorDTO error = gson.fromJson(content, EnvoyErrorDTO.class);
+
+                    logger.debug("Envoy returned an error: {}", error);
+                    throw new EnvoyConnectionException(error == null ? response.getReason() : error.info);
+                }
+            } catch (final JsonSyntaxException e) {
+                logger.debug("Error parsing json: {}", content, e);
+                throw new EnvoyConnectionException("Error parsing data: ", e);
+            }
+        } catch (final InterruptedException e) {
+            Thread.currentThread().interrupt();
+            throw new EnvoyConnectionException("Interrupted");
+        } catch (final TimeoutException e) {
+            logger.debug("TimeoutException: {}", e.getMessage());
+            throw new EnvoyConnectionException("Connection timeout: ", e);
+        } catch (final ExecutionException e) {
+            logger.debug("ExecutionException: {}", e.getMessage(), e);
+            throw new EnvoyConnectionException("Could not retrieve data: ", e.getCause());
+        }
+    }
+}
diff --git a/bundles/org.openhab.binding.enphase/src/main/resources/OH-INF/binding/binding.xml b/bundles/org.openhab.binding.enphase/src/main/resources/OH-INF/binding/binding.xml
new file mode 100644 (file)
index 0000000..ca4b319
--- /dev/null
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<binding:binding id="enphase" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+       xmlns:binding="https://openhab.org/schemas/binding/v1.0.0"
+       xsi:schemaLocation="https://openhab.org/schemas/binding/v1.0.0 https://openhab.org/schemas/binding-1.0.0.xsd">
+
+       <name>Enphase Envoy Binding</name>
+       <description>This is the binding for Enphase Envoy solar panels.</description>
+
+</binding:binding>
diff --git a/bundles/org.openhab.binding.enphase/src/main/resources/OH-INF/i18n/enphase_en.properties b/bundles/org.openhab.binding.enphase/src/main/resources/OH-INF/i18n/enphase_en.properties
new file mode 100644 (file)
index 0000000..db42ff0
--- /dev/null
@@ -0,0 +1,80 @@
+error.nodata=No Data
+envoy.global.ok=Normal
+
+envoy.cond_flags.acb_ctrl.bmuhardwareerror=BMU Hardware Error
+envoy.cond_flags.acb_ctrl.bmuimageerror=BMU Image Error
+envoy.cond_flags.acb_ctrl.bmumaxcurrentwarning=BMU Max Current Warning
+envoy.cond_flags.acb_ctrl.bmusenseerror=BMU Sense Error
+
+envoy.cond_flags.acb_ctrl.cellmaxtemperror=Cell Max Temperature Error
+envoy.cond_flags.acb_ctrl.cellmaxtempwarning=Cell Max Temperature Warning
+envoy.cond_flags.acb_ctrl.cellmaxvoltageerror=Cell Max Voltage Error
+envoy.cond_flags.acb_ctrl.cellmaxvoltagewarning=Cell Max Voltage Warning
+envoy.cond_flags.acb_ctrl.cellmintemperror=Cell Min Temperature Error
+envoy.cond_flags.acb_ctrl.cellmintempwarning=Cell Min Temperature Warning
+envoy.cond_flags.acb_ctrl.cellminvoltageerror=Cell Min Voltage Error
+envoy.cond_flags.acb_ctrl.cellminvoltagewarning=Cell Min Voltage Warning
+envoy.cond_flags.acb_ctrl.cibcanerror=CIB CAN Error
+envoy.cond_flags.acb_ctrl.cibimageerror=CIB Image Error
+envoy.cond_flags.acb_ctrl.cibspierror=CIB SPI Error"
+envoy.cond_flags.obs_strs.discovering=Discovering
+envoy.cond_flags.obs_strs.failure=Failure to report
+envoy.cond_flags.obs_strs.flasherror=Flash Error
+envoy.cond_flags.obs_strs.notmonitored=Not Monitored
+envoy.cond_flags.obs_strs.ok=Normal
+envoy.cond_flags.obs_strs.plmerror=PLM Error
+envoy.cond_flags.obs_strs.secmodeenterfailure=Secure mode enter failure
+envoy.cond_flags.obs_strs.secmodeexitfailure=Secure mode exit failure
+envoy.cond_flags.obs_strs.sleeping=Sleeping"
+
+envoy.cond_flags.pcu_chan.acMonitorError=AC Monitor Error
+envoy.cond_flags.pcu_chan.acfrequencyhigh=AC Frequency High
+envoy.cond_flags.pcu_chan.acfrequencylow=AC Frequency Low
+envoy.cond_flags.pcu_chan.acfrequencyoor=AC Frequency Out Of Range
+envoy.cond_flags.pcu_chan.acvoltage_avg_hi=AC Voltage Average High
+envoy.cond_flags.pcu_chan.acvoltagehigh=AC Voltage High
+envoy.cond_flags.pcu_chan.acvoltagelow=AC Voltage Low
+envoy.cond_flags.pcu_chan.acvoltageoor=AC Voltage Out Of Range
+envoy.cond_flags.pcu_chan.acvoltageoosp1=AC Voltage Out Of Range - Phase 1
+envoy.cond_flags.pcu_chan.acvoltageoosp2=AC Voltage Out Of Range - Phase 2
+envoy.cond_flags.pcu_chan.acvoltageoosp3=AC Voltage Out Of Range - Phase 3
+envoy.cond_flags.pcu_chan.agfpowerlimiting=AGF Power Limiting
+envoy.cond_flags.pcu_chan.dcresistancelow=DC Resistance Low
+envoy.cond_flags.pcu_chan.dcresistancelowpoweroff=DC Resistance Low - Power Off
+envoy.cond_flags.pcu_chan.dcvoltagetoohigh=DC Voltage Too High
+envoy.cond_flags.pcu_chan.dcvoltagetoolow=DC Voltage Too Low
+envoy.cond_flags.pcu_chan.dfdt=AC Frequency Changing too Fast
+envoy.cond_flags.pcu_chan.gfitripped=GFI Tripped
+envoy.cond_flags.pcu_chan.gridgone=Grid Gone
+envoy.cond_flags.pcu_chan.gridinstability=Grid Instability
+envoy.cond_flags.pcu_chan.gridoffsethi=Grid Offset Hi
+envoy.cond_flags.pcu_chan.gridoffsetlow=Grid Offset Low
+envoy.cond_flags.pcu_chan.hardwareError=Hardware Error
+envoy.cond_flags.pcu_chan.hardwareWarning=Hardware Warning
+envoy.cond_flags.pcu_chan.highskiprate=High Skip Rate
+envoy.cond_flags.pcu_chan.invalidinterval=Invalid Interval
+envoy.cond_flags.pcu_chan.pwrgenoffbycmd=Power generation off by command
+envoy.cond_flags.pcu_chan.skippedcycles=Skipped Cycles
+envoy.cond_flags.pcu_chan.vreferror=Voltage Ref Error"
+
+envoy.cond_flags.pcu_ctrl.alertactive=Alert Active
+envoy.cond_flags.pcu_ctrl.altpwrgenmode=Alternate Power Generation Mode
+envoy.cond_flags.pcu_ctrl.altvfsettings=Alternate Voltage and Frequency Settings
+envoy.cond_flags.pcu_ctrl.badflashimage=Bad Flash Image
+envoy.cond_flags.pcu_ctrl.bricked=No Grid Profile
+envoy.cond_flags.pcu_ctrl.commandedreset=Commanded Reset
+envoy.cond_flags.pcu_ctrl.criticaltemperature=Critical Temperature
+envoy.cond_flags.pcu_ctrl.dc-pwr-low=DC Power Too Low
+envoy.cond_flags.pcu_ctrl.iuplinkproblem=IUP Link Problem
+envoy.cond_flags.pcu_ctrl.manutestmode=In Manu Test Mode
+envoy.cond_flags.pcu_ctrl.nsync=Grid Perturbation Unsynchronized
+envoy.cond_flags.pcu_ctrl.overtemperature=Over Temperature
+envoy.cond_flags.pcu_ctrl.poweronreset=Power On Reset
+envoy.cond_flags.pcu_ctrl.pwrgenoffbycmd=Power generation off by command
+envoy.cond_flags.pcu_ctrl.runningonac=Running on AC
+envoy.cond_flags.pcu_ctrl.tpmtest=Transient Grid Profile
+envoy.cond_flags.pcu_ctrl.unexpectedreset=Unexpected Reset
+envoy.cond_flags.pcu_ctrl.watchdogreset=Watchdog Reset
+
+envoy.cond_flags.rgm_chan.check_meter=Meter Error
+envoy.cond_flags.rgm_chan.power_quality=Poor Power Quality
diff --git a/bundles/org.openhab.binding.enphase/src/main/resources/OH-INF/thing/thing-types.xml b/bundles/org.openhab.binding.enphase/src/main/resources/OH-INF/thing/thing-types.xml
new file mode 100644 (file)
index 0000000..21d1a36
--- /dev/null
@@ -0,0 +1,228 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<thing:thing-descriptions bindingId="enphase"
+       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+       xmlns:thing="https://openhab.org/schemas/thing-description/v1.0.0"
+       xsi:schemaLocation="https://openhab.org/schemas/thing-description/v1.0.0 https://openhab.org/schemas/thing-description-1.0.0.xsd">
+
+
+       <bridge-type id="envoy">
+               <label>Envoy</label>
+               <description>Envoy gateway</description>
+
+               <channel-groups>
+                       <channel-group id="production" typeId="envoy-data">
+                               <label>Production</label>
+                               <description>Production data from the solar panels</description>
+                       </channel-group>
+                       <channel-group id="consumption" typeId="envoy-data">
+                               <label>Consumption</label>
+                               <description>Consumption data from the solar panels</description>
+                       </channel-group>
+               </channel-groups>
+
+               <representation-property>serialNumber</representation-property>
+
+               <config-description>
+                       <parameter name="serialNumber" type="text" pattern="[0-9]{12}" required="true">
+                               <label>Serial Number</label>
+                               <description>The serial number of the Envoy gateway which can be found on the gateway</description>
+                       </parameter>
+                       <parameter name="hostname" type="text">
+                               <label>Host Name / IP Address</label>
+                               <description>The host name/ip address of the Envoy gateway. Leave empty to auto detect</description>
+                               <advanced>true</advanced>
+                       </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>
+                               <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>
+                               <advanced>true</advanced>
+                       </parameter>
+                       <parameter name="refresh" type="integer" unit="min">
+                               <label>Refresh Time</label>
+                               <description>Period between updates. The default is 5 minutes, the refresh frequency of the Envoy itself</description>
+                               <default>5</default>
+                               <advanced>true</advanced>
+                       </parameter>
+               </config-description>
+       </bridge-type>
+
+
+       <thing-type id="inverter">
+               <supported-bridge-type-refs>
+                       <bridge-type-ref id="envoy"/>
+               </supported-bridge-type-refs>
+
+               <label>Inverter</label>
+               <description>Inverter</description>
+
+               <channels>
+                       <channel id="lastReportWatts" typeId="last-report-watts"/>
+                       <channel id="maxReportWatts" typeId="max-report-watts"/>
+                       <channel id="lastReportDate" typeId="last-report-date"/>
+                       <channel id="status" typeId="status"/>
+                       <channel id="producing" typeId="producing"/>
+                       <channel id="communicating" typeId="communicating"/>
+                       <channel id="provisioned" typeId="provisioned"/>
+                       <channel id="operating" typeId="operating"/>
+               </channels>
+
+               <properties>
+                       <property name="partNumber"/>
+               </properties>
+
+               <representation-property>serialNumber</representation-property>
+
+               <config-description>
+                       <parameter name="serialNumber" type="text" pattern="[0-9]{12}" required="true">
+                               <label>Serial Number</label>
+                               <description>The serial number of the inverter</description>
+                       </parameter>
+               </config-description>
+       </thing-type>
+
+       <thing-type id="relay">
+               <supported-bridge-type-refs>
+                       <bridge-type-ref id="envoy"/>
+               </supported-bridge-type-refs>
+
+               <label>Relay Controller</label>
+               <description>Network system relay controller</description>
+
+               <channels>
+                       <channel id="relay" typeId="relay"/>
+                       <channel id="line1Connected" typeId="line-connected">
+                               <label>Line 1 Connection Status</label>
+                       </channel>
+                       <channel id="line2Connected" typeId="line-connected">
+                               <label>Line 2 Connection Status</label>
+                       </channel>
+                       <channel id="line3Connected" typeId="line-connected">
+                               <label>Line 3 Connection Status</label>
+                       </channel>
+                       <channel id="status" typeId="status"/>
+                       <channel id="producing" typeId="producing"/>
+                       <channel id="communicating" typeId="communicating"/>
+                       <channel id="provisioned" typeId="provisioned"/>
+                       <channel id="operating" typeId="operating"/>
+               </channels>
+
+               <properties>
+                       <property name="partNumber"/>
+               </properties>
+
+               <representation-property>serialNumber</representation-property>
+
+               <config-description>
+                       <parameter name="serialNumber" type="text" pattern="[0-9]{12}" required="true">
+                               <label>Serial Number</label>
+                               <description>The serial number of the inverter</description>
+                       </parameter>
+               </config-description>
+       </thing-type>
+
+       <!-- Envoy gateway channels -->
+       <channel-group-type id="envoy-data">
+               <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"/>
+               </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>
+               <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>
+               <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>
+               <state pattern="%d %unit%" readOnly="true"/>
+       </channel-type>
+       <channel-type id="watts-now">
+               <item-type>Number:Power</item-type>
+               <label>Latest Power</label>
+               <description>Latest watts produced</description>
+               <state pattern="%d %unit%" readOnly="true"/>
+       </channel-type>
+
+       <!-- Inverter channels -->
+       <channel-type id="last-report-watts">
+               <item-type>Number:Power</item-type>
+               <label>Last Report</label>
+               <description>Last reported power delivery</description>
+               <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>
+               <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>
+               <state readOnly="true"/>
+       </channel-type>
+
+       <!-- Relay channels -->
+       <channel-type id="relay">
+               <item-type>Contact</item-type>
+               <label>Relay Status</label>
+               <state readOnly="true"/>
+       </channel-type>
+       <channel-type id="line-connected">
+               <item-type>Contact</item-type>
+               <label>Line Connection Status</label>
+               <description>When closed power line is connected</description>
+               <state readOnly="true"/>
+       </channel-type>
+
+       <!-- Generic device channels -->
+       <channel-type id="status">
+               <item-type>String</item-type>
+               <label>Status</label>
+               <description>The status of the Enphase device</description>
+               <state readOnly="true"/>
+       </channel-type>
+
+       <channel-type id="producing" advanced="true">
+               <item-type>Switch</item-type>
+               <label>Producing</label>
+               <state readOnly="true"/>
+       </channel-type>
+       <channel-type id="communicating" advanced="true">
+               <item-type>Switch</item-type>
+               <label>Communicating</label>
+               <state readOnly="true"/>
+       </channel-type>
+       <channel-type id="provisioned" advanced="true">
+               <item-type>Switch</item-type>
+               <label>Provisioned</label>
+               <state readOnly="true"/>
+       </channel-type>
+       <channel-type id="operating" advanced="true">
+               <item-type>Switch</item-type>
+               <label>Operating</label>
+               <state readOnly="true"/>
+       </channel-type>
+
+</thing:thing-descriptions>
index 7a0c0bd64036463f9e2758538cff5c44bc4d91e9..cd0d7f3c7279eb28ace46b5553017ad88ded7658 100644 (file)
     <module>org.openhab.binding.energenie</module>
     <module>org.openhab.binding.enigma2</module>
     <module>org.openhab.binding.enocean</module>
+    <module>org.openhab.binding.enphase</module>
     <module>org.openhab.binding.enturno</module>
     <module>org.openhab.binding.epsonprojector</module>
     <module>org.openhab.binding.etherrain</module>