diff --git a/CODEOWNERS b/CODEOWNERS index 48f1fc1855175..e8ce863eefa44 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -48,6 +48,7 @@ /bundles/org.openhab.binding.dwdpollenflug/ @DerOetzi /bundles/org.openhab.binding.dwdunwetter/ @limdul79 /bundles/org.openhab.binding.elerotransmitterstick/ @vbier +/bundles/org.openhab.binding.energenie/ @hmerk /bundles/org.openhab.binding.enocean/ @fruggy83 /bundles/org.openhab.binding.enturno/ @klocsson /bundles/org.openhab.binding.etherrain/ @dfad1469 diff --git a/bom/openhab-addons/pom.xml b/bom/openhab-addons/pom.xml index 00064b888e0fa..3772d424888d7 100644 --- a/bom/openhab-addons/pom.xml +++ b/bom/openhab-addons/pom.xml @@ -231,6 +231,11 @@ org.openhab.binding.elerotransmitterstick ${project.version} + + org.openhab.addons.bundles + org.openhab.binding.energenie + ${project.version} + org.openhab.addons.bundles org.openhab.binding.enocean diff --git a/bundles/org.openhab.binding.energenie/.classpath b/bundles/org.openhab.binding.energenie/.classpath new file mode 100644 index 0000000000000..a5d95095ccaaf --- /dev/null +++ b/bundles/org.openhab.binding.energenie/.classpath @@ -0,0 +1,32 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/bundles/org.openhab.binding.energenie/.project b/bundles/org.openhab.binding.energenie/.project new file mode 100644 index 0000000000000..32000eb2789d5 --- /dev/null +++ b/bundles/org.openhab.binding.energenie/.project @@ -0,0 +1,23 @@ + + + org.openhab.binding.energenie + + + + + + org.eclipse.jdt.core.javabuilder + + + + + org.eclipse.m2e.core.maven2Builder + + + + + + org.eclipse.jdt.core.javanature + org.eclipse.m2e.core.maven2Nature + + diff --git a/bundles/org.openhab.binding.energenie/NOTICE b/bundles/org.openhab.binding.energenie/NOTICE new file mode 100644 index 0000000000000..38d625e349232 --- /dev/null +++ b/bundles/org.openhab.binding.energenie/NOTICE @@ -0,0 +1,13 @@ +This content is produced and maintained by the openHAB project. + +* Project home: https://www.openhab.org + +== Declared Project Licenses + +This program and the accompanying materials are made available under the terms +of the Eclipse Public License 2.0 which is available at +https://www.eclipse.org/legal/epl-2.0/. + +== Source Code + +https://github.com/openhab/openhab-addons diff --git a/bundles/org.openhab.binding.energenie/README.md b/bundles/org.openhab.binding.energenie/README.md new file mode 100644 index 0000000000000..602843cbb12fd --- /dev/null +++ b/bundles/org.openhab.binding.energenie/README.md @@ -0,0 +1,95 @@ +# Gembird energenie Binding + +This binding integrates the Gembird energenie range of power extenders by using the Energenie Data Exchange Protocol and power reading devices through HTTP interface. + + +## Supported Things + +The Binding supports PM2-LAN, PMS-LAN, PMS2-LAN or PMS-WLAN power extenders as well as PWM-LAN power measurement devices. + +## Discovery + +Gembird energenie devices don't support autodiscovery. +All Things need to be created manually either in PaperUI or within Things files. + +## Binding Configuration + +The Binding does not need any specific configuration + +## Thing Configuration + +The device requires the IP-address and a password as a configuration value in order for the binding to know where to access it and to login to the device. + +| Parameter | Description | +|-----------|------------------------------------------------------| +| host | IP-Address of energenie device | +| password | Password to access energenie device, defaults to "1" | + +## Channels + +The following channels are supported by PM2-LAN, PMS-LAN, PMS2-LAN or PMS-WLAN devices + +| channel | type | description | +|----------|--------|----------------------------------------------------| +| socket1 | Switch | This is the control channel for the first socket | +| socket2 | Switch | This is the control channel for the second socket | +| socket3 | Switch | This is the control channel for the third socket | +| socket4 | Switch | This is the control channel for the fourth socket | + +PWM-LAN devices support the following channels + +| channel | type | description | +|----------|--------------------------|------------------------------------------| +| voltage | Number:ElectricPotential | Channel for output voltage measurement | +| current | Number:ElectricCurrent | Channel for output current measurement | +| power | Number:Power | Channel for output power measurement | +| energy | Number:Energy | channel for output energy measurement | + +## Full Example + +Things + +``` +Thing energenie:pm2lan:pm2lan [ host="xxx.xxx.xxx.xxx", password="your password" ] +Thing energenie:pmslan:pmslan [ host="xxx.xxx.xxx.xxx", password="your password" ] +Thing energenie:pms2lan:pms2lan [ host="xxx.xxx.xxx.xxx", password="your password" ] +Thing energenie:pmswlan:pmswlan [ host="xxx.xxx.xxx.xxx", password="your password" ] +Thing energenie:pwmlan:pwmlan [ host="xxx.xxx.xxx.xxx", password="your password" ] +``` + +Items + +``` +//Power extenders +Switch Socket1 { channel="energenie:pm2lan:pm2lan:socket1" } +Switch Socket2 { channel="energenie:pm2lan:pm2lan:socket2" } +Switch Socket3 { channel="energenie:pm2lan:pm2lan:socket3" } +Switch Socket4 { channel="energenie:pm2lan:pm2lan:socket4" } + +//Power measurement +Number Voltage { channel="energenie:pwmlan:pwmlan:voltage" } +Number Current { channel="energenie:pwmlan:pwmlan:current" } +Number Power { channel="energenie:pwmlan:pwmlan:power" } +Number Energy { channel="energenie:pwmlan:pwmlan:energy" } +``` + +Sitemap + +``` +sitemap energenie label="Energenie Devices" +{ + Frame { + // Power extenders + Switch item=Socket1 + Switch item=Socket2 + Switch item=Socket3 + Switch item=Socket4 + + // Power measurement + Number item=Voltage + Number item=Current + Number item=Power + Number item=Energy + } +} +``` diff --git a/bundles/org.openhab.binding.energenie/pom.xml b/bundles/org.openhab.binding.energenie/pom.xml new file mode 100644 index 0000000000000..6f9af69ac4fc9 --- /dev/null +++ b/bundles/org.openhab.binding.energenie/pom.xml @@ -0,0 +1,16 @@ + + + + 4.0.0 + + + org.openhab.addons.bundles + org.openhab.addons.reactor.bundles + 2.5.5-SNAPSHOT + + + org.openhab.binding.energenie + + openHAB Add-ons :: Bundles :: Energenie Binding + + diff --git a/bundles/org.openhab.binding.energenie/src/main/feature/feature.xml b/bundles/org.openhab.binding.energenie/src/main/feature/feature.xml new file mode 100644 index 0000000000000..7bc80e9c06c0f --- /dev/null +++ b/bundles/org.openhab.binding.energenie/src/main/feature/feature.xml @@ -0,0 +1,9 @@ + + + mvn:org.openhab.core.features.karaf/org.openhab.core.features.karaf.openhab-core/${ohc.version}/xml/features + + + openhab-runtime-base + mvn:org.openhab.addons.bundles/org.openhab.binding.energenie/${project.version} + + diff --git a/bundles/org.openhab.binding.energenie/src/main/java/org/openhab/binding/energenie/internal/EnergenieBindingConstants.java b/bundles/org.openhab.binding.energenie/src/main/java/org/openhab/binding/energenie/internal/EnergenieBindingConstants.java new file mode 100644 index 0000000000000..fa40899f741b5 --- /dev/null +++ b/bundles/org.openhab.binding.energenie/src/main/java/org/openhab/binding/energenie/internal/EnergenieBindingConstants.java @@ -0,0 +1,79 @@ +/** + * Copyright (c) 2010-2020 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.energenie.internal; + +import java.util.Collections; +import java.util.Set; +import java.util.regex.Pattern; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.smarthome.core.thing.ThingTypeUID; + +/** + * The {@link EnergenieBindingConstants} class defines common constants, which are + * used across the whole binding. + * + * @author Hans-Jörg Merk - Initial contribution + */ +@NonNullByDefault +public class EnergenieBindingConstants { + + private static final String BINDING_ID = "energenie"; + + public static final int TCP_PORT = 5000; + + public static final int STATCRYP_LEN = 4; + public static final int CTRLCRYP_LEN = 4; + public static final int KEY_LEN = 8; + public static final int TASK_LEN = 4; + public static final int SOLUTION_LEN = 4; + + public static final String STATE_ON = "0x11"; + public static final String STATE_ON_NO_VOLTAGE = "0x12"; + public static final String STATE_OFF = "0x22"; + public static final String STATE_OFF_NO_VOLTAGE = "0x21"; + + public static final String V21_STATE_ON = "0x41"; + public static final String V21_STATE_OFF = "0x82"; + + public static final String WLAN_STATE_ON = "0x51"; + public static final String WLAN_STATE_OFF = "0x92"; + + public static final byte SWITCH_ON = 0x01; + public static final byte SWITCH_OFF = 0x02; + public static final byte DONT_SWITCH = 0x04; + + public static final int SOCKET_COUNT = 4; // AC power sockets, not network ones + + // List of all Thing Type UIDs + public static final ThingTypeUID THING_TYPE_PM2LAN = new ThingTypeUID(BINDING_ID, "pm2lan"); + public static final ThingTypeUID THING_TYPE_PMSLAN = new ThingTypeUID(BINDING_ID, "pmslan"); + public static final ThingTypeUID THING_TYPE_PMS2LAN = new ThingTypeUID(BINDING_ID, "pms2lan"); + public static final ThingTypeUID THING_TYPE_PMSWLAN = new ThingTypeUID(BINDING_ID, "pmswlan"); + public static final ThingTypeUID THING_TYPE_PWMLAN = new ThingTypeUID(BINDING_ID, "pwmlan"); + + // List of all Channel ids + public static final Pattern CHANNEL_SOCKET = Pattern.compile("socket(\\d)"); + + public static final String VOLTAGE = "voltage"; + public static final String CURRENT = "current"; + public static final String POWER = "power"; + public static final String ENERGY = "energy"; + + public static final Set SUPPORTED_THING_TYPES_UIDS = Collections.unmodifiableSet( + Stream.of(THING_TYPE_PM2LAN, THING_TYPE_PMSLAN, THING_TYPE_PMS2LAN, THING_TYPE_PMSLAN, THING_TYPE_PWMLAN) + .collect(Collectors.toSet())); + +} diff --git a/bundles/org.openhab.binding.energenie/src/main/java/org/openhab/binding/energenie/internal/EnergenieHandlerFactory.java b/bundles/org.openhab.binding.energenie/src/main/java/org/openhab/binding/energenie/internal/EnergenieHandlerFactory.java new file mode 100644 index 0000000000000..9bb7af2ea4979 --- /dev/null +++ b/bundles/org.openhab.binding.energenie/src/main/java/org/openhab/binding/energenie/internal/EnergenieHandlerFactory.java @@ -0,0 +1,64 @@ +/** + * Copyright (c) 2010-2020 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.energenie.internal; + +import static org.openhab.binding.energenie.internal.EnergenieBindingConstants.*; + +import java.util.Set; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.eclipse.smarthome.core.thing.Thing; +import org.eclipse.smarthome.core.thing.ThingTypeUID; +import org.eclipse.smarthome.core.thing.binding.BaseThingHandlerFactory; +import org.eclipse.smarthome.core.thing.binding.ThingHandler; +import org.eclipse.smarthome.core.thing.binding.ThingHandlerFactory; +import org.openhab.binding.energenie.internal.handler.EnergenieHandler; +import org.openhab.binding.energenie.internal.handler.EnergeniePWMHandler; +import org.osgi.service.component.annotations.Component; + +/** + * The {@link EnergenieHandlerFactory} is responsible for creating things and thing + * handlers. + * + * @author Hans-Jörg Merk - Initial contribution + */ +@NonNullByDefault +@Component(configurationPid = "binding.energenie", service = ThingHandlerFactory.class) +public class EnergenieHandlerFactory extends BaseThingHandlerFactory { + + private static final Set SUPPORTED_THING_TYPES_UIDS = EnergenieBindingConstants.SUPPORTED_THING_TYPES_UIDS; + + @Override + public boolean supportsThingType(ThingTypeUID thingTypeUID) { + return SUPPORTED_THING_TYPES_UIDS.contains(thingTypeUID); + } + + @Override + protected @Nullable ThingHandler createHandler(Thing thing) { + ThingTypeUID thingTypeUID = thing.getThingTypeUID(); + + if (THING_TYPE_PMSLAN.equals(thingTypeUID)) { + return new EnergenieHandler(thing, EnergenieProtocolEnum.V20); + } else if (THING_TYPE_PM2LAN.equals(thingTypeUID)) { + return new EnergenieHandler(thing, EnergenieProtocolEnum.V20); + } else if (THING_TYPE_PMS2LAN.equals(thingTypeUID)) { + return new EnergenieHandler(thing, EnergenieProtocolEnum.V21); + } else if (THING_TYPE_PMSWLAN.equals(thingTypeUID)) { + return new EnergenieHandler(thing, EnergenieProtocolEnum.WLAN); + } else if (THING_TYPE_PWMLAN.equals(thingTypeUID)) { + return new EnergeniePWMHandler(thing); + } + return null; + } +} diff --git a/bundles/org.openhab.binding.energenie/src/main/java/org/openhab/binding/energenie/internal/EnergeniePWMStateEnum.java b/bundles/org.openhab.binding.energenie/src/main/java/org/openhab/binding/energenie/internal/EnergeniePWMStateEnum.java new file mode 100644 index 0000000000000..12f17c89001b7 --- /dev/null +++ b/bundles/org.openhab.binding.energenie/src/main/java/org/openhab/binding/energenie/internal/EnergeniePWMStateEnum.java @@ -0,0 +1,93 @@ +/** + * Copyright (c) 2010-2020 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.energenie.internal; + +import javax.measure.Unit; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.smarthome.core.library.types.QuantityType; +import org.eclipse.smarthome.core.library.unit.SmartHomeUnits; +import org.eclipse.smarthome.core.types.State; +import org.eclipse.smarthome.core.types.UnDefType; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * The {@link EnergeniePWMStateEnum} contains informations for parsing the readState() result. + * + * @author Hans-Jörg Merk - Initial contribution + */ + +@NonNullByDefault +public enum EnergeniePWMStateEnum { + VOLTAGE("var V = ", 9, 20, 10, SmartHomeUnits.VOLT), + CURRENT("var V = ", 9, 20, 100, SmartHomeUnits.AMPERE), + POWER("var P=", 6, 20, 466, SmartHomeUnits.WATT), + ENERGY("var E=", 6, 20, 25600, SmartHomeUnits.WATT_HOUR); + + private final Logger logger = LoggerFactory.getLogger(EnergeniePWMStateEnum.class); + + private final String stateResponseSearch; + private final int start; + private final int stop; + private final int divisor; + private final Unit unit; + + private EnergeniePWMStateEnum(final String stateResponseSearch, final int start, final int stop, final int divisor, + final Unit unit) { + this.stateResponseSearch = stateResponseSearch; + this.start = start; + this.stop = stop; + this.divisor = divisor; + this.unit = unit; + } + + public String getStateResponseSearch() { + return stateResponseSearch; + } + + public int getStart() { + return start; + } + + public int getStop() { + return stop; + } + + public int getDivisor() { + return divisor; + } + + public State readState(final String loginResponseString) { + final int findState = loginResponseString.lastIndexOf(stateResponseSearch); + + if (findState > 0) { + logger.trace("searchstring {} found at position {}", stateResponseSearch, findState); + final String slicedResponseTmp = loginResponseString.substring(findState + start, findState + stop); + logger.trace("transformed state response = {}", slicedResponseTmp); + final String[] slicedResponse = slicedResponseTmp.split(";"); + logger.trace("transformed state response = {} - {}", slicedResponse[0], slicedResponse[1]); + final double value; + try { + value = Double.parseDouble(slicedResponse[0]) / divisor; + return QuantityType.valueOf(value, unit); + } catch (NumberFormatException e) { + logger.debug("Could not Parse State", e); + return UnDefType.UNDEF; + } + } else { + logger.trace("searchstring '{} not found", stateResponseSearch); + return UnDefType.UNDEF; + } + } +} diff --git a/bundles/org.openhab.binding.energenie/src/main/java/org/openhab/binding/energenie/internal/EnergenieProtocolEnum.java b/bundles/org.openhab.binding.energenie/src/main/java/org/openhab/binding/energenie/internal/EnergenieProtocolEnum.java new file mode 100644 index 0000000000000..1e3709890df6d --- /dev/null +++ b/bundles/org.openhab.binding.energenie/src/main/java/org/openhab/binding/energenie/internal/EnergenieProtocolEnum.java @@ -0,0 +1,40 @@ +/** + * Copyright (c) 2010-2020 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.energenie.internal; + +import static org.openhab.binding.energenie.internal.EnergenieBindingConstants.*; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * The {@link EnergenieProtocolEnum} contains informations for parsing the readState() result. + * + * @author Hans-Jörg Merk - Initial contribution + */ + +@NonNullByDefault +public enum EnergenieProtocolEnum { + V20(STATE_ON), + V21(V21_STATE_ON), + WLAN(WLAN_STATE_ON); + + private final String statusOn; + + private EnergenieProtocolEnum(String statusOn) { + this.statusOn = statusOn; + } + + public String getStatusOn() { + return statusOn; + } +} diff --git a/bundles/org.openhab.binding.energenie/src/main/java/org/openhab/binding/energenie/internal/config/EnergenieConfiguration.java b/bundles/org.openhab.binding.energenie/src/main/java/org/openhab/binding/energenie/internal/config/EnergenieConfiguration.java new file mode 100644 index 0000000000000..b2c74434c7fcf --- /dev/null +++ b/bundles/org.openhab.binding.energenie/src/main/java/org/openhab/binding/energenie/internal/config/EnergenieConfiguration.java @@ -0,0 +1,31 @@ +/** + * Copyright (c) 2010-2020 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.energenie.internal.config; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * The {@link EnergenieConfiguration} class contains fields mapping thing configuration parameters. + * + * @author Hans-Jörg Merk - Initial contribution + */ +@NonNullByDefault +public class EnergenieConfiguration { + /** + * The default refresh interval in Seconds. + */ + public static final int DEFAULT_REFRESH_INTERVAL = 60; + + public String host = ""; + public String password = ""; +} diff --git a/bundles/org.openhab.binding.energenie/src/main/java/org/openhab/binding/energenie/internal/handler/EnergenieHandler.java b/bundles/org.openhab.binding.energenie/src/main/java/org/openhab/binding/energenie/internal/handler/EnergenieHandler.java new file mode 100644 index 0000000000000..cc6b945adedee --- /dev/null +++ b/bundles/org.openhab.binding.energenie/src/main/java/org/openhab/binding/energenie/internal/handler/EnergenieHandler.java @@ -0,0 +1,161 @@ +/** + * Copyright (c) 2010-2020 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.energenie.internal.handler; + +import static org.openhab.binding.energenie.internal.EnergenieBindingConstants.*; + +import java.io.IOException; +import java.net.UnknownHostException; +import java.time.Duration; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.TimeUnit; +import java.util.regex.Matcher; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.eclipse.smarthome.core.cache.ExpiringCache; +import org.eclipse.smarthome.core.library.types.OnOffType; +import org.eclipse.smarthome.core.thing.ChannelUID; +import org.eclipse.smarthome.core.thing.Thing; +import org.eclipse.smarthome.core.thing.ThingStatus; +import org.eclipse.smarthome.core.thing.ThingStatusDetail; +import org.eclipse.smarthome.core.thing.binding.BaseThingHandler; +import org.eclipse.smarthome.core.thing.util.ThingHandlerHelper; +import org.eclipse.smarthome.core.types.Command; +import org.eclipse.smarthome.core.types.RefreshType; +import org.openhab.binding.energenie.internal.EnergenieProtocolEnum; +import org.openhab.binding.energenie.internal.config.EnergenieConfiguration; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * The {@link EnergenieHandler} is responsible for handling commands, which are + * sent to one of the channels. + * + * @author Hans-Jörg Merk - Initial contribution + */ +@NonNullByDefault +public class EnergenieHandler extends BaseThingHandler { + + private static final String CHANNEL_SOCKET_PREFIX = "socket"; + private final Logger logger = LoggerFactory.getLogger(EnergenieHandler.class); + + /** + * Use cache for refresh command to not update again when call is made within 4 seconds of previous call. + */ + private final ExpiringCache refreshCache = new ExpiringCache<>(Duration.ofSeconds(5), this::refreshState); + private final String statusOn; + + private @Nullable EnergenieSocket energenieSocket; + private @Nullable ScheduledFuture refreshJob; + + private int refreshInterval; + private @Nullable String host; + + public EnergenieHandler(final Thing thing, final EnergenieProtocolEnum protocol) { + super(thing); + this.statusOn = protocol.getStatusOn(); + } + + @Override + public void handleCommand(final ChannelUID channelUID, final Command command) { + if (command instanceof RefreshType) { + refreshCache.getValue(); + } else if (command instanceof OnOffType) { + final byte[] ctrl = { DONT_SWITCH, DONT_SWITCH, DONT_SWITCH, DONT_SWITCH }; + final Matcher matcher = CHANNEL_SOCKET.matcher(channelUID.getId()); + + try { + boolean found = false; + + if (matcher.find()) { + final int index = Integer.parseInt(matcher.group(1)) - 1; + + if (index >= 0 && index < SOCKET_COUNT) { + ctrl[index] = OnOffType.ON.equals(command) ? SWITCH_ON : SWITCH_OFF; + stateUpdate(energenieSocket.sendCommand(ctrl)); + found = true; + } + } + if (!found) { + logger.debug("Invalid channel id {}, should be value between 1 and {}", channelUID, SOCKET_COUNT); + } + } catch (final IOException e) { + updateStatus(ThingStatus.OFFLINE); + logger.debug("Couldn't get I/O for the connection to: {}:{}.", host, TCP_PORT, e); + } + } + } + + @Override + public void initialize() { + final EnergenieConfiguration config = getConfigAs(EnergenieConfiguration.class); + + if (!config.host.isEmpty() && !config.password.isEmpty()) { + refreshInterval = EnergenieConfiguration.DEFAULT_REFRESH_INTERVAL; + host = config.host; + logger.debug("Initializing EnergenieHandler for Host '{}'", config.host); + energenieSocket = new EnergenieSocket(config.host, config.password); + + updateStatus(ThingStatus.UNKNOWN); + refreshJob = scheduler.scheduleWithFixedDelay(this::refreshState, 6, refreshInterval, TimeUnit.SECONDS); + } else { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, + "Can not access device , IP-Address or password not set"); + } + } + + @Override + public void dispose() { + logger.debug("EnergenieHandler disposed."); + final ScheduledFuture refreshJob = this.refreshJob; + + if (refreshJob != null) { + refreshJob.cancel(true); + this.refreshJob = null; + } + } + + private boolean refreshState() { + final EnergenieSocket socket = this.energenieSocket; + + if (socket != null) { + try { + stateUpdate(socket.retrieveStatus()); + if (thing.getStatus() != ThingStatus.ONLINE && ThingHandlerHelper.isHandlerInitialized(thing)) { + updateStatus(ThingStatus.ONLINE); + } + return true; + } catch (final UnknownHostException e) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, + "Can't find host: " + e.getMessage()); + } catch (final IOException e) { + logger.debug("Couldn't get I/O for the connection to: {}:{}.", host, TCP_PORT, e); + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, + "Couldn't get I/O for the connection"); + } catch (final RuntimeException e) { + logger.debug("Unexpected error", e); + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.NONE, e.getMessage()); + } + } + return false; + } + + public void stateUpdate(final byte[] status) { + for (int i = 0; i < 4; i++) { + final String socket = CHANNEL_SOCKET_PREFIX + (i + 1); + final String stringStatus = String.format("0x%02x", status[i]); + updateState(socket, OnOffType.from(stringStatus.equals(statusOn))); + } + } +} diff --git a/bundles/org.openhab.binding.energenie/src/main/java/org/openhab/binding/energenie/internal/handler/EnergeniePWMHandler.java b/bundles/org.openhab.binding.energenie/src/main/java/org/openhab/binding/energenie/internal/handler/EnergeniePWMHandler.java new file mode 100644 index 0000000000000..5cf140864fe65 --- /dev/null +++ b/bundles/org.openhab.binding.energenie/src/main/java/org/openhab/binding/energenie/internal/handler/EnergeniePWMHandler.java @@ -0,0 +1,132 @@ +/** + * Copyright (c) 2010-2020 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.energenie.internal.handler; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.TimeUnit; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.eclipse.smarthome.core.thing.ChannelUID; +import org.eclipse.smarthome.core.thing.Thing; +import org.eclipse.smarthome.core.thing.ThingStatus; +import org.eclipse.smarthome.core.thing.ThingStatusDetail; +import org.eclipse.smarthome.core.thing.binding.BaseThingHandler; +import org.eclipse.smarthome.core.types.Command; +import org.eclipse.smarthome.core.types.RefreshType; +import org.eclipse.smarthome.io.net.http.HttpUtil; +import org.openhab.binding.energenie.internal.EnergeniePWMStateEnum; +import org.openhab.binding.energenie.internal.config.EnergenieConfiguration; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * The {@link EnergeniePWMHandler} is responsible for reading states and update PWM channels. + * + * @author Hans-Jörg Merk - Initial contribution + */ + +@NonNullByDefault +public class EnergeniePWMHandler extends BaseThingHandler { + + private final Logger logger = LoggerFactory.getLogger(EnergeniePWMHandler.class); + + private static final int HTTP_TIMEOUT_MILLISECONDS = 6000; + + private String host = ""; + private String password = ""; + private int refreshInterval; + + private @Nullable ScheduledFuture refreshJob; + + public EnergeniePWMHandler(Thing thing) { + super(thing); + } + + @Override + public void handleCommand(ChannelUID channelUID, Command command) { + if (command instanceof RefreshType) { + scheduler.execute(this::getState); + } + } + + @Override + public void initialize() { + EnergenieConfiguration config = getConfigAs(EnergenieConfiguration.class); + + if (!config.host.isEmpty() && !config.password.isEmpty()) { + host = config.host; + password = config.password; + refreshInterval = EnergenieConfiguration.DEFAULT_REFRESH_INTERVAL; + logger.debug("Initializing EnergeniePWMHandler for Host '{}'", host); + updateStatus(ThingStatus.ONLINE); + onUpdate(); + } else { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, + "Can not access device , IP-Address or password not set"); + } + } + + @Override + public void dispose() { + logger.debug("EnergeniePWMHandler disposed."); + final ScheduledFuture refreshJob = this.refreshJob; + + if (refreshJob != null) { + refreshJob.cancel(true); + this.refreshJob = null; + } + } + + public synchronized void getState() { + String url = "http://" + host + "/login.html"; + String urlParameters = "pw=" + password; + InputStream urlContent = new ByteArrayInputStream(urlParameters.getBytes(StandardCharsets.UTF_8)); + String loginResponseString = null; + + try { + logger.trace("sending 'POST' request to URL : {}", url); + loginResponseString = HttpUtil.executeUrl("POST", url, urlContent, "TEXT/PLAIN", HTTP_TIMEOUT_MILLISECONDS); + + if (loginResponseString != null) { + updateState("voltage", EnergeniePWMStateEnum.VOLTAGE.readState(loginResponseString)); + updateState("current", EnergeniePWMStateEnum.CURRENT.readState(loginResponseString)); + updateState("power", EnergeniePWMStateEnum.POWER.readState(loginResponseString)); + updateState("energy", EnergeniePWMStateEnum.ENERGY.readState(loginResponseString)); + try { + HttpUtil.executeUrl("POST", url, HTTP_TIMEOUT_MILLISECONDS); + logger.trace("logout from ip {}", host); + } catch (IOException e) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, + "failed to logout: " + e.getMessage()); + } + } + + } catch (IOException e) { + logger.debug("energenie: failed to login to {} with ip {}", thing.getUID(), host, e); + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage()); + } + } + + private synchronized void onUpdate() { + ScheduledFuture refreshJob = this.refreshJob; + if (refreshJob == null || refreshJob.isCancelled()) { + this.refreshJob = scheduler.scheduleWithFixedDelay(this::getState, 5, refreshInterval, TimeUnit.SECONDS); + } + } + +} diff --git a/bundles/org.openhab.binding.energenie/src/main/java/org/openhab/binding/energenie/internal/handler/EnergenieSocket.java b/bundles/org.openhab.binding.energenie/src/main/java/org/openhab/binding/energenie/internal/handler/EnergenieSocket.java new file mode 100644 index 0000000000000..ab9e476f60860 --- /dev/null +++ b/bundles/org.openhab.binding.energenie/src/main/java/org/openhab/binding/energenie/internal/handler/EnergenieSocket.java @@ -0,0 +1,182 @@ +/** + * Copyright (c) 2010-2020 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.energenie.internal.handler; + +import static org.openhab.binding.energenie.internal.EnergenieBindingConstants.*; + +import java.io.Closeable; +import java.io.DataInputStream; +import java.io.IOException; +import java.io.OutputStream; +import java.net.Socket; +import java.net.UnknownHostException; +import java.nio.ByteBuffer; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.smarthome.core.util.HexUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Class handling the Socket connections. + * + * @author Hans-Jörg Merk - Initial contribution + * @author Hilbrand Bouwkamp - Moved Socket to it's own class + */ +@NonNullByDefault +class EnergenieSocket { + + private static final int SOCKET_TIMEOUT_MILLISECONDS = 1500; + private static final byte[] MESSAGE = { 0x11 }; + + private final Logger logger = LoggerFactory.getLogger(EnergenieSocket.class); + private final String host; + private final byte[] key; + + public EnergenieSocket(final String host, final String password) { + this.host = host; + this.key = getKey(password); + } + + private static byte[] getKey(final String password) { + final int passwordLength = password.length(); + String passwordString = password; + for (int i = 0; i < (8 - passwordLength); i++) { + passwordString = passwordString + " "; + } + return passwordString.getBytes(); + } + + public synchronized byte[] sendCommand(final byte[] ctrl) throws IOException { + try (final TaskSocket taskSocket = authorize()) { + final OutputStream output = taskSocket.socket.getOutputStream(); + final DataInputStream input = new DataInputStream(taskSocket.socket.getInputStream()); + + if (output == null) { + throw new IOException("No connection"); + } else { + if (logger.isTraceEnabled()) { + logger.trace("Control message send to EG (int) '{}' (hex)'{}'", ctrl, HexUtils.bytesToHex(ctrl)); + } + output.write(encryptControls(ctrl, taskSocket.task)); + output.flush(); + readStatus(input, taskSocket); + return updateStatus(taskSocket); + } + } + } + + public synchronized byte[] retrieveStatus() throws IOException { + try (final TaskSocket taskSocket = authorize()) { + return updateStatus(taskSocket); + } + } + + private TaskSocket authorize() throws IOException { + final TaskSocket taskSocket = new TaskSocket(); + final OutputStream output = taskSocket.socket.getOutputStream(); + final DataInputStream input = new DataInputStream(taskSocket.socket.getInputStream()); + if (output == null) { + throw new IOException("No connection"); + } + output.write(MESSAGE); + output.flush(); + logger.trace("Start Condition '{}' send to EG", MESSAGE); + input.readFully(taskSocket.task); + + if (logger.isTraceEnabled()) { + logger.trace("EG responded with task (int) '{}' (hex) '{}'", taskSocket.task, + HexUtils.bytesToHex(taskSocket.task)); + } + final byte[] solutionMessage = calculateSolution(taskSocket.task); + + output.write(solutionMessage); + output.flush(); + logger.trace("Solution '{}' send to EG", solutionMessage); + readStatus(input, taskSocket); + return taskSocket; + } + + private void readStatus(final DataInputStream input, final TaskSocket taskSocket) throws IOException { + input.readFully(taskSocket.statcryp); + if (logger.isTraceEnabled()) { + logger.trace("EG responded with statcryp (int) '{}' (hex) '{}'", taskSocket.statcryp, + HexUtils.bytesToHex(taskSocket.statcryp)); + } + } + + private byte[] updateStatus(final TaskSocket taskSocket) throws IOException { + final byte[] status = decryptStatus(taskSocket); + + if (logger.isTraceEnabled()) { + logger.trace("EG responded with status (int) '{}' (hex) '{}'", status, HexUtils.bytesToHex(status)); + } + return status; + } + + private byte[] calculateSolution(final byte[] task) { + final int[] uIntTask = new int[4]; + + for (int i = 0; i < 4; i++) { + uIntTask[i] = Byte.toUnsignedInt(task[i]); + } + final int solutionLoword = (((uIntTask[0] ^ key[2]) * key[0]) ^ (key[6] | (key[4] << 8)) ^ uIntTask[2]); + final byte[] loword = ByteBuffer.allocate(4).putInt(solutionLoword).array(); + + final int solutionHiword = (((uIntTask[1] ^ key[3]) * key[1]) ^ (key[7] | (key[5] << 8)) ^ uIntTask[3]); + final byte[] hiword = ByteBuffer.allocate(4).putInt(solutionHiword).array(); + final byte[] solution = new byte[SOLUTION_LEN]; + + solution[0] = loword[3]; + solution[1] = loword[2]; + solution[2] = hiword[3]; + solution[3] = hiword[2]; + + return solution; + } + + private byte[] decryptStatus(final TaskSocket taskSocket) { + final byte[] status = new byte[4]; + + for (int i = 0; i < 4; i++) { + status[i] = (byte) ((((taskSocket.statcryp[3 - i] - key[1]) ^ key[0]) - taskSocket.task[3]) + ^ taskSocket.task[2]); + } + return status; + } + + private byte[] encryptControls(final byte[] controls, final byte[] task) { + final byte[] ctrlcryp = new byte[CTRLCRYP_LEN]; + + for (int i = 0; i < 4; i++) { + ctrlcryp[i] = (byte) ((((controls[3 - i] ^ task[2]) + task[3]) ^ key[0]) + key[1]); + } + return ctrlcryp; + } + + private class TaskSocket implements Closeable { + final Socket socket; + final byte[] task = new byte[TASK_LEN]; + final byte[] statcryp = new byte[STATCRYP_LEN]; + + public TaskSocket() throws UnknownHostException, IOException { + socket = new Socket(host, TCP_PORT); + socket.setSoTimeout(SOCKET_TIMEOUT_MILLISECONDS); + } + + @Override + public void close() throws IOException { + socket.close(); + } + } +} diff --git a/bundles/org.openhab.binding.energenie/src/main/resources/ESH-INF/binding/binding.xml b/bundles/org.openhab.binding.energenie/src/main/resources/ESH-INF/binding/binding.xml new file mode 100644 index 0000000000000..8582bce616318 --- /dev/null +++ b/bundles/org.openhab.binding.energenie/src/main/resources/ESH-INF/binding/binding.xml @@ -0,0 +1,10 @@ + + + + Energenie Binding + This is the binding for Energenie. + Hans-Jörg Merk + + diff --git a/bundles/org.openhab.binding.energenie/src/main/resources/ESH-INF/config/config.xml b/bundles/org.openhab.binding.energenie/src/main/resources/ESH-INF/config/config.xml new file mode 100644 index 0000000000000..5b79eac6206ee --- /dev/null +++ b/bundles/org.openhab.binding.energenie/src/main/resources/ESH-INF/config/config.xml @@ -0,0 +1,19 @@ + + + + + + The IP address of this device + network-address + + + + The password of this device + 1 + password + + + diff --git a/bundles/org.openhab.binding.energenie/src/main/resources/ESH-INF/thing/thing-types.xml b/bundles/org.openhab.binding.energenie/src/main/resources/ESH-INF/thing/thing-types.xml new file mode 100644 index 0000000000000..947734b5333ab --- /dev/null +++ b/bundles/org.openhab.binding.energenie/src/main/resources/ESH-INF/thing/thing-types.xml @@ -0,0 +1,134 @@ + + + + + + Energenie programmable power strip with LAN interface + + + + + + + + + + + + + + + Energenie programmable power strip with LAN interface + + + + + + + + + + + + + + + Energenie programmable power strip with LAN interface + + + + + + + + + + + + + + + Energenie programmable power strip with WLAN interface + + + + + + + + + + + + + + + Energenie energy meter with LAN interface + + + + + + + + + + + + + + Switch + + Channel representing the state of socket 1 + + + + Switch + + Channel representing the state of socket 2 + + + + Switch + + Channel representing the state of socket 3 + + + + Switch + + Channel representing the state of socket 4 + + + + Number:ElectricPotential + + Channel representing the voltage + + + + + Number:ElectricCurrent + + Channel representing the current + + + + + Number:Power + + Channel representing the power consumption + + + + + Number:Energy + + Channel representing the energy consumption + + + + diff --git a/bundles/pom.xml b/bundles/pom.xml index 132ec52fca9f6..2566c42672532 100644 --- a/bundles/pom.xml +++ b/bundles/pom.xml @@ -83,6 +83,7 @@ org.openhab.binding.dwdunwetter org.openhab.binding.ecobee org.openhab.binding.elerotransmitterstick + org.openhab.binding.energenie org.openhab.binding.enocean org.openhab.binding.enturno org.openhab.binding.etherrain