[bondhome] Initial contribution (#13459)

* First commit on newly created branch, taking code from c8b8e210dfd23f98526763782eadbca49509baf9
* [bondhome] update snapshot version, and some typos
* [bondhome] Address (most) comments from prior review from #7260
* [bondhome] simplify channels

 * lastUpdate is unnecessary; turn on persistence or add a rule on update if
   you care to keep track of it
 * use a single string command channel for all shoot-and-forget commands, instead
   of multiple switch channels
 * use a rollershutter channel for shades (accepting UP, DOWN, STOP, 0%, and 100%)
 * on all dimmer channels, accept ON and OFF, as well as 0% to imply OFF, instead
   of having to write rules to control ON/OFF state separately.
 * if the dimmer channel exists, prune the corresponding power channel, since
   the dimmer channel is now a pure superset of its functionality
 * overload fan#speed to be ceiling fan or a fireplace's fan, depending on the
   device type
* [bondhome] add bundle to the BOM pom
* [bondhome] clean up BondDeviceHandler a bit

 * there's no need to delay initialization; ThingManager won't
   even attempt to initialize a child thing until its bridge is online
 * Remove some extra initialization checks that can never be false
 * slightly refactor some methods to return early, rather than
   nest a giant `else`
 * remove some info logging that will get triggered in normal usage
* [bondhome] fix bridge discovery

 * Bridge property and config serial number need to be the same name
 * Don't arbitrarily delay the BPUPListener
 * Automatically update the IP if the BPUPListener finds it
 * Provide the new bridge with its discovered IP to avoid an additional
   DNS query
 * Don't get the bridge version after every keep-alive response
* [bondhome] trigger end-device discovery as soon as the bridge comes online
* [bondhome] remove internal binding version
* [bondhome] change addr property to string

Certain values seen in the wild when interpreted as a long are too big for that
storage. Also, the Bond API documentation describes the addr property on a
device to be a string.

OpenHAB already has infrastructure to have things update their
channel definitions when a binding is updated.

* [bondhome] ignore any device that starts with _

In v3 of their API, Bond added a new special entry of __. Because no valid
device id would start with an underscore, ignore everything that starts with an
underscore to fix v3 and maybe futureproof.

* address review comments

mostly adding i18n to error states, and cleaning up error handling of
HTTP requests.

* use builtin translation services

instead of plumbing our own provider through

* use System.nanoTime instead of currentTimeMillis

so that it will be a monotonic clock, not (as) susceptible to the clock
changing

* [bondhome] ignore BPUP messasges that aren't state

In recent firmware, bond is now sending action messages via BPUP as well as state.
This change ignores all messages that aren't state.

* [bondhome] Improve error handling, and remove dummy constants

Just use a single BondException class to communicate any sort of
error from within bond, and avoid throwing, catching, and re-throwing
the same (or slightly modified) exception.

Also remove dummy constants that might give the wrong impression
of the details of your Bond device. Then implement proper null checks,
especially setting a configuration error if key config properties
aren't set on the thing.

* [bondhome] avoid setting device status when bridge just went offline

* address static analysis tool problems

Also-by: Sara Damiano <sdamiano@stroudcenter.org>
Also-by: Keith T. Garner <kgarner@kgarner.com>
Signed-off-by: Cody Cutrer <cody@cutrer.us>
This commit is contained in:
Cody Cutrer 2022-12-05 09:19:35 -07:00 committed by GitHub
parent dd8b7c8b65
commit 52b7b7981f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
36 changed files with 3550 additions and 0 deletions

View File

@ -44,6 +44,7 @@
/bundles/org.openhab.binding.bluetooth.govee/ @cpmeister /bundles/org.openhab.binding.bluetooth.govee/ @cpmeister
/bundles/org.openhab.binding.bluetooth.roaming/ @cpmeister /bundles/org.openhab.binding.bluetooth.roaming/ @cpmeister
/bundles/org.openhab.binding.bluetooth.ruuvitag/ @ssalonen /bundles/org.openhab.binding.bluetooth.ruuvitag/ @ssalonen
/bundles/org.openhab.binding.bondhome/ @ccutrer
/bundles/org.openhab.binding.boschindego/ @jofleck @jlaur /bundles/org.openhab.binding.boschindego/ @jofleck @jlaur
/bundles/org.openhab.binding.boschshc/ @stefan-kaestle @coeing @GerdZanker /bundles/org.openhab.binding.boschshc/ @stefan-kaestle @coeing @GerdZanker
/bundles/org.openhab.binding.bosesoundtouch/ @marvkis @tratho /bundles/org.openhab.binding.bosesoundtouch/ @marvkis @tratho

View File

@ -211,6 +211,11 @@
<artifactId>org.openhab.binding.bluetooth.ruuvitag</artifactId> <artifactId>org.openhab.binding.bluetooth.ruuvitag</artifactId>
<version>${project.version}</version> <version>${project.version}</version>
</dependency> </dependency>
<dependency>
<groupId>org.openhab.addons.bundles</groupId>
<artifactId>org.openhab.binding.bondhome</artifactId>
<version>${project.version}</version>
</dependency>
<dependency> <dependency>
<groupId>org.openhab.addons.bundles</groupId> <groupId>org.openhab.addons.bundles</groupId>
<artifactId>org.openhab.binding.boschindego</artifactId> <artifactId>org.openhab.binding.boschindego</artifactId>

View File

@ -0,0 +1,13 @@
This content is produced and maintained by the openHAB project.
* Project home: https://www.openhab.org
== Declared Project Licenses
This program and the accompanying materials are made available under the terms
of the Eclipse Public License 2.0 which is available at
https://www.eclipse.org/legal/epl-2.0/.
== Source Code
https://github.com/openhab/openhab-addons

View File

@ -0,0 +1,105 @@
# Bond Home Binding
This binding connects the [Bond Home](https://bondhome.io/) Bridge to openHAB using the [BOND V2 Local HTTP API](http://docs-local.appbond.com).
You'll need to acquire your [Local Token](http://docs-local.appbond.com/#section/Getting-Started/Getting-the-Bond-Token).
The easiest way is to open the Bond Home app on your mobile device, tap on your bridge device, open the Advanced Settings, and copy it from the Local Token entry.
## Supported Things
| Thing Type | Description |
|------------------|-------------------------------------------------------------------|
| bondBridge | The RF/IR/WiFi Bridge |
| bondFan | An RF or IR remote controlled ceiling fan with or without a light |
| bondFireplace | An RF or IR remote controlled fireplace with or without a fan |
| bondGenericThing | A generic RF or IR remote controlled device |
| bondShades | An RF or IR remote controlled motorized shade |
## Discovery
Once the bridge has been added, individual devices will be auto-discovered and added to the inbox.
## Thing Configuration
### bondBridge
| Parameter | Description | Required |
|--------------------|-----------------------------------------------------------------------|----------|
| bondId | The Bond ID of the bridge from the Bond Home app. | Yes |
| localToken | The authentication token for the local API. | Yes |
| bondIpAddress | The exact IP address to connect to the Bond Hub on the local network | No |
## Channels
Not all channels will be available for every device.
They are dependent on how the device is configured in the Bond Home app.
### `common` Group
| Channel | Type | Description |
|------------|----------|-----------------------------------------------------------------|
| power | Switch | Device Power |
| command | String | Send a command to the device |
Available commands:
| Command | Description |
|---------------------------|---------------------------------------------------|
| STOP | Stop any in-progress dimming operation |
| PRESET | Move a shade to a preset |
| DIM_START_STOP | Dim the fan light (cyclically) |
| DIM_INCREASE | Start increasing the brightness of the fan light |
| DIM_DECREASE | Start decreasing the brightness of the fan light |
| UP_LIGHT_DIM_START_STOP | Dim the fan light (cyclically) |
| UP_LIGHT_DIM_INCREASE | Start increasing the brightness of the up light |
| UP_LIGHT_DIM_DECREASE | Start decreasing the brightness of the up light |
| DOWN_LIGHT_DIM_START_STOP | Dim the fan light (cyclically) |
| DOWN_LIGHT_DIM_INCREASE | Start increasing the brightness of the down light |
| DOWN_LIGHT_DIM_DECREASE | Start decreasing the brightness of the down light |
### `fan` Group
| Channel | Type | Description |
|-------------------|----------|---------------------------------------------------|
| power | Switch | Fan power (only applicable to fireplace fans) |
| speed | Dimmer | Sets the fan speed. The 0-100% value will be scaled to however many speeds the fan actually has. Note that you cannot set the fan to speed 0 - you must turn `OFF` the power channel instead. |
| breezeState | Switch | Enables or disables breeze mode |
| breezeMean | Dimmer | Sets the average speed in breeze mode |
| breezeVariability | Dimmer | Sets the variability of the speed in breeze mode. |
| direction | String | Sets the fan direction - "Summer" or "Winter" |
| timer | Number | Sets an automatic off timer for s seconds (turning on the fan if necessary) |
### `light`, `upLight`, `downLight` Groups
| Channel | Type | Description |
|-----------------|--------|--------------------------------------------------------|
| power | Switch | Turns the light on or off |
| brightness | Dimmer | Adjusts the brightness of the light |
### `fireplace` Group
| Channel | Type | Description |
|----------|--------|----------------------------------------|
| flame | Dimmer | Adjust the flame level |
### `shade` Group
| Channel | Type | Description |
|---------------|---------------|--------------------------------------------------|
| rollershutter | Rollershutter | Only UP, DOWN, STOP, 0%, and 100% are supported. |
## Full Example
### `bond.things` File
```
bondhome:bondBridge:BD123456 "Bond Bridge" [ ipAddress="192.168.0.10", localToken="abc123", serialNumber="BD123456" ]
bondhome:bondFan:BD123456:0d11f00 "Living Room Fan" (bondhome:bondBridge:BD123456) [ deviceId="0d11f00" ]
```
### `bond.items` File
```
Switch GreatFan_Switch "Great Room Fan" { channel="bondhome:bondFan:BD123456:0d11f00:common#power" }
Dimmer GreatFan_Dimmer "Great Room Fan" { channel="bondhome:bondFan:BD123456:0d11f00:fan#speed" }
String GreatFan_Rotation "Great Room Fan Rotation" { channel="bondhome:bondFan:BD123456:0d11f00:fan#direction" }
Switch GreatFanLight_Switch "Great Room Fan Light" { channel="bondhome:bondFan:BD123456:0d11f00:light#power" }
```

View File

@ -0,0 +1,17 @@
<?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 https://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.4.0-SNAPSHOT</version>
</parent>
<artifactId>org.openhab.binding.bondhome</artifactId>
<name>openHAB Add-ons :: Bundles :: Bond Home Binding</name>
</project>

View File

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<features name="org.openhab.binding.bondhome-${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-bondhome" description="BondHome Binding" version="${project.version}">
<feature>openhab-runtime-base</feature>
<bundle start-level="80">mvn:org.openhab.addons.bundles/org.openhab.binding.bondhome/${project.version}</bundle>
</feature>
</features>

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.bondhome.internal;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* Thrown for various API issues.
*
* @author Cody Cutrer - Initial contribution
*/
@NonNullByDefault
public class BondException extends Exception {
private boolean wasBridgeSetOffline;
public BondException(String message) {
this(message, false);
}
public BondException(String message, boolean wasBridgeSetOffline) {
super(message);
this.wasBridgeSetOffline = wasBridgeSetOffline;
}
public boolean wasBridgeSetOffline() {
return wasBridgeSetOffline;
}
}

View File

@ -0,0 +1,114 @@
/**
* 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.bondhome.internal;
import java.util.Set;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.core.thing.ThingTypeUID;
/**
* The {@link BondHomeBindingConstants} class defines common constants, which are
* used across the whole binding.
*
* @author Sara Geleskie Damiano - Initial contribution
*/
@NonNullByDefault
public class BondHomeBindingConstants {
public static final String BINDING_ID = "bondhome";
/**
* List of all Thing Type UIDs.
*/
public static final ThingTypeUID THING_TYPE_BOND_BRIDGE = new ThingTypeUID(BINDING_ID, "bondBridge");
public static final ThingTypeUID THING_TYPE_BOND_FAN = new ThingTypeUID(BINDING_ID, "bondFan");
public static final ThingTypeUID THING_TYPE_BOND_SHADES = new ThingTypeUID(BINDING_ID, "bondShades");
public static final ThingTypeUID THING_TYPE_BOND_FIREPLACE = new ThingTypeUID(BINDING_ID, "bondFireplace");
public static final ThingTypeUID THING_TYPE_BOND_GENERIC = new ThingTypeUID(BINDING_ID, "bondGenericThing");
/**
* The supported thing types.
*/
public static final Set<ThingTypeUID> SUPPORTED_THING_TYPES = Set.of(THING_TYPE_BOND_FAN, THING_TYPE_BOND_SHADES,
THING_TYPE_BOND_FIREPLACE, THING_TYPE_BOND_GENERIC);
public static final Set<ThingTypeUID> SUPPORTED_BRIDGE_TYPES = Set.of(THING_TYPE_BOND_BRIDGE);
/**
* List of all Channel ids - these match the id fields in the OH-INF xml files
*/
// Universal channels
public static final String CHANNEL_GROUP_COMMON = "common";
public static final String CHANNEL_POWER = CHANNEL_GROUP_COMMON + "#power";
public static final String CHANNEL_COMMAND = CHANNEL_GROUP_COMMON + "command";
// Ceiling fan channels
public static final String CHANNEL_GROUP_FAN = "fan";
public static final String CHANNEL_FAN_POWER = CHANNEL_GROUP_FAN + "#power";
public static final String CHANNEL_FAN_SPEED = CHANNEL_GROUP_FAN + "#speed";
public static final String CHANNEL_FAN_BREEZE_STATE = CHANNEL_GROUP_FAN + "#breezeState";
public static final String CHANNEL_FAN_BREEZE_MEAN = CHANNEL_GROUP_FAN + "#breezeMean";
public static final String CHANNEL_FAN_BREEZE_VAR = CHANNEL_GROUP_FAN + "#breezeVariability";
public static final String CHANNEL_FAN_DIRECTION = CHANNEL_GROUP_FAN + "#direction";
public static final String CHANNEL_FAN_TIMER = CHANNEL_GROUP_FAN + "#timer";
// Fan light channels
public static final String CHANNEL_GROUP_LIGHT = "light";
public static final String CHANNEL_LIGHT_POWER = CHANNEL_GROUP_LIGHT + "#power";
public static final String CHANNEL_LIGHT_BRIGHTNESS = CHANNEL_GROUP_LIGHT + "#brightness";
public static final String CHANNEL_GROUP_UP_LIGHT = "upLight";
public static final String CHANNEL_UP_LIGHT_POWER = CHANNEL_GROUP_UP_LIGHT + "#power";
public static final String CHANNEL_UP_LIGHT_ENABLE = CHANNEL_GROUP_UP_LIGHT + "#enable";
public static final String CHANNEL_UP_LIGHT_BRIGHTNESS = CHANNEL_GROUP_UP_LIGHT + "#brightness";
public static final String CHANNEL_GROUP_DOWN_LIGHT = "downLight";
public static final String CHANNEL_DOWN_LIGHT_POWER = CHANNEL_GROUP_DOWN_LIGHT + "#power";
public static final String CHANNEL_DOWN_LIGHT_ENABLE = CHANNEL_GROUP_DOWN_LIGHT + "#enable";
public static final String CHANNEL_DOWN_LIGHT_BRIGHTNESS = CHANNEL_GROUP_DOWN_LIGHT + "#brightness";
// Fireplace channels
public static final String CHANNEL_GROUP_FIREPLACE = "fireplace";
public static final String CHANNEL_FLAME = CHANNEL_GROUP_FIREPLACE + "#flame";
// Motorize shade channels
public static final String CHANNEL_GROUP_SHADES = "shade";
public static final String CHANNEL_ROLLERSHUTTER = CHANNEL_GROUP_SHADES + "#rollershutter";
/**
* Configuration arguments
*/
public static final String CONFIG_SERIAL_NUMBER = "serialNumber";
public static final String CONFIG_IP_ADDRESS = "ipAddress";
public static final String CONFIG_LOCAL_TOKEN = "localToken";
public static final String CONFIG_DEVICE_ID = "deviceId";
public static final String CONFIG_LATEST_HASH = "lastDeviceConfigurationHash";
/**
* Device Properties
*/
public static final String PROPERTIES_DEVICE_NAME = "deviceName";
public static final String PROPERTIES_TEMPLATE_NAME = "template";
public static final String PROPERTIES_MAX_SPEED = "maxSpeed";
public static final String PROPERTIES_TRUST_STATE = "trustState";
public static final String PROPERTIES_ADDRESS = "addr";
public static final String PROPERTIES_RF_FREQUENCY = "freq";
/**
* Constants
*/
public static final int BOND_BPUP_PORT = 30007;
public static final int BOND_API_TIMEOUT_MS = 3000;
}

View File

@ -0,0 +1,85 @@
/**
* 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.bondhome.internal;
import static org.openhab.binding.bondhome.internal.BondHomeBindingConstants.*;
import java.util.HashMap;
import java.util.Map;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.bondhome.internal.handler.BondBridgeHandler;
import org.openhab.binding.bondhome.internal.handler.BondDeviceHandler;
import org.openhab.core.io.net.http.HttpClientFactory;
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.ComponentContext;
import org.osgi.service.component.annotations.Activate;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Reference;
/**
* The {@link BondHomeHandlerFactory} is responsible for creating things and thing
* handlers.
*
* @author Sara Geleskie Damiano - Initial contribution
*/
@NonNullByDefault
@Component(configurationPid = "binding.bondhome", service = ThingHandlerFactory.class)
public class BondHomeHandlerFactory extends BaseThingHandlerFactory {
private Map<ThingUID, ServiceRegistration<?>> discoveryServiceRegs = new HashMap<>();
private final HttpClientFactory httpClientFactory;
@Activate
public BondHomeHandlerFactory(final @Reference HttpClientFactory httpClientFactory,
ComponentContext componentContext) {
super.activate(componentContext);
this.httpClientFactory = httpClientFactory;
}
@Override
public boolean supportsThingType(ThingTypeUID thingTypeUID) {
return SUPPORTED_BRIDGE_TYPES.contains(thingTypeUID) || SUPPORTED_THING_TYPES.contains(thingTypeUID);
}
@Override
protected @Nullable ThingHandler createHandler(Thing thing) {
ThingTypeUID thingTypeUID = thing.getThingTypeUID();
if (THING_TYPE_BOND_BRIDGE.equals(thingTypeUID)) {
final BondBridgeHandler handler = new BondBridgeHandler((Bridge) thing, httpClientFactory);
return handler;
} else if (SUPPORTED_THING_TYPES.contains(thingTypeUID)) {
return new BondDeviceHandler(thing);
}
return null;
}
@Override
protected synchronized void removeHandler(ThingHandler thingHandler) {
if (thingHandler instanceof BondBridgeHandler) {
ServiceRegistration<?> serviceReg = this.discoveryServiceRegs.remove(thingHandler.getThing().getUID());
if (serviceReg != null) {
serviceReg.unregister();
}
}
}
}

View File

@ -0,0 +1,282 @@
/**
* 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.bondhome.internal.api;
import static java.nio.charset.StandardCharsets.*;
import static org.openhab.binding.bondhome.internal.BondHomeBindingConstants.*;
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
import java.net.SocketException;
import java.net.SocketTimeoutException;
import java.util.concurrent.Executor;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.bondhome.internal.handler.BondBridgeHandler;
import org.openhab.core.thing.ThingStatusDetail;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.JsonParseException;
/**
* This Thread is responsible maintaining the Bond Push UDP Protocol
*
* @author Sara Geleskie Damiano - Initial contribution
*
*/
@NonNullByDefault
public class BPUPListener implements Runnable {
private static final int SOCKET_TIMEOUT_MILLISECONDS = 3000;
private static final int SOCKET_RETRY_TIMEOUT_MILLISECONDS = 3000;
private final Logger logger = LoggerFactory.getLogger(BPUPListener.class);
// To parse the JSON responses
private final Gson gsonBuilder;
// Used for callbacks to handler
private final BondBridgeHandler bridgeHandler;
// UDP socket used to receive status events
private @Nullable DatagramSocket socket;
public @Nullable String lastRequestId;
private long timeOfLastKeepAlivePacket;
private boolean shutdown;
private int numberOfKeepAliveTimeouts;
/**
* Constructor of the receiver runnable thread.
*
* @param address The address of the Bond Bridge
* @throws SocketException is some problem occurs opening the socket.
*/
public BPUPListener(BondBridgeHandler bridgeHandler) {
logger.debug("Starting BPUP Listener...");
this.bridgeHandler = bridgeHandler;
this.timeOfLastKeepAlivePacket = -1;
this.numberOfKeepAliveTimeouts = 0;
GsonBuilder gsonBuilder = new GsonBuilder();
gsonBuilder.excludeFieldsWithoutExposeAnnotation();
Gson gson = gsonBuilder.create();
this.gsonBuilder = gson;
this.shutdown = true;
}
public boolean isRunning() {
return !shutdown;
}
public void start(Executor executor) {
shutdown = false;
executor.execute(this);
}
/**
* Send keep-alive as necessary and listen for push messages
*/
@Override
public void run() {
receivePackets();
}
/**
* Gracefully shutdown thread. Worst case takes TIMEOUT_TO_DATAGRAM_RECEPTION to
* shutdown.
*/
public void shutdown() {
this.shutdown = true;
DatagramSocket s = this.socket;
if (s != null) {
s.close();
logger.debug("Listener closed socket");
this.socket = null;
}
}
private void sendBPUPKeepAlive() {
// Create a buffer and packet for the response
byte[] buffer = new byte[256];
DatagramPacket inPacket = new DatagramPacket(buffer, buffer.length);
DatagramSocket sock = this.socket;
if (sock != null) {
logger.trace("Sending keep-alive request ('\\n')");
try {
byte[] outBuffer = { (byte) '\n' };
InetAddress inetAddress = InetAddress.getByName(bridgeHandler.getBridgeIpAddress());
DatagramPacket outPacket = new DatagramPacket(outBuffer, 1, inetAddress, BOND_BPUP_PORT);
sock.send(outPacket);
sock.receive(inPacket);
BPUPUpdate response = transformUpdatePacket(inPacket);
if (response != null) {
if (!response.bondId.equalsIgnoreCase(bridgeHandler.getBridgeId())) {
logger.warn("Response isn't from expected Bridge! Expected: {} Got: {}",
bridgeHandler.getBridgeId(), response.bondId);
} else {
bridgeHandler.setBridgeOnline(inPacket.getAddress().getHostAddress());
numberOfKeepAliveTimeouts = 0;
}
}
} catch (SocketTimeoutException e) {
numberOfKeepAliveTimeouts++;
logger.trace("BPUP Socket timeout, number of timeouts: {}", numberOfKeepAliveTimeouts);
if (numberOfKeepAliveTimeouts > 10) {
bridgeHandler.setBridgeOffline(ThingStatusDetail.COMMUNICATION_ERROR,
"@text/offline.comm-error.timeout");
}
} catch (IOException e) {
logger.debug("One exception has occurred", e);
}
}
}
private void receivePackets() {
try {
DatagramSocket s = new DatagramSocket(null);
s.setSoTimeout(SOCKET_TIMEOUT_MILLISECONDS);
s.bind(null);
socket = s;
logger.debug("Listener created UDP socket on port {} with timeout {}", s.getPort(),
SOCKET_TIMEOUT_MILLISECONDS);
} catch (SocketException e) {
logger.debug("Listener got SocketException", e);
datagramSocketHealthRoutine();
}
// Create a buffer and packet for the response
byte[] buffer = new byte[256];
DatagramPacket inPacket = new DatagramPacket(buffer, buffer.length);
DatagramSocket sock = this.socket;
while (sock != null && !this.shutdown) {
// Check if we're due to send something to keep the connection
long now = System.nanoTime() / 1000000L;
long timePassedFromLastKeepAlive = now - timeOfLastKeepAlivePacket;
if (timeOfLastKeepAlivePacket == -1 || timePassedFromLastKeepAlive >= 60000L) {
sendBPUPKeepAlive();
timeOfLastKeepAlivePacket = now;
}
try {
sock.receive(inPacket);
processPacket(inPacket);
} catch (SocketTimeoutException e) {
// Ignore. Means there was no updates while we waited.
// We'll just loop around and try again after sending a keep alive.
} catch (IOException e) {
logger.debug("Listener got IOException waiting for datagram: {}", e.getMessage());
datagramSocketHealthRoutine();
}
}
logger.debug("Listener exiting");
}
private void processPacket(DatagramPacket packet) {
logger.trace("Got datagram of length {} from {}", packet.getLength(), packet.getAddress().getHostAddress());
BPUPUpdate update = transformUpdatePacket(packet);
if (update != null) {
if (!update.bondId.equalsIgnoreCase(bridgeHandler.getBridgeId())) {
logger.warn("Response isn't from expected Bridge! Expected: {} Got: {}", bridgeHandler.getBridgeId(),
update.bondId);
}
// Check for duplicate packet
if (isDuplicate(update)) {
logger.trace("Dropping duplicate packet");
return;
}
// Send the update the the bridge for it to pass on to the devices
if (update.topic != null) {
logger.trace("Forwarding message to bridge handler");
bridgeHandler.forwardUpdateToThing(update);
} else {
logger.debug("No topic in incoming message!");
}
}
}
/**
* Method that transforms {@link DatagramPacket} to a {@link BPUPUpdate} Object
*
* @param packet the {@link DatagramPacket}
* @return the {@link BPUPUpdate}
*/
public @Nullable BPUPUpdate transformUpdatePacket(final DatagramPacket packet) {
String responseJson = new String(packet.getData(), 0, packet.getLength(), UTF_8);
logger.debug("Message from {}:{} -> {}", packet.getAddress().getHostAddress(), packet.getPort(), responseJson);
@Nullable
BPUPUpdate response = null;
try {
response = this.gsonBuilder.fromJson(responseJson, BPUPUpdate.class);
} catch (JsonParseException e) {
logger.warn("Error parsing json! {}", e.getMessage());
}
return response;
}
private boolean isDuplicate(BPUPUpdate update) {
boolean packetIsDuplicate = false;
String newReqestId = update.requestId;
String lastRequestId = this.lastRequestId;
if (lastRequestId != null && newReqestId != null) {
if (lastRequestId.equalsIgnoreCase(newReqestId)) {
packetIsDuplicate = true;
}
}
// Remember this packet for duplicate check
lastRequestId = newReqestId;
return packetIsDuplicate;
}
private void datagramSocketHealthRoutine() {
@Nullable
DatagramSocket datagramSocket = this.socket;
if (datagramSocket == null || (datagramSocket.isClosed() || !datagramSocket.isConnected())) {
logger.trace("Datagram Socket is disconnected or has been closed, reconnecting...");
try {
// close the socket before trying to reopen
if (datagramSocket != null) {
datagramSocket.close();
}
logger.trace("Old socket closed.");
try {
Thread.sleep(SOCKET_RETRY_TIMEOUT_MILLISECONDS);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
DatagramSocket s = new DatagramSocket(null);
s.setSoTimeout(SOCKET_TIMEOUT_MILLISECONDS);
s.bind(null);
this.socket = s;
logger.trace("Datagram Socket reconnected using port {}.", s.getPort());
} catch (SocketException exception) {
logger.warn("Problem creating new socket : {}", exception.getLocalizedMessage());
}
}
}
}

View File

@ -0,0 +1,63 @@
/**
* 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.bondhome.internal.api;
import static org.openhab.binding.bondhome.internal.BondHomeBindingConstants.*;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import com.google.gson.annotations.Expose;
import com.google.gson.annotations.SerializedName;
/**
* This POJO represents update datagram sent by the Bond Push UDP Protocol
*
* The incoming JSON looks like this:
*
* {"B": "ZZBL12345", "t": "devices/aabbccdd/state", "i": "00112233bbeeeeff", "s" :200, "m": 0, "f": 255, "b": {"_":
* "ab9284ef", "power": 1, "speed": 2}}
*
* @author Sara Geleskie Damiano - Initial contribution
*/
@NonNullByDefault
public class BPUPUpdate {
// The Bond ID
@SerializedName("B")
@Expose(serialize = true, deserialize = true)
public @Nullable String bondId;
// The topic (the path from HTTP URL)
@SerializedName("t")
@Expose(serialize = true, deserialize = true)
public @Nullable String topic;
// The request ID
@SerializedName("i")
@Expose(serialize = true, deserialize = true)
public @Nullable String requestId;
// The HTTP status code
@SerializedName("s")
@Expose(serialize = true, deserialize = true)
public int statusCode;
// HTTP method (0=GET, 1=POST, 2=PUT, 3=DELETE, 4=PATCH)
@SerializedName("m")
@Expose(serialize = true, deserialize = true)
public int method;
// flags (Olibra-internal use)
@SerializedName("f")
@Expose(serialize = true, deserialize = true)
public int flag;
// HTTP response body
@SerializedName("b")
@Expose(serialize = true, deserialize = true)
public @Nullable BondDeviceState deviceState;
}

View File

@ -0,0 +1,64 @@
/**
* 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.bondhome.internal.api;
import static org.openhab.binding.bondhome.internal.BondHomeBindingConstants.*;
import java.util.Arrays;
import java.util.List;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import com.google.gson.annotations.Expose;
import com.google.gson.annotations.SerializedName;
/**
* This POJO represents a bond device
*
* The incoming JSON looks like this:
*
* {"name": "My Fan", "type": "CF", "template": "A1", "location": "Kitchen",
* "actions": {"_": "7fc1e84b"}, "properties": {"_": "84cd8a43"}, "state": {"_":
* "ad9bcde4"}, "commands": {"_": "ad9bcde4" }}
*
* @author Sara Geleskie Damiano - Initial contribution
*/
@NonNullByDefault
public class BondDevice {
// The current device hash
@SerializedName("_")
@Expose(serialize = false, deserialize = true)
public @Nullable String hash;
// The name associated with the device in the bond app
@Expose(serialize = true, deserialize = true)
public @Nullable String name;
// The device type
@Expose(serialize = true, deserialize = true)
public BondDeviceType type = BondDeviceType.GENERIC_DEVICE;
// The remote control template being used
@Expose(serialize = true, deserialize = true)
public @Nullable String template;
// A list of the available actions
@Expose(serialize = false, deserialize = true)
public List<BondDeviceAction> actions = Arrays.asList(BondDeviceAction.TURN_ON);
// The current hash of the properties object
@Expose(serialize = false, deserialize = true)
public @Nullable BondHash properties;
// The current hash of the state object
@Expose(serialize = false, deserialize = true)
public @Nullable BondHash state;
// The current hash of the commands object - only applies to a bridge
@Expose(serialize = false, deserialize = true)
public @Nullable BondHash commands;
}

View File

@ -0,0 +1,317 @@
/**
* 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.bondhome.internal.api;
import static org.openhab.binding.bondhome.internal.BondHomeBindingConstants.*;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import com.google.gson.annotations.SerializedName;
/**
* This enum represents the possible device actions
*
* @author Sara Geleskie Damiano - Initial contribution
*
*/
@NonNullByDefault
public enum BondDeviceAction {
// State Variables
// power: (integer) 1 = on, 0 = off
// Actions
@SerializedName("TurnOn")
TURN_ON("TurnOn", CHANNEL_GROUP_COMMON, CHANNEL_POWER),
// ^^ Turn device power on.
@SerializedName("TurnOff")
TURN_OFF("TurnOff", CHANNEL_GROUP_COMMON, CHANNEL_POWER),
// ^^ Turn device power off.
@SerializedName("TogglePower")
TOGGLE_POWER("TogglePower", CHANNEL_GROUP_COMMON, CHANNEL_POWER),
// ^^ Change device power from on to off, or off to on.
// State Variables
// timer: (integer) seconds remaining on timer, or 0 meaning no timer running
// Actions
@SerializedName("SetTimer")
SET_TIMER("SetTimer", CHANNEL_GROUP_COMMON, CHANNEL_FAN_TIMER),
// ^^ Start timer for s seconds. If power if off, device is implicitly turned
// on. If argument is zero, the timer is
// canceled without turning off the device.
// Properties
// max_speed: (integer) highest speed available
// State Variables
// speed: (integer) value from 1 to max_speed. If power=0, speed represents the
// last speed setting and the speed to
// which the device resumes when user asks to turn on.
// Actions
@SerializedName("SetSpeed")
SET_SPEED("SetSpeed", CHANNEL_GROUP_FAN, CHANNEL_FAN_SPEED),
// ^^ Set speed and turn on. If speed>max_speed, max_speed is assumed. If the
// fan is off, implicitly turn on the
// power. Setting speed to zero or a negative value is ignored.
@SerializedName("IncreaseSpeed")
INCREASE_SPEED("IncreaseSpeed", CHANNEL_GROUP_FAN, CHANNEL_FAN_SPEED),
// ^^ Increase speed of fan by specified number of speeds. If the fan is off,
// implicitly turn on the power.
@SerializedName("DecreaseSpeed")
DECREASE_SPEED("DecreaseSpeed", CHANNEL_GROUP_FAN, CHANNEL_FAN_SPEED),
// ^^ Decrease fan speed by specified number of speeds. If attempting to
// decrease fan speed below 1, the fan will
// remain at speed 1. That is, power will not be implicitly turned off. If the
// power is already off, DecreaseSpeed
// is ignored.
// State Variables
// breeze: (array) array of the form [ <mode>, <mean>, <var> ]:
// mode: (integer) 0 = breeze mode disabled, 1 = breeze mode enabled
// mean: (integer) sets the average speed. 0 = minimum average speed (calm), 100
// = maximum average speed (storm)
// var: (integer) sets the variability of the speed. 0 = minimum variation
// (steady), 100 = maximum variation (gusty)
// Actions
@SerializedName("BreezeOn")
BREEZE_ON("BreezeOn", CHANNEL_GROUP_FAN, CHANNEL_FAN_BREEZE_STATE),
// ^^ Enable breeze with remembered parameters. Defaults to [50,50].
@SerializedName("BreezeOff")
BREEZE_OFF("BreezeOff", CHANNEL_GROUP_FAN, CHANNEL_FAN_BREEZE_STATE),
// ^^ Stop breeze. Fan remains on at current speed.
@SerializedName("SetBreeze")
SET_BREEZE("SetBreeze", CHANNEL_GROUP_FAN, CHANNEL_FAN_BREEZE_MEAN),
// ^^ Enable breeze with specified parameters (same as breeze state variable).
// Example SetBreeze([1, 20, 90]).
// State Variables
// direction: (integer) 1 = forward, -1 = reverse.
// The forward and reverse modes are sometimes called Summer and Winter,
// respectively.
// Actions
@SerializedName("SetDirection")
SET_DIRECTION("SetDirection", CHANNEL_GROUP_FAN, CHANNEL_FAN_DIRECTION),
// ^^ Control forward and reverse.
@SerializedName("ToggleDirection")
TOGGLE_DIRECTION("ToggleDirection", CHANNEL_GROUP_FAN, CHANNEL_FAN_DIRECTION),
// ^^ Reverse the direction of the fan.
// State Variables
// light: (integer) 1 = light on, 0 = light off
// Actions
@SerializedName("TurnLightOn")
TURN_LIGHT_ON("TurnLightOn", CHANNEL_GROUP_LIGHT, CHANNEL_LIGHT_POWER),
// ^^ Turn light on.
@SerializedName("TurnLightOff")
TURN_LIGHT_OFF("TurnLightOff", CHANNEL_GROUP_LIGHT, CHANNEL_LIGHT_POWER),
// ^^ Turn off light.
@SerializedName("ToggleLight")
TOGGLE_LIGHT("ToggleLight", CHANNEL_GROUP_LIGHT, CHANNEL_LIGHT_POWER),
// ^^ Change light from on to off, or off to on.
// State Variables
// up_light: (integer) 1 = up light enabled, 0 = up light disabled
// down_light: (integer) 1 = down light enabled, 0 = down light disabled
// If both up_light and light are 1, then the up light will be on, and similar
// for down light.
// Note that both up_light and down_light may not be simultaneously zero, so
// that the device is always ready to
// respond to a TurnLightOn request.
// Actions
@SerializedName("TurnUpLightOn")
TURN_UP_LIGHT_ON("TurnUpLightOn", CHANNEL_GROUP_UP_LIGHT, CHANNEL_UP_LIGHT_ENABLE),
// ^^ Turn up light on.
@SerializedName("TurnDownLightOn")
TURN_DOWN_LIGHT_ON("TurnDownLightOn", CHANNEL_GROUP_DOWN_LIGHT, CHANNEL_DOWN_LIGHT_ENABLE),
// ^^ Turn down light on.
@SerializedName("TurnUpLightOff")
TURN_UP_LIGHT_OFF("TurnUpLightOff", CHANNEL_GROUP_UP_LIGHT, CHANNEL_UP_LIGHT_POWER),
// ^^ Turn off up light.
@SerializedName("TurnDownLightOff")
TURN_DOWN_LIGHT_OFF("TurnDownLightOff", CHANNEL_GROUP_DOWN_LIGHT, CHANNEL_DOWN_LIGHT_POWER),
// ^^ Turn off down light.
@SerializedName("ToggleUpLight")
TOGGLE_UP_LIGHT("ToggleUpLight", CHANNEL_GROUP_UP_LIGHT, CHANNEL_UP_LIGHT_POWER),
// ^^ Change up light from on to off, or off to on.
@SerializedName("ToggleDownLight")
TOGGLE_DOWN_LIGHT("ToggleDownLight", CHANNEL_GROUP_DOWN_LIGHT, CHANNEL_DOWN_LIGHT_POWER),
// ^^ Change down light from on to off, or off to on.
// State Variables
// brightness: (integer) percentage value of brightness, 1-100. If light=0,
// brightness represents the last
// brightness setting and the brightness to resume when user turns on light. If
// fan has no dimmer or a non-stateful
// dimmer, brightness is always 100.
// Actions
@SerializedName("SetBrightness")
SET_BRIGHTNESS("SetBrightness", CHANNEL_GROUP_LIGHT, CHANNEL_LIGHT_BRIGHTNESS),
// ^^ Set the brightness of the light to specified percentage. Value of 0 is
// ignored, use TurnLightOff instead.
@SerializedName("IncreaseBrightness")
INCREASE_BRIGHTNESS("IncreaseBrightness", CHANNEL_GROUP_LIGHT, CHANNEL_LIGHT_BRIGHTNESS),
// will be turned on at (0 + amount).
DECREASE_BRIGHTNESS("DecreaseBrightness", CHANNEL_GROUP_LIGHT, CHANNEL_LIGHT_BRIGHTNESS),
// ^^ Decrease light brightness by specified percentage. If attempting to
// decrease brightness below 1%, light will
// remain at 1%. Use TurnLightOff to turn off the light. If the light is off,
// the light will remain off but the
// remembered brightness will be decreased.
// State Variables
// up_light_brightness: (integer) percentage value of up light brightness,
// 1-100.
// down_light_brightness: (integer) percentage value of down light brightness,
// 1-100.
// Actions
@SerializedName("SetUpLightBrightness")
SET_UP_LIGHT_BRIGHTNESS("SetUpLightBrightness", CHANNEL_GROUP_UP_LIGHT, CHANNEL_UP_LIGHT_BRIGHTNESS),
// ^^ Similar to SetBrightness but only for the up light.
@SerializedName("SetDownLightBrightness")
SET_DOWN_LIGHT_BRIGHTNESS("SetDownLightBrightness", CHANNEL_GROUP_DOWN_LIGHT, CHANNEL_DOWN_LIGHT_BRIGHTNESS),
// ^^ Similar to SetBrightness but only for the down light.
@SerializedName("IncreaseUpLightBrightness")
INCREASE_UP_LIGHT_BRIGHTNESS("IncreaseUpLightBrightness", CHANNEL_GROUP_UP_LIGHT, CHANNEL_UP_LIGHT_BRIGHTNESS),
// ^^ Similar to IncreaseBrightness but only for the up light.
@SerializedName("InreaseDownLightBrightness")
INCREASE_DOWN_LIGHT_BRIGHTNESS("IncreaseDownLightBrightness", CHANNEL_GROUP_DOWN_LIGHT,
CHANNEL_DOWN_LIGHT_BRIGHTNESS),
// ^^ Similar to IncreaseBrightness but only for the down light.
@SerializedName("DecreaseUpLightBrightness")
DECREASE_UP_LIGHT_BRIGHTNESS("DecreaseUpLightBrightness", CHANNEL_GROUP_UP_LIGHT, CHANNEL_UP_LIGHT_BRIGHTNESS),
// ^^ Similar to DecreaseBrightness but only for the up light.
@SerializedName("DecreaseDownLightBrightness")
DECREASE_DOWN_LIGHT_BRIGHTNESS("DecreaseDownLightBrightness", CHANNEL_GROUP_DOWN_LIGHT,
CHANNEL_DOWN_LIGHT_BRIGHTNESS),
// ^^ Similar to DecreaseBrightness but only for the down light.
// State Variables
// flame: (integer) value from 1 to 100. If power=0, flame represents the last
// flame setting and the flame to which
// the device resumes when user asks to turn on.
// Actions
@SerializedName("SetFlame")
SET_FLAME("SetFlame", CHANNEL_GROUP_FIREPLACE, CHANNEL_FLAME),
// ^^ Set flame and turn on. If flame>100, 100 is assumed. If the fireplace is
// off, implicitly turn on the power.
// Setting flame to zero or a negative value is ignored.
@SerializedName("IncreaseFlame")
INCREASE_FLAME("IncreaseFlame", CHANNEL_GROUP_FIREPLACE, CHANNEL_FLAME),
// ^^ Increase flame level of fireplace by specified number of flames. If the
// fireplace is off, implicitly turn on
// the power.
@SerializedName("DecreaseFlame")
DECREASE_FLAME("DecreaseFlame", CHANNEL_GROUP_FIREPLACE, CHANNEL_FLAME),
// ^^ Decrease flame level by specified number of flames. If attempting to
// decrease fireplace flame below 1, the
// fireplace will remain at flame 1. That is, power will not be implicitly
// turned off. If the power is already off,
// DecreaseFlame is ignored.
// State Variables
// fpfan_power: (integer) 1 = on, 0 = off
// fpfan_speed: (integer) from 1-100
// Actions
@SerializedName("TurnFpFanOff")
TURN_FP_FAN_OFF("TurnFpFanOff", CHANNEL_GROUP_FAN, CHANNEL_FAN_SPEED),
// ^^ Turn the fireplace fan off
@SerializedName("TurnFpFanOn")
TURN_FP_FAN_ON("TurnFpFanOn", CHANNEL_GROUP_FAN, CHANNEL_FAN_POWER),
// ^^ Turn the fireplace fan on, restoring the previous speed
@SerializedName("SetFpFan")
SET_FP_FAN("SetFpFan", CHANNEL_GROUP_FAN, CHANNEL_FAN_SPEED),
// ^^ Sets the speed of the fireplace fan
// State Variables
// open: (integer) 1 = open, 0 = closed
// Actions
@SerializedName("Open")
OPEN("Open", CHANNEL_GROUP_SHADES, CHANNEL_ROLLERSHUTTER),
// ^^ Open the device.
@SerializedName("Close")
CLOSE("Close", CHANNEL_GROUP_SHADES, CHANNEL_ROLLERSHUTTER),
// ^^ Close the device.
@SerializedName("ToggleOpen")
TOGGLE_OPEN("ToggleOpen", CHANNEL_GROUP_SHADES, CHANNEL_ROLLERSHUTTER),
// ^^ Close the device if it's open, open it if it's closed
@SerializedName("Preset")
PRESET("Preset", CHANNEL_GROUP_COMMON, CHANNEL_COMMAND),
// ^^ Sets a shade to a preset level
// Other actions
@SerializedName("Stop")
STOP("Stop", CHANNEL_GROUP_COMMON, CHANNEL_COMMAND),
// ^^ This action tells the Bond to stop any in-progress transmission and empty
// its transmission queue.
@SerializedName("Hold")
HOLD("Hold", CHANNEL_GROUP_SHADES, CHANNEL_COMMAND),
// ^^ Can be used when a signal is required to tell a device to stop moving or
// the like, since Stop is a special
// "stop transmitting" action
@SerializedName("Pair")
PAIR("Pair", CHANNEL_GROUP_COMMON, null),
// ^^ Used in devices that need to be paired with a receiver.
@SerializedName("StartDimmer")
START_DIMMER("StartDimmer", CHANNEL_GROUP_COMMON, CHANNEL_COMMAND),
// ^^ Start dimming. The Bond should time out its transmission after 30 seconds,
// or when the Stop action is called.
@SerializedName("StartUpLightDimmer")
START_UP_LIGHT_DIMMER("StartUpLightDimmer", CHANNEL_GROUP_COMMON, CHANNEL_COMMAND),
// ^^ Use this and the StartDownLightDimmer instead of StartDimmer if your
// device has two dimmable lights.
@SerializedName("StartDownLightDimmer")
START_DOWN_LIGHT_DIMMER("StartDownLightDimmer", CHANNEL_GROUP_COMMON, CHANNEL_COMMAND),
// ^^ The counterpart to StartUpLightDimmer
@SerializedName("StartIncreasingBrightness")
START_INCREASING_BRIGHTNESS("StartIncreasingBrightness", CHANNEL_GROUP_COMMON, CHANNEL_COMMAND),
@SerializedName("StartDecreasingBrightness")
START_DECREASING_BRIGHTNESS("StartDecreasingBrightness", CHANNEL_GROUP_COMMON, CHANNEL_COMMAND),
// More actions
@SerializedName("OEMRandom")
OEM_RANDOM("OEMRandom", CHANNEL_GROUP_COMMON, null),
@SerializedName("OEMTimer")
OEM_TIMER("OEMTimer", CHANNEL_GROUP_COMMON, null),
@SerializedName("Unknown")
UNKNOWN("Unknown", CHANNEL_GROUP_COMMON, null);
private String actionId;
private String channelGroupTypeId;
private @Nullable String channelTypeId;
private BondDeviceAction(final String actionId, String channelGroupTypeId, @Nullable String channelTypeId) {
this.actionId = actionId;
this.channelGroupTypeId = channelGroupTypeId;
this.channelTypeId = channelTypeId;
}
/**
* @return the actionId
*/
public String getActionId() {
return actionId;
}
/**
* @return the channelGroupTypeId
*/
public String getChannelGroupTypeId() {
return channelGroupTypeId;
}
/**
* @return the channelTypeId
*/
public @Nullable String getChannelTypeId() {
return channelTypeId;
}
}

View File

@ -0,0 +1,60 @@
/**
* 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.bondhome.internal.api;
import static org.openhab.binding.bondhome.internal.BondHomeBindingConstants.*;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import com.google.gson.annotations.Expose;
import com.google.gson.annotations.SerializedName;
/**
* This POJO represents the properties of a Bond device
*
* The incoming JSON looks like this:
*
* {"max_speed": 3, "trust_state": false, "addr": "10101", "freq": 434300, "bps": 3000, "zero_gap": 30}
*
* @author Sara Geleskie Damiano - Initial contribution
*/
@NonNullByDefault
public class BondDeviceProperties {
// The current properties hash
@SerializedName("_")
@Expose(serialize = false, deserialize = true)
public @Nullable String hash;
// The maximum speed of a fan
@SerializedName("max_speed")
@Expose(serialize = true, deserialize = true)
public int maxSpeed;
// Whether or not to "trust" that the device state remembered by the bond bridge is
// correct for toggle switches
@SerializedName("trust_state")
@Expose(serialize = true, deserialize = true)
public boolean trustState;
// The device address
@Expose(serialize = true, deserialize = true)
public String addr = "";
// The fan radio frequency
@Expose(serialize = true, deserialize = true)
public int freq;
// Undocumented
@Expose(serialize = true, deserialize = true)
public int bps;
// Undocumented
@SerializedName("zero_gap")
@Expose(serialize = true, deserialize = true)
public int zeroGap;
}

View File

@ -0,0 +1,112 @@
/**
* 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.bondhome.internal.api;
import static org.openhab.binding.bondhome.internal.BondHomeBindingConstants.*;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import com.google.gson.annotations.Expose;
import com.google.gson.annotations.SerializedName;
/**
* This POJO represents the Bond Device state
*
* The incoming JSON looks like this:
*
* { "breeze": [ 1, 0.2, 0.9 ], "brightness": 75, "light": 1, "power": 0,
* "speed": 2, "timer": 3599 }
*
* @author Sara Geleskie Damiano - Initial contribution
*/
@NonNullByDefault
public class BondDeviceState {
// The current state hash
@SerializedName("_")
@Expose(serialize = false, deserialize = true)
public @Nullable String hash;
// The device power state 1 = on, 0 = off
@Expose(serialize = true, deserialize = true)
public int power;
// The seconds remaining on timer, or 0 meaning no timer running
@Expose(serialize = true, deserialize = true)
public int timer;
// The fan speed - value from 1 to max_speed. If power=0, speed represents the
// last speed setting and the speed to which the device resumes when user asks
// to turn on.
@Expose(serialize = true, deserialize = true)
public int speed;
// The current breeze setting (for a ceiling fan)
// array of the form[<mode>,<mean>,<var>]:
// mode: (integer) 0 = breeze mode disabled, 1 = breeze mode enabled
// mean: (integer) sets the average speed. 0 = minimum average speed (calm), 100 = maximum average speed (storm)
// var: (integer) sets the variability of the speed. 0 = minimum variation (steady), 100 = maximum variation (gusty)
@Expose(serialize = true, deserialize = true)
public int[] breeze = { 0, 50, 50 };
// The direction of a fan with a reversible motor 1 = forward, -1 = reverse.
// The forward and reverse modes are sometimes called Summer and Winter, respectively.
@Expose(serialize = true, deserialize = true)
public int direction;
// The fan light state 1 = light on, 0 = light off
@Expose(serialize = true, deserialize = true)
public int light;
// Whether separate up and down lights are enabled, if applicable
// 1 = enabled, 0 = disabled
// If both up_light and light are 1, then the up light will be on, and similar for down light.
@SerializedName("up_light")
@Expose(serialize = true, deserialize = true)
public int upLight;
@SerializedName("down_light")
@Expose(serialize = true, deserialize = true)
public int downLight;
// The brightness of a fan light or lights
@Expose(serialize = true, deserialize = true)
public int brightness;
@Expose(serialize = true, deserialize = true)
public int upLightBrightness;
@Expose(serialize = true, deserialize = true)
public int downLightBrightness;
// The flame level of a fireplace - value from 1 to 100. If power=0, flame represents the last flame setting and
// the flame to which the device resumes when user asks to turn on
@Expose(serialize = true, deserialize = true)
public int flame;
// Whether a device is open or closed (for motorized shades and garage doors)
// 1 = open, 0 = closed
@Expose(serialize = true, deserialize = true)
public int open;
// Fan settings for a fireplace fan
// fpfan_power: (integer) 1 = on, 0 = off
// fpfan_speed: (integer) from 1-100
@SerializedName("fpfan_power")
@Expose(serialize = true, deserialize = true)
public int fpfanPower;
@SerializedName("fpfan_speed")
@Expose(serialize = true, deserialize = true)
public int fpfanSpeed;
}

View File

@ -0,0 +1,53 @@
/**
* 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.bondhome.internal.api;
import static org.openhab.binding.bondhome.internal.BondHomeBindingConstants.*;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.core.thing.ThingTypeUID;
import com.google.gson.annotations.SerializedName;
/**
* This enum represents the possible device types
*
* @author Sara Geleskie Damiano - Initial contribution
*
*/
@NonNullByDefault
public enum BondDeviceType {
@SerializedName("CF")
CEILING_FAN(THING_TYPE_BOND_FAN),
@SerializedName("MS")
MOTORIZED_SHADES(THING_TYPE_BOND_SHADES),
@SerializedName("FP")
FIREPLACE(THING_TYPE_BOND_FIREPLACE),
@SerializedName("GX")
GENERIC_DEVICE(THING_TYPE_BOND_GENERIC);
private ThingTypeUID deviceTypeUid;
private BondDeviceType(final ThingTypeUID deviceTypeUid) {
this.deviceTypeUid = deviceTypeUid;
}
/**
* Gets the device type name for request deviceType
*
* @return the deviceType name
*/
public ThingTypeUID getThingTypeUID() {
return deviceTypeUid;
}
}

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.bondhome.internal.api;
import static org.openhab.binding.bondhome.internal.BondHomeBindingConstants.*;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import com.google.gson.annotations.Expose;
import com.google.gson.annotations.SerializedName;
/**
* This POJO represents a bond tree node hash state
*
* The incoming JSON looks like this:
*
* { "_": "b32ae527" }
*
* @author Sara Geleskie Damiano - Initial contribution
*/
@NonNullByDefault
public class BondHash {
// The name associated with the device in the bond app
@SerializedName("_")
@Expose(serialize = false, deserialize = true)
public @Nullable String hash;
}

View File

@ -0,0 +1,260 @@
/**
* 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.bondhome.internal.api;
import static org.openhab.binding.bondhome.internal.BondHomeBindingConstants.*;
import java.io.ByteArrayInputStream;
import java.io.InputStream;
import java.io.UnsupportedEncodingException;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.eclipse.jetty.client.HttpClient;
import org.eclipse.jetty.client.api.ContentResponse;
import org.eclipse.jetty.client.api.Request;
import org.eclipse.jetty.client.util.InputStreamContentProvider;
import org.eclipse.jetty.http.HttpMethod;
import org.eclipse.jetty.http.HttpStatus;
import org.openhab.binding.bondhome.internal.BondException;
import org.openhab.binding.bondhome.internal.handler.BondBridgeHandler;
import org.openhab.core.io.net.http.HttpClientFactory;
import org.openhab.core.thing.ThingStatusDetail;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.google.gson.Gson;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonParseException;
import com.google.gson.JsonParser;
/**
* {@link BondHttpApi} wraps the Bond REST API and provides various low
* level function to access the device api (not cloud api).
*
* @author Sara Geleskie Damiano - Initial contribution
*/
@NonNullByDefault
public class BondHttpApi {
private final Logger logger = LoggerFactory.getLogger(BondHttpApi.class);
private final BondBridgeHandler bridgeHandler;
private final HttpClientFactory httpClientFactory;
private Gson gson = new Gson();
public BondHttpApi(BondBridgeHandler bridgeHandler, final HttpClientFactory httpClientFactory) {
this.bridgeHandler = bridgeHandler;
this.httpClientFactory = httpClientFactory;
}
/**
* Gets version information about the Bond bridge
*
* @return the {@link BondSysVersion}
* @throws BondException
*/
public BondSysVersion getBridgeVersion() throws BondException {
String json = request("/v2/sys/version");
logger.trace("BondHome device info : {}", json);
try {
return Objects.requireNonNull(gson.fromJson(json, BondSysVersion.class));
} catch (JsonParseException e) {
logger.debug("Could not parse sys/version JSON '{}'", json, e);
throw new BondException("@text/offline.comm-error.unparseable-response");
}
}
/**
* Gets a list of the attached devices
*
* @return an array of device id's
* @throws BondException
*/
public List<String> getDevices() throws BondException {
List<String> list = new ArrayList<>();
String json = request("/v2/devices/");
try {
JsonParser parser = new JsonParser();
JsonElement element = parser.parse(json);
JsonObject obj = element.getAsJsonObject();
Set<Map.Entry<String, JsonElement>> entries = obj.entrySet();
for (Map.Entry<String, JsonElement> entry : entries) {
String key = entry.getKey();
if (!key.startsWith("_")) {
list.add(key);
}
}
return list;
} catch (JsonParseException e) {
logger.debug("Could not parse devices JSON '{}'", json, e);
throw new BondException("@text/offline.comm-error.unparseable-response");
}
}
/**
* Gets basic device information
*
* @param deviceId The ID of the device
* @return the {@link BondDevice}
* @throws BondException
*/
public BondDevice getDevice(String deviceId) throws BondException {
String json = request("/v2/devices/" + deviceId);
logger.trace("BondHome device info : {}", json);
try {
return Objects.requireNonNull(gson.fromJson(json, BondDevice.class));
} catch (JsonParseException e) {
logger.debug("Could not parse device {}'s JSON '{}'", deviceId, json, e);
throw new BondException("@text/offline.comm-error.unparseable-response");
}
}
/**
* Gets the current state of a device
*
* @param deviceId The ID of the device
* @return the {@link BondDeviceState}
* @throws BondException
*/
public BondDeviceState getDeviceState(String deviceId) throws BondException {
String json = request("/v2/devices/" + deviceId + "/state");
logger.trace("BondHome device state : {}", json);
try {
return Objects.requireNonNull(gson.fromJson(json, BondDeviceState.class));
} catch (JsonParseException e) {
logger.debug("Could not parse device {}'s state JSON '{}'", deviceId, json, e);
throw new BondException("@text/offline.comm-error.unparseable-response");
}
}
/**
* Gets the current properties of a device
*
* @param deviceId The ID of the device
* @return the {@link BondDeviceProperties}
* @throws BondException
*/
public BondDeviceProperties getDeviceProperties(String deviceId) throws BondException {
String json = request("/v2/devices/" + deviceId + "/properties");
logger.trace("BondHome device properties : {}", json);
try {
return Objects.requireNonNull(gson.fromJson(json, BondDeviceProperties.class));
} catch (JsonParseException e) {
logger.debug("Could not parse device {}'s property JSON '{}'", deviceId, json, e);
throw new BondException("@text/offline.comm-error.unparseable-response");
}
}
/**
* Executes a device action
*
* @param deviceId The ID of the device
* @param actionId The Bond action
* @param argument An additional argument for the actions (such as the fan speed)
*/
public synchronized void executeDeviceAction(String deviceId, BondDeviceAction action, @Nullable Integer argument) {
String url = "http://" + bridgeHandler.getBridgeIpAddress() + "/v2/devices/" + deviceId + "/actions/"
+ action.getActionId();
String payload = "{}";
if (argument != null) {
payload = "{\"argument\":" + argument + "}";
}
InputStream content = new ByteArrayInputStream(payload.getBytes());
logger.debug("HTTP PUT to {} with content {}", url, payload);
final HttpClient httpClient = httpClientFactory.getCommonHttpClient();
final Request request = httpClient.newRequest(url).method(HttpMethod.PUT)
.header("BOND-Token", bridgeHandler.getBridgeToken())
.timeout(BOND_API_TIMEOUT_MS, TimeUnit.MILLISECONDS);
try (final InputStreamContentProvider inputStreamContentProvider = new InputStreamContentProvider(content)) {
request.content(inputStreamContentProvider, "application/json");
}
ContentResponse response;
try {
response = request.send();
} catch (Exception e) {
logger.warn("Unable to execute device action {} against device {}: {}", deviceId, action, e.getMessage());
return;
}
logger.debug("HTTP response from {}: {}", deviceId, response.getStatus());
}
/**
* Submit GET request and return response, check for invalid responses
*
* @param uri: URI (e.g. "/settings")
*/
private synchronized String request(String uri) throws BondException {
String httpResponse = "ERROR";
String url = "http://" + bridgeHandler.getBridgeIpAddress() + uri;
int numRetriesRemaining = 3;
do {
try {
logger.debug("HTTP GET to {}", url);
final HttpClient httpClient = httpClientFactory.getCommonHttpClient();
final Request request = httpClient.newRequest(url).method(HttpMethod.GET).header("BOND-Token",
bridgeHandler.getBridgeToken());
ContentResponse response;
response = request.send();
String encoding = response.getEncoding() != null ? response.getEncoding().replaceAll("\"", "").trim()
: StandardCharsets.UTF_8.name();
try {
httpResponse = new String(response.getContent(), encoding);
} catch (UnsupportedEncodingException e) {
throw new BondException("@text/offline.comm-error.no-response");
}
// handle known errors
if (response.getStatus() == HttpStatus.UNAUTHORIZED_401) {
// Don't retry or throw an exception if we get unauthorized, just set the bridge offline
bridgeHandler.setBridgeOffline(ThingStatusDetail.CONFIGURATION_ERROR,
"@text/offline.conf-error.incorrect-local-token");
throw new BondException("@text/offline.conf-error.incorrect-local-token", true);
}
if (response.getStatus() == HttpStatus.NOT_FOUND_404) {
throw new BondException("@text/offline.comm-error.device-not-found");
}
// all api responses return Json. If we get something else it must
// be an error message, e.g. http result code
if (!httpResponse.startsWith("{") && !httpResponse.startsWith("[")) {
throw new BondException("@text/offline.comm-error.unexpected-response");
}
logger.debug("HTTP response from request to {}: {}", uri, httpResponse);
return httpResponse;
} catch (InterruptedException | TimeoutException | ExecutionException e) {
logger.debug("Last request to Bond Bridge failed; {} retries remaining: {}", numRetriesRemaining,
e.getMessage());
numRetriesRemaining--;
if (numRetriesRemaining == 0) {
logger.debug("Repeated Bond API calls to {} failed.", uri);
bridgeHandler.setBridgeOffline(ThingStatusDetail.COMMUNICATION_ERROR,
"@text/offline.comm-error.api-call-failed");
throw new BondException("@text/offline.conf-error.api-call-failed", true);
}
}
} while (true);
}
}

View File

@ -0,0 +1,75 @@
/**
* 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.bondhome.internal.api;
import static org.openhab.binding.bondhome.internal.BondHomeBindingConstants.*;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import com.google.gson.annotations.Expose;
import com.google.gson.annotations.SerializedName;
/**
* This POJO represents the version information of the bond bridge
*
* The incoming JSON looks like this:
*
* {"target": "snowbird", "fw_ver": "v2.5.2", "fw_date": "Fri Feb 22 14:13:25
* -03 2019", "make": "Olibra LLC", "model": "model", "branding_profile":
* "O_SNOWBIRD", "uptime_s": 380, "_": "c342ae74"}
*
* @author Sara Geleskie Damiano - Initial contribution
*/
@NonNullByDefault
public class BondSysVersion {
// The current state hash
@SerializedName("_")
@Expose(serialize = false, deserialize = true)
public @Nullable String hash;
@Expose(serialize = true, deserialize = true)
public @Nullable String target;
@SerializedName("fw_ver")
@Expose(serialize = true, deserialize = true)
public @Nullable String firmwareVersion;
@SerializedName("fw_date")
@Expose(serialize = true, deserialize = true)
public @Nullable String firmwareDate;
@Expose(serialize = true, deserialize = true)
public @Nullable String make;
@Expose(serialize = true, deserialize = true)
public @Nullable String model;
@SerializedName("branding_profile")
@Expose(serialize = true, deserialize = true)
public @Nullable String brandingProfile;
@Expose(serialize = true, deserialize = true)
public @Nullable String bondid;
@SerializedName("upgrade_http")
@Expose(serialize = true, deserialize = true)
public @Nullable Boolean upgradeHttp;
@Expose(serialize = true, deserialize = true)
public int api;
@SerializedName("uptime_s")
@Expose(serialize = true, deserialize = true)
public int uptimeSeconds;
}

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.bondhome.internal.config;
import static org.openhab.binding.bondhome.internal.BondHomeBindingConstants.*;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
/**
* The {@link BondBridgeConfiguration} class contains fields mapping thing
* configuration parameters.
*
* @author Sara Geleskie Damiano - Initial contribution
*/
@NonNullByDefault
public class BondBridgeConfiguration {
/**
* Configuration for a Bond Bridge
*/
public @Nullable String serialNumber;
public @Nullable String localToken;
public @Nullable String ipAddress;
public @Nullable String getIpAddress() {
return ipAddress;
}
public void setIpAddress(String ipAddress) {
this.ipAddress = ipAddress;
}
}

View File

@ -0,0 +1,33 @@
/**
* 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.bondhome.internal.config;
import static org.openhab.binding.bondhome.internal.BondHomeBindingConstants.*;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
/**
* The {@link BondHomeConfiguration} class contains fields mapping thing configuration parameters.
*
* @author Sara Geleskie Damiano - Initial contribution
*/
@NonNullByDefault
public class BondDeviceConfiguration {
/**
* Configuration for a Bond Device
*/
public @Nullable String deviceId;
public @Nullable String lastDeviceConfigurationHash;
}

View File

@ -0,0 +1,124 @@
/**
* 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.bondhome.internal.discovery;
import static org.openhab.binding.bondhome.internal.BondHomeBindingConstants.*;
import java.util.List;
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.binding.bondhome.internal.BondException;
import org.openhab.binding.bondhome.internal.api.BondDevice;
import org.openhab.binding.bondhome.internal.api.BondHttpApi;
import org.openhab.binding.bondhome.internal.handler.BondBridgeHandler;
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.openhab.core.thing.binding.ThingHandler;
import org.openhab.core.thing.binding.ThingHandlerService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* This class does discovery of discoverable things
*
* @author Sara Geleskie Damiano - Initial contribution
*/
@NonNullByDefault
public class BondDiscoveryService extends AbstractDiscoveryService implements ThingHandlerService {
private static final long REFRESH_INTERVAL_MINUTES = 60;
private final Logger logger = LoggerFactory.getLogger(BondDiscoveryService.class);
private @Nullable ScheduledFuture<?> discoveryJob;
private @Nullable BondBridgeHandler bridgeHandler;
private @Nullable BondHttpApi api;
public BondDiscoveryService() {
super(SUPPORTED_THING_TYPES, 10);
this.discoveryJob = null;
}
@Override
public void deactivate() {
super.deactivate();
}
@Override
public void setThingHandler(@Nullable ThingHandler handler) {
if (handler instanceof BondBridgeHandler) {
bridgeHandler = (BondBridgeHandler) handler;
bridgeHandler.setDiscoveryService(this);
api = bridgeHandler.getBridgeAPI();
}
}
@Override
public @Nullable ThingHandler getThingHandler() {
return bridgeHandler;
}
@Override
protected void startBackgroundDiscovery() {
discoverNow();
}
public synchronized void discoverNow() {
ScheduledFuture<?> localDiscoveryJob = discoveryJob;
if (localDiscoveryJob != null) {
localDiscoveryJob.cancel(true);
}
discoveryJob = scheduler.scheduleWithFixedDelay(this::startScan, 0, REFRESH_INTERVAL_MINUTES, TimeUnit.MINUTES);
}
@Override
protected synchronized void startScan() {
logger.debug("Start scan for Bond devices.");
try {
final ThingUID bridgeUid = bridgeHandler.getThing().getUID();
api = bridgeHandler.getBridgeAPI();
List<String> deviceList = api.getDevices();
if (deviceList != null) {
for (final String deviceId : deviceList) {
BondDevice thisDevice = api.getDevice(deviceId);
String deviceName;
if (thisDevice != null && (deviceName = thisDevice.name) != null) {
final ThingUID deviceUid = new ThingUID(thisDevice.type.getThingTypeUID(), bridgeUid, deviceId);
final DiscoveryResult discoveryResult = DiscoveryResultBuilder.create(deviceUid)
.withBridge(bridgeUid).withLabel(thisDevice.name)
.withProperty(CONFIG_DEVICE_ID, deviceId)
.withProperty(PROPERTIES_DEVICE_NAME, deviceName)
.withRepresentationProperty(CONFIG_DEVICE_ID).build();
thingDiscovered(discoveryResult);
}
}
}
} catch (BondException ignored) {
logger.warn("Error getting devices for discovery: {}", ignored.getMessage());
} finally {
removeOlderResults(getTimestampOfLastScan());
}
}
@Override
protected void stopBackgroundDiscovery() {
stopScan();
ScheduledFuture<?> discoveryJob = this.discoveryJob;
if (discoveryJob != null) {
discoveryJob.cancel(true);
this.discoveryJob = null;
}
}
}

View File

@ -0,0 +1,75 @@
/**
* 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.bondhome.internal.discovery;
import static org.openhab.binding.bondhome.internal.BondHomeBindingConstants.*;
import static org.openhab.core.thing.Thing.*;
import java.util.Set;
import javax.jmdns.ServiceInfo;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.core.config.discovery.DiscoveryResult;
import org.openhab.core.config.discovery.DiscoveryResultBuilder;
import org.openhab.core.config.discovery.mdns.MDNSDiscoveryParticipant;
import org.openhab.core.thing.ThingTypeUID;
import org.openhab.core.thing.ThingUID;
import org.osgi.service.component.annotations.Component;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* This class identifies Bond Bridges by their mDNS service information.
*
* @author Sara Geleskie Damiano - Initial contribution
*/
@Component(service = MDNSDiscoveryParticipant.class, configurationPid = "discovery.mdns.bondhome")
@NonNullByDefault
public class BondMDNSDiscoveryParticipant implements MDNSDiscoveryParticipant {
private final Logger logger = LoggerFactory.getLogger(BondMDNSDiscoveryParticipant.class);
private static final String SERVICE_TYPE = "_bond._tcp.local.";
@Override
public Set<ThingTypeUID> getSupportedThingTypeUIDs() {
return SUPPORTED_BRIDGE_TYPES;
}
@Override
public String getServiceType() {
return SERVICE_TYPE;
}
@Override
public @Nullable ThingUID getThingUID(@Nullable ServiceInfo service) {
if (service != null) {
return new ThingUID(THING_TYPE_BOND_BRIDGE, service.getName());
}
return null;
}
@Override
public @Nullable DiscoveryResult createResult(ServiceInfo service) {
ThingUID thingUID = getThingUID(service);
if (thingUID != null) {
logger.debug("Discovered Bond Bridge: {}", service);
return DiscoveryResultBuilder.create(thingUID).withProperty(PROPERTY_SERIAL_NUMBER, service.getName())
.withLabel("@text/discovery.bridge.label").withRepresentationProperty(PROPERTY_SERIAL_NUMBER)
.build();
}
return null;
}
}

View File

@ -0,0 +1,343 @@
/**
* 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.bondhome.internal.handler;
import static org.openhab.binding.bondhome.internal.BondHomeBindingConstants.*;
import static org.openhab.core.thing.Thing.*;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ScheduledExecutorService;
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.binding.bondhome.internal.BondException;
import org.openhab.binding.bondhome.internal.api.BPUPListener;
import org.openhab.binding.bondhome.internal.api.BPUPUpdate;
import org.openhab.binding.bondhome.internal.api.BondDeviceState;
import org.openhab.binding.bondhome.internal.api.BondHttpApi;
import org.openhab.binding.bondhome.internal.api.BondSysVersion;
import org.openhab.binding.bondhome.internal.config.BondBridgeConfiguration;
import org.openhab.binding.bondhome.internal.discovery.BondDiscoveryService;
import org.openhab.core.common.ThreadPoolManager;
import org.openhab.core.config.core.Configuration;
import org.openhab.core.io.net.http.HttpClientFactory;
import org.openhab.core.thing.Bridge;
import org.openhab.core.thing.ChannelUID;
import org.openhab.core.thing.Thing;
import org.openhab.core.thing.ThingStatus;
import org.openhab.core.thing.ThingStatusDetail;
import org.openhab.core.thing.binding.BaseBridgeHandler;
import org.openhab.core.thing.binding.ThingHandler;
import org.openhab.core.thing.binding.ThingHandlerService;
import org.openhab.core.types.Command;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* The {@link BondBridgeHandler} is responsible for handling commands, which are
* sent to one of the channels.
*
* @author Sara Geleskie Damiano - Initial contribution
*/
@NonNullByDefault
public class BondBridgeHandler extends BaseBridgeHandler {
private final Logger logger = LoggerFactory.getLogger(BondBridgeHandler.class);
// Get a dedicated threadpool for the long-running listener thread.
// Intent is to not permanently tie up the common scheduler pool.
private final ScheduledExecutorService bondScheduler = ThreadPoolManager.getScheduledPool("bondBridgeHandler");
private final BPUPListener udpListener;
private final BondHttpApi api;
private @Nullable BondBridgeConfiguration config;
private @Nullable BondDiscoveryService discoveryService;
private final Set<BondDeviceHandler> handlers = Collections.synchronizedSet(new HashSet<>());
private @Nullable ScheduledFuture<?> initializer;
public BondBridgeHandler(Bridge bridge, final HttpClientFactory httpClientFactory) {
super(bridge);
udpListener = new BPUPListener(this);
api = new BondHttpApi(this, httpClientFactory);
logger.debug("Created a BondBridgeHandler for thing '{}'", getThing().getUID());
}
@Override
public void handleCommand(ChannelUID channelUID, Command command) {
// Not needed, all commands are handled in the {@link BondDeviceHandler}
}
@Override
public void initialize() {
config = getConfigAs(BondBridgeConfiguration.class);
// set the thing status to UNKNOWN temporarily
updateStatus(ThingStatus.UNKNOWN);
this.initializer = scheduler.schedule(this::initializeThing, 0L, TimeUnit.MILLISECONDS);
}
private void initializeThing() {
BondBridgeConfiguration localConfig = config;
if (localConfig.localToken == null) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
"@text/offline.conf-error.incorrect-local-token");
this.initializer = null;
return;
}
if (localConfig.ipAddress == null) {
try {
String lookupAddress = localConfig.serialNumber + ".local";
logger.debug("Attempting to get IP address for Bond Bridge {}", lookupAddress);
InetAddress ia = InetAddress.getByName(lookupAddress);
String ip = ia.getHostAddress();
Configuration c = editConfiguration();
c.put(CONFIG_IP_ADDRESS, ip);
updateConfiguration(c);
config = getConfigAs(BondBridgeConfiguration.class);
} catch (UnknownHostException ignored) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
"@text/offline.conf-error.unknown-host");
this.initializer = null;
return;
}
} else {
try {
InetAddress.getByName(localConfig.ipAddress);
} catch (UnknownHostException ignored) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
"@text/offline.conf-error.invalid-host");
this.initializer = null;
return;
}
}
// Ask the bridge its current status and update the properties with the info
// This will also set the thing status to online/offline based on whether it
// succeeds in getting the properties from the bridge.
updateBridgeProperties();
this.initializer = null;
}
@Override
public void dispose() {
// The listener should already have been stopped when the last child was
// disposed,
// but we'll call the stop here for good measure.
stopUDPListenerJob();
ScheduledFuture<?> localInitializer = initializer;
if (localInitializer != null) {
localInitializer.cancel(true);
}
}
private synchronized void startUDPListenerJob() {
if (udpListener.isRunning()) {
return;
}
logger.debug("Started listener job");
udpListener.start(bondScheduler);
}
private synchronized void stopUDPListenerJob() {
logger.trace("Stopping UDP listener job");
if (udpListener.isRunning()) {
udpListener.shutdown();
logger.debug("UDP listener job stopped");
}
}
@Override
public void childHandlerInitialized(ThingHandler childHandler, Thing childThing) {
super.childHandlerInitialized(childHandler, childThing);
if (childHandler instanceof BondDeviceHandler) {
BondDeviceHandler handler = (BondDeviceHandler) childHandler;
synchronized (handlers) {
// Start the BPUP update service after the first child device is added
startUDPListenerJob();
if (!handlers.contains(handler)) {
handlers.add(handler);
}
}
}
}
@Override
public void childHandlerDisposed(ThingHandler childHandler, Thing childThing) {
if (childHandler instanceof BondDeviceHandler) {
BondDeviceHandler handler = (BondDeviceHandler) childHandler;
synchronized (handlers) {
handlers.remove(handler);
if (handlers.isEmpty()) {
// Stop the update service when the last child is removed
stopUDPListenerJob();
}
}
}
super.childHandlerDisposed(childHandler, childThing);
}
/**
* Forwards a push update to a device
*
* @param the {@link BPUPUpdate object}
*/
public void forwardUpdateToThing(BPUPUpdate pushUpdate) {
updateStatus(ThingStatus.ONLINE);
BondDeviceState updateState = pushUpdate.deviceState;
String topic = pushUpdate.topic;
String deviceId = null;
String topicType = null;
if (topic != null) {
String parts[] = topic.split("/");
deviceId = parts[1];
topicType = parts[2];
}
// We can't use getThingByUID because we don't know the type of device and thus
// don't know the full uid (that is we cannot tell a fan from a fireplace, etc,
// from the contents of the update)
if (deviceId != null) {
if (topicType != null && "state".equals(topicType)) {
synchronized (handlers) {
for (BondDeviceHandler handler : handlers) {
String handlerDeviceId = handler.getDeviceId();
if (handlerDeviceId.equalsIgnoreCase(deviceId)) {
handler.updateChannelsFromState(updateState);
break;
}
}
}
} else {
logger.trace("could not read topic type from push update or type was not state.");
}
} else {
logger.warn("Can not read device Id from push update.");
}
}
/**
* Returns the Id of the bridge associated with the handler
*/
public String getBridgeId() {
String serialNumber = config.serialNumber;
return serialNumber == null ? "" : serialNumber;
}
/**
* Returns the Ip Address of the bridge associated with the handler as a string
*/
public @Nullable String getBridgeIpAddress() {
return config.ipAddress;
}
/**
* Returns the local token of the bridge associated with the handler as a string
*/
public String getBridgeToken() {
String localToken = config.localToken;
return localToken == null ? "" : localToken;
}
/**
* Returns the api instance
*/
public BondHttpApi getBridgeAPI() {
return this.api;
}
/**
* Set the bridge status offline.
*
* Called by the dependents to set the bridge offline when repeated requests
* fail.
*
* NOTE: This does NOT stop the UDP listener job, which will keep pinging the
* bridge's IP once a minute. The listener job will set the bridge back online
* if it receives a proper response from the bridge.
*/
public void setBridgeOffline(ThingStatusDetail detail, String description) {
updateStatus(ThingStatus.OFFLINE, detail, description);
}
/**
* Set the bridge status back online.
*
* Called by the UDP listener when it gets a proper response.
*/
public void setBridgeOnline(String bridgeAddress) {
BondBridgeConfiguration localConfig = config;
if (localConfig.ipAddress == null || !localConfig.ipAddress.equals(bridgeAddress)) {
logger.debug("IP address of Bond {} has changed to {}", localConfig.serialNumber, bridgeAddress);
Configuration c = editConfiguration();
c.put(CONFIG_IP_ADDRESS, bridgeAddress);
updateConfiguration(c);
updateBridgeProperties();
return;
}
// don't bother updating on every keepalive packet
if (getThing().getStatus() != ThingStatus.ONLINE) {
updateBridgeProperties();
}
}
private void updateProperty(Map<String, String> thingProperties, String key, @Nullable String value) {
if (value == null) {
return;
}
thingProperties.put(key, value);
}
private void updateBridgeProperties() {
BondSysVersion myVersion;
try {
myVersion = api.getBridgeVersion();
} catch (BondException e) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
return;
}
// Update all the thing properties based on the result
Map<String, String> thingProperties = editProperties();
updateProperty(thingProperties, PROPERTY_VENDOR, myVersion.make);
updateProperty(thingProperties, PROPERTY_MODEL_ID, myVersion.model);
updateProperty(thingProperties, PROPERTY_SERIAL_NUMBER, myVersion.bondid);
updateProperty(thingProperties, PROPERTY_FIRMWARE_VERSION, myVersion.firmwareVersion);
updateProperties(thingProperties);
updateStatus(ThingStatus.ONLINE);
BondDiscoveryService localDiscoveryService = discoveryService;
if (localDiscoveryService != null) {
localDiscoveryService.discoverNow();
}
}
@Override
public Collection<Class<? extends ThingHandlerService>> getServices() {
return Collections.singleton(BondDiscoveryService.class);
}
public void setDiscoveryService(BondDiscoveryService discoveryService) {
this.discoveryService = discoveryService;
}
}

View File

@ -0,0 +1,756 @@
/**
* 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.bondhome.internal.handler;
import static org.openhab.binding.bondhome.internal.BondHomeBindingConstants.*;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
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.binding.bondhome.internal.BondException;
import org.openhab.binding.bondhome.internal.api.BondDevice;
import org.openhab.binding.bondhome.internal.api.BondDeviceAction;
import org.openhab.binding.bondhome.internal.api.BondDeviceProperties;
import org.openhab.binding.bondhome.internal.api.BondDeviceState;
import org.openhab.binding.bondhome.internal.api.BondDeviceType;
import org.openhab.binding.bondhome.internal.api.BondHttpApi;
import org.openhab.binding.bondhome.internal.config.BondDeviceConfiguration;
import org.openhab.core.config.core.Configuration;
import org.openhab.core.library.types.DecimalType;
import org.openhab.core.library.types.IncreaseDecreaseType;
import org.openhab.core.library.types.OnOffType;
import org.openhab.core.library.types.PercentType;
import org.openhab.core.library.types.StopMoveType;
import org.openhab.core.library.types.StringType;
import org.openhab.core.library.types.UpDownType;
import org.openhab.core.thing.Bridge;
import org.openhab.core.thing.Channel;
import org.openhab.core.thing.ChannelUID;
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.builder.ThingBuilder;
import org.openhab.core.types.Command;
import org.openhab.core.types.RefreshType;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* The {@link BondDeviceHandler} is responsible for handling commands, which are
* sent to one of the channels.
*
* @author Sara Geleskie Damiano - Initial contribution
* @author Cody Cutrer - Significant rework on channels to more closely match openHAB's model.
*/
@NonNullByDefault
public class BondDeviceHandler extends BaseThingHandler {
private final Logger logger = LoggerFactory.getLogger(BondDeviceHandler.class);
private @NonNullByDefault({}) BondDeviceConfiguration config;
private @Nullable BondHttpApi api;
private @Nullable BondDevice deviceInfo;
private @Nullable BondDeviceProperties deviceProperties;
private @Nullable BondDeviceState deviceState;
private @Nullable ScheduledFuture<?> pollingJob;
private volatile boolean disposed;
private volatile boolean fullyInitialized;
public BondDeviceHandler(Thing thing) {
super(thing);
disposed = true;
fullyInitialized = false;
}
@Override
public void handleCommand(ChannelUID channelUID, Command command) {
if (hasConfigurationError() || !fullyInitialized) {
logger.trace(
"Bond device handler for {} received command {} on channel {} but is not yet prepared to handle it.",
config.deviceId, command, channelUID);
return;
}
String deviceId = Objects.requireNonNull(config.deviceId);
logger.trace("Bond device handler for {} received command {} on channel {}", config.deviceId, command,
channelUID);
final BondHttpApi api = this.api;
if (api == null) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, "@text/offline.comm-error.no-api");
// Re-attempt initialization
scheduler.schedule(() -> {
logger.trace("Re-attempting initialization");
initialize();
}, 30, TimeUnit.SECONDS);
return;
}
if (command instanceof RefreshType) {
logger.trace("Executing refresh command");
try {
deviceState = api.getDeviceState(deviceId);
updateChannelsFromState(deviceState);
} catch (BondException e) {
if (!e.wasBridgeSetOffline()) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
}
}
return;
}
BondDeviceAction action = null;
@Nullable
Integer value = null;
final BondDevice devInfo = Objects.requireNonNull(this.deviceInfo);
switch (channelUID.getId()) {
case CHANNEL_POWER:
logger.trace("Power state command");
api.executeDeviceAction(deviceId,
command == OnOffType.ON ? BondDeviceAction.TURN_ON : BondDeviceAction.TURN_OFF, null);
break;
case CHANNEL_COMMAND:
logger.trace("{} command", command.toString());
try {
action = BondDeviceAction.valueOf(command.toString());
} catch (IllegalArgumentException e) {
logger.warn("Received unknown command {}.", command);
break;
}
if (devInfo.actions.contains(action)) {
api.executeDeviceAction(deviceId, action, null);
} else {
logger.warn("Device {} does not support command {}.", config.deviceId, command);
}
break;
case CHANNEL_FAN_POWER:
logger.trace("Fan power state command");
api.executeDeviceAction(deviceId,
command == OnOffType.ON ? BondDeviceAction.TURN_FP_FAN_ON : BondDeviceAction.TURN_FP_FAN_OFF,
null);
break;
case CHANNEL_FAN_SPEED:
logger.trace("Fan speed command");
if (command instanceof PercentType) {
if (devInfo.actions.contains(BondDeviceAction.SET_FP_FAN)) {
value = ((PercentType) command).intValue();
if (value == 0) {
action = BondDeviceAction.TURN_FP_FAN_OFF;
value = null;
} else {
action = BondDeviceAction.SET_FP_FAN;
}
} else {
BondDeviceProperties devProperties = this.deviceProperties;
if (devProperties != null) {
int maxSpeed = devProperties.maxSpeed;
value = (int) Math.ceil(((PercentType) command).intValue() * maxSpeed / 100);
} else {
value = 1;
}
if (value == 0) {
action = BondDeviceAction.TURN_OFF;
value = null;
} else {
action = BondDeviceAction.SET_SPEED;
}
}
logger.trace("Fan speed command with speed set as {}", value);
api.executeDeviceAction(deviceId, action, value);
} else if (command instanceof IncreaseDecreaseType) {
logger.trace("Fan increase/decrease speed command");
api.executeDeviceAction(deviceId,
((IncreaseDecreaseType) command == IncreaseDecreaseType.INCREASE
? BondDeviceAction.INCREASE_SPEED
: BondDeviceAction.DECREASE_SPEED),
null);
} else if (command instanceof OnOffType) {
logger.trace("Fan speed command {}", command);
if (devInfo.actions.contains(BondDeviceAction.TURN_FP_FAN_ON)) {
action = command == OnOffType.ON ? BondDeviceAction.TURN_FP_FAN_ON
: BondDeviceAction.TURN_FP_FAN_OFF;
} else if (devInfo.actions.contains(BondDeviceAction.TURN_ON)) {
action = command == OnOffType.ON ? BondDeviceAction.TURN_ON : BondDeviceAction.TURN_OFF;
}
if (action != null) {
api.executeDeviceAction(deviceId, action, null);
}
} else {
logger.info("Unsupported command on fan speed channel");
}
break;
case CHANNEL_FAN_BREEZE_STATE:
logger.trace("Fan enable/disable breeze command");
api.executeDeviceAction(deviceId,
command == OnOffType.ON ? BondDeviceAction.BREEZE_ON : BondDeviceAction.BREEZE_OFF, null);
break;
case CHANNEL_FAN_BREEZE_MEAN:
// TODO(SRGDamia1): write array command fxn
logger.trace("Support for fan breeze settings not yet available");
break;
case CHANNEL_FAN_BREEZE_VAR:
// TODO(SRGDamia1): write array command fxn
logger.trace("Support for fan breeze settings not yet available");
break;
case CHANNEL_FAN_DIRECTION:
logger.trace("Fan direction command {}", command.toString());
if (command instanceof StringType) {
api.executeDeviceAction(deviceId, BondDeviceAction.SET_DIRECTION,
command.toString().equals("winter") ? -1 : 1);
}
break;
case CHANNEL_LIGHT_POWER:
logger.trace("Fan light state command");
api.executeDeviceAction(deviceId,
command == OnOffType.ON ? BondDeviceAction.TURN_LIGHT_ON : BondDeviceAction.TURN_LIGHT_OFF,
null);
break;
case CHANNEL_LIGHT_BRIGHTNESS:
if (command instanceof PercentType) {
PercentType pctCommand = (PercentType) command;
value = pctCommand.intValue();
if (value == 0) {
action = BondDeviceAction.TURN_LIGHT_OFF;
value = null;
} else {
action = BondDeviceAction.SET_BRIGHTNESS;
}
logger.trace("Fan light brightness command with value of {}", value);
api.executeDeviceAction(deviceId, action, value);
} else if (command instanceof IncreaseDecreaseType) {
logger.trace("Fan light brightness increase/decrease command {}", command);
api.executeDeviceAction(deviceId,
((IncreaseDecreaseType) command == IncreaseDecreaseType.INCREASE
? BondDeviceAction.INCREASE_BRIGHTNESS
: BondDeviceAction.DECREASE_BRIGHTNESS),
null);
} else if (command instanceof OnOffType) {
logger.trace("Fan light brightness command {}", command);
api.executeDeviceAction(deviceId,
command == OnOffType.ON ? BondDeviceAction.TURN_LIGHT_ON : BondDeviceAction.TURN_LIGHT_OFF,
null);
} else {
logger.info("Unsupported command on fan light brightness channel");
}
break;
case CHANNEL_UP_LIGHT_ENABLE:
api.executeDeviceAction(deviceId, command == OnOffType.ON ? BondDeviceAction.TURN_UP_LIGHT_ON
: BondDeviceAction.TURN_UP_LIGHT_OFF, null);
break;
case CHANNEL_UP_LIGHT_POWER:
// To turn on the up light, we first have to enable it and then turn on the lights
enableUpLight();
api.executeDeviceAction(deviceId,
command == OnOffType.ON ? BondDeviceAction.TURN_LIGHT_ON : BondDeviceAction.TURN_LIGHT_OFF,
null);
break;
case CHANNEL_UP_LIGHT_BRIGHTNESS:
enableUpLight();
if (command instanceof PercentType) {
PercentType pctCommand = (PercentType) command;
value = pctCommand.intValue();
if (value == 0) {
action = BondDeviceAction.TURN_LIGHT_OFF;
value = null;
} else {
action = BondDeviceAction.SET_UP_LIGHT_BRIGHTNESS;
}
logger.trace("Fan up light brightness command with value of {}", value);
api.executeDeviceAction(deviceId, action, value);
} else if (command instanceof IncreaseDecreaseType) {
logger.trace("Fan uplight brightness increase/decrease command {}", command);
api.executeDeviceAction(deviceId,
((IncreaseDecreaseType) command == IncreaseDecreaseType.INCREASE
? BondDeviceAction.INCREASE_UP_LIGHT_BRIGHTNESS
: BondDeviceAction.DECREASE_UP_LIGHT_BRIGHTNESS),
null);
} else if (command instanceof OnOffType) {
logger.trace("Fan up light brightness command {}", command);
api.executeDeviceAction(deviceId,
command == OnOffType.ON ? BondDeviceAction.TURN_LIGHT_ON : BondDeviceAction.TURN_LIGHT_OFF,
null);
} else {
logger.info("Unsupported command on fan up light brightness channel");
}
break;
case CHANNEL_DOWN_LIGHT_ENABLE:
api.executeDeviceAction(deviceId, command == OnOffType.ON ? BondDeviceAction.TURN_DOWN_LIGHT_ON
: BondDeviceAction.TURN_DOWN_LIGHT_OFF, null);
break;
case CHANNEL_DOWN_LIGHT_POWER:
// To turn on the down light, we first have to enable it and then turn on the lights
api.executeDeviceAction(deviceId, BondDeviceAction.TURN_DOWN_LIGHT_ON, null);
api.executeDeviceAction(deviceId,
command == OnOffType.ON ? BondDeviceAction.TURN_LIGHT_ON : BondDeviceAction.TURN_LIGHT_OFF,
null);
break;
case CHANNEL_DOWN_LIGHT_BRIGHTNESS:
enableDownLight();
if (command instanceof PercentType) {
PercentType pctCommand = (PercentType) command;
value = pctCommand.intValue();
if (value == 0) {
action = BondDeviceAction.TURN_LIGHT_OFF;
value = null;
} else {
action = BondDeviceAction.SET_DOWN_LIGHT_BRIGHTNESS;
}
logger.trace("Fan down light brightness command with value of {}", value);
api.executeDeviceAction(deviceId, action, value);
} else if (command instanceof IncreaseDecreaseType) {
logger.trace("Fan down light brightness increase/decrease command");
api.executeDeviceAction(deviceId,
((IncreaseDecreaseType) command == IncreaseDecreaseType.INCREASE
? BondDeviceAction.INCREASE_DOWN_LIGHT_BRIGHTNESS
: BondDeviceAction.DECREASE_DOWN_LIGHT_BRIGHTNESS),
null);
} else if (command instanceof OnOffType) {
logger.trace("Fan down light brightness command {}", command);
api.executeDeviceAction(deviceId,
command == OnOffType.ON ? BondDeviceAction.TURN_LIGHT_ON : BondDeviceAction.TURN_LIGHT_OFF,
null);
} else {
logger.debug("Unsupported command on fan down light brightness channel");
}
break;
case CHANNEL_FLAME:
if (command instanceof PercentType) {
PercentType pctCommand = (PercentType) command;
value = pctCommand.intValue();
if (value == 0) {
action = BondDeviceAction.TURN_OFF;
value = null;
} else {
action = BondDeviceAction.SET_FLAME;
}
logger.trace("Fireplace flame command with value of {}", value);
api.executeDeviceAction(deviceId, action, value);
} else if (command instanceof IncreaseDecreaseType) {
logger.trace("Fireplace flame increase/decrease command");
api.executeDeviceAction(deviceId,
((IncreaseDecreaseType) command == IncreaseDecreaseType.INCREASE
? BondDeviceAction.INCREASE_FLAME
: BondDeviceAction.DECREASE_FLAME),
null);
} else if (command instanceof OnOffType) {
api.executeDeviceAction(deviceId,
command == OnOffType.ON ? BondDeviceAction.TURN_ON : BondDeviceAction.TURN_OFF, null);
} else {
logger.info("Unsupported command on flame channel");
}
break;
case CHANNEL_ROLLERSHUTTER:
logger.trace("Rollershutter command {}", command);
if (command.equals(PercentType.ZERO)) {
command = UpDownType.UP;
} else if (command.equals(PercentType.HUNDRED)) {
command = UpDownType.DOWN;
}
if (command == UpDownType.UP) {
action = BondDeviceAction.OPEN;
} else if (command == UpDownType.DOWN) {
action = BondDeviceAction.CLOSE;
} else if (command == StopMoveType.STOP) {
action = BondDeviceAction.HOLD;
}
if (action != null) {
api.executeDeviceAction(deviceId, action, null);
}
break;
default:
logger.info("Command {} on unknown channel {}, {}", command.toFullString(), channelUID.getId(),
channelUID.toString());
return;
}
}
private void enableUpLight() {
Objects.requireNonNull(api).executeDeviceAction(Objects.requireNonNull(config.deviceId),
BondDeviceAction.TURN_UP_LIGHT_ON, null);
}
private void enableDownLight() {
Objects.requireNonNull(api).executeDeviceAction(Objects.requireNonNull(config.deviceId),
BondDeviceAction.TURN_DOWN_LIGHT_ON, null);
}
@Override
public void initialize() {
config = getConfigAs(BondDeviceConfiguration.class);
logger.trace("Starting initialization for Bond device with device id {}.", config.deviceId);
fullyInitialized = false;
disposed = false;
// set the thing status to UNKNOWN temporarily
updateStatus(ThingStatus.UNKNOWN);
scheduler.execute(this::initializeThing);
}
@Override
public synchronized void dispose() {
logger.debug("Disposing thing handler for {}.", this.getThing().getUID());
// Mark handler as disposed as soon as possible to halt updates
disposed = true;
fullyInitialized = false;
final ScheduledFuture<?> pollingJob = this.pollingJob;
if (pollingJob != null && !pollingJob.isCancelled()) {
pollingJob.cancel(true);
}
this.pollingJob = null;
}
private void initializeThing() {
String deviceId = config.deviceId;
if (deviceId == null) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
"@text/offline.conf-error.no-device-id");
return;
}
if (!getBridgeAndAPI()) {
return;
}
BondHttpApi api = this.api;
if (api == null) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, "@text/offline.comm-error.no-api");
return;
}
try {
logger.trace("Getting device information for {} ({})", config.deviceId, this.getThing().getLabel());
deviceInfo = api.getDevice(deviceId);
logger.trace("Getting device properties for {} ({})", config.deviceId, this.getThing().getLabel());
deviceProperties = api.getDeviceProperties(deviceId);
} catch (BondException e) {
if (!e.wasBridgeSetOffline()) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
}
return;
}
final BondDevice devInfo = this.deviceInfo;
final BondDeviceProperties devProperties = this.deviceProperties;
BondDeviceType devType;
String devHash;
if (devInfo == null || devProperties == null || (devType = devInfo.type) == null
|| (devHash = devInfo.hash) == null) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
"@text/offline.conf-error.no-device-properties");
return;
}
// Anytime the configuration has changed or the binding has been updated,
// recreate the thing to make sure all possible channels are available
// NOTE: This will cause the thing to be disposed and re-initialized
if (wasThingUpdatedExternally(devInfo)) {
recreateAllChannels(devType, devHash);
return;
}
updateDevicePropertiesFromBond(devInfo, devProperties);
deleteExtraChannels(devInfo.actions);
startPollingJob();
// Now we're online!
updateStatus(ThingStatus.ONLINE);
fullyInitialized = true;
logger.debug("Finished initializing device!");
}
private void updateProperty(Map<String, String> thingProperties, String key, @Nullable String value) {
if (value == null) {
return;
}
thingProperties.put(key, value);
}
private void updateDevicePropertiesFromBond(BondDevice devInfo, BondDeviceProperties devProperties) {
// Update all the thing properties based on the result
Map<String, String> thingProperties = new HashMap<String, String>();
updateProperty(thingProperties, CONFIG_DEVICE_ID, config.deviceId);
logger.trace("Updating device name to {}", devInfo.name);
updateProperty(thingProperties, PROPERTIES_DEVICE_NAME, devInfo.name);
logger.trace("Updating other device properties for {} ({})", config.deviceId, this.getThing().getLabel());
updateProperty(thingProperties, PROPERTIES_TEMPLATE_NAME, devInfo.template);
thingProperties.put(PROPERTIES_MAX_SPEED, String.valueOf(devProperties.maxSpeed));
thingProperties.put(PROPERTIES_TRUST_STATE, String.valueOf(devProperties.trustState));
thingProperties.put(PROPERTIES_ADDRESS, String.valueOf(devProperties.addr));
thingProperties.put(PROPERTIES_RF_FREQUENCY, String.valueOf(devProperties.freq));
logger.trace("Saving properties for {} ({})", config.deviceId, this.getThing().getLabel());
updateProperties(thingProperties);
}
private synchronized void recreateAllChannels(BondDeviceType currentType, String currentHash) {
if (hasConfigurationError()) {
logger.trace("Don't recreate channels, I've been disposed!");
return;
}
logger.debug("Recreating all possible channels for a {} for {} ({})",
currentType.getThingTypeUID().getAsString(), config.deviceId, this.getThing().getLabel());
// Create a new configuration
final Map<String, Object> map = new HashMap<>();
map.put(CONFIG_DEVICE_ID, Objects.requireNonNull(config.deviceId));
map.put(CONFIG_LATEST_HASH, currentHash);
Configuration newConfiguration = new Configuration(map);
// Change the thing type back to itself to force all channels to be re-created from XML
changeThingType(currentType.getThingTypeUID(), newConfiguration);
}
private synchronized void deleteExtraChannels(List<BondDeviceAction> currentActions) {
logger.trace("Deleting channels based on the available actions");
// Get the thing to edit
ThingBuilder thingBuilder = editThing();
// Now, look at the whole list of possible channels
List<Channel> possibleChannels = this.getThing().getChannels();
Set<String> availableChannelIds = new HashSet<>();
for (BondDeviceAction action : currentActions) {
String actionType = action.getChannelTypeId();
if (actionType != null) {
availableChannelIds.add(actionType);
logger.trace(" Action: {}, Relevant Channel Type Id: {}", action.getActionId(), actionType);
}
}
// Remove power channels if we have a dimmer channel for them;
// the dimmer channel already covers the power case
if (availableChannelIds.contains(CHANNEL_FAN_SPEED)) {
availableChannelIds.remove(CHANNEL_POWER);
availableChannelIds.remove(CHANNEL_FAN_POWER);
}
if (availableChannelIds.contains(CHANNEL_LIGHT_BRIGHTNESS)) {
availableChannelIds.remove(CHANNEL_LIGHT_POWER);
}
if (availableChannelIds.contains(CHANNEL_UP_LIGHT_BRIGHTNESS)) {
availableChannelIds.remove(CHANNEL_UP_LIGHT_POWER);
}
if (availableChannelIds.contains(CHANNEL_DOWN_LIGHT_BRIGHTNESS)) {
availableChannelIds.remove(CHANNEL_DOWN_LIGHT_POWER);
}
if (availableChannelIds.contains(CHANNEL_FLAME)) {
availableChannelIds.remove(CHANNEL_POWER);
}
for (Channel channel : possibleChannels) {
if (availableChannelIds.contains(channel.getUID().getId())) {
logger.trace(" ++++ Keeping: {}", channel.getUID().getId());
} else {
thingBuilder.withoutChannel(channel.getUID());
logger.trace(" ---- Dropping: {}", channel.getUID().getId());
}
}
// Add all the channels
logger.trace("Saving the thing with extra channels removed");
updateThing(thingBuilder.build());
}
public String getDeviceId() {
String deviceId = config.deviceId;
return deviceId == null ? "" : deviceId;
}
public synchronized void updateChannelsFromState(@Nullable BondDeviceState updateState) {
if (hasConfigurationError()) {
return;
}
if (updateState == null) {
logger.debug("No state information provided to update channels with");
return;
}
logger.debug("Updating channels from state for {} ({})", config.deviceId, this.getThing().getLabel());
updateStatus(ThingStatus.ONLINE);
updateState(CHANNEL_POWER, updateState.power == 0 ? OnOffType.OFF : OnOffType.ON);
boolean fanOn;
final BondDevice devInfo = this.deviceInfo;
if (devInfo != null && devInfo.actions.contains(BondDeviceAction.TURN_FP_FAN_OFF)) {
fanOn = updateState.fpfanPower != 0;
updateState(CHANNEL_FAN_POWER, fanOn ? OnOffType.OFF : OnOffType.ON);
updateState(CHANNEL_FAN_SPEED, new PercentType(updateState.fpfanSpeed));
} else {
fanOn = updateState.power != 0;
int value = 1;
BondDeviceProperties devProperties = this.deviceProperties;
if (devProperties != null) {
double maxSpeed = devProperties.maxSpeed;
value = (int) (((double) updateState.speed / maxSpeed) * 100);
logger.trace("Raw fan speed: {}, Percent: {}", updateState.speed, value);
} else if (updateState.speed != 0 && this.getThing().getThingTypeUID().equals(THING_TYPE_BOND_FAN)) {
logger.info("Unable to convert fan speed to a percent for {}!", this.getThing().getLabel());
}
updateState(CHANNEL_FAN_SPEED, formPercentType(fanOn, value));
}
updateState(CHANNEL_FAN_BREEZE_STATE, updateState.breeze[0] == 0 ? OnOffType.OFF : OnOffType.ON);
updateState(CHANNEL_FAN_BREEZE_MEAN, new PercentType(updateState.breeze[1]));
updateState(CHANNEL_FAN_BREEZE_VAR, new PercentType(updateState.breeze[2]));
updateState(CHANNEL_FAN_DIRECTION,
updateState.direction == 1 ? new StringType("summer") : new StringType("winter"));
updateState(CHANNEL_FAN_TIMER, new DecimalType(updateState.timer));
updateState(CHANNEL_LIGHT_POWER, updateState.light == 0 ? OnOffType.OFF : OnOffType.ON);
updateState(CHANNEL_LIGHT_BRIGHTNESS, formPercentType(updateState.light != 0, updateState.brightness));
updateState(CHANNEL_UP_LIGHT_ENABLE, updateState.upLight == 0 ? OnOffType.OFF : OnOffType.ON);
updateState(CHANNEL_UP_LIGHT_POWER,
(updateState.upLight == 1 && updateState.light == 1) ? OnOffType.ON : OnOffType.OFF);
updateState(CHANNEL_UP_LIGHT_BRIGHTNESS,
formPercentType((updateState.upLight == 1 && updateState.light == 1), updateState.upLightBrightness));
updateState(CHANNEL_DOWN_LIGHT_ENABLE, updateState.downLight == 0 ? OnOffType.OFF : OnOffType.ON);
updateState(CHANNEL_DOWN_LIGHT_POWER,
(updateState.downLight == 1 && updateState.light == 1) ? OnOffType.ON : OnOffType.OFF);
updateState(CHANNEL_DOWN_LIGHT_BRIGHTNESS, formPercentType(
(updateState.downLight == 1 && updateState.light == 1), updateState.downLightBrightness));
updateState(CHANNEL_FLAME, formPercentType(updateState.power != 0, updateState.flame));
updateState(CHANNEL_ROLLERSHUTTER, formPercentType(updateState.open != 0, 100));
}
private PercentType formPercentType(boolean isOn, int value) {
if (!isOn) {
return PercentType.ZERO;
} else {
return new PercentType(value);
}
}
private boolean hasConfigurationError() {
final ThingStatusInfo statusInfo = getThing().getStatusInfo();
return statusInfo.getStatus() == ThingStatus.OFFLINE
&& statusInfo.getStatusDetail() == ThingStatusDetail.CONFIGURATION_ERROR || disposed
|| config.deviceId == null;
}
private synchronized boolean wasThingUpdatedExternally(BondDevice devInfo) {
// Check if the Bond hash tree has changed
final String lastDeviceConfigurationHash = config.lastDeviceConfigurationHash;
boolean updatedHashTree = !devInfo.hash.equals(lastDeviceConfigurationHash);
if (updatedHashTree) {
logger.debug("Hash tree of device has been updated by Bond.");
logger.debug("Current state is {}, prior state was {}.", devInfo.hash, lastDeviceConfigurationHash);
}
return updatedHashTree;
}
private boolean getBridgeAndAPI() {
Bridge myBridge = this.getBridge();
if (myBridge == null) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
"@text/offline.conf-error.no-bridge");
return false;
} else {
BondBridgeHandler myBridgeHandler = (BondBridgeHandler) myBridge.getHandler();
if (myBridgeHandler != null) {
this.api = myBridgeHandler.getBridgeAPI();
return true;
} else {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
"@text/offline.conf-error.no-bridge");
return false;
}
}
}
// Start polling for state
private synchronized void startPollingJob() {
final ScheduledFuture<?> pollingJob = this.pollingJob;
if (pollingJob == null || pollingJob.isCancelled()) {
Runnable pollingCommand = () -> {
BondHttpApi api = this.api;
if (api == null) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
"@text/offline.comm-error.no-api");
return;
}
String deviceId = Objects.requireNonNull(config.deviceId);
logger.trace("Polling for current state for {} ({})", deviceId, this.getThing().getLabel());
try {
deviceState = api.getDeviceState(deviceId);
updateChannelsFromState(deviceState);
} catch (BondException e) {
if (!e.wasBridgeSetOffline()) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
}
}
};
this.pollingJob = scheduler.scheduleWithFixedDelay(pollingCommand, 60, 300, TimeUnit.SECONDS);
}
}
@Override
public void bridgeStatusChanged(ThingStatusInfo bridgeStatusInfo) {
if (bridgeStatusInfo.getStatus() == ThingStatus.ONLINE
&& getThing().getStatusInfo().getStatusDetail() == ThingStatusDetail.BRIDGE_OFFLINE) {
if (!fullyInitialized) {
scheduler.execute(this::initializeThing);
} else {
updateStatus(ThingStatus.ONLINE, ThingStatusDetail.NONE);
// restart the polling job when the bridge goes back online
startPollingJob();
}
} else if (bridgeStatusInfo.getStatus() == ThingStatus.OFFLINE) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_OFFLINE);
// stop the polling job when the bridge goes offline
ScheduledFuture<?> pollingJob = this.pollingJob;
if (pollingJob != null) {
pollingJob.cancel(true);
this.pollingJob = null;
}
}
}
}

View File

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<binding:binding id="bondhome" 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>Bond Home Binding</name>
<description>This is the binding for the Bond Bridge for Ceiling Fans and and other RF devices.</description>
<author>Sara Geleskie Damiano</author>
</binding:binding>

View File

@ -0,0 +1,37 @@
<?xml version="1.0" encoding="UTF-8"?>
<config-description:config-descriptions
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:config-description="https://openhab.org/schemas/config-description/v1.0.0"
xsi:schemaLocation="https://openhab.org/schemas/config-description/v1.0.0 https://openhab.org/schemas/config-description-1.0.0.xsd">
<config-description uri="thing-type:bondhome:bondbridge">
<parameter name="serialNumber" type="text" required="true">
<label>Bond Serial Number</label>
<description>The Bond ID (serial number) of the Bond Bridge. Viewable in the Bond Home app or on the bottom of the
bridge itself.</description>
</parameter>
<parameter name="localToken" type="text" required="true">
<label>Bond Bridge Local Token</label>
<description>The local token associated with the Bond Bridge. This is viewable in the Bond Home app.</description>
</parameter>
<parameter name="ipAddress" type="text" required="false">
<context>network-address</context>
<label>Bond Bridge IP Address</label>
<description>The IP Address of the Bond Bridge.</description>
<advanced>true</advanced>
</parameter>
</config-description>
<config-description uri="thing-type:bondhome:bonddevice">
<parameter name="deviceId" type="text" required="true">
<label>Device ID</label>
<description>The device ID assigned to the fan. Available in the Bond Home app.</description>
</parameter>
<parameter name="lastDeviceConfigurationHash" type="text" required="false">
<label>Bond Device Hash State</label>
<description>The current hash value of the device.</description>
<advanced>true</advanced>
</parameter>
</config-description>
</config-description:config-descriptions>

View File

@ -0,0 +1,101 @@
# binding
binding.bondhome.name = Bond Home Binding
binding.bondhome.description = This is the binding for the Bond Bridge for Ceiling Fans and and other RF devices.
# thing types
thing-type.bondhome.bondBridge.label = Bond Home Bridge
thing-type.bondhome.bondBridge.description = The RF/IR/Wifi Bridge
thing-type.bondhome.bondFan.label = Bond Home Ceiling Fan
thing-type.bondhome.bondFan.description = An RF or IR remote controlled ceiling fan with or without a light
thing-type.bondhome.bondFireplace.label = Bond Home Fireplace
thing-type.bondhome.bondFireplace.description = An RF or IR remote controlled fireplace with or without a fan
thing-type.bondhome.bondGenericThing.label = Bond Home Generic Remote
thing-type.bondhome.bondGenericThing.description = A generic RF or IR remote controlled device
thing-type.bondhome.bondShades.label = Bond Home Motorized Shades
thing-type.bondhome.bondShades.description = An RF or IR remote controlled motorized shade
# thing types config
thing-type.config.bondhome.bondbridge.ipAddress.label = Bond Bridge IP Address
thing-type.config.bondhome.bondbridge.ipAddress.description = The IP Address of the Bond Bridge.
thing-type.config.bondhome.bondbridge.localToken.label = Bond Bridge Local Token
thing-type.config.bondhome.bondbridge.localToken.description = The local token associated with the Bond Bridge. This is viewable in the Bond Home app.
thing-type.config.bondhome.bondbridge.serialNumber.label = Bond Serial Number
thing-type.config.bondhome.bondbridge.serialNumber.description = The Bond ID (serial number) of the Bond Bridge. Viewable in the Bond Home app or on the bottom of the bridge itself.
thing-type.config.bondhome.bonddevice.deviceId.label = Device ID
thing-type.config.bondhome.bonddevice.deviceId.description = The device ID assigned to the fan. Available in the Bond Home app.
thing-type.config.bondhome.bonddevice.lastDeviceConfigurationHash.label = Bond Device Hash State
thing-type.config.bondhome.bonddevice.lastDeviceConfigurationHash.description = The current hash value of the device.
# channel group types
channel-group-type.bondhome.ceilingFanChannelGroup.label = Ceiling Fan
channel-group-type.bondhome.commonChannelGroup.label = Common
channel-group-type.bondhome.downLightChannelGroup.label = Down Light
channel-group-type.bondhome.fireplaceChannelGroup.label = Fireplace
channel-group-type.bondhome.lightChannelGroup.label = Light
channel-group-type.bondhome.shadeChannelGroup.label = Motorized Shades
channel-group-type.bondhome.upLightChannelGroup.label = Up Light
# channel types
channel-type.bondhome.breezeMeanChannelType.label = Mean Breeze Speed
channel-type.bondhome.breezeMeanChannelType.description = Sets the average speed in breeze mode. 0 = minimum average speed (calm), 100 = maximum average speed (storm)
channel-type.bondhome.breezeStateChannelType.label = Breeze Mode
channel-type.bondhome.breezeStateChannelType.description = Enables or disables breeze mode
channel-type.bondhome.breezeVariabilityChannelType.label = Breeze Variability
channel-type.bondhome.breezeVariabilityChannelType.description = Sets the variability of the speed in breeze mode. 0 = minimum variation (steady), 100 = maximum variation (gusty)
channel-type.bondhome.commandChannelType.label = Command
channel-type.bondhome.commandChannelType.description = Sends a command to a device
channel-type.bondhome.commandChannelType.command.option.STOP = Stop Dimming
channel-type.bondhome.commandChannelType.command.option.HOLD = Hold Rollershutter
channel-type.bondhome.commandChannelType.command.option.PRESET = Send shade to preset
channel-type.bondhome.commandChannelType.command.option.DIM_START_STOP = Start/Stop Dimming
channel-type.bondhome.commandChannelType.command.option.DIM_INCREASE = Increase Brightness Until Stopped
channel-type.bondhome.commandChannelType.command.option.DIM_DECREASE = Decrease Brightness Until Stopped
channel-type.bondhome.commandChannelType.command.option.UP_LIGHT_DIM_START_STOP = Start/Stop Dimming
channel-type.bondhome.commandChannelType.command.option.UP_LIGHT_DIM_INCREASE = Increase Brightness Until Stopped
channel-type.bondhome.commandChannelType.command.option.UP_LIGHT_DIM_DECREASE = Decrease Brightness Until Stopped
channel-type.bondhome.commandChannelType.command.option.DOWN_LIGHT_DIM_START_STOP = Start/Stop Dimming
channel-type.bondhome.commandChannelType.command.option.DOWN_LIGHT_DIM_INCREASE = Increase Brightness Until Stopped
channel-type.bondhome.commandChannelType.command.option.DOWN_LIGHT_DIM_DECREASE = Decrease Brightness Until Stopped
channel-type.bondhome.directionChannelType.label = Fan Direction
channel-type.bondhome.directionChannelType.description = Sets the fan direction; forward or reverse. The forward and reverse modes are sometimes called Summer and Winter
channel-type.bondhome.directionChannelType.state.option.summer = Summer
channel-type.bondhome.directionChannelType.state.option.winter = Winter
channel-type.bondhome.enableChannelType.label = Enable Up or Down Light
channel-type.bondhome.enableChannelType.description = Enables or disables the up or down light of the ceiling fan. The light must also be on to turn on the up light.
channel-type.bondhome.fanSpeedChannelType.label = Fan Speed
channel-type.bondhome.fanSpeedChannelType.description = Sets fan speed
channel-type.bondhome.flameChannelType.label = Flame Level
channel-type.bondhome.flameChannelType.description = Turns on or adjust the flame level
channel-type.bondhome.fpFanSpeedChannelType.label = Fireplace Fan Speed
channel-type.bondhome.fpFanSpeedChannelType.description = Adjusts the speed of the fireplace fan
channel-type.bondhome.lightChannelType.label = Light
channel-type.bondhome.lightChannelType.description = Turns the light on the ceiling fan on or off
channel-type.bondhome.rollershutterChannelType.label = Shade
channel-type.bondhome.rollershutterChannelType.description = Opens, closes, or stops motorized shades
channel-type.bondhome.timerChannelType.label = Timer
channel-type.bondhome.timerChannelType.description = Starts a timer for s seconds. If power if off, device is implicitly turned on
# thing status descriptions
offline.comm-error.api-call-failed = Bond API call to {} failed: {}
offline.comm-error.device-not-found = No Bond device found with the given device id.
offline.comm-error.no-api = Bond Bridge API not available.
offline.comm-error.no-response = No response received!
offline.comm-error.timeout = Repeated timeouts attempting to reach bridge.
offline.comm-error.unexpected-response = Unexpected HTTP response: {}
offline.comm-error.unparseable-response = Unable to parse JSON response.
offline.conf-error.no-device-id = No device ID set.
offline.conf-error.incorrect-local-token = Incorrect local token for Bond Bridge.
offline.conf-error.invalid-host = IP Address or host name for Bond Bridge is not valid.
offline.conf-error.no-bridge = No Bond Bridge is associated with this Bond device.
offline.conf-error.no-device-properties = Unable to get device properties from Bond.
offline.conf-error.unknown-host = Unable to get an IP Address for Bond Bridge
# discovery result
discovery.bridge.label = Bond Bridge

View File

@ -0,0 +1,18 @@
<?xml version="1.0" encoding="UTF-8"?>
<thing:thing-descriptions bindingId="bondhome"
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">
<!-- The Bond Bridge -->
<bridge-type id="bondBridge">
<label>Bond Home Bridge</label>
<description>The RF/IR/Wifi Bridge</description>
<representation-property>serialNumber</representation-property>
<config-description-ref uri="thing-type:bondhome:bondbridge"/>
</bridge-type>
</thing:thing-descriptions>

View File

@ -0,0 +1,30 @@
<?xml version="1.0" encoding="UTF-8"?>
<thing:thing-descriptions bindingId="bondhome"
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">
<!-- A Ceiling Fan Thing -->
<thing-type id="bondFan">
<supported-bridge-type-refs>
<bridge-type-ref id="bondBridge"/>
</supported-bridge-type-refs>
<label>Bond Home Ceiling Fan</label>
<description>An RF or IR remote controlled ceiling fan with or without a light</description>
<channel-groups>
<channel-group id="common" typeId="commonChannelGroup"/>
<channel-group id="fan" typeId="ceilingFanChannelGroup"/>
<channel-group id="light" typeId="lightChannelGroup"/>
<channel-group id="upLight" typeId="upLightChannelGroup"/>
<channel-group id="downLight" typeId="downLightChannelGroup"/>
</channel-groups>
<representation-property>deviceId</representation-property>
<config-description-ref uri="thing-type:bondhome:bonddevice"/>
</thing-type>
</thing:thing-descriptions>

View File

@ -0,0 +1,71 @@
<?xml version="1.0" encoding="UTF-8"?>
<thing:thing-descriptions bindingId="bondhome"
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">
<!-- Channels Groups -->
<channel-group-type id="commonChannelGroup">
<label>Common</label>
<channels>
<channel id="power" typeId="system.power"/>
<channel id="command" typeId="commandChannelType"/>
</channels>
</channel-group-type>
<channel-group-type id="ceilingFanChannelGroup">
<label>Ceiling Fan</label>
<channels>
<channel id="speed" typeId="fanSpeedChannelType"/>
<channel id="breezeState" typeId="breezeStateChannelType"/>
<channel id="breezeMean" typeId="breezeMeanChannelType"/>
<channel id="breezeVariability" typeId="breezeVariabilityChannelType"/>
<channel id="direction" typeId="directionChannelType"/>
<channel id="timer" typeId="timerChannelType"/>
</channels>
</channel-group-type>
<channel-group-type id="lightChannelGroup">
<label>Light</label>
<channels>
<channel id="power" typeId="lightChannelType"/>
<channel id="brightness" typeId="system.brightness"/>
</channels>
</channel-group-type>
<channel-group-type id="upLightChannelGroup">
<label>Up Light</label>
<channels>
<channel id="power" typeId="system.power"/>
<channel id="enable" typeId="enableChannelType"/>
<channel id="brightness" typeId="system.brightness"/>
</channels>
</channel-group-type>
<channel-group-type id="downLightChannelGroup">
<label>Down Light</label>
<channels>
<channel id="power" typeId="system.power"/>
<channel id="enable" typeId="enableChannelType"/>
<channel id="brightness" typeId="system.brightness"/>
</channels>
</channel-group-type>
<channel-group-type id="fireplaceChannelGroup">
<label>Fireplace</label>
<channels>
<channel id="flame" typeId="flameChannelType"/>
<channel id="fanPower" typeId="system.power"/>
<channel id="fanSpeed" typeId="fpFanSpeedChannelType"/>
</channels>
</channel-group-type>
<channel-group-type id="shadeChannelGroup">
<label>Motorized Shades</label>
<channels>
<channel id="rollershutter" typeId="rollershutterChannelType"/>
</channels>
</channel-group-type>
</thing:thing-descriptions>

View File

@ -0,0 +1,117 @@
<?xml version="1.0" encoding="UTF-8"?>
<thing:thing-descriptions bindingId="bondhome"
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">
<!-- Individual Channels -->
<channel-type id="commandChannelType">
<item-type>String</item-type>
<label>Command</label>
<description>Sends a command to a device</description>
<command>
<options>
<option value="STOP">Stop Dimming</option>
<option value="HOLD">Hold Rollershutter</option>
<option value="PRESET">Send shade to preset</option>
<option value="DIM_START_STOP">Start/Stop Dimming</option>
<option value="DIM_INCREASE">Increase Brightness Until Stopped</option>
<option value="DIM_DECREASE">Decrease Brightness Until Stopped</option>
<option value="UP_LIGHT_DIM_START_STOP">Start/Stop Dimming</option>
<option value="UP_LIGHT_DIM_INCREASE">Increase Brightness Until Stopped</option>
<option value="UP_LIGHT_DIM_DECREASE">Decrease Brightness Until Stopped</option>
<option value="DOWN_LIGHT_DIM_START_STOP">Start/Stop Dimming</option>
<option value="DOWN_LIGHT_DIM_INCREASE">Increase Brightness Until Stopped</option>
<option value="DOWN_LIGHT_DIM_DECREASE">Decrease Brightness Until Stopped</option>
</options>
</command>
<autoUpdatePolicy>veto</autoUpdatePolicy>
</channel-type>
<channel-type id="fanSpeedChannelType">
<item-type>Dimmer</item-type>
<label>Fan Speed</label>
<description>Sets fan speed</description>
<category>Heating</category>
</channel-type>
<channel-type id="breezeStateChannelType">
<item-type>Switch</item-type>
<label>Breeze Mode</label>
<description>Enables or disables breeze mode</description>
</channel-type>
<channel-type id="breezeMeanChannelType">
<item-type>Dimmer</item-type>
<label>Mean Breeze Speed</label>
<description>Sets the average speed in breeze mode. 0 = minimum average speed (calm), 100 = maximum average speed
(storm)</description>
</channel-type>
<channel-type id="breezeVariabilityChannelType">
<item-type>Dimmer</item-type>
<label>Breeze Variability</label>
<description>Sets the variability of the speed in breeze mode. 0 = minimum variation (steady), 100 = maximum
variation
(gusty)</description>
</channel-type>
<channel-type id="directionChannelType">
<item-type>String</item-type>
<label>Fan Direction</label>
<description>Sets the fan direction; forward or reverse. The forward and reverse modes are sometimes called Summer
and
Winter</description>
<state readOnly="false">
<options>
<option value="summer">Summer</option>
<option value="winter">Winter</option>
</options>
</state>
</channel-type>
<channel-type id="timerChannelType">
<item-type>Number</item-type>
<label>Timer</label>
<description>Starts a timer for s seconds. If power if off, device is implicitly turned on</description>
<category>Time</category>
</channel-type>
<channel-type id="lightChannelType">
<item-type>Switch</item-type>
<label>Light</label>
<description>Turns the light on the ceiling fan on or off</description>
<category>Light</category>
</channel-type>
<channel-type id="enableChannelType">
<item-type>Switch</item-type>
<label>Enable Up or Down Light</label>
<description>Enables or disables the up or down light of the ceiling fan. The light must also be on to turn on the up
light.</description>
<category>Light</category>
</channel-type>
<channel-type id="flameChannelType">
<item-type>Dimmer</item-type>
<label>Flame Level</label>
<description>Turns on or adjust the flame level</description>
<category>Heating</category>
</channel-type>
<channel-type id="fpFanSpeedChannelType">
<item-type>Dimmer</item-type>
<label>Fireplace Fan Speed</label>
<description>Adjusts the speed of the fireplace fan</description>
<category>Heating</category>
</channel-type>
<channel-type id="rollershutterChannelType">
<item-type>Rollershutter</item-type>
<label>Shade</label>
<description>Opens, closes, or stops motorized shades</description>
<category>Rollershutter</category>
</channel-type>
</thing:thing-descriptions>

View File

@ -0,0 +1,27 @@
<?xml version="1.0" encoding="UTF-8"?>
<thing:thing-descriptions bindingId="bondhome"
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">
<!-- A Fireplace Thing -->
<thing-type id="bondFireplace">
<supported-bridge-type-refs>
<bridge-type-ref id="bondBridge"/>
</supported-bridge-type-refs>
<label>Bond Home Fireplace</label>
<description>An RF or IR remote controlled fireplace with or without a fan</description>
<channel-groups>
<channel-group id="common" typeId="commonChannelGroup"/>
<channel-group id="fireplace" typeId="fireplaceChannelGroup"/>
</channel-groups>
<representation-property>deviceId</representation-property>
<config-description-ref uri="thing-type:bondhome:bonddevice"/>
</thing-type>
</thing:thing-descriptions>

View File

@ -0,0 +1,26 @@
<?xml version="1.0" encoding="UTF-8"?>
<thing:thing-descriptions bindingId="bondhome"
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">
<!-- A Generic Thing -->
<thing-type id="bondGenericThing">
<supported-bridge-type-refs>
<bridge-type-ref id="bondBridge"/>
</supported-bridge-type-refs>
<label>Bond Home Generic Remote</label>
<description>A generic RF or IR remote controlled device</description>
<channel-groups>
<channel-group id="common" typeId="commonChannelGroup"/>
</channel-groups>
<representation-property>deviceId</representation-property>
<config-description-ref uri="thing-type:bondhome:bonddevice"/>
</thing-type>
</thing:thing-descriptions>

View File

@ -0,0 +1,27 @@
<?xml version="1.0" encoding="UTF-8"?>
<thing:thing-descriptions bindingId="bondhome"
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">
<!-- A Motorized Shade Thing -->
<thing-type id="bondShades">
<supported-bridge-type-refs>
<bridge-type-ref id="bondBridge"/>
</supported-bridge-type-refs>
<label>Bond Home Motorized Shades</label>
<description>An RF or IR remote controlled motorized shade</description>
<channel-groups>
<channel-group id="common" typeId="commonChannelGroup"/>
<channel-group id="shade" typeId="shadeChannelGroup"/>
</channel-groups>
<representation-property>deviceId</representation-property>
<config-description-ref uri="thing-type:bondhome:bonddevice"/>
</thing-type>
</thing:thing-descriptions>

View File

@ -76,6 +76,7 @@
<module>org.openhab.binding.bluetooth.govee</module> <module>org.openhab.binding.bluetooth.govee</module>
<module>org.openhab.binding.bluetooth.roaming</module> <module>org.openhab.binding.bluetooth.roaming</module>
<module>org.openhab.binding.bluetooth.ruuvitag</module> <module>org.openhab.binding.bluetooth.ruuvitag</module>
<module>org.openhab.binding.bondhome</module>
<module>org.openhab.binding.boschindego</module> <module>org.openhab.binding.boschindego</module>
<module>org.openhab.binding.boschshc</module> <module>org.openhab.binding.boschshc</module>
<module>org.openhab.binding.bosesoundtouch</module> <module>org.openhab.binding.bosesoundtouch</module>