From 9caec47295a02c0ebbd00ea375023bead8873320 Mon Sep 17 00:00:00 2001 From: tobof Date: Thu, 22 Dec 2016 13:43:01 +0100 Subject: [PATCH] Move current development master (#42) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Skeleton creation * Merged old code into new plugin Code merged and adjustments made to discovery process. UNTESTED!! * Added missing files from last commit * Tested against current OH2 beta: working! * added baudrate configurable * Added script for testing purpose and missing pom.xml * Changed thing description for serial gateway to support different baud rates. * Fixed documentation regarding baud rate of the serial gateway * esh-inf added parameter and serial closing to allow binding restart correctly * removed debug log before initialize * thread managing improved * little fix allows correct start and stop for binding * Added missing STOP for item "Rollershutter" Moved event handling from handleUpdate() to handleCommand(). * Doc fixed * requestAck feature added Currently it is not possible to automatically revert to the old state. * writer thread implementation changed and serial port class changed to remove the annoying exception on reading * adding back the message to queue if is not time to sent it again * reader thread moved in abstract class as the writer * tryying to move to SerialPort, no success... * Minor fix in visibilty of overridden method * Code cleanup and fix for sendDelay - removed superflous debug messages - removed superflous variables, methods - fixed missing implementation of sendDelay * Error handling on disconnect of ethernet gw The binding status is updated to OFFLINE and the threads for reader and writer are beeing stopped. Works for ethernet gw only. * Added jar and fixed docs * requestAck incoming ACKs are now acknoledged and the message is removed from the queue * requestAck: revert item status if no ack is received If no ACK is received from the node, the binding will try to revert the status of the item in OH2 to its old value. Only works if a status is known. (After restart of OH2 no status is known). Tested with light (STATUS) and dimmer (percent). * ensure gateway was started correctly * event driven i_version handing * removed debug messages * disconnect on connection fail now child id 255 can respond to i_version * Added S_HVAC constant * HVAC Thermostat added Temperature channel implemented * Added channels to HVAC "hvac-flow-state-channel" "hvac-flow-mode-channel" "hvac-setPoint-heat-channel" "hvac-setPoint-cool-channel" "hvac-speed-channel" "var1 to var5-channel" * Added CHANNEL_HVAC_FLOW_MODE and CHANNEL_HVAC_FLOW_STATE to MySensorHandler * Added support for return messages to sensor on V_VAR1 to V_VAR5 Changed V_VAR2 to number. * requestAck feature redesigned. Reader/Writer are now in sync. * Removed debug messages. Added updated jar of the binding. * Requesting of thing/item values supported + minor fix OH2 binding now answers requests for item status. minor fix - battery ch. added to hvacThermostat * Added jar build * Delay before I_VERSION check for SerialGW, new jar file * Compatibility fixes - Implemented a method to register more than one serial port (two or more serial gw) due to a lack/bug in the used nrserialjava - Longer timeout for I_VERSION check (5s) * just a little restyle * I_TIME and I_CONFIG response implemented (not tested) * I_VERSION skip now allowed * Binding jar refresh * fix for I_TIME/I_CONFIG, not requesting that nodeId = 0 * updated testscript.sh * Code review - Added legal information to .java files - Removed references, files and comments that are not needed * Fixed value requests from nodes for sensors with multiple subtypes and added correct deregistration to prevent NPEs on shutdown. * Added WaterMeter Sensor * pom.xml refresh * New jar-Release after merge * Fixed issue with serial Reader Fixed issue with Exception while trying to read zero data from serial * New jar after merge with master * Added the customSensor, containing the channels for V_VARx - can be used as a helper to send values to the nodes from Openhab Signed-off-by: Juergen Messmer * Fixed mistake in documentation There was an error in the creation of the bridge. This results in Items not updating although the bridge itself seems fine in openhab2 * Fixed issue with high CPU usage * added possibility to not revert state if no ack received * Changed type of vars for "reverted" para from int to boolean * removed debug logging of messages in queues * Reduced sleep time in reader * Added Pressure channel to thing barometer Pressure channel was missing in barometer thing definition. * New jar * Packaged new jar! * datetime implemented in channel * last update fixes * last update fixes #2 * Fixed bug that sends status of things on startup OH2 core now calls "handleCommand()" on startup with command == REFRESH. REFRESH is ignored now and the bug fixed. * Removed constructor task, moved to initialize * Debug line added * minor bug fixing and little refactor * minor bug fixing #2 * minor bug fixing #3 * minor bug fixing #4 * minor bug fixing #5 * fixed log level for some logging * fixed log level for some logging * disconnection fixes * Legal header adjusted, Current jar attached Signed-off-by: Tim Oberföll (github: github_handle) * started working on network watchdog * Update MySensorsBindingConstants.java * Update MySensorsBindingConstants.java * working on network sanity check * Update constants to comply with OH2 Added following things: THING_TYPE_LOCK THING_TYPE_LEVEL THING_TYPE_RGB_LIGHT THING_TYPE_RGBW_LIGHT THING_TYPE_RGBW_LIGHT THING_TYPE_PH_METER * Update MySensorsBindingConstants.java * Update MySensorsBindingConstants.java * Update MySensorsBindingConstants.java * Updates to comply with My Sensors 2.0 * Update MySensorsDiscoveryService.java * Update MySensorsDiscoveryService.java * Update MySensorsBindingConstants.java * Update thing-types.xml * Update thing-types.xml * Update MySensorsBindingConstants.java * working on network sanity check #2 * Added missing constants, removed typos * resolved dependecies for bridge connection * Generated new jar * new feature and refactoring testing #1 * new feature and refactoring testing #1 * new feature and refactoring testing #2 * Added expert mode Expert mode allows rule based message parsing and sending. * [PARTIAL] start joining connector and connection * connection and connector merged * invalidating requestDisconnection flag * connection tested and debugged * [FIX] adding listener before connection * cache factory implemented, now managing only given_ids one * windows disconnect fix and cache minor fix * new packaging and structure initial restyle * some changes * moving to new version of ESM framework * bridgeHandelerInitialized (deprecated) has changed to bridgeStatusChanged * cache usage changed after refactoring * [FIX] java.util.ConcurrentModificationException * final fixing, now testing * utility message type moved into message class * build.xml file removed * new jar * [FIX] asList cause UnsupportedOperationException * [FIX] startup check not work on connection retry after disconnection * [FIX] initialize thing to offline, cause problem if left uninitialized * [WIP] V_TEXT added Thing added that supports sending and receiving V_TEXT. * [WIP] Added V_IR_SEND & V_IR_RECEIVE Both things work in both directions (to and from MySensors network) * fixing mvn strange behaviour * Created new jar && Modified Testscript * Fixed bug that prevents answering of internal messages If skipStartupCheck is set to true, there was no answer to I_CONFIG, I_TIME and ACK was not working. * [WiP] Refactor ThingHandler Cleaning up code for incoming messages. * Fixing issue in MySensorsBridgeConnection which detached the EventListener - EventListener now listening independant from skipStartupCheck - Removed from things eth-gateway and ser-gateway to allow configuration of advanced options in PaperUI * Fixed recent issue related to updates on Smarthome API fixes #40 --- .../org.openhab.binding.mysensors/.classpath | 4 +- .../org.openhab.binding.mysensors/.gitignore | 1 + .../.settings/org.eclipse.jdt.core.prefs | 7 + .../ESH-INF/thing/thing-types.xml | 169 ++++++- .../META-INF/MANIFEST.MF | 15 +- .../OSGI-INF/MySensorsHandlerFactory.xml | 5 +- .../mysensors/MySensorsBindingConstants.java | 22 +- .../config/MySensorsBridgeConfiguration.java | 2 +- .../DiscoveryThread.java | 25 +- .../discovery/MySensorsDiscoveryService.java | 21 +- .../handler/MySensorsBridgeHandler.java | 293 ------------ .../handler/MySensorsStatusUpdateEvent.java | 33 -- .../internal/MySensorsBridgeConnection.java | 164 ------- .../mysensors/internal/MySensorsUtility.java | 5 + .../internal/event/MySensorsEventType.java | 16 + .../event/MySensorsStatusUpdateEvent.java | 82 ++++ .../event}/MySensorsUpdateListener.java | 4 +- .../exception/NoMoreIdsException.java | 10 + .../factory/MySensorsCacheFactory.java | 108 +++++ .../MySensorsHandlerFactory.java | 21 +- .../handler/MySensorsBridgeHandler.java | 268 +++++++++++ .../handler/MySensorsThingHandler.java} | 251 +++++++---- .../protocol/MySensorsBridgeConnection.java | 417 ++++++++++++++++++ .../MySensorsNetworkSanityChecker.java | 122 +++++ .../protocol/MySensorsReader.java | 42 +- .../protocol/MySensorsWriter.java | 37 +- .../protocol/ip/MySensorIpReader.java | 4 +- .../protocol/ip/MySensorsIpConnection.java | 88 ++++ .../protocol/ip/MySensorsIpWriter.java | 4 +- .../message}/MySensorsMessage.java | 82 +++- .../message}/MySensorsMessageParser.java | 2 +- .../serial/MySensorsSerialConnection.java | 89 ++-- .../serial/MySensorsSerialReader.java | 4 +- .../serial/MySensorsSerialWriter.java | 4 +- .../internal/sensors/MySensorsChild.java | 42 ++ .../sensors/MySensorsDeviceManager.java | 196 ++++++++ .../internal/sensors/MySensorsNode.java | 46 ++ .../protocol/ip/MySensorsIpConnection.java | 80 ---- .../binding/mysensors/test/CacheTest.java | 31 ++ ...enhab.binding.mysensors-2.0.0-SNAPSHOT.jar | Bin 0 -> 76167 bytes .../test/iVersionMessage.sh | 5 + .../test/testscript.sh | 54 ++- 42 files changed, 2062 insertions(+), 813 deletions(-) create mode 100644 addons/binding/org.openhab.binding.mysensors/.gitignore create mode 100644 addons/binding/org.openhab.binding.mysensors/.settings/org.eclipse.jdt.core.prefs rename addons/binding/org.openhab.binding.mysensors/src/main/java/org/openhab/binding/mysensors/{service => discovery}/DiscoveryThread.java (54%) delete mode 100644 addons/binding/org.openhab.binding.mysensors/src/main/java/org/openhab/binding/mysensors/handler/MySensorsBridgeHandler.java delete mode 100644 addons/binding/org.openhab.binding.mysensors/src/main/java/org/openhab/binding/mysensors/handler/MySensorsStatusUpdateEvent.java delete mode 100644 addons/binding/org.openhab.binding.mysensors/src/main/java/org/openhab/binding/mysensors/internal/MySensorsBridgeConnection.java create mode 100644 addons/binding/org.openhab.binding.mysensors/src/main/java/org/openhab/binding/mysensors/internal/MySensorsUtility.java create mode 100644 addons/binding/org.openhab.binding.mysensors/src/main/java/org/openhab/binding/mysensors/internal/event/MySensorsEventType.java create mode 100644 addons/binding/org.openhab.binding.mysensors/src/main/java/org/openhab/binding/mysensors/internal/event/MySensorsStatusUpdateEvent.java rename addons/binding/org.openhab.binding.mysensors/src/main/java/org/openhab/binding/mysensors/{handler => internal/event}/MySensorsUpdateListener.java (89%) create mode 100644 addons/binding/org.openhab.binding.mysensors/src/main/java/org/openhab/binding/mysensors/internal/exception/NoMoreIdsException.java create mode 100644 addons/binding/org.openhab.binding.mysensors/src/main/java/org/openhab/binding/mysensors/internal/factory/MySensorsCacheFactory.java rename addons/binding/org.openhab.binding.mysensors/src/main/java/org/openhab/binding/mysensors/internal/{ => factory}/MySensorsHandlerFactory.java (83%) create mode 100644 addons/binding/org.openhab.binding.mysensors/src/main/java/org/openhab/binding/mysensors/internal/handler/MySensorsBridgeHandler.java rename addons/binding/org.openhab.binding.mysensors/src/main/java/org/openhab/binding/mysensors/{handler/MySensorsHandler.java => internal/handler/MySensorsThingHandler.java} (55%) create mode 100644 addons/binding/org.openhab.binding.mysensors/src/main/java/org/openhab/binding/mysensors/internal/protocol/MySensorsBridgeConnection.java create mode 100644 addons/binding/org.openhab.binding.mysensors/src/main/java/org/openhab/binding/mysensors/internal/protocol/MySensorsNetworkSanityChecker.java rename addons/binding/org.openhab.binding.mysensors/src/main/java/org/openhab/binding/mysensors/{ => internal}/protocol/MySensorsReader.java (71%) rename addons/binding/org.openhab.binding.mysensors/src/main/java/org/openhab/binding/mysensors/{ => internal}/protocol/MySensorsWriter.java (82%) rename addons/binding/org.openhab.binding.mysensors/src/main/java/org/openhab/binding/mysensors/{ => internal}/protocol/ip/MySensorIpReader.java (84%) create mode 100644 addons/binding/org.openhab.binding.mysensors/src/main/java/org/openhab/binding/mysensors/internal/protocol/ip/MySensorsIpConnection.java rename addons/binding/org.openhab.binding.mysensors/src/main/java/org/openhab/binding/mysensors/{ => internal}/protocol/ip/MySensorsIpWriter.java (86%) rename addons/binding/org.openhab.binding.mysensors/src/main/java/org/openhab/binding/mysensors/internal/{ => protocol/message}/MySensorsMessage.java (60%) rename addons/binding/org.openhab.binding.mysensors/src/main/java/org/openhab/binding/mysensors/internal/{ => protocol/message}/MySensorsMessageParser.java (97%) rename addons/binding/org.openhab.binding.mysensors/src/main/java/org/openhab/binding/mysensors/{ => internal}/protocol/serial/MySensorsSerialConnection.java (62%) rename addons/binding/org.openhab.binding.mysensors/src/main/java/org/openhab/binding/mysensors/{ => internal}/protocol/serial/MySensorsSerialReader.java (84%) rename addons/binding/org.openhab.binding.mysensors/src/main/java/org/openhab/binding/mysensors/{ => internal}/protocol/serial/MySensorsSerialWriter.java (83%) create mode 100644 addons/binding/org.openhab.binding.mysensors/src/main/java/org/openhab/binding/mysensors/internal/sensors/MySensorsChild.java create mode 100644 addons/binding/org.openhab.binding.mysensors/src/main/java/org/openhab/binding/mysensors/internal/sensors/MySensorsDeviceManager.java create mode 100644 addons/binding/org.openhab.binding.mysensors/src/main/java/org/openhab/binding/mysensors/internal/sensors/MySensorsNode.java delete mode 100644 addons/binding/org.openhab.binding.mysensors/src/main/java/org/openhab/binding/mysensors/protocol/ip/MySensorsIpConnection.java create mode 100644 addons/binding/org.openhab.binding.mysensors/src/main/test/org/openhab/binding/mysensors/test/CacheTest.java create mode 100644 addons/binding/org.openhab.binding.mysensors/target/org.openhab.binding.mysensors-2.0.0-SNAPSHOT.jar create mode 100755 addons/binding/org.openhab.binding.mysensors/test/iVersionMessage.sh diff --git a/addons/binding/org.openhab.binding.mysensors/.classpath b/addons/binding/org.openhab.binding.mysensors/.classpath index a95e0906ca013..1ff253c6199b9 100644 --- a/addons/binding/org.openhab.binding.mysensors/.classpath +++ b/addons/binding/org.openhab.binding.mysensors/.classpath @@ -1,7 +1,9 @@ - + + + diff --git a/addons/binding/org.openhab.binding.mysensors/.gitignore b/addons/binding/org.openhab.binding.mysensors/.gitignore new file mode 100644 index 0000000000000..6fac264911ecf --- /dev/null +++ b/addons/binding/org.openhab.binding.mysensors/.gitignore @@ -0,0 +1 @@ +/*.cached diff --git a/addons/binding/org.openhab.binding.mysensors/.settings/org.eclipse.jdt.core.prefs b/addons/binding/org.openhab.binding.mysensors/.settings/org.eclipse.jdt.core.prefs new file mode 100644 index 0000000000000..0c68a61dca867 --- /dev/null +++ b/addons/binding/org.openhab.binding.mysensors/.settings/org.eclipse.jdt.core.prefs @@ -0,0 +1,7 @@ +eclipse.preferences.version=1 +org.eclipse.jdt.core.compiler.codegen.inlineJsrBytecode=enabled +org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.8 +org.eclipse.jdt.core.compiler.compliance=1.8 +org.eclipse.jdt.core.compiler.problem.assertIdentifier=error +org.eclipse.jdt.core.compiler.problem.enumIdentifier=error +org.eclipse.jdt.core.compiler.source=1.8 diff --git a/addons/binding/org.openhab.binding.mysensors/ESH-INF/thing/thing-types.xml b/addons/binding/org.openhab.binding.mysensors/ESH-INF/thing/thing-types.xml index b586d2266757c..d65bfc8715007 100644 --- a/addons/binding/org.openhab.binding.mysensors/ESH-INF/thing/thing-types.xml +++ b/addons/binding/org.openhab.binding.mysensors/ESH-INF/thing/thing-types.xml @@ -11,13 +11,11 @@ - serial-port The serial port the MySensors Gateway is connected to - senddelay The delay between messages send to the gateway @@ -30,17 +28,20 @@ 115200 - skipstartupchek Allow the user to bypass the I_VERSION check on startup false - imperialconfig Metric answer with imperial instead of metric false + + + Network sanity check periodically unsure that gateway is up and running + false + @@ -51,37 +52,37 @@ - ipAddress The IP Address the MySensors Gateway uses - tcpport The TCP Port the MySensors Gateway uses (usually 5003) 5003 - senddelay The delay between messages send to the gateway 100 - skipstartupchek Allow the user to bypass the I_VERSION check on startup false - imperialconfig Metric answer with imperial instead of metric false + + + Network sanity check periodically unsure that gateway is up and running + false + @@ -895,7 +896,133 @@ + + + + + + + Shows the complete MySensors Message + + + + + + + + + NodeId + + The ID of the node in the MySensors network + + 999 + + + ChildId + + The ID of the child of a node in the MySensors network + + 999 + + + + + + + + + + + + V_TEXT + + + + + + + + + NodeId + + The ID of the node in the MySensors network + + 999 + + + ChildId + + The ID of the child of a node in the MySensors network + + 999 + + + + + + + + + + + + Send IR codes + + + + + + + + + NodeId + + The ID of the node in the MySensors network + + 999 + + + ChildId + + The ID of the child of a node in the MySensors network + + 999 + + + + + + + + + + + + Receive IR codes + + + + + + + + + NodeId + + The ID of the node in the MySensors network + + 999 + + + ChildId + + The ID of the child of a node in the MySensors network + + 999 + + + @@ -1153,4 +1280,28 @@ MySensors Water Electric Condictivity Channel + + String + + MySensors Message Channel + + + + String + + MySensors Text Channel + Variable + + + String + + MySensors IR Send Channel + Variable + + + String + + MySensors IR Receive Channel + Variable + diff --git a/addons/binding/org.openhab.binding.mysensors/META-INF/MANIFEST.MF b/addons/binding/org.openhab.binding.mysensors/META-INF/MANIFEST.MF index 1444955748848..741ef532a34dc 100644 --- a/addons/binding/org.openhab.binding.mysensors/META-INF/MANIFEST.MF +++ b/addons/binding/org.openhab.binding.mysensors/META-INF/MANIFEST.MF @@ -7,12 +7,11 @@ Bundle-Version: 2.0.0.qualifier Bundle-RequiredExecutionEnvironment: JavaSE-1.7 Bundle-ClassPath: . Import-Package: - com.google.common.collect, com.google.common.base, + com.google.common.collect, + com.google.gson;version="2.2.4", gnu.io, org.apache.commons.lang, - org.openhab.binding.mysensors, - org.openhab.binding.mysensors.handler, org.eclipse.smarthome.config.core, org.eclipse.smarthome.config.discovery, org.eclipse.smarthome.core.library.types, @@ -21,11 +20,15 @@ Import-Package: org.eclipse.smarthome.core.thing.binding.builder, org.eclipse.smarthome.core.thing.type, org.eclipse.smarthome.core.types, + org.openhab.binding.mysensors, + org.openhab.binding.mysensors.internal.handler, + org.osgi.framework, org.osgi.service.cm, org.osgi.service.component, - org.osgi.framework, org.slf4j Service-Component: OSGI-INF/*.xml Export-Package: org.openhab.binding.mysensors, - org.openhab.binding.mysensors.handler -Require-Bundle: org.eclipse.smarthome.core + org.openhab.binding.mysensors.internal.handler +Require-Bundle: org.eclipse.smarthome.core, + org.openhab.io.transport.serial, + com.google.gson diff --git a/addons/binding/org.openhab.binding.mysensors/OSGI-INF/MySensorsHandlerFactory.xml b/addons/binding/org.openhab.binding.mysensors/OSGI-INF/MySensorsHandlerFactory.xml index d5595ec6dfcb8..f3df8bb5169f5 100644 --- a/addons/binding/org.openhab.binding.mysensors/OSGI-INF/MySensorsHandlerFactory.xml +++ b/addons/binding/org.openhab.binding.mysensors/OSGI-INF/MySensorsHandlerFactory.xml @@ -8,10 +8,9 @@ http://www.eclipse.org/legal/epl-v10.html --> - - - + + diff --git a/addons/binding/org.openhab.binding.mysensors/src/main/java/org/openhab/binding/mysensors/MySensorsBindingConstants.java b/addons/binding/org.openhab.binding.mysensors/src/main/java/org/openhab/binding/mysensors/MySensorsBindingConstants.java index 4f9178df533b2..eac82bb422efc 100644 --- a/addons/binding/org.openhab.binding.mysensors/src/main/java/org/openhab/binding/mysensors/MySensorsBindingConstants.java +++ b/addons/binding/org.openhab.binding.mysensors/src/main/java/org/openhab/binding/mysensors/MySensorsBindingConstants.java @@ -13,7 +13,7 @@ import java.util.Set; import org.eclipse.smarthome.core.thing.ThingTypeUID; -import org.openhab.binding.mysensors.internal.MySensorsMessage; +import org.openhab.binding.mysensors.internal.protocol.message.MySensorsMessage; import com.google.common.collect.ImmutableSet; import com.google.common.collect.Lists; @@ -61,6 +61,7 @@ public class MySensorsBindingConstants { public static final int MYSENSORS_SUBTYPE_S_DISTANCE = 15; public static final int MYSENSORS_SUBTYPE_S_LIGHT_LEVEL = 16; public static final int MYSENSORS_SUBTYPE_S_LOCK = 19; + public static final int MYSENSORS_SUBTYPE_S_IR = 20; public static final int MYSENSORS_SUBTYPE_S_WATER = 21; public static final int MYSENSORS_SUBTYPE_S_CUSTOM = 23; public static final int MYSENSORS_SUBTYPE_S_RGB_LIGHT = 26; @@ -197,6 +198,10 @@ public class MySensorsBindingConstants { public final static ThingTypeUID THING_TYPE_RGB_LIGHT = new ThingTypeUID(BINDING_ID, "rgbLight"); public final static ThingTypeUID THING_TYPE_RGBW_LIGHT = new ThingTypeUID(BINDING_ID, "rgbwLight"); public final static ThingTypeUID THING_TYPE_WATER_QUALITY = new ThingTypeUID(BINDING_ID, "waterQuality"); + public final static ThingTypeUID THING_TYPE_MYSENSORS_MESSAGE = new ThingTypeUID(BINDING_ID, "mySensorsMessage"); + public final static ThingTypeUID THING_TYPE_TEXT = new ThingTypeUID(BINDING_ID, "text"); + public final static ThingTypeUID THING_TYPE_IR_SEND = new ThingTypeUID(BINDING_ID, "irSend"); + public final static ThingTypeUID THING_TYPE_IR_RECEIVE = new ThingTypeUID(BINDING_ID, "irReceive"); public final static ThingTypeUID THING_TYPE_BRIDGE_SER = new ThingTypeUID(BINDING_ID, "bridge-ser"); public final static ThingTypeUID THING_TYPE_BRIDGE_ETH = new ThingTypeUID(BINDING_ID, "bridge-eth"); @@ -254,7 +259,9 @@ public class MySensorsBindingConstants { public final static String CHANNEL_VAR = "var"; public final static String CHANNEL_VA = "va"; public final static String CHANNEL_POWER_FACTOR = "power-factor"; - + public final static String CHANNEL_IR_SEND = "irSend"; + public final static String CHANNEL_IR_RECEIVE = "irReceive"; + public final static String CHANNEL_MYSENSORS_MESSAGE = "mySensorsMessage"; public final static String CHANNEL_LAST_UPDATE = "lastupdate"; // Wait time Arduino reset @@ -322,7 +329,9 @@ public class MySensorsBindingConstants { put(MYSENSORS_SUBTYPE_V_VAR, CHANNEL_VAR); put(MYSENSORS_SUBTYPE_V_VA, CHANNEL_VA); put(MYSENSORS_SUBTYPE_V_POWER_FACTOR, CHANNEL_POWER_FACTOR); - + put(MYSENSORS_SUBTYPE_V_TEXT, CHANNEL_TEXT); + put(MYSENSORS_SUBTYPE_V_IR_SEND, CHANNEL_IR_SEND); + put(MYSENSORS_SUBTYPE_V_IR_RECEIVE, CHANNEL_IR_RECEIVE); } }; @@ -344,7 +353,8 @@ public class MySensorsBindingConstants { THING_TYPE_DOOR, THING_TYPE_MOTION, THING_TYPE_SMOKE, THING_TYPE_DIMMER, THING_TYPE_COVER, THING_TYPE_WIND, THING_TYPE_RAIN, THING_TYPE_UV, THING_TYPE_WEIGHT, THING_TYPE_DISTANCE, THING_TYPE_LIGHT_LEVEL, THING_TYPE_HVAC, THING_TYPE_WATER, THING_TYPE_CUSTOM, THING_TYPE_LOCK, THING_TYPE_SOUND, - THING_TYPE_RGB_LIGHT, THING_TYPE_RGBW_LIGHT, THING_TYPE_WATER_QUALITY); + THING_TYPE_RGB_LIGHT, THING_TYPE_RGBW_LIGHT, THING_TYPE_WATER_QUALITY, THING_TYPE_MYSENSORS_MESSAGE, + THING_TYPE_TEXT, THING_TYPE_IR_SEND, THING_TYPE_IR_RECEIVE); /** Supported bridges */ public final static Set SUPPORTED_BRIDGE_THING_TYPES_UIDS = ImmutableSet.of(THING_TYPE_BRIDGE_SER, THING_TYPE_BRIDGE_ETH); @@ -355,6 +365,6 @@ public class MySensorsBindingConstants { THING_TYPE_DOOR, THING_TYPE_MOTION, THING_TYPE_SMOKE, THING_TYPE_DIMMER, THING_TYPE_COVER, THING_TYPE_WIND, THING_TYPE_RAIN, THING_TYPE_UV, THING_TYPE_WEIGHT, THING_TYPE_DISTANCE, THING_TYPE_LIGHT_LEVEL, THING_TYPE_HVAC, THING_TYPE_WATER, THING_TYPE_CUSTOM, THING_TYPE_LOCK, THING_TYPE_SOUND, - THING_TYPE_RGB_LIGHT, THING_TYPE_RGBW_LIGHT, THING_TYPE_WATER_QUALITY, THING_TYPE_BRIDGE_SER, - THING_TYPE_BRIDGE_ETH); + THING_TYPE_RGB_LIGHT, THING_TYPE_RGBW_LIGHT, THING_TYPE_WATER_QUALITY, THING_TYPE_MYSENSORS_MESSAGE, + THING_TYPE_TEXT, THING_TYPE_IR_SEND, THING_TYPE_IR_RECEIVE, THING_TYPE_BRIDGE_SER, THING_TYPE_BRIDGE_ETH); } diff --git a/addons/binding/org.openhab.binding.mysensors/src/main/java/org/openhab/binding/mysensors/config/MySensorsBridgeConfiguration.java b/addons/binding/org.openhab.binding.mysensors/src/main/java/org/openhab/binding/mysensors/config/MySensorsBridgeConfiguration.java index 8d7d0cc4e6a8f..3215440dc1160 100644 --- a/addons/binding/org.openhab.binding.mysensors/src/main/java/org/openhab/binding/mysensors/config/MySensorsBridgeConfiguration.java +++ b/addons/binding/org.openhab.binding.mysensors/src/main/java/org/openhab/binding/mysensors/config/MySensorsBridgeConfiguration.java @@ -16,5 +16,5 @@ public class MySensorsBridgeConfiguration { public Integer baudRate; public Boolean imperial; public Boolean skipStartupCheck; - + public Boolean enableNetworkSanCheck; } diff --git a/addons/binding/org.openhab.binding.mysensors/src/main/java/org/openhab/binding/mysensors/service/DiscoveryThread.java b/addons/binding/org.openhab.binding.mysensors/src/main/java/org/openhab/binding/mysensors/discovery/DiscoveryThread.java similarity index 54% rename from addons/binding/org.openhab.binding.mysensors/src/main/java/org/openhab/binding/mysensors/service/DiscoveryThread.java rename to addons/binding/org.openhab.binding.mysensors/src/main/java/org/openhab/binding/mysensors/discovery/DiscoveryThread.java index 2b91e88d5ef3f..bd7faeb1334c0 100644 --- a/addons/binding/org.openhab.binding.mysensors/src/main/java/org/openhab/binding/mysensors/service/DiscoveryThread.java +++ b/addons/binding/org.openhab.binding.mysensors/src/main/java/org/openhab/binding/mysensors/discovery/DiscoveryThread.java @@ -5,12 +5,13 @@ * which accompanies this distribution, and is available at * http://www.eclipse.org/legal/epl-v10.html */ -package org.openhab.binding.mysensors.service; +package org.openhab.binding.mysensors.discovery; -import org.openhab.binding.mysensors.discovery.MySensorsDiscoveryService; -import org.openhab.binding.mysensors.handler.MySensorsStatusUpdateEvent; -import org.openhab.binding.mysensors.handler.MySensorsUpdateListener; -import org.openhab.binding.mysensors.internal.MySensorsBridgeConnection; +import org.openhab.binding.mysensors.internal.event.MySensorsEventType; +import org.openhab.binding.mysensors.internal.event.MySensorsStatusUpdateEvent; +import org.openhab.binding.mysensors.internal.event.MySensorsUpdateListener; +import org.openhab.binding.mysensors.internal.protocol.MySensorsBridgeConnection; +import org.openhab.binding.mysensors.internal.protocol.message.MySensorsMessage; public class DiscoveryThread implements MySensorsUpdateListener { private MySensorsBridgeConnection mysCon; @@ -22,22 +23,18 @@ public DiscoveryThread(MySensorsBridgeConnection mysCon, MySensorsDiscoveryServi } public void start() { - mysCon.addUpdateListener(this); + mysCon.addEventListener(this); } public void stop() { - mysCon.removeUpdateListener(this); + mysCon.removeEventListener(this); } @Override public void statusUpdateReceived(MySensorsStatusUpdateEvent event) { - mysDiscoServ.newDevicePresented(event.getData()); - - } - - @Override - public void disconnectEvent() { - // TODO Auto-generated method stub + if (event.getEventType() == MySensorsEventType.INCOMING_MESSAGE) { + mysDiscoServ.newDevicePresented((MySensorsMessage) event.getData()); + } } } diff --git a/addons/binding/org.openhab.binding.mysensors/src/main/java/org/openhab/binding/mysensors/discovery/MySensorsDiscoveryService.java b/addons/binding/org.openhab.binding.mysensors/src/main/java/org/openhab/binding/mysensors/discovery/MySensorsDiscoveryService.java index 71a6dcf0a1db6..c14d67b132432 100644 --- a/addons/binding/org.openhab.binding.mysensors/src/main/java/org/openhab/binding/mysensors/discovery/MySensorsDiscoveryService.java +++ b/addons/binding/org.openhab.binding.mysensors/src/main/java/org/openhab/binding/mysensors/discovery/MySensorsDiscoveryService.java @@ -16,9 +16,8 @@ import org.eclipse.smarthome.config.discovery.DiscoveryResult; import org.eclipse.smarthome.config.discovery.DiscoveryResultBuilder; import org.eclipse.smarthome.core.thing.ThingUID; -import org.openhab.binding.mysensors.handler.MySensorsBridgeHandler; -import org.openhab.binding.mysensors.internal.MySensorsMessage; -import org.openhab.binding.mysensors.service.DiscoveryThread; +import org.openhab.binding.mysensors.internal.handler.MySensorsBridgeHandler; +import org.openhab.binding.mysensors.internal.protocol.message.MySensorsMessage; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -169,18 +168,22 @@ public void newDevicePresented(MySensorsMessage msg) { uid = new ThingUID(THING_TYPE_SOUND, bridgeHandler.getThing().getUID(), "Sound_level_" + msg.getNodeId() + "_" + msg.getChildId()); break; - case MYSENSORS_SUBTYPE_S_RGB_LIGHT : - uid = new ThingUID(THING_TYPE_RGB_LIGHT , bridgeHandler.getThing().getUID(), + case MYSENSORS_SUBTYPE_S_RGB_LIGHT: + uid = new ThingUID(THING_TYPE_RGB_LIGHT, bridgeHandler.getThing().getUID(), "RGB_light" + msg.getNodeId() + "_" + msg.getChildId()); break; - case MYSENSORS_SUBTYPE_S_RGBW_LIGHT : - uid = new ThingUID(THING_TYPE_RGBW_LIGHT , bridgeHandler.getThing().getUID(), + case MYSENSORS_SUBTYPE_S_RGBW_LIGHT: + uid = new ThingUID(THING_TYPE_RGBW_LIGHT, bridgeHandler.getThing().getUID(), "RGBW_light" + msg.getNodeId() + "_" + msg.getChildId()); break; - case MYSENSORS_SUBTYPE_S_WATER_QUALITY : - uid = new ThingUID(THING_TYPE_WATER_QUALITY , bridgeHandler.getThing().getUID(), + case MYSENSORS_SUBTYPE_S_WATER_QUALITY: + uid = new ThingUID(THING_TYPE_WATER_QUALITY, bridgeHandler.getThing().getUID(), "Water_quality" + msg.getNodeId() + "_" + msg.getChildId()); break; + case MYSENSORS_SUBTYPE_S_INFO: + uid = new ThingUID(THING_TYPE_TEXT, bridgeHandler.getThing().getUID(), + "Text" + msg.getNodeId() + "_" + msg.getChildId()); + break; } if (uid != null) { Map properties = new HashMap<>(2); diff --git a/addons/binding/org.openhab.binding.mysensors/src/main/java/org/openhab/binding/mysensors/handler/MySensorsBridgeHandler.java b/addons/binding/org.openhab.binding.mysensors/src/main/java/org/openhab/binding/mysensors/handler/MySensorsBridgeHandler.java deleted file mode 100644 index 6369146b80523..0000000000000 --- a/addons/binding/org.openhab.binding.mysensors/src/main/java/org/openhab/binding/mysensors/handler/MySensorsBridgeHandler.java +++ /dev/null @@ -1,293 +0,0 @@ -/** - * Copyright (c) 2014-2016 openHAB UG (haftungsbeschraenkt) and others. - * All rights reserved. This program and the accompanying materials - * are made available under the terms of the Eclipse Public License v1.0 - * which accompanies this distribution, and is available at - * http://www.eclipse.org/legal/epl-v10.html - */ -package org.openhab.binding.mysensors.handler; - -import static org.openhab.binding.mysensors.MySensorsBindingConstants.*; - -import java.util.ArrayList; -import java.util.Collection; -import java.util.Iterator; -import java.util.List; -import java.util.Random; - -import org.eclipse.smarthome.config.core.Configuration; -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.binding.BaseBridgeHandler; -import org.eclipse.smarthome.core.types.Command; -import org.openhab.binding.mysensors.config.MySensorsBridgeConfiguration; -import org.openhab.binding.mysensors.discovery.MySensorsDiscoveryService; -import org.openhab.binding.mysensors.internal.MySensorsBridgeConnection; -import org.openhab.binding.mysensors.internal.MySensorsMessage; -import org.openhab.binding.mysensors.protocol.ip.MySensorsIpConnection; -import org.openhab.binding.mysensors.protocol.serial.MySensorsSerialConnection; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import com.google.common.collect.Lists; - -/** - * @author Tim Oberföll - * - * MySensorsBridgeHandler is used to initialize a new bridge (in MySensors: Gateway) - * The sensors are connected via the gateway/bridge to the controller - */ -public class MySensorsBridgeHandler extends BaseBridgeHandler implements MySensorsUpdateListener { - - private Logger logger = LoggerFactory.getLogger(MySensorsBridgeHandler.class); - - public Collection connectedThings = Lists.newArrayList(); - - // List of Ids that OpenHAB has given, in response to an id request from a sensor node - private List givenIds = new ArrayList(); - - private MySensorsBridgeConnection mysCon = null; - - // Is (I)mperial or (M)etric? - private String iConfig = null; - - private boolean skipStartupCheck = false; - - public MySensorsBridgeHandler(Bridge bridge) { - super(bridge); - } - - /* - * (non-Javadoc) - * - * @see org.eclipse.smarthome.core.thing.binding.BaseThingHandler#initialize() - */ - @Override - public void initialize() { - logger.debug("Initialization of the MySensors Bridge"); - - MySensorsBridgeConfiguration configuration = getConfigAs(MySensorsBridgeConfiguration.class); - - boolean imperial = configuration.imperial; - iConfig = imperial ? "I" : "M"; - - logger.debug("Using {} measure unit", (imperial ? "Imperial" : "Metric")); - - skipStartupCheck = configuration.skipStartupCheck; - - logger.debug("Set skip check on startup to: {}", skipStartupCheck); - - if (getThing().getThingTypeUID().equals(THING_TYPE_BRIDGE_SER)) { - mysCon = new MySensorsSerialConnection(configuration.serialPort, configuration.baudRate, - configuration.sendDelay, skipStartupCheck); - } else if (getThing().getThingTypeUID().equals(THING_TYPE_BRIDGE_ETH)) { - mysCon = new MySensorsIpConnection(configuration.ipAddress, configuration.tcpPort, configuration.sendDelay, - skipStartupCheck); - } - - mysCon.addUpdateListener(this); - - if (mysCon.connect()) { - updateStatus(ThingStatus.ONLINE); - - // Start discovery service - MySensorsDiscoveryService discoveryService = new MySensorsDiscoveryService(this); - discoveryService.activate(); - } else { - mysCon.removeUpdateListener(this); - updateStatus(ThingStatus.OFFLINE); - } - - } - - /* - * (non-Javadoc) - * - * @see org.eclipse.smarthome.core.thing.binding.BaseThingHandler#dispose() - */ - @Override - public void dispose() { - if (mysCon != null) { - mysCon.disconnect(); - } - } - - /* - * (non-Javadoc) - * - * @see - * org.eclipse.smarthome.core.thing.binding.ThingHandler#handleCommand(org.eclipse.smarthome.core.thing.ChannelUID, - * org.eclipse.smarthome.core.types.Command) - */ - @Override - public void handleCommand(ChannelUID channelUID, Command command) { - // TODO Auto-generated method stub - } - - /* - * (non-Javadoc) - * - * @see - * org.openhab.binding.mysensors.handler.MySensorsUpdateListener#statusUpdateReceived(org.openhab.binding.mysensors. - * handler.MySensorsStatusUpdateEvent) - */ - @Override - public void statusUpdateReceived(MySensorsStatusUpdateEvent event) { - MySensorsMessage msg = event.getData(); - // logger.debug("updateRecieved: " + msg.getDebugInfo()); - // Do we get an ACK? - if (msg.getAck() == 1) { - logger.debug(String.format("ACK received! Node: %d, Child: %d", msg.nodeId, msg.childId)); - mysCon.removeMySensorsOutboundMessage(msg); - } - - // Are we getting a Request ID Message? - if (msg.getNodeId() == 255) { - if (msg.getChildId() == 255) { - if (msg.getMsgType() == MYSENSORS_MSG_TYPE_INTERNAL) { - if (msg.getAck() == 0) { - if (msg.getSubType() == 3) { - answerIDRequest(); - } - } - } - } - } - - // Have we get a I_VERSION message? - if (msg.getNodeId() == 0) { - if (msg.getChildId() == 0 || msg.getChildId() == 255) { - if (msg.getMsgType() == MYSENSORS_MSG_TYPE_INTERNAL) { - if (msg.getAck() == 0) { - if (msg.getSubType() == MYSENSORS_SUBTYPE_I_VERSION) { - handleIncomingVersionMessage(msg.msg); - } - } - } - } - } - - // Have we get a I_CONFIG message? - if (msg.getChildId() == 0 || msg.getChildId() == 255) { - if (msg.getMsgType() == MYSENSORS_MSG_TYPE_INTERNAL) { - if (msg.getAck() == 0) { - if (msg.getSubType() == MYSENSORS_SUBTYPE_I_CONFIG) { - answerIConfigMessage(msg); - } - } - } - } - - // Have we get a I_TIME message? - if (msg.getChildId() == 0 || msg.getChildId() == 255) { - if (msg.getMsgType() == MYSENSORS_MSG_TYPE_INTERNAL) { - if (msg.getAck() == 0) { - if (msg.getSubType() == MYSENSORS_SUBTYPE_I_TIME) { - answerITimeMessage(msg); - } - } - } - } - } - - /** - * Answer to I_TIME message for gateway time request from sensor - * - * @param msg, the incoming I_TIME message from sensor - */ - private void answerITimeMessage(MySensorsMessage msg) { - logger.info("I_TIME request received from {}, answering...", msg.nodeId); - - String time = Long.toString(System.currentTimeMillis() / 1000); - MySensorsMessage newMsg = new MySensorsMessage(msg.nodeId, msg.childId, MYSENSORS_MSG_TYPE_INTERNAL, 0, false, - MYSENSORS_SUBTYPE_I_TIME, time); - mysCon.addMySensorsOutboundMessage(newMsg); - - } - - /** - * Answer to I_CONFIG message for imperial/metric request from sensor - * - * @param msg, the incoming I_CONFIG message from sensor - */ - private void answerIConfigMessage(MySensorsMessage msg) { - logger.debug("I_CONFIG request received from {}, answering...", msg.nodeId); - - MySensorsMessage newMsg = new MySensorsMessage(msg.nodeId, msg.childId, MYSENSORS_MSG_TYPE_INTERNAL, 0, false, - MYSENSORS_SUBTYPE_I_CONFIG, iConfig); - mysCon.addMySensorsOutboundMessage(newMsg); - - } - - /** - * If an ID -Request from a sensor is received the controller will send an id to the sensor - */ - private void answerIDRequest() { - logger.debug("ID Request received"); - - int newId = getFreeId(); - givenIds.add(newId); - MySensorsMessage newMsg = new MySensorsMessage(255, 255, 3, 0, false, 4, newId + ""); - mysCon.addMySensorsOutboundMessage(newMsg); - logger.info("New Node in the MySensors network has requested an ID. ID is: {}", newId); - } - - /** - * Wake up main thread that is waiting for confirmation of link up - */ - private void handleIncomingVersionMessage(String message) { - mysCon.iVersionMessageReceived(message); - } - - private int getFreeId() { - int id = 1; - - List takenIds = new ArrayList(); - - // Which ids are taken in Thing list of OpenHAB - Collection thingList = thingRegistry.getAll(); - Iterator iterator = thingList.iterator(); - - while (iterator.hasNext()) { - Thing thing = iterator.next(); - Configuration conf = thing.getConfiguration(); - if (conf != null) { - Object nodeIdobj = conf.get("nodeId"); - if (nodeIdobj != null) { - int nodeId = Integer.parseInt(nodeIdobj.toString()); - takenIds.add(nodeId); - } - } - } - - // Which ids are already given by the binding, but not yet in the thing list? - Iterator iteratorGiven = givenIds.iterator(); - while (iteratorGiven.hasNext()) { - takenIds.add(iteratorGiven.next()); - } - - // generate new id - boolean foundId = false; - while (!foundId) { - Random rand = new Random(System.currentTimeMillis()); - int newId = rand.nextInt((254 - 1) + 1) + 1; - if (!takenIds.contains(newId)) { - id = newId; - foundId = true; - } - } - - return id; - } - - public MySensorsBridgeConnection getBridgeConnection() { - return mysCon; - } - - @Override - public void disconnectEvent() { - updateStatus(ThingStatus.OFFLINE); - } -} diff --git a/addons/binding/org.openhab.binding.mysensors/src/main/java/org/openhab/binding/mysensors/handler/MySensorsStatusUpdateEvent.java b/addons/binding/org.openhab.binding.mysensors/src/main/java/org/openhab/binding/mysensors/handler/MySensorsStatusUpdateEvent.java deleted file mode 100644 index b9929219c1bfc..0000000000000 --- a/addons/binding/org.openhab.binding.mysensors/src/main/java/org/openhab/binding/mysensors/handler/MySensorsStatusUpdateEvent.java +++ /dev/null @@ -1,33 +0,0 @@ -/** - * Copyright (c) 2014-2016 openHAB UG (haftungsbeschraenkt) and others. - * All rights reserved. This program and the accompanying materials - * are made available under the terms of the Eclipse Public License v1.0 - * which accompanies this distribution, and is available at - * http://www.eclipse.org/legal/epl-v10.html - */ -package org.openhab.binding.mysensors.handler; - -import org.openhab.binding.mysensors.internal.MySensorsMessage; - -/** - * @author Tim Oberföll - * - * If a new message from the gateway/bridge is received - * a MySensorsStatusUpdateEvent is generated containing the MySensors message - */ -public class MySensorsStatusUpdateEvent { - private MySensorsMessage data; - - public MySensorsStatusUpdateEvent(MySensorsMessage data) { - this.data = data; - } - - public MySensorsMessage getData() { - return data; - } - - public void setData(MySensorsMessage data) { - this.data = data; - } - -} diff --git a/addons/binding/org.openhab.binding.mysensors/src/main/java/org/openhab/binding/mysensors/internal/MySensorsBridgeConnection.java b/addons/binding/org.openhab.binding.mysensors/src/main/java/org/openhab/binding/mysensors/internal/MySensorsBridgeConnection.java deleted file mode 100644 index 578003d189273..0000000000000 --- a/addons/binding/org.openhab.binding.mysensors/src/main/java/org/openhab/binding/mysensors/internal/MySensorsBridgeConnection.java +++ /dev/null @@ -1,164 +0,0 @@ -/** - * Copyright (c) 2014-2016 openHAB UG (haftungsbeschraenkt) and others. - * All rights reserved. This program and the accompanying materials - * are made available under the terms of the Eclipse Public License v1.0 - * which accompanies this distribution, and is available at - * http://www.eclipse.org/legal/epl-v10.html - */ -package org.openhab.binding.mysensors.internal; - -import java.util.ArrayList; -import java.util.Iterator; -import java.util.List; -import java.util.concurrent.BlockingQueue; -import java.util.concurrent.LinkedBlockingQueue; -import java.util.concurrent.TimeUnit; - -import org.openhab.binding.mysensors.MySensorsBindingConstants; -import org.openhab.binding.mysensors.handler.MySensorsUpdateListener; -import org.openhab.binding.mysensors.protocol.MySensorsReader; -import org.openhab.binding.mysensors.protocol.MySensorsWriter; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -public abstract class MySensorsBridgeConnection { - - private Logger logger = LoggerFactory.getLogger(MySensorsBridgeConnection.class); - - public List updateListeners; - public boolean pauseWriter = false; - - private BlockingQueue outboundMessageQueue = null; - - protected boolean connected = false; - - private MySensorsBridgeConnection waitingObj = null; - private boolean iVersionResponse = false; - - private boolean skipStartupCheck = false; - - public MySensorsBridgeConnection(boolean skipStartupCheck) { - outboundMessageQueue = new LinkedBlockingQueue(); - updateListeners = new ArrayList<>(); - - this.skipStartupCheck = skipStartupCheck; - } - - /** - * startup connection with bridge - * - * @return - */ - public abstract boolean connect(); - - /** - * shutodown method that allows the correct disconnection with the used bridge - * - * @return - */ - public abstract void disconnect(); - - /** - * Start thread managing the incoming/outgoing messages. It also have the task to test the connection to gateway by - * sending a special message (I_VERSION) to it - * - * @return true if the gateway test pass successfully - */ - protected boolean startReaderWriterThread(MySensorsReader reader, MySensorsWriter writer) { - - reader.startReader(); - writer.startWriter(); - - if (!skipStartupCheck) { - try { - int i = 0; - synchronized (this) { - while (!iVersionResponse && i < 5) { - addMySensorsOutboundMessage(MySensorsBindingConstants.I_VERSION_MESSAGE); - waitingObj = this; - waitingObj.wait(1000); - i++; - } - } - } catch (Exception e) { - logger.error("Exception on waiting for I_VERSION message", e); - } - } else { - logger.warn("Skipping I_VERSION connection test, not recommended..."); - iVersionResponse = true; - } - - if (!iVersionResponse) { - logger.error("Cannot start reading/writing thread, probably sync message (I_VERSION) not received"); - } - - return iVersionResponse; - } - - /** - * @param listener An Object, that wants to listen on status updates - */ - public void addUpdateListener(MySensorsUpdateListener listener) { - synchronized (updateListeners) { - updateListeners.add(listener); - } - } - - public void removeUpdateListener(MySensorsUpdateListener listener) { - synchronized (updateListeners) { - if (updateListeners.contains(listener)) { - updateListeners.remove(listener); - } - } - } - - public MySensorsMessage pollMySensorsOutboundQueue() throws InterruptedException { - return outboundMessageQueue.poll(1, TimeUnit.DAYS); - } - - public void addMySensorsOutboundMessage(MySensorsMessage msg) { - addMySensorsOutboundMessage(msg, 1); - } - - public void addMySensorsOutboundMessage(MySensorsMessage msg, int copy) { - try { - for (int i = 0; i < copy; i++) { - outboundMessageQueue.put(msg); - } - } catch (InterruptedException e) { - logger.error("Interrupted message while ruuning"); - } - } - - public void removeMySensorsOutboundMessage(MySensorsMessage msg) { - - pauseWriter = true; - - Iterator iterator = outboundMessageQueue.iterator(); - if (iterator != null) { - while (iterator.hasNext()) { - MySensorsMessage msgInQueue = iterator.next(); - // logger.debug("Msg in Queue: " + msgInQueue.getDebugInfo()); - if (msgInQueue.getNodeId() == msg.getNodeId() && msgInQueue.getChildId() == msg.getChildId() - && msgInQueue.getMsgType() == msg.getMsgType() && msgInQueue.getSubType() == msg.getSubType() - && msgInQueue.getAck() == msg.getAck() && msgInQueue.getMsg().equals(msg.getMsg())) { - iterator.remove(); - // logger.debug("Message removed: " + msg.getDebugInfo()); - } else { - logger.debug("Message NOT removed: " + msg.getDebugInfo()); - } - } - } - - pauseWriter = false; - } - - public void iVersionMessageReceived(String msg) { - iVersionResponse = true; - synchronized (waitingObj) { - waitingObj.notifyAll(); - waitingObj = null; - } - logger.debug("Good,Gateway is up and running! (Ver:{})", msg); - } -} diff --git a/addons/binding/org.openhab.binding.mysensors/src/main/java/org/openhab/binding/mysensors/internal/MySensorsUtility.java b/addons/binding/org.openhab.binding.mysensors/src/main/java/org/openhab/binding/mysensors/internal/MySensorsUtility.java new file mode 100644 index 0000000000000..2b02196f1058a --- /dev/null +++ b/addons/binding/org.openhab.binding.mysensors/src/main/java/org/openhab/binding/mysensors/internal/MySensorsUtility.java @@ -0,0 +1,5 @@ +package org.openhab.binding.mysensors.internal; + +public class MySensorsUtility { + +} diff --git a/addons/binding/org.openhab.binding.mysensors/src/main/java/org/openhab/binding/mysensors/internal/event/MySensorsEventType.java b/addons/binding/org.openhab.binding.mysensors/src/main/java/org/openhab/binding/mysensors/internal/event/MySensorsEventType.java new file mode 100644 index 0000000000000..15f2006ea3cd1 --- /dev/null +++ b/addons/binding/org.openhab.binding.mysensors/src/main/java/org/openhab/binding/mysensors/internal/event/MySensorsEventType.java @@ -0,0 +1,16 @@ +/** + * Copyright (c) 2014-2016 openHAB UG (haftungsbeschraenkt) and others. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.openhab.binding.mysensors.internal.event; + +public enum MySensorsEventType { + INCOMING_MESSAGE, + NEW_NODE_DISCOVERED, + NODE_STATUS_UPDATE, + BRIDGE_STATUS_UPDATE, + CHILD_VALUE_UPDATED +} diff --git a/addons/binding/org.openhab.binding.mysensors/src/main/java/org/openhab/binding/mysensors/internal/event/MySensorsStatusUpdateEvent.java b/addons/binding/org.openhab.binding.mysensors/src/main/java/org/openhab/binding/mysensors/internal/event/MySensorsStatusUpdateEvent.java new file mode 100644 index 0000000000000..c3dec685f1132 --- /dev/null +++ b/addons/binding/org.openhab.binding.mysensors/src/main/java/org/openhab/binding/mysensors/internal/event/MySensorsStatusUpdateEvent.java @@ -0,0 +1,82 @@ +/** + * Copyright (c) 2014-2016 openHAB UG (haftungsbeschraenkt) and others. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.openhab.binding.mysensors.internal.event; + +import org.openhab.binding.mysensors.internal.protocol.message.MySensorsMessage; + +/** + * @author Tim Oberföll + * + * If a new message from the gateway/bridge is received + * a MySensorsStatusUpdateEvent is generated containing the MySensors message + */ +public class MySensorsStatusUpdateEvent { + private Object data; + + private MySensorsEventType eventType; + + public MySensorsStatusUpdateEvent(MySensorsEventType eventType, Object data) { + this.eventType = eventType; + this.data = data; + } + + public void setEventType(MySensorsEventType event) { + this.eventType = event; + } + + public MySensorsEventType getEventType() { + return eventType; + } + + public Object getData() { + return data; + } + + public void setData(MySensorsMessage data) { + this.data = data; + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + ((data == null) ? 0 : data.hashCode()); + result = prime * result + ((eventType == null) ? 0 : eventType.hashCode()); + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + MySensorsStatusUpdateEvent other = (MySensorsStatusUpdateEvent) obj; + if (data == null) { + if (other.data != null) { + return false; + } + } else if (!data.equals(other.data)) { + return false; + } + if (eventType != other.eventType) { + return false; + } + return true; + } + + @Override + public String toString() { + return "MySensorsStatusUpdateEvent [data=" + data + ", event=" + eventType + "]"; + } +} diff --git a/addons/binding/org.openhab.binding.mysensors/src/main/java/org/openhab/binding/mysensors/handler/MySensorsUpdateListener.java b/addons/binding/org.openhab.binding.mysensors/src/main/java/org/openhab/binding/mysensors/internal/event/MySensorsUpdateListener.java similarity index 89% rename from addons/binding/org.openhab.binding.mysensors/src/main/java/org/openhab/binding/mysensors/handler/MySensorsUpdateListener.java rename to addons/binding/org.openhab.binding.mysensors/src/main/java/org/openhab/binding/mysensors/internal/event/MySensorsUpdateListener.java index fd737fed7ed9c..0446c86dd011c 100644 --- a/addons/binding/org.openhab.binding.mysensors/src/main/java/org/openhab/binding/mysensors/handler/MySensorsUpdateListener.java +++ b/addons/binding/org.openhab.binding.mysensors/src/main/java/org/openhab/binding/mysensors/internal/event/MySensorsUpdateListener.java @@ -5,7 +5,7 @@ * which accompanies this distribution, and is available at * http://www.eclipse.org/legal/epl-v10.html */ -package org.openhab.binding.mysensors.handler; +package org.openhab.binding.mysensors.internal.event; import java.util.EventListener; @@ -19,6 +19,4 @@ public interface MySensorsUpdateListener extends EventListener { * Procedure for receive status update from MySensorsNetwork. */ public void statusUpdateReceived(MySensorsStatusUpdateEvent event); - - public void disconnectEvent(); } diff --git a/addons/binding/org.openhab.binding.mysensors/src/main/java/org/openhab/binding/mysensors/internal/exception/NoMoreIdsException.java b/addons/binding/org.openhab.binding.mysensors/src/main/java/org/openhab/binding/mysensors/internal/exception/NoMoreIdsException.java new file mode 100644 index 0000000000000..048c879db3801 --- /dev/null +++ b/addons/binding/org.openhab.binding.mysensors/src/main/java/org/openhab/binding/mysensors/internal/exception/NoMoreIdsException.java @@ -0,0 +1,10 @@ +package org.openhab.binding.mysensors.internal.exception; + +public class NoMoreIdsException extends Exception { + + /** + * + */ + private static final long serialVersionUID = -8486356274123302462L; + +} diff --git a/addons/binding/org.openhab.binding.mysensors/src/main/java/org/openhab/binding/mysensors/internal/factory/MySensorsCacheFactory.java b/addons/binding/org.openhab.binding.mysensors/src/main/java/org/openhab/binding/mysensors/internal/factory/MySensorsCacheFactory.java new file mode 100644 index 0000000000000..62ef3669cc679 --- /dev/null +++ b/addons/binding/org.openhab.binding.mysensors/src/main/java/org/openhab/binding/mysensors/internal/factory/MySensorsCacheFactory.java @@ -0,0 +1,108 @@ +package org.openhab.binding.mysensors.internal.factory; + +import java.io.File; +import java.io.FileReader; +import java.io.FileWriter; +import java.io.IOException; +import java.lang.reflect.Type; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.google.gson.Gson; +import com.google.gson.stream.JsonReader; +import com.google.gson.stream.JsonWriter; + +public class MySensorsCacheFactory { + private Logger logger = LoggerFactory.getLogger(getClass()); + + private static MySensorsCacheFactory singleton = null; + + private static final String CACHE_BASE_PATH = "./mysensors/cache"; + private static final String CACHE_FILE_SUFFIX = ".cached"; + + public static final String GIVEN_IDS_CACHE_FILE = "given_ids"; + + private Gson gson = null; + + public static MySensorsCacheFactory getCacheFactory() { + if (singleton == null) { + singleton = new MySensorsCacheFactory(); + } + + return singleton; + } + + private MySensorsCacheFactory() { + gson = new Gson(); + initializeCacheDir(); + } + + private void initializeCacheDir() { + File f = new File(CACHE_BASE_PATH); + if (!f.exists()) { + logger.debug("Creating cache directory..."); + f.mkdirs(); + } + } + + public T readCache(String cacheName, T defaulT, Type clasz) { + return jsonFromFile(cacheName, defaulT, clasz); + } + + public void writeCache(String cacheName, T obj, Type clasz) { + jsonToFile(cacheName, obj, clasz); + } + + private synchronized T jsonFromFile(String fileName, T def, Type clasz) { + + T ret = def; + + try { + File f = new File(CACHE_BASE_PATH + "/" + GIVEN_IDS_CACHE_FILE + CACHE_FILE_SUFFIX); + + if (f.exists()) { + logger.debug("Cache file: {} exist.", GIVEN_IDS_CACHE_FILE + CACHE_FILE_SUFFIX); + JsonReader jReader = new JsonReader(new FileReader(f)); + ret = gson.fromJson(jReader, clasz); + } else { + logger.debug("Cache file: {} not exist.", GIVEN_IDS_CACHE_FILE + CACHE_FILE_SUFFIX); + if (def != null) { + logger.debug("Cache file: {} not exist. Default passed, creating it...", + GIVEN_IDS_CACHE_FILE + CACHE_FILE_SUFFIX); + jsonToFile(fileName, def, clasz); + } else { + logger.warn("Cache file: {} not exist. Default NOT passed, cache won't be created!", + GIVEN_IDS_CACHE_FILE + CACHE_FILE_SUFFIX); + } + } + } catch (Exception e) { + logger.error("Cache reading throws an exception, cause: {} ({})", e.getClass(), e.getMessage()); + } + + logger.debug("Cache ({}) content: {}", GIVEN_IDS_CACHE_FILE, ret); + return ret; + } + + private synchronized void jsonToFile(String fileName, T obj, Type clasz) { + JsonWriter jsonWriter = null; + try { + File f = new File(CACHE_BASE_PATH + "/" + GIVEN_IDS_CACHE_FILE + CACHE_FILE_SUFFIX); + + jsonWriter = new JsonWriter(new FileWriter(f)); + + logger.debug("Writing on cache {}, content: {}", GIVEN_IDS_CACHE_FILE, gson.toJson(obj, clasz)); + gson.toJson(obj, clasz, jsonWriter); + } catch (Exception e) { + logger.error("Cache writing throws an exception, cause: {} ({})", e.getClass(), e.getMessage()); + } finally { + if (jsonWriter != null) { + try { + jsonWriter.close(); + } catch (IOException e) { + logger.error("Cannot close Json writer"); + } + } + } + } +} diff --git a/addons/binding/org.openhab.binding.mysensors/src/main/java/org/openhab/binding/mysensors/internal/MySensorsHandlerFactory.java b/addons/binding/org.openhab.binding.mysensors/src/main/java/org/openhab/binding/mysensors/internal/factory/MySensorsHandlerFactory.java similarity index 83% rename from addons/binding/org.openhab.binding.mysensors/src/main/java/org/openhab/binding/mysensors/internal/MySensorsHandlerFactory.java rename to addons/binding/org.openhab.binding.mysensors/src/main/java/org/openhab/binding/mysensors/internal/factory/MySensorsHandlerFactory.java index e5290f25da373..c1fed65a5d870 100644 --- a/addons/binding/org.openhab.binding.mysensors/src/main/java/org/openhab/binding/mysensors/internal/MySensorsHandlerFactory.java +++ b/addons/binding/org.openhab.binding.mysensors/src/main/java/org/openhab/binding/mysensors/internal/factory/MySensorsHandlerFactory.java @@ -5,7 +5,7 @@ * which accompanies this distribution, and is available at * http://www.eclipse.org/legal/epl-v10.html */ -package org.openhab.binding.mysensors.internal; +package org.openhab.binding.mysensors.internal.factory; import static org.openhab.binding.mysensors.MySensorsBindingConstants.*; @@ -22,9 +22,11 @@ import org.eclipse.smarthome.core.thing.binding.BaseThingHandlerFactory; import org.eclipse.smarthome.core.thing.binding.ThingHandler; import org.openhab.binding.mysensors.discovery.MySensorsDiscoveryService; -import org.openhab.binding.mysensors.handler.MySensorsBridgeHandler; -import org.openhab.binding.mysensors.handler.MySensorsHandler; +import org.openhab.binding.mysensors.internal.handler.MySensorsBridgeHandler; +import org.openhab.binding.mysensors.internal.handler.MySensorsThingHandler; import org.osgi.framework.ServiceRegistration; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; /** * The {@link MySensorsHandlerFactory} is responsible for creating things and thing @@ -34,6 +36,8 @@ */ public class MySensorsHandlerFactory extends BaseThingHandlerFactory { + private Logger logger = LoggerFactory.getLogger(getClass()); + private Map> discoveryServiceRegs = new HashMap<>(); @Override @@ -54,7 +58,7 @@ protected ThingHandler createHandler(Thing thing) { ThingTypeUID thingTypeUID = thing.getThingTypeUID(); if (SUPPORTED_THING_TYPES_UIDS.contains(thingTypeUID)) { - return new MySensorsHandler(thing); + return new MySensorsThingHandler(thing); } if (thingTypeUID.equals(THING_TYPE_BRIDGE_SER) || thingTypeUID.equals(THING_TYPE_BRIDGE_ETH)) { @@ -63,6 +67,8 @@ protected ThingHandler createHandler(Thing thing) { return handler; } + logger.error("Thing {} cannot be configured, is this thing supported by the binding?", thingTypeUID); + return null; } @@ -84,8 +90,15 @@ private void registerDeviceDiscoveryService(MySensorsBridgeHandler mySensorsBrid .registerService(DiscoveryService.class.getName(), discoveryService, new Hashtable())); } + @Override + public void removeThing(ThingUID thingUID) { + logger.trace("Removing thing: {}", thingUID); + super.removeThing(thingUID); + } + @Override protected void removeHandler(ThingHandler thingHandler) { + logger.trace("Removing handler: {}", thingHandler); if (thingHandler instanceof MySensorsBridgeHandler) { ServiceRegistration serviceReg = this.discoveryServiceRegs.get(thingHandler.getThing().getUID()); if (serviceReg != null) { diff --git a/addons/binding/org.openhab.binding.mysensors/src/main/java/org/openhab/binding/mysensors/internal/handler/MySensorsBridgeHandler.java b/addons/binding/org.openhab.binding.mysensors/src/main/java/org/openhab/binding/mysensors/internal/handler/MySensorsBridgeHandler.java new file mode 100644 index 0000000000000..1a11430cf5ac2 --- /dev/null +++ b/addons/binding/org.openhab.binding.mysensors/src/main/java/org/openhab/binding/mysensors/internal/handler/MySensorsBridgeHandler.java @@ -0,0 +1,268 @@ +/** + * Copyright (c) 2014-2016 openHAB UG (haftungsbeschraenkt) and others. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.openhab.binding.mysensors.internal.handler; + +import static org.openhab.binding.mysensors.MySensorsBindingConstants.*; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Iterator; +import java.util.List; +import java.util.Set; + +import org.eclipse.smarthome.config.core.Configuration; +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.binding.BaseBridgeHandler; +import org.eclipse.smarthome.core.types.Command; +import org.openhab.binding.mysensors.config.MySensorsBridgeConfiguration; +import org.openhab.binding.mysensors.internal.event.MySensorsStatusUpdateEvent; +import org.openhab.binding.mysensors.internal.event.MySensorsUpdateListener; +import org.openhab.binding.mysensors.internal.factory.MySensorsCacheFactory; +import org.openhab.binding.mysensors.internal.protocol.MySensorsBridgeConnection; +import org.openhab.binding.mysensors.internal.protocol.ip.MySensorsIpConnection; +import org.openhab.binding.mysensors.internal.protocol.serial.MySensorsSerialConnection; +import org.openhab.binding.mysensors.internal.sensors.MySensorsDeviceManager; +import org.openhab.binding.mysensors.internal.sensors.MySensorsNode; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.google.gson.reflect.TypeToken; + +/** + * @author Tim Oberföll + * + * MySensorsBridgeHandler is used to initialize a new bridge (in MySensors: Gateway) + * The sensors are connected via the gateway/bridge to the controller + */ +public class MySensorsBridgeHandler extends BaseBridgeHandler implements MySensorsUpdateListener { + + private Logger logger = LoggerFactory.getLogger(MySensorsBridgeHandler.class); + + // Bridge connection + private MySensorsBridgeConnection myCon = null; + + // Device manager + private MySensorsDeviceManager myDevManager = null; + + // Configuration from thing file + private MySensorsBridgeConfiguration myConfiguration = null; + + // Cache file + private MySensorsCacheFactory bindingCacheFile = null; + + public MySensorsBridgeHandler(Bridge bridge) { + super(bridge); + } + + /* + * (non-Javadoc) + * + * @see org.eclipse.smarthome.core.thing.binding.BaseThingHandler#initialize() + */ + @Override + public void initialize() { + logger.debug("Initialization of the MySensors bridge"); + + myConfiguration = getConfigAs(MySensorsBridgeConfiguration.class); + + if (getThing().getThingTypeUID().equals(THING_TYPE_BRIDGE_SER)) { + myCon = new MySensorsSerialConnection(this, myConfiguration.serialPort, myConfiguration.baudRate, + myConfiguration.sendDelay); + } else if (getThing().getThingTypeUID().equals(THING_TYPE_BRIDGE_ETH)) { + myCon = new MySensorsIpConnection(this, myConfiguration.ipAddress, myConfiguration.tcpPort, + myConfiguration.sendDelay); + } else { + logger.error("Not recognized bridge: {}", getThing().getThingTypeUID()); + } + + if (myCon != null) { + myCon.initialize(); + myCon.addEventListener(this); + } + + myDevManager = new MySensorsDeviceManager(myCon, loadCacheFile()); + myCon.addEventListener(myDevManager); + + logger.debug("Initialization of the MySensors bridge DONE!"); + } + + /* + * (non-Javadoc) + * + * @see org.eclipse.smarthome.core.thing.binding.BaseThingHandler#dispose() + */ + @Override + public void dispose() { + logger.debug("Disposing of the MySensors bridge"); + if (myCon != null) { + myCon.removeEventListener(myDevManager); + myCon.removeEventListener(this); + myCon.destroy(); + } + + saveCacheFile(); + + super.dispose(); + } + + /* + * (non-Javadoc) + * + * @see + * org.eclipse.smarthome.core.thing.binding.ThingHandler#handleCommand(org.eclipse.smarthome.core.thing.ChannelUID, + * org.eclipse.smarthome.core.types.Command) + */ + @Override + public void handleCommand(ChannelUID channelUID, Command command) { + + } + + public MySensorsBridgeConfiguration getBridgeConfiguration() { + return myConfiguration; + } + + public MySensorsBridgeConnection getBridgeConnection() { + return myCon; + } + + public MySensorsDeviceManager getDeviceManager() { + return myDevManager; + } + + @Override + public void statusUpdateReceived(MySensorsStatusUpdateEvent event) { + switch (event.getEventType()) { + case NEW_NODE_DISCOVERED: + updateCacheFile((MySensorsNode) event.getData()); + break; + case BRIDGE_STATUS_UPDATE: + if (((MySensorsBridgeConnection) event.getData()).isConnected()) { + updateStatus(ThingStatus.ONLINE); + } else { + updateStatus(ThingStatus.OFFLINE); + } + break; + default: + break; + } + + } + + private List loadCacheFile() { + MySensorsCacheFactory cacheFactory = MySensorsCacheFactory.getCacheFactory(); + List nodes = new ArrayList(); + + List givenIds = cacheFactory.readCache(MySensorsCacheFactory.GIVEN_IDS_CACHE_FILE, + new ArrayList(), new TypeToken>() { + }.getType()); + + // Add ids taken by Thing list of OpenHAB + Collection thingList = thingRegistry.getAll(); + Iterator iterator = thingList.iterator(); + while (iterator.hasNext()) { + Thing thing = iterator.next(); + Configuration conf = thing.getConfiguration(); + if (conf != null) { + Object nodeIdobj = conf.get("nodeId"); + if (nodeIdobj != null) { + int nodeId = Integer.parseInt(nodeIdobj.toString()); + if (!givenIds.contains(nodeId)) { + givenIds.add(nodeId); + } + } + } + } + + for (Integer i : givenIds) { + if (i != null) { + nodes.add(new MySensorsNode(i)); + } + } + + return nodes; + } + + private void updateCacheFile(MySensorsNode newNode) { + + MySensorsCacheFactory cacheFactory = MySensorsCacheFactory.getCacheFactory(); + + List givenIds = cacheFactory.readCache(MySensorsCacheFactory.GIVEN_IDS_CACHE_FILE, + new ArrayList(), new TypeToken>() { + }.getType()); + + // Add ids taken by Thing list of OpenHAB + Collection thingList = thingRegistry.getAll(); + Iterator iterator = thingList.iterator(); + + while (iterator.hasNext()) { + Thing thing = iterator.next(); + Configuration conf = thing.getConfiguration(); + if (conf != null) { + Object nodeIdobj = conf.get("nodeId"); + if (nodeIdobj != null) { + int nodeId = Integer.parseInt(nodeIdobj.toString()); + if (!givenIds.contains(nodeId)) { + givenIds.add(nodeId); + } + } + } + } + + if (newNode != null) { + givenIds.add(newNode.getNodeId()); + } + + cacheFactory.writeCache(MySensorsCacheFactory.GIVEN_IDS_CACHE_FILE, givenIds.toArray(new Integer[] {}), + Integer[].class); + + } + + private void saveCacheFile() { + + if (myDevManager != null) { + + MySensorsCacheFactory cacheFactory = MySensorsCacheFactory.getCacheFactory(); + + List givenIds = cacheFactory.readCache(MySensorsCacheFactory.GIVEN_IDS_CACHE_FILE, + new ArrayList(), new TypeToken>() { + }.getType()); + + // Add ids taken by Thing list of OpenHAB + Collection thingList = thingRegistry.getAll(); + Iterator iterator = thingList.iterator(); + + while (iterator.hasNext()) { + Thing thing = iterator.next(); + Configuration conf = thing.getConfiguration(); + if (conf != null) { + Object nodeIdobj = conf.get("nodeId"); + if (nodeIdobj != null) { + int nodeId = Integer.parseInt(nodeIdobj.toString()); + if (!givenIds.contains(nodeId)) { + givenIds.add(nodeId); + } + } + } + } + + Set onDeviceManager = getDeviceManager().getGivenIds(); + for (Integer i : onDeviceManager) { + if (i != null && !givenIds.contains(i)) { + givenIds.add(i); + } + } + + cacheFactory.writeCache(MySensorsCacheFactory.GIVEN_IDS_CACHE_FILE, givenIds.toArray(new Integer[] {}), + Integer[].class); + } + + } +} diff --git a/addons/binding/org.openhab.binding.mysensors/src/main/java/org/openhab/binding/mysensors/handler/MySensorsHandler.java b/addons/binding/org.openhab.binding.mysensors/src/main/java/org/openhab/binding/mysensors/internal/handler/MySensorsThingHandler.java similarity index 55% rename from addons/binding/org.openhab.binding.mysensors/src/main/java/org/openhab/binding/mysensors/handler/MySensorsHandler.java rename to addons/binding/org.openhab.binding.mysensors/src/main/java/org/openhab/binding/mysensors/internal/handler/MySensorsThingHandler.java index 395b37412dfd9..a98f793e8da38 100644 --- a/addons/binding/org.openhab.binding.mysensors/src/main/java/org/openhab/binding/mysensors/handler/MySensorsHandler.java +++ b/addons/binding/org.openhab.binding.mysensors/src/main/java/org/openhab/binding/mysensors/internal/handler/MySensorsThingHandler.java @@ -5,7 +5,7 @@ * which accompanies this distribution, and is available at * http://www.eclipse.org/legal/epl-v10.html */ -package org.openhab.binding.mysensors.handler; +package org.openhab.binding.mysensors.internal.handler; import static org.openhab.binding.mysensors.MySensorsBindingConstants.*; @@ -24,24 +24,29 @@ 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.ThingStatusInfo; import org.eclipse.smarthome.core.thing.binding.BaseThingHandler; -import org.eclipse.smarthome.core.thing.binding.ThingHandler; import org.eclipse.smarthome.core.types.Command; import org.eclipse.smarthome.core.types.RefreshType; import org.openhab.binding.mysensors.config.MySensorsSensorConfiguration; -import org.openhab.binding.mysensors.internal.MySensorsMessage; +import org.openhab.binding.mysensors.internal.event.MySensorsStatusUpdateEvent; +import org.openhab.binding.mysensors.internal.event.MySensorsUpdateListener; +import org.openhab.binding.mysensors.internal.protocol.message.MySensorsMessage; +import org.openhab.binding.mysensors.internal.protocol.message.MySensorsMessageParser; +import org.openhab.binding.mysensors.internal.sensors.MySensorsNode; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** - * The {@link MySensorsHandler} is responsible for handling commands, which are + * The {@link MySensorsThingHandler} is responsible for handling commands, which are * sent to one of the channels. * * @author Tim Oberföll */ -public class MySensorsHandler extends BaseThingHandler implements MySensorsUpdateListener { +public class MySensorsThingHandler extends BaseThingHandler implements MySensorsUpdateListener { - private Logger logger = LoggerFactory.getLogger(MySensorsHandler.class); + private Logger logger = LoggerFactory.getLogger(MySensorsThingHandler.class); private MySensorsSensorConfiguration configuration = null; @@ -54,34 +59,39 @@ public class MySensorsHandler extends BaseThingHandler implements MySensorsUpdat private Map oldMsgContent = new HashMap(); - public MySensorsHandler(Thing thing) { + public MySensorsThingHandler(Thing thing) { super(thing); } @Override public void initialize() { - super.initialize(); - configuration = getConfigAs(MySensorsSensorConfiguration.class); nodeId = Integer.parseInt(configuration.nodeId); childId = Integer.parseInt(configuration.childId); requestAck = configuration.requestAck; revertState = configuration.revertState; - logger.debug( - String.format("Configuration: node %d, chiledId: %d, revertState: %b", nodeId, childId, revertState)); + logger.debug("Configuration: node {}, chiledId: {}, revertState: {}", nodeId, childId, revertState); + if (!getBridgeHandler().getBridgeConnection().isEventListenerRegisterd(this)) { + logger.debug("Event listener for node {}-{} not registered yet, registering...", nodeId, childId); + getBridgeHandler().getBridgeConnection().addEventListener(this); + } updateStatus(ThingStatus.ONLINE); } @Override - public void handleRemoval() { - getBridgeHandler().getBridgeConnection().removeUpdateListener(this); - super.handleRemoval(); - } + public void bridgeStatusChanged(ThingStatusInfo bridgeStatusInfo) { + logger.debug("MySensors Bridge Status updated to {} for device: {}", bridgeStatusInfo.getStatus().toString(), + getThing().getUID().toString()); + if (bridgeStatusInfo.getStatus().equals(ThingStatus.ONLINE) + || bridgeStatusInfo.getStatus().equals(ThingStatus.OFFLINE)) { + if (!getBridgeHandler().getBridgeConnection().isEventListenerRegisterd(this)) { + logger.debug("Event listener for node {}-{} not registered yet, registering...", nodeId, childId); + getBridgeHandler().getBridgeConnection().addEventListener(this); + } - @Override - public void bridgeHandlerDisposed(ThingHandler thingHandler, Bridge bridge) { - updateStatus(ThingStatus.OFFLINE); - super.bridgeHandlerDisposed(thingHandler, bridge); + // the node has the same status of the bridge + updateStatus(bridgeStatusInfo.getStatus()); + } } /* @@ -108,7 +118,16 @@ public void handleCommand(ChannelUID channelUID, Command command) { if (requestAck) { int_requestack = 1; } - if (channelUID.getId().equals(CHANNEL_STATUS)) { + + // just forward the message in case it is received via this channel. This is special! + if (channelUID.getId().equals(CHANNEL_MYSENSORS_MESSAGE)) { + if (command instanceof StringType) { + StringType stringTypeMessage = (StringType) command; + MySensorsMessage msg = MySensorsMessageParser.parse(stringTypeMessage.toString()); + getBridgeHandler().getBridgeConnection().addMySensorsOutboundMessage(msg); + return; + } + } else if (channelUID.getId().equals(CHANNEL_STATUS)) { subType = MYSENSORS_SUBTYPE_V_STATUS; @@ -190,6 +209,16 @@ public void handleCommand(ChannelUID channelUID, Command command) { } else if (channelUID.getId().equals(CHANNEL_VOLUME)) { subType = MYSENSORS_SUBTYPE_V_VOLUME; msgPayload = command.toString(); + } else if (channelUID.getId().equals(CHANNEL_TEXT)) { + subType = MYSENSORS_SUBTYPE_V_TEXT; + msgPayload = command.toString(); + } else if (channelUID.getId().equals(CHANNEL_IR_SEND)) { + subType = MYSENSORS_SUBTYPE_V_IR_SEND; + msgPayload = command.toString(); + } else if (channelUID.getId().equals(CHANNEL_IR_RECEIVE)) { + subType = MYSENSORS_SUBTYPE_V_IR_RECEIVE; + msgPayload = command.toString(); + } else { msgPayload = ""; } @@ -215,7 +244,7 @@ public void handleCommand(ChannelUID channelUID, Command command) { */ @Override public void handleUpdate(ChannelUID channelUID, org.eclipse.smarthome.core.types.State newState) { - logger.debug("handleUpdate called"); + // logger.debug("handleUpdate called"); } /* @@ -227,10 +256,43 @@ public void handleUpdate(ChannelUID channelUID, org.eclipse.smarthome.core.types */ @Override public void statusUpdateReceived(MySensorsStatusUpdateEvent event) { - MySensorsMessage msg = event.getData(); + switch (event.getEventType()) { + case INCOMING_MESSAGE: + handleIncomingMessageEvent((MySensorsMessage) event.getData()); + break; + case NODE_STATUS_UPDATE: + // TODO Network Sanity Checker could put node to 'unreachable' causing, here, to set this thing to + // OFFLINE + if (!((MySensorsNode) event.getData()).isReachable()) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR); + } + break; + default: + break; + } + } + + /** + * Returns the BridgeHandler of the bridge/gateway to the MySensors network + * + * @return BridgeHandler of the bridge/gateway to the MySensors network + */ + private synchronized MySensorsBridgeHandler getBridgeHandler() { + MySensorsBridgeHandler myBridgeHandler = null; + + Bridge bridge = getBridge(); + myBridgeHandler = (MySensorsBridgeHandler) bridge.getHandler(); + + return myBridgeHandler; + } - // or is this an update message? - if (nodeId == msg.getNodeId()) { // is this message for me? + private void handleIncomingMessageEvent(MySensorsMessage msg) { + // Am I the all knowing node that receives all messages? + if (nodeId == 999 && childId == 999) { + updateState(CHANNEL_MYSENSORS_MESSAGE, + new StringType(MySensorsMessageParser.generateAPIString(msg).replaceAll("(\\r|\\n)", ""))); + + } else if (nodeId == msg.getNodeId()) { // is this message for me? updateLastUpdate(); @@ -248,38 +310,88 @@ public void statusUpdateReceived(MySensorsStatusUpdateEvent event) { if (childId == msg.getChildId()) { // which child should be updated? if (CHANNEL_MAP.containsKey(msg.getSubType())) { String channel = CHANNEL_MAP.get(msg.getSubType()); - if (channel.equals(CHANNEL_BARO)) { - updateState(channel, new StringType(msg.getMsg())); - } else if (channel.equals(CHANNEL_STATUS)) { - if (msg.getMsg().equals("1")) { - updateState(channel, OnOffType.ON); - } else if (msg.getMsg().equals("0")) { - updateState(channel, OnOffType.OFF); - } - } else if (channel.equals(CHANNEL_ARMED) || channel.equals(CHANNEL_TRIPPED)) { - if (msg.getMsg().equals("1")) { - updateState(channel, OpenClosedType.OPEN); - } else { - updateState(channel, OpenClosedType.CLOSED); - } - } else if (channel.equals(CHANNEL_DIMMER)) { - updateState(channel, new PercentType(msg.getMsg())); - } else if (channel.equals(CHANNEL_COVER)) { - if (msg.getMsg().equals(MYSENSORS_SUBTYPE_V_UP)) { - updateState(channel, UpDownType.UP); - } else if (msg.getMsg().equals(MYSENSORS_SUBTYPE_V_DOWN)) { - updateState(channel, UpDownType.DOWN); - } - } else if (channel.equals(CHANNEL_HVAC_FLOW_STATE)) { - updateState(channel, new StringType(msg.getMsg())); - } else if (channel.equals(CHANNEL_HVAC_FLOW_MODE)) { - updateState(channel, new StringType(msg.getMsg())); - } else if (channel.equals(CHANNEL_HVAC_SPEED)) { - updateState(channel, new StringType(msg.getMsg())); - } else { - updateState(channel, new DecimalType(msg.getMsg())); + + switch (channel) { + case CHANNEL_STATUS: + if (msg.getMsg().equals("1")) { + updateState(channel, OnOffType.ON); + } else if (msg.getMsg().equals("0")) { + updateState(channel, OnOffType.OFF); + } + break; + + case CHANNEL_ARMED: + case CHANNEL_TRIPPED: + case CHANNEL_LOCK_STATUS: + if (msg.getMsg().equals("1")) { + updateState(channel, OpenClosedType.OPEN); + } else { + updateState(channel, OpenClosedType.CLOSED); + } + break; + + case CHANNEL_DIMMER: + updateState(channel, new PercentType(msg.getMsg())); + break; + + case CHANNEL_BARO: + case CHANNEL_HVAC_FLOW_STATE: + case CHANNEL_HVAC_FLOW_MODE: + case CHANNEL_HVAC_SPEED: + case CHANNEL_TEXT: + case CHANNEL_IR_SEND: + case CHANNEL_IR_RECEIVE: + case CHANNEL_GUST: + case CHANNEL_RAIN: + case CHANNEL_VAR1: + case CHANNEL_VAR2: + case CHANNEL_VAR3: + case CHANNEL_VAR4: + case CHANNEL_VAR5: + case CHANNEL_RGB: + case CHANNEL_RGBW: + case CHANNEL_CUSTOM: + case CHANNEL_POSITION: + case CHANNEL_IR_RECORD: + updateState(channel, new StringType(msg.getMsg())); + break; + + case CHANNEL_COVER: + if (msg.getMsg().equals(MYSENSORS_SUBTYPE_V_UP)) { + updateState(channel, UpDownType.UP); + } else if (msg.getMsg().equals(MYSENSORS_SUBTYPE_V_DOWN)) { + updateState(channel, UpDownType.DOWN); + } + break; + + case CHANNEL_TEMP: + case CHANNEL_HUM: + case CHANNEL_VOLT: + case CHANNEL_WATT: + case CHANNEL_KWH: + case CHANNEL_PRESSURE: + case CHANNEL_WIND: + case CHANNEL_UV: + case CHANNEL_RAINRATE: + case CHANNEL_WEIGHT: + case CHANNEL_IMPEDANCE: + case CHANNEL_CURRENT: + case CHANNEL_DISTANCE: + case CHANNEL_LIGHT_LEVEL: + case CHANNEL_FLOW: + case CHANNEL_VOLUME: + case CHANNEL_LEVEL: + case CHANNEL_PH: + updateState(channel, new DecimalType(msg.getMsg())); + break; + + default: + updateState(channel, new DecimalType(msg.getMsg())); + } + oldMsgContent.put(msg.getSubType(), msg.getMsg()); + } } } else if (msg.getMsgType() == MYSENSORS_MSG_TYPE_REQ) { @@ -298,32 +410,6 @@ public void statusUpdateReceived(MySensorsStatusUpdateEvent event) { } } - /** - * Returns the BridgeHandler of the bridge/gateway to the MySensors network - * - * @return BridgeHandler of the bridge/gateway to the MySensors network - */ - public synchronized MySensorsBridgeHandler getBridgeHandler() { - MySensorsBridgeHandler myBridgeHandler = null; - - Bridge bridge = getBridge(); - myBridgeHandler = (MySensorsBridgeHandler) bridge.getHandler(); - - return myBridgeHandler; - } - - @Override - public void bridgeHandlerInitialized(ThingHandler thingHandler, Bridge bridge) { - MySensorsBridgeHandler bridgeHandler = (MySensorsBridgeHandler) thingHandler; - if (bridgeHandler.getBridgeConnection() == null) { - logger.warn("Bridge connection not estblished yet - can't subscribe for node: {} child: {}", nodeId, - childId); - } else { - logger.debug("Bridge connection estblished - subscribing update for node: {} child: {}", nodeId, childId); - bridgeHandler.getBridgeConnection().addUpdateListener(this); - } - } - private void updateLastUpdate() { // Don't always fire last update channel, do it only after a minute by if (lastUpdate == null || (System.currentTimeMillis() > (lastUpdate.getCalendar().getTimeInMillis() + 60000))) { @@ -333,9 +419,4 @@ private void updateLastUpdate() { logger.debug("Setting last update for node {} to {}", nodeId, dt.toString()); } } - - @Override - public void disconnectEvent() { - - } } diff --git a/addons/binding/org.openhab.binding.mysensors/src/main/java/org/openhab/binding/mysensors/internal/protocol/MySensorsBridgeConnection.java b/addons/binding/org.openhab.binding.mysensors/src/main/java/org/openhab/binding/mysensors/internal/protocol/MySensorsBridgeConnection.java new file mode 100644 index 0000000000000..05737a7aad666 --- /dev/null +++ b/addons/binding/org.openhab.binding.mysensors/src/main/java/org/openhab/binding/mysensors/internal/protocol/MySensorsBridgeConnection.java @@ -0,0 +1,417 @@ +/** + * Copyright (c) 2014-2016 openHAB UG (haftungsbeschraenkt) and others. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.openhab.binding.mysensors.internal.protocol; + +import static org.openhab.binding.mysensors.MySensorsBindingConstants.*; + +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; + +import org.openhab.binding.mysensors.MySensorsBindingConstants; +import org.openhab.binding.mysensors.discovery.MySensorsDiscoveryService; +import org.openhab.binding.mysensors.internal.event.MySensorsEventType; +import org.openhab.binding.mysensors.internal.event.MySensorsStatusUpdateEvent; +import org.openhab.binding.mysensors.internal.event.MySensorsUpdateListener; +import org.openhab.binding.mysensors.internal.handler.MySensorsBridgeHandler; +import org.openhab.binding.mysensors.internal.protocol.message.MySensorsMessage; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public abstract class MySensorsBridgeConnection implements Runnable, MySensorsUpdateListener { + + private Logger logger = LoggerFactory.getLogger(getClass()); + + // Connector will check for connection status every CONNECTOR_INTERVAL_CHECK seconds + public static final int CONNECTOR_INTERVAL_CHECK = 10; + + // ?? + private boolean pauseWriter = false; + + // Blocking queue wait for message + private BlockingQueue outboundMessageQueue = null; + + // Flag setted to true while connection is up + private boolean connected = false; + + // Flag to be set (through available method below) + private boolean requestDisconnection = false; + + private MySensorsBridgeConnection waitingObj = null; + + // I_VERSION response flag + private boolean iVersionResponse = false; + + // Check connection on startup flag + private boolean skipStartupCheck = false; + + // Reader and writer thread + protected MySensorsWriter mysConWriter = null; + protected MySensorsReader mysConReader = null; + + // Bridge handler dependency + private MySensorsBridgeHandler bridgeHandler = null; + + // Sanity checker + private MySensorsNetworkSanityChecker netSanityChecker = null; + + // Connection retry done + private int numOfRetry = 0; + + // Update listener + private List updateListeners = null; + + // Connection status watchdog + private ScheduledExecutorService watchdogExecutor = null; + private Future futureWatchdog = null; + + public MySensorsBridgeConnection(MySensorsBridgeHandler bridgeHandler) { + this.outboundMessageQueue = new LinkedBlockingQueue(); + this.bridgeHandler = bridgeHandler; + this.updateListeners = new ArrayList<>(); + this.watchdogExecutor = Executors.newSingleThreadScheduledExecutor(); + this.iVersionResponse = false; + } + + public void initialize() { + logger.debug("Set skip check on startup to: {}", bridgeHandler.getBridgeConfiguration().skipStartupCheck); + skipStartupCheck = bridgeHandler.getBridgeConfiguration().skipStartupCheck; + + // Launch connection watchdog + logger.debug("Enabling connection watchdog"); + futureWatchdog = watchdogExecutor.scheduleWithFixedDelay(this, 0, CONNECTOR_INTERVAL_CHECK, TimeUnit.SECONDS); + } + + @Override + public void run() { + Thread.currentThread().setName(MySensorsBridgeConnection.class.getName()); + + if (requestingDisconnection()) { + logger.info("Connection request disconnection..."); + requestDisconnection(false); + disconnect(); + } + + if (!connected) { + if (connect()) { + logger.info("Successfully connected to MySensors Bridge."); + + numOfRetry = 0; + + // Start discovery service + MySensorsDiscoveryService discoveryService = new MySensorsDiscoveryService(bridgeHandler); + discoveryService.activate(); + + if (bridgeHandler.getBridgeConfiguration().enableNetworkSanCheck) { + + // Start network sanity check + netSanityChecker = new MySensorsNetworkSanityChecker(this); + netSanityChecker.start(); + + } else { + logger.warn("Network Sanity Checker thread disabled from bridge configuration"); + } + + } else { + logger.error("Failed connecting to bridge...next retry in {} seconds (Retry No.:{})", + CONNECTOR_INTERVAL_CHECK, numOfRetry); + numOfRetry++; + disconnect(); + } + + } else { + logger.debug("Bridge is connected, connection skipped"); + } + + } + + /** + * Startup connection with bridge + * + * @return true, if connection established correctly + */ + private boolean connect() { + connected = _connect(); + broadCastEvent(new MySensorsStatusUpdateEvent(MySensorsEventType.BRIDGE_STATUS_UPDATE, this)); + return connected; + } + + protected abstract boolean _connect(); + + /** + * Shutdown method that allows the correct disconnection with the used bridge + */ + private void disconnect() { + + if (netSanityChecker != null) { + netSanityChecker.stop(); + netSanityChecker = null; + } + + _disconnect(); + connected = false; + requestDisconnection = false; + iVersionResponse = false; + + broadCastEvent(new MySensorsStatusUpdateEvent(MySensorsEventType.BRIDGE_STATUS_UPDATE, this)); + } + + protected abstract void _disconnect(); + + public void destroy() { + logger.debug("Destroying connection"); + + if (connected) { + disconnect(); + } + + if (futureWatchdog != null) { + futureWatchdog.cancel(true); + futureWatchdog = null; + } + + if (watchdogExecutor != null) { + watchdogExecutor.shutdown(); + watchdogExecutor.shutdownNow(); + } + } + + /** + * Start thread managing the incoming/outgoing messages. It also have the task to test the connection to gateway by + * sending a special message (I_VERSION) to it + * + * @return true if the gateway test pass successfully + */ + protected boolean startReaderWriterThread(MySensorsReader reader, MySensorsWriter writer) { + + reader.startReader(); + writer.startWriter(); + + addEventListener(this); + if (!skipStartupCheck) { + try { + int i = 0; + synchronized (this) { + while (!iVersionResponse && i < 5) { + addMySensorsOutboundMessage(MySensorsBindingConstants.I_VERSION_MESSAGE); + waitingObj = this; + waitingObj.wait(1000); + i++; + } + } + } catch (Exception e) { + logger.error("Exception on waiting for I_VERSION message", e); + } + } else { + logger.warn("Skipping I_VERSION connection test, not recommended..."); + iVersionResponse = true; + } + + if (!iVersionResponse) { + logger.error( + "Cannot start reading/writing thread, probably sync message (I_VERSION) not received. Try set skipStartupCheck to true"); + } + + return iVersionResponse; + } + + public void addMySensorsOutboundMessage(MySensorsMessage msg) { + addMySensorsOutboundMessage(msg, 1); + } + + public void addMySensorsOutboundMessage(MySensorsMessage msg, int copy) { + synchronized (outboundMessageQueue) { + try { + for (int i = 0; i < copy; i++) { + outboundMessageQueue.put(msg); + } + } catch (InterruptedException e) { + logger.error("Interrupted message while ruuning"); + } + } + + } + + public MySensorsMessage pollMySensorsOutboundQueue() throws InterruptedException { + return outboundMessageQueue.poll(1, TimeUnit.DAYS); + } + + public boolean isEventListenerRegisterd(MySensorsUpdateListener listener) { + boolean ret = false; + synchronized (updateListeners) { + ret = updateListeners.contains(listener); + } + + return ret; + } + + /** + * @param listener An Object, that wants to listen on status updates + */ + public void addEventListener(MySensorsUpdateListener listener) { + synchronized (updateListeners) { + if (!updateListeners.contains(listener)) { + logger.trace("Adding listener: " + listener); + updateListeners.add(listener); + } + } + } + + public void removeEventListener(MySensorsUpdateListener listener) { + synchronized (updateListeners) { + if (updateListeners.contains(listener)) { + logger.trace("Removing listener: " + listener); + updateListeners.remove(listener); + } + } + } + + public List getEventListeners() { + return updateListeners; + } + + public void broadCastEvent(MySensorsStatusUpdateEvent event) { + synchronized (updateListeners) { + for (MySensorsUpdateListener mySensorsEventListener : updateListeners) { + logger.trace("Broadcasting event to: " + mySensorsEventListener); + mySensorsEventListener.statusUpdateReceived(event); + } + } + } + + public void removeMySensorsOutboundMessage(MySensorsMessage msg) { + + pauseWriter = true; + + Iterator iterator = outboundMessageQueue.iterator(); + if (iterator != null) { + while (iterator.hasNext()) { + MySensorsMessage msgInQueue = iterator.next(); + // logger.debug("Msg in Queue: " + msgInQueue.getDebugInfo()); + if (msgInQueue.getNodeId() == msg.getNodeId() && msgInQueue.getChildId() == msg.getChildId() + && msgInQueue.getMsgType() == msg.getMsgType() && msgInQueue.getSubType() == msg.getSubType() + && msgInQueue.getAck() == msg.getAck() && msgInQueue.getMsg().equals(msg.getMsg())) { + iterator.remove(); + // logger.debug("Message removed: " + msg.getDebugInfo()); + } else { + logger.debug("Message NOT removed: " + msg.getDebugInfo()); + } + } + } + + pauseWriter = false; + } + + public boolean isWriterPaused() { + return pauseWriter; + } + + public boolean isConnected() { + return connected; + } + + public boolean requestingDisconnection() { + return requestDisconnection; + } + + public void requestDisconnection(boolean flag) { + logger.debug("Request disconnection flag setted to: " + flag); + requestDisconnection = flag; + } + + /** + * Wake up main thread that is waiting for confirmation of link up + */ + private void handleIncomingVersionMessage(String message) { + iVersionMessageReceived(message); + } + + private void iVersionMessageReceived(String msg) { + if (waitingObj != null) { + logger.debug("Good,Gateway is up and running! (Ver:{})", msg); + synchronized (waitingObj) { + iVersionResponse = true; + waitingObj.notifyAll(); + waitingObj = null; + } + } + } + + private void handleIncomingMessageEvent(MySensorsMessage msg) { + // Do we get an ACK? + if (msg.getAck() == 1) { + logger.debug(String.format("ACK received! Node: %d, Child: %d", msg.nodeId, msg.childId)); + removeMySensorsOutboundMessage(msg); + } + + // Have we get a I_CONFIG message? + if (msg.isIConfigMessage()) { + answerIConfigMessage(msg); + } + + // Have we get a I_TIME message? + if (msg.isITimeMessage()) { + answerITimeMessage(msg); + } + + // Have we get a I_VERSION message? + if (msg.isIVersionMessage()) { + handleIncomingVersionMessage(msg.msg); + } + } + + @Override + public void statusUpdateReceived(MySensorsStatusUpdateEvent event) { + switch (event.getEventType()) { + case INCOMING_MESSAGE: + handleIncomingMessageEvent((MySensorsMessage) event.getData()); + break; + default: + break; + } + } + + /** + * Answer to I_TIME message for gateway time request from sensor + * + * @param msg, the incoming I_TIME message from sensor + */ + private void answerITimeMessage(MySensorsMessage msg) { + logger.info("I_TIME request received from {}, answering...", msg.nodeId); + + String time = Long.toString(System.currentTimeMillis() / 1000); + MySensorsMessage newMsg = new MySensorsMessage(msg.nodeId, msg.childId, MYSENSORS_MSG_TYPE_INTERNAL, 0, false, + MYSENSORS_SUBTYPE_I_TIME, time); + addMySensorsOutboundMessage(newMsg); + + } + + /** + * Answer to I_CONFIG message for imperial/metric request from sensor + * + * @param msg, the incoming I_CONFIG message from sensor + */ + private void answerIConfigMessage(MySensorsMessage msg) { + boolean imperial = bridgeHandler.getBridgeConfiguration().imperial; + String iConfig = imperial ? "I" : "M"; + + logger.debug("I_CONFIG request received from {}, answering: (is imperial?){}", iConfig, imperial); + + MySensorsMessage newMsg = new MySensorsMessage(msg.nodeId, msg.childId, MYSENSORS_MSG_TYPE_INTERNAL, 0, false, + MYSENSORS_SUBTYPE_I_CONFIG, iConfig); + addMySensorsOutboundMessage(newMsg); + + } + +} diff --git a/addons/binding/org.openhab.binding.mysensors/src/main/java/org/openhab/binding/mysensors/internal/protocol/MySensorsNetworkSanityChecker.java b/addons/binding/org.openhab.binding.mysensors/src/main/java/org/openhab/binding/mysensors/internal/protocol/MySensorsNetworkSanityChecker.java new file mode 100644 index 0000000000000..20d56ef53c80f --- /dev/null +++ b/addons/binding/org.openhab.binding.mysensors/src/main/java/org/openhab/binding/mysensors/internal/protocol/MySensorsNetworkSanityChecker.java @@ -0,0 +1,122 @@ +/** + * Copyright (c) 2014-2016 openHAB UG (haftungsbeschraenkt) and others. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.openhab.binding.mysensors.internal.protocol; + +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.TimeUnit; + +import org.openhab.binding.mysensors.MySensorsBindingConstants; +import org.openhab.binding.mysensors.internal.event.MySensorsEventType; +import org.openhab.binding.mysensors.internal.event.MySensorsStatusUpdateEvent; +import org.openhab.binding.mysensors.internal.event.MySensorsUpdateListener; +import org.openhab.binding.mysensors.internal.protocol.message.MySensorsMessage; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class MySensorsNetworkSanityChecker implements MySensorsUpdateListener, Runnable { + + private Logger logger = LoggerFactory.getLogger(getClass()); + + private static final int SHEDULE_MINUTES_DELAY = 1; // only for test will be: 3 + private static final int MAX_ATTEMPTS_BEFORE_DISCONNECT = 1; // only for test will be: 3 + + private MySensorsBridgeConnection bridgeConnection = null; + + private ScheduledExecutorService scheduler = null; + private ScheduledFuture future = null; + + private Integer iVersionMessageMissing = 0; + private boolean iVersionMessageArrived = false; + + public MySensorsNetworkSanityChecker(MySensorsBridgeConnection bridgeConnection) { + this.bridgeConnection = bridgeConnection; + } + + public void reset() { + synchronized (iVersionMessageMissing) { + iVersionMessageArrived = false; + iVersionMessageMissing = 0; + } + } + + public void start() { + reset(); + + scheduler = Executors.newSingleThreadScheduledExecutor(); + future = scheduler.scheduleWithFixedDelay(this, SHEDULE_MINUTES_DELAY, SHEDULE_MINUTES_DELAY, TimeUnit.MINUTES); + + } + + public void stop() { + logger.info("Network Sanity Checker thread stopped"); + + if (future != null) { + future.cancel(true); + future = null; + } + + if (scheduler != null) { + scheduler.shutdown(); + scheduler.shutdownNow(); + scheduler = null; + } + + } + + @Override + public void run() { + Thread.currentThread().setName(MySensorsNetworkSanityChecker.class.getName()); + + try { + bridgeConnection.addEventListener(this); + + bridgeConnection.addMySensorsOutboundMessage(MySensorsBindingConstants.I_VERSION_MESSAGE); + + Thread.sleep(3000); + + synchronized (iVersionMessageMissing) { + if (!iVersionMessageArrived) { + logger.warn("I_VERSION message response is not arrived. Remained attempts before disconnection {}", + MAX_ATTEMPTS_BEFORE_DISCONNECT - iVersionMessageMissing); + + if ((MAX_ATTEMPTS_BEFORE_DISCONNECT - iVersionMessageMissing) <= 0) { + logger.error("Retry period expired, gateway is down. Disconneting bridge..."); + + bridgeConnection.requestDisconnection(true); + + } else { + iVersionMessageMissing++; + } + } else { + logger.debug("Network sanity check: PASSED"); + iVersionMessageMissing = 0; + } + + iVersionMessageArrived = false; + } + + } catch (InterruptedException e) { + logger.error("interrupted exception in network sanity thread checker"); + } finally { + bridgeConnection.removeEventListener(this); + } + } + + @Override + public void statusUpdateReceived(MySensorsStatusUpdateEvent event) { + if (event.getEventType() == MySensorsEventType.INCOMING_MESSAGE) { + synchronized (iVersionMessageMissing) { + if (!iVersionMessageArrived) { + iVersionMessageArrived = ((MySensorsMessage) event.getData()).isIVersionMessage(); + } + } + } + } +} \ No newline at end of file diff --git a/addons/binding/org.openhab.binding.mysensors/src/main/java/org/openhab/binding/mysensors/protocol/MySensorsReader.java b/addons/binding/org.openhab.binding.mysensors/src/main/java/org/openhab/binding/mysensors/internal/protocol/MySensorsReader.java similarity index 71% rename from addons/binding/org.openhab.binding.mysensors/src/main/java/org/openhab/binding/mysensors/protocol/MySensorsReader.java rename to addons/binding/org.openhab.binding.mysensors/src/main/java/org/openhab/binding/mysensors/internal/protocol/MySensorsReader.java index b832934c698f3..03f6f814860f2 100644 --- a/addons/binding/org.openhab.binding.mysensors/src/main/java/org/openhab/binding/mysensors/protocol/MySensorsReader.java +++ b/addons/binding/org.openhab.binding.mysensors/src/main/java/org/openhab/binding/mysensors/internal/protocol/MySensorsReader.java @@ -5,7 +5,7 @@ * which accompanies this distribution, and is available at * http://www.eclipse.org/legal/epl-v10.html */ -package org.openhab.binding.mysensors.protocol; +package org.openhab.binding.mysensors.internal.protocol; import java.io.BufferedReader; import java.io.IOException; @@ -14,11 +14,11 @@ import java.util.concurrent.Executors; import java.util.concurrent.Future; -import org.openhab.binding.mysensors.handler.MySensorsStatusUpdateEvent; -import org.openhab.binding.mysensors.handler.MySensorsUpdateListener; -import org.openhab.binding.mysensors.internal.MySensorsBridgeConnection; -import org.openhab.binding.mysensors.internal.MySensorsMessage; -import org.openhab.binding.mysensors.internal.MySensorsMessageParser; +import org.openhab.binding.mysensors.internal.event.MySensorsEventType; +import org.openhab.binding.mysensors.internal.event.MySensorsStatusUpdateEvent; +import org.openhab.binding.mysensors.internal.event.MySensorsUpdateListener; +import org.openhab.binding.mysensors.internal.protocol.message.MySensorsMessage; +import org.openhab.binding.mysensors.internal.protocol.message.MySensorsMessageParser; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -41,6 +41,7 @@ public void startReader() { @Override public void run() { + Thread.currentThread().setName(MySensorsReader.class.getName()); String line = null; while (!stopReader) { @@ -55,17 +56,17 @@ public void run() { // We lost connection if (line == null) { - broadCastDisconnect(); + logger.warn("Connection to Gateway lost!"); + mysCon.requestDisconnection(true); break; } logger.debug(line); MySensorsMessage msg = MySensorsMessageParser.parse(line); if (msg != null) { - MySensorsStatusUpdateEvent event = new MySensorsStatusUpdateEvent(msg); - for (MySensorsUpdateListener mySensorsEventListener : mysCon.updateListeners) { - mySensorsEventListener.statusUpdateReceived(event); - } + MySensorsStatusUpdateEvent event = new MySensorsStatusUpdateEvent( + MySensorsEventType.INCOMING_MESSAGE, msg); + mysCon.broadCastEvent(event); } } catch (Exception e) { logger.error("({}) on reading from serial port, message: {}", e, getClass(), e.getMessage()); @@ -75,15 +76,6 @@ public void run() { } - private void broadCastDisconnect() { - logger.warn("Connection to Gateway lost!"); - stopReader(); - - for (MySensorsUpdateListener mySensorsEventListener : mysCon.updateListeners) { - mySensorsEventListener.disconnectEvent(); - } - } - public void stopReader() { logger.debug("Stopping Reader thread"); @@ -92,20 +84,24 @@ public void stopReader() { if (future != null) { future.cancel(true); + future = null; } if (executor != null) { executor.shutdown(); executor.shutdownNow(); + executor = null; } try { if (reads != null) { reads.close(); + reads = null; } if (inStream != null) { inStream.close(); + inStream = null; } } catch (IOException e) { logger.error("Cannot close reader stream"); @@ -115,13 +111,7 @@ public void stopReader() { @Override public void statusUpdateReceived(MySensorsStatusUpdateEvent event) { - // TODO Auto-generated method stub } - @Override - public void disconnectEvent() { - stopReader(); - } - } diff --git a/addons/binding/org.openhab.binding.mysensors/src/main/java/org/openhab/binding/mysensors/protocol/MySensorsWriter.java b/addons/binding/org.openhab.binding.mysensors/src/main/java/org/openhab/binding/mysensors/internal/protocol/MySensorsWriter.java similarity index 82% rename from addons/binding/org.openhab.binding.mysensors/src/main/java/org/openhab/binding/mysensors/protocol/MySensorsWriter.java rename to addons/binding/org.openhab.binding.mysensors/src/main/java/org/openhab/binding/mysensors/internal/protocol/MySensorsWriter.java index c163f6bec16f8..e46916c9d4c16 100644 --- a/addons/binding/org.openhab.binding.mysensors/src/main/java/org/openhab/binding/mysensors/protocol/MySensorsWriter.java +++ b/addons/binding/org.openhab.binding.mysensors/src/main/java/org/openhab/binding/mysensors/internal/protocol/MySensorsWriter.java @@ -5,7 +5,7 @@ * which accompanies this distribution, and is available at * http://www.eclipse.org/legal/epl-v10.html */ -package org.openhab.binding.mysensors.protocol; +package org.openhab.binding.mysensors.internal.protocol; import java.io.IOException; import java.io.OutputStream; @@ -15,11 +15,11 @@ import java.util.concurrent.Future; import org.openhab.binding.mysensors.MySensorsBindingConstants; -import org.openhab.binding.mysensors.handler.MySensorsStatusUpdateEvent; -import org.openhab.binding.mysensors.handler.MySensorsUpdateListener; -import org.openhab.binding.mysensors.internal.MySensorsBridgeConnection; -import org.openhab.binding.mysensors.internal.MySensorsMessage; -import org.openhab.binding.mysensors.internal.MySensorsMessageParser; +import org.openhab.binding.mysensors.internal.event.MySensorsEventType; +import org.openhab.binding.mysensors.internal.event.MySensorsStatusUpdateEvent; +import org.openhab.binding.mysensors.internal.event.MySensorsUpdateListener; +import org.openhab.binding.mysensors.internal.protocol.message.MySensorsMessage; +import org.openhab.binding.mysensors.internal.protocol.message.MySensorsMessageParser; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -43,9 +43,9 @@ public void startWriter() { @Override public void run() { - + Thread.currentThread().setName(MySensorsWriter.class.getName()); while (!stopWriting) { - if (!mysCon.pauseWriter) { + if (!mysCon.isWriterPaused()) { try { MySensorsMessage msg = mysCon.pollMySensorsOutboundQueue(); @@ -68,10 +68,9 @@ public void run() { logger.debug("Reverting status!"); msg.setMsg(msg.getOldMsg()); msg.setAck(0); - MySensorsStatusUpdateEvent event = new MySensorsStatusUpdateEvent(msg); - for (MySensorsUpdateListener mySensorsEventListener : mysCon.updateListeners) { - mySensorsEventListener.statusUpdateReceived(event); - } + MySensorsStatusUpdateEvent event = new MySensorsStatusUpdateEvent( + MySensorsEventType.INCOMING_MESSAGE, msg); + mysCon.broadCastEvent(event); } else if (!msg.getRevert()) { logger.debug("Not reverted due to configuration!"); } @@ -91,8 +90,8 @@ public void run() { logger.warn("Message returned from queue is null"); } - } catch (InterruptedException e) { - logger.warn("Writer thread interrupted"); + } catch (Exception e) { + logger.error("({}) on writing from serial port, message: {}", e, getClass(), e.getMessage()); } } } @@ -111,21 +110,25 @@ public void stopWriting() { if (future != null) { future.cancel(true); + future = null; } if (executor != null) { executor.shutdown(); executor.shutdownNow(); + executor = null; } try { if (outs != null) { outs.flush(); outs.close(); + outs = null; } if (outStream != null) { outStream.close(); + outStream = null; } } catch (IOException e) { logger.error("Cannot close writer stream"); @@ -135,12 +138,6 @@ public void stopWriting() { @Override public void statusUpdateReceived(MySensorsStatusUpdateEvent event) { - // TODO Auto-generated method stub - - } - - @Override - public void disconnectEvent() { } } diff --git a/addons/binding/org.openhab.binding.mysensors/src/main/java/org/openhab/binding/mysensors/protocol/ip/MySensorIpReader.java b/addons/binding/org.openhab.binding.mysensors/src/main/java/org/openhab/binding/mysensors/internal/protocol/ip/MySensorIpReader.java similarity index 84% rename from addons/binding/org.openhab.binding.mysensors/src/main/java/org/openhab/binding/mysensors/protocol/ip/MySensorIpReader.java rename to addons/binding/org.openhab.binding.mysensors/src/main/java/org/openhab/binding/mysensors/internal/protocol/ip/MySensorIpReader.java index c7a6279afaf8e..a6c9d1b77af13 100644 --- a/addons/binding/org.openhab.binding.mysensors/src/main/java/org/openhab/binding/mysensors/protocol/ip/MySensorIpReader.java +++ b/addons/binding/org.openhab.binding.mysensors/src/main/java/org/openhab/binding/mysensors/internal/protocol/ip/MySensorIpReader.java @@ -5,13 +5,13 @@ * which accompanies this distribution, and is available at * http://www.eclipse.org/legal/epl-v10.html */ -package org.openhab.binding.mysensors.protocol.ip; +package org.openhab.binding.mysensors.internal.protocol.ip; import java.io.BufferedReader; import java.io.InputStream; import java.io.InputStreamReader; -import org.openhab.binding.mysensors.protocol.MySensorsReader; +import org.openhab.binding.mysensors.internal.protocol.MySensorsReader; public class MySensorIpReader extends MySensorsReader { public MySensorIpReader(InputStream inStream, MySensorsIpConnection mysCon) { diff --git a/addons/binding/org.openhab.binding.mysensors/src/main/java/org/openhab/binding/mysensors/internal/protocol/ip/MySensorsIpConnection.java b/addons/binding/org.openhab.binding.mysensors/src/main/java/org/openhab/binding/mysensors/internal/protocol/ip/MySensorsIpConnection.java new file mode 100644 index 0000000000000..21183aa554adf --- /dev/null +++ b/addons/binding/org.openhab.binding.mysensors/src/main/java/org/openhab/binding/mysensors/internal/protocol/ip/MySensorsIpConnection.java @@ -0,0 +1,88 @@ +/** + * Copyright (c) 2014-2016 openHAB UG (haftungsbeschraenkt) and others. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.openhab.binding.mysensors.internal.protocol.ip; + +import java.io.IOException; +import java.net.Socket; +import java.net.UnknownHostException; + +import org.openhab.binding.mysensors.internal.handler.MySensorsBridgeHandler; +import org.openhab.binding.mysensors.internal.protocol.MySensorsBridgeConnection; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class MySensorsIpConnection extends MySensorsBridgeConnection { + + private Logger logger = LoggerFactory.getLogger(MySensorsIpConnection.class); + + private String ipAddress = ""; + private int tcpPort = 0; + public int sendDelay = 0; + + private Socket sock = null; + + public MySensorsIpConnection(MySensorsBridgeHandler bridgeHandler, String ipAddress, int tcpPort, int sendDelay) { + super(bridgeHandler); + this.ipAddress = ipAddress; + this.tcpPort = tcpPort; + this.sendDelay = sendDelay; + } + + @Override + public boolean _connect() { + logger.debug("Connecting to IP bridge [{}:{}]", ipAddress, tcpPort); + + boolean ret = false; + + if (ipAddress == null || ipAddress.isEmpty()) { + logger.error("IP must be not null/empty"); + } else { + try { + sock = new Socket(ipAddress, tcpPort); + mysConReader = new MySensorIpReader(sock.getInputStream(), this); + mysConWriter = new MySensorsIpWriter(sock, this, sendDelay); + + ret = startReaderWriterThread(mysConReader, mysConWriter); + } catch (UnknownHostException e) { + logger.error("Error while trying to connect to: " + ipAddress + ":" + tcpPort); + e.printStackTrace(); + } catch (IOException e) { + logger.error("Error while trying to connect InputStreamReader"); + e.printStackTrace(); + } + } + + return ret; + } + + @Override + public void _disconnect() { + logger.debug("Disconnecting from IP bridge ..."); + + if (mysConWriter != null) { + mysConWriter.stopWriting(); + mysConWriter = null; + } + + if (mysConReader != null) { + mysConReader.stopReader(); + mysConReader = null; + } + + // Shut down socket + try { + if (sock != null && sock.isConnected()) { + sock.close(); + sock = null; + } + } catch (IOException e) { + logger.error("cannot disconnect from socket, message: {}", e.getMessage()); + } + + } +} diff --git a/addons/binding/org.openhab.binding.mysensors/src/main/java/org/openhab/binding/mysensors/protocol/ip/MySensorsIpWriter.java b/addons/binding/org.openhab.binding.mysensors/src/main/java/org/openhab/binding/mysensors/internal/protocol/ip/MySensorsIpWriter.java similarity index 86% rename from addons/binding/org.openhab.binding.mysensors/src/main/java/org/openhab/binding/mysensors/protocol/ip/MySensorsIpWriter.java rename to addons/binding/org.openhab.binding.mysensors/src/main/java/org/openhab/binding/mysensors/internal/protocol/ip/MySensorsIpWriter.java index 0a670bd8871a8..559fea99d794c 100644 --- a/addons/binding/org.openhab.binding.mysensors/src/main/java/org/openhab/binding/mysensors/protocol/ip/MySensorsIpWriter.java +++ b/addons/binding/org.openhab.binding.mysensors/src/main/java/org/openhab/binding/mysensors/internal/protocol/ip/MySensorsIpWriter.java @@ -5,13 +5,13 @@ * which accompanies this distribution, and is available at * http://www.eclipse.org/legal/epl-v10.html */ -package org.openhab.binding.mysensors.protocol.ip; +package org.openhab.binding.mysensors.internal.protocol.ip; import java.io.IOException; import java.io.PrintWriter; import java.net.Socket; -import org.openhab.binding.mysensors.protocol.MySensorsWriter; +import org.openhab.binding.mysensors.internal.protocol.MySensorsWriter; public class MySensorsIpWriter extends MySensorsWriter { diff --git a/addons/binding/org.openhab.binding.mysensors/src/main/java/org/openhab/binding/mysensors/internal/MySensorsMessage.java b/addons/binding/org.openhab.binding.mysensors/src/main/java/org/openhab/binding/mysensors/internal/protocol/message/MySensorsMessage.java similarity index 60% rename from addons/binding/org.openhab.binding.mysensors/src/main/java/org/openhab/binding/mysensors/internal/MySensorsMessage.java rename to addons/binding/org.openhab.binding.mysensors/src/main/java/org/openhab/binding/mysensors/internal/protocol/message/MySensorsMessage.java index ced0d4969ee01..952814ca733d2 100644 --- a/addons/binding/org.openhab.binding.mysensors/src/main/java/org/openhab/binding/mysensors/internal/MySensorsMessage.java +++ b/addons/binding/org.openhab.binding.mysensors/src/main/java/org/openhab/binding/mysensors/internal/protocol/message/MySensorsMessage.java @@ -5,7 +5,9 @@ * which accompanies this distribution, and is available at * http://www.eclipse.org/legal/epl-v10.html */ -package org.openhab.binding.mysensors.internal; +package org.openhab.binding.mysensors.internal.protocol.message; + +import static org.openhab.binding.mysensors.MySensorsBindingConstants.*; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -131,4 +133,82 @@ public void setOldMsg(String oldMsg) { this.oldMsg = oldMsg; } + public boolean isIConfigMessage() { + boolean ret = false; + + if (childId == 0 || childId == 255) { + if (msgType == MYSENSORS_MSG_TYPE_INTERNAL) { + if (ack == 0) { + if (subType == MYSENSORS_SUBTYPE_I_CONFIG) { + ret = true; + } + } + } + } + + return ret; + } + + public boolean isIVersionMessage() { + boolean ret = false; + + if (nodeId == 0) { + if (childId == 0 || childId == 255) { + if (msgType == MYSENSORS_MSG_TYPE_INTERNAL) { + if (ack == 0) { + if (subType == MYSENSORS_SUBTYPE_I_VERSION) { + ret = true; + } + } + } + } + } + + return ret; + } + + public boolean isITimeMessage() { + boolean ret = false; + + if (childId == 0 || childId == 255) { + if (msgType == MYSENSORS_MSG_TYPE_INTERNAL) { + if (ack == 0) { + if (subType == MYSENSORS_SUBTYPE_I_TIME) { + ret = true; + } + } + } + } + + return ret; + } + + public boolean isIdRequestMessage() { + boolean ret = false; + + if (nodeId == 255) { + if (childId == 255) { + if (msgType == MYSENSORS_SUBTYPE_I_ID_REQUEST) { + if (ack == 0) { + if (subType == MYSENSORS_MSG_TYPE_INTERNAL) { + ret = true; + } + } + } + } + } + + return ret; + } + + public boolean isPresentationMessage() { + boolean ret = false; + + if (msgType == MYSENSORS_MSG_TYPE_PRESENTATION) { + ret = true; + } + + return ret; + } + } diff --git a/addons/binding/org.openhab.binding.mysensors/src/main/java/org/openhab/binding/mysensors/internal/MySensorsMessageParser.java b/addons/binding/org.openhab.binding.mysensors/src/main/java/org/openhab/binding/mysensors/internal/protocol/message/MySensorsMessageParser.java similarity index 97% rename from addons/binding/org.openhab.binding.mysensors/src/main/java/org/openhab/binding/mysensors/internal/MySensorsMessageParser.java rename to addons/binding/org.openhab.binding.mysensors/src/main/java/org/openhab/binding/mysensors/internal/protocol/message/MySensorsMessageParser.java index 848c130ec9f3b..e26b44283007b 100644 --- a/addons/binding/org.openhab.binding.mysensors/src/main/java/org/openhab/binding/mysensors/internal/MySensorsMessageParser.java +++ b/addons/binding/org.openhab.binding.mysensors/src/main/java/org/openhab/binding/mysensors/internal/protocol/message/MySensorsMessageParser.java @@ -5,7 +5,7 @@ * which accompanies this distribution, and is available at * http://www.eclipse.org/legal/epl-v10.html */ -package org.openhab.binding.mysensors.internal; +package org.openhab.binding.mysensors.internal.protocol.message; import org.openhab.binding.mysensors.MySensorsBindingConstants; diff --git a/addons/binding/org.openhab.binding.mysensors/src/main/java/org/openhab/binding/mysensors/protocol/serial/MySensorsSerialConnection.java b/addons/binding/org.openhab.binding.mysensors/src/main/java/org/openhab/binding/mysensors/internal/protocol/serial/MySensorsSerialConnection.java similarity index 62% rename from addons/binding/org.openhab.binding.mysensors/src/main/java/org/openhab/binding/mysensors/protocol/serial/MySensorsSerialConnection.java rename to addons/binding/org.openhab.binding.mysensors/src/main/java/org/openhab/binding/mysensors/internal/protocol/serial/MySensorsSerialConnection.java index d1dea3e87ddec..db13483ed8c53 100644 --- a/addons/binding/org.openhab.binding.mysensors/src/main/java/org/openhab/binding/mysensors/protocol/serial/MySensorsSerialConnection.java +++ b/addons/binding/org.openhab.binding.mysensors/src/main/java/org/openhab/binding/mysensors/internal/protocol/serial/MySensorsSerialConnection.java @@ -5,15 +5,17 @@ * which accompanies this distribution, and is available at * http://www.eclipse.org/legal/epl-v10.html */ -package org.openhab.binding.mysensors.protocol.serial; +package org.openhab.binding.mysensors.internal.protocol.serial; +import java.io.File; import java.util.ArrayList; import java.util.Arrays; import java.util.Enumeration; import org.apache.commons.lang.StringUtils; import org.openhab.binding.mysensors.MySensorsBindingConstants; -import org.openhab.binding.mysensors.internal.MySensorsBridgeConnection; +import org.openhab.binding.mysensors.internal.handler.MySensorsBridgeHandler; +import org.openhab.binding.mysensors.internal.protocol.MySensorsBridgeConnection; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -32,31 +34,30 @@ public class MySensorsSerialConnection extends MySensorsBridgeConnection { private String serialPort = ""; private int baudRate = 115200; private int sendDelay = 0; - private boolean skipStartupCheck = false; private NRSerialPort serialConnection = null; - private MySensorsSerialWriter mysConWriter = null; - private MySensorsSerialReader mysConReader = null; - - public MySensorsSerialConnection(String serialPort, int baudRate, int sendDelay, boolean skipStartupCheck) { - super(skipStartupCheck); + public MySensorsSerialConnection(MySensorsBridgeHandler bridgeHandler, String serialPort, int baudRate, + int sendDelay) { + super(bridgeHandler); this.serialPort = serialPort; this.baudRate = baudRate; this.sendDelay = sendDelay; - this.skipStartupCheck = skipStartupCheck; - } @Override - public boolean connect() { + public boolean _connect() { logger.debug("Connecting to {} [baudRate:{}]", serialPort, baudRate); + boolean ret = false; + updateSerialProperties(serialPort); + // deleteLockFile(serialPort); + serialConnection = new NRSerialPort(serialPort, baudRate); if (serialConnection.connect()) { - logger.info("Successfully connected to serial port."); + logger.debug("Successfully connected to serial port."); try { logger.debug("Waiting {} seconds to allow correct reset trigger on serial connection opening", @@ -69,39 +70,75 @@ public boolean connect() { mysConReader = new MySensorsSerialReader(serialConnection.getInputStream(), this); mysConWriter = new MySensorsSerialWriter(serialConnection.getOutputStream(), this, sendDelay); - connected = startReaderWriterThread(mysConReader, mysConWriter); + ret = startReaderWriterThread(mysConReader, mysConWriter); } else { logger.error("Can't connect to serial port. Wrong port?"); } - return connected; + return ret; } @Override - public void disconnect() { - logger.info("Shutting down serial connection!"); + public void _disconnect() { + logger.debug("Shutting down serial connection!"); if (mysConWriter != null) { mysConWriter.stopWriting(); + mysConWriter = null; } if (mysConReader != null) { mysConReader.stopReader(); + mysConReader = null; } - if (serialConnection != null && serialConnection.isConnected()) { - serialConnection.disconnect(); + if (serialConnection != null) { + try { + serialConnection.disconnect(); + } catch (Exception e) { + } + serialConnection = null; } } - private void updateSerialProperties(String devName) { + /** + * This method delete lock file (if present). + * RXTX library not removes them after call NRSerialPort.disconnect(). + * If lock file was not removed, new connection fail to startup + * + * @param devName is the device used as COM/UART port + */ + @SuppressWarnings("unused") + private void deleteLockFile(String devName) { + try { + String[] namePart = devName.split("/"); + File lockFile = new File("/var/lock/LCK.." + namePart[namePart.length - 1]); + if (lockFile.exists()) { + logger.warn("Lock file found ({}), this is not good...try removing it", lockFile.toString()); + lockFile.delete(); + + if (lockFile.exists()) { + logger.error("Warn! Lock file cannot be deleted, probably connection will fail"); + } else { + logger.info("Ok, lock file removed..."); + } + } else { + logger.debug("Lock file doesn't not exists"); + } + } catch (Exception e) { + logger.error("Serial lock file not removed, cause: {} ({})", e.getClass(), e.getMessage()); + } + } - /* - * By default, RXTX searches only devices /dev/ttyS* and - * /dev/ttyUSB*, and will therefore not find devices that - * have been symlinked. Adding them however is tricky, see below. - */ + /** + * By default, RXTX searches only devices /dev/ttyS* and + * /dev/ttyUSB*, and will therefore not find devices that + * have been symlinked. Adding them however is tricky, see below. + * + * @param devName is the device used as COM/UART port + */ + private void updateSerialProperties(String devName) { // // first go through the port identifiers to find any that are not in @@ -116,7 +153,7 @@ private void updateSerialProperties(String devName) { allPorts.add(id.getName()); } } - logger.trace("ports found from identifiers: {}", StringUtils.join(allPorts, ":")); + logger.trace("Ports found from identifiers: {}", StringUtils.join(allPorts, ":")); // // now add our port so it's in the list // @@ -137,7 +174,7 @@ private void updateSerialProperties(String devName) { } } String finalPorts = StringUtils.join(allPorts, ":"); - logger.trace("final port list: {}", finalPorts); + logger.debug("Final port list: {}", finalPorts); // // Finally overwrite the "gnu.io.rxtx.SerialPorts" System property. diff --git a/addons/binding/org.openhab.binding.mysensors/src/main/java/org/openhab/binding/mysensors/protocol/serial/MySensorsSerialReader.java b/addons/binding/org.openhab.binding.mysensors/src/main/java/org/openhab/binding/mysensors/internal/protocol/serial/MySensorsSerialReader.java similarity index 84% rename from addons/binding/org.openhab.binding.mysensors/src/main/java/org/openhab/binding/mysensors/protocol/serial/MySensorsSerialReader.java rename to addons/binding/org.openhab.binding.mysensors/src/main/java/org/openhab/binding/mysensors/internal/protocol/serial/MySensorsSerialReader.java index debb30d07a31f..e096c8a040a4d 100644 --- a/addons/binding/org.openhab.binding.mysensors/src/main/java/org/openhab/binding/mysensors/protocol/serial/MySensorsSerialReader.java +++ b/addons/binding/org.openhab.binding.mysensors/src/main/java/org/openhab/binding/mysensors/internal/protocol/serial/MySensorsSerialReader.java @@ -5,13 +5,13 @@ * which accompanies this distribution, and is available at * http://www.eclipse.org/legal/epl-v10.html */ -package org.openhab.binding.mysensors.protocol.serial; +package org.openhab.binding.mysensors.internal.protocol.serial; import java.io.BufferedReader; import java.io.InputStream; import java.io.InputStreamReader; -import org.openhab.binding.mysensors.protocol.MySensorsReader; +import org.openhab.binding.mysensors.internal.protocol.MySensorsReader; public class MySensorsSerialReader extends MySensorsReader { diff --git a/addons/binding/org.openhab.binding.mysensors/src/main/java/org/openhab/binding/mysensors/protocol/serial/MySensorsSerialWriter.java b/addons/binding/org.openhab.binding.mysensors/src/main/java/org/openhab/binding/mysensors/internal/protocol/serial/MySensorsSerialWriter.java similarity index 83% rename from addons/binding/org.openhab.binding.mysensors/src/main/java/org/openhab/binding/mysensors/protocol/serial/MySensorsSerialWriter.java rename to addons/binding/org.openhab.binding.mysensors/src/main/java/org/openhab/binding/mysensors/internal/protocol/serial/MySensorsSerialWriter.java index 189896d604355..5b7522b1fd8eb 100644 --- a/addons/binding/org.openhab.binding.mysensors/src/main/java/org/openhab/binding/mysensors/protocol/serial/MySensorsSerialWriter.java +++ b/addons/binding/org.openhab.binding.mysensors/src/main/java/org/openhab/binding/mysensors/internal/protocol/serial/MySensorsSerialWriter.java @@ -5,12 +5,12 @@ * which accompanies this distribution, and is available at * http://www.eclipse.org/legal/epl-v10.html */ -package org.openhab.binding.mysensors.protocol.serial; +package org.openhab.binding.mysensors.internal.protocol.serial; import java.io.OutputStream; import java.io.PrintWriter; -import org.openhab.binding.mysensors.protocol.MySensorsWriter; +import org.openhab.binding.mysensors.internal.protocol.MySensorsWriter; public class MySensorsSerialWriter extends MySensorsWriter { diff --git a/addons/binding/org.openhab.binding.mysensors/src/main/java/org/openhab/binding/mysensors/internal/sensors/MySensorsChild.java b/addons/binding/org.openhab.binding.mysensors/src/main/java/org/openhab/binding/mysensors/internal/sensors/MySensorsChild.java new file mode 100644 index 0000000000000..87c2a85bd7e2a --- /dev/null +++ b/addons/binding/org.openhab.binding.mysensors/src/main/java/org/openhab/binding/mysensors/internal/sensors/MySensorsChild.java @@ -0,0 +1,42 @@ +/** + * Copyright (c) 2014-2016 openHAB UG (haftungsbeschraenkt) and others. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.openhab.binding.mysensors.internal.sensors; + +import java.util.Date; + +public class MySensorsChild { + + private Integer childId = 0; + private T childValue = null; + + private Date childLastUpdate = null; + + public MySensorsChild(int childId, T initialValue) { + this.childId = childId; + this.childValue = initialValue; + } + + public int getChildId() { + return childId; + } + + public void setChildValue(T newValue) { + childLastUpdate = new Date(); + childValue = newValue; + } + + public T getChildValue() { + return childValue; + } + + @Override + public String toString() { + return "MySensorsChild [childId=" + childId + ", nodeValue=" + childValue + "]"; + } + +} diff --git a/addons/binding/org.openhab.binding.mysensors/src/main/java/org/openhab/binding/mysensors/internal/sensors/MySensorsDeviceManager.java b/addons/binding/org.openhab.binding.mysensors/src/main/java/org/openhab/binding/mysensors/internal/sensors/MySensorsDeviceManager.java new file mode 100644 index 0000000000000..6fe58bbe4b9cf --- /dev/null +++ b/addons/binding/org.openhab.binding.mysensors/src/main/java/org/openhab/binding/mysensors/internal/sensors/MySensorsDeviceManager.java @@ -0,0 +1,196 @@ +package org.openhab.binding.mysensors.internal.sensors; + +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import org.openhab.binding.mysensors.internal.event.MySensorsEventType; +import org.openhab.binding.mysensors.internal.event.MySensorsStatusUpdateEvent; +import org.openhab.binding.mysensors.internal.event.MySensorsUpdateListener; +import org.openhab.binding.mysensors.internal.exception.NoMoreIdsException; +import org.openhab.binding.mysensors.internal.protocol.MySensorsBridgeConnection; +import org.openhab.binding.mysensors.internal.protocol.message.MySensorsMessage; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class MySensorsDeviceManager implements MySensorsUpdateListener { + private Logger logger = LoggerFactory.getLogger(getClass()); + + private MySensorsBridgeConnection myCon = null; + + private Map nodeMap = null; + + public MySensorsDeviceManager(MySensorsBridgeConnection myCon) { + this.myCon = myCon; + this.nodeMap = new HashMap(); + } + + public MySensorsDeviceManager(MySensorsBridgeConnection myCon, Map nodeMap) { + this.myCon = myCon; + if (nodeMap == null) { + throw new NullPointerException("Cannot create MySensorsDeviceManager null node map passed"); + } + this.nodeMap = nodeMap; + } + + public MySensorsDeviceManager(MySensorsBridgeConnection myCon, List nodeList) { + this.myCon = myCon; + this.nodeMap = new HashMap(); + + if (nodeList == null) { + throw new NullPointerException("Cannot create MySensorsDeviceManager null node list passed"); + } + + if (nodeList != null) { + for (MySensorsNode n : nodeList) { + if (n != null) { + nodeMap.put(n.getNodeId(), n); + } + } + } + + } + + public MySensorsNode getNode(int nodeId) { + return nodeMap.get(nodeId); + } + + public void addNode(MySensorsNode node) { + synchronized (nodeMap) { + nodeMap.put(node.getNodeId(), node); + } + } + + public void addChild(int nodeId, MySensorsChild child) { + synchronized (nodeMap) { + MySensorsNode node = nodeMap.get(nodeId); + if (node != null) { + node.addChild(child); + } else { + logger.warn("Node {} not found in map", nodeId); + } + } + } + + public Set getGivenIds() { + return nodeMap.keySet(); + } + + public Integer reserveId() throws NoMoreIdsException { + int newId = 1; + + // clearNullOnMap(); + + Set takenIds = getGivenIds(); + + synchronized (takenIds) { + while (newId < 255) { + if (!takenIds.contains(newId)) { + nodeMap.put(newId, null); + break; + } else { + newId++; + } + } + } + + if (newId == 255) { + throw new NoMoreIdsException(); + } + + return newId; + } + + @Override + public void statusUpdateReceived(MySensorsStatusUpdateEvent event) { + switch (event.getEventType()) { + case INCOMING_MESSAGE: + handleIncomingMessageEvent((MySensorsMessage) event.getData()); + break; + default: + break; + } + + } + + private void handleIncomingMessageEvent(MySensorsMessage msg) { + // Are we getting a Request ID Message? + if (msg.isIdRequestMessage()) { + answerIDRequest(); + return; + } + + // Register node if not present + checkNodeFound(msg); + // checkChildFound(msg); TODO + } + + private void checkNodeFound(MySensorsMessage msg) { + MySensorsNode node = null; + synchronized (nodeMap) { + if (msg.nodeId != 0 && msg.nodeId != 255) { + if (nodeMap.containsKey(msg.nodeId) && (nodeMap.get(msg.nodeId) == null) + || (!nodeMap.containsKey(msg.nodeId))) { + logger.debug("Node {} found!", msg.getNodeId()); + + node = new MySensorsNode(msg.nodeId); + addNode(node); + } + } + } + + if (node != null) { + MySensorsStatusUpdateEvent evt = new MySensorsStatusUpdateEvent(MySensorsEventType.NEW_NODE_DISCOVERED, + node); + myCon.broadCastEvent(evt); + } + } + + @SuppressWarnings("unused") + private void checkChildFound(MySensorsMessage msg) { + synchronized (nodeMap) { + if (msg.childId != 255 && !nodeMap.containsKey(msg.childId)) { + logger.debug("New child {} for node {} found!", msg.getChildId(), msg.getNodeId()); + + MySensorsChild child = new MySensorsChild(msg.childId, null); + addChild(msg.nodeId, child); + } + } + } + + /** + * Removes null element from map, null element represent reserved, but not used, id for nodes. + * Null elements will disappear if the sensor accept the ID (so start transmitting information with that ID) + */ + @SuppressWarnings("unused") + private void clearNullOnMap() { + synchronized (nodeMap) { + Iterator iterator = getGivenIds().iterator(); + while (iterator.hasNext()) { + Integer i = iterator.next(); + if (getNode(i) == null) { + nodeMap.remove(i); + } + } + } + } + + /** + * If an ID -Request from a sensor is received the controller will send an id to the sensor + */ + private void answerIDRequest() { + logger.info("ID Request received"); + + int newId = 0; + try { + newId = reserveId(); + MySensorsMessage newMsg = new MySensorsMessage(255, 255, 3, 0, false, 4, newId + ""); + myCon.addMySensorsOutboundMessage(newMsg); + logger.info("New Node in the MySensors network has requested an ID. ID is: {}", newId); + } catch (NoMoreIdsException e) { + logger.error("No more IDs available for this node, try cleaning cache"); + } + } +} diff --git a/addons/binding/org.openhab.binding.mysensors/src/main/java/org/openhab/binding/mysensors/internal/sensors/MySensorsNode.java b/addons/binding/org.openhab.binding.mysensors/src/main/java/org/openhab/binding/mysensors/internal/sensors/MySensorsNode.java new file mode 100644 index 0000000000000..1fded19bc4555 --- /dev/null +++ b/addons/binding/org.openhab.binding.mysensors/src/main/java/org/openhab/binding/mysensors/internal/sensors/MySensorsNode.java @@ -0,0 +1,46 @@ +/** + * Copyright (c) 2014-2016 openHAB UG (haftungsbeschraenkt) and others. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.openhab.binding.mysensors.internal.sensors; + +import java.util.Date; +import java.util.HashMap; +import java.util.Map; + +public class MySensorsNode { + + private Integer nodeId = null; + + private boolean reachable = true; + + private Map> chidldMap = null; + + private Date lastUpdate = null; + + public MySensorsNode(int nodeId) { + this.nodeId = nodeId; + this.chidldMap = new HashMap>(); + } + + public int getNodeId() { + return nodeId; + } + + public void addChild(MySensorsChild child) { + chidldMap.put(child.getChildId(), child); + } + + public boolean isReachable() { + return reachable; + } + + @Override + public String toString() { + return "MySensorsNode [nodeId=" + nodeId + ", chidldList=" + chidldMap + "]"; + } + +} diff --git a/addons/binding/org.openhab.binding.mysensors/src/main/java/org/openhab/binding/mysensors/protocol/ip/MySensorsIpConnection.java b/addons/binding/org.openhab.binding.mysensors/src/main/java/org/openhab/binding/mysensors/protocol/ip/MySensorsIpConnection.java deleted file mode 100644 index 2883119f09a5a..0000000000000 --- a/addons/binding/org.openhab.binding.mysensors/src/main/java/org/openhab/binding/mysensors/protocol/ip/MySensorsIpConnection.java +++ /dev/null @@ -1,80 +0,0 @@ -/** - * Copyright (c) 2014-2016 openHAB UG (haftungsbeschraenkt) and others. - * All rights reserved. This program and the accompanying materials - * are made available under the terms of the Eclipse Public License v1.0 - * which accompanies this distribution, and is available at - * http://www.eclipse.org/legal/epl-v10.html - */ -package org.openhab.binding.mysensors.protocol.ip; - -import java.io.IOException; -import java.net.Socket; -import java.net.UnknownHostException; - -import org.openhab.binding.mysensors.internal.MySensorsBridgeConnection; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -public class MySensorsIpConnection extends MySensorsBridgeConnection { - - private Logger logger = LoggerFactory.getLogger(MySensorsIpConnection.class); - - private String ipAddress = ""; - private int tcpPort = 0; - public int sendDelay = 0; - - private Socket sock = null; - - private MySensorsIpWriter mysConWriter = null; - private MySensorIpReader mysConReader = null; - - public MySensorsIpConnection(String ipAddress, int tcpPort, int sendDelay, boolean skipStartupCheck) { - super(skipStartupCheck); - this.ipAddress = ipAddress; - this.tcpPort = tcpPort; - this.sendDelay = sendDelay; - } - - @Override - public boolean connect() { - logger.debug("Connecting to bridge ..."); - - try { - sock = new Socket(ipAddress, tcpPort); - mysConReader = new MySensorIpReader(sock.getInputStream(), this); - mysConWriter = new MySensorsIpWriter(sock, this, sendDelay); - - connected = startReaderWriterThread(mysConReader, mysConWriter); - } catch (UnknownHostException e) { - logger.error("Error while trying to connect to: " + ipAddress + ":" + tcpPort); - e.printStackTrace(); - } catch (IOException e) { - logger.error("Error while trying to connect InputStreamReader"); - e.printStackTrace(); - } - - return connected; - } - - @Override - public void disconnect() { - - if (mysConWriter != null) { - mysConWriter.stopWriting(); - } - - if (mysConReader != null) { - mysConReader.stopReader(); - } - - // Shut down socket - try { - if (sock != null && sock.isConnected()) { - sock.close(); - } - } catch (IOException e) { - logger.error("cannot disconnect from socket, message: {}", e.getMessage()); - } - - } -} diff --git a/addons/binding/org.openhab.binding.mysensors/src/main/test/org/openhab/binding/mysensors/test/CacheTest.java b/addons/binding/org.openhab.binding.mysensors/src/main/test/org/openhab/binding/mysensors/test/CacheTest.java new file mode 100644 index 0000000000000..8a41e310d86c0 --- /dev/null +++ b/addons/binding/org.openhab.binding.mysensors/src/main/test/org/openhab/binding/mysensors/test/CacheTest.java @@ -0,0 +1,31 @@ +package org.openhab.binding.mysensors.test; + +import java.util.ArrayList; + +import org.junit.Test; +import org.openhab.binding.mysensors.internal.factory.MySensorsCacheFactory; + +import com.google.gson.reflect.TypeToken; + +public class CacheTest { + + @Test + public void writeGivenIdsCache() { + MySensorsCacheFactory c = MySensorsCacheFactory.getCacheFactory(); + ArrayList ids = new ArrayList(); + ids.add(2); + ids.add(3); + ids.add(5); + c.writeCache(MySensorsCacheFactory.GIVEN_IDS_CACHE_FILE, ids, new TypeToken>() { + }.getType()); + } + + @Test + public void readGivenIdsCache() { + MySensorsCacheFactory c = MySensorsCacheFactory.getCacheFactory(); + System.out.println(c.readCache(MySensorsCacheFactory.GIVEN_IDS_CACHE_FILE, new ArrayList(), + new TypeToken>() { + }.getType())); + } + +} diff --git a/addons/binding/org.openhab.binding.mysensors/target/org.openhab.binding.mysensors-2.0.0-SNAPSHOT.jar b/addons/binding/org.openhab.binding.mysensors/target/org.openhab.binding.mysensors-2.0.0-SNAPSHOT.jar new file mode 100644 index 0000000000000000000000000000000000000000..5d3edde0759dc27aea2c902f8a9e920ab41b5fac GIT binary patch literal 76167 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 G(EkH1hsJ>b literal 0 HcmV?d00001 diff --git a/addons/binding/org.openhab.binding.mysensors/test/iVersionMessage.sh b/addons/binding/org.openhab.binding.mysensors/test/iVersionMessage.sh new file mode 100755 index 0000000000000..f4b57c7b80d78 --- /dev/null +++ b/addons/binding/org.openhab.binding.mysensors/test/iVersionMessage.sh @@ -0,0 +1,5 @@ +#!/bin/sh + +serialPort=$1 + +echo "0;0;3;0;2;0" > $serialPort diff --git a/addons/binding/org.openhab.binding.mysensors/test/testscript.sh b/addons/binding/org.openhab.binding.mysensors/test/testscript.sh index 180ceed8d5476..def31b9c91db0 100755 --- a/addons/binding/org.openhab.binding.mysensors/test/testscript.sh +++ b/addons/binding/org.openhab.binding.mysensors/test/testscript.sh @@ -17,13 +17,13 @@ echo "255;255;3;0;3" > $serialPort #Sensor --> gateway # Representation of a new (started) sensor #0;0;3;0;9;read: 172-172-0 s=255,c=0,t=18,pt=0,l=5:1.4.1 -echo "172;255;0;0;18;1.4.1" > $serialPort +#echo "172;255;0;0;18;1.4.1" > $serialPort #0;0;3;0;9;read: 172-172-0 s=255,c=3,t=6,pt=1,l=1:0 -echo "172;255;3;0;6;0" > $serialPort ############################### is metric? + #0;0;3;0;9;read: 172-172-0 s=255,c=3,t=11,pt=0,l=23:Humidity + Temp + -echo "172;255;3;0;11;Humidity + Temp + Relay" > $serialPort +#echo "172;255;3;0;11;Humidity + Temp + Relay" > $serialPort #0;0;3;0;9;read: 172-172-0 s=255,c=3,t=12,pt=0,l=3:1.0 -echo "172;255;3;0;12;1.0" > $serialPort +#echo "172;255;3;0;12;1.0" > $serialPort #0;0;3;0;9;read: 172-172-0 s=0,c=0,t=7,pt=0,l=5:1.4.1 #echo "172;0;0;0;7;1.4.1" > $serialPort #0;0;3;0;9;read: 172-172-0 s=1,c=0,t=6,pt=0,l=5:1.4.1 @@ -33,30 +33,46 @@ echo "172;255;3;0;12;1.0" > $serialPort #0;0;3;0;9;read: 172-172-0 s=3,c=0,t=3,pt=0,l=5:1.4.1 #echo "172;3;0;0;3;1.4.1" > $serialPort +# Request I_TIME +#echo "172;255;3;0;1;0" > $serialPort # What time is it? + +#echo "172;255;3;0;6;0" > $serialPort ############################### is metric? + # Set Humidty status echo "172;0;1;0;1;87" > $serialPort +# Set Humidty status +echo "173;0;1;0;1;44" > $serialPort + # Set Temperature status echo "172;1;1;0;0;27" > $serialPort +# Set V_TEXT +echo "123;123;1;0;47;ipsumlorum" > $serialPort + +# Set V_IR_RECEIVE +#echo "111;111;1;0;33;FADEXXFE" > $serialPort + +# Set V_IR_SEND +#echo "111;112;1;0;32;ABCDEFGHIJKL" > $serialPort #### Represent door -#echo "172;4;0;0;0;1.4.1" > $serialPort +echo "172;4;0;0;0;1.4.1" > $serialPort # Set Tripped status -#echo "172;4;1;0;16;1" > $serialPort +echo "172;4;1;0;16;1" > $serialPort # Set Armed status -#echo "172;4;1;0;15;1" > $serialPort +echo "172;4;1;0;15;1" > $serialPort #### Represent motion -echo "172;5;0;0;1;1.4.1" > $serialPort +echo "174;0;0;0;1;2.0.1" > $serialPort # Set Tripped status -echo "172;5;1;0;16;1" > $serialPort +echo "174;0;1;0;16;1" > $serialPort # Set Armed status -echo "172;5;1;0;15;0" > $serialPort +echo "174;0;1;0;15;0" > $serialPort #### Represent smoke #echo "172;6;0;0;2;1.4.1" > $serialPort @@ -65,7 +81,7 @@ echo "172;5;1;0;15;0" > $serialPort #echo "172;6;1;0;16;1" > $serialPort #### Represent Dimmer -echo "172;7;0;0;4;1.4.1" > $serialPort +#echo "172;7;0;0;4;1.4.1" > $serialPort # Set dimmer status echo "172;7;1;0;3;49" > $serialPort @@ -77,10 +93,10 @@ echo "172;7;1;0;3;49" > $serialPort #echo "172;8;0;0;5;1.4.1" > $serialPort # Set cover status UP(29) == 1, DOWN(30) == 1 -#echo "172;8;1;0;29;1" > $serialPort +echo "172;8;1;0;29;1" > $serialPort # Set cover status -#echo "172;8;1;0;30;1" > $serialPort +echo "172;8;1;0;30;1" > $serialPort #### Represent wind @@ -125,11 +141,10 @@ echo "172;7;1;0;3;49" > $serialPort #echo "172;13;1;0;13;1543.98" > $serialPort #### Represent LIGHT_LEVEL -echo "172;14;0;0;16;1.4.1" > $serialPort +#echo "172;14;0;0;16;1.4.1" > $serialPort # Set LIGHT_LEVEL -echo "172;14;1;0;23;1543.98" > $serialPort - +#echo "172;14;1;0;23;1543.98" > $serialPort # Set watt status #echo "172;7;1;0;17;0815" > $serialPort @@ -147,14 +162,13 @@ echo "172;14;1;0;23;1543.98" > $serialPort #echo "3;1;1;0;18;106.2550" > $serialPort # the overall power usage since sensor boot (KWH) # Representation of a S_BARO -echo "6;3;0;0;8;1.4.1" > $serialPort +#echo "6;3;0;0;8;1.4.1" > $serialPort # Barometer forecast V_BARO -echo "6;3;1;0;5;stable" > $serialPort +#echo "6;3;1;0;5;stable" > $serialPort # Barometer pressure V_PRESSURE -echo "6;3;1;0;4;1.2" > $serialPort - +#echo "6;3;1;0;4;1.2" > $serialPort # Representation of S_MULTIMETER Sensor #echo "12;3;0;0;30;1.5.0" > $serialPort