]> git.basschouten.com Git - openhab-addons.git/commitdiff
[flicbutton] Initial contribution FlicButton Binding (#9234)
authorPatrick Fink <mail@pfink.de>
Sun, 20 Feb 2022 20:53:30 +0000 (21:53 +0100)
committerGitHub <noreply@github.com>
Sun, 20 Feb 2022 20:53:30 +0000 (21:53 +0100)
* [flicbutton] Initial contribution FlicButton Binding

Signed-off-by: Patrick Fink <mail@pfink.de>
* [flicbutton] Add config parameter address for FlicButton thing

Signed-off-by: Patrick Fink <mail@pfink.de>
* [flicbutton] Run spotless

Signed-off-by: Patrick Fink <mail@pfink.de>
* [flicbutton] Code cleanup & docs improvement

Signed-off-by: Patrick Fink <mail@pfink.de>
* Apply suggestions from code review

Co-authored-by: Fabian Wolter <github@fabian-wolter.de>
* [flicbutton] Update LICENSE

Signed-off-by: Patrick Fink <mail@pfink.de>
* [flicbutton] Apply suggestions from code review (2) & update to 3.1-SNAPSHOT

Signed-off-by: Patrick Fink <mail@pfink.de>
* [flicbutton] Apply suggestions from code review (3) & fix offline status

Signed-off-by: Patrick Fink <mail@pfink.de>
* [flicbutton] Fix 3rd party source for proper IDE integration

Signed-off-by: Patrick Fink <mail@pfink.de>
* [flicbutton] Simplify config parsing

Signed-off-by: Patrick Fink <mail@pfink.de>
* [flicbutton] Move everything to internal package

Signed-off-by: Patrick Fink <mail@pfink.de>
* [flicbutton] Remove hyphens from port parameter docs example

Signed-off-by: Patrick Fink <mail@pfink.de>
* [flicbutton] Change maintainer to openHAB project

Signed-off-by: Patrick Fink <mail@pfink.de>
* Apply docs suggestions + update to 3.2.0-SNAPSHOT

Signed-off-by: Patrick Fink <mail@pfink.de>
Co-authored-by: Matthew Skinner <matt@pcmus.com>
* [flicbutton] Fix bridge offline & reconnect handling

Signed-off-by: Patrick Fink <mail@pfink.de>
* [flicbutton] Close open socket on dispose

Signed-off-by: Patrick Fink <mail@pfink.de>
* [flicbutton] Improve exception error message in ThingStatus

Signed-off-by: Patrick Fink <mail@pfink.de>
* [flicbutton] Fix README title

Signed-off-by: Patrick Fink <mail@pfink.de>
* [flicbutton] Improve exception error message in ThingStatus

Signed-off-by: Patrick Fink <mail@pfink.de>
* [flicbutton] Style fixes

Signed-off-by: Patrick Fink <mail@pfink.de>
* [flicbutton] Use trace log level for button clicks & status changes

Signed-off-by: Patrick Fink <mail@pfink.de>
* Apply doc improvements from code review

Signed-off-by: Patrick Fink <mail@pfink.de>
Co-authored-by: Matthew Skinner <matt@pcmus.com>
* [flicbutton] Add binding to bom/openhab-addons

Signed-off-by: Patrick Fink <mail@pfink.de>
* [flicbutton] Cleanup / remove guava leftover

Signed-off-by: Patrick Fink <mail@pfink.de>
* [flicbutton] Remove online status description

Signed-off-by: Patrick Fink <mail@pfink.de>
* [flicbutton] Improve flicd hostname label

Signed-off-by: Patrick Fink <mail@pfink.de>
Co-authored-by: Fabian Wolter <github@fabian-wolter.de>
* [flicbutton] Do not catch IllegalArgumentException anymore as its not neeed

Signed-off-by: Patrick Fink <mail@pfink.de>
* [flicbutton] Use debug log level instead of info

Signed-off-by: Patrick Fink <mail@pfink.de>
* [flicbutton] Update version and license

Signed-off-by: Patrick Fink <mail@pfink.de>
* [flicbutton] Fix SAT warnings, e.g. add null handling annotations

Signed-off-by: Patrick Fink <mail@pfink.de>
* [flicbutton] Fix SAT warnings (2)

Signed-off-by: Patrick Fink <mail@pfink.de>
* [flicbutton] Concurrency refactoring & fixes

Signed-off-by: Patrick Fink <mail@pfink.de>
* [flicbutton] Cancel initialization task also when already running

Signed-off-by: Patrick Fink <mail@pfink.de>
* [flicbutton] Add javadoc and move FLIC_OPENHAB_EVENT_TRIGGER_MAP constant to constants class

Signed-off-by: Patrick Fink <mail@pfink.de>
* [flicbutton] Use ThingStatusDetail.OFFLINE.GONE when Flic button was removed from bridge

Signed-off-by: Patrick Fink <mail@pfink.de>
* [flicbutton] Fix FlicSimpleclientDiscoveryServiceImpl javadoc

Signed-off-by: Patrick Fink <mail@pfink.de>
* [flicbutton] Fix required definition of thing types

Signed-off-by: Patrick Fink <mail@pfink.de>
Co-authored-by: Fabian Wolter <github@fabian-wolter.de>
Co-authored-by: Matthew Skinner <matt@pcmus.com>
42 files changed:
CODEOWNERS
bom/openhab-addons/pom.xml
bundles/org.openhab.binding.flicbutton/NOTICE [new file with mode: 0644]
bundles/org.openhab.binding.flicbutton/README.md [new file with mode: 0644]
bundles/org.openhab.binding.flicbutton/pom.xml [new file with mode: 0644]
bundles/org.openhab.binding.flicbutton/src/3rdparty/LICENSE [new file with mode: 0644]
bundles/org.openhab.binding.flicbutton/src/3rdparty/java/io/flic/fliclib/javaclient/BatteryStatusListener.java [new file with mode: 0644]
bundles/org.openhab.binding.flicbutton/src/3rdparty/java/io/flic/fliclib/javaclient/Bdaddr.java [new file with mode: 0644]
bundles/org.openhab.binding.flicbutton/src/3rdparty/java/io/flic/fliclib/javaclient/ButtonConnectionChannel.java [new file with mode: 0644]
bundles/org.openhab.binding.flicbutton/src/3rdparty/java/io/flic/fliclib/javaclient/ButtonScanner.java [new file with mode: 0644]
bundles/org.openhab.binding.flicbutton/src/3rdparty/java/io/flic/fliclib/javaclient/FlicClient.java [new file with mode: 0644]
bundles/org.openhab.binding.flicbutton/src/3rdparty/java/io/flic/fliclib/javaclient/GeneralCallbacks.java [new file with mode: 0644]
bundles/org.openhab.binding.flicbutton/src/3rdparty/java/io/flic/fliclib/javaclient/GetButtonInfoResponseCallback.java [new file with mode: 0644]
bundles/org.openhab.binding.flicbutton/src/3rdparty/java/io/flic/fliclib/javaclient/GetInfoResponseCallback.java [new file with mode: 0644]
bundles/org.openhab.binding.flicbutton/src/3rdparty/java/io/flic/fliclib/javaclient/Packets.java [new file with mode: 0644]
bundles/org.openhab.binding.flicbutton/src/3rdparty/java/io/flic/fliclib/javaclient/ScanWizard.java [new file with mode: 0644]
bundles/org.openhab.binding.flicbutton/src/3rdparty/java/io/flic/fliclib/javaclient/StreamUtils.java [new file with mode: 0644]
bundles/org.openhab.binding.flicbutton/src/3rdparty/java/io/flic/fliclib/javaclient/TimerTask.java [new file with mode: 0644]
bundles/org.openhab.binding.flicbutton/src/3rdparty/java/io/flic/fliclib/javaclient/enums/BdAddrType.java [new file with mode: 0644]
bundles/org.openhab.binding.flicbutton/src/3rdparty/java/io/flic/fliclib/javaclient/enums/BluetoothControllerState.java [new file with mode: 0644]
bundles/org.openhab.binding.flicbutton/src/3rdparty/java/io/flic/fliclib/javaclient/enums/ClickType.java [new file with mode: 0644]
bundles/org.openhab.binding.flicbutton/src/3rdparty/java/io/flic/fliclib/javaclient/enums/ConnectionStatus.java [new file with mode: 0644]
bundles/org.openhab.binding.flicbutton/src/3rdparty/java/io/flic/fliclib/javaclient/enums/CreateConnectionChannelError.java [new file with mode: 0644]
bundles/org.openhab.binding.flicbutton/src/3rdparty/java/io/flic/fliclib/javaclient/enums/DisconnectReason.java [new file with mode: 0644]
bundles/org.openhab.binding.flicbutton/src/3rdparty/java/io/flic/fliclib/javaclient/enums/LatencyMode.java [new file with mode: 0644]
bundles/org.openhab.binding.flicbutton/src/3rdparty/java/io/flic/fliclib/javaclient/enums/RemovedReason.java [new file with mode: 0644]
bundles/org.openhab.binding.flicbutton/src/3rdparty/java/io/flic/fliclib/javaclient/enums/ScanWizardResult.java [new file with mode: 0644]
bundles/org.openhab.binding.flicbutton/src/main/feature/feature.xml [new file with mode: 0644]
bundles/org.openhab.binding.flicbutton/src/main/java/org/openhab/binding/flicbutton/internal/FlicButtonBindingConstants.java [new file with mode: 0644]
bundles/org.openhab.binding.flicbutton/src/main/java/org/openhab/binding/flicbutton/internal/FlicButtonHandlerFactory.java [new file with mode: 0644]
bundles/org.openhab.binding.flicbutton/src/main/java/org/openhab/binding/flicbutton/internal/discovery/FlicButtonDiscoveryService.java [new file with mode: 0644]
bundles/org.openhab.binding.flicbutton/src/main/java/org/openhab/binding/flicbutton/internal/discovery/FlicSimpleclientDiscoveryServiceImpl.java [new file with mode: 0644]
bundles/org.openhab.binding.flicbutton/src/main/java/org/openhab/binding/flicbutton/internal/handler/ChildThingHandler.java [new file with mode: 0644]
bundles/org.openhab.binding.flicbutton/src/main/java/org/openhab/binding/flicbutton/internal/handler/FlicButtonBatteryLevelListener.java [new file with mode: 0644]
bundles/org.openhab.binding.flicbutton/src/main/java/org/openhab/binding/flicbutton/internal/handler/FlicButtonEventListener.java [new file with mode: 0644]
bundles/org.openhab.binding.flicbutton/src/main/java/org/openhab/binding/flicbutton/internal/handler/FlicButtonHandler.java [new file with mode: 0644]
bundles/org.openhab.binding.flicbutton/src/main/java/org/openhab/binding/flicbutton/internal/handler/FlicDaemonBridgeConfiguration.java [new file with mode: 0644]
bundles/org.openhab.binding.flicbutton/src/main/java/org/openhab/binding/flicbutton/internal/handler/FlicDaemonBridgeHandler.java [new file with mode: 0644]
bundles/org.openhab.binding.flicbutton/src/main/java/org/openhab/binding/flicbutton/internal/util/FlicButtonUtils.java [new file with mode: 0644]
bundles/org.openhab.binding.flicbutton/src/main/resources/OH-INF/binding/binding.xml [new file with mode: 0644]
bundles/org.openhab.binding.flicbutton/src/main/resources/OH-INF/thing/thing-types.xml [new file with mode: 0644]
bundles/pom.xml

index 1e9001fe90f0e80d5f1f26f4f842e15c404efaef..63317c9bafabe8e7deb729e3a93f301d4df628db 100644 (file)
@@ -93,6 +93,7 @@
 /bundles/org.openhab.binding.exec/ @kgoderis
 /bundles/org.openhab.binding.feed/ @svilenvul
 /bundles/org.openhab.binding.feican/ @Hilbrand
+/bundles/org.openhab.binding.flicbutton/ @pfink
 /bundles/org.openhab.binding.fmiweather/ @ssalonen
 /bundles/org.openhab.binding.folderwatcher/ @goopilot
 /bundles/org.openhab.binding.folding/ @fa2k
index 415aecf36118d2bd2be2d6b75299f8b059161907..bb4e528761e3bf04932b06ef8ea31ccfe9840dce 100644 (file)
       <artifactId>org.openhab.binding.feican</artifactId>
       <version>${project.version}</version>
     </dependency>
+    <dependency>
+      <groupId>org.openhab.addons.bundles</groupId>
+      <artifactId>org.openhab.binding.flicbutton</artifactId>
+      <version>${project.version}</version>
+    </dependency>
     <dependency>
       <groupId>org.openhab.addons.bundles</groupId>
       <artifactId>org.openhab.binding.fmiweather</artifactId>
diff --git a/bundles/org.openhab.binding.flicbutton/NOTICE b/bundles/org.openhab.binding.flicbutton/NOTICE
new file mode 100644 (file)
index 0000000..ca1e0d6
--- /dev/null
@@ -0,0 +1,21 @@
+This content is produced and maintained by the openHAB project.
+
+* Project home: https://www.openhab.org
+
+== Declared Project Licenses
+
+This program and the accompanying materials are made available under the terms
+of the Eclipse Public License 2.0 which is available at
+https://www.eclipse.org/legal/epl-2.0/.
+
+== Source Code
+
+https://github.com/openhab/openhab-addons
+
+== Third-party Content
+
+fliclib-javaclient (files under src/3rdparty)
+
+* License: CC0 1.0
+* Project: https://github.com/50ButtonsEach/fliclib-linux-hci
+* Source:  https://github.com/50ButtonsEach/fliclib-linux-hci/tree/master/clientlib/java/lib/src/main/java/io/flic/fliclib/javaclient
diff --git a/bundles/org.openhab.binding.flicbutton/README.md b/bundles/org.openhab.binding.flicbutton/README.md
new file mode 100644 (file)
index 0000000..87e81df
--- /dev/null
@@ -0,0 +1,130 @@
+# Flic Button Binding 
+
+openHAB binding for using [Flic Buttons](https://flic.io/)
+with a [fliclib-linux-hci](https://github.com/50ButtonsEach/fliclib-linux-hci) bridge.
+
+Currently, although Flic Buttons are BLE devices, this binding only supports fliclib-linux-hci (flicd) as a bridge.
+The openHAB Bluetooth Bindings are not supported.
+Flicd requires a seperate Bluetooth adapter to work, so if you use this binding together with e.g. the
+[Bluez Binding](https://www.openhab.org/addons/bindings/bluetooth.bluez/),
+two physical Bluetooth adapters are required (one for Bluez and one for flicd).
+Be aware that flicd requires an initial internet connection for the verification of the buttons.
+After buttons are initially added to flicd, an internet connection is not required anymore.
+
+## Supported Things
+
+| Thing Type ID   | Description               |
+| --------------- | ------------------------- |
+| flicd-bridge | The bridge representing a running instance of [fliclib-linux-hci (flicd)](https://github.com/50ButtonsEach/fliclib-linux-hci) on the server. |
+| button | The Flic button (supports Flic 1 buttons as well as Flic 2 buttons) |
+
+## Discovery
+
+* There is no automatic discovery for flicd-bridge available.
+* After flicd-bridge is (manually) configured, buttons will be automatically discovered via background discovery as soon
+  as they're added with [simpleclient](https://github.com/50ButtonsEach/fliclib-linux-hci).
+
+If they're already attached to the flicd-bridge before configuring this binding, they can be discovered by triggering an
+active scan.
+
+## Thing Configuration
+
+### flicd-bridge
+
+Example for textual configuration:
+
+```
+Bridge flicbutton:flicd-bridge:mybridge
+```
+
+The default host is localhost:5551 (this should be sufficient if flicd is running with default settings on the same server as openHAB).
+If your flicd service is running somewhere else, specify it like this:
+
+```
+Bridge flicbutton:flicd-bridge:mybridge [ hostname="<YOUR_HOSTNAME>",  port=<YOUR_PORT>]
+```
+
+If flicd is running on a remote host, please do not forget to start it with the parameter `-s <openHAB IP>`, otherwise it won't be accessible for openHAB (more details on [fliclib-linux-hci](https://github.com/50ButtonsEach/fliclib-linux-hci)).
+
+### button
+
+For the button, the only config parameter is the MAC address.
+Normally, no textual configuration is necessary as buttons are auto discovered as soon as the bridge is configured.
+If you want to use textual configuration anyway, you can do it like this:
+
+```
+Bridge flicbutton:flicd-bridge:mybridge [ hostname="<YOUR_HOSTNAME>",  port=<YOUR_PORT>] {
+    Thing button myflic1 "<YOUR_LABEL>" [address ="<MAC_ADDRESS>"]
+    Thing button myflic2 "<YOUR_LABEL>" [address ="<MAC_ADDRESS>"]
+    ...
+}
+```
+
+You can lookup the MAC addresses of your buttons within the inbox of the UI.
+You're free to choose any label you like for your button.
+
+## Channels
+
+| Channel ID                | Channel Type             | Item Type                | Description                    |
+| ------------------------- | ------------------------ | --------------------------| ------------------------------ |
+| rawbutton                | [System Trigger Channel](https://www.openhab.org/docs/developer/bindings/thing-xml.html#system-trigger-channel-types) `system.rawbutton`  | Depends on the  [Trigger Profile](https://www.openhab.org/docs/configuration/items.html#profiles) used | Raw Button channel that triggers `PRESSED` and `RELEASED` events, allows to use openHAB profiles or own implementations via rules to detect e.g. double clicks, long presses etc.  |
+| button                   | [System Trigger Channel](https://www.openhab.org/docs/developer/bindings/thing-xml.html#system-trigger-channel-types) `system.button`    | Depends on the [Trigger Profile](https://www.openhab.org/docs/configuration/items.html#profiles) used | Button channel that is using Flic's implementation for detecting long, short or double clicks. Triggers `SHORT_PRESSED`,`DOUBLE_PRESSED` and `LONG_PRESSED` events.   |
+| battery-level            | [System State Channel](https://www.openhab.org/docs/developer/bindings/thing-xml.html#system-state-channel-types) `system.battery-level`     | Number | Represents the battery level as a percentage (0-100%).
+## Example
+
+### Initial setup
+
+1. Setup and run flicd as described in [fliclib-linux-hci](https://github.com/50ButtonsEach/fliclib-linux-hci). 
+   Please consider that you need a separate Bluetooth adapter. Shared usage with other Bluetooth services (e.g. Bluez)
+   is not possible.
+1. Connect your buttons to flicd using the simpleclient as described in
+   [fliclib-linux-hci](https://github.com/50ButtonsEach/fliclib-linux-hci). Flicd has to run in background the whole
+   time, simpleclient can be killed after you successfully test the button connects.
+1. Add a flicd-bridge via the UI or textual configuration. Please consider that flicd only accepts connections from
+   localhost by default, to enable remote connections from openHAB you have to use the `--server-addr` parameter as
+   described in [fliclib-linux-hci](https://github.com/50ButtonsEach/fliclib-linux-hci).
+1. When the bridge is online, buttons newly added via simpleclient will automatically get discovered via background
+   discovery. To discover buttons that were set up before the binding was setup, please run an active scan.
+
+### Configuration Example using Profiles
+
+[Profiles](https://www.openhab.org/docs/configuration/items.html#profiles) are the recommended way to use this binding.
+
+demo.things:
+
+```
+Bridge flicbutton:flicd-bridge:local-flicd {
+       Thing button flic_livingroom "Yellow Button Living Room" [address = "60:13:B3:02:18:BD"]
+       Thing button flic_kitchen "Black Button Kitchen" [address = "B5:7E:59:78:86:9F"]
+}
+```
+
+demo.items:
+
+```
+Dimmer Light_LivingRoom  { channel="milight:rgbLed:milight2:4:ledbrightness", channel="flicbutton:button:local-flicd:flic_livingroom:rawbutton" [profile="rawbutton-toggle-switch"], channel="flicbutton:button:local-flicd:flic_kitchen:rawbutton" [profile="rawbutton-toggle-switch"] }  // We have a combined kitchen / livingroom, so we control the living room lights with switches from the living room and from the kitchen
+Switch Light_Kitchen    { channel="hue:group:1:kitchen-bulbs:switch", channel="flicbutton:button:local-flicd:flic_kitchen:rawbutton" [profile="rawbutton-toggle-switch"] }
+```
+
+### Configuration Example using Rules
+
+It's also possible to setup [Rules](https://www.openhab.org/docs/configuration/rules-dsl.html).
+The following rules help to initially test your setup as they'll trigger log messages on incoming events.
+
+```
+rule "Button rule using the button channel"
+
+when
+    Channel "flicbutton:button:local-flicd:flic_livingroom:button" triggered SHORT_PRESSED
+then
+    logInfo("Flic", "Flic 'short pressed' triggered")
+end
+
+rule "Button rule directly using the rawbutton channel"
+
+when
+    Channel "flicbutton:button:local-flicd:flic_livingroom:rawbutton" triggered
+then
+    logInfo("Flic", "Flic pressed: " + receivedEvent.event)
+end
+```
diff --git a/bundles/org.openhab.binding.flicbutton/pom.xml b/bundles/org.openhab.binding.flicbutton/pom.xml
new file mode 100644 (file)
index 0000000..9163b24
--- /dev/null
@@ -0,0 +1,38 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+  xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+
+  <modelVersion>4.0.0</modelVersion>
+
+  <parent>
+    <groupId>org.openhab.addons.bundles</groupId>
+    <artifactId>org.openhab.addons.reactor.bundles</artifactId>
+    <version>3.3.0-SNAPSHOT</version>
+  </parent>
+
+  <artifactId>org.openhab.binding.flicbutton</artifactId>
+
+  <name>openHAB Add-ons :: Bundles :: FlicButton Binding</name>
+
+  <build>
+    <plugins>
+      <plugin>
+        <groupId>org.codehaus.mojo</groupId>
+        <artifactId>build-helper-maven-plugin</artifactId>
+        <executions>
+          <execution>
+            <goals>
+              <goal>add-source</goal>
+            </goals>
+            <phase>generate-sources</phase>
+            <configuration>
+              <sources>
+                <source>src/3rdparty/java</source>
+              </sources>
+            </configuration>
+          </execution>
+        </executions>
+      </plugin>
+    </plugins>
+  </build>
+</project>
diff --git a/bundles/org.openhab.binding.flicbutton/src/3rdparty/LICENSE b/bundles/org.openhab.binding.flicbutton/src/3rdparty/LICENSE
new file mode 100644 (file)
index 0000000..1625c17
--- /dev/null
@@ -0,0 +1,121 @@
+Creative Commons Legal Code
+
+CC0 1.0 Universal
+
+    CREATIVE COMMONS CORPORATION IS NOT A LAW FIRM AND DOES NOT PROVIDE
+    LEGAL SERVICES. DISTRIBUTION OF THIS DOCUMENT DOES NOT CREATE AN
+    ATTORNEY-CLIENT RELATIONSHIP. CREATIVE COMMONS PROVIDES THIS
+    INFORMATION ON AN "AS-IS" BASIS. CREATIVE COMMONS MAKES NO WARRANTIES
+    REGARDING THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS
+    PROVIDED HEREUNDER, AND DISCLAIMS LIABILITY FOR DAMAGES RESULTING FROM
+    THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS PROVIDED
+    HEREUNDER.
+
+Statement of Purpose
+
+The laws of most jurisdictions throughout the world automatically confer
+exclusive Copyright and Related Rights (defined below) upon the creator
+and subsequent owner(s) (each and all, an "owner") of an original work of
+authorship and/or a database (each, a "Work").
+
+Certain owners wish to permanently relinquish those rights to a Work for
+the purpose of contributing to a commons of creative, cultural and
+scientific works ("Commons") that the public can reliably and without fear
+of later claims of infringement build upon, modify, incorporate in other
+works, reuse and redistribute as freely as possible in any form whatsoever
+and for any purposes, including without limitation commercial purposes.
+These owners may contribute to the Commons to promote the ideal of a free
+culture and the further production of creative, cultural and scientific
+works, or to gain reputation or greater distribution for their Work in
+part through the use and efforts of others.
+
+For these and/or other purposes and motivations, and without any
+expectation of additional consideration or compensation, the person
+associating CC0 with a Work (the "Affirmer"), to the extent that he or she
+is an owner of Copyright and Related Rights in the Work, voluntarily
+elects to apply CC0 to the Work and publicly distribute the Work under its
+terms, with knowledge of his or her Copyright and Related Rights in the
+Work and the meaning and intended legal effect of CC0 on those rights.
+
+1. Copyright and Related Rights. A Work made available under CC0 may be
+protected by copyright and related or neighboring rights ("Copyright and
+Related Rights"). Copyright and Related Rights include, but are not
+limited to, the following:
+
+  i. the right to reproduce, adapt, distribute, perform, display,
+     communicate, and translate a Work;
+ ii. moral rights retained by the original author(s) and/or performer(s);
+iii. publicity and privacy rights pertaining to a person's image or
+     likeness depicted in a Work;
+ iv. rights protecting against unfair competition in regards to a Work,
+     subject to the limitations in paragraph 4(a), below;
+  v. rights protecting the extraction, dissemination, use and reuse of data
+     in a Work;
+ vi. database rights (such as those arising under Directive 96/9/EC of the
+     European Parliament and of the Council of 11 March 1996 on the legal
+     protection of databases, and under any national implementation
+     thereof, including any amended or successor version of such
+     directive); and
+vii. other similar, equivalent or corresponding rights throughout the
+     world based on applicable law or treaty, and any national
+     implementations thereof.
+
+2. Waiver. To the greatest extent permitted by, but not in contravention
+of, applicable law, Affirmer hereby overtly, fully, permanently,
+irrevocably and unconditionally waives, abandons, and surrenders all of
+Affirmer's Copyright and Related Rights and associated claims and causes
+of action, whether now known or unknown (including existing as well as
+future claims and causes of action), in the Work (i) in all territories
+worldwide, (ii) for the maximum duration provided by applicable law or
+treaty (including future time extensions), (iii) in any current or future
+medium and for any number of copies, and (iv) for any purpose whatsoever,
+including without limitation commercial, advertising or promotional
+purposes (the "Waiver"). Affirmer makes the Waiver for the benefit of each
+member of the public at large and to the detriment of Affirmer's heirs and
+successors, fully intending that such Waiver shall not be subject to
+revocation, rescission, cancellation, termination, or any other legal or
+equitable action to disrupt the quiet enjoyment of the Work by the public
+as contemplated by Affirmer's express Statement of Purpose.
+
+3. Public License Fallback. Should any part of the Waiver for any reason
+be judged legally invalid or ineffective under applicable law, then the
+Waiver shall be preserved to the maximum extent permitted taking into
+account Affirmer's express Statement of Purpose. In addition, to the
+extent the Waiver is so judged Affirmer hereby grants to each affected
+person a royalty-free, non transferable, non sublicensable, non exclusive,
+irrevocable and unconditional license to exercise Affirmer's Copyright and
+Related Rights in the Work (i) in all territories worldwide, (ii) for the
+maximum duration provided by applicable law or treaty (including future
+time extensions), (iii) in any current or future medium and for any number
+of copies, and (iv) for any purpose whatsoever, including without
+limitation commercial, advertising or promotional purposes (the
+"License"). The License shall be deemed effective as of the date CC0 was
+applied by Affirmer to the Work. Should any part of the License for any
+reason be judged legally invalid or ineffective under applicable law, such
+partial invalidity or ineffectiveness shall not invalidate the remainder
+of the License, and in such case Affirmer hereby affirms that he or she
+will not (i) exercise any of his or her remaining Copyright and Related
+Rights in the Work or (ii) assert any associated claims and causes of
+action with respect to the Work, in either case contrary to Affirmer's
+express Statement of Purpose.
+
+4. Limitations and Disclaimers.
+
+ a. No trademark or patent rights held by Affirmer are waived, abandoned,
+    surrendered, licensed or otherwise affected by this document.
+ b. Affirmer offers the Work as-is and makes no representations or
+    warranties of any kind concerning the Work, express, implied,
+    statutory or otherwise, including without limitation warranties of
+    title, merchantability, fitness for a particular purpose, non
+    infringement, or the absence of latent or other defects, accuracy, or
+    the present or absence of errors, whether or not discoverable, all to
+    the greatest extent permissible under applicable law.
+ c. Affirmer disclaims responsibility for clearing rights of other persons
+    that may apply to the Work or any use thereof, including without
+    limitation any person's Copyright and Related Rights in the Work.
+    Further, Affirmer disclaims responsibility for obtaining any necessary
+    consents, permissions or other rights required for any use of the
+    Work.
+ d. Affirmer understands and acknowledges that Creative Commons is not a
+    party to this document and has no duty or obligation with respect to
+    this CC0 or use of the Work.
\ No newline at end of file
diff --git a/bundles/org.openhab.binding.flicbutton/src/3rdparty/java/io/flic/fliclib/javaclient/BatteryStatusListener.java b/bundles/org.openhab.binding.flicbutton/src/3rdparty/java/io/flic/fliclib/javaclient/BatteryStatusListener.java
new file mode 100644 (file)
index 0000000..bb562f1
--- /dev/null
@@ -0,0 +1,45 @@
+package io.flic.fliclib.javaclient;
+
+import java.io.IOException;
+import java.util.concurrent.atomic.AtomicInteger;
+
+/**
+ * Battery status listener.
+ *
+ * Add this listener to a {@link FlicClient} by executing {@link FlicClient#addBatteryStatusListener(BatteryStatusListener)}.
+ */
+public class BatteryStatusListener {
+    private static AtomicInteger nextId = new AtomicInteger();
+    int listenerId = nextId.getAndIncrement();
+
+    private Bdaddr bdaddr;
+    Callbacks callbacks;
+
+    public BatteryStatusListener(Bdaddr bdaddr, Callbacks callbacks) {
+        if (bdaddr == null) {
+            throw new IllegalArgumentException("bdaddr is null");
+        }
+        if (callbacks == null) {
+            throw new IllegalArgumentException("callbacks is null");
+        }
+        this.bdaddr = bdaddr;
+        this.callbacks = callbacks;
+    }
+
+    public Bdaddr getBdaddr() {
+        return bdaddr;
+    }
+
+    public abstract static class Callbacks {
+        /**
+         * This will be called when the battery status has been updated.
+         * It will also be called immediately after the battery status listener has been created.
+         * If the button stays connected, this method will be called approximately every three hours.
+         *
+         * @param bdaddr            Bluetooth device address
+         * @param batteryPercentage A number between 0 and 100 for the battery level. Will be -1 if unknown.
+         * @param timestamp         Standard UNIX timestamp, in seconds, for the event.
+         */
+        public abstract void onBatteryStatus(Bdaddr bdaddr, int batteryPercentage, long timestamp) throws IOException;
+    }
+}
\ No newline at end of file
diff --git a/bundles/org.openhab.binding.flicbutton/src/3rdparty/java/io/flic/fliclib/javaclient/Bdaddr.java b/bundles/org.openhab.binding.flicbutton/src/3rdparty/java/io/flic/fliclib/javaclient/Bdaddr.java
new file mode 100644 (file)
index 0000000..16a7b39
--- /dev/null
@@ -0,0 +1,68 @@
+package io.flic.fliclib.javaclient;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.Arrays;
+
+/**
+ * Bluetooth address.
+ */
+public class Bdaddr {
+    private byte[] bytes;
+
+    /**
+     * Creates a Bdaddr given the bluetooth address in string format.
+     *
+     * @param addr address of the format xx:xx:xx:xx:xx:xx
+     */
+    public Bdaddr(String addr) {
+        bytes = new byte[6];
+        bytes[5] = (byte)Integer.parseInt(addr.substring(0, 2), 16);
+        bytes[4] = (byte)Integer.parseInt(addr.substring(3, 5), 16);
+        bytes[3] = (byte)Integer.parseInt(addr.substring(6, 8), 16);
+        bytes[2] = (byte)Integer.parseInt(addr.substring(9, 11), 16);
+        bytes[1] = (byte)Integer.parseInt(addr.substring(12, 14), 16);
+        bytes[0] = (byte)Integer.parseInt(addr.substring(15, 17), 16);
+    }
+
+    Bdaddr(InputStream stream) throws IOException {
+        bytes = new byte[6];
+        for (int i = 0; i < 6; i++) {
+            bytes[i] = (byte)stream.read();
+        }
+    }
+
+    byte[] getBytes() {
+        return bytes.clone();
+    }
+
+    /**
+     * Create a string representing the bluetooth address.
+     *
+     * @return A string of the bdaddr
+     */
+    @Override
+    public String toString() {
+        return String.format("%02x:%02x:%02x:%02x:%02x:%02x", bytes[5], bytes[4], bytes[3], bytes[2], bytes[1], bytes[0]);
+    }
+
+    @Override
+    public int hashCode() {
+        return (bytes[0] & 0xff) ^ ((bytes[1] & 0xff) << 8) ^ ((bytes[2] & 0xff) << 16) ^ ((bytes[3] & 0xff) << 24) ^ (bytes[4] & 0xff) ^ ((bytes[5] & 0xff) << 8);
+    }
+
+    @Override
+    public boolean equals(Object obj) {
+        if (this == obj) {
+            return true;
+        }
+        if (obj == null) {
+            return false;
+        }
+        if (!(obj instanceof Bdaddr)) {
+            return false;
+        }
+        Bdaddr other = (Bdaddr)obj;
+        return Arrays.equals(bytes, other.bytes);
+    }
+}
diff --git a/bundles/org.openhab.binding.flicbutton/src/3rdparty/java/io/flic/fliclib/javaclient/ButtonConnectionChannel.java b/bundles/org.openhab.binding.flicbutton/src/3rdparty/java/io/flic/fliclib/javaclient/ButtonConnectionChannel.java
new file mode 100644 (file)
index 0000000..a006aea
--- /dev/null
@@ -0,0 +1,188 @@
+package io.flic.fliclib.javaclient;
+
+import java.io.IOException;
+import java.util.concurrent.atomic.AtomicInteger;
+
+import io.flic.fliclib.javaclient.enums.*;
+
+/**
+ * Button connection channel.
+ *
+ * Add this button connection channel to a {@link FlicClient} by executing {@link FlicClient#addConnectionChannel(ButtonConnectionChannel)}.
+ * You may only have this connection channel added to one {@link FlicClient} at a time.
+ */
+public class ButtonConnectionChannel {
+    private static AtomicInteger nextId = new AtomicInteger();
+    int connId = nextId.getAndIncrement();
+
+    FlicClient client;
+
+    private Bdaddr bdaddr;
+    private LatencyMode latencyMode;
+    private short autoDisconnectTime;
+    Callbacks callbacks;
+
+    final Object lock = new Object();
+
+    /**
+     * Create a connection channel using the specified parameters.
+     *
+     * Add this button connection channel to a {@link FlicClient} by executing {@link FlicClient#addConnectionChannel(ButtonConnectionChannel)}.
+     *
+     * @param bdaddr
+     * @param latencyMode
+     * @param autoDisconnectTime Number of seconds (0 - 511) until disconnect if no button event happens. 512 disables this feature.
+     * @param callbacks
+     */
+    public ButtonConnectionChannel(Bdaddr bdaddr, LatencyMode latencyMode, short autoDisconnectTime, Callbacks callbacks) {
+        if (bdaddr == null) {
+            throw new IllegalArgumentException("bdaddr is null");
+        }
+        if (latencyMode == null) {
+            throw new IllegalArgumentException("latencyMode is null");
+        }
+        if (callbacks == null) {
+            throw new IllegalArgumentException("callbacks is null");
+        }
+        this.bdaddr = bdaddr;
+        this.latencyMode = latencyMode;
+        this.autoDisconnectTime = autoDisconnectTime;
+        this.callbacks = callbacks;
+    }
+
+    /**
+     * Create a connection channel using the specified parameters.
+     *
+     * Add this button connection channel to a {@link FlicClient} by executing {@link FlicClient#addConnectionChannel(ButtonConnectionChannel)}.
+     *
+     * @param bdaddr
+     * @param callbacks
+     */
+    public ButtonConnectionChannel(Bdaddr bdaddr, Callbacks callbacks) {
+        this(bdaddr, LatencyMode.NormalLatency, (short)0x1ff, callbacks);
+    }
+
+    /**
+     * Get the {@link FlicClient} for this {@link ButtonConnectionChannel}.
+     *
+     * @return
+     */
+    public FlicClient getFlicClient() {
+        return client;
+    }
+
+    public Bdaddr getBdaddr() {
+        return bdaddr;
+    }
+
+    public LatencyMode getLatencyMode() {
+        return latencyMode;
+    }
+
+    public short getAutoDisconnectTime() {
+        return autoDisconnectTime;
+    }
+
+    /**
+     * Applies new latency mode parameter.
+     *
+     * @param latencyMode
+     */
+    public void setLatencyMode(LatencyMode latencyMode) throws IOException {
+        if (latencyMode == null) {
+            throw new IllegalArgumentException("latencyMode is null");
+        }
+        synchronized (lock) {
+            this.latencyMode = latencyMode;
+
+            FlicClient cl = client;
+            if (cl != null) {
+                CmdChangeModeParameters pkt = new CmdChangeModeParameters();
+                pkt.connId = connId;
+                pkt.latencyMode = latencyMode;
+                pkt.autoDisconnectTime = autoDisconnectTime;
+                cl.sendPacket(pkt);
+            }
+        }
+    }
+
+    /**
+     * Applies new auto disconnect time parameter.
+     *
+     * @param autoDisconnectTime Number of seconds (0 - 511) until disconnect if no button event happens. 512 disables this feature.
+     */
+    public void setAutoDisconnectTime(short autoDisconnectTime) throws IOException {
+        if (latencyMode == null) {
+            throw new IllegalArgumentException("latencyMode is null");
+        }
+        synchronized (lock) {
+            this.autoDisconnectTime = autoDisconnectTime;
+
+            FlicClient cl = client;
+            if (cl != null) {
+                CmdChangeModeParameters pkt = new CmdChangeModeParameters();
+                pkt.connId = connId;
+                pkt.latencyMode = latencyMode;
+                pkt.autoDisconnectTime = autoDisconnectTime;
+                cl.sendPacket(pkt);
+            }
+        }
+    }
+
+    /**
+     * User callbacks for incoming events.
+     *
+     * See the protocol specification for further details.
+     */
+    public abstract static class Callbacks {
+
+        /**
+         * Called when the server has received the create connection channel command.
+         *
+         * If createConnectionChannelError is {@link CreateConnectionChannelError#NoError}, other events will arrive until {@link #onRemoved} is received.
+         * There will be no {@link #onRemoved} if an error occurred.
+         *
+         * @param channel
+         * @param createConnectionChannelError
+         * @param connectionStatus
+         * @throws IOException
+         */
+        public void onCreateConnectionChannelResponse(ButtonConnectionChannel channel, CreateConnectionChannelError createConnectionChannelError, ConnectionStatus connectionStatus) throws IOException {
+        }
+
+        /**
+         * Called when the connection channel has been removed.
+         *
+         * Check the removedReason to find out why. From this point, the connection channel can be re-added again if you wish.
+         *
+         * @param channel
+         * @param removedReason
+         * @throws IOException
+         */
+        public void onRemoved(ButtonConnectionChannel channel, RemovedReason removedReason) throws IOException {
+        }
+
+        /**
+         * Called when the connection status changes.
+         *
+         * @param channel
+         * @param connectionStatus
+         * @param disconnectReason Only valid if connectionStatus is {@link ConnectionStatus#Disconnected}
+         * @throws IOException
+         */
+        public void onConnectionStatusChanged(ButtonConnectionChannel channel, ConnectionStatus connectionStatus, DisconnectReason disconnectReason) throws IOException {
+        }
+
+        public void onButtonUpOrDown(ButtonConnectionChannel channel, ClickType clickType, boolean wasQueued, int timeDiff) throws IOException {
+        }
+
+        public void onButtonClickOrHold(ButtonConnectionChannel channel, ClickType clickType, boolean wasQueued, int timeDiff) throws IOException {
+        }
+
+        public void onButtonSingleOrDoubleClick(ButtonConnectionChannel channel, ClickType clickType, boolean wasQueued, int timeDiff) throws IOException {
+        }
+
+        public void onButtonSingleOrDoubleClickOrHold(ButtonConnectionChannel channel, ClickType clickType, boolean wasQueued, int timeDiff) throws IOException {
+        }
+    }
+}
diff --git a/bundles/org.openhab.binding.flicbutton/src/3rdparty/java/io/flic/fliclib/javaclient/ButtonScanner.java b/bundles/org.openhab.binding.flicbutton/src/3rdparty/java/io/flic/fliclib/javaclient/ButtonScanner.java
new file mode 100644 (file)
index 0000000..41ca148
--- /dev/null
@@ -0,0 +1,28 @@
+package io.flic.fliclib.javaclient;
+
+import java.io.IOException;
+import java.util.concurrent.atomic.AtomicInteger;
+
+/**
+ * Button scanner class.
+ *
+ * Inherit this class and override the {@link #onAdvertisementPacket(Bdaddr, String, int, boolean, boolean)} method.
+ * Then add this button scanner to a {@link FlicClient} using {@link FlicClient#addScanner(ButtonScanner)} to start it.
+ */
+public abstract class ButtonScanner {
+    private static AtomicInteger nextId = new AtomicInteger();
+    int scanId = nextId.getAndIncrement();
+
+    /**
+     * This will be called for every received advertisement packet from a Flic button.
+     *
+     * @param bdaddr Bluetooth address
+     * @param name Advertising name
+     * @param rssi RSSI value in dBm
+     * @param isPrivate The button is private and won't accept new connections from non-bonded clients
+     * @param alreadyVerified The server has already verified this button, which means you can connect to it even if it's private
+     * @param alreadyConnectedToThisDevice The button is already connected to this device
+     * @param alreadyConnectedToOtherDevice The button is already connected to another device
+     */
+    public abstract void onAdvertisementPacket(Bdaddr bdaddr, String name, int rssi, boolean isPrivate, boolean alreadyVerified, boolean alreadyConnectedToThisDevice, boolean alreadyConnectedToOtherDevice) throws IOException;
+}
diff --git a/bundles/org.openhab.binding.flicbutton/src/3rdparty/java/io/flic/fliclib/javaclient/FlicClient.java b/bundles/org.openhab.binding.flicbutton/src/3rdparty/java/io/flic/fliclib/javaclient/FlicClient.java
new file mode 100644 (file)
index 0000000..07f0544
--- /dev/null
@@ -0,0 +1,630 @@
+package io.flic.fliclib.javaclient;
+
+import io.flic.fliclib.javaclient.enums.CreateConnectionChannelError;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.net.Socket;
+import java.net.SocketTimeoutException;
+import java.net.UnknownHostException;
+import java.util.ArrayDeque;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ConcurrentLinkedQueue;
+import java.util.concurrent.ConcurrentSkipListMap;
+
+/**
+ * Implements a FlicClient over a TCP Socket.
+ *
+ * When this class is constructed, a socket connection is established.
+ *
+ * You may then send commands to the server and set timers.
+ *
+ * Once you are ready with the initialization you must call the {@link #handleEvents()} method which is a main loop that never exits, unless the socket is closed.
+ *
+ * For a more detailed description of all commands, events and enums, check the protocol specification.
+ */
+public class FlicClient {
+    private Socket socket;
+    private InputStream socketInputStream;
+    private OutputStream socketOutputStream;
+
+    private ConcurrentHashMap<Integer, ButtonScanner> scanners = new ConcurrentHashMap<>();
+    private ConcurrentHashMap<Integer, ButtonConnectionChannel> connectionChannels = new ConcurrentHashMap<>();
+    private ConcurrentHashMap<Integer, ScanWizard> scanWizards = new ConcurrentHashMap<>();
+    private ConcurrentHashMap<Integer, BatteryStatusListener> batteryStatusListeners = new ConcurrentHashMap<>();
+    private ConcurrentLinkedQueue<GetInfoResponseCallback> getInfoResponseCallbackQueue = new ConcurrentLinkedQueue<>();
+    private ArrayDeque<GetButtonInfoResponseCallback> getButtonInfoResponseCallbackQueue = new ArrayDeque<>();
+
+    private volatile GeneralCallbacks generalCallbacks = new GeneralCallbacks();
+
+    private ConcurrentSkipListMap<Long, TimerTask> timers = new ConcurrentSkipListMap<>();
+
+    private Thread handleEventsThread;
+
+    /**
+     * Create a FlicClient and connect to the specified hostName and TCP port
+     *
+     * @param hostName
+     * @param port
+     * @throws UnknownHostException
+     * @throws IOException
+     */
+    public FlicClient(String hostName, int port) throws UnknownHostException, IOException {
+        socket = new Socket(hostName, port);
+        socket.setKeepAlive(true);
+        socketInputStream = socket.getInputStream();
+        socketOutputStream = socket.getOutputStream();
+    }
+
+    /**
+     * Create a FlicClient and connect to the specified hostName using the default TCP port
+     *
+     * @param hostName
+     * @throws UnknownHostException
+     * @throws IOException
+     */
+    public FlicClient(String hostName) throws UnknownHostException, IOException {
+        this(hostName, 5551);
+    }
+
+    /**
+     * Close the socket.
+     *
+     * From this point any use of this FlicClient is illegal.
+     * The {@link #handleEvents()} will return as soon as the closing is done.
+     *
+     * @throws IOException
+     */
+    public void close() throws IOException {
+        runOnHandleEventsThread(new TimerTask() {
+            @Override
+            public void run() throws IOException {
+                socket.close();
+            }
+        });
+    }
+
+    /**
+     * Set general callbacks to be called upon receiving some specific events.
+     *
+     * @param callbacks
+     */
+    public void setGeneralCallbacks(GeneralCallbacks callbacks) {
+        if (callbacks == null) {
+            callbacks = new GeneralCallbacks();
+        }
+        generalCallbacks = callbacks;
+    }
+
+    /**
+     * Get info about the current state of the server.
+     *
+     * The server will send back its information directly and the callback will be called once the response arrives.
+     *
+     * @param callback
+     * @throws IOException
+     */
+    public void getInfo(GetInfoResponseCallback callback) throws IOException {
+        if (callback == null) {
+            throw new IllegalArgumentException("callback is null");
+        }
+        getInfoResponseCallbackQueue.add(callback);
+
+        CmdGetInfo pkt = new CmdGetInfo();
+        sendPacket(pkt);
+    }
+
+    /**
+     * Get button info for a verified button.
+     *
+     * The server will send back its information directly and the callback will be called once the response arrives.
+     * Responses will arrive in the same order as requested.
+     *
+     * If the button isn't verified, the data sent to callback will be null.
+     *
+     * @param bdaddr The bluetooth address.
+     * @param callback Callback for the response.
+     * @throws IOException
+     */
+    public void getButtonInfo(final Bdaddr bdaddr, final GetButtonInfoResponseCallback callback) throws IOException {
+        if (callback == null) {
+            throw new IllegalArgumentException("callback is null");
+        }
+        // Run on events thread to ensure ordering if multiple requests are issued at the same time
+        runOnHandleEventsThread(new TimerTask() {
+            @Override
+            public void run() throws IOException {
+                getButtonInfoResponseCallbackQueue.add(callback);
+
+                CmdGetButtonInfo pkt = new CmdGetButtonInfo();
+                pkt.bdaddr = bdaddr;
+                sendPacket(pkt);
+            }
+        });
+    }
+
+    /**
+     * Add a scanner.
+     *
+     * The scan will start directly once the scanner is added.
+     *
+     * @param buttonScanner
+     * @throws IOException
+     */
+    public void addScanner(ButtonScanner buttonScanner) throws IOException {
+        if (buttonScanner == null) {
+            throw new IllegalArgumentException("buttonScanner is null");
+        }
+        if (scanners.putIfAbsent(buttonScanner.scanId, buttonScanner) != null) {
+            throw new IllegalArgumentException("Button scanner already added");
+        }
+
+        CmdCreateScanner pkt = new CmdCreateScanner();
+        pkt.scanId = buttonScanner.scanId;
+        sendPacket(pkt);
+    }
+
+    /**
+     * Remove a scanner.
+     *
+     * @param buttonScanner The same scanner that was used in {@link #addScanner(ButtonScanner)}
+     * @throws IOException
+     */
+    public void removeScanner(ButtonScanner buttonScanner) throws IOException {
+        if (buttonScanner == null) {
+            throw new IllegalArgumentException("buttonScanner is null");
+        }
+        if (scanners.remove(buttonScanner.scanId) == null) {
+            throw new IllegalArgumentException("Button scanner was never added");
+        }
+
+        CmdRemoveScanner pkt = new CmdRemoveScanner();
+        pkt.scanId = buttonScanner.scanId;
+        sendPacket(pkt);
+    }
+    
+    /**
+     * Add a scan wizard.
+     *
+     * The scan wizard will start directly once the scan wizard is added.
+     *
+     * @param scanWizard
+     * @throws IOException
+     */
+    public void addScanWizard(ScanWizard scanWizard) throws IOException {
+        if (scanWizard == null) {
+            throw new IllegalArgumentException("scanWizard is null");
+        }
+        if (scanWizards.putIfAbsent(scanWizard.scanWizardId, scanWizard) != null) {
+            throw new IllegalArgumentException("Scan wizard already added");
+        }
+        
+        CmdCreateScanWizard pkt = new CmdCreateScanWizard();
+        pkt.scanWizardId = scanWizard.scanWizardId;
+        sendPacket(pkt);
+    }
+    
+    /**
+     * Cancel a scan wizard.
+     *
+     * This will cancel an ongoing scan wizard.
+     *
+     * If cancelled due to this request, the result of the scan wizard will be WizardCancelledByUser.
+     *
+     * @param scanWizard The same scan wizard that was used in {@link #addScanWizard(ScanWizard)}
+     * @throws IOException
+     */
+    public void cancelScanWizard(ScanWizard scanWizard) throws IOException {
+        if (scanWizard == null) {
+            throw new IllegalArgumentException("scanWizard is null");
+        }
+        
+        CmdCancelScanWizard pkt = new CmdCancelScanWizard();
+        pkt.scanWizardId = scanWizard.scanWizardId;
+        sendPacket(pkt);
+    }
+
+    /**
+     * Adds a connection channel to a specific Flic button.
+     *
+     * This will start listening for a specific Flic button's connection and button events.
+     * Make sure the Flic is either in public mode (by holding it down for 7 seconds) or already verified before calling this method.
+     *
+     * The {@link ButtonConnectionChannel.Callbacks#onCreateConnectionChannelResponse}
+     * method will be called after this command has been received by the server.
+     *
+     * You may have as many connection channels as you wish for a specific Flic Button.
+     *
+     * @param channel
+     * @throws IOException
+     */
+    public void addConnectionChannel(ButtonConnectionChannel channel) throws IOException {
+        if (channel == null) {
+            throw new IllegalArgumentException("channel is null");
+        }
+        if (connectionChannels.putIfAbsent(channel.connId, channel) != null) {
+            throw new IllegalArgumentException("Connection channel already added");
+        }
+
+        synchronized (channel.lock) {
+            channel.client = this;
+
+            CmdCreateConnectionChannel pkt = new CmdCreateConnectionChannel();
+            pkt.connId = channel.connId;
+            pkt.bdaddr = channel.getBdaddr();
+            pkt.latencyMode = channel.getLatencyMode();
+            pkt.autoDisconnectTime = channel.getAutoDisconnectTime();
+            sendPacket(pkt);
+        }
+    }
+
+    /**
+     * Remove a connection channel.
+     *
+     * This will stop listening for new events for a specific connection channel that has previously been added.
+     * Note: The effect of this command will take place at the time the {@link ButtonConnectionChannel.Callbacks#onRemoved} event arrives.
+     *
+     * @param channel
+     * @throws IOException
+     */
+    public void removeConnectionChannel(ButtonConnectionChannel channel) throws IOException {
+        if (channel == null) {
+            throw new IllegalArgumentException("channel is null");
+        }
+
+        CmdRemoveConnectionChannel pkt = new CmdRemoveConnectionChannel();
+        pkt.connId = channel.connId;
+        sendPacket(pkt);
+    }
+
+    /**
+     * Force disconnection or cancel pending connection of a specific Flic button.
+     *
+     * This removes all connection channels for all clients connected to the server for this specific Flic button.
+     *
+     * @param bdaddr
+     * @throws IOException
+     */
+    public void forceDisconnect(Bdaddr bdaddr) throws IOException {
+        if (bdaddr == null) {
+            throw new IllegalArgumentException("bdaddr is null");
+        }
+
+        CmdForceDisconnect pkt = new CmdForceDisconnect();
+        pkt.bdaddr = bdaddr;
+        sendPacket(pkt);
+    }
+    
+    /**
+     * Delete a button.
+     *
+     * @param bdaddr
+     * @throws IOException
+     */
+    public void deleteButton(Bdaddr bdaddr) throws IOException {
+        if (bdaddr == null) {
+            throw new IllegalArgumentException("bdaddr is null");
+        }
+
+        CmdDeleteButton pkt = new CmdDeleteButton();
+        pkt.bdaddr = bdaddr;
+        sendPacket(pkt);
+    }
+
+    /**
+     * Add a battery status listener.
+     *
+     * @param listener
+     * @throws IOException
+     */
+    public void addBatteryStatusListener(BatteryStatusListener listener) throws IOException {
+        if (listener == null) {
+            throw new IllegalArgumentException("listener is null");
+        }
+        if (batteryStatusListeners.putIfAbsent(listener.listenerId, listener) != null) {
+            throw new IllegalArgumentException("Battery status listener already added");
+        }
+
+        CmdCreateBatteryStatusListener pkt = new CmdCreateBatteryStatusListener();
+        pkt.listenerId = listener.listenerId;
+        pkt.bdaddr = listener.getBdaddr();
+        sendPacket(pkt);
+    }
+
+    /**
+     * Remove a battery status listener
+     *
+     * @param listener
+     * @throws IOException
+     */
+    public void removeBatteryStatusListener(BatteryStatusListener listener) throws IOException {
+        if (listener == null) {
+            throw new IllegalArgumentException("buttonScanner is null");
+        }
+        if (batteryStatusListeners.remove(listener.listenerId) == null) {
+            throw new IllegalArgumentException("Battery status listener was never added");
+        }
+
+        CmdRemoveBatteryStatusListener pkt = new CmdRemoveBatteryStatusListener();
+        pkt.listenerId = listener.listenerId;
+        sendPacket(pkt);
+    }
+
+    void sendPacket(CommandPacket packet) throws IOException {
+        byte[] bytes = packet.construct();
+        synchronized (socketOutputStream) {
+            socketOutputStream.write(bytes);
+        }
+    }
+
+    /**
+     * Set a timer.
+     *
+     * This timer task will run after the specified timeoutMillis on the thread that handles the events.
+     *
+     * @param timeoutMillis
+     * @param timerTask
+     * @throws IOException
+     */
+    public void setTimer(int timeoutMillis, TimerTask timerTask) throws IOException {
+        long pointInTime = System.nanoTime() + timeoutMillis * 1000000L;
+        while (timers.putIfAbsent(pointInTime, timerTask) != null) {
+            pointInTime++;
+        }
+        if (handleEventsThread != Thread.currentThread()) {
+            CmdPing pkt = new CmdPing();
+            pkt.pingId = 0;
+            sendPacket(pkt);
+        }
+    }
+
+    /**
+     * Run a task on the thread that handles the events.
+     *
+     * @param task
+     * @throws IOException
+     */
+    public void runOnHandleEventsThread(TimerTask task) throws IOException {
+        if (handleEventsThread == Thread.currentThread()) {
+            task.run();
+        } else {
+            setTimer(0, task);
+        }
+    }
+
+    /**
+     * Start the main loop for this client.
+     *
+     * This method will not return until the socket has been closed.
+     * Once it has returned, any use of this FlicClient is illegal.
+     *
+     * @throws IOException
+     */
+    public void handleEvents() throws IOException {
+        handleEventsThread = Thread.currentThread();
+        while (!Thread.currentThread().isInterrupted()) {
+            Map.Entry<Long, TimerTask> firstTimer = timers.firstEntry();
+            long timeout = 0;
+            if (firstTimer != null) {
+                timeout = firstTimer.getKey() - System.nanoTime();
+                if (timeout <= 0) {
+                    timers.remove(firstTimer.getKey(), firstTimer.getValue());
+                    firstTimer.getValue().run();
+                    continue;
+                }
+            }
+
+            if (socket.isClosed()) {
+                break;
+            }
+
+            int len0;
+            socket.setSoTimeout((int)(timeout / 1000000));
+            try {
+                len0 = socketInputStream.read();
+            } catch (SocketTimeoutException e) {
+                continue;
+            }
+            int len1 = socketInputStream.read();
+            int len = len0 | (len1 << 8);
+            if ((len >> 16) == -1) {
+                break;
+            }
+            if (len == 0) {
+                continue;
+            }
+            byte[] pkt = new byte[len];
+
+            int pos = 0;
+            while (pos < len) {
+                int nbytes = socketInputStream.read(pkt, pos, len - pos);
+                if (nbytes == -1) {
+                    break;
+                }
+                pos += nbytes;
+            }
+            if (len == 1) {
+                continue;
+            }
+            dispatchPacket(pkt);
+        }
+        socket.close();
+    }
+
+    private void dispatchPacket(byte[] packet) throws IOException {
+        int opcode = packet[0];
+        switch (opcode) {
+            case EventPacket.EVT_ADVERTISEMENT_PACKET_OPCODE: {
+                EvtAdvertisementPacket pkt = new EvtAdvertisementPacket();
+                pkt.parse(packet);
+                ButtonScanner scanner = scanners.get(pkt.scanId);
+                if (scanner != null) {
+                    scanner.onAdvertisementPacket(pkt.addr, pkt.name, pkt.rssi, pkt.isPrivate, pkt.alreadyVerified, pkt.alreadyConnectedToThisDevice, pkt.alreadyConnectedToOtherDevice);
+                }
+                break;
+            }
+            case EventPacket.EVT_CREATE_CONNECTION_CHANNEL_RESPONSE_OPCODE: {
+                EvtCreateConnectionChannelResponse pkt = new EvtCreateConnectionChannelResponse();
+                pkt.parse(packet);
+                ButtonConnectionChannel channel = connectionChannels.get(pkt.connId);
+                if (channel != null) {
+                    if (pkt.connectionChannelError != CreateConnectionChannelError.NoError) {
+                        connectionChannels.remove(channel.connId);
+                    }
+                    channel.callbacks.onCreateConnectionChannelResponse(channel, pkt.connectionChannelError, pkt.connectionStatus);
+                }
+                break;
+            }
+            case EventPacket.EVT_CONNECTION_STATUS_CHANGED_OPCODE: {
+                EvtConnectionStatusChanged pkt = new EvtConnectionStatusChanged();
+                pkt.parse(packet);
+                ButtonConnectionChannel channel = connectionChannels.get(pkt.connId);
+                if (channel != null) {
+                    channel.callbacks.onConnectionStatusChanged(channel, pkt.connectionStatus, pkt.disconnectReason);
+                }
+                break;
+            }
+            case EventPacket.EVT_CONNECTION_CHANNEL_REMOVED_OPCODE: {
+                EvtConnectionChannelRemoved pkt = new EvtConnectionChannelRemoved();
+                pkt.parse(packet);
+                ButtonConnectionChannel channel = connectionChannels.get(pkt.connId);
+                if (channel != null) {
+                    connectionChannels.remove(channel.connId);
+                    channel.callbacks.onRemoved(channel, pkt.removedReason);
+                }
+                break;
+            }
+            case EventPacket.EVT_BUTTON_UP_OR_DOWN_OPCODE:
+            case EventPacket.EVT_BUTTON_CLICK_OR_HOLD_OPCODE:
+            case EventPacket.EVT_BUTTON_SINGLE_OR_DOUBLE_CLICK_OPCODE:
+            case EventPacket.EVT_BUTTON_SINGLE_OR_DOUBLE_CLICK_OR_HOLD_OPCODE: {
+                EvtButtonEvent pkt = new EvtButtonEvent();
+                pkt.parse(packet);
+                ButtonConnectionChannel channel = connectionChannels.get(pkt.connId);
+                if (channel != null) {
+                    if (opcode == EventPacket.EVT_BUTTON_UP_OR_DOWN_OPCODE) {
+                        channel.callbacks.onButtonUpOrDown(channel, pkt.clickType, pkt.wasQueued, pkt.timeDiff);
+                    } else if (opcode == EventPacket.EVT_BUTTON_CLICK_OR_HOLD_OPCODE) {
+                        channel.callbacks.onButtonClickOrHold(channel, pkt.clickType, pkt.wasQueued, pkt.timeDiff);
+                    } else if (opcode == EventPacket.EVT_BUTTON_SINGLE_OR_DOUBLE_CLICK_OPCODE) {
+                        channel.callbacks.onButtonSingleOrDoubleClick(channel, pkt.clickType, pkt.wasQueued, pkt.timeDiff);
+                    } else if (opcode == EventPacket.EVT_BUTTON_SINGLE_OR_DOUBLE_CLICK_OR_HOLD_OPCODE) {
+                        channel.callbacks.onButtonSingleOrDoubleClickOrHold(channel, pkt.clickType, pkt.wasQueued, pkt.timeDiff);
+                    }
+                }
+                break;
+            }
+            case EventPacket.EVT_NEW_VERIFIED_BUTTON_OPCODE: {
+                EvtNewVerifiedButton pkt = new EvtNewVerifiedButton();
+                pkt.parse(packet);
+                GeneralCallbacks gc = generalCallbacks;
+                if (gc != null) {
+                    gc.onNewVerifiedButton(pkt.bdaddr);
+                }
+                break;
+            }
+            case EventPacket.EVT_GET_INFO_RESPONSE_OPCODE: {
+                EvtGetInfoResponse pkt = new EvtGetInfoResponse();
+                pkt.parse(packet);
+                getInfoResponseCallbackQueue.remove().onGetInfoResponse(pkt.bluetoothControllerState, pkt.myBdAddr, pkt.myBdAddrType, pkt.maxPendingConnections, pkt.maxConcurrentlyConnectedButtons, pkt.currentPendingConnections, pkt.currentlyNoSpaceForNewConnections, pkt.bdAddrOfVerifiedButtons);
+                break;
+            }
+            case EventPacket.EVT_NO_SPACE_FOR_NEW_CONNECTION_OPCODE: {
+                EvtNoSpaceForNewConnection pkt = new EvtNoSpaceForNewConnection();
+                pkt.parse(packet);
+                GeneralCallbacks gc = generalCallbacks;
+                if (gc != null) {
+                    gc.onNoSpaceForNewConnection(pkt.maxConcurrentlyConnectedButtons);
+                }
+                break;
+            }
+            case EventPacket.EVT_GOT_SPACE_FOR_NEW_CONNECTION_OPCODE: {
+                EvtGotSpaceForNewConnection pkt = new EvtGotSpaceForNewConnection();
+                pkt.parse(packet);
+                GeneralCallbacks gc = generalCallbacks;
+                if (gc != null) {
+                    gc.onGotSpaceForNewConnection(pkt.maxConcurrentlyConnectedButtons);
+                }
+                break;
+            }
+            case EventPacket.EVT_BLUETOOTH_CONTROLLER_STATE_CHANGE_OPCODE: {
+                EvtBluetoothControllerStateChange pkt = new EvtBluetoothControllerStateChange();
+                pkt.parse(packet);
+                GeneralCallbacks gc = generalCallbacks;
+                if (gc != null) {
+                    gc.onBluetoothControllerStateChange(pkt.state);
+                }
+                break;
+            }
+            case EventPacket.EVT_GET_BUTTON_INFO_RESPONSE_OPCODE: {
+                EvtGetButtonInfoResponse pkt = new EvtGetButtonInfoResponse();
+                pkt.parse(packet);
+                getButtonInfoResponseCallbackQueue.remove().onGetButtonInfoResponse(pkt.bdaddr, pkt.uuid, pkt.color, pkt.serialNumber);
+                break;
+            }
+            case EventPacket.EVT_SCAN_WIZARD_FOUND_PRIVATE_BUTTON_OPCODE: {
+                EvtScanWizardFoundPrivateButton pkt = new EvtScanWizardFoundPrivateButton();
+                pkt.parse(packet);
+                ScanWizard wizard = scanWizards.get(pkt.scanWizardId);
+                if (wizard != null) {
+                    wizard.onFoundPrivateButton();
+                }
+                break;
+            }
+            case EventPacket.EVT_SCAN_WIZARD_FOUND_PUBLIC_BUTTON_OPCODE: {
+                EvtScanWizardFoundPublicButton pkt = new EvtScanWizardFoundPublicButton();
+                pkt.parse(packet);
+                ScanWizard wizard = scanWizards.get(pkt.scanWizardId);
+                if (wizard != null) {
+                    wizard.bdaddr = pkt.addr;
+                    wizard.name = pkt.name;
+                    wizard.onFoundPublicButton(wizard.bdaddr, wizard.name);
+                }
+                break;
+            }
+            case EventPacket.EVT_SCAN_WIZARD_BUTTON_CONNECTED_OPCODE: {
+                EvtScanWizardButtonConnected pkt = new EvtScanWizardButtonConnected();
+                pkt.parse(packet);
+                ScanWizard wizard = scanWizards.get(pkt.scanWizardId);
+                if (wizard != null) {
+                    wizard.onButtonConnected(wizard.bdaddr, wizard.name);
+                }
+                break;
+            }
+            case EventPacket.EVT_SCAN_WIZARD_COMPLETED_OPCODE: {
+                EvtScanWizardCompleted pkt = new EvtScanWizardCompleted();
+                pkt.parse(packet);
+                ScanWizard wizard = scanWizards.get(pkt.scanWizardId);
+                scanWizards.remove(pkt.scanWizardId);
+                if (wizard != null) {
+                    Bdaddr bdaddr = wizard.bdaddr;
+                    String name = wizard.name;
+                    wizard.bdaddr = null;
+                    wizard.name = null;
+                    wizard.onCompleted(pkt.result, bdaddr, name);
+                }
+                break;
+            }
+            case EventPacket.EVT_BUTTON_DELETED_OPCODE: {
+                EvtButtonDeleted pkt = new EvtButtonDeleted();
+                pkt.parse(packet);
+                GeneralCallbacks gc = generalCallbacks;
+                if (gc != null) {
+                    gc.onButtonDeleted(pkt.bdaddr, pkt.deletedByThisClient);
+                }
+                break;
+            }
+            case EventPacket.EVT_BATTERY_STATUS_OPCODE: {
+                EvtBatteryStatus pkt = new EvtBatteryStatus();
+                pkt.parse(packet);
+                BatteryStatusListener listener = batteryStatusListeners.get(pkt.listenerId);
+                if (listener != null) {
+                    listener.callbacks.onBatteryStatus(listener.getBdaddr(), pkt.batteryPercentage, pkt.timestamp);
+                }
+                break;
+            }
+        }
+    }
+}
diff --git a/bundles/org.openhab.binding.flicbutton/src/3rdparty/java/io/flic/fliclib/javaclient/GeneralCallbacks.java b/bundles/org.openhab.binding.flicbutton/src/3rdparty/java/io/flic/fliclib/javaclient/GeneralCallbacks.java
new file mode 100644 (file)
index 0000000..77399d7
--- /dev/null
@@ -0,0 +1,28 @@
+package io.flic.fliclib.javaclient;
+
+import io.flic.fliclib.javaclient.enums.BluetoothControllerState;
+
+import java.io.IOException;
+
+/**
+ * GeneralCallbacks.
+ *
+ * See the protocol specification for further details.
+ */
+public class GeneralCallbacks {
+    public void onNewVerifiedButton(Bdaddr bdaddr) throws IOException {
+
+    }
+    public void onNoSpaceForNewConnection(int maxConcurrentlyConnectedButtons) throws IOException {
+
+    }
+    public void onGotSpaceForNewConnection(int maxConcurrentlyConnectedButtons) throws IOException {
+
+    }
+    public void onBluetoothControllerStateChange(BluetoothControllerState state) throws IOException {
+
+    }
+    public void onButtonDeleted(Bdaddr bdaddr, boolean deletedByThisClient) throws IOException {
+
+    }
+}
diff --git a/bundles/org.openhab.binding.flicbutton/src/3rdparty/java/io/flic/fliclib/javaclient/GetButtonInfoResponseCallback.java b/bundles/org.openhab.binding.flicbutton/src/3rdparty/java/io/flic/fliclib/javaclient/GetButtonInfoResponseCallback.java
new file mode 100644 (file)
index 0000000..740f8dd
--- /dev/null
@@ -0,0 +1,18 @@
+package io.flic.fliclib.javaclient;
+
+/**
+ * GetButtonInfoResponseCallback.
+ *
+ * Used in {@link FlicClient#getButtonInfo(Bdaddr, GetButtonInfoResponseCallback)}.
+ */
+public abstract class GetButtonInfoResponseCallback {
+    /**
+     * Called upon response.
+     *
+     * @param bdaddr Bluetooth address
+     * @param uuid Uuid of button, might be null if unknown
+     * @param color Color of button, might be null if unknown
+     * @param serialNumber Serial number of the button, will be null if the button is not found
+     */
+    public abstract void onGetButtonInfoResponse(Bdaddr bdaddr, String uuid, String color, String serialNumber);
+}
diff --git a/bundles/org.openhab.binding.flicbutton/src/3rdparty/java/io/flic/fliclib/javaclient/GetInfoResponseCallback.java b/bundles/org.openhab.binding.flicbutton/src/3rdparty/java/io/flic/fliclib/javaclient/GetInfoResponseCallback.java
new file mode 100644 (file)
index 0000000..642a356
--- /dev/null
@@ -0,0 +1,19 @@
+package io.flic.fliclib.javaclient;
+
+import io.flic.fliclib.javaclient.enums.BdAddrType;
+import io.flic.fliclib.javaclient.enums.BluetoothControllerState;
+
+import java.io.IOException;
+
+/**
+ * GetInfoResponseCallback.
+ *
+ * Used in {@link FlicClient#getInfo(GetInfoResponseCallback)}.
+ */
+public abstract class GetInfoResponseCallback {
+    public abstract void onGetInfoResponse(BluetoothControllerState bluetoothControllerState, Bdaddr myBdAddr,
+                                           BdAddrType myBdAddrType, int maxPendingConnections,
+                                           int maxConcurrentlyConnectedButtons, int currentPendingConnections,
+                                           boolean currentlyNoSpaceForNewConnection,
+                                           Bdaddr[] verifiedButtons) throws IOException;
+}
diff --git a/bundles/org.openhab.binding.flicbutton/src/3rdparty/java/io/flic/fliclib/javaclient/Packets.java b/bundles/org.openhab.binding.flicbutton/src/3rdparty/java/io/flic/fliclib/javaclient/Packets.java
new file mode 100644 (file)
index 0000000..7cdcf01
--- /dev/null
@@ -0,0 +1,455 @@
+package io.flic.fliclib.javaclient;
+
+import java.io.*;
+import java.nio.charset.StandardCharsets;
+
+import io.flic.fliclib.javaclient.enums.*;
+
+/**
+ * Flic Protocol Packets
+ */
+
+abstract class CommandPacket {
+    protected int opcode;
+
+    public final byte[] construct() {
+        ByteArrayOutputStream stream = new ByteArrayOutputStream();
+        try {
+            write(stream);
+        } catch (IOException e) {
+        }
+        byte[] res = new byte[3 + stream.size()];
+        res[0] = (byte)(1 + stream.size());
+        res[1] = (byte)((1 + stream.size()) >> 8);
+        res[2] = (byte)opcode;
+        System.arraycopy(stream.toByteArray(), 0, res, 3, stream.size());
+        return res;
+    }
+
+    abstract protected void write(OutputStream stream) throws IOException;
+}
+
+class CmdGetInfo extends CommandPacket {
+    @Override
+    protected void write(OutputStream stream) {
+        opcode = 0;
+    }
+}
+
+class CmdCreateScanner extends CommandPacket {
+    public int scanId;
+
+    @Override
+    protected void write(OutputStream stream) throws IOException {
+        opcode = 1;
+        StreamUtils.writeInt32(stream, scanId);
+    }
+}
+
+class CmdRemoveScanner extends CommandPacket {
+    public int scanId;
+
+    @Override
+    protected void write(OutputStream stream) throws IOException {
+        opcode = 2;
+        StreamUtils.writeInt32(stream, scanId);
+    }
+}
+
+class CmdCreateConnectionChannel extends CommandPacket {
+    public int connId;
+    public Bdaddr bdaddr;
+    public LatencyMode latencyMode;
+    public short autoDisconnectTime;
+
+    @Override
+    protected void write(OutputStream stream) throws IOException {
+        opcode = 3;
+        StreamUtils.writeInt32(stream, connId);
+        StreamUtils.writeBdaddr(stream, bdaddr);
+        StreamUtils.writeEnum(stream, latencyMode);
+        StreamUtils.writeInt16(stream, autoDisconnectTime);
+    }
+}
+
+class CmdRemoveConnectionChannel extends CommandPacket {
+    public int connId;
+
+    @Override
+    protected void write(OutputStream stream) throws IOException {
+        opcode = 4;
+        StreamUtils.writeInt32(stream, connId);
+    }
+}
+
+class CmdForceDisconnect extends CommandPacket {
+    public Bdaddr bdaddr;
+
+    @Override
+    protected void write(OutputStream stream) throws IOException {
+        opcode = 5;
+        StreamUtils.writeBdaddr(stream, bdaddr);
+    }
+}
+
+class CmdChangeModeParameters extends CommandPacket {
+    public int connId;
+    public LatencyMode latencyMode;
+    public short autoDisconnectTime;
+
+    @Override
+    protected void write(OutputStream stream) throws IOException {
+        opcode = 6;
+        StreamUtils.writeInt32(stream, connId);
+        StreamUtils.writeEnum(stream, latencyMode);
+        StreamUtils.writeInt16(stream, autoDisconnectTime);
+    }
+}
+
+class CmdPing extends CommandPacket {
+    public int pingId;
+
+    @Override
+    protected void write(OutputStream stream) throws IOException {
+        opcode = 7;
+        StreamUtils.writeInt32(stream, pingId);
+    }
+}
+
+class CmdGetButtonInfo extends CommandPacket {
+    public Bdaddr bdaddr;
+
+    @Override
+    protected void write(OutputStream stream) throws IOException {
+        opcode = 8;
+        StreamUtils.writeBdaddr(stream, bdaddr);
+    }
+}
+
+class CmdCreateScanWizard extends CommandPacket {
+    public int scanWizardId;
+
+    @Override
+    protected void write(OutputStream stream) throws IOException {
+        opcode = 9;
+        StreamUtils.writeInt32(stream, scanWizardId);
+    }
+}
+
+class CmdCancelScanWizard extends CommandPacket {
+    public int scanWizardId;
+
+    @Override
+    protected void write(OutputStream stream) throws IOException {
+        opcode = 10;
+        StreamUtils.writeInt32(stream, scanWizardId);
+    }
+}
+
+class CmdDeleteButton extends CommandPacket {
+    public Bdaddr bdaddr;
+
+    @Override
+    protected void write(OutputStream stream) throws IOException {
+        opcode = 11;
+        StreamUtils.writeBdaddr(stream, bdaddr);
+    }
+}
+
+class CmdCreateBatteryStatusListener extends CommandPacket {
+    public int listenerId;
+    public Bdaddr bdaddr;
+
+    @Override
+    protected void write(OutputStream stream) throws IOException {
+        opcode = 12;
+        StreamUtils.writeInt32(stream, listenerId);
+        StreamUtils.writeBdaddr(stream, bdaddr);
+    }
+}
+
+class CmdRemoveBatteryStatusListener extends CommandPacket {
+    public int listenerId;
+
+    @Override
+    protected void write(OutputStream stream) throws IOException {
+        opcode = 13;
+        StreamUtils.writeInt32(stream, listenerId);
+    }
+}
+
+abstract class EventPacket {
+    public static final int EVT_ADVERTISEMENT_PACKET_OPCODE = 0;
+    public static final int EVT_CREATE_CONNECTION_CHANNEL_RESPONSE_OPCODE = 1;
+    public static final int EVT_CONNECTION_STATUS_CHANGED_OPCODE = 2;
+    public static final int EVT_CONNECTION_CHANNEL_REMOVED_OPCODE = 3;
+    public static final int EVT_BUTTON_UP_OR_DOWN_OPCODE = 4;
+    public static final int EVT_BUTTON_CLICK_OR_HOLD_OPCODE = 5;
+    public static final int EVT_BUTTON_SINGLE_OR_DOUBLE_CLICK_OPCODE = 6;
+    public static final int EVT_BUTTON_SINGLE_OR_DOUBLE_CLICK_OR_HOLD_OPCODE = 7;
+    public static final int EVT_NEW_VERIFIED_BUTTON_OPCODE = 8;
+    public static final int EVT_GET_INFO_RESPONSE_OPCODE = 9;
+    public static final int EVT_NO_SPACE_FOR_NEW_CONNECTION_OPCODE = 10;
+    public static final int EVT_GOT_SPACE_FOR_NEW_CONNECTION_OPCODE = 11;
+    public static final int EVT_BLUETOOTH_CONTROLLER_STATE_CHANGE_OPCODE = 12;
+    public static final int EVT_PING_RESPONSE_OPCODE = 13;
+    public static final int EVT_GET_BUTTON_INFO_RESPONSE_OPCODE = 14;
+    public static final int EVT_SCAN_WIZARD_FOUND_PRIVATE_BUTTON_OPCODE = 15;
+    public static final int EVT_SCAN_WIZARD_FOUND_PUBLIC_BUTTON_OPCODE = 16;
+    public static final int EVT_SCAN_WIZARD_BUTTON_CONNECTED_OPCODE = 17;
+    public static final int EVT_SCAN_WIZARD_COMPLETED_OPCODE = 18;
+    public static final int EVT_BUTTON_DELETED_OPCODE = 19;
+    public static final int EVT_BATTERY_STATUS_OPCODE = 20;
+
+    public void parse(byte[] arr) {
+        InputStream stream = new ByteArrayInputStream(arr);
+        try {
+            stream.skip(1);
+            parseInternal(stream);
+        } catch(IOException e) {
+        }
+    }
+
+    abstract protected void parseInternal(InputStream stream) throws IOException;
+}
+
+class EvtAdvertisementPacket extends EventPacket {
+    public int scanId;
+    public Bdaddr addr;
+    public String name;
+    public int rssi;
+    public boolean isPrivate;
+    public boolean alreadyVerified;
+    public boolean alreadyConnectedToThisDevice;
+    public boolean alreadyConnectedToOtherDevice;
+
+    @Override
+    protected void parseInternal(InputStream stream) throws IOException {
+        scanId = StreamUtils.getInt32(stream);
+        addr = StreamUtils.getBdaddr(stream);
+        name = StreamUtils.getString(stream, 16);
+        rssi = StreamUtils.getInt8(stream);
+        isPrivate = StreamUtils.getBoolean(stream);
+        alreadyVerified = StreamUtils.getBoolean(stream);
+        alreadyConnectedToThisDevice = StreamUtils.getBoolean(stream);
+        alreadyConnectedToOtherDevice = StreamUtils.getBoolean(stream);
+    }
+}
+
+class EvtCreateConnectionChannelResponse extends EventPacket {
+    public int connId;
+    public CreateConnectionChannelError connectionChannelError;
+    public ConnectionStatus connectionStatus;
+
+    @Override
+    protected void parseInternal(InputStream stream) throws IOException {
+        connId = StreamUtils.getInt32(stream);
+        connectionChannelError = CreateConnectionChannelError.values()[StreamUtils.getUInt8(stream)];
+        connectionStatus = ConnectionStatus.values()[StreamUtils.getUInt8(stream)];
+    }
+}
+
+class EvtConnectionStatusChanged extends EventPacket {
+    public int connId;
+    public ConnectionStatus connectionStatus;
+    public DisconnectReason disconnectReason;
+
+    @Override
+    protected void parseInternal(InputStream stream) throws IOException {
+        connId = StreamUtils.getInt32(stream);
+        connectionStatus = ConnectionStatus.values()[StreamUtils.getUInt8(stream)];
+        disconnectReason = DisconnectReason.values()[StreamUtils.getUInt8(stream)];
+    }
+}
+
+class EvtConnectionChannelRemoved extends EventPacket {
+    public int connId;
+    public RemovedReason removedReason;
+
+    @Override
+    protected void parseInternal(InputStream stream) throws IOException {
+        connId = StreamUtils.getInt32(stream);
+        removedReason = RemovedReason.values()[StreamUtils.getUInt8(stream)];
+    }
+}
+
+class EvtButtonEvent extends EventPacket {
+    public int connId;
+    public ClickType clickType;
+    public boolean wasQueued;
+    public int timeDiff;
+
+    @Override
+    protected void parseInternal(InputStream stream) throws IOException {
+        connId = StreamUtils.getInt32(stream);
+        clickType = ClickType.values()[StreamUtils.getUInt8(stream)];
+        wasQueued = StreamUtils.getBoolean(stream);
+        timeDiff = StreamUtils.getInt32(stream);
+    }
+}
+
+class EvtNewVerifiedButton extends EventPacket {
+    public Bdaddr bdaddr;
+
+    @Override
+    protected void parseInternal(InputStream stream) throws IOException {
+        bdaddr = StreamUtils.getBdaddr(stream);
+    }
+}
+
+class EvtGetInfoResponse extends EventPacket {
+    public BluetoothControllerState bluetoothControllerState;
+    public Bdaddr myBdAddr;
+    public BdAddrType myBdAddrType;
+    public int maxPendingConnections;
+    public int maxConcurrentlyConnectedButtons;
+    public int currentPendingConnections;
+    public boolean currentlyNoSpaceForNewConnections;
+    public Bdaddr[] bdAddrOfVerifiedButtons;
+
+    @Override
+    protected void parseInternal(InputStream stream) throws IOException {
+        bluetoothControllerState = BluetoothControllerState.values()[StreamUtils.getUInt8(stream)];
+        myBdAddr = StreamUtils.getBdaddr(stream);
+        myBdAddrType = BdAddrType.values()[StreamUtils.getUInt8(stream)];
+        maxPendingConnections = StreamUtils.getUInt8(stream);
+        maxConcurrentlyConnectedButtons = StreamUtils.getInt16(stream);
+        currentPendingConnections = StreamUtils.getUInt8(stream);
+        currentlyNoSpaceForNewConnections = StreamUtils.getBoolean(stream);
+        int nbVerifiedButtons = StreamUtils.getUInt16(stream);
+        bdAddrOfVerifiedButtons = new Bdaddr[nbVerifiedButtons];
+        for (int i = 0; i < nbVerifiedButtons; i++) {
+            bdAddrOfVerifiedButtons[i] = StreamUtils.getBdaddr(stream);
+        }
+    }
+}
+
+class EvtNoSpaceForNewConnection extends EventPacket {
+    public int maxConcurrentlyConnectedButtons;
+
+    @Override
+    protected void parseInternal(InputStream stream) throws IOException {
+        maxConcurrentlyConnectedButtons = StreamUtils.getUInt8(stream);
+    }
+}
+
+class EvtGotSpaceForNewConnection extends EventPacket {
+    public int maxConcurrentlyConnectedButtons;
+
+    @Override
+    protected void parseInternal(InputStream stream) throws IOException {
+        maxConcurrentlyConnectedButtons = StreamUtils.getUInt8(stream);
+    }
+}
+
+class EvtBluetoothControllerStateChange extends EventPacket {
+    public BluetoothControllerState state;
+
+    @Override
+    protected void parseInternal(InputStream stream) throws IOException {
+        state = BluetoothControllerState.values()[StreamUtils.getUInt8(stream)];
+    }
+}
+
+class EvtGetButtonInfoResponse extends EventPacket {
+    public Bdaddr bdaddr;
+    public String uuid;
+    public String color;
+    public String serialNumber;
+
+    @Override
+    protected void parseInternal(InputStream stream) throws IOException {
+        bdaddr = StreamUtils.getBdaddr(stream);
+        byte[] uuidBytes = StreamUtils.getByteArr(stream, 16);
+        StringBuilder sb = new StringBuilder(32);
+        for (int i = 0; i < 16; i++) {
+            sb.append(String.format("%02x", uuidBytes[i]));
+        }
+        uuid = sb.toString();
+        if (uuid.equals("00000000000000000000000000000000")) {
+            uuid = null;
+        }
+        color = StreamUtils.getString(stream, 16);
+        if (color.isEmpty()) {
+            color = null;
+        }
+        serialNumber = StreamUtils.getString(stream, 16);
+        if (serialNumber.isEmpty()) {
+            serialNumber = null;
+        }
+    }
+}
+
+class EvtScanWizardFoundPrivateButton extends EventPacket {
+    public int scanWizardId;
+    
+    @Override
+    protected void parseInternal(InputStream stream) throws IOException {
+        scanWizardId = StreamUtils.getInt32(stream);
+    }
+}
+
+class EvtScanWizardFoundPublicButton extends EventPacket {
+    public int scanWizardId;
+    public Bdaddr addr;
+    public String name;
+    
+    @Override
+    protected void parseInternal(InputStream stream) throws IOException {
+        scanWizardId = StreamUtils.getInt32(stream);
+        addr = StreamUtils.getBdaddr(stream);
+        int nameLen = StreamUtils.getUInt8(stream);
+        byte[] bytes = new byte[nameLen];
+        for (int i = 0; i < nameLen; i++) {
+            bytes[i] = (byte)stream.read();
+        }
+        for (int i = nameLen; i < 16; i++) {
+            stream.skip(1);
+        }
+        name = new String(bytes, StandardCharsets.UTF_8);
+    }
+}
+
+class EvtScanWizardButtonConnected extends EventPacket {
+    public int scanWizardId;
+    
+    @Override
+    protected void parseInternal(InputStream stream) throws IOException {
+        scanWizardId = StreamUtils.getInt32(stream);
+    }
+}
+
+class EvtScanWizardCompleted extends EventPacket {
+    public int scanWizardId;
+    public ScanWizardResult result;
+    
+    @Override
+    protected void parseInternal(InputStream stream) throws IOException {
+        scanWizardId = StreamUtils.getInt32(stream);
+        result = ScanWizardResult.values()[StreamUtils.getUInt8(stream)];
+    }
+}
+
+class EvtButtonDeleted extends EventPacket {
+    public Bdaddr bdaddr;
+    public boolean deletedByThisClient;
+
+    @Override
+    protected void parseInternal(InputStream stream) throws IOException {
+        bdaddr = StreamUtils.getBdaddr(stream);
+        deletedByThisClient = StreamUtils.getBoolean(stream);
+    }
+}
+
+class EvtBatteryStatus extends EventPacket {
+    public int listenerId;
+    public int batteryPercentage;
+    public long timestamp;
+
+    @Override
+    protected void parseInternal(InputStream stream) throws IOException {
+        listenerId = StreamUtils.getInt32(stream);
+        batteryPercentage = StreamUtils.getInt8(stream);
+        timestamp = StreamUtils.getInt64(stream);
+    }
+}
diff --git a/bundles/org.openhab.binding.flicbutton/src/3rdparty/java/io/flic/fliclib/javaclient/ScanWizard.java b/bundles/org.openhab.binding.flicbutton/src/3rdparty/java/io/flic/fliclib/javaclient/ScanWizard.java
new file mode 100644 (file)
index 0000000..3e573f6
--- /dev/null
@@ -0,0 +1,64 @@
+package io.flic.fliclib.javaclient;
+
+import java.io.IOException;
+import java.util.concurrent.atomic.AtomicInteger;
+
+import io.flic.fliclib.javaclient.enums.ScanWizardResult;
+
+/**
+ * Scan wizard class.
+ *
+ * This class will scan for a new button and pair it automatically.
+ * There are internal timeouts that make sure operations don't take too long time.
+ *
+ * Inherit this class and override the methods.
+ * Then add this scan wizard to a {@link FlicClient} using {@link FlicClient#addScanWizard(ScanWizard)} to start it.
+ * You can cancel by calling {@link FlicClient#cancelScanWizard(ScanWizard)}.
+ */
+public abstract class ScanWizard {
+    private static AtomicInteger nextId = new AtomicInteger();
+    int scanWizardId = nextId.getAndIncrement();
+    Bdaddr bdaddr;
+    String name;
+
+    /**
+     * This will be called once if a private button is found.
+     *
+     * Tell the user to hold down the button for 7 seconds in order to make it public.
+     *
+     */
+    public abstract void onFoundPrivateButton() throws IOException;
+    
+    /**
+     * This will be called once a public button is found.
+     *
+     * Now a connection attempt will be made to the device in order to pair and verify it.
+     *
+     * @param bdaddr Bluetooth Device Address
+     * @param name Advertising name
+     */
+    public abstract void onFoundPublicButton(Bdaddr bdaddr, String name) throws IOException;
+    
+    /**
+     * This will be called once the bluetooth connection has been established.
+     *
+     * Now a pair attempt will be made.
+     *
+     * @param bdaddr Bluetooth Device Address
+     * @param name Advertising name
+     */
+    public abstract void onButtonConnected(Bdaddr bdaddr, String name) throws IOException;
+    
+    /**
+     * Scan wizard completed.
+     *
+     * If the result is success, you can now create a connection channel to the button.
+     *
+     * The ScanWizard is now detached from the FlicClient and can now be recycled.
+     *
+     * @param result Result of the scan wizard
+     * @param bdaddr Bluetooth Device Address or null, depending on if {@link #onFoundPublicButton} has been called or not
+     * @param name Advertising name or null, depending on if {@link #onFoundPublicButton} has been called or not
+     */
+    public abstract void onCompleted(ScanWizardResult result, Bdaddr bdaddr, String name) throws IOException;
+}
diff --git a/bundles/org.openhab.binding.flicbutton/src/3rdparty/java/io/flic/fliclib/javaclient/StreamUtils.java b/bundles/org.openhab.binding.flicbutton/src/3rdparty/java/io/flic/fliclib/javaclient/StreamUtils.java
new file mode 100644 (file)
index 0000000..0db0ede
--- /dev/null
@@ -0,0 +1,81 @@
+package io.flic.fliclib.javaclient;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.nio.charset.StandardCharsets;
+
+class StreamUtils {
+    public static boolean getBoolean(InputStream stream) throws IOException {
+        return stream.read() != 0;
+    }
+    public static int getUInt8(InputStream stream) throws IOException {
+        return stream.read();
+    }
+
+    public static int getInt8(InputStream stream) throws IOException {
+        return (byte)stream.read();
+    }
+
+    public static int getUInt16(InputStream stream) throws IOException {
+        return stream.read() | (stream.read() << 8);
+    }
+
+    public static int getInt16(InputStream stream) throws IOException {
+        return (short)getUInt16(stream);
+    }
+
+    public static int getInt32(InputStream stream) throws IOException {
+        return stream.read() | (stream.read() << 8) | (stream.read() << 16) | (stream.read() << 24);
+    }
+
+    public static long getInt64(InputStream stream) throws IOException {
+        return (getInt32(stream) & 0xffffffffL) | ((long)getInt32(stream) << 32);
+    }
+
+    public static Bdaddr getBdaddr(InputStream stream) throws IOException {
+        return new Bdaddr(stream);
+    }
+
+    public static byte[] getByteArr(InputStream stream, int len) throws IOException {
+        byte[] arr = new byte[len];
+        for (int i = 0; i < len; i++) {
+            arr[i] = (byte)stream.read();
+        }
+        return arr;
+    }
+
+    public static String getString(InputStream stream, int maxlen) throws IOException {
+        int len = getInt8(stream);
+        byte[] arr = new byte[len];
+        for (int i = 0; i < len; i++) {
+            arr[i] = (byte)stream.read();
+        }
+        for (int i = len; i < maxlen; i++) {
+            stream.skip(1);
+        }
+        return new String(arr, StandardCharsets.UTF_8);
+    }
+
+    public static void writeEnum(OutputStream stream, Enum<?> enumValue) throws IOException {
+        stream.write(enumValue.ordinal());
+    }
+
+    public static void writeInt8(OutputStream stream, int v) throws IOException {
+        stream.write(v);
+    }
+
+    public static void writeInt16(OutputStream stream, int v) throws IOException {
+        stream.write(v & 0xff);
+        stream.write(v >> 8);
+    }
+
+    public static void writeInt32(OutputStream stream, int v) throws IOException {
+        writeInt16(stream, v);
+        writeInt16(stream, v >> 16);
+    }
+
+    public static void writeBdaddr(OutputStream stream, Bdaddr addr) throws IOException {
+        stream.write(addr.getBytes());
+    }
+}
diff --git a/bundles/org.openhab.binding.flicbutton/src/3rdparty/java/io/flic/fliclib/javaclient/TimerTask.java b/bundles/org.openhab.binding.flicbutton/src/3rdparty/java/io/flic/fliclib/javaclient/TimerTask.java
new file mode 100644 (file)
index 0000000..8ed0919
--- /dev/null
@@ -0,0 +1,14 @@
+package io.flic.fliclib.javaclient;
+
+import java.io.IOException;
+
+/**
+ * TimerTask.
+ *
+ * Use this interface instead of {@link Runnable} to avoid having to deal with IOExceptions.
+ * Invocations of the run method on this interface from the {@link FlicClient} will propagate IOExceptions to the caller of {@link FlicClient#handleEvents()}.
+ *
+ */
+public interface TimerTask {
+    void run() throws IOException;
+}
diff --git a/bundles/org.openhab.binding.flicbutton/src/3rdparty/java/io/flic/fliclib/javaclient/enums/BdAddrType.java b/bundles/org.openhab.binding.flicbutton/src/3rdparty/java/io/flic/fliclib/javaclient/enums/BdAddrType.java
new file mode 100644 (file)
index 0000000..650e719
--- /dev/null
@@ -0,0 +1,9 @@
+package io.flic.fliclib.javaclient.enums;
+
+/**
+ * Created by Emil on 2016-05-03.
+ */
+public enum BdAddrType {
+    PublicBdAddrType,
+    RandomBdAddrType
+}
diff --git a/bundles/org.openhab.binding.flicbutton/src/3rdparty/java/io/flic/fliclib/javaclient/enums/BluetoothControllerState.java b/bundles/org.openhab.binding.flicbutton/src/3rdparty/java/io/flic/fliclib/javaclient/enums/BluetoothControllerState.java
new file mode 100644 (file)
index 0000000..24ee268
--- /dev/null
@@ -0,0 +1,10 @@
+package io.flic.fliclib.javaclient.enums;
+
+/**
+ * Created by Emil on 2016-05-03.
+ */
+public enum BluetoothControllerState {
+    Detached,
+    Resetting,
+    Attached
+}
diff --git a/bundles/org.openhab.binding.flicbutton/src/3rdparty/java/io/flic/fliclib/javaclient/enums/ClickType.java b/bundles/org.openhab.binding.flicbutton/src/3rdparty/java/io/flic/fliclib/javaclient/enums/ClickType.java
new file mode 100644 (file)
index 0000000..d1a767b
--- /dev/null
@@ -0,0 +1,13 @@
+package io.flic.fliclib.javaclient.enums;
+
+/**
+ * Created by Emil on 2016-05-03.
+ */
+public enum ClickType {
+    ButtonDown,
+    ButtonUp,
+    ButtonClick,
+    ButtonSingleClick,
+    ButtonDoubleClick,
+    ButtonHold
+}
diff --git a/bundles/org.openhab.binding.flicbutton/src/3rdparty/java/io/flic/fliclib/javaclient/enums/ConnectionStatus.java b/bundles/org.openhab.binding.flicbutton/src/3rdparty/java/io/flic/fliclib/javaclient/enums/ConnectionStatus.java
new file mode 100644 (file)
index 0000000..ad0673a
--- /dev/null
@@ -0,0 +1,10 @@
+package io.flic.fliclib.javaclient.enums;
+
+/**
+ * Created by Emil on 2016-05-03.
+ */
+public enum ConnectionStatus {
+    Disconnected,
+    Connected,
+    Ready
+}
diff --git a/bundles/org.openhab.binding.flicbutton/src/3rdparty/java/io/flic/fliclib/javaclient/enums/CreateConnectionChannelError.java b/bundles/org.openhab.binding.flicbutton/src/3rdparty/java/io/flic/fliclib/javaclient/enums/CreateConnectionChannelError.java
new file mode 100644 (file)
index 0000000..aa78274
--- /dev/null
@@ -0,0 +1,9 @@
+package io.flic.fliclib.javaclient.enums;
+
+/**
+ * Created by Emil on 2016-05-03.
+ */
+public enum CreateConnectionChannelError {
+    NoError,
+    MaxPendingConnectionsReached
+}
diff --git a/bundles/org.openhab.binding.flicbutton/src/3rdparty/java/io/flic/fliclib/javaclient/enums/DisconnectReason.java b/bundles/org.openhab.binding.flicbutton/src/3rdparty/java/io/flic/fliclib/javaclient/enums/DisconnectReason.java
new file mode 100644 (file)
index 0000000..a875a72
--- /dev/null
@@ -0,0 +1,11 @@
+package io.flic.fliclib.javaclient.enums;
+
+/**
+ * Created by Emil on 2016-05-03.
+ */
+public enum DisconnectReason {
+    Unspecified,
+    ConnectionEstablishmentFailed,
+    TimedOut,
+    BondingKeysMismatch
+}
diff --git a/bundles/org.openhab.binding.flicbutton/src/3rdparty/java/io/flic/fliclib/javaclient/enums/LatencyMode.java b/bundles/org.openhab.binding.flicbutton/src/3rdparty/java/io/flic/fliclib/javaclient/enums/LatencyMode.java
new file mode 100644 (file)
index 0000000..135693f
--- /dev/null
@@ -0,0 +1,10 @@
+package io.flic.fliclib.javaclient.enums;
+
+/**
+ * Created by Emil on 2016-05-03.
+ */
+public enum LatencyMode {
+    NormalLatency,
+    LowLatency,
+    HighLatency
+}
diff --git a/bundles/org.openhab.binding.flicbutton/src/3rdparty/java/io/flic/fliclib/javaclient/enums/RemovedReason.java b/bundles/org.openhab.binding.flicbutton/src/3rdparty/java/io/flic/fliclib/javaclient/enums/RemovedReason.java
new file mode 100644 (file)
index 0000000..48bc410
--- /dev/null
@@ -0,0 +1,22 @@
+package io.flic.fliclib.javaclient.enums;
+
+/**
+ * Created by Emil on 2016-05-03.
+ */
+public enum RemovedReason {
+    RemovedByThisClient,
+    ForceDisconnectedByThisClient,
+    ForceDisconnectedByOtherClient,
+
+    ButtonIsPrivate,
+    VerifyTimeout,
+    InternetBackendError,
+    InvalidData,
+    
+    CouldntLoadDevice,
+    
+    DeletedByThisClient,
+    DeletedByOtherClient,
+    ButtonBelongsToOtherPartner,
+    DeletedFromButton
+}
diff --git a/bundles/org.openhab.binding.flicbutton/src/3rdparty/java/io/flic/fliclib/javaclient/enums/ScanWizardResult.java b/bundles/org.openhab.binding.flicbutton/src/3rdparty/java/io/flic/fliclib/javaclient/enums/ScanWizardResult.java
new file mode 100644 (file)
index 0000000..e31582a
--- /dev/null
@@ -0,0 +1,13 @@
+package io.flic.fliclib.javaclient.enums;
+
+public enum ScanWizardResult {
+    WizardSuccess,
+    WizardCancelledByUser,
+    WizardFailedTimeout,
+    WizardButtonIsPrivate,
+    WizardBluetoothUnavailable,
+    WizardInternetBackendError,
+    WizardInvalidData,
+    WizardButtonBelongsToOtherPartner,
+    WizardButtonAlreadyConnectedToOtherDevice
+}
diff --git a/bundles/org.openhab.binding.flicbutton/src/main/feature/feature.xml b/bundles/org.openhab.binding.flicbutton/src/main/feature/feature.xml
new file mode 100644 (file)
index 0000000..45d9067
--- /dev/null
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<features name="org.openhab.binding.flicbutton-${project.version}" xmlns="http://karaf.apache.org/xmlns/features/v1.4.0">
+       <repository>mvn:org.openhab.core.features.karaf/org.openhab.core.features.karaf.openhab-core/${ohc.version}/xml/features</repository>
+
+       <feature name="openhab-binding-flicbutton" description="FlicButton Binding" version="${project.version}">
+               <feature>openhab-runtime-base</feature>
+               <bundle start-level="80">mvn:org.openhab.addons.bundles/org.openhab.binding.flicbutton/${project.version}</bundle>
+       </feature>
+</features>
diff --git a/bundles/org.openhab.binding.flicbutton/src/main/java/org/openhab/binding/flicbutton/internal/FlicButtonBindingConstants.java b/bundles/org.openhab.binding.flicbutton/src/main/java/org/openhab/binding/flicbutton/internal/FlicButtonBindingConstants.java
new file mode 100644 (file)
index 0000000..0aa1773
--- /dev/null
@@ -0,0 +1,65 @@
+/**
+ * Copyright (c) 2010-2022 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.flicbutton.internal;
+
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Set;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.openhab.core.thing.CommonTriggerEvents;
+import org.openhab.core.thing.ThingTypeUID;
+
+/**
+ * The {@link FlicButtonBindingConstants} class defines common constants, which are
+ * used across the whole binding.
+ *
+ * @author Patrick Fink - Initial contribution
+ */
+@NonNullByDefault
+public class FlicButtonBindingConstants {
+
+    public static final String BINDING_ID = "flicbutton";
+
+    // List of all Thing Type UIDs
+    public static final ThingTypeUID BRIDGE_THING_TYPE = new ThingTypeUID(BINDING_ID, "flicd-bridge");
+    public static final ThingTypeUID FLICBUTTON_THING_TYPE = new ThingTypeUID(BINDING_ID, "button");
+
+    public static final Set<ThingTypeUID> BRIDGE_THING_TYPES_UIDS = Collections.singleton(BRIDGE_THING_TYPE);
+    public static final Set<ThingTypeUID> SUPPORTED_THING_TYPES_UIDS = Collections.singleton(FLICBUTTON_THING_TYPE);
+
+    // List of all configuration options
+    public static final String CONFIG_HOST_NAME = "hostname";
+    public static final String CONFIG_PORT = "port";
+    public static final String CONFIG_ADDRESS = "address";
+
+    // List of all Channel ids
+    public static final String CHANNEL_ID_RAWBUTTON_EVENTS = "rawbutton";
+    public static final String CHANNEL_ID_BUTTON_EVENTS = "button";
+    public static final String CHANNEL_ID_BATTERY_LEVEL = "battery-level";
+
+    // Other stuff
+    public static final int BUTTON_OFFLINE_GRACE_PERIOD_SECONDS = 60;
+
+    public static final Map<String, String> FLIC_OPENHAB_TRIGGER_EVENT_MAP = Collections
+            .unmodifiableMap(new HashMap<String, String>() {
+                {
+                    put("ButtonSingleClick", CommonTriggerEvents.SHORT_PRESSED);
+                    put("ButtonDoubleClick", CommonTriggerEvents.DOUBLE_PRESSED);
+                    put("ButtonHold", CommonTriggerEvents.LONG_PRESSED);
+                    put("ButtonDown", CommonTriggerEvents.PRESSED);
+                    put("ButtonUp", CommonTriggerEvents.RELEASED);
+                }
+            });
+}
diff --git a/bundles/org.openhab.binding.flicbutton/src/main/java/org/openhab/binding/flicbutton/internal/FlicButtonHandlerFactory.java b/bundles/org.openhab.binding.flicbutton/src/main/java/org/openhab/binding/flicbutton/internal/FlicButtonHandlerFactory.java
new file mode 100644 (file)
index 0000000..5e44ab7
--- /dev/null
@@ -0,0 +1,104 @@
+/**
+ * Copyright (c) 2010-2022 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.flicbutton.internal;
+
+import java.util.HashMap;
+import java.util.Hashtable;
+import java.util.Map;
+import java.util.Set;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.binding.flicbutton.internal.discovery.FlicButtonDiscoveryService;
+import org.openhab.binding.flicbutton.internal.discovery.FlicSimpleclientDiscoveryServiceImpl;
+import org.openhab.binding.flicbutton.internal.handler.FlicButtonHandler;
+import org.openhab.binding.flicbutton.internal.handler.FlicDaemonBridgeHandler;
+import org.openhab.core.config.discovery.DiscoveryService;
+import org.openhab.core.thing.Bridge;
+import org.openhab.core.thing.Thing;
+import org.openhab.core.thing.ThingTypeUID;
+import org.openhab.core.thing.ThingUID;
+import org.openhab.core.thing.binding.BaseThingHandlerFactory;
+import org.openhab.core.thing.binding.ThingHandler;
+import org.openhab.core.thing.binding.ThingHandlerFactory;
+import org.osgi.framework.ServiceRegistration;
+import org.osgi.service.component.annotations.Component;
+
+/**
+ * The {@link FlicButtonHandlerFactory} is responsible for creating things and thing
+ * handlers.
+ *
+ * @author Patrick Fink - Initial contribution
+ */
+@Component(service = ThingHandlerFactory.class, configurationPid = "binding.flicbutton")
+@NonNullByDefault
+public class FlicButtonHandlerFactory extends BaseThingHandlerFactory {
+
+    private static final Set<ThingTypeUID> SUPPORTED_THING_TYPES_UIDS = Stream
+            .concat(FlicButtonBindingConstants.BRIDGE_THING_TYPES_UIDS.stream(),
+                    FlicButtonBindingConstants.SUPPORTED_THING_TYPES_UIDS.stream())
+            .collect(Collectors.toSet());
+    private final Map<ThingUID, @Nullable ServiceRegistration<?>> discoveryServiceRegs = new HashMap<>();
+
+    @Override
+    public boolean supportsThingType(ThingTypeUID thingTypeUID) {
+        return SUPPORTED_THING_TYPES_UIDS.contains(thingTypeUID);
+    }
+
+    @Override
+    @Nullable
+    protected ThingHandler createHandler(Thing thing) {
+        ThingTypeUID thingTypeUID = thing.getThingTypeUID();
+
+        if (thingTypeUID.equals(FlicButtonBindingConstants.FLICBUTTON_THING_TYPE)) {
+            return new FlicButtonHandler(thing);
+        } else if (thingTypeUID.equals(FlicButtonBindingConstants.BRIDGE_THING_TYPE)) {
+            FlicButtonDiscoveryService discoveryService = new FlicSimpleclientDiscoveryServiceImpl(thing.getUID());
+            FlicDaemonBridgeHandler bridgeHandler = new FlicDaemonBridgeHandler((Bridge) thing, discoveryService);
+            registerDiscoveryService(discoveryService, thing.getUID());
+
+            return bridgeHandler;
+        }
+
+        return null;
+    }
+
+    @Override
+    protected synchronized void removeHandler(ThingHandler thingHandler) {
+        if (thingHandler instanceof FlicDaemonBridgeHandler) {
+            unregisterDiscoveryService(thingHandler.getThing().getUID());
+        }
+        super.removeHandler(thingHandler);
+    }
+
+    private synchronized void registerDiscoveryService(FlicButtonDiscoveryService discoveryService,
+            ThingUID bridgeUID) {
+        this.discoveryServiceRegs.put(bridgeUID, getBundleContext().registerService(DiscoveryService.class.getName(),
+                discoveryService, new Hashtable<String, Object>()));
+    }
+
+    private synchronized void unregisterDiscoveryService(ThingUID bridgeUID) {
+        ServiceRegistration<?> serviceReg = this.discoveryServiceRegs.get(bridgeUID);
+        if (serviceReg != null) {
+            FlicButtonDiscoveryService service = (FlicButtonDiscoveryService) getBundleContext()
+                    .getService(serviceReg.getReference());
+            if (service != null) {
+                service.deactivate();
+            }
+            serviceReg.unregister();
+            discoveryServiceRegs.remove(bridgeUID);
+        }
+    }
+}
diff --git a/bundles/org.openhab.binding.flicbutton/src/main/java/org/openhab/binding/flicbutton/internal/discovery/FlicButtonDiscoveryService.java b/bundles/org.openhab.binding.flicbutton/src/main/java/org/openhab/binding/flicbutton/internal/discovery/FlicButtonDiscoveryService.java
new file mode 100644 (file)
index 0000000..917beb9
--- /dev/null
@@ -0,0 +1,41 @@
+/**
+ * Copyright (c) 2010-2022 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.flicbutton.internal.discovery;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.openhab.core.config.discovery.DiscoveryService;
+import org.openhab.core.thing.ThingUID;
+
+import io.flic.fliclib.javaclient.Bdaddr;
+import io.flic.fliclib.javaclient.FlicClient;
+
+/**
+ * A {@link DiscoveryService} for Flic buttons.
+ *
+ * @author Patrick Fink - Initial contribution
+ *
+ */
+@NonNullByDefault
+public interface FlicButtonDiscoveryService extends DiscoveryService {
+
+    /**
+     *
+     * @param bdaddr Bluetooth address of the discovered Flic button
+     * @return UID that was created by the discovery service
+     */
+    public ThingUID flicButtonDiscovered(Bdaddr bdaddr);
+
+    public void activate(FlicClient client);
+
+    public void deactivate();
+}
diff --git a/bundles/org.openhab.binding.flicbutton/src/main/java/org/openhab/binding/flicbutton/internal/discovery/FlicSimpleclientDiscoveryServiceImpl.java b/bundles/org.openhab.binding.flicbutton/src/main/java/org/openhab/binding/flicbutton/internal/discovery/FlicSimpleclientDiscoveryServiceImpl.java
new file mode 100644 (file)
index 0000000..3caa071
--- /dev/null
@@ -0,0 +1,138 @@
+/**
+ * Copyright (c) 2010-2022 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.flicbutton.internal.discovery;
+
+import java.io.IOException;
+
+import org.eclipse.jdt.annotation.NonNull;
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.binding.flicbutton.internal.FlicButtonBindingConstants;
+import org.openhab.binding.flicbutton.internal.util.FlicButtonUtils;
+import org.openhab.core.config.discovery.AbstractDiscoveryService;
+import org.openhab.core.config.discovery.DiscoveryResult;
+import org.openhab.core.config.discovery.DiscoveryResultBuilder;
+import org.openhab.core.thing.ThingUID;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import io.flic.fliclib.javaclient.Bdaddr;
+import io.flic.fliclib.javaclient.FlicClient;
+import io.flic.fliclib.javaclient.GeneralCallbacks;
+import io.flic.fliclib.javaclient.GetInfoResponseCallback;
+import io.flic.fliclib.javaclient.enums.BdAddrType;
+import io.flic.fliclib.javaclient.enums.BluetoothControllerState;
+
+/**
+ * For each configured flicd service, there is a {@link FlicSimpleclientDiscoveryServiceImpl} which will be initialized
+ * by {@link org.openhab.binding.flicbutton.internal.FlicButtonHandlerFactory}.
+ *
+ * It can scan for Flic Buttons already that are already added to fliclib-linux-hci ("verified" buttons), *
+ * but it does not support adding and verify new buttons on it's own.
+ * New buttons have to be added (verified) e.g. via simpleclient by Shortcut Labs.
+ * Background discovery listens for new buttons that are getting verified.
+ *
+ * @author Patrick Fink - Initial contribution
+ */
+@NonNullByDefault
+public class FlicSimpleclientDiscoveryServiceImpl extends AbstractDiscoveryService
+        implements FlicButtonDiscoveryService {
+    private final Logger logger = LoggerFactory.getLogger(FlicSimpleclientDiscoveryServiceImpl.class);
+
+    private boolean activated = false;
+    private ThingUID bridgeUID;
+    private @Nullable FlicClient flicClient;
+
+    public FlicSimpleclientDiscoveryServiceImpl(ThingUID bridgeUID) {
+        super(FlicButtonBindingConstants.SUPPORTED_THING_TYPES_UIDS, 2, true);
+        this.bridgeUID = bridgeUID;
+    }
+
+    @Override
+    public void activate(FlicClient flicClient) {
+        this.flicClient = flicClient;
+        activated = true;
+        super.activate(null);
+    }
+
+    @Override
+    public void deactivate() {
+        activated = false;
+        super.deactivate();
+    }
+
+    @Override
+    protected void startScan() {
+        try {
+            if (activated) {
+                discoverVerifiedButtons();
+            }
+        } catch (IOException e) {
+            logger.warn("Error occured during button discovery", e);
+            if (this.scanListener != null) {
+                scanListener.onErrorOccurred(e);
+            }
+        }
+    }
+
+    protected void discoverVerifiedButtons() throws IOException {
+        flicClient.getInfo(new GetInfoResponseCallback() {
+            @Override
+            public void onGetInfoResponse(@Nullable BluetoothControllerState bluetoothControllerState,
+                    @Nullable Bdaddr myBdAddr, @Nullable BdAddrType myBdAddrType, int maxPendingConnections,
+                    int maxConcurrentlyConnectedButtons, int currentPendingConnections,
+                    boolean currentlyNoSpaceForNewConnection, Bdaddr @Nullable [] verifiedButtons) throws IOException {
+                for (final @Nullable Bdaddr bdaddr : verifiedButtons) {
+                    if (bdaddr != null) {
+                        flicButtonDiscovered((@NonNull Bdaddr) bdaddr);
+                    }
+                }
+            }
+        });
+    }
+
+    @Override
+    protected void startBackgroundDiscovery() {
+        super.startBackgroundDiscovery();
+        flicClient.setGeneralCallbacks(new GeneralCallbacks() {
+            @Override
+            public void onNewVerifiedButton(@Nullable Bdaddr bdaddr) throws IOException {
+                logger.debug("A new Flic button was added by an external flicd client: {}", bdaddr);
+                if (bdaddr != null) {
+                    flicButtonDiscovered((@NonNull Bdaddr) bdaddr);
+                }
+            }
+        });
+    }
+
+    @Override
+    protected void stopBackgroundDiscovery() {
+        super.stopBackgroundDiscovery();
+        if (flicClient != null) {
+            flicClient.setGeneralCallbacks(null);
+        }
+    }
+
+    @Override
+    public ThingUID flicButtonDiscovered(Bdaddr bdaddr) {
+        logger.debug("Flic Button {} discovered!", bdaddr);
+        ThingUID flicButtonUID = FlicButtonUtils.getThingUIDFromBdAddr(bdaddr, bridgeUID);
+
+        DiscoveryResult discoveryResult = DiscoveryResultBuilder.create(flicButtonUID).withBridge(bridgeUID)
+                .withLabel("Flic Button " + bdaddr.toString().replace(":", ""))
+                .withProperty(FlicButtonBindingConstants.CONFIG_ADDRESS, bdaddr.toString())
+                .withRepresentationProperty(FlicButtonBindingConstants.CONFIG_ADDRESS).build();
+        this.thingDiscovered(discoveryResult);
+        return flicButtonUID;
+    }
+}
diff --git a/bundles/org.openhab.binding.flicbutton/src/main/java/org/openhab/binding/flicbutton/internal/handler/ChildThingHandler.java b/bundles/org.openhab.binding.flicbutton/src/main/java/org/openhab/binding/flicbutton/internal/handler/ChildThingHandler.java
new file mode 100644 (file)
index 0000000..58793d2
--- /dev/null
@@ -0,0 +1,86 @@
+/**
+ * Copyright (c) 2010-2022 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+
+package org.openhab.binding.flicbutton.internal.handler;
+
+import java.util.Collection;
+import java.util.Collections;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.core.thing.Thing;
+import org.openhab.core.thing.ThingStatus;
+import org.openhab.core.thing.ThingStatusDetail;
+import org.openhab.core.thing.ThingStatusInfo;
+import org.openhab.core.thing.binding.BaseThingHandler;
+import org.openhab.core.thing.binding.BridgeHandler;
+
+/**
+ * The {@link ChildThingHandler} class is an abstract class for handlers that are dependent from a parent
+ * {@link BridgeHandler}.
+ *
+ * @author Patrick Fink - Initial contribution
+ * @param <BridgeHandlerType> The bridge type this child handler depends on
+ */
+@NonNullByDefault
+public abstract class ChildThingHandler<BridgeHandlerType extends BridgeHandler> extends BaseThingHandler {
+    private static final Collection<ThingStatus> DEFAULT_TOLERATED_BRIDGE_STATUSES = Collections
+            .singleton(ThingStatus.ONLINE);
+    protected boolean bridgeValid = false;
+    protected @Nullable BridgeHandlerType bridgeHandler;
+
+    public ChildThingHandler(Thing thing) {
+        super(thing);
+    }
+
+    @Override
+    public void initialize() {
+        setStatusBasedOnBridge();
+        if (getBridge() != null) {
+            linkBridge();
+        }
+    }
+
+    protected void linkBridge() {
+        try {
+            BridgeHandler bridgeHandlerUncasted = getBridge().getHandler();
+            bridgeHandler = (BridgeHandlerType) bridgeHandlerUncasted;
+        } catch (ClassCastException e) {
+            updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "Bridge Type is invalid.");
+        }
+    }
+
+    protected void setStatusBasedOnBridge() {
+        setStatusBasedOnBridge(DEFAULT_TOLERATED_BRIDGE_STATUSES);
+    }
+
+    @Override
+    public void bridgeStatusChanged(ThingStatusInfo bridgeStatusInfo) {
+        this.setStatusBasedOnBridge();
+    }
+
+    protected void setStatusBasedOnBridge(Collection<ThingStatus> toleratedBridgeStatuses) {
+        if (getBridge() != null) {
+            if (toleratedBridgeStatuses.contains(getBridge().getStatus())) {
+                bridgeValid = true;
+            } else {
+                bridgeValid = false;
+                updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_OFFLINE,
+                        "Bridge in unsupported status: " + getBridge().getStatus());
+            }
+        } else {
+            bridgeValid = false;
+            updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_UNINITIALIZED, "Bridge missing.");
+        }
+    }
+}
diff --git a/bundles/org.openhab.binding.flicbutton/src/main/java/org/openhab/binding/flicbutton/internal/handler/FlicButtonBatteryLevelListener.java b/bundles/org.openhab.binding.flicbutton/src/main/java/org/openhab/binding/flicbutton/internal/handler/FlicButtonBatteryLevelListener.java
new file mode 100644 (file)
index 0000000..43bdda8
--- /dev/null
@@ -0,0 +1,43 @@
+/**
+ * Copyright (c) 2010-2022 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.flicbutton.internal.handler;
+
+import java.io.IOException;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+
+import io.flic.fliclib.javaclient.BatteryStatusListener;
+import io.flic.fliclib.javaclient.Bdaddr;
+
+/**
+ * Each {@link FlicButtonBatteryLevelListener} object listens to the battery status of a specific Flic button
+ * and calls updates the {@link FlicButtonHandler} accordingly.
+ *
+ * @author Patrick Fink - Initial contribution
+ *
+ */
+@NonNullByDefault
+public class FlicButtonBatteryLevelListener extends BatteryStatusListener.Callbacks {
+
+    private final FlicButtonHandler thingHandler;
+
+    FlicButtonBatteryLevelListener(FlicButtonHandler thingHandler) {
+        this.thingHandler = thingHandler;
+    }
+
+    @Override
+    public void onBatteryStatus(@Nullable Bdaddr bdaddr, int i, long l) throws IOException {
+        thingHandler.updateBatteryChannel(i);
+    }
+}
diff --git a/bundles/org.openhab.binding.flicbutton/src/main/java/org/openhab/binding/flicbutton/internal/handler/FlicButtonEventListener.java b/bundles/org.openhab.binding.flicbutton/src/main/java/org/openhab/binding/flicbutton/internal/handler/FlicButtonEventListener.java
new file mode 100644 (file)
index 0000000..77c2d07
--- /dev/null
@@ -0,0 +1,104 @@
+/**
+ * Copyright (c) 2010-2022 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.flicbutton.internal.handler;
+
+import java.io.IOException;
+import java.util.concurrent.Semaphore;
+
+import org.eclipse.jdt.annotation.NonNull;
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.binding.flicbutton.internal.FlicButtonBindingConstants;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import io.flic.fliclib.javaclient.ButtonConnectionChannel;
+import io.flic.fliclib.javaclient.enums.ClickType;
+import io.flic.fliclib.javaclient.enums.ConnectionStatus;
+import io.flic.fliclib.javaclient.enums.CreateConnectionChannelError;
+import io.flic.fliclib.javaclient.enums.DisconnectReason;
+import io.flic.fliclib.javaclient.enums.RemovedReason;
+
+/**
+ * Each {@link FlicButtonEventListener} object listens to events of a specific Flic button and calls the
+ * associated {@link FlicButtonHandler} back accordingly.
+ *
+ * @author Patrick Fink - Initial contribution
+ *
+ */
+@NonNullByDefault
+public class FlicButtonEventListener extends ButtonConnectionChannel.Callbacks {
+    private final Logger logger = LoggerFactory.getLogger(FlicButtonEventListener.class);
+
+    private final FlicButtonHandler thingHandler;
+    private final Semaphore channelResponseSemaphore = new Semaphore(0);
+
+    FlicButtonEventListener(FlicButtonHandler thingHandler) {
+        this.thingHandler = thingHandler;
+    }
+
+    public Semaphore getChannelResponseSemaphore() {
+        return channelResponseSemaphore;
+    }
+
+    @Override
+    public synchronized void onCreateConnectionChannelResponse(@Nullable ButtonConnectionChannel channel,
+            @Nullable CreateConnectionChannelError createConnectionChannelError,
+            @Nullable ConnectionStatus connectionStatus) {
+        logger.debug("Create response {}: {}, {}", channel.getBdaddr(), createConnectionChannelError, connectionStatus);
+        // Handling does not differ from Status change, so redirect
+        if (connectionStatus != null) {
+            thingHandler.initializeStatus((@NonNull ConnectionStatus) connectionStatus);
+            channelResponseSemaphore.release();
+        }
+    }
+
+    @Override
+    public void onRemoved(@Nullable ButtonConnectionChannel channel, @Nullable RemovedReason removedReason) {
+        thingHandler.flicButtonRemoved();
+        logger.debug("Button {} removed. ThingStatus updated to OFFLINE. Reason: {}", channel.getBdaddr(),
+                removedReason);
+    }
+
+    @Override
+    public void onConnectionStatusChanged(@Nullable ButtonConnectionChannel channel,
+            @Nullable ConnectionStatus connectionStatus, @Nullable DisconnectReason disconnectReason) {
+        logger.trace("New status for {}: {}", channel.getBdaddr(),
+                connectionStatus + (connectionStatus == ConnectionStatus.Disconnected ? ", " + disconnectReason : ""));
+        if (connectionStatus != null) {
+            thingHandler.connectionStatusChanged((@NonNull ConnectionStatus) connectionStatus, disconnectReason);
+        }
+    }
+
+    @Override
+    public void onButtonUpOrDown(@Nullable ButtonConnectionChannel channel, @Nullable ClickType clickType,
+            boolean wasQueued, int timeDiff) throws IOException {
+        if (channel != null && clickType != null) {
+            logger.trace("{} {}", channel.getBdaddr(), clickType.name());
+            String commonTriggerEvent = FlicButtonBindingConstants.FLIC_OPENHAB_TRIGGER_EVENT_MAP.get(clickType.name());
+            if (commonTriggerEvent != null) {
+                thingHandler.fireTriggerEvent(commonTriggerEvent);
+            }
+        }
+    }
+
+    @Override
+    public void onButtonSingleOrDoubleClickOrHold(@Nullable ButtonConnectionChannel channel,
+            @Nullable ClickType clickType, boolean wasQueued, int timeDiff) throws IOException {
+        // Handling does not differ from up/down events, so redirect
+        if (channel != null && clickType != null) {
+            onButtonUpOrDown((@NonNull ButtonConnectionChannel) channel, (@NonNull ClickType) clickType, wasQueued,
+                    timeDiff);
+        }
+    }
+}
diff --git a/bundles/org.openhab.binding.flicbutton/src/main/java/org/openhab/binding/flicbutton/internal/handler/FlicButtonHandler.java b/bundles/org.openhab.binding.flicbutton/src/main/java/org/openhab/binding/flicbutton/internal/handler/FlicButtonHandler.java
new file mode 100644 (file)
index 0000000..ffda46a
--- /dev/null
@@ -0,0 +1,213 @@
+/**
+ * Copyright (c) 2010-2022 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.flicbutton.internal.handler;
+
+import static org.openhab.binding.flicbutton.internal.FlicButtonBindingConstants.*;
+
+import java.io.IOException;
+import java.util.Objects;
+import java.util.concurrent.Future;
+import java.util.concurrent.ScheduledFuture;
+import java.util.concurrent.TimeUnit;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.core.library.types.DecimalType;
+import org.openhab.core.thing.ChannelUID;
+import org.openhab.core.thing.CommonTriggerEvents;
+import org.openhab.core.thing.Thing;
+import org.openhab.core.thing.ThingStatus;
+import org.openhab.core.thing.ThingStatusDetail;
+import org.openhab.core.thing.ThingStatusInfo;
+import org.openhab.core.types.Command;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import io.flic.fliclib.javaclient.BatteryStatusListener;
+import io.flic.fliclib.javaclient.Bdaddr;
+import io.flic.fliclib.javaclient.ButtonConnectionChannel;
+import io.flic.fliclib.javaclient.enums.ConnectionStatus;
+import io.flic.fliclib.javaclient.enums.DisconnectReason;
+
+/**
+ * The {@link FlicButtonHandler} is responsible for initializing the online status of Flic Buttons
+ * and trigger channel events when they're used.
+ *
+ * @author Patrick Fink - Initial contribution
+ */
+@NonNullByDefault
+public class FlicButtonHandler extends ChildThingHandler<FlicDaemonBridgeHandler> {
+
+    private Logger logger = LoggerFactory.getLogger(FlicButtonHandler.class);
+    private @Nullable ScheduledFuture<?> delayedDisconnectTask;
+    private @Nullable Future<?> initializationTask;
+    private @Nullable DisconnectReason latestDisconnectReason;
+    private @Nullable ButtonConnectionChannel eventConnection;
+    private @Nullable Bdaddr bdaddr;
+    private @Nullable BatteryStatusListener batteryConnection;
+
+    public FlicButtonHandler(Thing thing) {
+        super(thing);
+    }
+
+    public @Nullable Bdaddr getBdaddr() {
+        return bdaddr;
+    }
+
+    @Override
+    public void handleCommand(ChannelUID channelUID, Command command) {
+        // Pure sensor -> no commands have to be handled
+    }
+
+    @Override
+    public void initialize() {
+        super.initialize();
+        bdaddr = new Bdaddr((String) this.getThing().getConfiguration().get(CONFIG_ADDRESS));
+        if (bridgeValid) {
+            initializationTask = scheduler.submit(this::initializeThing);
+        }
+    }
+
+    @Override
+    public void bridgeStatusChanged(ThingStatusInfo bridgeStatusInfo) {
+        super.bridgeStatusChanged(bridgeStatusInfo);
+        if (getThing().getStatusInfo().getStatusDetail() == ThingStatusDetail.BRIDGE_OFFLINE && bridgeValid) {
+            dispose();
+            initializationTask = scheduler.submit(this::initializeThing);
+        }
+    }
+
+    private void initializeThing() {
+        try {
+            initializeBatteryListener();
+            initializeEventListener();
+            // EventListener calls initializeStatus() before releasing so that ThingStatus should be set at this point
+            if (this.getThing().getStatus().equals(ThingStatus.INITIALIZING)) {
+                updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
+                        "Got no response by eventListener");
+            }
+        } catch (IOException | InterruptedException e) {
+            updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
+                    "Connection setup failed: {}" + e.getMessage());
+        }
+    }
+
+    private void initializeBatteryListener() throws IOException {
+        FlicButtonBatteryLevelListener batteryListener = new FlicButtonBatteryLevelListener(this);
+        BatteryStatusListener batteryConnection = new BatteryStatusListener(getBdaddr(), batteryListener);
+        bridgeHandler.getFlicClient().addBatteryStatusListener(batteryConnection);
+        this.batteryConnection = batteryConnection;
+    }
+
+    public void initializeEventListener() throws IOException, InterruptedException {
+        FlicButtonEventListener eventListener = new FlicButtonEventListener(this);
+        ButtonConnectionChannel eventConnection = new ButtonConnectionChannel(getBdaddr(), eventListener);
+        bridgeHandler.getFlicClient().addConnectionChannel(eventConnection);
+        this.eventConnection = eventConnection;
+        eventListener.getChannelResponseSemaphore().tryAcquire(5, TimeUnit.SECONDS);
+    }
+
+    @Override
+    public void dispose() {
+        cancelDelayedDisconnectTask();
+        cancelInitializationTask();
+        try {
+            if (eventConnection != null) {
+                bridgeHandler.getFlicClient().removeConnectionChannel(eventConnection);
+            }
+            if (batteryConnection != null) {
+                bridgeHandler.getFlicClient().removeBatteryStatusListener(this.batteryConnection);
+            }
+        } catch (IOException e) {
+            logger.warn("Button channel could not be properly removed", e);
+        }
+
+        super.dispose();
+    }
+
+    void initializeStatus(ConnectionStatus connectionStatus) {
+        if (connectionStatus == ConnectionStatus.Disconnected) {
+            setOffline();
+        } else {
+            setOnline();
+        }
+    }
+
+    void connectionStatusChanged(ConnectionStatus connectionStatus, @Nullable DisconnectReason disconnectReason) {
+        latestDisconnectReason = disconnectReason;
+        if (connectionStatus == ConnectionStatus.Disconnected) {
+            // Status change to offline have to be scheduled to improve stability,
+            // see https://github.com/pfink/openhab2-flicbutton/issues/2
+            scheduleStatusChangeToOffline();
+        } else {
+            setOnline();
+        }
+    }
+
+    private void scheduleStatusChangeToOffline() {
+        if (delayedDisconnectTask == null) {
+            delayedDisconnectTask = scheduler.schedule(this::setOffline, BUTTON_OFFLINE_GRACE_PERIOD_SECONDS,
+                    TimeUnit.SECONDS);
+        }
+    }
+
+    protected void setOnline() {
+        updateStatus(ThingStatus.ONLINE, ThingStatusDetail.NONE);
+    }
+
+    protected void setOffline() {
+        updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.NONE,
+                "Disconnect Reason: " + Objects.toString(latestDisconnectReason));
+    }
+
+    // Cleanup delayedDisconnect on status change to online
+    @Override
+    protected void updateStatus(ThingStatus status, ThingStatusDetail statusDetail, @Nullable String description) {
+        if (status == ThingStatus.ONLINE) {
+            cancelDelayedDisconnectTask();
+        }
+        super.updateStatus(status, statusDetail, description);
+    }
+
+    private void cancelInitializationTask() {
+        if (initializationTask != null) {
+            initializationTask.cancel(true);
+            initializationTask = null;
+        }
+    }
+
+    private void cancelDelayedDisconnectTask() {
+        if (delayedDisconnectTask != null) {
+            delayedDisconnectTask.cancel(false);
+            delayedDisconnectTask = null;
+        }
+    }
+
+    void updateBatteryChannel(int percent) {
+        DecimalType batteryLevel = new DecimalType(percent);
+        updateState(CHANNEL_ID_BATTERY_LEVEL, batteryLevel);
+    }
+
+    void flicButtonRemoved() {
+        updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.GONE,
+                "Button was removed/detached from flicd (e.g. by simpleclient).");
+    }
+
+    void fireTriggerEvent(String event) {
+        String channelID = event.equals(CommonTriggerEvents.PRESSED) || event.equals(CommonTriggerEvents.RELEASED)
+                ? CHANNEL_ID_RAWBUTTON_EVENTS
+                : CHANNEL_ID_BUTTON_EVENTS;
+        updateStatus(ThingStatus.ONLINE);
+        triggerChannel(channelID, event);
+    }
+}
diff --git a/bundles/org.openhab.binding.flicbutton/src/main/java/org/openhab/binding/flicbutton/internal/handler/FlicDaemonBridgeConfiguration.java b/bundles/org.openhab.binding.flicbutton/src/main/java/org/openhab/binding/flicbutton/internal/handler/FlicDaemonBridgeConfiguration.java
new file mode 100644 (file)
index 0000000..1f176b2
--- /dev/null
@@ -0,0 +1,41 @@
+/**
+ * Copyright (c) 2010-2022 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.flicbutton.internal.handler;
+
+import java.net.InetAddress;
+import java.net.UnknownHostException;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+
+/**
+ * The configuration of a flicd bridge handled by {@link FlicDaemonBridgeHandler}.
+ *
+ * @author Patrick Fink - Initial contribution
+ *
+ */
+@NonNullByDefault
+public class FlicDaemonBridgeConfiguration {
+
+    @Nullable
+    private String hostname;
+    private int port;
+
+    public @Nullable InetAddress getHost() throws UnknownHostException {
+        return InetAddress.getByName(hostname);
+    }
+
+    public int getPort() {
+        return port;
+    }
+}
diff --git a/bundles/org.openhab.binding.flicbutton/src/main/java/org/openhab/binding/flicbutton/internal/handler/FlicDaemonBridgeHandler.java b/bundles/org.openhab.binding.flicbutton/src/main/java/org/openhab/binding/flicbutton/internal/handler/FlicDaemonBridgeHandler.java
new file mode 100644 (file)
index 0000000..cba4319
--- /dev/null
@@ -0,0 +1,152 @@
+/**
+ * Copyright (c) 2010-2022 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.flicbutton.internal.handler;
+
+import java.io.IOException;
+import java.net.UnknownHostException;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.concurrent.Future;
+import java.util.concurrent.TimeUnit;
+
+import org.eclipse.jdt.annotation.NonNull;
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.binding.flicbutton.internal.discovery.FlicButtonDiscoveryService;
+import org.openhab.core.thing.Bridge;
+import org.openhab.core.thing.ChannelUID;
+import org.openhab.core.thing.ThingStatus;
+import org.openhab.core.thing.ThingStatusDetail;
+import org.openhab.core.thing.binding.BaseBridgeHandler;
+import org.openhab.core.types.Command;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import io.flic.fliclib.javaclient.FlicClient;
+
+/**
+ * The {@link FlicDaemonBridgeHandler} handles a running instance of the fliclib-linux-hci server (flicd).
+ *
+ * @author Patrick Fink - Initial contribution
+ */
+@NonNullByDefault
+public class FlicDaemonBridgeHandler extends BaseBridgeHandler {
+    private final Logger logger = LoggerFactory.getLogger(FlicDaemonBridgeHandler.class);
+    private static final long REINITIALIZE_DELAY_SECONDS = 10;
+    // Config parameters
+    private @Nullable FlicDaemonBridgeConfiguration cfg;
+    // Services
+    private FlicButtonDiscoveryService buttonDiscoveryService;
+    private @Nullable Future<?> flicClientFuture;
+    // For disposal
+    private Collection<@Nullable Future<?>> startedTasks = new ArrayList<>(3);
+    private @Nullable FlicClient flicClient;
+
+    public FlicDaemonBridgeHandler(Bridge bridge, FlicButtonDiscoveryService buttonDiscoveryService) {
+        super(bridge);
+        this.buttonDiscoveryService = buttonDiscoveryService;
+    }
+
+    public @Nullable FlicClient getFlicClient() {
+        return flicClient;
+    }
+
+    @Override
+    public void initialize() {
+        startedTasks.add(scheduler.submit(this::initializeThing));
+    }
+
+    public void initializeThing() {
+        try {
+            initConfigParameters();
+            startFlicdClientAsync();
+            activateButtonDiscoveryService();
+            initThingStatus();
+        } catch (UnknownHostException ignored) {
+            updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "Hostname wrong or unknown!");
+        } catch (IOException e) {
+            updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
+                    "Error connecting to flicd: " + e.getMessage());
+            dispose();
+            scheduleReinitialize();
+        }
+    }
+
+    private void initConfigParameters() {
+        cfg = getConfigAs(FlicDaemonBridgeConfiguration.class);
+    }
+
+    private void activateButtonDiscoveryService() {
+        if (flicClient != null) {
+            buttonDiscoveryService.activate((@NonNull FlicClient) flicClient);
+        } else {
+            throw new IllegalStateException("flicClient not properly initialized");
+        }
+    }
+
+    private void startFlicdClientAsync() throws IOException {
+        flicClient = new FlicClient(cfg.getHost().getHostAddress(), cfg.getPort());
+        Runnable flicClientService = () -> {
+            try {
+                flicClient.handleEvents();
+                flicClient.close();
+                logger.debug("Listening to flicd ended");
+            } catch (IOException e) {
+                logger.debug("Error occured while listening to flicd", e);
+            } finally {
+                if (Thread.currentThread().isInterrupted()) {
+                    onClientFailure();
+                }
+            }
+        };
+
+        if (!Thread.currentThread().isInterrupted()) {
+            flicClientFuture = scheduler.submit(flicClientService);
+            startedTasks.add(flicClientFuture);
+        }
+    }
+
+    private void onClientFailure() {
+        updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
+                "flicd client terminated, probably flicd is not reachable anymore.");
+        dispose();
+        scheduleReinitialize();
+    }
+
+    private void initThingStatus() {
+        if (!flicClientFuture.isDone()) {
+            updateStatus(ThingStatus.ONLINE);
+        } else {
+            updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
+                    "flicd client could not be started, probably flicd is not reachable.");
+        }
+    }
+
+    @Override
+    public void dispose() {
+        super.dispose();
+        startedTasks.forEach(task -> task.cancel(true));
+        startedTasks = new ArrayList<>(2);
+        buttonDiscoveryService.deactivate();
+    }
+
+    private void scheduleReinitialize() {
+        startedTasks.add(scheduler.schedule(this::initialize, REINITIALIZE_DELAY_SECONDS, TimeUnit.SECONDS));
+    }
+
+    @Override
+    public void handleCommand(ChannelUID channelUID, Command command) {
+        // No commands to the fliclib-linux-hci are supported.
+        // So there is nothing to handle in the bridge handler
+    }
+}
diff --git a/bundles/org.openhab.binding.flicbutton/src/main/java/org/openhab/binding/flicbutton/internal/util/FlicButtonUtils.java b/bundles/org.openhab.binding.flicbutton/src/main/java/org/openhab/binding/flicbutton/internal/util/FlicButtonUtils.java
new file mode 100644 (file)
index 0000000..3f59ce4
--- /dev/null
@@ -0,0 +1,38 @@
+/**
+ * Copyright (c) 2010-2022 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.flicbutton.internal.util;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.openhab.binding.flicbutton.internal.FlicButtonBindingConstants;
+import org.openhab.core.thing.ThingUID;
+
+import io.flic.fliclib.javaclient.Bdaddr;
+
+/**
+ * The {@link FlicButtonUtils} class defines static utility methods that are used within the binding.
+ *
+ * @author Patrick Fink - Initial contribution
+ *
+ */
+@NonNullByDefault
+public class FlicButtonUtils {
+    public static ThingUID getThingUIDFromBdAddr(Bdaddr bdaddr, ThingUID bridgeUID) {
+        String thingID = bdaddr.toString().replace(":", "-");
+        return new ThingUID(FlicButtonBindingConstants.FLICBUTTON_THING_TYPE, bridgeUID, thingID);
+    }
+
+    public static Bdaddr getBdAddrFromThingUID(ThingUID thingUID) {
+        String bdaddrRaw = thingUID.getId().replace("-", ":");
+        return new Bdaddr(bdaddrRaw);
+    }
+}
diff --git a/bundles/org.openhab.binding.flicbutton/src/main/resources/OH-INF/binding/binding.xml b/bundles/org.openhab.binding.flicbutton/src/main/resources/OH-INF/binding/binding.xml
new file mode 100644 (file)
index 0000000..8055604
--- /dev/null
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<binding:binding id="flicbutton" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+       xmlns:binding="https://openhab.org/schemas/binding/v1.0.0"
+       xsi:schemaLocation="https://openhab.org/schemas/binding/v1.0.0 https://openhab.org/schemas/binding-1.0.0.xsd">
+
+       <name>FlicButton Binding</name>
+       <description>This is the binding for Flic buttons by Shortcut Labs.</description>
+
+</binding:binding>
diff --git a/bundles/org.openhab.binding.flicbutton/src/main/resources/OH-INF/thing/thing-types.xml b/bundles/org.openhab.binding.flicbutton/src/main/resources/OH-INF/thing/thing-types.xml
new file mode 100644 (file)
index 0000000..e5c79c2
--- /dev/null
@@ -0,0 +1,44 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<thing:thing-descriptions bindingId="flicbutton"
+       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+       xmlns:thing="https://openhab.org/schemas/thing-description/v1.0.0"
+       xsi:schemaLocation="https://openhab.org/schemas/thing-description/v1.0.0 https://openhab.org/schemas/thing-description-1.0.0.xsd">
+
+       <bridge-type id="flicd-bridge">
+               <label>FlicButton Bridge</label>
+               <description>This bridge represents a running instance of the fliclib-linux-hci server (flicd).</description>
+
+               <config-description>
+                       <parameter name="hostname" type="text" required="false">
+                               <context>network-address</context>
+                               <label>Flic Daemon (flicd) Hostname</label>
+                               <description>IP or Host name of the Flic daemon (flicd).</description>
+                               <default>localhost</default>
+                       </parameter>
+                       <parameter name="port" type="integer" required="false">
+                               <label>Flic Daemon (flicd) Port</label>
+                               <description>Port where flicd is running. Defaults to 5551.</description>
+                               <default>5551</default>
+                       </parameter>
+               </config-description>
+       </bridge-type>
+
+       <thing-type id="button">
+               <supported-bridge-type-refs>
+                       <bridge-type-ref id="flicd-bridge"/>
+               </supported-bridge-type-refs>
+               <label>Flic Button</label>
+               <description>The thing(-type) representing a Flic Button</description>
+               <channels>
+                       <channel id="rawbutton" typeId="system.rawbutton"/>
+                       <channel id="button" typeId="system.button"/>
+                       <channel id="battery-level" typeId="system.battery-level"/>
+               </channels>
+               <config-description>
+                       <parameter name="address" type="text" required="true">
+                               <label>Address</label>
+                               <description>Bluetooth address in XX:XX:XX:XX:XX:XX format</description>
+                       </parameter>
+               </config-description>
+       </thing-type>
+</thing:thing-descriptions>
index 191b76beade4dd6576da4a778fcfe41f13715bd0..43b52c4f89d221333c917733017d0ca52334b13e 100644 (file)
     <module>org.openhab.binding.exec</module>
     <module>org.openhab.binding.feed</module>
     <module>org.openhab.binding.feican</module>
+    <module>org.openhab.binding.flicbutton</module>
     <module>org.openhab.binding.fmiweather</module>
     <module>org.openhab.binding.folderwatcher</module>
     <module>org.openhab.binding.folding</module>