From 9744553979f45a6c7154da8f50b1caa53dec2fe2 Mon Sep 17 00:00:00 2001 From: Patrick Fink Date: Sun, 20 Feb 2022 21:53:30 +0100 Subject: [PATCH] [flicbutton] Initial contribution FlicButton Binding (#9234) * [flicbutton] Initial contribution FlicButton Binding Signed-off-by: Patrick Fink * [flicbutton] Add config parameter address for FlicButton thing Signed-off-by: Patrick Fink * [flicbutton] Run spotless Signed-off-by: Patrick Fink * [flicbutton] Code cleanup & docs improvement Signed-off-by: Patrick Fink * Apply suggestions from code review Co-authored-by: Fabian Wolter * [flicbutton] Update LICENSE Signed-off-by: Patrick Fink * [flicbutton] Apply suggestions from code review (2) & update to 3.1-SNAPSHOT Signed-off-by: Patrick Fink * [flicbutton] Apply suggestions from code review (3) & fix offline status Signed-off-by: Patrick Fink * [flicbutton] Fix 3rd party source for proper IDE integration Signed-off-by: Patrick Fink * [flicbutton] Simplify config parsing Signed-off-by: Patrick Fink * [flicbutton] Move everything to internal package Signed-off-by: Patrick Fink * [flicbutton] Remove hyphens from port parameter docs example Signed-off-by: Patrick Fink * [flicbutton] Change maintainer to openHAB project Signed-off-by: Patrick Fink * Apply docs suggestions + update to 3.2.0-SNAPSHOT Signed-off-by: Patrick Fink Co-authored-by: Matthew Skinner * [flicbutton] Fix bridge offline & reconnect handling Signed-off-by: Patrick Fink * [flicbutton] Close open socket on dispose Signed-off-by: Patrick Fink * [flicbutton] Improve exception error message in ThingStatus Signed-off-by: Patrick Fink * [flicbutton] Fix README title Signed-off-by: Patrick Fink * [flicbutton] Improve exception error message in ThingStatus Signed-off-by: Patrick Fink * [flicbutton] Style fixes Signed-off-by: Patrick Fink * [flicbutton] Use trace log level for button clicks & status changes Signed-off-by: Patrick Fink * Apply doc improvements from code review Signed-off-by: Patrick Fink Co-authored-by: Matthew Skinner * [flicbutton] Add binding to bom/openhab-addons Signed-off-by: Patrick Fink * [flicbutton] Cleanup / remove guava leftover Signed-off-by: Patrick Fink * [flicbutton] Remove online status description Signed-off-by: Patrick Fink * [flicbutton] Improve flicd hostname label Signed-off-by: Patrick Fink Co-authored-by: Fabian Wolter * [flicbutton] Do not catch IllegalArgumentException anymore as its not neeed Signed-off-by: Patrick Fink * [flicbutton] Use debug log level instead of info Signed-off-by: Patrick Fink * [flicbutton] Update version and license Signed-off-by: Patrick Fink * [flicbutton] Fix SAT warnings, e.g. add null handling annotations Signed-off-by: Patrick Fink * [flicbutton] Fix SAT warnings (2) Signed-off-by: Patrick Fink * [flicbutton] Concurrency refactoring & fixes Signed-off-by: Patrick Fink * [flicbutton] Cancel initialization task also when already running Signed-off-by: Patrick Fink * [flicbutton] Add javadoc and move FLIC_OPENHAB_EVENT_TRIGGER_MAP constant to constants class Signed-off-by: Patrick Fink * [flicbutton] Use ThingStatusDetail.OFFLINE.GONE when Flic button was removed from bridge Signed-off-by: Patrick Fink * [flicbutton] Fix FlicSimpleclientDiscoveryServiceImpl javadoc Signed-off-by: Patrick Fink * [flicbutton] Fix required definition of thing types Signed-off-by: Patrick Fink Co-authored-by: Fabian Wolter Co-authored-by: Matthew Skinner Signed-off-by: Andras Uhrin --- CODEOWNERS | 1 + bom/openhab-addons/pom.xml | 5 + bundles/org.openhab.binding.flicbutton/NOTICE | 21 + .../org.openhab.binding.flicbutton/README.md | 130 ++++ .../org.openhab.binding.flicbutton/pom.xml | 38 ++ .../src/3rdparty/LICENSE | 121 ++++ .../javaclient/BatteryStatusListener.java | 45 ++ .../io/flic/fliclib/javaclient/Bdaddr.java | 68 ++ .../javaclient/ButtonConnectionChannel.java | 188 ++++++ .../fliclib/javaclient/ButtonScanner.java | 28 + .../flic/fliclib/javaclient/FlicClient.java | 630 ++++++++++++++++++ .../fliclib/javaclient/GeneralCallbacks.java | 28 + .../GetButtonInfoResponseCallback.java | 18 + .../javaclient/GetInfoResponseCallback.java | 19 + .../io/flic/fliclib/javaclient/Packets.java | 455 +++++++++++++ .../flic/fliclib/javaclient/ScanWizard.java | 64 ++ .../flic/fliclib/javaclient/StreamUtils.java | 81 +++ .../io/flic/fliclib/javaclient/TimerTask.java | 14 + .../fliclib/javaclient/enums/BdAddrType.java | 9 + .../enums/BluetoothControllerState.java | 10 + .../fliclib/javaclient/enums/ClickType.java | 13 + .../javaclient/enums/ConnectionStatus.java | 10 + .../enums/CreateConnectionChannelError.java | 9 + .../javaclient/enums/DisconnectReason.java | 11 + .../fliclib/javaclient/enums/LatencyMode.java | 10 + .../javaclient/enums/RemovedReason.java | 22 + .../javaclient/enums/ScanWizardResult.java | 13 + .../src/main/feature/feature.xml | 9 + .../internal/FlicButtonBindingConstants.java | 65 ++ .../internal/FlicButtonHandlerFactory.java | 104 +++ .../discovery/FlicButtonDiscoveryService.java | 41 ++ .../FlicSimpleclientDiscoveryServiceImpl.java | 138 ++++ .../internal/handler/ChildThingHandler.java | 86 +++ .../FlicButtonBatteryLevelListener.java | 43 ++ .../handler/FlicButtonEventListener.java | 104 +++ .../internal/handler/FlicButtonHandler.java | 213 ++++++ .../FlicDaemonBridgeConfiguration.java | 41 ++ .../handler/FlicDaemonBridgeHandler.java | 152 +++++ .../internal/util/FlicButtonUtils.java | 38 ++ .../main/resources/OH-INF/binding/binding.xml | 9 + .../resources/OH-INF/thing/thing-types.xml | 44 ++ bundles/pom.xml | 1 + 42 files changed, 3149 insertions(+) create mode 100644 bundles/org.openhab.binding.flicbutton/NOTICE create mode 100644 bundles/org.openhab.binding.flicbutton/README.md create mode 100644 bundles/org.openhab.binding.flicbutton/pom.xml create mode 100644 bundles/org.openhab.binding.flicbutton/src/3rdparty/LICENSE create mode 100644 bundles/org.openhab.binding.flicbutton/src/3rdparty/java/io/flic/fliclib/javaclient/BatteryStatusListener.java create mode 100644 bundles/org.openhab.binding.flicbutton/src/3rdparty/java/io/flic/fliclib/javaclient/Bdaddr.java create mode 100644 bundles/org.openhab.binding.flicbutton/src/3rdparty/java/io/flic/fliclib/javaclient/ButtonConnectionChannel.java create mode 100644 bundles/org.openhab.binding.flicbutton/src/3rdparty/java/io/flic/fliclib/javaclient/ButtonScanner.java create mode 100644 bundles/org.openhab.binding.flicbutton/src/3rdparty/java/io/flic/fliclib/javaclient/FlicClient.java create mode 100644 bundles/org.openhab.binding.flicbutton/src/3rdparty/java/io/flic/fliclib/javaclient/GeneralCallbacks.java create mode 100644 bundles/org.openhab.binding.flicbutton/src/3rdparty/java/io/flic/fliclib/javaclient/GetButtonInfoResponseCallback.java create mode 100644 bundles/org.openhab.binding.flicbutton/src/3rdparty/java/io/flic/fliclib/javaclient/GetInfoResponseCallback.java create mode 100644 bundles/org.openhab.binding.flicbutton/src/3rdparty/java/io/flic/fliclib/javaclient/Packets.java create mode 100644 bundles/org.openhab.binding.flicbutton/src/3rdparty/java/io/flic/fliclib/javaclient/ScanWizard.java create mode 100644 bundles/org.openhab.binding.flicbutton/src/3rdparty/java/io/flic/fliclib/javaclient/StreamUtils.java create mode 100644 bundles/org.openhab.binding.flicbutton/src/3rdparty/java/io/flic/fliclib/javaclient/TimerTask.java create mode 100644 bundles/org.openhab.binding.flicbutton/src/3rdparty/java/io/flic/fliclib/javaclient/enums/BdAddrType.java create mode 100644 bundles/org.openhab.binding.flicbutton/src/3rdparty/java/io/flic/fliclib/javaclient/enums/BluetoothControllerState.java create mode 100644 bundles/org.openhab.binding.flicbutton/src/3rdparty/java/io/flic/fliclib/javaclient/enums/ClickType.java create mode 100644 bundles/org.openhab.binding.flicbutton/src/3rdparty/java/io/flic/fliclib/javaclient/enums/ConnectionStatus.java create mode 100644 bundles/org.openhab.binding.flicbutton/src/3rdparty/java/io/flic/fliclib/javaclient/enums/CreateConnectionChannelError.java create mode 100644 bundles/org.openhab.binding.flicbutton/src/3rdparty/java/io/flic/fliclib/javaclient/enums/DisconnectReason.java create mode 100644 bundles/org.openhab.binding.flicbutton/src/3rdparty/java/io/flic/fliclib/javaclient/enums/LatencyMode.java create mode 100644 bundles/org.openhab.binding.flicbutton/src/3rdparty/java/io/flic/fliclib/javaclient/enums/RemovedReason.java create mode 100644 bundles/org.openhab.binding.flicbutton/src/3rdparty/java/io/flic/fliclib/javaclient/enums/ScanWizardResult.java create mode 100644 bundles/org.openhab.binding.flicbutton/src/main/feature/feature.xml create mode 100644 bundles/org.openhab.binding.flicbutton/src/main/java/org/openhab/binding/flicbutton/internal/FlicButtonBindingConstants.java create mode 100644 bundles/org.openhab.binding.flicbutton/src/main/java/org/openhab/binding/flicbutton/internal/FlicButtonHandlerFactory.java create mode 100644 bundles/org.openhab.binding.flicbutton/src/main/java/org/openhab/binding/flicbutton/internal/discovery/FlicButtonDiscoveryService.java create mode 100644 bundles/org.openhab.binding.flicbutton/src/main/java/org/openhab/binding/flicbutton/internal/discovery/FlicSimpleclientDiscoveryServiceImpl.java create mode 100644 bundles/org.openhab.binding.flicbutton/src/main/java/org/openhab/binding/flicbutton/internal/handler/ChildThingHandler.java create mode 100644 bundles/org.openhab.binding.flicbutton/src/main/java/org/openhab/binding/flicbutton/internal/handler/FlicButtonBatteryLevelListener.java create mode 100644 bundles/org.openhab.binding.flicbutton/src/main/java/org/openhab/binding/flicbutton/internal/handler/FlicButtonEventListener.java create mode 100644 bundles/org.openhab.binding.flicbutton/src/main/java/org/openhab/binding/flicbutton/internal/handler/FlicButtonHandler.java create mode 100644 bundles/org.openhab.binding.flicbutton/src/main/java/org/openhab/binding/flicbutton/internal/handler/FlicDaemonBridgeConfiguration.java create mode 100644 bundles/org.openhab.binding.flicbutton/src/main/java/org/openhab/binding/flicbutton/internal/handler/FlicDaemonBridgeHandler.java create mode 100644 bundles/org.openhab.binding.flicbutton/src/main/java/org/openhab/binding/flicbutton/internal/util/FlicButtonUtils.java create mode 100644 bundles/org.openhab.binding.flicbutton/src/main/resources/OH-INF/binding/binding.xml create mode 100644 bundles/org.openhab.binding.flicbutton/src/main/resources/OH-INF/thing/thing-types.xml diff --git a/CODEOWNERS b/CODEOWNERS index 3ca334187ffde..f236dd025e13b 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -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 diff --git a/bom/openhab-addons/pom.xml b/bom/openhab-addons/pom.xml index 6a84df61dc5ba..b65930de3ee40 100644 --- a/bom/openhab-addons/pom.xml +++ b/bom/openhab-addons/pom.xml @@ -456,6 +456,11 @@ org.openhab.binding.feican ${project.version} + + org.openhab.addons.bundles + org.openhab.binding.flicbutton + ${project.version} + org.openhab.addons.bundles org.openhab.binding.fmiweather diff --git a/bundles/org.openhab.binding.flicbutton/NOTICE b/bundles/org.openhab.binding.flicbutton/NOTICE new file mode 100644 index 0000000000000..ca1e0d671e175 --- /dev/null +++ b/bundles/org.openhab.binding.flicbutton/NOTICE @@ -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 index 0000000000000..87e81dfdf2d6d --- /dev/null +++ b/bundles/org.openhab.binding.flicbutton/README.md @@ -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="", port=] +``` + +If flicd is running on a remote host, please do not forget to start it with the parameter `-s `, 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="", port=] { + Thing button myflic1 "" [address =""] + Thing button myflic2 "" [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 index 0000000000000..9163b246e9d4b --- /dev/null +++ b/bundles/org.openhab.binding.flicbutton/pom.xml @@ -0,0 +1,38 @@ + + + + 4.0.0 + + + org.openhab.addons.bundles + org.openhab.addons.reactor.bundles + 3.3.0-SNAPSHOT + + + org.openhab.binding.flicbutton + + openHAB Add-ons :: Bundles :: FlicButton Binding + + + + + org.codehaus.mojo + build-helper-maven-plugin + + + + add-source + + generate-sources + + + src/3rdparty/java + + + + + + + + diff --git a/bundles/org.openhab.binding.flicbutton/src/3rdparty/LICENSE b/bundles/org.openhab.binding.flicbutton/src/3rdparty/LICENSE new file mode 100644 index 0000000000000..1625c17936079 --- /dev/null +++ b/bundles/org.openhab.binding.flicbutton/src/3rdparty/LICENSE @@ -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 index 0000000000000..bb562f191500b --- /dev/null +++ b/bundles/org.openhab.binding.flicbutton/src/3rdparty/java/io/flic/fliclib/javaclient/BatteryStatusListener.java @@ -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 index 0000000000000..16a7b3989d3ff --- /dev/null +++ b/bundles/org.openhab.binding.flicbutton/src/3rdparty/java/io/flic/fliclib/javaclient/Bdaddr.java @@ -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 index 0000000000000..a006aea8b2eea --- /dev/null +++ b/bundles/org.openhab.binding.flicbutton/src/3rdparty/java/io/flic/fliclib/javaclient/ButtonConnectionChannel.java @@ -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 index 0000000000000..41ca1487e8cf9 --- /dev/null +++ b/bundles/org.openhab.binding.flicbutton/src/3rdparty/java/io/flic/fliclib/javaclient/ButtonScanner.java @@ -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 index 0000000000000..07f0544bbf4e0 --- /dev/null +++ b/bundles/org.openhab.binding.flicbutton/src/3rdparty/java/io/flic/fliclib/javaclient/FlicClient.java @@ -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 scanners = new ConcurrentHashMap<>(); + private ConcurrentHashMap connectionChannels = new ConcurrentHashMap<>(); + private ConcurrentHashMap scanWizards = new ConcurrentHashMap<>(); + private ConcurrentHashMap batteryStatusListeners = new ConcurrentHashMap<>(); + private ConcurrentLinkedQueue getInfoResponseCallbackQueue = new ConcurrentLinkedQueue<>(); + private ArrayDeque getButtonInfoResponseCallbackQueue = new ArrayDeque<>(); + + private volatile GeneralCallbacks generalCallbacks = new GeneralCallbacks(); + + private ConcurrentSkipListMap 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 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 index 0000000000000..77399d7aa382b --- /dev/null +++ b/bundles/org.openhab.binding.flicbutton/src/3rdparty/java/io/flic/fliclib/javaclient/GeneralCallbacks.java @@ -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 index 0000000000000..740f8dd0b4204 --- /dev/null +++ b/bundles/org.openhab.binding.flicbutton/src/3rdparty/java/io/flic/fliclib/javaclient/GetButtonInfoResponseCallback.java @@ -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 index 0000000000000..642a356658346 --- /dev/null +++ b/bundles/org.openhab.binding.flicbutton/src/3rdparty/java/io/flic/fliclib/javaclient/GetInfoResponseCallback.java @@ -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 index 0000000000000..7cdcf011c9d5d --- /dev/null +++ b/bundles/org.openhab.binding.flicbutton/src/3rdparty/java/io/flic/fliclib/javaclient/Packets.java @@ -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 index 0000000000000..3e573f6bbe18d --- /dev/null +++ b/bundles/org.openhab.binding.flicbutton/src/3rdparty/java/io/flic/fliclib/javaclient/ScanWizard.java @@ -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 index 0000000000000..0db0ededd8e73 --- /dev/null +++ b/bundles/org.openhab.binding.flicbutton/src/3rdparty/java/io/flic/fliclib/javaclient/StreamUtils.java @@ -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 index 0000000000000..8ed0919d2edf6 --- /dev/null +++ b/bundles/org.openhab.binding.flicbutton/src/3rdparty/java/io/flic/fliclib/javaclient/TimerTask.java @@ -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 index 0000000000000..650e719ce8104 --- /dev/null +++ b/bundles/org.openhab.binding.flicbutton/src/3rdparty/java/io/flic/fliclib/javaclient/enums/BdAddrType.java @@ -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 index 0000000000000..24ee268192204 --- /dev/null +++ b/bundles/org.openhab.binding.flicbutton/src/3rdparty/java/io/flic/fliclib/javaclient/enums/BluetoothControllerState.java @@ -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 index 0000000000000..d1a767b705b79 --- /dev/null +++ b/bundles/org.openhab.binding.flicbutton/src/3rdparty/java/io/flic/fliclib/javaclient/enums/ClickType.java @@ -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 index 0000000000000..ad0673ad5fc26 --- /dev/null +++ b/bundles/org.openhab.binding.flicbutton/src/3rdparty/java/io/flic/fliclib/javaclient/enums/ConnectionStatus.java @@ -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 index 0000000000000..aa78274ecd547 --- /dev/null +++ b/bundles/org.openhab.binding.flicbutton/src/3rdparty/java/io/flic/fliclib/javaclient/enums/CreateConnectionChannelError.java @@ -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 index 0000000000000..a875a725ca68b --- /dev/null +++ b/bundles/org.openhab.binding.flicbutton/src/3rdparty/java/io/flic/fliclib/javaclient/enums/DisconnectReason.java @@ -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 index 0000000000000..135693f5d2983 --- /dev/null +++ b/bundles/org.openhab.binding.flicbutton/src/3rdparty/java/io/flic/fliclib/javaclient/enums/LatencyMode.java @@ -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 index 0000000000000..48bc410f6bb68 --- /dev/null +++ b/bundles/org.openhab.binding.flicbutton/src/3rdparty/java/io/flic/fliclib/javaclient/enums/RemovedReason.java @@ -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 index 0000000000000..e31582a98a182 --- /dev/null +++ b/bundles/org.openhab.binding.flicbutton/src/3rdparty/java/io/flic/fliclib/javaclient/enums/ScanWizardResult.java @@ -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 index 0000000000000..45d90679a3968 --- /dev/null +++ b/bundles/org.openhab.binding.flicbutton/src/main/feature/feature.xml @@ -0,0 +1,9 @@ + + + mvn:org.openhab.core.features.karaf/org.openhab.core.features.karaf.openhab-core/${ohc.version}/xml/features + + + openhab-runtime-base + mvn:org.openhab.addons.bundles/org.openhab.binding.flicbutton/${project.version} + + 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 index 0000000000000..0aa1773aff3b8 --- /dev/null +++ b/bundles/org.openhab.binding.flicbutton/src/main/java/org/openhab/binding/flicbutton/internal/FlicButtonBindingConstants.java @@ -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 BRIDGE_THING_TYPES_UIDS = Collections.singleton(BRIDGE_THING_TYPE); + public static final Set 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 FLIC_OPENHAB_TRIGGER_EVENT_MAP = Collections + .unmodifiableMap(new HashMap() { + { + 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 index 0000000000000..5e44ab7fcc0a0 --- /dev/null +++ b/bundles/org.openhab.binding.flicbutton/src/main/java/org/openhab/binding/flicbutton/internal/FlicButtonHandlerFactory.java @@ -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 SUPPORTED_THING_TYPES_UIDS = Stream + .concat(FlicButtonBindingConstants.BRIDGE_THING_TYPES_UIDS.stream(), + FlicButtonBindingConstants.SUPPORTED_THING_TYPES_UIDS.stream()) + .collect(Collectors.toSet()); + private final Map> 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())); + } + + 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 index 0000000000000..917beb9570843 --- /dev/null +++ b/bundles/org.openhab.binding.flicbutton/src/main/java/org/openhab/binding/flicbutton/internal/discovery/FlicButtonDiscoveryService.java @@ -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 index 0000000000000..3caa071093be2 --- /dev/null +++ b/bundles/org.openhab.binding.flicbutton/src/main/java/org/openhab/binding/flicbutton/internal/discovery/FlicSimpleclientDiscoveryServiceImpl.java @@ -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 index 0000000000000..58793d295d0d3 --- /dev/null +++ b/bundles/org.openhab.binding.flicbutton/src/main/java/org/openhab/binding/flicbutton/internal/handler/ChildThingHandler.java @@ -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 The bridge type this child handler depends on + */ +@NonNullByDefault +public abstract class ChildThingHandler extends BaseThingHandler { + private static final Collection 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 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 index 0000000000000..43bdda8dd6817 --- /dev/null +++ b/bundles/org.openhab.binding.flicbutton/src/main/java/org/openhab/binding/flicbutton/internal/handler/FlicButtonBatteryLevelListener.java @@ -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 index 0000000000000..77c2d0780ae53 --- /dev/null +++ b/bundles/org.openhab.binding.flicbutton/src/main/java/org/openhab/binding/flicbutton/internal/handler/FlicButtonEventListener.java @@ -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 index 0000000000000..ffda46abbb5cb --- /dev/null +++ b/bundles/org.openhab.binding.flicbutton/src/main/java/org/openhab/binding/flicbutton/internal/handler/FlicButtonHandler.java @@ -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 { + + 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 index 0000000000000..1f176b2bac5a8 --- /dev/null +++ b/bundles/org.openhab.binding.flicbutton/src/main/java/org/openhab/binding/flicbutton/internal/handler/FlicDaemonBridgeConfiguration.java @@ -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 index 0000000000000..cba4319de0e45 --- /dev/null +++ b/bundles/org.openhab.binding.flicbutton/src/main/java/org/openhab/binding/flicbutton/internal/handler/FlicDaemonBridgeHandler.java @@ -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 index 0000000000000..3f59ce45b4615 --- /dev/null +++ b/bundles/org.openhab.binding.flicbutton/src/main/java/org/openhab/binding/flicbutton/internal/util/FlicButtonUtils.java @@ -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 index 0000000000000..8055604ca2001 --- /dev/null +++ b/bundles/org.openhab.binding.flicbutton/src/main/resources/OH-INF/binding/binding.xml @@ -0,0 +1,9 @@ + + + + FlicButton Binding + This is the binding for Flic buttons by Shortcut Labs. + + 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 index 0000000000000..e5c79c2ce9008 --- /dev/null +++ b/bundles/org.openhab.binding.flicbutton/src/main/resources/OH-INF/thing/thing-types.xml @@ -0,0 +1,44 @@ + + + + + + This bridge represents a running instance of the fliclib-linux-hci server (flicd). + + + + network-address + + IP or Host name of the Flic daemon (flicd). + localhost + + + + Port where flicd is running. Defaults to 5551. + 5551 + + + + + + + + + + The thing(-type) representing a Flic Button + + + + + + + + + Bluetooth address in XX:XX:XX:XX:XX:XX format + + + + diff --git a/bundles/pom.xml b/bundles/pom.xml index 32554e52a0600..5bcac28e59ed1 100644 --- a/bundles/pom.xml +++ b/bundles/pom.xml @@ -125,6 +125,7 @@ org.openhab.binding.exec org.openhab.binding.feed org.openhab.binding.feican + org.openhab.binding.flicbutton org.openhab.binding.fmiweather org.openhab.binding.folderwatcher org.openhab.binding.folding