/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
<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>
--- /dev/null
+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
--- /dev/null
+# 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" }
+```
--- /dev/null
+<?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>
--- /dev/null
+<?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>
--- /dev/null
+/**
+ * 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;
+ }
+}
--- /dev/null
+/**
+ * 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;
+ }
+}
--- /dev/null
+/**
+ * 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 + "]";
+ }
+}
--- /dev/null
+/**
+ * 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);
+ }
+}
--- /dev/null
+/**
+ * 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);
+}
--- /dev/null
+/**
+ * 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);
+ }
+}
--- /dev/null
+/**
+ * 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());
+ }
+}
--- /dev/null
+/**
+ * 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);
+ }
+}
--- /dev/null
+/**
+ * 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;
+ }
+}
--- /dev/null
+/**
+ * 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;
+}
--- /dev/null
+/**
+ * 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
+ + "]";
+ }
+}
--- /dev/null
+/**
+ * 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;
+}
--- /dev/null
+/**
+ * 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;
+ }
+}
--- /dev/null
+/**
+ * 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;
+}
--- /dev/null
+/**
+ * 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);
+ }
+ }
+}
--- /dev/null
+/**
+ * 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);
+ }
+}
--- /dev/null
+/**
+ * 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);
+ }
+}
--- /dev/null
+/**
+ * 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();
+ }
+}
--- /dev/null
+/**
+ * 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());
+ }
+ }
+}
--- /dev/null
+<?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>
--- /dev/null
+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
--- /dev/null
+<?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>
<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>