diff --git a/CODEOWNERS b/CODEOWNERS
index 5ea043a9d73..2714540d6da 100644
--- a/CODEOWNERS
+++ b/CODEOWNERS
@@ -44,6 +44,7 @@
/bundles/org.openhab.binding.bluetooth.govee/ @cpmeister
/bundles/org.openhab.binding.bluetooth.roaming/ @cpmeister
/bundles/org.openhab.binding.bluetooth.ruuvitag/ @ssalonen
+/bundles/org.openhab.binding.bondhome/ @ccutrer
/bundles/org.openhab.binding.boschindego/ @jofleck @jlaur
/bundles/org.openhab.binding.boschshc/ @stefan-kaestle @coeing @GerdZanker
/bundles/org.openhab.binding.bosesoundtouch/ @marvkis @tratho
diff --git a/bom/openhab-addons/pom.xml b/bom/openhab-addons/pom.xml
index 0af4b341a7c..53d0bec852d 100644
--- a/bom/openhab-addons/pom.xml
+++ b/bom/openhab-addons/pom.xml
@@ -211,6 +211,11 @@
org.openhab.binding.bluetooth.ruuvitag
${project.version}
+
+ org.openhab.addons.bundles
+ org.openhab.binding.bondhome
+ ${project.version}
+
org.openhab.addons.bundles
org.openhab.binding.boschindego
diff --git a/bundles/org.openhab.binding.bondhome/NOTICE b/bundles/org.openhab.binding.bondhome/NOTICE
new file mode 100644
index 00000000000..38d625e3492
--- /dev/null
+++ b/bundles/org.openhab.binding.bondhome/NOTICE
@@ -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
diff --git a/bundles/org.openhab.binding.bondhome/README.md b/bundles/org.openhab.binding.bondhome/README.md
new file mode 100644
index 00000000000..d136642a6c2
--- /dev/null
+++ b/bundles/org.openhab.binding.bondhome/README.md
@@ -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" }
+```
diff --git a/bundles/org.openhab.binding.bondhome/pom.xml b/bundles/org.openhab.binding.bondhome/pom.xml
new file mode 100644
index 00000000000..d848c76463d
--- /dev/null
+++ b/bundles/org.openhab.binding.bondhome/pom.xml
@@ -0,0 +1,17 @@
+
+
+
+ 4.0.0
+
+
+ org.openhab.addons.bundles
+ org.openhab.addons.reactor.bundles
+ 3.4.0-SNAPSHOT
+
+
+ org.openhab.binding.bondhome
+
+ openHAB Add-ons :: Bundles :: Bond Home Binding
+
+
diff --git a/bundles/org.openhab.binding.bondhome/src/main/feature/feature.xml b/bundles/org.openhab.binding.bondhome/src/main/feature/feature.xml
new file mode 100644
index 00000000000..d7ad5f9108d
--- /dev/null
+++ b/bundles/org.openhab.binding.bondhome/src/main/feature/feature.xml
@@ -0,0 +1,9 @@
+
+
+ mvn:org.openhab.core.features.karaf/org.openhab.core.features.karaf.openhab-core/${ohc.version}/xml/features
+
+
+ openhab-runtime-base
+ mvn:org.openhab.addons.bundles/org.openhab.binding.bondhome/${project.version}
+
+
diff --git a/bundles/org.openhab.binding.bondhome/src/main/java/org/openhab/binding/bondhome/internal/BondException.java b/bundles/org.openhab.binding.bondhome/src/main/java/org/openhab/binding/bondhome/internal/BondException.java
new file mode 100644
index 00000000000..dd41d7bcccd
--- /dev/null
+++ b/bundles/org.openhab.binding.bondhome/src/main/java/org/openhab/binding/bondhome/internal/BondException.java
@@ -0,0 +1,38 @@
+/**
+ * Copyright (c) 2010-2022 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.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;
+ }
+}
diff --git a/bundles/org.openhab.binding.bondhome/src/main/java/org/openhab/binding/bondhome/internal/BondHomeBindingConstants.java b/bundles/org.openhab.binding.bondhome/src/main/java/org/openhab/binding/bondhome/internal/BondHomeBindingConstants.java
new file mode 100644
index 00000000000..c2b85a130f2
--- /dev/null
+++ b/bundles/org.openhab.binding.bondhome/src/main/java/org/openhab/binding/bondhome/internal/BondHomeBindingConstants.java
@@ -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 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 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;
+}
diff --git a/bundles/org.openhab.binding.bondhome/src/main/java/org/openhab/binding/bondhome/internal/BondHomeHandlerFactory.java b/bundles/org.openhab.binding.bondhome/src/main/java/org/openhab/binding/bondhome/internal/BondHomeHandlerFactory.java
new file mode 100644
index 00000000000..30ec2aadb77
--- /dev/null
+++ b/bundles/org.openhab.binding.bondhome/src/main/java/org/openhab/binding/bondhome/internal/BondHomeHandlerFactory.java
@@ -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> 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();
+ }
+ }
+ }
+}
diff --git a/bundles/org.openhab.binding.bondhome/src/main/java/org/openhab/binding/bondhome/internal/api/BPUPListener.java b/bundles/org.openhab.binding.bondhome/src/main/java/org/openhab/binding/bondhome/internal/api/BPUPListener.java
new file mode 100644
index 00000000000..b088a8bbdde
--- /dev/null
+++ b/bundles/org.openhab.binding.bondhome/src/main/java/org/openhab/binding/bondhome/internal/api/BPUPListener.java
@@ -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());
+ }
+ }
+ }
+}
diff --git a/bundles/org.openhab.binding.bondhome/src/main/java/org/openhab/binding/bondhome/internal/api/BPUPUpdate.java b/bundles/org.openhab.binding.bondhome/src/main/java/org/openhab/binding/bondhome/internal/api/BPUPUpdate.java
new file mode 100644
index 00000000000..8f76f4d143a
--- /dev/null
+++ b/bundles/org.openhab.binding.bondhome/src/main/java/org/openhab/binding/bondhome/internal/api/BPUPUpdate.java
@@ -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;
+}
diff --git a/bundles/org.openhab.binding.bondhome/src/main/java/org/openhab/binding/bondhome/internal/api/BondDevice.java b/bundles/org.openhab.binding.bondhome/src/main/java/org/openhab/binding/bondhome/internal/api/BondDevice.java
new file mode 100644
index 00000000000..cddc071d45c
--- /dev/null
+++ b/bundles/org.openhab.binding.bondhome/src/main/java/org/openhab/binding/bondhome/internal/api/BondDevice.java
@@ -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 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;
+}
diff --git a/bundles/org.openhab.binding.bondhome/src/main/java/org/openhab/binding/bondhome/internal/api/BondDeviceAction.java b/bundles/org.openhab.binding.bondhome/src/main/java/org/openhab/binding/bondhome/internal/api/BondDeviceAction.java
new file mode 100644
index 00000000000..37e816fd041
--- /dev/null
+++ b/bundles/org.openhab.binding.bondhome/src/main/java/org/openhab/binding/bondhome/internal/api/BondDeviceAction.java
@@ -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: (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;
+ }
+}
diff --git a/bundles/org.openhab.binding.bondhome/src/main/java/org/openhab/binding/bondhome/internal/api/BondDeviceProperties.java b/bundles/org.openhab.binding.bondhome/src/main/java/org/openhab/binding/bondhome/internal/api/BondDeviceProperties.java
new file mode 100644
index 00000000000..077009546f6
--- /dev/null
+++ b/bundles/org.openhab.binding.bondhome/src/main/java/org/openhab/binding/bondhome/internal/api/BondDeviceProperties.java
@@ -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;
+}
diff --git a/bundles/org.openhab.binding.bondhome/src/main/java/org/openhab/binding/bondhome/internal/api/BondDeviceState.java b/bundles/org.openhab.binding.bondhome/src/main/java/org/openhab/binding/bondhome/internal/api/BondDeviceState.java
new file mode 100644
index 00000000000..e1a49958f79
--- /dev/null
+++ b/bundles/org.openhab.binding.bondhome/src/main/java/org/openhab/binding/bondhome/internal/api/BondDeviceState.java
@@ -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: (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;
+}
diff --git a/bundles/org.openhab.binding.bondhome/src/main/java/org/openhab/binding/bondhome/internal/api/BondDeviceType.java b/bundles/org.openhab.binding.bondhome/src/main/java/org/openhab/binding/bondhome/internal/api/BondDeviceType.java
new file mode 100644
index 00000000000..e22517437c7
--- /dev/null
+++ b/bundles/org.openhab.binding.bondhome/src/main/java/org/openhab/binding/bondhome/internal/api/BondDeviceType.java
@@ -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;
+ }
+}
diff --git a/bundles/org.openhab.binding.bondhome/src/main/java/org/openhab/binding/bondhome/internal/api/BondHash.java b/bundles/org.openhab.binding.bondhome/src/main/java/org/openhab/binding/bondhome/internal/api/BondHash.java
new file mode 100644
index 00000000000..f41d1a470b6
--- /dev/null
+++ b/bundles/org.openhab.binding.bondhome/src/main/java/org/openhab/binding/bondhome/internal/api/BondHash.java
@@ -0,0 +1,38 @@
+/**
+ * Copyright (c) 2010-2022 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.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;
+}
diff --git a/bundles/org.openhab.binding.bondhome/src/main/java/org/openhab/binding/bondhome/internal/api/BondHttpApi.java b/bundles/org.openhab.binding.bondhome/src/main/java/org/openhab/binding/bondhome/internal/api/BondHttpApi.java
new file mode 100644
index 00000000000..06d48b57c2a
--- /dev/null
+++ b/bundles/org.openhab.binding.bondhome/src/main/java/org/openhab/binding/bondhome/internal/api/BondHttpApi.java
@@ -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 getDevices() throws BondException {
+
+ List list = new ArrayList<>();
+ String json = request("/v2/devices/");
+ try {
+ JsonParser parser = new JsonParser();
+ JsonElement element = parser.parse(json);
+ JsonObject obj = element.getAsJsonObject();
+ Set> entries = obj.entrySet();
+ for (Map.Entry 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);
+ }
+}
diff --git a/bundles/org.openhab.binding.bondhome/src/main/java/org/openhab/binding/bondhome/internal/api/BondSysVersion.java b/bundles/org.openhab.binding.bondhome/src/main/java/org/openhab/binding/bondhome/internal/api/BondSysVersion.java
new file mode 100644
index 00000000000..86eb6445a04
--- /dev/null
+++ b/bundles/org.openhab.binding.bondhome/src/main/java/org/openhab/binding/bondhome/internal/api/BondSysVersion.java
@@ -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;
+}
diff --git a/bundles/org.openhab.binding.bondhome/src/main/java/org/openhab/binding/bondhome/internal/config/BondBridgeConfiguration.java b/bundles/org.openhab.binding.bondhome/src/main/java/org/openhab/binding/bondhome/internal/config/BondBridgeConfiguration.java
new file mode 100644
index 00000000000..0708e10a9a1
--- /dev/null
+++ b/bundles/org.openhab.binding.bondhome/src/main/java/org/openhab/binding/bondhome/internal/config/BondBridgeConfiguration.java
@@ -0,0 +1,43 @@
+/**
+ * Copyright (c) 2010-2022 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.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;
+ }
+}
diff --git a/bundles/org.openhab.binding.bondhome/src/main/java/org/openhab/binding/bondhome/internal/config/BondDeviceConfiguration.java b/bundles/org.openhab.binding.bondhome/src/main/java/org/openhab/binding/bondhome/internal/config/BondDeviceConfiguration.java
new file mode 100644
index 00000000000..7f821637dae
--- /dev/null
+++ b/bundles/org.openhab.binding.bondhome/src/main/java/org/openhab/binding/bondhome/internal/config/BondDeviceConfiguration.java
@@ -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;
+}
diff --git a/bundles/org.openhab.binding.bondhome/src/main/java/org/openhab/binding/bondhome/internal/discovery/BondDiscoveryService.java b/bundles/org.openhab.binding.bondhome/src/main/java/org/openhab/binding/bondhome/internal/discovery/BondDiscoveryService.java
new file mode 100644
index 00000000000..d94d2a847d3
--- /dev/null
+++ b/bundles/org.openhab.binding.bondhome/src/main/java/org/openhab/binding/bondhome/internal/discovery/BondDiscoveryService.java
@@ -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 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;
+ }
+ }
+}
diff --git a/bundles/org.openhab.binding.bondhome/src/main/java/org/openhab/binding/bondhome/internal/discovery/BondMDNSDiscoveryParticipant.java b/bundles/org.openhab.binding.bondhome/src/main/java/org/openhab/binding/bondhome/internal/discovery/BondMDNSDiscoveryParticipant.java
new file mode 100644
index 00000000000..ec5ea2372db
--- /dev/null
+++ b/bundles/org.openhab.binding.bondhome/src/main/java/org/openhab/binding/bondhome/internal/discovery/BondMDNSDiscoveryParticipant.java
@@ -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 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;
+ }
+}
diff --git a/bundles/org.openhab.binding.bondhome/src/main/java/org/openhab/binding/bondhome/internal/handler/BondBridgeHandler.java b/bundles/org.openhab.binding.bondhome/src/main/java/org/openhab/binding/bondhome/internal/handler/BondBridgeHandler.java
new file mode 100644
index 00000000000..93e852d0198
--- /dev/null
+++ b/bundles/org.openhab.binding.bondhome/src/main/java/org/openhab/binding/bondhome/internal/handler/BondBridgeHandler.java
@@ -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 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 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 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> getServices() {
+ return Collections.singleton(BondDiscoveryService.class);
+ }
+
+ public void setDiscoveryService(BondDiscoveryService discoveryService) {
+ this.discoveryService = discoveryService;
+ }
+}
diff --git a/bundles/org.openhab.binding.bondhome/src/main/java/org/openhab/binding/bondhome/internal/handler/BondDeviceHandler.java b/bundles/org.openhab.binding.bondhome/src/main/java/org/openhab/binding/bondhome/internal/handler/BondDeviceHandler.java
new file mode 100644
index 00000000000..6f4e88cefd6
--- /dev/null
+++ b/bundles/org.openhab.binding.bondhome/src/main/java/org/openhab/binding/bondhome/internal/handler/BondDeviceHandler.java
@@ -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 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 thingProperties = new HashMap();
+ 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 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 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 possibleChannels = this.getThing().getChannels();
+ Set 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;
+ }
+ }
+ }
+}
diff --git a/bundles/org.openhab.binding.bondhome/src/main/resources/OH-INF/binding/binding.xml b/bundles/org.openhab.binding.bondhome/src/main/resources/OH-INF/binding/binding.xml
new file mode 100644
index 00000000000..44319972d42
--- /dev/null
+++ b/bundles/org.openhab.binding.bondhome/src/main/resources/OH-INF/binding/binding.xml
@@ -0,0 +1,10 @@
+
+
+
+ Bond Home Binding
+ This is the binding for the Bond Bridge for Ceiling Fans and and other RF devices.
+ Sara Geleskie Damiano
+
+
diff --git a/bundles/org.openhab.binding.bondhome/src/main/resources/OH-INF/config/config.xml b/bundles/org.openhab.binding.bondhome/src/main/resources/OH-INF/config/config.xml
new file mode 100644
index 00000000000..f4fe51abb75
--- /dev/null
+++ b/bundles/org.openhab.binding.bondhome/src/main/resources/OH-INF/config/config.xml
@@ -0,0 +1,37 @@
+
+
+
+
+
+
+ The Bond ID (serial number) of the Bond Bridge. Viewable in the Bond Home app or on the bottom of the
+ bridge itself.
+
+
+
+ The local token associated with the Bond Bridge. This is viewable in the Bond Home app.
+
+
+ network-address
+
+ The IP Address of the Bond Bridge.
+ true
+
+
+
+
+
+
+ The device ID assigned to the fan. Available in the Bond Home app.
+
+
+
+ The current hash value of the device.
+ true
+
+
+
+
diff --git a/bundles/org.openhab.binding.bondhome/src/main/resources/OH-INF/i18n/bondhome.properties b/bundles/org.openhab.binding.bondhome/src/main/resources/OH-INF/i18n/bondhome.properties
new file mode 100644
index 00000000000..b102047a2c7
--- /dev/null
+++ b/bundles/org.openhab.binding.bondhome/src/main/resources/OH-INF/i18n/bondhome.properties
@@ -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
diff --git a/bundles/org.openhab.binding.bondhome/src/main/resources/OH-INF/thing/Bridge.xml b/bundles/org.openhab.binding.bondhome/src/main/resources/OH-INF/thing/Bridge.xml
new file mode 100644
index 00000000000..55f83163fc5
--- /dev/null
+++ b/bundles/org.openhab.binding.bondhome/src/main/resources/OH-INF/thing/Bridge.xml
@@ -0,0 +1,18 @@
+
+
+
+
+
+
+ The RF/IR/Wifi Bridge
+
+ serialNumber
+
+
+
+
+
+
diff --git a/bundles/org.openhab.binding.bondhome/src/main/resources/OH-INF/thing/CeilingFan.xml b/bundles/org.openhab.binding.bondhome/src/main/resources/OH-INF/thing/CeilingFan.xml
new file mode 100644
index 00000000000..158168ffb6b
--- /dev/null
+++ b/bundles/org.openhab.binding.bondhome/src/main/resources/OH-INF/thing/CeilingFan.xml
@@ -0,0 +1,30 @@
+
+
+
+
+
+
+
+
+
+
+ An RF or IR remote controlled ceiling fan with or without a light
+
+
+
+
+
+
+
+
+
+ deviceId
+
+
+
+
+
+
diff --git a/bundles/org.openhab.binding.bondhome/src/main/resources/OH-INF/thing/ChannelGroups.xml b/bundles/org.openhab.binding.bondhome/src/main/resources/OH-INF/thing/ChannelGroups.xml
new file mode 100644
index 00000000000..358cb01aa8b
--- /dev/null
+++ b/bundles/org.openhab.binding.bondhome/src/main/resources/OH-INF/thing/ChannelGroups.xml
@@ -0,0 +1,71 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/bundles/org.openhab.binding.bondhome/src/main/resources/OH-INF/thing/Channels.xml b/bundles/org.openhab.binding.bondhome/src/main/resources/OH-INF/thing/Channels.xml
new file mode 100644
index 00000000000..9314fb03b12
--- /dev/null
+++ b/bundles/org.openhab.binding.bondhome/src/main/resources/OH-INF/thing/Channels.xml
@@ -0,0 +1,117 @@
+
+
+
+
+
+
+ String
+
+ Sends a command to a device
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ veto
+
+
+
+ Dimmer
+
+ Sets fan speed
+ Heating
+
+
+
+ Switch
+
+ Enables or disables breeze mode
+
+
+
+ Dimmer
+
+ Sets the average speed in breeze mode. 0 = minimum average speed (calm), 100 = maximum average speed
+ (storm)
+
+
+
+ Dimmer
+
+ Sets the variability of the speed in breeze mode. 0 = minimum variation (steady), 100 = maximum
+ variation
+ (gusty)
+
+
+
+ String
+
+ Sets the fan direction; forward or reverse. The forward and reverse modes are sometimes called Summer
+ and
+ Winter
+
+
+
+
+
+
+
+
+
+ Number
+
+ Starts a timer for s seconds. If power if off, device is implicitly turned on
+ Time
+
+
+
+ Switch
+
+ Turns the light on the ceiling fan on or off
+ Light
+
+
+
+ Switch
+
+ Enables or disables the up or down light of the ceiling fan. The light must also be on to turn on the up
+ light.
+ Light
+
+
+
+ Dimmer
+
+ Turns on or adjust the flame level
+ Heating
+
+
+
+ Dimmer
+
+ Adjusts the speed of the fireplace fan
+ Heating
+
+
+
+ Rollershutter
+
+ Opens, closes, or stops motorized shades
+ Rollershutter
+
+
+
diff --git a/bundles/org.openhab.binding.bondhome/src/main/resources/OH-INF/thing/Fireplace.xml b/bundles/org.openhab.binding.bondhome/src/main/resources/OH-INF/thing/Fireplace.xml
new file mode 100644
index 00000000000..0821a474e54
--- /dev/null
+++ b/bundles/org.openhab.binding.bondhome/src/main/resources/OH-INF/thing/Fireplace.xml
@@ -0,0 +1,27 @@
+
+
+
+
+
+
+
+
+
+
+ An RF or IR remote controlled fireplace with or without a fan
+
+
+
+
+
+
+ deviceId
+
+
+
+
+
+
diff --git a/bundles/org.openhab.binding.bondhome/src/main/resources/OH-INF/thing/GenericDevice.xml b/bundles/org.openhab.binding.bondhome/src/main/resources/OH-INF/thing/GenericDevice.xml
new file mode 100644
index 00000000000..d77376496ae
--- /dev/null
+++ b/bundles/org.openhab.binding.bondhome/src/main/resources/OH-INF/thing/GenericDevice.xml
@@ -0,0 +1,26 @@
+
+
+
+
+
+
+
+
+
+
+ A generic RF or IR remote controlled device
+
+
+
+
+
+ deviceId
+
+
+
+
+
+
diff --git a/bundles/org.openhab.binding.bondhome/src/main/resources/OH-INF/thing/MotorizedShades.xml b/bundles/org.openhab.binding.bondhome/src/main/resources/OH-INF/thing/MotorizedShades.xml
new file mode 100644
index 00000000000..6f3728b6c70
--- /dev/null
+++ b/bundles/org.openhab.binding.bondhome/src/main/resources/OH-INF/thing/MotorizedShades.xml
@@ -0,0 +1,27 @@
+
+
+
+
+
+
+
+
+
+
+ An RF or IR remote controlled motorized shade
+
+
+
+
+
+
+ deviceId
+
+
+
+
+
+
diff --git a/bundles/pom.xml b/bundles/pom.xml
index 8f0b5b7a8c8..633af03dcf8 100644
--- a/bundles/pom.xml
+++ b/bundles/pom.xml
@@ -76,6 +76,7 @@
org.openhab.binding.bluetooth.govee
org.openhab.binding.bluetooth.roaming
org.openhab.binding.bluetooth.ruuvitag
+ org.openhab.binding.bondhome
org.openhab.binding.boschindego
org.openhab.binding.boschshc
org.openhab.binding.bosesoundtouch