[enphase] Initial contribution (#9883)

Signed-off-by: Hilbrand Bouwkamp <hilbrand@h72.nl>
This commit is contained in:
Hilbrand Bouwkamp
2021-04-11 19:54:08 +02:00
committed by GitHub
parent 4f018b8aec
commit 53d168991c
29 changed files with 2278 additions and 0 deletions

View File

@@ -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>

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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 + "]";
}
}

View File

@@ -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);
}
}

View File

@@ -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);
}

View File

@@ -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);
}
}

View File

@@ -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());
}
}

View File

@@ -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);
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}

View File

@@ -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
+ "]";
}
}

View File

@@ -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;
}

View File

@@ -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;
}
}

View File

@@ -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;
}

View File

@@ -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);
}
}
}

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View File

@@ -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();
}
}

View File

@@ -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());
}
}
}

View File

@@ -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>

View File

@@ -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

View File

@@ -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>