diff --git a/CODEOWNERS b/CODEOWNERS index 2a9b4f87bc07e..153e8f9f8a061 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -101,6 +101,7 @@ /bundles/org.openhab.binding.nanoleaf/ @raepple /bundles/org.openhab.binding.neato/ @jjlauterbach /bundles/org.openhab.binding.neeo/ @tmrobert8 +/bundles/org.openhab.binding.neohub/ @andrewfg /bundles/org.openhab.binding.nest/ @wborn /bundles/org.openhab.binding.netatmo/ @clinique @cweitkamp @lolodomo /bundles/org.openhab.binding.network/ @davidgraeff @mettke diff --git a/bom/openhab-addons/pom.xml b/bom/openhab-addons/pom.xml index 9f96c97d5d438..e4aaa69cff8c0 100644 --- a/bom/openhab-addons/pom.xml +++ b/bom/openhab-addons/pom.xml @@ -495,6 +495,11 @@ org.openhab.binding.neeo ${project.version} + + org.openhab.addons.bundles + org.openhab.binding.neohub + ${project.version} + org.openhab.addons.bundles org.openhab.binding.nest diff --git a/bundles/org.openhab.binding.neohub/.classpath b/bundles/org.openhab.binding.neohub/.classpath new file mode 100644 index 0000000000000..a5d95095ccaaf --- /dev/null +++ b/bundles/org.openhab.binding.neohub/.classpath @@ -0,0 +1,32 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/bundles/org.openhab.binding.neohub/.project b/bundles/org.openhab.binding.neohub/.project new file mode 100644 index 0000000000000..a8259078fd388 --- /dev/null +++ b/bundles/org.openhab.binding.neohub/.project @@ -0,0 +1,23 @@ + + + org.openhab.binding.neohub + + + + + + 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.neohub/.settings/org.eclipse.core.resources.prefs b/bundles/org.openhab.binding.neohub/.settings/org.eclipse.core.resources.prefs new file mode 100644 index 0000000000000..839d647eef851 --- /dev/null +++ b/bundles/org.openhab.binding.neohub/.settings/org.eclipse.core.resources.prefs @@ -0,0 +1,5 @@ +eclipse.preferences.version=1 +encoding//src/main/java=UTF-8 +encoding//src/main/resources=UTF-8 +encoding//src/test/java=UTF-8 +encoding/=UTF-8 diff --git a/bundles/org.openhab.binding.neohub/.settings/org.eclipse.jdt.core.prefs b/bundles/org.openhab.binding.neohub/.settings/org.eclipse.jdt.core.prefs new file mode 100644 index 0000000000000..2f5cc74c3a857 --- /dev/null +++ b/bundles/org.openhab.binding.neohub/.settings/org.eclipse.jdt.core.prefs @@ -0,0 +1,8 @@ +eclipse.preferences.version=1 +org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.8 +org.eclipse.jdt.core.compiler.compliance=1.8 +org.eclipse.jdt.core.compiler.problem.enablePreviewFeatures=disabled +org.eclipse.jdt.core.compiler.problem.forbiddenReference=warning +org.eclipse.jdt.core.compiler.problem.reportPreviewFeatures=ignore +org.eclipse.jdt.core.compiler.release=disabled +org.eclipse.jdt.core.compiler.source=1.8 diff --git a/bundles/org.openhab.binding.neohub/NOTICE b/bundles/org.openhab.binding.neohub/NOTICE new file mode 100644 index 0000000000000..4c20ef446c1e4 --- /dev/null +++ b/bundles/org.openhab.binding.neohub/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/openhab2-addons diff --git a/bundles/org.openhab.binding.neohub/README.md b/bundles/org.openhab.binding.neohub/README.md new file mode 100644 index 0000000000000..f24569f648ffd --- /dev/null +++ b/bundles/org.openhab.binding.neohub/README.md @@ -0,0 +1,112 @@ +# NeoHub Binding + +This is a binding for integrating Heatmiser room and underfloor heating control products. +The NeoHub (bridge) binding allows you to connect openHab via TCP/IP to Heatmiser's NeoHub and integrate your NeoStat smart thermostats and NeoPlug smart plugs onto the bus. + +See the manufacturers web site for more details: https://www.heatmiser.com + +## Supported Things + +The binding supports three types of Thing as follows.. + +| Thing Type | Description | +|------------|-------------------------------------------------------------------------------------------| +| NeoHub | The Heatmiser NeoHub bridge which is used to communicate with NeoStat and NeoPlug devices | +| NeoStat | Heatmiser Neostat Smart Thermostat | +| NeoPlug | Heatmiser NeoPlug Smart Plug | + +## Discovery + +You have to manually create a single (Bridge) Thing for the NeoHub, and enter the required Configuration Parameters (see Thing Configuration for NeoHub below). +If the Configuration Parameters are all valid, then the NeoHub Thing will automatically attempt to connect and sign on to the hub. +If the sign on succeeds, the Thing will indicate its status as Online, otherwise it will show an error status. + +Once the NeoHub Thing has been created and it has successfully signed on, it will automatically interrogate the server to discover all the respective NeoStat and NeoPlug Things that are connected to the hub. + +## Thing Configuration for "NeoHub" + +The NeoHub Thing connects to the hub (bridge) to communicate with any respective connected NeoStat and NeoPlug Things. +It signs on to the hub using the supplied connection parameters, and it polls the hub at regular intervals to read and write the data for each NeoXxx device. +Before it can connect to the hub, the following Configuration Parameters must be entered. + +| Configuration Parameter | Description | +|-------------------------|-------------------------------------------------------------------------------------------| +| hostName | Host name (IP address) of the NeoHub (example 192.168.1.123) | +| portNumber | Port number of the NeoHub (Default=4242) | +| pollingInterval | Time (seconds) between polling requests to the NeoHub (Minimum=4; Maximum=60; Default=60) | + +## Thing Configuration for "NeoStat" and "NeoPlug" + +The NeoHub Thing connects to the hub (bridge) to communicate with any NeoStat or NeoPlug devices that are connected to it. +Each such NeoStat or NeoPlug device is identified by means of a unique Device Name in the hub. +The Device Name is automatically discovered by the NeoHub Thing, and it is also visible (and changeable) via the Heatmiser App. + +| Configuration Parameter | Description | +|-------------------------|-------------------------------------------------------------------------------| +| deviceNameInHub | Device Name that identifies the NeoXxx device in the NeoHub and Heatmiser App | + +## Channels for "NeoStat" + +The following Channels, and their associated channel types are shown below. + +| Channel | Data Type | Description | +|-----------------------|--------------------|-----------------------------------------------------------------------------| +| roomTemperature | Number:Temperature | Actual room temperature | +| targetTemperature | Number:Temperature | Target temperature setting for the room | +| floorTemperature | Number:Temperature | Actual floor temperature | +| thermostatOutputState | String | Status of whether the thermostat is Off, or calling for Heat | +| occupancyModePresent | Switch | The Thermostat is in the Present Occupancy Mode (Off=Absent, On=Present) | + +## Channels for "NeoPlug" + +The following Channels, and their associated channel types are shown below. + +| Channel | Data Type | Description | +|----------------------|-----------|----------------------------------------------------------| +| plugOutputState | Switch | The output state of the Plug switch (Off, On) | +| plugAutoMode | Switch | The Plug is in Automatic Mode (Off=Manual, On=Automatic) | + + +## Full Example + +### `demo.things` File + +``` +Bridge neohub:neohub:myhubname "Heatmiser NeoHub" [ hostName="192.168.1.123", portNumber=4242, pollingInterval=60 ] { + Thing neohub:neoplug:myhubname:mydownstairs "Downstairs Plug" @ "Hall" [ deviceNameInHub="Hall Plug" ] + Thing neohub:neostat:myhubname:myupstairs "Upstairs Thermostat" @ "Landing" [ deviceNameInHub="Landing Thermostat" ] +} +``` + +### `demo.items` File + +``` +Number:Temperature Upstairs_RoomTemperature "Room Temperature" { channel="neohub:neostat:myhubname:myupstairs:roomTemperature" } +Number:Temperature Upstairs_TargetTemperature "Target Temperature" { channel="neohub:neostat:myhubname:myupstairs:targetTemperature" } +Number:Temperature Upstairs_FloorTemperature "Floor Temperature" { channel="neohub:neostat:myhubname:myupstairs:floorTemperature" } +String Upstairs_ThermostatOutputState "Heating State" { channel="neohub:neostat:myhubname:myupstairs:thermostatOutputState" } +Switch Upstairs_OccupancyModePresent "Occupancy Mode Present" { channel="neohub:neostat:myhubname:myupstairs:myupstairs:occupancyModePresent" } + +Switch Downstairs_PlugAutoMode "Plug Auto Mode" { channel="neohub:neoplug:myhubname:mydownstairs:plugAutoMode" } +Switch Downstairs_PlugOutputState "Plug Output State" { channel="neohub:neoplug:myhubname:mydownstairs:plugOutputState" } +``` + +### `demo.sitemap` File + +``` +sitemap neohub label="Heatmiser NeoHub" +{ + Frame label="Heating" { + Text item=Upstairs_RoomTemperature + Setpoint item=Upstairs_TargetTemperature minValue=15 maxValue=30 step=1 + Text item=Upstairs_ThermostatOutputState + Switch item=Upstairs_OccupancyModePresent + Text item=Upstairs_FloorTemperature + } + + Frame label="Plug" { + Switch item=Downstairs_PlugOutputState + Switch item=Downstairs_PlugAutoMode + } +} +``` diff --git a/bundles/org.openhab.binding.neohub/pom.xml b/bundles/org.openhab.binding.neohub/pom.xml new file mode 100644 index 0000000000000..c91d7146a2644 --- /dev/null +++ b/bundles/org.openhab.binding.neohub/pom.xml @@ -0,0 +1,17 @@ + + + + 4.0.0 + + + org.openhab.addons.bundles + org.openhab.addons.reactor.bundles + 2.5.0-SNAPSHOT + + + org.openhab.binding.neohub + openHAB Add-ons :: Bundles :: NeoHub Binding + + diff --git a/bundles/org.openhab.binding.neohub/src/main/feature/feature.xml b/bundles/org.openhab.binding.neohub/src/main/feature/feature.xml new file mode 100644 index 0000000000000..40faeba0407ef --- /dev/null +++ b/bundles/org.openhab.binding.neohub/src/main/feature/feature.xml @@ -0,0 +1,9 @@ + + + mvn:org.openhab.core.features.karaf/org.openhab.core.features.karaf.openhab-core/${project.version}/xml/features + + + openhab-runtime-base + mvn:org.openhab.addons.bundles/org.openhab.binding.neohub/${project.version} + + diff --git a/bundles/org.openhab.binding.neohub/src/main/history/dependencies.xml b/bundles/org.openhab.binding.neohub/src/main/history/dependencies.xml new file mode 100644 index 0000000000000..4f17f473dd1d0 --- /dev/null +++ b/bundles/org.openhab.binding.neohub/src/main/history/dependencies.xml @@ -0,0 +1,7 @@ + + + + openhab-runtime-base + mvn:org.openhab.addons.bundles/org.openhab.binding.neohub/2.5.0-SNAPSHOT + + diff --git a/bundles/org.openhab.binding.neohub/src/main/java/org/openhab/binding/neohub/internal/NeoBaseConfiguration.java b/bundles/org.openhab.binding.neohub/src/main/java/org/openhab/binding/neohub/internal/NeoBaseConfiguration.java new file mode 100644 index 0000000000000..7bc4537d4a9ff --- /dev/null +++ b/bundles/org.openhab.binding.neohub/src/main/java/org/openhab/binding/neohub/internal/NeoBaseConfiguration.java @@ -0,0 +1,25 @@ +/** + * Copyright (c) 2010-2019 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.neohub.internal; + +/** + * The {@link NeoBaseConfiguration} class contains the thing configuration + * parameters for NeoStat and NeoPlug devices + * + * @author Andrew Fiddian-Green - Initial contribution + */ +public class NeoBaseConfiguration { + + public String deviceNameInHub; + +} diff --git a/bundles/org.openhab.binding.neohub/src/main/java/org/openhab/binding/neohub/internal/NeoBaseHandler.java b/bundles/org.openhab.binding.neohub/src/main/java/org/openhab/binding/neohub/internal/NeoBaseHandler.java new file mode 100644 index 0000000000000..55ea40868825e --- /dev/null +++ b/bundles/org.openhab.binding.neohub/src/main/java/org/openhab/binding/neohub/internal/NeoBaseHandler.java @@ -0,0 +1,235 @@ +/** + * Copyright (c) 2010-2019 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.neohub.internal; + +import org.eclipse.jdt.annotation.Nullable; +import org.eclipse.smarthome.core.library.types.OnOffType; +import org.eclipse.smarthome.core.thing.Bridge; +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.binding.BridgeHandler; +import org.eclipse.smarthome.core.types.Command; +import org.eclipse.smarthome.core.types.RefreshType; +import org.eclipse.smarthome.core.types.State; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * The {@link NeoBaseHandler} is the openHAB Handler for NeoPlug devices + * + * @author Andrew Fiddian-Green - Initial contribution + * + */ +public class NeoBaseHandler extends BaseThingHandler { + + protected final Logger logger = LoggerFactory.getLogger(NeoBaseHandler.class); + + protected NeoBaseConfiguration config; + + /* + * error messages + */ + private static final String MSG_HUB_CONFIG = "hub needs to be initialized!"; + private static final String MSG_HUB_COMM = "error communicating with the hub!"; + private static final String MSG_FMT_DEVICE_CONFIG = "device {} needs to configured in hub!"; + private static final String MSG_FMT_DEVICE_COMM = "device {} not communicating with hub!"; + private static final String MSG_FMT_COMMAND_OK = "command for {} succeeded."; + private static final String MSG_FMT_COMMAND_BAD = "{} is an invalid or empty command!"; + private static final String MSG_MISSING_PARAM = "Missing parameter \"deviceNameInHub\""; + + /* + * an object used to de-bounce state changes between openHAB and the NeoHub + */ + protected NeoHubDebouncer debouncer = new NeoHubDebouncer(); + + public NeoBaseHandler(Thing thing) { + super(thing); + } + + // ======== BaseThingHandler methods that are overridden ============= + + @Override + public void handleCommand(ChannelUID channelUID, Command command) { + if (command == RefreshType.REFRESH) { + @Nullable + NeoHubHandler hub; + + if ((hub = getNeoHub()) != null) { + hub.startFastPollingBurst(); + } + return; + } + + toNeoHubSendCommandSet(channelUID.getId(), command); + } + + @Override + public void initialize() { + config = getConfigAs(NeoBaseConfiguration.class); + + if (config == null || config.deviceNameInHub == null || config.deviceNameInHub.isEmpty()) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, MSG_MISSING_PARAM); + return; + } + refreshStateOnline(getNeoHub()); + } + + // ======== helper methods used by this class or descendants =========== + + /** + * refresh the handler online state + * + * @return true if the handler is online + */ + private boolean refreshStateOnline(NeoHubHandler hub) { + if (hub == null) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.NONE); + logger.warn("Unexpected situation - please report a bug: " + MSG_HUB_CONFIG); + return false; + } + + if (!hub.isConfigured(config.deviceNameInHub)) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.NONE); + logger.warn("Unexpected situation - please report a bug: " + MSG_FMT_DEVICE_CONFIG, getThing().getLabel()); + return false; + } + + if (!hub.isOnline(config.deviceNameInHub)) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR); + logger.debug(MSG_FMT_DEVICE_COMM, getThing().getLabel()); + return false; + } + + updateStatus(ThingStatus.ONLINE, ThingStatusDetail.NONE); + return true; + } + + /* + * this method is called back by the NeoHub handler to inform this handler about + * polling results from the hub handler + */ + public void toBaseSendPollResponse(NeoHubHandler hub, NeoHubInfoResponse infoResponse) { + NeoHubInfoResponse.DeviceInfo myInfo; + + if (refreshStateOnline(hub) && (myInfo = infoResponse.getDeviceInfo(config.deviceNameInHub)) != null) { + toOpenHabSendChannelValues(myInfo); + } + } + + /* + * internal method used by by sendChannelValuesToOpenHab(). It checks the + * de-bouncer before actually sending the channel value to openHAB + */ + protected void toOpenHabSendValueDebounced(String channelId, State state) { + if (debouncer.timeExpired(channelId)) { + updateState(channelId, state); + } + } + + /* + * sends a channel command & value from openHAB => NeoHub. It delegates upwards + * to the NeoHub to handle the command + */ + protected void toNeoHubSendCommand(String channelId, Command command) { + String cmdStr = toNeoHubBuildCommandString(channelId, command); + + if (!cmdStr.isEmpty()) { + NeoHubHandler hub = getNeoHub(); + + if (hub != null) { + /* + * issue command, check result, and update status accordingly + */ + switch (hub.toNeoHubSendChannelValue(cmdStr)) { + case SUCCEEDED: + logger.debug(MSG_FMT_COMMAND_OK, getThing().getLabel()); + + if (getThing().getStatus() != ThingStatus.ONLINE) { + updateStatus(ThingStatus.ONLINE, ThingStatusDetail.NONE); + } + + // initialize the de-bouncer for this channel + debouncer.initialize(channelId); + + break; + + case ERR_COMMUNICATION: + logger.debug(MSG_HUB_COMM); + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR); + break; + + case ERR_INITIALIZATION: + logger.warn("Unexpected situation - please report a bug: " + MSG_HUB_CONFIG); + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_OFFLINE); + break; + } + } else { + logger.debug(MSG_HUB_CONFIG); + } + } else { + logger.debug(MSG_FMT_COMMAND_BAD, command.toString()); + } + } + + /** + * internal getter returns the NeoHub handler + * + * @return the neohub handler or null + */ + private @Nullable NeoHubHandler getNeoHub() { + @Nullable + Bridge b; + + @Nullable + BridgeHandler h; + + if ((b = getBridge()) != null && (h = b.getHandler()) != null && h instanceof NeoHubHandler) { + return (NeoHubHandler) h; + } + + return null; + } + + // ========= methods that MAY / MUST be overridden in descendants ============ + + /* + * NOTE: descendant classes MUST override this method. It builds the command + * string to be sent to the NeoHub + */ + protected String toNeoHubBuildCommandString(String channelId, Command command) { + return ""; + } + + /* + * NOTE: descendant classes MAY override this method e.g. to send additional + * commands for dependent channels (if any) + */ + protected void toNeoHubSendCommandSet(String channelId, Command command) { + toNeoHubSendCommand(channelId, command); + } + + /* + * NOTE: descendant classes MUST override this method method by which the + * handler informs openHAB about channel state changes + */ + protected void toOpenHabSendChannelValues(NeoHubInfoResponse.DeviceInfo deviceInfo) { + } + + protected OnOffType invert(OnOffType value) { + return OnOffType.from(value == OnOffType.OFF); + } +} diff --git a/bundles/org.openhab.binding.neohub/src/main/java/org/openhab/binding/neohub/internal/NeoHubBindingConstants.java b/bundles/org.openhab.binding.neohub/src/main/java/org/openhab/binding/neohub/internal/NeoHubBindingConstants.java new file mode 100644 index 0000000000000..6ff6812f7a4cc --- /dev/null +++ b/bundles/org.openhab.binding.neohub/src/main/java/org/openhab/binding/neohub/internal/NeoHubBindingConstants.java @@ -0,0 +1,120 @@ +/** + * Copyright (c) 2010-2019 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.neohub.internal; + +import org.eclipse.smarthome.core.thing.ThingTypeUID; + +/** + * The {@link NeoHubBindingConstants} class defines common constants + * + * @author Sebastian Prehn - Initial contribution (NeoHub command codes) + * @author Andrew Fiddian-Green - Initial contribution (OpenHAB v2.x binding + * code) + * + */ +public class NeoHubBindingConstants { + + /* + * binding id + */ + public static final String BINDING_ID = "neohub"; + + /* + * device id's + */ + public static final String DEVICE_ID_NEOHUB = "neohub"; + public static final String DEVICE_ID_NEOSTAT = "neostat"; + public static final String DEVICE_ID_NEOPLUG = "neoplug"; + + /* + * Thing Type UIDs + */ + public static final ThingTypeUID THING_TYPE_NEOHUB = new ThingTypeUID(BINDING_ID, DEVICE_ID_NEOHUB); + public static final ThingTypeUID THING_TYPE_NEOSTAT = new ThingTypeUID(BINDING_ID, DEVICE_ID_NEOSTAT); + public static final ThingTypeUID THING_TYPE_NEOPLUG = new ThingTypeUID(BINDING_ID, DEVICE_ID_NEOPLUG); + + /* + * Channel IDs for NeoStats + */ + public static final String CHAN_ROOM_TEMP = "roomTemperature"; + public static final String CHAN_TARGET_TEMP = "targetTemperature"; + public static final String CHAN_FLOOR_TEMP = "floorTemperature"; + public static final String CHAN_OCC_MODE_PRESENT = "occupancyModePresent"; + public static final String CHAN_STAT_OUTPUT_STATE = "thermostatOutputState"; + + /* + * Channel IDs for NeoPlugs + */ + public static final String CHAN_PLUG_OUTPUT_STATE = "plugOutputState"; + public static final String CHAN_PLUG_AUTO_MODE = "plugAutoMode"; + + /* + * enumerator for results of method calls + */ + public static enum NeoHubReturnResult { + SUCCEEDED, ERR_COMMUNICATION, ERR_INITIALIZATION + }; + + /* + * the property IdD for the name of a thing in the NeoHub note: names may differ + * between the NeoHub and the OpenHAB framework + */ + public static final String DEVICE_NAME = "deviceNameInHub"; + + /* + * socket timeout in seconds for the TCP connection to the hub + */ + public static final int TCP_SOCKET_IMEOUT = 5; + + /* + * setup parameters for de-bouncing of state changes (time in seconds) so state + * changes that occur within this time window are ignored + */ + public static final long DEBOUNCE_DELAY = 15; + + /* + * setup parameters for lazy polling + */ + public static final int LAZY_POLL_INTERVAL = 60; + + /* + * setup parameters for fast polling bursts a burst comprises FAST_POLL_CYCLES + * polling calls spaced at FAST_POLL_INTERVAL for example 5 polling calls made + * at 4 second intervals (e.g. 5 x 4 => 20 seconds) + */ + public static final int FAST_POLL_CYCLES = 5; + public static final int FAST_POLL_INTERVAL = 4; + + /* + * setup parameters for device discovery + */ + public static final int DISCOVERY_TIMEOUT = 5; + public static final int DISCOVERY_START_DELAY = 30; + public static final int DISCOVERY_REFRESH_PERIOD = 600; + + /* + * NeoHub JSON command codes strings: Thanks to Sebastian Prehn !! + */ + public static final String CMD_CODE_INFO = "{\"INFO\":0}"; + public static final String CMD_CODE_TEMP = "{\"SET_TEMP\":[%s, \"%s\"]}"; + public static final String CMD_CODE_AWAY = "{\"FROST_%s\":\"%s\"}"; + public static final String CMD_CODE_TIMER = "{\"TIMER_%s\":\"%s\"}"; + public static final String CMD_CODE_MANUAL = "{\"MANUAL_%s\":\"%s\"}"; + + /* + * openHAB status strings + */ + public static final String VAL_OFF = "Off"; + public static final String VAL_HEATING = "Heating"; + +} diff --git a/bundles/org.openhab.binding.neohub/src/main/java/org/openhab/binding/neohub/internal/NeoHubConfiguration.java b/bundles/org.openhab.binding.neohub/src/main/java/org/openhab/binding/neohub/internal/NeoHubConfiguration.java new file mode 100644 index 0000000000000..3874d244272b0 --- /dev/null +++ b/bundles/org.openhab.binding.neohub/src/main/java/org/openhab/binding/neohub/internal/NeoHubConfiguration.java @@ -0,0 +1,27 @@ +/** + * Copyright (c) 2010-2019 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.neohub.internal; + +/** + * The {@link NeoHubConfiguration} class contains the thing configuration + * parameters + * + * @author Andrew Fiddian-Green - Initial contribution + */ +public class NeoHubConfiguration { + + public String hostName; + public int portNumber; + public int pollingInterval; + +} diff --git a/bundles/org.openhab.binding.neohub/src/main/java/org/openhab/binding/neohub/internal/NeoHubDebouncer.java b/bundles/org.openhab.binding.neohub/src/main/java/org/openhab/binding/neohub/internal/NeoHubDebouncer.java new file mode 100644 index 0000000000000..8effdc49dfbae --- /dev/null +++ b/bundles/org.openhab.binding.neohub/src/main/java/org/openhab/binding/neohub/internal/NeoHubDebouncer.java @@ -0,0 +1,57 @@ +/** + * Copyright (c) 2010-2019 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.neohub.internal; + +import static org.openhab.binding.neohub.internal.NeoHubBindingConstants.*; + +import java.util.Date; +import java.util.HashMap; +import java.util.Map; + +/** + * The {@link NeoHubDebouncer} determines if change events should be forwarded + * to a channel + * + * @author Andrew Fiddian-Green - Initial contribution + */ +public class NeoHubDebouncer { + + private final Map channels = new HashMap(); + + static class DebounceDelay { + + private long expireTime; + + public DebounceDelay(Boolean enabled) { + if (enabled) { + expireTime = new Date().getTime() + (DEBOUNCE_DELAY * 1000); + } + } + + public Boolean timeExpired() { + return (expireTime < new Date().getTime()); + } + } + + public NeoHubDebouncer() { + } + + public void initialize(String channelId) { + channels.put(channelId, new DebounceDelay(true)); + } + + public Boolean timeExpired(String channelId) { + return (channels.containsKey(channelId) ? channels.get(channelId).timeExpired() : true); + } + +} diff --git a/bundles/org.openhab.binding.neohub/src/main/java/org/openhab/binding/neohub/internal/NeoHubDiscoveryService.java b/bundles/org.openhab.binding.neohub/src/main/java/org/openhab/binding/neohub/internal/NeoHubDiscoveryService.java new file mode 100644 index 0000000000000..364f8d8ebc74e --- /dev/null +++ b/bundles/org.openhab.binding.neohub/src/main/java/org/openhab/binding/neohub/internal/NeoHubDiscoveryService.java @@ -0,0 +1,136 @@ +/** + * Copyright (c) 2010-2019 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.neohub.internal; + +import static org.openhab.binding.neohub.internal.NeoHubBindingConstants.*; +import org.openhab.binding.neohub.internal.NeoHubInfoResponse.DeviceInfo; + +import java.util.Collections; +import java.util.List; +import java.util.Set; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import org.eclipse.smarthome.config.discovery.AbstractDiscoveryService; +import org.eclipse.smarthome.config.discovery.DiscoveryResult; +import org.eclipse.smarthome.config.discovery.DiscoveryResultBuilder; +import org.eclipse.smarthome.core.thing.ThingStatus; +import org.eclipse.smarthome.core.thing.ThingTypeUID; +import org.eclipse.smarthome.core.thing.ThingUID; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Discovery service for neo devices + * + * @author Andrew Fiddian-Green - Initial contribution + * + */ +public class NeoHubDiscoveryService extends AbstractDiscoveryService { + + private final Logger logger = LoggerFactory.getLogger(NeoHubDiscoveryService.class); + + private ScheduledFuture discoveryScheduler; + private NeoHubHandler hub; + + public static final Set DISCOVERABLE_THING_TYPES_UIDS = Collections + .unmodifiableSet(Stream.of(THING_TYPE_NEOSTAT, THING_TYPE_NEOPLUG).collect(Collectors.toSet())); + + public NeoHubDiscoveryService(NeoHubHandler hub) { + // note: background discovery is enabled in the super method + super(DISCOVERABLE_THING_TYPES_UIDS, DISCOVERY_TIMEOUT); + this.hub = hub; + } + + public void activate() { + super.activate(null); + } + + @Override + public void deactivate() { + super.deactivate(); + } + + @Override + protected void startScan() { + if (hub.getThing().getStatus() == ThingStatus.ONLINE) { + discoverDevices(); + } + } + + @Override + protected void startBackgroundDiscovery() { + logger.debug("start background discovery.."); + + if (discoveryScheduler == null || discoveryScheduler.isCancelled()) { + discoveryScheduler = scheduler.scheduleWithFixedDelay(this::startScan, 10, DISCOVERY_REFRESH_PERIOD, + TimeUnit.SECONDS); + } + } + + @Override + protected void stopBackgroundDiscovery() { + logger.debug("stop background discovery.."); + + if (discoveryScheduler != null && !discoveryScheduler.isCancelled()) { + discoveryScheduler.cancel(true); + } + } + + private void discoverDevices() { + NeoHubInfoResponse infoResponse; + if ((infoResponse = hub.fromNeoHubFetchPollingResponse()) != null) { + List devices; + if ((devices = infoResponse.getDevices()) != null) { + for (DeviceInfo device : devices) { + publishDevice(device); + } + } + } + } + + private void publishDevice(DeviceInfo deviceInfo) { + String deviceType; + String deviceOpenHabId; + String deviceNeohubName; + ThingUID bridgeUID; + ThingUID deviceUID; + ThingTypeUID deviceTypeUID; + DiscoveryResult device; + + bridgeUID = hub.getThing().getUID(); + + if (deviceInfo.getDeviceType().intValue() == 6) { + deviceType = DEVICE_ID_NEOPLUG; + deviceTypeUID = THING_TYPE_NEOPLUG; + } else { + deviceType = DEVICE_ID_NEOSTAT; + deviceTypeUID = THING_TYPE_NEOSTAT; + } + + deviceNeohubName = deviceInfo.getDeviceName(); + deviceOpenHabId = deviceNeohubName.replaceAll("\\s+", "_"); + deviceUID = new ThingUID(deviceTypeUID, bridgeUID, deviceOpenHabId); + + device = DiscoveryResultBuilder.create(deviceUID).withBridge(bridgeUID).withLabel(deviceOpenHabId) + .withProperty(DEVICE_NAME, deviceNeohubName).withRepresentationProperty(DEVICE_NAME).build(); + + thingDiscovered(device); + + logger.debug("discovered device={}, name={} ..", deviceType, deviceOpenHabId); + } + +} diff --git a/bundles/org.openhab.binding.neohub/src/main/java/org/openhab/binding/neohub/internal/NeoHubException.java b/bundles/org.openhab.binding.neohub/src/main/java/org/openhab/binding/neohub/internal/NeoHubException.java new file mode 100644 index 0000000000000..3cead5ce1a23f --- /dev/null +++ b/bundles/org.openhab.binding.neohub/src/main/java/org/openhab/binding/neohub/internal/NeoHubException.java @@ -0,0 +1,28 @@ +/** + * Copyright (c) 2010-2019 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.neohub.internal; + +/** + * The {@link NeoHubException} is a custom exception for NeoHub + * + * @author Andrew Fiddian-Green - Initial contribution + * + */ +public class NeoHubException extends RuntimeException { + + private static final long serialVersionUID = -7358712540781217363L; + + public NeoHubException(String message) { + super(message); + } +} diff --git a/bundles/org.openhab.binding.neohub/src/main/java/org/openhab/binding/neohub/internal/NeoHubHandler.java b/bundles/org.openhab.binding.neohub/src/main/java/org/openhab/binding/neohub/internal/NeoHubHandler.java new file mode 100644 index 0000000000000..24cf156a29e4d --- /dev/null +++ b/bundles/org.openhab.binding.neohub/src/main/java/org/openhab/binding/neohub/internal/NeoHubHandler.java @@ -0,0 +1,264 @@ +/** + * Copyright (c) 2010-2019 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.neohub.internal; + +import static org.openhab.binding.neohub.internal.NeoHubBindingConstants.*; + +import java.util.List; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; + +import org.eclipse.jdt.annotation.Nullable; +import org.eclipse.smarthome.core.thing.Bridge; +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.BaseBridgeHandler; +import org.eclipse.smarthome.core.thing.binding.ThingHandler; +import org.eclipse.smarthome.core.types.Command; + +import org.openhab.binding.neohub.internal.NeoHubInfoResponse.DeviceInfo; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * The {@link NeoHubHandler} is the openHAB Handler for NeoHub devices + * + * @author Andrew Fiddian-Green - Initial contribution (v2.x binding code) + * @author Sebastian Prehn - Initial contribution (v1.x hub communication) + * + */ +public class NeoHubHandler extends BaseBridgeHandler { + + private final Logger logger = LoggerFactory.getLogger(NeoHubHandler.class); + + private NeoHubConfiguration config; + private NeoHubSocket socket; + + private NeoHubInfoResponse lastInfoResponse = null; + + private ScheduledFuture lazyPollingScheduler; + private ScheduledFuture fastPollingScheduler; + + private final AtomicInteger fastPollingCallsToGo = new AtomicInteger(); + + public NeoHubHandler(Bridge bridge) { + super(bridge); + } + + @Override + public void handleCommand(ChannelUID channelUID, Command command) { + // future: currently there is nothing to do for a NeoHub + } + + @Override + public void initialize() { + config = getConfigAs(NeoHubConfiguration.class); + + if (config == null) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, + "parameter(s) hostName, portNumber, pollingInterval must be set!"); + return; + } + + if (logger.isDebugEnabled()) + logger.debug("hostname={}", config.hostName); + + if (config.hostName.isEmpty()) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "parameter hostName must be set!"); + return; + } + + if (logger.isDebugEnabled()) + logger.debug("port={}", config.portNumber); + + if (config.portNumber <= 0 || config.portNumber > 0xFFFF) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "portNumber is invalid!"); + return; + } + + if (logger.isDebugEnabled()) + logger.debug("polling interval={}", config.pollingInterval); + + if (config.pollingInterval < FAST_POLL_INTERVAL || config.pollingInterval > LAZY_POLL_INTERVAL) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, String + .format("pollingInterval must be in range [%d..%d]!", FAST_POLL_INTERVAL, LAZY_POLL_INTERVAL)); + return; + } + + socket = new NeoHubSocket(config.hostName, config.portNumber); + + if (logger.isDebugEnabled()) + logger.debug("start background polling.."); + + // create a "lazy" polling scheduler + if (lazyPollingScheduler == null || lazyPollingScheduler.isCancelled()) { + lazyPollingScheduler = scheduler.scheduleWithFixedDelay(this::lazyPollingSchedulerExecute, + config.pollingInterval, config.pollingInterval, TimeUnit.SECONDS); + } + + // create a "fast" polling scheduler + fastPollingCallsToGo.set(FAST_POLL_CYCLES); + if (fastPollingScheduler == null || fastPollingScheduler.isCancelled()) { + fastPollingScheduler = scheduler.scheduleWithFixedDelay(this::fastPollingSchedulerExecute, + FAST_POLL_INTERVAL, FAST_POLL_INTERVAL, TimeUnit.SECONDS); + } + + updateStatus(ThingStatus.UNKNOWN); + + // start a fast polling burst to ensure the NeHub is initialized quickly + startFastPollingBurst(); + } + + @Override + public void dispose() { + if (logger.isDebugEnabled()) + logger.debug("stop background polling.."); + + // clean up the lazy polling scheduler + if (lazyPollingScheduler != null && !lazyPollingScheduler.isCancelled()) { + lazyPollingScheduler.cancel(true); + lazyPollingScheduler = null; + } + + // clean up the fast polling scheduler + if (fastPollingScheduler != null && !fastPollingScheduler.isCancelled()) { + fastPollingScheduler.cancel(true); + fastPollingScheduler = null; + } + } + + /* + * device handlers call this to initiate a burst of fast polling requests ( + * improves response time to users when openHAB changes a channel value ) + */ + public void startFastPollingBurst() { + fastPollingCallsToGo.set(FAST_POLL_CYCLES); + } + + /* + * device handlers call this method to issue commands to the NeoHub + */ + public synchronized NeoHubReturnResult toNeoHubSendChannelValue(String commandStr) { + if (socket == null || config == null) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.NONE); + return NeoHubReturnResult.ERR_INITIALIZATION; + } + + try { + socket.sendMessage(commandStr); + + // start a fast polling burst (to confirm the status change) + startFastPollingBurst(); + + return NeoHubReturnResult.SUCCEEDED; + } catch (Exception e) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR); + logger.warn("set value error \"{}\"", e.getMessage()); + return NeoHubReturnResult.ERR_COMMUNICATION; + } + } + + /** + * sends a JSON "INFO" request to the NeoHub + * + * @return a class that contains the full status of all devices + * + */ + protected NeoHubInfoResponse fromNeoHubFetchPollingResponse() { + if (socket == null || config == null) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.NONE); + return null; + } + + try { + @Nullable + String response = socket.sendMessage(CMD_CODE_INFO); + + lastInfoResponse = NeoHubInfoResponse.createInfoResponse(response); + + if (getThing().getStatus() != ThingStatus.ONLINE) { + updateStatus(ThingStatus.ONLINE, ThingStatusDetail.NONE); + } + + return lastInfoResponse; + } catch (Exception e) { + logger.warn("set value error \"{}\"", e.getMessage()); + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR); + return null; + } + } + + /* + * this is the callback used by the lazy polling scheduler.. fetches the info + * for all devices from the NeoHub, and passes the results the respective device + * handlers + */ + private synchronized void lazyPollingSchedulerExecute() { + fromNeoHubFetchPollingResponse(); + + if (lastInfoResponse != null) { + List children = getThing().getThings(); + + // dispatch the infoResponse to each of the hub's owned devices .. + for (Thing child : children) { + ThingHandler device = child.getHandler(); + if (device instanceof NeoBaseHandler) { + ((NeoBaseHandler) device).toBaseSendPollResponse(this, lastInfoResponse); + } + } + } + + if (fastPollingCallsToGo.get() > 0) { + fastPollingCallsToGo.decrementAndGet(); + } + } + + /* + * this is the callback used by the fast polling scheduler.. checks if a fast + * polling burst is scheduled, and if so calls lazyPollingSchedulerExecute + */ + private void fastPollingSchedulerExecute() { + if (fastPollingCallsToGo.get() > 0) { + lazyPollingSchedulerExecute(); + } + } + + /** + * determine if the particular device name is configured in the hub + * + * @return the device is fully configured + * + */ + public boolean isConfigured(String deviceName) { + if (lastInfoResponse == null) { + fromNeoHubFetchPollingResponse(); + } + return lastInfoResponse != null && lastInfoResponse.getDeviceInfo(deviceName) != null; + } + + /** + * determine if the particular device name is communicating with the hub + * + * @return the device is fully configured and talking to the hub + * + */ + public boolean isOnline(String deviceName) { + DeviceInfo deviceInfo; + return isConfigured(deviceName) && (deviceInfo = lastInfoResponse.getDeviceInfo(deviceName)) != null + && !deviceInfo.isOffline(); + } +} diff --git a/bundles/org.openhab.binding.neohub/src/main/java/org/openhab/binding/neohub/internal/NeoHubHandlerFactory.java b/bundles/org.openhab.binding.neohub/src/main/java/org/openhab/binding/neohub/internal/NeoHubHandlerFactory.java new file mode 100644 index 0000000000000..0fe8418c884e6 --- /dev/null +++ b/bundles/org.openhab.binding.neohub/src/main/java/org/openhab/binding/neohub/internal/NeoHubHandlerFactory.java @@ -0,0 +1,123 @@ +/** + * Copyright (c) 2010-2019 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.neohub.internal; + +import static org.openhab.binding.neohub.internal.NeoHubBindingConstants.*; + +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Hashtable; +import java.util.Map; +import java.util.Set; + +import org.eclipse.jdt.annotation.Nullable; +import org.eclipse.smarthome.config.discovery.DiscoveryService; +import org.eclipse.smarthome.core.thing.Bridge; +import org.eclipse.smarthome.core.thing.Thing; +import org.eclipse.smarthome.core.thing.ThingTypeUID; +import org.eclipse.smarthome.core.thing.ThingUID; +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.osgi.framework.ServiceRegistration; +import org.osgi.service.component.annotations.Component; + +/** + * The {@link NeoHubHandlerFactory} creates things and thing handlers + * + * @author Andrew Fiddian-Green - Initial contribution + */ +@Component(configurationPid = "binding.neohub", service = ThingHandlerFactory.class) +public class NeoHubHandlerFactory extends BaseThingHandlerFactory { + + private static final Set SUPPORTED_THING_TYPES_UIDS = Collections + .unmodifiableSet(new HashSet<>(Arrays.asList(THING_TYPE_NEOHUB, THING_TYPE_NEOSTAT, THING_TYPE_NEOPLUG))); + + private final Map> discoServices = new HashMap<>(); + + @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 ((thingTypeUID.equals(THING_TYPE_NEOHUB)) && (thing instanceof Bridge)) { + NeoHubHandler handler = new NeoHubHandler((Bridge) thing); + createDiscoveryService(handler); + return handler; + } + + if (thingTypeUID.equals(THING_TYPE_NEOSTAT)) + return new NeoStatHandler(thing); + + if (thingTypeUID.equals(THING_TYPE_NEOPLUG)) + return new NeoPlugHandler(thing); + + return null; + } + + @Override + protected synchronized void removeHandler(ThingHandler handler) { + if (handler instanceof NeoHubHandler) { + destroyDiscoveryService((NeoHubHandler) handler); + } + } + + /* + * create a discovery service so that a newly created hub will find the + * respective things tht are inside it + */ + private synchronized void createDiscoveryService(NeoHubHandler handler) { + // create a new discovery service + NeoHubDiscoveryService ds = new NeoHubDiscoveryService(handler); + + // activate the discovery service + ds.activate(); + + // register the discovery service + ServiceRegistration serviceReg = bundleContext.registerService(DiscoveryService.class.getName(), ds, + new Hashtable()); + + /* + * store service registration in a list so we can destroy it when the respective + * hub is destroyed + */ + discoServices.put(handler.getThing().getUID(), serviceReg); + } + + /* + * destroy the discovery service + */ + private synchronized void destroyDiscoveryService(NeoHubHandler handler) { + // fetch the respective thing's service registration from our list + ServiceRegistration serviceReg = discoServices.remove(handler.getThing().getUID()); + + if (serviceReg != null) { + // retrieve the respective discovery service + NeoHubDiscoveryService disco = (NeoHubDiscoveryService) bundleContext.getService(serviceReg.getReference()); + + // and unregister the service + serviceReg.unregister(); + + // deactivate the service + if (disco != null) + disco.deactivate(); + } + } + +} diff --git a/bundles/org.openhab.binding.neohub/src/main/java/org/openhab/binding/neohub/internal/NeoHubInfoResponse.java b/bundles/org.openhab.binding.neohub/src/main/java/org/openhab/binding/neohub/internal/NeoHubInfoResponse.java new file mode 100644 index 0000000000000..9a268b10d1d70 --- /dev/null +++ b/bundles/org.openhab.binding.neohub/src/main/java/org/openhab/binding/neohub/internal/NeoHubInfoResponse.java @@ -0,0 +1,192 @@ +/** + * Copyright (c) 2010-2019 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.neohub.internal; + +import java.math.BigDecimal; +import java.util.List; + +import org.eclipse.jdt.annotation.Nullable; + +import com.google.gson.Gson; +import com.google.gson.JsonSyntaxException; +import com.google.gson.annotations.SerializedName; + +/** + * A wrapper around the JSON response to the JSON INFO request + * + * @author Sebastian Prehn - Initial contribution + * @author Andrew Fiddian-Green - Refactoring for openHAB v2.x + * + */ +class NeoHubInfoResponse { + + private static final Gson GSON = new Gson(); + + @SerializedName("devices") + private List deviceInfos; + + static class StatMode { + @SerializedName("MANUAL_OFF") + private Boolean manualOff; + @SerializedName("MANUAL_ON") + private Boolean manualOn; + + private Boolean stateManualOn() { + return (manualOn != null && manualOn); + } + + private Boolean stateManualOff() { + return (manualOff != null && manualOff); + } + } + + static class DeviceInfo { + + @SerializedName("device") + private String deviceName; + @SerializedName("CURRENT_SET_TEMPERATURE") + private BigDecimal currentSetTemperature; + @SerializedName("CURRENT_TEMPERATURE") + private BigDecimal currentTemperature; + @SerializedName("CURRENT_FLOOR_TEMPERATURE") + private BigDecimal currentFloorTemperature; + @SerializedName("AWAY") + private Boolean away; + @SerializedName("HOLIDAY") + private Boolean holiday; + @SerializedName("HOLIDAY_DAYS") + private BigDecimal holidayDays; + @SerializedName("STANDBY") + private Boolean standby; + @SerializedName("HEATING") + private Boolean heating; + @SerializedName("PREHEAT") + private Boolean preHeat; + @SerializedName("TIMER") + private Boolean timerOn; + @SerializedName("DEVICE_TYPE") + private BigDecimal deviceType; + @SerializedName("OFFLINE") + private Boolean offline; + @SerializedName("STAT_MODE") + private StatMode statMode = new StatMode(); + + protected Boolean safeBoolean(Boolean value) { + return (value != null && value); + } + + protected BigDecimal safeBigDecimal(BigDecimal value) { + return value != null ? value : BigDecimal.ZERO; + } + + public String getDeviceName() { + return deviceName != null ? deviceName : ""; + } + + public BigDecimal getTargetTemperature() { + return safeBigDecimal(currentSetTemperature); + } + + public BigDecimal getRoomTemperature() { + return safeBigDecimal(currentTemperature); + } + + public BigDecimal getFloorTemperature() { + return safeBigDecimal(currentFloorTemperature); + } + + public Boolean isAway() { + return safeBoolean(away); + } + + public Boolean isHoliday() { + return safeBoolean(holiday); + } + + public BigDecimal getHolidayDays() { + return safeBigDecimal(holidayDays); + } + + public BigDecimal getDeviceType() { + return safeBigDecimal(deviceType); + } + + public Boolean isStandby() { + return safeBoolean(standby); + } + + public Boolean isHeating() { + return safeBoolean(heating); + } + + public Boolean isPreHeating() { + return safeBoolean(preHeat); + } + + public Boolean isTimerOn() { + return safeBoolean(timerOn); + } + + public Boolean isOffline() { + return safeBoolean(offline); + } + + public Boolean stateManual() { + return (statMode != null && statMode.stateManualOn()); + } + + public Boolean stateAuto() { + return (statMode != null && statMode.stateManualOff()); + } + + public Boolean hasStatMode() { + return statMode != null; + } + + } + + /** + * Create wrapper around the JSON response + * + * @param response the JSON INFO request + * @return a NeoHubInfoResponse wrapper around the JSON response + * @throws JsonSyntaxException + * + */ + static @Nullable NeoHubInfoResponse createInfoResponse(String response) throws JsonSyntaxException { + return GSON.fromJson(response, NeoHubInfoResponse.class); + } + + /* + * returns the DeviceInfo corresponding to a given device name + * + * @param deviceName the device name + * + * @return its respective DeviceInfo + */ + public DeviceInfo getDeviceInfo(String deviceName) { + for (DeviceInfo d : deviceInfos) { + if (deviceName.equals(d.getDeviceName())) { + return d; + } + } + return null; + } + + /* + * @return the full list of DeviceInfo objects + */ + public List getDevices() { + return deviceInfos; + } +} diff --git a/bundles/org.openhab.binding.neohub/src/main/java/org/openhab/binding/neohub/internal/NeoHubSocket.java b/bundles/org.openhab.binding.neohub/src/main/java/org/openhab/binding/neohub/internal/NeoHubSocket.java new file mode 100644 index 0000000000000..d432fcea2ad0a --- /dev/null +++ b/bundles/org.openhab.binding.neohub/src/main/java/org/openhab/binding/neohub/internal/NeoHubSocket.java @@ -0,0 +1,107 @@ +/** + * Copyright (c) 2010-2019 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.neohub.internal; + +import static org.openhab.binding.neohub.internal.NeoHubBindingConstants.*; + +import static java.nio.charset.StandardCharsets.US_ASCII; + +import java.io.IOException; +import java.io.InputStreamReader; +import java.io.OutputStreamWriter; + +import java.net.InetSocketAddress; +import java.net.Socket; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * NeoHubConnector handles the ASCII based communication via TCP between openHAB + * and NeoHub + * + * @author Sebastian Prehn - Initial contribution + * @author Andrew Fiddian-Green - Refactoring for openHAB v2.x + * + */ +public class NeoHubSocket { + + private final Logger logger = LoggerFactory.getLogger(NeoHubSocket.class); + + /** + * Name of host or IP to connect to. + */ + private final String hostname; + + /** + * The port to connect to + */ + private final int port; + + public NeoHubSocket(final String hostname, final int portNumber) { + this.hostname = hostname; + this.port = portNumber; + } + + /** + * sends the message over the network to the NeoHub and returns its response + * + * @param request the message to be sent to the NeoHub + * @return response received from NeoHub + * @throws IOException, RuntimeException + * + */ + public synchronized String sendMessage(final String request) throws IOException, NeoHubException { + try (Socket socket = new Socket()) { + socket.connect(new InetSocketAddress(hostname, port), TCP_SOCKET_IMEOUT * 1000); + + try (InputStreamReader reader = new InputStreamReader(socket.getInputStream(), US_ASCII); + OutputStreamWriter writer = new OutputStreamWriter(socket.getOutputStream(), US_ASCII);) { + if (logger.isDebugEnabled()) { + logger.debug("sending {} characters..", request.length()); + logger.debug(">> {}", request); + } + + writer.write(request); + writer.write(0); // NULL terminate the command string + writer.flush(); + + StringBuilder builder = new StringBuilder(); + int inChar; + while ((inChar = reader.read()) > 0) { // NULL termination & end of stream (-1) + builder.append((char) inChar); + } + + String response = builder.toString(); + + if (logger.isTraceEnabled()) { + logger.trace("received {} characters..", response.length()); + logger.trace("<< {}", response); + } else + + if (logger.isDebugEnabled()) { + logger.debug("received {} characters (set log level to TRACE to see full string)..", + response.length()); + logger.debug("<< {} ...", response.substring(1, Math.min(response.length(), 30))); + } + + if (response.isEmpty()) { + throw new NeoHubException("empty response string"); + } + + return response; + } + } + } + +} diff --git a/bundles/org.openhab.binding.neohub/src/main/java/org/openhab/binding/neohub/internal/NeoPlugHandler.java b/bundles/org.openhab.binding.neohub/src/main/java/org/openhab/binding/neohub/internal/NeoPlugHandler.java new file mode 100644 index 0000000000000..0bff3f0468f26 --- /dev/null +++ b/bundles/org.openhab.binding.neohub/src/main/java/org/openhab/binding/neohub/internal/NeoPlugHandler.java @@ -0,0 +1,64 @@ +/** + * Copyright (c) 2010-2019 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.neohub.internal; + +import static org.openhab.binding.neohub.internal.NeoHubBindingConstants.*; + +import org.eclipse.smarthome.core.library.types.OnOffType; +import org.eclipse.smarthome.core.thing.Thing; +import org.eclipse.smarthome.core.types.Command; + +/** + * The {@link NeoPlugHandler} is the OpenHAB Handler for NeoPlug devices Note: + * inherits almost all the functionality of a {@link NeoBaseHandler} + * + * @author Andrew Fiddian-Green - Initial contribution + * + */ +public class NeoPlugHandler extends NeoBaseHandler { + + public NeoPlugHandler(Thing thing) { + super(thing); + } + + // =========== methods of NeoBaseHandler that are overridden ================ + + @Override + protected String toNeoHubBuildCommandString(String channelId, Command command) { + if (command instanceof OnOffType && channelId.equals(CHAN_PLUG_OUTPUT_STATE)) { + return String.format(CMD_CODE_TIMER, ((OnOffType) command).toString(), config.deviceNameInHub); + } else + + if (command instanceof OnOffType && channelId.equals(CHAN_PLUG_AUTO_MODE)) { + return String.format(CMD_CODE_MANUAL, invert((OnOffType) command).toString(), config.deviceNameInHub); + } + return ""; + } + + @Override + protected void toNeoHubSendCommandSet(String channelId, Command command) { + // if this is a manual command, switch to manual mode first.. + if (channelId.equals(CHAN_PLUG_OUTPUT_STATE) && command instanceof OnOffType) { + toNeoHubSendCommand(CHAN_PLUG_AUTO_MODE, OnOffType.from(false)); + } + // send the actual command to the hub + toNeoHubSendCommand(channelId, command); + } + + @Override + protected void toOpenHabSendChannelValues(NeoHubInfoResponse.DeviceInfo deviceInfo) { + toOpenHabSendValueDebounced(CHAN_PLUG_AUTO_MODE, OnOffType.from(!deviceInfo.stateManual())); + + toOpenHabSendValueDebounced(CHAN_PLUG_OUTPUT_STATE, OnOffType.from(deviceInfo.isTimerOn())); + } +} diff --git a/bundles/org.openhab.binding.neohub/src/main/java/org/openhab/binding/neohub/internal/NeoStatHandler.java b/bundles/org.openhab.binding.neohub/src/main/java/org/openhab/binding/neohub/internal/NeoStatHandler.java new file mode 100644 index 0000000000000..e8da339215632 --- /dev/null +++ b/bundles/org.openhab.binding.neohub/src/main/java/org/openhab/binding/neohub/internal/NeoStatHandler.java @@ -0,0 +1,71 @@ +/** + * Copyright (c) 2010-2019 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.neohub.internal; + +import static org.openhab.binding.neohub.internal.NeoHubBindingConstants.*; + +import javax.measure.quantity.Temperature; + +import org.eclipse.smarthome.core.library.types.OnOffType; +import org.eclipse.smarthome.core.library.types.QuantityType; +import org.eclipse.smarthome.core.library.types.StringType; +import org.eclipse.smarthome.core.library.unit.SIUnits; + +import org.eclipse.smarthome.core.thing.Thing; +import org.eclipse.smarthome.core.types.Command; + +/** + * The {@link NeoStatHandler} is the openHAB Handler for NeoStat devices Note: + * inherits almost all the functionality of a {@link NeoBaseHandler} + * + * @author Andrew Fiddian-Green - Initial contribution + * + */ +public class NeoStatHandler extends NeoBaseHandler { + + public NeoStatHandler(Thing thing) { + super(thing); + } + + @Override + protected String toNeoHubBuildCommandString(String channelId, Command command) { + if (command instanceof QuantityType && channelId.equals(CHAN_TARGET_TEMP)) { + return String.format(CMD_CODE_TEMP, ((QuantityType) command).toBigDecimal().toString(), + config.deviceNameInHub); + } else + + if (command instanceof OnOffType && channelId.equals(CHAN_OCC_MODE_PRESENT)) { + return String.format(CMD_CODE_AWAY, invert((OnOffType) command).toString(), config.deviceNameInHub); + } + return ""; + } + + @Override + protected void toOpenHabSendChannelValues(NeoHubInfoResponse.DeviceInfo deviceInfo) { + toOpenHabSendValueDebounced(CHAN_TARGET_TEMP, + new QuantityType(deviceInfo.getTargetTemperature(), SIUnits.CELSIUS)); + + toOpenHabSendValueDebounced(CHAN_ROOM_TEMP, + new QuantityType(deviceInfo.getRoomTemperature(), SIUnits.CELSIUS)); + + toOpenHabSendValueDebounced(CHAN_FLOOR_TEMP, + new QuantityType(deviceInfo.getFloorTemperature(), SIUnits.CELSIUS)); + + toOpenHabSendValueDebounced(CHAN_OCC_MODE_PRESENT, OnOffType.from(!deviceInfo.isStandby())); + + toOpenHabSendValueDebounced(CHAN_STAT_OUTPUT_STATE, + (deviceInfo.isHeating() || deviceInfo.isPreHeating() ? new StringType(VAL_HEATING) + : new StringType(VAL_OFF))); + } + +} diff --git a/bundles/org.openhab.binding.neohub/src/main/resources/ESH-INF/binding/binding.xml b/bundles/org.openhab.binding.neohub/src/main/resources/ESH-INF/binding/binding.xml new file mode 100644 index 0000000000000..db04f439fa05e --- /dev/null +++ b/bundles/org.openhab.binding.neohub/src/main/resources/ESH-INF/binding/binding.xml @@ -0,0 +1,11 @@ + + + + NeoHub Binding + This is the binding for Heatmiser NeoHub devices + Andrew Fiddian-Green + + diff --git a/bundles/org.openhab.binding.neohub/src/main/resources/ESH-INF/thing/thing-types.xml b/bundles/org.openhab.binding.neohub/src/main/resources/ESH-INF/thing/thing-types.xml new file mode 100644 index 0000000000000..5127dbc087940 --- /dev/null +++ b/bundles/org.openhab.binding.neohub/src/main/resources/ESH-INF/thing/thing-types.xml @@ -0,0 +1,180 @@ + + + + + + + Heatmiser NeoHub bridge to NeoStat and NeoPlug devices + + + Heatmiser + NeoHub + + + + + + + network-address + Host name (IP address) of the NeoHub + + + + + Port number of the NeoHub + 4242 + + + + + Time (seconds) between polling the NeoHub (min=4, max/default=60) + 60 + + + + + + + + + + + + + + Heatmiser Neostat Smart Thermostat + + + + + Actual room temperature + + + + + Target temperature setting for the room + + + + + Actual floor temperature + + + + + Status of whether the thermostat is Off, or calling for Heat + + + + + The Thermostat is in the Present Occupancy Mode (Off=Absent, On=Present) + + + + + + Heatmiser + NeoStat + + deviceNameInHub + + + + + Device Name that identifies the NeoStat device in the NeoHub and Heatmiser App + + + + + + + + + + + + + Heatmiser NeoPlug Smart Plug + + + + + The output state of the Plug switch (Off, On) + + + + + The Plug is in Automatic Mode (Off=Manual, On=Automatic) + + + + + Heatmiser + NeoPlug + + deviceNameInHub + + + + + Device Name that identifies the NeoPlugt device in the NeoHub and Heatmiser App + + + + + + + Number:Temperature + + Measured temperature value (Read-Only) + temperature + + + + + Number:Temperature + + Target temperature setting + temperature + + + + + String + + Status of whether the thermostat is Off, or calling for Heat + fire + + + + + Switch + + The Thermostat is in the Present Occupancy Mode (Off=Absent, On=Present) + presence + + + + + Switch + + The Plug is in Automatic Mode (Off=Manual, On=Automatic) + + + + + Switch + + The state of the Plug switch, Off or On + + + + diff --git a/bundles/org.openhab.binding.neohub/src/test/java/org/openhab/binding/neohub/test/NeoHubTestData.java b/bundles/org.openhab.binding.neohub/src/test/java/org/openhab/binding/neohub/test/NeoHubTestData.java new file mode 100644 index 0000000000000..f6fc4849a502f --- /dev/null +++ b/bundles/org.openhab.binding.neohub/src/test/java/org/openhab/binding/neohub/test/NeoHubTestData.java @@ -0,0 +1,39 @@ +package org.openhab.binding.neohub.test; +/** + * Copyright (c) 2010-2019 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 + */ + + +/** + * The {@link NeoHubTestData} class defines common constants, which are + * used across the whole binding. + * + * @author Andrew Fiddian-Green - Initial contribution + */ +public class NeoHubTestData { + + public static final String NEOHUB_JSON_TEST_STRING = + "{\"devices\":[" + + + "{\"AWAY\":false,\"COOLING\":false,\"COOLING_ENABLED\":false,\"COOLING_TEMPERATURE_IN_WHOLE_DEGREES\":23,\"COOL_INP\":false,\"COUNT_DOWN_TIME\":\"0:00\",\"CRADLE_PAIRED_TO_REMOTE_SENSOR\":false,\"CRADLE_PAIRED_TO_STAT\":false,\"CURRENT_FLOOR_TEMPERATURE\":23,\"CURRENT_SET_TEMPERATURE\":\"22.0\",\"CURRENT_TEMPERATURE\":\"22.2\",\"DEMAND\":false,\"DEVICE_TYPE\":12,\"ENABLE_BOILER\":false,\"ENABLE_COOLING\":false,\"ENABLE_PUMP\":false,\"ENABLE_VALVE\":false,\"ENABLE_ZONE\":false,\"FAILSAFE_STATE\":false,\"FAIL_SAFE_ENABLED\":false,\"FLOOR_LIMIT\":false,\"FULL/PARTIAL_LOCK_AVAILABLE\":false,\"HEAT/COOL_MODE\":false,\"HEATING\":false,\"HOLD_TEMPERATURE\":21,\"HOLD_TIME\":\"0:00\",\"HOLIDAY\":false,\"HOLIDAY_DAYS\":0,\"HUMIDITY\":0,\"LOCK\":false,\"LOCK_PIN_NUMBER\":\"0000\",\"LOW_BATTERY\":false,\"MAX_TEMPERATURE\":\"22.0\",\"MIN_TEMPERATURE\":\"21.0\",\"MODULATION_LEVEL\":0,\"NEXT_ON_TIME\":\"6 days 23:00\",\"OFFLINE\":false,\"OUPUT_DELAY\":false,\"OUTPUT_DELAY\":0,\"PREHEAT\":false,\"PREHEAT_TIME\":\"2:42\",\"PROGRAM_MODE\":\"24HOURSFIXED\",\"PUMP_DELAY\":false,\"RADIATORS_OR_UNDERFLOOR\":false,\"SENSOR_SELECTION\":\"BUILT_IN_AND_FLOOR\",\"SET_COUNTDOWN_TIME\":0,\"STANDBY\":false,\"STAT_MODE\":{\"4_HEAT_LEVELS\":true,\"MANUAL_OFF\":true,\"THERMOSTAT\":true},\"TEMPERATURE_FORMAT\":false,\"TEMP_HOLD\":false,\"TIMECLOCK_MODE\":false,\"TIMER\":false,\"TIME_CLOCK_OVERIDE_BIT\":false,\"ULTRA_VERSION\":0,\"VERSION_NUMBER\":20,\"WRITE_COUNT\":0,\"ZONE_1PAIRED_TO_MULTILINK\":true,\"ZONE_1_OR_2\":false,\"ZONE_2_PAIRED_TO_MULTILINK\":false,\"device\":\"Dining Room\"}," + + "{\"AWAY\":false,\"COOLING\":false,\"COOLING_ENABLED\":false,\"COOLING_TEMPERATURE_IN_WHOLE_DEGREES\":23,\"COOL_INP\":false,\"COUNT_DOWN_TIME\":\"0:00\",\"CRADLE_PAIRED_TO_REMOTE_SENSOR\":false,\"CRADLE_PAIRED_TO_STAT\":false,\"CURRENT_FLOOR_TEMPERATURE\":127,\"CURRENT_SET_TEMPERATURE\":\"22.0\",\"CURRENT_TEMPERATURE\":\"22.6\",\"DEMAND\":false,\"DEVICE_TYPE\":12,\"ENABLE_BOILER\":false,\"ENABLE_COOLING\":false,\"ENABLE_PUMP\":false,\"ENABLE_VALVE\":false,\"ENABLE_ZONE\":false,\"FAILSAFE_STATE\":false,\"FAIL_SAFE_ENABLED\":false,\"FLOOR_LIMIT\":false,\"FULL/PARTIAL_LOCK_AVAILABLE\":false,\"HEAT/COOL_MODE\":false,\"HEATING\":false,\"HOLD_TEMPERATURE\":23,\"HOLD_TIME\":\"0:00\",\"HOLIDAY\":false,\"HOLIDAY_DAYS\":0,\"HUMIDITY\":0,\"LOCK\":false,\"LOCK_PIN_NUMBER\":\"0000\",\"LOW_BATTERY\":false,\"MAX_TEMPERATURE\":\"24.0\",\"MIN_TEMPERATURE\":\"23.0\",\"MODULATION_LEVEL\":0,\"NEXT_ON_TIME\":\"6 days 23:00\",\"OFFLINE\":false,\"OUPUT_DELAY\":false,\"OUTPUT_DELAY\":0,\"PREHEAT\":false,\"PREHEAT_TIME\":\"255:255\",\"PROGRAM_MODE\":\"24HOURSFIXED\",\"PUMP_DELAY\":false,\"RADIATORS_OR_UNDERFLOOR\":false,\"SENSOR_SELECTION\":\"REMOTE_AIR_SENSOR\",\"SET_COUNTDOWN_TIME\":0,\"STANDBY\":false,\"STAT_MODE\":{\"4_HEAT_LEVELS\":true,\"MANUAL_OFF\":true,\"THERMOSTAT\":true},\"TEMPERATURE_FORMAT\":false,\"TEMP_HOLD\":false,\"TIMECLOCK_MODE\":false,\"TIMER\":false,\"TIME_CLOCK_OVERIDE_BIT\":false,\"ULTRA_VERSION\":0,\"VERSION_NUMBER\":20,\"WRITE_COUNT\":4,\"ZONE_1PAIRED_TO_MULTILINK\":true,\"ZONE_1_OR_2\":false,\"ZONE_2_PAIRED_TO_MULTILINK\":false,\"device\":\"Shower Room\"}," + + "{\"AWAY\":false,\"COOLING\":false,\"COOLING_ENABLED\":false,\"COOLING_TEMPERATURE_IN_WHOLE_DEGREES\":23,\"COOL_INP\":false,\"COUNT_DOWN_TIME\":\"0:00\",\"CRADLE_PAIRED_TO_REMOTE_SENSOR\":false,\"CRADLE_PAIRED_TO_STAT\":false,\"CURRENT_FLOOR_TEMPERATURE\":26,\"CURRENT_SET_TEMPERATURE\":\"22.0\",\"CURRENT_TEMPERATURE\":\"23.3\",\"DEMAND\":false,\"DEVICE_TYPE\":12,\"ENABLE_BOILER\":false,\"ENABLE_COOLING\":false,\"ENABLE_PUMP\":false,\"ENABLE_VALVE\":false,\"ENABLE_ZONE\":false,\"FAILSAFE_STATE\":false,\"FAIL_SAFE_ENABLED\":false,\"FLOOR_LIMIT\":false,\"FULL/PARTIAL_LOCK_AVAILABLE\":false,\"HEAT/COOL_MODE\":false,\"HEATING\":false,\"HOLD_TEMPERATURE\":16,\"HOLD_TIME\":\"0:00\",\"HOLIDAY\":false,\"HOLIDAY_DAYS\":0,\"HUMIDITY\":0,\"LOCK\":false,\"LOCK_PIN_NUMBER\":\"0000\",\"LOW_BATTERY\":false,\"MAX_TEMPERATURE\":\"25.0\",\"MIN_TEMPERATURE\":\"20.0\",\"MODULATION_LEVEL\":0,\"NEXT_ON_TIME\":\"6 days 23:00\",\"OFFLINE\":false,\"OUPUT_DELAY\":false,\"OUTPUT_DELAY\":0,\"PREHEAT\":false,\"PREHEAT_TIME\":\"3:48\",\"PROGRAM_MODE\":\"24HOURSFIXED\",\"PUMP_DELAY\":false,\"RADIATORS_OR_UNDERFLOOR\":false,\"SENSOR_SELECTION\":\"BUILT_IN_AND_FLOOR\",\"SET_COUNTDOWN_TIME\":0,\"STANDBY\":false,\"STAT_MODE\":{\"4_HEAT_LEVELS\":true,\"MANUAL_OFF\":true,\"THERMOSTAT\":true},\"TEMPERATURE_FORMAT\":false,\"TEMP_HOLD\":false,\"TIMECLOCK_MODE\":false,\"TIMER\":false,\"TIME_CLOCK_OVERIDE_BIT\":false,\"ULTRA_VERSION\":0,\"VERSION_NUMBER\":20,\"WRITE_COUNT\":0,\"ZONE_1PAIRED_TO_MULTILINK\":true,\"ZONE_1_OR_2\":false,\"ZONE_2_PAIRED_TO_MULTILINK\":false,\"device\":\"Conservatory\"}," + + "{\"AWAY\":false,\"COOLING\":false,\"COOLING_ENABLED\":false,\"COOLING_TEMPERATURE_IN_WHOLE_DEGREES\":23,\"COOL_INP\":false,\"COUNT_DOWN_TIME\":\"0:00\",\"CRADLE_PAIRED_TO_REMOTE_SENSOR\":false,\"CRADLE_PAIRED_TO_STAT\":false,\"CURRENT_FLOOR_TEMPERATURE\":28,\"CURRENT_SET_TEMPERATURE\":\"22.0\",\"CURRENT_TEMPERATURE\":\"22.6\",\"DEMAND\":false,\"DEVICE_TYPE\":12,\"ENABLE_BOILER\":false,\"ENABLE_COOLING\":false,\"ENABLE_PUMP\":false,\"ENABLE_VALVE\":false,\"ENABLE_ZONE\":false,\"FAILSAFE_STATE\":false,\"FAIL_SAFE_ENABLED\":false,\"FLOOR_LIMIT\":false,\"FULL/PARTIAL_LOCK_AVAILABLE\":false,\"HEAT/COOL_MODE\":false,\"HEATING\":false,\"HOLD_TEMPERATURE\":21,\"HOLD_TIME\":\"0:00\",\"HOLIDAY\":false,\"HOLIDAY_DAYS\":0,\"HUMIDITY\":0,\"LOCK\":false,\"LOCK_PIN_NUMBER\":\"0000\",\"LOW_BATTERY\":false,\"MAX_TEMPERATURE\":\"24.0\",\"MIN_TEMPERATURE\":\"19.0\",\"MODULATION_LEVEL\":0,\"NEXT_ON_TIME\":\"6 days 23:00\",\"OFFLINE\":false,\"OUPUT_DELAY\":false,\"OUTPUT_DELAY\":0,\"PREHEAT\":false,\"PREHEAT_TIME\":\"3:16\",\"PROGRAM_MODE\":\"24HOURSFIXED\",\"PUMP_DELAY\":false,\"RADIATORS_OR_UNDERFLOOR\":false,\"SENSOR_SELECTION\":\"BUILT_IN_AND_FLOOR\",\"SET_COUNTDOWN_TIME\":0,\"STANDBY\":false,\"STAT_MODE\":{\"4_HEAT_LEVELS\":true,\"MANUAL_OFF\":true,\"THERMOSTAT\":true},\"TEMPERATURE_FORMAT\":false,\"TEMP_HOLD\":false,\"TIMECLOCK_MODE\":false,\"TIMER\":false,\"TIME_CLOCK_OVERIDE_BIT\":false,\"ULTRA_VERSION\":0,\"VERSION_NUMBER\":20,\"WRITE_COUNT\":2,\"ZONE_1PAIRED_TO_MULTILINK\":true,\"ZONE_1_OR_2\":false,\"ZONE_2_PAIRED_TO_MULTILINK\":false,\"device\":\"Living Room\"}," + + "{\"AWAY\":false,\"COOLING\":false,\"COOLING_ENABLED\":false,\"COOLING_TEMPERATURE_IN_WHOLE_DEGREES\":23,\"COOL_INP\":false,\"COUNT_DOWN_TIME\":\"0:00\",\"CRADLE_PAIRED_TO_REMOTE_SENSOR\":false,\"CRADLE_PAIRED_TO_STAT\":false,\"CURRENT_FLOOR_TEMPERATURE\":23,\"CURRENT_SET_TEMPERATURE\":\"22.0\",\"CURRENT_TEMPERATURE\":\"23.6\",\"DEMAND\":false,\"DEVICE_TYPE\":12,\"ENABLE_BOILER\":false,\"ENABLE_COOLING\":false,\"ENABLE_PUMP\":false,\"ENABLE_VALVE\":false,\"ENABLE_ZONE\":false,\"FAILSAFE_STATE\":false,\"FAIL_SAFE_ENABLED\":false,\"FLOOR_LIMIT\":false,\"FULL/PARTIAL_LOCK_AVAILABLE\":false,\"HEAT/COOL_MODE\":false,\"HEATING\":false,\"HOLD_TEMPERATURE\":21,\"HOLD_TIME\":\"0:00\",\"HOLIDAY\":false,\"HOLIDAY_DAYS\":0,\"HUMIDITY\":0,\"LOCK\":false,\"LOCK_PIN_NUMBER\":\"0000\",\"LOW_BATTERY\":false,\"MAX_TEMPERATURE\":\"23.0\",\"MIN_TEMPERATURE\":\"20.0\",\"MODULATION_LEVEL\":0,\"NEXT_ON_TIME\":\"6 days 23:00\",\"OFFLINE\":false,\"OUPUT_DELAY\":false,\"OUTPUT_DELAY\":0,\"PREHEAT\":false,\"PREHEAT_TIME\":\"2:08\",\"PROGRAM_MODE\":\"24HOURSFIXED\",\"PUMP_DELAY\":false,\"RADIATORS_OR_UNDERFLOOR\":false,\"SENSOR_SELECTION\":\"BUILT_IN_AND_FLOOR\",\"SET_COUNTDOWN_TIME\":0,\"STANDBY\":false,\"STAT_MODE\":{\"4_HEAT_LEVELS\":true,\"MANUAL_OFF\":true,\"THERMOSTAT\":true},\"TEMPERATURE_FORMAT\":false,\"TEMP_HOLD\":false,\"TIMECLOCK_MODE\":false,\"TIMER\":false,\"TIME_CLOCK_OVERIDE_BIT\":false,\"ULTRA_VERSION\":0,\"VERSION_NUMBER\":20,\"WRITE_COUNT\":6,\"ZONE_1PAIRED_TO_MULTILINK\":true,\"ZONE_1_OR_2\":false,\"ZONE_2_PAIRED_TO_MULTILINK\":false,\"device\":\"Kitchen\"}," + + "{\"AWAY\":false,\"COOLING\":false,\"COOLING_ENABLED\":false,\"COOLING_TEMPERATURE_IN_WHOLE_DEGREES\":23,\"COOL_INP\":false,\"COUNT_DOWN_TIME\":\"0:00\",\"CRADLE_PAIRED_TO_REMOTE_SENSOR\":false,\"CRADLE_PAIRED_TO_STAT\":false,\"CURRENT_FLOOR_TEMPERATURE\":17,\"CURRENT_SET_TEMPERATURE\":\"20.0\",\"CURRENT_TEMPERATURE\":\"20.1\",\"DEMAND\":false,\"DEVICE_TYPE\":12,\"ENABLE_BOILER\":false,\"ENABLE_COOLING\":false,\"ENABLE_PUMP\":false,\"ENABLE_VALVE\":false,\"ENABLE_ZONE\":false,\"FAILSAFE_STATE\":false,\"FAIL_SAFE_ENABLED\":false,\"FLOOR_LIMIT\":false,\"FULL/PARTIAL_LOCK_AVAILABLE\":false,\"HEAT/COOL_MODE\":false,\"HEATING\":false,\"HOLD_TEMPERATURE\":21,\"HOLD_TIME\":\"0:00\",\"HOLIDAY\":false,\"HOLIDAY_DAYS\":0,\"HUMIDITY\":0,\"LOCK\":false,\"LOCK_PIN_NUMBER\":\"0000\",\"LOW_BATTERY\":false,\"MAX_TEMPERATURE\":\"21.0\",\"MIN_TEMPERATURE\":\"20.0\",\"MODULATION_LEVEL\":0,\"NEXT_ON_TIME\":\"7 days 00:00\",\"OFFLINE\":false,\"OUPUT_DELAY\":false,\"OUTPUT_DELAY\":0,\"PREHEAT\":false,\"PREHEAT_TIME\":\"255:255\",\"PROGRAM_MODE\":\"24HOURSFIXED\",\"PUMP_DELAY\":false,\"RADIATORS_OR_UNDERFLOOR\":false,\"SENSOR_SELECTION\":\"BUILT_IN_AND_FLOOR\",\"SET_COUNTDOWN_TIME\":0,\"STANDBY\":false,\"STAT_MODE\":{\"4_HEAT_LEVELS\":true,\"MANUAL_OFF\":true,\"THERMOSTAT\":true},\"TEMPERATURE_FORMAT\":false,\"TEMP_HOLD\":false,\"TIMECLOCK_MODE\":false,\"TIMER\":false,\"TIME_CLOCK_OVERIDE_BIT\":false,\"ULTRA_VERSION\":0,\"VERSION_NUMBER\":20,\"WRITE_COUNT\":0,\"ZONE_1PAIRED_TO_MULTILINK\":true,\"ZONE_1_OR_2\":false,\"ZONE_2_PAIRED_TO_MULTILINK\":false,\"device\":\"Hallway\"}," + + "{\"AWAY\":false,\"COOLING\":false,\"COOLING_ENABLED\":false,\"COOLING_TEMPERATURE_IN_WHOLE_DEGREES\":23,\"COOL_INP\":false,\"COUNT_DOWN_TIME\":\"0:00\",\"CRADLE_PAIRED_TO_REMOTE_SENSOR\":false,\"CRADLE_PAIRED_TO_STAT\":false,\"CURRENT_FLOOR_TEMPERATURE\":127,\"CURRENT_SET_TEMPERATURE\":\"10.0\",\"CURRENT_TEMPERATURE\":\"17.3\",\"DEMAND\":false,\"DEVICE_TYPE\":12,\"ENABLE_BOILER\":false,\"ENABLE_COOLING\":false,\"ENABLE_PUMP\":false,\"ENABLE_VALVE\":false,\"ENABLE_ZONE\":false,\"FAILSAFE_STATE\":false,\"FAIL_SAFE_ENABLED\":false,\"FLOOR_LIMIT\":false,\"FULL/PARTIAL_LOCK_AVAILABLE\":false,\"HEAT/COOL_MODE\":false,\"HEATING\":false,\"HOLD_TEMPERATURE\":10,\"HOLD_TIME\":\"0:00\",\"HOLIDAY\":false,\"HOLIDAY_DAYS\":0,\"HUMIDITY\":0,\"LOCK\":false,\"LOCK_PIN_NUMBER\":\"0000\",\"LOW_BATTERY\":false,\"MAX_TEMPERATURE\":\"19.0\",\"MIN_TEMPERATURE\":\"16.0\",\"MODULATION_LEVEL\":0,\"NEXT_ON_TIME\":\"7 days 08:00\",\"OFFLINE\":false,\"OUPUT_DELAY\":false,\"OUTPUT_DELAY\":5,\"PREHEAT\":false,\"PREHEAT_TIME\":\"255:255\",\"PROGRAM_MODE\":\"24HOURSFIXED\",\"PUMP_DELAY\":false,\"RADIATORS_OR_UNDERFLOOR\":false,\"SENSOR_SELECTION\":\"BUILT_IN_AIR_SENSOR\",\"SET_COUNTDOWN_TIME\":0,\"STANDBY\":false,\"STAT_MODE\":{\"4_HEAT_LEVELS\":true,\"MANUAL_OFF\":true,\"THERMOSTAT\":true},\"TEMPERATURE_FORMAT\":false,\"TEMP_HOLD\":false,\"TIMECLOCK_MODE\":false,\"TIMER\":false,\"TIME_CLOCK_OVERIDE_BIT\":false,\"ULTRA_VERSION\":0,\"VERSION_NUMBER\":20,\"WRITE_COUNT\":0,\"ZONE_1PAIRED_TO_MULTILINK\":true,\"ZONE_1_OR_2\":false,\"ZONE_2_PAIRED_TO_MULTILINK\":false,\"device\":\"Shed\"}," + + "{\"AWAY\":false,\"COOLING\":false,\"COOLING_ENABLED\":false,\"COOLING_TEMPERATURE_IN_WHOLE_DEGREES\":0,\"COOL_INP\":false,\"COUNT_DOWN_TIME\":\"0:00\",\"CRADLE_PAIRED_TO_REMOTE_SENSOR\":false,\"CRADLE_PAIRED_TO_STAT\":false,\"CURRENT_FLOOR_TEMPERATURE\":127,\"CURRENT_SET_TEMPERATURE\":\"0.0\",\"CURRENT_TEMPERATURE\":\"255.255\",\"DEMAND\":false,\"DEVICE_TYPE\":6,\"ENABLE_BOILER\":false,\"ENABLE_COOLING\":false,\"ENABLE_PUMP\":false,\"ENABLE_VALVE\":false,\"ENABLE_ZONE\":false,\"FAILSAFE_STATE\":false,\"FAIL_SAFE_ENABLED\":false,\"FLOOR_LIMIT\":false,\"FULL/PARTIAL_LOCK_AVAILABLE\":false,\"HEAT/COOL_MODE\":false,\"HEATING\":false,\"HOLD_TEMPERATURE\":1,\"HOLD_TIME\":\"0:00\",\"HOLIDAY\":false,\"HOLIDAY_DAYS\":0,\"HUMIDITY\":0,\"LOCK\":false,\"LOCK_PIN_NUMBER\":\"0000\",\"LOW_BATTERY\":false,\"MAX_TEMPERATURE\":\"255.255\",\"MIN_TEMPERATURE\":\"255.255\",\"MODULATION_LEVEL\":0,\"NEXT_ON_TIME\":\"255 days 255:255\",\"OFFLINE\":false,\"OUPUT_DELAY\":false,\"OUTPUT_DELAY\":0,\"PREHEAT\":false,\"PREHEAT_TIME\":\"255:255\",\"PROGRAM_MODE\":\"24HOURSFIXED\",\"PUMP_DELAY\":false,\"RADIATORS_OR_UNDERFLOOR\":false,\"SENSOR_SELECTION\":\"BUILT_IN_AIR_SENSOR\",\"SET_COUNTDOWN_TIME\":0,\"STANDBY\":false,\"STAT_MODE\":{\"4_HEAT_LEVELS\":true,\"MANUAL_ON\":true,\"TIMECLOCK\":true},\"TEMPERATURE_FORMAT\":false,\"TEMP_HOLD\":false,\"TIMECLOCK_MODE\":false,\"TIMER\":false,\"TIME_CLOCK_OVERIDE_BIT\":false,\"ULTRA_VERSION\":0,\"VERSION_NUMBER\":79,\"WRITE_COUNT\":11,\"ZONE_1PAIRED_TO_MULTILINK\":true,\"ZONE_1_OR_2\":false,\"ZONE_2_PAIRED_TO_MULTILINK\":false,\"device\":\"Plug South\"}," + + "{\"AWAY\":false,\"COOLING\":false,\"COOLING_ENABLED\":false,\"COOLING_TEMPERATURE_IN_WHOLE_DEGREES\":0,\"COOL_INP\":false,\"COUNT_DOWN_TIME\":\"0:00\",\"CRADLE_PAIRED_TO_REMOTE_SENSOR\":false,\"CRADLE_PAIRED_TO_STAT\":false,\"CURRENT_FLOOR_TEMPERATURE\":127,\"CURRENT_SET_TEMPERATURE\":\"0.0\",\"CURRENT_TEMPERATURE\":\"255.255\",\"DEMAND\":false,\"DEVICE_TYPE\":6,\"ENABLE_BOILER\":false,\"ENABLE_COOLING\":false,\"ENABLE_PUMP\":false,\"ENABLE_VALVE\":false,\"ENABLE_ZONE\":false,\"FAILSAFE_STATE\":false,\"FAIL_SAFE_ENABLED\":false,\"FLOOR_LIMIT\":false,\"FULL/PARTIAL_LOCK_AVAILABLE\":false,\"HEAT/COOL_MODE\":false,\"HEATING\":false,\"HOLD_TEMPERATURE\":0,\"HOLD_TIME\":\"0:00\",\"HOLIDAY\":false,\"HOLIDAY_DAYS\":0,\"HUMIDITY\":0,\"LOCK\":false,\"LOCK_PIN_NUMBER\":\"0000\",\"LOW_BATTERY\":false,\"MAX_TEMPERATURE\":\"255.255\",\"MIN_TEMPERATURE\":\"255.255\",\"MODULATION_LEVEL\":0,\"NEXT_ON_TIME\":\"255 days 255:255\",\"OFFLINE\":false,\"OUPUT_DELAY\":false,\"OUTPUT_DELAY\":0,\"PREHEAT\":false,\"PREHEAT_TIME\":\"255:255\",\"PROGRAM_MODE\":\"24HOURSFIXED\",\"PUMP_DELAY\":false,\"RADIATORS_OR_UNDERFLOOR\":false,\"SENSOR_SELECTION\":\"BUILT_IN_AIR_SENSOR\",\"SET_COUNTDOWN_TIME\":0,\"STANDBY\":false,\"STAT_MODE\":{\"4_HEAT_LEVELS\":true,\"MANUAL_ON\":true,\"TIMECLOCK\":true},\"TEMPERATURE_FORMAT\":false,\"TEMP_HOLD\":false,\"TIMECLOCK_MODE\":false,\"TIMER\":true,\"TIME_CLOCK_OVERIDE_BIT\":true,\"ULTRA_VERSION\":0,\"VERSION_NUMBER\":79,\"WRITE_COUNT\":7,\"ZONE_1PAIRED_TO_MULTILINK\":true,\"ZONE_1_OR_2\":false,\"ZONE_2_PAIRED_TO_MULTILINK\":false,\"device\":\"Plug North\"}," + + "{\"AWAY\":false,\"COOLING\":false,\"COOLING_ENABLED\":false,\"COOLING_TEMPERATURE_IN_WHOLE_DEGREES\":0,\"COOL_INP\":false,\"COUNT_DOWN_TIME\":\"0:00\",\"CRADLE_PAIRED_TO_REMOTE_SENSOR\":false,\"CRADLE_PAIRED_TO_STAT\":false,\"CURRENT_FLOOR_TEMPERATURE\":127,\"CURRENT_SET_TEMPERATURE\":\"0.0\",\"CURRENT_TEMPERATURE\":\"255.255\",\"DEMAND\":false,\"DEVICE_TYPE\":6,\"ENABLE_BOILER\":false,\"ENABLE_COOLING\":false,\"ENABLE_PUMP\":false,\"ENABLE_VALVE\":false,\"ENABLE_ZONE\":false,\"FAILSAFE_STATE\":false,\"FAIL_SAFE_ENABLED\":false,\"FLOOR_LIMIT\":false,\"FULL/PARTIAL_LOCK_AVAILABLE\":false,\"HEAT/COOL_MODE\":false,\"HEATING\":false,\"HOLD_TEMPERATURE\":1,\"HOLD_TIME\":\"0:00\",\"HOLIDAY\":false,\"HOLIDAY_DAYS\":0,\"HUMIDITY\":0,\"LOCK\":false,\"LOCK_PIN_NUMBER\":\"0000\",\"LOW_BATTERY\":false,\"MAX_TEMPERATURE\":\"255.255\",\"MIN_TEMPERATURE\":\"255.255\",\"MODULATION_LEVEL\":0,\"NEXT_ON_TIME\":\"255 days 255:255\",\"OFFLINE\":false,\"OUPUT_DELAY\":false,\"OUTPUT_DELAY\":0,\"PREHEAT\":false,\"PREHEAT_TIME\":\"255:255\",\"PROGRAM_MODE\":\"24HOURSFIXED\",\"PUMP_DELAY\":false,\"RADIATORS_OR_UNDERFLOOR\":false,\"SENSOR_SELECTION\":\"BUILT_IN_AIR_SENSOR\",\"SET_COUNTDOWN_TIME\":0,\"STANDBY\":false,\"STAT_MODE\":{\"4_HEAT_LEVELS\":true,\"MANUAL_OFF\":true,\"TIMECLOCK\":true},\"TEMPERATURE_FORMAT\":false,\"TEMP_HOLD\":false,\"TIMECLOCK_MODE\":false,\"TIMER\":false,\"TIME_CLOCK_OVERIDE_BIT\":false,\"ULTRA_VERSION\":0,\"VERSION_NUMBER\":79,\"WRITE_COUNT\":28,\"ZONE_1PAIRED_TO_MULTILINK\":true,\"ZONE_1_OR_2\":false,\"ZONE_2_PAIRED_TO_MULTILINK\":false,\"device\":\"Watering System\"}" + + + "]}"; +}