[flicbutton] Initial contribution FlicButton Binding (#9234)

* [flicbutton] Initial contribution FlicButton Binding

Signed-off-by: Patrick Fink <mail@pfink.de>

* [flicbutton] Add config parameter address for FlicButton thing

Signed-off-by: Patrick Fink <mail@pfink.de>

* [flicbutton] Run spotless

Signed-off-by: Patrick Fink <mail@pfink.de>

* [flicbutton] Code cleanup & docs improvement

Signed-off-by: Patrick Fink <mail@pfink.de>

* Apply suggestions from code review

Co-authored-by: Fabian Wolter <github@fabian-wolter.de>

* [flicbutton] Update LICENSE

Signed-off-by: Patrick Fink <mail@pfink.de>

* [flicbutton] Apply suggestions from code review (2) & update to 3.1-SNAPSHOT

Signed-off-by: Patrick Fink <mail@pfink.de>

* [flicbutton] Apply suggestions from code review (3) & fix offline status

Signed-off-by: Patrick Fink <mail@pfink.de>

* [flicbutton] Fix 3rd party source for proper IDE integration

Signed-off-by: Patrick Fink <mail@pfink.de>

* [flicbutton] Simplify config parsing

Signed-off-by: Patrick Fink <mail@pfink.de>

* [flicbutton] Move everything to internal package

Signed-off-by: Patrick Fink <mail@pfink.de>

* [flicbutton] Remove hyphens from port parameter docs example

Signed-off-by: Patrick Fink <mail@pfink.de>

* [flicbutton] Change maintainer to openHAB project

Signed-off-by: Patrick Fink <mail@pfink.de>

* Apply docs suggestions + update to 3.2.0-SNAPSHOT

Signed-off-by: Patrick Fink <mail@pfink.de>

Co-authored-by: Matthew Skinner <matt@pcmus.com>

* [flicbutton] Fix bridge offline & reconnect handling

Signed-off-by: Patrick Fink <mail@pfink.de>

* [flicbutton] Close open socket on dispose

Signed-off-by: Patrick Fink <mail@pfink.de>

* [flicbutton] Improve exception error message in ThingStatus

Signed-off-by: Patrick Fink <mail@pfink.de>

* [flicbutton] Fix README title

Signed-off-by: Patrick Fink <mail@pfink.de>

* [flicbutton] Improve exception error message in ThingStatus

Signed-off-by: Patrick Fink <mail@pfink.de>

* [flicbutton] Style fixes

Signed-off-by: Patrick Fink <mail@pfink.de>

* [flicbutton] Use trace log level for button clicks & status changes

Signed-off-by: Patrick Fink <mail@pfink.de>

* Apply doc improvements from code review

Signed-off-by: Patrick Fink <mail@pfink.de>

Co-authored-by: Matthew Skinner <matt@pcmus.com>

* [flicbutton] Add binding to bom/openhab-addons

Signed-off-by: Patrick Fink <mail@pfink.de>

* [flicbutton] Cleanup / remove guava leftover

Signed-off-by: Patrick Fink <mail@pfink.de>

* [flicbutton] Remove online status description

Signed-off-by: Patrick Fink <mail@pfink.de>

* [flicbutton] Improve flicd hostname label

Signed-off-by: Patrick Fink <mail@pfink.de>

Co-authored-by: Fabian Wolter <github@fabian-wolter.de>

* [flicbutton] Do not catch IllegalArgumentException anymore as its not neeed

Signed-off-by: Patrick Fink <mail@pfink.de>

* [flicbutton] Use debug log level instead of info

Signed-off-by: Patrick Fink <mail@pfink.de>

* [flicbutton] Update version and license

Signed-off-by: Patrick Fink <mail@pfink.de>

* [flicbutton] Fix SAT warnings, e.g. add null handling annotations

Signed-off-by: Patrick Fink <mail@pfink.de>

* [flicbutton] Fix SAT warnings (2)

Signed-off-by: Patrick Fink <mail@pfink.de>

* [flicbutton] Concurrency refactoring & fixes

Signed-off-by: Patrick Fink <mail@pfink.de>

* [flicbutton] Cancel initialization task also when already running

Signed-off-by: Patrick Fink <mail@pfink.de>

* [flicbutton] Add javadoc and move FLIC_OPENHAB_EVENT_TRIGGER_MAP constant to constants class

Signed-off-by: Patrick Fink <mail@pfink.de>

* [flicbutton] Use ThingStatusDetail.OFFLINE.GONE when Flic button was removed from bridge

Signed-off-by: Patrick Fink <mail@pfink.de>

* [flicbutton] Fix FlicSimpleclientDiscoveryServiceImpl javadoc

Signed-off-by: Patrick Fink <mail@pfink.de>

* [flicbutton] Fix required definition of thing types

Signed-off-by: Patrick Fink <mail@pfink.de>

Co-authored-by: Fabian Wolter <github@fabian-wolter.de>
Co-authored-by: Matthew Skinner <matt@pcmus.com>
This commit is contained in:
Patrick Fink 2022-02-20 21:53:30 +01:00 committed by GitHub
parent 51bbd34cd7
commit 6c104e241a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
42 changed files with 3149 additions and 0 deletions

View File

@ -93,6 +93,7 @@
/bundles/org.openhab.binding.exec/ @kgoderis
/bundles/org.openhab.binding.feed/ @svilenvul
/bundles/org.openhab.binding.feican/ @Hilbrand
/bundles/org.openhab.binding.flicbutton/ @pfink
/bundles/org.openhab.binding.fmiweather/ @ssalonen
/bundles/org.openhab.binding.folderwatcher/ @goopilot
/bundles/org.openhab.binding.folding/ @fa2k

View File

@ -456,6 +456,11 @@
<artifactId>org.openhab.binding.feican</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.openhab.addons.bundles</groupId>
<artifactId>org.openhab.binding.flicbutton</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.openhab.addons.bundles</groupId>
<artifactId>org.openhab.binding.fmiweather</artifactId>

View File

@ -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

View File

@ -0,0 +1,130 @@
# Flic Button Binding
openHAB binding for using [Flic Buttons](https://flic.io/)
with a [fliclib-linux-hci](https://github.com/50ButtonsEach/fliclib-linux-hci) bridge.
Currently, although Flic Buttons are BLE devices, this binding only supports fliclib-linux-hci (flicd) as a bridge.
The openHAB Bluetooth Bindings are not supported.
Flicd requires a seperate Bluetooth adapter to work, so if you use this binding together with e.g. the
[Bluez Binding](https://www.openhab.org/addons/bindings/bluetooth.bluez/),
two physical Bluetooth adapters are required (one for Bluez and one for flicd).
Be aware that flicd requires an initial internet connection for the verification of the buttons.
After buttons are initially added to flicd, an internet connection is not required anymore.
## Supported Things
| Thing Type ID | Description |
| --------------- | ------------------------- |
| flicd-bridge | The bridge representing a running instance of [fliclib-linux-hci (flicd)](https://github.com/50ButtonsEach/fliclib-linux-hci) on the server. |
| button | The Flic button (supports Flic 1 buttons as well as Flic 2 buttons) |
## Discovery
* There is no automatic discovery for flicd-bridge available.
* After flicd-bridge is (manually) configured, buttons will be automatically discovered via background discovery as soon
as they're added with [simpleclient](https://github.com/50ButtonsEach/fliclib-linux-hci).
If they're already attached to the flicd-bridge before configuring this binding, they can be discovered by triggering an
active scan.
## Thing Configuration
### flicd-bridge
Example for textual configuration:
```
Bridge flicbutton:flicd-bridge:mybridge
```
The default host is localhost:5551 (this should be sufficient if flicd is running with default settings on the same server as openHAB).
If your flicd service is running somewhere else, specify it like this:
```
Bridge flicbutton:flicd-bridge:mybridge [ hostname="<YOUR_HOSTNAME>", port=<YOUR_PORT>]
```
If flicd is running on a remote host, please do not forget to start it with the parameter `-s <openHAB IP>`, otherwise it won't be accessible for openHAB (more details on [fliclib-linux-hci](https://github.com/50ButtonsEach/fliclib-linux-hci)).
### button
For the button, the only config parameter is the MAC address.
Normally, no textual configuration is necessary as buttons are auto discovered as soon as the bridge is configured.
If you want to use textual configuration anyway, you can do it like this:
```
Bridge flicbutton:flicd-bridge:mybridge [ hostname="<YOUR_HOSTNAME>", port=<YOUR_PORT>] {
Thing button myflic1 "<YOUR_LABEL>" [address ="<MAC_ADDRESS>"]
Thing button myflic2 "<YOUR_LABEL>" [address ="<MAC_ADDRESS>"]
...
}
```
You can lookup the MAC addresses of your buttons within the inbox of the UI.
You're free to choose any label you like for your button.
## Channels
| Channel ID | Channel Type | Item Type | Description |
| ------------------------- | ------------------------ | --------------------------| ------------------------------ |
| rawbutton | [System Trigger Channel](https://www.openhab.org/docs/developer/bindings/thing-xml.html#system-trigger-channel-types) `system.rawbutton` | Depends on the [Trigger Profile](https://www.openhab.org/docs/configuration/items.html#profiles) used | Raw Button channel that triggers `PRESSED` and `RELEASED` events, allows to use openHAB profiles or own implementations via rules to detect e.g. double clicks, long presses etc. |
| button | [System Trigger Channel](https://www.openhab.org/docs/developer/bindings/thing-xml.html#system-trigger-channel-types) `system.button` | Depends on the [Trigger Profile](https://www.openhab.org/docs/configuration/items.html#profiles) used | Button channel that is using Flic's implementation for detecting long, short or double clicks. Triggers `SHORT_PRESSED`,`DOUBLE_PRESSED` and `LONG_PRESSED` events. |
| battery-level | [System State Channel](https://www.openhab.org/docs/developer/bindings/thing-xml.html#system-state-channel-types) `system.battery-level` | Number | Represents the battery level as a percentage (0-100%).
## Example
### Initial setup
1. Setup and run flicd as described in [fliclib-linux-hci](https://github.com/50ButtonsEach/fliclib-linux-hci).
Please consider that you need a separate Bluetooth adapter. Shared usage with other Bluetooth services (e.g. Bluez)
is not possible.
1. Connect your buttons to flicd using the simpleclient as described in
[fliclib-linux-hci](https://github.com/50ButtonsEach/fliclib-linux-hci). Flicd has to run in background the whole
time, simpleclient can be killed after you successfully test the button connects.
1. Add a flicd-bridge via the UI or textual configuration. Please consider that flicd only accepts connections from
localhost by default, to enable remote connections from openHAB you have to use the `--server-addr` parameter as
described in [fliclib-linux-hci](https://github.com/50ButtonsEach/fliclib-linux-hci).
1. When the bridge is online, buttons newly added via simpleclient will automatically get discovered via background
discovery. To discover buttons that were set up before the binding was setup, please run an active scan.
### Configuration Example using Profiles
[Profiles](https://www.openhab.org/docs/configuration/items.html#profiles) are the recommended way to use this binding.
demo.things:
```
Bridge flicbutton:flicd-bridge:local-flicd {
Thing button flic_livingroom "Yellow Button Living Room" [address = "60:13:B3:02:18:BD"]
Thing button flic_kitchen "Black Button Kitchen" [address = "B5:7E:59:78:86:9F"]
}
```
demo.items:
```
Dimmer Light_LivingRoom { channel="milight:rgbLed:milight2:4:ledbrightness", channel="flicbutton:button:local-flicd:flic_livingroom:rawbutton" [profile="rawbutton-toggle-switch"], channel="flicbutton:button:local-flicd:flic_kitchen:rawbutton" [profile="rawbutton-toggle-switch"] } // We have a combined kitchen / livingroom, so we control the living room lights with switches from the living room and from the kitchen
Switch Light_Kitchen { channel="hue:group:1:kitchen-bulbs:switch", channel="flicbutton:button:local-flicd:flic_kitchen:rawbutton" [profile="rawbutton-toggle-switch"] }
```
### Configuration Example using Rules
It's also possible to setup [Rules](https://www.openhab.org/docs/configuration/rules-dsl.html).
The following rules help to initially test your setup as they'll trigger log messages on incoming events.
```
rule "Button rule using the button channel"
when
Channel "flicbutton:button:local-flicd:flic_livingroom:button" triggered SHORT_PRESSED
then
logInfo("Flic", "Flic 'short pressed' triggered")
end
rule "Button rule directly using the rawbutton channel"
when
Channel "flicbutton:button:local-flicd:flic_livingroom:rawbutton" triggered
then
logInfo("Flic", "Flic pressed: " + receivedEvent.event)
end
```

View File

@ -0,0 +1,38 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.openhab.addons.bundles</groupId>
<artifactId>org.openhab.addons.reactor.bundles</artifactId>
<version>3.3.0-SNAPSHOT</version>
</parent>
<artifactId>org.openhab.binding.flicbutton</artifactId>
<name>openHAB Add-ons :: Bundles :: FlicButton Binding</name>
<build>
<plugins>
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>build-helper-maven-plugin</artifactId>
<executions>
<execution>
<goals>
<goal>add-source</goal>
</goals>
<phase>generate-sources</phase>
<configuration>
<sources>
<source>src/3rdparty/java</source>
</sources>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>

View File

@ -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.

View File

@ -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;
}
}

View File

@ -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);
}
}

View File

@ -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 {
}
}
}

View File

@ -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;
}

View File

@ -0,0 +1,630 @@
package io.flic.fliclib.javaclient;
import io.flic.fliclib.javaclient.enums.CreateConnectionChannelError;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.Socket;
import java.net.SocketTimeoutException;
import java.net.UnknownHostException;
import java.util.ArrayDeque;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.ConcurrentSkipListMap;
/**
* Implements a FlicClient over a TCP Socket.
*
* When this class is constructed, a socket connection is established.
*
* You may then send commands to the server and set timers.
*
* Once you are ready with the initialization you must call the {@link #handleEvents()} method which is a main loop that never exits, unless the socket is closed.
*
* For a more detailed description of all commands, events and enums, check the protocol specification.
*/
public class FlicClient {
private Socket socket;
private InputStream socketInputStream;
private OutputStream socketOutputStream;
private ConcurrentHashMap<Integer, ButtonScanner> scanners = new ConcurrentHashMap<>();
private ConcurrentHashMap<Integer, ButtonConnectionChannel> connectionChannels = new ConcurrentHashMap<>();
private ConcurrentHashMap<Integer, ScanWizard> scanWizards = new ConcurrentHashMap<>();
private ConcurrentHashMap<Integer, BatteryStatusListener> batteryStatusListeners = new ConcurrentHashMap<>();
private ConcurrentLinkedQueue<GetInfoResponseCallback> getInfoResponseCallbackQueue = new ConcurrentLinkedQueue<>();
private ArrayDeque<GetButtonInfoResponseCallback> getButtonInfoResponseCallbackQueue = new ArrayDeque<>();
private volatile GeneralCallbacks generalCallbacks = new GeneralCallbacks();
private ConcurrentSkipListMap<Long, TimerTask> timers = new ConcurrentSkipListMap<>();
private Thread handleEventsThread;
/**
* Create a FlicClient and connect to the specified hostName and TCP port
*
* @param hostName
* @param port
* @throws UnknownHostException
* @throws IOException
*/
public FlicClient(String hostName, int port) throws UnknownHostException, IOException {
socket = new Socket(hostName, port);
socket.setKeepAlive(true);
socketInputStream = socket.getInputStream();
socketOutputStream = socket.getOutputStream();
}
/**
* Create a FlicClient and connect to the specified hostName using the default TCP port
*
* @param hostName
* @throws UnknownHostException
* @throws IOException
*/
public FlicClient(String hostName) throws UnknownHostException, IOException {
this(hostName, 5551);
}
/**
* Close the socket.
*
* From this point any use of this FlicClient is illegal.
* The {@link #handleEvents()} will return as soon as the closing is done.
*
* @throws IOException
*/
public void close() throws IOException {
runOnHandleEventsThread(new TimerTask() {
@Override
public void run() throws IOException {
socket.close();
}
});
}
/**
* Set general callbacks to be called upon receiving some specific events.
*
* @param callbacks
*/
public void setGeneralCallbacks(GeneralCallbacks callbacks) {
if (callbacks == null) {
callbacks = new GeneralCallbacks();
}
generalCallbacks = callbacks;
}
/**
* Get info about the current state of the server.
*
* The server will send back its information directly and the callback will be called once the response arrives.
*
* @param callback
* @throws IOException
*/
public void getInfo(GetInfoResponseCallback callback) throws IOException {
if (callback == null) {
throw new IllegalArgumentException("callback is null");
}
getInfoResponseCallbackQueue.add(callback);
CmdGetInfo pkt = new CmdGetInfo();
sendPacket(pkt);
}
/**
* Get button info for a verified button.
*
* The server will send back its information directly and the callback will be called once the response arrives.
* Responses will arrive in the same order as requested.
*
* If the button isn't verified, the data sent to callback will be null.
*
* @param bdaddr The bluetooth address.
* @param callback Callback for the response.
* @throws IOException
*/
public void getButtonInfo(final Bdaddr bdaddr, final GetButtonInfoResponseCallback callback) throws IOException {
if (callback == null) {
throw new IllegalArgumentException("callback is null");
}
// Run on events thread to ensure ordering if multiple requests are issued at the same time
runOnHandleEventsThread(new TimerTask() {
@Override
public void run() throws IOException {
getButtonInfoResponseCallbackQueue.add(callback);
CmdGetButtonInfo pkt = new CmdGetButtonInfo();
pkt.bdaddr = bdaddr;
sendPacket(pkt);
}
});
}
/**
* Add a scanner.
*
* The scan will start directly once the scanner is added.
*
* @param buttonScanner
* @throws IOException
*/
public void addScanner(ButtonScanner buttonScanner) throws IOException {
if (buttonScanner == null) {
throw new IllegalArgumentException("buttonScanner is null");
}
if (scanners.putIfAbsent(buttonScanner.scanId, buttonScanner) != null) {
throw new IllegalArgumentException("Button scanner already added");
}
CmdCreateScanner pkt = new CmdCreateScanner();
pkt.scanId = buttonScanner.scanId;
sendPacket(pkt);
}
/**
* Remove a scanner.
*
* @param buttonScanner The same scanner that was used in {@link #addScanner(ButtonScanner)}
* @throws IOException
*/
public void removeScanner(ButtonScanner buttonScanner) throws IOException {
if (buttonScanner == null) {
throw new IllegalArgumentException("buttonScanner is null");
}
if (scanners.remove(buttonScanner.scanId) == null) {
throw new IllegalArgumentException("Button scanner was never added");
}
CmdRemoveScanner pkt = new CmdRemoveScanner();
pkt.scanId = buttonScanner.scanId;
sendPacket(pkt);
}
/**
* Add a scan wizard.
*
* The scan wizard will start directly once the scan wizard is added.
*
* @param scanWizard
* @throws IOException
*/
public void addScanWizard(ScanWizard scanWizard) throws IOException {
if (scanWizard == null) {
throw new IllegalArgumentException("scanWizard is null");
}
if (scanWizards.putIfAbsent(scanWizard.scanWizardId, scanWizard) != null) {
throw new IllegalArgumentException("Scan wizard already added");
}
CmdCreateScanWizard pkt = new CmdCreateScanWizard();
pkt.scanWizardId = scanWizard.scanWizardId;
sendPacket(pkt);
}
/**
* Cancel a scan wizard.
*
* This will cancel an ongoing scan wizard.
*
* If cancelled due to this request, the result of the scan wizard will be WizardCancelledByUser.
*
* @param scanWizard The same scan wizard that was used in {@link #addScanWizard(ScanWizard)}
* @throws IOException
*/
public void cancelScanWizard(ScanWizard scanWizard) throws IOException {
if (scanWizard == null) {
throw new IllegalArgumentException("scanWizard is null");
}
CmdCancelScanWizard pkt = new CmdCancelScanWizard();
pkt.scanWizardId = scanWizard.scanWizardId;
sendPacket(pkt);
}
/**
* Adds a connection channel to a specific Flic button.
*
* This will start listening for a specific Flic button's connection and button events.
* Make sure the Flic is either in public mode (by holding it down for 7 seconds) or already verified before calling this method.
*
* The {@link ButtonConnectionChannel.Callbacks#onCreateConnectionChannelResponse}
* method will be called after this command has been received by the server.
*
* You may have as many connection channels as you wish for a specific Flic Button.
*
* @param channel
* @throws IOException
*/
public void addConnectionChannel(ButtonConnectionChannel channel) throws IOException {
if (channel == null) {
throw new IllegalArgumentException("channel is null");
}
if (connectionChannels.putIfAbsent(channel.connId, channel) != null) {
throw new IllegalArgumentException("Connection channel already added");
}
synchronized (channel.lock) {
channel.client = this;
CmdCreateConnectionChannel pkt = new CmdCreateConnectionChannel();
pkt.connId = channel.connId;
pkt.bdaddr = channel.getBdaddr();
pkt.latencyMode = channel.getLatencyMode();
pkt.autoDisconnectTime = channel.getAutoDisconnectTime();
sendPacket(pkt);
}
}
/**
* Remove a connection channel.
*
* This will stop listening for new events for a specific connection channel that has previously been added.
* Note: The effect of this command will take place at the time the {@link ButtonConnectionChannel.Callbacks#onRemoved} event arrives.
*
* @param channel
* @throws IOException
*/
public void removeConnectionChannel(ButtonConnectionChannel channel) throws IOException {
if (channel == null) {
throw new IllegalArgumentException("channel is null");
}
CmdRemoveConnectionChannel pkt = new CmdRemoveConnectionChannel();
pkt.connId = channel.connId;
sendPacket(pkt);
}
/**
* Force disconnection or cancel pending connection of a specific Flic button.
*
* This removes all connection channels for all clients connected to the server for this specific Flic button.
*
* @param bdaddr
* @throws IOException
*/
public void forceDisconnect(Bdaddr bdaddr) throws IOException {
if (bdaddr == null) {
throw new IllegalArgumentException("bdaddr is null");
}
CmdForceDisconnect pkt = new CmdForceDisconnect();
pkt.bdaddr = bdaddr;
sendPacket(pkt);
}
/**
* Delete a button.
*
* @param bdaddr
* @throws IOException
*/
public void deleteButton(Bdaddr bdaddr) throws IOException {
if (bdaddr == null) {
throw new IllegalArgumentException("bdaddr is null");
}
CmdDeleteButton pkt = new CmdDeleteButton();
pkt.bdaddr = bdaddr;
sendPacket(pkt);
}
/**
* Add a battery status listener.
*
* @param listener
* @throws IOException
*/
public void addBatteryStatusListener(BatteryStatusListener listener) throws IOException {
if (listener == null) {
throw new IllegalArgumentException("listener is null");
}
if (batteryStatusListeners.putIfAbsent(listener.listenerId, listener) != null) {
throw new IllegalArgumentException("Battery status listener already added");
}
CmdCreateBatteryStatusListener pkt = new CmdCreateBatteryStatusListener();
pkt.listenerId = listener.listenerId;
pkt.bdaddr = listener.getBdaddr();
sendPacket(pkt);
}
/**
* Remove a battery status listener
*
* @param listener
* @throws IOException
*/
public void removeBatteryStatusListener(BatteryStatusListener listener) throws IOException {
if (listener == null) {
throw new IllegalArgumentException("buttonScanner is null");
}
if (batteryStatusListeners.remove(listener.listenerId) == null) {
throw new IllegalArgumentException("Battery status listener was never added");
}
CmdRemoveBatteryStatusListener pkt = new CmdRemoveBatteryStatusListener();
pkt.listenerId = listener.listenerId;
sendPacket(pkt);
}
void sendPacket(CommandPacket packet) throws IOException {
byte[] bytes = packet.construct();
synchronized (socketOutputStream) {
socketOutputStream.write(bytes);
}
}
/**
* Set a timer.
*
* This timer task will run after the specified timeoutMillis on the thread that handles the events.
*
* @param timeoutMillis
* @param timerTask
* @throws IOException
*/
public void setTimer(int timeoutMillis, TimerTask timerTask) throws IOException {
long pointInTime = System.nanoTime() + timeoutMillis * 1000000L;
while (timers.putIfAbsent(pointInTime, timerTask) != null) {
pointInTime++;
}
if (handleEventsThread != Thread.currentThread()) {
CmdPing pkt = new CmdPing();
pkt.pingId = 0;
sendPacket(pkt);
}
}
/**
* Run a task on the thread that handles the events.
*
* @param task
* @throws IOException
*/
public void runOnHandleEventsThread(TimerTask task) throws IOException {
if (handleEventsThread == Thread.currentThread()) {
task.run();
} else {
setTimer(0, task);
}
}
/**
* Start the main loop for this client.
*
* This method will not return until the socket has been closed.
* Once it has returned, any use of this FlicClient is illegal.
*
* @throws IOException
*/
public void handleEvents() throws IOException {
handleEventsThread = Thread.currentThread();
while (!Thread.currentThread().isInterrupted()) {
Map.Entry<Long, TimerTask> firstTimer = timers.firstEntry();
long timeout = 0;
if (firstTimer != null) {
timeout = firstTimer.getKey() - System.nanoTime();
if (timeout <= 0) {
timers.remove(firstTimer.getKey(), firstTimer.getValue());
firstTimer.getValue().run();
continue;
}
}
if (socket.isClosed()) {
break;
}
int len0;
socket.setSoTimeout((int)(timeout / 1000000));
try {
len0 = socketInputStream.read();
} catch (SocketTimeoutException e) {
continue;
}
int len1 = socketInputStream.read();
int len = len0 | (len1 << 8);
if ((len >> 16) == -1) {
break;
}
if (len == 0) {
continue;
}
byte[] pkt = new byte[len];
int pos = 0;
while (pos < len) {
int nbytes = socketInputStream.read(pkt, pos, len - pos);
if (nbytes == -1) {
break;
}
pos += nbytes;
}
if (len == 1) {
continue;
}
dispatchPacket(pkt);
}
socket.close();
}
private void dispatchPacket(byte[] packet) throws IOException {
int opcode = packet[0];
switch (opcode) {
case EventPacket.EVT_ADVERTISEMENT_PACKET_OPCODE: {
EvtAdvertisementPacket pkt = new EvtAdvertisementPacket();
pkt.parse(packet);
ButtonScanner scanner = scanners.get(pkt.scanId);
if (scanner != null) {
scanner.onAdvertisementPacket(pkt.addr, pkt.name, pkt.rssi, pkt.isPrivate, pkt.alreadyVerified, pkt.alreadyConnectedToThisDevice, pkt.alreadyConnectedToOtherDevice);
}
break;
}
case EventPacket.EVT_CREATE_CONNECTION_CHANNEL_RESPONSE_OPCODE: {
EvtCreateConnectionChannelResponse pkt = new EvtCreateConnectionChannelResponse();
pkt.parse(packet);
ButtonConnectionChannel channel = connectionChannels.get(pkt.connId);
if (channel != null) {
if (pkt.connectionChannelError != CreateConnectionChannelError.NoError) {
connectionChannels.remove(channel.connId);
}
channel.callbacks.onCreateConnectionChannelResponse(channel, pkt.connectionChannelError, pkt.connectionStatus);
}
break;
}
case EventPacket.EVT_CONNECTION_STATUS_CHANGED_OPCODE: {
EvtConnectionStatusChanged pkt = new EvtConnectionStatusChanged();
pkt.parse(packet);
ButtonConnectionChannel channel = connectionChannels.get(pkt.connId);
if (channel != null) {
channel.callbacks.onConnectionStatusChanged(channel, pkt.connectionStatus, pkt.disconnectReason);
}
break;
}
case EventPacket.EVT_CONNECTION_CHANNEL_REMOVED_OPCODE: {
EvtConnectionChannelRemoved pkt = new EvtConnectionChannelRemoved();
pkt.parse(packet);
ButtonConnectionChannel channel = connectionChannels.get(pkt.connId);
if (channel != null) {
connectionChannels.remove(channel.connId);
channel.callbacks.onRemoved(channel, pkt.removedReason);
}
break;
}
case EventPacket.EVT_BUTTON_UP_OR_DOWN_OPCODE:
case EventPacket.EVT_BUTTON_CLICK_OR_HOLD_OPCODE:
case EventPacket.EVT_BUTTON_SINGLE_OR_DOUBLE_CLICK_OPCODE:
case EventPacket.EVT_BUTTON_SINGLE_OR_DOUBLE_CLICK_OR_HOLD_OPCODE: {
EvtButtonEvent pkt = new EvtButtonEvent();
pkt.parse(packet);
ButtonConnectionChannel channel = connectionChannels.get(pkt.connId);
if (channel != null) {
if (opcode == EventPacket.EVT_BUTTON_UP_OR_DOWN_OPCODE) {
channel.callbacks.onButtonUpOrDown(channel, pkt.clickType, pkt.wasQueued, pkt.timeDiff);
} else if (opcode == EventPacket.EVT_BUTTON_CLICK_OR_HOLD_OPCODE) {
channel.callbacks.onButtonClickOrHold(channel, pkt.clickType, pkt.wasQueued, pkt.timeDiff);
} else if (opcode == EventPacket.EVT_BUTTON_SINGLE_OR_DOUBLE_CLICK_OPCODE) {
channel.callbacks.onButtonSingleOrDoubleClick(channel, pkt.clickType, pkt.wasQueued, pkt.timeDiff);
} else if (opcode == EventPacket.EVT_BUTTON_SINGLE_OR_DOUBLE_CLICK_OR_HOLD_OPCODE) {
channel.callbacks.onButtonSingleOrDoubleClickOrHold(channel, pkt.clickType, pkt.wasQueued, pkt.timeDiff);
}
}
break;
}
case EventPacket.EVT_NEW_VERIFIED_BUTTON_OPCODE: {
EvtNewVerifiedButton pkt = new EvtNewVerifiedButton();
pkt.parse(packet);
GeneralCallbacks gc = generalCallbacks;
if (gc != null) {
gc.onNewVerifiedButton(pkt.bdaddr);
}
break;
}
case EventPacket.EVT_GET_INFO_RESPONSE_OPCODE: {
EvtGetInfoResponse pkt = new EvtGetInfoResponse();
pkt.parse(packet);
getInfoResponseCallbackQueue.remove().onGetInfoResponse(pkt.bluetoothControllerState, pkt.myBdAddr, pkt.myBdAddrType, pkt.maxPendingConnections, pkt.maxConcurrentlyConnectedButtons, pkt.currentPendingConnections, pkt.currentlyNoSpaceForNewConnections, pkt.bdAddrOfVerifiedButtons);
break;
}
case EventPacket.EVT_NO_SPACE_FOR_NEW_CONNECTION_OPCODE: {
EvtNoSpaceForNewConnection pkt = new EvtNoSpaceForNewConnection();
pkt.parse(packet);
GeneralCallbacks gc = generalCallbacks;
if (gc != null) {
gc.onNoSpaceForNewConnection(pkt.maxConcurrentlyConnectedButtons);
}
break;
}
case EventPacket.EVT_GOT_SPACE_FOR_NEW_CONNECTION_OPCODE: {
EvtGotSpaceForNewConnection pkt = new EvtGotSpaceForNewConnection();
pkt.parse(packet);
GeneralCallbacks gc = generalCallbacks;
if (gc != null) {
gc.onGotSpaceForNewConnection(pkt.maxConcurrentlyConnectedButtons);
}
break;
}
case EventPacket.EVT_BLUETOOTH_CONTROLLER_STATE_CHANGE_OPCODE: {
EvtBluetoothControllerStateChange pkt = new EvtBluetoothControllerStateChange();
pkt.parse(packet);
GeneralCallbacks gc = generalCallbacks;
if (gc != null) {
gc.onBluetoothControllerStateChange(pkt.state);
}
break;
}
case EventPacket.EVT_GET_BUTTON_INFO_RESPONSE_OPCODE: {
EvtGetButtonInfoResponse pkt = new EvtGetButtonInfoResponse();
pkt.parse(packet);
getButtonInfoResponseCallbackQueue.remove().onGetButtonInfoResponse(pkt.bdaddr, pkt.uuid, pkt.color, pkt.serialNumber);
break;
}
case EventPacket.EVT_SCAN_WIZARD_FOUND_PRIVATE_BUTTON_OPCODE: {
EvtScanWizardFoundPrivateButton pkt = new EvtScanWizardFoundPrivateButton();
pkt.parse(packet);
ScanWizard wizard = scanWizards.get(pkt.scanWizardId);
if (wizard != null) {
wizard.onFoundPrivateButton();
}
break;
}
case EventPacket.EVT_SCAN_WIZARD_FOUND_PUBLIC_BUTTON_OPCODE: {
EvtScanWizardFoundPublicButton pkt = new EvtScanWizardFoundPublicButton();
pkt.parse(packet);
ScanWizard wizard = scanWizards.get(pkt.scanWizardId);
if (wizard != null) {
wizard.bdaddr = pkt.addr;
wizard.name = pkt.name;
wizard.onFoundPublicButton(wizard.bdaddr, wizard.name);
}
break;
}
case EventPacket.EVT_SCAN_WIZARD_BUTTON_CONNECTED_OPCODE: {
EvtScanWizardButtonConnected pkt = new EvtScanWizardButtonConnected();
pkt.parse(packet);
ScanWizard wizard = scanWizards.get(pkt.scanWizardId);
if (wizard != null) {
wizard.onButtonConnected(wizard.bdaddr, wizard.name);
}
break;
}
case EventPacket.EVT_SCAN_WIZARD_COMPLETED_OPCODE: {
EvtScanWizardCompleted pkt = new EvtScanWizardCompleted();
pkt.parse(packet);
ScanWizard wizard = scanWizards.get(pkt.scanWizardId);
scanWizards.remove(pkt.scanWizardId);
if (wizard != null) {
Bdaddr bdaddr = wizard.bdaddr;
String name = wizard.name;
wizard.bdaddr = null;
wizard.name = null;
wizard.onCompleted(pkt.result, bdaddr, name);
}
break;
}
case EventPacket.EVT_BUTTON_DELETED_OPCODE: {
EvtButtonDeleted pkt = new EvtButtonDeleted();
pkt.parse(packet);
GeneralCallbacks gc = generalCallbacks;
if (gc != null) {
gc.onButtonDeleted(pkt.bdaddr, pkt.deletedByThisClient);
}
break;
}
case EventPacket.EVT_BATTERY_STATUS_OPCODE: {
EvtBatteryStatus pkt = new EvtBatteryStatus();
pkt.parse(packet);
BatteryStatusListener listener = batteryStatusListeners.get(pkt.listenerId);
if (listener != null) {
listener.callbacks.onBatteryStatus(listener.getBdaddr(), pkt.batteryPercentage, pkt.timestamp);
}
break;
}
}
}
}

View File

@ -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 {
}
}

View File

@ -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);
}

View File

@ -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;
}

View File

@ -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);
}
}

View File

@ -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;
}

View File

@ -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());
}
}

View File

@ -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;
}

View File

@ -0,0 +1,9 @@
package io.flic.fliclib.javaclient.enums;
/**
* Created by Emil on 2016-05-03.
*/
public enum BdAddrType {
PublicBdAddrType,
RandomBdAddrType
}

View File

@ -0,0 +1,10 @@
package io.flic.fliclib.javaclient.enums;
/**
* Created by Emil on 2016-05-03.
*/
public enum BluetoothControllerState {
Detached,
Resetting,
Attached
}

View File

@ -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
}

View File

@ -0,0 +1,10 @@
package io.flic.fliclib.javaclient.enums;
/**
* Created by Emil on 2016-05-03.
*/
public enum ConnectionStatus {
Disconnected,
Connected,
Ready
}

View File

@ -0,0 +1,9 @@
package io.flic.fliclib.javaclient.enums;
/**
* Created by Emil on 2016-05-03.
*/
public enum CreateConnectionChannelError {
NoError,
MaxPendingConnectionsReached
}

View File

@ -0,0 +1,11 @@
package io.flic.fliclib.javaclient.enums;
/**
* Created by Emil on 2016-05-03.
*/
public enum DisconnectReason {
Unspecified,
ConnectionEstablishmentFailed,
TimedOut,
BondingKeysMismatch
}

View File

@ -0,0 +1,10 @@
package io.flic.fliclib.javaclient.enums;
/**
* Created by Emil on 2016-05-03.
*/
public enum LatencyMode {
NormalLatency,
LowLatency,
HighLatency
}

View File

@ -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
}

View File

@ -0,0 +1,13 @@
package io.flic.fliclib.javaclient.enums;
public enum ScanWizardResult {
WizardSuccess,
WizardCancelledByUser,
WizardFailedTimeout,
WizardButtonIsPrivate,
WizardBluetoothUnavailable,
WizardInternetBackendError,
WizardInvalidData,
WizardButtonBelongsToOtherPartner,
WizardButtonAlreadyConnectedToOtherDevice
}

View File

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<features name="org.openhab.binding.flicbutton-${project.version}" xmlns="http://karaf.apache.org/xmlns/features/v1.4.0">
<repository>mvn:org.openhab.core.features.karaf/org.openhab.core.features.karaf.openhab-core/${ohc.version}/xml/features</repository>
<feature name="openhab-binding-flicbutton" description="FlicButton Binding" version="${project.version}">
<feature>openhab-runtime-base</feature>
<bundle start-level="80">mvn:org.openhab.addons.bundles/org.openhab.binding.flicbutton/${project.version}</bundle>
</feature>
</features>

View File

@ -0,0 +1,65 @@
/**
* Copyright (c) 2010-2022 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.flicbutton.internal;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.core.thing.CommonTriggerEvents;
import org.openhab.core.thing.ThingTypeUID;
/**
* The {@link FlicButtonBindingConstants} class defines common constants, which are
* used across the whole binding.
*
* @author Patrick Fink - Initial contribution
*/
@NonNullByDefault
public class FlicButtonBindingConstants {
public static final String BINDING_ID = "flicbutton";
// List of all Thing Type UIDs
public static final ThingTypeUID BRIDGE_THING_TYPE = new ThingTypeUID(BINDING_ID, "flicd-bridge");
public static final ThingTypeUID FLICBUTTON_THING_TYPE = new ThingTypeUID(BINDING_ID, "button");
public static final Set<ThingTypeUID> BRIDGE_THING_TYPES_UIDS = Collections.singleton(BRIDGE_THING_TYPE);
public static final Set<ThingTypeUID> SUPPORTED_THING_TYPES_UIDS = Collections.singleton(FLICBUTTON_THING_TYPE);
// List of all configuration options
public static final String CONFIG_HOST_NAME = "hostname";
public static final String CONFIG_PORT = "port";
public static final String CONFIG_ADDRESS = "address";
// List of all Channel ids
public static final String CHANNEL_ID_RAWBUTTON_EVENTS = "rawbutton";
public static final String CHANNEL_ID_BUTTON_EVENTS = "button";
public static final String CHANNEL_ID_BATTERY_LEVEL = "battery-level";
// Other stuff
public static final int BUTTON_OFFLINE_GRACE_PERIOD_SECONDS = 60;
public static final Map<String, String> FLIC_OPENHAB_TRIGGER_EVENT_MAP = Collections
.unmodifiableMap(new HashMap<String, String>() {
{
put("ButtonSingleClick", CommonTriggerEvents.SHORT_PRESSED);
put("ButtonDoubleClick", CommonTriggerEvents.DOUBLE_PRESSED);
put("ButtonHold", CommonTriggerEvents.LONG_PRESSED);
put("ButtonDown", CommonTriggerEvents.PRESSED);
put("ButtonUp", CommonTriggerEvents.RELEASED);
}
});
}

View File

@ -0,0 +1,104 @@
/**
* Copyright (c) 2010-2022 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.flicbutton.internal;
import java.util.HashMap;
import java.util.Hashtable;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.flicbutton.internal.discovery.FlicButtonDiscoveryService;
import org.openhab.binding.flicbutton.internal.discovery.FlicSimpleclientDiscoveryServiceImpl;
import org.openhab.binding.flicbutton.internal.handler.FlicButtonHandler;
import org.openhab.binding.flicbutton.internal.handler.FlicDaemonBridgeHandler;
import org.openhab.core.config.discovery.DiscoveryService;
import org.openhab.core.thing.Bridge;
import org.openhab.core.thing.Thing;
import org.openhab.core.thing.ThingTypeUID;
import org.openhab.core.thing.ThingUID;
import org.openhab.core.thing.binding.BaseThingHandlerFactory;
import org.openhab.core.thing.binding.ThingHandler;
import org.openhab.core.thing.binding.ThingHandlerFactory;
import org.osgi.framework.ServiceRegistration;
import org.osgi.service.component.annotations.Component;
/**
* The {@link FlicButtonHandlerFactory} is responsible for creating things and thing
* handlers.
*
* @author Patrick Fink - Initial contribution
*/
@Component(service = ThingHandlerFactory.class, configurationPid = "binding.flicbutton")
@NonNullByDefault
public class FlicButtonHandlerFactory extends BaseThingHandlerFactory {
private static final Set<ThingTypeUID> SUPPORTED_THING_TYPES_UIDS = Stream
.concat(FlicButtonBindingConstants.BRIDGE_THING_TYPES_UIDS.stream(),
FlicButtonBindingConstants.SUPPORTED_THING_TYPES_UIDS.stream())
.collect(Collectors.toSet());
private final Map<ThingUID, @Nullable ServiceRegistration<?>> discoveryServiceRegs = new HashMap<>();
@Override
public boolean supportsThingType(ThingTypeUID thingTypeUID) {
return SUPPORTED_THING_TYPES_UIDS.contains(thingTypeUID);
}
@Override
@Nullable
protected ThingHandler createHandler(Thing thing) {
ThingTypeUID thingTypeUID = thing.getThingTypeUID();
if (thingTypeUID.equals(FlicButtonBindingConstants.FLICBUTTON_THING_TYPE)) {
return new FlicButtonHandler(thing);
} else if (thingTypeUID.equals(FlicButtonBindingConstants.BRIDGE_THING_TYPE)) {
FlicButtonDiscoveryService discoveryService = new FlicSimpleclientDiscoveryServiceImpl(thing.getUID());
FlicDaemonBridgeHandler bridgeHandler = new FlicDaemonBridgeHandler((Bridge) thing, discoveryService);
registerDiscoveryService(discoveryService, thing.getUID());
return bridgeHandler;
}
return null;
}
@Override
protected synchronized void removeHandler(ThingHandler thingHandler) {
if (thingHandler instanceof FlicDaemonBridgeHandler) {
unregisterDiscoveryService(thingHandler.getThing().getUID());
}
super.removeHandler(thingHandler);
}
private synchronized void registerDiscoveryService(FlicButtonDiscoveryService discoveryService,
ThingUID bridgeUID) {
this.discoveryServiceRegs.put(bridgeUID, getBundleContext().registerService(DiscoveryService.class.getName(),
discoveryService, new Hashtable<String, Object>()));
}
private synchronized void unregisterDiscoveryService(ThingUID bridgeUID) {
ServiceRegistration<?> serviceReg = this.discoveryServiceRegs.get(bridgeUID);
if (serviceReg != null) {
FlicButtonDiscoveryService service = (FlicButtonDiscoveryService) getBundleContext()
.getService(serviceReg.getReference());
if (service != null) {
service.deactivate();
}
serviceReg.unregister();
discoveryServiceRegs.remove(bridgeUID);
}
}
}

View File

@ -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();
}

View File

@ -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;
}
}

View File

@ -0,0 +1,86 @@
/**
* Copyright (c) 2010-2022 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.flicbutton.internal.handler;
import java.util.Collection;
import java.util.Collections;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.core.thing.Thing;
import org.openhab.core.thing.ThingStatus;
import org.openhab.core.thing.ThingStatusDetail;
import org.openhab.core.thing.ThingStatusInfo;
import org.openhab.core.thing.binding.BaseThingHandler;
import org.openhab.core.thing.binding.BridgeHandler;
/**
* The {@link ChildThingHandler} class is an abstract class for handlers that are dependent from a parent
* {@link BridgeHandler}.
*
* @author Patrick Fink - Initial contribution
* @param <BridgeHandlerType> The bridge type this child handler depends on
*/
@NonNullByDefault
public abstract class ChildThingHandler<BridgeHandlerType extends BridgeHandler> extends BaseThingHandler {
private static final Collection<ThingStatus> DEFAULT_TOLERATED_BRIDGE_STATUSES = Collections
.singleton(ThingStatus.ONLINE);
protected boolean bridgeValid = false;
protected @Nullable BridgeHandlerType bridgeHandler;
public ChildThingHandler(Thing thing) {
super(thing);
}
@Override
public void initialize() {
setStatusBasedOnBridge();
if (getBridge() != null) {
linkBridge();
}
}
protected void linkBridge() {
try {
BridgeHandler bridgeHandlerUncasted = getBridge().getHandler();
bridgeHandler = (BridgeHandlerType) bridgeHandlerUncasted;
} catch (ClassCastException e) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "Bridge Type is invalid.");
}
}
protected void setStatusBasedOnBridge() {
setStatusBasedOnBridge(DEFAULT_TOLERATED_BRIDGE_STATUSES);
}
@Override
public void bridgeStatusChanged(ThingStatusInfo bridgeStatusInfo) {
this.setStatusBasedOnBridge();
}
protected void setStatusBasedOnBridge(Collection<ThingStatus> toleratedBridgeStatuses) {
if (getBridge() != null) {
if (toleratedBridgeStatuses.contains(getBridge().getStatus())) {
bridgeValid = true;
} else {
bridgeValid = false;
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_OFFLINE,
"Bridge in unsupported status: " + getBridge().getStatus());
}
} else {
bridgeValid = false;
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_UNINITIALIZED, "Bridge missing.");
}
}
}

View File

@ -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);
}
}

View File

@ -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);
}
}
}

View File

@ -0,0 +1,213 @@
/**
* Copyright (c) 2010-2022 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.flicbutton.internal.handler;
import static org.openhab.binding.flicbutton.internal.FlicButtonBindingConstants.*;
import java.io.IOException;
import java.util.Objects;
import java.util.concurrent.Future;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.core.library.types.DecimalType;
import org.openhab.core.thing.ChannelUID;
import org.openhab.core.thing.CommonTriggerEvents;
import org.openhab.core.thing.Thing;
import org.openhab.core.thing.ThingStatus;
import org.openhab.core.thing.ThingStatusDetail;
import org.openhab.core.thing.ThingStatusInfo;
import org.openhab.core.types.Command;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import io.flic.fliclib.javaclient.BatteryStatusListener;
import io.flic.fliclib.javaclient.Bdaddr;
import io.flic.fliclib.javaclient.ButtonConnectionChannel;
import io.flic.fliclib.javaclient.enums.ConnectionStatus;
import io.flic.fliclib.javaclient.enums.DisconnectReason;
/**
* The {@link FlicButtonHandler} is responsible for initializing the online status of Flic Buttons
* and trigger channel events when they're used.
*
* @author Patrick Fink - Initial contribution
*/
@NonNullByDefault
public class FlicButtonHandler extends ChildThingHandler<FlicDaemonBridgeHandler> {
private Logger logger = LoggerFactory.getLogger(FlicButtonHandler.class);
private @Nullable ScheduledFuture<?> delayedDisconnectTask;
private @Nullable Future<?> initializationTask;
private @Nullable DisconnectReason latestDisconnectReason;
private @Nullable ButtonConnectionChannel eventConnection;
private @Nullable Bdaddr bdaddr;
private @Nullable BatteryStatusListener batteryConnection;
public FlicButtonHandler(Thing thing) {
super(thing);
}
public @Nullable Bdaddr getBdaddr() {
return bdaddr;
}
@Override
public void handleCommand(ChannelUID channelUID, Command command) {
// Pure sensor -> no commands have to be handled
}
@Override
public void initialize() {
super.initialize();
bdaddr = new Bdaddr((String) this.getThing().getConfiguration().get(CONFIG_ADDRESS));
if (bridgeValid) {
initializationTask = scheduler.submit(this::initializeThing);
}
}
@Override
public void bridgeStatusChanged(ThingStatusInfo bridgeStatusInfo) {
super.bridgeStatusChanged(bridgeStatusInfo);
if (getThing().getStatusInfo().getStatusDetail() == ThingStatusDetail.BRIDGE_OFFLINE && bridgeValid) {
dispose();
initializationTask = scheduler.submit(this::initializeThing);
}
}
private void initializeThing() {
try {
initializeBatteryListener();
initializeEventListener();
// EventListener calls initializeStatus() before releasing so that ThingStatus should be set at this point
if (this.getThing().getStatus().equals(ThingStatus.INITIALIZING)) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
"Got no response by eventListener");
}
} catch (IOException | InterruptedException e) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
"Connection setup failed: {}" + e.getMessage());
}
}
private void initializeBatteryListener() throws IOException {
FlicButtonBatteryLevelListener batteryListener = new FlicButtonBatteryLevelListener(this);
BatteryStatusListener batteryConnection = new BatteryStatusListener(getBdaddr(), batteryListener);
bridgeHandler.getFlicClient().addBatteryStatusListener(batteryConnection);
this.batteryConnection = batteryConnection;
}
public void initializeEventListener() throws IOException, InterruptedException {
FlicButtonEventListener eventListener = new FlicButtonEventListener(this);
ButtonConnectionChannel eventConnection = new ButtonConnectionChannel(getBdaddr(), eventListener);
bridgeHandler.getFlicClient().addConnectionChannel(eventConnection);
this.eventConnection = eventConnection;
eventListener.getChannelResponseSemaphore().tryAcquire(5, TimeUnit.SECONDS);
}
@Override
public void dispose() {
cancelDelayedDisconnectTask();
cancelInitializationTask();
try {
if (eventConnection != null) {
bridgeHandler.getFlicClient().removeConnectionChannel(eventConnection);
}
if (batteryConnection != null) {
bridgeHandler.getFlicClient().removeBatteryStatusListener(this.batteryConnection);
}
} catch (IOException e) {
logger.warn("Button channel could not be properly removed", e);
}
super.dispose();
}
void initializeStatus(ConnectionStatus connectionStatus) {
if (connectionStatus == ConnectionStatus.Disconnected) {
setOffline();
} else {
setOnline();
}
}
void connectionStatusChanged(ConnectionStatus connectionStatus, @Nullable DisconnectReason disconnectReason) {
latestDisconnectReason = disconnectReason;
if (connectionStatus == ConnectionStatus.Disconnected) {
// Status change to offline have to be scheduled to improve stability,
// see https://github.com/pfink/openhab2-flicbutton/issues/2
scheduleStatusChangeToOffline();
} else {
setOnline();
}
}
private void scheduleStatusChangeToOffline() {
if (delayedDisconnectTask == null) {
delayedDisconnectTask = scheduler.schedule(this::setOffline, BUTTON_OFFLINE_GRACE_PERIOD_SECONDS,
TimeUnit.SECONDS);
}
}
protected void setOnline() {
updateStatus(ThingStatus.ONLINE, ThingStatusDetail.NONE);
}
protected void setOffline() {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.NONE,
"Disconnect Reason: " + Objects.toString(latestDisconnectReason));
}
// Cleanup delayedDisconnect on status change to online
@Override
protected void updateStatus(ThingStatus status, ThingStatusDetail statusDetail, @Nullable String description) {
if (status == ThingStatus.ONLINE) {
cancelDelayedDisconnectTask();
}
super.updateStatus(status, statusDetail, description);
}
private void cancelInitializationTask() {
if (initializationTask != null) {
initializationTask.cancel(true);
initializationTask = null;
}
}
private void cancelDelayedDisconnectTask() {
if (delayedDisconnectTask != null) {
delayedDisconnectTask.cancel(false);
delayedDisconnectTask = null;
}
}
void updateBatteryChannel(int percent) {
DecimalType batteryLevel = new DecimalType(percent);
updateState(CHANNEL_ID_BATTERY_LEVEL, batteryLevel);
}
void flicButtonRemoved() {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.GONE,
"Button was removed/detached from flicd (e.g. by simpleclient).");
}
void fireTriggerEvent(String event) {
String channelID = event.equals(CommonTriggerEvents.PRESSED) || event.equals(CommonTriggerEvents.RELEASED)
? CHANNEL_ID_RAWBUTTON_EVENTS
: CHANNEL_ID_BUTTON_EVENTS;
updateStatus(ThingStatus.ONLINE);
triggerChannel(channelID, event);
}
}

View File

@ -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;
}
}

View File

@ -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
}
}

View File

@ -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);
}
}

View File

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<binding:binding id="flicbutton" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:binding="https://openhab.org/schemas/binding/v1.0.0"
xsi:schemaLocation="https://openhab.org/schemas/binding/v1.0.0 https://openhab.org/schemas/binding-1.0.0.xsd">
<name>FlicButton Binding</name>
<description>This is the binding for Flic buttons by Shortcut Labs.</description>
</binding:binding>

View File

@ -0,0 +1,44 @@
<?xml version="1.0" encoding="UTF-8"?>
<thing:thing-descriptions bindingId="flicbutton"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:thing="https://openhab.org/schemas/thing-description/v1.0.0"
xsi:schemaLocation="https://openhab.org/schemas/thing-description/v1.0.0 https://openhab.org/schemas/thing-description-1.0.0.xsd">
<bridge-type id="flicd-bridge">
<label>FlicButton Bridge</label>
<description>This bridge represents a running instance of the fliclib-linux-hci server (flicd).</description>
<config-description>
<parameter name="hostname" type="text" required="false">
<context>network-address</context>
<label>Flic Daemon (flicd) Hostname</label>
<description>IP or Host name of the Flic daemon (flicd).</description>
<default>localhost</default>
</parameter>
<parameter name="port" type="integer" required="false">
<label>Flic Daemon (flicd) Port</label>
<description>Port where flicd is running. Defaults to 5551.</description>
<default>5551</default>
</parameter>
</config-description>
</bridge-type>
<thing-type id="button">
<supported-bridge-type-refs>
<bridge-type-ref id="flicd-bridge"/>
</supported-bridge-type-refs>
<label>Flic Button</label>
<description>The thing(-type) representing a Flic Button</description>
<channels>
<channel id="rawbutton" typeId="system.rawbutton"/>
<channel id="button" typeId="system.button"/>
<channel id="battery-level" typeId="system.battery-level"/>
</channels>
<config-description>
<parameter name="address" type="text" required="true">
<label>Address</label>
<description>Bluetooth address in XX:XX:XX:XX:XX:XX format</description>
</parameter>
</config-description>
</thing-type>
</thing:thing-descriptions>

View File

@ -125,6 +125,7 @@
<module>org.openhab.binding.exec</module>
<module>org.openhab.binding.feed</module>
<module>org.openhab.binding.feican</module>
<module>org.openhab.binding.flicbutton</module>
<module>org.openhab.binding.fmiweather</module>
<module>org.openhab.binding.folderwatcher</module>
<module>org.openhab.binding.folding</module>