[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
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
29 changed files with 2278 additions and 0 deletions

View File

@ -69,6 +69,7 @@
/bundles/org.openhab.binding.energenie/ @hmerk
/bundles/org.openhab.binding.enigma2/ @gdolfen
/bundles/org.openhab.binding.enocean/ @fruggy83
/bundles/org.openhab.binding.enphase/ @Hilbrand
/bundles/org.openhab.binding.enturno/ @klocsson
/bundles/org.openhab.binding.epsonprojector/ @mlobstein
/bundles/org.openhab.binding.etherrain/ @dfad1469

View File

@ -331,6 +331,11 @@
<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>

View File

@ -0,0 +1,13 @@
This content is produced and maintained by the openHAB project.
* Project home: https://www.openhab.org
== Declared Project Licenses
This program and the accompanying materials are made available under the terms
of the Eclipse Public License 2.0 which is available at
https://www.eclipse.org/legal/epl-2.0/.
== Source Code
https://github.com/openhab/openhab-addons

View File

@ -0,0 +1,113 @@
# Enphase Binding
This is the binding for the [Enphase](https://enphase.com/) Envoy Solar Panel gateway.
The binding uses the local API of the Envoy gateway.
Some calls can be made without authentication and some use a user name and password.
The default user name is `envoy` and the default password is the last 6 numbers of the serial number.
The Envoy gateway updates the data every 5 minutes.
Therefore using a refresh rate shorter doesn't provide more information.
## Supported Things
The follow things are supported:
* `envoy` The Envoy gateway thing, which is a bridge thing.
* `inverter` A Enphase micro inverter connected to a solar panel.
* `relay` A Enphase relay.
Not all Envoy gateways support all channels and things.
Therefore some data on inverters and the relay may not be available.
The binding auto detects which data is available and will report this in the log on initialization of the gateway bridge.
## Discovery
The binding can discover Envoy gateways, micro inverters and relays.
## Thing Configuration
The Envoy gateway thing `envoy` has the following configuration options:
| parameter | required | description |
|--------------|----------|-------------------------------------------------------------------------------------------------------------|
| serialNumber | yes | The serial number of the Envoy gateway which can be found on the gateway |
| hostname | no | The host name/ip address of the Envoy gateway. Leave empty to auto detect |
| username | no | The user name to the Envoy gateway. Leave empty when using the default user name |
| password | no | The password to the Envoy gateway. Leave empty when using the default password |
| refresh | no | Period between data updates. The default is the same 5 minutes the data is actual refreshed on the Envoy |
The micro inverter `inverter` and `relay` things have only 1 parameter:
| parameter | required | description |
|--------------|----------|-----------------------------------|
| serialNumber | yes | The serial number of the inverter |
## Channels
The `envoy` thing has can show both production as well as consumption data.
There are channel groups for `production` and `consumption` data.
The `consumption` data is only available if the gateway reports this.
A example of a production channel name is: `production#wattsNow`.
| channel | type | description |
|--------------------|---------------|---------------------------------------|
| wattHoursToday | Number:Energy | Watt hours produced today |
| wattHoursSevenDays | Number:Energy | Watt hours produced the last 7 days |
| wattHoursLifetime | Number:Energy | Watt hours produced over the lifetime |
| wattsNow | Number:Power | Latest watts produced |
The `inverter` thing has the following channels:
| channel | type | description |
|-----------------|--------------|--------------------------------------|
| lastReportWatts | Number:Power | Last reported power delivery |
| maxReportWatts | Number:Power | Maximum reported power |
| lastReportDate | DateTime | Date of last reported power delivery |
The following channels are only available if supported by the Envoy gateway:
The `relay` thing has the following channels:
| channel | type | description |
|-----------------|--------------|--------------------------------------------------------|
| relay | Contact | Status of the relay. |
| line1Connected | Contact | If power line 1 is connected. If closed it's connected |
| line2Connected | Contact | If power line 2 is connected. If closed it's connected |
| line2Connected | Contact | If power line 3 is connected. If closed it's connected |
The `inverter` and `relay` have the following additional advanced channels:
| channel | type | description |
|-----------------|--------------------|--------------------------------------|
| producing | Switch (Read Only) | If the device is producing |
| communicating | Switch (Read Only) | If the device is communicating |
| provisioned | Switch (Read Only) | If the device is provisioned |
| operating | Switch (Read Only) | If the device is operating |
## Full Example
Things example:
```
Bridge enphase:envoy:789012 "Envoy" [ serialNumber="12345789012" ] {
Things:
inverter 123456 "Enphase Inverter 123456" [ serialNumber="789012123456" ]
inverter 223456 "Enphase Inverter 223456" [ serialNumber="789012223456" ]
}
```
Items example:
```
Number:Power envoyWattsNow "Watts Now [%d %unit%]" { channel="enphase:envoy:789012:production#wattsNow" }
Number:Energy envoyWattHoursToday "Watt Hours Today [%d %unit%]" { channel="enphase:envoy:789012:production#wattHoursToday" }
Number:Energy envoyWattHours7Days "Watt Hours 7 Days [%.1f kWh]" { channel="enphase:envoy:789012:production#wattHoursSevenDays" }
Number:Energy envoyWattHoursLifetime "Watt Hours Lifetime [%.1f kWh]" { channel="enphase:envoy:789012:production#wattHoursLifetime" }
Number:Power i1LastReportWatts "Last Report [%d %unit%]" { channel="enphase:inverter:789012:123456:lastReportWatts" }
Number:Power i1MaxReportWatts "Max Report [%d %unit%]" { channel="enphase:inverter:789012:123456:maxReportWatts" }
DateTime i1LastReportDate "Last Report Date [%1$tY-%1$tm-%1$td %1$tH:%1$tM]" { channel="enphase:inverter:789012:123456:lastReportDate" }
Number:Power i2LastReportWatts "Last Report [%d %unit%]" { channel="enphase:inverter:789012:223456:lastReportWatts" }
Number:Power i21MaxReportWatts "Max Report [%d %unit%]" { channel="enphase:inverter:789012:223456:maxReportWatts" }
DateTime i2LastReportDate "Last Report Date [%1$tY-%1$tm-%1$td %1$tH:%1$tM]" { channel="enphase:inverter:789012:223456:lastReportDate" }
```

View File

@ -0,0 +1,17 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://maven.apache.org/POM/4.0.0"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.openhab.addons.bundles</groupId>
<artifactId>org.openhab.addons.reactor.bundles</artifactId>
<version>3.1.0-SNAPSHOT</version>
</parent>
<artifactId>org.openhab.binding.enphase</artifactId>
<name>openHAB Add-ons :: Bundles :: Enphase Binding</name>
</project>

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>

View File

@ -100,6 +100,7 @@
<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>